@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/lib/onboarding.js
CHANGED
|
@@ -104,20 +104,28 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
|
|
|
104
104
|
|
|
105
105
|
// Step 1: Auth
|
|
106
106
|
console.log(`\n [1/${steps}] Checking authentication...`);
|
|
107
|
+
let signedIn = false;
|
|
107
108
|
let authInfo = "not signed in";
|
|
108
109
|
try {
|
|
109
110
|
const auth = JSON.parse(fs.readFileSync(path.join(require("os").homedir(), ".0dai", "auth.json"), "utf8"));
|
|
110
111
|
if (auth.access_token) {
|
|
112
|
+
signedIn = true;
|
|
111
113
|
authInfo = `signed in (${auth.plan || "free"} plan)`;
|
|
112
114
|
}
|
|
113
115
|
} catch {}
|
|
114
|
-
|
|
116
|
+
if (signedIn) {
|
|
117
|
+
console.log(` \u2713 ${authInfo}`);
|
|
118
|
+
} else {
|
|
119
|
+
console.log(` \u26a0 ${authInfo} \u2192 run 0dai auth login`);
|
|
120
|
+
}
|
|
115
121
|
|
|
116
122
|
// Step 2: Init
|
|
117
123
|
console.log(` [2/${steps}] Checking project config...`);
|
|
118
124
|
const aiExists = fs.existsSync(path.join(target, "ai", "VERSION"));
|
|
119
125
|
if (aiExists) {
|
|
120
126
|
console.log(" \u2713 ai/ layer found");
|
|
127
|
+
} else if (!signedIn) {
|
|
128
|
+
console.log(" \u26a0 skipped \u2192 sign in before first init");
|
|
121
129
|
} else {
|
|
122
130
|
console.log(" initializing...");
|
|
123
131
|
try {
|
|
@@ -132,9 +140,9 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
|
|
|
132
140
|
|
|
133
141
|
// Step 3: Doctor
|
|
134
142
|
console.log(` [3/${steps}] Running health check...`);
|
|
135
|
-
if (fs.existsSync(path.join(target, "ai"))) {
|
|
143
|
+
if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
|
|
136
144
|
try {
|
|
137
|
-
cmdDoctor(target);
|
|
145
|
+
cmdDoctor(target, { suppressExitCode: true });
|
|
138
146
|
} catch {}
|
|
139
147
|
console.log(" \u2713 health check complete");
|
|
140
148
|
} else {
|
|
@@ -143,7 +151,7 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
|
|
|
143
151
|
|
|
144
152
|
// Step 4: Status
|
|
145
153
|
console.log(` [4/${steps}] Project status:`);
|
|
146
|
-
if (fs.existsSync(path.join(target, "ai"))) {
|
|
154
|
+
if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
|
|
147
155
|
try {
|
|
148
156
|
cmdStatus(target);
|
|
149
157
|
} catch {}
|
|
@@ -155,9 +163,15 @@ async function cmdQuickstart(target, { cmdDoctor, cmdStatus, cmdInit, log, ensur
|
|
|
155
163
|
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
156
164
|
console.log(` [5/${steps}] Ready! (${elapsed}s)`);
|
|
157
165
|
console.log("");
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
if (!signedIn && !aiExists) {
|
|
167
|
+
console.log(" Next:");
|
|
168
|
+
console.log(" 0dai auth login Sign in to unlock init and sync");
|
|
169
|
+
console.log(" 0dai quickstart Re-run after sign-in");
|
|
170
|
+
} else {
|
|
171
|
+
console.log(" Your project is set up. Try:");
|
|
172
|
+
console.log(" 0dai swarm run --goal \"add auth\" (Pro)");
|
|
173
|
+
console.log(" 0dai graph push (Pro)");
|
|
174
|
+
}
|
|
161
175
|
console.log("");
|
|
162
176
|
}
|
|
163
177
|
|
package/lib/shared.js
CHANGED
|
@@ -11,6 +11,7 @@ const http = require("http");
|
|
|
11
11
|
const fs = require("fs");
|
|
12
12
|
const path = require("path");
|
|
13
13
|
const os = require("os");
|
|
14
|
+
const crypto = require("crypto");
|
|
14
15
|
const { spawnSync } = require("child_process");
|
|
15
16
|
|
|
16
17
|
const VERSION = require("../package.json").version;
|
|
@@ -48,10 +49,15 @@ const CONFIG_DIR = path.join(os.homedir(), ".0dai");
|
|
|
48
49
|
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
49
50
|
const VERSION_CHECK_FILE = path.join(CONFIG_DIR, ".version_check");
|
|
50
51
|
const PROJECTS_FILE = path.join(CONFIG_DIR, "projects.json");
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const DRIFT_TRACKED_CONFIGS = [
|
|
53
|
+
"CLAUDE.md",
|
|
54
|
+
"AGENTS.md",
|
|
55
|
+
"GEMINI.md",
|
|
56
|
+
"opencode.json",
|
|
57
|
+
".cursorrules",
|
|
58
|
+
".windsurfrules",
|
|
59
|
+
".aider.conf.yml",
|
|
60
|
+
];
|
|
55
61
|
|
|
56
62
|
// --- API ---
|
|
57
63
|
function apiCall(endpoint, data) {
|
|
@@ -88,7 +94,7 @@ function apiCall(endpoint, data) {
|
|
|
88
94
|
});
|
|
89
95
|
});
|
|
90
96
|
req.on("error", (e) => resolve({ error: `${e.message}. Is ${API_URL} reachable?` }));
|
|
91
|
-
req.on("timeout", () => { req.destroy(); resolve({ error: "
|
|
97
|
+
req.on("timeout", () => { req.destroy(); resolve({ error: "request timed out after 60s. Check your internet connection or try again." }); });
|
|
92
98
|
if (body) req.write(body);
|
|
93
99
|
req.end();
|
|
94
100
|
});
|
|
@@ -164,14 +170,123 @@ async function ensureLicenseActivation() {
|
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
// --- Project Heartbeat ---
|
|
167
|
-
|
|
173
|
+
function _hashFileSha256(filePath) {
|
|
174
|
+
const buf = fs.readFileSync(filePath);
|
|
175
|
+
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function computeProjectDriftSummary(target) {
|
|
179
|
+
const hashesPath = path.join(target, "ai", "manifest", "config_hashes.json");
|
|
180
|
+
if (!fs.existsSync(hashesPath)) return null;
|
|
181
|
+
let hashes;
|
|
182
|
+
try {
|
|
183
|
+
hashes = JSON.parse(fs.readFileSync(hashesPath, "utf8"));
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const findings = [];
|
|
188
|
+
let totalConfigs = 0;
|
|
189
|
+
for (const name of DRIFT_TRACKED_CONFIGS) {
|
|
190
|
+
const filePath = path.join(target, name);
|
|
191
|
+
const exists = fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
|
192
|
+
const recorded = hashes[name];
|
|
193
|
+
if (exists) totalConfigs += 1;
|
|
194
|
+
if (recorded && !exists) {
|
|
195
|
+
findings.push({ config: name, type: "missing", severity: "warning" });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!recorded && exists) {
|
|
199
|
+
findings.push({ config: name, type: "extra", severity: "info" });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (recorded && exists) {
|
|
203
|
+
try {
|
|
204
|
+
const currentHash = _hashFileSha256(filePath);
|
|
205
|
+
if (currentHash !== String(recorded.hash || "")) {
|
|
206
|
+
findings.push({ config: name, type: "modified", severity: "warning" });
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
findings.push({ config: name, type: "unreadable", severity: "warning" });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const driftedCount = findings.filter((f) => f.type === "modified" || f.type === "missing").length;
|
|
214
|
+
return {
|
|
215
|
+
available: true,
|
|
216
|
+
clean: findings.length === 0,
|
|
217
|
+
drifted_count: driftedCount,
|
|
218
|
+
total_configs: totalConfigs,
|
|
219
|
+
findings,
|
|
220
|
+
updated_at: new Date().toISOString(),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function sendProjectHeartbeat(target, identity, result, extra = {}) {
|
|
225
|
+
const drift = computeProjectDriftSummary(target);
|
|
168
226
|
return apiCall("/v1/projects/heartbeat", {
|
|
169
227
|
project_id: identity.project_id, stack: result.stack || identity.stack || "unknown",
|
|
170
228
|
cli_version: VERSION, activation_status: "active", binding_status: "bound",
|
|
171
|
-
runtime_sessions: 0, swarm_active: 0, swarm_done: 0, channel: "npm",
|
|
229
|
+
runtime_sessions: 0, swarm_active: 0, swarm_done: 0, channel: "npm",
|
|
230
|
+
...(drift ? { drift } : {}),
|
|
231
|
+
...extra,
|
|
172
232
|
});
|
|
173
233
|
}
|
|
174
234
|
|
|
235
|
+
// --- First-run proof gate (issue #342) ---
|
|
236
|
+
//
|
|
237
|
+
// Idempotently appends a `first_run.success` event to ai/manifest/audit.jsonl
|
|
238
|
+
// when all 4 activation gates have passed. Matches scripts/audit.py shape.
|
|
239
|
+
// Returns { fired: boolean, elapsedS: number|null }.
|
|
240
|
+
function logFirstRunSuccess(target, gates) {
|
|
241
|
+
try {
|
|
242
|
+
const auditPath = path.join(target, "ai", "manifest", "audit.jsonl");
|
|
243
|
+
if (!fs.existsSync(path.dirname(auditPath))) {
|
|
244
|
+
fs.mkdirSync(path.dirname(auditPath), { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Idempotency: skip if a first_run.success event already exists for this project.
|
|
248
|
+
if (fs.existsSync(auditPath)) {
|
|
249
|
+
const existing = fs.readFileSync(auditPath, "utf8");
|
|
250
|
+
if (existing.includes('"action":"first_run.success"') ||
|
|
251
|
+
existing.includes('"action": "first_run.success"')) {
|
|
252
|
+
return { fired: false, elapsedS: null };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Compute elapsed_s from the local first-run marker.
|
|
257
|
+
let elapsedS = null;
|
|
258
|
+
try {
|
|
259
|
+
const ob = require("./onboarding");
|
|
260
|
+
ob.trackFirstInit(target);
|
|
261
|
+
elapsedS = ob.getTimeToInit(target);
|
|
262
|
+
} catch {}
|
|
263
|
+
|
|
264
|
+
// Read current ai/VERSION for the entry's ai_version field.
|
|
265
|
+
let aiVersion = null;
|
|
266
|
+
try {
|
|
267
|
+
const vf = path.join(target, "ai", "VERSION");
|
|
268
|
+
if (fs.existsSync(vf)) aiVersion = fs.readFileSync(vf, "utf8").trim();
|
|
269
|
+
} catch {}
|
|
270
|
+
|
|
271
|
+
const entry = {
|
|
272
|
+
timestamp: new Date().toISOString(),
|
|
273
|
+
action: "first_run.success",
|
|
274
|
+
actor: "cli",
|
|
275
|
+
user: process.env.USER || process.env.USERNAME || "unknown",
|
|
276
|
+
ai_version: aiVersion,
|
|
277
|
+
details: JSON.stringify({
|
|
278
|
+
elapsed_s: elapsedS,
|
|
279
|
+
gates: gates || {},
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
fs.appendFileSync(auditPath, JSON.stringify(entry) + "\n", "utf8");
|
|
284
|
+
return { fired: true, elapsedS };
|
|
285
|
+
} catch {
|
|
286
|
+
return { fired: false, elapsedS: null };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
175
290
|
// --- File Writing ---
|
|
176
291
|
function mergeSettingsJson(existing, incoming) {
|
|
177
292
|
try {
|
|
@@ -184,31 +299,6 @@ function mergeSettingsJson(existing, incoming) {
|
|
|
184
299
|
} catch { return incoming; }
|
|
185
300
|
}
|
|
186
301
|
|
|
187
|
-
function mergeManagedMarkdown(existing, incoming) {
|
|
188
|
-
let src = incoming;
|
|
189
|
-
if (src.startsWith("# managed: true")) {
|
|
190
|
-
src = src.split("\n").slice(1).join("\n").trimStart();
|
|
191
|
-
}
|
|
192
|
-
const managedBody = `${MANAGED_BEGIN}\n${src.trim()}\n${MANAGED_END}\n`;
|
|
193
|
-
if (existing.includes(MANAGED_BEGIN) && existing.includes(MANAGED_END)) {
|
|
194
|
-
const start = existing.indexOf(MANAGED_BEGIN);
|
|
195
|
-
const finish = existing.indexOf(MANAGED_END) + MANAGED_END.length;
|
|
196
|
-
return existing.slice(0, start) + managedBody + existing.slice(finish);
|
|
197
|
-
}
|
|
198
|
-
if (existing.includes(LEGACY_MANAGED_BEGIN) && existing.includes(LEGACY_MANAGED_END)) {
|
|
199
|
-
const finish = existing.indexOf(LEGACY_MANAGED_END) + LEGACY_MANAGED_END.length;
|
|
200
|
-
const rest = existing.slice(finish).trimStart();
|
|
201
|
-
return rest ? `${managedBody}\n${rest}` : managedBody;
|
|
202
|
-
}
|
|
203
|
-
return `${managedBody}\n${existing}`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function contentLooksManaged(existing) {
|
|
207
|
-
return existing.includes("managed: true") || existing.includes('"managed": true') ||
|
|
208
|
-
(existing.includes(MANAGED_BEGIN) && existing.includes(MANAGED_END)) ||
|
|
209
|
-
(existing.includes(LEGACY_MANAGED_BEGIN) && existing.includes(LEGACY_MANAGED_END));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
302
|
function writeFiles(target, files) {
|
|
213
303
|
let created = 0, updated = 0, unchanged = 0, merged = 0, skipped = 0;
|
|
214
304
|
const targetResolved = path.resolve(target);
|
|
@@ -241,54 +331,6 @@ function writeFiles(target, files) {
|
|
|
241
331
|
return created + updated;
|
|
242
332
|
}
|
|
243
333
|
|
|
244
|
-
function writeManagedFiles(target, files) {
|
|
245
|
-
let created = 0, updated = 0, unchanged = 0, merged = 0, staged = 0, skipped = 0;
|
|
246
|
-
const targetResolved = path.resolve(target);
|
|
247
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
248
|
-
if (typeof rel !== "string" || !rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
|
|
249
|
-
skipped++; continue;
|
|
250
|
-
}
|
|
251
|
-
const p = path.resolve(targetResolved, rel);
|
|
252
|
-
if (!p.startsWith(targetResolved + path.sep) && p !== targetResolved) { skipped++; continue; }
|
|
253
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
254
|
-
if (!fs.existsSync(p)) {
|
|
255
|
-
fs.writeFileSync(p, content, "utf8");
|
|
256
|
-
created++;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const existing = fs.readFileSync(p, "utf8");
|
|
260
|
-
if (existing === content) {
|
|
261
|
-
unchanged++;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (rel.endsWith("settings.json")) {
|
|
266
|
-
fs.writeFileSync(p, mergeSettingsJson(existing, content), "utf8");
|
|
267
|
-
merged++;
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
if (rel === "AGENTS.md" || rel.endsWith("/CLAUDE.md")) {
|
|
271
|
-
fs.writeFileSync(p, mergeManagedMarkdown(existing, content), "utf8");
|
|
272
|
-
merged++;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (!contentLooksManaged(existing)) {
|
|
276
|
-
fs.writeFileSync(`${p}.generated`, content, "utf8");
|
|
277
|
-
staged++;
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
fs.writeFileSync(p, content, "utf8");
|
|
282
|
-
updated++;
|
|
283
|
-
}
|
|
284
|
-
const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
|
|
285
|
-
if (merged) parts.push(`${merged} merged`);
|
|
286
|
-
if (staged) parts.push(`${staged} staged`);
|
|
287
|
-
if (skipped) parts.push(`${skipped} skipped (unsafe path)`);
|
|
288
|
-
log(parts.join(", "));
|
|
289
|
-
return created + updated + merged;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
334
|
// --- Repo Script Lookup ---
|
|
293
335
|
function findRepoScript(target, scriptName) {
|
|
294
336
|
const candidates = [
|
|
@@ -350,9 +392,9 @@ module.exports = {
|
|
|
350
392
|
// Plan / Tier
|
|
351
393
|
_detectPlanLocal, requirePlan, getSwarmQuotaLocal,
|
|
352
394
|
// Project
|
|
353
|
-
sendProjectHeartbeat, recordExperienceEvent,
|
|
395
|
+
sendProjectHeartbeat, recordExperienceEvent, logFirstRunSuccess,
|
|
354
396
|
// Files
|
|
355
|
-
mergeSettingsJson,
|
|
397
|
+
mergeSettingsJson, writeFiles, findRepoScript,
|
|
356
398
|
// Version
|
|
357
399
|
checkVersion,
|
|
358
400
|
// Re-exports for convenience
|