- uninstaller: taskkill only the first, digits-only postmaster.pid line (the for /f loop ran taskkill on the port/epoch lines — potential unrelated-process kill) - git-missing dialogs use new ConfirmOptions.alwaysAsk so global skip-confirmations cannot silently pick an unseen choice - Windows quit prompt: embedded-local runtimes only, skipped during OS session end (sync dialog blocked Windows shutdown) - 'leave it running' detaches the embedded lifecycle (disarms its process shutdown hook) so Electron exit cannot kill the postmaster the operator chose to keep (new detachKeepingEmbedded) - wizard: ref-based double-submit guard around the async git preflight - clone route: ENOENT invalidate-and-retry matching runGitCommand - openExternalUrl: drop the async window.open fallback (always popup-blocked); log bridge failures instead - DirectoryPicker: close the panel when listing the created folder fails so Select cannot re-commit the parent - git status probe bounded to two spawns (PATH + first candidate) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import type { ExecFileException } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { wellKnownGitBinaryPaths } from "./git-binary.js";
|
|
|
|
export const GIT_INSTALL_URL = "https://git-scm.com/downloads";
|
|
export const DEFAULT_GIT_CLI_STATUS_TIMEOUT_MS = 2_500;
|
|
|
|
export interface GitCliStatus {
|
|
available: boolean;
|
|
version?: string;
|
|
installUrl: string;
|
|
}
|
|
|
|
export interface ProbeGitCliStatusOptions {
|
|
timeoutMs?: number;
|
|
/**
|
|
* FNXC:Onboarding 2026-07-18-03:20:
|
|
* Absolute git candidates probed when the PATH lookup ENOENTs. Defaults to
|
|
* the platform's existing well-known install locations. Injectable so tests
|
|
* stay hermetic (the default probes the real filesystem).
|
|
*/
|
|
fallbackGitPaths?: readonly string[];
|
|
}
|
|
|
|
function parseGitVersion(stdout: string): string | undefined {
|
|
const trimmed = stdout.trim();
|
|
if (!trimmed) return undefined;
|
|
const match = trimmed.match(/git version\s+(.+)/i);
|
|
return match?.[1]?.trim() || trimmed;
|
|
}
|
|
|
|
type ProbeAttempt = { status: GitCliStatus } | "enoent" | "error";
|
|
|
|
function attemptGitVersion(binary: string, timeoutMs: number): Promise<ProbeAttempt> {
|
|
return new Promise((resolve) => {
|
|
const child = execFile(
|
|
binary,
|
|
["--version"],
|
|
{
|
|
encoding: "utf-8",
|
|
timeout: timeoutMs,
|
|
windowsHide: true,
|
|
},
|
|
(error: ExecFileException | null, stdout: string | Buffer) => {
|
|
if (error) {
|
|
resolve(error.code === "ENOENT" ? "enoent" : "error");
|
|
return;
|
|
}
|
|
resolve({
|
|
status: {
|
|
available: true,
|
|
version: parseGitVersion(String(stdout)),
|
|
installUrl: GIT_INSTALL_URL,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
child.stdin?.end();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* FNXC:Onboarding 2026-07-03-00:00:
|
|
* First-run GitHub onboarding must detect whether `git` is available on the Fusion server host before clone/init flows fail later.
|
|
* Keep this probe bounded and argument-vector based so auth status can include prerequisite guidance without shell interpolation or long subprocess hangs.
|
|
*
|
|
* FNXC:Onboarding 2026-07-18-03:20:
|
|
* Field report: git installed DURING first-run setup stayed "missing" (and project
|
|
* setup kept failing spawn-git ENOENT) because the server's PATH snapshot predates
|
|
* the install. On a PATH ENOENT, probe the platform's well-known install locations
|
|
* so a just-installed git is detected without restarting Fusion.
|
|
*/
|
|
export async function probeGitCliStatus(options: ProbeGitCliStatusOptions = {}): Promise<GitCliStatus> {
|
|
const timeoutMs = options.timeoutMs ?? DEFAULT_GIT_CLI_STATUS_TIMEOUT_MS;
|
|
|
|
const first = await attemptGitVersion("git", timeoutMs);
|
|
if (typeof first === "object") return first.status;
|
|
if (first === "enoent") {
|
|
/*
|
|
FNXC:Onboarding 2026-07-18-06:00:
|
|
Review finding: probing EVERY existing candidate could take (1+N) x
|
|
timeoutMs on hosts littered with leftover git installs. Keep the probe
|
|
bounded at two spawns: PATH plus the FIRST existing well-known location
|
|
(if that one exists but cannot run, git is broken regardless).
|
|
*/
|
|
const candidates = options.fallbackGitPaths ?? wellKnownGitBinaryPaths().filter((p) => existsSync(p));
|
|
const candidate = candidates[0];
|
|
if (candidate !== undefined) {
|
|
const attempt = await attemptGitVersion(candidate, timeoutMs);
|
|
if (typeof attempt === "object") return attempt.status;
|
|
}
|
|
}
|
|
return { available: false, installUrl: GIT_INSTALL_URL };
|
|
}
|