FN-7470: show Git prerequisite during onboarding

Add Git availability checks to the first-run GitHub onboarding flow so missing host prerequisites are visible before project setup.

- Probe the server-host Git CLI with a bounded core helper and expose status through the auth status API.
- Render ready/missing Git prerequisite guidance in the GitHub onboarding step with install instructions and localized strings.
- Cover the probe, auth route, and onboarding UI states with regression tests, docs, and a changeset.

Files changed:
 .changeset/fn-7470-git-onboarding.md               |  7 ++
 docs/dashboard-guide.md                            |  2 +
 docs/getting-started.md                            |  2 +-
 packages/core/src/__tests__/git-cli-status.test.ts | 83 +++++++++++++++++++++
 packages/core/src/git-cli-status.ts                | 56 ++++++++++++++
 packages/core/src/index.ts                         |  7 ++
 packages/dashboard/app/api/legacy.ts               |  8 ++
 .../app/components/ModelOnboardingModal.css        | 59 +++++++++++++++
 .../app/components/ModelOnboardingModal.tsx        | 53 ++++++++++++-
 .../__tests__/ModelOnboardingModal.test.tsx        | 87 ++++++++++++++++++++++
 .../dashboard/src/__tests__/routes-auth.test.ts    | 62 ++++++++++++++-
 .../dashboard/src/routes/register-auth-routes.ts   | 13 +++-
 packages/i18n/locales/en/app.json                  | 11 ++-
 packages/i18n/locales/es/app.json                  | 44 ++++++++++-
 packages/i18n/locales/fr/app.json                  | 44 ++++++++++-
 packages/i18n/locales/ko/app.json                  | 44 ++++++++++-
 packages/i18n/locales/zh-CN/app.json               | 44 ++++++++++-
 packages/i18n/locales/zh-TW/app.json               | 44 ++++++++++-
 packages/i18n/src/resources.d.ts                   |  9 +++
 19 files changed, 661 insertions(+), 18 deletions(-)

Fusion-Task-Id: FN-7470
Fusion-Task-Lineage: 56d6f118-0d82-4f89-bcaa-b01e72a1bf8b
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-03 16:15:18 -07:00
parent 50786f29d4
commit 315f3bc32c
19 changed files with 661 additions and 18 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Show Git prerequisite guidance during first-run GitHub onboarding.
category: feature
dev: Adds bounded server-host git availability to auth status and onboarding.

View File

@@ -793,6 +793,8 @@ Memory view provides a multi-file editor for project and daily memory files. Its
## Setup Wizard Project Registration
First-run setup checks the **GitHub (Optional)** step for the `git` executable on the Fusion server host before users continue into repository setup. If Git is installed, the step shows a low-noise prerequisite-ready note. If Git is missing, the step shows platform-aware install guidance for macOS, Windows, and Linux plus a Git downloads link, while still allowing users to skip optional GitHub authentication. The check reflects the machine or service container running Fusion, not the browser device.
First-run setup and embedded project setup both expose a **Repository setup** section before path entry:
- **Use Existing Directory** registers an existing git repository or workspace root. Workspace detection and the workspace-mode checkbox only appear in this mode.

View File

