@mison/ling 1.2.2 → 1.2.3

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 CHANGED
@@ -7,6 +7,12 @@
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [ling-1.2.3] - 2026-03-15
11
+
12
+ ### 修复
13
+
14
+ - 修复 Gemini CLI 全局 Skill 冲突:当 `~/.agents/skills/` 已提供同名 universal Skill 时,`ling global sync --target gemini` 与默认全局同步会自动备份并清理 `~/.gemini/skills/` 中的重复副本,避免 Gemini 启动时出现 `Skill conflict detected`。
15
+
10
16
  ## [ling-1.2.2] - 2026-03-14
11
17
 
12
18
  ### 修复
package/README.md CHANGED
@@ -47,7 +47,7 @@ ling global sync --target antigravity
47
47
  | 你要做什么 | 命令 | 结果 |
48
48
  | --- | --- | --- |
49
49
  | 给当前项目安装完整资产 | `ling init` | 项目内生成 `.agent/` / `.agents/`;共享 `.agent/` 时会维护 `.ling/install-state.json` |
50
- | 给电脑全局同步可复用 Skills | `ling global sync` | 写入 `~/.agents/skills/`、`~/.gemini/skills/` |
50
+ | 给电脑全局同步可复用 Skills | `ling global sync` | 写入 `~/.agents/skills/`、`~/.gemini/antigravity/skills/` 等,并自动清理 Gemini CLI 的重复副本 |
51
51
  | 给项目启用 Spec 工作流 | `ling spec init` | 项目内生成 `issues.csv` 等 Spec 资产 |
52
52
 
53
53
  一句话区分:
