@mison/ling 1.0.1 → 1.1.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/CHANGELOG.md +22 -3
- package/README.md +38 -25
- package/bin/adapters/codex.js +6 -6
- package/bin/adapters/gemini.js +1 -1
- package/bin/interactive.js +68 -1
- package/bin/{ag-kit.js → ling-cli.js} +553 -120
- package/bin/ling.js +1 -1
- package/bin/utils/managed-block.js +17 -4
- package/bin/utils.js +1 -1
- package/docs/TECH.md +16 -7
- package/package.json +7 -5
- package/scripts/ci-verify.js +9 -3
- package/scripts/postinstall-check.js +5 -6
- package/tests/clean-script.test.js +1 -1
- package/tests/cli-smoke.test.js +85 -0
- package/tests/managed-block.test.js +18 -1
- package/tests/phase-c.test.js +2 -2
- package/tests/standards-compliance.test.js +1 -1
- package/tests/transformer.test.js +1 -1
- package/tests/versioning.test.js +0 -10
|
@@ -11,13 +11,11 @@ const AtomicWriter = require("./utils/atomic-writer");
|
|
|
11
11
|
const CodexBuilder = require("./core/builder");
|
|
12
12
|
const GeminiAdapter = require("./adapters/gemini");
|
|
13
13
|
const CodexAdapter = require("./adapters/codex");
|
|
14
|
-
const { selectTargets } = require("./interactive");
|
|
14
|
+
const { selectTargets, createConflictPrompter } = require("./interactive");
|
|
15
15
|
|
|
16
16
|
const BUNDLED_AGENT_DIR = path.resolve(__dirname, "../.agents");
|
|
17
17
|
const BUNDLED_SPEC_DIR = path.resolve(__dirname, "../.spec");
|
|
18
18
|
const PRIMARY_CLI_NAME = "ling";
|
|
19
|
-
const LEGACY_CLI_NAME = "ag-kit";
|
|
20
|
-
const CURRENT_CLI_BASENAME = path.basename(process.argv[1] || "", path.extname(process.argv[1] || "")) || PRIMARY_CLI_NAME;
|
|
21
19
|
const WORKSPACE_INDEX_VERSION = 2;
|
|
22
20
|
const UPSTREAM_GLOBAL_PACKAGE = "@vudovn/ag-kit";
|
|
23
21
|
const TOOLKIT_PACKAGE_NAMES = new Set(["@mison/ling", "@mison/ag-kit-cn", "antigravity-kit-cn", "antigravity-kit"]);
|
|
@@ -67,36 +65,11 @@ function getVersionTag() {
|
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
function getControlHomeDir() {
|
|
70
|
-
|
|
71
|
-
const legacy = path.join(os.homedir(), ".ag-kit");
|
|
72
|
-
|
|
73
|
-
if (fs.existsSync(preferred)) {
|
|
74
|
-
return preferred;
|
|
75
|
-
}
|
|
76
|
-
if (fs.existsSync(legacy)) {
|
|
77
|
-
return legacy;
|
|
78
|
-
}
|
|
79
|
-
return preferred;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function migrateLegacyControlHomeDir() {
|
|
83
|
-
if (process.env.LING_INDEX_PATH || process.env.AG_KIT_INDEX_PATH) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const preferred = path.join(os.homedir(), ".ling");
|
|
88
|
-
const legacy = path.join(os.homedir(), ".ag-kit");
|
|
89
|
-
|
|
90
|
-
if (fs.existsSync(preferred) || !fs.existsSync(legacy)) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
copyDirRecursive(legacy, preferred);
|
|
95
|
-
return { from: legacy, to: preferred };
|
|
68
|
+
return path.join(os.homedir(), ".ling");
|
|
96
69
|
}
|
|
97
70
|
|
|
98
71
|
function getWorkspaceIndexPath() {
|
|
99
|
-
const customPath = process.env.LING_INDEX_PATH
|
|
72
|
+
const customPath = process.env.LING_INDEX_PATH;
|
|
100
73
|
if (customPath) {
|
|
101
74
|
return path.resolve(process.cwd(), customPath);
|
|
102
75
|
}
|
|
@@ -113,7 +86,7 @@ function createEmptyWorkspaceIndex() {
|
|
|
113
86
|
}
|
|
114
87
|
|
|
115
88
|
function resolveGlobalRootDir() {
|
|
116
|
-
const customRoot = process.env.LING_GLOBAL_ROOT
|
|
89
|
+
const customRoot = process.env.LING_GLOBAL_ROOT;
|
|
117
90
|
if (typeof customRoot === "string" && customRoot.trim()) {
|
|
118
91
|
return path.resolve(process.cwd(), customRoot);
|
|
119
92
|
}
|
|
@@ -156,6 +129,22 @@ function copyDirRecursive(src, dest) {
|
|
|
156
129
|
}
|
|
157
130
|
}
|
|
158
131
|
|
|
132
|
+
function backupWorkspaceDir(workspaceRoot, sourceDir, backupRootName, timestamp, options, label) {
|
|
133
|
+
if (!fs.existsSync(sourceDir)) {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
const assetName = path.basename(sourceDir) || "asset";
|
|
137
|
+
const backupDir = path.join(workspaceRoot, backupRootName, timestamp, "preflight", assetName);
|
|
138
|
+
if (options.dryRun) {
|
|
139
|
+
log(options, `[dry-run] 将备份 ${label}: ${sourceDir} -> ${backupDir}`);
|
|
140
|
+
return backupDir;
|
|
141
|
+
}
|
|
142
|
+
fs.mkdirSync(path.dirname(backupDir), { recursive: true });
|
|
143
|
+
copyDirRecursive(sourceDir, backupDir);
|
|
144
|
+
log(options, `[backup] 已备份 ${label}: ${sourceDir} -> ${backupDir}`);
|
|
145
|
+
return backupDir;
|
|
146
|
+
}
|
|
147
|
+
|
|
159
148
|
function areDirectoriesEqual(leftDir, rightDir) {
|
|
160
149
|
const left = ManifestManager.generateFromDir(leftDir);
|
|
161
150
|
const right = ManifestManager.generateFromDir(rightDir);
|
|
@@ -428,10 +417,10 @@ function isPathExcludedByList(excludedPaths, workspacePath) {
|
|
|
428
417
|
|
|
429
418
|
function isToolkitSourceDirectory(workspacePath) {
|
|
430
419
|
const packageJsonPath = path.join(workspacePath, "package.json");
|
|
431
|
-
const
|
|
420
|
+
const cliPath = path.join(workspacePath, "bin", "ling-cli.js");
|
|
432
421
|
const primaryCliPath = path.join(workspacePath, "bin", "ling.js");
|
|
433
422
|
|
|
434
|
-
if (!fs.existsSync(packageJsonPath) || (!fs.existsSync(
|
|
423
|
+
if (!fs.existsSync(packageJsonPath) || (!fs.existsSync(cliPath) && !fs.existsSync(primaryCliPath))) {
|
|
435
424
|
return false;
|
|
436
425
|
}
|
|
437
426
|
|
|
@@ -856,7 +845,7 @@ function maybeWarnUpstreamGlobalConflict(command, options) {
|
|
|
856
845
|
if (options.quiet) {
|
|
857
846
|
return;
|
|
858
847
|
}
|
|
859
|
-
if (process.env.LING_SKIP_UPSTREAM_CHECK === "1"
|
|
848
|
+
if (process.env.LING_SKIP_UPSTREAM_CHECK === "1") {
|
|
860
849
|
return;
|
|
861
850
|
}
|
|
862
851
|
const shouldWarn =
|
|
@@ -879,20 +868,8 @@ function maybeWarnUpstreamGlobalConflict(command, options) {
|
|
|
879
868
|
}
|
|
880
869
|
|
|
881
870
|
log(options, `[warn] 检测到全局已安装上游英文版 ${UPSTREAM_GLOBAL_PACKAGE}。`);
|
|
882
|
-
log(options, `[
|
|
883
|
-
log(options, `[
|
|
884
|
-
log(options, `[info] 正式命令已切换为 \`${PRIMARY_CLI_NAME}\`。若你通过 bun install -g 安装,Bun 默认会阻止本包 postinstall;因此这里会在首次执行 CLI 时再次提醒。`);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function maybeWarnLegacyCliAlias(options) {
|
|
888
|
-
if (options.quiet) {
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
if (CURRENT_CLI_BASENAME !== LEGACY_CLI_NAME) {
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
console.log(`[warn] \`${LEGACY_CLI_NAME}\` 已进入兼容模式,请改用 \`${PRIMARY_CLI_NAME}\`.`);
|
|
871
|
+
log(options, `[hint] 如需仅保留一个来源,请执行: npm uninstall -g ${UPSTREAM_GLOBAL_PACKAGE}`);
|
|
872
|
+
log(options, `[info] 当前项目的正式命令为 \`${PRIMARY_CLI_NAME}\`。`);
|
|
896
873
|
}
|
|
897
874
|
|
|
898
875
|
function normalizeTargets(rawTargets) {
|
|
@@ -1219,9 +1196,106 @@ function syncGlobalSkillsFromRoot(targetName, skillsRoot, timestamp, options) {
|
|
|
1219
1196
|
};
|
|
1220
1197
|
}
|
|
1221
1198
|
|
|
1199
|
+
function planGlobalSyncTasks(targetName, agentDir) {
|
|
1200
|
+
const destinations = getGlobalDestinations(targetName);
|
|
1201
|
+
|
|
1202
|
+
if (targetName === "codex") {
|
|
1203
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ling-global-codex-"));
|
|
1204
|
+
const mockRoot = path.join(tempRoot, "source");
|
|
1205
|
+
const mockAgent = path.join(mockRoot, ".agents");
|
|
1206
|
+
const outputDir = path.join(tempRoot, "out");
|
|
1207
|
+
|
|
1208
|
+
copyDirRecursive(agentDir, mockAgent);
|
|
1209
|
+
CodexBuilder.build(mockRoot, outputDir);
|
|
1210
|
+
const skillsRoot = path.join(outputDir, "skills");
|
|
1211
|
+
const skillNames = listSkillDirectories(skillsRoot);
|
|
1212
|
+
|
|
1213
|
+
const tasks = [];
|
|
1214
|
+
for (const destination of destinations) {
|
|
1215
|
+
for (const skillName of skillNames) {
|
|
1216
|
+
tasks.push({
|
|
1217
|
+
destination,
|
|
1218
|
+
skillName,
|
|
1219
|
+
srcDir: path.join(skillsRoot, skillName),
|
|
1220
|
+
destDir: path.join(destination.skillsRoot, skillName),
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return {
|
|
1226
|
+
tasks,
|
|
1227
|
+
cleanup: () => fs.rmSync(tempRoot, { recursive: true, force: true }),
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (targetName === "gemini") {
|
|
1232
|
+
const skillsRoot = path.join(agentDir, "skills");
|
|
1233
|
+
const skillNames = listSkillDirectories(skillsRoot);
|
|
1234
|
+
const tasks = [];
|
|
1235
|
+
for (const destination of destinations) {
|
|
1236
|
+
for (const skillName of skillNames) {
|
|
1237
|
+
tasks.push({
|
|
1238
|
+
destination,
|
|
1239
|
+
skillName,
|
|
1240
|
+
srcDir: path.join(skillsRoot, skillName),
|
|
1241
|
+
destDir: path.join(destination.skillsRoot, skillName),
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return { tasks, cleanup: null };
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
throw new Error(`未知目标: ${targetName}`);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function summarizeGlobalSync(tasks) {
|
|
1252
|
+
const perDestination = new Map();
|
|
1253
|
+
let synced = 0;
|
|
1254
|
+
let skipped = 0;
|
|
1255
|
+
let backedUp = 0;
|
|
1256
|
+
|
|
1257
|
+
for (const task of tasks) {
|
|
1258
|
+
const id = task.destination.id;
|
|
1259
|
+
if (!perDestination.has(id)) {
|
|
1260
|
+
perDestination.set(id, {
|
|
1261
|
+
targetName: id,
|
|
1262
|
+
family: task.destination.targetName,
|
|
1263
|
+
destRoot: task.destination.skillsRoot,
|
|
1264
|
+
total: 0,
|
|
1265
|
+
synced: 0,
|
|
1266
|
+
skipped: 0,
|
|
1267
|
+
backedUp: 0,
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
const entry = perDestination.get(id);
|
|
1271
|
+
entry.total += 1;
|
|
1272
|
+
if (task.result === "skipped") {
|
|
1273
|
+
skipped += 1;
|
|
1274
|
+
entry.skipped += 1;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (task.result === "synced") {
|
|
1278
|
+
synced += 1;
|
|
1279
|
+
entry.synced += 1;
|
|
1280
|
+
}
|
|
1281
|
+
if (task.backedUp) {
|
|
1282
|
+
backedUp += 1;
|
|
1283
|
+
entry.backedUp += 1;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
return {
|
|
1288
|
+
total: tasks.length,
|
|
1289
|
+
synced,
|
|
1290
|
+
skipped,
|
|
1291
|
+
backedUp,
|
|
1292
|
+
destinations: Array.from(perDestination.values()),
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1222
1296
|
function applyGlobalSync(targetName, agentDir, timestamp, options) {
|
|
1223
1297
|
if (targetName === "codex") {
|
|
1224
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "
|
|
1298
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ling-global-codex-"));
|
|
1225
1299
|
const mockRoot = path.join(tempRoot, "source");
|
|
1226
1300
|
const mockAgent = path.join(mockRoot, ".agents");
|
|
1227
1301
|
const outputDir = path.join(tempRoot, "out");
|
|
@@ -1248,21 +1322,70 @@ async function commandGlobalSync(options) {
|
|
|
1248
1322
|
const targets = await resolveTargetsForGlobalSync(options);
|
|
1249
1323
|
const { agentDir, cleanup, sourceLabel } = resolveAgentInstallSource(options);
|
|
1250
1324
|
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
1325
|
+
const prompter = createConflictPrompter(options);
|
|
1251
1326
|
|
|
1252
1327
|
try {
|
|
1253
1328
|
log(options, `[global] 全局同步源: ${sourceLabel}`);
|
|
1254
1329
|
for (const target of targets) {
|
|
1255
1330
|
log(options, `[sync] 正在同步全局目标 [${target}] ...`);
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1331
|
+
const plan = planGlobalSyncTasks(target, agentDir);
|
|
1332
|
+
try {
|
|
1333
|
+
for (const task of plan.tasks) {
|
|
1334
|
+
const exists = fs.existsSync(task.destDir);
|
|
1335
|
+
const equal = exists ? areDirectoriesEqual(task.srcDir, task.destDir) : false;
|
|
1336
|
+
if (exists && equal) {
|
|
1337
|
+
task.result = "skipped";
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (exists && !equal && prompter) {
|
|
1342
|
+
const action = await prompter.resolveConflict({
|
|
1343
|
+
category: `global:${task.destination.id}`,
|
|
1344
|
+
label: `全局 Skill ${task.destination.id}/${task.skillName}`,
|
|
1345
|
+
path: task.destDir,
|
|
1346
|
+
});
|
|
1347
|
+
task.action = action;
|
|
1348
|
+
} else if (exists && !equal && !prompter && (options.nonInteractive || !process.stdin.isTTY)) {
|
|
1349
|
+
// 非交互环境保持原有行为:备份后覆盖
|
|
1350
|
+
task.action = "backup";
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (task.action === "keep") {
|
|
1354
|
+
task.result = "skipped";
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (options.dryRun) {
|
|
1359
|
+
log(options, `[dry-run] 将同步全局 Skill: ${task.destination.id}/${task.skillName}`);
|
|
1360
|
+
task.result = "skipped";
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
if (exists && task.action !== "remove") {
|
|
1365
|
+
backupSkillDirectory(task.destination.id, task.skillName, task.destDir, timestamp, options);
|
|
1366
|
+
task.backedUp = true;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const logger = options.quiet ? (() => {}) : log.bind(null, options);
|
|
1370
|
+
AtomicWriter.atomicCopyDir(task.srcDir, task.destDir, { logger });
|
|
1371
|
+
log(options, `[ok] 已同步全局 Skill: ${task.destination.id}/${task.skillName}`);
|
|
1372
|
+
task.result = "synced";
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const summary = summarizeGlobalSync(plan.tasks);
|
|
1376
|
+
if (!options.dryRun) {
|
|
1377
|
+
log(options, `[summary] 全局同步完成 [${target}]:总计 ${summary.total},新增/覆盖 ${summary.synced},跳过 ${summary.skipped},备份 ${summary.backedUp}`);
|
|
1378
|
+
for (const item of summary.destinations) {
|
|
1379
|
+
log(options, ` - ${item.targetName}: ${item.destRoot}(每目标 ${item.total} 个 Skills)`);
|
|
1380
|
+
}
|
|
1261
1381
|
}
|
|
1382
|
+
} finally {
|
|
1383
|
+
if (plan.cleanup) plan.cleanup();
|
|
1262
1384
|
}
|
|
1263
1385
|
}
|
|
1264
1386
|
} finally {
|
|
1265
1387
|
if (cleanup) cleanup();
|
|
1388
|
+
if (prompter) prompter.close();
|
|
1266
1389
|
}
|
|
1267
1390
|
}
|
|
1268
1391
|
|
|
@@ -1437,7 +1560,7 @@ function removeDirIfExists(targetDir, options, label) {
|
|
|
1437
1560
|
log(options, `[clean] 已删除 ${label}: ${targetDir}`);
|
|
1438
1561
|
}
|
|
1439
1562
|
|
|
1440
|
-
function ensureSpecAssetsInstalled(state, timestamp, options) {
|
|
1563
|
+
async function ensureSpecAssetsInstalled(state, timestamp, options, prompter) {
|
|
1441
1564
|
const specHome = getSpecHomeDir();
|
|
1442
1565
|
const assets = {
|
|
1443
1566
|
templates: {
|
|
@@ -1455,25 +1578,70 @@ function ensureSpecAssetsInstalled(state, timestamp, options) {
|
|
|
1455
1578
|
continue;
|
|
1456
1579
|
}
|
|
1457
1580
|
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1581
|
+
const exists = fs.existsSync(config.destDir);
|
|
1582
|
+
let action = exists ? "backup" : "";
|
|
1583
|
+
if (exists && prompter) {
|
|
1584
|
+
action = await prompter.resolveConflict({
|
|
1585
|
+
category: "spec:assets",
|
|
1586
|
+
label: `Spec ${assetName}`,
|
|
1587
|
+
path: config.destDir,
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
if (action === "keep") {
|
|
1592
|
+
log(options, `[skip] 已保留 Spec ${assetName} 资产,不覆盖: ${config.destDir}`);
|
|
1593
|
+
state.assets[assetName] = {
|
|
1594
|
+
destPath: config.destDir,
|
|
1595
|
+
backupPath: "",
|
|
1596
|
+
installedAt: nowISO(),
|
|
1597
|
+
mode: "kept",
|
|
1598
|
+
};
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
let backupPath = "";
|
|
1603
|
+
if (exists && action !== "remove") {
|
|
1604
|
+
backupPath = backupDirSnapshot(
|
|
1605
|
+
config.destDir,
|
|
1606
|
+
path.join(resolveSpecBackupRoot(timestamp), "assets", assetName),
|
|
1607
|
+
options,
|
|
1608
|
+
`Spec ${assetName}`,
|
|
1609
|
+
);
|
|
1610
|
+
} else if (exists && action === "remove") {
|
|
1611
|
+
removeDirIfExists(config.destDir, options, `Spec ${assetName}`);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1464
1614
|
applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
|
|
1465
1615
|
state.assets[assetName] = {
|
|
1466
1616
|
destPath: config.destDir,
|
|
1467
1617
|
backupPath,
|
|
1468
1618
|
installedAt: nowISO(),
|
|
1619
|
+
mode: exists ? (action === "remove" ? "replaced" : "backup") : "created",
|
|
1469
1620
|
};
|
|
1470
1621
|
}
|
|
1471
1622
|
}
|
|
1472
1623
|
|
|
1624
|
+
function normalizeSpecAssetMode(assetState) {
|
|
1625
|
+
if (!assetState || typeof assetState !== "object") {
|
|
1626
|
+
return "";
|
|
1627
|
+
}
|
|
1628
|
+
if (typeof assetState.mode === "string" && assetState.mode) {
|
|
1629
|
+
return assetState.mode;
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof assetState.backupPath === "string" && assetState.backupPath) {
|
|
1632
|
+
return "backup";
|
|
1633
|
+
}
|
|
1634
|
+
return "created";
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1473
1637
|
function restoreSpecAsset(assetState, options, label) {
|
|
1474
1638
|
if (!assetState || typeof assetState.destPath !== "string") {
|
|
1475
1639
|
return;
|
|
1476
1640
|
}
|
|
1641
|
+
const mode = normalizeSpecAssetMode(assetState);
|
|
1642
|
+
if (mode === "kept") {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1477
1645
|
if (assetState.backupPath && fs.existsSync(assetState.backupPath)) {
|
|
1478
1646
|
applyDirSnapshot(assetState.backupPath, assetState.destPath, options, label);
|
|
1479
1647
|
return;
|
|
@@ -1481,7 +1649,7 @@ function restoreSpecAsset(assetState, options, label) {
|
|
|
1481
1649
|
removeDirIfExists(assetState.destPath, options, label);
|
|
1482
1650
|
}
|
|
1483
1651
|
|
|
1484
|
-
function enableSpecTarget(targetName, state, timestamp, options) {
|
|
1652
|
+
async function enableSpecTarget(targetName, state, timestamp, options, prompter) {
|
|
1485
1653
|
if (state.targets[targetName]) {
|
|
1486
1654
|
log(options, `[skip] Spec 目标已启用: ${targetName}`);
|
|
1487
1655
|
return;
|
|
@@ -1501,17 +1669,45 @@ function enableSpecTarget(targetName, state, timestamp, options) {
|
|
|
1501
1669
|
for (const skillName of SPEC_SKILL_NAMES) {
|
|
1502
1670
|
const srcDir = path.join(BUNDLED_SPEC_DIR, "skills", skillName);
|
|
1503
1671
|
const destDir = path.join(destination.skillsRoot, skillName);
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1672
|
+
const exists = fs.existsSync(destDir);
|
|
1673
|
+
let action = exists ? "backup" : "";
|
|
1674
|
+
if (exists && prompter) {
|
|
1675
|
+
action = await prompter.resolveConflict({
|
|
1676
|
+
category: `spec:skills:${destination.id}`,
|
|
1677
|
+
label: `Spec Skill ${destination.id}/${skillName}`,
|
|
1678
|
+
path: destDir,
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
if (action === "keep") {
|
|
1683
|
+
log(options, `[skip] 已保留 Spec Skill,不覆盖: ${destination.id}/${skillName}`);
|
|
1684
|
+
consumerState.skills.push({
|
|
1685
|
+
name: skillName,
|
|
1686
|
+
destPath: destDir,
|
|
1687
|
+
backupPath: "",
|
|
1688
|
+
mode: "kept",
|
|
1689
|
+
});
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
let backupPath = "";
|
|
1694
|
+
if (exists && action !== "remove") {
|
|
1695
|
+
backupPath = backupDirSnapshot(
|
|
1696
|
+
destDir,
|
|
1697
|
+
path.join(resolveSpecBackupRoot(timestamp), destination.id, "skills", skillName),
|
|
1698
|
+
options,
|
|
1699
|
+
`Spec Skill ${destination.id}/${skillName}`,
|
|
1700
|
+
);
|
|
1701
|
+
} else if (exists && action === "remove") {
|
|
1702
|
+
removeDirIfExists(destDir, options, `Spec Skill ${destination.id}/${skillName}`);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1510
1705
|
applyDirSnapshot(srcDir, destDir, options, `Spec Skill ${destination.id}/${skillName}`);
|
|
1511
1706
|
consumerState.skills.push({
|
|
1512
1707
|
name: skillName,
|
|
1513
1708
|
destPath: destDir,
|
|
1514
1709
|
backupPath,
|
|
1710
|
+
mode: exists ? (action === "remove" ? "replaced" : "backup") : "created",
|
|
1515
1711
|
});
|
|
1516
1712
|
}
|
|
1517
1713
|
|
|
@@ -1530,6 +1726,10 @@ function disableSpecTarget(targetName, state, options) {
|
|
|
1530
1726
|
|
|
1531
1727
|
for (const [consumerId, consumerState] of Object.entries(targetState.consumers || {})) {
|
|
1532
1728
|
for (const skill of consumerState.skills || []) {
|
|
1729
|
+
const mode = normalizeSpecAssetMode(skill);
|
|
1730
|
+
if (mode === "kept") {
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1533
1733
|
if (skill.backupPath && fs.existsSync(skill.backupPath)) {
|
|
1534
1734
|
applyDirSnapshot(skill.backupPath, skill.destPath, options, `恢复 Spec Skill ${consumerId}/${skill.name}`);
|
|
1535
1735
|
} else {
|
|
@@ -1608,15 +1808,20 @@ function commandSpecStatus(options) {
|
|
|
1608
1808
|
setQuietStatusExitCode(summary.state);
|
|
1609
1809
|
}
|
|
1610
1810
|
|
|
1611
|
-
function commandSpecEnable(options) {
|
|
1811
|
+
async function commandSpecEnable(options) {
|
|
1612
1812
|
ensureBundledSpecResources();
|
|
1613
1813
|
const targets = resolveTargetsForSpec(options);
|
|
1614
1814
|
const { statePath, state } = readSpecState();
|
|
1615
1815
|
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
1816
|
+
const prompter = createConflictPrompter(options);
|
|
1616
1817
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1818
|
+
try {
|
|
1819
|
+
await ensureSpecAssetsInstalled(state, timestamp, options, prompter);
|
|
1820
|
+
for (const targetName of targets) {
|
|
1821
|
+
await enableSpecTarget(targetName, state, timestamp, options, prompter);
|
|
1822
|
+
}
|
|
1823
|
+
} finally {
|
|
1824
|
+
if (prompter) prompter.close();
|
|
1620
1825
|
}
|
|
1621
1826
|
|
|
1622
1827
|
state.updatedAt = nowISO();
|
|
@@ -1654,13 +1859,13 @@ function commandSpecDisable(options) {
|
|
|
1654
1859
|
log(options, `[ok] Spec Profile 已停用 (Targets: ${targets.join(", ")})`);
|
|
1655
1860
|
}
|
|
1656
1861
|
|
|
1657
|
-
function commandSpec(options) {
|
|
1862
|
+
async function commandSpec(options) {
|
|
1658
1863
|
const subcommand = String(options.subcommand || "status").toLowerCase();
|
|
1659
1864
|
if (subcommand === "status") {
|
|
1660
1865
|
return commandSpecStatus(options);
|
|
1661
1866
|
}
|
|
1662
1867
|
if (subcommand === "enable") {
|
|
1663
|
-
return commandSpecEnable(options);
|
|
1868
|
+
return await commandSpecEnable(options);
|
|
1664
1869
|
}
|
|
1665
1870
|
if (subcommand === "disable") {
|
|
1666
1871
|
return commandSpecDisable(options);
|
|
@@ -1671,12 +1876,83 @@ function commandSpec(options) {
|
|
|
1671
1876
|
async function commandInit(options) {
|
|
1672
1877
|
const workspaceRoot = resolveWorkspaceRoot(options.path);
|
|
1673
1878
|
const targets = await resolveTargetsForInit(options);
|
|
1879
|
+
const prompter = createConflictPrompter(options);
|
|
1674
1880
|
|
|
1675
|
-
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1881
|
+
try {
|
|
1882
|
+
for (const target of targets) {
|
|
1883
|
+
const runOptions = { ...options };
|
|
1884
|
+
const conflicts = [];
|
|
1885
|
+
|
|
1886
|
+
if (target === "gemini") {
|
|
1887
|
+
const agentDir = path.join(workspaceRoot, ".agent");
|
|
1888
|
+
if (fs.existsSync(agentDir)) {
|
|
1889
|
+
conflicts.push({
|
|
1890
|
+
category: "project:gemini",
|
|
1891
|
+
label: ".agent",
|
|
1892
|
+
path: agentDir,
|
|
1893
|
+
target,
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (target === "codex") {
|
|
1899
|
+
const managedDir = path.join(workspaceRoot, ".agents");
|
|
1900
|
+
const legacyDir = path.join(workspaceRoot, ".codex");
|
|
1901
|
+
if (fs.existsSync(managedDir) || fs.existsSync(legacyDir)) {
|
|
1902
|
+
if (fs.existsSync(managedDir)) {
|
|
1903
|
+
conflicts.push({
|
|
1904
|
+
category: "project:codex",
|
|
1905
|
+
label: ".agents",
|
|
1906
|
+
path: managedDir,
|
|
1907
|
+
target,
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
if (fs.existsSync(legacyDir)) {
|
|
1911
|
+
conflicts.push({
|
|
1912
|
+
category: "project:codex",
|
|
1913
|
+
label: ".codex",
|
|
1914
|
+
path: legacyDir,
|
|
1915
|
+
target,
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
if (conflicts.length > 0) {
|
|
1922
|
+
if (!prompter && !runOptions.force) {
|
|
1923
|
+
throw new Error("检测到已有资产且当前环境不可交互,请使用 --force 或在交互终端中重试。");
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
1927
|
+
let shouldSkip = false;
|
|
1928
|
+
|
|
1929
|
+
for (const conflict of conflicts) {
|
|
1930
|
+
const action = prompter ? await prompter.resolveConflict(conflict) : "backup";
|
|
1931
|
+
if (action === "keep") {
|
|
1932
|
+
log(options, `[skip] 已保留现有资产,跳过初始化: ${conflict.label}`);
|
|
1933
|
+
shouldSkip = true;
|
|
1934
|
+
break;
|
|
1935
|
+
}
|
|
1936
|
+
if (action === "backup") {
|
|
1937
|
+
const backupRootName = conflict.label === ".agent" ? ".agent-backup" : ".agents-backup";
|
|
1938
|
+
backupWorkspaceDir(workspaceRoot, conflict.path, backupRootName, timestamp, options, `工作区资产 ${conflict.label}`);
|
|
1939
|
+
}
|
|
1940
|
+
// remove/backup 都需要强制覆盖才能继续
|
|
1941
|
+
runOptions.force = true;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
if (shouldSkip) {
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
const adapter = createAdapter(target, workspaceRoot, runOptions);
|
|
1950
|
+
log(options, `[sync] 正在初始化目标 [${target}] ...`);
|
|
1951
|
+
adapter.install(BUNDLED_AGENT_DIR);
|
|
1952
|
+
registerWorkspaceTarget(workspaceRoot, target, runOptions);
|
|
1953
|
+
}
|
|
1954
|
+
} finally {
|
|
1955
|
+
if (prompter) prompter.close();
|
|
1680
1956
|
}
|
|
1681
1957
|
|
|
1682
1958
|
if (targets.length > 0) {
|
|
@@ -1684,9 +1960,62 @@ async function commandInit(options) {
|
|
|
1684
1960
|
}
|
|
1685
1961
|
}
|
|
1686
1962
|
|
|
1963
|
+
function evaluateCodexUpdateConflict(activeDir) {
|
|
1964
|
+
const manifestPath = path.join(activeDir, "manifest.json");
|
|
1965
|
+
const details = {
|
|
1966
|
+
hasConflict: false,
|
|
1967
|
+
manifestMissing: false,
|
|
1968
|
+
modifiedCount: 0,
|
|
1969
|
+
unknownCount: 0,
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
if (!fs.existsSync(manifestPath)) {
|
|
1973
|
+
details.hasConflict = true;
|
|
1974
|
+
details.manifestMissing = true;
|
|
1975
|
+
return details;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
try {
|
|
1979
|
+
const manager = new ManifestManager(manifestPath, { target: "codex" });
|
|
1980
|
+
manager.load();
|
|
1981
|
+
const drift = manager.checkDrift(activeDir);
|
|
1982
|
+
if (drift.modified.length > 0) {
|
|
1983
|
+
details.hasConflict = true;
|
|
1984
|
+
details.modifiedCount = drift.modified.length;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
const manifestFiles = manager.manifest.files || {};
|
|
1988
|
+
const stack = [activeDir];
|
|
1989
|
+
while (stack.length > 0) {
|
|
1990
|
+
const dir = stack.pop();
|
|
1991
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1992
|
+
for (const entry of entries) {
|
|
1993
|
+
const fullPath = path.join(dir, entry.name);
|
|
1994
|
+
if (entry.isDirectory()) {
|
|
1995
|
+
stack.push(fullPath);
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
const relPath = path.relative(activeDir, fullPath).split(path.sep).join("/");
|
|
1999
|
+
if (!manifestFiles[relPath]) {
|
|
2000
|
+
details.unknownCount += 1;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (details.unknownCount > 0) {
|
|
2005
|
+
details.hasConflict = true;
|
|
2006
|
+
}
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
details.hasConflict = true;
|
|
2009
|
+
details.manifestMissing = true;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
return details;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1687
2015
|
async function commandUpdate(options) {
|
|
1688
2016
|
const workspaceRoot = resolveWorkspaceRoot(options.path);
|
|
1689
2017
|
const targets = resolveTargetsForUpdate(workspaceRoot, options);
|
|
2018
|
+
const prompter = createConflictPrompter(options);
|
|
1690
2019
|
|
|
1691
2020
|
if (targets.length === 0) {
|
|
1692
2021
|
throw new Error(`此目录未检测到 ${PRIMARY_CLI_NAME} 安装,无法更新。请先执行 init。`);
|
|
@@ -1695,7 +2024,8 @@ async function commandUpdate(options) {
|
|
|
1695
2024
|
log(options, `[update] 正在更新 Ling (Targets: ${targets.join(", ")})...`);
|
|
1696
2025
|
|
|
1697
2026
|
let updatedAny = false;
|
|
1698
|
-
|
|
2027
|
+
try {
|
|
2028
|
+
for (const target of targets) {
|
|
1699
2029
|
if (!isTargetInstalled(workspaceRoot, target) && options.targets.length > 0) {
|
|
1700
2030
|
throw new Error(`目标未安装: ${target}`);
|
|
1701
2031
|
}
|
|
@@ -1705,15 +2035,69 @@ async function commandUpdate(options) {
|
|
|
1705
2035
|
}
|
|
1706
2036
|
|
|
1707
2037
|
const runOptions = { ...options, force: true };
|
|
2038
|
+
|
|
2039
|
+
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
2040
|
+
if (target === "gemini") {
|
|
2041
|
+
const agentDir = path.join(workspaceRoot, ".agent");
|
|
2042
|
+
if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
|
|
2043
|
+
let action = "backup";
|
|
2044
|
+
if (prompter) {
|
|
2045
|
+
action = await prompter.resolveConflict({
|
|
2046
|
+
category: "project:gemini",
|
|
2047
|
+
label: ".agent",
|
|
2048
|
+
path: agentDir,
|
|
2049
|
+
target,
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
if (action === "keep") {
|
|
2053
|
+
log(options, "[skip] 已保留现有资产,跳过更新: .agent");
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
if (action === "backup") {
|
|
2057
|
+
backupWorkspaceDir(workspaceRoot, agentDir, ".agent-backup", timestamp, options, "工作区资产 .agent");
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
if (target === "codex") {
|
|
2063
|
+
const managedDir = path.join(workspaceRoot, ".agents");
|
|
2064
|
+
const legacyDir = path.join(workspaceRoot, ".codex");
|
|
2065
|
+
const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
|
|
2066
|
+
const conflict = evaluateCodexUpdateConflict(activeDir);
|
|
2067
|
+
if (conflict.hasConflict) {
|
|
2068
|
+
let action = "backup";
|
|
2069
|
+
if (prompter) {
|
|
2070
|
+
action = await prompter.resolveConflict({
|
|
2071
|
+
category: "project:codex",
|
|
2072
|
+
label: fs.existsSync(managedDir) ? ".agents" : ".codex",
|
|
2073
|
+
path: activeDir,
|
|
2074
|
+
target,
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
if (action === "keep") {
|
|
2078
|
+
log(options, `[skip] 已保留现有资产,跳过更新: ${path.basename(activeDir)}`);
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
if (action === "backup") {
|
|
2082
|
+
const backupRootName = ".agents-backup";
|
|
2083
|
+
backupWorkspaceDir(workspaceRoot, activeDir, backupRootName, timestamp, options, `工作区资产 ${path.basename(activeDir)}`);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1708
2088
|
const adapter = createAdapter(target, workspaceRoot, runOptions);
|
|
1709
2089
|
log(options, `[sync] 更新 [${target}] ...`);
|
|
1710
2090
|
adapter.update(BUNDLED_AGENT_DIR);
|
|
1711
2091
|
registerWorkspaceTarget(workspaceRoot, target, runOptions);
|
|
1712
2092
|
updatedAny = true;
|
|
2093
|
+
}
|
|
2094
|
+
} finally {
|
|
2095
|
+
if (prompter) prompter.close();
|
|
1713
2096
|
}
|
|
1714
2097
|
|
|
1715
2098
|
if (!updatedAny) {
|
|
1716
|
-
|
|
2099
|
+
log(options, "[skip] 未执行更新(已保留现有资产)");
|
|
2100
|
+
return;
|
|
1717
2101
|
}
|
|
1718
2102
|
}
|
|
1719
2103
|
|
|
@@ -1754,6 +2138,7 @@ async function commandUpdateAll(options) {
|
|
|
1754
2138
|
|
|
1755
2139
|
log(options, `[update] 开始批量更新工作区(共 ${records.length} 个)...`);
|
|
1756
2140
|
log(options, `[index] 索引文件: ${indexPath}`);
|
|
2141
|
+
const prompter = createConflictPrompter(options);
|
|
1757
2142
|
|
|
1758
2143
|
let updated = 0;
|
|
1759
2144
|
let skipped = 0;
|
|
@@ -1764,7 +2149,8 @@ async function commandUpdateAll(options) {
|
|
|
1764
2149
|
const nextRecords = [];
|
|
1765
2150
|
const removedRecordKeys = new Set();
|
|
1766
2151
|
|
|
1767
|
-
|
|
2152
|
+
try {
|
|
2153
|
+
for (let i = 0; i < records.length; i++) {
|
|
1768
2154
|
const item = normalizeWorkspaceRecordV2(records[i], normalizeAbsolutePath(records[i].path));
|
|
1769
2155
|
const workspacePath = normalizeAbsolutePath(item.path);
|
|
1770
2156
|
const exclusion = evaluateWorkspaceExclusion(index, workspacePath);
|
|
@@ -1825,6 +2211,54 @@ async function commandUpdateAll(options) {
|
|
|
1825
2211
|
path: workspacePath,
|
|
1826
2212
|
silentIndexLog: true,
|
|
1827
2213
|
};
|
|
2214
|
+
|
|
2215
|
+
const timestampForBackup = nowISO().replace(/[:.]/g, "-");
|
|
2216
|
+
|
|
2217
|
+
if (target === "gemini") {
|
|
2218
|
+
const agentDir = path.join(workspacePath, ".agent");
|
|
2219
|
+
if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
|
|
2220
|
+
let action = "backup";
|
|
2221
|
+
if (prompter) {
|
|
2222
|
+
action = await prompter.resolveConflict({
|
|
2223
|
+
category: "update-all:project:gemini",
|
|
2224
|
+
label: `.agent (${workspacePath})`,
|
|
2225
|
+
path: agentDir,
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
if (action === "keep") {
|
|
2229
|
+
log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [gemini]`);
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
if (action === "backup") {
|
|
2233
|
+
backupWorkspaceDir(workspacePath, agentDir, ".agent-backup", timestampForBackup, options, `工作区资产 .agent (${workspacePath})`);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
if (target === "codex") {
|
|
2239
|
+
const managedDir = path.join(workspacePath, ".agents");
|
|
2240
|
+
const legacyDir = path.join(workspacePath, ".codex");
|
|
2241
|
+
const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
|
|
2242
|
+
const conflict = evaluateCodexUpdateConflict(activeDir);
|
|
2243
|
+
if (conflict.hasConflict) {
|
|
2244
|
+
let action = "backup";
|
|
2245
|
+
if (prompter) {
|
|
2246
|
+
action = await prompter.resolveConflict({
|
|
2247
|
+
category: "update-all:project:codex",
|
|
2248
|
+
label: `${path.basename(activeDir)} (${workspacePath})`,
|
|
2249
|
+
path: activeDir,
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
if (action === "keep") {
|
|
2253
|
+
log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [codex]`);
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
if (action === "backup") {
|
|
2257
|
+
backupWorkspaceDir(workspacePath, activeDir, ".agents-backup", timestampForBackup, options, `工作区资产 ${path.basename(activeDir)} (${workspacePath})`);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
1828
2262
|
const adapter = createAdapter(target, workspacePath, runOptions);
|
|
1829
2263
|
adapter.update(BUNDLED_AGENT_DIR);
|
|
1830
2264
|
updatedTargets.push(target);
|
|
@@ -1844,44 +2278,49 @@ async function commandUpdateAll(options) {
|
|
|
1844
2278
|
skipped += 1;
|
|
1845
2279
|
nextRecords.push(item);
|
|
1846
2280
|
}
|
|
1847
|
-
|
|
2281
|
+
}
|
|
1848
2282
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2283
|
+
if (!options.dryRun) {
|
|
2284
|
+
withWorkspaceIndexLock(indexPath, () => {
|
|
2285
|
+
const { index: latestIndex } = readWorkspaceIndex();
|
|
2286
|
+
const mergedMap = new Map();
|
|
1853
2287
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2288
|
+
for (const item of latestIndex.workspaces || []) {
|
|
2289
|
+
if (!item || typeof item.path !== "string") continue;
|
|
2290
|
+
mergedMap.set(pathCompareKey(item.path), normalizeWorkspaceRecordV2(item, normalizeAbsolutePath(item.path)));
|
|
2291
|
+
}
|
|
1858
2292
|
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2293
|
+
for (const removedKey of removedRecordKeys) {
|
|
2294
|
+
mergedMap.delete(removedKey);
|
|
2295
|
+
}
|
|
1862
2296
|
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
2297
|
+
for (const item of nextRecords) {
|
|
2298
|
+
if (!item || typeof item.path !== "string") continue;
|
|
2299
|
+
mergedMap.set(pathCompareKey(item.path), normalizeWorkspaceRecordV2(item, normalizeAbsolutePath(item.path)));
|
|
2300
|
+
}
|
|
1867
2301
|
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2302
|
+
latestIndex.workspaces = Array.from(mergedMap.values()).sort((a, b) => a.path.localeCompare(b.path));
|
|
2303
|
+
latestIndex.updatedAt = timestamp;
|
|
2304
|
+
writeWorkspaceIndex(indexPath, latestIndex);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
1873
2307
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2308
|
+
log(options, "[summary] 批量更新完成");
|
|
2309
|
+
log(options, ` 成功: ${updated}`);
|
|
2310
|
+
log(options, ` 跳过: ${skipped}`);
|
|
2311
|
+
log(options, ` 失败: ${failed}`);
|
|
2312
|
+
log(options, ` 清理排除路径: ${removedExcluded}`);
|
|
2313
|
+
if (options.pruneMissing) {
|
|
2314
|
+
log(options, ` 清理失效索引: ${removedMissing}`);
|
|
2315
|
+
}
|
|
1882
2316
|
|
|
1883
|
-
|
|
1884
|
-
|
|
2317
|
+
if (failed > 0) {
|
|
2318
|
+
process.exitCode = 1;
|
|
2319
|
+
}
|
|
2320
|
+
} finally {
|
|
2321
|
+
if (prompter) {
|
|
2322
|
+
prompter.close();
|
|
2323
|
+
}
|
|
1885
2324
|
}
|
|
1886
2325
|
}
|
|
1887
2326
|
|
|
@@ -2191,14 +2630,8 @@ function commandStatus(options) {
|
|
|
2191
2630
|
|
|
2192
2631
|
async function main() {
|
|
2193
2632
|
try {
|
|
2194
|
-
const migration = migrateLegacyControlHomeDir();
|
|
2195
2633
|
const { command, options, providedFlags } = parseArgs(process.argv.slice(2));
|
|
2196
2634
|
|
|
2197
|
-
if (migration && !options.quiet) {
|
|
2198
|
-
console.log(`[migrate] 已迁移控制目录: ${migration.from} -> ${migration.to}`);
|
|
2199
|
-
}
|
|
2200
|
-
maybeWarnLegacyCliAlias(options);
|
|
2201
|
-
|
|
2202
2635
|
if (!command || command === "--help" || command === "-h") {
|
|
2203
2636
|
printUsage();
|
|
2204
2637
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -2240,7 +2673,7 @@ async function main() {
|
|
|
2240
2673
|
}
|
|
2241
2674
|
|
|
2242
2675
|
if (command === "spec") {
|
|
2243
|
-
commandSpec(options);
|
|
2676
|
+
await commandSpec(options);
|
|
2244
2677
|
return;
|
|
2245
2678
|
}
|
|
2246
2679
|
|