feat(installer): add Claude CLI preflight checks (found, version, login, auto-mode)

Four IEnvironmentCheck implementations in src/ClaudeDo.Installer/Checks/:
- ClaudeCliCheck (Error) — resolves ctx.ClaudeBin via ExecutableResolver, runs
  --version; failure message lists searched PATH entries + fallback dirs, flags
  shim resolution (.cmd/.ps1) in Detail.
- ClaudeVersionCheck (Error) — tolerant version parsing (ignores surrounding
  text), numeric System.Version comparison against a named floor constant
  (2.1.220, see docs/explore-notes/installer-preflight.md §3). Unparseable or
  missing CLI -> Unknown, never Failed.
- ClaudeAuthCheck (Error) — `claude auth status --json`, never sends a prompt.
- PermissionModeAutoCheck (Warning) — per the note's §2 conclusion, real
  auto-mode eligibility (org/model/plan) has no cheap static signal, so this
  only confirms `claude --help` still lists "auto" as a --permission-mode
  choice. Kept as its own check rather than folded into ClaudeVersionCheck,
  since the note explicitly separates "flag recognized" from "mode eligible"
  and only the former is checkable at all.

All four share a new ClaudeCliLookup that resolves the CLI and runs
--version exactly once per install run (memoized, semaphore-guarded), so
none of the three version-dependent checks repeats that call.

Foundation prerequisites this task depends on (docs/explore-notes/
installer-preflight.md, ExecutableResolver, the IEnvironmentCheck/CheckResult/
EnvironmentCheckService abstraction, ProcessCommand, IProcessRunner) existed
only on an unmerged sibling branch, not on main. Rather than merging that
whole unreviewed branch, the specific prerequisite files were copied in
as-is (git history shows their origin). GitCheck/GitIdentityCheck/PortCheck/
WriteAccessCheck from that branch were intentionally NOT brought in — out of
scope for this task.

Deviations/decisions worth flagging:
- Added ExecutableResolver.FallbackDirectories() (public) so ClaudeCliCheck
  can name the checked fallback locations in its failure message; the prior
  branch kept that list private.
- Paths.cs now qualifies System.Environment explicitly. Adding the
  ClaudeDo.Data.Environment namespace makes bare `Environment.X` inside any
  ClaudeDo.Data.* namespace resolve to the sibling namespace instead of
  System.Environment (C# prefers nested/enclosing namespace members over
  usings) — this broke the build until qualified.

Not done (explicitly out of scope): no DI wiring into the wizard UI, no
XAML — matches how the prerequisite Git/Port/WriteAccess checks were also
left unwired.
This commit is contained in:
mika kuns
2026-08-05 19:43:34 +02:00
parent bdee731376
commit d743a9d0e9
24 changed files with 1344 additions and 3 deletions
@@ -0,0 +1,66 @@
using System.Text.RegularExpressions;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Checks;
/// <summary>
/// Static-only check per docs/explore-notes/installer-preflight.md §2: real auto-mode eligibility
/// (org/model/plan) has no cheap detectable signal, so this only confirms the CLI's argument
/// parser still recognizes "auto" as a --permission-mode choice. A warning, not a blocker — a
/// too-old CLI just degrades to acceptEdits/default with more prompts.
/// </summary>
public sealed class PermissionModeAutoCheck : IEnvironmentCheck
{
public const string CheckId = "claude-permission-mode-auto";
private static readonly Regex AutoWord = new(@"\bauto\b", RegexOptions.Compiled);
private const int SearchWindow = 300;
private readonly ClaudeCliLookup _lookup;
public PermissionModeAutoCheck(ClaudeCliLookup lookup)
{
_lookup = lookup;
}
public string Id => CheckId;
public CheckSeverity Severity => CheckSeverity.Warning;
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
{
var lookup = await _lookup.ResolveAsync(ctx, ct);
if (lookup.Resolved is null)
{
return CheckResult.Unknown(Id, Severity, "checks.permissionModeAuto.title",
"The claude CLI was not found — permission-mode support could not be checked.");
}
var (fileName, arguments) = ProcessCommand.For(lookup.Resolved, "--help");
var (exitCode, output) = await _lookup.ProcessRunner.RunAsync(fileName, arguments, null, ct);
if (exitCode != 0 || string.IsNullOrWhiteSpace(output))
{
return CheckResult.Unknown(Id, Severity, "checks.permissionModeAuto.title",
"Could not determine whether --permission-mode auto is supported.");
}
if (AutoIsListed(output))
{
return CheckResult.Ok(Id, Severity, "checks.permissionModeAuto.title",
"--permission-mode auto is recognized by this CLI.");
}
return CheckResult.Fail(Id, Severity, "checks.permissionModeAuto.title",
"This claude CLI version does not list 'auto' as a --permission-mode choice.",
"checks.permissionModeAuto.hint");
}
private static bool AutoIsListed(string helpOutput)
{
var index = helpOutput.IndexOf("--permission-mode", StringComparison.Ordinal);
if (index < 0) return false;
var windowEnd = Math.Min(helpOutput.Length, index + SearchWindow);
return AutoWord.IsMatch(helpOutput[index..windowEnd]);
}
}