diff --git a/packages/cli/package.json b/packages/cli/package.json index 7269994..5790655 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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" } } diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 6457455..31fb1fd 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -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 ]\n"); - process.exit(1); - } - } - - return { root }; -} - -function readConfig(nerveRoot: string): ReturnType { - 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 { - const subcommand = process.argv[2]; - - if (subcommand !== "start") { - process.stderr.write("Usage: nerve start [--root ]\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 { - 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); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts new file mode 100644 index 0000000..ebca351 --- /dev/null +++ b/packages/cli/src/commands/init.ts @@ -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 { + 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 { + const { spawn } = await import("node:child_process"); + await new Promise((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", + ); + }, +}); diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts new file mode 100644 index 0000000..522b885 --- /dev/null +++ b/packages/cli/src/commands/start.ts @@ -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 { + 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 { + 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 { + 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 { + 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); + } + }, +}); diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts new file mode 100644 index 0000000..8a2a513 --- /dev/null +++ b/packages/cli/src/commands/status.ts @@ -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"); + }, +}); diff --git a/packages/cli/src/commands/stop.ts b/packages/cli/src/commands/stop.ts new file mode 100644 index 0000000..2e9979f --- /dev/null +++ b/packages/cli/src/commands/stop.ts @@ -0,0 +1,58 @@ +import { defineCommand } from "citty"; + +import { isRunning, readPidFile, removePidFile } from "../workspace.js"; + +async function waitForExit(pid: number, timeoutMs: number): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + process.kill(pid, 0); + } catch { + return true; + } + await new Promise((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"); + }, +}); diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts new file mode 100644 index 0000000..cc53f84 --- /dev/null +++ b/packages/cli/src/commands/validate.ts @@ -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`, + ); + }, +}); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d41c4b7..f5dc52f 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -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"; diff --git a/packages/cli/src/workspace.ts b/packages/cli/src/workspace.ts new file mode 100644 index 0000000..c8c7a2a --- /dev/null +++ b/packages/cli/src/workspace.ts @@ -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; + } +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index a086b14..c449f63 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "composite": false, + "types": ["node"] }, "include": ["src"] } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index a086b14..9036088 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "composite": false }, "include": ["src"] } diff --git a/packages/daemon/src/kernel.ts b/packages/daemon/src/kernel.ts index e1f0356..4fbbdde 100644 --- a/packages/daemon/src/kernel.ts +++ b/packages/daemon/src/kernel.ts @@ -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((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) { diff --git a/packages/daemon/tsconfig.json b/packages/daemon/tsconfig.json index a086b14..9036088 100644 --- a/packages/daemon/tsconfig.json +++ b/packages/daemon/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "composite": false }, "include": ["src"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10d830f..eb21479 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {}