@0dai-dev/cli 4.3.6 → 4.3.8
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 +12 -11
- package/bin/0dai.js +133 -33
- package/lib/ai/manifest/mcp-exposure-contract.json +121 -0
- package/lib/ai/meta/manifest/mcp-tool-tiers.json +435 -0
- package/lib/ai/registry/mcp-catalog.json +98 -0
- package/lib/commands/auth.js +2 -1
- package/lib/commands/compliance.js +1 -1
- package/lib/commands/doctor.js +707 -12
- package/lib/commands/experience.js +40 -5
- package/lib/commands/feedback.js +157 -15
- package/lib/commands/gh.js +26 -0
- package/lib/commands/graph.js +9 -4
- package/lib/commands/heatmap.js +1 -1
- package/lib/commands/init.js +298 -27
- package/lib/commands/mcp.js +111 -33
- package/lib/commands/models.js +138 -41
- package/lib/commands/play.js +20 -4
- package/lib/commands/provider.js +30 -59
- package/lib/commands/quota.js +1 -1
- package/lib/commands/receipt.js +1 -1
- package/lib/commands/run.js +14 -6
- package/lib/commands/runner.js +31 -1
- package/lib/commands/status.js +176 -11
- package/lib/commands/swarm.js +130 -12
- package/lib/commands/trust.js +1 -1
- package/lib/commands/update.js +184 -38
- package/lib/commands/usage.js +1 -1
- package/lib/commands/validate.js +32 -3
- package/lib/commands/vault.js +43 -8
- package/lib/python/__init__.py +0 -0
- package/lib/python/agent_quotas.py +525 -0
- package/lib/python/anomaly_alert.py +397 -0
- package/lib/python/anti_pattern_detector.py +799 -0
- package/lib/python/auth.py +443 -0
- package/lib/python/capi_profile_guard.py +477 -0
- package/lib/python/compliance_report.py +581 -0
- package/lib/python/drift_detector.py +388 -0
- package/lib/python/experience_pipeline.py +1130 -0
- package/lib/python/graph.py +19 -0
- package/lib/python/graph_core.py +293 -0
- package/lib/python/graph_io.py +179 -0
- package/lib/python/graph_legacy.py +2052 -0
- package/lib/python/graph_legacy_helpers.py +221 -0
- package/lib/python/graph_outcomes_core.py +85 -0
- package/lib/python/graph_queries.py +171 -0
- package/lib/python/graph_slice.py +198 -0
- package/lib/python/graph_slicer.py +576 -0
- package/lib/python/graph_slicer_cli.py +60 -0
- package/lib/python/graph_validation.py +64 -0
- package/lib/python/heatmap.py +943 -0
- package/lib/python/json_utils.py +193 -0
- package/lib/python/mcp_exposure_check.py +247 -0
- package/lib/python/model_router.py +1434 -0
- package/lib/python/project_manager.py +621 -0
- package/lib/python/provider_profiles.py +1618 -0
- package/lib/python/provider_registry.py +1211 -0
- package/lib/python/provider_registry_cli.py +125 -0
- package/lib/python/receipt_png.py +727 -0
- package/lib/python/structural_memory.py +325 -0
- package/lib/python/swarm_cost.py +177 -0
- package/lib/python/usage_ledger.py +569 -0
- package/lib/scripts/mcp_tier_config.py +240 -0
- package/lib/shared.js +96 -12
- package/lib/tui/index.mjs +35174 -0
- package/lib/utils/activation_telemetry.js +1 -4
- package/lib/utils/constants.js +7 -1
- package/lib/utils/identity.js +184 -0
- package/lib/utils/mcp-auth.js +81 -15
- package/lib/utils/plan.js +1 -1
- package/lib/vault/index.js +19 -3
- package/lib/vault/storage.js +21 -2
- package/lib/wizard.js +5 -2
- package/package.json +9 -3
- package/scripts/build-python-bundle.js +106 -0
- package/scripts/build-tui.js +14 -1
- package/scripts/harvest_experience.py +523 -0
- package/scripts/postinstall.js +15 -9
package/lib/commands/init.js
CHANGED
|
@@ -3,23 +3,38 @@ const crypto = require("crypto");
|
|
|
3
3
|
const shared = require("../shared");
|
|
4
4
|
const {
|
|
5
5
|
T, R, D, W, log,
|
|
6
|
-
fs, path,
|
|
6
|
+
fs, path, os,
|
|
7
7
|
VERSION, SUPPORTED_CLIS,
|
|
8
8
|
CONFIG_DIR, PROJECTS_FILE,
|
|
9
9
|
apiCall, makeEnsureAuthenticated, ensureLicenseActivation, loadAuthState,
|
|
10
|
-
collectMetadata,
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
collectMetadata, inferProjectName, detectStackHint,
|
|
11
|
+
buildProjectIdentity, registerProject, projectIdFor, hashManifestFiles,
|
|
12
|
+
loadProjectBinding, boundProjectIdentityFromBinding, validateProjectDisplayName,
|
|
13
|
+
writeProjectBinding,
|
|
14
|
+
writeFiles, missingLiveManifestDefaults, ensureLiveManifestDefaults, LIVE_MANIFEST_DEFAULTS,
|
|
15
|
+
sendProjectHeartbeat, recordExperienceEvent,
|
|
16
|
+
logFirstRunSuccess, checkVersion,
|
|
13
17
|
} = shared;
|
|
14
18
|
const { cmdAuthLogin, ensureAccountForActivation, parseActivationArgs } = require("./auth");
|
|
15
19
|
const { ensureGithubFlowPolicy, warnHooksPathDrift } = require("./gh");
|
|
16
20
|
const { renderFileMapDiff, confirmOrExit, shouldAutoYes } = require("../utils/diff-preview");
|
|
17
21
|
const { bootstrapMcp } = require("../utils/mcp-auth");
|
|
18
22
|
const { recordActivationInit } = require("../utils/activation_telemetry");
|
|
23
|
+
const { projectIdForExplicitRebindFromBinding, readProjectManifest } = require("../utils/identity");
|
|
19
24
|
|
|
20
25
|
const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
|
|
21
26
|
const SYNC_FULL_CONTENT_LIMIT = 10000;
|
|
22
27
|
const CLOUD_INIT_CHECKPOINT_REL = path.join(".0dai", "cloud-init-checkpoint.json");
|
|
28
|
+
const INIT_RUNTIME_GITIGNORE_LINES = [
|
|
29
|
+
".0dai/",
|
|
30
|
+
"ai/.backups/",
|
|
31
|
+
"ai/manifest/audit.jsonl",
|
|
32
|
+
"ai/manifest/wal.jsonl",
|
|
33
|
+
"ai/manifest/project_graph_usage.json",
|
|
34
|
+
"ai/experience/archive/",
|
|
35
|
+
"ai/experience/events/",
|
|
36
|
+
"ai/experience/warnings.json",
|
|
37
|
+
];
|
|
23
38
|
|
|
24
39
|
function hashFile(filePath) {
|
|
25
40
|
const hash = crypto.createHash("sha256");
|
|
@@ -123,6 +138,16 @@ function registerProjectOrExit(target, name, stack) {
|
|
|
123
138
|
}
|
|
124
139
|
}
|
|
125
140
|
|
|
141
|
+
function projectBindHostRegistryInfo(target) {
|
|
142
|
+
return {
|
|
143
|
+
scope: "host-local",
|
|
144
|
+
host: os.hostname(),
|
|
145
|
+
registry: PROJECTS_FILE,
|
|
146
|
+
project_path: path.resolve(target),
|
|
147
|
+
note: "0dai project bind writes this host's registry. A remote operator MCP server reads its own host registry and will not see this bind automatically.",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
126
151
|
function collectCurrentAiFiles(target) {
|
|
127
152
|
const currentFiles = {};
|
|
128
153
|
const aiDir = path.join(target, "ai");
|
|
@@ -225,6 +250,26 @@ function clearCloudInitCheckpoint(target) {
|
|
|
225
250
|
try { fs.unlinkSync(cloudInitCheckpointPath(target)); } catch {}
|
|
226
251
|
}
|
|
227
252
|
|
|
253
|
+
function ensureRuntimeGitignore(target) {
|
|
254
|
+
const gi = path.join(target, ".gitignore");
|
|
255
|
+
try {
|
|
256
|
+
const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
|
|
257
|
+
const existing = new Set(text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
258
|
+
const missing = INIT_RUNTIME_GITIGNORE_LINES.filter((line) => !existing.has(line));
|
|
259
|
+
if (!missing.length) return [];
|
|
260
|
+
|
|
261
|
+
let block = "";
|
|
262
|
+
if (text.length && !text.endsWith("\n")) block += "\n";
|
|
263
|
+
if (text.length) block += "\n";
|
|
264
|
+
block += "# 0dai runtime state\n";
|
|
265
|
+
block += `${missing.join("\n")}\n`;
|
|
266
|
+
fs.appendFileSync(gi, block, "utf8");
|
|
267
|
+
return missing;
|
|
268
|
+
} catch {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
228
273
|
function maybePauseCloudInitForAuth(target, args = [], options = {}) {
|
|
229
274
|
if (options.dryRun || options.localMode) return false;
|
|
230
275
|
if (_hasAuthToken()) return false;
|
|
@@ -232,6 +277,7 @@ function maybePauseCloudInitForAuth(target, args = [], options = {}) {
|
|
|
232
277
|
if (parsed.authCode) return false;
|
|
233
278
|
if (process.stdout.isTTY && process.stdin.isTTY) return false;
|
|
234
279
|
|
|
280
|
+
ensureRuntimeGitignore(target);
|
|
235
281
|
const checkpoint = writeCloudInitCheckpoint(target, {
|
|
236
282
|
args,
|
|
237
283
|
stage: "auth_required",
|
|
@@ -241,23 +287,89 @@ function maybePauseCloudInitForAuth(target, args = [], options = {}) {
|
|
|
241
287
|
console.log(` ${D}Checkpoint: ${CLOUD_INIT_CHECKPOINT_REL}${R}`);
|
|
242
288
|
console.log(` ${D}Run: ${checkpoint.next_command}${R}`);
|
|
243
289
|
console.log(` ${D}Then: ${checkpoint.resume_command}${R}`);
|
|
290
|
+
console.log(` ${D}Or run offline without signing in: 0dai init --local${R}`);
|
|
244
291
|
process.exit(1);
|
|
245
292
|
}
|
|
246
293
|
|
|
294
|
+
function parseProjectBindOptions(args = []) {
|
|
295
|
+
const options = { json: args.includes("--json"), name: "" };
|
|
296
|
+
for (let i = 0; i < args.length; i++) {
|
|
297
|
+
const arg = String(args[i] || "");
|
|
298
|
+
if (arg === "--name") {
|
|
299
|
+
const next = args[i + 1];
|
|
300
|
+
if (!next || String(next).startsWith("--")) {
|
|
301
|
+
return { ...options, error: "--name requires a project display name" };
|
|
302
|
+
}
|
|
303
|
+
const validated = validateProjectDisplayName(next);
|
|
304
|
+
if (!validated.ok) return { ...options, error: validated.error };
|
|
305
|
+
options.name = validated.name;
|
|
306
|
+
i++;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (arg.startsWith("--name=")) {
|
|
310
|
+
const validated = validateProjectDisplayName(arg.slice("--name=".length));
|
|
311
|
+
if (!validated.ok) return { ...options, error: validated.error };
|
|
312
|
+
options.name = validated.name;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return options;
|
|
316
|
+
}
|
|
317
|
+
|
|
247
318
|
async function cmdProjectBind(target, args = []) {
|
|
248
|
-
const
|
|
319
|
+
const bindOptions = parseProjectBindOptions(args);
|
|
320
|
+
if (bindOptions.error) {
|
|
321
|
+
log(`error: ${bindOptions.error}`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
const json = bindOptions.json;
|
|
249
325
|
const authStatus = await ensureAuthenticated("project bind");
|
|
250
326
|
const license = await ensureLicenseActivation();
|
|
251
327
|
const metadata = collectMetadata(target);
|
|
328
|
+
const existingBinding = loadProjectBinding(target);
|
|
329
|
+
const existingBoundIdentity = boundProjectIdentityFromBinding(target, existingBinding);
|
|
330
|
+
const explicitRebindProjectId = projectIdForExplicitRebindFromBinding(existingBinding);
|
|
252
331
|
const identity = buildProjectIdentity(target, metadata);
|
|
332
|
+
if (bindOptions.name) {
|
|
333
|
+
identity.project_name = bindOptions.name;
|
|
334
|
+
identity.project_id = projectIdFor(target, bindOptions.name, identity.remote_origin || path.resolve(target));
|
|
335
|
+
}
|
|
336
|
+
if (existingBoundIdentity && existingBoundIdentity.project_id) {
|
|
337
|
+
identity.project_id = existingBoundIdentity.project_id;
|
|
338
|
+
} else if (explicitRebindProjectId) {
|
|
339
|
+
identity.project_id = explicitRebindProjectId;
|
|
340
|
+
}
|
|
253
341
|
const boundProject = await bindProjectForCloud(target, metadata, identity);
|
|
342
|
+
const projectID = boundProject.project_id || identity.project_id;
|
|
343
|
+
const projectName = bindOptions.name || boundProject.name || identity.project_name;
|
|
344
|
+
identity.project_id = projectID;
|
|
345
|
+
identity.project_name = projectName;
|
|
254
346
|
const stack = boundProject.stack || identity.stack || "unknown";
|
|
255
|
-
|
|
256
|
-
|
|
347
|
+
const registryName = bindOptions.name || path.basename(target);
|
|
348
|
+
if (await guardRegistryDrift(target, registryName, args)) {
|
|
349
|
+
registerProjectOrExit(target, registryName, stack);
|
|
257
350
|
}
|
|
258
351
|
const heartbeat = await sendProjectHeartbeat(target, identity, { stack }, {
|
|
259
|
-
project_id:
|
|
352
|
+
project_id: projectID,
|
|
260
353
|
}).catch((err) => ({ error: err.message || String(err) }));
|
|
354
|
+
const savedBinding = writeProjectBinding(target, {
|
|
355
|
+
identity,
|
|
356
|
+
server_project: {
|
|
357
|
+
...boundProject,
|
|
358
|
+
project_id: projectID,
|
|
359
|
+
name: projectName,
|
|
360
|
+
stack,
|
|
361
|
+
binding_status: boundProject.binding_status || "bound",
|
|
362
|
+
},
|
|
363
|
+
project_id: projectID,
|
|
364
|
+
project_name: projectName,
|
|
365
|
+
stack,
|
|
366
|
+
binding_status: boundProject.binding_status || "bound",
|
|
367
|
+
binding_source: bindOptions.name ? "project bind --name" : "project bind",
|
|
368
|
+
account_email: authStatus.email || "",
|
|
369
|
+
account_plan: authStatus.plan || license.plan || "free",
|
|
370
|
+
activation_status: license.status || "active",
|
|
371
|
+
activation_id: license.activation_id || "",
|
|
372
|
+
});
|
|
261
373
|
const payload = {
|
|
262
374
|
ok: true,
|
|
263
375
|
account: {
|
|
@@ -266,12 +378,18 @@ async function cmdProjectBind(target, args = []) {
|
|
|
266
378
|
activation: license.status || "active",
|
|
267
379
|
},
|
|
268
380
|
project: {
|
|
269
|
-
project_id:
|
|
270
|
-
name:
|
|
381
|
+
project_id: projectID,
|
|
382
|
+
name: projectName,
|
|
271
383
|
stack,
|
|
272
384
|
binding_status: boundProject.binding_status || "bound",
|
|
273
385
|
},
|
|
274
386
|
heartbeat: !(heartbeat && heartbeat.error),
|
|
387
|
+
host_registry: projectBindHostRegistryInfo(target),
|
|
388
|
+
local_binding: {
|
|
389
|
+
target: savedBinding.target,
|
|
390
|
+
target_aliases: savedBinding.target_aliases || [],
|
|
391
|
+
updated_at: savedBinding.updated_at,
|
|
392
|
+
},
|
|
275
393
|
};
|
|
276
394
|
if (json) {
|
|
277
395
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -279,25 +397,125 @@ async function cmdProjectBind(target, args = []) {
|
|
|
279
397
|
log("project bound");
|
|
280
398
|
console.log(` account: ${payload.account.email || "unknown"} · plan: ${payload.account.plan} · activation: ${payload.account.activation}`);
|
|
281
399
|
console.log(` project: ${payload.project.project_id}`);
|
|
400
|
+
console.log(` registry: ${payload.host_registry.registry}`);
|
|
401
|
+
console.log(` ${D}${payload.host_registry.note} See #4084.${R}`);
|
|
282
402
|
if (!payload.heartbeat) console.log(` ${D}heartbeat deferred; run 0dai status to inspect local state${R}`);
|
|
283
403
|
}
|
|
284
404
|
return payload;
|
|
285
405
|
}
|
|
286
406
|
|
|
407
|
+
// buildLocalDryRunPreview — pure, offline preview for `0dai init --local --dry-run`.
|
|
408
|
+
// Detects the stack and renders the configs the local (offline) init path
|
|
409
|
+
// produces, entirely IN MEMORY. Makes ZERO auth/license/server/network calls:
|
|
410
|
+
// it only reads the target dir via collectMetadata + the local detection helpers
|
|
411
|
+
// (inferProjectName / detectStackHint), all of which run on-disk with no egress.
|
|
412
|
+
// Returns { projectName, stack, clis, files, agentTargets } where:
|
|
413
|
+
// files — { relPath: content } the offline path would write
|
|
414
|
+
// agentTargets — [{ cli, files: [...] }] the per-CLI config paths a full
|
|
415
|
+
// (authenticated) `0dai init` would manage for each detected CLI
|
|
416
|
+
function buildLocalDryRunPreview(target) {
|
|
417
|
+
const metadata = collectMetadata(target);
|
|
418
|
+
const { projectFiles, manifestContents, clis } = metadata;
|
|
419
|
+
const projectName = inferProjectName(target, manifestContents);
|
|
420
|
+
const stack = detectStackHint(projectFiles, manifestContents);
|
|
421
|
+
|
|
422
|
+
// Mirror the offline generator (lib/wizard.js stepGenerate) so the preview
|
|
423
|
+
// matches what `0dai init --local` writes without auth — no fabricated bodies.
|
|
424
|
+
const claudeMd = `# ${projectName}\n\nStack: ${stack}\nGenerated by 0dai (local mode).\n\n## Commands\n\nSee ai/manifest/ for full configuration.\n`;
|
|
425
|
+
const agentsMd = `# Agent Configuration\n\nProject: ${projectName}\nStack: ${stack}\n\nSee ai/manifest/ for configuration details.\n`;
|
|
426
|
+
const discovery = {
|
|
427
|
+
project_name: projectName,
|
|
428
|
+
stack,
|
|
429
|
+
detected_stack: [stack],
|
|
430
|
+
selected_agents: clis.length ? clis : SUPPORTED_CLIS.map((c) => c.name),
|
|
431
|
+
local: true,
|
|
432
|
+
};
|
|
433
|
+
const files = {
|
|
434
|
+
"CLAUDE.md": claudeMd,
|
|
435
|
+
"AGENTS.md": agentsMd,
|
|
436
|
+
"ai/manifest/discovery.json": JSON.stringify(discovery, null, 2) + "\n",
|
|
437
|
+
"ai/manifest/project.yaml": `plan: free\nname: ${projectName}\nstack: ${stack}\n`,
|
|
438
|
+
// Live manifest scaffolds the offline path writes via ensureLiveManifestDefaults
|
|
439
|
+
// (lib/wizard.js stepGenerate). Use the canonical bodies, not fabricated ones.
|
|
440
|
+
...LIVE_MANIFEST_DEFAULTS,
|
|
441
|
+
"ai/VERSION": `${VERSION}\n`,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// Per-CLI native config paths the full managed init would generate. We list
|
|
445
|
+
// paths (factual, from SUPPORTED_CLIS) rather than inventing offline bodies.
|
|
446
|
+
const agentTargets = SUPPORTED_CLIS
|
|
447
|
+
.filter((c) => Array.isArray(c.agentFiles) && c.agentFiles.length)
|
|
448
|
+
.map((c) => ({ cli: c.name, files: c.agentFiles }));
|
|
449
|
+
|
|
450
|
+
return { projectName, stack, clis, files, agentTargets };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// cmdInitLocalDryRun — offline `0dai init --local --dry-run`. Prints the
|
|
454
|
+
// preview to stdout and returns. Writes nothing to disk and makes no network
|
|
455
|
+
// calls. This is the only path that bypasses the auth/license preflight, and
|
|
456
|
+
// only when BOTH --local AND --dry-run are set (see cmdInit).
|
|
457
|
+
function cmdInitLocalDryRun(target) {
|
|
458
|
+
const { projectName, stack, clis, files, agentTargets } = buildLocalDryRunPreview(target);
|
|
459
|
+
log(`local dry-run: no account or network needed`);
|
|
460
|
+
console.log(` project: ${projectName}`);
|
|
461
|
+
console.log(` stack: ${stack}`);
|
|
462
|
+
console.log(` agent CLIs detected: ${clis.length ? clis.join(", ") : "none"}`);
|
|
463
|
+
console.log("");
|
|
464
|
+
const names = Object.keys(files);
|
|
465
|
+
log(`local dry-run: would write ${names.length} file(s) (nothing written):`);
|
|
466
|
+
for (const name of names) {
|
|
467
|
+
console.log(` ${D}+ ${name}${R}`);
|
|
468
|
+
}
|
|
469
|
+
console.log("");
|
|
470
|
+
log(`local dry-run: per-CLI native configs a full 0dai init would manage:`);
|
|
471
|
+
for (const target_ of agentTargets) {
|
|
472
|
+
console.log(` ${D}${target_.cli}: ${target_.files.join(", ")}${R}`);
|
|
473
|
+
}
|
|
474
|
+
console.log("");
|
|
475
|
+
for (const name of names) {
|
|
476
|
+
console.log(`${T}--- ${name} ---${R}`);
|
|
477
|
+
console.log(files[name].replace(/\n$/, ""));
|
|
478
|
+
console.log("");
|
|
479
|
+
}
|
|
480
|
+
console.log(` ${D}Sign in to write these for real: 0dai init --local${R}`);
|
|
481
|
+
return { projectName, stack, clis, files, agentTargets };
|
|
482
|
+
}
|
|
483
|
+
|
|
287
484
|
async function cmdInit(target, args = []) {
|
|
288
485
|
const dryRun = args.includes("--dry-run");
|
|
289
486
|
const minimal = args.includes("--minimal");
|
|
290
487
|
const noWizard = args.includes("--no-wizard");
|
|
291
488
|
const localMode = args.includes("--local");
|
|
292
489
|
|
|
490
|
+
// Offline preview: `0dai init --local --dry-run` renders configs in memory,
|
|
491
|
+
// prints them, and returns. Bypasses the auth/license/server preflight ONLY
|
|
492
|
+
// when BOTH flags are present (issue #4475). Plain --dry-run (server plan)
|
|
493
|
+
// and plain --local (wizard, writes) are unchanged.
|
|
494
|
+
if (localMode && dryRun) {
|
|
495
|
+
cmdInitLocalDryRun(target);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
293
499
|
if (fs.existsSync(path.join(target, "ai", "VERSION"))) {
|
|
294
500
|
const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
|
|
295
501
|
log(`ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
|
|
296
502
|
if (args.includes("--resume")) clearCloudInitCheckpoint(target);
|
|
297
|
-
if (!dryRun)
|
|
503
|
+
if (!dryRun) {
|
|
504
|
+
ensureRuntimeGitignore(target);
|
|
505
|
+
await runMcpBootstrap(target, args);
|
|
506
|
+
}
|
|
298
507
|
return;
|
|
299
508
|
}
|
|
300
509
|
|
|
510
|
+
if (!dryRun && !args.includes("--json")) {
|
|
511
|
+
await checkVersion({
|
|
512
|
+
force: true,
|
|
513
|
+
background: false,
|
|
514
|
+
timeoutMs: 1500,
|
|
515
|
+
initHint: true,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
301
519
|
// Pre-check: verify init quota before starting wizard (avoid 10 min wizard → "limit reached")
|
|
302
520
|
if (!dryRun) {
|
|
303
521
|
try {
|
|
@@ -319,6 +537,7 @@ async function cmdInit(target, args = []) {
|
|
|
319
537
|
if (isInteractive()) {
|
|
320
538
|
const result = await runWizard(target);
|
|
321
539
|
if (result.completed) {
|
|
540
|
+
ensureRuntimeGitignore(target);
|
|
322
541
|
await runMcpBootstrap(target, args);
|
|
323
542
|
try {
|
|
324
543
|
const ob = require("../onboarding");
|
|
@@ -335,6 +554,7 @@ async function cmdInit(target, args = []) {
|
|
|
335
554
|
const { runWizard } = require("../wizard");
|
|
336
555
|
const result = await runWizard(target, { forceLocal: true });
|
|
337
556
|
if (result.completed) {
|
|
557
|
+
ensureRuntimeGitignore(target);
|
|
338
558
|
await runMcpBootstrap(target, args);
|
|
339
559
|
try {
|
|
340
560
|
const ob = require("../onboarding");
|
|
@@ -357,7 +577,9 @@ async function cmdInit(target, args = []) {
|
|
|
357
577
|
const { projectFiles, manifestContents, clis } = metadata;
|
|
358
578
|
const authStatus = await ensureAccountForActivation("init", args);
|
|
359
579
|
const license = await ensureLicenseActivation();
|
|
580
|
+
const explicitRebindProjectId = projectIdForExplicitRebindFromBinding(loadProjectBinding(target));
|
|
360
581
|
const identity = buildProjectIdentity(target, metadata);
|
|
582
|
+
if (explicitRebindProjectId) identity.project_id = explicitRebindProjectId;
|
|
361
583
|
const boundProject = await bindProjectForCloud(target, metadata, identity);
|
|
362
584
|
if (dryRun) log(`${D}dry-run: would generate ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)${R}`);
|
|
363
585
|
if (spinner) spinner.start(`${dryRun ? "[dry-run] " : ""}Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
@@ -399,6 +621,7 @@ async function cmdInit(target, args = []) {
|
|
|
399
621
|
return;
|
|
400
622
|
}
|
|
401
623
|
writeFiles(target, result.files || {});
|
|
624
|
+
ensureLiveManifestDefaults(target);
|
|
402
625
|
const envManifest = normalizeEnvironmentManifest(target);
|
|
403
626
|
if (envManifest.changed) {
|
|
404
627
|
log("environment manifest target normalized: ai/manifest/environment.yaml");
|
|
@@ -410,12 +633,7 @@ async function cmdInit(target, args = []) {
|
|
|
410
633
|
fs.mkdirSync(path.dirname(versionFile), { recursive: true });
|
|
411
634
|
fs.writeFileSync(versionFile, VERSION + "\n", "utf8");
|
|
412
635
|
|
|
413
|
-
|
|
414
|
-
const gi = path.join(target, ".gitignore");
|
|
415
|
-
try {
|
|
416
|
-
const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
|
|
417
|
-
if (!text.includes(".0dai")) fs.appendFileSync(gi, "\n.0dai/\n");
|
|
418
|
-
} catch {}
|
|
636
|
+
ensureRuntimeGitignore(target);
|
|
419
637
|
|
|
420
638
|
const gitPolicy = ensureGithubFlowPolicy(target);
|
|
421
639
|
if (gitPolicy && gitPolicy.protection && gitPolicy.protection.skipped) {
|
|
@@ -501,6 +719,7 @@ async function runMcpBootstrap(target, args = []) {
|
|
|
501
719
|
const verbs = [];
|
|
502
720
|
if (result.config.added && result.config.added.length) verbs.push(`${result.config.added.length} server(s) added`);
|
|
503
721
|
if (result.config.updated && result.config.updated.length) verbs.push(`${result.config.updated.length} server(s) reset`);
|
|
722
|
+
if (result.config.removed && result.config.removed.length) verbs.push(`${result.config.removed.length} server(s) removed`);
|
|
504
723
|
if (result.config.changed) {
|
|
505
724
|
log(`MCP config ready (${verbs.join(", ") || "updated"}): .mcp.json`);
|
|
506
725
|
}
|
|
@@ -516,7 +735,7 @@ async function runMcpBootstrap(target, args = []) {
|
|
|
516
735
|
} else if (result.auth.status === "disabled") {
|
|
517
736
|
log("MCP cloud auth skipped (--no-mcp-auth)");
|
|
518
737
|
} else if (result.auth.status === "skipped") {
|
|
519
|
-
console.log(` ${D}MCP cloud auth skipped. In Claude Code, run /mcp Authenticate for
|
|
738
|
+
console.log(` ${D}MCP cloud auth skipped. In Claude Code, run /mcp Authenticate for claude_ai_0dai${R}`);
|
|
520
739
|
}
|
|
521
740
|
}
|
|
522
741
|
for (const warning of result.warnings || []) log(`warn: ${warning}`);
|
|
@@ -620,10 +839,33 @@ function runDocLinkCheck(target, args = [], options = {}) {
|
|
|
620
839
|
return { ran: true, status: 0, strict, broken: false };
|
|
621
840
|
}
|
|
622
841
|
|
|
842
|
+
function printProtectedSyncUpdates(items, { quiet = false } = {}) {
|
|
843
|
+
if (quiet || !Array.isArray(items) || items.length === 0) return;
|
|
844
|
+
log(`${W}sync protected ${items.length} project-owned update(s)${R}`);
|
|
845
|
+
for (const item of items) {
|
|
846
|
+
const rel = item && item.path ? String(item.path) : "(unknown)";
|
|
847
|
+
const reason = item && item.reason ? String(item.reason) : "protected by default";
|
|
848
|
+
console.log(` ${D}! ${rel} — ${reason}${R}`);
|
|
849
|
+
}
|
|
850
|
+
console.log(` ${D}Pass --force-template-reset to apply these reset-style updates.${R}`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function isGenericStackName(value) {
|
|
854
|
+
return ["", "generic", "unknown", "auto"].includes(String(value || "").trim().toLowerCase());
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function chooseSyncStack(target, discoveryStack) {
|
|
858
|
+
const manifestStack = readProjectManifest(target).stack || "";
|
|
859
|
+
if (!isGenericStackName(manifestStack)) return manifestStack;
|
|
860
|
+
return discoveryStack || manifestStack || "generic";
|
|
861
|
+
}
|
|
862
|
+
|
|
623
863
|
async function cmdSync(target, args = []) {
|
|
624
|
-
const
|
|
625
|
-
const
|
|
864
|
+
const check = args.includes("--check");
|
|
865
|
+
const dryRun = args.includes("--dry-run") || check;
|
|
866
|
+
const quiet = args.includes("--quiet") || args.includes("-q") || check;
|
|
626
867
|
const force = args.includes("--force");
|
|
868
|
+
const forceTemplateReset = args.includes("--force-template-reset");
|
|
627
869
|
|
|
628
870
|
// Quick local check: skip API if already at current version (unless dry-run or force)
|
|
629
871
|
let version = "unknown";
|
|
@@ -631,12 +873,13 @@ async function cmdSync(target, args = []) {
|
|
|
631
873
|
|
|
632
874
|
const metadata = collectMetadata(target);
|
|
633
875
|
const { manifestContents, clis } = metadata;
|
|
634
|
-
let
|
|
876
|
+
let discoveryStack = "generic", agents = [];
|
|
635
877
|
try {
|
|
636
878
|
const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
|
|
637
|
-
|
|
879
|
+
discoveryStack = d.stack || "generic";
|
|
638
880
|
agents = d.selected_agents || [];
|
|
639
881
|
} catch {}
|
|
882
|
+
const stack = chooseSyncStack(target, discoveryStack);
|
|
640
883
|
const identity = buildProjectIdentity(target, metadata, stack);
|
|
641
884
|
|
|
642
885
|
if (dryRun) {
|
|
@@ -677,6 +920,7 @@ async function cmdSync(target, args = []) {
|
|
|
677
920
|
// server needs no raw manifest content — mirrors the /v1/init fix from #4030.
|
|
678
921
|
manifest_files: hashManifestFiles(manifestContents),
|
|
679
922
|
dry_run: dryRun, quiet, force,
|
|
923
|
+
force_template_reset: forceTemplateReset,
|
|
680
924
|
project_name: identity.project_name,
|
|
681
925
|
project_id: boundProject.project_id || identity.project_id,
|
|
682
926
|
remote_origin: identity.remote_origin,
|
|
@@ -690,7 +934,11 @@ async function cmdSync(target, args = []) {
|
|
|
690
934
|
process.exit(1);
|
|
691
935
|
}
|
|
692
936
|
|
|
693
|
-
const updated =
|
|
937
|
+
const updated = {
|
|
938
|
+
...(result.files_updated || {}),
|
|
939
|
+
...missingLiveManifestDefaults(target, result.files_updated || {}),
|
|
940
|
+
};
|
|
941
|
+
const protectedUpdates = Array.isArray(result.protected_updates) ? result.protected_updates : [];
|
|
694
942
|
if (dryRun) {
|
|
695
943
|
const files = Object.keys(updated);
|
|
696
944
|
if (files.length) {
|
|
@@ -706,8 +954,10 @@ async function cmdSync(target, args = []) {
|
|
|
706
954
|
} else {
|
|
707
955
|
log(`${D}dry-run: nothing to update${R}`);
|
|
708
956
|
}
|
|
957
|
+
printProtectedSyncUpdates(protectedUpdates, { quiet });
|
|
709
958
|
return;
|
|
710
959
|
}
|
|
960
|
+
printProtectedSyncUpdates(protectedUpdates, { quiet });
|
|
711
961
|
const changedCount = Object.keys(updated).length;
|
|
712
962
|
if (changedCount) {
|
|
713
963
|
if (!quiet && !shouldAutoYes(args)) {
|
|
@@ -772,12 +1022,28 @@ async function cmdSync(target, args = []) {
|
|
|
772
1022
|
if (force && result.native_configs) {
|
|
773
1023
|
const NATIVE_CONFIGS = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", "opencode.json", ".cursorrules", ".windsurfrules", ".aider.conf.yml"];
|
|
774
1024
|
let overwritten = 0;
|
|
1025
|
+
let backupDir = null;
|
|
775
1026
|
for (const name of NATIVE_CONFIGS) {
|
|
776
|
-
if (result.native_configs[name])
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1027
|
+
if (!result.native_configs[name]) continue;
|
|
1028
|
+
const dest = path.join(target, name);
|
|
1029
|
+
const next = result.native_configs[name];
|
|
1030
|
+
// Back up a pre-existing, differing native config before overwriting, so
|
|
1031
|
+
// a hand-edited CLAUDE.md/AGENTS.md/… is recoverable from ai/.backups
|
|
1032
|
+
// (sync --force used to clobber these with no backup, #4363).
|
|
1033
|
+
if (fs.existsSync(dest)) {
|
|
1034
|
+
const existing = fs.readFileSync(dest, "utf8");
|
|
1035
|
+
if (existing === next) continue;
|
|
1036
|
+
if (!backupDir) {
|
|
1037
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1038
|
+
backupDir = path.join(target, "ai", ".backups", timestamp);
|
|
1039
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
1040
|
+
}
|
|
1041
|
+
fs.writeFileSync(path.join(backupDir, name), existing, "utf8");
|
|
1042
|
+
if (!quiet) console.log(` [force] backed up ${name} to ${path.relative(target, path.join(backupDir, name))}`);
|
|
780
1043
|
}
|
|
1044
|
+
fs.writeFileSync(dest, next, "utf8");
|
|
1045
|
+
overwritten++;
|
|
1046
|
+
if (!quiet) console.log(` [force] ${name} overwritten from ai/ source`);
|
|
781
1047
|
}
|
|
782
1048
|
if (overwritten && !quiet) {
|
|
783
1049
|
log(`force: ${overwritten} native config file(s) overwritten`);
|
|
@@ -834,6 +1100,8 @@ function buildLocalSyncPreview(target, { version, stack, cliVersion }) {
|
|
|
834
1100
|
"ai/manifest/project.yaml",
|
|
835
1101
|
"ai/manifest/commands.yaml",
|
|
836
1102
|
"ai/manifest/discovery.json",
|
|
1103
|
+
"ai/manifest/current_state.json",
|
|
1104
|
+
"ai/manifest/current_task.json",
|
|
837
1105
|
];
|
|
838
1106
|
for (const rel of expectedAiFiles) {
|
|
839
1107
|
if (!fs.existsSync(path.join(target, rel))) changes.push(`${rel} (missing)`);
|
|
@@ -885,6 +1153,8 @@ module.exports = {
|
|
|
885
1153
|
cmdInit,
|
|
886
1154
|
cmdSync,
|
|
887
1155
|
cmdProjectBind,
|
|
1156
|
+
buildLocalDryRunPreview,
|
|
1157
|
+
cmdInitLocalDryRun,
|
|
888
1158
|
buildLocalSyncPreview,
|
|
889
1159
|
runMcpBootstrap,
|
|
890
1160
|
bindProjectForCloud,
|
|
@@ -892,6 +1162,7 @@ module.exports = {
|
|
|
892
1162
|
writeCloudInitCheckpoint,
|
|
893
1163
|
clearCloudInitCheckpoint,
|
|
894
1164
|
buildCloudInitResumeCommand,
|
|
1165
|
+
ensureRuntimeGitignore,
|
|
895
1166
|
collectCurrentAiFiles,
|
|
896
1167
|
hashFile,
|
|
897
1168
|
detectRegistryDrift,
|