@0dai-dev/cli 4.3.6 → 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 +127 -30
- 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 +506 -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 +209 -27
- package/lib/commands/mcp.js +111 -33
- 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 +14 -6
- package/lib/commands/runner.js +31 -1
- package/lib/commands/status.js +38 -10
- package/lib/commands/swarm.js +130 -12
- 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 +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 +95 -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,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, hashManifestFiles,
|
|
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,7 +490,9 @@ 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)...`);
|
|
@@ -399,6 +534,7 @@ async function cmdInit(target, args = []) {
|
|
|
399
534
|
return;
|
|
400
535
|
}
|
|
401
536
|
writeFiles(target, result.files || {});
|
|
537
|
+
ensureLiveManifestDefaults(target);
|
|
402
538
|
const envManifest = normalizeEnvironmentManifest(target);
|
|
403
539
|
if (envManifest.changed) {
|
|
404
540
|
log("environment manifest target normalized: ai/manifest/environment.yaml");
|
|
@@ -410,12 +546,7 @@ async function cmdInit(target, args = []) {
|
|
|
410
546
|
fs.mkdirSync(path.dirname(versionFile), { recursive: true });
|
|
411
547
|
fs.writeFileSync(versionFile, VERSION + "\n", "utf8");
|
|
412
548
|
|
|
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 {}
|
|
549
|
+
ensureRuntimeGitignore(target);
|
|
419
550
|
|
|
420
551
|
const gitPolicy = ensureGithubFlowPolicy(target);
|
|
421
552
|
if (gitPolicy && gitPolicy.protection && gitPolicy.protection.skipped) {
|
|
@@ -501,6 +632,7 @@ async function runMcpBootstrap(target, args = []) {
|
|
|
501
632
|
const verbs = [];
|
|
502
633
|
if (result.config.added && result.config.added.length) verbs.push(`${result.config.added.length} server(s) added`);
|
|
503
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`);
|
|
504
636
|
if (result.config.changed) {
|
|
505
637
|
log(`MCP config ready (${verbs.join(", ") || "updated"}): .mcp.json`);
|
|
506
638
|
}
|
|
@@ -516,7 +648,7 @@ async function runMcpBootstrap(target, args = []) {
|
|
|
516
648
|
} else if (result.auth.status === "disabled") {
|
|
517
649
|
log("MCP cloud auth skipped (--no-mcp-auth)");
|
|
518
650
|
} else if (result.auth.status === "skipped") {
|
|
519
|
-
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}`);
|
|
520
652
|
}
|
|
521
653
|
}
|
|
522
654
|
for (const warning of result.warnings || []) log(`warn: ${warning}`);
|
|
@@ -620,10 +752,33 @@ function runDocLinkCheck(target, args = [], options = {}) {
|
|
|
620
752
|
return { ran: true, status: 0, strict, broken: false };
|
|
621
753
|
}
|
|
622
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
|
+
|
|
623
776
|
async function cmdSync(target, args = []) {
|
|
624
|
-
const
|
|
625
|
-
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;
|
|
626
780
|
const force = args.includes("--force");
|
|
781
|
+
const forceTemplateReset = args.includes("--force-template-reset");
|
|
627
782
|
|
|
628
783
|
// Quick local check: skip API if already at current version (unless dry-run or force)
|
|
629
784
|
let version = "unknown";
|
|
@@ -631,12 +786,13 @@ async function cmdSync(target, args = []) {
|
|
|
631
786
|
|
|
632
787
|
const metadata = collectMetadata(target);
|
|
633
788
|
const { manifestContents, clis } = metadata;
|
|
634
|
-
let
|
|
789
|
+
let discoveryStack = "generic", agents = [];
|
|
635
790
|
try {
|
|
636
791
|
const d = JSON.parse(fs.readFileSync(path.join(target, "ai", "manifest", "discovery.json"), "utf8"));
|
|
637
|
-
|
|
792
|
+
discoveryStack = d.stack || "generic";
|
|
638
793
|
agents = d.selected_agents || [];
|
|
639
794
|
} catch {}
|
|
795
|
+
const stack = chooseSyncStack(target, discoveryStack);
|
|
640
796
|
const identity = buildProjectIdentity(target, metadata, stack);
|
|
641
797
|
|
|
642
798
|
if (dryRun) {
|
|
@@ -677,6 +833,7 @@ async function cmdSync(target, args = []) {
|
|
|
677
833
|
// server needs no raw manifest content — mirrors the /v1/init fix from #4030.
|
|
678
834
|
manifest_files: hashManifestFiles(manifestContents),
|
|
679
835
|
dry_run: dryRun, quiet, force,
|
|
836
|
+
force_template_reset: forceTemplateReset,
|
|
680
837
|
project_name: identity.project_name,
|
|
681
838
|
project_id: boundProject.project_id || identity.project_id,
|
|
682
839
|
remote_origin: identity.remote_origin,
|
|
@@ -690,7 +847,11 @@ async function cmdSync(target, args = []) {
|
|
|
690
847
|
process.exit(1);
|
|
691
848
|
}
|
|
692
849
|
|
|
693
|
-
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 : [];
|
|
694
855
|
if (dryRun) {
|
|
695
856
|
const files = Object.keys(updated);
|
|
696
857
|
if (files.length) {
|
|
@@ -706,8 +867,10 @@ async function cmdSync(target, args = []) {
|
|
|
706
867
|
} else {
|
|
707
868
|
log(`${D}dry-run: nothing to update${R}`);
|
|
708
869
|
}
|
|
870
|
+
printProtectedSyncUpdates(protectedUpdates, { quiet });
|
|
709
871
|
return;
|
|
710
872
|
}
|
|
873
|
+
printProtectedSyncUpdates(protectedUpdates, { quiet });
|
|
711
874
|
const changedCount = Object.keys(updated).length;
|
|
712
875
|
if (changedCount) {
|
|
713
876
|
if (!quiet && !shouldAutoYes(args)) {
|
|
@@ -772,12 +935,28 @@ async function cmdSync(target, args = []) {
|
|
|
772
935
|
if (force && result.native_configs) {
|
|
773
936
|
const NATIVE_CONFIGS = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", "opencode.json", ".cursorrules", ".windsurfrules", ".aider.conf.yml"];
|
|
774
937
|
let overwritten = 0;
|
|
938
|
+
let backupDir = null;
|
|
775
939
|
for (const name of NATIVE_CONFIGS) {
|
|
776
|
-
if (result.native_configs[name])
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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))}`);
|
|
780
956
|
}
|
|
957
|
+
fs.writeFileSync(dest, next, "utf8");
|
|
958
|
+
overwritten++;
|
|
959
|
+
if (!quiet) console.log(` [force] ${name} overwritten from ai/ source`);
|
|
781
960
|
}
|
|
782
961
|
if (overwritten && !quiet) {
|
|
783
962
|
log(`force: ${overwritten} native config file(s) overwritten`);
|
|
@@ -834,6 +1013,8 @@ function buildLocalSyncPreview(target, { version, stack, cliVersion }) {
|
|
|
834
1013
|
"ai/manifest/project.yaml",
|
|
835
1014
|
"ai/manifest/commands.yaml",
|
|
836
1015
|
"ai/manifest/discovery.json",
|
|
1016
|
+
"ai/manifest/current_state.json",
|
|
1017
|
+
"ai/manifest/current_task.json",
|
|
837
1018
|
];
|
|
838
1019
|
for (const rel of expectedAiFiles) {
|
|
839
1020
|
if (!fs.existsSync(path.join(target, rel))) changes.push(`${rel} (missing)`);
|
|
@@ -892,6 +1073,7 @@ module.exports = {
|
|
|
892
1073
|
writeCloudInitCheckpoint,
|
|
893
1074
|
clearCloudInitCheckpoint,
|
|
894
1075
|
buildCloudInitResumeCommand,
|
|
1076
|
+
ensureRuntimeGitignore,
|
|
895
1077
|
collectCurrentAiFiles,
|
|
896
1078
|
hashFile,
|
|
897
1079
|
detectRegistryDrift,
|