feat(installer): add Git, GitIdentity, Port, and WriteAccess preflight checks
Implements IEnvironmentCheck for the four checks derivable without a Claude CLI probe: - GitCheck (Error) - resolves git via ExecutableResolver (handles .cmd shims), parses `git --version`. - GitIdentityCheck (Warning) - user.name/user.email presence; Unknown (not Failed) if git itself is missing, so it doesn't duplicate GitCheck's failure. - PortCheck (Warning) - loopback bind probe for SignalRPort/ExternalMcpPort; resolves the owning process via a new NetstatPortOwnerResolver and treats a port held by the running ClaudeDo.Worker (update/repair case) as Ok. Both ports are configurable, hence a warning. - WriteAccessCheck (Error) - create+delete a probe file in InstallDirectory and ~/.todo-app (walking up to the first existing parent), not an ACL read (ACLs lie on virtualized paths). Process calls go through a new IProcessRunner wrapping the existing static ProcessRunner, so checks are fakeable in tests instead of spawning real processes. DotnetRuntimeCheck was intentionally not added: per docs/explore-notes/installer-preflight.md, App/Worker publish self-contained (no preinstalled runtime needed), and the Installer's own .NET 8 Desktop Runtime requirement is self-proving - a framework-dependent apphost can't reach managed code at all if that runtime is missing, so a check running from inside the process can never observe a failure. Brings in two prerequisite commits this task builds on that hadn't reached this branch yet: the IEnvironmentCheck/EnvironmentCheckService scaffolding and the installer-preflight.md research note.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
using ClaudeDo.Data.Environment;
|
||||
using ClaudeDo.Installer.Core;
|
||||
using ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>Trivial to fix after the fact (git config --global), and the Claude help-me button can resolve it — a warning, not a blocker.</summary>
|
||||
public sealed class GitIdentityCheck : IEnvironmentCheck
|
||||
{
|
||||
public const string CheckId = "git-identity";
|
||||
|
||||
private readonly IProcessRunner _processRunner;
|
||||
private readonly string? _pathOverride;
|
||||
private readonly string? _pathExtOverride;
|
||||
|
||||
public GitIdentityCheck(IProcessRunner processRunner, string? pathOverride = null, string? pathExtOverride = null)
|
||||
{
|
||||
_processRunner = processRunner;
|
||||
_pathOverride = pathOverride;
|
||||
_pathExtOverride = pathExtOverride;
|
||||
}
|
||||
|
||||
public string Id => CheckId;
|
||||
public CheckSeverity Severity => CheckSeverity.Warning;
|
||||
|
||||
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
var resolved = ExecutableResolver.Resolve("git", _pathOverride, _pathExtOverride);
|
||||
if (resolved is null)
|
||||
{
|
||||
return CheckResult.Unknown(Id, Severity, "checks.gitIdentity.title", "git was not found — identity could not be checked.");
|
||||
}
|
||||
|
||||
var name = await ReadConfigAsync(resolved, "user.name", ct);
|
||||
var email = await ReadConfigAsync(resolved, "user.email", ct);
|
||||
var hasName = !string.IsNullOrWhiteSpace(name);
|
||||
var hasEmail = !string.IsNullOrWhiteSpace(email);
|
||||
|
||||
if (hasName && hasEmail)
|
||||
{
|
||||
return CheckResult.Ok(Id, Severity, "checks.gitIdentity.title", $"{name} <{email}>");
|
||||
}
|
||||
|
||||
var missing = (hasName, hasEmail) switch
|
||||
{
|
||||
(false, false) => "user.name, user.email",
|
||||
(false, true) => "user.name",
|
||||
_ => "user.email",
|
||||
};
|
||||
|
||||
return CheckResult.Fail(Id, Severity, "checks.gitIdentity.title", $"Missing git identity: {missing}.", "checks.gitIdentity.hint");
|
||||
}
|
||||
|
||||
private async Task<string> ReadConfigAsync(ResolvedExecutable resolved, string key, CancellationToken ct)
|
||||
{
|
||||
var (fileName, arguments) = ProcessCommand.For(resolved, "config", "--get", key);
|
||||
var (_, output) = await _processRunner.RunAsync(fileName, arguments, null, ct);
|
||||
return output.Trim();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user