@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.
Files changed (173) hide show
  1. package/adapters/gate-4.sh +83 -18
  2. package/adapters/gate-8.sh +49 -5
  3. package/adapters/iac.sh +1 -1
  4. package/adapters/typescript.sh +15 -60
  5. package/bin/xp-gate.js +15 -16
  6. package/gate-10.sh +35 -2
  7. package/gate-4.sh +110 -21
  8. package/gate-8.sh +49 -5
  9. package/hooks/gate-10.sh +35 -2
  10. package/hooks/gate-4.sh +110 -21
  11. package/hooks/gate-8.sh +49 -5
  12. package/hooks/lib/jscpd-run.sh +63 -0
  13. package/hooks/lib/sprint-gate-report.sh +58 -0
  14. package/hooks/lib/test-failure.sh +152 -0
  15. package/hooks/lib/typecheck.sh +141 -0
  16. package/hooks/lib/validate-code-walkthrough.cjs +43 -5
  17. package/hooks/post-merge +100 -0
  18. package/hooks/pre-commit +389 -162
  19. package/hooks/pre-push +75 -19
  20. package/lib/__tests__/bootstrap.test.js +23 -41
  21. package/lib/__tests__/check-version.test.js +8 -1
  22. package/lib/__tests__/detect-deps.test.js +16 -0
  23. package/lib/__tests__/doctor-drift-round2.test.js +212 -0
  24. package/lib/__tests__/doctor-executed-surface.test.js +209 -0
  25. package/lib/__tests__/doctor-hook-drift.test.js +329 -0
  26. package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
  27. package/lib/__tests__/doctor-module-drift.test.js +206 -0
  28. package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
  29. package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
  30. package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
  31. package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
  32. package/lib/__tests__/doctor.test.js +155 -0
  33. package/lib/__tests__/gate-runner.test.js +19 -3
  34. package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
  35. package/lib/__tests__/init.test.js +44 -17
  36. package/lib/__tests__/install-skill-full-dir.test.js +151 -0
  37. package/lib/__tests__/install-skill.test.js +51 -132
  38. package/lib/__tests__/language-tools-detect.test.js +158 -0
  39. package/lib/__tests__/phase-transition.test.js +15 -1
  40. package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
  41. package/lib/__tests__/shared-paths.test.js +20 -0
  42. package/lib/__tests__/shared-utils.test.js +69 -0
  43. package/lib/__tests__/skill-flags.test.js +48 -0
  44. package/lib/__tests__/ui-detector.test.ts +6 -2
  45. package/lib/__tests__/uninstall.test.js +20 -0
  46. package/lib/__tests__/update-hooks.test.js +12 -0
  47. package/lib/__tests__/update-skill.test.js +7 -4
  48. package/lib/detect-deps.js +3 -20
  49. package/lib/doctor-tui.js +22 -5
  50. package/lib/doctor.js +1178 -31
  51. package/lib/init.js +184 -33
  52. package/lib/install-cmd.js +7 -4
  53. package/lib/install-skill.js +38 -95
  54. package/lib/language-tools.js +83 -4
  55. package/lib/mattpocock-license-verdict.json +1 -1
  56. package/lib/shared-paths.js +5 -2
  57. package/lib/shared-utils.js +47 -3
  58. package/lib/uninstall.js +12 -0
  59. package/lib/update-hooks.js +4 -4
  60. package/lib/update-skill.js +6 -7
  61. package/mock-policy/AGENTS.md +4 -4
  62. package/mutation/AGENTS.md +4 -4
  63. package/mutation/__tests__/gate-m.test.ts +66 -14
  64. package/mutation/__tests__/mutmut-runner.test.ts +91 -42
  65. package/mutation/__tests__/test-filter-parity.test.ts +37 -0
  66. package/mutation/gate-m.ts +21 -8
  67. package/package.json +1 -1
  68. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  69. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  70. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  71. package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
  72. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
  73. package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
  74. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  75. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
  76. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  77. package/plugins/dsh/README.md +169 -4
  78. package/plugins/dsh/dsh-overlay.patch.yml +67 -0
  79. package/plugins/dsh/package.json +1 -1
  80. package/plugins/dsh/scripts/prepack.cjs +24 -19
  81. package/plugins/dsh/setup-dsh.ps1 +160 -0
  82. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  84. package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
  85. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
  86. package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
  87. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  88. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
  89. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  90. package/plugins/dsh/src/command.test.ts +66 -2
  91. package/plugins/dsh/src/command.ts +36 -14
  92. package/plugins/dsh/src/contract.test.ts +31 -1
  93. package/plugins/dsh/src/delphi-config.test.ts +190 -0
  94. package/plugins/dsh/src/delphi-config.ts +223 -0
  95. package/plugins/dsh/src/delphi-run.test.ts +181 -0
  96. package/plugins/dsh/src/delphi-run.ts +129 -0
  97. package/plugins/dsh/src/delphi.test.ts +222 -0
  98. package/plugins/dsh/src/delphi.ts +247 -0
  99. package/plugins/dsh/src/dialect.test.ts +59 -0
  100. package/plugins/dsh/src/dialect.ts +40 -0
  101. package/plugins/dsh/src/index.ts +70 -4
  102. package/plugins/dsh/src/skills.test.ts +26 -19
  103. package/plugins/opencode/package.json +1 -1
  104. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  105. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  106. package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
  107. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
  108. package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
  109. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
  111. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  112. package/plugins/qoder/agents/delphi-architecture.md +1 -1
  113. package/plugins/qoder/agents/delphi-feasibility.md +1 -1
  114. package/plugins/qoder/agents/delphi-technical.md +1 -1
  115. package/plugins/qoder/plugin.json +1 -1
  116. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  117. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  118. package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
  119. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
  120. package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  122. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
  123. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  124. package/principles/AGENTS.md +9 -8
  125. package/principles/__tests__/adapter-metrics.test.ts +292 -0
  126. package/principles/__tests__/analyzer.test.ts +29 -6
  127. package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
  128. package/principles/__tests__/baseline-prune.test.ts +338 -0
  129. package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
  130. package/principles/__tests__/boy-scout-integration.test.ts +97 -6
  131. package/principles/__tests__/boy-scout.test.ts +253 -7
  132. package/principles/__tests__/config-enforcement.test.ts +469 -0
  133. package/principles/__tests__/config-git-timeout.test.ts +66 -0
  134. package/principles/__tests__/direct-execution.test.ts +187 -0
  135. package/principles/__tests__/index.test.ts +16 -3
  136. package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
  137. package/principles/adapters/typescript.ts +613 -13
  138. package/principles/analyzer.ts +22 -0
  139. package/principles/baseline-prune.ts +217 -0
  140. package/principles/boy-scout.ts +481 -51
  141. package/principles/config.ts +362 -54
  142. package/principles/direct-execution.ts +60 -0
  143. package/principles/index.ts +40 -17
  144. package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
  145. package/principles/rules/clean-code/code-duplication.ts +8 -8
  146. package/principles/rules/clean-code/deep-nesting.ts +8 -8
  147. package/principles/rules/clean-code/god-class.ts +8 -8
  148. package/principles/rules/clean-code/large-file.ts +15 -11
  149. package/principles/rules/clean-code/long-function.ts +8 -8
  150. package/principles/rules/clean-code/magic-numbers.ts +10 -7
  151. package/principles/rules/clean-code/many-exports.ts +7 -7
  152. package/principles/rules/clean-code/missing-error-handling.ts +5 -5
  153. package/principles/rules/clean-code/too-many-params.ts +8 -8
  154. package/principles/rules/clean-code/unused-imports.ts +5 -5
  155. package/principles/rules/solid/dip.ts +18 -8
  156. package/principles/rules/solid/isp.ts +9 -8
  157. package/principles/rules/solid/lsp.ts +6 -6
  158. package/principles/rules/solid/ocp.ts +6 -5
  159. package/principles/rules/solid/srp.ts +7 -8
  160. package/principles/types.ts +15 -0
  161. package/scripts/__tests__/sync-package-content.test.js +28 -0
  162. package/scripts/delphi-external-review.cjs +193 -10
  163. package/scripts/sync-package-content.js +7 -4
  164. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  165. package/skills/delphi-review/AGENTS.md +4 -4
  166. package/skills/delphi-review/SKILL.md +98 -14
  167. package/skills/delphi-review/references/code-walkthrough.md +36 -3
  168. package/skills/delphi-review/references/requirements.md +6 -6
  169. package/skills/sprint-flow/AGENTS.md +4 -4
  170. package/skills/sprint-flow/references/phase-2-design.md +3 -3
  171. package/skills/test-specification-alignment/AGENTS.md +4 -4
  172. package/lib/__tests__/download-skill.test.js +0 -281
  173. package/lib/download-skill.js +0 -96
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-09-24
4
- **Commit:** 467174d
5
- **Branch:** main
6
- **Version:** 0.19.3.0
3
+ **Generated:** 2026-10-07
4
+ **Commit:** ff55f35b
5
+ **Branch:** fix/batch-defects-20261007
6
+ **Version:** 0.20.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -89,9 +89,9 @@ grill 访谈达成共享理解后(或 CONTEXT.md 快速路径下读取已有
89
89
  "head_commit": "<git rev-parse HEAD>",
90
90
  "consensus_ratio": 1.0,
91
91
  "expert_verdicts": [
92
- { "role": "architecture", "verdict": "APPROVED", "result_type": "delphi_expert_result", "requested_model": "provider/model-a" },
93
- { "role": "technical", "verdict": "APPROVED", "result_type": "delphi_expert_result", "requested_model": "provider/model-b" },
94
- { "role": "feasibility", "verdict": "APPROVED", "result_type": "delphi_expert_result", "requested_model": "provider/model-c" }
92
+ { "role": "architecture", "verdict": "APPROVED", "result_type": "delphi_expert_result", "requested_model": "provider/model-a", "resolved_model": "provider/model-a", "channel": "external" },
93
+ { "role": "technical", "verdict": "APPROVED", "result_type": "delphi_expert_result", "requested_model": "provider/model-b", "resolved_model": "provider/model-b", "channel": "external" },
94
+ { "role": "feasibility", "verdict": "APPROVED", "result_type": "delphi_expert_result", "requested_model": "provider/model-c", "resolved_model": "provider/model-c", "channel": "external" }
95
95
  ],
96
96
  "rounds": 1,
97
97
  "gaps": []
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-09-24
4
- **Commit:** 467174d
5
- **Branch:** main
6
- **Version:** 0.19.3.0
3
+ **Generated:** 2026-10-07
4
+ **Commit:** ff55f35b
5
+ **Branch:** fix/batch-defects-20261007
6
+ **Version:** 0.20.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,281 +0,0 @@
1
- /**
2
- * @test download-skill
3
- * @intent Verify downloadFromGitHub/downloadTarball/downloadWithRetry/downloadFile/verifyChecksum behaviors
4
- */
5
- const fs = require('fs');
6
- const path = require('path');
7
- const os = require('os');
8
- const https = require('https');
9
- const crypto = require('crypto');
10
- const { EventEmitter } = require('events');
11
- const { Writable } = require('stream');
12
-
13
- /**
14
- * Mock https.get with controllable behavior.
15
- * opts (or array of opts for sequential calls):
16
- * - statusCode (default 200)
17
- * - body (default 'data')
18
- * - errorAfter (boolean) — emit 'error' on the request instead of responding
19
- * - redirectTo (string url) — sends 302 with location header
20
- */
21
- function mockHttpsGet(optsOrList) {
22
- const list = Array.isArray(optsOrList) ? [...optsOrList] : [optsOrList];
23
- // Synchronous fake createWriteStream — the real one opens the FD asynchronously
24
- // and races against downloadFile's unlinkSync(dest) in 301/302/error branches,
25
- // causing ENOENT unhandled errors. The fake writes via writeFileSync (sync,
26
- // file present immediately) and emits 'finish' on end().
27
- vi.spyOn(fs, 'createWriteStream').mockImplementation((dest) => {
28
- let buf = '';
29
- try {
30
- fs.writeFileSync(dest, '');
31
- } catch {
32
- /* dest dir may not exist; downstream code will surface that */
33
- }
34
- const stream = new EventEmitter();
35
- stream.write = (chunk) => {
36
- buf += chunk;
37
- return true;
38
- };
39
- stream.end = () => {
40
- try {
41
- fs.writeFileSync(dest, buf);
42
- } catch {
43
- /* file may have been unlinked by source; safe to ignore */
44
- }
45
- process.nextTick(() => stream.emit('finish'));
46
- };
47
- stream.close = () => {};
48
- return stream;
49
- });
50
-
51
- vi.spyOn(https, 'get').mockImplementation((url, options, cb) => {
52
- const callback = typeof options === 'function' ? options : cb;
53
- const req = new EventEmitter();
54
- const config = list.shift() || { statusCode: 200, body: 'data' };
55
- process.nextTick(() => {
56
- if (config.errorAfter) {
57
- req.emit('error', new Error('Network error'));
58
- return;
59
- }
60
- const response = new EventEmitter();
61
- response.statusCode = config.statusCode != null ? config.statusCode : 200;
62
- response.headers = config.redirectTo ? { location: config.redirectTo } : {};
63
- response.pipe = (file) => {
64
- file.write(config.body != null ? config.body : 'data');
65
- file.end();
66
- };
67
- callback(response);
68
- });
69
- return req;
70
- });
71
- }
72
-
73
- describe('download-skill', () => {
74
- let tmpHome, originalHome;
75
-
76
- beforeEach(() => {
77
- originalHome = process.env.HOME;
78
- tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dl-'));
79
- process.env.HOME = tmpHome;
80
- vi.resetModules();
81
- delete require.cache[require.resolve('../download-skill')];
82
- vi.spyOn(console, 'log').mockImplementation(() => {});
83
- vi.spyOn(console, 'error').mockImplementation(() => {});
84
- vi.spyOn(console, 'warn').mockImplementation(() => {});
85
- });
86
-
87
- afterEach(async () => {
88
- vi.useRealTimers();
89
- await new Promise((resolve) => setImmediate(resolve));
90
- process.env.HOME = originalHome;
91
- vi.restoreAllMocks();
92
- if (fs.existsSync(tmpHome)) {
93
- fs.rmSync(tmpHome, { recursive: true, force: true });
94
- }
95
- });
96
-
97
- function cacheDir() {
98
- return path.join(tmpHome, '.config', 'xp-gate', 'cache');
99
- }
100
-
101
- describe('downloadFromGitHub', () => {
102
- // Flat repo name avoids '/' becoming a subdir in cacheFile (source only mkdirs CACHE_DIR root).
103
- it('downloads file to cache dir and returns the path', async () => {
104
- mockHttpsGet({ statusCode: 200, body: '# Skill content' });
105
- const { downloadFromGitHub } = require('../download-skill');
106
-
107
- const result = await downloadFromGitHub('flatrepo', 'skills/foo/SKILL.md');
108
-
109
- expect(result).toBe(
110
- path.join(cacheDir(), 'flatrepo-skills-foo-SKILL.md.md')
111
- );
112
- expect(fs.existsSync(result)).toBe(true);
113
- expect(fs.readFileSync(result, 'utf8')).toBe('# Skill content');
114
- });
115
-
116
- it('creates cache directory if missing', async () => {
117
- mockHttpsGet({ statusCode: 200, body: 'data' });
118
- const { downloadFromGitHub } = require('../download-skill');
119
-
120
- expect(fs.existsSync(cacheDir())).toBe(false);
121
- await downloadFromGitHub('flatrepo', 'a/b');
122
- expect(fs.existsSync(cacheDir())).toBe(true);
123
- });
124
-
125
- it('uses default version=main when not specified', async () => {
126
- mockHttpsGet({ statusCode: 200, body: 'data' });
127
- const { downloadFromGitHub } = require('../download-skill');
128
- await downloadFromGitHub('flatrepo', 'path/to/file');
129
- expect(https.get).toHaveBeenCalled();
130
- const calledUrl = https.get.mock.calls[0][0];
131
- expect(calledUrl).toContain('/main/');
132
- });
133
-
134
- it('uses custom version when specified', async () => {
135
- mockHttpsGet({ statusCode: 200, body: 'data' });
136
- const { downloadFromGitHub } = require('../download-skill');
137
- await downloadFromGitHub('flatrepo', 'path/file', 'v1.2.3');
138
- expect(https.get.mock.calls[0][0]).toContain('/v1.2.3/');
139
- });
140
- });
141
-
142
- describe('downloadTarball', () => {
143
- it('downloads tarball with correct filename', async () => {
144
- mockHttpsGet({ statusCode: 200, body: 'tarball-bytes' });
145
- const { downloadTarball } = require('../download-skill');
146
-
147
- const result = await downloadTarball('boyingliu01/xp-gate');
148
-
149
- expect(result).toBe(path.join(cacheDir(), 'boyingliu01-xp-gate.tgz'));
150
- expect(fs.existsSync(result)).toBe(true);
151
- });
152
-
153
- it('uses default main version', async () => {
154
- mockHttpsGet({ statusCode: 200, body: 'data' });
155
- const { downloadTarball } = require('../download-skill');
156
- await downloadTarball('user/repo');
157
- expect(https.get.mock.calls[0][0]).toContain('/tarball/main');
158
- });
159
- });
160
-
161
- describe('downloadWithRetry', () => {
162
- it('succeeds on first try when no errors', async () => {
163
- mockHttpsGet({ statusCode: 200, body: 'ok' });
164
- const { downloadWithRetry } = require('../download-skill');
165
-
166
- const dest = path.join(tmpHome, 'out.txt');
167
- await downloadWithRetry('https://example.com/x', dest, 3);
168
-
169
- expect(https.get).toHaveBeenCalledTimes(1);
170
- expect(fs.readFileSync(dest, 'utf8')).toBe('ok');
171
- });
172
-
173
- it('retries and eventually succeeds (sleep delays stubbed)', async () => {
174
- // Stub setTimeout so sleep() resolves immediately, without patching setImmediate.
175
- vi.spyOn(global, 'setTimeout').mockImplementation((fn) => {
176
- Promise.resolve().then(fn);
177
- return 0;
178
- });
179
- mockHttpsGet([
180
- { errorAfter: true },
181
- { statusCode: 200, body: 'recovered' },
182
- ]);
183
- const { downloadWithRetry } = require('../download-skill');
184
-
185
- const dest = path.join(tmpHome, 'retry.txt');
186
- await downloadWithRetry('https://example.com/x', dest, 3);
187
-
188
- expect(fs.readFileSync(dest, 'utf8')).toBe('recovered');
189
- });
190
-
191
- it('throws original error after exhausting retries', async () => {
192
- vi.spyOn(global, 'setTimeout').mockImplementation((fn) => {
193
- Promise.resolve().then(fn);
194
- return 0;
195
- });
196
- mockHttpsGet([
197
- { errorAfter: true },
198
- { errorAfter: true },
199
- ]);
200
- const { downloadWithRetry } = require('../download-skill');
201
-
202
- const dest = path.join(tmpHome, 'fail.txt');
203
- await expect(
204
- downloadWithRetry('https://example.com/x', dest, 2)
205
- ).rejects.toThrow('Network error');
206
- });
207
- });
208
-
209
- describe('downloadFile (via downloadWithRetry retries=1)', () => {
210
- it('follows 302 redirect to new location', async () => {
211
- mockHttpsGet([
212
- { statusCode: 302, redirectTo: 'https://newlocation.example.com/final' },
213
- { statusCode: 200, body: 'redirected-content' },
214
- ]);
215
- const { downloadWithRetry } = require('../download-skill');
216
-
217
- const dest = path.join(tmpHome, 'redirect.txt');
218
- await downloadWithRetry('https://example.com/orig', dest, 1);
219
-
220
- expect(fs.readFileSync(dest, 'utf8')).toBe('redirected-content');
221
- });
222
-
223
- it('follows 301 redirect', async () => {
224
- mockHttpsGet([
225
- { statusCode: 301, redirectTo: 'https://newloc.example/final' },
226
- { statusCode: 200, body: 'permanent-redirect' },
227
- ]);
228
- const { downloadWithRetry } = require('../download-skill');
229
-
230
- const dest = path.join(tmpHome, 'r301.txt');
231
- await downloadWithRetry('https://example.com/orig', dest, 1);
232
-
233
- expect(fs.readFileSync(dest, 'utf8')).toBe('permanent-redirect');
234
- });
235
-
236
- it('rejects with HTTP error for non-2xx/3xx status', async () => {
237
- mockHttpsGet({ statusCode: 404 });
238
- const { downloadWithRetry } = require('../download-skill');
239
-
240
- const dest = path.join(tmpHome, '404.txt');
241
- await expect(downloadWithRetry('https://example.com/x', dest, 1)).rejects.toThrow(
242
- 'HTTP 404'
243
- );
244
- });
245
-
246
- it('rejects and unlinks dest on request error', async () => {
247
- mockHttpsGet({ errorAfter: true });
248
- const { downloadWithRetry } = require('../download-skill');
249
-
250
- const dest = path.join(tmpHome, 'err.txt');
251
- await expect(downloadWithRetry('https://example.com/x', dest, 1)).rejects.toThrow(
252
- 'Network error'
253
- );
254
- // The createWriteStream call will create the file, the error handler should unlink it
255
- expect(fs.existsSync(dest)).toBe(false);
256
- });
257
- });
258
-
259
- describe('verifyChecksum', () => {
260
- it('does not throw when checksum matches', () => {
261
- const file = path.join(tmpHome, 'data.txt');
262
- const content = 'hello world';
263
- fs.writeFileSync(file, content);
264
- const expected = crypto.createHash('sha256').update(content).digest('hex');
265
-
266
- const { verifyChecksum } = require('../download-skill');
267
- expect(() => verifyChecksum(file, expected)).not.toThrow();
268
- // File preserved
269
- expect(fs.existsSync(file)).toBe(true);
270
- });
271
-
272
- it('throws and deletes file on mismatch', () => {
273
- const file = path.join(tmpHome, 'bad.txt');
274
- fs.writeFileSync(file, 'real-content');
275
-
276
- const { verifyChecksum } = require('../download-skill');
277
- expect(() => verifyChecksum(file, 'wrong-hash-zzz')).toThrow(/Checksum mismatch/);
278
- expect(fs.existsSync(file)).toBe(false);
279
- });
280
- });
281
- });
@@ -1,96 +0,0 @@
1
- const https = require('https');
2
- const http = require('http');
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const crypto = require('crypto');
7
-
8
- // Cross-platform home directory resolution
9
- const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
10
-
11
- const CONFIG_DIR = path.join(HOME, '.config', 'xp-gate');
12
- const CACHE_DIR = path.join(CONFIG_DIR, 'cache');
13
-
14
- async function downloadFromGitHub(repo, pathInRepo, version = 'main') {
15
- const url = `https://raw.githubusercontent.com/${repo}/${version}/${pathInRepo}`;
16
- const cacheFile = path.join(CACHE_DIR, `${repo}-${pathInRepo.replace(/\//g, '-')}.md`);
17
-
18
- fs.mkdirSync(CACHE_DIR, { recursive: true });
19
-
20
- await downloadWithRetry(url, cacheFile);
21
-
22
- return cacheFile;
23
- }
24
-
25
- async function downloadWithRetry(url, dest, retries = 3) {
26
- for (let attempt = 1; attempt <= retries; attempt++) {
27
- try {
28
- await downloadFile(url, dest);
29
- return;
30
- } catch (err) {
31
- if (attempt === retries) throw err;
32
- console.log(`Retry ${attempt}/${retries}...`);
33
- await sleep(1000 * attempt);
34
- }
35
- }
36
- }
37
-
38
- async function downloadFile(url, dest) {
39
- return new Promise((resolve, reject) => {
40
- const file = fs.createWriteStream(dest);
41
-
42
- https.get(url, { timeout: 30000 }, (response) => {
43
- if (response.statusCode === 301 || response.statusCode === 302) {
44
- file.close();
45
- fs.unlinkSync(dest);
46
- downloadFile(response.headers.location, dest).then(resolve).catch(reject);
47
- return;
48
- }
49
-
50
- if (response.statusCode !== 200) {
51
- file.close();
52
- reject(new Error(`HTTP ${response.statusCode}`));
53
- return;
54
- }
55
-
56
- response.pipe(file);
57
- file.on('finish', () => {
58
- file.close();
59
- resolve();
60
- });
61
- }).on('error', (err) => {
62
- file.close();
63
- if (fs.existsSync(dest)) fs.unlinkSync(dest);
64
- reject(err);
65
- });
66
- });
67
- }
68
-
69
- async function downloadTarball(repo, version = 'main') {
70
- const tarballUrl = `https://api.github.com/repos/${repo}/tarball/${version}`;
71
- const cacheFile = path.join(CACHE_DIR, `${repo.replace('/', '-')}.tgz`);
72
-
73
- fs.mkdirSync(CACHE_DIR, { recursive: true });
74
-
75
- await downloadWithRetry(tarballUrl, cacheFile);
76
-
77
- return cacheFile;
78
- }
79
-
80
- function sleep(ms) {
81
- return new Promise(resolve => setTimeout(resolve, ms));
82
- }
83
-
84
- function verifyChecksum(file, expectedChecksum) {
85
- const hash = crypto.createHash('sha256');
86
- const content = fs.readFileSync(file);
87
- hash.update(content);
88
- const actualChecksum = hash.digest('hex');
89
-
90
- if (actualChecksum !== expectedChecksum) {
91
- fs.unlinkSync(file);
92
- throw new Error(`Checksum mismatch: expected ${expectedChecksum}, got ${actualChecksum}`);
93
- }
94
- }
95
-
96
- module.exports = { downloadFromGitHub, downloadTarball, downloadWithRetry, verifyChecksum };