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/init.js ADDED
@@ -0,0 +1,367 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+ const { checkDeps } = require('./detect-deps.js');
6
+ const { checkBash } = require('./detect-deps.js');
7
+
8
+ // Cross-platform home directory resolution with os.homedir() fallback
9
+ const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
10
+
11
+ const CONFIG_DIR = path.join(HOME_DIR, '.config', 'xp-gate');
12
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'xp-gate.json');
13
+ const TEMPLATE_DIR = path.join(HOME_DIR, '.config', 'opencode', 'git-hooks-template');
14
+ const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
15
+ const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
16
+
17
+ function copyHooks(srcDir, destDir) {
18
+ ['pre-commit', 'pre-push'].forEach(hook => {
19
+ const src = path.join(srcDir, 'hooks', hook);
20
+ const dest = path.join(destDir, hook);
21
+ if (fs.existsSync(src)) {
22
+ fs.copyFileSync(src, dest);
23
+ fs.chmodSync(dest, 0o755);
24
+ }
25
+ });
26
+ }
27
+
28
+ function copyAdapters(srcDir, destDir) {
29
+ const adapterSrc = path.join(srcDir, 'adapter-common.sh');
30
+ if (fs.existsSync(adapterSrc)) {
31
+ fs.copyFileSync(adapterSrc, path.join(destDir, 'adapter-common.sh'));
32
+ }
33
+ const adaptersDir = path.join(srcDir, 'adapters');
34
+ if (fs.existsSync(adaptersDir)) {
35
+ fs.readdirSync(adaptersDir).forEach(f => {
36
+ if (f.endsWith('.sh')) {
37
+ fs.copyFileSync(path.join(adaptersDir, f), path.join(destDir, f));
38
+ }
39
+ });
40
+ }
41
+ }
42
+
43
+ function logDeps(depCheck) {
44
+ if (!depCheck.ok) {
45
+ console.warn('Warning: Missing dependencies');
46
+ if (depCheck.missing) console.warn(` - ${depCheck.missing} (required)`);
47
+ if (depCheck.versionMismatch) {
48
+ console.warn(` - ${depCheck.versionMismatch.name}: need ${depCheck.versionMismatch.required}, found ${depCheck.versionMismatch.found}`);
49
+ }
50
+ console.warn('Skills may not work without these dependencies');
51
+ console.warn('Install from: https://github.com/boyingliu01/superpowers\n');
52
+ } else {
53
+ console.log('Dependencies: OK\n');
54
+ }
55
+ }
56
+
57
+ function printUsage() {
58
+ console.log('Choose installation mode:');
59
+ console.log(' 1) Global — all git projects use the same hooks (recommended)');
60
+ console.log(' 2) Local — install hooks into current project only\n');
61
+ console.log('Usage:');
62
+ console.log(' xp-gate init --global # all projects');
63
+ console.log(' xp-gate init # current project');
64
+ console.log(' xp-gate setup-global # all projects (alias)\n');
65
+ }
66
+
67
+ function getGitDir() {
68
+ try {
69
+ const { execSync } = require('child_process');
70
+ return execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function ensureConfigDir() {
77
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
78
+ }
79
+
80
+ function updateConfig(updates) {
81
+ let config = {};
82
+ if (fs.existsSync(CONFIG_FILE)) {
83
+ try {
84
+ config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
85
+ } catch {}
86
+ }
87
+ config = { ...config, ...updates };
88
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
89
+ }
90
+
91
+ function sha256File(filePath) {
92
+ try {
93
+ const content = fs.readFileSync(filePath);
94
+ return crypto.createHash('sha256').update(content).digest('hex');
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function generateManifest(srcDir, projectRoot) {
101
+ const manifest = {
102
+ version: 1,
103
+ files: {},
104
+ injectedSections: {}
105
+ };
106
+
107
+ const gitDir = path.join(projectRoot, '.git');
108
+ const hooksDir = path.join(gitDir, 'hooks');
109
+ const githooksDir = path.join(projectRoot, 'githooks');
110
+
111
+ // Hooks
112
+ ['pre-commit', 'pre-push'].forEach(hook => {
113
+ const hookPath = path.join(hooksDir, hook);
114
+ if (fs.existsSync(hookPath)) {
115
+ const stat = fs.statSync(hookPath);
116
+ manifest.files[`.git/hooks/${hook}`] = {
117
+ sha256: sha256File(hookPath),
118
+ size: stat.size
119
+ };
120
+ }
121
+ });
122
+
123
+ // adapter-common.sh
124
+ const adapterCommonPath = path.join(githooksDir, 'adapter-common.sh');
125
+ if (fs.existsSync(adapterCommonPath)) {
126
+ const stat = fs.statSync(adapterCommonPath);
127
+ manifest.files['githooks/adapter-common.sh'] = {
128
+ sha256: sha256File(adapterCommonPath),
129
+ size: stat.size
130
+ };
131
+ }
132
+
133
+ // Adapter scripts
134
+ const adaptersDir = path.join(githooksDir, 'adapters');
135
+ if (fs.existsSync(adaptersDir)) {
136
+ fs.readdirSync(adaptersDir).forEach(f => {
137
+ const fPath = path.join(adaptersDir, f);
138
+ if (fs.statSync(fPath).isFile()) {
139
+ const stat = fs.statSync(fPath);
140
+ manifest.files[`githooks/adapters/${f}`] = {
141
+ sha256: sha256File(fPath),
142
+ size: stat.size
143
+ };
144
+ }
145
+ });
146
+ }
147
+
148
+ // Template dir note
149
+ manifest.templateDir = TEMPLATE_DIR;
150
+
151
+ // Injected sections
152
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
153
+ if (fs.existsSync(agentsPath)) {
154
+ try {
155
+ const content = fs.readFileSync(agentsPath, 'utf8');
156
+ if (content.includes('## AI CODING DISCIPLINE (Karpathy Principles)')) {
157
+ manifest.injectedSections['AGENTS.md'] = '## AI CODING DISCIPLINE (Karpathy Principles)';
158
+ }
159
+ } catch {}
160
+ }
161
+
162
+ return manifest;
163
+ }
164
+
165
+ function generateGlobalManifest(srcDir) {
166
+ const manifest = {
167
+ version: 1,
168
+ files: {},
169
+ gitConfig: {}
170
+ };
171
+
172
+ // Global hooks
173
+ ['pre-commit', 'pre-push'].forEach(hook => {
174
+ const hookPath = path.join(GLOBAL_HOOKS_DIR, hook);
175
+ if (fs.existsSync(hookPath)) {
176
+ const stat = fs.statSync(hookPath);
177
+ manifest.files[`hooks/${hook}`] = {
178
+ sha256: sha256File(hookPath),
179
+ size: stat.size
180
+ };
181
+ }
182
+ });
183
+
184
+ // adapter-common.sh
185
+ const adapterCommonPath = path.join(GLOBAL_ADAPTERS_DIR, 'adapter-common.sh');
186
+ if (fs.existsSync(adapterCommonPath)) {
187
+ const stat = fs.statSync(adapterCommonPath);
188
+ manifest.files['adapters/adapter-common.sh'] = {
189
+ sha256: sha256File(adapterCommonPath),
190
+ size: stat.size
191
+ };
192
+ }
193
+
194
+ // Adapter scripts
195
+ if (fs.existsSync(GLOBAL_ADAPTERS_DIR)) {
196
+ fs.readdirSync(GLOBAL_ADAPTERS_DIR).forEach(f => {
197
+ const fPath = path.join(GLOBAL_ADAPTERS_DIR, f);
198
+ if (fs.statSync(fPath).isFile() && f !== 'adapter-common.sh') {
199
+ const stat = fs.statSync(fPath);
200
+ manifest.files[`adapters/${f}`] = {
201
+ sha256: sha256File(fPath),
202
+ size: stat.size
203
+ };
204
+ }
205
+ });
206
+ }
207
+
208
+ // git config
209
+ manifest.gitConfig = {
210
+ 'core.hooksPath': GLOBAL_HOOKS_DIR
211
+ };
212
+
213
+ manifest.templateDir = TEMPLATE_DIR;
214
+
215
+ return manifest;
216
+ }
217
+
218
+ async function init(args) {
219
+ console.log('XP-Gate Initialization');
220
+ console.log('====================\n');
221
+
222
+ // Check bash availability (required for shell hooks)
223
+ const bashCheck = checkBash();
224
+ if (bashCheck.ok) {
225
+ console.log(`Bash: ✓ ${bashCheck.path} (v${bashCheck.version})\n`);
226
+ } else {
227
+ console.warn(`Bash: ✗ NOT FOUND`);
228
+ console.warn(` ${bashCheck.message}\n`);
229
+ }
230
+
231
+ logDeps(await checkDeps());
232
+
233
+ const installMode = args.includes('--global') ? 'global' :
234
+ args.includes('--core-only') ? 'local' :
235
+ args.includes('--full') ? 'local' : null;
236
+
237
+ if (!installMode) { printUsage(); return 0; }
238
+ if (installMode === 'global') return setupGlobal(args);
239
+ return installLocal(args);
240
+ }
241
+
242
+ async function installLocal(args) {
243
+ const gitDir = getGitDir();
244
+ if (!gitDir) {
245
+ console.error('Error: Not a git repository');
246
+ console.error('Run xp-gate init from inside a git repository');
247
+ return 1;
248
+ }
249
+
250
+ const projectRoot = path.dirname(gitDir);
251
+ const hooksDir = path.join(projectRoot, '.git', 'hooks');
252
+ const srcDir = path.dirname(__dirname);
253
+
254
+ console.log(`Mode: Local (per-project)`);
255
+ console.log(`Project: ${projectRoot}`);
256
+ console.log(`Git hooks: ${hooksDir}\n`);
257
+ console.log('Installing hooks...');
258
+
259
+ copyHooks(srcDir, hooksDir);
260
+ console.log(' pre-commit -> .git/hooks/');
261
+ console.log(' pre-push -> .git/hooks/');
262
+
263
+ fs.mkdirSync(path.join(projectRoot, 'githooks', 'adapters'), { recursive: true });
264
+ copyAdapters(srcDir, path.join(projectRoot, 'githooks'));
265
+ console.log(` adapter-common.sh + adapters -> ${projectRoot}/githooks/`);
266
+
267
+ fs.mkdirSync(TEMPLATE_DIR, { recursive: true });
268
+ copyHooks(srcDir, TEMPLATE_DIR);
269
+ fs.mkdirSync(path.join(TEMPLATE_DIR, 'adapters'), { recursive: true });
270
+
271
+ ensureConfigDir();
272
+
273
+ const manifest = generateManifest(srcDir, projectRoot);
274
+ updateConfig({ lastInit: new Date().toISOString(), mode: 'local', manifest });
275
+
276
+ injectKarpathyPrinciples(projectRoot);
277
+
278
+ console.log('\nInstallation complete!');
279
+ console.log('Run git commit to trigger quality gates');
280
+ return 0;
281
+ }
282
+
283
+ async function setupGlobal(args) {
284
+ const srcDir = path.dirname(__dirname);
285
+
286
+ console.log('XP-Gate Global Setup');
287
+ console.log('====================\n');
288
+ console.log('Mode: Global (all git projects)');
289
+ console.log(`Global hooks: ${GLOBAL_HOOKS_DIR}`);
290
+ console.log(`Global adapters: ${GLOBAL_ADAPTERS_DIR}\n`);
291
+
292
+ fs.mkdirSync(GLOBAL_HOOKS_DIR, { recursive: true });
293
+ fs.mkdirSync(GLOBAL_ADAPTERS_DIR, { recursive: true });
294
+
295
+ copyHooks(srcDir, GLOBAL_HOOKS_DIR);
296
+ console.log('Installing hooks...');
297
+ console.log(` pre-commit -> ${GLOBAL_HOOKS_DIR}`);
298
+ console.log(` pre-push -> ${GLOBAL_HOOKS_DIR}`);
299
+
300
+ copyAdapters(srcDir, GLOBAL_ADAPTERS_DIR);
301
+ console.log(` adapter-common.sh + adapters -> ${GLOBAL_ADAPTERS_DIR}`);
302
+
303
+ const { execSync } = require('child_process');
304
+ try {
305
+ execSync(`git config --global core.hooksPath "${GLOBAL_HOOKS_DIR}"`);
306
+ console.log(`\n git config --global core.hooksPath "${GLOBAL_HOOKS_DIR}"`);
307
+ } catch (e) {
308
+ console.warn('Warning: Could not set git core.hooksPath config');
309
+ }
310
+
311
+ ensureConfigDir();
312
+
313
+ const manifest = generateGlobalManifest(srcDir);
314
+ updateConfig({ lastInit: new Date().toISOString(), mode: 'global', manifest });
315
+
316
+ console.log('\nGlobal setup complete!');
317
+ console.log('All git repositories will now use xp-gate quality gates.');
318
+ console.log('Per-project adapters can still override by creating <repo>/githooks/');
319
+ return 0;
320
+ }
321
+
322
+ function injectKarpathyPrinciples(projectRoot) {
323
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
324
+ if (!fs.existsSync(agentsPath)) return;
325
+
326
+ let content;
327
+ try {
328
+ content = fs.readFileSync(agentsPath, 'utf8');
329
+ } catch (e) {
330
+ console.warn(` Warning: could not read AGENTS.md: ${e.message}`);
331
+ return;
332
+ }
333
+
334
+ if (content.includes('## AI CODING DISCIPLINE (Karpathy Principles)')) return;
335
+
336
+ const section = `
337
+ ## AI CODING DISCIPLINE (Karpathy Principles)
338
+
339
+ **原则 3: Surgical Changes(外科手术式改动)**
340
+ - 只碰必须碰的代码。只清理自己制造的混乱。
341
+ - 编辑现有代码时,不"优化"相邻代码、注释或 formatting
342
+ - 不重构没坏的东西
343
+ - 匹配现有代码风格,即使 AI 更喜欢另一种
344
+ - 发现无关的死代码 → 提及但不要删除(除非用户明确要求)
345
+ - 自己的改动产生的 orphaned import/variable/function → 必须清理
346
+ - 判定标准: 每一行改动都应能直接追溯到用户的请求
347
+
348
+ **原则 4: Goal-Driven Execution(目标驱动执行)**
349
+ - 定义成功标准。循环直到验证。
350
+ - 把指令转化为可验证目标:
351
+ - "加验证" → "写测试 → 让测试通过"
352
+ - "修 bug" → "写复现测试 → 让测试通过"
353
+ - "重构 X" → "确保重构前后测试都通过"
354
+ - 多步骤任务列出验证点
355
+ - 改完任何代码后必须运行测试确认无 regression
356
+
357
+ `;
358
+
359
+ try {
360
+ fs.appendFileSync(agentsPath, section, 'utf8');
361
+ console.log(' Karpathy Principles injected into AGENTS.md');
362
+ } catch (e) {
363
+ console.warn(` Warning: could not write to AGENTS.md: ${e.message}`);
364
+ }
365
+ }
366
+
367
+ module.exports = { init };
@@ -0,0 +1,184 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const https = require('https');
5
+ const http = require('http');
6
+ const { execSync } = require('child_process');
7
+ const { checkDeps } = require('./detect-deps.js');
8
+ const { downloadFromGitHub } = require('./download-skill.js');
9
+ const { rollback } = require('./rollback.js');
10
+
11
+ // Cross-platform home directory resolution
12
+ const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
13
+
14
+ const CONFIG_DIR = path.join(HOME, '.config', 'xp-gate');
15
+ const SKILLS_DIR = path.join(HOME, '.config', 'opencode', 'skills');
16
+
17
+ const SKILLS_REGISTRY = {
18
+ 'sprint-flow': { repo: 'boyingliu01/xp-gate', path: 'skills/sprint-flow' },
19
+ 'delphi-review': { repo: 'boyingliu01/xp-gate', path: 'skills/delphi-review' },
20
+ 'test-spec': { repo: 'boyingliu01/xp-gate', path: 'skills/test-spec' },
21
+ 'ralph-loop': { repo: 'boyingliu01/xp-gate', path: 'skills/ralph-loop' }
22
+ };
23
+
24
+ async function installSkill(name, options = {}) {
25
+ const { offline = false, verbose = false, force = false } = options;
26
+
27
+ const depCheck = await checkDeps();
28
+ if (!depCheck.ok) {
29
+ if (depCheck.missing) {
30
+ console.error(`Error: ${depCheck.missing} is required but not installed`);
31
+ console.error('Please install superpowers and gstack first');
32
+ console.error('See: https://github.com/boyingliu01/superpowers');
33
+ return 1;
34
+ }
35
+ if (depCheck.versionMismatch) {
36
+ console.error(`Error: ${depCheck.versionMismatch.name} version too old`);
37
+ console.error(`Need: ${depCheck.versionMismatch.required}, Found: ${depCheck.versionMismatch.found}`);
38
+ return 1;
39
+ }
40
+ }
41
+
42
+ const skillInfo = SKILLS_REGISTRY[name];
43
+ if (!skillInfo) {
44
+ console.error(`Error: Unknown skill: ${name}`);
45
+ console.error('Available skills: ' + Object.keys(SKILLS_REGISTRY).join(', '));
46
+ return 1;
47
+ }
48
+
49
+ const targetDir = path.join(SKILLS_DIR, name);
50
+ if (fs.existsSync(targetDir) && !force) {
51
+ console.error(`Error: ${name} is already installed`);
52
+ console.error('Use --force to overwrite');
53
+ return 1;
54
+ }
55
+
56
+ const installId = `${name}-${Date.now()}`;
57
+ const backupDir = path.join(CONFIG_DIR, 'backup', installId);
58
+
59
+ if (fs.existsSync(targetDir)) {
60
+ fs.mkdirSync(backupDir, { recursive: true });
61
+ copyDirRecursive(targetDir, backupDir);
62
+ }
63
+
64
+ try {
65
+ console.log(`Installing ${name}...`);
66
+
67
+ const skillUrl = `https://raw.githubusercontent.com/${skillInfo.repo}/main/${skillInfo.path}/SKILL.md`;
68
+ const targetFile = path.join(targetDir, 'SKILL.md');
69
+
70
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
71
+
72
+ let downloaded = false;
73
+ if (!offline) {
74
+ try {
75
+ await downloadFile(skillUrl, targetFile, verbose);
76
+ downloaded = true;
77
+ } catch (err) {
78
+ if (verbose) console.warn(`Download failed: ${err.message}`);
79
+ }
80
+ }
81
+
82
+ if (!downloaded) {
83
+ if (offline) {
84
+ console.error(`Error: --offline specified but ${name} not in cache`);
85
+ return 2;
86
+ }
87
+ console.error(`Error: Failed to download ${name}`);
88
+ console.error('Check network connection');
89
+ return 1;
90
+ }
91
+
92
+ ensureConfigDir();
93
+ updateConfig({
94
+ installedSkills: {
95
+ ...(getConfig().installedSkills || {}),
96
+ [name]: { version: '1.0.0', installedAt: new Date().toISOString() }
97
+ }
98
+ });
99
+
100
+ if (verbose) console.log(`Installed to ${targetDir}`);
101
+ console.log(`✓ ${name} installed`);
102
+
103
+ return 0;
104
+ } catch (err) {
105
+ console.error(`Error: Install failed - ${err.message}`);
106
+ await rollback(installId);
107
+ return 1;
108
+ }
109
+ }
110
+
111
+ async function downloadFile(url, dest, verbose) {
112
+ return new Promise((resolve, reject) => {
113
+ const file = fs.createWriteStream(dest);
114
+
115
+ const protocol = url.startsWith('https') ? https : http;
116
+
117
+ if (verbose) console.log(`Downloading ${url}...`);
118
+
119
+ protocol.get(url, { timeout: 30000 }, (response) => {
120
+ if (response.statusCode === 301 || response.statusCode === 302) {
121
+ const redirectUrl = response.headers.location;
122
+ file.close();
123
+ fs.unlinkSync(dest);
124
+ downloadFile(redirectUrl, dest, verbose).then(resolve).catch(reject);
125
+ return;
126
+ }
127
+
128
+ if (response.statusCode !== 200) {
129
+ file.close();
130
+ reject(new Error(`HTTP ${response.statusCode}`));
131
+ return;
132
+ }
133
+
134
+ response.pipe(file);
135
+ file.on('finish', () => {
136
+ file.close();
137
+ resolve();
138
+ });
139
+ }).on('error', (err) => {
140
+ file.close();
141
+ if (fs.existsSync(dest)) fs.unlinkSync(dest);
142
+ reject(err);
143
+ });
144
+ });
145
+ }
146
+
147
+ function copyDirRecursive(src, dest) {
148
+ fs.mkdirSync(dest, { recursive: true });
149
+ const entries = fs.readdirSync(src, { withFileTypes: true });
150
+
151
+ for (const entry of entries) {
152
+ const srcPath = path.join(src, entry.name);
153
+ const destPath = path.join(dest, entry.name);
154
+
155
+ if (entry.isDirectory()) {
156
+ copyDirRecursive(srcPath, destPath);
157
+ } else {
158
+ fs.copyFileSync(srcPath, destPath);
159
+ }
160
+ }
161
+ }
162
+
163
+ function ensureConfigDir() {
164
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
165
+ }
166
+
167
+ function getConfig() {
168
+ const configFile = path.join(CONFIG_DIR, 'xp-gate.json');
169
+ if (fs.existsSync(configFile)) {
170
+ try {
171
+ return JSON.parse(fs.readFileSync(configFile, 'utf8'));
172
+ } catch {}
173
+ }
174
+ return {};
175
+ }
176
+
177
+ function updateConfig(updates) {
178
+ const configFile = path.join(CONFIG_DIR, 'xp-gate.json');
179
+ const config = getConfig();
180
+ Object.assign(config, updates);
181
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
182
+ }
183
+
184
+ module.exports = { installSkill };
package/lib/migrate.js ADDED
@@ -0,0 +1,120 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ // Cross-platform home directory resolution (matches other modules pattern)
6
+ const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
7
+
8
+ /**
9
+ * Migration helper for v0.4.x → v0.5.x.
10
+ * - Cleans GitHub Packages PAT lines from ~/.npmrc
11
+ * - Checks ~/.config/xp-gate/cache/ for old cached downloads
12
+ *
13
+ * Safety: Only removes lines that contain 'npm.pkg.github.com'.
14
+ * Generic PAT lines (other registries) are never touched.
15
+ *
16
+ * @param {string[]} args - CLI arguments (--dry-run supported)
17
+ * @returns {Promise<number>} exit code (0 = success)
18
+ */
19
+ async function migrate(args = []) {
20
+ const options = { dryRun: args.includes('--dry-run') };
21
+
22
+ const npmrcPath = path.join(HOME, '.npmrc');
23
+ const cacheDir = path.join(HOME, '.config', 'xp-gate', 'cache');
24
+
25
+ let npmrcChanged = false;
26
+ let npmrcRemovedCount = 0;
27
+
28
+ // === Phase 1: ~/.npmrc cleanup ===
29
+ console.log('Checking ~/.npmrc for GitHub Packages residue...');
30
+
31
+ if (!fs.existsSync(npmrcPath)) {
32
+ console.log(' No ~/.npmrc found — nothing to clean.');
33
+ } else {
34
+ const content = fs.readFileSync(npmrcPath, 'utf8');
35
+ const lines = content.split('\n');
36
+
37
+ // Find lines that reference npm.pkg.github.com
38
+ const linesToRemove = [];
39
+ const keptLines = [];
40
+
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i];
43
+ if (line.includes('npm.pkg.github.com')) {
44
+ linesToRemove.push(line);
45
+ } else {
46
+ keptLines.push(line);
47
+ }
48
+ }
49
+
50
+ npmrcRemovedCount = linesToRemove.length;
51
+
52
+ if (npmrcRemovedCount === 0) {
53
+ console.log(' No GitHub Packages lines found — ~/.npmrc is clean.');
54
+ } else {
55
+ console.log(` Found ${npmrcRemovedCount} npm.pkg.github.com line(s):`);
56
+
57
+ for (const line of linesToRemove) {
58
+ // Mask auth tokens in output for safety
59
+ const masked = line.replace(/(:_authToken=).+/, '$1***');
60
+ console.log(` - ${masked}`);
61
+ }
62
+
63
+ if (options.dryRun) {
64
+ console.log('');
65
+ console.log(' [Dry-run] No changes made. Would remove the above line(s).');
66
+ } else {
67
+ // Write back without GitHub Packages lines
68
+ const newContent = keptLines.join('\n');
69
+ fs.writeFileSync(npmrcPath, newContent, 'utf8');
70
+ console.log(' Cleaned successfully.');
71
+ npmrcChanged = true;
72
+ }
73
+ }
74
+ }
75
+
76
+ // === Phase 2: Cache check ===
77
+ console.log('');
78
+ console.log('Checking ~/.config/xp-gate/cache/ for old downloads...');
79
+
80
+ if (!fs.existsSync(cacheDir)) {
81
+ console.log(' No old cache directory found.');
82
+ } else {
83
+ const items = fs.readdirSync(cacheDir);
84
+
85
+ if (items.length === 0) {
86
+ console.log(' Cache directory exists but is empty.');
87
+ } else {
88
+ console.log(` Found ${items.length} cached file(s) from old installation.`);
89
+ for (const item of items) {
90
+ const itemPath = path.join(cacheDir, item);
91
+ const stat = fs.statSync(itemPath);
92
+ const size = stat.isFile() ? `(${formatSize(stat.size)})` : '(directory)';
93
+ console.log(` - ${item} ${size}`);
94
+ }
95
+
96
+ if (!options.dryRun) {
97
+ console.log(' Note: These files are harmless but no longer needed.');
98
+ console.log(' You can safely remove them with: rm -rf ' + cacheDir);
99
+ }
100
+ }
101
+ }
102
+
103
+ // === Phase 3: Summary ===
104
+ console.log('');
105
+ console.log('Migration Summary:');
106
+ console.log(` ~/.npmrc: ${npmrcRemovedCount > 0 ? `${npmrcRemovedCount} GitHub Packages line(s) ${options.dryRun ? 'would be' : ''}removed` : 'No changes needed'}`);
107
+ console.log(` Cache: ${fs.existsSync(cacheDir) && fs.readdirSync(cacheDir).length > 0 ? 'Old files found (can be cleaned manually)' : 'No old cache found'}`);
108
+ console.log('');
109
+ console.log('Migration complete. xp-gate v0.5.x no longer requires GitHub Packages or PAT tokens.');
110
+
111
+ return 0;
112
+ }
113
+
114
+ function formatSize(bytes) {
115
+ if (bytes < 1024) return bytes + ' B';
116
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
117
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
118
+ }
119
+
120
+ module.exports = { migrate };