@0dai-dev/cli 4.0.0 → 4.2.0
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/bin/0dai.js +82 -7
- package/lib/commands/auth.js +67 -28
- package/lib/commands/doctor.js +59 -13
- package/lib/commands/graph.js +103 -4
- package/lib/commands/init.js +142 -9
- package/lib/commands/models.js +49 -10
- package/lib/commands/persona-simulate.js +19 -0
- package/lib/commands/provider.js +18 -0
- package/lib/commands/ssh.js +416 -0
- package/lib/commands/status.js +105 -35
- package/lib/commands/swarm.js +39 -1
- package/lib/commands/tui.js +49 -0
- package/lib/commands/workspace.js +297 -0
- package/lib/onboarding.js +21 -7
- package/lib/shared.js +123 -4
- package/lib/tui/index.mjs +34610 -0
- package/lib/utils/model_ratings.js +77 -0
- package/lib/wizard.js +1 -1
- package/package.json +18 -5
- package/scripts/build-tui.js +77 -0
package/lib/commands/init.js
CHANGED
|
@@ -4,9 +4,10 @@ const {
|
|
|
4
4
|
T, R, D, log,
|
|
5
5
|
fs, path,
|
|
6
6
|
VERSION, SUPPORTED_CLIS,
|
|
7
|
-
apiCall, makeEnsureAuthenticated, ensureLicenseActivation,
|
|
7
|
+
apiCall, makeEnsureAuthenticated, ensureLicenseActivation, loadAuthState,
|
|
8
8
|
collectMetadata, buildProjectIdentity, registerProject,
|
|
9
9
|
writeFiles, sendProjectHeartbeat, recordExperienceEvent,
|
|
10
|
+
logFirstRunSuccess,
|
|
10
11
|
} = shared;
|
|
11
12
|
const { cmdAuthLogin } = require("./auth");
|
|
12
13
|
|
|
@@ -42,6 +43,20 @@ async function cmdInit(target, args = []) {
|
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// Pre-check: verify init quota before starting wizard (avoid 10 min wizard → "limit reached")
|
|
47
|
+
if (!dryRun) {
|
|
48
|
+
try {
|
|
49
|
+
const precheck = await apiCall("/v1/projects/precheck", {
|
|
50
|
+
device_id: shared.deviceFingerprint(),
|
|
51
|
+
});
|
|
52
|
+
if (precheck.error && precheck.error.includes("limit")) {
|
|
53
|
+
log(`${precheck.error}`);
|
|
54
|
+
if (precheck.hint) console.log(` ${D}${precheck.hint}${R}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
// First-run wizard (unless --no-wizard or non-interactive)
|
|
46
61
|
if (!noWizard && !dryRun && !minimal) {
|
|
47
62
|
try {
|
|
@@ -150,9 +165,9 @@ async function cmdInit(target, args = []) {
|
|
|
150
165
|
}
|
|
151
166
|
console.log(` ${D}3.${R} Open dashboard: ${D}https://0dai.dev/dashboard${R}`);
|
|
152
167
|
|
|
153
|
-
await sendProjectHeartbeat(identity, result, {
|
|
168
|
+
const heartbeat = await sendProjectHeartbeat(target, identity, result, {
|
|
154
169
|
project_id: boundProject.project_id || identity.project_id,
|
|
155
|
-
}).catch(() =>
|
|
170
|
+
}).catch(() => null);
|
|
156
171
|
recordExperienceEvent(target, {
|
|
157
172
|
event_type: "config_generated",
|
|
158
173
|
agent: "cli",
|
|
@@ -162,6 +177,20 @@ async function cmdInit(target, args = []) {
|
|
|
162
177
|
context: { stack: result.stack || identity.stack || "unknown", files_touched: Number(result.file_count || 0), tests_passed: true },
|
|
163
178
|
});
|
|
164
179
|
|
|
180
|
+
// First-run proof gate (issue #342). All 4 gates pass once we reach here:
|
|
181
|
+
// license active (line above), project bound, ai/ layer written, heartbeat sent.
|
|
182
|
+
// Idempotent — only fires once per project. See docs/first-run.md.
|
|
183
|
+
const firstRun = logFirstRunSuccess(target, {
|
|
184
|
+
license: true,
|
|
185
|
+
project_bound: true,
|
|
186
|
+
layer_written: true,
|
|
187
|
+
heartbeat: !!heartbeat && !heartbeat.error,
|
|
188
|
+
});
|
|
189
|
+
if (firstRun.fired) {
|
|
190
|
+
const suffix = typeof firstRun.elapsedS === "number" ? ` (${firstRun.elapsedS}s)` : "";
|
|
191
|
+
console.log(` ${D}first-run gate: success${suffix}${R}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
165
194
|
// Send anonymous usage ping
|
|
166
195
|
apiCall("/v1/feedback", { report: {
|
|
167
196
|
stack_detected: result.stack || "?", _auto: true, _plan: result.plan || "trial",
|
|
@@ -172,15 +201,14 @@ async function cmdInit(target, args = []) {
|
|
|
172
201
|
async function cmdSync(target, args = []) {
|
|
173
202
|
const dryRun = args.includes("--dry-run");
|
|
174
203
|
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
204
|
+
const force = args.includes("--force");
|
|
175
205
|
|
|
176
|
-
// Quick local check: skip API if already at current version (unless dry-run)
|
|
206
|
+
// Quick local check: skip API if already at current version (unless dry-run or force)
|
|
177
207
|
let version = "unknown";
|
|
178
208
|
try { version = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim(); } catch {}
|
|
179
209
|
|
|
180
210
|
const metadata = collectMetadata(target);
|
|
181
211
|
const { manifestContents, clis } = metadata;
|
|
182
|
-
const authStatus = await ensureAuthenticated("sync");
|
|
183
|
-
const license = await ensureLicenseActivation();
|
|
184
212
|
let stack = "generic", agents = [];
|
|
185
213
|
try {
|
|
186
214
|
const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
|
|
@@ -188,6 +216,28 @@ async function cmdSync(target, args = []) {
|
|
|
188
216
|
agents = d.selected_agents || [];
|
|
189
217
|
} catch {}
|
|
190
218
|
const identity = buildProjectIdentity(target, metadata, stack);
|
|
219
|
+
|
|
220
|
+
if (dryRun) {
|
|
221
|
+
const auth = loadAuthState();
|
|
222
|
+
const hasAuth = !!(auth && (auth.api_key || auth.access_token || auth.token));
|
|
223
|
+
if (!hasAuth) {
|
|
224
|
+
const preview = buildLocalSyncPreview(target, { version, stack, cliVersion: VERSION });
|
|
225
|
+
log(`${D}dry-run: local preview without auth (exact cloud plan unavailable)${R}`);
|
|
226
|
+
console.log(` stack: ${preview.stack}`);
|
|
227
|
+
console.log(` ai version: ${preview.current_version} ${preview.version_matches ? `${D}(matches CLI ${preview.cli_version})${R}` : `${D}(CLI ${preview.cli_version})${R}`}`);
|
|
228
|
+
if (preview.changes.length) {
|
|
229
|
+
console.log(" likely changes:");
|
|
230
|
+
for (const change of preview.changes) console.log(` ~ ${change}`);
|
|
231
|
+
} else {
|
|
232
|
+
console.log(` ${D}no obvious local drift found${R}`);
|
|
233
|
+
}
|
|
234
|
+
console.log(` ${D}Run: 0dai auth login for exact managed diff and write-mode sync${R}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const authStatus = await ensureAuthenticated("sync");
|
|
240
|
+
const license = await ensureLicenseActivation();
|
|
191
241
|
const boundProject = await bindProjectForCloud(target, metadata, identity);
|
|
192
242
|
|
|
193
243
|
// Collect current ai/ files
|
|
@@ -210,11 +260,12 @@ async function cmdSync(target, args = []) {
|
|
|
210
260
|
}
|
|
211
261
|
|
|
212
262
|
if (dryRun) log(`${D}dry-run: checking what sync would change...${R}`);
|
|
263
|
+
if (force && !dryRun) log(`${T}force mode: will overwrite native configs from ai/ source${R}`);
|
|
213
264
|
|
|
214
265
|
const result = await apiCall("/v1/sync", {
|
|
215
266
|
ai_version: version, stack, agents: agents.length ? agents : clis,
|
|
216
267
|
current_files: currentFiles, manifest_contents: manifestContents,
|
|
217
|
-
dry_run: dryRun, quiet,
|
|
268
|
+
dry_run: dryRun, quiet, force,
|
|
218
269
|
project_name: identity.project_name,
|
|
219
270
|
project_id: boundProject.project_id || identity.project_id,
|
|
220
271
|
remote_origin: identity.remote_origin,
|
|
@@ -250,6 +301,34 @@ async function cmdSync(target, args = []) {
|
|
|
250
301
|
} else {
|
|
251
302
|
log("already up to date");
|
|
252
303
|
}
|
|
304
|
+
|
|
305
|
+
// --force: also overwrite native configs (CLAUDE.md, AGENTS.md, etc.) from ai/ source
|
|
306
|
+
if (force && result.native_configs) {
|
|
307
|
+
const NATIVE_CONFIGS = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", "opencode.json", ".cursorrules", ".windsurfrules", ".aider.conf.yml"];
|
|
308
|
+
let overwritten = 0;
|
|
309
|
+
for (const name of NATIVE_CONFIGS) {
|
|
310
|
+
if (result.native_configs[name]) {
|
|
311
|
+
fs.writeFileSync(path.join(target, name), result.native_configs[name], "utf8");
|
|
312
|
+
overwritten++;
|
|
313
|
+
if (!quiet) console.log(` [force] ${name} overwritten from ai/ source`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (overwritten && !quiet) {
|
|
317
|
+
log(`force: ${overwritten} native config file(s) overwritten`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --force: update drift baseline hashes so drift clears after regeneration
|
|
322
|
+
if (force) {
|
|
323
|
+
try {
|
|
324
|
+
const { spawnSync } = require("child_process");
|
|
325
|
+
const driftScript = path.join(target, "scripts", "drift_detector.py");
|
|
326
|
+
if (fs.existsSync(driftScript)) {
|
|
327
|
+
spawnSync("python3", [driftScript, "record", "--target", target], { stdio: "inherit" });
|
|
328
|
+
}
|
|
329
|
+
} catch {}
|
|
330
|
+
}
|
|
331
|
+
|
|
253
332
|
if (!quiet) {
|
|
254
333
|
console.log(` account: ${authStatus.email} · plan: ${authStatus.plan || license.plan || "free"} · activation: ${license.status}`);
|
|
255
334
|
console.log(` project: ${boundProject.project_id || identity.project_id}`);
|
|
@@ -266,7 +345,7 @@ async function cmdSync(target, args = []) {
|
|
|
266
345
|
|
|
267
346
|
// Update portfolio registry
|
|
268
347
|
registerProject(target, path.basename(target), stack);
|
|
269
|
-
await sendProjectHeartbeat(identity, result, {
|
|
348
|
+
await sendProjectHeartbeat(target, identity, result, {
|
|
270
349
|
project_id: boundProject.project_id || identity.project_id,
|
|
271
350
|
}).catch(() => {});
|
|
272
351
|
recordExperienceEvent(target, {
|
|
@@ -279,4 +358,58 @@ async function cmdSync(target, args = []) {
|
|
|
279
358
|
});
|
|
280
359
|
}
|
|
281
360
|
|
|
282
|
-
|
|
361
|
+
function buildLocalSyncPreview(target, { version, stack, cliVersion }) {
|
|
362
|
+
const changes = [];
|
|
363
|
+
const expectedAiFiles = [
|
|
364
|
+
"ai/VERSION",
|
|
365
|
+
"ai/manifest/project.yaml",
|
|
366
|
+
"ai/manifest/commands.yaml",
|
|
367
|
+
"ai/manifest/discovery.json",
|
|
368
|
+
];
|
|
369
|
+
for (const rel of expectedAiFiles) {
|
|
370
|
+
if (!fs.existsSync(path.join(target, rel))) changes.push(`${rel} (missing)`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (version !== "unknown" && version !== cliVersion) {
|
|
374
|
+
changes.push(`ai/VERSION (${version} -> ${cliVersion})`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const driftTracked = [
|
|
378
|
+
"CLAUDE.md",
|
|
379
|
+
"AGENTS.md",
|
|
380
|
+
"GEMINI.md",
|
|
381
|
+
"opencode.json",
|
|
382
|
+
".cursorrules",
|
|
383
|
+
".windsurfrules",
|
|
384
|
+
".aider.conf.yml",
|
|
385
|
+
];
|
|
386
|
+
const hashesPath = path.join(target, "ai", "manifest", "config_hashes.json");
|
|
387
|
+
try {
|
|
388
|
+
const hashes = JSON.parse(fs.readFileSync(hashesPath, "utf8"));
|
|
389
|
+
const crypto = require("crypto");
|
|
390
|
+
for (const rel of driftTracked) {
|
|
391
|
+
const filePath = path.join(target, rel);
|
|
392
|
+
const recorded = hashes[rel];
|
|
393
|
+
const exists = fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
|
394
|
+
if (recorded && !exists) changes.push(`${rel} (missing from workspace)`);
|
|
395
|
+
if (recorded && exists) {
|
|
396
|
+
const currentHash = crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
397
|
+
if (currentHash !== String(recorded.hash || "")) changes.push(`${rel} (local edits)`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch {}
|
|
401
|
+
|
|
402
|
+
if (!fs.existsSync(path.join(target, "ai"))) {
|
|
403
|
+
changes.push("ai/ layer missing — run 0dai init after auth");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
stack,
|
|
408
|
+
current_version: version,
|
|
409
|
+
cli_version: cliVersion,
|
|
410
|
+
version_matches: version === cliVersion,
|
|
411
|
+
changes,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
module.exports = { cmdInit, cmdSync, buildLocalSyncPreview };
|
package/lib/commands/models.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const shared = require("../shared");
|
|
3
|
-
const { T, R } = shared;
|
|
3
|
+
const { T, R, SUPPORTED_CLIS } = shared;
|
|
4
|
+
const {
|
|
5
|
+
probeInstalledCliNames,
|
|
6
|
+
summarizeModelAvailability,
|
|
7
|
+
formatAvailableFooter,
|
|
8
|
+
} = require("../utils/model_ratings");
|
|
4
9
|
|
|
5
10
|
function cmdModels(filter) {
|
|
6
11
|
// Scores from benchmark_models.py (3-task: read/count/review, 2026-04-06)
|
|
@@ -23,35 +28,69 @@ function cmdModels(filter) {
|
|
|
23
28
|
{ name: "MiniMax M2.5", tier: "slow", score: 57, cli: "opencode", flag: "-m opencode-go/minimax-m2.5", tested: true },
|
|
24
29
|
];
|
|
25
30
|
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
for (const cli of ["claude", "codex", "opencode", "gemini", "aider"]) {
|
|
29
|
-
try { execFileSync("/bin/sh", ["-c", `command -v ${cli}`], { stdio: "ignore" }); available.add(cli); } catch {}
|
|
30
|
-
}
|
|
31
|
+
const installedCliNames = probeInstalledCliNames(SUPPORTED_CLIS);
|
|
32
|
+
const availability = summarizeModelAvailability(MODELS, SUPPORTED_CLIS, installedCliNames);
|
|
31
33
|
|
|
32
34
|
const isTTY = process.stdout.isTTY;
|
|
33
35
|
const Y = isTTY ? "\x1b[33m" : "";
|
|
34
36
|
const G = isTTY ? "\x1b[32m" : "";
|
|
35
37
|
const DIM = isTTY ? "\x1b[2m" : "";
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
const allModels = [...MODELS].sort((a, b) => b.score - a.score);
|
|
40
|
+
let models = [...allModels];
|
|
38
41
|
if (filter === "--fast") models = models.filter(m => m.tier === "fast");
|
|
39
42
|
if (filter === "--balanced") models = models.filter(m => m.tier === "balanced");
|
|
40
43
|
if (filter === "--deep") models = models.filter(m => m.tier === "deep");
|
|
41
|
-
if (filter === "--available") models = models.filter(m =>
|
|
44
|
+
if (filter === "--available") models = models.filter(m => installedCliNames.has(m.cli));
|
|
42
45
|
|
|
43
46
|
const tc = (t) => t === "deep" ? T : t === "balanced" ? G : DIM;
|
|
44
47
|
console.log(`\n ${T}0dai${R} model ratings — ${models.length} models\n`);
|
|
45
48
|
console.log(` ${"SCORE".padEnd(6)} ${"MODEL".padEnd(22)} ${"TIER".padEnd(10)} ${"CLI".padEnd(10)} FLAG`);
|
|
46
49
|
console.log(` ${"-".repeat(64)}`);
|
|
47
50
|
for (const m of models) {
|
|
48
|
-
const dim =
|
|
51
|
+
const dim = installedCliNames.has(m.cli) ? "" : DIM;
|
|
49
52
|
const mark = m.tested ? ` ${G}✓${R}` : "";
|
|
50
53
|
console.log(`${dim} ${Y}${String(m.score).padEnd(6)}${R} ${m.name.padEnd(22)} ${tc(m.tier)}${m.tier.padEnd(10)}${R} ${m.cli.padEnd(10)} ${DIM}${m.flag}${R}${mark}${dim ? R : ""}`);
|
|
51
54
|
}
|
|
52
55
|
console.log(`\n ${DIM}✓ = swarm-benchmarked | dimmed = CLI not installed${R}`);
|
|
53
56
|
console.log(` ${DIM}Filter: --fast --balanced --deep --available${R}`);
|
|
57
|
+
if (filter === "--available") {
|
|
58
|
+
console.log(` ${DIM}${formatAvailableFooter(availability, models.length)}${R}`);
|
|
59
|
+
}
|
|
54
60
|
console.log(` ${DIM}Full table: https://0dai.dev/models${R}\n`);
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
async function cmdModelsRecommend(target, args) {
|
|
64
|
+
const shared = require("../shared");
|
|
65
|
+
const { log, T, R, D, findRepoScript, spawnSync, requirePlan } = shared;
|
|
66
|
+
|
|
67
|
+
const gate = requirePlan("pro", "Model Recommend", target);
|
|
68
|
+
if (gate) { log(gate.error); log(gate.hint); return; }
|
|
69
|
+
|
|
70
|
+
const taskType = args.find((_, i) => args[i - 1] === "--task") || "";
|
|
71
|
+
const goal = args.find((_, i) => args[i - 1] === "--goal") || "";
|
|
72
|
+
const maxCost = parseFloat(args.find((_, i) => args[i - 1] === "--max-cost") || "0");
|
|
73
|
+
const minQuality = parseFloat(args.find((_, i) => args[i - 1] === "--min-quality") || "0");
|
|
74
|
+
const asJson = args.includes("--json");
|
|
75
|
+
|
|
76
|
+
if (!taskType && !goal) {
|
|
77
|
+
console.log("Usage: 0dai models recommend --task TYPE [--goal '...'] [--max-cost N] [--min-quality N] [--json]");
|
|
78
|
+
console.log(" TYPE: feat, fix, refactor, test, docs");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const recScript = findRepoScript(target, "model_router.py");
|
|
83
|
+
if (!recScript) { log("model router unavailable"); return; }
|
|
84
|
+
|
|
85
|
+
const fwd = [recScript, "recommend", "--target", target];
|
|
86
|
+
if (taskType) fwd.push("--task", taskType);
|
|
87
|
+
if (goal) fwd.push("--goal", goal);
|
|
88
|
+
if (maxCost > 0) fwd.push("--max-cost", String(maxCost));
|
|
89
|
+
if (minQuality > 0) fwd.push("--min-quality", String(minQuality));
|
|
90
|
+
if (asJson) fwd.push("--json");
|
|
91
|
+
|
|
92
|
+
const result = spawnSync("python3", fwd, { stdio: "inherit" });
|
|
93
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { cmdModels, cmdModelsRecommend };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const shared = require("../shared");
|
|
3
|
+
const { log, D, R, spawnSync, findRepoScript } = shared;
|
|
4
|
+
|
|
5
|
+
function cmdPersonaSimulate(target, args) {
|
|
6
|
+
const script = findRepoScript(target, "persona_simulate.py");
|
|
7
|
+
if (!script) {
|
|
8
|
+
log("persona-simulate unavailable in this environment");
|
|
9
|
+
console.log(` ${D}Expected scripts/persona_simulate.py in repo checkout${R}`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const forwarded = [script, ...args, "--target", target];
|
|
14
|
+
const result = spawnSync("python3", forwarded, { stdio: "inherit" });
|
|
15
|
+
if (typeof result.status === "number") process.exit(result.status);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { cmdPersonaSimulate };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const shared = require("../shared");
|
|
4
|
+
const { log, spawnSync, findRepoScript } = shared;
|
|
5
|
+
|
|
6
|
+
function cmdProvider(target, args) {
|
|
7
|
+
const script = findRepoScript(target, "provider_profiles.py");
|
|
8
|
+
if (!script) {
|
|
9
|
+
log("provider profiles unavailable in this environment");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const forwarded = [script, ...args];
|
|
13
|
+
const result = spawnSync("python3", forwarded, { stdio: "inherit" });
|
|
14
|
+
if (typeof result.status === "number") process.exit(result.status);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { cmdProvider };
|