@mison/ling 1.1.1 → 1.2.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/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
  };
@@ -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"],
@@ -170,13 +173,13 @@ function printUsage() {
170
173
  console.log(` ${PRIMARY_CLI_NAME} update [--path <dir>] [--branch <name>] [--target <name>|--targets <a,b>] [--no-index] [--quiet] [--dry-run]`);
171
174
  console.log(` ${PRIMARY_CLI_NAME} update-all [--branch <name>] [--targets <a,b>] [--prune-missing] [--quiet] [--dry-run]`);
172
175
  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)`);
176
+ console.log(` ${PRIMARY_CLI_NAME} global sync [--target <name>|--targets <a,b>] [--branch <name>] [--quiet] [--dry-run] # 默认同步 codex + gemini + antigravity`);
174
177
  console.log(` ${PRIMARY_CLI_NAME} global status [--quiet]`);
175
178
  console.log(` ${PRIMARY_CLI_NAME} spec enable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
176
179
  console.log(` ${PRIMARY_CLI_NAME} spec disable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
177
180
  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]`);
181
+ 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]`);
182
+ console.log(` ${PRIMARY_CLI_NAME} spec doctor [--path <dir>] [--spec-workspace] [--quiet]`);
180
183
  console.log(` ${PRIMARY_CLI_NAME} exclude list [--quiet]`);
181
184
  console.log(` ${PRIMARY_CLI_NAME} exclude add --path <dir> [--dry-run] [--quiet]`);
182
185
  console.log(` ${PRIMARY_CLI_NAME} exclude remove --path <dir> [--dry-run] [--quiet]`);
