diff --git a/.changeset/fn-7470-git-onboarding.md b/.changeset/fn-7470-git-onboarding.md
new file mode 100644
index 0000000000..f58fd6e383
--- /dev/null
+++ b/.changeset/fn-7470-git-onboarding.md
@@ -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.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 8e011bdb2b..c993aee024 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -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.
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 8febe68f34..03e9856126 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -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.
diff --git a/packages/core/src/__tests__/git-cli-status.test.ts b/packages/core/src/__tests__/git-cli-status.test.ts
new file mode 100644
index 0000000000..c30bb51f87
--- /dev/null
+++ b/packages/core/src/__tests__/git-cli-status.test.ts
@@ -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",
+ });
+ });
+});
diff --git a/packages/core/src/git-cli-status.ts b/packages/core/src/git-cli-status.ts
new file mode 100644
index 0000000000..b62cb8d5d7
--- /dev/null
+++ b/packages/core/src/git-cli-status.ts
@@ -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 {
+ 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();
+ });
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 746ce443eb..78d589d7d4 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -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,
diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts
index 5f82f06326..a23c5a8539 100644
--- a/packages/dashboard/app/api/legacy.ts
+++ b/packages/dashboard/app/api/legacy.ts
@@ -2338,14 +2338,22 @@ export async function probeProviderModels(params: ProbeModelsParams): Promise {
return dedupe("/auth/status", () => api<{
providers: AuthProvider[];
ghCli?: { available: boolean; authenticated: boolean };
+ gitCli?: GitCliStatus;
}>("/auth/status"), options);
}
diff --git a/packages/dashboard/app/components/ModelOnboardingModal.css b/packages/dashboard/app/components/ModelOnboardingModal.css
index aa8104484a..0d22fdef22 100644
--- a/packages/dashboard/app/components/ModelOnboardingModal.css
+++ b/packages/dashboard/app/components/ModelOnboardingModal.css
@@ -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
diff --git a/packages/dashboard/app/components/ModelOnboardingModal.tsx b/packages/dashboard/app/components/ModelOnboardingModal.tsx
index 837b099adf..4e2734fd3b 100644
--- a/packages/dashboard/app/components/ModelOnboardingModal.tsx
+++ b/packages/dashboard/app/components/ModelOnboardingModal.tsx
@@ -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([]);
const [ghCliStatus, setGhCliStatus] = useState(undefined);
+ const [gitCliStatus, setGitCliStatus] = useState(undefined);
const [authLoading, setAuthLoading] = useState(true);
const [authActionInProgress, setAuthActionInProgress] = useState(null);
const [loginInstructions, setLoginInstructions] = useState>({});
@@ -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 = {};
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.")}
)}
+ {/*
+ 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 && (
+
+
+
+
+ {gitCliStatus.available
+ ? t("setup.gitPrerequisiteReadyTitle", "Git prerequisite ready")
+ : t("setup.gitPrerequisiteMissingTitle", "Install Git before project setup")}
+
+
+ {gitCliStatus.available ? (
+
{gitVersionLabel}
+ ) : (
+ <>
+
+ {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.")}
+
+
+ - {t("setup.gitPrerequisiteMac", "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.")}
+ - {t("setup.gitPrerequisiteWindows", "Windows: install Git for Windows and restart the Fusion host shell or service.")}
+ - {t("setup.gitPrerequisiteLinux", "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.")}
+
+
+ {t("setup.gitPrerequisiteInstallLink", "Open Git install downloads")}
+
+ >
+ )}
+
+ )}
+
{!isGitHubReady && (
diff --git a/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx b/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx
index 3a53534d93..bbc0732752 100644
--- a/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx
+++ b/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx
@@ -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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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: [
diff --git a/packages/dashboard/src/__tests__/routes-auth.test.ts b/packages/dashboard/src/__tests__/routes-auth.test.ts
index 3eee0e51c2..e74d325797 100644
--- a/packages/dashboard/src/__tests__/routes-auth.test.ts
+++ b/packages/dashboard/src/__tests__/routes-auth.test.ts
@@ -118,8 +118,10 @@ vi.mock("@fusion/core", async (importOriginal) => {
const { createCoreMock } = await import("../test/mockCoreEngine.js");
return createCoreMock(() => importOriginal(), {
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).mockReturnValue([
{ id: "github-copilot", name: "GitHub Copilot" },
diff --git a/packages/dashboard/src/routes/register-auth-routes.ts b/packages/dashboard/src/routes/register-auth-routes.ts
index db8a3d66ad..5117fc3e71 100644
--- a/packages/dashboard/src/routes/register-auth-routes.ts
+++ b/packages/dashboard/src/routes/register-auth-routes.ts
@@ -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>;
+ 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;
diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json
index 7935b39cda..752d08667c 100644
--- a/packages/i18n/locales/en/app.json
+++ b/packages/i18n/locales/en/app.json
@@ -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",
diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json
index fe625fd279..1dc687784d 100644
--- a/packages/i18n/locales/es/app.json
+++ b/packages/i18n/locales/es/app.json
@@ -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": {
diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json
index 06167b01c2..e96dd61622 100644
--- a/packages/i18n/locales/fr/app.json
+++ b/packages/i18n/locales/fr/app.json
@@ -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": {
diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json
index 8015e66af1..905aa4cf1c 100644
--- a/packages/i18n/locales/ko/app.json
+++ b/packages/i18n/locales/ko/app.json
@@ -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": {
diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json
index 5cd4e58566..7efce8040d 100644
--- a/packages/i18n/locales/zh-CN/app.json
+++ b/packages/i18n/locales/zh-CN/app.json
@@ -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": {
diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json
index 35f119dae2..7101b35d36 100644
--- a/packages/i18n/locales/zh-TW/app.json
+++ b/packages/i18n/locales/zh-TW/app.json
@@ -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": {
diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts
index 5ff87656ae..e512959c0a 100644
--- a/packages/i18n/src/resources.d.ts
+++ b/packages/i18n/src/resources.d.ts
@@ -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.",