@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/doctor.js
CHANGED
|
@@ -87,114 +87,120 @@ function checkEnv(checks) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
* Returns { checks: Array
|
|
90
|
+
* Validate config file exists, is parseable, and has a known mode.
|
|
91
|
+
* Returns { config: object|null, checks: Array, issues: number }
|
|
92
|
+
* When config is unrecoverable, returns null to stop further checks.
|
|
92
93
|
*/
|
|
93
|
-
function
|
|
94
|
-
const checks = [];
|
|
95
|
-
let issues = 0;
|
|
94
|
+
function checkConfig() {
|
|
96
95
|
const config = getConfig();
|
|
97
|
-
|
|
98
|
-
// --- Check 1: Config file ---
|
|
99
96
|
if (config === null) {
|
|
100
|
-
checks
|
|
101
|
-
issues++;
|
|
102
|
-
return { checks, issues }; // Cannot proceed without config
|
|
97
|
+
return { config: null, checks: [{ name: 'Config file', status: 'FAIL', detail: 'Not found' }], issues: 1 };
|
|
103
98
|
}
|
|
104
|
-
|
|
105
99
|
if (config === 'corrupt') {
|
|
106
|
-
checks
|
|
107
|
-
issues++;
|
|
108
|
-
return { checks, issues }; // Cannot proceed with corrupt config
|
|
100
|
+
return { config: null, checks: [{ name: 'Config file', status: 'FAIL', detail: 'Corrupt JSON' }], issues: 1 };
|
|
109
101
|
}
|
|
110
|
-
|
|
111
|
-
checks.push({ name: 'Config file', status: 'PASS', detail: CONFIG_FILE });
|
|
112
|
-
|
|
113
|
-
// Check mode
|
|
114
102
|
if (config.mode !== 'local' && config.mode !== 'global') {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
103
|
+
return { config: null, checks: [
|
|
104
|
+
{ name: 'Config file', status: 'PASS', detail: CONFIG_FILE },
|
|
105
|
+
{ name: 'Install mode', status: 'FAIL', detail: `Unknown: ${config.mode}` }
|
|
106
|
+
], issues: 1 };
|
|
118
107
|
}
|
|
108
|
+
return { config, checks: [
|
|
109
|
+
{ name: 'Config file', status: 'PASS', detail: CONFIG_FILE },
|
|
110
|
+
{ name: 'Install mode', status: 'PASS', detail: config.mode }
|
|
111
|
+
], issues: 0 };
|
|
112
|
+
}
|
|
119
113
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!fs.existsSync(preCommit) || !isXpGateFile(preCommit, SIGNATURES['pre-commit'])) {
|
|
134
|
-
checks.push({ name: 'Hooks: pre-commit', status: 'FAIL', detail: 'Missing or not xp-gate' });
|
|
135
|
-
issues++;
|
|
136
|
-
} else {
|
|
137
|
-
checks.push({ name: 'Hooks: pre-commit', status: 'PASS', detail: preCommit });
|
|
138
|
-
}
|
|
114
|
+
function checkLocalHooks(checks) {
|
|
115
|
+
let issues = 0;
|
|
116
|
+
const gitDir = getGitDir();
|
|
117
|
+
if (!gitDir) {
|
|
118
|
+
checks.push({ name: 'Git repository', status: 'FAIL', detail: 'Not in a git repo' });
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
122
|
+
issues += checkSingleHook(hooksDir, 'pre-commit', SIGNATURES['pre-commit'], 'Hooks', checks);
|
|
123
|
+
issues += checkSingleHook(hooksDir, 'pre-push', SIGNATURES['pre-push'], 'Hooks', checks);
|
|
124
|
+
return issues;
|
|
125
|
+
}
|
|
139
126
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
checks.push({ name: 'Hooks: pre-push', status: 'PASS', detail: prePush });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
} else if (config.mode === 'global') {
|
|
148
|
-
// Check global hooks directory
|
|
149
|
-
const preCommit = path.join(GLOBAL_HOOKS_DIR, 'pre-commit');
|
|
150
|
-
const prePush = path.join(GLOBAL_HOOKS_DIR, 'pre-push');
|
|
151
|
-
|
|
152
|
-
if (!fs.existsSync(preCommit) || !isXpGateFile(preCommit, SIGNATURES['pre-commit'])) {
|
|
153
|
-
checks.push({ name: 'Global hooks: pre-commit', status: 'FAIL', detail: 'Missing or not xp-gate' });
|
|
154
|
-
issues++;
|
|
155
|
-
} else {
|
|
156
|
-
checks.push({ name: 'Global hooks: pre-commit', status: 'PASS', detail: preCommit });
|
|
157
|
-
}
|
|
127
|
+
function checkGlobalHooks(checks) {
|
|
128
|
+
let issues = 0;
|
|
129
|
+
issues += checkSingleHook(GLOBAL_HOOKS_DIR, 'pre-commit', SIGNATURES['pre-commit'], 'Global hooks', checks);
|
|
130
|
+
issues += checkSingleHook(GLOBAL_HOOKS_DIR, 'pre-push', SIGNATURES['pre-push'], 'Global hooks', checks);
|
|
158
131
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
132
|
+
const hooksPath = getCurrentHooksPath();
|
|
133
|
+
if (hooksPath === null || hooksPath === '') {
|
|
134
|
+
checks.push({ name: 'Git core.hooksPath', status: 'FAIL', detail: 'Not set' });
|
|
135
|
+
issues++;
|
|
136
|
+
} else if (hooksPath !== GLOBAL_HOOKS_DIR) {
|
|
137
|
+
checks.push({ name: 'Git core.hooksPath', status: 'FAIL', detail: `Expected ${GLOBAL_HOOKS_DIR}, got ${hooksPath}` });
|
|
138
|
+
issues++;
|
|
139
|
+
} else {
|
|
140
|
+
checks.push({ name: 'Git core.hooksPath', status: 'PASS', detail: GLOBAL_HOOKS_DIR });
|
|
141
|
+
}
|
|
142
|
+
return issues;
|
|
143
|
+
}
|
|
165
144
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
} else {
|
|
175
|
-
checks.push({ name: 'Git core.hooksPath', status: 'PASS', detail: GLOBAL_HOOKS_DIR });
|
|
176
|
-
}
|
|
145
|
+
/**
|
|
146
|
+
* Check a single hook file exists and is an xp-gate file.
|
|
147
|
+
*/
|
|
148
|
+
function checkSingleHook(hooksDir, name, signature, label, checks) {
|
|
149
|
+
const hookPath = path.join(hooksDir, name);
|
|
150
|
+
if (!fs.existsSync(hookPath) || !isXpGateFile(hookPath, signature)) {
|
|
151
|
+
checks.push({ name: `${label}: ${name}`, status: 'FAIL', detail: 'Missing or not xp-gate' });
|
|
152
|
+
return 1;
|
|
177
153
|
}
|
|
154
|
+
checks.push({ name: `${label}: ${name}`, status: 'PASS', detail: hookPath });
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
178
157
|
|
|
179
|
-
|
|
180
|
-
const adaptersDir =
|
|
181
|
-
? path.join(path.dirname(
|
|
158
|
+
function checkAdapters(checks, mode, gitDir) {
|
|
159
|
+
const adaptersDir = mode === 'local'
|
|
160
|
+
? path.join(path.dirname(gitDir || ''), 'githooks', 'adapters')
|
|
182
161
|
: GLOBAL_ADAPTERS_DIR;
|
|
183
162
|
|
|
184
163
|
if (!adaptersDir || !fs.existsSync(adaptersDir)) {
|
|
185
164
|
checks.push({ name: 'Adapters directory', status: 'FAIL', detail: 'Missing' });
|
|
186
|
-
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
const adapterFiles = fs.readdirSync(adaptersDir).filter(f => f.endsWith('.sh'));
|
|
168
|
+
if (adapterFiles.length === 0) {
|
|
169
|
+
checks.push({ name: 'Adapters directory', status: 'FAIL', detail: 'Empty directory' });
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
checks.push({ name: 'Adapters directory', status: 'PASS', detail: `${adapterFiles.length} adapter(s)` });
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Build check report for the doctor.
|
|
178
|
+
* Returns { checks: Array<{name, status, detail}>, issues: number }
|
|
179
|
+
*/
|
|
180
|
+
function diagnose() {
|
|
181
|
+
const checks = [];
|
|
182
|
+
let issues = 0;
|
|
183
|
+
|
|
184
|
+
// --- Check 1: Config file ---
|
|
185
|
+
const configResult = checkConfig();
|
|
186
|
+
if (configResult.config === null) {
|
|
187
|
+
return { checks: configResult.checks, issues: configResult.issues };
|
|
188
|
+
}
|
|
189
|
+
const { config } = configResult;
|
|
190
|
+
checks.push(...configResult.checks);
|
|
191
|
+
issues += configResult.issues;
|
|
192
|
+
|
|
193
|
+
// --- Check 2: Hooks files ---
|
|
194
|
+
if (config.mode === 'local') {
|
|
195
|
+
issues += checkLocalHooks(checks);
|
|
187
196
|
} else {
|
|
188
|
-
|
|
189
|
-
if (adapterFiles.length === 0) {
|
|
190
|
-
checks.push({ name: 'Adapters directory', status: 'FAIL', detail: 'Empty directory' });
|
|
191
|
-
issues++;
|
|
192
|
-
} else {
|
|
193
|
-
checks.push({ name: 'Adapters directory', status: 'PASS', detail: `${adapterFiles.length} adapter(s)` });
|
|
194
|
-
}
|
|
197
|
+
issues += checkGlobalHooks(checks);
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
// --- Check
|
|
200
|
+
// --- Check 3: Adapters directory ---
|
|
201
|
+
issues += checkAdapters(checks, config.mode, getGitDir());
|
|
202
|
+
|
|
203
|
+
// --- Check 4: Environment dependencies ---
|
|
198
204
|
checkEnv(checks);
|
|
199
205
|
|
|
200
206
|
return { checks, issues };
|
|
@@ -214,6 +220,19 @@ function printReport(checks) {
|
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Copy a hook file from package source to target, creating parent dir if needed.
|
|
225
|
+
*/
|
|
226
|
+
function restoreHook(srcFile, destFile, label) {
|
|
227
|
+
if (!fs.existsSync(srcFile)) return false;
|
|
228
|
+
const destDir = path.dirname(destFile);
|
|
229
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
230
|
+
fs.copyFileSync(srcFile, destFile);
|
|
231
|
+
fs.chmodSync(destFile, 0o755);
|
|
232
|
+
console.log(` ✓ Restored ${label}`);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
217
236
|
/**
|
|
218
237
|
* Attempt to fix known issues.
|
|
219
238
|
* Only operates when mode === 'active' (local or global).
|
|
@@ -231,58 +250,17 @@ function fixIssues(checks, config) {
|
|
|
231
250
|
const gitDir = getGitDir();
|
|
232
251
|
if (gitDir) {
|
|
233
252
|
const hooksDir = path.join(gitDir, 'hooks');
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (!fs.existsSync(preCommit) || !isXpGateFile(preCommit, SIGNATURES['pre-commit'])) {
|
|
238
|
-
const src = path.join(srcDir, 'hooks', 'pre-commit');
|
|
239
|
-
if (fs.existsSync(src)) {
|
|
240
|
-
fs.mkdirSync(hooksDir, { recursive: true });
|
|
241
|
-
fs.copyFileSync(src, preCommit);
|
|
242
|
-
fs.chmodSync(preCommit, 0o755);
|
|
243
|
-
console.log(' ✓ Restored pre-commit hook');
|
|
244
|
-
fixed = true;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (!fs.existsSync(prePush) || !isXpGateFile(prePush, SIGNATURES['pre-push'])) {
|
|
249
|
-
const src = path.join(srcDir, 'hooks', 'pre-push');
|
|
250
|
-
if (fs.existsSync(src)) {
|
|
251
|
-
fs.mkdirSync(hooksDir, { recursive: true });
|
|
252
|
-
fs.copyFileSync(src, prePush);
|
|
253
|
-
fs.chmodSync(prePush, 0o755);
|
|
254
|
-
console.log(' ✓ Restored pre-push hook');
|
|
255
|
-
fixed = true;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
} else if (config.mode === 'global') {
|
|
260
|
-
const preCommit = path.join(GLOBAL_HOOKS_DIR, 'pre-commit');
|
|
261
|
-
const prePush = path.join(GLOBAL_HOOKS_DIR, 'pre-push');
|
|
262
|
-
|
|
263
|
-
if (!fs.existsSync(preCommit) || !isXpGateFile(preCommit, SIGNATURES['pre-commit'])) {
|
|
264
|
-
const src = path.join(srcDir, 'hooks', 'pre-commit');
|
|
265
|
-
if (fs.existsSync(src)) {
|
|
266
|
-
fs.mkdirSync(GLOBAL_HOOKS_DIR, { recursive: true });
|
|
267
|
-
fs.copyFileSync(src, preCommit);
|
|
268
|
-
fs.chmodSync(preCommit, 0o755);
|
|
269
|
-
console.log(' ✓ Restored global pre-commit hook');
|
|
270
|
-
fixed = true;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (!fs.existsSync(prePush) || !isXpGateFile(prePush, SIGNATURES['pre-push'])) {
|
|
275
|
-
const src = path.join(srcDir, 'hooks', 'pre-push');
|
|
276
|
-
if (fs.existsSync(src)) {
|
|
277
|
-
fs.mkdirSync(GLOBAL_HOOKS_DIR, { recursive: true });
|
|
278
|
-
fs.copyFileSync(src, prePush);
|
|
279
|
-
fs.chmodSync(prePush, 0o755);
|
|
280
|
-
console.log(' ✓ Restored global pre-push hook');
|
|
281
|
-
fixed = true;
|
|
282
|
-
}
|
|
253
|
+
fixed = restoreHook(path.join(srcDir, 'hooks', 'pre-commit'), path.join(hooksDir, 'pre-commit'), 'pre-commit hook') || fixed;
|
|
254
|
+
fixed = restoreHook(path.join(srcDir, 'hooks', 'pre-push'), path.join(hooksDir, 'pre-push'), 'pre-push hook') || fixed;
|
|
283
255
|
}
|
|
256
|
+
} else {
|
|
257
|
+
const hooksDir = GLOBAL_HOOKS_DIR;
|
|
258
|
+
fixed = restoreHook(path.join(srcDir, 'hooks', 'pre-commit'), path.join(hooksDir, 'pre-commit'), 'global pre-commit hook') || fixed;
|
|
259
|
+
fixed = restoreHook(path.join(srcDir, 'hooks', 'pre-push'), path.join(hooksDir, 'pre-push'), 'global pre-push hook') || fixed;
|
|
260
|
+
}
|
|
284
261
|
|
|
285
|
-
|
|
262
|
+
// Fix core.hooksPath (global mode only)
|
|
263
|
+
if (config.mode === 'global') {
|
|
286
264
|
const hooksPath = getCurrentHooksPath();
|
|
287
265
|
if (hooksPath !== GLOBAL_HOOKS_DIR) {
|
|
288
266
|
try {
|
|
@@ -324,6 +302,14 @@ function fixIssues(checks, config) {
|
|
|
324
302
|
* @param {string[]} args CLI arguments
|
|
325
303
|
* @returns {number} exit code (0 = all clear, 1 = issues found)
|
|
326
304
|
*/
|
|
305
|
+
function isActiveMode(config) {
|
|
306
|
+
return config && config !== 'corrupt' && (config.mode === 'local' || config.mode === 'global');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isUninstalledMode(config) {
|
|
310
|
+
return config && config !== 'corrupt' && config.mode === 'uninstalled';
|
|
311
|
+
}
|
|
312
|
+
|
|
327
313
|
async function doctor(args) {
|
|
328
314
|
const fixMode = args.includes('--fix');
|
|
329
315
|
|
|
@@ -333,14 +319,14 @@ async function doctor(args) {
|
|
|
333
319
|
const config = getConfig();
|
|
334
320
|
|
|
335
321
|
// §4.8: mode === "uninstalled" → print "xp-gate is not installed"
|
|
336
|
-
if (config
|
|
322
|
+
if (isUninstalledMode(config)) {
|
|
337
323
|
console.log('xp-gate is not installed.');
|
|
338
324
|
console.log('Run xp-gate init to install.');
|
|
339
325
|
return 0;
|
|
340
326
|
}
|
|
341
327
|
|
|
342
328
|
// §4.13: --fix only when mode === "active"
|
|
343
|
-
if (fixMode &&
|
|
329
|
+
if (fixMode && isActiveMode(config)) {
|
|
344
330
|
fixIssues(null, config);
|
|
345
331
|
}
|
|
346
332
|
|
|
@@ -355,8 +341,8 @@ async function doctor(args) {
|
|
|
355
341
|
|
|
356
342
|
console.log(`\n✗ ${issues} issue(s) found`);
|
|
357
343
|
|
|
358
|
-
|
|
359
|
-
|
|
344
|
+
// Re-run diagnosis after fix to report updated status
|
|
345
|
+
if (fixMode && isActiveMode(config)) {
|
|
360
346
|
console.log('\nRe-running diagnosis after fix...');
|
|
361
347
|
const { checks: postChecks } = diagnose();
|
|
362
348
|
printReport(postChecks);
|
package/lib/gate-audit.ts
CHANGED
|
@@ -117,12 +117,9 @@ export function readTailEntries(
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
*
|
|
120
|
+
* Read and parse all valid audit entries from the log file.
|
|
121
121
|
*/
|
|
122
|
-
|
|
123
|
-
repoRoot: string = process.cwd(),
|
|
124
|
-
): { gate_id: string; pass_pct: string; avg_ms: number; avg_issues: number }[] {
|
|
125
|
-
const logPath = join(repoRoot, AUDIT_DIR, AUDIT_FILE);
|
|
122
|
+
function parseAuditEntries(logPath: string): GateAuditEntry[] {
|
|
126
123
|
if (!existsSync(logPath)) {
|
|
127
124
|
return [];
|
|
128
125
|
}
|
|
@@ -132,7 +129,6 @@ export function computeStats(
|
|
|
132
129
|
return [];
|
|
133
130
|
}
|
|
134
131
|
|
|
135
|
-
// Parse all valid entries
|
|
136
132
|
const entries: GateAuditEntry[] = [];
|
|
137
133
|
for (const line of content.split('\n')) {
|
|
138
134
|
if (line.trim()) {
|
|
@@ -143,8 +139,13 @@ export function computeStats(
|
|
|
143
139
|
}
|
|
144
140
|
}
|
|
145
141
|
}
|
|
142
|
+
return entries;
|
|
143
|
+
}
|
|
146
144
|
|
|
147
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Aggregate audit entries by gate_id, computing sums for passed/ms/issues.
|
|
147
|
+
*/
|
|
148
|
+
function aggregateByGate(entries: GateAuditEntry[]): Map<string, { total: number; passed: number; ms: number; issues: number }> {
|
|
148
149
|
const buckets = new Map<string, { total: number; passed: number; ms: number; issues: number }>();
|
|
149
150
|
|
|
150
151
|
for (const e of entries) {
|
|
@@ -156,6 +157,23 @@ export function computeStats(
|
|
|
156
157
|
buckets.set(e.gate_id, b);
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
return buckets;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Compute per-gate aggregate statistics.
|
|
165
|
+
*/
|
|
166
|
+
export function computeStats(
|
|
167
|
+
repoRoot: string = process.cwd(),
|
|
168
|
+
): { gate_id: string; pass_pct: string; avg_ms: number; avg_issues: number }[] {
|
|
169
|
+
const logPath = join(repoRoot, AUDIT_DIR, AUDIT_FILE);
|
|
170
|
+
const entries = parseAuditEntries(logPath);
|
|
171
|
+
if (entries.length === 0) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const buckets = aggregateByGate(entries);
|
|
176
|
+
|
|
159
177
|
const results: { gate_id: string; pass_pct: string; avg_ms: number; avg_issues: number }[] = [];
|
|
160
178
|
|
|
161
179
|
for (const [gate_id, b] of Array.from(buckets.entries())) {
|
|
@@ -167,7 +185,6 @@ export function computeStats(
|
|
|
167
185
|
});
|
|
168
186
|
}
|
|
169
187
|
|
|
170
|
-
// Sort by gate_id for stable output
|
|
171
188
|
results.sort((a, b) => a.gate_id.localeCompare(b.gate_id));
|
|
172
189
|
return results;
|
|
173
190
|
}
|
package/lib/init.js
CHANGED
|
@@ -86,7 +86,7 @@ function printUsage() {
|
|
|
86
86
|
console.log(' 2) Local — install hooks into current project only\n');
|
|
87
87
|
console.log('Usage:');
|
|
88
88
|
console.log(' xp-gate init --global # all projects');
|
|
89
|
-
console.log(' xp-gate init
|
|
89
|
+
console.log(' xp-gate init --baseline # current project + create lint baseline');
|
|
90
90
|
console.log(' xp-gate setup-global # all projects (alias)\n');
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -265,7 +265,18 @@ async function init(args) {
|
|
|
265
265
|
|
|
266
266
|
if (!installMode) { printUsage(); return 0; }
|
|
267
267
|
if (installMode === 'global') return setupGlobal(args);
|
|
268
|
-
|
|
268
|
+
const code = await installLocal(args);
|
|
269
|
+
if (code === 0 && args.includes('--baseline')) {
|
|
270
|
+
try {
|
|
271
|
+
const { createBaseline } = require('./baseline.js');
|
|
272
|
+
console.log('\nCreating lint baseline...');
|
|
273
|
+
const baseline = await createBaseline();
|
|
274
|
+
console.log(`✅ Lint baseline created — ${Object.keys(baseline).length} files tracked.`);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.log(`ℹ️ Lint baseline creation skipped: ${e.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return code;
|
|
269
280
|
}
|
|
270
281
|
|
|
271
282
|
async function installLocal(args) {
|
package/lib/migrate.js
CHANGED
|
@@ -16,98 +16,99 @@ const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
|
16
16
|
* @param {string[]} args - CLI arguments (--dry-run supported)
|
|
17
17
|
* @returns {Promise<number>} exit code (0 = success)
|
|
18
18
|
*/
|
|
19
|
-
|
|
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
|
-
|
|
19
|
+
function cleanNpmrc({ npmrcPath, dryRun }) {
|
|
31
20
|
if (!fs.existsSync(npmrcPath)) {
|
|
32
21
|
console.log(' No ~/.npmrc found — nothing to clean.');
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
}
|
|
22
|
+
return { changed: false, removedCount: 0 };
|
|
23
|
+
}
|
|
49
24
|
|
|
50
|
-
|
|
25
|
+
const content = fs.readFileSync(npmrcPath, 'utf8');
|
|
26
|
+
const lines = content.split('\n');
|
|
27
|
+
const linesToRemove = [];
|
|
28
|
+
const keptLines = [];
|
|
51
29
|
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
for (let i = 0; i < lines.length; i++) {
|
|
31
|
+
const line = lines[i];
|
|
32
|
+
if (line.includes('npm.pkg.github.com')) {
|
|
33
|
+
linesToRemove.push(line);
|
|
54
34
|
} else {
|
|
55
|
-
|
|
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
|
-
}
|
|
35
|
+
keptLines.push(line);
|
|
73
36
|
}
|
|
74
37
|
}
|
|
75
38
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
39
|
+
if (linesToRemove.length === 0) {
|
|
40
|
+
console.log(' No GitHub Packages lines found — ~/.npmrc is clean.');
|
|
41
|
+
return { changed: false, removedCount: 0 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(` Found ${linesToRemove.length} npm.pkg.github.com line(s):`);
|
|
45
|
+
for (const line of linesToRemove) {
|
|
46
|
+
const masked = line.replace(/(:_authToken=).+/, '$1***');
|
|
47
|
+
console.log(` - ${masked}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(' [Dry-run] No changes made. Would remove the above line(s).');
|
|
53
|
+
return { changed: false, removedCount: linesToRemove.length };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const newContent = keptLines.join('\n');
|
|
57
|
+
fs.writeFileSync(npmrcPath, newContent, 'utf8');
|
|
58
|
+
console.log(' Cleaned successfully.');
|
|
59
|
+
return { changed: true, removedCount: linesToRemove.length };
|
|
60
|
+
}
|
|
79
61
|
|
|
62
|
+
function checkCacheDir({ cacheDir, dryRun }) {
|
|
80
63
|
if (!fs.existsSync(cacheDir)) {
|
|
81
64
|
console.log(' No old cache directory found.');
|
|
82
|
-
|
|
83
|
-
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
84
67
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
}
|
|
68
|
+
const items = fs.readdirSync(cacheDir);
|
|
69
|
+
if (items.length === 0) {
|
|
70
|
+
console.log(' Cache directory exists but is empty.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(` Found ${items.length} cached file(s) from old installation.`);
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
const itemPath = path.join(cacheDir, item);
|
|
77
|
+
const stat = fs.statSync(itemPath);
|
|
78
|
+
const size = stat.isFile() ? `(${formatSize(stat.size)})` : '(directory)';
|
|
79
|
+
console.log(` - ${item} ${size}`);
|
|
101
80
|
}
|
|
102
81
|
|
|
103
|
-
|
|
82
|
+
if (!dryRun) {
|
|
83
|
+
console.log(' Note: These files are harmless but no longer needed.');
|
|
84
|
+
console.log(' You can safely remove them with: rm -rf ' + cacheDir);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function printSummary(removedCount, cacheDir, wasDryRun) {
|
|
89
|
+
const hasCacheItems = fs.existsSync(cacheDir) && fs.readdirSync(cacheDir).length > 0;
|
|
90
|
+
const removedLabel = removedCount > 0 ? `${removedCount} GitHub Packages line(s) ${wasDryRun ? 'would be ' : ''}removed` : 'No changes needed';
|
|
104
91
|
console.log('');
|
|
105
92
|
console.log('Migration Summary:');
|
|
106
|
-
console.log(` ~/.npmrc: ${
|
|
107
|
-
console.log(` Cache: ${
|
|
93
|
+
console.log(` ~/.npmrc: ${removedLabel}`);
|
|
94
|
+
console.log(` Cache: ${hasCacheItems ? 'Old files found (can be cleaned manually)' : 'No old cache found'}`);
|
|
108
95
|
console.log('');
|
|
109
96
|
console.log('Migration complete. xp-gate v0.5.x no longer requires GitHub Packages or PAT tokens.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function migrate(args = []) {
|
|
100
|
+
const options = { dryRun: args.includes('--dry-run') };
|
|
101
|
+
const npmrcPath = path.join(HOME, '.npmrc');
|
|
102
|
+
const cacheDir = path.join(HOME, '.config', 'xp-gate', 'cache');
|
|
103
|
+
|
|
104
|
+
console.log('Checking ~/.npmrc for GitHub Packages residue...');
|
|
105
|
+
const { removedCount } = cleanNpmrc({ npmrcPath, dryRun: options.dryRun });
|
|
106
|
+
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log('Checking ~/.config/xp-gate/cache/ for old downloads...');
|
|
109
|
+
checkCacheDir({ cacheDir, dryRun: options.dryRun });
|
|
110
110
|
|
|
111
|
+
printSummary(removedCount, cacheDir, options.dryRun);
|
|
111
112
|
return 0;
|
|
112
113
|
}
|
|
113
114
|
|