@@ -202,6 +205,8 @@ function parseArgs(argv) {
202
205
  nonInteractive: false,
203
206
  noIndex: false,
204
207
  fix: false,
208
+ csvOnly: false,
209
+ specWorkspace: false,
205
210
  subcommand: "",
206
211
  path: "",
207
212
  branch: "",
@@ -252,6 +257,12 @@ function parseArgs(argv) {
252
257
  } else if (arg === "--fix") {
253
258
  providedFlags.push(arg);
254
259
  options.fix = true;
260
+ } else if (arg === "--csv-only") {
261
+ providedFlags.push(arg);
262
+ options.csvOnly = true;
263
+ } else if (arg === "--spec-workspace") {
264
+ providedFlags.push(arg);
265
+ options.specWorkspace = true;
255
266
  } else if (arg === "--path") {
256
267
  providedFlags.push(arg);
257
268
  if (i + 1 >= argv.length) {
@@ -295,8 +306,8 @@ const COMMAND_ALLOWED_FLAGS = {
295
306
  "spec:enable": ["--target", "--targets", "--quiet", "--dry-run"],
296
307
  "spec:disable": ["--target", "--targets", "--quiet", "--dry-run"],
297
308
  "spec:status": ["--quiet"],
298
- "spec:init": ["--force", "--path", "--branch", "--target", "--targets", "--non-interactive", "--no-index", "--quiet", "--dry-run"],
299
- "spec:doctor": ["--path", "--quiet"],
309
+ "spec:init": ["--force", "--path", "--spec-workspace", "--branch", "--csv-only", "--target", "--targets", "--non-interactive", "--no-index", "--quiet", "--dry-run"],
310
+ "spec:doctor": ["--path", "--spec-workspace", "--quiet"],
300
311
  "exclude:list": ["--quiet"],
301
312
  "exclude:add": ["--path", "--dry-run", "--quiet"],
302
313
  "exclude:remove": ["--path", "--dry-run", "--quiet"],
@@ -389,6 +400,12 @@ function pathCompareKey(inputPath) {
389
400
  return normalized;
390
401
  }
391
402
 
403
+ function findWorkspaceRecord(index, workspaceRoot) {
404
+ const normalizedPath = normalizeAbsolutePath(workspaceRoot);
405
+ const targetKey = pathCompareKey(normalizedPath);
406
+ return (index.workspaces || []).find((item) => pathCompareKey(item.path) === targetKey) || null;
407
+ }
408
+
392
409
  function normalizePathList(items) {
393
410
  const map = new Map();
394
411
  for (const item of items) {
@@ -627,6 +644,95 @@ function writeWorkspaceIndex(indexPath, index) {
627
644
  fs.writeFileSync(indexPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
628
645
  }
629
646
 
647
+ function getWorkspaceInstallStatePath(workspaceRoot) {
648
+ return path.join(normalizeAbsolutePath(workspaceRoot), ".ling", "install-state.json");
649
+ }
650
+
651
+ function createEmptyWorkspaceInstallState() {
652
+ return {
653
+ version: 1,
654
+ updatedAt: "",
655
+ targets: {},
656
+ };
657
+ }
658
+
659
+ function readWorkspaceInstallState(workspaceRoot) {
660
+ const statePath = getWorkspaceInstallStatePath(workspaceRoot);
661
+ if (!fs.existsSync(statePath)) {
662
+ return { statePath, state: createEmptyWorkspaceInstallState() };
663
+ }
664
+
665
+ const raw = fs.readFileSync(statePath, "utf8").trim();
666
+ if (!raw) {
667
+ return { statePath, state: createEmptyWorkspaceInstallState() };
668
+ }
669
+
670
+ let parsed;
671
+ try {
672
+ parsed = JSON.parse(raw);
673
+ } catch (_err) {
674
+ return { statePath, state: createEmptyWorkspaceInstallState() };
675
+ }
676
+
677
+ const state = createEmptyWorkspaceInstallState();
678
+ state.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
679
+ if (parsed && parsed.targets && typeof parsed.targets === "object") {
680
+ for (const [targetName, targetState] of Object.entries(parsed.targets)) {
681
+ const normalizedTargetName = normalizeIndexTargetName(targetName);
682
+ const normalizedTargetState = normalizeTargetState(targetState);
683
+ if (normalizedTargetName && normalizedTargetState) {
684
+ state.targets[normalizedTargetName] = normalizedTargetState;
685
+ }
686
+ }
687
+ }
688
+
689
+ return { statePath, state };
690
+ }
691
+
692
+ function writeWorkspaceInstallState(statePath, state) {
693
+ const payload = {
694
+ version: 1,
695
+ updatedAt: state.updatedAt || nowISO(),
696
+ targets: state.targets || {},
697
+ };
698
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
699
+ fs.writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
700
+ }
701
+
702
+ function recordWorkspaceInstallTargets(workspaceRoot, targetNames, options) {
703
+ const logicalTargets = normalizeTargets(targetNames).filter((targetName) => SHARED_AGENT_TARGETS.includes(targetName));
704
+ if (logicalTargets.length === 0) {
705
+ return;
706
+ }
707
+
708
+ if (options.dryRun) {
709
+ log(options, `[dry-run] 将写入工作区安装状态: ${getWorkspaceInstallStatePath(workspaceRoot)} [${logicalTargets.join(", ")}]`);
710
+ return;
711
+ }
712
+
713
+ const { statePath, state } = readWorkspaceInstallState(workspaceRoot);
714
+ const timestamp = nowISO();
715
+ for (const targetName of logicalTargets) {
716
+ const prev = normalizeTargetState(state.targets[targetName]) || {
717
+ version: "",
718
+ installedAt: "",
719
+ updatedAt: "",
720
+ };
721
+ state.targets[targetName] = {
722
+ version: pkg.version,
723
+ installedAt: prev.installedAt || timestamp,
724
+ updatedAt: timestamp,
725
+ };
726
+ }
727
+ state.updatedAt = timestamp;
728
+ writeWorkspaceInstallState(statePath, state);
729
+ }
730
+
731
+ function resolveWorkspaceInstallStateTargets(workspaceRoot) {
732
+ const { state } = readWorkspaceInstallState(workspaceRoot);
733
+ return normalizeTargets(Object.keys(state.targets || {}));
734
+ }
735
+
630
736
  function sleepSync(ms) {
631
737
  const buffer = new SharedArrayBuffer(4);
632
738
  const view = new Int32Array(buffer);
@@ -906,19 +1012,41 @@ function normalizeTargets(rawTargets) {
906
1012
  return result;
907
1013
  }
908
1014
 
1015
+ function resolveIndexedWorkspaceTargets(workspaceRoot) {
1016
+ try {
1017
+ const { index } = readWorkspaceIndex();
1018
+ const record = findWorkspaceRecord(index, workspaceRoot);
1019
+ if (!record) {
1020
+ return [];
1021
+ }
1022
+ return normalizeTargets(Object.keys(record.targets || {}));
1023
+ } catch (_err) {
1024
+ return [];
1025
+ }
1026
+ }
1027
+
909
1028
  function detectInstalledTargets(workspaceRoot) {
910
1029
  const targets = [];
1030
+ const localTargets = resolveWorkspaceInstallStateTargets(workspaceRoot);
1031
+ const indexedTargets = resolveIndexedWorkspaceTargets(workspaceRoot);
911
1032
  if (fs.existsSync(path.join(workspaceRoot, ".agent"))) {
912
- targets.push("gemini");
1033
+ const sharedTargets = localTargets
1034
+ .filter((target) => SHARED_AGENT_TARGETS.includes(target))
1035
+ .concat(indexedTargets.filter((target) => SHARED_AGENT_TARGETS.includes(target)));
1036
+ if (sharedTargets.length > 0) {
1037
+ targets.push(...sharedTargets);
1038
+ } else {
1039
+ targets.push("gemini");
1040
+ }
913
1041
  }
914
1042
  if (fs.existsSync(path.join(workspaceRoot, ".agents")) || fs.existsSync(path.join(workspaceRoot, ".codex"))) {
915
1043
  targets.push("codex");
916
1044
  }
917
- return targets;
1045
+ return normalizeTargets(targets);
918
1046
  }
919
1047
 
920
1048
  function isTargetInstalled(workspaceRoot, targetName) {
921
- if (targetName === "gemini") {
1049
+ if (SHARED_AGENT_TARGETS.includes(targetName)) {
922
1050
  return fs.existsSync(path.join(workspaceRoot, ".agent"));
923
1051
  }
924
1052
  if (targetName === "codex") {
@@ -927,6 +1055,27 @@ function isTargetInstalled(workspaceRoot, targetName) {
927
1055
  return false;
928
1056
  }
929
1057
 
1058
+ function groupTargetsByInstallSurface(targets) {
1059
+ const normalizedTargets = normalizeTargets(targets);
1060
+ const groups = [];
1061
+ const sharedTargets = normalizedTargets.filter((target) => SHARED_AGENT_TARGETS.includes(target));
1062
+ if (sharedTargets.length > 0) {
1063
+ groups.push({
1064
+ installSurface: ".agent",
1065
+ adapterTarget: sharedTargets.includes("gemini") ? "gemini" : "antigravity",
1066
+ logicalTargets: sharedTargets,
1067
+ });
1068
+ }
1069
+ if (normalizedTargets.includes("codex")) {
1070
+ groups.push({
1071
+ installSurface: ".agents",
1072
+ adapterTarget: "codex",
1073
+ logicalTargets: ["codex"],
1074
+ });
1075
+ }
1076
+ return groups;
1077
+ }
1078
+
930
1079
  function setQuietStatusExitCode(state) {
931
1080
  process.exitCode = Object.prototype.hasOwnProperty.call(QUIET_STATUS_EXIT_CODES, state)
932
1081
  ? QUIET_STATUS_EXIT_CODES[state]
@@ -1022,8 +1171,11 @@ function evaluateGlobalState() {
1022
1171
  }
1023
1172
 
1024
1173
  function createAdapter(targetName, workspaceRoot, options) {
1025
- if (targetName === "gemini") {
1026
- return new GeminiAdapter(workspaceRoot, options);
1174
+ if (SHARED_AGENT_TARGETS.includes(targetName)) {
1175
+ return new GeminiAdapter(workspaceRoot, {
1176
+ ...options,
1177
+ targetName,
1178
+ });
1027
1179
  }
1028
1180
  if (targetName === "codex") {
1029
1181
  return new CodexAdapter(workspaceRoot, options);
@@ -1067,8 +1219,7 @@ function resolveTargetsForGlobalSync(options) {
1067
1219
  if (requested.length > 0) {
1068
1220
  return requested;
1069
1221
  }
1070
- // 保持 global sync 简洁:默认同步 codex + gemini;其中 gemini 会展开为 gemini-cli 与 antigravity。
1071
- return ["codex", "gemini"];
1222
+ return [...SUPPORTED_TARGETS];
1072
1223
  }
1073
1224
 
1074
1225
  function resolveAgentInstallSource(options) {
@@ -1235,7 +1386,7 @@ function planGlobalSyncTasks(targetName, agentDir) {
1235
1386
  };
1236
1387
  }
1237
1388
 
1238
- if (targetName === "gemini") {
1389
+ if (SHARED_AGENT_TARGETS.includes(targetName)) {
1239
1390
  const skillsRoot = path.join(agentDir, "skills");
1240
1391
  const skillNames = listSkillDirectories(skillsRoot);
1241
1392
  const tasks = [];
@@ -1317,7 +1468,7 @@ function applyGlobalSync(targetName, agentDir, timestamp, options) {
1317
1468
  }
1318
1469
  }
1319
1470
 
1320
- if (targetName === "gemini") {
1471
+ if (SHARED_AGENT_TARGETS.includes(targetName)) {
1321
1472
  const skillsRoot = path.join(agentDir, "skills");
1322
1473
  return syncGlobalSkillsFromRoot(targetName, skillsRoot, timestamp, options);
1323
1474
  }
@@ -2092,9 +2243,21 @@ function checkSpecProjectIntegrity(workspaceRoot) {
2092
2243
  const profilesDir = path.join(specDir, "profiles");
2093
2244
 
2094
2245
  const hasSpecDir = fs.existsSync(specDir);
2246
+ const globalSpecSummary = !hasSpecDir && issuesResult.status !== "missing" ? evaluateSpecState() : null;
2247
+ const canFallbackToGlobalSpec = Boolean(globalSpecSummary && globalSpecSummary.state === "installed");
2095
2248
  if (!hasSpecDir) {
2096
2249
  if (issuesResult.status !== "missing") {
2097
- issues.push("Missing .ling/spec directory (run: ling spec init)");
2250
+ if (!canFallbackToGlobalSpec) {
2251
+ issues.push("Missing .ling/spec directory (run: ling spec init)");
2252
+ if (globalSpecSummary && globalSpecSummary.state === "missing") {
2253
+ issues.push("Global Spec is not enabled (run: ling spec enable)");
2254
+ } else if (globalSpecSummary && globalSpecSummary.state === "broken") {
2255
+ issues.push("Global Spec is broken (run: ling spec enable)");
2256
+ for (const issue of globalSpecSummary.issues || []) {
2257
+ issues.push(`Global Spec issue: ${issue}`);
2258
+ }
2259
+ }
2260
+ }
2098
2261
  }
2099
2262
  } else {
2100
2263
  if (issuesResult.status === "missing") {
@@ -2141,7 +2304,10 @@ function resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp) {
2141
2304
  }
2142
2305
 
2143
2306
  async function commandSpecInit(options) {
2144
- const workspaceRoot = options.path ? resolveWorkspaceRoot(options.path) : getSpecWorkspaceDir();
2307
+ const workspaceRoot = options.path
2308
+ ? resolveWorkspaceRoot(options.path)
2309
+ : (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
2310
+ const csvOnly = Boolean(options.csvOnly);
2145
2311
  const prompter = createConflictPrompter(options);
2146
2312
  const timestamp = nowISO().replace(/[:.]/g, "-");
2147
2313
  const backupRoot = resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp);
@@ -2175,39 +2341,49 @@ async function commandSpecInit(options) {
2175
2341
  try {
2176
2342
  fs.mkdirSync(workspaceRoot, { recursive: true });
2177
2343
 
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
- }
2344
+ if (!csvOnly) {
2345
+ for (const [assetName, config] of Object.entries(assets)) {
2346
+ const exists = fs.existsSync(config.destDir);
2347
+ if (exists && areDirectoriesEqual(config.sourceDir, config.destDir)) {
2348
+ log(options, `[skip] Spec ${assetName} 已最新,无需覆盖: ${config.destDir}`);
2349
+ continue;
2350
+ }
2184
2351
 
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
- }
2352
+ let action = exists ? "backup" : "";
2353
+ if (exists && prompter) {
2354
+ action = await prompter.resolveConflict({
2355
+ category: "spec:project:assets",
2356
+ label: `Spec ${assetName}`,
2357
+ path: config.destDir,
2358
+ });
2359
+ } else if (exists && !prompter && (options.force || options.nonInteractive || !process.stdin.isTTY)) {
2360
+ action = "backup";
2361
+ }
2195
2362
 
2196
- if (action === "keep") {
2197
- log(options, `[skip] 已保留 Spec ${assetName} 资产,不覆盖: ${config.destDir}`);
2198
- continue;
2199
- }
2363
+ if (action === "keep") {
2364
+ log(options, `[skip] 已保留 Spec ${assetName} 资产,不覆盖: ${config.destDir}`);
2365
+ continue;
2366
+ }
2200
2367
 
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}`);
2368
+ if (exists && action !== "remove") {
2369
+ backupDirSnapshot(config.destDir, path.join(backupRoot, "assets", assetName), options, `Spec ${assetName}`);
2370
+ } else if (exists && action === "remove") {
2371
+ removeDirIfExists(config.destDir, options, `Spec ${assetName}`);
2372
+ }
2373
+ applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
2205
2374
  }
2206
- applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
2207
2375
  }
2208
2376
 
2209
2377
  const issuesPath = path.join(workspaceRoot, "issues.csv");
2210
- const issuesTemplatePath = path.join(specSourceDir, "templates", "issues.template.csv");
2378
+ let issuesTemplatePath = path.join(specSourceDir, "templates", "issues.template.csv");
2379
+ if (csvOnly && !options.branch) {
2380
+ const globalTemplatePath = path.join(getSpecHomeDir(), "templates", "issues.template.csv");
2381
+ if (fs.existsSync(globalTemplatePath)) {
2382
+ issuesTemplatePath = globalTemplatePath;
2383
+ } else if (!options.quiet) {
2384
+ log(options, "[warn] 未检测到全局 Spec templates,已使用内置模板生成 issues.csv。可执行: ling spec enable");
2385
+ }
2386
+ }
2211
2387
  const issuesTemplate = stripUtf8Bom(fs.readFileSync(issuesTemplatePath, "utf8"));
2212
2388
  const hasIssues = fs.existsSync(issuesPath);
2213
2389
  if (hasIssues) {
@@ -2246,7 +2422,8 @@ async function commandSpecInit(options) {
2246
2422
  }
2247
2423
 
2248
2424
  const requestedTargets = normalizeTargets(options.targets);
2249
- const shouldInitTargets = options.path ? requestedTargets.length > 0 : true;
2425
+ const isSpecWorkspaceMode = Boolean(!options.path && options.specWorkspace);
2426
+ const shouldInitTargets = isSpecWorkspaceMode ? true : requestedTargets.length > 0;
2250
2427
  const targets = shouldInitTargets ? (requestedTargets.length > 0 ? requestedTargets : [...SUPPORTED_TARGETS]) : [];
2251
2428
  if (targets.length > 0) {
2252
2429
  await initTargets(workspaceRoot, targets, options, prompter);
@@ -2260,7 +2437,9 @@ async function commandSpecInit(options) {
2260
2437
  }
2261
2438
 
2262
2439
  function commandSpecDoctor(options) {
2263
- const workspaceRoot = options.path ? resolveWorkspaceRoot(options.path) : getSpecWorkspaceDir();
2440
+ const workspaceRoot = options.path
2441
+ ? resolveWorkspaceRoot(options.path)
2442
+ : (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
2264
2443
  const result = checkSpecProjectIntegrity(workspaceRoot);
2265
2444
  const state = result.status === "ok" ? "installed" : result.status;
2266
2445
 
@@ -2279,6 +2458,21 @@ function commandSpecDoctor(options) {
2279
2458
 
2280
2459
  console.log(state === "installed" ? "[ok] Spec 项目资产状态正常" : "[warn] Spec 项目资产存在问题");
2281
2460
  console.log(` 工作区: ${workspaceRoot}`);
2461
+ const specDir = path.join(workspaceRoot, ".ling", "spec");
2462
+ const hasSpecDir = fs.existsSync(specDir);
2463
+ const issuesPath = path.join(workspaceRoot, "issues.csv");
2464
+ const hasIssues = fs.existsSync(issuesPath);
2465
+ if (hasSpecDir) {
2466
+ console.log(" 模式: project");
2467
+ console.log(` Spec 根目录: ${specDir}`);
2468
+ } else if (hasIssues) {
2469
+ const globalSpecSummary = evaluateSpecState();
2470
+ const fallback = globalSpecSummary.state === "installed";
2471
+ console.log(` 模式: csv-only${fallback ? " (global fallback)" : ""}`);
2472
+ if (fallback) {
2473
+ console.log(` Spec 根目录: ${getSpecHomeDir()}`);
2474
+ }
2475
+ }
2282
2476
  console.log(` 任务数: ${result.stats.total}`);
2283
2477
  console.log(` 进行中: ${result.stats.inProgress}`);
2284
2478
  for (const issue of result.issues || []) {
@@ -2368,23 +2562,23 @@ async function commandSpec(options) {
2368
2562
  }
2369
2563
 
2370
2564
  async function initTargets(workspaceRoot, targets, options, prompter) {
2371
- for (const target of targets) {
2565
+ for (const group of groupTargetsByInstallSurface(targets)) {
2372
2566
  const runOptions = { ...options };
2373
2567
  const conflicts = [];
2374
2568
 
2375
- if (target === "gemini") {
2569
+ if (group.installSurface === ".agent") {
2376
2570
  const agentDir = path.join(workspaceRoot, ".agent");
2377
2571
  if (fs.existsSync(agentDir)) {
2378
2572
  conflicts.push({
2379
- category: "project:gemini",
2573
+ category: "project:shared-agent",
2380
2574
  label: ".agent",
2381
2575
  path: agentDir,
2382
- target,
2576
+ target: group.logicalTargets.join(","),
2383
2577
  });
2384
2578
  }
2385
2579
  }
2386
2580
 
2387
- if (target === "codex") {
2581
+ if (group.adapterTarget === "codex") {
2388
2582
  const managedDir = path.join(workspaceRoot, ".agents");
2389
2583
  const legacyDir = path.join(workspaceRoot, ".codex");
2390
2584
  if (fs.existsSync(managedDir) || fs.existsSync(legacyDir)) {
@@ -2435,10 +2629,13 @@ async function initTargets(workspaceRoot, targets, options, prompter) {
2435
2629
  }
2436
2630
  }
2437
2631
 
2438
- const adapter = createAdapter(target, workspaceRoot, runOptions);
2439
- log(options, `[sync] 正在初始化目标 [${target}] ...`);
2632
+ const adapter = createAdapter(group.adapterTarget, workspaceRoot, runOptions);
2633
+ log(options, `[sync] 正在初始化目标 [${group.logicalTargets.join(", ")}] ...`);
2440
2634
  adapter.install(BUNDLED_AGENT_DIR);
2441
- registerWorkspaceTarget(workspaceRoot, target, runOptions);
2635
+ recordWorkspaceInstallTargets(workspaceRoot, group.logicalTargets, runOptions);
2636
+ for (const target of group.logicalTargets) {
2637
+ registerWorkspaceTarget(workspaceRoot, target, runOptions);
2638
+ }
2442
2639
  }
2443
2640
  }
2444
2641
 
@@ -2523,71 +2720,73 @@ async function commandUpdate(options) {
2523
2720
 
2524
2721
  let updatedAny = false;
2525
2722
  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
- }
2723
+ for (const group of groupTargetsByInstallSurface(targets)) {
2724
+ if (!isTargetInstalled(workspaceRoot, group.adapterTarget) && options.targets.length > 0) {
2725
+ throw new Error(`目标未安装: ${group.logicalTargets.join(", ")}`);
2726
+ }
2727
+ if (!isTargetInstalled(workspaceRoot, group.adapterTarget)) {
2728
+ log(options, `[skip] 目标未安装,跳过: ${group.logicalTargets.join(", ")}`);
2729
+ continue;
2730
+ }
2534
2731
 
2535
- const runOptions = { ...options, force: true };
2732
+ const runOptions = { ...options, force: true };
2733
+ const timestamp = nowISO().replace(/[:.]/g, "-");
2536
2734
 
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");
2735
+ if (group.installSurface === ".agent") {
2736
+ const agentDir = path.join(workspaceRoot, ".agent");
2737
+ if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
2738
+ let action = "backup";
2739
+ if (prompter) {
2740
+ action = await prompter.resolveConflict({
2741
+ category: "project:shared-agent",
2742
+ label: ".agent",
2743
+ path: agentDir,
2744
+ target: group.logicalTargets.join(","),
2745
+ });
2746
+ }
2747
+ if (action === "keep") {
2748
+ log(options, "[skip] 已保留现有资产,跳过更新: .agent");
2749
+ continue;
2750
+ }
2751
+ if (action === "backup") {
2752
+ backupWorkspaceDir(workspaceRoot, agentDir, ".agent-backup", timestamp, options, "工作区资产 .agent");
2753
+ }
2556
2754
  }
2557
2755
  }
2558
- }
2559
2756
 
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)}`);
2757
+ if (group.adapterTarget === "codex") {
2758
+ const managedDir = path.join(workspaceRoot, ".agents");
2759
+ const legacyDir = path.join(workspaceRoot, ".codex");
2760
+ const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
2761
+ const conflict = evaluateCodexUpdateConflict(activeDir);
2762
+ if (conflict.hasConflict) {
2763
+ let action = "backup";
2764
+ if (prompter) {
2765
+ action = await prompter.resolveConflict({
2766
+ category: "project:codex",
2767
+ label: fs.existsSync(managedDir) ? ".agents" : ".codex",
2768
+ path: activeDir,
2769
+ target: group.logicalTargets.join(","),
2770
+ });
2771
+ }
2772
+ if (action === "keep") {
2773
+ log(options, `[skip] 已保留现有资产,跳过更新: ${path.basename(activeDir)}`);
2774
+ continue;
2775
+ }
2776
+ if (action === "backup") {
2777
+ backupWorkspaceDir(workspaceRoot, activeDir, ".agents-backup", timestamp, options, `工作区资产 ${path.basename(activeDir)}`);
2778
+ }
2582
2779
  }
2583
2780
  }
2584
- }
2585
2781
 
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;
2782
+ const adapter = createAdapter(group.adapterTarget, workspaceRoot, runOptions);
2783
+ log(options, `[sync] 更新 [${group.logicalTargets.join(", ")}] ...`);
2784
+ adapter.update(BUNDLED_AGENT_DIR);
2785
+ recordWorkspaceInstallTargets(workspaceRoot, group.logicalTargets, runOptions);
2786
+ for (const target of group.logicalTargets) {
2787
+ registerWorkspaceTarget(workspaceRoot, target, runOptions);
2788
+ }
2789
+ updatedAny = true;
2591
2790
  }
2592
2791
  } finally {
2593
2792
  if (prompter) prompter.close();
@@ -2680,7 +2879,7 @@ async function commandUpdateAll(options) {
2680
2879
  const installedTargets = detectInstalledTargets(workspacePath);
2681
2880
  let targets = [];
2682
2881
  if (requestedTargets.length > 0) {
2683
- targets = installedTargets.filter((target) => requestedTargets.includes(target));
2882
+ targets = requestedTargets.filter((target) => isTargetInstalled(workspacePath, target));
2684
2883
  } else {
2685
2884
  targets = [...Object.keys(item.targets || {}), ...installedTargets];
2686
2885
  }
@@ -2696,9 +2895,9 @@ async function commandUpdateAll(options) {
2696
2895
  log(options, `[sync] [${i + 1}/${records.length}] 更新: ${workspacePath} [${targets.join(", ")}]`);
2697
2896
 
2698
2897
  const updatedTargets = [];
2699
- for (const target of targets) {
2700
- if (!isTargetInstalled(workspacePath, target)) {
2701
- log(options, `[skip] [${i + 1}/${records.length}] 目标未安装,跳过: ${target}`);
2898
+ for (const group of groupTargetsByInstallSurface(targets)) {
2899
+ if (!isTargetInstalled(workspacePath, group.adapterTarget)) {
2900
+ log(options, `[skip] [${i + 1}/${records.length}] 目标未安装,跳过: ${group.logicalTargets.join(", ")}`);
2702
2901
  continue;
2703
2902
  }
2704
2903
 
@@ -2712,19 +2911,19 @@ async function commandUpdateAll(options) {
2712
2911
 
2713
2912
  const timestampForBackup = nowISO().replace(/[:.]/g, "-");
2714
2913
 
2715
- if (target === "gemini") {
2914
+ if (group.installSurface === ".agent") {
2716
2915
  const agentDir = path.join(workspacePath, ".agent");
2717
2916
  if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
2718
2917
  let action = "backup";
2719
2918
  if (prompter) {
2720
2919
  action = await prompter.resolveConflict({
2721
- category: "update-all:project:gemini",
2920
+ category: "update-all:project:shared-agent",
2722
2921
  label: `.agent (${workspacePath})`,
2723
2922
  path: agentDir,
2724
2923
  });
2725
2924
  }
2726
2925
  if (action === "keep") {
2727
- log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [gemini]`);
2926
+ log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
2728
2927
  continue;
2729
2928
  }
2730
2929
  if (action === "backup") {
@@ -2733,7 +2932,7 @@ async function commandUpdateAll(options) {
2733
2932
  }
2734
2933
  }
2735
2934
 
2736
- if (target === "codex") {
2935
+ if (group.adapterTarget === "codex") {
2737
2936
  const managedDir = path.join(workspacePath, ".agents");
2738
2937
  const legacyDir = path.join(workspacePath, ".codex");
2739
2938
  const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
@@ -2757,13 +2956,14 @@ async function commandUpdateAll(options) {
2757
2956
  }
2758
2957
  }
2759
2958
 
2760
- const adapter = createAdapter(target, workspacePath, runOptions);
2959
+ const adapter = createAdapter(group.adapterTarget, workspacePath, runOptions);
2761
2960
  adapter.update(BUNDLED_AGENT_DIR);
2762
- updatedTargets.push(target);
2961
+ recordWorkspaceInstallTargets(workspacePath, group.logicalTargets, runOptions);
2962
+ updatedTargets.push(...group.logicalTargets);
2763
2963
  } catch (err) {
2764
2964
  failed += 1;
2765
2965
  if (!options.quiet) {
2766
- console.error(`[error] 更新失败: ${workspacePath} [${target}]`);
2966
+ console.error(`[error] 更新失败: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
2767
2967
  console.error(` ${err.message}`);
2768
2968
  }
2769
2969
  }
@@ -3068,6 +3268,25 @@ function countSkillsRecursive(dir) {
3068
3268
  return count;
3069
3269
  }
3070
3270
 
3271
+ function printSharedAgentTargetStatus(workspaceRoot, targetState, label) {
3272
+ const agentDir = path.join(workspaceRoot, ".agent");
3273
+ const agentsCount = countFilesIfExists(path.join(agentDir, "agents"), (name) => name.endsWith(".md"));
3274
+ const workflowsCount = countFilesIfExists(path.join(agentDir, "workflows"), (name) => name.endsWith(".md"));
3275
+ const skillsCount = countSkillsRecursive(path.join(agentDir, "skills"));
3276
+ console.log(`\n[${label}]`);
3277
+ console.log(` 状态: ${targetState.state}`);
3278
+ console.log(` 路径: ${agentDir}`);
3279
+ if (targetState.state === "installed") {
3280
+ console.log(` Agents: ${agentsCount}`);
3281
+ console.log(` Skills: ${skillsCount}`);
3282
+ console.log(` Workflows: ${workflowsCount}`);
3283
+ return;
3284
+ }
3285
+ for (const issue of targetState.integrity.issues || []) {
3286
+ console.log(` Issue: ${issue}`);
3287
+ }
3288
+ }
3289
+
3071
3290
  function commandStatus(options) {
3072
3291
  const workspaceRoot = resolveWorkspaceRoot(options.path);
3073
3292
  const summary = evaluateWorkspaceState(workspaceRoot, options);
@@ -3098,22 +3317,12 @@ function commandStatus(options) {
3098
3317
 
3099
3318
  const geminiState = summary.targets.find((item) => item.targetName === "gemini");
3100
3319
  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
- }
3320
+ printSharedAgentTargetStatus(workspaceRoot, geminiState, "gemini");
3321
+ }
3322
+
3323
+ const antigravityState = summary.targets.find((item) => item.targetName === "antigravity");
3324
+ if (antigravityState) {
3325
+ printSharedAgentTargetStatus(workspaceRoot, antigravityState, "antigravity");
3117
3326
  }
3118
3327
 
3119
3328
  const codexState = summary.targets.find((item) => item.targetName === "codex");