@boyingliu01/xp-gate 0.8.2 → 0.8.5

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/lib/doctor.js CHANGED
@@ -87,114 +87,120 @@ function checkEnv(checks) {
87
87
  }
88
88
 
89
89
  /**
90
- * Build check report for the doctor.
91
- * Returns { checks: Array<{name, status, detail}>, issues: number }
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 diagnose() {
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.push({ name: 'Config file', status: 'FAIL', detail: 'Not found' });
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.push({ name: 'Config file', status: 'FAIL', detail: 'Corrupt JSON' });
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
- checks.push({ name: 'Install mode', status: 'FAIL', detail: `Unknown: ${config.mode}` });
116
- issues++;
117
- return { checks, issues };
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
- checks.push({ name: 'Install mode', status: 'PASS', detail: config.mode });
121
-
122
- // --- Check 2: Hooks files ---
123
- if (config.mode === 'local') {
124
- const gitDir = getGitDir();
125
- if (!gitDir) {
126
- checks.push({ name: 'Git repository', status: 'FAIL', detail: 'Not in a git repo' });
127
- issues++;
128
- } else {
129
- const hooksDir = path.join(gitDir, 'hooks');
130
- const preCommit = path.join(hooksDir, 'pre-commit');
131
- const prePush = path.join(hooksDir, 'pre-push');
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
- if (!fs.existsSync(prePush) || !isXpGateFile(prePush, SIGNATURES['pre-push'])) {
141
- checks.push({ name: 'Hooks: pre-push', status: 'FAIL', detail: 'Missing or not xp-gate' });
142
- issues++;
143
- } else {
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
- if (!fs.existsSync(prePush) || !isXpGateFile(prePush, SIGNATURES['pre-push'])) {
160
- checks.push({ name: 'Global hooks: pre-push', status: 'FAIL', detail: 'Missing or not xp-gate' });
161
- issues++;
162
- } else {
163
- checks.push({ name: 'Global hooks: pre-push', status: 'PASS', detail: prePush });
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
- // --- Check 4: core.hooksPath (global mode only) ---
167
- const hooksPath = getCurrentHooksPath();
168
- if (hooksPath === null || hooksPath === '') {
169
- checks.push({ name: 'Git core.hooksPath', status: 'FAIL', detail: 'Not set' });
170
- issues++;
171
- } else if (hooksPath !== GLOBAL_HOOKS_DIR) {
172
- checks.push({ name: 'Git core.hooksPath', status: 'FAIL', detail: `Expected ${GLOBAL_HOOKS_DIR}, got ${hooksPath}` });
173
- issues++;
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
- // --- Check 3: Adapters directory ---
180
- const adaptersDir = config.mode === 'local'
181
- ? path.join(path.dirname(getGitDir() || ''), 'githooks', 'adapters')
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
- issues++;
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
- const adapterFiles = fs.readdirSync(adaptersDir).filter(f => f.endsWith('.sh'));
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 5: Environment dependencies ---
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
- const preCommit = path.join(hooksDir, 'pre-commit');
235
- const prePush = path.join(hooksDir, 'pre-push');
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
- // Fix core.hooksPath
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 && config !== 'corrupt' && config.mode === 'uninstalled') {
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 && config && config !== 'corrupt' && (config.mode === 'local' || config.mode === 'global')) {
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
- if (fixMode && config && config !== 'corrupt' && (config.mode === 'local' || config.mode === 'global')) {
359
- // Re-run diagnosis after fix to report updated status
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
- * Compute per-gate aggregate statistics.
120
+ * Read and parse all valid audit entries from the log file.
121
121
  */
122
- export function computeStats(
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
- // Aggregate by gate_id
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 # current project');
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
- return installLocal(args);
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
- 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
-
19
+ function cleanNpmrc({ npmrcPath, dryRun }) {
31
20
  if (!fs.existsSync(npmrcPath)) {
32
21
  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
- }
22
+ return { changed: false, removedCount: 0 };
23
+ }
49
24
 
50
- npmrcRemovedCount = linesToRemove.length;
25
+ const content = fs.readFileSync(npmrcPath, 'utf8');
26
+ const lines = content.split('\n');
27
+ const linesToRemove = [];
28
+ const keptLines = [];
51
29
 
52
- if (npmrcRemovedCount === 0) {
53
- console.log(' No GitHub Packages lines found — ~/.npmrc is clean.');
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
- 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
- }
35
+ keptLines.push(line);
73
36
  }
74
37
  }
75
38
 
76
- // === Phase 2: Cache check ===
77
- console.log('');
78
- console.log('Checking ~/.config/xp-gate/cache/ for old downloads...');
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
- } else {
83
- const items = fs.readdirSync(cacheDir);
65
+ return;
66
+ }
84
67
 
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
- }
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
- // === Phase 3: Summary ===
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: ${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'}`);
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