@mison/ling 1.2.2 → 1.2.4
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 +16 -0
- package/README.md +4 -2
- package/bin/ling-cli.js +61 -0
- package/docs/PLAN.md +2 -1
- package/docs/TECH.md +5 -3
- package/package.json +1 -1
- package/scripts/ci-verify.js +3 -2
- package/scripts/health-check.js +3 -1
- package/tests/global-sync.test.js +26 -1
- package/tests/spec-profile.test.js +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [ling-1.2.4] - 2026-03-15
|
|
11
|
+
|
|
12
|
+
### 修复
|
|
13
|
+
|
|
14
|
+
- 修复 `ling spec enable` 在全局 `codex` Skills 已存在时重新写回 Gemini CLI 重复 Skill 的问题,并避免将仅同名但内容不同的 Gemini 专用 Skill 误判为重复副本。
|
|
15
|
+
|
|
16
|
+
### 维护
|
|
17
|
+
|
|
18
|
+
- 完善仓库根 `.gitignore`,补充测试临时目录、日志缓存和 Web 构建产物忽略规则。
|
|
19
|
+
|
|
20
|
+
## [ling-1.2.3] - 2026-03-15
|
|
21
|
+
|
|
22
|
+
### 修复
|
|
23
|
+
|
|
24
|
+
- 修复 Gemini CLI 全局 Skill 冲突:当 `~/.agents/skills/` 已提供同名 universal Skill 时,`ling global sync --target gemini` 与默认全局同步会自动备份并清理 `~/.gemini/skills/` 中的重复副本,避免 Gemini 启动时出现 `Skill conflict detected`。
|
|
25
|
+
|
|
10
26
|
## [ling-1.2.2] - 2026-03-14
|
|
11
27
|
|
|
12
28
|
### 修复
|
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/skills/`、`~/.gemini/antigravity/skills/`;若与 universal 根目录重复,会自动清理 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/...`)。
|
|
@@ -128,6 +128,8 @@ ling spec init --csv-only
|
|
|
128
128
|
- `--csv-only`:`<project>/issues.csv`、`<project>/docs/reviews/`、`<project>/docs/handoff/`
|
|
129
129
|
- 全局 Spec 资源:`~/.ling/spec/templates/`、`~/.ling/spec/references/`、`~/.ling/spec/profiles/`
|
|
130
130
|
|
|
131
|
+
如果这台电脑已经有 `~/.agents/skills/`,`ling spec enable` 在启用 `gemini` 目标时也会自动清理 `~/.gemini/skills/` 里的同名重复副本;若同名 Skill 内容不同,则会保留 Gemini 专用版本,不会误删。
|
|
132
|
+
|
|
131
133
|
如果你只想要一个本机演练空间,而不是某个真实项目:
|
|
132
134
|
|
|
133
135
|
```bash
|
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,59 @@ 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, skippedConflicts: 0 };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
let removed = 0;
|
|
1371
|
+
let backedUp = 0;
|
|
1372
|
+
let skippedConflicts = 0;
|
|
1373
|
+
for (const skillName of listSkillDirectories(geminiRoot)) {
|
|
1374
|
+
const geminiSkillDir = path.join(geminiRoot, skillName);
|
|
1375
|
+
const universalSkillDir = path.join(universalRoot, skillName);
|
|
1376
|
+
if (!fs.existsSync(universalSkillDir)) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (!areDirectoriesEqual(geminiSkillDir, universalSkillDir)) {
|
|
1381
|
+
log(options, `[warn] Gemini CLI Skill 与 universal 根目录同名但内容不同,已保留: ${geminiSkillDir}`);
|
|
1382
|
+
skippedConflicts += 1;
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (options.dryRun) {
|
|
1387
|
+
log(options, `[dry-run] 将移除 Gemini CLI 重复 Skill(由 universal 根目录提供): ${geminiSkillDir}`);
|
|
1388
|
+
removed += 1;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
backupSkillDirectory(
|
|
1393
|
+
REDUNDANT_GEMINI_GLOBAL_SKILLS_BACKUP_TARGET,
|
|
1394
|
+
skillName,
|
|
1395
|
+
geminiSkillDir,
|
|
1396
|
+
timestamp,
|
|
1397
|
+
options,
|
|
1398
|
+
);
|
|
1399
|
+
backedUp += 1;
|
|
1400
|
+
fs.rmSync(geminiSkillDir, { recursive: true, force: true });
|
|
1401
|
+
log(options, `[clean] 已移除 Gemini CLI 重复 Skill: ${geminiSkillDir}`);
|
|
1402
|
+
removed += 1;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (!options.dryRun && fs.existsSync(geminiRoot) && fs.readdirSync(geminiRoot).length === 0) {
|
|
1406
|
+
fs.rmSync(geminiRoot, { recursive: true, force: true });
|
|
1407
|
+
log(options, `[clean] 已删除空的 Gemini CLI skills 根目录: ${geminiRoot}`);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
return { removed, backedUp, skippedConflicts };
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1359
1413
|
function syncSkillDirectory(destination, srcDir, destDir, timestamp, options) {
|
|
1360
1414
|
const exists = fs.existsSync(destDir);
|
|
1361
1415
|
if (exists) {
|
|
@@ -1625,6 +1679,10 @@ async function commandGlobalSync(options) {
|
|
|
1625
1679
|
|
|
1626
1680
|
if (target === "codex") {
|
|
1627
1681
|
migrateLegacyCodexGlobalSkills(timestamp, options);
|
|
1682
|
+
reconcileGeminiGlobalSkillsAgainstUniversalRoot(timestamp, options);
|
|
1683
|
+
}
|
|
1684
|
+
if (target === "gemini") {
|
|
1685
|
+
reconcileGeminiGlobalSkillsAgainstUniversalRoot(timestamp, options);
|
|
1628
1686
|
}
|
|
1629
1687
|
} finally {
|
|
1630
1688
|
if (plan.cleanup) plan.cleanup();
|
|
@@ -2701,6 +2759,9 @@ async function commandSpecEnable(options) {
|
|
|
2701
2759
|
if (targets.includes("codex")) {
|
|
2702
2760
|
migrateLegacyCodexGlobalSkills(timestamp, options);
|
|
2703
2761
|
}
|
|
2762
|
+
if (targets.includes("codex") || targets.includes("gemini")) {
|
|
2763
|
+
reconcileGeminiGlobalSkillsAgainstUniversalRoot(timestamp, options);
|
|
2764
|
+
}
|
|
2704
2765
|
|
|
2705
2766
|
log(options, `[ok] Spec Profile 已启用 (Targets: ${targets.join(", ")})`);
|
|
2706
2767
|
}
|
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/...`)。
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
- References:`$HOME/.ling/spec/references/`
|
|
42
42
|
- Profiles:`$HOME/.ling/spec/profiles/`
|
|
43
43
|
- 回退原则:启用前先备份同名资源;停用时优先恢复原资源
|
|
44
|
+
- 与全局 Skills 的关系:若机器上已存在 `$HOME/.agents/skills/`,启用包含 `gemini` 的 Spec 目标时会自动清理 `$HOME/.gemini/skills/` 中与 universal 根目录完全相同的重复副本;同名但内容不同的 Gemini 专用 Skill 会保留
|
|
44
45
|
- 项目层(工作区级)命令:`ling spec init` / `ling spec doctor`
|
|
45
46
|
- 默认作用于当前目录;`--spec-workspace` 才会使用 `$HOME/.ling/spec-workspace`
|
|
46
47
|
- 完整模式:写入 `.ling/spec/`、`issues.csv`、`docs/reviews/`、`docs/handoff/`
|
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>/...`;若只是同名但内容不同,则记录警告并保留 Gemini 专用版本
|
|
68
69
|
|
|
69
70
|
### 测试隔离
|
|
70
71
|
- `LING_GLOBAL_ROOT`:替代 `$HOME`(用于测试与 CI,避免污染真实用户目录)
|
|
@@ -88,6 +89,7 @@ cd web && npm install && npm run lint
|
|
|
88
89
|
- 会校验 `issues.csv` 表头与状态枚举
|
|
89
90
|
- 当项目缺少 `.ling/spec/` 但已启用全局 Spec 时,会使用全局 Spec 资产作为后备
|
|
90
91
|
- Spec 源目录:`.spec/`
|
|
92
|
+
- 若 `spec enable` 包含 `gemini` 或 `codex`,启用后还会执行一次 Gemini CLI 重复 Skill 清理,避免全局 universal Skill 与 Gemini CLI 同时暴露同名副本
|
|
91
93
|
|
|
92
94
|
### 心智模型
|
|
93
95
|
- `ling spec enable`:给这台电脑安装 Spec 工具箱
|
|
@@ -171,8 +173,8 @@ Windows PowerShell 示例:
|
|
|
171
173
|
```powershell
|
|
172
174
|
$ts = "2026-03-12T12-00-00-000Z"
|
|
173
175
|
$skill = "clean-code"
|
|
174
|
-
Remove-Item "$HOME\\.
|
|
175
|
-
Copy-Item "$HOME\\.ling\\backups\\global\\$ts\\codex\\$skill" "$HOME\\.
|
|
176
|
+
Remove-Item "$HOME\\.agents\\skills\\$skill" -Recurse -Force -ErrorAction SilentlyContinue
|
|
177
|
+
Copy-Item "$HOME\\.ling\\backups\\global\\$ts\\codex\\$skill" "$HOME\\.agents\\skills\\$skill" -Recurse -Force
|
|
176
178
|
```
|
|
177
179
|
|
|
178
180
|
Gemini CLI 回滚示例:
|
package/package.json
CHANGED
package/scripts/ci-verify.js
CHANGED
|
@@ -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
|
}
|
package/scripts/health-check.js
CHANGED
|
@@ -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(
|
|
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 },
|
|
@@ -142,6 +142,47 @@ describe("Spec Profile", () => {
|
|
|
142
142
|
assert.ok(!fs.existsSync(antigravitySkill), "gemini spec enable should not install antigravity skill");
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
test("spec enable should prune redundant gemini skills when codex universal skills already exist", () => {
|
|
146
|
+
const env = { LING_GLOBAL_ROOT: tempRoot };
|
|
147
|
+
|
|
148
|
+
const globalResult = runCli(["global", "sync", "--target", "codex", "--quiet"], { env });
|
|
149
|
+
assert.strictEqual(globalResult.status, 0, globalResult.stderr || globalResult.stdout);
|
|
150
|
+
|
|
151
|
+
const enableResult = runCli(["spec", "enable", "--quiet"], { env });
|
|
152
|
+
assert.strictEqual(enableResult.status, 0, enableResult.stderr || enableResult.stdout);
|
|
153
|
+
|
|
154
|
+
const geminiSkillRoot = path.join(tempRoot, ".gemini", "skills");
|
|
155
|
+
const antigravitySkill = path.join(tempRoot, ".gemini", "antigravity", "skills", "harness-engineering", "SKILL.md");
|
|
156
|
+
const codexSkill = path.join(tempRoot, ".agents", "skills", "harness-engineering", "SKILL.md");
|
|
157
|
+
assert.ok(!fs.existsSync(geminiSkillRoot), "redundant gemini spec skills should be pruned after enable");
|
|
158
|
+
assert.ok(fs.existsSync(antigravitySkill), "antigravity spec skill should still be installed");
|
|
159
|
+
assert.ok(fs.existsSync(codexSkill), "codex spec skill should still be installed");
|
|
160
|
+
|
|
161
|
+
const backupRoot = path.join(tempRoot, ".ling", "backups", "global");
|
|
162
|
+
const timestamps = fs
|
|
163
|
+
.readdirSync(backupRoot, { withFileTypes: true })
|
|
164
|
+
.filter((entry) => entry.isDirectory())
|
|
165
|
+
.map((entry) => entry.name);
|
|
166
|
+
assert.ok(timestamps.length > 0, "backup timestamp directory missing");
|
|
167
|
+
|
|
168
|
+
const redundantBackup = path.join(backupRoot, timestamps[timestamps.length - 1], "gemini-cli-redundant", "harness-engineering", "SKILL.md");
|
|
169
|
+
assert.ok(fs.existsSync(redundantBackup), "backup for redundant gemini spec skill missing");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("spec enable should keep gemini-only skill when content differs from codex universal root", () => {
|
|
173
|
+
const env = { LING_GLOBAL_ROOT: tempRoot };
|
|
174
|
+
const codexSkillDir = path.join(tempRoot, ".agents", "skills", "harness-engineering");
|
|
175
|
+
fs.mkdirSync(codexSkillDir, { recursive: true });
|
|
176
|
+
fs.writeFileSync(path.join(codexSkillDir, "SKILL.md"), "codex-only variant", "utf8");
|
|
177
|
+
|
|
178
|
+
const enableResult = runCli(["spec", "enable", "--target", "gemini", "--quiet"], { env });
|
|
179
|
+
assert.strictEqual(enableResult.status, 0, enableResult.stderr || enableResult.stdout);
|
|
180
|
+
|
|
181
|
+
const geminiSkill = path.join(tempRoot, ".gemini", "skills", "harness-engineering", "SKILL.md");
|
|
182
|
+
assert.ok(fs.existsSync(geminiSkill), "gemini-only skill should be kept when content differs");
|
|
183
|
+
assert.strictEqual(fs.readFileSync(geminiSkill, "utf8"), fs.readFileSync(path.join(REPO_ROOT, ".spec", "skills", "harness-engineering", "SKILL.md"), "utf8"));
|
|
184
|
+
});
|
|
185
|
+
|
|
145
186
|
test("spec enable should migrate legacy codex global skill path to ~/.agents/skills", () => {
|
|
146
187
|
const env = { LING_GLOBAL_ROOT: tempRoot };
|
|
147
188
|
const stateDir = path.join(tempRoot, ".ling", "spec");
|