@@ -93,7 +93,7 @@ fn dashboard
On first launch, Fusion opens an onboarding wizard with guided setup steps:
1. **AI Setup** — choose a provider and authenticate (you only need one to start). Anthropic/Claude and OpenAI Codex use a pasted authorization-code OAuth flow in onboarding and Settings (sign in, then paste the final redirect URL or code back into Fusion), and Fusion warns before login so you remember to copy the browser address bar URL before the redirect tab appears to fail. After the initial Claude OAuth login, Fusion normally refreshes the OAuth credential automatically with the stored refresh token when the access token expires, so repeated manual re-login is not usually required. **Anthropic — via Claude CLI** remains available as a separate optional path. Deprecated Google Gemini CLI / Antigravity entries are hidden; Google/Gemini API key, Google Generative AI, Vertex, and Cloud Code options remain available.
2. **GitHub (Optional)** — connect GitHub for issue import and PR workflows.
2. **GitHub (Optional)** — connect GitHub for issue import and PR workflows. This step also checks whether the Fusion host can run `git`; if Git is missing, onboarding shows platform install guidance before you reach clone, init, or repository registration flows. Install Git on the machine or service container running Fusion, not just on the browser/client device. See [Git downloads](https://git-scm.com/downloads) for macOS, Windows, and Linux options.
3. **Project Setup** — choose how Fusion should prepare a repository:
- **Use Existing Directory** registers a folder that is already a git repository or a workspace root with detected sub-repositories.
- **Initialize New Repository** registers an existing local folder and lets the server run `git init` during registration when the folder is not already a git repository.

View File

@@ -0,0 +1,83 @@
// @vitest-environment node
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { ExecFileException } from "node:child_process";
import { EventEmitter } from "node:events";
const { mockExecFile } = vi.hoisted(() => ({
mockExecFile: vi.fn(),
}));
vi.mock("node:child_process", () => ({
execFile: mockExecFile,
}));
import { GIT_INSTALL_URL, probeGitCliStatus } from "../git-cli-status.js";
function mockChildProcess() {
return Object.assign(new EventEmitter(), {
stdin: { end: vi.fn() },
});
}
function callbackFromExecFileCall() {
const callback = mockExecFile.mock.calls[0]?.[3];
if (typeof callback !== "function") {
throw new Error("execFile callback was not captured");
}
return callback as (error: ExecFileException | null, stdout: string, stderr: string) => void;
}
describe("probeGitCliStatus", () => {
beforeEach(() => {
mockExecFile.mockReset();
mockExecFile.mockReturnValue(mockChildProcess());
});
it("runs git --version with a bounded argument-vector probe", async () => {
const resultPromise = probeGitCliStatus({ timeoutMs: 1234 });
callbackFromExecFileCall()(null, "git version 2.45.1\n", "");
await expect(resultPromise).resolves.toEqual({
available: true,
version: "2.45.1",
installUrl: GIT_INSTALL_URL,
});
expect(mockExecFile).toHaveBeenCalledWith(
"git",
["--version"],
expect.objectContaining({ encoding: "utf-8", timeout: 1234, windowsHide: true }),
expect.any(Function),
);
});
it("reports missing git as unavailable with the install URL", async () => {
const resultPromise = probeGitCliStatus();
callbackFromExecFileCall()(Object.assign(new Error("ENOENT"), { code: "ENOENT" }) as ExecFileException, "", "");
await expect(resultPromise).resolves.toEqual({
available: false,
installUrl: GIT_INSTALL_URL,
});
});
it("reports probe errors as unavailable without throwing", async () => {
const resultPromise = probeGitCliStatus();
callbackFromExecFileCall()(Object.assign(new Error("timeout"), { code: "ETIMEDOUT" }) as ExecFileException, "", "");
await expect(resultPromise).resolves.toEqual({
available: false,
installUrl: GIT_INSTALL_URL,
});
});
it("tolerates version output that does not match the usual prefix", async () => {
const resultPromise = probeGitCliStatus();
callbackFromExecFileCall()(null, "custom-git-build\n", "");
await expect(resultPromise).resolves.toMatchObject({
available: true,
version: "custom-git-build",
});
});
});

View File

@@ -0,0 +1,56 @@
import { execFile } from "node:child_process";
import type { ExecFileException } from "node:child_process";
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;
}
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;
}
/**
* 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.
*/
export async function probeGitCliStatus(options: ProbeGitCliStatusOptions = {}): Promise<GitCliStatus> {
const timeoutMs = options.timeoutMs ?? DEFAULT_GIT_CLI_STATUS_TIMEOUT_MS;
return new Promise((resolve) => {
const child = execFile(
"git",
["--version"],
{
encoding: "utf-8",
timeout: timeoutMs,
windowsHide: true,
},
(error: ExecFileException | null, stdout: string | Buffer) => {
if (error) {
resolve({ available: false, installUrl: GIT_INSTALL_URL });
return;
}
resolve({
available: true,
version: parseGitVersion(String(stdout)),
installUrl: GIT_INSTALL_URL,
});
},
);
child.stdin?.end();
});
}

View File

@@ -936,6 +936,13 @@ export {
type GhErrorCode,
type StructuredGhError,
} from "./gh-cli.js";
export {
DEFAULT_GIT_CLI_STATUS_TIMEOUT_MS,
GIT_INSTALL_URL,
probeGitCliStatus,
type GitCliStatus,
type ProbeGitCliStatusOptions,
} from "./git-cli-status.js";
export {
parseRepoSlug,
isValidRepoSlug,

View File

@@ -2338,14 +2338,22 @@ export async function probeProviderModels(params: ProbeModelsParams): Promise<Pr
});
}
export interface GitCliStatus {
available: boolean;
version?: string;
installUrl?: string;
}
/** Fetch authentication status for all OAuth providers */
export function fetchAuthStatus(options?: FetchOptions): Promise<{
providers: AuthProvider[];
ghCli?: { available: boolean; authenticated: boolean };
gitCli?: GitCliStatus;
}> {
return dedupe("/auth/status", () => api<{
providers: AuthProvider[];
ghCli?: { available: boolean; authenticated: boolean };
gitCli?: GitCliStatus;
}>("/auth/status"), options);
}

