@0dai-dev/cli 4.1.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 +25 -6
- package/lib/commands/auth.js +67 -28
- package/lib/commands/doctor.js +59 -13
- package/lib/commands/init.js +116 -28
- package/lib/commands/models.js +15 -9
- 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/tui.js +49 -0
- package/lib/commands/workspace.js +1 -0
- package/lib/onboarding.js +21 -7
- package/lib/shared.js +124 -82
- package/lib/tui/index.mjs +34610 -0
- package/lib/utils/model_ratings.js +77 -0
- package/package.json +12 -4
- package/scripts/build-tui.js +77 -0
package/bin/0dai.js
CHANGED
|
@@ -33,6 +33,10 @@ const { cmdGraph } = require("../lib/commands/graph");
|
|
|
33
33
|
const { cmdReport } = require("../lib/commands/report");
|
|
34
34
|
const { cmdExperience } = require("../lib/commands/experience");
|
|
35
35
|
const { cmdWorkspace } = require("../lib/commands/workspace");
|
|
36
|
+
const { cmdSsh } = require("../lib/commands/ssh");
|
|
37
|
+
const { cmdTui } = require("../lib/commands/tui");
|
|
38
|
+
const { cmdPersonaSimulate } = require("../lib/commands/persona-simulate");
|
|
39
|
+
const { cmdProvider } = require("../lib/commands/provider");
|
|
36
40
|
|
|
37
41
|
async function main() {
|
|
38
42
|
const args = process.argv.slice(2);
|
|
@@ -103,13 +107,21 @@ async function main() {
|
|
|
103
107
|
case "init": await cmdInit(target, args); break;
|
|
104
108
|
case "sync": await cmdSync(target, args); break;
|
|
105
109
|
case "detect": await cmdDetect(target); break;
|
|
106
|
-
case "doctor":
|
|
107
|
-
|
|
110
|
+
case "doctor": {
|
|
111
|
+
const driftMode = args.includes("--drift");
|
|
112
|
+
cmdDoctor(target, { drift: driftMode });
|
|
108
113
|
if (args.includes("--drift")) {
|
|
109
114
|
const ds = findRepoScript(target, "drift_detector.py");
|
|
110
|
-
|
|
115
|
+
console.log("\n drift report:");
|
|
116
|
+
if (ds) {
|
|
117
|
+
const result = spawnSync("python3", [ds, "report", "--target", target], { stdio: "inherit" });
|
|
118
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
119
|
+
} else {
|
|
120
|
+
console.log(` ${D}drift detector unavailable in this environment${R}`);
|
|
121
|
+
}
|
|
111
122
|
}
|
|
112
123
|
break;
|
|
124
|
+
}
|
|
113
125
|
case "drift": {
|
|
114
126
|
const ds = findRepoScript(target, "drift_detector.py");
|
|
115
127
|
if (!ds) { log("drift detector unavailable"); break; }
|
|
@@ -127,7 +139,7 @@ async function main() {
|
|
|
127
139
|
case "update": cmdUpdate(args); break;
|
|
128
140
|
case "metrics": cmdMetrics(target); break;
|
|
129
141
|
case "portfolio": cmdPortfolio(); break;
|
|
130
|
-
case "status": cmdStatus(target); break;
|
|
142
|
+
case "status": cmdStatus(target, { json: args.includes("--json") }); break;
|
|
131
143
|
case "auth":
|
|
132
144
|
if (sub === "login") await cmdAuthLogin();
|
|
133
145
|
else if (sub === "logout") cmdAuthLogout();
|
|
@@ -142,9 +154,13 @@ async function main() {
|
|
|
142
154
|
case "session": cmdSession(target, sub, args); break;
|
|
143
155
|
case "swarm": cmdSwarm(target, sub, args); break;
|
|
144
156
|
case "workspace": cmdWorkspace(target, sub, args.slice(2)); break;
|
|
157
|
+
case "ssh": await cmdSsh(target, sub, args); break;
|
|
158
|
+
case "provider": cmdProvider(target, args.slice(1)); break;
|
|
159
|
+
case "tui": case "dashboard": await cmdTui(target, args.slice(1)); break;
|
|
145
160
|
case "feedback": await cmdFeedback(target, sub, args); break;
|
|
146
161
|
case "report": cmdReport(target, sub, args); break;
|
|
147
162
|
case "experience": cmdExperience(target, sub, args); break;
|
|
163
|
+
case "persona-simulate": cmdPersonaSimulate(target, args.slice(1)); break;
|
|
148
164
|
case "graph": await cmdGraph(target, sub, args); break;
|
|
149
165
|
case "models": cmdModels(sub || args[1]); break;
|
|
150
166
|
case "delegate": case "delegation": {
|
|
@@ -245,17 +261,19 @@ async function main() {
|
|
|
245
261
|
console.log(" init Initialize ai/ layer (via API) [--dry-run] [--minimal]");
|
|
246
262
|
console.log(" sync Update ai/ layer (via API) [--dry-run] [--quiet] [--force]");
|
|
247
263
|
console.log(" detect Show detected stack");
|
|
248
|
-
console.log(" doctor Check health + credentials checklist");
|
|
264
|
+
console.log(" doctor Check health + credentials checklist [--drift]");
|
|
249
265
|
console.log(" update Update all installed agent CLIs to latest [--dry-run]");
|
|
250
266
|
console.log(" validate Validate ai/ layer completeness");
|
|
251
267
|
console.log(" reflect Session reflection: delivered, delegation rate, blockers");
|
|
252
268
|
console.log(" metrics Effectiveness score: adoption funnel, sessions, delegation");
|
|
253
269
|
console.log(" portfolio All tracked projects: score, sessions, agents, last activity");
|
|
254
|
-
console.log(" status Show maturity, swarm, session");
|
|
270
|
+
console.log(" status Show maturity, swarm, session [--json]");
|
|
255
271
|
console.log(" session save Save session for roaming");
|
|
256
272
|
console.log(" swarm status Task queue & delegation");
|
|
257
273
|
console.log(" swarm webhook add Register webhook (fires on task done/failed)");
|
|
258
274
|
console.log(" swarm webhook list Show registered webhooks");
|
|
275
|
+
console.log(" ssh Manage SSH keys, hosts, grants, and host-side sync");
|
|
276
|
+
console.log(" provider Local provider profiles, bindings, and direct invoke");
|
|
259
277
|
console.log(" swarm webhook test Send test ping to a webhook URL");
|
|
260
278
|
console.log(" workspace init Create tmux workspace config (auto-detect services)");
|
|
261
279
|
console.log(" workspace up Start all workspace sessions");
|
|
@@ -264,6 +282,7 @@ async function main() {
|
|
|
264
282
|
console.log(" report preview Preview privacy-safe project report");
|
|
265
283
|
console.log(" report push Send report to 0dai (with offline queue)");
|
|
266
284
|
console.log(" report status Show last report, queue, and auto-report status");
|
|
285
|
+
console.log(" persona-simulate Produce a focus-group report and optional issue drafts");
|
|
267
286
|
console.log(" experience list Show recent structured experience events");
|
|
268
287
|
console.log(" experience stats Show success and cost stats by agent/model/type");
|
|
269
288
|
console.log(" graph push Upload local graph to server (Pro: edges, Free: nodes)");
|
package/lib/commands/auth.js
CHANGED
|
@@ -8,6 +8,40 @@ const {
|
|
|
8
8
|
makeEnsureAuthenticated, ensureLicenseActivation,
|
|
9
9
|
} = shared;
|
|
10
10
|
|
|
11
|
+
async function resolveSessionState() {
|
|
12
|
+
const auth = loadAuthState();
|
|
13
|
+
const hasCachedToken = !!(auth && (auth.api_key || auth.access_token || auth.token));
|
|
14
|
+
if (!hasCachedToken) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
degraded: false,
|
|
18
|
+
auth: null,
|
|
19
|
+
status: null,
|
|
20
|
+
message: "Not logged in. Run: 0dai auth login",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const status = await fetchAuthStatus();
|
|
25
|
+
if (!status || status.error || !status.email) {
|
|
26
|
+
const email = auth.email || auth.user || "unknown";
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
degraded: true,
|
|
30
|
+
auth,
|
|
31
|
+
status,
|
|
32
|
+
message: `Saved session for ${email}, but cloud validation failed. Run: 0dai auth login`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
degraded: false,
|
|
39
|
+
auth,
|
|
40
|
+
status,
|
|
41
|
+
message: "",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
11
45
|
async function cmdAuthLogin() {
|
|
12
46
|
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
13
47
|
|
|
@@ -131,8 +165,8 @@ async function cmdAuthLogin() {
|
|
|
131
165
|
process.exit(1);
|
|
132
166
|
}
|
|
133
167
|
}
|
|
134
|
-
s.stop("
|
|
135
|
-
p.log.error("
|
|
168
|
+
s.stop("Device code expired");
|
|
169
|
+
p.log.error("The code expired after 10 minutes. Run '0dai auth login' to get a new code.");
|
|
136
170
|
process.exit(1);
|
|
137
171
|
|
|
138
172
|
} else {
|
|
@@ -158,7 +192,7 @@ async function cmdAuthLogin() {
|
|
|
158
192
|
return;
|
|
159
193
|
}
|
|
160
194
|
}
|
|
161
|
-
log("
|
|
195
|
+
log("Device code expired after 10 minutes. Run '0dai auth login' again.");
|
|
162
196
|
process.exit(1);
|
|
163
197
|
}
|
|
164
198
|
}
|
|
@@ -196,26 +230,25 @@ async function cmdRedeem(code) {
|
|
|
196
230
|
}
|
|
197
231
|
|
|
198
232
|
async function cmdAuthStatus() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
log("Not logged in. Run: 0dai auth login");
|
|
233
|
+
const session = await resolveSessionState();
|
|
234
|
+
if (!session.ok) {
|
|
235
|
+
log(session.message);
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { auth, status } = session;
|
|
241
|
+
const email = status.email || auth.email || auth.user || "unknown";
|
|
242
|
+
log(`${email} (${status.plan || auth.plan || "free"} plan)`);
|
|
243
|
+
if (status.usage_today) {
|
|
244
|
+
console.log(" Usage today:");
|
|
245
|
+
for (const [k, v] of Object.entries(status.usage_today))
|
|
246
|
+
console.log(` ${k}: ${v} / ${status.limits[k]}`);
|
|
247
|
+
}
|
|
248
|
+
const license = status.license || auth.license || { status: "inactive" };
|
|
249
|
+
console.log(` Activation: ${license.status || "inactive"}${license.activation_id ? ` (${license.activation_id})` : ""}`);
|
|
250
|
+
if (status.projects && status.projects.length) {
|
|
251
|
+
console.log(` Projects bound: ${status.projects.length} / ${status.project_limit || "?"}`);
|
|
219
252
|
}
|
|
220
253
|
}
|
|
221
254
|
|
|
@@ -229,13 +262,19 @@ async function cmdActivateFree() {
|
|
|
229
262
|
}
|
|
230
263
|
|
|
231
264
|
async function cmdActivateStatus() {
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
265
|
+
const session = await resolveSessionState();
|
|
266
|
+
if (!session.ok) {
|
|
267
|
+
log(session.message);
|
|
268
|
+
process.exitCode = 1;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const licenseResult = await apiCall("/v1/licenses/status");
|
|
273
|
+
const license = licenseResult.license || session.status.license || session.auth.license || { status: "inactive" };
|
|
235
274
|
updateAuthState({ license });
|
|
236
275
|
log(`license ${license.status || "inactive"}`);
|
|
237
276
|
if (license.activation_id) console.log(` activation id: ${license.activation_id}`);
|
|
238
|
-
console.log(` plan: ${license.plan || status.plan || "free"}`);
|
|
277
|
+
console.log(` plan: ${license.plan || session.status.plan || session.auth.plan || "free"}`);
|
|
239
278
|
}
|
|
240
279
|
|
|
241
|
-
module.exports = { cmdAuthLogin, cmdAuthLogout, cmdRedeem, cmdAuthStatus, cmdActivateFree, cmdActivateStatus };
|
|
280
|
+
module.exports = { cmdAuthLogin, cmdAuthLogout, cmdRedeem, cmdAuthStatus, cmdActivateFree, cmdActivateStatus, resolveSessionState };
|
package/lib/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const shared = require("../shared");
|
|
3
3
|
const { log, T, R, D, fs, path, spawnSync, findRepoScript, SUPPORTED_CLIS, recordExperienceEvent } = shared;
|
|
4
4
|
|
|
5
|
-
function cmdDoctor(target) {
|
|
5
|
+
function cmdDoctor(target, options = {}) {
|
|
6
6
|
const ai = path.join(target, "ai");
|
|
7
7
|
if (!fs.existsSync(ai)) { log("No 0dai config found. Run: 0dai init"); return; }
|
|
8
8
|
let v = "?", stack = "generic";
|
|
@@ -108,6 +108,36 @@ function cmdDoctor(target) {
|
|
|
108
108
|
console.log(` ${mark.padEnd(22)} ${c.name}${hint}`);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
console.log("\n provider profiles:");
|
|
112
|
+
try {
|
|
113
|
+
const providerScript = findRepoScript(target, "provider_profiles.py");
|
|
114
|
+
if (!providerScript) {
|
|
115
|
+
console.log(` ${D}—${R2} unavailable in this environment`);
|
|
116
|
+
} else {
|
|
117
|
+
const pr = spawnSync(
|
|
118
|
+
"python3",
|
|
119
|
+
[providerScript, "status", "--target", target, "--json"],
|
|
120
|
+
{ stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 },
|
|
121
|
+
);
|
|
122
|
+
if (pr.stdout) {
|
|
123
|
+
const payload = JSON.parse(pr.stdout);
|
|
124
|
+
const bindings = payload.bindings && typeof payload.bindings === "object" ? payload.bindings : {};
|
|
125
|
+
const resolved = Object.entries(bindings).filter(([, profileId]) => Boolean(profileId));
|
|
126
|
+
if (!resolved.length) {
|
|
127
|
+
console.log(` ${D}—${R2} no provider profiles bound`);
|
|
128
|
+
} else {
|
|
129
|
+
for (const [agent, profileId] of resolved.sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
130
|
+
console.log(` ${G}ok${R2} ${agent} ${D}→ ${profileId}${R2}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
console.log(` ${D}—${R2} no provider profiles bound`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
console.log(` ${D}—${R2} unable to resolve provider profile status`);
|
|
139
|
+
}
|
|
140
|
+
|
|
111
141
|
// --- agent CLIs check ---
|
|
112
142
|
const { execFileSync: _ef2 } = require("child_process");
|
|
113
143
|
let updatesAvailable = 0;
|
|
@@ -172,23 +202,39 @@ function cmdDoctor(target) {
|
|
|
172
202
|
review_needed: warnings > 0,
|
|
173
203
|
},
|
|
174
204
|
});
|
|
175
|
-
if (errors) process.exitCode = 1;
|
|
205
|
+
if (errors && !options.suppressExitCode) process.exitCode = 1;
|
|
176
206
|
|
|
177
207
|
// Drift summary (lightweight — full report via --drift flag)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
if (!options.drift) {
|
|
209
|
+
try {
|
|
210
|
+
const ds = findRepoScript(target, "drift_detector.py");
|
|
211
|
+
if (ds) {
|
|
212
|
+
const dr = spawnSync("python3", [ds, "report", "--target", target],
|
|
213
|
+
{ stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
|
|
214
|
+
if (dr.stdout && dr.stdout.includes("MODIFIED")) {
|
|
215
|
+
const lines = dr.stdout.trim().split("\n");
|
|
216
|
+
const driftCount = lines.filter(l => l.includes("MODIFIED") || l.includes("CONTRADICTS")).length;
|
|
217
|
+
if (driftCount > 0) {
|
|
218
|
+
console.log(`\n config drift: ${driftCount} issue(s) — run: 0dai doctor --drift`);
|
|
219
|
+
}
|
|
188
220
|
}
|
|
189
221
|
}
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --fix: auto-repair by running sync
|
|
226
|
+
if (process.argv.includes("--fix") && errors) {
|
|
227
|
+
console.log(`\n ${G}auto-fix:${R2} running 0dai sync to regenerate missing files...`);
|
|
228
|
+
try {
|
|
229
|
+
const { cmdSync } = require("./init");
|
|
230
|
+
cmdSync(target, ["--quiet"]);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.log(` ${E}auto-fix failed:${R2} ${e.message}`);
|
|
233
|
+
console.log(` ${D}Run manually: 0dai sync${R2}`);
|
|
190
234
|
}
|
|
191
|
-
}
|
|
235
|
+
} else if (errors) {
|
|
236
|
+
console.log(`\n ${D}Tip: run 0dai doctor --fix to auto-repair${R2}`);
|
|
237
|
+
}
|
|
192
238
|
}
|
|
193
239
|
|
|
194
240
|
module.exports = { cmdDoctor };
|
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
|
-
writeFiles,
|
|
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",
|
|
@@ -173,7 +202,6 @@ async function cmdSync(target, args = []) {
|
|
|
173
202
|
const dryRun = args.includes("--dry-run");
|
|
174
203
|
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
175
204
|
const force = args.includes("--force");
|
|
176
|
-
const updateTemplates = args.includes("--update-templates");
|
|
177
205
|
|
|
178
206
|
// Quick local check: skip API if already at current version (unless dry-run or force)
|
|
179
207
|
let version = "unknown";
|
|
@@ -181,8 +209,6 @@ async function cmdSync(target, args = []) {
|
|
|
181
209
|
|
|
182
210
|
const metadata = collectMetadata(target);
|
|
183
211
|
const { manifestContents, clis } = metadata;
|
|
184
|
-
const authStatus = await ensureAuthenticated("sync");
|
|
185
|
-
const license = await ensureLicenseActivation();
|
|
186
212
|
let stack = "generic", agents = [];
|
|
187
213
|
try {
|
|
188
214
|
const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
|
|
@@ -190,6 +216,28 @@ async function cmdSync(target, args = []) {
|
|
|
190
216
|
agents = d.selected_agents || [];
|
|
191
217
|
} catch {}
|
|
192
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();
|
|
193
241
|
const boundProject = await bindProjectForCloud(target, metadata, identity);
|
|
194
242
|
|
|
195
243
|
// Collect current ai/ files
|
|
@@ -213,12 +261,11 @@ async function cmdSync(target, args = []) {
|
|
|
213
261
|
|
|
214
262
|
if (dryRun) log(`${D}dry-run: checking what sync would change...${R}`);
|
|
215
263
|
if (force && !dryRun) log(`${T}force mode: will overwrite native configs from ai/ source${R}`);
|
|
216
|
-
if (updateTemplates && !dryRun) log(`${T}template update mode: will refresh managed native configs from latest templates${R}`);
|
|
217
264
|
|
|
218
265
|
const result = await apiCall("/v1/sync", {
|
|
219
266
|
ai_version: version, stack, agents: agents.length ? agents : clis,
|
|
220
267
|
current_files: currentFiles, manifest_contents: manifestContents,
|
|
221
|
-
dry_run: dryRun, quiet, force,
|
|
268
|
+
dry_run: dryRun, quiet, force,
|
|
222
269
|
project_name: identity.project_name,
|
|
223
270
|
project_id: boundProject.project_id || identity.project_id,
|
|
224
271
|
remote_origin: identity.remote_origin,
|
|
@@ -241,9 +288,6 @@ async function cmdSync(target, args = []) {
|
|
|
241
288
|
} else {
|
|
242
289
|
log(`${D}dry-run: nothing to update${R}`);
|
|
243
290
|
}
|
|
244
|
-
if (result.template_update_available) {
|
|
245
|
-
console.log(` ${D}template update available: run 0dai sync --update-templates${R}`);
|
|
246
|
-
}
|
|
247
291
|
return;
|
|
248
292
|
}
|
|
249
293
|
const changedCount = Object.keys(updated).length;
|
|
@@ -258,18 +302,13 @@ async function cmdSync(target, args = []) {
|
|
|
258
302
|
log("already up to date");
|
|
259
303
|
}
|
|
260
304
|
|
|
261
|
-
if (result.template_update_available && !updateTemplates && !quiet) {
|
|
262
|
-
console.log(` ${D}Template update available: run 0dai sync --update-templates to refresh managed native configs${R}`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
305
|
// --force: also overwrite native configs (CLAUDE.md, AGENTS.md, etc.) from ai/ source
|
|
266
306
|
if (force && result.native_configs) {
|
|
307
|
+
const NATIVE_CONFIGS = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", "opencode.json", ".cursorrules", ".windsurfrules", ".aider.conf.yml"];
|
|
267
308
|
let overwritten = 0;
|
|
268
|
-
for (const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (content) {
|
|
272
|
-
fs.writeFileSync(targetPath, content, "utf8");
|
|
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");
|
|
273
312
|
overwritten++;
|
|
274
313
|
if (!quiet) console.log(` [force] ${name} overwritten from ai/ source`);
|
|
275
314
|
}
|
|
@@ -277,11 +316,6 @@ async function cmdSync(target, args = []) {
|
|
|
277
316
|
if (overwritten && !quiet) {
|
|
278
317
|
log(`force: ${overwritten} native config file(s) overwritten`);
|
|
279
318
|
}
|
|
280
|
-
} else if (updateTemplates && result.native_configs) {
|
|
281
|
-
writeManagedFiles(target, result.native_configs);
|
|
282
|
-
if (!quiet) {
|
|
283
|
-
log("template update: managed native configs refreshed");
|
|
284
|
-
}
|
|
285
319
|
}
|
|
286
320
|
|
|
287
321
|
// --force: update drift baseline hashes so drift clears after regeneration
|
|
@@ -311,7 +345,7 @@ async function cmdSync(target, args = []) {
|
|
|
311
345
|
|
|
312
346
|
// Update portfolio registry
|
|
313
347
|
registerProject(target, path.basename(target), stack);
|
|
314
|
-
await sendProjectHeartbeat(identity, result, {
|
|
348
|
+
await sendProjectHeartbeat(target, identity, result, {
|
|
315
349
|
project_id: boundProject.project_id || identity.project_id,
|
|
316
350
|
}).catch(() => {});
|
|
317
351
|
recordExperienceEvent(target, {
|
|
@@ -324,4 +358,58 @@ async function cmdSync(target, args = []) {
|
|
|
324
358
|
});
|
|
325
359
|
}
|
|
326
360
|
|
|
327
|
-
|
|
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,34 +28,35 @@ 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
|
|
|
@@ -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 };
|