@mison/ling 1.1.1 → 1.2.2

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/bin/ling-cli.js CHANGED
@@ -19,7 +19,8 @@ const PRIMARY_CLI_NAME = "ling";
19
19
  const WORKSPACE_INDEX_VERSION = 2;
20
20
  const UPSTREAM_GLOBAL_PACKAGE = "@vudovn/ag-kit";
21
21
  const TOOLKIT_PACKAGE_NAMES = new Set(["@mison/ling", "@mison/ag-kit-cn", "antigravity-kit-cn", "antigravity-kit"]);
22
- const SUPPORTED_TARGETS = ["gemini", "codex"];
22
+ const SUPPORTED_TARGETS = ["gemini", "antigravity", "codex"];
23
+ const SHARED_AGENT_TARGETS = ["gemini", "antigravity"];
23
24
  const LEGACY_INDEX_TARGET_ALIASES = {
24
25
  full: "gemini",
25
26
  };
@@ -27,8 +28,8 @@ const GLOBAL_TARGET_DESTINATIONS = {
27
28
  codex: [
28
29
  {
29
30
  id: "codex",
30
- rootParts: [".codex"],
31
- skillsParts: [".codex", "skills"],
31
+ rootParts: [".agents"],
32
+ skillsParts: [".agents", "skills"],
32
33
  },
33
34
  ],
34
35
  gemini: [
@@ -37,6 +38,8 @@ const GLOBAL_TARGET_DESTINATIONS = {
37
38
  rootParts: [".gemini", "skills"],
38
39
  skillsParts: [".gemini", "skills"],
39
40
  },
41
+ ],
42
+ antigravity: [
40
43
  {
41
44
  id: "antigravity",
42
45
  rootParts: [".gemini", "antigravity"],
@@ -58,6 +61,7 @@ const VERSION_TAG_PREFIX = "ling-";
58
61
  const SPEC_TEMPLATE_REQUIRED_FILES = ["issues.template.csv", "driver-prompt.md", "review-report.md", "phase-acceptance.md", "handoff.md"];
59
62
  const SPEC_REFERENCE_REQUIRED_FILES = ["README.md", "harness-engineering-digest.md", "gda-framework.md", "cse-quickstart.md"];
60
63
  const SPEC_PROFILE_REQUIRED_FILES = ["codex/AGENTS.spec.md", "codex/ling.spec.rules.md", "gemini/GEMINI.spec.md"];
64
+ const LEGACY_CODEX_GLOBAL_SKILLS_BACKUP_TARGET = "codex-legacy";
61
65
 
62
66
  function nowISO() {
63
67
  return new Date().toISOString();
@@ -170,13 +174,13 @@ function printUsage() {
170
174
  console.log(` ${PRIMARY_CLI_NAME} update [--path <dir>] [--branch <name>] [--target <name>|--targets <a,b>] [--no-index] [--quiet] [--dry-run]`);
171
175
  console.log(` ${PRIMARY_CLI_NAME} update-all [--branch <name>] [--targets <a,b>] [--prune-missing] [--quiet] [--dry-run]`);
172
176
  console.log(` ${PRIMARY_CLI_NAME} doctor [--path <dir>] [--target <name>|--targets <a,b>] [--fix] [--quiet]`);
173
- console.log(` ${PRIMARY_CLI_NAME} global sync [--target <name>|--targets <a,b>] [--branch <name>] [--quiet] [--dry-run] # 默认同步 codex + gemini(cli+antigravity)`);
177
+ console.log(` ${PRIMARY_CLI_NAME} global sync [--target <name>|--targets <a,b>] [--branch <name>] [--quiet] [--dry-run] # 默认同步 codex + gemini + antigravity`);
174
178
  console.log(` ${PRIMARY_CLI_NAME} global status [--quiet]`);
175
179
  console.log(` ${PRIMARY_CLI_NAME} spec enable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
176
180
  console.log(` ${PRIMARY_CLI_NAME} spec disable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
177
181
  console.log(` ${PRIMARY_CLI_NAME} spec status [--quiet]`);
178
- console.log(` ${PRIMARY_CLI_NAME} spec init [--path <dir>] [--target <name>|--targets <a,b>] [--branch <name>] [--force] [--non-interactive] [--no-index] [--quiet] [--dry-run]`);
179
- console.log(` ${PRIMARY_CLI_NAME} spec doctor [--path <dir>] [--quiet]`);
182
+ console.log(` ${PRIMARY_CLI_NAME} spec init [--path <dir>] [--spec-workspace] [--csv-only] [--target <name>|--targets <a,b>] [--branch <name>] [--force] [--non-interactive] [--no-index] [--quiet] [--dry-run]`);
183
+ console.log(` ${PRIMARY_CLI_NAME} spec doctor [--path <dir>] [--spec-workspace] [--quiet]`);
180
184
  console.log(` ${PRIMARY_CLI_NAME} exclude list [--quiet]`);
181
185
  console.log(` ${PRIMARY_CLI_NAME} exclude add --path <dir> [--dry-run] [--quiet]`);
182
186
  console.log(` ${PRIMARY_CLI_NAME} exclude remove --path <dir> [--dry-run] [--quiet]`);
@@ -202,6 +206,8 @@ function parseArgs(argv) {
202
206
  nonInteractive: false,
203
207
  noIndex: false,
204
208
  fix: false,
209
+ csvOnly: false,
210
+ specWorkspace: false,
205
211
  subcommand: "",
206
212
  path: "",
207
213
  branch: "",
@@ -252,6 +258,12 @@ function parseArgs(argv) {
252
258
  } else if (arg === "--fix") {
253
259
  providedFlags.push(arg);
254
260
  options.fix = true;
261
+ } else if (arg === "--csv-only") {
262
+ providedFlags.push(arg);
263
+ options.csvOnly = true;
264
+ } else if (arg === "--spec-workspace") {
265
+ providedFlags.push(arg);
266
+ options.specWorkspace = true;
255
267
  } else if (arg === "--path") {
256
268
  providedFlags.push(arg);
257
269
  if (i + 1 >= argv.length) {
@@ -295,8 +307,8 @@ const COMMAND_ALLOWED_FLAGS = {
295
307
  "spec:enable": ["--target", "--targets", "--quiet", "--dry-run"],
296
308
  "spec:disable": ["--target", "--targets", "--quiet", "--dry-run"],
297
309
  "spec:status": ["--quiet"],
298
- "spec:init": ["--force", "--path", "--branch", "--target", "--targets", "--non-interactive", "--no-index", "--quiet", "--dry-run"],
299
- "spec:doctor": ["--path", "--quiet"],
310
+ "spec:init": ["--force", "--path", "--spec-workspace", "--branch", "--csv-only", "--target", "--targets", "--non-interactive", "--no-index", "--quiet", "--dry-run"],
311
+ "spec:doctor": ["--path", "--spec-workspace", "--quiet"],
300
312
  "exclude:list": ["--quiet"],
301
313
  "exclude:add": ["--path", "--dry-run", "--quiet"],
302
314
  "exclude:remove": ["--path", "--dry-run", "--quiet"],
@@ -389,6 +401,12 @@ function pathCompareKey(inputPath) {
389
401
  return normalized;
390
402
  }
391
403
 
404
+ function findWorkspaceRecord(index, workspaceRoot) {
405
+ const normalizedPath = normalizeAbsolutePath(workspaceRoot);
406
+ const targetKey = pathCompareKey(normalizedPath);
407
+ return (index.workspaces || []).find((item) => pathCompareKey(item.path) === targetKey) || null;
408
+ }
409
+
392
410
  function normalizePathList(items) {
393
411
  const map = new Map();
394
412
  for (const item of items) {
@@ -627,6 +645,95 @@ function writeWorkspaceIndex(indexPath, index) {
627
645
  fs.writeFileSync(indexPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
628
646
  }
629
647
 
648
+ function getWorkspaceInstallStatePath(workspaceRoot) {
649
+ return path.join(normalizeAbsolutePath(workspaceRoot), ".ling", "install-state.json");
650
+ }
651
+
652
+ function createEmptyWorkspaceInstallState() {
653
+ return {
654
+ version: 1,
655
+ updatedAt: "",
656
+ targets: {},
657
+ };
658
+ }
659
+
660
+ function readWorkspaceInstallState(workspaceRoot) {
661
+ const statePath = getWorkspaceInstallStatePath(workspaceRoot);
662
+ if (!fs.existsSync(statePath)) {
663
+ return { statePath, state: createEmptyWorkspaceInstallState() };
664
+ }
665
+
666
+ const raw = fs.readFileSync(statePath, "utf8").trim();
667
+ if (!raw) {
668
+ return { statePath, state: createEmptyWorkspaceInstallState() };
669
+ }
670
+
671
+ let parsed;
672
+ try {
673
+ parsed = JSON.parse(raw);
674
+ } catch (_err) {
675
+ return { statePath, state: createEmptyWorkspaceInstallState() };
676
+ }
677
+
678
+ const state = createEmptyWorkspaceInstallState();
679
+ state.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
680
+ if (parsed && parsed.targets && typeof parsed.targets === "object") {
681
+ for (const [targetName, targetState] of Object.entries(parsed.targets)) {
682
+ const normalizedTargetName = normalizeIndexTargetName(targetName);
683
+ const normalizedTargetState = normalizeTargetState(targetState);
684
+ if (normalizedTargetName && normalizedTargetState) {
685
+ state.targets[normalizedTargetName] = normalizedTargetState;
686
+ }
687
+ }
688
+ }
689
+
690
+ return { statePath, state };
691
+ }
692
+
693
+ function writeWorkspaceInstallState(statePath, state) {
694
+ const payload = {
695
+ version: 1,
696
+ updatedAt: state.updatedAt || nowISO(),
697
+ targets: state.targets || {},
698
+ };
699
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
700
+ fs.writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
701
+ }
702
+
703
+ function recordWorkspaceInstallTargets(workspaceRoot, targetNames, options) {
704
+ const logicalTargets = normalizeTargets(targetNames).filter((targetName) => SHARED_AGENT_TARGETS.includes(targetName));
705
+ if (logicalTargets.length === 0) {
706
+ return;
707
+ }
708
+
709
+ if (options.dryRun) {
710
+ log(options, `[dry-run] 将写入工作区安装状态: ${getWorkspaceInstallStatePath(workspaceRoot)} [${logicalTargets.join(", ")}]`);
711
+ return;
712
+ }
713
+
714
+ const { statePath, state } = readWorkspaceInstallState(workspaceRoot);
715
+ const timestamp = nowISO();
716
+ for (const targetName of logicalTargets) {
717
+ const prev = normalizeTargetState(state.targets[targetName]) || {
718
+ version: "",
719
+ installedAt: "",
720
+ updatedAt: "",
721
+ };
722
+ state.targets[targetName] = {
723
+ version: pkg.version,
724
+ installedAt: prev.installedAt || timestamp,
725
+ updatedAt: timestamp,
726
+ };
727
+ }
728
+ state.updatedAt = timestamp;
729
+ writeWorkspaceInstallState(statePath, state);
730
+ }
731
+
732
+ function resolveWorkspaceInstallStateTargets(workspaceRoot) {
733
+ const { state } = readWorkspaceInstallState(workspaceRoot);
734
+ return normalizeTargets(Object.keys(state.targets || {}));
735
+ }
736
+
630
737
  function sleepSync(ms) {
631
738
  const buffer = new SharedArrayBuffer(4);
632
739
  const view = new Int32Array(buffer);
@@ -906,19 +1013,41 @@ function normalizeTargets(rawTargets) {
906
1013
  return result;
907
1014
  }
908
1015
 
1016
+ function resolveIndexedWorkspaceTargets(workspaceRoot) {
1017
+ try {
1018
+ const { index } = readWorkspaceIndex();
1019
+ const record = findWorkspaceRecord(index, workspaceRoot);
1020
+ if (!record) {
1021
+ return [];
1022
+ }
1023
+ return normalizeTargets(Object.keys(record.targets || {}));
1024
+ } catch (_err) {
1025
+ return [];
1026
+ }
1027
+ }
1028
+
909
1029
  function detectInstalledTargets(workspaceRoot) {
910
1030
  const targets = [];
1031
+ const localTargets = resolveWorkspaceInstallStateTargets(workspaceRoot);
1032
+ const indexedTargets = resolveIndexedWorkspaceTargets(workspaceRoot);
911
1033
  if (fs.existsSync(path.join(workspaceRoot, ".agent"))) {
912
- targets.push("gemini");
1034
+ const sharedTargets = localTargets
1035
+ .filter((target) => SHARED_AGENT_TARGETS.includes(target))
1036
+ .concat(indexedTargets.filter((target) => SHARED_AGENT_TARGETS.includes(target)));
1037
+ if (sharedTargets.length > 0) {
1038
+ targets.push(...sharedTargets);
1039
+ } else {
1040
+ targets.push("gemini");
1041
+ }
913
1042
  }
914
1043
  if (fs.existsSync(path.join(workspaceRoot, ".agents")) || fs.existsSync(path.join(workspaceRoot, ".codex"))) {
915
1044
  targets.push("codex");
916
1045
  }
917
- return targets;
1046
+ return normalizeTargets(targets);
918
1047
  }
919
1048
 
920
1049
  function isTargetInstalled(workspaceRoot, targetName) {
921
- if (targetName === "gemini") {
1050
+ if (SHARED_AGENT_TARGETS.includes(targetName)) {
922
1051
  return fs.existsSync(path.join(workspaceRoot, ".agent"));
923
1052
  }
924
1053
  if (targetName === "codex") {
@@ -927,6 +1056,27 @@ function isTargetInstalled(workspaceRoot, targetName) {
927
1056
  return false;
928
1057
  }
929
1058
 
1059
+ function groupTargetsByInstallSurface(targets) {
1060
+ const normalizedTargets = normalizeTargets(targets);
1061
+ const groups = [];
1062
+ const sharedTargets = normalizedTargets.filter((target) => SHARED_AGENT_TARGETS.includes(target));
1063
+ if (sharedTargets.length > 0) {
1064
+ groups.push({
1065
+ installSurface: ".agent",
1066
+ adapterTarget: sharedTargets.includes("gemini") ? "gemini" : "antigravity",
1067
+ logicalTargets: sharedTargets,
1068
+ });
1069
+ }
1070
+ if (normalizedTargets.includes("codex")) {
1071
+ groups.push({
1072
+ installSurface: ".agents",
1073
+ adapterTarget: "codex",
1074
+ logicalTargets: ["codex"],
1075
+ });
1076
+ }
1077
+ return groups;
1078
+ }
1079
+
930
1080
  function setQuietStatusExitCode(state) {
931
1081
  process.exitCode = Object.prototype.hasOwnProperty.call(QUIET_STATUS_EXIT_CODES, state)
932
1082
  ? QUIET_STATUS_EXIT_CODES[state]
@@ -1005,6 +1155,22 @@ function evaluateGlobalState() {
1005
1155
  };
1006
1156
  }).filter((item) => item.state !== "missing");
1007
1157
 
1158
+ const legacyCodexSkillsRoot = path.join(globalRoot, ".codex", "skills");
1159
+ const hasCurrentCodexRoot = targetStates.some((item) => item.targetName === "codex");
1160
+ if (!hasCurrentCodexRoot && fs.existsSync(legacyCodexSkillsRoot)) {
1161
+ targetStates.push({
1162
+ targetName: "codex",
1163
+ family: "codex",
1164
+ state: "broken",
1165
+ rootDir: path.join(globalRoot, ".codex"),
1166
+ skillsRoot: legacyCodexSkillsRoot,
1167
+ skillsCount: countSkillsRecursive(legacyCodexSkillsRoot),
1168
+ issues: [
1169
+ "检测到旧版 Codex 全局 Skill 目录;当前版本改用 ~/.agents/skills(运行: ling global sync --target codex)",
1170
+ ],
1171
+ });
1172
+ }
1173
+
1008
1174
  if (targetStates.length === 0) {
1009
1175
  return {
1010
1176
  globalRoot,
@@ -1022,8 +1188,11 @@ function evaluateGlobalState() {
1022
1188
  }
1023
1189
 
1024
1190
  function createAdapter(targetName, workspaceRoot, options) {
1025
- if (targetName === "gemini") {
1026
- return new GeminiAdapter(workspaceRoot, options);
1191
+ if (SHARED_AGENT_TARGETS.includes(targetName)) {
1192
+ return new GeminiAdapter(workspaceRoot, {
1193
+ ...options,
1194
+ targetName,
1195
+ });
1027
1196
  }
1028
1197
  if (targetName === "codex") {
1029
1198
  return new CodexAdapter(workspaceRoot, options);
@@ -1067,8 +1236,7 @@ function resolveTargetsForGlobalSync(options) {
1067
1236
  if (requested.length > 0) {
1068
1237
  return requested;
1069
1238
  }
1070
- // 保持 global sync 简洁:默认同步 codex + gemini;其中 gemini 会展开为 gemini-cli 与 antigravity。
1071
- return ["codex", "gemini"];
1239
+ return [...SUPPORTED_TARGETS];
1072
1240
  }
1073
1241
 
1074
1242
  function resolveAgentInstallSource(options) {
@@ -1120,6 +1288,74 @@ function backupSkillDirectory(targetName, skillName, sourceDir, timestamp, optio
1120
1288
  log(options, `[backup] 已备份 ${targetName} 全局 Skill: ${skillName} -> ${backupDir}`);
1121
1289
  }
1122
1290
 
1291
+ function migrateLegacyCodexGlobalSkills(timestamp, options) {
1292
+ const globalRoot = resolveGlobalRootDir();
1293
+ const legacyRoot = path.join(globalRoot, ".codex", "skills");
1294
+ if (!fs.existsSync(legacyRoot)) {
1295
+ return { migrated: 0, removed: 0, backedUp: 0, conflicts: 0, remaining: 0 };
1296
+ }
1297
+
1298
+ const destRoot = path.join(globalRoot, ".agents", "skills");
1299
+ const legacyDirEntries = fs.readdirSync(legacyRoot, { withFileTypes: true });
1300
+ const legacyEntries = legacyDirEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1301
+ if (legacyEntries.length === 0) {
1302
+ if (legacyDirEntries.length === 0) {
1303
+ removeDirIfExists(legacyRoot, options, "遗留 Codex skills 根目录");
1304
+ } else {
1305
+ log(options, `[warn] 遗留 Codex skills 根目录包含非目录条目,已跳过迁移清理: ${legacyRoot}`);
1306
+ }
1307
+ return { migrated: 0, removed: 0, backedUp: 0, conflicts: 0, remaining: legacyDirEntries.length };
1308
+ }
1309
+
1310
+ let migrated = 0;
1311
+ let removed = 0;
1312
+ let backedUp = 0;
1313
+ let conflicts = 0;
1314
+
1315
+ for (const entryName of legacyEntries) {
1316
+ const legacyDir = path.join(legacyRoot, entryName);
1317
+ const destDir = path.join(destRoot, entryName);
1318
+ const destExists = fs.existsSync(destDir);
1319
+
1320
+ if (!destExists) {
1321
+ if (options.dryRun) {
1322
+ log(options, `[dry-run] 将迁移遗留 Codex Skill 目录: ${legacyDir} -> ${destDir}`);
1323
+ } else {
1324
+ const logger = options.quiet ? (() => {}) : log.bind(null, options);
1325
+ AtomicWriter.atomicCopyDir(legacyDir, destDir, { logger });
1326
+ log(options, `[ok] 已迁移遗留 Codex Skill 目录: ${legacyDir} -> ${destDir}`);
1327
+ }
1328
+ migrated += 1;
1329
+ } else if (!areDirectoriesEqual(legacyDir, destDir)) {
1330
+ conflicts += 1;
1331
+ if (options.dryRun) {
1332
+ log(options, `[dry-run] 将备份并移除遗留 Codex Skill(与现有目录冲突): ${legacyDir}`);
1333
+ } else {
1334
+ backupSkillDirectory(LEGACY_CODEX_GLOBAL_SKILLS_BACKUP_TARGET, entryName, legacyDir, timestamp, options);
1335
+ backedUp += 1;
1336
+ }
1337
+ }
1338
+
1339
+ if (options.dryRun) {
1340
+ log(options, `[dry-run] 将删除遗留 Codex Skill 目录: ${legacyDir}`);
1341
+ continue;
1342
+ }
1343
+
1344
+ fs.rmSync(legacyDir, { recursive: true, force: true });
1345
+ log(options, `[clean] 已删除遗留 Codex Skill 目录: ${legacyDir}`);
1346
+ removed += 1;
1347
+ }
1348
+
1349
+ const remaining = fs.existsSync(legacyRoot) ? fs.readdirSync(legacyRoot).length : 0;
1350
+ if (remaining === 0) {
1351
+ removeDirIfExists(legacyRoot, options, "遗留 Codex skills 根目录");
1352
+ } else {
1353
+ log(options, `[warn] 遗留 Codex skills 根目录仍有残留条目(${remaining}),已保留: ${legacyRoot}`);
1354
+ }
1355
+
1356
+ return { migrated, removed, backedUp, conflicts, remaining };
1357
+ }
1358
+
1123
1359
  function syncSkillDirectory(destination, srcDir, destDir, timestamp, options) {
1124
1360
  const exists = fs.existsSync(destDir);
1125
1361
  if (exists) {
@@ -1235,7 +1471,7 @@ function planGlobalSyncTasks(targetName, agentDir) {
1235
1471
  };
1236
1472
  }
1237
1473
 
1238
- if (targetName === "gemini") {
1474
+ if (SHARED_AGENT_TARGETS.includes(targetName)) {
1239
1475
  const skillsRoot = path.join(agentDir, "skills");
1240
1476
  const skillNames = listSkillDirectories(skillsRoot);
1241
1477
  const tasks = [];
@@ -1317,7 +1553,7 @@ function applyGlobalSync(targetName, agentDir, timestamp, options) {
1317
1553
  }
1318
1554
  }
1319
1555
 
1320
- if (targetName === "gemini") {
1556
+ if (SHARED_AGENT_TARGETS.includes(targetName)) {
1321
1557
  const skillsRoot = path.join(agentDir, "skills");
1322
1558
  return syncGlobalSkillsFromRoot(targetName, skillsRoot, timestamp, options);
1323
1559
  }
@@ -1386,6 +1622,10 @@ async function commandGlobalSync(options) {
1386
1622
  log(options, ` - ${item.targetName}: ${item.destRoot}(每目标 ${item.total} 个 Skills)`);
1387
1623
  }
1388
1624
  }
1625
+
1626
+ if (target === "codex") {
1627
+ migrateLegacyCodexGlobalSkills(timestamp, options);
1628
+ }
1389
1629
  } finally {
1390
1630
  if (plan.cleanup) plan.cleanup();
1391
1631
  }
@@ -1475,15 +1715,121 @@ function createEmptySpecState() {
1475
1715
  };
1476
1716
  }
1477
1717
 
1718
+ function rewriteLegacyCodexGlobalSkillPath(targetPath) {
1719
+ if (typeof targetPath !== "string" || targetPath === "") {
1720
+ return targetPath;
1721
+ }
1722
+ const normalizedPath = targetPath.replace(/\\/g, "/");
1723
+ if (!normalizedPath.includes("/.codex/skills")) {
1724
+ return targetPath;
1725
+ }
1726
+ return targetPath.replace(/([\\/])\.codex([\\/])skills/g, "$1.agents$2skills");
1727
+ }
1728
+
1729
+ function normalizeSpecSkillState(targetName, consumerId, skillState) {
1730
+ if (!skillState || typeof skillState !== "object" || typeof skillState.name !== "string" || !skillState.name) {
1731
+ return null;
1732
+ }
1733
+ const destPath = typeof skillState.destPath === "string" ? skillState.destPath : "";
1734
+ return {
1735
+ name: skillState.name,
1736
+ destPath: targetName === "codex" && consumerId === "codex"
1737
+ ? rewriteLegacyCodexGlobalSkillPath(destPath)
1738
+ : destPath,
1739
+ backupPath: typeof skillState.backupPath === "string" ? skillState.backupPath : "",
1740
+ mode: normalizeSpecAssetMode(skillState),
1741
+ };
1742
+ }
1743
+
1744
+ function normalizeSpecConsumerState(targetName, consumerId, consumerState) {
1745
+ const skills = [];
1746
+ let migrated = false;
1747
+ for (const skillState of Array.isArray(consumerState && consumerState.skills) ? consumerState.skills : []) {
1748
+ const normalizedSkill = normalizeSpecSkillState(targetName, consumerId, skillState);
1749
+ if (normalizedSkill) {
1750
+ skills.push(normalizedSkill);
1751
+ if (normalizedSkill.destPath !== (typeof skillState.destPath === "string" ? skillState.destPath : "")) {
1752
+ migrated = true;
1753
+ }
1754
+ }
1755
+ }
1756
+ return { skills, migrated };
1757
+ }
1758
+
1759
+ function normalizeSpecTargetState(targetName, targetState) {
1760
+ const allowedConsumers = new Set(getGlobalDestinations(targetName).map((destination) => destination.id));
1761
+ const consumers = {};
1762
+ let migrated = false;
1763
+ for (const consumerId of allowedConsumers) {
1764
+ if (targetState && targetState.consumers && targetState.consumers[consumerId]) {
1765
+ const normalizedConsumerState = normalizeSpecConsumerState(targetName, consumerId, targetState.consumers[consumerId]);
1766
+ consumers[consumerId] = { skills: normalizedConsumerState.skills };
1767
+ if (normalizedConsumerState.migrated) {
1768
+ migrated = true;
1769
+ }
1770
+ }
1771
+ }
1772
+ return {
1773
+ enabledAt: targetState && typeof targetState.enabledAt === "string" ? targetState.enabledAt : "",
1774
+ consumers,
1775
+ migrated,
1776
+ };
1777
+ }
1778
+
1779
+ function normalizeSpecTargets(rawTargets) {
1780
+ const normalizedTargets = {};
1781
+ let migrated = false;
1782
+
1783
+ for (const targetName of SUPPORTED_TARGETS) {
1784
+ if (rawTargets && rawTargets[targetName] && typeof rawTargets[targetName] === "object") {
1785
+ normalizedTargets[targetName] = rawTargets[targetName];
1786
+ }
1787
+ }
1788
+
1789
+ const geminiState = normalizedTargets.gemini;
1790
+ if (
1791
+ geminiState
1792
+ && geminiState.consumers
1793
+ && geminiState.consumers.antigravity
1794
+ && !normalizedTargets.antigravity
1795
+ ) {
1796
+ normalizedTargets.antigravity = {
1797
+ enabledAt: typeof geminiState.enabledAt === "string" ? geminiState.enabledAt : "",
1798
+ consumers: {
1799
+ antigravity: geminiState.consumers.antigravity,
1800
+ },
1801
+ };
1802
+ delete geminiState.consumers.antigravity;
1803
+ migrated = true;
1804
+ }
1805
+
1806
+ const finalTargets = {};
1807
+ for (const targetName of Object.keys(normalizedTargets)) {
1808
+ const normalizedTargetState = normalizeSpecTargetState(targetName, normalizedTargets[targetName]);
1809
+ const consumerIds = Object.keys(normalizedTargetState.consumers);
1810
+ const rawConsumerIds = Object.keys((normalizedTargets[targetName] && normalizedTargets[targetName].consumers) || {});
1811
+ if (consumerIds.length !== rawConsumerIds.length || rawConsumerIds.some((consumerId) => !consumerIds.includes(consumerId))) {
1812
+ migrated = true;
1813
+ }
1814
+ if (normalizedTargetState.migrated) {
1815
+ migrated = true;
1816
+ }
1817
+ finalTargets[targetName] = normalizedTargetState;
1818
+ delete finalTargets[targetName].migrated;
1819
+ }
1820
+
1821
+ return { targets: finalTargets, migrated };
1822
+ }
1823
+
1478
1824
  function readSpecState() {
1479
1825
  const statePath = getSpecStatePath();
1480
1826
  if (!fs.existsSync(statePath)) {
1481
- return { statePath, state: createEmptySpecState() };
1827
+ return { statePath, state: createEmptySpecState(), migrated: false };
1482
1828
  }
1483
1829
 
1484
1830
  const raw = fs.readFileSync(statePath, "utf8").trim();
1485
1831
  if (!raw) {
1486
- return { statePath, state: createEmptySpecState() };
1832
+ return { statePath, state: createEmptySpecState(), migrated: false };
1487
1833
  }
1488
1834
 
1489
1835
  let parsed;
@@ -1495,9 +1841,10 @@ function readSpecState() {
1495
1841
 
1496
1842
  const state = createEmptySpecState();
1497
1843
  state.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
1498
- state.targets = parsed && typeof parsed.targets === "object" && parsed.targets ? parsed.targets : {};
1844
+ const normalizedTargets = normalizeSpecTargets(parsed && typeof parsed.targets === "object" && parsed.targets ? parsed.targets : {});
1845
+ state.targets = normalizedTargets.targets;
1499
1846
  state.assets = parsed && typeof parsed.assets === "object" && parsed.assets ? parsed.assets : {};
1500
- return { statePath, state };
1847
+ return { statePath, state, migrated: normalizedTargets.migrated };
1501
1848
  }
1502
1849
 
1503
1850
  function writeSpecState(statePath, state) {
@@ -2092,9 +2439,21 @@ function checkSpecProjectIntegrity(workspaceRoot) {
2092
2439
  const profilesDir = path.join(specDir, "profiles");
2093
2440
 
2094
2441
  const hasSpecDir = fs.existsSync(specDir);
2442
+ const globalSpecSummary = !hasSpecDir && issuesResult.status !== "missing" ? evaluateSpecState() : null;
2443
+ const canFallbackToGlobalSpec = Boolean(globalSpecSummary && globalSpecSummary.state === "installed");
2095
2444
  if (!hasSpecDir) {
2096
2445
  if (issuesResult.status !== "missing") {
2097
- issues.push("Missing .ling/spec directory (run: ling spec init)");
2446
+ if (!canFallbackToGlobalSpec) {
2447
+ issues.push("Missing .ling/spec directory (run: ling spec init)");
2448
+ if (globalSpecSummary && globalSpecSummary.state === "missing") {
2449
+ issues.push("Global Spec is not enabled (run: ling spec enable)");
2450
+ } else if (globalSpecSummary && globalSpecSummary.state === "broken") {
2451
+ issues.push("Global Spec is broken (run: ling spec enable)");
2452
+ for (const issue of globalSpecSummary.issues || []) {
2453
+ issues.push(`Global Spec issue: ${issue}`);
2454
+ }
2455
+ }
2456
+ }
2098
2457
  }
2099
2458
  } else {
2100
2459
  if (issuesResult.status === "missing") {
@@ -2141,7 +2500,10 @@ function resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp) {
2141
2500
  }
2142
2501
 
2143
2502
  async function commandSpecInit(options) {
2144
- const workspaceRoot = options.path ? resolveWorkspaceRoot(options.path) : getSpecWorkspaceDir();
2503
+ const workspaceRoot = options.path
2504
+ ? resolveWorkspaceRoot(options.path)
2505
+ : (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
2506
+ const csvOnly = Boolean(options.csvOnly);
2145
2507
  const prompter = createConflictPrompter(options);
2146
2508
  const timestamp = nowISO().replace(/[:.]/g, "-");
2147
2509
  const backupRoot = resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp);
@@ -2175,39 +2537,49 @@ async function commandSpecInit(options) {
2175
2537
  try {
2176
2538
  fs.mkdirSync(workspaceRoot, { recursive: true });
2177
2539
 
2178
- for (const [assetName, config] of Object.entries(assets)) {
2179
- const exists = fs.existsSync(config.destDir);
2180
- if (exists && areDirectoriesEqual(config.sourceDir, config.destDir)) {
2181
- log(options, `[skip] Spec ${assetName} 已最新,无需覆盖: ${config.destDir}`);
2182
- continue;
2183
- }
2540
+ if (!csvOnly) {
2541
+ for (const [assetName, config] of Object.entries(assets)) {
2542
+ const exists = fs.existsSync(config.destDir);
2543
+ if (exists && areDirectoriesEqual(config.sourceDir, config.destDir)) {
2544
+ log(options, `[skip] Spec ${assetName} 已最新,无需覆盖: ${config.destDir}`);
2545
+ continue;
2546
+ }
2184
2547
 
2185
- let action = exists ? "backup" : "";
2186
- if (exists && prompter) {
2187
- action = await prompter.resolveConflict({
2188
- category: "spec:project:assets",
2189
- label: `Spec ${assetName}`,
2190
- path: config.destDir,
2191
- });
2192
- } else if (exists && !prompter && (options.force || options.nonInteractive || !process.stdin.isTTY)) {
2193
- action = "backup";
2194
- }
2548
+ let action = exists ? "backup" : "";
2549
+ if (exists && prompter) {
2550
+ action = await prompter.resolveConflict({
2551
+ category: "spec:project:assets",
2552
+ label: `Spec ${assetName}`,
2553
+ path: config.destDir,
2554
+ });
2555
+ } else if (exists && !prompter && (options.force || options.nonInteractive || !process.stdin.isTTY)) {
2556
+ action = "backup";
2557
+ }
2195
2558
 
2196
- if (action === "keep") {
2197
- log(options, `[skip] 已保留 Spec ${assetName} 资产,不覆盖: ${config.destDir}`);
2198
- continue;
2199
- }
2559
+ if (action === "keep") {
2560
+ log(options, `[skip] 已保留 Spec ${assetName} 资产,不覆盖: ${config.destDir}`);
2561
+ continue;
2562
+ }
2200
2563
 
2201
- if (exists && action !== "remove") {
2202
- backupDirSnapshot(config.destDir, path.join(backupRoot, "assets", assetName), options, `Spec ${assetName}`);
2203
- } else if (exists && action === "remove") {
2204
- removeDirIfExists(config.destDir, options, `Spec ${assetName}`);
2564
+ if (exists && action !== "remove") {
2565
+ backupDirSnapshot(config.destDir, path.join(backupRoot, "assets", assetName), options, `Spec ${assetName}`);
2566
+ } else if (exists && action === "remove") {
2567
+ removeDirIfExists(config.destDir, options, `Spec ${assetName}`);
2568
+ }
2569
+ applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
2205
2570
  }
2206
- applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
2207
2571
  }
2208
2572
 
2209
2573
  const issuesPath = path.join(workspaceRoot, "issues.csv");
2210
- const issuesTemplatePath = path.join(specSourceDir, "templates", "issues.template.csv");
2574
+ let issuesTemplatePath = path.join(specSourceDir, "templates", "issues.template.csv");
2575
+ if (csvOnly && !options.branch) {
2576
+ const globalTemplatePath = path.join(getSpecHomeDir(), "templates", "issues.template.csv");
2577
+ if (fs.existsSync(globalTemplatePath)) {
2578
+ issuesTemplatePath = globalTemplatePath;
2579
+ } else if (!options.quiet) {
2580
+ log(options, "[warn] 未检测到全局 Spec templates,已使用内置模板生成 issues.csv。可执行: ling spec enable");
2581
+ }
2582
+ }
2211
2583
  const issuesTemplate = stripUtf8Bom(fs.readFileSync(issuesTemplatePath, "utf8"));
2212
2584
  const hasIssues = fs.existsSync(issuesPath);
2213
2585
  if (hasIssues) {
@@ -2246,7 +2618,8 @@ async function commandSpecInit(options) {
2246
2618
  }
2247
2619
 
2248
2620
  const requestedTargets = normalizeTargets(options.targets);
2249
- const shouldInitTargets = options.path ? requestedTargets.length > 0 : true;
2621
+ const isSpecWorkspaceMode = Boolean(!options.path && options.specWorkspace);
2622
+ const shouldInitTargets = isSpecWorkspaceMode ? true : requestedTargets.length > 0;
2250
2623
  const targets = shouldInitTargets ? (requestedTargets.length > 0 ? requestedTargets : [...SUPPORTED_TARGETS]) : [];
2251
2624
  if (targets.length > 0) {
2252
2625
  await initTargets(workspaceRoot, targets, options, prompter);
@@ -2260,7 +2633,9 @@ async function commandSpecInit(options) {
2260
2633
  }
2261
2634
 
2262
2635
  function commandSpecDoctor(options) {
2263
- const workspaceRoot = options.path ? resolveWorkspaceRoot(options.path) : getSpecWorkspaceDir();
2636
+ const workspaceRoot = options.path
2637
+ ? resolveWorkspaceRoot(options.path)
2638
+ : (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
2264
2639
  const result = checkSpecProjectIntegrity(workspaceRoot);
2265
2640
  const state = result.status === "ok" ? "installed" : result.status;
2266
2641
 
@@ -2279,6 +2654,21 @@ function commandSpecDoctor(options) {
2279
2654
 
2280
2655
  console.log(state === "installed" ? "[ok] Spec 项目资产状态正常" : "[warn] Spec 项目资产存在问题");
2281
2656
  console.log(` 工作区: ${workspaceRoot}`);
2657
+ const specDir = path.join(workspaceRoot, ".ling", "spec");
2658
+ const hasSpecDir = fs.existsSync(specDir);
2659
+ const issuesPath = path.join(workspaceRoot, "issues.csv");
2660
+ const hasIssues = fs.existsSync(issuesPath);
2661
+ if (hasSpecDir) {
2662
+ console.log(" 模式: project");
2663
+ console.log(` Spec 根目录: ${specDir}`);
2664
+ } else if (hasIssues) {
2665
+ const globalSpecSummary = evaluateSpecState();
2666
+ const fallback = globalSpecSummary.state === "installed";
2667
+ console.log(` 模式: csv-only${fallback ? " (global fallback)" : ""}`);
2668
+ if (fallback) {
2669
+ console.log(` Spec 根目录: ${getSpecHomeDir()}`);
2670
+ }
2671
+ }
2282
2672
  console.log(` 任务数: ${result.stats.total}`);
2283
2673
  console.log(` 进行中: ${result.stats.inProgress}`);
2284
2674
  for (const issue of result.issues || []) {
@@ -2308,6 +2698,10 @@ async function commandSpecEnable(options) {
2308
2698
  writeSpecState(statePath, state);
2309
2699
  }
2310
2700
 
2701
+ if (targets.includes("codex")) {
2702
+ migrateLegacyCodexGlobalSkills(timestamp, options);
2703
+ }
2704
+
2311
2705
  log(options, `[ok] Spec Profile 已启用 (Targets: ${targets.join(", ")})`);
2312
2706
  }
2313
2707
 
@@ -2368,23 +2762,23 @@ async function commandSpec(options) {
2368
2762
  }
2369
2763
 
2370
2764
  async function initTargets(workspaceRoot, targets, options, prompter) {
2371
- for (const target of targets) {
2765
+ for (const group of groupTargetsByInstallSurface(targets)) {
2372
2766
  const runOptions = { ...options };
2373
2767
  const conflicts = [];
2374
2768
 
2375
- if (target === "gemini") {
2769
+ if (group.installSurface === ".agent") {
2376
2770
  const agentDir = path.join(workspaceRoot, ".agent");
2377
2771
  if (fs.existsSync(agentDir)) {
2378
2772
  conflicts.push({
2379
- category: "project:gemini",
2773
+ category: "project:shared-agent",
2380
2774
  label: ".agent",
2381
2775
  path: agentDir,
2382
- target,
2776
+ target: group.logicalTargets.join(","),
2383
2777
  });
2384
2778
  }
2385
2779
  }
2386
2780
 
2387
- if (target === "codex") {
2781
+ if (group.adapterTarget === "codex") {
2388
2782
  const managedDir = path.join(workspaceRoot, ".agents");
2389
2783
  const legacyDir = path.join(workspaceRoot, ".codex");
2390
2784
  if (fs.existsSync(managedDir) || fs.existsSync(legacyDir)) {
@@ -2435,10 +2829,13 @@ async function initTargets(workspaceRoot, targets, options, prompter) {
2435
2829
  }
2436
2830
  }
2437
2831
 
2438
- const adapter = createAdapter(target, workspaceRoot, runOptions);
2439
- log(options, `[sync] 正在初始化目标 [${target}] ...`);
2832
+ const adapter = createAdapter(group.adapterTarget, workspaceRoot, runOptions);
2833
+ log(options, `[sync] 正在初始化目标 [${group.logicalTargets.join(", ")}] ...`);
2440
2834
  adapter.install(BUNDLED_AGENT_DIR);
2441
- registerWorkspaceTarget(workspaceRoot, target, runOptions);
2835
+ recordWorkspaceInstallTargets(workspaceRoot, group.logicalTargets, runOptions);
2836
+ for (const target of group.logicalTargets) {
2837
+ registerWorkspaceTarget(workspaceRoot, target, runOptions);
2838
+ }
2442
2839
  }
2443
2840
  }
2444
2841
 
@@ -2523,71 +2920,73 @@ async function commandUpdate(options) {
2523
2920
 
2524
2921
  let updatedAny = false;
2525
2922
  try {
2526
- for (const target of targets) {
2527
- if (!isTargetInstalled(workspaceRoot, target) && options.targets.length > 0) {
2528
- throw new Error(`目标未安装: ${target}`);
2529
- }
2530
- if (!isTargetInstalled(workspaceRoot, target)) {
2531
- log(options, `[skip] 目标未安装,跳过: ${target}`);
2532
- continue;
2533
- }
2923
+ for (const group of groupTargetsByInstallSurface(targets)) {
2924
+ if (!isTargetInstalled(workspaceRoot, group.adapterTarget) && options.targets.length > 0) {
2925
+ throw new Error(`目标未安装: ${group.logicalTargets.join(", ")}`);
2926
+ }
2927
+ if (!isTargetInstalled(workspaceRoot, group.adapterTarget)) {
2928
+ log(options, `[skip] 目标未安装,跳过: ${group.logicalTargets.join(", ")}`);
2929
+ continue;
2930
+ }
2534
2931
 
2535
- const runOptions = { ...options, force: true };
2932
+ const runOptions = { ...options, force: true };
2933
+ const timestamp = nowISO().replace(/[:.]/g, "-");
2536
2934
 
2537
- const timestamp = nowISO().replace(/[:.]/g, "-");
2538
- if (target === "gemini") {
2539
- const agentDir = path.join(workspaceRoot, ".agent");
2540
- if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
2541
- let action = "backup";
2542
- if (prompter) {
2543
- action = await prompter.resolveConflict({
2544
- category: "project:gemini",
2545
- label: ".agent",
2546
- path: agentDir,
2547
- target,
2548
- });
2549
- }
2550
- if (action === "keep") {
2551
- log(options, "[skip] 已保留现有资产,跳过更新: .agent");
2552
- continue;
2553
- }
2554
- if (action === "backup") {
2555
- backupWorkspaceDir(workspaceRoot, agentDir, ".agent-backup", timestamp, options, "工作区资产 .agent");
2935
+ if (group.installSurface === ".agent") {
2936
+ const agentDir = path.join(workspaceRoot, ".agent");
2937
+ if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
2938
+ let action = "backup";
2939
+ if (prompter) {
2940
+ action = await prompter.resolveConflict({
2941
+ category: "project:shared-agent",
2942
+ label: ".agent",
2943
+ path: agentDir,
2944
+ target: group.logicalTargets.join(","),
2945
+ });
2946
+ }
2947
+ if (action === "keep") {
2948
+ log(options, "[skip] 已保留现有资产,跳过更新: .agent");
2949
+ continue;
2950
+ }
2951
+ if (action === "backup") {
2952
+ backupWorkspaceDir(workspaceRoot, agentDir, ".agent-backup", timestamp, options, "工作区资产 .agent");
2953
+ }
2556
2954
  }
2557
2955
  }
2558
- }
2559
2956
 
2560
- if (target === "codex") {
2561
- const managedDir = path.join(workspaceRoot, ".agents");
2562
- const legacyDir = path.join(workspaceRoot, ".codex");
2563
- const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
2564
- const conflict = evaluateCodexUpdateConflict(activeDir);
2565
- if (conflict.hasConflict) {
2566
- let action = "backup";
2567
- if (prompter) {
2568
- action = await prompter.resolveConflict({
2569
- category: "project:codex",
2570
- label: fs.existsSync(managedDir) ? ".agents" : ".codex",
2571
- path: activeDir,
2572
- target,
2573
- });
2574
- }
2575
- if (action === "keep") {
2576
- log(options, `[skip] 已保留现有资产,跳过更新: ${path.basename(activeDir)}`);
2577
- continue;
2578
- }
2579
- if (action === "backup") {
2580
- const backupRootName = ".agents-backup";
2581
- backupWorkspaceDir(workspaceRoot, activeDir, backupRootName, timestamp, options, `工作区资产 ${path.basename(activeDir)}`);
2957
+ if (group.adapterTarget === "codex") {
2958
+ const managedDir = path.join(workspaceRoot, ".agents");
2959
+ const legacyDir = path.join(workspaceRoot, ".codex");
2960
+ const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
2961
+ const conflict = evaluateCodexUpdateConflict(activeDir);
2962
+ if (conflict.hasConflict) {
2963
+ let action = "backup";
2964
+ if (prompter) {
2965
+ action = await prompter.resolveConflict({
2966
+ category: "project:codex",
2967
+ label: fs.existsSync(managedDir) ? ".agents" : ".codex",
2968
+ path: activeDir,
2969
+ target: group.logicalTargets.join(","),
2970
+ });
2971
+ }
2972
+ if (action === "keep") {
2973
+ log(options, `[skip] 已保留现有资产,跳过更新: ${path.basename(activeDir)}`);
2974
+ continue;
2975
+ }
2976
+ if (action === "backup") {
2977
+ backupWorkspaceDir(workspaceRoot, activeDir, ".agents-backup", timestamp, options, `工作区资产 ${path.basename(activeDir)}`);
2978
+ }
2582
2979
  }
2583
2980
  }
2584
- }
2585
2981
 
2586
- const adapter = createAdapter(target, workspaceRoot, runOptions);
2587
- log(options, `[sync] 更新 [${target}] ...`);
2588
- adapter.update(BUNDLED_AGENT_DIR);
2589
- registerWorkspaceTarget(workspaceRoot, target, runOptions);
2590
- updatedAny = true;
2982
+ const adapter = createAdapter(group.adapterTarget, workspaceRoot, runOptions);
2983
+ log(options, `[sync] 更新 [${group.logicalTargets.join(", ")}] ...`);
2984
+ adapter.update(BUNDLED_AGENT_DIR);
2985
+ recordWorkspaceInstallTargets(workspaceRoot, group.logicalTargets, runOptions);
2986
+ for (const target of group.logicalTargets) {
2987
+ registerWorkspaceTarget(workspaceRoot, target, runOptions);
2988
+ }
2989
+ updatedAny = true;
2591
2990
  }
2592
2991
  } finally {
2593
2992
  if (prompter) prompter.close();
@@ -2680,7 +3079,7 @@ async function commandUpdateAll(options) {
2680
3079
  const installedTargets = detectInstalledTargets(workspacePath);
2681
3080
  let targets = [];
2682
3081
  if (requestedTargets.length > 0) {
2683
- targets = installedTargets.filter((target) => requestedTargets.includes(target));
3082
+ targets = requestedTargets.filter((target) => isTargetInstalled(workspacePath, target));
2684
3083
  } else {
2685
3084
  targets = [...Object.keys(item.targets || {}), ...installedTargets];
2686
3085
  }
@@ -2696,9 +3095,9 @@ async function commandUpdateAll(options) {
2696
3095
  log(options, `[sync] [${i + 1}/${records.length}] 更新: ${workspacePath} [${targets.join(", ")}]`);
2697
3096
 
2698
3097
  const updatedTargets = [];
2699
- for (const target of targets) {
2700
- if (!isTargetInstalled(workspacePath, target)) {
2701
- log(options, `[skip] [${i + 1}/${records.length}] 目标未安装,跳过: ${target}`);
3098
+ for (const group of groupTargetsByInstallSurface(targets)) {
3099
+ if (!isTargetInstalled(workspacePath, group.adapterTarget)) {
3100
+ log(options, `[skip] [${i + 1}/${records.length}] 目标未安装,跳过: ${group.logicalTargets.join(", ")}`);
2702
3101
  continue;
2703
3102
  }
2704
3103
 
@@ -2712,19 +3111,19 @@ async function commandUpdateAll(options) {
2712
3111
 
2713
3112
  const timestampForBackup = nowISO().replace(/[:.]/g, "-");
2714
3113
 
2715
- if (target === "gemini") {
3114
+ if (group.installSurface === ".agent") {
2716
3115
  const agentDir = path.join(workspacePath, ".agent");
2717
3116
  if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
2718
3117
  let action = "backup";
2719
3118
  if (prompter) {
2720
3119
  action = await prompter.resolveConflict({
2721
- category: "update-all:project:gemini",
3120
+ category: "update-all:project:shared-agent",
2722
3121
  label: `.agent (${workspacePath})`,
2723
3122
  path: agentDir,
2724
3123
  });
2725
3124
  }
2726
3125
  if (action === "keep") {
2727
- log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [gemini]`);
3126
+ log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
2728
3127
  continue;
2729
3128
  }
2730
3129
  if (action === "backup") {
@@ -2733,7 +3132,7 @@ async function commandUpdateAll(options) {
2733
3132
  }
2734
3133
  }
2735
3134
 
2736
- if (target === "codex") {
3135
+ if (group.adapterTarget === "codex") {
2737
3136
  const managedDir = path.join(workspacePath, ".agents");
2738
3137
  const legacyDir = path.join(workspacePath, ".codex");
2739
3138
  const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
@@ -2757,13 +3156,14 @@ async function commandUpdateAll(options) {
2757
3156
  }
2758
3157
  }
2759
3158
 
2760
- const adapter = createAdapter(target, workspacePath, runOptions);
3159
+ const adapter = createAdapter(group.adapterTarget, workspacePath, runOptions);
2761
3160
  adapter.update(BUNDLED_AGENT_DIR);
2762
- updatedTargets.push(target);
3161
+ recordWorkspaceInstallTargets(workspacePath, group.logicalTargets, runOptions);
3162
+ updatedTargets.push(...group.logicalTargets);
2763
3163
  } catch (err) {
2764
3164
  failed += 1;
2765
3165
  if (!options.quiet) {
2766
- console.error(`[error] 更新失败: ${workspacePath} [${target}]`);
3166
+ console.error(`[error] 更新失败: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
2767
3167
  console.error(` ${err.message}`);
2768
3168
  }
2769
3169
  }
@@ -3068,6 +3468,25 @@ function countSkillsRecursive(dir) {
3068
3468
  return count;
3069
3469
  }
3070
3470
 
3471
+ function printSharedAgentTargetStatus(workspaceRoot, targetState, label) {
3472
+ const agentDir = path.join(workspaceRoot, ".agent");
3473
+ const agentsCount = countFilesIfExists(path.join(agentDir, "agents"), (name) => name.endsWith(".md"));
3474
+ const workflowsCount = countFilesIfExists(path.join(agentDir, "workflows"), (name) => name.endsWith(".md"));
3475
+ const skillsCount = countSkillsRecursive(path.join(agentDir, "skills"));
3476
+ console.log(`\n[${label}]`);
3477
+ console.log(` 状态: ${targetState.state}`);
3478
+ console.log(` 路径: ${agentDir}`);
3479
+ if (targetState.state === "installed") {
3480
+ console.log(` Agents: ${agentsCount}`);
3481
+ console.log(` Skills: ${skillsCount}`);
3482
+ console.log(` Workflows: ${workflowsCount}`);
3483
+ return;
3484
+ }
3485
+ for (const issue of targetState.integrity.issues || []) {
3486
+ console.log(` Issue: ${issue}`);
3487
+ }
3488
+ }
3489
+
3071
3490
  function commandStatus(options) {
3072
3491
  const workspaceRoot = resolveWorkspaceRoot(options.path);
3073
3492
  const summary = evaluateWorkspaceState(workspaceRoot, options);
@@ -3098,22 +3517,12 @@ function commandStatus(options) {
3098
3517
 
3099
3518
  const geminiState = summary.targets.find((item) => item.targetName === "gemini");
3100
3519
  if (geminiState) {
3101
- const agentDir = path.join(workspaceRoot, ".agent");
3102
- const agentsCount = countFilesIfExists(path.join(agentDir, "agents"), (name) => name.endsWith(".md"));
3103
- const workflowsCount = countFilesIfExists(path.join(agentDir, "workflows"), (name) => name.endsWith(".md"));
3104
- const skillsCount = countSkillsRecursive(path.join(agentDir, "skills"));
3105
- console.log("\n[gemini]");
3106
- console.log(` 状态: ${geminiState.state}`);
3107
- console.log(` 路径: ${agentDir}`);
3108
- if (geminiState.state === "installed") {
3109
- console.log(` Agents: ${agentsCount}`);
3110
- console.log(` Skills: ${skillsCount}`);
3111
- console.log(` Workflows: ${workflowsCount}`);
3112
- } else {
3113
- for (const issue of geminiState.integrity.issues || []) {
3114
- console.log(` Issue: ${issue}`);
3115
- }
3116
- }
3520
+ printSharedAgentTargetStatus(workspaceRoot, geminiState, "gemini");
3521
+ }
3522
+
3523
+ const antigravityState = summary.targets.find((item) => item.targetName === "antigravity");
3524
+ if (antigravityState) {
3525
+ printSharedAgentTargetStatus(workspaceRoot, antigravityState, "antigravity");
3117
3526
  }
3118
3527
 
3119
3528
  const codexState = summary.targets.find((item) => item.targetName === "codex");