Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e0eb4eec6 | |||
| cf2b0ac223 |
+21
-1
@@ -12,6 +12,26 @@ import { storeCommand } from "./commands/store.js";
|
||||
import { validateCommand } from "./commands/validate.js";
|
||||
import { workflowCommand } from "./commands/workflow.js";
|
||||
|
||||
/**
|
||||
* Citty picks the first non-flag token as a subcommand name. Rewrite
|
||||
* `nerve init --from <url>` so the URL is not mistaken for `workflow`/`workspace`.
|
||||
*/
|
||||
function normalizeNerveArgv(argv: string[]): string[] {
|
||||
const initIdx = argv.indexOf("init");
|
||||
if (initIdx === -1) return argv;
|
||||
const tail = argv.slice(initIdx + 1);
|
||||
const fromAt = tail.indexOf("--from");
|
||||
if (fromAt === -1) return argv;
|
||||
const beforeFrom = tail.slice(0, fromAt);
|
||||
if (beforeFrom.some((a) => !a.startsWith("-"))) return argv;
|
||||
const next = tail[fromAt + 1];
|
||||
if (next === undefined || next.startsWith("-")) return argv;
|
||||
const reserved = new Set(["workflow", "workspace"]);
|
||||
if (reserved.has(next)) return argv;
|
||||
const mergedTail = [...tail.slice(0, fromAt), `--from=${next}`, ...tail.slice(fromAt + 2)];
|
||||
return [...argv.slice(0, initIdx + 1), ...mergedTail];
|
||||
}
|
||||
|
||||
const main = defineCommand({
|
||||
meta: {
|
||||
name: "nerve",
|
||||
@@ -32,4 +52,4 @@ const main = defineCommand({
|
||||
},
|
||||
});
|
||||
|
||||
runMain(main);
|
||||
runMain(main, { rawArgs: normalizeNerveArgv(process.argv.slice(2)) });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
@@ -236,6 +236,76 @@ async function verifyNodeSqlite(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
function isNerveRootNonEmpty(nerveRoot: string): boolean {
|
||||
if (!existsSync(nerveRoot)) return false;
|
||||
return readdirSync(nerveRoot).length > 0;
|
||||
}
|
||||
|
||||
async function runInitFromGit(url: string): Promise<void> {
|
||||
const trimmed = url.trim();
|
||||
if (trimmed.length === 0) {
|
||||
process.stderr.write("❌ --from requires a non-empty git URL.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const nerveRoot = getNerveRoot();
|
||||
if (isNerveRootNonEmpty(nerveRoot)) {
|
||||
process.stderr.write(
|
||||
`❌ ${nerveRoot} already exists and is not empty. Remove it (or empty it) before using --from.\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await execFileAsync("git", ["--version"]);
|
||||
} catch {
|
||||
process.stderr.write("❌ git is not available. Install git and retry.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await execFileAsync("pnpm", ["--version"]);
|
||||
} catch {
|
||||
process.stderr.write("❌ pnpm is not available. Install pnpm and retry.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.stdout.write(`Cloning ${trimmed} → ${nerveRoot} …\n`);
|
||||
try {
|
||||
await runCommand("git", ["clone", trimmed, nerveRoot], process.cwd());
|
||||
} catch {
|
||||
process.stderr.write("❌ git clone failed.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(join(nerveRoot, "nerve.yaml"))) {
|
||||
process.stdout.write(`⚠️ ${join(nerveRoot, "nerve.yaml")} not found after clone.\n`);
|
||||
}
|
||||
if (!existsSync(join(nerveRoot, "package.json"))) {
|
||||
process.stdout.write(`⚠️ ${join(nerveRoot, "package.json")} not found after clone.\n`);
|
||||
}
|
||||
|
||||
process.stdout.write("Installing dependencies with pnpm …\n");
|
||||
try {
|
||||
await runCommand("pnpm", ["install", "--no-cache"], nerveRoot);
|
||||
} catch {
|
||||
process.stdout.write(
|
||||
`⚠️ pnpm install failed. Try manually:\n cd ${nerveRoot} && pnpm install --no-cache\n`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!(await verifyNodeSqlite())) {
|
||||
process.stdout.write(
|
||||
"⚠️ Built-in SQLite (node:sqlite) is not available in this Node.js build. " +
|
||||
"The daemon requires Node.js 22.5 or newer with SQLite enabled.\n",
|
||||
);
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`✅ Workspace cloned to ${nerveRoot}\n\n💡 Next steps:\n 1. Review nerve.yaml and install any missing tooling.\n 2. Run \`nerve start\` to launch the daemon.\n`,
|
||||
);
|
||||
}
|
||||
|
||||
async function runInitWorkspace(force: boolean): Promise<void> {
|
||||
const nerveRoot = getNerveRoot();
|
||||
|
||||
@@ -294,7 +364,7 @@ export const initCommand = defineCommand({
|
||||
meta: {
|
||||
name: "init",
|
||||
description:
|
||||
"Initialize workspace (nerve init) or scaffold templates (nerve init workflow <name>)",
|
||||
"Initialize workspace (nerve init), clone from git (nerve init --from <url>), or scaffold templates (nerve init workflow <name>)",
|
||||
},
|
||||
args: {
|
||||
force: {
|
||||
@@ -302,12 +372,21 @@ export const initCommand = defineCommand({
|
||||
description: "Reinitialize even if workspace already exists (preserves data/)",
|
||||
default: false,
|
||||
},
|
||||
from: {
|
||||
type: "string",
|
||||
description: "Clone an existing git repo into ~/.uncaged-nerve instead of scaffolding",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
subCommands: {
|
||||
workflow: initWorkflowCommand,
|
||||
workspace: initWorkspaceCommand,
|
||||
},
|
||||
async run({ args }) {
|
||||
if (args.from !== undefined) {
|
||||
await runInitFromGit(String(args.from));
|
||||
return;
|
||||
}
|
||||
await runInitWorkspace(args.force);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user