From 1511cfd595a18ea6fb90074afa0963ce99bee40e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Wed, 22 Apr 2026 21:45:55 +0800 Subject: [PATCH 1/5] fix: daemon spawn uses CLI entry path instead of command module The runDaemon function was using import.meta.url (pointing to start.js) as the script for the spawned child process. This meant the child ran `node start.js start` which has no CLI entry logic and exits immediately. Added cliEntryScript() that resolves to the correct CLI entry (cli.js) regardless of whether the code is bundled or split into separate files. Closes #27 --- packages/cli/src/commands/start.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index b2e8ee3..76a3b8b 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -1,7 +1,7 @@ import { createWriteStream } from "node:fs"; import { readFileSync } from "node:fs"; import { mkdir } from "node:fs/promises"; -import { join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { parseNerveConfig } from "@uncaged/nerve-core"; @@ -85,6 +85,22 @@ async function runForeground(nerveRoot: string): Promise { await kernel.ready; } +/** Path to the CLI entry script (for spawning `start` without `-d`). */ +function cliEntryScript(): string { + const argv1 = process.argv[1]; + if (argv1) { + const base = basename(argv1); + if (base === "cli.js" || base === "cli.ts") return argv1; + } + const here = fileURLToPath(import.meta.url); + const base = basename(here); + if (base === "cli.js" || base === "cli.ts") return here; + if (base === "start.js" || base === "start.ts") { + return join(dirname(here), "..", base === "start.ts" ? "cli.ts" : "cli.js"); + } + return here; +} + async function runDaemon(nerveRoot: string): Promise { if (isRunning()) { const pid = readPidFile(); @@ -108,9 +124,9 @@ async function runDaemon(nerveRoot: string): Promise { else resolve(); }); - const selfPath = fileURLToPath(import.meta.url); + const cliPath = cliEntryScript(); - const child = spawn(process.execPath, [selfPath, "start"], { + const child = spawn(process.execPath, [cliPath, "start"], { detached: true, stdio: ["ignore", logStream.fd, logStream.fd], env: { ...process.env, NERVE_DAEMON_MODE: "1" }, -- 2.43.0 From 3f2c9df75dd385e113675f8571db92c84ad9e716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Wed, 22 Apr 2026 21:57:34 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20simplify=20cliEntryScript()=20?= =?UTF-8?q?=E2=80=94=20remove=20multi-level=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback from xiaoju: the three-level fallback was over-defensive. Since start.ts and cli.ts have a fixed relative position (commands/start.ts → ../cli.ts), we can derive the path directly from import.meta.url with an existsSync guard. This makes path errors explicit (throw) instead of silently falling back to a potentially wrong path. --- packages/cli/src/commands/start.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 76a3b8b..1715860 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -1,7 +1,6 @@ -import { createWriteStream } from "node:fs"; -import { readFileSync } from "node:fs"; +import { createWriteStream, existsSync, readFileSync } from "node:fs"; import { mkdir } from "node:fs/promises"; -import { basename, dirname, join } from "node:path"; +import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { parseNerveConfig } from "@uncaged/nerve-core"; @@ -87,18 +86,13 @@ async function runForeground(nerveRoot: string): Promise { /** Path to the CLI entry script (for spawning `start` without `-d`). */ function cliEntryScript(): string { - const argv1 = process.argv[1]; - if (argv1) { - const base = basename(argv1); - if (base === "cli.js" || base === "cli.ts") return argv1; - } const here = fileURLToPath(import.meta.url); - const base = basename(here); - if (base === "cli.js" || base === "cli.ts") return here; - if (base === "start.js" || base === "start.ts") { - return join(dirname(here), "..", base === "start.ts" ? "cli.ts" : "cli.js"); + const ext = here.endsWith(".ts") ? ".ts" : ".js"; + const cliPath = join(dirname(here), "..", `cli${ext}`); + if (!existsSync(cliPath)) { + throw new Error(`CLI entry not found: ${cliPath}`); } - return here; + return cliPath; } async function runDaemon(nerveRoot: string): Promise { -- 2.43.0 From 97305bd9afe79f033ad16bf09db2e94047541895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Wed, 22 Apr 2026 22:06:57 +0800 Subject: [PATCH 3/5] fix(cli): resolve CLI entry path for bundled dist output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cliEntryScript() assumed source directory structure (src/commands/start.ts → ../cli.ts), but after tsup bundles everything into dist/cli.js, import.meta.url points to dist/cli.js and the '../cli.js' path resolves to a non-existent file. Use candidate-based lookup: try same-dir, parent-dir, then self (bundled case). --- packages/cli/src/commands/start.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 1715860..e375996 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -88,9 +88,16 @@ async function runForeground(nerveRoot: string): Promise { function cliEntryScript(): string { const here = fileURLToPath(import.meta.url); const ext = here.endsWith(".ts") ? ".ts" : ".js"; - const cliPath = join(dirname(here), "..", `cli${ext}`); - if (!existsSync(cliPath)) { - throw new Error(`CLI entry not found: ${cliPath}`); + // When bundled, `here` is already the CLI entry (e.g. dist/cli.js). + // When running from source, `here` is src/commands/start.ts → go up to src/cli.ts. + const candidates = [ + join(dirname(here), `cli${ext}`), + join(dirname(here), "..", `cli${ext}`), + here, // bundled: this file IS the CLI entry + ]; + const cliPath = candidates.find((p) => existsSync(p)); + if (!cliPath) { + throw new Error(`CLI entry not found (searched: ${candidates.join(", ")})`); } return cliPath; } -- 2.43.0 From 606eff6d70428c91c6257b69e4b1be2f227e3974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Wed, 22 Apr 2026 22:11:58 +0800 Subject: [PATCH 4/5] fix(cli): remove self-fallback in cliEntryScript candidates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: third candidate (here) is wrong — if bundled and source candidates both miss, falling back to self reproduces the original bug. Keep only the two valid candidates and throw on miss. --- packages/cli/src/commands/start.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index e375996..2df8ef5 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -91,9 +91,8 @@ function cliEntryScript(): string { // When bundled, `here` is already the CLI entry (e.g. dist/cli.js). // When running from source, `here` is src/commands/start.ts → go up to src/cli.ts. const candidates = [ - join(dirname(here), `cli${ext}`), - join(dirname(here), "..", `cli${ext}`), - here, // bundled: this file IS the CLI entry + join(dirname(here), `cli${ext}`), // bundled: dist/cli.js + join(dirname(here), "..", `cli${ext}`), // source: src/commands/start.ts → src/cli.ts ]; const cliPath = candidates.find((p) => existsSync(p)); if (!cliPath) { -- 2.43.0 From 85fa282d2ee9f984e5e106d953f375de7a60279a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Wed, 22 Apr 2026 22:13:21 +0800 Subject: [PATCH 5/5] fix(cli): create initial git commit after workspace init git init without add+commit leaves the workspace in a dirty state with no baseline to diff against. --- packages/cli/src/commands/init.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index e365609..6fb03f4 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -248,6 +248,8 @@ async function runInitWorkspace(force: boolean): Promise { if (!existsSync(join(nerveRoot, ".git"))) { try { await runCommand("git", ["init"], nerveRoot); + await runCommand("git", ["add", "."], nerveRoot); + await runCommand("git", ["commit", "-m", "Initial nerve workspace"], nerveRoot); } catch { process.stdout.write("⚠️ git init failed — skipping.\n"); } -- 2.43.0