fix(desktop,cli): never auto-create a project in home/cwd; onboard instead
The desktop embedded runtime auto-registered its runtime root (the user's HOME directory) as a project on first launch, and bare `fusion` / `fn` / `fn dashboard` / `fusion dashboard` auto-registered the CWD as a project. Both silently created a "cwd-mode" project the operator never chose and dropped them onto a board for it. - Desktop: replace ensureDesktopRuntimeProject (which registered home) with resolveDesktopRuntimePrimaryProject, which only PICKS an already-registered project as the primary engine target and registers nothing. With no projects the server starts engine-less (createServer's engine is optional) and the dashboard shows its onboarding empty state. Applied to both the primary (local-runtime) and legacy (local-server) desktop server paths. - CLI dashboard command: ensureCwdProjectRegistered now runs with autoRegister:false, so it uses the CWD project only if already registered, else starts with none and the dashboard onboards. (serve/daemon keep their existing --no-auto-register flag; the CLI `desktop` launcher unchanged.) Verified: with zero projects the embedded server starts, /api/health -> 200, /api/projects -> [], / serves the client. Unit test asserts resolveDesktopRuntimePrimaryProject registers nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ReturnType<typeof ensureCwdProjectRegistered>> | undefined),
|
||||
(async () => {
|
||||
try {
|
||||
|
||||
40
packages/desktop/src/__tests__/engine-runtime.test.ts
Normal file
40
packages/desktop/src/__tests__/engine-runtime.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<RegisteredProject> {
|
||||
const existing = await centralCore.getProjectByPath(rootDir);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
export async function resolveDesktopRuntimePrimaryProject(
|
||||
centralCore: CentralCore,
|
||||
): Promise<RegisteredProject | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<void>;
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user