Compare commits

...

4 Commits

Author SHA1 Message Date
xiaoju 640f170de8 refactor: add daemon subcommand group and dev foreground mode
- Create 'nerve daemon' subcommand group: start, stop, status, restart, logs
- Create 'nerve dev' for foreground mode (replaces old start without -d)
- 'nerve daemon start' is always background (removed -d/--daemon flag)
- Keep top-level aliases: nerve start/stop/status/logs → nerve daemon *
- Extract runStopCommand() for restart reuse
- Add daemon-cli tests

Closes #53

小橘 🍊(NEKO Team)
2026-04-23 01:16:13 +00:00
xiaoju 119b1f3722 chore: enforce pnpm publish for all packages unconditionally
小橘 <xiaoju@shazhou.work>
2026-04-23 00:49:39 +00:00
xiaoju 96ea4b46ff chore: add prepublish guard against npm publish with workspace:* deps
小橘 <xiaoju@shazhou.work>
2026-04-23 00:47:56 +00:00
xiaoju 57881533a8 docs: fix publish skill — use pnpm publish for workspace:* conversion
小橘 <xiaoju@shazhou.work>
2026-04-23 00:43:51 +00:00
11 changed files with 146 additions and 65 deletions
+4 -3
View File
@@ -56,9 +56,10 @@ pnpm -r run build
```bash ```bash
# Only publish packages that have version bumps # Only publish packages that have version bumps
cd packages/core && npm publish --access public # MUST use pnpm publish (not npm) — pnpm converts workspace:* to real versions
cd packages/daemon && npm publish --access public cd packages/core && pnpm publish --access public --no-git-checks
cd packages/cli && npm publish --access public cd packages/daemon && pnpm publish --access public --no-git-checks
cd packages/cli && pnpm publish --access public --no-git-checks
``` ```
### 5. Commit & tag ### 5. Commit & tag
+1
View File
@@ -14,6 +14,7 @@
"access": "public" "access": "public"
}, },
"scripts": { "scripts": {
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
"build": "tsup", "build": "tsup",
"test": "vitest run" "test": "vitest run"
}, },
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { daemonCommand } from "../commands/daemon.js";
import { devCommand } from "../commands/dev.js";
import { daemonStartCommand } from "../commands/start.js";
describe("nerve daemon command group", () => {
it("exposes start, stop, status, restart, and logs subcommands", () => {
const subs = daemonCommand.subCommands;
expect(subs).toBeDefined();
if (!subs) {
throw new Error("expected daemonCommand.subCommands");
}
expect(Object.keys(subs).sort()).toEqual(["logs", "restart", "start", "status", "stop"]);
});
it("shares the same start command object as top-level nerve start alias", () => {
const subs = daemonCommand.subCommands;
expect(subs?.start).toBe(daemonStartCommand);
});
});
describe("nerve dev", () => {
it("is a foreground dev command", () => {
expect(devCommand.meta?.name).toBe("dev");
expect(devCommand.meta?.description).toMatch(/foreground/i);
});
});
+6 -2
View File
@@ -1,9 +1,11 @@
import { defineCommand, runMain } from "citty"; import { defineCommand, runMain } from "citty";
import { daemonCommand } from "./commands/daemon.js";
import { devCommand } from "./commands/dev.js";
import { initCommand } from "./commands/init.js"; import { initCommand } from "./commands/init.js";
import { logsCommand } from "./commands/logs.js"; import { logsCommand } from "./commands/logs.js";
import { senseCommand } from "./commands/sense.js"; import { senseCommand } from "./commands/sense.js";
import { startCommand } from "./commands/start.js"; import { daemonStartCommand } from "./commands/start.js";
import { statusCommand } from "./commands/status.js"; import { statusCommand } from "./commands/status.js";
import { stopCommand } from "./commands/stop.js"; import { stopCommand } from "./commands/stop.js";
import { storeCommand } from "./commands/store.js"; import { storeCommand } from "./commands/store.js";
@@ -17,7 +19,9 @@ const main = defineCommand({
}, },
subCommands: { subCommands: {
init: initCommand, init: initCommand,
start: startCommand, daemon: daemonCommand,
dev: devCommand,
start: daemonStartCommand,
stop: stopCommand, stop: stopCommand,
status: statusCommand, status: statusCommand,
logs: logsCommand, logs: logsCommand,
+31
View File
@@ -0,0 +1,31 @@
import { defineCommand } from "citty";
import { logsCommand } from "./logs.js";
import { daemonStartCommand, runDaemonStartCommand } from "./start.js";
import { statusCommand } from "./status.js";
import { runStopCommand, stopCommand } from "./stop.js";
const daemonRestartCommand = defineCommand({
meta: {
name: "restart",
description: "Stop then start the nerve daemon",
},
async run() {
await runStopCommand();
await runDaemonStartCommand();
},
});
export const daemonCommand = defineCommand({
meta: {
name: "daemon",
description: "Manage the nerve background daemon",
},
subCommands: {
start: daemonStartCommand,
stop: stopCommand,
status: statusCommand,
restart: daemonRestartCommand,
logs: logsCommand,
},
});
+17
View File
@@ -0,0 +1,17 @@
import { defineCommand } from "citty";
import { runForegroundKernelSession } from "../run-foreground-kernel.js";
import { loadDaemonModule } from "../workspace-daemon.js";
import { getNerveRoot } from "../workspace.js";
export const devCommand = defineCommand({
meta: {
name: "dev",
description: "Run the nerve kernel in the foreground (development mode)",
},
async run() {
const nerveRoot = getNerveRoot();
const { createKernel } = await loadDaemonModule(nerveRoot);
await runForegroundKernelSession(nerveRoot, createKernel);
},
});
+11 -27
View File
@@ -5,8 +5,6 @@ import { fileURLToPath } from "node:url";
import { defineCommand } from "citty"; import { defineCommand } from "citty";
import { runForegroundKernelSession } from "../run-foreground-kernel.js";
import { loadDaemonModule } from "../workspace-daemon.js";
import { import {
getLogPath, getLogPath,
getNerveRoot, getNerveRoot,
@@ -52,15 +50,10 @@ function daemonBootstrapScript(): string {
return bootstrapJs; return bootstrapJs;
} }
throw new Error( throw new Error(
`daemon-bootstrap.js not found next to CLI at ${bootstrapJs}. Build the CLI package (e.g. \`pnpm --filter @uncaged/nerve-cli build\`) before using background mode (\`nerve start -d\`).`, `daemon-bootstrap.js not found next to CLI at ${bootstrapJs}. Build the CLI package (e.g. \`pnpm --filter @uncaged/nerve-cli build\`) before using \`nerve daemon start\`.`,
); );
} }
async function runForeground(nerveRoot: string): Promise<void> {
const { createKernel } = await loadDaemonModule(nerveRoot);
await runForegroundKernelSession(nerveRoot, createKernel);
}
async function runDaemon(nerveRoot: string): Promise<void> { async function runDaemon(nerveRoot: string): Promise<void> {
if (isRunning()) { if (isRunning()) {
const pid = readPidFile(); const pid = readPidFile();
@@ -110,29 +103,20 @@ async function runDaemon(nerveRoot: string): Promise<void> {
process.stdout.write(`✅ Nerve daemon started (pid ${pid}).\n`); process.stdout.write(`✅ Nerve daemon started (pid ${pid}).\n`);
process.stdout.write(` Logs: ${logPath}\n`); process.stdout.write(` Logs: ${logPath}\n`);
process.stdout.write(" Run `nerve stop` to stop.\n"); process.stdout.write(" Run `nerve daemon stop` (or `nerve stop`) to stop.\n");
} }
export const startCommand = defineCommand({ /** Background daemon only — use `nerve dev` for foreground mode. */
export async function runDaemonStartCommand(): Promise<void> {
await runDaemon(getNerveRoot());
}
export const daemonStartCommand = defineCommand({
meta: { meta: {
name: "start", name: "start",
description: "Start the nerve daemon", description: "Start the nerve daemon in the background",
}, },
args: { async run() {
daemon: { await runDaemonStartCommand();
type: "boolean",
alias: "d",
description: "Run as background daemon",
default: false,
},
},
async run({ args }) {
const nerveRoot = getNerveRoot();
if (args.daemon) {
await runDaemon(nerveRoot);
} else {
await runForeground(nerveRoot);
}
}, },
}); });
+38 -33
View File
@@ -15,44 +15,49 @@ async function waitForExit(pid: number, timeoutMs: number): Promise<boolean> {
return false; return false;
} }
/** Core stop logic — also used by `nerve daemon restart`. */
export async function runStopCommand(): Promise<void> {
const pid = readPidFile();
if (pid === null) {
process.stdout.write("⚠️ No PID file found — daemon may not be running.\n");
return;
}
if (!isRunning()) {
process.stdout.write("⚠️ Daemon is not running (stale PID file). Cleaning up.\n");
removePidFile();
return;
}
process.stdout.write(`Stopping nerve daemon (pid ${pid})…\n`);
try {
process.kill(pid, "SIGTERM");
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
process.stderr.write(`❌ Failed to send SIGTERM: ${msg}\n`);
process.exit(1);
}
const graceful = await waitForExit(pid, 10_000);
if (!graceful) {
process.stdout.write("⚠️ Daemon did not exit in 10s — sending SIGKILL.\n");
try {
process.kill(pid, "SIGKILL");
} catch {
// already dead
}
}
removePidFile();
process.stdout.write("✅ Daemon stopped.\n");
}
export const stopCommand = defineCommand({ export const stopCommand = defineCommand({
meta: { meta: {
name: "stop", name: "stop",
description: "Stop the nerve daemon", description: "Stop the nerve daemon",
}, },
async run() { async run() {
const pid = readPidFile(); await runStopCommand();
if (pid === null) {
process.stdout.write("⚠️ No PID file found — daemon may not be running.\n");
return;
}
if (!isRunning()) {
process.stdout.write("⚠️ Daemon is not running (stale PID file). Cleaning up.\n");
removePidFile();
return;
}
process.stdout.write(`Stopping nerve daemon (pid ${pid})…\n`);
try {
process.kill(pid, "SIGTERM");
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
process.stderr.write(`❌ Failed to send SIGTERM: ${msg}\n`);
process.exit(1);
}
const graceful = await waitForExit(pid, 10_000);
if (!graceful) {
process.stdout.write("⚠️ Daemon did not exit in 10s — sending SIGKILL.\n");
try {
process.kill(pid, "SIGKILL");
} catch {
// already dead
}
}
removePidFile();
process.stdout.write("✅ Daemon stopped.\n");
}, },
}); });
+1
View File
@@ -5,6 +5,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"scripts": { "scripts": {
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
"build": "tsup", "build": "tsup",
"test": "vitest run" "test": "vitest run"
}, },
+1
View File
@@ -11,6 +11,7 @@
"access": "public" "access": "public"
}, },
"scripts": { "scripts": {
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
"build": "tsup", "build": "tsup",
"test": "vitest run" "test": "vitest run"
}, },
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# All packages must use pnpm publish. Block npm publish unconditionally.
if [ -z "$npm_execpath" ] || [[ "$npm_execpath" != *pnpm* ]]; then
echo "❌ Use 'pnpm publish' instead of 'npm publish'."
echo " pnpm auto-converts workspace:* dependencies to real versions."
exit 1
fi