@boyingliu01/xp-gate 0.19.3 → 0.20.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/adapters/gate-4.sh +83 -18
- package/adapters/gate-8.sh +49 -5
- package/adapters/iac.sh +1 -1
- package/adapters/typescript.sh +15 -60
- package/bin/xp-gate.js +15 -16
- package/gate-10.sh +35 -2
- package/gate-4.sh +110 -21
- package/gate-8.sh +49 -5
- package/hooks/gate-10.sh +35 -2
- package/hooks/gate-4.sh +110 -21
- package/hooks/gate-8.sh +49 -5
- 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 +389 -162
- 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 +1 -1
- 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 +481 -51
- 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 +7 -4
- 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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-416
|
|
3
|
+
* @intent install-skill/update-skill 只从 GitHub main 下载单个 SKILL.md,导致 13 个技能里
|
|
4
|
+
* 8 个的 references/templates/scripts 全部缺失、SKILL.md 内的路径引用悬空,而
|
|
5
|
+
* update 先删目录再只写回 SKILL.md 会把用户已有的完整目录清空(#416)
|
|
6
|
+
* @covers AC-416-01, AC-416-02, AC-416-03, AC-416-04
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const BUNDLED_SKILLS = path.join(__dirname, '..', '..', 'skills');
|
|
13
|
+
|
|
14
|
+
function listFiles(dir, prefix = '') {
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
18
|
+
if (entry.isDirectory()) out.push(...listFiles(path.join(dir, entry.name), rel));
|
|
19
|
+
else out.push(rel);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('install-skill full skill directory (#416)', () => {
|
|
25
|
+
let tmpHome;
|
|
26
|
+
let originalHome;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
originalHome = process.env.HOME;
|
|
30
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-full-'));
|
|
31
|
+
process.env.HOME = tmpHome;
|
|
32
|
+
vi.resetModules();
|
|
33
|
+
for (const mod of ['../install-skill', '../update-skill', '../detect-deps', '../shared-paths', '../rollback']) {
|
|
34
|
+
delete require.cache[require.resolve(mod)];
|
|
35
|
+
}
|
|
36
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
37
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
38
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
39
|
+
|
|
40
|
+
// Dependency gate: superpowers + gstack must look installed.
|
|
41
|
+
for (const name of ['superpowers', 'gstack']) {
|
|
42
|
+
const dir = path.join(tmpHome, '.config', 'opencode', 'skills', name);
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ version: '2.0.0' }));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
process.env.HOME = originalHome;
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('installs every file the bundled skill ships, not only SKILL.md', async () => {
|
|
55
|
+
const { installSkill } = require('../install-skill');
|
|
56
|
+
const result = await installSkill('ralph-loop');
|
|
57
|
+
|
|
58
|
+
expect(result).toBe(0);
|
|
59
|
+
const installed = path.join(tmpHome, '.config', 'opencode', 'skills', 'ralph-loop');
|
|
60
|
+
expect(listFiles(installed)).toEqual(listFiles(path.join(BUNDLED_SKILLS, 'ralph-loop')));
|
|
61
|
+
expect(fs.existsSync(path.join(installed, 'references'))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('installs from the package without touching the network', async () => {
|
|
65
|
+
// The npm package already carries the content, so there is no `main`-branch
|
|
66
|
+
// download left to drift against the installed CLI version.
|
|
67
|
+
const https = require('https');
|
|
68
|
+
const getSpy = vi.spyOn(https, 'get').mockImplementation(() => {
|
|
69
|
+
throw new Error('install-skill must not reach the network');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { installSkill } = require('../install-skill');
|
|
73
|
+
const result = await installSkill('delphi-review');
|
|
74
|
+
|
|
75
|
+
expect(result).toBe(0);
|
|
76
|
+
expect(getSpy).not.toHaveBeenCalled();
|
|
77
|
+
const installed = path.join(tmpHome, '.config', 'opencode', 'skills', 'delphi-review');
|
|
78
|
+
expect(fs.readFileSync(path.join(installed, 'SKILL.md'), 'utf8'))
|
|
79
|
+
.toBe(fs.readFileSync(path.join(BUNDLED_SKILLS, 'delphi-review', 'SKILL.md'), 'utf8'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('replaces a stale installed directory with the complete bundled one on update', async () => {
|
|
83
|
+
const installed = path.join(tmpHome, '.config', 'opencode', 'skills', 'ralph-loop');
|
|
84
|
+
fs.mkdirSync(installed, { recursive: true });
|
|
85
|
+
fs.writeFileSync(path.join(installed, 'SKILL.md'), '# old');
|
|
86
|
+
fs.writeFileSync(path.join(installed, 'obsolete-reference.md'), '# deleted upstream');
|
|
87
|
+
|
|
88
|
+
const { updateSkill } = require('../update-skill');
|
|
89
|
+
const configDir = path.join(tmpHome, '.config', 'xp-gate');
|
|
90
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
path.join(configDir, 'xp-gate.json'),
|
|
93
|
+
JSON.stringify({ installedSkills: { 'ralph-loop': { version: '0.0.1' } } })
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const result = await updateSkill('ralph-loop', {});
|
|
97
|
+
expect(result).toBe(0);
|
|
98
|
+
expect(listFiles(installed)).toEqual(listFiles(path.join(BUNDLED_SKILLS, 'ralph-loop')));
|
|
99
|
+
expect(fs.existsSync(path.join(installed, 'obsolete-reference.md'))).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('updates the skills directory of the platform actually in use', async () => {
|
|
103
|
+
// update-skill used to hardcode the opencode path while install-skill wrote
|
|
104
|
+
// to the platform dir, so on Qoder it deleted nothing (or the wrong copy).
|
|
105
|
+
for (const name of ['superpowers', 'gstack']) {
|
|
106
|
+
const dir = path.join(tmpHome, '.qoder', 'skills', name);
|
|
107
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
108
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ version: '2.0.0' }));
|
|
109
|
+
}
|
|
110
|
+
const qoderSkills = path.join(tmpHome, '.qoder', 'skills', 'ralph-loop');
|
|
111
|
+
fs.mkdirSync(qoderSkills, { recursive: true });
|
|
112
|
+
fs.writeFileSync(path.join(qoderSkills, 'SKILL.md'), '# old qoder copy');
|
|
113
|
+
fs.writeFileSync(path.join(qoderSkills, 'obsolete.md'), 'stale');
|
|
114
|
+
const opencodeSkills = path.join(tmpHome, '.config', 'opencode', 'skills', 'ralph-loop');
|
|
115
|
+
fs.mkdirSync(opencodeSkills, { recursive: true });
|
|
116
|
+
fs.writeFileSync(path.join(opencodeSkills, 'SKILL.md'), 'untouched');
|
|
117
|
+
|
|
118
|
+
const configDir = path.join(tmpHome, '.config', 'xp-gate');
|
|
119
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
120
|
+
fs.writeFileSync(
|
|
121
|
+
path.join(configDir, 'xp-gate.json'),
|
|
122
|
+
JSON.stringify({ installedSkills: { 'ralph-loop': { version: '0.0.1' } } })
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const { updateSkill } = require('../update-skill');
|
|
126
|
+
expect(await updateSkill('ralph-loop', {})).toBe(0);
|
|
127
|
+
|
|
128
|
+
expect(fs.existsSync(path.join(qoderSkills, 'obsolete.md'))).toBe(false);
|
|
129
|
+
expect(listFiles(qoderSkills)).toEqual(listFiles(path.join(BUNDLED_SKILLS, 'ralph-loop')));
|
|
130
|
+
expect(fs.readFileSync(path.join(opencodeSkills, 'SKILL.md'), 'utf8')).toBe('untouched');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('keeps the installed directory when the package no longer bundles the skill', async () => {
|
|
134
|
+
// update-skill used to rmSync the target before delegating to install, so a
|
|
135
|
+
// skill the installed package does not carry was destroyed first and the
|
|
136
|
+
// update failed afterwards — data loss with nothing to show for it.
|
|
137
|
+
const installed = path.join(tmpHome, '.config', 'opencode', 'skills', 'removed-in-this-version');
|
|
138
|
+
fs.mkdirSync(installed, { recursive: true });
|
|
139
|
+
fs.writeFileSync(path.join(installed, 'SKILL.md'), '# user copy');
|
|
140
|
+
const configDir = path.join(tmpHome, '.config', 'xp-gate');
|
|
141
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
path.join(configDir, 'xp-gate.json'),
|
|
144
|
+
JSON.stringify({ installedSkills: { 'removed-in-this-version': { version: '0.0.1' } } })
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const { updateSkill } = require('../update-skill');
|
|
148
|
+
expect(await updateSkill('removed-in-this-version', {})).toBe(1);
|
|
149
|
+
expect(fs.readFileSync(path.join(installed, 'SKILL.md'), 'utf8')).toBe('# user copy');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -1,62 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @test install-skill
|
|
3
|
-
* @intent Verify installSkill() handles deps check,
|
|
3
|
+
* @intent Verify installSkill() handles deps check, bundle lookup, config, backup, and errors
|
|
4
|
+
* @covers AC-416-01, AC-416-05
|
|
4
5
|
*/
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const path = require('path');
|
|
7
8
|
const os = require('os');
|
|
8
|
-
const https = require('https');
|
|
9
|
-
const { EventEmitter } = require('events');
|
|
10
|
-
|
|
11
|
-
function mockHttpsGet(optsOrList) {
|
|
12
|
-
const list = Array.isArray(optsOrList) ? [...optsOrList] : [optsOrList];
|
|
13
|
-
// Synchronous fake createWriteStream — the real one opens the FD asynchronously
|
|
14
|
-
// and races against downloadFile's unlinkSync(dest) in 301/302/error branches,
|
|
15
|
-
// causing ENOENT unhandled errors.
|
|
16
|
-
vi.spyOn(fs, 'createWriteStream').mockImplementation((dest) => {
|
|
17
|
-
let buf = '';
|
|
18
|
-
try {
|
|
19
|
-
fs.writeFileSync(dest, '');
|
|
20
|
-
} catch {
|
|
21
|
-
/* dest dir may not exist; downstream code will surface that */
|
|
22
|
-
}
|
|
23
|
-
const stream = new EventEmitter();
|
|
24
|
-
stream.write = (chunk) => {
|
|
25
|
-
buf += chunk;
|
|
26
|
-
return true;
|
|
27
|
-
};
|
|
28
|
-
stream.end = () => {
|
|
29
|
-
try {
|
|
30
|
-
fs.writeFileSync(dest, buf);
|
|
31
|
-
} catch {
|
|
32
|
-
/* file may have been unlinked by source; safe to ignore */
|
|
33
|
-
}
|
|
34
|
-
process.nextTick(() => stream.emit('finish'));
|
|
35
|
-
};
|
|
36
|
-
stream.close = () => {};
|
|
37
|
-
return stream;
|
|
38
|
-
});
|
|
39
9
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
const response = new EventEmitter();
|
|
50
|
-
response.statusCode = config.statusCode != null ? config.statusCode : 200;
|
|
51
|
-
response.headers = config.redirectTo ? { location: config.redirectTo } : {};
|
|
52
|
-
response.pipe = (file) => {
|
|
53
|
-
file.write(config.body != null ? config.body : '# Skill');
|
|
54
|
-
file.end();
|
|
55
|
-
};
|
|
56
|
-
callback(response);
|
|
57
|
-
});
|
|
58
|
-
return req;
|
|
59
|
-
});
|
|
10
|
+
const BUNDLED_SKILLS = path.join(__dirname, '..', '..', 'skills');
|
|
11
|
+
|
|
12
|
+
function listDirs(root) {
|
|
13
|
+
return fs
|
|
14
|
+
.readdirSync(root, { withFileTypes: true })
|
|
15
|
+
.filter((entry) => entry.isDirectory())
|
|
16
|
+
.map((entry) => entry.name)
|
|
17
|
+
.sort();
|
|
60
18
|
}
|
|
61
19
|
|
|
62
20
|
describe('install-skill', () => {
|
|
@@ -67,11 +25,9 @@ describe('install-skill', () => {
|
|
|
67
25
|
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-in-'));
|
|
68
26
|
process.env.HOME = tmpHome;
|
|
69
27
|
vi.resetModules();
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
delete require.cache[require.resolve('../download-skill')];
|
|
74
|
-
delete require.cache[require.resolve('../rollback')];
|
|
28
|
+
for (const mod of ['../install-skill', '../detect-deps', '../shared-paths', '../rollback']) {
|
|
29
|
+
delete require.cache[require.resolve(mod)];
|
|
30
|
+
}
|
|
75
31
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
76
32
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
77
33
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
@@ -145,7 +101,7 @@ describe('install-skill', () => {
|
|
|
145
101
|
);
|
|
146
102
|
});
|
|
147
103
|
|
|
148
|
-
it('returns 1 + Unknown skill error for
|
|
104
|
+
it('returns 1 + Unknown skill error for a name the bundle does not carry', async () => {
|
|
149
105
|
setupValidDeps();
|
|
150
106
|
const { installSkill } = require('../install-skill');
|
|
151
107
|
const result = await installSkill('not-a-real-skill');
|
|
@@ -153,23 +109,50 @@ describe('install-skill', () => {
|
|
|
153
109
|
expect(console.error).toHaveBeenCalledWith('Error: Unknown skill: not-a-real-skill');
|
|
154
110
|
});
|
|
155
111
|
|
|
156
|
-
|
|
112
|
+
// The old hand-maintained SKILLS_REGISTRY could list a skill the bundle did not
|
|
113
|
+
// ship (or miss one it did). The error hint must come from the bundle itself.
|
|
114
|
+
it('names exactly the bundled skills in the unknown-skill hint', async () => {
|
|
115
|
+
setupValidDeps();
|
|
116
|
+
const { installSkill } = require('../install-skill');
|
|
117
|
+
await installSkill('not-a-real-skill');
|
|
118
|
+
|
|
119
|
+
const hint = console.error.mock.calls
|
|
120
|
+
.map(([message]) => message)
|
|
121
|
+
.find((message) => typeof message === 'string' && message.includes('bundles:'));
|
|
122
|
+
expect(hint).toBeDefined();
|
|
123
|
+
expect(hint.match(/bundles: ([^.]*)\./)[1].split(', ').sort()).toEqual(listDirs(BUNDLED_SKILLS));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('keeps an existing directory when the skill is not bundled', async () => {
|
|
127
|
+
// Validation must precede backupExisting(), which rmSyncs the target: a
|
|
128
|
+
// leftover skill from an older package must never be wiped by a failed install.
|
|
129
|
+
setupValidDeps();
|
|
130
|
+
const target = path.join(skillsDir(), 'removed-in-this-version');
|
|
131
|
+
fs.mkdirSync(target, { recursive: true });
|
|
132
|
+
fs.writeFileSync(path.join(target, 'SKILL.md'), 'keep me');
|
|
133
|
+
|
|
134
|
+
const { installSkill } = require('../install-skill');
|
|
135
|
+
const result = await installSkill('removed-in-this-version');
|
|
136
|
+
|
|
137
|
+
expect(result).toBe(1);
|
|
138
|
+
expect(fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8')).toBe('keep me');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns 0 and installs the bundled skill when install succeeds', async () => {
|
|
157
142
|
setupValidDeps();
|
|
158
|
-
mockHttpsGet({ statusCode: 200, body: '# Sprint Flow\nskill content' });
|
|
159
143
|
|
|
160
144
|
const { installSkill } = require('../install-skill');
|
|
161
145
|
const result = await installSkill('sprint-flow');
|
|
162
146
|
|
|
163
147
|
expect(result).toBe(0);
|
|
164
148
|
const installedFile = path.join(skillsDir(), 'sprint-flow', 'SKILL.md');
|
|
165
|
-
expect(fs.
|
|
166
|
-
|
|
149
|
+
expect(fs.readFileSync(installedFile, 'utf8'))
|
|
150
|
+
.toBe(fs.readFileSync(path.join(BUNDLED_SKILLS, 'sprint-flow', 'SKILL.md'), 'utf8'));
|
|
167
151
|
expect(console.log).toHaveBeenCalledWith('✓ sprint-flow installed');
|
|
168
152
|
});
|
|
169
153
|
|
|
170
154
|
it('updates config with installedSkills metadata after successful install', async () => {
|
|
171
155
|
setupValidDeps();
|
|
172
|
-
mockHttpsGet({ statusCode: 200, body: '# Content' });
|
|
173
156
|
|
|
174
157
|
const { installSkill } = require('../install-skill');
|
|
175
158
|
await installSkill('delphi-review');
|
|
@@ -202,15 +185,15 @@ describe('install-skill', () => {
|
|
|
202
185
|
const target = path.join(skillsDir(), 'sprint-flow');
|
|
203
186
|
fs.mkdirSync(target, { recursive: true });
|
|
204
187
|
fs.writeFileSync(path.join(target, 'OLD.md'), 'old-content');
|
|
205
|
-
mockHttpsGet({ statusCode: 200, body: '# New Content' });
|
|
206
188
|
|
|
207
189
|
const { installSkill } = require('../install-skill');
|
|
208
190
|
const result = await installSkill('sprint-flow', { force: true });
|
|
209
191
|
|
|
210
192
|
expect(result).toBe(0);
|
|
211
193
|
const skillMd = path.join(target, 'SKILL.md');
|
|
212
|
-
expect(fs.
|
|
213
|
-
|
|
194
|
+
expect(fs.readFileSync(skillMd, 'utf8'))
|
|
195
|
+
.toBe(fs.readFileSync(path.join(BUNDLED_SKILLS, 'sprint-flow', 'SKILL.md'), 'utf8'));
|
|
196
|
+
expect(fs.existsSync(path.join(target, 'OLD.md'))).toBe(false);
|
|
214
197
|
|
|
215
198
|
const backupRoot = path.join(configDir(), 'backup');
|
|
216
199
|
expect(fs.existsSync(backupRoot)).toBe(true);
|
|
@@ -221,66 +204,16 @@ describe('install-skill', () => {
|
|
|
221
204
|
expect(fs.readFileSync(backedUpFile, 'utf8')).toBe('old-content');
|
|
222
205
|
});
|
|
223
206
|
|
|
224
|
-
it('
|
|
225
|
-
setupValidDeps();
|
|
226
|
-
mockHttpsGet({ statusCode: 404 });
|
|
227
|
-
|
|
228
|
-
const { installSkill } = require('../install-skill');
|
|
229
|
-
const result = await installSkill('sprint-flow');
|
|
230
|
-
|
|
231
|
-
expect(result).toBe(1);
|
|
232
|
-
expect(console.error).toHaveBeenCalledWith('Error: Failed to download sprint-flow');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('returns 2 + offline-cache error when offline=true and no cache', async () => {
|
|
236
|
-
setupValidDeps();
|
|
237
|
-
|
|
238
|
-
const { installSkill } = require('../install-skill');
|
|
239
|
-
const result = await installSkill('sprint-flow', { offline: true });
|
|
240
|
-
|
|
241
|
-
expect(result).toBe(2);
|
|
242
|
-
expect(console.error).toHaveBeenCalledWith(
|
|
243
|
-
expect.stringContaining('--offline specified but sprint-flow not in cache')
|
|
244
|
-
);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('verbose=true prints Downloading and Installed-to logs', async () => {
|
|
207
|
+
it('verbose=true prints Installing and Installed-to logs', async () => {
|
|
248
208
|
setupValidDeps();
|
|
249
|
-
mockHttpsGet({ statusCode: 200, body: '# X' });
|
|
250
209
|
|
|
251
210
|
const { installSkill } = require('../install-skill');
|
|
252
211
|
await installSkill('test-driven-development', { verbose: true });
|
|
253
212
|
|
|
254
|
-
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('
|
|
213
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Installing '));
|
|
255
214
|
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Installed to '));
|
|
256
215
|
});
|
|
257
216
|
|
|
258
|
-
it('verbose=true prints download-failure warning when network errors', async () => {
|
|
259
|
-
setupValidDeps();
|
|
260
|
-
mockHttpsGet({ statusCode: 500 });
|
|
261
|
-
|
|
262
|
-
const { installSkill } = require('../install-skill');
|
|
263
|
-
const result = await installSkill('ralph-loop', { verbose: true });
|
|
264
|
-
|
|
265
|
-
expect(result).toBe(1);
|
|
266
|
-
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Download failed'));
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('follows redirect (302) to download successfully', async () => {
|
|
270
|
-
setupValidDeps();
|
|
271
|
-
mockHttpsGet([
|
|
272
|
-
{ statusCode: 302, redirectTo: 'https://new.example.com/skill.md' },
|
|
273
|
-
{ statusCode: 200, body: '# Redirected Content' },
|
|
274
|
-
]);
|
|
275
|
-
|
|
276
|
-
const { installSkill } = require('../install-skill');
|
|
277
|
-
const result = await installSkill('sprint-flow');
|
|
278
|
-
|
|
279
|
-
expect(result).toBe(0);
|
|
280
|
-
const installedFile = path.join(skillsDir(), 'sprint-flow', 'SKILL.md');
|
|
281
|
-
expect(fs.readFileSync(installedFile, 'utf8')).toContain('Redirected Content');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
217
|
it('merges new skill into existing installedSkills config', async () => {
|
|
285
218
|
setupValidDeps();
|
|
286
219
|
fs.mkdirSync(configDir(), { recursive: true });
|
|
@@ -291,7 +224,6 @@ describe('install-skill', () => {
|
|
|
291
224
|
otherSetting: true,
|
|
292
225
|
})
|
|
293
226
|
);
|
|
294
|
-
mockHttpsGet({ statusCode: 200, body: '# X' });
|
|
295
227
|
|
|
296
228
|
const { installSkill } = require('../install-skill');
|
|
297
229
|
await installSkill('delphi-review');
|
|
@@ -308,22 +240,9 @@ describe('install-skill', () => {
|
|
|
308
240
|
setupValidDeps();
|
|
309
241
|
fs.mkdirSync(configDir(), { recursive: true });
|
|
310
242
|
fs.writeFileSync(path.join(configDir(), 'xp-gate.json'), '{not-valid-json');
|
|
311
|
-
mockHttpsGet({ statusCode: 200, body: '# X' });
|
|
312
243
|
|
|
313
244
|
const { installSkill } = require('../install-skill');
|
|
314
245
|
const result = await installSkill('sprint-flow');
|
|
315
246
|
expect(result).toBe(0);
|
|
316
247
|
});
|
|
317
|
-
|
|
318
|
-
it('returns 1 + Failed to download on network error (non-verbose silent)', async () => {
|
|
319
|
-
setupValidDeps();
|
|
320
|
-
mockHttpsGet({ errorAfter: true });
|
|
321
|
-
|
|
322
|
-
const { installSkill } = require('../install-skill');
|
|
323
|
-
const result = await installSkill('test-driven-development');
|
|
324
|
-
|
|
325
|
-
expect(result).toBe(1);
|
|
326
|
-
expect(console.warn).not.toHaveBeenCalled();
|
|
327
|
-
expect(console.error).toHaveBeenCalledWith('Error: Failed to download test-driven-development');
|
|
328
|
-
});
|
|
329
248
|
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-468
|
|
3
|
+
* @intent detectProjectLanguages() 只按 configFiles 判定语言,而 shell/powershell 两条
|
|
4
|
+
* 注册表项的 configFiles 为空数组 —— 这两种没有包管理器配置的语言因此永远检不出,
|
|
5
|
+
* doctor/check-tools 对 147 个 .sh + 27 个 .ps1 的仓库视而不见(#468)
|
|
6
|
+
* @covers AC-468-01, AC-468-02, AC-468-03, AC-468-04, AC-468-05
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
function makeFixture(spec) {
|
|
13
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-lang-'));
|
|
14
|
+
for (const [rel, content] of Object.entries(spec)) {
|
|
15
|
+
const target = path.join(root, rel);
|
|
16
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
17
|
+
fs.writeFileSync(target, content);
|
|
18
|
+
}
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('detectProjectLanguages extension fallback (#468)', () => {
|
|
23
|
+
let loadModule;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.resetModules();
|
|
27
|
+
for (const mod of ['../language-tools']) {
|
|
28
|
+
delete require.cache[require.resolve(mod)];
|
|
29
|
+
}
|
|
30
|
+
loadModule = () => require('../language-tools');
|
|
31
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function detectedIn(spec) {
|
|
39
|
+
const root = makeFixture(spec);
|
|
40
|
+
try {
|
|
41
|
+
return loadModule().detectProjectLanguages(root).detected;
|
|
42
|
+
} finally {
|
|
43
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it('detects Shell from .sh files with no shell config file present', () => {
|
|
48
|
+
expect(detectedIn({ 'scripts/build.sh': '#!/bin/sh\necho hi\n' })).toContain('shell');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('detects PowerShell from .ps1 files with no config file present', () => {
|
|
52
|
+
expect(detectedIn({ 'tools/audit.ps1': 'Write-Host hi\n' })).toContain('powershell');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('does not report Shell for a project that has no shell script', () => {
|
|
56
|
+
const detected = detectedIn({ 'package.json': '{}' });
|
|
57
|
+
expect(detected).not.toContain('shell');
|
|
58
|
+
expect(detected).not.toContain('powershell');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// node_modules ships .sh files, so scanning it would label every Node project a
|
|
62
|
+
// Shell project and make check-tools demand shellcheck forever.
|
|
63
|
+
it('ignores dependency and VCS directories when scanning extensions', () => {
|
|
64
|
+
const detected = detectedIn({
|
|
65
|
+
'package.json': '{}',
|
|
66
|
+
'node_modules/left-pad/build.sh': '#!/bin/sh\n',
|
|
67
|
+
'.git/hooks/pre-commit.ps1': 'x\n',
|
|
68
|
+
});
|
|
69
|
+
expect(detected).not.toContain('shell');
|
|
70
|
+
expect(detected).not.toContain('powershell');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// The fallback is only for languages that have no config-file signal. Extensions
|
|
74
|
+
// are registered for every language, and IaC lists yaml/yml — applying them
|
|
75
|
+
// across the board would make any repo with a CI YAML an IaC project.
|
|
76
|
+
it('does not turn an unrelated YAML file into an IaC project', () => {
|
|
77
|
+
const detected = detectedIn({
|
|
78
|
+
'package.json': '{}',
|
|
79
|
+
'.github/workflows/ci.yml': 'name: ci\n',
|
|
80
|
+
});
|
|
81
|
+
expect(detected).not.toContain('iac');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('keeps config-file detection for the languages that already had it', () => {
|
|
85
|
+
const root = makeFixture({
|
|
86
|
+
'package.json': '{"name":"x"}',
|
|
87
|
+
'tsconfig.json': '{}',
|
|
88
|
+
});
|
|
89
|
+
let result;
|
|
90
|
+
try {
|
|
91
|
+
result = loadModule().detectProjectLanguages(root);
|
|
92
|
+
} finally {
|
|
93
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
expect(result.detected).toContain('typescript');
|
|
96
|
+
expect(result.configFiles.typescript).toContain('tsconfig.json');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('check-tools language selection (#468)', () => {
|
|
101
|
+
let fixtureRoot;
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
vi.resetModules();
|
|
105
|
+
delete require.cache[require.resolve('../language-tools')];
|
|
106
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
107
|
+
fixtureRoot = makeFixture({ 'scripts/build.sh': '#!/bin/sh\necho hi\n' });
|
|
108
|
+
// vitest workers forbid process.chdir(), and the command resolves the project
|
|
109
|
+
// root through process.cwd().
|
|
110
|
+
vi.spyOn(process, 'cwd').mockReturnValue(fixtureRoot);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
|
115
|
+
vi.restoreAllMocks();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('--languages <lang> with a space is not silently ignored', async () => {
|
|
119
|
+
const { handleCheckTools } = require('../language-tools');
|
|
120
|
+
const code = await handleCheckTools(['--languages', 'shell', '--json']);
|
|
121
|
+
|
|
122
|
+
expect(code).toBe(0);
|
|
123
|
+
const reported = JSON.parse(console.log.mock.calls.at(-1)[0]);
|
|
124
|
+
expect(Object.keys(reported)).toEqual(['shell']);
|
|
125
|
+
expect(reported.shell.map((t) => t.name)).toContain('shellcheck');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('--languages=<lang> keeps working', async () => {
|
|
129
|
+
const { handleCheckTools } = require('../language-tools');
|
|
130
|
+
const code = await handleCheckTools(['--languages=shell', '--json']);
|
|
131
|
+
|
|
132
|
+
expect(code).toBe(0);
|
|
133
|
+
const reported = JSON.parse(console.log.mock.calls.at(-1)[0]);
|
|
134
|
+
expect(Object.keys(reported)).toEqual(['shell']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('auto-detection reaches Shell, so check-tools with no flag lists shellcheck', async () => {
|
|
138
|
+
const { handleCheckTools } = require('../language-tools');
|
|
139
|
+
const code = await handleCheckTools(['--json']);
|
|
140
|
+
|
|
141
|
+
expect(code).toBe(0);
|
|
142
|
+
const reported = JSON.parse(console.log.mock.calls.at(-1)[0]);
|
|
143
|
+
expect(Object.keys(reported)).toContain('shell');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Round 1 review (architecture MI-02): `--languages=` split into [''], which
|
|
147
|
+
// was treated as an explicit selection and reported a language named '' instead
|
|
148
|
+
// of falling back to detection like the bare flag does.
|
|
149
|
+
it('--languages= with an empty value falls back to auto-detection', async () => {
|
|
150
|
+
const { handleCheckTools } = require('../language-tools');
|
|
151
|
+
const code = await handleCheckTools(['--languages=', '--json']);
|
|
152
|
+
|
|
153
|
+
expect(code).toBe(0);
|
|
154
|
+
const reported = JSON.parse(console.log.mock.calls.at(-1)[0]);
|
|
155
|
+
expect(Object.keys(reported)).not.toContain('');
|
|
156
|
+
expect(Object.keys(reported)).toContain('shell');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -697,7 +697,21 @@ describe('phase-transition', () => {
|
|
|
697
697
|
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'requirements-context-'));
|
|
698
698
|
const outsideFile = path.join(outsideDir, 'CONTEXT.md');
|
|
699
699
|
fs.writeFileSync(outsideFile, 'outside context\n', 'utf8');
|
|
700
|
-
|
|
700
|
+
// Creating a symlink needs SeCreateSymbolicLinkPrivilege (or Developer Mode)
|
|
701
|
+
// on Windows and fails with EPERM otherwise. Returning early here would
|
|
702
|
+
// leave an assertion that can never fail, so the gap is reported instead --
|
|
703
|
+
// and the try/finally below still cleans up the temp dir.
|
|
704
|
+
try {
|
|
705
|
+
fs.symlinkSync(outsideFile, path.join(tmpDir, 'CONTEXT.md'));
|
|
706
|
+
} catch (error) {
|
|
707
|
+
fs.rmSync(outsideDir, { recursive: true, force: true });
|
|
708
|
+
console.warn(
|
|
709
|
+
'[skip] symlink privilege unavailable on this host; the outside-root ' +
|
|
710
|
+
'context symlink rejection is NOT covered by this run.',
|
|
711
|
+
);
|
|
712
|
+
expect(error.code).toBe('EPERM');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
701
715
|
const evidence = validRequirementsReview(tmpDir, { context_file_used: 'CONTEXT.md' });
|
|
702
716
|
writeRequirementsReview(tmpDir, evidence);
|
|
703
717
|
|