@@ -90,7 +90,7 @@ ling global status
90
90
  ```
91
91
 
92
92
  - `codex` -> `~/.agents/skills/`
93
- - `gemini` -> `~/.gemini/skills/`
93
+ - `gemini` -> `~/.gemini/skills/`(若已存在 `~/.agents/skills/`,会自动移除重复副本,避免 Gemini CLI 冲突提示)
94
94
  - `antigravity` -> `~/.gemini/antigravity/skills/`
95
95
 
96
96
  若检测到旧版 `~/.codex/skills/`,`ling global sync --target codex` 会将其迁移到 `~/.agents/skills/` 并清理,避免 Skills 重复(冲突内容会先备份到 `~/.ling/backups/global/<timestamp>/codex-legacy/...`)。
package/bin/ling-cli.js CHANGED
@@ -62,6 +62,7 @@ const SPEC_TEMPLATE_REQUIRED_FILES = ["issues.template.csv", "driver-prompt.md",
62
62
  const SPEC_REFERENCE_REQUIRED_FILES = ["README.md", "harness-engineering-digest.md", "gda-framework.md", "cse-quickstart.md"];
63
63
  const SPEC_PROFILE_REQUIRED_FILES = ["codex/AGENTS.spec.md", "codex/ling.spec.rules.md", "gemini/GEMINI.spec.md"];
64
64
  const LEGACY_CODEX_GLOBAL_SKILLS_BACKUP_TARGET = "codex-legacy";
65
+ const REDUNDANT_GEMINI_GLOBAL_SKILLS_BACKUP_TARGET = "gemini-cli-redundant";
65
66
 
66
67
  function nowISO() {
67
68
  return new Date().toISOString();
@@ -1356,6 +1357,52 @@ function migrateLegacyCodexGlobalSkills(timestamp, options) {
1356
1357
  return { migrated, removed, backedUp, conflicts, remaining };
1357
1358
  }
1358
1359
 
1360
+ function reconcileGeminiGlobalSkillsAgainstUniversalRoot(timestamp, options) {
1361
+ const [codexDestination] = getGlobalDestinations("codex");
1362
+ const [geminiDestination] = getGlobalDestinations("gemini");
1363
+ const universalRoot = codexDestination.skillsRoot;
1364
+ const geminiRoot = geminiDestination.skillsRoot;
1365
+
1366
+ if (!fs.existsSync(universalRoot) || !fs.existsSync(geminiRoot)) {
1367
+ return { removed: 0, backedUp: 0 };
1368
+ }
1369
+
1370
+ let removed = 0;
1371
+ let backedUp = 0;
1372
+ for (const skillName of listSkillDirectories(geminiRoot)) {
1373
+ const geminiSkillDir = path.join(geminiRoot, skillName);
1374
+ const universalSkillDir = path.join(universalRoot, skillName);
1375
+ if (!fs.existsSync(universalSkillDir)) {
1376
+ continue;
1377
+ }
1378
+
1379
+ if (options.dryRun) {
1380
+ log(options, `[dry-run] 将移除 Gemini CLI 重复 Skill(由 universal 根目录提供): ${geminiSkillDir}`);
1381
+ removed += 1;
1382
+ continue;
1383
+ }
1384
+
1385
+ backupSkillDirectory(
1386
+ REDUNDANT_GEMINI_GLOBAL_SKILLS_BACKUP_TARGET,
1387
+ skillName,
1388
+ geminiSkillDir,
1389
+ timestamp,
1390
+ options,
1391
+ );
1392
+ backedUp += 1;
1393
+ fs.rmSync(geminiSkillDir, { recursive: true, force: true });
1394
+ log(options, `[clean] 已移除 Gemini CLI 重复 Skill: ${geminiSkillDir}`);
1395
+ removed += 1;
1396
+ }
1397
+
1398
+ if (!options.dryRun && fs.existsSync(geminiRoot) && fs.readdirSync(geminiRoot).length === 0) {
1399
+ fs.rmSync(geminiRoot, { recursive: true, force: true });
1400
+ log(options, `[clean] 已删除空的 Gemini CLI skills 根目录: ${geminiRoot}`);
1401
+ }
1402
+
1403
+ return { removed, backedUp };
1404
+ }
1405
+
1359
1406
  function syncSkillDirectory(destination, srcDir, destDir, timestamp, options) {
1360
1407
  const exists = fs.existsSync(destDir);
1361
1408
  if (exists) {
@@ -1625,6 +1672,10 @@ async function commandGlobalSync(options) {
1625
1672
 
1626
1673
  if (target === "codex") {
1627
1674
  migrateLegacyCodexGlobalSkills(timestamp, options);
1675
+ reconcileGeminiGlobalSkillsAgainstUniversalRoot(timestamp, options);
1676
+ }
1677
+ if (target === "gemini") {
1678
+ reconcileGeminiGlobalSkillsAgainstUniversalRoot(timestamp, options);
1628
1679
  }
1629
1680
  } finally {
1630
1681
  if (plan.cleanup) plan.cleanup();
package/docs/PLAN.md CHANGED
@@ -21,7 +21,7 @@
21
21
  - 默认行为:`ling global sync` 未指定 `--target/--targets` 时,同步 `codex + gemini + antigravity`
22
22
  - 目标路径:
23
23
  - `codex` -> `$HOME/.agents/skills/`
24
- - `gemini` -> `$HOME/.gemini/skills/`
24
+ - `gemini` -> `$HOME/.gemini/skills/`(若与 `$HOME/.agents/skills/` 重叠则清理重复副本)
25
25
  - `antigravity` -> `$HOME/.gemini/antigravity/skills/`
26
26
  - 安全边界:全局只同步 Skills,不写入全局 Rules/Agents/Workflows。
27
27
  - 旧版迁移:若存在遗留 `~/.codex/skills/`,同步 codex 时会迁移到 `$HOME/.agents/skills/` 并清理遗留根目录,避免 Skills 重复(冲突内容会备份到 `$HOME/.ling/backups/global/<timestamp>/codex-legacy/...`)。
package/docs/TECH.md CHANGED
@@ -28,7 +28,7 @@ cd web && npm install && npm run lint
28
28
 
29
29
  ### 全局级(仅同步 Skills)
30
30
  - `codex`:`$HOME/.agents/skills/`
31
- - `gemini-cli`:`$HOME/.gemini/skills/`
31
+ - `gemini-cli`:`$HOME/.gemini/skills/`(若存在与 `$HOME/.agents/skills/` 重叠的同名 Skill,灵轨会在同步后清理重复副本)
32
32
  - `antigravity`:`$HOME/.gemini/antigravity/skills/`
33
33
 
34
34
  > 说明:仓库内 Skills 源路径为 `.agents/skills/`,全局同步会将其投影到真实工具读取的全局目录;仓库 Canonical 仍是 `.agents/`。
@@ -65,6 +65,7 @@ cd web && npm install && npm run lint
65
65
  - 覆盖前备份:覆盖同名 Skill 前备份到 `$HOME/.ling/backups/global/<timestamp>/<consumer>/<skill>/...`
66
66
  - `consumer` 可能是 `codex`、`gemini-cli`、`antigravity`
67
67
  - 遗留迁移:若检测到旧版 `~/.codex/skills/`,同步 codex 时会迁移到 `~/.agents/skills/` 并清理,避免 Skills 重复(冲突内容会备份到 `.../codex-legacy/<skill>/...`)
68
+ - Gemini 去重:若 `~/.agents/skills/` 已存在,同步 gemini 后会移除 `~/.gemini/skills/` 内同名重复目录,并备份到 `.../gemini-cli-redundant/<skill>/...`
68
69
 
69
70
  ### 测试隔离
70
71
  - `LING_GLOBAL_ROOT`:替代 `$HOME`(用于测试与 CI,避免污染真实用户目录)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mison/ling",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "面向 Gemini CLI、Antigravity 与 Codex 的中文 AI Agent 模板工具包",
5
5
  "repository": {
6
6
  "type": "git",
@@ -97,12 +97,13 @@ function main() {
97
97
  }
98
98
 
99
99
  const codexSkill = path.join(globalRoot, ".agents", "skills", "workflow-plan", "SKILL.md");
100
- const geminiCliSkill = path.join(globalRoot, ".gemini", "skills", "clean-code", "SKILL.md");
101
100
  const antigravitySkill = path.join(globalRoot, ".gemini", "antigravity", "skills", "clean-code", "SKILL.md");
102
101
  const specCodexSkill = path.join(globalRoot, ".agents", "skills", "harness-engineering");
103
102
  ensureExists(codexSkill, "全局 Codex workflow-plan Skill");
104
- ensureExists(geminiCliSkill, "全局 Gemini CLI clean-code Skill");
105
103
  ensureExists(antigravitySkill, "全局 Antigravity clean-code Skill");
104
+ if (fs.existsSync(path.join(globalRoot, ".gemini", "skills"))) {
105
+ throw new Error("默认全局同步后不应保留重复的 Gemini CLI skills 根目录");
106
+ }
106
107
  if (fs.existsSync(specCodexSkill)) {
107
108
  throw new Error(`spec disable 后仍残留 Spec Skill: ${specCodexSkill}`);
108
109
  }
@@ -114,7 +114,6 @@ function main() {
114
114
 
115
115
  const globalChecks = [
116
116
  path.join(globalRoot, ".agents", "skills", "workflow-plan", "SKILL.md"),
117
- path.join(globalRoot, ".gemini", "skills", "clean-code", "SKILL.md"),
118
117
  path.join(globalRoot, ".gemini", "antigravity", "skills", "clean-code", "SKILL.md"),
119
118
  ];
120
119
  for (const targetPath of globalChecks) {
@@ -122,6 +121,9 @@ function main() {
122
121
  throw new Error(`global sync 未生成预期文件: ${targetPath}`);
123
122
  }
124
123
  }
124
+ if (fs.existsSync(path.join(globalRoot, ".gemini", "skills"))) {
125
+ throw new Error("默认全局同步后不应保留重复的 Gemini CLI skills 根目录");
126
+ }
125
127
  } finally {
126
128
  fs.rmSync(tempRoot, { recursive: true, force: true });
127
129
  }
@@ -136,7 +136,7 @@ describe("Global Sync", () => {
136
136
  assert.ok(fs.existsSync(path.join(codexRoot, "workflow-plan", "SKILL.md")), "missing expected codex workflow skill: workflow-plan");
137
137
 
138
138
  const geminiCliRoot = path.join(tempDir, ".gemini", "skills");
139
- assert.ok(fs.existsSync(path.join(geminiCliRoot, "clean-code", "SKILL.md")), "missing expected gemini-cli skill: clean-code");
139
+ assert.ok(!fs.existsSync(geminiCliRoot), "default sync should prune redundant gemini-cli skills when universal root exists");
140
140
 
141
141
  const antigravityRoot = path.join(tempDir, ".gemini", "antigravity", "skills");
142
142
  assert.ok(fs.existsSync(path.join(antigravityRoot, "clean-code", "SKILL.md")), "missing expected antigravity skill: clean-code");
@@ -169,6 +169,31 @@ describe("Global Sync", () => {
169
169
  assert.ok(!fs.existsSync(antigravityRoot), "gemini sync should not create antigravity skills root");
170
170
  });
171
171
 
172
+ test("global sync gemini should prune redundant ~/.gemini/skills when universal root already exists", () => {
173
+ const codexResult = runCli(["global", "sync", "--target", "codex", "--quiet"], {
174
+ env: { LING_GLOBAL_ROOT: tempDir },
175
+ });
176
+ assert.strictEqual(codexResult.status, 0, codexResult.stderr || codexResult.stdout);
177
+
178
+ const geminiResult = runCli(["global", "sync", "--target", "gemini", "--quiet"], {
179
+ env: { LING_GLOBAL_ROOT: tempDir },
180
+ });
181
+ assert.strictEqual(geminiResult.status, 0, geminiResult.stderr || geminiResult.stdout);
182
+
183
+ const geminiCliRoot = path.join(tempDir, ".gemini", "skills");
184
+ assert.ok(!fs.existsSync(geminiCliRoot), "redundant gemini-cli skills root should be pruned");
185
+
186
+ const backupRoot = path.join(tempDir, ".ling", "backups", "global");
187
+ const timestamps = fs
188
+ .readdirSync(backupRoot, { withFileTypes: true })
189
+ .filter((entry) => entry.isDirectory())
190
+ .map((entry) => entry.name);
191
+ assert.ok(timestamps.length > 0, "backup timestamp directory missing");
192
+
193
+ const redundantBackup = path.join(backupRoot, timestamps[timestamps.length - 1], "gemini-cli-redundant", "clean-code", "SKILL.md");
194
+ assert.ok(fs.existsSync(redundantBackup), "backup for redundant gemini-cli skill missing");
195
+ });
196
+
172
197
  test("global sync should install antigravity skills only into ~/.gemini/antigravity/skills", () => {
173
198
  const result = runCli(["global", "sync", "--target", "antigravity", "--quiet"], {
174
199
  env: { LING_GLOBAL_ROOT: tempDir },