xp-gate 0.5.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.
Files changed (90) hide show
  1. package/adapter-common.sh +192 -0
  2. package/adapters/cpp.sh +76 -0
  3. package/adapters/dart.sh +41 -0
  4. package/adapters/flutter.sh +41 -0
  5. package/adapters/go.sh +59 -0
  6. package/adapters/iac.sh +189 -0
  7. package/adapters/java.sh +191 -0
  8. package/adapters/kotlin.sh +77 -0
  9. package/adapters/objectivec.sh +38 -0
  10. package/adapters/powershell.sh +138 -0
  11. package/adapters/python.sh +104 -0
  12. package/adapters/shell.sh +55 -0
  13. package/adapters/swift.sh +44 -0
  14. package/adapters/typescript.sh +61 -0
  15. package/bin/xp-gate.js +157 -0
  16. package/hooks/adapter-common.sh +192 -0
  17. package/hooks/pre-commit +1667 -0
  18. package/hooks/pre-push +395 -0
  19. package/lib/__tests__/detect-deps.test.js +209 -0
  20. package/lib/__tests__/doctor.test.js +448 -0
  21. package/lib/__tests__/download-skill.test.js +281 -0
  22. package/lib/__tests__/init.test.js +327 -0
  23. package/lib/__tests__/install-skill.test.js +326 -0
  24. package/lib/__tests__/migrate.test.js +212 -0
  25. package/lib/__tests__/rollback.test.js +183 -0
  26. package/lib/__tests__/ui-detector.test.ts +200 -0
  27. package/lib/__tests__/uninstall-skill.test.js +189 -0
  28. package/lib/__tests__/uninstall.test.js +589 -0
  29. package/lib/__tests__/update-skill.test.js +276 -0
  30. package/lib/detect-deps.js +157 -0
  31. package/lib/doctor.js +370 -0
  32. package/lib/download-skill.js +96 -0
  33. package/lib/init.js +367 -0
  34. package/lib/install-skill.js +184 -0
  35. package/lib/migrate.js +120 -0
  36. package/lib/rollback.js +78 -0
  37. package/lib/ui-detector.ts +99 -0
  38. package/lib/uninstall-skill.js +69 -0
  39. package/lib/uninstall.js +401 -0
  40. package/lib/update-skill.js +90 -0
  41. package/package.json +39 -0
  42. package/plugins/claude-code/.claude-plugin/plugin.json +21 -0
  43. package/plugins/claude-code/bin/delphi-review-guard.sh +68 -0
  44. package/plugins/claude-code/bin/xp-gate-check +47 -0
  45. package/plugins/claude-code/hooks/hooks.json +37 -0
  46. package/skills/delphi-review/.delphi-config.json.example +45 -0
  47. package/skills/delphi-review/AGENTS.md +54 -0
  48. package/skills/delphi-review/INSTALL.md +152 -0
  49. package/skills/delphi-review/SKILL.md +371 -0
  50. package/skills/delphi-review/evals/evals.json +82 -0
  51. package/skills/delphi-review/opencode.json.delphi.example +56 -0
  52. package/skills/delphi-review/references/code-walkthrough.md +486 -0
  53. package/skills/ralph-loop/SKILL.md +330 -0
  54. package/skills/ralph-loop/evals/evals.json +311 -0
  55. package/skills/ralph-loop/evolution-history.json +59 -0
  56. package/skills/ralph-loop/evolution-log.md +16 -0
  57. package/skills/ralph-loop/references/components/memory.md +55 -0
  58. package/skills/ralph-loop/references/components/middleware.md +54 -0
  59. package/skills/ralph-loop/references/components/skill-invocations.md +39 -0
  60. package/skills/ralph-loop/references/components/system-prompt.md +24 -0
  61. package/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
  62. package/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
  63. package/skills/ralph-loop/templates/progress-log.md +36 -0
  64. package/skills/sprint-flow/SKILL.md +600 -0
  65. package/skills/sprint-flow/evals/evals.json +78 -0
  66. package/skills/sprint-flow/evolution-history.json +39 -0
  67. package/skills/sprint-flow/evolution-log.md +23 -0
  68. package/skills/sprint-flow/references/components/memory.md +87 -0
  69. package/skills/sprint-flow/references/components/middleware.md +72 -0
  70. package/skills/sprint-flow/references/components/skill-invocations.md +104 -0
  71. package/skills/sprint-flow/references/components/system-prompt.md +27 -0
  72. package/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
  73. package/skills/sprint-flow/references/phase-0-think.md +115 -0
  74. package/skills/sprint-flow/references/phase-1-plan.md +178 -0
  75. package/skills/sprint-flow/references/phase-2-build.md +198 -0
  76. package/skills/sprint-flow/references/phase-3-review.md +213 -0
  77. package/skills/sprint-flow/references/phase-4-uat.md +125 -0
  78. package/skills/sprint-flow/references/phase-5-feedback.md +100 -0
  79. package/skills/sprint-flow/references/phase-6-ship.md +193 -0
  80. package/skills/sprint-flow/references/phase-7-land.md +140 -0
  81. package/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
  82. package/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
  83. package/skills/sprint-flow/templates/pain-document-template.md +115 -0
  84. package/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
  85. package/skills/test-specification-alignment/AGENTS.md +59 -0
  86. package/skills/test-specification-alignment/SKILL.md +605 -0
  87. package/skills/test-specification-alignment/evals/evals.json +75 -0
  88. package/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
  89. package/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
  90. package/skills/test-specification-alignment/references/specification-format.md +348 -0
