@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/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
- console.log(` \u2713 ${authInfo}`);
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
- console.log(" Your project is set up. Try:");
159
- console.log(" 0dai swarm run --goal \"add auth\" (Pro)");
160
- console.log(" 0dai graph push (Pro)");
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 MANAGED_BEGIN = "<!-- 0dai:managed:begin -->";
52
- const MANAGED_END = "<!-- 0dai:managed:end -->";
53
- const LEGACY_MANAGED_BEGIN = "<!-- zerodayai:managed:begin -->";
54
- const LEGACY_MANAGED_END = "<!-- zerodayai:managed:end -->";
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: "timeout" }); });
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
- async function sendProjectHeartbeat(identity, result, extra = {}) {
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", ...extra,
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, mergeManagedMarkdown, writeFiles, writeManagedFiles, findRepoScript,
397
+ mergeSettingsJson, writeFiles, findRepoScript,
356
398
  // Version
357
399
  checkVersion,
358
400
  // Re-exports for convenience