View File

@@ -717,6 +717,61 @@
}
/* === Onboarding GitHub Step === */
.onboarding-github-git-prerequisite {
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
color: var(--text-muted);
line-height: 1.5;
}
.onboarding-github-git-prerequisite--ready {
border-color: color-mix(in srgb, var(--color-success) 35%, var(--border));
background: color-mix(in srgb, var(--color-success) 8%, transparent);
}
.onboarding-github-git-prerequisite--missing {
border-color: color-mix(in srgb, var(--color-warning) 45%, var(--border));
border-left: var(--space-xs) solid var(--color-warning);
background: color-mix(in srgb, var(--color-warning) 10%, transparent);
}
.onboarding-github-git-prerequisite__heading {
display: flex;
align-items: center;
gap: var(--space-xs);
color: var(--text);
}
.onboarding-github-git-prerequisite__heading svg {
flex: 0 0 auto;
color: var(--color-info);
}
.onboarding-github-git-prerequisite--missing .onboarding-github-git-prerequisite__heading svg {
color: var(--color-warning);
}
.onboarding-github-git-prerequisite p,
.onboarding-github-git-prerequisite ul {
margin: 0;
}
.onboarding-github-git-prerequisite a {
align-self: flex-start;
}
.onboarding-github-git-install-list {
display: flex;
flex-direction: column;
gap: var(--space-xxs);
padding-left: var(--space-xl);
}
.onboarding-github-benefits {
margin: 0;
padding-left: var(--space-xl);
@@ -1260,6 +1315,10 @@
min-height: 36px;
}
.onboarding-github-git-prerequisite a {
align-self: stretch;
}
/* Compact provider card on mobile.
Earlier rule forced flex-direction: column so the icon, body, and
actions each took their own full-width row — combined with --space-lg

View File

@@ -20,6 +20,7 @@ import {
fetchCustomProviders,
createCustomProvider,
type AgentOnboardingSummary,
type GitCliStatus,
} from "../api";
import type { ToastType } from "../hooks/useToast";
import { useModalResizePersist } from "../hooks/useModalResizePersist";
@@ -594,6 +595,8 @@ interface GhCliStatus {
authenticated: boolean;
}
const GIT_INSTALL_URL = "https://git-scm.com/downloads";
/** Maximum number of poll cycles before timing out (150 × 2s = 5 minutes) */
const MAX_POLL_CYCLES = 150;
@@ -658,6 +661,7 @@ export function ModelOnboardingModal({
const [isAgentInterviewOpen, setIsAgentInterviewOpen] = useState(false);
const [authProviders, setAuthProviders] = useState<AuthProvider[]>([]);
const [ghCliStatus, setGhCliStatus] = useState<GhCliStatus | undefined>(undefined);
const [gitCliStatus, setGitCliStatus] = useState<GitCliStatus | undefined>(undefined);
const [authLoading, setAuthLoading] = useState(true);
const [authActionInProgress, setAuthActionInProgress] = useState<string | null>(null);
const [loginInstructions, setLoginInstructions] = useState<Record<string, string>>({});
@@ -819,10 +823,11 @@ export function ModelOnboardingModal({
// Load auth providers
const loadAuthStatus = useCallback(async () => {
try {
const { providers, ghCli } = await fetchAuthStatus();
const { providers, ghCli, gitCli } = await fetchAuthStatus();
const visibleProviders = filterVisibleOnboardingAndSettingsProviders(providers);
setAuthProviders(visibleProviders);
setGhCliStatus(ghCli);
setGitCliStatus(gitCli);
setLoginInstructions((prev) => {
const next: Record<string, string> = {};
for (const [providerId, instructions] of Object.entries(prev)) {
@@ -916,6 +921,10 @@ export function ModelOnboardingModal({
const isGithubAuthenticated = githubProvider?.authenticated ?? false;
const isGithubLoginInProgress = githubProvider?.loginInProgress ?? false;
const isGithubCliAuthenticated = ghCliStatus?.authenticated ?? false;
const gitInstallUrl = gitCliStatus?.installUrl ?? GIT_INSTALL_URL;
const gitVersionLabel = gitCliStatus?.version
? t("setup.gitPrerequisiteInstalledVersion", "Git is installed on the Fusion host ({{version}}).", { version: gitCliStatus.version })
: t("setup.gitPrerequisiteInstalled", "Git is installed on the Fusion host.");
// Effective GitHub readiness (matches useSetupReadiness): OAuth OR authenticated gh CLI session.
const isGitHubReady = isGithubAuthenticated || isGithubCliAuthenticated;
const isGitHubReadyViaCli = !isGithubAuthenticated && isGithubCliAuthenticated;
@@ -1331,10 +1340,11 @@ export function ModelOnboardingModal({
}
try {
const { providers, ghCli } = await fetchAuthStatus();
const { providers, ghCli, gitCli } = await fetchAuthStatus();
const visibleProviders = filterVisibleOnboardingAndSettingsProviders(providers);
setAuthProviders(visibleProviders);
setGhCliStatus(ghCli);
setGitCliStatus(gitCli);
const provider = visibleProviders.find((p) => p.id === providerId);
if (provider?.authenticated) {
if (pollIntervalRef.current) {
@@ -2675,6 +2685,45 @@ export function ModelOnboardingModal({
{t("setup.githubConnectionDescription", "Connecting GitHub unlocks issue imports and pull request tracking. You can skip this — task creation works without it.")}
</p>
)}
{/*
FNXC:Onboarding 2026-07-03-00:00:
The GitHub step must surface missing `git` on the Fusion server host before users reach project clone/init workflows.
This prerequisite warning is separate from GitHub OAuth/gh readiness so users can still skip optional GitHub auth.
*/}
{gitCliStatus && (
<div
className={`onboarding-github-git-prerequisite ${gitCliStatus.available ? "onboarding-github-git-prerequisite--ready" : "onboarding-github-git-prerequisite--missing"}`}
data-testid="onboarding-git-prerequisite"
role={gitCliStatus.available ? "status" : "alert"}
>
<div className="onboarding-github-git-prerequisite__heading">
<GitPullRequest size={16} aria-hidden="true" />
<strong>
{gitCliStatus.available
? t("setup.gitPrerequisiteReadyTitle", "Git prerequisite ready")
: t("setup.gitPrerequisiteMissingTitle", "Install Git before project setup")}
</strong>
</div>
{gitCliStatus.available ? (
<p>{gitVersionLabel}</p>
) : (
<>
<p>
{t("setup.gitPrerequisiteMissingBody", "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces.")}
</p>
<ul className="onboarding-github-git-install-list">
<li>{t("setup.gitPrerequisiteMac", "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.")}</li>
<li>{t("setup.gitPrerequisiteWindows", "Windows: install Git for Windows and restart the Fusion host shell or service.")}</li>
<li>{t("setup.gitPrerequisiteLinux", "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.")}</li>
</ul>
<a href={gitInstallUrl} target="_blank" rel="noreferrer">
{t("setup.gitPrerequisiteInstallLink", "Open Git install downloads")}
</a>
</>
)}
</div>
)}
{!isGitHubReady && (
<div className="onboarding-feature-list">
<ul>

View File

@@ -1740,6 +1740,93 @@ describe("ModelOnboardingModal", () => {
expect(screen.getByText(/task creation works without it/i)).toBeTruthy();
});
it("shows a low-noise installed Git prerequisite note without replacing GitHub auth state", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
],
gitCli: { available: true, version: "2.45.1", installUrl: "https://git-scm.com/downloads" },
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await navigateToGitHubStep();
const prerequisite = screen.getByTestId("onboarding-git-prerequisite");
expect(prerequisite).toHaveClass("onboarding-github-git-prerequisite--ready");
expect(prerequisite).toHaveTextContent("Git prerequisite ready");
expect(prerequisite).toHaveTextContent("2.45.1");
expect(screen.getByTestId("github-status-badge")).toHaveTextContent("Not connected");
expect(screen.getByRole("button", { name: /Connect/ })).toBeTruthy();
});
it("shows platform-aware install guidance when Git is missing on the Fusion host", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
],
gitCli: { available: false, installUrl: "https://git-scm.com/downloads" },
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await navigateToGitHubStep();
const prerequisite = screen.getByTestId("onboarding-git-prerequisite");
expect(prerequisite).toHaveAttribute("role", "alert");
expect(prerequisite).toHaveClass("onboarding-github-git-prerequisite--missing");
expect(prerequisite).toHaveTextContent("Install Git before project setup");
expect(prerequisite).toHaveTextContent("server host running Fusion");
expect(prerequisite).toHaveTextContent("macOS");
expect(prerequisite).toHaveTextContent("Windows");
expect(prerequisite).toHaveTextContent("Linux");
expect(screen.getByRole("link", { name: "Open Git install downloads" })).toHaveAttribute("href", "https://git-scm.com/downloads");
expect(screen.getByRole("button", { name: /Connect/ })).toBeTruthy();
expect(screen.getByText("No worries if you're not ready — connect GitHub anytime from Settings → Authentication.")).toBeTruthy();
});
it("keeps legacy auth status responses without gitCli free of empty prerequisite shells", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
],
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await navigateToGitHubStep();
expect(screen.queryByTestId("onboarding-git-prerequisite")).toBeNull();
expect(screen.getByTestId("github-status-badge")).toHaveTextContent("Not connected");
});
it("preserves missing-Git guidance when GitHub OAuth provider is absent and gh CLI is ready", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [],
ghCli: { available: true, authenticated: true },
gitCli: { available: false, installUrl: "https://git-scm.com/downloads" },
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await navigateToGitHubStep();
expect(screen.getByTestId("onboarding-git-prerequisite")).toHaveTextContent("Install Git before project setup");
expect(screen.getByRole("button", { name: "Continue with gh CLI auth →" })).toBeTruthy();
expect(screen.getByRole("button", { name: /Connect OAuth/ })).toBeTruthy();
});
it("does not show a Git prerequisite shell when auth status fails to load", async () => {
mockFetchAuthStatus.mockRejectedValueOnce(new Error("auth unavailable"));
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await navigateToGitHubStep();
expect(screen.queryByTestId("onboarding-git-prerequisite")).toBeNull();
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
});
it("GitHub step shows connected state when already authenticated", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [

View File

@@ -118,8 +118,10 @@ vi.mock("@fusion/core", async (importOriginal) => {
const { createCoreMock } = await import("../test/mockCoreEngine.js");
return createCoreMock(() => importOriginal<typeof import("@fusion/core")>(), {
resolveGlobalDir: vi.fn().mockReturnValue("/tmp/fusion-test"),
GIT_INSTALL_URL: "https://git-scm.com/downloads",
isGhAvailable: vi.fn(),
isGhAuthenticated: vi.fn(),
probeGitCliStatus: vi.fn(),
isQmdAvailable: vi.fn().mockResolvedValue(false),
CentralCore: vi.fn().mockImplementation(function () { return {
init: mockCentralInit,
@@ -175,11 +177,12 @@ vi.mock("@fusion/engine", async () => {
});
});
import { AgentStore, Database, RoutineStore, isGhAvailable, isGhAuthenticated } from "@fusion/core";
import { AgentStore, Database, RoutineStore, isGhAvailable, isGhAuthenticated, probeGitCliStatus } from "@fusion/core";
import { createFnAgent } from "@fusion/engine";
const mockIsGhAvailable = vi.mocked(isGhAvailable);
const mockIsGhAuthenticated = vi.mocked(isGhAuthenticated);
const mockProbeGitCliStatus = vi.mocked(probeGitCliStatus);
function createMockGlobalSettingsStore() {
return {
@@ -864,6 +867,14 @@ describe("GET /auth/status", () => {
});
beforeEach(() => {
mockProbeGitCliStatus.mockResolvedValue({
available: true,
version: "2.45.1",
installUrl: "https://git-scm.com/downloads",
});
mockIsGhAvailable.mockReturnValue(false);
mockIsGhAuthenticated.mockReturnValue(false);
vi.spyOn(claudeCliProbeModule, "probeClaudeCli").mockResolvedValue({
available: false,
reason: "mocked unavailable",
@@ -929,6 +940,55 @@ describe("GET /auth/status", () => {
expect(authStorage.reload).toHaveBeenCalled();
});
it("includes git CLI status while preserving gh CLI status", async () => {
mockIsGhAvailable.mockReturnValue(true);
mockIsGhAuthenticated.mockReturnValue(true);
mockProbeGitCliStatus.mockResolvedValue({
available: true,
version: "2.45.1",
installUrl: "https://git-scm.com/downloads",
});
const res = await GET(app, "/api/auth/status");
expect(res.status).toBe(200);
expect(res.body.ghCli).toEqual({ available: true, authenticated: true });
expect(res.body.gitCli).toEqual({
available: true,
version: "2.45.1",
installUrl: "https://git-scm.com/downloads",
});
});
it("reports missing git CLI without changing provider readiness", async () => {
mockProbeGitCliStatus.mockResolvedValue({
available: false,
installUrl: "https://git-scm.com/downloads",
});
const res = await GET(app, "/api/auth/status");
expect(res.status).toBe(200);
expect(res.body.gitCli).toEqual({
available: false,
installUrl: "https://git-scm.com/downloads",
});
expect(res.body.providers).toEqual(expect.any(Array));
expect(res.body.ghCli).toEqual({ available: false, authenticated: false });
});
it("degrades git CLI probe errors to an unavailable status", async () => {
mockProbeGitCliStatus.mockRejectedValue(new Error("probe failed"));
const res = await GET(app, "/api/auth/status");
expect(res.status).toBe(200);
expect(res.body.gitCli).toEqual({
available: false,
installUrl: "https://git-scm.com/downloads",
});
});
it("includes GitHub Copilot as oauth when auth storage reports it", async () => {
(authStorage.getOAuthProviders as ReturnType<typeof vi.fn>).mockReturnValue([
{ id: "github-copilot", name: "GitHub Copilot" },

View File

@@ -2,7 +2,7 @@ import type { Request } from "express";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { existsSync, readFileSync } from "node:fs";
import { isGhAvailable, isGhAuthenticated } from "@fusion/core";
import { GIT_INSTALL_URL, isGhAvailable, isGhAuthenticated, probeGitCliStatus } from "@fusion/core";
import { probeClaudeCli } from "../claude-cli-probe.js";
import { probeDroidCli } from "../droid-cli-probe.js";
import { probeCursorCliProvider } from "../runtime-provider-probes.js";
@@ -315,7 +315,8 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
* Includes both OAuth-backed and API-key-backed providers.
* Response: {
* providers: [{ id, name, authenticated, type, keyHint? }],
* ghCli: { available: boolean, authenticated: boolean }
* ghCli: { available: boolean, authenticated: boolean },
* gitCli: { available: boolean, version?: string, installUrl: string }
* }
*/
router.get("/auth/status", async (req, res) => {
@@ -471,8 +472,14 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
available: isGhAvailable(),
authenticated: isGhAuthenticated(),
};
let gitCli: Awaited<ReturnType<typeof probeGitCliStatus>>;
try {
gitCli = await probeGitCliStatus();
} catch {
gitCli = { available: false, installUrl: GIT_INSTALL_URL };
}
res.json({ providers, ghCli });
res.json({ providers, ghCli, gitCli });
} catch (err: unknown) {
if (err instanceof ApiError) {
throw err;

View File

@@ -6926,7 +6926,16 @@
"repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.",
"repositorySetupTitle": "Repository setup",
"useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root.",
"connectGithub": "Connect GitHub"
"connectGithub": "Connect GitHub",
"gitPrerequisiteInstalled": "Git is installed on the Fusion host.",
"gitPrerequisiteInstalledVersion": "Git is installed on the Fusion host ({{version}}).",
"gitPrerequisiteInstallLink": "Open Git install downloads",
"gitPrerequisiteLinux": "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.",
"gitPrerequisiteMac": "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.",
"gitPrerequisiteMissingBody": "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces.",
"gitPrerequisiteMissingTitle": "Install Git before project setup",
"gitPrerequisiteReadyTitle": "Git prerequisite ready",
"gitPrerequisiteWindows": "Windows: install Git for Windows and restart the Fusion host shell or service."
},
"shell": {
"activePill": "Active",

View File

@@ -6916,7 +6916,16 @@
"repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.",
"repositorySetupTitle": "Repository setup",
"useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root.",
"connectGithub": "Connect GitHub"
"connectGithub": "Connect GitHub",
"gitPrerequisiteInstalled": "Git is installed on the Fusion host.",
"gitPrerequisiteInstalledVersion": "Git is installed on the Fusion host ({{version}}).",
"gitPrerequisiteInstallLink": "Open Git install downloads",
"gitPrerequisiteLinux": "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.",
"gitPrerequisiteMac": "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.",
"gitPrerequisiteMissingBody": "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces.",
"gitPrerequisiteMissingTitle": "Install Git before project setup",
"gitPrerequisiteReadyTitle": "Git prerequisite ready",
"gitPrerequisiteWindows": "Windows: install Git for Windows and restart the Fusion host shell or service."
},
"shell": {
"activePill": "Activo",
@@ -7565,7 +7574,9 @@
"none": "(ninguna)",
"provider": "Proveedor",
"repository": "Repositorio",
"url": "URL"
"url": "URL",
"gitlabAriaLabel": "",
"gitlabBadge": ""
},
"spec": {
"aiReviseHeading": "Pedir revisión a la IA",
@@ -7666,6 +7677,35 @@
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
},
"gitlabTracking": {
"collapse": "",
"expand": "",
"instance": "",
"item": "",
"itemUnlinked": "",
"kind": "",
"kindGroupIssue": "",
"kindMergeRequest": "",
"kindProjectIssue": "",
"label": "",
"lastSynced": "",
"namespace": "",
"openAriaLabel": "",
"openBtn": "",
"stale": "",
"staleUnknown": "",
"state": "",
"stateUnknown": "",
"statusAriaLabel": "",
"statusLinked": "",
"statusStale": "",
"statusUnlinked": "",
"unlinkBtn": "",
"unlinkConfirm": "",
"unlinkMessage": "",
"unlinkTitle": "",
"unlinked": ""
}
},
"taskDocuments": {

View File

@@ -6916,7 +6916,16 @@
"repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.",
"repositorySetupTitle": "Repository setup",
"useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root.",
"connectGithub": "Connect GitHub"
"connectGithub": "Connect GitHub",
"gitPrerequisiteInstalled": "Git is installed on the Fusion host.",
"gitPrerequisiteInstalledVersion": "Git is installed on the Fusion host ({{version}}).",
"gitPrerequisiteInstallLink": "Open Git install downloads",
"gitPrerequisiteLinux": "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.",
"gitPrerequisiteMac": "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.",
"gitPrerequisiteMissingBody": "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces.",
"gitPrerequisiteMissingTitle": "Install Git before project setup",
"gitPrerequisiteReadyTitle": "Git prerequisite ready",
"gitPrerequisiteWindows": "Windows: install Git for Windows and restart the Fusion host shell or service."
},
"shell": {
"activePill": "Actif",
@@ -7565,7 +7574,9 @@
"none": "(aucune)",
"provider": "Fournisseur",
"repository": "Dépôt",
"url": "URL"
"url": "URL",
"gitlabAriaLabel": "",
"gitlabBadge": ""
},
"spec": {
"aiReviseHeading": "Demander une révision à l'IA",
@@ -7666,6 +7677,35 @@
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
},
"gitlabTracking": {
"collapse": "",
"expand": "",
"instance": "",
"item": "",
"itemUnlinked": "",
"kind": "",
"kindGroupIssue": "",
"kindMergeRequest": "",
"kindProjectIssue": "",
"label": "",
"lastSynced": "",
"namespace": "",
"openAriaLabel": "",
"openBtn": "",
"stale": "",
"staleUnknown": "",
"state": "",
"stateUnknown": "",
"statusAriaLabel": "",
"statusLinked": "",
"statusStale": "",
"statusUnlinked": "",
"unlinkBtn": "",
"unlinkConfirm": "",
"unlinkMessage": "",
"unlinkTitle": "",
"unlinked": ""
}
},
"taskDocuments": {

View File

@@ -6916,7 +6916,16 @@
"repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.",
"repositorySetupTitle": "Repository setup",
"useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root.",
"connectGithub": "Connect GitHub"
"connectGithub": "Connect GitHub",
"gitPrerequisiteInstalled": "Git is installed on the Fusion host.",
"gitPrerequisiteInstalledVersion": "Git is installed on the Fusion host ({{version}}).",
"gitPrerequisiteInstallLink": "Open Git install downloads",
"gitPrerequisiteLinux": "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.",
"gitPrerequisiteMac": "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.",
"gitPrerequisiteMissingBody": "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces.",
"gitPrerequisiteMissingTitle": "Install Git before project setup",
"gitPrerequisiteReadyTitle": "Git prerequisite ready",
"gitPrerequisiteWindows": "Windows: install Git for Windows and restart the Fusion host shell or service."
},
"shell": {
"activePill": "활성",
@@ -7565,7 +7574,9 @@
"none": "(없음)",
"provider": "제공자",
"repository": "저장소",
"url": "URL"
"url": "URL",
"gitlabAriaLabel": "",
"gitlabBadge": ""
},
"spec": {
"aiReviseHeading": "AI에 수정 요청",
@@ -7666,6 +7677,35 @@
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
},
"gitlabTracking": {
"collapse": "",
"expand": "",
"instance": "",
"item": "",
"itemUnlinked": "",
"kind": "",
"kindGroupIssue": "",
"kindMergeRequest": "",
"kindProjectIssue": "",
"label": "",
"lastSynced": "",
"namespace": "",
"openAriaLabel": "",
"openBtn": "",
"stale": "",
"staleUnknown": "",
"state": "",
"stateUnknown": "",
"statusAriaLabel": "",
"statusLinked": "",
"statusStale": "",
"statusUnlinked": "",
"unlinkBtn": "",
"unlinkConfirm": "",
"unlinkMessage": "",
"unlinkTitle": "",
"unlinked": ""
}
},
"taskDocuments": {

View File

@@ -6916,7 +6916,16 @@
"repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.",
"repositorySetupTitle": "Repository setup",
"useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root.",
"connectGithub": "Connect GitHub"
"connectGithub": "Connect GitHub",
"gitPrerequisiteInstalled": "Git is installed on the Fusion host.",
"gitPrerequisiteInstalledVersion": "Git is installed on the Fusion host ({{version}}).",
"gitPrerequisiteInstallLink": "Open Git install downloads",
"gitPrerequisiteLinux": "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.",
"gitPrerequisiteMac": "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.",
"gitPrerequisiteMissingBody": "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces.",
"gitPrerequisiteMissingTitle": "Install Git before project setup",
"gitPrerequisiteReadyTitle": "Git prerequisite ready",
"gitPrerequisiteWindows": "Windows: install Git for Windows and restart the Fusion host shell or service."
},
"shell": {
"activePill": "活跃",
@@ -7565,7 +7574,9 @@
"none": "(无)",
"provider": "提供商",
"repository": "仓库",
"url": "URL"
"url": "URL",
"gitlabAriaLabel": "",
"gitlabBadge": ""
},
"spec": {
"aiReviseHeading": "请 AI 修订",
@@ -7666,6 +7677,35 @@
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
},
"gitlabTracking": {
"collapse": "",
"expand": "",
"instance": "",
"item": "",
"itemUnlinked": "",
"kind": "",
"kindGroupIssue": "",
"kindMergeRequest": "",
"kindProjectIssue": "",
"label": "",
"lastSynced": "",
"namespace": "",
"openAriaLabel": "",
"openBtn": "",
"stale": "",
"staleUnknown": "",
"state": "",
"stateUnknown": "",
"statusAriaLabel": "",
"statusLinked": "",
"statusStale": "",
"statusUnlinked": "",
"unlinkBtn": "",
"unlinkConfirm": "",
"unlinkMessage": "",
"unlinkTitle": "",
"unlinked": ""
}
},
"taskDocuments": {

View File

@@ -6916,7 +6916,16 @@
"repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.",
"repositorySetupTitle": "Repository setup",
"useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root.",
"connectGithub": "Connect GitHub"
"connectGithub": "Connect GitHub",
"gitPrerequisiteInstalled": "Git is installed on the Fusion host.",
"gitPrerequisiteInstalledVersion": "Git is installed on the Fusion host ({{version}}).",
"gitPrerequisiteInstallLink": "Open Git install downloads",
"gitPrerequisiteLinux": "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.",
"gitPrerequisiteMac": "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.",
"gitPrerequisiteMissingBody": "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces.",
"gitPrerequisiteMissingTitle": "Install Git before project setup",
"gitPrerequisiteReadyTitle": "Git prerequisite ready",
"gitPrerequisiteWindows": "Windows: install Git for Windows and restart the Fusion host shell or service."
},
"shell": {
"activePill": "作用中",
@@ -7565,7 +7574,9 @@
"none": "(無)",
"provider": "提供商",
"repository": "儲存庫",
"url": "URL"
"url": "URL",
"gitlabAriaLabel": "",
"gitlabBadge": ""
},
"spec": {
"aiReviseHeading": "請 AI 修訂",
@@ -7666,6 +7677,35 @@
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
},
"gitlabTracking": {
"collapse": "",
"expand": "",
"instance": "",
"item": "",
"itemUnlinked": "",
"kind": "",
"kindGroupIssue": "",
"kindMergeRequest": "",
"kindProjectIssue": "",
"label": "",
"lastSynced": "",
"namespace": "",
"openAriaLabel": "",
"openBtn": "",
"stale": "",
"staleUnknown": "",
"state": "",
"stateUnknown": "",
"statusAriaLabel": "",
"statusLinked": "",
"statusStale": "",
"statusUnlinked": "",
"unlinkBtn": "",
"unlinkConfirm": "",
"unlinkMessage": "",
"unlinkTitle": "",
"unlinked": ""
}
},
"taskDocuments": {

View File

@@ -6735,6 +6735,15 @@ export default interface Resources {
"firstTaskReady": "Your first task is ready!",
"getApiKeyLink": "Get your API key →",
"getStarted": "Get Started",
"gitPrerequisiteInstallLink": "Open Git install downloads",
"gitPrerequisiteInstalled": "Git is installed on the Fusion host.",
"gitPrerequisiteInstalledVersion": "Git is installed on the Fusion host ({{version}}).",
"gitPrerequisiteLinux": "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.",
"gitPrerequisiteMac": "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.",
"gitPrerequisiteMissingBody": "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces.",
"gitPrerequisiteMissingTitle": "Install Git before project setup",
"gitPrerequisiteReadyTitle": "Git prerequisite ready",
"gitPrerequisiteWindows": "Windows: install Git for Windows and restart the Fusion host shell or service.",
"githubCliAlreadyAuth": "GitHub CLI is already authenticated — issue imports and pull request tracking work right now. You're all set; no further action needed.",
"githubCliAuthNote": "GitHub CLI is already authenticated, so imports and PR tracking work now. OAuth from the dashboard is optional and only controls dashboard-managed connect/disconnect.",
"githubCliAuthSuccess": "GitHub CLI is authenticated. Imports and pull request tracking are available. Connect OAuth in Settings → Authentication if you want dashboard-managed sign-in controls.",