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.
- package/adapter-common.sh +192 -0
- package/adapters/cpp.sh +76 -0
- package/adapters/dart.sh +41 -0
- package/adapters/flutter.sh +41 -0
- package/adapters/go.sh +59 -0
- package/adapters/iac.sh +189 -0
- package/adapters/java.sh +191 -0
- package/adapters/kotlin.sh +77 -0
- package/adapters/objectivec.sh +38 -0
- package/adapters/powershell.sh +138 -0
- package/adapters/python.sh +104 -0
- package/adapters/shell.sh +55 -0
- package/adapters/swift.sh +44 -0
- package/adapters/typescript.sh +61 -0
- package/bin/xp-gate.js +157 -0
- package/hooks/adapter-common.sh +192 -0
- package/hooks/pre-commit +1667 -0
- package/hooks/pre-push +395 -0
- package/lib/__tests__/detect-deps.test.js +209 -0
- package/lib/__tests__/doctor.test.js +448 -0
- package/lib/__tests__/download-skill.test.js +281 -0
- package/lib/__tests__/init.test.js +327 -0
- package/lib/__tests__/install-skill.test.js +326 -0
- package/lib/__tests__/migrate.test.js +212 -0
- package/lib/__tests__/rollback.test.js +183 -0
- package/lib/__tests__/ui-detector.test.ts +200 -0
- package/lib/__tests__/uninstall-skill.test.js +189 -0
- package/lib/__tests__/uninstall.test.js +589 -0
- package/lib/__tests__/update-skill.test.js +276 -0
- package/lib/detect-deps.js +157 -0
- package/lib/doctor.js +370 -0
- package/lib/download-skill.js +96 -0
- package/lib/init.js +367 -0
- package/lib/install-skill.js +184 -0
- package/lib/migrate.js +120 -0
- package/lib/rollback.js +78 -0
- package/lib/ui-detector.ts +99 -0
- package/lib/uninstall-skill.js +69 -0
- package/lib/uninstall.js +401 -0
- package/lib/update-skill.js +90 -0
- package/package.json +39 -0
- package/plugins/claude-code/.claude-plugin/plugin.json +21 -0
- package/plugins/claude-code/bin/delphi-review-guard.sh +68 -0
- package/plugins/claude-code/bin/xp-gate-check +47 -0
- package/plugins/claude-code/hooks/hooks.json +37 -0
- package/skills/delphi-review/.delphi-config.json.example +45 -0
- package/skills/delphi-review/AGENTS.md +54 -0
- package/skills/delphi-review/INSTALL.md +152 -0
- package/skills/delphi-review/SKILL.md +371 -0
- package/skills/delphi-review/evals/evals.json +82 -0
- package/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/skills/ralph-loop/SKILL.md +330 -0
- package/skills/ralph-loop/evals/evals.json +311 -0
- package/skills/ralph-loop/evolution-history.json +59 -0
- package/skills/ralph-loop/evolution-log.md +16 -0
- package/skills/ralph-loop/references/components/memory.md +55 -0
- package/skills/ralph-loop/references/components/middleware.md +54 -0
- package/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/skills/ralph-loop/templates/progress-log.md +36 -0
- package/skills/sprint-flow/SKILL.md +600 -0
- package/skills/sprint-flow/evals/evals.json +78 -0
- package/skills/sprint-flow/evolution-history.json +39 -0
- package/skills/sprint-flow/evolution-log.md +23 -0
- package/skills/sprint-flow/references/components/memory.md +87 -0
- package/skills/sprint-flow/references/components/middleware.md +72 -0
- package/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/skills/test-specification-alignment/AGENTS.md +59 -0
- package/skills/test-specification-alignment/SKILL.md +605 -0
- package/skills/test-specification-alignment/evals/evals.json +75 -0
- package/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- 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 };
|