feat(installer): pull in preflight check implementations as prerequisite for SystemCheckPage
Git/GitIdentity/Port/WriteAccess and Claude CLI/Version/Auth/PermissionModeAuto checks plus the ExecutableResolver they depend on were built in two sibling task branches that hadn't landed on main yet. Vendored the finished files in from those branches (same content, verified building + tests green) so the SystemCheckPage task has something to consume.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
using SysEnvironment = System.Environment;
|
||||
|
||||
namespace ClaudeDo.Data.Environment;
|
||||
|
||||
public sealed record ResolvedExecutable(string Path, bool IsShim);
|
||||
|
||||
public sealed record ShimStartInfo(string FileName, string Arguments);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a command the way Windows' CreateProcess/PATH search does, but also finds
|
||||
/// non-.exe shims (.cmd/.bat/.ps1) that UseShellExecute=false alone would miss.
|
||||
/// </summary>
|
||||
public static class ExecutableResolver
|
||||
{
|
||||
private const string DefaultPathExt = ".COM;.EXE;.BAT;.CMD";
|
||||
|
||||
// Known npm/claude install locations to try when PATH search comes up empty.
|
||||
private static readonly string[] FallbackDirectoryTemplates =
|
||||
{
|
||||
"%APPDATA%\\npm",
|
||||
"%LOCALAPPDATA%\\Programs\\claude",
|
||||
"%USERPROFILE%\\.local\\bin",
|
||||
};
|
||||
|
||||
public static ResolvedExecutable? Resolve(string command, string? pathOverride = null, string? pathExtOverride = null)
|
||||
{
|
||||
var pathExts = ParsePathExt(pathExtOverride);
|
||||
|
||||
if (LooksLikePath(command))
|
||||
{
|
||||
return ResolveAsPath(command, pathExts);
|
||||
}
|
||||
|
||||
var directories = ParsePath(pathOverride);
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
var resolved = ResolveInDirectory(dir, command, pathExts);
|
||||
if (resolved is not null) return resolved;
|
||||
}
|
||||
|
||||
foreach (var template in FallbackDirectoryTemplates)
|
||||
{
|
||||
var dir = SysEnvironment.ExpandEnvironmentVariables(template);
|
||||
var resolved = ResolveInDirectory(dir, command, pathExts);
|
||||
if (resolved is not null) return resolved;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Expanded fallback directories tried when PATH search comes up empty (for diagnostics).</summary>
|
||||
public static IReadOnlyList<string> FallbackDirectories() =>
|
||||
FallbackDirectoryTemplates.Select(SysEnvironment.ExpandEnvironmentVariables).ToList();
|
||||
|
||||
public static ShimStartInfo BuildShimStartInfo(string shimPath, IReadOnlyList<string> arguments)
|
||||
{
|
||||
var parts = new List<string> { "/c", Quote(shimPath) };
|
||||
parts.AddRange(arguments.Select(Quote));
|
||||
return new ShimStartInfo("cmd.exe", string.Join(' ', parts));
|
||||
}
|
||||
|
||||
private static bool LooksLikePath(string command) =>
|
||||
command.Contains(Path.DirectorySeparatorChar) || command.Contains(Path.AltDirectorySeparatorChar);
|
||||
|
||||
private static ResolvedExecutable? ResolveAsPath(string command, IReadOnlyList<string> pathExts)
|
||||
{
|
||||
if (File.Exists(command)) return new ResolvedExecutable(command, IsShimExtension(Path.GetExtension(command)));
|
||||
|
||||
if (Path.HasExtension(command)) return null;
|
||||
|
||||
foreach (var ext in pathExts)
|
||||
{
|
||||
var candidate = command + ext;
|
||||
if (File.Exists(candidate)) return new ResolvedExecutable(candidate, IsShimExtension(ext));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ResolvedExecutable? ResolveInDirectory(string directory, string command, IReadOnlyList<string> pathExts)
|
||||
{
|
||||
if (!Directory.Exists(directory)) return null;
|
||||
|
||||
if (Path.HasExtension(command))
|
||||
{
|
||||
var candidate = Path.Combine(directory, command);
|
||||
return File.Exists(candidate) ? new ResolvedExecutable(candidate, IsShimExtension(Path.GetExtension(candidate))) : null;
|
||||
}
|
||||
|
||||
foreach (var ext in pathExts)
|
||||
{
|
||||
var candidate = Path.Combine(directory, command + ext);
|
||||
if (File.Exists(candidate)) return new ResolvedExecutable(candidate, IsShimExtension(ext));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsShimExtension(string extension) =>
|
||||
!extension.Equals(".exe", StringComparison.OrdinalIgnoreCase)
|
||||
&& !extension.Equals(".com", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static IReadOnlyList<string> ParsePathExt(string? pathExtOverride)
|
||||
{
|
||||
var raw = pathExtOverride ?? SysEnvironment.GetEnvironmentVariable("PATHEXT") ?? DefaultPathExt;
|
||||
return raw.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ParsePath(string? pathOverride)
|
||||
{
|
||||
var raw = pathOverride ?? SysEnvironment.GetEnvironmentVariable("PATH") ?? "";
|
||||
return raw.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
private static string Quote(string value) => value.Contains(' ') ? $"\"{value}\"" : value;
|
||||
}
|
||||
Reference in New Issue
Block a user