feat: implement Phase 5 CLI & User Workspace

- Restructure CLI with citty multi-command framework
- nerve init: create workspace skeleton at ~/.uncaged-nerve/
- nerve start: foreground + daemon (-d) modes with graceful shutdown
- nerve stop: SIGTERM → 10s wait → SIGKILL, PID file cleanup
- nerve status: show pid, uptime, senses, workers
- nerve validate: parse nerve.yaml with error reporting
- workspace.ts: shared utilities (PID file, paths, isRunning)
- Example cpu-usage sense with realistic os.cpus() compute

小橘 🍊(NEKO Team)
This commit is contained in:
2026-04-22 10:16:41 +00:00
parent 31d1eae44a
commit ad2b40dd4f
14 changed files with 574 additions and 110 deletions
+5 -1
View File
@@ -13,6 +13,10 @@
},
"dependencies": {
"@uncaged/nerve-core": "workspace:*",
"@uncaged/nerve-daemon": "workspace:*"
"@uncaged/nerve-daemon": "workspace:*",
"citty": "^0.1.6"
},
"devDependencies": {
"@types/node": "^22.0.0"
}
}
+20 -99
View File
@@ -1,104 +1,25 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { defineCommand, runMain } from "citty";
import { parseNerveConfig } from "@uncaged/nerve-core";
import { createKernel } from "@uncaged/nerve-daemon";
import { initCommand } from "./commands/init.js";
import { startCommand } from "./commands/start.js";
import { statusCommand } from "./commands/status.js";
import { stopCommand } from "./commands/stop.js";
import { validateCommand } from "./commands/validate.js";
const DEFAULT_NERVE_ROOT = join(homedir(), ".uncaged-nerve");
function parseArgs(argv: string[]): { root: string } {
// Skip argv[0] (node), argv[1] (script), argv[2] ('start' subcommand)
const args = argv.slice(3);
let root = DEFAULT_NERVE_ROOT;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--root" && i + 1 < args.length) {
root = args[i + 1];
i++;
} else if (!args[i].startsWith("-")) {
process.stderr.write("Usage: nerve start [--root <path>]\n");
process.exit(1);
}
}
return { root };
}
function readConfig(nerveRoot: string): ReturnType<typeof parseNerveConfig> {
const configPath = join(nerveRoot, "nerve.yaml");
let raw: string;
try {
raw = readFileSync(configPath, "utf8");
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
return { ok: false, error: new Error(`Cannot read ${configPath}: ${msg}`) };
}
return parseNerveConfig(raw);
}
async function main(): Promise<void> {
const subcommand = process.argv[2];
if (subcommand !== "start") {
process.stderr.write("Usage: nerve start [--root <path>]\n");
process.exit(1);
}
const { root } = parseArgs(process.argv);
const configResult = readConfig(root);
if (!configResult.ok) {
process.stderr.write(`[nerve] Config error: ${configResult.error.message}\n`);
process.exit(1);
}
const config = configResult.value;
const kernel = createKernel(config, root);
process.stderr.write(
`[nerve] Starting — ${kernel.groups.size} group(s), ${kernel.senseCount} sense(s)\n`,
);
for (const group of kernel.groups) {
const groupSenses = Object.entries(config.senses)
.filter(([, sc]) => sc.group === group)
.map(([name]) => name);
process.stderr.write(`[nerve] group "${group}": ${groupSenses.join(", ")}\n`);
}
let shuttingDown = false;
async function shutdown(): Promise<void> {
if (shuttingDown) return;
shuttingDown = true;
process.stderr.write("\n[nerve] Shutting down…\n");
await kernel.stop();
process.exit(0);
}
process.on("SIGINT", () => {
shutdown().catch((e: unknown) => {
const msg = e instanceof Error ? e.message : String(e);
process.stderr.write(`[nerve] Shutdown error: ${msg}\n`);
process.exit(1);
});
});
process.on("SIGTERM", () => {
shutdown().catch((e: unknown) => {
const msg = e instanceof Error ? e.message : String(e);
process.stderr.write(`[nerve] Shutdown error: ${msg}\n`);
process.exit(1);
});
});
}
main().catch((e: unknown) => {
const msg = e instanceof Error ? e.message : String(e);
process.stderr.write(`[nerve] Fatal error: ${msg}\n`);
process.exit(1);
const main = defineCommand({
meta: {
name: "nerve",
description: "Nerve — an AI agent kernel",
},
subCommands: {
init: initCommand,
start: startCommand,
stop: stopCommand,
status: statusCommand,
validate: validateCommand,
},
});
runMain(main);
+153
View File
@@ -0,0 +1,153 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { defineCommand } from "citty";
import { getNerveRoot } from "../workspace.js";
const NERVE_YAML = `# nerve.yaml — Nerve workspace configuration
senses:
cpu-usage:
group: system
throttle: 5s
timeout: 10s
grace_period: null
reflexes:
- kind: sense
sense: cpu-usage
interval: 10s
`;
const PACKAGE_JSON = `{
"name": "my-nerve-workspace",
"version": "0.0.1",
"private": true,
"type": "module",
"dependencies": {
"@uncaged/nerve-core": "latest",
"drizzle-orm": "latest"
},
"devDependencies": {
"drizzle-kit": "latest"
}
}
`;
const GITIGNORE = `data/
node_modules/
`;
const CPU_SCHEMA_TS = `import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const cpuUsage = sqliteTable("cpu_usage", {
id: integer("id").primaryKey({ autoIncrement: true }),
ts: integer("ts").notNull(),
model: text("model").notNull(),
loadPercent: real("load_percent").notNull(),
});
`;
const CPU_INDEX_TS = `import { cpus } from "node:os";
export async function compute(): Promise<unknown> {
const cpuList = cpus();
let totalIdle = 0;
let totalTick = 0;
for (const cpu of cpuList) {
for (const [, time] of Object.entries(cpu.times)) {
totalTick += time;
}
totalIdle += cpu.times.idle;
}
const loadPercent = totalTick === 0 ? 0 : ((totalTick - totalIdle) / totalTick) * 100;
return {
model: cpuList[0]?.model ?? "unknown",
loadPercent: Math.round(loadPercent * 100) / 100,
ts: Date.now(),
};
}
`;
const CPU_MIGRATION_SQL = `CREATE TABLE IF NOT EXISTS cpu_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
model TEXT NOT NULL,
load_percent REAL NOT NULL
);
`;
function writeFile(filePath: string, content: string): void {
const dir = join(filePath, "..");
mkdirSync(dir, { recursive: true });
writeFileSync(filePath, content, "utf8");
}
async function runCommand(cmd: string, args: string[], cwd: string): Promise<void> {
const { spawn } = await import("node:child_process");
await new Promise<void>((resolve, reject) => {
const child = spawn(cmd, args, { cwd, stdio: "inherit" });
child.on("close", (code) => {
if (code === 0) resolve();
else reject(new Error(`${cmd} exited with code ${code}`));
});
child.on("error", reject);
});
}
export const initCommand = defineCommand({
meta: {
name: "init",
description: "Initialize the ~/.uncaged-nerve/ workspace",
},
args: {
force: {
type: "boolean",
description: "Reinitialize even if workspace already exists (preserves data/)",
default: false,
},
},
async run({ args }) {
const nerveRoot = getNerveRoot();
if (existsSync(nerveRoot) && !args.force) {
process.stderr.write("⚠️ ~/.uncaged-nerve/ already exists. Use --force to reinitialize.\n");
process.exit(1);
}
mkdirSync(join(nerveRoot, "data"), { recursive: true });
mkdirSync(join(nerveRoot, "senses", "cpu-usage", "migrations"), { recursive: true });
writeFile(join(nerveRoot, "nerve.yaml"), NERVE_YAML);
writeFile(join(nerveRoot, "package.json"), PACKAGE_JSON);
writeFile(join(nerveRoot, ".gitignore"), GITIGNORE);
writeFile(join(nerveRoot, "senses", "cpu-usage", "schema.ts"), CPU_SCHEMA_TS);
writeFile(join(nerveRoot, "senses", "cpu-usage", "index.ts"), CPU_INDEX_TS);
writeFile(
join(nerveRoot, "senses", "cpu-usage", "migrations", "0001_init.sql"),
CPU_MIGRATION_SQL,
);
process.stdout.write("Installing dependencies…\n");
try {
await runCommand("pnpm", ["install", "--no-cache"], nerveRoot);
} catch {
process.stdout.write("⚠️ pnpm install failed — you may need to install manually.\n");
}
if (!existsSync(join(nerveRoot, ".git"))) {
try {
await runCommand("git", ["init"], nerveRoot);
} catch {
process.stdout.write("⚠️ git init failed — skipping.\n");
}
}
process.stdout.write(
"✅ Workspace created at ~/.uncaged-nerve/\n 1 example sense: cpu-usage\n Run `nerve start` to launch the daemon.\n",
);
},
});
+138
View File
@@ -0,0 +1,138 @@
import { createWriteStream } from "node:fs";
import { readFileSync } from "node:fs";
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { parseNerveConfig } from "@uncaged/nerve-core";
import { createKernel } from "@uncaged/nerve-daemon";
import { defineCommand } from "citty";
import { getLogPath, getNerveRoot, isRunning, readPidFile, writePidFile } from "../workspace.js";
function readConfig(nerveRoot: string): ReturnType<typeof parseNerveConfig> {
const configPath = join(nerveRoot, "nerve.yaml");
let raw: string;
try {
raw = readFileSync(configPath, "utf8");
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
return { ok: false, error: new Error(`❌ Cannot read ${configPath}: ${msg}`) };
}
return parseNerveConfig(raw);
}
async function runForeground(nerveRoot: string): Promise<void> {
const configResult = readConfig(nerveRoot);
if (!configResult.ok) {
process.stderr.write(`${configResult.error.message}\n`);
process.exit(1);
}
const config = configResult.value;
const kernel = createKernel(config, nerveRoot);
const senseNames = Object.keys(config.senses);
const groups = [...kernel.groups];
process.stdout.write(
`✅ Nerve starting — ${senseNames.length} sense(s), ${groups.length} group(s)\n`,
);
for (const group of groups) {
const groupSenses = Object.entries(config.senses)
.filter(([, sc]) => sc.group === group)
.map(([name]) => name);
process.stdout.write(` group "${group}": ${groupSenses.join(", ")}\n`);
}
process.stdout.write(" Press Ctrl+C to stop.\n");
let shuttingDown = false;
async function shutdown(): Promise<void> {
if (shuttingDown) return;
shuttingDown = true;
process.stdout.write("\n[nerve] Shutting down…\n");
await kernel.stop();
process.exit(0);
}
process.on("SIGINT", () => {
shutdown().catch((e: unknown) => {
const msg = e instanceof Error ? e.message : String(e);
process.stderr.write(`[nerve] Shutdown error: ${msg}\n`);
process.exit(1);
});
});
process.on("SIGTERM", () => {
shutdown().catch((e: unknown) => {
const msg = e instanceof Error ? e.message : String(e);
process.stderr.write(`[nerve] Shutdown error: ${msg}\n`);
process.exit(1);
});
});
await kernel.ready;
}
async function runDaemon(nerveRoot: string): Promise<void> {
if (isRunning()) {
const pid = readPidFile();
process.stderr.write(`⚠️ Nerve daemon is already running (pid ${pid}).\n`);
process.exit(1);
}
const configResult = readConfig(nerveRoot);
if (!configResult.ok) {
process.stderr.write(`${configResult.error.message}\n`);
process.exit(1);
}
const logPath = getLogPath();
await mkdir(join(nerveRoot, "logs"), { recursive: true });
const { spawn } = await import("node:child_process");
const logStream = createWriteStream(logPath, { flags: "a" });
const child = spawn(process.execPath, [process.argv[1], "start"], {
detached: true,
stdio: ["ignore", logStream as unknown as "pipe", logStream as unknown as "pipe"],
env: { ...process.env, NERVE_DAEMON_MODE: "1" },
});
child.unref();
const pid = child.pid;
if (!pid) {
process.stderr.write("❌ Failed to spawn daemon process.\n");
process.exit(1);
}
writePidFile(pid);
process.stdout.write(`✅ Nerve daemon started (pid ${pid}).\n`);
process.stdout.write(` Logs: ${logPath}\n`);
process.stdout.write(" Run `nerve stop` to stop.\n");
}
export const startCommand = defineCommand({
meta: {
name: "start",
description: "Start the nerve daemon",
},
args: {
daemon: {
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);
}
},
});
+65
View File
@@ -0,0 +1,65 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { parseNerveConfig } from "@uncaged/nerve-core";
import { defineCommand } from "citty";
import { getNerveRoot, isRunning, readPidFile } from "../workspace.js";
function formatUptime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
if (minutes > 0) return `${minutes}m ${seconds}s`;
return `${seconds}s`;
}
export const statusCommand = defineCommand({
meta: {
name: "status",
description: "Show nerve daemon status",
},
async run() {
if (!isRunning()) {
process.stdout.write("😴 Nerve daemon is not running.\n");
return;
}
const pid = readPidFile() as number;
const configPath = join(getNerveRoot(), "nerve.yaml");
let senseList: string[] = [];
let workerGroups: string[] = [];
try {
const raw = readFileSync(configPath, "utf8");
const result = parseNerveConfig(raw);
if (result.ok) {
senseList = Object.keys(result.value.senses);
workerGroups = [...new Set(Object.values(result.value.senses).map((s) => s.group))];
}
} catch {
// config may not be readable; continue with what we have
}
const pidStat = readFileSync(`/proc/${pid}/stat`, "utf8").split(" ");
const startJiffies = Number(pidStat[21]);
const clkTck = 100;
const uptimeRaw = readFileSync("/proc/uptime", "utf8").split(" ")[0];
const systemUptimeSec = Number.parseFloat(uptimeRaw);
const procesStartSec = startJiffies / clkTck;
const uptimeSec = systemUptimeSec - procesStartSec;
const uptimeMs = uptimeSec * 1000;
process.stdout.write("✅ Nerve daemon is running.\n");
process.stdout.write(` pid: ${pid}\n`);
process.stdout.write(` uptime: ${formatUptime(uptimeMs)}\n`);
process.stdout.write(` senses: ${senseList.length > 0 ? senseList.join(", ") : "(none)"}\n`);
process.stdout.write(
` workers: ${workerGroups.length > 0 ? workerGroups.join(", ") : "(none)"}\n`,
);
process.stdout.write(" signals: (pending SignalBus persistence)\n");
},
});
+58
View File
@@ -0,0 +1,58 @@
import { defineCommand } from "citty";
import { isRunning, readPidFile, removePidFile } from "../workspace.js";
async function waitForExit(pid: number, timeoutMs: number): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
process.kill(pid, 0);
} catch {
return true;
}
await new Promise<void>((resolve) => setTimeout(resolve, 200));
}
return false;
}
export const stopCommand = defineCommand({
meta: {
name: "stop",
description: "Stop the nerve daemon",
},
async run() {
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");
},
});
+41
View File
@@ -0,0 +1,41 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { parseNerveConfig } from "@uncaged/nerve-core";
import { defineCommand } from "citty";
import { getNerveRoot } from "../workspace.js";
export const validateCommand = defineCommand({
meta: {
name: "validate",
description: "Validate nerve.yaml configuration",
},
async run() {
const configPath = join(getNerveRoot(), "nerve.yaml");
let raw: string;
try {
raw = readFileSync(configPath, "utf8");
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
process.stderr.write(`❌ Cannot read ${configPath}: ${msg}\n`);
process.exit(1);
}
const result = parseNerveConfig(raw);
if (!result.ok) {
process.stderr.write("❌ Config validation failed:\n");
process.stderr.write(` 1. ${result.error.message}\n`);
process.exit(1);
}
const config = result.value;
const senseCount = Object.keys(config.senses).length;
const reflexCount = config.reflexes.length;
const workflowCount = config.workflows ? Object.keys(config.workflows).length : 0;
process.stdout.write(
`✅ nerve.yaml is valid — ${senseCount} sense(s), ${reflexCount} reflex(es), ${workflowCount} workflow(s)\n`,
);
},
});
+9 -2
View File
@@ -1,2 +1,9 @@
export { createKernel } from "@uncaged/nerve-daemon";
export type { Kernel } from "@uncaged/nerve-daemon";
export {
getNerveRoot,
getPidPath,
getLogPath,
readPidFile,
writePidFile,
removePidFile,
isRunning,
} from "./workspace.js";
+45
View File
@@ -0,0 +1,45 @@
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
export function getNerveRoot(): string {
return join(homedir(), ".uncaged-nerve");
}
export function getPidPath(): string {
return join(getNerveRoot(), "nerve.pid");
}
export function getLogPath(): string {
return join(getNerveRoot(), "logs", "nerve.log");
}
export function readPidFile(): number | null {
const pidPath = getPidPath();
if (!existsSync(pidPath)) return null;
const raw = readFileSync(pidPath, "utf8").trim();
const pid = Number.parseInt(raw, 10);
return Number.isNaN(pid) ? null : pid;
}
export function writePidFile(pid: number): void {
writeFileSync(getPidPath(), String(pid), "utf8");
}
export function removePidFile(): void {
const pidPath = getPidPath();
if (existsSync(pidPath)) {
rmSync(pidPath);
}
}
export function isRunning(): boolean {
const pid = readPidFile();
if (pid === null) return false;
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
+3 -1
View File
@@ -2,7 +2,9 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
"rootDir": "src",
"composite": false,
"types": ["node"]
},
"include": ["src"]
}
+2 -1
View File
@@ -2,7 +2,8 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
"rootDir": "src",
"composite": false
},
"include": ["src"]
}
+3 -3
View File
@@ -115,7 +115,7 @@ export function createKernel(
// eslint-disable-next-line prefer-const
let scheduler: ReflexScheduler = null as unknown as ReflexScheduler;
let readyResolve: () => void;
let readyResolve: (() => void) | undefined;
const ready = new Promise<void>((resolve) => {
readyResolve = resolve;
});
@@ -138,7 +138,7 @@ export function createKernel(
if (msg.type === "ready") {
pendingReadyCount -= 1;
if (pendingReadyCount <= 0) {
readyResolve();
readyResolve?.();
}
return;
}
@@ -205,7 +205,7 @@ export function createKernel(
scheduler = createReflexScheduler(config, bus, triggerFn);
if (groups.size === 0) {
readyResolve();
readyResolve?.();
}
for (const group of groups) {
+2 -1
View File
@@ -2,7 +2,8 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
"rootDir": "src",
"composite": false
},
"include": ["src"]
}
+30 -2
View File
@@ -26,6 +26,13 @@ importers:
'@uncaged/nerve-daemon':
specifier: workspace:*
version: link:../daemon
citty:
specifier: ^0.1.6
version: 0.1.6
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.19.17
packages/core:
dependencies:
@@ -559,6 +566,9 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/node@22.19.17':
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
'@types/node@25.6.0':
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
@@ -639,6 +649,9 @@ packages:
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@@ -1142,6 +1155,9 @@ packages:
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici-types@7.19.2:
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
@@ -1534,7 +1550,7 @@ snapshots:
'@types/better-sqlite3@7.6.13':
dependencies:
'@types/node': 25.6.0
'@types/node': 22.19.17
'@types/chai@5.2.3':
dependencies:
@@ -1545,9 +1561,14 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/node@22.19.17':
dependencies:
undici-types: 6.21.0
'@types/node@25.6.0':
dependencies:
undici-types: 7.19.2
optional: true
'@vitest/expect@4.1.5':
dependencies:
@@ -1633,6 +1654,10 @@ snapshots:
chownr@1.1.4: {}
citty@0.1.6:
dependencies:
consola: 3.4.2
commander@4.1.1: {}
confbox@0.1.8: {}
@@ -2057,7 +2082,10 @@ snapshots:
ufo@1.6.3: {}
undici-types@7.19.2: {}
undici-types@6.21.0: {}
undici-types@7.19.2:
optional: true
util-deprecate@1.0.2: {}