@0dai-dev/cli 4.2.0 → 4.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/bin/0dai.js +289 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +341 -98
- package/lib/commands/boneyard.js +44 -0
- package/lib/commands/ci.js +329 -0
- package/lib/commands/compliance.js +20 -0
- package/lib/commands/doctor.js +20 -1
- package/lib/commands/experience.js +5 -1
- package/lib/commands/feedback.js +92 -5
- package/lib/commands/gh.js +506 -0
- package/lib/commands/graph.js +78 -10
- package/lib/commands/heatmap.js +17 -0
- package/lib/commands/import_claude_code_agents.js +367 -0
- package/lib/commands/init.js +440 -28
- package/lib/commands/loop.js +108 -0
- package/lib/commands/mcp.js +410 -0
- package/lib/commands/models.js +27 -3
- package/lib/commands/paste.js +114 -0
- package/lib/commands/play.js +173 -0
- package/lib/commands/provider.js +69 -0
- package/lib/commands/quota.js +76 -0
- package/lib/commands/receipt.js +53 -0
- package/lib/commands/report.js +29 -2
- package/lib/commands/run.js +44 -4
- package/lib/commands/runner.js +527 -0
- package/lib/commands/session.js +1 -7
- package/lib/commands/standup.js +40 -0
- package/lib/commands/status.js +26 -1
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +81 -13
- package/lib/commands/usage.js +87 -0
- package/lib/commands/vault.js +246 -0
- package/lib/onboarding.js +9 -3
- package/lib/shared.js +29 -14
- package/lib/tui/index.mjs +571 -187
- package/lib/utils/auth.js +1 -0
- package/lib/utils/canonical-counts.js +54 -0
- package/lib/utils/diff-preview.js +192 -0
- package/lib/utils/identity.js +76 -18
- package/lib/utils/mcp-auth.js +607 -0
- package/lib/utils/plan.js +37 -2
- package/lib/vault/cipher.js +125 -0
- package/lib/vault/identity.js +122 -0
- package/lib/vault/index.js +184 -0
- package/lib/vault/storage.js +84 -0
- package/lib/wizard.js +19 -12
- package/package.json +2 -2
package/lib/commands/swarm.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const shared = require("../shared");
|
|
3
|
-
const { log, T, R, D, fs, path, https, requirePlan } = shared;
|
|
3
|
+
const { log, T, R, D, fs, path, https, requirePlan, spawnSync, findRepoScript } = shared;
|
|
4
4
|
|
|
5
5
|
function cmdSwarm(target, sub, args) {
|
|
6
6
|
const swarmDir = path.join(target, "ai", "swarm");
|
|
7
7
|
const queueDir = path.join(swarmDir, "queue");
|
|
8
8
|
|
|
9
|
+
if (sub === "swarm-run") {
|
|
10
|
+
cmdSwarmRun(target, args.slice(2));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
9
13
|
if (sub === "status") {
|
|
10
14
|
const count = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
|
|
11
15
|
const q = count(path.join(swarmDir, "queue"));
|
|
@@ -192,8 +196,97 @@ function cmdSwarm(target, sub, args) {
|
|
|
192
196
|
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
193
197
|
return;
|
|
194
198
|
}
|
|
195
|
-
|
|
196
|
-
|
|
199
|
+
if (sub === "sessions") {
|
|
200
|
+
const script = findRepoScript(target, "swarm_session_registry.py");
|
|
201
|
+
if (!script) { log("swarm session registry unavailable"); return; }
|
|
202
|
+
const fwd = [script, "rebuild", "--stale-after", "600"];
|
|
203
|
+
const asJson = args.includes("--json");
|
|
204
|
+
if (!asJson) fwd.push("--write", path.join(swarmDir, "session_registry.json"));
|
|
205
|
+
const r = spawnSync("python3", fwd, { encoding: "utf8", timeout: 15000 });
|
|
206
|
+
if (r.error) { log(`error: ${r.error.message}`); return; }
|
|
207
|
+
if (r.status !== 0 && r.status !== null) { process.exit(r.status); }
|
|
208
|
+
if (asJson) {
|
|
209
|
+
console.log(r.stdout);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const snapFile = path.join(swarmDir, "session_registry.json");
|
|
213
|
+
if (!fs.existsSync(snapFile)) {
|
|
214
|
+
log("no session data yet — run a few tasks first");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let snap;
|
|
218
|
+
try { snap = JSON.parse(fs.readFileSync(snapFile, "utf8")); } catch { log("could not read registry snapshot"); return; }
|
|
219
|
+
const sessions = snap.sessions || {};
|
|
220
|
+
if (Object.keys(sessions).length === 0) {
|
|
221
|
+
console.log("\n No sessions in registry.\n");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const B = process.stdout.isTTY ? "\x1b[1m" : "";
|
|
225
|
+
const R_ = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
226
|
+
const W2 = process.stdout.isTTY ? "\x1b[33m" : "";
|
|
227
|
+
const G2 = process.stdout.isTTY ? "\x1b[32m" : "";
|
|
228
|
+
const M2 = process.stdout.isTTY ? "\x1b[35m" : "";
|
|
229
|
+
const dim = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
230
|
+
const reset = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
231
|
+
console.log(`\n ${B}Swarm Sessions${R_} (updated ${snap.updated_at || "?"})`);
|
|
232
|
+
console.log(` ${dim}${"─".repeat(80)}${reset}`);
|
|
233
|
+
const rows = Object.entries(sessions).sort(([a], [b]) => a.localeCompare(b));
|
|
234
|
+
for (const [name, s] of rows) {
|
|
235
|
+
const status = s.current_status || "???";
|
|
236
|
+
const statusCol = status === "idle" ? G2 : status === "busy" ? W2 : status === "stale" ? M2 : "";
|
|
237
|
+
const task = s.current_task || "-";
|
|
238
|
+
const branch = s.current_branch || "-";
|
|
239
|
+
const claimed = s.claimed_at
|
|
240
|
+
? new Date(s.claimed_at).toISOString().replace("T", " ").slice(0, 16)
|
|
241
|
+
: "-";
|
|
242
|
+
console.log(
|
|
243
|
+
` ${(statusCol + status + reset).padEnd(8)} ${(name || "").padEnd(22)} ${(s.agent_family || "").padEnd(8)} ${(task || "").padEnd(20)} ${(branch || "").padEnd(25)} ${claimed}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
console.log(` ${dim}${"─".repeat(80)}${reset}\n`);
|
|
247
|
+
const idle = rows.filter(([, s]) => s.current_status === "idle").length;
|
|
248
|
+
const busy = rows.filter(([, s]) => s.current_status === "busy").length;
|
|
249
|
+
const stale = rows.filter(([, s]) => s.current_status === "stale").length;
|
|
250
|
+
console.log(` ${G2}idle${reset}: ${idle} ${W2}busy${reset}: ${busy} ${M2}stale${reset}: ${stale} total: ${rows.length}\n`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
console.log("Usage: 0dai swarm [status|add|delegate|budget|estimate|quality|sessions|swarm-run] [--task '...'] [--to agent]");
|
|
254
|
+
console.log(" sessions Show live session registry table (idle/busy/stale per session) [--json]");
|
|
255
|
+
console.log(" swarm-run Add, dispatch, and wait for one swarm task as JSON");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function cmdSwarmRun(target, args) {
|
|
259
|
+
// #2143 hardening:
|
|
260
|
+
// 1. `ODAI_SWARM_RUN_SCRIPT` env override lets tests inject a deterministic
|
|
261
|
+
// script path, bypassing the `findRepoScript` heuristic that walks up
|
|
262
|
+
// multiple parent dirs and can drift on CI runners with stale layouts.
|
|
263
|
+
// 2. `ODAI_SWARM_RUN_PYTHON` env override picks the interpreter (defaults to
|
|
264
|
+
// `python3`). Lets a runner with `python3.12` aliased differently still
|
|
265
|
+
// invoke the helper without modifying the test.
|
|
266
|
+
// 3. When the helper cannot be located, log every candidate that was probed
|
|
267
|
+
// so CI failures point at the right runner-layout issue instead of a
|
|
268
|
+
// generic "unavailable" line.
|
|
269
|
+
let script = process.env.ODAI_SWARM_RUN_SCRIPT || "";
|
|
270
|
+
if (script && !fs.existsSync(script)) {
|
|
271
|
+
log(`swarm-run helper unavailable: ODAI_SWARM_RUN_SCRIPT=${script} does not exist`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
if (!script) script = findRepoScript(target, "swarm_run.py");
|
|
275
|
+
if (!script) {
|
|
276
|
+
const cwd = process.cwd();
|
|
277
|
+
log("swarm-run helper unavailable");
|
|
278
|
+
log(` probed: ${path.join(target, "scripts", "swarm_run.py")}`);
|
|
279
|
+
log(` probed: ${path.join(cwd, "scripts", "swarm_run.py")}`);
|
|
280
|
+
log(` hint: set ODAI_SWARM_RUN_SCRIPT to override the lookup`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
const python = process.env.ODAI_SWARM_RUN_PYTHON || "python3";
|
|
284
|
+
const result = spawnSync(python, [script, "--target", target, ...args], { stdio: "inherit" });
|
|
285
|
+
if (result.error) {
|
|
286
|
+
log(`swarm-run failed: ${result.error.message}`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
197
290
|
}
|
|
198
291
|
|
|
199
|
-
module.exports = { cmdSwarm };
|
|
292
|
+
module.exports = { cmdSwarm, cmdSwarmRun };
|
package/lib/commands/tui.js
CHANGED
|
@@ -1,27 +1,95 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
3
4
|
const shared = require("../shared");
|
|
4
|
-
const { log, D, R, VERSION } = shared;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
const { log, D, R, VERSION, spawnSync } = shared;
|
|
6
|
+
|
|
7
|
+
function findRepoScript(target, name) {
|
|
8
|
+
const candidates = [
|
|
9
|
+
path.join(target, "scripts", name),
|
|
10
|
+
path.join(__dirname, "../../../../scripts", name),
|
|
11
|
+
path.join("/root/0dai/scripts", name),
|
|
12
|
+
];
|
|
13
|
+
for (const c of candidates) {
|
|
14
|
+
try { if (fs.existsSync(c)) return c; } catch {}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function argAfter(args, flag) {
|
|
20
|
+
const i = args.indexOf(flag);
|
|
21
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cmdTuiLayout(target, args = []) {
|
|
25
|
+
const layout = argAfter(args, "--layout");
|
|
26
|
+
if (layout !== "launchpad") {
|
|
27
|
+
console.error(`Unknown --layout: ${layout || "(missing)"}. Available: launchpad`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
const script = findRepoScript(target, "tui.py");
|
|
31
|
+
if (!script) {
|
|
32
|
+
console.error("tui.py not found locally. Is the 0dai project set up?");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const fwd = [script, "--target", target, "--layout", "launchpad"];
|
|
36
|
+
const exportPng = argAfter(args, "--export-png");
|
|
37
|
+
if (exportPng) fwd.push("--export-png", exportPng);
|
|
38
|
+
if (args.includes("--no-color") || !process.stdout.isTTY) fwd.push("--no-color");
|
|
39
|
+
const res = spawnSync("python3", fwd, { stdio: "inherit" });
|
|
40
|
+
process.exit(typeof res.status === "number" ? res.status : 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
13
43
|
async function cmdTui(target, args = []) {
|
|
44
|
+
// Short-circuit for `--layout launchpad` — one-shot render, no TTY needed.
|
|
45
|
+
if (args.includes("--layout")) {
|
|
46
|
+
return cmdTuiLayout(target, args);
|
|
47
|
+
}
|
|
48
|
+
|
|
14
49
|
if (!process.stdout.isTTY) {
|
|
15
50
|
console.error("0dai tui requires an interactive TTY. Try: 0dai status");
|
|
16
51
|
process.exit(2);
|
|
17
52
|
}
|
|
18
53
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
54
|
+
// SPEC-026 Phase 3: --remote <url> + --project <id> flip hooks from
|
|
55
|
+
// local fs to HTTP. Env vars ODAI_TUI_REMOTE_URL + ODAI_TUI_PROJECT
|
|
56
|
+
// act as fallbacks so PC / mobile launchers can set them once.
|
|
57
|
+
const remoteUrl =
|
|
58
|
+
argAfter(args, "--remote") || process.env.ODAI_TUI_REMOTE_URL || undefined;
|
|
59
|
+
const projectId =
|
|
60
|
+
argAfter(args, "--project") || process.env.ODAI_TUI_PROJECT || undefined;
|
|
61
|
+
if (remoteUrl && !projectId) {
|
|
62
|
+
console.error(
|
|
63
|
+
"0dai tui --remote requires --project <id> (or ODAI_TUI_PROJECT env)",
|
|
64
|
+
);
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
if (remoteUrl) {
|
|
68
|
+
log(`${D}TUI remote-mode: ${remoteUrl} (project=${projectId})${R}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const writeFlag = args.includes("--write");
|
|
72
|
+
const writeEnv = process.env.ODAI_TUI_WRITE === "1";
|
|
73
|
+
const writeMode = writeFlag || writeEnv;
|
|
74
|
+
if (writeFlag && !writeEnv) {
|
|
75
|
+
log(`${D}TUI write-mode requested via --write. Set ODAI_TUI_WRITE=1 to persist this preference.${R}`);
|
|
76
|
+
}
|
|
77
|
+
if (writeMode) {
|
|
78
|
+
log(`${D}TUI write-mode ENABLED — destructive actions will require diff preview + confirmation.${R}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const standupScript = findRepoScript(target, "standup.py");
|
|
82
|
+
if (standupScript) {
|
|
83
|
+
spawnSync(
|
|
84
|
+
"python3",
|
|
85
|
+
[standupScript, "--target", target, "--if-stale", "--launch-hook"],
|
|
86
|
+
{ stdio: "inherit" },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
21
90
|
const bundlePath = path.join(__dirname, "..", "tui", "index.mjs");
|
|
22
91
|
let tui;
|
|
23
92
|
try {
|
|
24
|
-
// Use the file:// URL form so Windows paths with drive letters load too.
|
|
25
93
|
const url = require("url").pathToFileURL(bundlePath).href;
|
|
26
94
|
tui = await import(url);
|
|
27
95
|
} catch (err) {
|
|
@@ -43,7 +111,7 @@ async function cmdTui(target, args = []) {
|
|
|
43
111
|
// keep default plan
|
|
44
112
|
}
|
|
45
113
|
|
|
46
|
-
await tui.run({ target, version: VERSION, plan });
|
|
114
|
+
await tui.run({ target, version: VERSION, plan, writeMode, remoteUrl, projectId });
|
|
47
115
|
}
|
|
48
116
|
|
|
49
117
|
module.exports = { cmdTui };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const shared = require("../shared");
|
|
4
|
+
const { log, D, R, spawnSync, findRepoScript } = shared;
|
|
5
|
+
|
|
6
|
+
const SUBCOMMANDS = new Set(["status", "daily", "monthly"]);
|
|
7
|
+
|
|
8
|
+
function printUsageHelp() {
|
|
9
|
+
console.log("Usage:");
|
|
10
|
+
console.log(" 0dai usage [status] [--plan free|pro|team|enterprise] [--json] [--target PATH]");
|
|
11
|
+
console.log(" 0dai usage daily [--date YYYY-MM-DD] [--json] [--target PATH]");
|
|
12
|
+
console.log(" 0dai usage monthly [--month YYYY-MM] [--json] [--target PATH]");
|
|
13
|
+
console.log("");
|
|
14
|
+
console.log("Shows token, task, and USD usage from the local usage ledger.");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _forwardArgs(subcommand, args) {
|
|
18
|
+
const forwarded = [subcommand];
|
|
19
|
+
const allowedFlags = new Set(["--json"]);
|
|
20
|
+
const valueFlags = new Set(
|
|
21
|
+
subcommand === "daily" ? ["--date"] :
|
|
22
|
+
subcommand === "monthly" ? ["--month"] :
|
|
23
|
+
["--plan"],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
27
|
+
const arg = args[i];
|
|
28
|
+
if (allowedFlags.has(arg)) {
|
|
29
|
+
forwarded.push(arg);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (valueFlags.has(arg)) {
|
|
33
|
+
if (!args[i + 1] || args[i + 1].startsWith("-")) {
|
|
34
|
+
throw new Error(`${arg} requires a value`);
|
|
35
|
+
}
|
|
36
|
+
forwarded.push(arg, args[i + 1]);
|
|
37
|
+
i += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`unknown usage option: ${arg}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return forwarded;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cmdUsage(target, rawArgs = []) {
|
|
47
|
+
const args = rawArgs.slice();
|
|
48
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
49
|
+
printUsageHelp();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const first = args[0] || "";
|
|
54
|
+
const subcommand = first && !first.startsWith("-") ? args.shift() : "status";
|
|
55
|
+
if (!SUBCOMMANDS.has(subcommand)) {
|
|
56
|
+
log(`unknown usage subcommand: ${subcommand}`);
|
|
57
|
+
printUsageHelp();
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let forwarded;
|
|
62
|
+
try {
|
|
63
|
+
forwarded = _forwardArgs(subcommand, args);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
log(err.message);
|
|
66
|
+
printUsageHelp();
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const script = findRepoScript(target, "usage_ledger.py");
|
|
71
|
+
if (!script) {
|
|
72
|
+
log("usage ledger unavailable");
|
|
73
|
+
console.log(` ${D}Expected scripts/usage_ledger.py in this project${R}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = spawnSync("python3", [script, ...forwarded], {
|
|
78
|
+
cwd: target,
|
|
79
|
+
stdio: "inherit",
|
|
80
|
+
timeout: 15000,
|
|
81
|
+
});
|
|
82
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
83
|
+
process.exit(result.status);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { cmdUsage, _forwardArgs };
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0dai vault CLI — Phase 1 SCAFFOLD (Task #104, F6 G2).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the shape of ci.js (#2696): tiny dispatcher, JSON or human
|
|
5
|
+
* output, sub-commands forwarded to ../vault/index.js. Phase 1 implements
|
|
6
|
+
* only `init`; all other sub-commands print a "P2 deferred" message and
|
|
7
|
+
* exit with status 2 so scripts can branch on it.
|
|
8
|
+
*
|
|
9
|
+
* See docs/runbooks/0dai-vault.md for the phase plan and SPEC pointer.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const shared = require("../shared");
|
|
15
|
+
const { T, R, D, log } = shared;
|
|
16
|
+
|
|
17
|
+
const vault = require("../vault");
|
|
18
|
+
|
|
19
|
+
// Phase 2a ships `add` + `get`. The rest stay P2-gated until operator
|
|
20
|
+
// approves Phase 2b (list/inject/rotate scope).
|
|
21
|
+
const P2_GATED = new Set(["list", "inject", "rotate"]);
|
|
22
|
+
|
|
23
|
+
function hasFlag(args, name) {
|
|
24
|
+
return args.includes(name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function printUsage() {
|
|
28
|
+
console.log("Usage: 0dai vault <init|add|get|list|inject|rotate> [--json]");
|
|
29
|
+
console.log(" Phase 1+2a ships `init`, `add`, `get`. `list`/`inject`/`rotate` gated on operator P2b approval.");
|
|
30
|
+
console.log(" add: 0dai vault add <scope> <name> <value> (or value via --stdin)");
|
|
31
|
+
console.log(" get: 0dai vault get <scope> <name>");
|
|
32
|
+
console.log(" Runbook: docs/runbooks/0dai-vault.md");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function printInitHuman(result) {
|
|
36
|
+
console.log(`\n ${T}0dai vault${R} — Phase 1 init`);
|
|
37
|
+
console.log(` vault dir : ${result.vaultDir}`);
|
|
38
|
+
console.log(` identity : ${result.identityPath}`);
|
|
39
|
+
console.log(` public key : ${result.publicKey || "(missing — re-run init)"}`);
|
|
40
|
+
if (result.created) {
|
|
41
|
+
console.log(` ${T}new keypair generated${R} — back up ${result.identityPath} offline`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log(` ${D}existing keypair detected — no changes${R}`);
|
|
44
|
+
}
|
|
45
|
+
console.log(` ${D}Phase 2 (get/add/list/inject/rotate) gated on operator P2 approval${R}`);
|
|
46
|
+
console.log(` ${D}runbook: docs/runbooks/0dai-vault.md${R}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cmdVaultInit(args) {
|
|
50
|
+
const asJson = hasFlag(args, "--json");
|
|
51
|
+
try {
|
|
52
|
+
const result = vault.init();
|
|
53
|
+
if (asJson) {
|
|
54
|
+
console.log(JSON.stringify({
|
|
55
|
+
schema_version: 1,
|
|
56
|
+
command: "vault init",
|
|
57
|
+
phase: 1,
|
|
58
|
+
...result,
|
|
59
|
+
}, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
printInitHuman(result);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (asJson) {
|
|
65
|
+
console.log(JSON.stringify({
|
|
66
|
+
schema_version: 1,
|
|
67
|
+
command: "vault init",
|
|
68
|
+
error: err.message,
|
|
69
|
+
code: err.code || "VAULT_INIT_FAILED",
|
|
70
|
+
}, null, 2));
|
|
71
|
+
} else {
|
|
72
|
+
log(`vault init failed: ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _readStdinSync() {
|
|
79
|
+
const fs = require("node:fs");
|
|
80
|
+
try {
|
|
81
|
+
return fs.readFileSync(0, "utf8");
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function cmdVaultAdd(args) {
|
|
88
|
+
const asJson = hasFlag(args, "--json");
|
|
89
|
+
const useStdin = hasFlag(args, "--stdin");
|
|
90
|
+
// positional args after stripping flags
|
|
91
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
92
|
+
const [scope, name, value] = positional;
|
|
93
|
+
if (!scope || !name) {
|
|
94
|
+
log("usage: 0dai vault add <scope> <name> <value>|--stdin");
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let payload = value;
|
|
99
|
+
if (useStdin) {
|
|
100
|
+
payload = _readStdinSync();
|
|
101
|
+
if (!payload) {
|
|
102
|
+
log("vault add --stdin: no input on stdin");
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (payload == null || payload === "") {
|
|
108
|
+
log("vault add: <value> required (or --stdin)");
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const result = vault.add(scope, name, payload);
|
|
114
|
+
if (asJson) {
|
|
115
|
+
console.log(JSON.stringify({
|
|
116
|
+
schema_version: 1,
|
|
117
|
+
command: "vault add",
|
|
118
|
+
phase: "2a",
|
|
119
|
+
...result,
|
|
120
|
+
}, null, 2));
|
|
121
|
+
} else {
|
|
122
|
+
console.log(`\n ${T}0dai vault add${R}`);
|
|
123
|
+
console.log(` scope/name : ${scope}/${name}`);
|
|
124
|
+
console.log(` path : ${result.path}`);
|
|
125
|
+
console.log(` bytes : ${result.bytes}`);
|
|
126
|
+
console.log(` ${result.overwritten ? T + "overwritten" + R : D + "new" + R}\n`);
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (asJson) {
|
|
130
|
+
console.log(JSON.stringify({
|
|
131
|
+
schema_version: 1,
|
|
132
|
+
command: "vault add",
|
|
133
|
+
error: err.message,
|
|
134
|
+
code: err.code || "VAULT_ADD_FAILED",
|
|
135
|
+
}, null, 2));
|
|
136
|
+
} else {
|
|
137
|
+
log(`vault add failed: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
process.exitCode = err.code === "VAULT_INVALID_NAME" ? 2 : 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function cmdVaultGet(args) {
|
|
144
|
+
const asJson = hasFlag(args, "--json");
|
|
145
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
146
|
+
const [scope, name] = positional;
|
|
147
|
+
if (!scope || !name) {
|
|
148
|
+
log("usage: 0dai vault get <scope> <name>");
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const result = vault.get(scope, name);
|
|
154
|
+
if (asJson) {
|
|
155
|
+
console.log(JSON.stringify({
|
|
156
|
+
schema_version: 1,
|
|
157
|
+
command: "vault get",
|
|
158
|
+
phase: "2a",
|
|
159
|
+
scope: result.scope,
|
|
160
|
+
name: result.name,
|
|
161
|
+
value: result.value,
|
|
162
|
+
path: result.path,
|
|
163
|
+
}, null, 2));
|
|
164
|
+
} else {
|
|
165
|
+
// Human mode: print value to stdout unwrapped so shell pipelines work.
|
|
166
|
+
process.stdout.write(result.value);
|
|
167
|
+
if (!result.value.endsWith("\n")) process.stdout.write("\n");
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (asJson) {
|
|
171
|
+
console.log(JSON.stringify({
|
|
172
|
+
schema_version: 1,
|
|
173
|
+
command: "vault get",
|
|
174
|
+
error: err.message,
|
|
175
|
+
code: err.code || "VAULT_GET_FAILED",
|
|
176
|
+
}, null, 2));
|
|
177
|
+
} else {
|
|
178
|
+
log(`vault get failed: ${err.message}`);
|
|
179
|
+
}
|
|
180
|
+
if (err.code === "VAULT_SECRET_NOT_FOUND") {
|
|
181
|
+
process.exitCode = 4; // distinct: "no such secret"
|
|
182
|
+
} else if (err.code === "VAULT_INVALID_NAME") {
|
|
183
|
+
process.exitCode = 2;
|
|
184
|
+
} else {
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cmdVaultDeferred(sub, args) {
|
|
191
|
+
const asJson = hasFlag(args, "--json");
|
|
192
|
+
const message = `vault ${sub} is gated on operator P2 approval — see docs/runbooks/0dai-vault.md`;
|
|
193
|
+
if (asJson) {
|
|
194
|
+
console.log(JSON.stringify({
|
|
195
|
+
schema_version: 1,
|
|
196
|
+
command: `vault ${sub}`,
|
|
197
|
+
phase: 2,
|
|
198
|
+
deferred: true,
|
|
199
|
+
message,
|
|
200
|
+
}, null, 2));
|
|
201
|
+
} else {
|
|
202
|
+
console.log(`\n ${T}0dai vault ${sub}${R}`);
|
|
203
|
+
console.log(` ${D}${message}${R}\n`);
|
|
204
|
+
}
|
|
205
|
+
// Distinct exit code so wrapper scripts can detect "not implemented yet"
|
|
206
|
+
// vs a real failure (1).
|
|
207
|
+
process.exitCode = 2;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function cmdVault(_target, sub, args) {
|
|
211
|
+
const command = sub && !sub.startsWith("-") ? sub : "";
|
|
212
|
+
const forwarded = command === sub ? args.slice(2) : args.slice(1);
|
|
213
|
+
|
|
214
|
+
if (!command || command === "help" || command === "-h" || command === "--help") {
|
|
215
|
+
printUsage();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (command === "init") {
|
|
220
|
+
cmdVaultInit(forwarded);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (command === "add") {
|
|
225
|
+
cmdVaultAdd(forwarded);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (command === "get") {
|
|
230
|
+
cmdVaultGet(forwarded);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (P2_GATED.has(command)) {
|
|
235
|
+
cmdVaultDeferred(command, forwarded);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
log(`unknown vault sub-command: ${command}`);
|
|
240
|
+
printUsage();
|
|
241
|
+
process.exitCode = 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = {
|
|
245
|
+
cmdVault,
|
|
246
|
+
};
|
package/lib/onboarding.js
CHANGED
|
@@ -23,12 +23,16 @@ function _loadState(target) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function _saveState(target, state) {
|
|
26
|
+
function _saveState(target, state, options = {}) {
|
|
27
|
+
const createAiDir = options.createAiDir !== false;
|
|
27
28
|
try {
|
|
28
29
|
const dir = path.join(target, "ai");
|
|
30
|
+
if (!fs.existsSync(dir) && !createAiDir) return false;
|
|
29
31
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
30
32
|
fs.writeFileSync(_statePath(target), JSON.stringify(state, null, 2) + "\n");
|
|
33
|
+
return true;
|
|
31
34
|
} catch {}
|
|
35
|
+
return false;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
// ---------------------------------------------------------------------------
|
|
@@ -73,14 +77,16 @@ function trackFirstRun(target) {
|
|
|
73
77
|
const state = _loadState(target);
|
|
74
78
|
if (!state.first_run_at) {
|
|
75
79
|
state.first_run_at = new Date().toISOString();
|
|
76
|
-
_saveState(target, state);
|
|
80
|
+
_saveState(target, state, { createAiDir: false });
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function trackFirstInit(target) {
|
|
81
85
|
const state = _loadState(target);
|
|
82
86
|
if (!state.first_init_at) {
|
|
83
|
-
|
|
87
|
+
const now = new Date().toISOString();
|
|
88
|
+
if (!state.first_run_at) state.first_run_at = now;
|
|
89
|
+
state.first_init_at = now;
|
|
84
90
|
_saveState(target, state);
|
|
85
91
|
}
|
|
86
92
|
}
|