package/lib/doctor.js ADDED
@@ -0,0 +1,370 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+
6
+ // Cross-platform home directory resolution
7
+ const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
8
+
9
+ const CONFIG_DIR = path.join(HOME_DIR, '.config', 'xp-gate');
10
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'xp-gate.json');
11
+ const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
12
+ const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
13
+
14
+ // npm package source dir (template hooks/adapters)
15
+ const PKG_DIR = path.dirname(__dirname);
16
+
17
+ /**
18
+ * Signature strings used to verify file ownership.
19
+ */
20
+ const SIGNATURES = {
21
+ 'pre-commit': 'OpenCode Quality Gates - Pre-Commit Hook',
22
+ 'pre-push': 'Pre-push Hook - Code Walkthrough Result Validator',
23
+ 'adapter-common.sh': 'detect_project_lang()'
24
+ };
25
+
26
+ function isXpGateFile(filePath, signature) {
27
+ if (!fs.existsSync(filePath)) return false;
28
+ try {
29
+ const content = fs.readFileSync(filePath, 'utf8');
30
+ return content.includes(signature);
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function getConfig() {
37
+ if (!fs.existsSync(CONFIG_FILE)) return null;
38
+ try {
39
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
40
+ } catch {
41
+ return 'corrupt';
42
+ }
43
+ }
44
+
45
+ function getGitDir() {
46
+ try {
47
+ return execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function getCurrentHooksPath() {
54
+ try {
55
+ const result = execSync('git config --global core.hooksPath', {
56
+ encoding: 'utf8',
57
+ stdio: ['pipe', 'pipe', 'pipe']
58
+ });
59
+ return result.trim();
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function checkEnv(checks) {
66
+ const envChecks = [
67
+ { name: 'Node.js', cmd: 'node --version', label: null },
68
+ { name: 'Git', cmd: 'git --version', label: null },
69
+ { name: 'Bash', cmd: 'bash --version', label: null }
70
+ ];
71
+
72
+ let allOk = true;
73
+ for (const env of envChecks) {
74
+ try {
75
+ const output = execSync(env.cmd, {
76
+ encoding: 'utf8',
77
+ stdio: ['ignore', 'pipe', 'pipe']
78
+ });
79
+ const version = output.trim().split('\n')[0];
80
+ env.label = version;
81
+ checks.push({ name: `Environment: ${env.name}`, status: 'PASS', detail: version });
82
+ } catch {
83
+ checks.push({ name: `Environment: ${env.name}`, status: 'FAIL', detail: 'Not found' });
84
+ allOk = false;
85
+ }
86
+ }
87
+
88
+ return allOk;
89
+ }
90
+
91
+ /**
92
+ * Build check report for the doctor.
93
+ * Returns { checks: Array<{name, status, detail}>, issues: number }
94
+ */
95
+ function diagnose() {
96
+ const checks = [];
97
+ let issues = 0;
98
+ const config = getConfig();
99
+
100
+ // --- Check 1: Config file ---
101
+ if (config === null) {
102
+ checks.push({ name: 'Config file', status: 'FAIL', detail: 'Not found' });
103
+ issues++;
104
+ return { checks, issues }; // Cannot proceed without config
105
+ }
106
+
107
+ if (config === 'corrupt') {
108
+ checks.push({ name: 'Config file', status: 'FAIL', detail: 'Corrupt JSON' });
109
+ issues++;
110
+ return { checks, issues }; // Cannot proceed with corrupt config
111
+ }
112
+
113
+ checks.push({ name: 'Config file', status: 'PASS', detail: CONFIG_FILE });
114
+
115
+ // Check mode
116
+ if (config.mode !== 'local' && config.mode !== 'global') {
117
+ checks.push({ name: 'Install mode', status: 'FAIL', detail: `Unknown: ${config.mode}` });
118
+ issues++;
119
+ return { checks, issues };
120
+ }
121
+
122
+ checks.push({ name: 'Install mode', status: 'PASS', detail: config.mode });
123
+
124
+ // --- Check 2: Hooks files ---
125
+ if (config.mode === 'local') {
126
+ const gitDir = getGitDir();
127
+ if (!gitDir) {
128
+ checks.push({ name: 'Git repository', status: 'FAIL', detail: 'Not in a git repo' });
129
+ issues++;
130
+ } else {
131
+ const hooksDir = path.join(gitDir, 'hooks');
132
+ const preCommit = path.join(hooksDir, 'pre-commit');
133
+ const prePush = path.join(hooksDir, 'pre-push');
134
+
135
+ if (!fs.existsSync(preCommit) || !isXpGateFile(preCommit, SIGNATURES['pre-commit'])) {
136
+ checks.push({ name: 'Hooks: pre-commit', status: 'FAIL', detail: 'Missing or not xp-gate' });
137
+ issues++;
138
+ } else {
139
+ checks.push({ name: 'Hooks: pre-commit', status: 'PASS', detail: preCommit });
140
+ }
141
+
142
+ if (!fs.existsSync(prePush) || !isXpGateFile(prePush, SIGNATURES['pre-push'])) {
143
+ checks.push({ name: 'Hooks: pre-push', status: 'FAIL', detail: 'Missing or not xp-gate' });
144
+ issues++;
145
+ } else {
146
+ checks.push({ name: 'Hooks: pre-push', status: 'PASS', detail: prePush });
147
+ }
148
+ }
149
+ } else if (config.mode === 'global') {
150
+ // Check global hooks directory
151
+ const preCommit = path.join(GLOBAL_HOOKS_DIR, 'pre-commit');
152
+ const prePush = path.join(GLOBAL_HOOKS_DIR, 'pre-push');
153
+
154
+ if (!fs.existsSync(preCommit) || !isXpGateFile(preCommit, SIGNATURES['pre-commit'])) {
155
+ checks.push({ name: 'Global hooks: pre-commit', status: 'FAIL', detail: 'Missing or not xp-gate' });
156
+ issues++;
157
+ } else {
158
+ checks.push({ name: 'Global hooks: pre-commit', status: 'PASS', detail: preCommit });
159
+ }
160
+
161
+ if (!fs.existsSync(prePush) || !isXpGateFile(prePush, SIGNATURES['pre-push'])) {
162
+ checks.push({ name: 'Global hooks: pre-push', status: 'FAIL', detail: 'Missing or not xp-gate' });
163
+ issues++;
164
+ } else {
165
+ checks.push({ name: 'Global hooks: pre-push', status: 'PASS', detail: prePush });
166
+ }
167
+
168
+ // --- Check 4: core.hooksPath (global mode only) ---
169
+ const hooksPath = getCurrentHooksPath();
170
+ if (hooksPath === null || hooksPath === '') {
171
+ checks.push({ name: 'Git core.hooksPath', status: 'FAIL', detail: 'Not set' });
172
+ issues++;
173
+ } else if (hooksPath !== GLOBAL_HOOKS_DIR) {
174
+ checks.push({ name: 'Git core.hooksPath', status: 'FAIL', detail: `Expected ${GLOBAL_HOOKS_DIR}, got ${hooksPath}` });
175
+ issues++;
176
+ } else {
177
+ checks.push({ name: 'Git core.hooksPath', status: 'PASS', detail: GLOBAL_HOOKS_DIR });
178
+ }
179
+ }
180
+
181
+ // --- Check 3: Adapters directory ---
182
+ const adaptersDir = config.mode === 'local'
183
+ ? path.join(path.dirname(getGitDir() || ''), 'githooks', 'adapters')
184
+ : GLOBAL_ADAPTERS_DIR;
185
+
186
+ if (!adaptersDir || !fs.existsSync(adaptersDir)) {
187
+ checks.push({ name: 'Adapters directory', status: 'FAIL', detail: 'Missing' });
188
+ issues++;
189
+ } else {
190
+ const adapterFiles = fs.readdirSync(adaptersDir).filter(f => f.endsWith('.sh'));
191
+ if (adapterFiles.length === 0) {
192
+ checks.push({ name: 'Adapters directory', status: 'FAIL', detail: 'Empty directory' });
193
+ issues++;
194
+ } else {
195
+ checks.push({ name: 'Adapters directory', status: 'PASS', detail: `${adapterFiles.length} adapter(s)` });
196
+ }
197
+ }
198
+
199
+ // --- Check 5: Environment dependencies ---
200
+ checkEnv(checks);
201
+
202
+ return { checks, issues };
203
+ }
204
+
205
+ /**
206
+ * Print the check results in a readable format.
207
+ */
208
+ function printReport(checks) {
209
+ console.log('');
210
+ console.log('Diagnosis Report:');
211
+ console.log('-----------------');
212
+
213
+ for (const check of checks) {
214
+ const statusSymbol = check.status === 'PASS' ? ' ✓' : ' ✗';
215
+ console.log(` ${statusSymbol} ${check.name}: ${check.detail}`);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Attempt to fix known issues.
221
+ * Only operates when mode === 'active' (local or global).
222
+ */
223
+ function fixIssues(checks, config) {
224
+ console.log('');
225
+ console.log('Attempting fixes...');
226
+ console.log('-------------------');
227
+
228
+ const srcDir = PKG_DIR;
229
+ let fixed = false;
230
+
231
+ // Fix missing hooks
232
+ if (config.mode === 'local') {
233
+ const gitDir = getGitDir();
234
+ if (gitDir) {
235
+ const hooksDir = path.join(gitDir, 'hooks');
236
+ const preCommit = path.join(hooksDir, 'pre-commit');
237
+ const prePush = path.join(hooksDir, 'pre-push');
238
+
239
+ if (!fs.existsSync(preCommit) || !isXpGateFile(preCommit, SIGNATURES['pre-commit'])) {
240
+ const src = path.join(srcDir, 'hooks', 'pre-commit');
241
+ if (fs.existsSync(src)) {
242
+ fs.mkdirSync(hooksDir, { recursive: true });
243
+ fs.copyFileSync(src, preCommit);
244
+ fs.chmodSync(preCommit, 0o755);
245
+ console.log(' ✓ Restored pre-commit hook');
246
+ fixed = true;
247
+ }
248
+ }
249
+
250
+ if (!fs.existsSync(prePush) || !isXpGateFile(prePush, SIGNATURES['pre-push'])) {
251
+ const src = path.join(srcDir, 'hooks', 'pre-push');
252
+ if (fs.existsSync(src)) {
253
+ fs.mkdirSync(hooksDir, { recursive: true });
254
+ fs.copyFileSync(src, prePush);
255
+ fs.chmodSync(prePush, 0o755);
256
+ console.log(' ✓ Restored pre-push hook');
257
+ fixed = true;
258
+ }
259
+ }
260
+ }
261
+ } else if (config.mode === 'global') {
262
+ const preCommit = path.join(GLOBAL_HOOKS_DIR, 'pre-commit');
263
+ const prePush = path.join(GLOBAL_HOOKS_DIR, 'pre-push');
264
+
265
+ if (!fs.existsSync(preCommit) || !isXpGateFile(preCommit, SIGNATURES['pre-commit'])) {
266
+ const src = path.join(srcDir, 'hooks', 'pre-commit');
267
+ if (fs.existsSync(src)) {
268
+ fs.mkdirSync(GLOBAL_HOOKS_DIR, { recursive: true });
269
+ fs.copyFileSync(src, preCommit);
270
+ fs.chmodSync(preCommit, 0o755);
271
+ console.log(' ✓ Restored global pre-commit hook');
272
+ fixed = true;
273
+ }
274
+ }
275
+
276
+ if (!fs.existsSync(prePush) || !isXpGateFile(prePush, SIGNATURES['pre-push'])) {
277
+ const src = path.join(srcDir, 'hooks', 'pre-push');
278
+ if (fs.existsSync(src)) {
279
+ fs.mkdirSync(GLOBAL_HOOKS_DIR, { recursive: true });
280
+ fs.copyFileSync(src, prePush);
281
+ fs.chmodSync(prePush, 0o755);
282
+ console.log(' ✓ Restored global pre-push hook');
283
+ fixed = true;
284
+ }
285
+ }
286
+
287
+ // Fix core.hooksPath
288
+ const hooksPath = getCurrentHooksPath();
289
+ if (hooksPath !== GLOBAL_HOOKS_DIR) {
290
+ try {
291
+ execSync(`git config --global core.hooksPath "${GLOBAL_HOOKS_DIR}"`, {
292
+ stdio: ['pipe', 'pipe', 'pipe']
293
+ });
294
+ console.log(` ✓ Set core.hooksPath to ${GLOBAL_HOOKS_DIR}`);
295
+ fixed = true;
296
+ } catch (e) {
297
+ console.log(` ✗ Could not set core.hooksPath: ${e.message}`);
298
+ }
299
+ }
300
+ }
301
+
302
+ // Fix missing adapters
303
+ const adaptersDir = config.mode === 'local'
304
+ ? path.join(path.dirname(getGitDir() || ''), 'githooks', 'adapters')
305
+ : GLOBAL_ADAPTERS_DIR;
306
+
307
+ if (adaptersDir && (!fs.existsSync(adaptersDir) || fs.readdirSync(adaptersDir).filter(f => f.endsWith('.sh')).length === 0)) {
308
+ const pkgAdaptersDir = path.join(srcDir, 'adapters');
309
+ if (fs.existsSync(pkgAdaptersDir)) {
310
+ fs.mkdirSync(adaptersDir, { recursive: true });
311
+ const adapterFiles = fs.readdirSync(pkgAdaptersDir).filter(f => f.endsWith('.sh'));
312
+ for (const f of adapterFiles) {
313
+ fs.copyFileSync(path.join(pkgAdaptersDir, f), path.join(adaptersDir, f));
314
+ }
315
+ console.log(` ✓ Restored ${adapterFiles.length} adapter(s)`);
316
+ fixed = true;
317
+ }
318
+ }
319
+
320
+ if (!fixed) {
321
+ console.log(' No fixable issues found.');
322
+ }
323
+ }
324
+
325
+ /**
326
+ * @param {string[]} args CLI arguments
327
+ * @returns {number} exit code (0 = all clear, 1 = issues found)
328
+ */
329
+ async function doctor(args) {
330
+ const fixMode = args.includes('--fix');
331
+
332
+ console.log('XP-Gate Doctor');
333
+ console.log('==============');
334
+
335
+ const config = getConfig();
336
+
337
+ // §4.8: mode === "uninstalled" → print "xp-gate is not installed"
338
+ if (config && config !== 'corrupt' && config.mode === 'uninstalled') {
339
+ console.log('xp-gate is not installed.');
340
+ console.log('Run xp-gate init to install.');
341
+ return 0;
342
+ }
343
+
344
+ // §4.13: --fix only when mode === "active"
345
+ if (fixMode && config && config !== 'corrupt' && (config.mode === 'local' || config.mode === 'global')) {
346
+ fixIssues(null, config);
347
+ }
348
+
349
+ const { checks, issues } = diagnose();
350
+
351
+ printReport(checks);
352
+
353
+ if (issues === 0) {
354
+ console.log('\n✓ All checks passed');
355
+ return 0;
356
+ }
357
+
358
+ console.log(`\n✗ ${issues} issue(s) found`);
359
+
360
+ if (fixMode && config && config !== 'corrupt' && (config.mode === 'local' || config.mode === 'global')) {
361
+ // Re-run diagnosis after fix to report updated status
362
+ console.log('\nRe-running diagnosis after fix...');
363
+ const { checks: postChecks } = diagnose();
364
+ printReport(postChecks);
365
+ }
366
+
367
+ return issues > 0 ? 1 : 0;
368
+ }
369
+
370
+ module.exports = { doctor, isXpGateFile, SIGNATURES };
@@ -0,0 +1,96 @@
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 };