feat: pnpm dev --isolated runs against its own database and project

Working on Fusion from inside a machine that already runs one, a plain
`pnpm dev` shares that instance's LIVE database. Everything durable hangs off
$HOME/.fusion — settings, credentials, central DB, the embedded Postgres data
dir — and a process pointed at a data dir whose postmaster is already running
attaches to it instead of starting its own.

--isolated spawns the dev child with HOME pointed at a sandbox, so it gets its
own settings, credentials and Postgres cluster on its own port. It also moves
the child's cwd, which is the half that is easy to miss: `fn dashboard`
derives its project from the working directory and has no project flag, so
isolating HOME alone leaves both instances on `<repo>/.fusion` — including
`.fusion/tasks/<id>/`, which the orphaned-task-dir sweep re-imports, so a
fresh dev database would adopt the real instance's tasks.

The sandbox defaults to ~/.fusion-dev/<checkout-name>/{home,project}: outside
the work tree so it neither shows up in git status nor dies on a clean
checkout, and keyed by checkout so two clones cannot collide. The project dir
is git init-ed on first use because Fusion projects are git work trees.
Changing cwd is safe because PRELOAD/LOADER/ENTRY are already absolute.

Verified in a container beside a running Fusion: the isolated instance
reported zero projects while the real one reported two, on separate Postgres
clusters (ports 42617 and 38311).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-08-19 21:14:31 -07:00
parent 7ae238cc67
commit 338dc173ff
5 changed files with 167 additions and 1 deletions

View File

@@ -133,6 +133,8 @@ export function parseDevWrapperArgs(rawArgs, env = process.env) {
`--tunnel=PORT` targets a port other than the dashboard's (e.g. a Vite server on 5173).
*/
let tunnel = env.FUSION_DEV_TUNNEL === "1";
let isolated = env.FUSION_DEV_ISOLATED === "1";
let isolatedDir = env.FUSION_DEV_ISOLATED_DIR || undefined;
let tunnelPort = env.FUSION_DEV_TUNNEL_PORT ? Number(env.FUSION_DEV_TUNNEL_PORT) : undefined;
for (let i = 0; i < rawArgs.length; i += 1) {
@@ -168,6 +170,35 @@ export function parseDevWrapperArgs(rawArgs, env = process.env) {
continue;
}
/*
FNXC:DevIsolation 2026-08-20-04:10:
`--isolated` runs the dev server against its OWN database and its OWN project directory, for
working on Fusion from inside a machine that is already running one. Everything durable hangs
off $HOME/.fusion — global settings, credentials, the central DB, the embedded Postgres data dir
— and a second process pointed at a data dir whose postmaster is already running simply ATTACHES
to it, so a plain `pnpm dev` inside a Fusion container silently shares the live database.
Isolating HOME alone is not enough: `fn dashboard` derives its project from the working
directory, so both instances would still share `<repo>/.fusion`, including `.fusion/tasks/<id>/`
— and self-healing's orphaned-task-dir sweep re-imports task directories that have no row,
meaning a fresh dev database would adopt the real instance's tasks. The flag therefore moves the
working directory too.
`--isolated=<dir>` puts the sandbox somewhere specific; otherwise it is a stable per-repo path so
the dev database survives restarts.
*/
if (arg === "--isolated") {
isolated = true;
continue;
}
if (arg.startsWith("--isolated=")) {
isolated = true;
isolatedDir = arg.slice("--isolated=".length);
if (!isolatedDir) throw new Error("Missing directory for --isolated=<dir>.");
continue;
}
if (arg === "--tunnel") {
tunnel = true;
const next = rawArgs[i + 1];
@@ -201,9 +232,33 @@ export function parseDevWrapperArgs(rawArgs, env = process.env) {
watchSourceFromFlag,
tunnel,
tunnelPort,
isolated,
isolatedDir,
};
}
/**
* Where an isolated dev instance keeps its state.
*
* FNXC:DevIsolation 2026-08-20-04:10:
* `home` becomes the child's HOME, giving it its own `.fusion` — settings, credentials, central DB,
* and an embedded Postgres cluster on its own port (a fresh data dir binds a free port; an existing
* one would have been attached to instead). `project` becomes the child's working directory, so the
* dev instance cannot reach the real instance's `.fusion/tasks/` and adopt its tasks.
*
* The default lives under the REAL home rather than inside the repo: a project directory inside a
* git work tree shows up in status and risks being committed, and the dev database should not be
* wiped by a clean checkout. It is keyed by repo directory name so several checkouts do not collide.
*/
export function resolveIsolatedDevPaths({ repoRoot, home, explicitDir } = {}) {
if (!repoRoot) throw new Error("resolveIsolatedDevPaths requires repoRoot");
if (!home && !explicitDir) throw new Error("resolveIsolatedDevPaths requires home or explicitDir");
const repoName = repoRoot.split(/[\\/]+/).filter(Boolean).pop() || "fusion";
const base = explicitDir ?? `${home}/.fusion-dev/${repoName}`;
return { base, home: `${base}/home`, project: `${base}/project` };
}
/*
FNXC:DevTunnel 2026-08-19-02:05:
Mirrors DEV_SERVER_LISTENING_MESSAGE in packages/cli/src/commands/dev-source-restart.ts. The literal