@boyingliu01/xp-gate 0.8.2 → 0.8.4
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/bin/xp-gate.js +111 -129
- package/hooks/pre-commit +5 -4
- package/lib/__tests__/baseline.test.js +123 -0
- package/lib/__tests__/migrate.test.js +20 -0
- package/lib/__tests__/uninstall.test.js +36 -0
- package/lib/__tests__/update-skill.test.js +11 -0
- package/lib/baseline.js +392 -0
- package/lib/doctor.js +127 -141
- package/lib/gate-audit.ts +25 -8
- package/lib/init.js +13 -2
- package/lib/migrate.js +74 -73
- package/lib/uninstall.js +63 -31
- package/lib/update-skill.js +32 -34
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +31 -0
- package/plugins/opencode/package.json +28 -4
- package/plugins/opencode/scripts/prepack.cjs +85 -0
- package/plugins/opencode/skills/sprint-flow/SKILL.md +31 -0
- package/skills/sprint-flow/SKILL.md +31 -0
package/lib/baseline.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* baseline.js — `xp-gate baseline` CLI handler
|
|
3
|
+
*
|
|
4
|
+
* Manages per-project lint baselines stored in .xp-gate/lint-baseline.json.
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* xp-gate baseline create — Full-repo scan → save baseline
|
|
8
|
+
* xp-gate baseline show — Display current baseline
|
|
9
|
+
* xp-gate baseline reset — Force re-scan and replace baseline
|
|
10
|
+
* xp-gate baseline diff — Diff current state against stored baseline
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
function getXpGateDir() {
|
|
18
|
+
return path.join(process.cwd(), '.xp-gate');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getBaselineFile() {
|
|
22
|
+
return path.join(getXpGateDir(), 'lint-baseline.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ensureDir(dirPath) {
|
|
26
|
+
if (!fs.existsSync(dirPath)) {
|
|
27
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(subargs) {
|
|
32
|
+
return {
|
|
33
|
+
tool: subargs[0] || null,
|
|
34
|
+
rest: subargs.slice(1),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run a lint tool and return its JSON output.
|
|
40
|
+
*/
|
|
41
|
+
function runToolJson(toolCmd, pipeToJsonFlag) {
|
|
42
|
+
try {
|
|
43
|
+
const cmd = `${toolCmd} ${pipeToJsonFlag}`;
|
|
44
|
+
return execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }).trim();
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// Lint tools exit non-zero when they find issues - that's expected
|
|
47
|
+
if (e.stdout) return e.stdout.trim();
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Collect all source files for a given language.
|
|
54
|
+
*/
|
|
55
|
+
function findFilesByExt(extensions) {
|
|
56
|
+
try {
|
|
57
|
+
const patterns = extensions.map(e => `"**/*${e}"`).join(' ');
|
|
58
|
+
const result = execSync(`git ls-files ${patterns}`, {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
61
|
+
}).trim();
|
|
62
|
+
return result ? result.split('\n') : [];
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create baseline: scan all source files with available lint tools.
|
|
70
|
+
* Called by: xp-gate baseline create|reset
|
|
71
|
+
*/
|
|
72
|
+
async function createBaseline() {
|
|
73
|
+
ensureDir(getXpGateDir());
|
|
74
|
+
|
|
75
|
+
const baseline = {};
|
|
76
|
+
|
|
77
|
+
// TypeScript/JavaScript — ESLint
|
|
78
|
+
const tsFiles = findFilesByExt(['.ts', '.tsx', '.js', '.jsx']);
|
|
79
|
+
if (tsFiles.length > 0) {
|
|
80
|
+
try {
|
|
81
|
+
const eslintConfigExists = fs.existsSync('.eslintrc.json') ||
|
|
82
|
+
fs.existsSync('.eslintrc.js') ||
|
|
83
|
+
fs.existsSync('.eslintrc.cjs') ||
|
|
84
|
+
fs.existsSync('eslint.config.js');
|
|
85
|
+
const eslintBin = fs.existsSync('node_modules/.bin/eslint') ? 'node_modules/.bin/eslint' : null;
|
|
86
|
+
|
|
87
|
+
if (eslintConfigExists && (eslintBin || execSync('npx eslint --version', { encoding: 'utf8' }))) {
|
|
88
|
+
const eslintCmd = eslintBin || 'npx eslint';
|
|
89
|
+
const output = runToolJson(`${eslintCmd} ${tsFiles.join(' ')}`, '-f json --no-warn-ignored');
|
|
90
|
+
if (output && output !== '[]') {
|
|
91
|
+
const parsed = JSON.parse(output);
|
|
92
|
+
for (const fileResult of parsed) {
|
|
93
|
+
const relativePath = fileResult.filePath;
|
|
94
|
+
const fileName = tsFiles.find(f => relativePath.endsWith(f));
|
|
95
|
+
if (!fileName) continue;
|
|
96
|
+
|
|
97
|
+
baseline[fileName] = {
|
|
98
|
+
eslint: {
|
|
99
|
+
warnings: fileResult.warningCount || 0,
|
|
100
|
+
errors: fileResult.errorCount || 0,
|
|
101
|
+
},
|
|
102
|
+
totalWarnings: fileResult.warningCount || 0,
|
|
103
|
+
lastAnalyzed: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// ESLint unavailable — skip
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Python — ruff
|
|
114
|
+
const pyFiles = findFilesByExt(['.py']);
|
|
115
|
+
if (pyFiles.length > 0) {
|
|
116
|
+
try {
|
|
117
|
+
execSync('ruff --version', { encoding: 'utf8' });
|
|
118
|
+
const output = runToolJson(`ruff check ${pyFiles.join(' ')}`, '--output-format json');
|
|
119
|
+
if (output && output !== '[]') {
|
|
120
|
+
const parsed = JSON.parse(output);
|
|
121
|
+
for (const fileResult of parsed) {
|
|
122
|
+
if (!baseline[fileResult.file]) {
|
|
123
|
+
const messages = fileResult.messages || [];
|
|
124
|
+
baseline[fileResult.file] = {
|
|
125
|
+
ruff: { warnings: messages.length, errors: 0 },
|
|
126
|
+
totalWarnings: messages.length,
|
|
127
|
+
lastAnalyzed: new Date().toISOString(),
|
|
128
|
+
};
|
|
129
|
+
} else {
|
|
130
|
+
const messages = fileResult.messages || [];
|
|
131
|
+
baseline[fileResult.file].ruff = { warnings: messages.length, errors: 0 };
|
|
132
|
+
baseline[fileResult.file].totalWarnings += messages.length;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Ruff unavailable — skip
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Go — golangci-lint
|
|
142
|
+
const goFiles = findFilesByExt(['.go']);
|
|
143
|
+
if (goFiles.length > 0) {
|
|
144
|
+
try {
|
|
145
|
+
execSync('golangci-lint --version', { encoding: 'utf8' });
|
|
146
|
+
const output = runToolJson('golangci-lint run', '--out-format json');
|
|
147
|
+
if (output) {
|
|
148
|
+
const parsed = JSON.parse(output).Issues || [];
|
|
149
|
+
const warningsByFile = {};
|
|
150
|
+
for (const issue of parsed) {
|
|
151
|
+
const matchedFile = goFiles.find(f => issue.file.endsWith(f));
|
|
152
|
+
if (!matchedFile) continue;
|
|
153
|
+
if (!warningsByFile[matchedFile]) {
|
|
154
|
+
warningsByFile[matchedFile] = { warnings: 0, errors: 0 };
|
|
155
|
+
}
|
|
156
|
+
if (issue.severity === 'error') {
|
|
157
|
+
warningsByFile[matchedFile].errors++;
|
|
158
|
+
} else {
|
|
159
|
+
warningsByFile[matchedFile].warnings++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const [file, counts] of Object.entries(warningsByFile)) {
|
|
163
|
+
if (baseline[file]) {
|
|
164
|
+
baseline[file].golangci = counts;
|
|
165
|
+
baseline[file].totalWarnings += counts.warnings;
|
|
166
|
+
} else {
|
|
167
|
+
baseline[file] = {
|
|
168
|
+
golangci: counts,
|
|
169
|
+
totalWarnings: counts.warnings,
|
|
170
|
+
lastAnalyzed: new Date().toISOString(),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// golangci-lint unavailable — skip
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Shell — shellcheck
|
|
181
|
+
const shFiles = findFilesByExt(['.sh']);
|
|
182
|
+
if (shFiles.length > 0) {
|
|
183
|
+
try {
|
|
184
|
+
execSync('shellcheck --version', { encoding: 'utf8' });
|
|
185
|
+
const output = runToolJson(`shellcheck -f json ${shFiles.join(' ')}`, '');
|
|
186
|
+
if (output && output !== '[]') {
|
|
187
|
+
const parsed = JSON.parse(output);
|
|
188
|
+
const warningsByFile = {};
|
|
189
|
+
for (const item of parsed) {
|
|
190
|
+
const matchedFile = shFiles.find(f => item.file.endsWith(f));
|
|
191
|
+
if (!matchedFile) continue;
|
|
192
|
+
if (!warningsByFile[matchedFile]) {
|
|
193
|
+
warningsByFile[matchedFile] = { warnings: 0, errors: 0 };
|
|
194
|
+
}
|
|
195
|
+
if (item.level === 'error') {
|
|
196
|
+
warningsByFile[matchedFile].errors++;
|
|
197
|
+
} else {
|
|
198
|
+
warningsByFile[matchedFile].warnings++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const [file, counts] of Object.entries(warningsByFile)) {
|
|
202
|
+
if (baseline[file]) {
|
|
203
|
+
baseline[file].shellcheck = counts;
|
|
204
|
+
baseline[file].totalWarnings += counts.warnings;
|
|
205
|
+
} else {
|
|
206
|
+
baseline[file] = {
|
|
207
|
+
shellcheck: counts,
|
|
208
|
+
totalWarnings: counts.warnings,
|
|
209
|
+
lastAnalyzed: new Date().toISOString(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// shellcheck unavailable — skip
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fs.writeFileSync(getBaselineFile(), JSON.stringify(baseline, null, 2));
|
|
220
|
+
return baseline;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Show current baseline.
|
|
225
|
+
*/
|
|
226
|
+
function showBaseline() {
|
|
227
|
+
if (!fs.existsSync(getBaselineFile())) {
|
|
228
|
+
console.log('No baseline found. Run `xp-gate baseline create` to initialize.');
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const data = JSON.parse(fs.readFileSync(getBaselineFile(), 'utf8'));
|
|
233
|
+
const files = Object.keys(data);
|
|
234
|
+
|
|
235
|
+
if (files.length === 0) {
|
|
236
|
+
console.log('Baseline is empty (no lint issues found on last scan).');
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const totalWarnings = files.reduce((s, f) => s + data[f].totalWarnings, 0);
|
|
241
|
+
let eslintCount = 0;
|
|
242
|
+
let ruffCount = 0;
|
|
243
|
+
let golangciCount = 0;
|
|
244
|
+
let shellcheckCount = 0;
|
|
245
|
+
|
|
246
|
+
for (const entry of Object.values(data)) {
|
|
247
|
+
if (entry.eslint) eslintCount++;
|
|
248
|
+
if (entry.ruff) ruffCount++;
|
|
249
|
+
if (entry.golangci) golangciCount++;
|
|
250
|
+
if (entry.shellcheck) shellcheckCount++;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log('Lint Baseline:');
|
|
254
|
+
console.log(` Created: ${data[files[0]]?.lastAnalyzed?.slice(0, 10) || 'N/A'}`);
|
|
255
|
+
console.log(` Files tracked: ${files.length}`);
|
|
256
|
+
console.log(` Total warnings: ${totalWarnings}`);
|
|
257
|
+
if (eslintCount > 0) console.log(` ESLint: ${eslintCount} files`);
|
|
258
|
+
if (ruffCount > 0) console.log(` Ruff: ${ruffCount} files`);
|
|
259
|
+
if (golangciCount > 0) console.log(` golangci-lint: ${golangciCount} files`);
|
|
260
|
+
if (shellcheckCount > 0) console.log(` ShellCheck: ${shellcheckCount} files`);
|
|
261
|
+
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Reset baseline: force re-scan and replace.
|
|
267
|
+
*/
|
|
268
|
+
async function resetBaseline() {
|
|
269
|
+
const baseline = await createBaseline();
|
|
270
|
+
console.log(`Baseline reset. ${Object.keys(baseline).length} files tracked.`);
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Diff current lint state against stored baseline.
|
|
276
|
+
*/
|
|
277
|
+
async function diffBaseline() {
|
|
278
|
+
if (!fs.existsSync(getBaselineFile())) {
|
|
279
|
+
console.log('No baseline found. Run `xp-gate baseline create` first.');
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const oldBaseline = JSON.parse(fs.readFileSync(getBaselineFile(), 'utf8'));
|
|
284
|
+
const currentBaseline = await createBaseline();
|
|
285
|
+
|
|
286
|
+
const allFiles = new Set([
|
|
287
|
+
...Object.keys(oldBaseline),
|
|
288
|
+
...Object.keys(currentBaseline),
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
let totalWarningsDelta = 0;
|
|
292
|
+
const increased = [];
|
|
293
|
+
const decreased = [];
|
|
294
|
+
const added = [];
|
|
295
|
+
const removed = [];
|
|
296
|
+
|
|
297
|
+
for (const file of allFiles) {
|
|
298
|
+
const oldW = oldBaseline[file]?.totalWarnings || 0;
|
|
299
|
+
const newW = currentBaseline[file]?.totalWarnings || 0;
|
|
300
|
+
const delta = newW - oldW;
|
|
301
|
+
|
|
302
|
+
totalWarningsDelta += delta;
|
|
303
|
+
|
|
304
|
+
if (!oldBaseline[file] && currentBaseline[file]) {
|
|
305
|
+
added.push(` + ${file} (${newW} warnings)`);
|
|
306
|
+
} else if (oldBaseline[file] && !currentBaseline[file]) {
|
|
307
|
+
removed.push(` - ${file} (all ${oldW} warnings cleared)`);
|
|
308
|
+
} else if (delta > 0) {
|
|
309
|
+
increased.push(` ↑ ${file} (${oldW} → ${newW}, +${delta})`);
|
|
310
|
+
} else if (delta < 0) {
|
|
311
|
+
decreased.push(` ↓ ${file} (${oldW} → ${newW}, ${delta})`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log('Lint Baseline Diff:');
|
|
316
|
+
console.log(` Total warnings delta: ${totalWarningsDelta >= 0 ? '+' : ''}${totalWarningsDelta}`);
|
|
317
|
+
|
|
318
|
+
if (added.length > 0) {
|
|
319
|
+
console.log(`\nFiles added (${added.length}):`);
|
|
320
|
+
added.forEach(l => console.log(l));
|
|
321
|
+
}
|
|
322
|
+
if (removed.length > 0) {
|
|
323
|
+
console.log(`\nFiles removed (${removed.length}):`);
|
|
324
|
+
removed.forEach(l => console.log(l));
|
|
325
|
+
}
|
|
326
|
+
if (increased.length > 0) {
|
|
327
|
+
console.log(`\nWarnings increased (${increased.length}):`);
|
|
328
|
+
increased.forEach(l => console.log(l));
|
|
329
|
+
}
|
|
330
|
+
if (decreased.length > 0) {
|
|
331
|
+
console.log(`\nWarnings decreased (${decreased.length}):`);
|
|
332
|
+
decreased.forEach(l => console.log(l));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (added.length === 0 && removed.length === 0 && increased.length === 0 && decreased.length === 0) {
|
|
336
|
+
console.log(' No change from baseline.');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return totalWarningsDelta > 0 ? 1 : 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Main handler called from xp-gate CLI.
|
|
344
|
+
* @param {string[]} subargs CLI sub-arguments
|
|
345
|
+
* @returns {Promise<number>} exit code
|
|
346
|
+
*/
|
|
347
|
+
async function handleBaseline(subargs) {
|
|
348
|
+
const { tool, rest } = parseArgs(subargs);
|
|
349
|
+
|
|
350
|
+
switch (tool) {
|
|
351
|
+
case 'create':
|
|
352
|
+
console.log('Creating lint baseline...');
|
|
353
|
+
const created = await createBaseline();
|
|
354
|
+
console.log(`✅ Baseline created — ${Object.keys(created).length} files tracked.`);
|
|
355
|
+
return 0;
|
|
356
|
+
|
|
357
|
+
case 'show':
|
|
358
|
+
return showBaseline();
|
|
359
|
+
|
|
360
|
+
case 'reset':
|
|
361
|
+
console.log('Resetting lint baseline...');
|
|
362
|
+
return resetBaseline();
|
|
363
|
+
|
|
364
|
+
case 'diff':
|
|
365
|
+
return diffBaseline();
|
|
366
|
+
|
|
367
|
+
case 'help':
|
|
368
|
+
case '--help':
|
|
369
|
+
case null:
|
|
370
|
+
console.log(`Usage: xp-gate baseline <subcommand>
|
|
371
|
+
|
|
372
|
+
Subcommands:
|
|
373
|
+
create Scan all source files and save lint baseline
|
|
374
|
+
show Display current lint baseline summary
|
|
375
|
+
reset Force re-scan and replace baseline
|
|
376
|
+
diff Compare current lint state against baseline
|
|
377
|
+
|
|
378
|
+
Examples:
|
|
379
|
+
xp-gate baseline create
|
|
380
|
+
xp-gate baseline show
|
|
381
|
+
xp-gate baseline reset
|
|
382
|
+
xp-gate baseline diff`);
|
|
383
|
+
return 0;
|
|
384
|
+
|
|
385
|
+
default:
|
|
386
|
+
console.error(`Unknown baseline subcommand: ${tool}`);
|
|
387
|
+
console.error('Run `xp-gate baseline --help` for usage.');
|
|
388
|
+
return 1;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = { handleBaseline, createBaseline, showBaseline, resetBaseline, diffBaseline };
|