diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index 41f4f3008a..fa5d0a09d6 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -1791,7 +1791,16 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?: cwd, central: centralCoreForEngine, logPrefix: "dashboard", - autoRegister: true, + /* + * FNXC:CliProjectOnboarding 2026-07-03-03:45: + * Bare `fusion` / `fn` / `fn dashboard` / `fusion dashboard` must NOT auto-register the CWD as + * a project. Auto-registration silently created a project for whatever directory the dashboard + * happened to launch from. Instead: use the CWD project only if it is ALREADY registered, + * otherwise start with no CWD project and let the dashboard prompt the operator through + * onboarding (ProjectOverview "Add your first project" -> SetupWizard). Operators who want the + * CWD registered can still run `fn init`. + */ + autoRegister: false, }).catch(() => undefined as Awaited> | undefined), (async () => { try { diff --git a/packages/desktop/src/__tests__/engine-runtime.test.ts b/packages/desktop/src/__tests__/engine-runtime.test.ts new file mode 100644 index 0000000000..5bde5a2367 --- /dev/null +++ b/packages/desktop/src/__tests__/engine-runtime.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { resolveDesktopRuntimePrimaryProject } from "../engine-runtime"; + +/* + * The desktop embedded runtime must NEVER auto-register a project (e.g. the home directory). + * resolveDesktopRuntimePrimaryProject only picks an already-registered project as the primary + * engine target, and registers nothing. + */ +describe("resolveDesktopRuntimePrimaryProject", () => { + it("returns null when no projects are registered (never auto-registers)", async () => { + let registerCalled = false; + const central = { + listProjects: async () => [], + registerProject: async () => { + registerCalled = true; + throw new Error("resolveDesktopRuntimePrimaryProject must not register a project"); + }, + } as unknown as import("@fusion/core").CentralCore; + + const result = await resolveDesktopRuntimePrimaryProject(central); + expect(result).toBeNull(); + expect(registerCalled).toBe(false); + }); + + it("returns the first existing project as primary without registering", async () => { + const projects = [{ id: "proj_1" }, { id: "proj_2" }]; + let registerCalled = false; + const central = { + listProjects: async () => projects, + registerProject: async () => { + registerCalled = true; + return projects[0]; + }, + } as unknown as import("@fusion/core").CentralCore; + + const result = await resolveDesktopRuntimePrimaryProject(central); + expect(result?.id).toBe("proj_1"); + expect(registerCalled).toBe(false); + }); +}); diff --git a/packages/desktop/src/engine-runtime.ts b/packages/desktop/src/engine-runtime.ts index a6fbbd217d..80d62bcd3e 100644 --- a/packages/desktop/src/engine-runtime.ts +++ b/packages/desktop/src/engine-runtime.ts @@ -1,26 +1,21 @@ -import { basename } from "node:path"; - import type { CentralCore, RegisteredProject } from "@fusion/core"; /* - * FNXC:DesktopRuntime 2026-06-21-02:04: - * Desktop local mode starts engines by default, so the embedded server should prefer the project represented by the desktop runtime root instead of whichever registered engine happens to be first. The runtime root may be a home directory, so this path must not call helpers that initialize Git repositories as a side effect. + * FNXC:DesktopRuntime 2026-07-03-03:30: + * The desktop app must NEVER auto-register a project for its runtime root (the user's home + * directory). Doing so created a bogus "cwd-mode" project in ~ on first launch and dropped the + * operator straight onto a board for a directory they never chose. Instead the embedded runtime + * starts with NO default project when none exist, and the dashboard's empty state prompts the + * operator through onboarding (ProjectOverview "Add your first project" -> SetupWizard -> + * POST /api/projects) to register a real project directory. + * + * This resolver only PICKS an existing project as the primary engine target (for operators who + * already onboarded projects); it registers nothing. Returns null when there are no projects yet. + * It must not call helpers that initialize Git repositories as a side effect. */ -export async function ensureDesktopRuntimeProject(centralCore: CentralCore, rootDir: string): Promise { - const existing = await centralCore.getProjectByPath(rootDir); - if (existing) { - return existing; - } - +export async function resolveDesktopRuntimePrimaryProject( + centralCore: CentralCore, +): Promise { const projects = await centralCore.listProjects(); - if (projects.length > 0) { - return projects[0]!; - } - - const registered = await centralCore.registerProject({ - path: rootDir, - name: basename(rootDir) || "Fusion Desktop", - isolationMode: "in-process", - }); - return centralCore.updateProject(registered.id, { status: "active" }); + return projects.length > 0 ? projects[0]! : null; } diff --git a/packages/desktop/src/local-runtime.ts b/packages/desktop/src/local-runtime.ts index 2a8bf4db6e..f660853e10 100644 --- a/packages/desktop/src/local-runtime.ts +++ b/packages/desktop/src/local-runtime.ts @@ -3,7 +3,7 @@ import { appendFileSync } from "node:fs"; import type { Server } from "node:http"; import type { AddressInfo } from "node:net"; -import { ensureDesktopRuntimeProject } from "./engine-runtime.js"; +import { resolveDesktopRuntimePrimaryProject } from "./engine-runtime.js"; /* * FNXC:DesktopRuntime 2026-07-02-14:35: @@ -83,17 +83,25 @@ async function createDashboardServerDefault(store: TaskStoreLike, rootDir: strin try { strace("createDashboardServer: centralCore.init"); await centralCore.init(); - strace("createDashboardServer: ensureDesktopRuntimeProject"); - const rootProject = await ensureDesktopRuntimeProject(centralCore, rootDir); - strace(`createDashboardServer: startAll (rootProject=${rootProject.id})`); + /* + * FNXC:DesktopRuntime 2026-07-03-03:30: + * Do NOT auto-register the home directory as a project. Start engines for whatever projects the + * operator has already onboarded (none on a fresh install), and only pick a default/primary engine + * when such a project exists. With zero projects the server starts engine-less and the dashboard + * shows its onboarding empty state; new projects register via POST /api/projects and their engines + * spin up lazily through onProjectFirstAccessed / reconciliation. + */ + void rootDir; // runtime root no longer implies a project; kept for signature/back-compat. + strace("createDashboardServer: startAll"); await engineManager.startAll(); strace("createDashboardServer: startAll DONE; startReconciliation"); engineManager.startReconciliation(); - strace("createDashboardServer: ensureEngine(rootProject)"); - const primaryEngine = await engineManager.ensureEngine(rootProject.id); + const rootProject = await resolveDesktopRuntimePrimaryProject(centralCore); + strace(`createDashboardServer: primaryProject=${rootProject?.id ?? "none"}`); + const primaryEngine = rootProject ? await engineManager.ensureEngine(rootProject.id) : undefined; strace("createDashboardServer: createServer"); const app = createServer(store as never, { - engine: primaryEngine, + ...(primaryEngine ? { engine: primaryEngine } : {}), engineManager, centralCore, onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId), diff --git a/packages/desktop/src/local-server.ts b/packages/desktop/src/local-server.ts index e6faa5ce95..853c37617b 100644 --- a/packages/desktop/src/local-server.ts +++ b/packages/desktop/src/local-server.ts @@ -2,7 +2,7 @@ import type { AddressInfo } from "node:net"; import { once } from "node:events"; import type { Server } from "node:http"; -import { ensureDesktopRuntimeProject } from "./engine-runtime.js"; +import { resolveDesktopRuntimePrimaryProject } from "./engine-runtime.js"; type TaskStoreLike = { init(): Promise; @@ -70,12 +70,13 @@ export class DesktopLocalServerManager { await centralCore.close?.(); }; await centralCore.init(); - const rootProject = await ensureDesktopRuntimeProject(centralCore, this.rootDir); + // FNXC:DesktopRuntime 2026-07-03-03:30: never auto-register the runtime root as a project (see engine-runtime.ts). await engineManager.startAll(); engineManager.startReconciliation(); - const primaryEngine = await engineManager.ensureEngine(rootProject.id); + const rootProject = await resolveDesktopRuntimePrimaryProject(centralCore); + const primaryEngine = rootProject ? await engineManager.ensureEngine(rootProject.id) : undefined; const app = createServer(store as never, { - engine: primaryEngine, + ...(primaryEngine ? { engine: primaryEngine } : {}), engineManager, centralCore, onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),