diff --git a/packages/core/src/__tests__/git-repository.test.ts b/packages/core/src/__tests__/git-repository.test.ts index d26f0fa052..4c6634904c 100644 --- a/packages/core/src/__tests__/git-repository.test.ts +++ b/packages/core/src/__tests__/git-repository.test.ts @@ -154,8 +154,45 @@ describe("ensureGitRepositoryForProjectPath", () => { await git(fakePkg, ["add", "index.js"]); await git(fakePkg, ["commit", "-m", "init"]); + // Also create a real sibling sub-repo to prove it IS detected while node_modules is excluded + const realRepo = join(projectPath, "my-app"); + mkdirSync(realRepo, { recursive: true }); + await git(realRepo, ["init", "-b", "main"]); + await git(realRepo, ["config", "user.email", "test@test.com"]); + await git(realRepo, ["config", "user.name", "Test"]); + writeFileSync(join(realRepo, "README.md"), "# my-app\n"); + await git(realRepo, ["add", "README.md"]); + await git(realRepo, ["commit", "-m", "init"]); + const detected = await detectWorkspaceRepos(projectPath); - expect(detected).toEqual([]); + // node_modules is excluded; my-app is detected + expect(detected).toEqual(["my-app"]); + }); + + it("skips auto-detection when workspaceMode is explicitly false in config.json", async () => { + const projectPath = tempDir("fusion-git-workspace-disabled-"); + // Create a real git sub-repo so detectWorkspaceRepos would find it + const subRepo = join(projectPath, "repo-a"); + mkdirSync(subRepo, { recursive: true }); + await git(subRepo, ["init", "-b", "main"]); + await git(subRepo, ["config", "user.email", "test@test.com"]); + await git(subRepo, ["config", "user.name", "Test"]); + writeFileSync(join(subRepo, "README.md"), "# repo-a\n"); + await git(subRepo, ["add", "README.md"]); + await git(subRepo, ["commit", "-m", "init"]); + + // Write config.json with workspaceMode: false (user disabled it via dashboard) + mkdirSync(join(projectPath, ".fusion"), { recursive: true }); + writeFileSync( + join(projectPath, ".fusion", "config.json"), + JSON.stringify({ settings: { workspaceMode: false } }), + ); + + const outcome = await ensureGitRepositoryForProjectPath(projectPath); + + // Should proceed to git init, not workspace detection + expect(outcome).toBe("initialized"); + expect(existsSync(join(projectPath, ".git"))).toBe(true); }); }); diff --git a/packages/core/src/git-repository.ts b/packages/core/src/git-repository.ts index a835db73d0..d55d354c49 100644 --- a/packages/core/src/git-repository.ts +++ b/packages/core/src/git-repository.ts @@ -68,16 +68,24 @@ export async function ensureGitRepositoryForProjectPath( CLI interactive setup writes workspace.json explicitly, but dashboard POST /api/projects and `fn project add` do not — without this fallback they would create a stray .git at the workspace root because loadWorkspaceConfig returned null. + + FNXC:Workspace 2026-06-24-17:00: + If the user has explicitly disabled workspace mode (workspaceMode: false in config.json), + skip auto-detection and proceed to git init. Without this guard, toggling workspace mode off + via the dashboard would have no lasting effect — the fallback would re-detect sub-repos and + re-create workspace.json on the next registration call. */ - const detectedRepos = await detectWorkspaceRepos(projectPath, runner, timeout); - if (detectedRepos.length > 0) { - try { - await saveWorkspaceConfig(projectPath, { repos: detectedRepos }); - } catch { - // Best-effort: persist for the fast path on future calls, but don't fail - // the current registration if the write fails (permissions, disk full, etc.). + if (!(await isWorkspaceModeExplicitlyDisabled(projectPath))) { + const detectedRepos = await detectWorkspaceRepos(projectPath, runner, timeout); + if (detectedRepos.length > 0) { + try { + await saveWorkspaceConfig(projectPath, { repos: detectedRepos }); + } catch { + // Best-effort: persist for the fast path on future calls, but don't fail + // the current registration if the write fails (permissions, disk full, etc.). + } + return "existing"; } - return "existing"; } try { @@ -195,6 +203,23 @@ export interface WorkspaceConfig { const WORKSPACE_CONFIG_FILENAME = "workspace.json"; +/** + * Reads .fusion/config.json and returns true when `workspaceMode` is explicitly + * set to `false`. This guards the auto-detection fallback so a user who has + * intentionally disabled workspace mode doesn't get it silently re-enabled. + */ +async function isWorkspaceModeExplicitlyDisabled(projectPath: string): Promise { + try { + const { readFile } = await import("node:fs/promises"); + const { join } = await import("node:path"); + const raw = await readFile(join(projectPath, ".fusion", "config.json"), "utf-8"); + const config = JSON.parse(raw) as { settings?: { workspaceMode?: boolean } }; + return config.settings?.workspaceMode === false; + } catch { + return false; + } +} + /* FNXC:Workspace 2026-06-22-00:00: Workspace repo entries are later joined onto the workspace root to resolve worktrees, so an