@boyingliu01/xp-gate 0.19.3 → 0.20.1
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/adapters/gate-4.sh +83 -18
- package/adapters/gate-8.sh +115 -29
- package/adapters/iac.sh +1 -1
- package/adapters/powershell.sh +60 -5
- package/adapters/python.sh +38 -4
- package/adapters/typescript.sh +15 -60
- package/bin/xp-gate.js +15 -16
- package/build-integrity/__tests__/import-resolver.test.ts +59 -0
- package/build-integrity/gate-10.ts +17 -2
- package/gate-10.sh +35 -2
- package/gate-4.sh +110 -21
- package/gate-8.sh +115 -29
- package/hooks/gate-10.sh +35 -2
- package/hooks/gate-4.sh +110 -21
- package/hooks/gate-8.sh +115 -29
- package/hooks/lib/jscpd-run.sh +63 -0
- package/hooks/lib/sprint-gate-report.sh +58 -0
- package/hooks/lib/test-failure.sh +152 -0
- package/hooks/lib/typecheck.sh +141 -0
- package/hooks/lib/validate-code-walkthrough.cjs +43 -5
- package/hooks/post-merge +100 -0
- package/hooks/pre-commit +514 -184
- package/hooks/pre-push +75 -19
- package/lib/__tests__/bootstrap.test.js +23 -41
- package/lib/__tests__/check-version.test.js +8 -1
- package/lib/__tests__/detect-deps.test.js +16 -0
- package/lib/__tests__/doctor-drift-round2.test.js +212 -0
- package/lib/__tests__/doctor-executed-surface.test.js +209 -0
- package/lib/__tests__/doctor-hook-drift.test.js +329 -0
- package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
- package/lib/__tests__/doctor-module-drift.test.js +206 -0
- package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
- package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
- package/lib/__tests__/doctor.test.js +155 -0
- package/lib/__tests__/gate-runner.test.js +19 -3
- package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
- package/lib/__tests__/init.test.js +44 -17
- package/lib/__tests__/install-skill-full-dir.test.js +151 -0
- package/lib/__tests__/install-skill.test.js +51 -132
- package/lib/__tests__/language-tools-detect.test.js +158 -0
- package/lib/__tests__/phase-transition.test.js +15 -1
- package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
- package/lib/__tests__/shared-paths.test.js +20 -0
- package/lib/__tests__/shared-utils.test.js +69 -0
- package/lib/__tests__/skill-flags.test.js +48 -0
- package/lib/__tests__/ui-detector.test.ts +6 -2
- package/lib/__tests__/uninstall.test.js +20 -0
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/update-skill.test.js +7 -4
- package/lib/detect-deps.js +3 -20
- package/lib/doctor-tui.js +22 -5
- package/lib/doctor.js +1178 -31
- package/lib/init.js +184 -33
- package/lib/install-cmd.js +7 -4
- package/lib/install-skill.js +38 -95
- package/lib/language-tools.js +83 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-paths.js +5 -2
- package/lib/shared-utils.js +47 -3
- package/lib/uninstall.js +12 -0
- package/lib/update-hooks.js +4 -4
- package/lib/update-skill.js +6 -7
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/gate-m.test.ts +66 -14
- package/mutation/__tests__/mutmut-runner.test.ts +91 -42
- package/mutation/__tests__/test-filter-parity.test.ts +37 -0
- package/mutation/gate-m.ts +21 -8
- package/package.json +2 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +169 -4
- package/plugins/dsh/dsh-overlay.patch.yml +67 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +24 -19
- package/plugins/dsh/setup-dsh.ps1 +160 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/command.test.ts +66 -2
- package/plugins/dsh/src/command.ts +36 -14
- package/plugins/dsh/src/contract.test.ts +31 -1
- package/plugins/dsh/src/delphi-config.test.ts +190 -0
- package/plugins/dsh/src/delphi-config.ts +223 -0
- package/plugins/dsh/src/delphi-run.test.ts +181 -0
- package/plugins/dsh/src/delphi-run.ts +129 -0
- package/plugins/dsh/src/delphi.test.ts +222 -0
- package/plugins/dsh/src/delphi.ts +247 -0
- package/plugins/dsh/src/dialect.test.ts +59 -0
- package/plugins/dsh/src/dialect.ts +40 -0
- package/plugins/dsh/src/index.ts +70 -4
- package/plugins/dsh/src/skills.test.ts +26 -19
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/agents/delphi-architecture.md +1 -1
- package/plugins/qoder/agents/delphi-feasibility.md +1 -1
- package/plugins/qoder/agents/delphi-technical.md +1 -1
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +9 -8
- package/principles/__tests__/adapter-metrics.test.ts +292 -0
- package/principles/__tests__/analyzer.test.ts +29 -6
- package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
- package/principles/__tests__/baseline-prune.test.ts +338 -0
- package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
- package/principles/__tests__/boy-scout-integration.test.ts +97 -6
- package/principles/__tests__/boy-scout.test.ts +253 -7
- package/principles/__tests__/config-enforcement.test.ts +469 -0
- package/principles/__tests__/config-git-timeout.test.ts +66 -0
- package/principles/__tests__/direct-execution.test.ts +187 -0
- package/principles/__tests__/index.test.ts +16 -3
- package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
- package/principles/adapters/typescript.ts +613 -13
- package/principles/analyzer.ts +22 -0
- package/principles/baseline-prune.ts +217 -0
- package/principles/boy-scout.ts +516 -56
- package/principles/config.ts +362 -54
- package/principles/direct-execution.ts +60 -0
- package/principles/index.ts +40 -17
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
- package/principles/rules/clean-code/code-duplication.ts +8 -8
- package/principles/rules/clean-code/deep-nesting.ts +8 -8
- package/principles/rules/clean-code/god-class.ts +8 -8
- package/principles/rules/clean-code/large-file.ts +15 -11
- package/principles/rules/clean-code/long-function.ts +8 -8
- package/principles/rules/clean-code/magic-numbers.ts +10 -7
- package/principles/rules/clean-code/many-exports.ts +7 -7
- package/principles/rules/clean-code/missing-error-handling.ts +5 -5
- package/principles/rules/clean-code/too-many-params.ts +8 -8
- package/principles/rules/clean-code/unused-imports.ts +5 -5
- package/principles/rules/solid/dip.ts +18 -8
- package/principles/rules/solid/isp.ts +9 -8
- package/principles/rules/solid/lsp.ts +6 -6
- package/principles/rules/solid/ocp.ts +6 -5
- package/principles/rules/solid/srp.ts +7 -8
- package/principles/types.ts +15 -0
- package/scripts/__tests__/sync-package-content.test.js +28 -0
- package/scripts/delphi-external-review.cjs +193 -10
- package/scripts/sync-package-content.js +20 -10
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/delphi-review/SKILL.md +98 -14
- package/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/skills/delphi-review/references/requirements.md +6 -6
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/lib/__tests__/download-skill.test.js +0 -281
- package/lib/download-skill.js +0 -96
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test qoder-delphi-agents
|
|
3
|
+
* @intent Guard the invariants behind Issue #417: Qoder Custom Agents must bind a
|
|
4
|
+
* built-in model as "[DisplayName](modelId)", stale or diverged deployments must
|
|
5
|
+
* surface a warning instead of silently falling back to the session model, and
|
|
6
|
+
* agent deployment must never abort an in-flight setup.
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const childProcess = require('child_process');
|
|
11
|
+
const {
|
|
12
|
+
resetLibModules, muteConsole, makeTempDirs, removeTempDirs,
|
|
13
|
+
} = require('./helpers/lib-sandbox.cjs');
|
|
14
|
+
|
|
15
|
+
const LIB_MODULES = ['../init', '../detect-deps.js', '../shared-paths'].map(name => require.resolve(name));
|
|
16
|
+
|
|
17
|
+
describe('Qoder Delphi agent templates (#417)', () => {
|
|
18
|
+
let tmpDirs;
|
|
19
|
+
let tmpHome;
|
|
20
|
+
let logSpy;
|
|
21
|
+
let warnSpy;
|
|
22
|
+
let originalHome;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmpDirs = makeTempDirs(['xpgate-agents-home']);
|
|
26
|
+
tmpHome = tmpDirs['xpgate-agents-home'];
|
|
27
|
+
originalHome = process.env.HOME;
|
|
28
|
+
process.env.HOME = tmpHome;
|
|
29
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills'), { recursive: true });
|
|
30
|
+
resetLibModules(vi, LIB_MODULES);
|
|
31
|
+
({ log: logSpy, warn: warnSpy } = muteConsole(vi));
|
|
32
|
+
vi.spyOn(childProcess, 'execSync').mockReturnValue(Buffer.from(''));
|
|
33
|
+
vi.spyOn(childProcess, 'spawnSync').mockReturnValue({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
if (originalHome === undefined) delete process.env.HOME; else process.env.HOME = originalHome;
|
|
38
|
+
removeTempDirs(tmpDirs);
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function repoRoot() {
|
|
43
|
+
// __dirname = src/npm-package/lib/__tests__
|
|
44
|
+
return path.join(__dirname, '..', '..', '..', '..');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function warned() {
|
|
48
|
+
return warnSpy.mock.calls.map(c => String(c[0])).join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function logged() {
|
|
52
|
+
return logSpy.mock.calls.map(c => String(c[0])).join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function agentTemplate(modelLine, name) {
|
|
56
|
+
return `---\nname: ${name}\n${modelLine}\n---\n# ${name}\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tempAgentPair(filename, srcBody, destBody) {
|
|
60
|
+
const dirs = makeTempDirs(['xpgate-agentsrc', 'xpgate-agentdst']);
|
|
61
|
+
const srcDir = dirs['xpgate-agentsrc'];
|
|
62
|
+
const targetRoot = dirs['xpgate-agentdst'];
|
|
63
|
+
const srcAgents = path.join(srcDir, 'plugins', 'qoder', 'agents');
|
|
64
|
+
const destAgents = path.join(targetRoot, '.qoder', 'agents');
|
|
65
|
+
fs.mkdirSync(srcAgents, { recursive: true });
|
|
66
|
+
fs.mkdirSync(destAgents, { recursive: true });
|
|
67
|
+
if (srcBody !== null) fs.writeFileSync(path.join(srcAgents, filename), srcBody);
|
|
68
|
+
if (destBody !== null) fs.writeFileSync(path.join(destAgents, filename), destBody);
|
|
69
|
+
return { dirs, srcDir, targetRoot, srcAgents, destAgents };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function withDirs(dirs, fn) {
|
|
73
|
+
try {
|
|
74
|
+
return fn();
|
|
75
|
+
} finally {
|
|
76
|
+
removeTempDirs(dirs.dirs);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it('binds every expert template mirror to three distinct, identical built-in models', () => {
|
|
81
|
+
const { readQoderModelId } = require('../init');
|
|
82
|
+
const mirrors = [
|
|
83
|
+
path.join(repoRoot(), 'plugins', 'qoder', 'agents'),
|
|
84
|
+
path.join(repoRoot(), '.qoder', 'agents'),
|
|
85
|
+
path.join(repoRoot(), 'src', 'npm-package', 'plugins', 'qoder', 'agents'),
|
|
86
|
+
];
|
|
87
|
+
const names = ['delphi-architecture', 'delphi-technical', 'delphi-feasibility'];
|
|
88
|
+
|
|
89
|
+
for (const dir of mirrors) {
|
|
90
|
+
const models = names.map(name => {
|
|
91
|
+
const file = path.join(dir, `${name}.md`);
|
|
92
|
+
expect(fs.existsSync(file)).toBe(true);
|
|
93
|
+
const modelId = readQoderModelId(file);
|
|
94
|
+
expect(modelId).not.toBeNull();
|
|
95
|
+
return modelId;
|
|
96
|
+
});
|
|
97
|
+
expect(new Set(models).size).toBe(3);
|
|
98
|
+
// Bodies may diverge (`.qoder/agents/` is this repo's own install), but the
|
|
99
|
+
// model each expert pins must not.
|
|
100
|
+
for (const [index, name] of names.entries()) {
|
|
101
|
+
const ids = mirrors.map(dir => readQoderModelId(path.join(dir, `${name}.md`)));
|
|
102
|
+
expect(new Set(ids).size).toBe(1);
|
|
103
|
+
expect(ids[0]).toBe(models[index]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('reads the modelId only from a quoted frontmatter binding', () => {
|
|
109
|
+
const { readQoderModelId } = require('../init');
|
|
110
|
+
const dirs = tempAgentPair('probe.md', null, null);
|
|
111
|
+
return withDirs(dirs, () => {
|
|
112
|
+
const write = body => {
|
|
113
|
+
const file = path.join(dirs.destAgents, 'probe.md');
|
|
114
|
+
fs.writeFileSync(file, body);
|
|
115
|
+
return readQoderModelId(file);
|
|
116
|
+
};
|
|
117
|
+
expect(write('---\nmodel: "[Qwen3.8-Flash](qfmodel)"\n---\n')).toBe('qfmodel');
|
|
118
|
+
expect(write("---\nmodel: '[Qwen3.8-Flash](qfmodel)'\n---\n")).toBe('qfmodel');
|
|
119
|
+
// Unquoted is not a legal Custom Agent binding: the docs require
|
|
120
|
+
// "[Name](modelId)", and accepting the bare form lets a hand-edited file
|
|
121
|
+
// pass the audit while Qoder reads something else entirely.
|
|
122
|
+
expect(write('---\nmodel: [Qwen3.8-Flash](qfmodel)\n---\n')).toBeNull();
|
|
123
|
+
expect(write('---\nmodel: "[Qwen3.8-Flash](qfmodel)\n---\n')).toBeNull(); // unbalanced quote
|
|
124
|
+
expect(write('---\nmodel: Qwen3.7-Max\n---\n')).toBeNull(); // bare name
|
|
125
|
+
expect(write('---\nname: x\n---\n\nExample:\n```md\nmodel: "[Doc](docmodel)"\n```\n')).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('warns about a preserved existing agent whose model binding is stale', () => {
|
|
130
|
+
const dirs = tempAgentPair(
|
|
131
|
+
'delphi-technical.md',
|
|
132
|
+
agentTemplate('model: "[DeepSeek-Flash](dfmodel)"', 'delphi-technical'),
|
|
133
|
+
agentTemplate('model: GLM-5.2', 'delphi-technical')
|
|
134
|
+
);
|
|
135
|
+
withDirs(dirs, () => {
|
|
136
|
+
const { configureQoderDelphiAgents } = require('../init');
|
|
137
|
+
configureQoderDelphiAgents(dirs.srcDir, dirs.targetRoot);
|
|
138
|
+
|
|
139
|
+
expect(warned()).toContain('delphi-technical.md');
|
|
140
|
+
expect(warned()).toContain('session model');
|
|
141
|
+
expect(fs.readFileSync(path.join(dirs.destAgents, 'delphi-technical.md'), 'utf8')).toContain('GLM-5.2');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('warns when a preserved agent binds a different model than this version ships', () => {
|
|
146
|
+
const dirs = tempAgentPair(
|
|
147
|
+
'delphi-feasibility.md',
|
|
148
|
+
agentTemplate('model: "[DeepSeek-Flash](dfmodel)"', 'delphi-feasibility'),
|
|
149
|
+
agentTemplate('model: "[GLM-5.3-Flash](gfmodel)"', 'delphi-feasibility')
|
|
150
|
+
);
|
|
151
|
+
withDirs(dirs, () => {
|
|
152
|
+
const { configureQoderDelphiAgents } = require('../init');
|
|
153
|
+
configureQoderDelphiAgents(dirs.srcDir, dirs.targetRoot);
|
|
154
|
+
|
|
155
|
+
expect(warned()).toContain('binds "gfmodel"');
|
|
156
|
+
expect(warned()).toContain('pins "dfmodel"');
|
|
157
|
+
expect(fs.existsSync(path.join(dirs.destAgents, 'delphi-feasibility.md'))).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('audits deployed agents even when the bundled templates are missing', () => {
|
|
162
|
+
const dirs = tempAgentPair('delphi-architecture.md', null, agentTemplate('model: Qwen3.7-Max', 'delphi-architecture'));
|
|
163
|
+
fs.rmSync(dirs.srcAgents, { recursive: true, force: true });
|
|
164
|
+
withDirs(dirs, () => {
|
|
165
|
+
const { configureQoderDelphiAgents } = require('../init');
|
|
166
|
+
configureQoderDelphiAgents(dirs.srcDir, dirs.targetRoot);
|
|
167
|
+
|
|
168
|
+
expect(warned()).toContain('templates not bundled');
|
|
169
|
+
expect(warned()).toContain('delphi-architecture.md');
|
|
170
|
+
// Nothing came from this install, so the summary must not claim there were
|
|
171
|
+
// simply no templates, nor lecture about a binding we never deployed.
|
|
172
|
+
expect(logged()).not.toContain('no agent templates found');
|
|
173
|
+
expect(warned()).not.toContain('does not adopt');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('refuses a bundled template whose binding is unquoted', () => {
|
|
178
|
+
const dirs = tempAgentPair(
|
|
179
|
+
'delphi-technical.md',
|
|
180
|
+
agentTemplate('model: [DeepSeek-Flash](dfmodel)', 'delphi-technical'),
|
|
181
|
+
null
|
|
182
|
+
);
|
|
183
|
+
withDirs(dirs, () => {
|
|
184
|
+
const { configureQoderDelphiAgents } = require('../init');
|
|
185
|
+
configureQoderDelphiAgents(dirs.srcDir, dirs.targetRoot);
|
|
186
|
+
|
|
187
|
+
expect(fs.existsSync(path.join(dirs.destAgents, 'delphi-technical.md'))).toBe(false);
|
|
188
|
+
expect(warned()).toContain('1 bundled template(s) rejected');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('leaves unrelated user agents alone', () => {
|
|
193
|
+
const dirs = tempAgentPair(
|
|
194
|
+
'delphi-technical.md',
|
|
195
|
+
agentTemplate('model: "[DeepSeek-Flash](dfmodel)"', 'delphi-technical'),
|
|
196
|
+
null
|
|
197
|
+
);
|
|
198
|
+
fs.writeFileSync(path.join(dirs.destAgents, 'my-other-agent.md'), '# no frontmatter at all\n');
|
|
199
|
+
withDirs(dirs, () => {
|
|
200
|
+
const { configureQoderDelphiAgents } = require('../init');
|
|
201
|
+
configureQoderDelphiAgents(dirs.srcDir, dirs.targetRoot);
|
|
202
|
+
|
|
203
|
+
expect(warned()).not.toContain('my-other-agent.md');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('refuses to deploy a bundled template without a valid model binding', () => {
|
|
208
|
+
const dirs = tempAgentPair('delphi-architecture.md', agentTemplate('model: Qwen3.7-Max', 'delphi-architecture'), null);
|
|
209
|
+
withDirs(dirs, () => {
|
|
210
|
+
const { configureQoderDelphiAgents } = require('../init');
|
|
211
|
+
configureQoderDelphiAgents(dirs.srcDir, dirs.targetRoot);
|
|
212
|
+
|
|
213
|
+
expect(fs.existsSync(path.join(dirs.destAgents, 'delphi-architecture.md'))).toBe(false);
|
|
214
|
+
expect(warned()).toContain('no valid model binding');
|
|
215
|
+
expect(warned()).toContain('1 bundled template(s) rejected');
|
|
216
|
+
expect(logged()).not.toContain('no agent templates found');
|
|
217
|
+
expect(warned()).not.toContain('does not adopt');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('swallows deployment failures instead of aborting setup half-installed', () => {
|
|
222
|
+
const dirs = tempAgentPair(
|
|
223
|
+
'delphi-technical.md',
|
|
224
|
+
agentTemplate('model: "[DeepSeek-Flash](dfmodel)"', 'delphi-technical'),
|
|
225
|
+
null
|
|
226
|
+
);
|
|
227
|
+
const originalCopy = fs.copyFileSync;
|
|
228
|
+
try {
|
|
229
|
+
fs.copyFileSync = () => { throw 'EPERM: operation not permitted'; };
|
|
230
|
+
const { deployQoderDelphiAgents } = require('../init');
|
|
231
|
+
expect(() => deployQoderDelphiAgents(dirs.srcDir, dirs.targetRoot)).not.toThrow();
|
|
232
|
+
expect(warned()).toContain('deployment failed: EPERM: operation not permitted');
|
|
233
|
+
} finally {
|
|
234
|
+
fs.copyFileSync = originalCopy;
|
|
235
|
+
fs.rmSync(dirs.srcDir, { recursive: true, force: true });
|
|
236
|
+
fs.rmSync(dirs.targetRoot, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('states the platform boundary even when every deployed binding is valid', () => {
|
|
241
|
+
const dirs = tempAgentPair(
|
|
242
|
+
'delphi-architecture.md',
|
|
243
|
+
agentTemplate('model: "[Qwen3.8-Flash](qfmodel)"', 'delphi-architecture'),
|
|
244
|
+
null
|
|
245
|
+
);
|
|
246
|
+
withDirs(dirs, () => {
|
|
247
|
+
const { configureQoderDelphiAgents } = require('../init');
|
|
248
|
+
configureQoderDelphiAgents(dirs.srcDir, dirs.targetRoot);
|
|
249
|
+
|
|
250
|
+
// A legal binding is not an executed binding. Without this line the user
|
|
251
|
+
// leaves `init` believing three experts now run on three models.
|
|
252
|
+
expect(fs.existsSync(path.join(dirs.destAgents, 'delphi-architecture.md'))).toBe(true);
|
|
253
|
+
expect(warned()).toContain('does not adopt');
|
|
254
|
+
expect(warned()).toContain('.delphi-config.json');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('does not state the boundary on a non-Qoder platform', () => {
|
|
259
|
+
// Remove the whole ~/.qoder dir (#424): any ~/.qoder content now means Qoder.
|
|
260
|
+
fs.rmSync(path.join(tmpHome, '.qoder'), { recursive: true, force: true });
|
|
261
|
+
const dirs = tempAgentPair(
|
|
262
|
+
'delphi-architecture.md',
|
|
263
|
+
agentTemplate('model: "[Qwen3.8-Flash](qfmodel)"', 'delphi-architecture'),
|
|
264
|
+
null
|
|
265
|
+
);
|
|
266
|
+
withDirs(dirs, () => {
|
|
267
|
+
const { configureQoderDelphiAgents } = require('../init');
|
|
268
|
+
configureQoderDelphiAgents(dirs.srcDir, dirs.targetRoot);
|
|
269
|
+
|
|
270
|
+
expect(warned()).not.toContain('does not adopt');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('reports the detected platform when skipping agent deployment', () => {
|
|
275
|
+
fs.rmSync(path.join(tmpHome, '.qoder'), { recursive: true, force: true });
|
|
276
|
+
const dirs = tempAgentPair('delphi-technical.md', null, null);
|
|
277
|
+
withDirs(dirs, () => {
|
|
278
|
+
const { configureQoderDelphiAgents } = require('../init');
|
|
279
|
+
configureQoderDelphiAgents(dirs.srcDir, dirs.targetRoot);
|
|
280
|
+
|
|
281
|
+
expect(logged()).toContain('platform detected:');
|
|
282
|
+
expect(warned()).not.toContain('templates not bundled');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -86,6 +86,26 @@ describe('shared-paths', () => {
|
|
|
86
86
|
expect(detectPlatform()).toBe('claude-code');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
// ── #424: brand-new Qoder installs have no skills/ subdirectory yet ──
|
|
90
|
+
|
|
91
|
+
it('detectPlatform returns qoder when only ~/.qoder exists (brand-new install, no skills dir)', () => {
|
|
92
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder'), { recursive: true });
|
|
93
|
+
const { detectPlatform } = require('../shared-paths');
|
|
94
|
+
expect(detectPlatform()).toBe('qoder');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('detectPlatform returns qoder when only ~/.qoder/agents exists (no skills dir)', () => {
|
|
98
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder', 'agents'), { recursive: true });
|
|
99
|
+
const { detectPlatform } = require('../shared-paths');
|
|
100
|
+
expect(detectPlatform()).toBe('qoder');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('detect-deps re-exports the SAME detectPlatform as shared-paths (no divergent copies)', () => {
|
|
104
|
+
const fromShared = require('../shared-paths').detectPlatform;
|
|
105
|
+
const fromDetectDeps = require('../detect-deps').detectPlatform;
|
|
106
|
+
expect(fromDetectDeps).toBe(fromShared);
|
|
107
|
+
});
|
|
108
|
+
|
|
89
109
|
it('getTemplateDir returns correct path for each platform', () => {
|
|
90
110
|
const { getTemplateDir } = require('../shared-paths');
|
|
91
111
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test shared-utils
|
|
3
|
+
* @intent Verify copyAdapters deploys shipped gate scripts (gate-*.sh, sprint-gate.sh) from the package root so npm-installed init/setup-global produce working gates
|
|
4
|
+
* @covers gate-script deployment into ADAPTER_DIR / PROJECT_GITHOOKS for init and setup-global
|
|
5
|
+
*/
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
|
|
10
|
+
describe('shared-utils', () => {
|
|
11
|
+
let tmpRoot;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-shared-utils-'));
|
|
15
|
+
vi.resetModules();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('copyAdapters', () => {
|
|
24
|
+
it('copies gate-*.sh and sprint-gate.sh from the package root (npm install layout)', () => {
|
|
25
|
+
// npm install layout: the package root carries the shipped copies of
|
|
26
|
+
// gate-*.sh + sprint-gate.sh (placed by sync-package-content.js) and the
|
|
27
|
+
// repo-level githooks/ directory does not exist.
|
|
28
|
+
const pkgRoot = path.join(tmpRoot, 'node_modules', '@boyingliu01', 'xp-gate');
|
|
29
|
+
fs.mkdirSync(pkgRoot, { recursive: true });
|
|
30
|
+
fs.writeFileSync(path.join(pkgRoot, 'adapter-common.sh'), '# adapter-common');
|
|
31
|
+
fs.writeFileSync(path.join(pkgRoot, 'gate-3.sh'), '# gate-3');
|
|
32
|
+
fs.writeFileSync(path.join(pkgRoot, 'gate-12-file-hygiene.sh'), '# gate-12');
|
|
33
|
+
fs.writeFileSync(path.join(pkgRoot, 'sprint-gate.sh'), '# sprint-gate');
|
|
34
|
+
fs.mkdirSync(path.join(pkgRoot, 'adapters'), { recursive: true });
|
|
35
|
+
fs.writeFileSync(path.join(pkgRoot, 'adapters', 'typescript.sh'), '# ts');
|
|
36
|
+
|
|
37
|
+
const destDir = path.join(tmpRoot, 'project-githooks');
|
|
38
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const { copyAdapters } = require('../shared-utils');
|
|
41
|
+
copyAdapters(pkgRoot, destDir);
|
|
42
|
+
|
|
43
|
+
expect(fs.existsSync(path.join(destDir, 'adapter-common.sh'))).toBe(true);
|
|
44
|
+
expect(fs.existsSync(path.join(destDir, 'typescript.sh'))).toBe(true);
|
|
45
|
+
expect(fs.existsSync(path.join(destDir, 'gate-3.sh'))).toBe(true);
|
|
46
|
+
expect(fs.existsSync(path.join(destDir, 'gate-12-file-hygiene.sh'))).toBe(true);
|
|
47
|
+
expect(fs.existsSync(path.join(destDir, 'sprint-gate.sh'))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('copies only gate-*.sh and sprint-gate.sh from the package root (filters other files)', () => {
|
|
51
|
+
const pkgRoot = path.join(tmpRoot, 'pkg');
|
|
52
|
+
fs.mkdirSync(path.join(pkgRoot, 'adapters'), { recursive: true });
|
|
53
|
+
fs.writeFileSync(path.join(pkgRoot, 'adapter-common.sh'), '# adapter-common');
|
|
54
|
+
fs.writeFileSync(path.join(pkgRoot, 'gate-3.sh'), '# gate-3');
|
|
55
|
+
fs.writeFileSync(path.join(pkgRoot, 'verify.sh'), '# verify');
|
|
56
|
+
fs.writeFileSync(path.join(pkgRoot, 'VERSION'), '1.0.0.0');
|
|
57
|
+
|
|
58
|
+
const destDir = path.join(tmpRoot, 'dest');
|
|
59
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
const { copyAdapters } = require('../shared-utils');
|
|
62
|
+
copyAdapters(pkgRoot, destDir);
|
|
63
|
+
|
|
64
|
+
expect(fs.existsSync(path.join(destDir, 'gate-3.sh'))).toBe(true);
|
|
65
|
+
expect(fs.existsSync(path.join(destDir, 'verify.sh'))).toBe(false);
|
|
66
|
+
expect(fs.existsSync(path.join(destDir, 'VERSION'))).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-416
|
|
3
|
+
* @intent install-skill 的用法字符串删掉了 --offline,但 parseOptions 对认不出的 flag
|
|
4
|
+
* 照旧静默忽略 —— 这正是 #488 给 doctor 修掉的同一类缺陷:用户执行了已不存在的
|
|
5
|
+
* flag,得到 exit 0 与「安装成功」,而 flag 从未生效(Round 1 架构席位 MA-02)
|
|
6
|
+
* @covers AC-416-06
|
|
7
|
+
*/
|
|
8
|
+
const { parseSkillFlags } = require('../shared-utils');
|
|
9
|
+
|
|
10
|
+
describe('parseSkillFlags (#416)', () => {
|
|
11
|
+
it('parses the flags the skill commands implement', () => {
|
|
12
|
+
const { options, unknown } = parseSkillFlags(['--verbose', '--force'], 'install-skill');
|
|
13
|
+
|
|
14
|
+
expect(options).toMatchObject({ verbose: true, force: true, all: false, check: false });
|
|
15
|
+
expect(unknown).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('names a flag it does not implement instead of swallowing it', () => {
|
|
19
|
+
const { unknown } = parseSkillFlags(['--offline', '--verbose'], 'install-skill');
|
|
20
|
+
|
|
21
|
+
expect(unknown).toEqual(['--offline']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('treats a value argument as unknown rather than silently dropping it', () => {
|
|
25
|
+
expect(parseSkillFlags(['main'], 'install-skill').unknown).toEqual(['main']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('reports no unknown flag for an empty argument list', () => {
|
|
29
|
+
expect(parseSkillFlags([], 'install-skill')).toEqual({
|
|
30
|
+
options: { verbose: false, force: false, all: false, check: false },
|
|
31
|
+
unknown: [],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Round 2 architecture MI-04: one global table meant `install-skill x --all`
|
|
36
|
+
// parsed clean and then dropped options.all on the floor -- parsed-but-not-
|
|
37
|
+
// consumed is the same silent no-op this branch exists to remove.
|
|
38
|
+
it('accepts a flag only for the command that consumes it', () => {
|
|
39
|
+
expect(parseSkillFlags(['--all'], 'install-skill').unknown).toEqual(['--all']);
|
|
40
|
+
expect(parseSkillFlags(['--check'], 'uninstall-skill').unknown).toEqual(['--check']);
|
|
41
|
+
expect(parseSkillFlags(['--verbose'], 'uninstall-skill').unknown).toEqual(['--verbose']);
|
|
42
|
+
|
|
43
|
+
const update = parseSkillFlags(['--all', '--check', '--verbose'], 'update-skill');
|
|
44
|
+
expect(update.unknown).toEqual([]);
|
|
45
|
+
expect(update.options).toMatchObject({ all: true, check: true, verbose: true });
|
|
46
|
+
expect(parseSkillFlags(['--force'], 'uninstall-skill').unknown).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -372,13 +372,17 @@ describe('ui-detector', () => {
|
|
|
372
372
|
|
|
373
373
|
const UI_DETECTOR_PATH = path.resolve(__dirname, '../ui-detector.ts');
|
|
374
374
|
|
|
375
|
+
// Resolve the local tsx CLI once: spawning `npx tsx` per test re-resolves
|
|
376
|
+
// the package each call and needs shell:true on Windows; running the
|
|
377
|
+
// resolved CLI through process.execPath is far faster and shell-free.
|
|
378
|
+
const tsxCli = require.resolve('tsx/cli');
|
|
379
|
+
|
|
375
380
|
function runCli(args: string[], stdin?: string, cwd?: string): { code: number; stdout: string; stderr: string } {
|
|
376
381
|
const coverageDir = process.env.NODE_V8_COVERAGE ?? path.join(process.cwd(), 'coverage', '.tmp');
|
|
377
|
-
const result = realSpawnSync(
|
|
382
|
+
const result = realSpawnSync(process.execPath, [tsxCli, UI_DETECTOR_PATH, ...args], {
|
|
378
383
|
cwd: cwd ?? process.cwd(),
|
|
379
384
|
encoding: 'utf-8',
|
|
380
385
|
input: stdin,
|
|
381
|
-
shell: true, // Required for npx.cmd resolution on Windows
|
|
382
386
|
env: { ...process.env, NODE_V8_COVERAGE: coverageDir },
|
|
383
387
|
});
|
|
384
388
|
return {
|
|
@@ -97,6 +97,14 @@ describe('uninstall', () => {
|
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function createXpGatePostMerge(dir) {
|
|
101
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
path.join(dir, 'post-merge'),
|
|
104
|
+
'#!/usr/bin/env bash\n# post-merge - XP-Gate VERSION sync hook\n'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
function createXpGateAdapterCommon(dir) {
|
|
101
109
|
fs.mkdirSync(dir, { recursive: true });
|
|
102
110
|
fs.writeFileSync(
|
|
@@ -131,6 +139,7 @@ describe('uninstall', () => {
|
|
|
131
139
|
// Create project .git/hooks with xp-gate hooks
|
|
132
140
|
createXpGatePreCommit(projectHooksDir());
|
|
133
141
|
createXpGatePrePush(projectHooksDir());
|
|
142
|
+
createXpGatePostMerge(projectHooksDir());
|
|
134
143
|
|
|
135
144
|
// Create project githooks/ with adapters
|
|
136
145
|
createXpGateAdapterCommon(projectGithooksDir());
|
|
@@ -248,6 +257,17 @@ describe('uninstall', () => {
|
|
|
248
257
|
expect(cfg.uninstalled).toBeDefined();
|
|
249
258
|
});
|
|
250
259
|
|
|
260
|
+
it('AC-01: uninstall in local mode removes the deployed post-merge hook', async () => {
|
|
261
|
+
setupLocalInstall();
|
|
262
|
+
mockExecSuccess();
|
|
263
|
+
const { uninstall } = require('../uninstall');
|
|
264
|
+
|
|
265
|
+
const result = await uninstall([]);
|
|
266
|
+
|
|
267
|
+
expect(result).toBe(0);
|
|
268
|
+
expect(fs.existsSync(path.join(projectHooksDir(), 'post-merge'))).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
251
271
|
it('AC-01: uninstall returns 0 even when no files exist (graceful)', async () => {
|
|
252
272
|
fs.mkdirSync(path.dirname(configFile()), { recursive: true });
|
|
253
273
|
fs.writeFileSync(configFile(), JSON.stringify({ mode: 'local', lastInit: '2025-01-01' }, null, 2));
|
|
@@ -44,6 +44,7 @@ describe('updateHooks', () => {
|
|
|
44
44
|
|
|
45
45
|
fs.writeFileSync(path.join(hooksDir, 'pre-commit'), overrides['hooks/pre-commit'] || '#!/bin/bash\necho "hook-v2"');
|
|
46
46
|
fs.writeFileSync(path.join(hooksDir, 'pre-push'), overrides['hooks/pre-push'] || '#!/bin/bash\necho "push-v2"');
|
|
47
|
+
fs.writeFileSync(path.join(hooksDir, 'post-merge'), overrides['hooks/post-merge'] || '#!/bin/bash\necho "merge-v2"');
|
|
47
48
|
fs.writeFileSync(path.join(hooksLibDir, 'now-ms.sh'), overrides['hooks/lib/now-ms.sh'] || 'now_ms() { echo 123; }');
|
|
48
49
|
fs.writeFileSync(path.join(tmpPackage, 'adapter-common.sh'), overrides['adapter-common.sh'] || '#!/bin/bash\necho "adapter-common-v2"');
|
|
49
50
|
fs.writeFileSync(path.join(adaptersDir, 'typescript.sh'), overrides['adapters/typescript.sh'] || '#!/bin/bash\necho "ts-v2"');
|
|
@@ -81,6 +82,17 @@ describe('updateHooks', () => {
|
|
|
81
82
|
expect(fs.readFileSync(path.join(dest, 'pre-push'), 'utf8')).toContain('push-v2');
|
|
82
83
|
});
|
|
83
84
|
|
|
85
|
+
it('copies post-merge to destination (VERSION sync hook)', () => {
|
|
86
|
+
createPackageSource();
|
|
87
|
+
const mod = getModule();
|
|
88
|
+
const dest = path.join(tmpProject, 'hooks-postmerge');
|
|
89
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
90
|
+
|
|
91
|
+
mod.copyHooks(tmpPackage, dest, false, true);
|
|
92
|
+
|
|
93
|
+
expect(fs.readFileSync(path.join(dest, 'post-merge'), 'utf8')).toContain('merge-v2');
|
|
94
|
+
});
|
|
95
|
+
|
|
84
96
|
it('creates destination directory if it does not exist', () => {
|
|
85
97
|
createPackageSource();
|
|
86
98
|
const mod = getModule();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @test update-skill
|
|
3
3
|
* @intent Verify updateSkill() handles check/all/single-name modes, config parsing, and delegates to installSkill correctly
|
|
4
|
+
* @covers AC-416-04
|
|
4
5
|
*/
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const path = require('path');
|
|
@@ -218,21 +219,23 @@ describe('update-skill', () => {
|
|
|
218
219
|
expect(logSpy).not.toHaveBeenCalledWith('✓ foo updated');
|
|
219
220
|
});
|
|
220
221
|
|
|
221
|
-
it('
|
|
222
|
+
it('leaves the target directory for installSkill to back up and replace', async () => {
|
|
222
223
|
writeConfig({ foo: { version: '1.0.0' } });
|
|
223
224
|
const dir = makeSkillDir('foo');
|
|
224
225
|
expect(fs.existsSync(dir)).toBe(true);
|
|
225
226
|
|
|
226
|
-
let
|
|
227
|
+
let dirPresentDuringInstall = false;
|
|
227
228
|
installSkillMock.mockImplementation(async () => {
|
|
228
|
-
|
|
229
|
+
dirPresentDuringInstall = fs.existsSync(dir);
|
|
229
230
|
return 0;
|
|
230
231
|
});
|
|
231
232
|
|
|
232
233
|
const { updateSkill } = require('../update-skill');
|
|
233
234
|
const result = await updateSkill('foo');
|
|
234
235
|
expect(result).toBe(0);
|
|
235
|
-
|
|
236
|
+
// update-skill has no delete of its own: installSkill backs the directory up
|
|
237
|
+
// before replacing it (#416).
|
|
238
|
+
expect(dirPresentDuringInstall).toBe(true);
|
|
236
239
|
});
|
|
237
240
|
|
|
238
241
|
it('does not crash when targetDir does not exist', async () => {
|
package/lib/detect-deps.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
5
|
|
|
6
|
-
const { HOME_DIR } = require('./shared-paths.js');
|
|
6
|
+
const { HOME_DIR, detectPlatform } = require('./shared-paths.js');
|
|
7
7
|
|
|
8
8
|
const REQUIRED_DEPS = [
|
|
9
9
|
{ name: 'superpowers', repo: 'obra/superpowers', minVersion: '1.0.0' },
|
|
@@ -257,25 +257,8 @@ const PLATFORM_PROFILES = {
|
|
|
257
257
|
},
|
|
258
258
|
};
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
* Checks for platform-specific directories in the user's home.
|
|
263
|
-
* Falls back to 'opencode' if no platform is detected.
|
|
264
|
-
*
|
|
265
|
-
* @returns {'opencode' | 'claude-code' | 'qoder'}
|
|
266
|
-
*/
|
|
267
|
-
function detectPlatform() {
|
|
268
|
-
// Check for Qoder-specific marker
|
|
269
|
-
if (fs.existsSync(path.join(HOME_DIR, '.qoder', 'skills'))) {
|
|
270
|
-
return 'qoder';
|
|
271
|
-
}
|
|
272
|
-
// Check for Claude Code-specific marker
|
|
273
|
-
if (fs.existsSync(path.join(HOME_DIR, '.claude', 'skills'))) {
|
|
274
|
-
return 'claude-code';
|
|
275
|
-
}
|
|
276
|
-
// Default to opencode (most common, backward compatible)
|
|
277
|
-
return 'opencode';
|
|
278
|
-
}
|
|
260
|
+
// detectPlatform() is re-exported from shared-paths.js — the single
|
|
261
|
+
// implementation lives there (#424 removed the divergent copy).
|
|
279
262
|
|
|
280
263
|
/**
|
|
281
264
|
* Get the skills directories for a given platform.
|
package/lib/doctor-tui.js
CHANGED
|
@@ -64,13 +64,29 @@ function diagnoseTuiRegistration(checks) {
|
|
|
64
64
|
* @returns {string|undefined}
|
|
65
65
|
*/
|
|
66
66
|
function extractSkillVersion(content) {
|
|
67
|
-
|
|
67
|
+
// A line terminator, not "LF": a working tree checked out with
|
|
68
|
+
// core.autocrlf=true ships CRLF SKILL.md files, and the LF-only pattern
|
|
69
|
+
// returned undefined for every one of them -- pushing each skill into the
|
|
70
|
+
// byte-comparison branch and reporting it Outdated (#439).
|
|
71
|
+
const match = content.match(/^---(?:\r\n|\r|\n)([\s\S]*?)(?:\r\n|\r|\n)---/);
|
|
68
72
|
if (!match) return undefined;
|
|
69
73
|
const frontmatter = match[1];
|
|
70
74
|
const versionMatch = frontmatter.match(/version:\s*(\S+)/);
|
|
71
75
|
return versionMatch ? versionMatch[1] : undefined;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Compare two SKILL.md bodies ignoring line-ending style. Bundled copies come from
|
|
80
|
+
* whichever checkout packed them, installed copies come from whatever the platform's
|
|
81
|
+
* writer chose, so endings are never evidence of staleness (#439). Every convention
|
|
82
|
+
* normalizes: a lone CR is as much a line ending as CRLF, which is what this name
|
|
83
|
+
* already promises.
|
|
84
|
+
*/
|
|
85
|
+
function sameContentIgnoringLineEndings(a, b) {
|
|
86
|
+
const normalize = (s) => s.replace(/\r\n|\r|\n/g, '\n');
|
|
87
|
+
return normalize(a) === normalize(b);
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
/**
|
|
75
91
|
* Compare two semver strings. Returns:
|
|
76
92
|
* -1 if a < b, 0 if a == b, 1 if a > b, undefined if either is not valid semver.
|
|
@@ -145,8 +161,8 @@ function diagnoseInstalledSkills(config, checks) {
|
|
|
145
161
|
if (bundledVersion !== undefined && userVersion !== undefined) {
|
|
146
162
|
const cmp = compareSemver(userVersion, bundledVersion);
|
|
147
163
|
if (cmp === undefined) {
|
|
148
|
-
// Version strings not valid semver — fall back to
|
|
149
|
-
if (bundledContent
|
|
164
|
+
// Version strings not valid semver — fall back to content comparison
|
|
165
|
+
if (!sameContentIgnoringLineEndings(bundledContent, userContent)) {
|
|
150
166
|
checks.push({
|
|
151
167
|
name: `Skill: ${name}`,
|
|
152
168
|
status: 'WARN',
|
|
@@ -168,8 +184,9 @@ function diagnoseInstalledSkills(config, checks) {
|
|
|
168
184
|
checks.push({ name: `Skill: ${name}`, status: 'PASS', detail: 'Up to date' });
|
|
169
185
|
}
|
|
170
186
|
} else {
|
|
171
|
-
// No version in at least one file —
|
|
172
|
-
|
|
187
|
+
// No version in at least one file — compare content, but not byte-for-byte:
|
|
188
|
+
// line endings alone are not evidence of staleness (#439).
|
|
189
|
+
if (!sameContentIgnoringLineEndings(bundledContent, userContent)) {
|
|
173
190
|
checks.push({
|
|
174
191
|
name: `Skill: ${name}`,
|
|
175
192
|
status: 'WARN',
|