@0dai-dev/cli 4.1.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 +308 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +404 -122
- 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 +79 -14
- 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 +553 -53
- package/lib/commands/loop.js +108 -0
- package/lib/commands/mcp.js +410 -0
- package/lib/commands/models.js +42 -12
- package/lib/commands/paste.js +114 -0
- package/lib/commands/persona-simulate.js +19 -0
- package/lib/commands/play.js +173 -0
- package/lib/commands/provider.js +87 -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/ssh.js +416 -0
- package/lib/commands/standup.js +40 -0
- package/lib/commands/status.js +131 -36
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +117 -0
- package/lib/commands/usage.js +87 -0
- package/lib/commands/vault.js +246 -0
- package/lib/commands/workspace.js +1 -0
- package/lib/onboarding.js +30 -10
- package/lib/shared.js +153 -96
- package/lib/tui/index.mjs +34994 -0
- 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/model_ratings.js +77 -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 +13 -5
- package/scripts/build-tui.js +77 -0
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 };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const shared = require("../shared");
|
|
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
|
+
|
|
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
|
+
|
|
49
|
+
if (!process.stdout.isTTY) {
|
|
50
|
+
console.error("0dai tui requires an interactive TTY. Try: 0dai status");
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
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
|
+
|
|
90
|
+
const bundlePath = path.join(__dirname, "..", "tui", "index.mjs");
|
|
91
|
+
let tui;
|
|
92
|
+
try {
|
|
93
|
+
const url = require("url").pathToFileURL(bundlePath).href;
|
|
94
|
+
tui = await import(url);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const code = err && err.code;
|
|
97
|
+
const msg = String(err && err.message || "");
|
|
98
|
+
if (code === "ERR_MODULE_NOT_FOUND" || msg.includes("Cannot find module")) {
|
|
99
|
+
log("TUI bundle missing. Rebuild with:");
|
|
100
|
+
console.log(` ${D}cd $(npm root -g)/@0dai-dev/cli && node scripts/build-tui.js${R}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let plan = "free";
|
|
107
|
+
try {
|
|
108
|
+
const auth = shared.loadAuthState();
|
|
109
|
+
if (auth && auth.plan) plan = auth.plan;
|
|
110
|
+
} catch {
|
|
111
|
+
// keep default plan
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await tui.run({ target, version: VERSION, plan, writeMode, remoteUrl, projectId });
|
|
115
|
+
}
|
|
116
|
+
|
|
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
|
+
};
|
|
@@ -108,6 +108,7 @@ function detectSessions(target) {
|
|
|
108
108
|
function cmdWorkspaceInit(target, args) {
|
|
109
109
|
const globalFlag = args.includes("--global");
|
|
110
110
|
|
|
111
|
+
const sessions = detectSessions(target);
|
|
111
112
|
if (sessions.length === 0) {
|
|
112
113
|
log("no services detected. Create workspace config manually with: 0dai workspace add");
|
|
113
114
|
return;
|
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
|
}
|
|
@@ -104,20 +110,28 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
|
|
|
104
110
|
|
|
105
111
|
// Step 1: Auth
|
|
106
112
|
console.log(`\n [1/${steps}] Checking authentication...`);
|
|
113
|
+
let signedIn = false;
|
|
107
114
|
let authInfo = "not signed in";
|
|
108
115
|
try {
|
|
109
116
|
const auth = JSON.parse(fs.readFileSync(path.join(require("os").homedir(), ".0dai", "auth.json"), "utf8"));
|
|
110
117
|
if (auth.access_token) {
|
|
118
|
+
signedIn = true;
|
|
111
119
|
authInfo = `signed in (${auth.plan || "free"} plan)`;
|
|
112
120
|
}
|
|
113
121
|
} catch {}
|
|
114
|
-
|
|
122
|
+
if (signedIn) {
|
|
123
|
+
console.log(` \u2713 ${authInfo}`);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(` \u26a0 ${authInfo} \u2192 run 0dai auth login`);
|
|
126
|
+
}
|
|
115
127
|
|
|
116
128
|
// Step 2: Init
|
|
117
129
|
console.log(` [2/${steps}] Checking project config...`);
|
|
118
130
|
const aiExists = fs.existsSync(path.join(target, "ai", "VERSION"));
|
|
119
131
|
if (aiExists) {
|
|
120
132
|
console.log(" \u2713 ai/ layer found");
|
|
133
|
+
} else if (!signedIn) {
|
|
134
|
+
console.log(" \u26a0 skipped \u2192 sign in before first init");
|
|
121
135
|
} else {
|
|
122
136
|
console.log(" initializing...");
|
|
123
137
|
try {
|
|
@@ -132,9 +146,9 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
|
|
|
132
146
|
|
|
133
147
|
// Step 3: Doctor
|
|
134
148
|
console.log(` [3/${steps}] Running health check...`);
|
|
135
|
-
if (fs.existsSync(path.join(target, "ai"))) {
|
|
149
|
+
if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
|
|
136
150
|
try {
|
|
137
|
-
cmdDoctor(target);
|
|
151
|
+
cmdDoctor(target, { suppressExitCode: true });
|
|
138
152
|
} catch {}
|
|
139
153
|
console.log(" \u2713 health check complete");
|
|
140
154
|
} else {
|
|
@@ -143,7 +157,7 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
|
|
|
143
157
|
|
|
144
158
|
// Step 4: Status
|
|
145
159
|
console.log(` [4/${steps}] Project status:`);
|
|
146
|
-
if (fs.existsSync(path.join(target, "ai"))) {
|
|
160
|
+
if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
|
|
147
161
|
try {
|
|
148
162
|
cmdStatus(target);
|
|
149
163
|
} catch {}
|
|
@@ -155,9 +169,15 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
|
|
|
155
169
|
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
156
170
|
console.log(` [5/${steps}] Ready! (${elapsed}s)`);
|
|
157
171
|
console.log("");
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
172
|
+
if (!signedIn && !aiExists) {
|
|
173
|
+
console.log(" Next:");
|
|
174
|
+
console.log(" 0dai auth login Sign in to unlock init and sync");
|
|
175
|
+
console.log(" 0dai quickstart Re-run after sign-in");
|
|
176
|
+
} else {
|
|
177
|
+
console.log(" Your project is set up. Try:");
|
|
178
|
+
console.log(" 0dai swarm run --goal \"add auth\" (Pro)");
|
|
179
|
+
console.log(" 0dai graph push (Pro)");
|
|
180
|
+
}
|
|
161
181
|
console.log("");
|
|
162
182
|
}
|
|
163
183
|
|