@0dai-dev/cli 4.3.5 → 4.3.7
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 +214 -40
- 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 +55 -1
- package/lib/commands/compliance.js +1 -1
- package/lib/commands/detect.js +10 -4
- package/lib/commands/doctor.js +545 -26
- package/lib/commands/experience.js +40 -5
- package/lib/commands/export.js +73 -0
- 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 +222 -30
- package/lib/commands/mcp.js +129 -21
- package/lib/commands/models.js +138 -41
- 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 +18 -7
- package/lib/commands/runner.js +31 -1
- package/lib/commands/status.js +44 -11
- package/lib/commands/swarm.js +130 -12
- package/lib/commands/trust.js +286 -0
- 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 +46 -9
- 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 +934 -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 +97 -14
- package/lib/tui/index.mjs +35174 -0
- package/lib/utils/activation_telemetry.js +230 -11
- package/lib/utils/constants.js +7 -1
- package/lib/utils/export-bundler.js +285 -0
- package/lib/utils/identity.js +198 -1
- 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,37 @@ 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, buildProjectIdentity, registerProject,
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
collectMetadata, buildProjectIdentity, registerProject, projectIdFor, hashManifestFiles,
|
|
11
|
+
loadProjectBinding, boundProjectIdentityFromBinding, validateProjectDisplayName,
|
|
12
|
+
writeProjectBinding,
|
|
13
|
+
writeFiles, missingLiveManifestDefaults, ensureLiveManifestDefaults,
|
|
14
|
+
sendProjectHeartbeat, recordExperienceEvent,
|
|
15
|
+
logFirstRunSuccess, checkVersion,
|
|
13
16
|
} = shared;
|
|
14
17
|
const { cmdAuthLogin, ensureAccountForActivation, parseActivationArgs } = require("./auth");
|
|
15
18
|
const { ensureGithubFlowPolicy, warnHooksPathDrift } = require("./gh");
|
|
16
19
|
const { renderFileMapDiff, confirmOrExit, shouldAutoYes } = require("../utils/diff-preview");
|
|
17
20
|
const { bootstrapMcp } = require("../utils/mcp-auth");
|
|
18
21
|
const { recordActivationInit } = require("../utils/activation_telemetry");
|
|
22
|
+
const { projectIdForExplicitRebindFromBinding, readProjectManifest } = require("../utils/identity");
|
|
19
23
|
|
|
20
24
|
const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
|
|
21
25
|
const SYNC_FULL_CONTENT_LIMIT = 10000;
|
|
22
26
|
const CLOUD_INIT_CHECKPOINT_REL = path.join(".0dai", "cloud-init-checkpoint.json");
|
|
27
|
+
const INIT_RUNTIME_GITIGNORE_LINES = [
|
|
28
|
+
".0dai/",
|
|
29
|
+
"ai/.backups/",
|
|
30
|
+
"ai/manifest/audit.jsonl",
|
|
31
|
+
"ai/manifest/wal.jsonl",
|
|
32
|
+
"ai/manifest/project_graph_usage.json",
|
|
33
|
+
"ai/experience/archive/",
|
|
34
|
+
"ai/experience/events/",
|
|
35
|
+
"ai/experience/warnings.json",
|
|
36
|
+
];
|
|
23
37
|
|
|
24
38
|
function hashFile(filePath) {
|
|
25
39
|
const hash = crypto.createHash("sha256");
|
|
@@ -123,6 +137,16 @@ function registerProjectOrExit(target, name, stack) {
|
|
|
123
137
|
}
|
|
124
138
|
}
|
|
125
139
|
|
|
140
|
+
function projectBindHostRegistryInfo(target) {
|
|
141
|
+
return {
|
|
142
|
+
scope: "host-local",
|
|
143
|
+
host: os.hostname(),
|
|
144
|
+
registry: PROJECTS_FILE,
|
|
145
|
+
project_path: path.resolve(target),
|
|
146
|
+
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.",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
126
150
|
function collectCurrentAiFiles(target) {
|
|
127
151
|
const currentFiles = {};
|
|
128
152
|
const aiDir = path.join(target, "ai");
|
|
@@ -225,6 +249,26 @@ function clearCloudInitCheckpoint(target) {
|
|
|
225
249
|
try { fs.unlinkSync(cloudInitCheckpointPath(target)); } catch {}
|
|
226
250
|
}
|
|
227
251
|
|
|
252
|
+
function ensureRuntimeGitignore(target) {
|
|
253
|
+
const gi = path.join(target, ".gitignore");
|
|
254
|
+
try {
|
|
255
|
+
const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
|
|
256
|
+
const existing = new Set(text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
257
|
+
const missing = INIT_RUNTIME_GITIGNORE_LINES.filter((line) => !existing.has(line));
|
|
258
|
+
if (!missing.length) return [];
|
|
259
|
+
|
|
260
|
+
let block = "";
|
|
261
|
+
if (text.length && !text.endsWith("\n")) block += "\n";
|
|
262
|
+
if (text.length) block += "\n";
|
|
263
|
+
block += "# 0dai runtime state\n";
|
|
264
|
+
block += `${missing.join("\n")}\n`;
|
|
265
|
+
fs.appendFileSync(gi, block, "utf8");
|
|
266
|
+
return missing;
|
|
267
|
+
} catch {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
228
272
|
function maybePauseCloudInitForAuth(target, args = [], options = {}) {
|
|
229
273
|
if (options.dryRun || options.localMode) return false;
|
|
230
274
|
if (_hasAuthToken()) return false;
|
|
@@ -232,6 +276,7 @@ function maybePauseCloudInitForAuth(target, args = [], options = {}) {
|
|
|
232
276
|
if (parsed.authCode) return false;
|
|
233
277
|
if (process.stdout.isTTY && process.stdin.isTTY) return false;
|
|
234
278
|
|
|
279
|
+
ensureRuntimeGitignore(target);
|
|
235
280
|
const checkpoint = writeCloudInitCheckpoint(target, {
|
|
236
281
|
args,
|
|
237
282
|
stage: "auth_required",
|
|
@@ -241,23 +286,89 @@ function maybePauseCloudInitForAuth(target, args = [], options = {}) {
|
|
|
241
286
|
console.log(` ${D}Checkpoint: ${CLOUD_INIT_CHECKPOINT_REL}${R}`);
|
|
242
287
|
console.log(` ${D}Run: ${checkpoint.next_command}${R}`);
|
|
243
288
|
console.log(` ${D}Then: ${checkpoint.resume_command}${R}`);
|
|
289
|
+
console.log(` ${D}Or run offline without signing in: 0dai init --local${R}`);
|
|
244
290
|
process.exit(1);
|
|
245
291
|
}
|
|
246
292
|
|
|
293
|
+
function parseProjectBindOptions(args = []) {
|
|
294
|
+
const options = { json: args.includes("--json"), name: "" };
|
|
295
|
+
for (let i = 0; i < args.length; i++) {
|
|
296
|
+
const arg = String(args[i] || "");
|
|
297
|
+
if (arg === "--name") {
|
|
298
|
+
const next = args[i + 1];
|
|
299
|
+
if (!next || String(next).startsWith("--")) {
|
|
300
|
+
return { ...options, error: "--name requires a project display name" };
|
|
301
|
+
}
|
|
302
|
+
const validated = validateProjectDisplayName(next);
|
|
303
|
+
if (!validated.ok) return { ...options, error: validated.error };
|
|
304
|
+
options.name = validated.name;
|
|
305
|
+
i++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (arg.startsWith("--name=")) {
|
|
309
|
+
const validated = validateProjectDisplayName(arg.slice("--name=".length));
|
|
310
|
+
if (!validated.ok) return { ...options, error: validated.error };
|
|
311
|
+
options.name = validated.name;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return options;
|
|
315
|
+
}
|
|
316
|
+
|
|
247
317
|
async function cmdProjectBind(target, args = []) {
|
|
248
|
-
const
|
|
318
|
+
const bindOptions = parseProjectBindOptions(args);
|
|
319
|
+
if (bindOptions.error) {
|
|
320
|
+
log(`error: ${bindOptions.error}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
const json = bindOptions.json;
|
|
249
324
|
const authStatus = await ensureAuthenticated("project bind");
|
|
250
325
|
const license = await ensureLicenseActivation();
|
|
251
326
|
const metadata = collectMetadata(target);
|
|
327
|
+
const existingBinding = loadProjectBinding(target);
|
|
328
|
+
const existingBoundIdentity = boundProjectIdentityFromBinding(target, existingBinding);
|
|
329
|
+
const explicitRebindProjectId = projectIdForExplicitRebindFromBinding(existingBinding);
|
|
252
330
|
const identity = buildProjectIdentity(target, metadata);
|
|
331
|
+
if (bindOptions.name) {
|
|
332
|
+
identity.project_name = bindOptions.name;
|
|
333
|
+
identity.project_id = projectIdFor(target, bindOptions.name, identity.remote_origin || path.resolve(target));
|
|
334
|
+
}
|
|
335
|
+
if (existingBoundIdentity && existingBoundIdentity.project_id) {
|
|
336
|
+
identity.project_id = existingBoundIdentity.project_id;
|
|
337
|
+
} else if (explicitRebindProjectId) {
|
|
338
|
+
identity.project_id = explicitRebindProjectId;
|
|
339
|
+
}
|
|
253
340
|
const boundProject = await bindProjectForCloud(target, metadata, identity);
|
|
341
|
+
const projectID = boundProject.project_id || identity.project_id;
|
|
342
|
+
const projectName = bindOptions.name || boundProject.name || identity.project_name;
|
|
343
|
+
identity.project_id = projectID;
|
|
344
|
+
identity.project_name = projectName;
|
|
254
345
|
const stack = boundProject.stack || identity.stack || "unknown";
|
|
255
|
-
|
|
256
|
-
|
|
346
|
+
const registryName = bindOptions.name || path.basename(target);
|
|
347
|
+
if (await guardRegistryDrift(target, registryName, args)) {
|
|
348
|
+
registerProjectOrExit(target, registryName, stack);
|
|
257
349
|
}
|
|
258
350
|
const heartbeat = await sendProjectHeartbeat(target, identity, { stack }, {
|
|
259
|
-
project_id:
|
|
351
|
+
project_id: projectID,
|
|
260
352
|
}).catch((err) => ({ error: err.message || String(err) }));
|
|
353
|
+
const savedBinding = writeProjectBinding(target, {
|
|
354
|
+
identity,
|
|
355
|
+
server_project: {
|
|
356
|
+
...boundProject,
|
|
357
|
+
project_id: projectID,
|
|
358
|
+
name: projectName,
|
|
359
|
+
stack,
|
|
360
|
+
binding_status: boundProject.binding_status || "bound",
|
|
361
|
+
},
|
|
362
|
+
project_id: projectID,
|
|
363
|
+
project_name: projectName,
|
|
364
|
+
stack,
|
|
365
|
+
binding_status: boundProject.binding_status || "bound",
|
|
366
|
+
binding_source: bindOptions.name ? "project bind --name" : "project bind",
|
|
367
|
+
account_email: authStatus.email || "",
|
|
368
|
+
account_plan: authStatus.plan || license.plan || "free",
|
|
369
|
+
activation_status: license.status || "active",
|
|
370
|
+
activation_id: license.activation_id || "",
|
|
371
|
+
});
|
|
261
372
|
const payload = {
|
|
262
373
|
ok: true,
|
|
263
374
|
account: {
|
|
@@ -266,12 +377,18 @@ async function cmdProjectBind(target, args = []) {
|
|
|
266
377
|
activation: license.status || "active",
|
|
267
378
|
},
|
|
268
379
|
project: {
|
|
269
|
-
project_id:
|
|
270
|
-
name:
|
|
380
|
+
project_id: projectID,
|
|
381
|
+
name: projectName,
|
|
271
382
|
stack,
|
|
272
383
|
binding_status: boundProject.binding_status || "bound",
|
|
273
384
|
},
|
|
274
385
|
heartbeat: !(heartbeat && heartbeat.error),
|
|
386
|
+
host_registry: projectBindHostRegistryInfo(target),
|
|
387
|
+
local_binding: {
|
|
388
|
+
target: savedBinding.target,
|
|
389
|
+
target_aliases: savedBinding.target_aliases || [],
|
|
390
|
+
updated_at: savedBinding.updated_at,
|
|
391
|
+
},
|
|
275
392
|
};
|
|
276
393
|
if (json) {
|
|
277
394
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -279,6 +396,8 @@ async function cmdProjectBind(target, args = []) {
|
|
|
279
396
|
log("project bound");
|
|
280
397
|
console.log(` account: ${payload.account.email || "unknown"} · plan: ${payload.account.plan} · activation: ${payload.account.activation}`);
|
|
281
398
|
console.log(` project: ${payload.project.project_id}`);
|
|
399
|
+
console.log(` registry: ${payload.host_registry.registry}`);
|
|
400
|
+
console.log(` ${D}${payload.host_registry.note} See #4084.${R}`);
|
|
282
401
|
if (!payload.heartbeat) console.log(` ${D}heartbeat deferred; run 0dai status to inspect local state${R}`);
|
|
283
402
|
}
|
|
284
403
|
return payload;
|
|
@@ -294,10 +413,22 @@ async function cmdInit(target, args = []) {
|
|
|
294
413
|
const v = fs.readFileSync(path.join(target, "ai", "VERSION"), "utf8").trim();
|
|
295
414
|
log(`ai/ layer already exists (v${v}). Run '0dai sync' to update.`);
|
|
296
415
|
if (args.includes("--resume")) clearCloudInitCheckpoint(target);
|
|
297
|
-
if (!dryRun)
|
|
416
|
+
if (!dryRun) {
|
|
417
|
+
ensureRuntimeGitignore(target);
|
|
418
|
+
await runMcpBootstrap(target, args);
|
|
419
|
+
}
|
|
298
420
|
return;
|
|
299
421
|
}
|
|
300
422
|
|
|
423
|
+
if (!dryRun && !args.includes("--json")) {
|
|
424
|
+
await checkVersion({
|
|
425
|
+
force: true,
|
|
426
|
+
background: false,
|
|
427
|
+
timeoutMs: 1500,
|
|
428
|
+
initHint: true,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
301
432
|
// Pre-check: verify init quota before starting wizard (avoid 10 min wizard → "limit reached")
|
|
302
433
|
if (!dryRun) {
|
|
303
434
|
try {
|
|
@@ -319,6 +450,7 @@ async function cmdInit(target, args = []) {
|
|
|
319
450
|
if (isInteractive()) {
|
|
320
451
|
const result = await runWizard(target);
|
|
321
452
|
if (result.completed) {
|
|
453
|
+
ensureRuntimeGitignore(target);
|
|
322
454
|
await runMcpBootstrap(target, args);
|
|
323
455
|
try {
|
|
324
456
|
const ob = require("../onboarding");
|
|
@@ -335,6 +467,7 @@ async function cmdInit(target, args = []) {
|
|
|
335
467
|
const { runWizard } = require("../wizard");
|
|
336
468
|
const result = await runWizard(target, { forceLocal: true });
|
|
337
469
|
if (result.completed) {
|
|
470
|
+
ensureRuntimeGitignore(target);
|
|
338
471
|
await runMcpBootstrap(target, args);
|
|
339
472
|
try {
|
|
340
473
|
const ob = require("../onboarding");
|
|
@@ -357,18 +490,24 @@ async function cmdInit(target, args = []) {
|
|
|
357
490
|
const { projectFiles, manifestContents, clis } = metadata;
|
|
358
491
|
const authStatus = await ensureAccountForActivation("init", args);
|
|
359
492
|
const license = await ensureLicenseActivation();
|
|
493
|
+
const explicitRebindProjectId = projectIdForExplicitRebindFromBinding(loadProjectBinding(target));
|
|
360
494
|
const identity = buildProjectIdentity(target, metadata);
|
|
495
|
+
if (explicitRebindProjectId) identity.project_id = explicitRebindProjectId;
|
|
361
496
|
const boundProject = await bindProjectForCloud(target, metadata, identity);
|
|
362
497
|
if (dryRun) log(`${D}dry-run: would generate ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)${R}`);
|
|
363
498
|
if (spinner) spinner.start(`${dryRun ? "[dry-run] " : ""}Generating ai/ layer (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
364
499
|
else if (!dryRun) log(`sending to API (${projectFiles.length} files, ${clis.length} CLIs)...`);
|
|
365
500
|
const result = await apiCall("/v1/init", {
|
|
366
501
|
project_files: projectFiles,
|
|
367
|
-
|
|
502
|
+
// Privacy: send only filenames + SHA-256 hashes, never raw content (closes #4016).
|
|
503
|
+
// Local detection (inferProjectName, detectStackHint) still reads content locally —
|
|
504
|
+
// the results are passed as project_name and stack so the server needs no raw content.
|
|
505
|
+
manifest_files: hashManifestFiles(manifestContents),
|
|
368
506
|
available_clis: clis,
|
|
369
507
|
dry_run: dryRun,
|
|
370
508
|
minimal: minimal,
|
|
371
509
|
project_name: identity.project_name,
|
|
510
|
+
stack: identity.stack,
|
|
372
511
|
project_id: boundProject.project_id || identity.project_id,
|
|
373
512
|
remote_origin: identity.remote_origin,
|
|
374
513
|
origin: identity.origin,
|
|
@@ -395,6 +534,7 @@ async function cmdInit(target, args = []) {
|
|
|
395
534
|
return;
|
|
396
535
|
}
|
|
397
536
|
writeFiles(target, result.files || {});
|
|
537
|
+
ensureLiveManifestDefaults(target);
|
|
398
538
|
const envManifest = normalizeEnvironmentManifest(target);
|
|
399
539
|
if (envManifest.changed) {
|
|
400
540
|
log("environment manifest target normalized: ai/manifest/environment.yaml");
|
|
@@ -406,12 +546,7 @@ async function cmdInit(target, args = []) {
|
|
|
406
546
|
fs.mkdirSync(path.dirname(versionFile), { recursive: true });
|
|
407
547
|
fs.writeFileSync(versionFile, VERSION + "\n", "utf8");
|
|
408
548
|
|
|
409
|
-
|
|
410
|
-
const gi = path.join(target, ".gitignore");
|
|
411
|
-
try {
|
|
412
|
-
const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
|
|
413
|
-
if (!text.includes(".0dai")) fs.appendFileSync(gi, "\n.0dai/\n");
|
|
414
|
-
} catch {}
|
|
549
|
+
ensureRuntimeGitignore(target);
|
|
415
550
|
|
|
416
551
|
const gitPolicy = ensureGithubFlowPolicy(target);
|
|
417
552
|
if (gitPolicy && gitPolicy.protection && gitPolicy.protection.skipped) {
|
|
@@ -464,7 +599,9 @@ async function cmdInit(target, args = []) {
|
|
|
464
599
|
// First-run proof gate (issue #342). All 4 gates pass once we reach here:
|
|
465
600
|
// license active (line above), project bound, ai/ layer written, heartbeat sent.
|
|
466
601
|
// Idempotent — only fires once per project. See docs/first-run.md.
|
|
467
|
-
recordActivationInit(target, boundProject.project_id || identity.project_id
|
|
602
|
+
recordActivationInit(target, boundProject.project_id || identity.project_id, {
|
|
603
|
+
stack: result.stack || identity.stack || "unknown",
|
|
604
|
+
});
|
|
468
605
|
|
|
469
606
|
const firstRun = logFirstRunSuccess(target, {
|
|
470
607
|
license: true,
|
|
@@ -495,6 +632,7 @@ async function runMcpBootstrap(target, args = []) {
|
|
|
495
632
|
const verbs = [];
|
|
496
633
|
if (result.config.added && result.config.added.length) verbs.push(`${result.config.added.length} server(s) added`);
|
|
497
634
|
if (result.config.updated && result.config.updated.length) verbs.push(`${result.config.updated.length} server(s) reset`);
|
|
635
|
+
if (result.config.removed && result.config.removed.length) verbs.push(`${result.config.removed.length} server(s) removed`);
|
|
498
636
|
if (result.config.changed) {
|
|
499
637
|
log(`MCP config ready (${verbs.join(", ") || "updated"}): .mcp.json`);
|
|
500
638
|
}
|
|
@@ -510,7 +648,7 @@ async function runMcpBootstrap(target, args = []) {
|
|
|
510
648
|
} else if (result.auth.status === "disabled") {
|
|
511
649
|
log("MCP cloud auth skipped (--no-mcp-auth)");
|
|
512
650
|
} else if (result.auth.status === "skipped") {
|
|
513
|
-
console.log(` ${D}MCP cloud auth skipped. In Claude Code, run /mcp Authenticate for
|
|
651
|
+
console.log(` ${D}MCP cloud auth skipped. In Claude Code, run /mcp Authenticate for claude_ai_0dai${R}`);
|
|
514
652
|
}
|
|
515
653
|
}
|
|
516
654
|
for (const warning of result.warnings || []) log(`warn: ${warning}`);
|
|
@@ -614,10 +752,33 @@ function runDocLinkCheck(target, args = [], options = {}) {
|
|
|
614
752
|
return { ran: true, status: 0, strict, broken: false };
|
|
615
753
|
}
|
|
616
754
|
|
|
755
|
+
function printProtectedSyncUpdates(items, { quiet = false } = {}) {
|
|
756
|
+
if (quiet || !Array.isArray(items) || items.length === 0) return;
|
|
757
|
+
log(`${W}sync protected ${items.length} project-owned update(s)${R}`);
|
|
758
|
+
for (const item of items) {
|
|
759
|
+
const rel = item && item.path ? String(item.path) : "(unknown)";
|
|
760
|
+
const reason = item && item.reason ? String(item.reason) : "protected by default";
|
|
761
|
+
console.log(` ${D}! ${rel} — ${reason}${R}`);
|
|
762
|
+
}
|
|
763
|
+
console.log(` ${D}Pass --force-template-reset to apply these reset-style updates.${R}`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function isGenericStackName(value) {
|
|
767
|
+
return ["", "generic", "unknown", "auto"].includes(String(value || "").trim().toLowerCase());
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function chooseSyncStack(target, discoveryStack) {
|
|
771
|
+
const manifestStack = readProjectManifest(target).stack || "";
|
|
772
|
+
if (!isGenericStackName(manifestStack)) return manifestStack;
|
|
773
|
+
return discoveryStack || manifestStack || "generic";
|
|
774
|
+
}
|
|
775
|
+
|
|
617
776
|
async function cmdSync(target, args = []) {
|
|
618
|
-
const
|
|
619
|
-
const
|
|
777
|
+
const check = args.includes("--check");
|
|
778
|
+
const dryRun = args.includes("--dry-run") || check;
|
|
779
|
+
const quiet = args.includes("--quiet") || args.includes("-q") || check;
|
|
620
780
|
const force = args.includes("--force");
|
|
781
|
+
const forceTemplateReset = args.includes("--force-template-reset");
|
|
621
782
|
|
|
622
783
|
// Quick local check: skip API if already at current version (unless dry-run or force)
|
|
623
784
|
let version = "unknown";
|
|
@@ -625,12 +786,13 @@ async function cmdSync(target, args = []) {
|
|
|
625
786
|
|
|
626
787
|
const metadata = collectMetadata(target);
|
|
627
788
|
const { manifestContents, clis } = metadata;
|
|
628
|
-
let
|
|
789
|
+
let discoveryStack = "generic", agents = [];
|
|
629
790
|
try {
|
|
630
791
|
const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
|
|
631
|
-
|
|
792
|
+
discoveryStack = d.stack || "generic";
|
|
632
793
|
agents = d.selected_agents || [];
|
|
633
794
|
} catch {}
|
|
795
|
+
const stack = chooseSyncStack(target, discoveryStack);
|
|
634
796
|
const identity = buildProjectIdentity(target, metadata, stack);
|
|
635
797
|
|
|
636
798
|
if (dryRun) {
|
|
@@ -665,8 +827,13 @@ async function cmdSync(target, args = []) {
|
|
|
665
827
|
|
|
666
828
|
const result = await apiCall("/v1/sync", {
|
|
667
829
|
ai_version: version, stack, agents: agents.length ? agents : clis,
|
|
668
|
-
current_files: currentFiles,
|
|
830
|
+
current_files: currentFiles,
|
|
831
|
+
// Privacy: send only filenames + SHA-256 hashes, never raw content (closes #4031).
|
|
832
|
+
// Local detection (stack, agents) is derived locally and sent separately so the
|
|
833
|
+
// server needs no raw manifest content — mirrors the /v1/init fix from #4030.
|
|
834
|
+
manifest_files: hashManifestFiles(manifestContents),
|
|
669
835
|
dry_run: dryRun, quiet, force,
|
|
836
|
+
force_template_reset: forceTemplateReset,
|
|
670
837
|
project_name: identity.project_name,
|
|
671
838
|
project_id: boundProject.project_id || identity.project_id,
|
|
672
839
|
remote_origin: identity.remote_origin,
|
|
@@ -680,7 +847,11 @@ async function cmdSync(target, args = []) {
|
|
|
680
847
|
process.exit(1);
|
|
681
848
|
}
|
|
682
849
|
|
|
683
|
-
const updated =
|
|
850
|
+
const updated = {
|
|
851
|
+
...(result.files_updated || {}),
|
|
852
|
+
...missingLiveManifestDefaults(target, result.files_updated || {}),
|
|
853
|
+
};
|
|
854
|
+
const protectedUpdates = Array.isArray(result.protected_updates) ? result.protected_updates : [];
|
|
684
855
|
if (dryRun) {
|
|
685
856
|
const files = Object.keys(updated);
|
|
686
857
|
if (files.length) {
|
|
@@ -696,8 +867,10 @@ async function cmdSync(target, args = []) {
|
|
|
696
867
|
} else {
|
|
697
868
|
log(`${D}dry-run: nothing to update${R}`);
|
|
698
869
|
}
|
|
870
|
+
printProtectedSyncUpdates(protectedUpdates, { quiet });
|
|
699
871
|
return;
|
|
700
872
|
}
|
|
873
|
+
printProtectedSyncUpdates(protectedUpdates, { quiet });
|
|
701
874
|
const changedCount = Object.keys(updated).length;
|
|
702
875
|
if (changedCount) {
|
|
703
876
|
if (!quiet && !shouldAutoYes(args)) {
|
|
@@ -762,12 +935,28 @@ async function cmdSync(target, args = []) {
|
|
|
762
935
|
if (force && result.native_configs) {
|
|
763
936
|
const NATIVE_CONFIGS = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", "opencode.json", ".cursorrules", ".windsurfrules", ".aider.conf.yml"];
|
|
764
937
|
let overwritten = 0;
|
|
938
|
+
let backupDir = null;
|
|
765
939
|
for (const name of NATIVE_CONFIGS) {
|
|
766
|
-
if (result.native_configs[name])
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
940
|
+
if (!result.native_configs[name]) continue;
|
|
941
|
+
const dest = path.join(target, name);
|
|
942
|
+
const next = result.native_configs[name];
|
|
943
|
+
// Back up a pre-existing, differing native config before overwriting, so
|
|
944
|
+
// a hand-edited CLAUDE.md/AGENTS.md/… is recoverable from ai/.backups
|
|
945
|
+
// (sync --force used to clobber these with no backup, #4363).
|
|
946
|
+
if (fs.existsSync(dest)) {
|
|
947
|
+
const existing = fs.readFileSync(dest, "utf8");
|
|
948
|
+
if (existing === next) continue;
|
|
949
|
+
if (!backupDir) {
|
|
950
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
951
|
+
backupDir = path.join(target, "ai", ".backups", timestamp);
|
|
952
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
953
|
+
}
|
|
954
|
+
fs.writeFileSync(path.join(backupDir, name), existing, "utf8");
|
|
955
|
+
if (!quiet) console.log(` [force] backed up ${name} to ${path.relative(target, path.join(backupDir, name))}`);
|
|
770
956
|
}
|
|
957
|
+
fs.writeFileSync(dest, next, "utf8");
|
|
958
|
+
overwritten++;
|
|
959
|
+
if (!quiet) console.log(` [force] ${name} overwritten from ai/ source`);
|
|
771
960
|
}
|
|
772
961
|
if (overwritten && !quiet) {
|
|
773
962
|
log(`force: ${overwritten} native config file(s) overwritten`);
|
|
@@ -824,6 +1013,8 @@ function buildLocalSyncPreview(target, { version, stack, cliVersion }) {
|
|
|
824
1013
|
"ai/manifest/project.yaml",
|
|
825
1014
|
"ai/manifest/commands.yaml",
|
|
826
1015
|
"ai/manifest/discovery.json",
|
|
1016
|
+
"ai/manifest/current_state.json",
|
|
1017
|
+
"ai/manifest/current_task.json",
|
|
827
1018
|
];
|
|
828
1019
|
for (const rel of expectedAiFiles) {
|
|
829
1020
|
if (!fs.existsSync(path.join(target, rel))) changes.push(`${rel} (missing)`);
|
|
@@ -882,6 +1073,7 @@ module.exports = {
|
|
|
882
1073
|
writeCloudInitCheckpoint,
|
|
883
1074
|
clearCloudInitCheckpoint,
|
|
884
1075
|
buildCloudInitResumeCommand,
|
|
1076
|
+
ensureRuntimeGitignore,
|
|
885
1077
|
collectCurrentAiFiles,
|
|
886
1078
|
hashFile,
|
|
887
1079
|
detectRegistryDrift,
|