fuzzrunx 0.1.7 → 0.2.0

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/src/cli.js CHANGED
@@ -1,205 +1,284 @@
1
- #!/usr/bin/env node
2
- // FuzzRun: minimal auto-correct runner for mistyped commands/subcommands.
3
- // Runs the command once; if it fails, tries a high-confidence fix (edit distance 1 or CLI suggestion)
4
- // and re-runs automatically.
5
-
1
+ #!/usr/bin/env node
2
+ // FuzzRun: minimal auto-correct runner for mistyped commands/subcommands.
3
+ // Runs the command once; if it fails, tries a high-confidence fix (edit distance 1 or CLI suggestion)
4
+ // and re-runs automatically.
5
+
6
6
  const { spawnSync } = require('child_process');
7
7
  const fs = require('fs');
8
8
  const os = require('os');
9
9
  const path = require('path');
10
10
  const installer = require('./installer');
11
-
12
- const MAX_DISTANCE = Number.isFinite(Number(process.env.FUZZRUN_MAX_DISTANCE))
13
- ? Math.max(1, Number(process.env.FUZZRUN_MAX_DISTANCE))
14
- : 1;
15
-
16
- const DEFAULT_PRIORITY_BASES = [
17
- 'git',
18
- 'npm',
19
- 'yarn',
20
- 'pnpm',
21
- 'node',
22
- 'python',
23
- 'python3',
24
- 'pip',
25
- 'pip3',
26
- 'docker',
27
- 'kubectl',
28
- 'gh',
29
- 'go',
30
- 'cargo',
31
- 'dotnet',
32
- 'java',
33
- 'mvn',
34
- 'gradle'
35
- ];
36
- const ENV_PRIORITY_BASES = (process.env.FUZZRUN_PREFER_BASES || '')
37
- .split(',')
38
- .map((value) => normalizeToken(value).trim())
39
- .filter(Boolean);
40
- const PRIORITY_BASES = new Set([...DEFAULT_PRIORITY_BASES, ...ENV_PRIORITY_BASES]);
41
-
42
- const DANGEROUS_BASE = new Set(['rm', 'mv', 'dd', 'shutdown', 'reboot', 'halt', 'poweroff']);
43
-
44
- const COMMON_SUBCOMMANDS = {
45
- git: [
46
- 'add',
47
- 'bisect',
48
- 'branch',
49
- 'checkout',
50
- 'clone',
51
- 'commit',
52
- 'diff',
53
- 'fetch',
54
- 'init',
55
- 'log',
56
- 'merge',
57
- 'mv',
58
- 'pull',
59
- 'push',
60
- 'rebase',
61
- 'revert',
62
- 'rm',
63
- 'show',
64
- 'stash',
65
- 'status',
66
- 'switch',
67
- 'tag'
68
- ],
69
- npm: [
70
- 'install',
71
- 'init',
72
- 'run',
73
- 'test',
74
- 'publish',
75
- 'link',
76
- 'login',
77
- 'logout',
78
- 'ci',
79
- 'config',
80
- 'cache',
81
- 'start',
82
- 'stop',
83
- 'restart',
84
- 'update',
85
- 'outdated',
86
- 'list',
87
- 'prune',
88
- 'exec',
89
- 'root',
90
- 'pack',
91
- 'uninstall'
92
- ],
93
- yarn: [
94
- 'add',
95
- 'install',
96
- 'remove',
97
- 'run',
98
- 'test',
99
- 'init',
100
- 'upgrade',
101
- 'global',
102
- 'dlx',
103
- 'config',
104
- 'list'
105
- ],
106
- pnpm: [
107
- 'add',
108
- 'install',
109
- 'update',
110
- 'remove',
111
- 'run',
112
- 'exec',
113
- 'list',
114
- 'publish',
115
- 'install-test',
116
- 'fetch'
117
- ],
118
- pip: ['install', 'uninstall', 'list', 'freeze', 'show', 'search', 'cache', 'config'],
119
- docker: [
120
- 'build',
121
- 'commit',
122
- 'compose',
123
- 'cp',
124
- 'create',
125
- 'diff',
126
- 'events',
127
- 'exec',
128
- 'images',
129
- 'info',
130
- 'inspect',
131
- 'kill',
132
- 'load',
133
- 'logs',
134
- 'pause',
135
- 'port',
136
- 'ps',
137
- 'pull',
138
- 'push',
139
- 'rename',
140
- 'restart',
141
- 'rm',
142
- 'rmi',
143
- 'run',
144
- 'save',
145
- 'start',
146
- 'stats',
147
- 'stop',
148
- 'tag',
149
- 'top',
150
- 'unpause',
151
- 'update',
152
- 'version'
153
- ],
154
- kubectl: [
155
- 'apply',
156
- 'get',
157
- 'describe',
158
- 'delete',
159
- 'logs',
160
- 'exec',
161
- 'create',
162
- 'edit',
163
- 'explain',
164
- 'expose',
165
- 'port-forward',
166
- 'top',
167
- 'cp',
168
- 'scale',
169
- 'rollout',
170
- 'set',
171
- 'explain',
172
- 'label',
173
- 'annotate',
174
- 'cordon',
175
- 'drain',
176
- 'uncordon'
177
- ],
178
- gh: ['auth', 'repo', 'issue', 'pr', 'gist', 'alias', 'api', 'search', 'run', 'workflow', 'status', 'label']
179
- };
180
-
181
- const SAFE_SUBCOMMAND_BASES = new Set(Object.keys(COMMON_SUBCOMMANDS));
182
- const ALLOW_ANY_SUBCOMMANDS = process.env.FUZZRUN_ALLOW_ANY_SUBCOMMANDS === '1';
183
- const SCRIPT_BASES = new Set(['npm', 'yarn', 'pnpm']);
184
- const RISKY_ARG_PATTERNS = [
185
- /^-f$/,
186
- /^-rf$/,
187
- /^-fr$/,
188
- /^--force$/i,
189
- /^--hard$/i,
190
- /^--delete$/i,
191
- /^--purge$/i,
192
- /^--no-preserve-root$/i
193
- ];
194
- const SCRIPT_ERROR_PATTERNS = [
195
- /missing script/i,
196
- /unknown script/i,
197
- /script.*not found/i,
198
- /couldn'?t find.*script/i,
199
- /command ".*" not found/i
200
- ];
201
- const GIT_PATHSPEC_PATTERN = /pathspec .* did not match/i;
202
-
11
+
12
+ const MAX_DISTANCE = Number.isFinite(Number(process.env.FUZZRUN_MAX_DISTANCE))
13
+ ? Math.max(1, Number(process.env.FUZZRUN_MAX_DISTANCE))
14
+ : 1;
15
+
16
+ // --- Output styling (zero-dependency ANSI) -------------------------------
17
+ const USE_COLOR =
18
+ !process.env.NO_COLOR &&
19
+ process.env.FUZZRUN_NO_COLOR !== '1' &&
20
+ (process.stderr.isTTY || process.env.FUZZRUN_FORCE_COLOR === '1');
21
+
22
+ function paint(code, text) {
23
+ return USE_COLOR ? `\x1b[${code}m${text}\x1b[0m` : String(text);
24
+ }
25
+ const dim = (t) => paint('2', t);
26
+ const bold = (t) => paint('1', t);
27
+ const green = (t) => paint('32', t);
28
+ const cyan = (t) => paint('36', t);
29
+ const yellow = (t) => paint('33', t);
30
+ const BOLT = USE_COLOR ? '' : '';
31
+
32
+ // --- Runtime modes -------------------------------------------------------
33
+ // Dry-run: detect and report the fix but never run the corrected command.
34
+ let DRY_RUN = process.env.FUZZRUN_DRY_RUN === '1';
35
+
36
+ const DEFAULT_PRIORITY_BASES = [
37
+ 'git',
38
+ 'npm',
39
+ 'yarn',
40
+ 'pnpm',
41
+ 'node',
42
+ 'python',
43
+ 'python3',
44
+ 'pip',
45
+ 'pip3',
46
+ 'docker',
47
+ 'kubectl',
48
+ 'gh',
49
+ 'go',
50
+ 'cargo',
51
+ 'dotnet',
52
+ 'java',
53
+ 'mvn',
54
+ 'gradle'
55
+ ];
56
+ const ENV_PRIORITY_BASES = (process.env.FUZZRUN_PREFER_BASES || '')
57
+ .split(',')
58
+ .map((value) => normalizeToken(value).trim())
59
+ .filter(Boolean);
60
+ const PRIORITY_BASES = new Set([...DEFAULT_PRIORITY_BASES, ...ENV_PRIORITY_BASES]);
61
+
62
+ const DANGEROUS_BASE = new Set(['rm', 'mv', 'dd', 'shutdown', 'reboot', 'halt', 'poweroff']);
63
+
64
+ const COMMON_SUBCOMMANDS = {
65
+ git: [
66
+ 'add',
67
+ 'bisect',
68
+ 'branch',
69
+ 'checkout',
70
+ 'clone',
71
+ 'commit',
72
+ 'diff',
73
+ 'fetch',
74
+ 'init',
75
+ 'log',
76
+ 'merge',
77
+ 'mv',
78
+ 'pull',
79
+ 'push',
80
+ 'rebase',
81
+ 'revert',
82
+ 'rm',
83
+ 'show',
84
+ 'stash',
85
+ 'status',
86
+ 'switch',
87
+ 'tag'
88
+ ],
89
+ npm: [
90
+ 'install',
91
+ 'init',
92
+ 'run',
93
+ 'test',
94
+ 'publish',
95
+ 'link',
96
+ 'login',
97
+ 'logout',
98
+ 'ci',
99
+ 'config',
100
+ 'cache',
101
+ 'start',
102
+ 'stop',
103
+ 'restart',
104
+ 'update',
105
+ 'outdated',
106
+ 'list',
107
+ 'prune',
108
+ 'exec',
109
+ 'root',
110
+ 'pack',
111
+ 'uninstall'
112
+ ],
113
+ yarn: [
114
+ 'add',
115
+ 'install',
116
+ 'remove',
117
+ 'run',
118
+ 'test',
119
+ 'init',
120
+ 'upgrade',
121
+ 'global',
122
+ 'dlx',
123
+ 'config',
124
+ 'list'
125
+ ],
126
+ pnpm: [
127
+ 'add',
128
+ 'install',
129
+ 'update',
130
+ 'remove',
131
+ 'run',
132
+ 'exec',
133
+ 'list',
134
+ 'publish',
135
+ 'install-test',
136
+ 'fetch'
137
+ ],
138
+ pip: ['install', 'uninstall', 'list', 'freeze', 'show', 'search', 'cache', 'config'],
139
+ docker: [
140
+ 'build',
141
+ 'commit',
142
+ 'compose',
143
+ 'cp',
144
+ 'create',
145
+ 'diff',
146
+ 'events',
147
+ 'exec',
148
+ 'images',
149
+ 'info',
150
+ 'inspect',
151
+ 'kill',
152
+ 'load',
153
+ 'logs',
154
+ 'pause',
155
+ 'port',
156
+ 'ps',
157
+ 'pull',
158
+ 'push',
159
+ 'rename',
160
+ 'restart',
161
+ 'rm',
162
+ 'rmi',
163
+ 'run',
164
+ 'save',
165
+ 'start',
166
+ 'stats',
167
+ 'stop',
168
+ 'tag',
169
+ 'top',
170
+ 'unpause',
171
+ 'update',
172
+ 'version'
173
+ ],
174
+ kubectl: [
175
+ 'apply',
176
+ 'get',
177
+ 'describe',
178
+ 'delete',
179
+ 'logs',
180
+ 'exec',
181
+ 'create',
182
+ 'edit',
183
+ 'explain',
184
+ 'expose',
185
+ 'port-forward',
186
+ 'top',
187
+ 'cp',
188
+ 'scale',
189
+ 'rollout',
190
+ 'set',
191
+ 'explain',
192
+ 'label',
193
+ 'annotate',
194
+ 'cordon',
195
+ 'drain',
196
+ 'uncordon'
197
+ ],
198
+ gh: ['auth', 'repo', 'issue', 'pr', 'gist', 'alias', 'api', 'search', 'run', 'workflow', 'status', 'label']
199
+ };
200
+
201
+ // Common long flags per base, used for fuzzy flag correction. Dangerous flags
202
+ // are intentionally omitted so we never auto-correct *into* a destructive flag.
203
+ const COMMON_FLAGS = {
204
+ git: [
205
+ '--message',
206
+ '--all',
207
+ '--amend',
208
+ '--branch',
209
+ '--verbose',
210
+ '--quiet',
211
+ '--patch',
212
+ '--set-upstream',
213
+ '--no-edit',
214
+ '--global',
215
+ '--list',
216
+ '--oneline',
217
+ '--graph',
218
+ '--staged',
219
+ '--cached'
220
+ ],
221
+ npm: [
222
+ '--save',
223
+ '--save-dev',
224
+ '--save-exact',
225
+ '--global',
226
+ '--production',
227
+ '--legacy-peer-deps',
228
+ '--workspace',
229
+ '--workspaces',
230
+ '--package-lock',
231
+ '--prefix'
232
+ ],
233
+ docker: [
234
+ '--detach',
235
+ '--interactive',
236
+ '--tty',
237
+ '--name',
238
+ '--volume',
239
+ '--publish',
240
+ '--env',
241
+ '--file',
242
+ '--network',
243
+ '--restart',
244
+ '--workdir'
245
+ ],
246
+ kubectl: ['--namespace', '--output', '--selector', '--filename', '--context', '--all-namespaces'],
247
+ gh: ['--repo', '--web', '--title', '--body', '--label', '--assignee', '--milestone']
248
+ };
249
+
250
+ const SAFE_SUBCOMMAND_BASES = new Set(Object.keys(COMMON_SUBCOMMANDS));
251
+ const SAFE_FLAG_BASES = new Set(Object.keys(COMMON_FLAGS));
252
+ const ALLOW_ANY_SUBCOMMANDS = process.env.FUZZRUN_ALLOW_ANY_SUBCOMMANDS === '1';
253
+ const SCRIPT_BASES = new Set(['npm', 'yarn', 'pnpm']);
254
+ const RISKY_ARG_PATTERNS = [
255
+ /^-f$/,
256
+ /^-rf$/,
257
+ /^-fr$/,
258
+ /^--force$/i,
259
+ /^--hard$/i,
260
+ /^--delete$/i,
261
+ /^--purge$/i,
262
+ /^--no-preserve-root$/i
263
+ ];
264
+ const SCRIPT_ERROR_PATTERNS = [
265
+ /missing script/i,
266
+ /unknown script/i,
267
+ /script.*not found/i,
268
+ /couldn'?t find.*script/i,
269
+ /command ".*" not found/i
270
+ ];
271
+ const FLAG_ERROR_PATTERNS = [
272
+ /unknown option/i,
273
+ /unrecognized option/i,
274
+ /invalid option/i,
275
+ /unknown flag/i,
276
+ /unknown switch/i,
277
+ /did you mean/i,
278
+ /no such option/i
279
+ ];
280
+ const GIT_PATHSPEC_PATTERN = /pathspec .* did not match/i;
281
+
203
282
  const suggestionPatterns = [
204
283
  /The most similar command is\s+([^\s]+)/i,
205
284
  /The most similar commands are:\s*\n\s*([^\s]+)/i,
@@ -238,7 +317,40 @@ function updateState(patch) {
238
317
  return next;
239
318
  }
240
319
 
320
+ // In-memory copy of persisted state, loaded once. Used for learning + stats.
321
+ const STATE = readState() || {};
322
+
323
+ // Record an accepted correction so we can learn typo->fix preferences and
324
+ // power `fuzzrun stats`.
325
+ function recordFix(kind, fromToken, toToken) {
326
+ try {
327
+ STATE.totalFixes = (STATE.totalFixes || 0) + 1;
328
+ STATE.byKind = STATE.byKind || {};
329
+ STATE.byKind[kind] = (STATE.byKind[kind] || 0) + 1;
330
+ STATE.history = STATE.history || {};
331
+ const key = normalizeToken(fromToken);
332
+ const entry = STATE.history[key] || { from: fromToken, to: toToken, count: 0 };
333
+ entry.to = toToken;
334
+ entry.count += 1;
335
+ entry.kind = kind;
336
+ entry.last = new Date().toISOString();
337
+ STATE.history[key] = entry;
338
+ STATE.firstFixAt = STATE.firstFixAt || entry.last;
339
+ writeState(STATE);
340
+ } catch (err) {
341
+ // Best-effort only.
342
+ }
343
+ }
344
+
345
+ // If the user has accepted a correction for this exact typo before, return the
346
+ // normalized target so we can break ambiguous ties in its favor.
347
+ function learnedChoiceFor(target) {
348
+ const entry = STATE.history && STATE.history[normalizeToken(target)];
349
+ return entry && entry.to ? normalizeToken(entry.to) : null;
350
+ }
351
+
241
352
  function showInstallBannerOnce() {
353
+ if (process.env.FUZZRUN_SKIP_ENABLE === '1') return;
242
354
  const state = readState() || {};
243
355
  if (state.bannerShown || state.disabled) return;
244
356
  const message =
@@ -256,252 +368,473 @@ function showInstallBannerOnce() {
256
368
  function normalizeToken(value) {
257
369
  return String(value || '').toLowerCase();
258
370
  }
259
-
260
- function damerauLevenshtein(a, b, maxDistance = 2) {
261
- const aNorm = normalizeToken(a);
262
- const bNorm = normalizeToken(b);
263
- if (aNorm === bNorm) return 0;
264
- if (Math.abs(aNorm.length - bNorm.length) > maxDistance) {
265
- return maxDistance + 1;
266
- }
267
- const dp = Array.from({ length: aNorm.length + 1 }, () => new Array(bNorm.length + 1).fill(0));
268
- for (let i = 0; i <= aNorm.length; i += 1) dp[i][0] = i;
269
- for (let j = 0; j <= bNorm.length; j += 1) dp[0][j] = j;
270
- for (let i = 1; i <= aNorm.length; i += 1) {
271
- let rowMin = maxDistance + 1;
272
- for (let j = 1; j <= bNorm.length; j += 1) {
273
- const cost = aNorm[i - 1] === bNorm[j - 1] ? 0 : 1;
274
- let value = Math.min(
275
- dp[i - 1][j] + 1,
276
- dp[i][j - 1] + 1,
277
- dp[i - 1][j - 1] + cost
278
- );
279
- if (i > 1 && j > 1 && aNorm[i - 1] === bNorm[j - 2] && aNorm[i - 2] === bNorm[j - 1]) {
280
- value = Math.min(value, dp[i - 2][j - 2] + 1);
281
- }
282
- dp[i][j] = value;
283
- if (value < rowMin) rowMin = value;
284
- }
285
- if (rowMin > maxDistance) {
286
- return maxDistance + 1;
287
- }
288
- }
289
- return dp[aNorm.length][bNorm.length];
290
- }
291
-
292
- function collectPathCommands() {
293
- const names = new Set();
294
- const pathEntries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
295
- const allowedExts = new Set(
296
- (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
297
- .toLowerCase()
298
- .split(';')
299
- .filter(Boolean)
300
- );
301
-
302
- for (const entry of pathEntries) {
303
- try {
304
- const items = fs.readdirSync(entry, { withFileTypes: true });
305
- for (const item of items) {
306
- if (item.isDirectory()) continue;
307
- if (process.platform === 'win32') {
308
- const ext = path.extname(item.name).toLowerCase();
309
- const base = path.basename(item.name, ext);
310
- if (!base) continue;
311
- if (ext && !allowedExts.has(ext)) continue;
312
- names.add(base);
313
- } else {
314
- names.add(item.name);
315
- }
316
- }
317
- } catch (err) {
318
- // Ignore unreadable PATH entries.
319
- }
320
- }
321
- return names;
322
- }
323
-
324
- const PATH_COMMANDS = collectPathCommands();
325
-
326
- function normalizePowerShellGetPrefix(command) {
327
- if (!command) return command;
328
- const lowered = normalizeToken(command);
329
- if (!lowered.startsWith('get-')) return command;
330
- if (PATH_COMMANDS.has(command) || PATH_COMMANDS.has(lowered)) return command;
331
- const stripped = command.slice(4);
332
- if (!stripped) return command;
333
- if (PATH_COMMANDS.has(stripped)) return stripped;
334
- const match = findBestMatch(PATH_COMMANDS, stripped, MAX_DISTANCE);
335
- if (match) return stripped;
336
- return command;
337
- }
338
-
339
- function findBestMatch(candidates, target, maxDistance = MAX_DISTANCE) {
340
- if (!candidates || !target) return null;
341
- let best = null;
342
- let bestDistance = maxDistance + 1;
343
- let ties = [];
344
- for (const candidate of candidates || []) {
345
- const dist = damerauLevenshtein(candidate, target, maxDistance);
346
- if (dist < bestDistance) {
347
- best = candidate;
348
- bestDistance = dist;
349
- ties = [candidate];
350
- } else if (dist === bestDistance) {
351
- ties.push(candidate);
352
- }
353
- }
354
- if (!best || bestDistance > maxDistance) return null;
355
- if (ties.length > 1) {
356
- const preferred = ties.filter((value) => PRIORITY_BASES.has(normalizeToken(value)));
357
- if (preferred.length === 1) {
358
- return { match: preferred[0], distance: bestDistance };
359
- }
360
- return null;
361
- }
362
- return { match: best, distance: bestDistance };
363
- }
364
-
365
- function parseSuggestion(text) {
366
- for (const pattern of suggestionPatterns) {
367
- const match = text.match(pattern);
368
- if (match && match[1]) {
369
- return match[1];
370
- }
371
- }
372
- return null;
373
- }
374
-
375
- function run(cmd, args) {
376
- const result = spawnSync(cmd, args, {
377
- encoding: 'utf8',
378
- stdio: ['inherit', 'pipe', 'pipe']
379
- });
380
- return {
381
- code: typeof result.status === 'number' ? result.status : result.error ? 1 : 0,
382
- stdout: result.stdout || '',
383
- stderr: result.stderr || '',
384
- error: result.error
385
- };
386
- }
387
-
388
- function logFix(from, to) {
389
- process.stderr.write(`fuzzrun: auto-correcting "${from}" -> "${to}"\n`);
390
- }
391
-
392
- function hasRiskyArgs(args) {
393
- return args.some((arg) => RISKY_ARG_PATTERNS.some((pattern) => pattern.test(arg)));
394
- }
395
-
396
- function tryBaseCorrection(command, args) {
397
- if (hasRiskyArgs(args)) return null;
398
- const suggestion = findBestMatch(PATH_COMMANDS, command, MAX_DISTANCE);
399
- if (suggestion && !DANGEROUS_BASE.has(suggestion.match) && suggestion.match !== command) {
400
- logFix(command, suggestion.match);
401
- return {
402
- command: suggestion.match,
403
- args,
404
- result: run(suggestion.match, args)
405
- };
406
- }
407
- return null;
408
- }
409
-
410
- function trySubcommandCorrection(command, args, combinedOutput) {
411
- if (!SAFE_SUBCOMMAND_BASES.has(command) && !ALLOW_ANY_SUBCOMMANDS) return null;
412
- if (!args.length) return null;
413
- const attemptedSub = args[0];
414
- if (attemptedSub.startsWith('-')) return null;
415
- if (hasRiskyArgs(args)) return null;
416
- const fromOutput = parseSuggestion(combinedOutput);
417
- const candidates = COMMON_SUBCOMMANDS[command] || [];
418
- const fromDict = findBestMatch(candidates, attemptedSub, MAX_DISTANCE);
419
- const outputDistance = fromOutput
420
- ? damerauLevenshtein(fromOutput, attemptedSub, MAX_DISTANCE)
421
- : MAX_DISTANCE + 1;
422
- const choice = outputDistance <= MAX_DISTANCE ? fromOutput : fromDict ? fromDict.match : null;
423
-
424
- if (choice && choice !== attemptedSub && damerauLevenshtein(choice, attemptedSub, MAX_DISTANCE) <= MAX_DISTANCE) {
425
- logFix(`${command} ${attemptedSub}`, `${command} ${choice}`);
426
- return run(command, [choice, ...args.slice(1)]);
427
- }
428
- return null;
429
- }
430
-
431
- function findPackageJson(startDir) {
432
- let current = startDir;
433
- while (current && current !== path.dirname(current)) {
434
- const candidate = path.join(current, 'package.json');
435
- if (fs.existsSync(candidate)) return candidate;
436
- current = path.dirname(current);
437
- }
438
- return null;
439
- }
440
-
441
- function getPackageScripts(cwd) {
442
- const pkgPath = findPackageJson(cwd);
443
- if (!pkgPath) return [];
444
- try {
445
- const raw = fs.readFileSync(pkgPath, 'utf8');
446
- const parsed = JSON.parse(raw);
447
- return Object.keys(parsed.scripts || {});
448
- } catch (err) {
449
- return [];
450
- }
451
- }
452
-
453
- function isScriptError(output) {
454
- return SCRIPT_ERROR_PATTERNS.some((pattern) => pattern.test(output));
455
- }
456
-
457
- function tryScriptCorrection(command, args, combinedOutput) {
458
- if (!SCRIPT_BASES.has(command)) return null;
459
- if (args.length < 2) return null;
460
- if (args[0] !== 'run') return null;
461
- const scriptName = args[1];
462
- if (!scriptName || scriptName.startsWith('-')) return null;
463
- if (!isScriptError(combinedOutput)) return null;
464
- if (hasRiskyArgs(args)) return null;
465
-
466
- const scripts = getPackageScripts(process.cwd());
467
- const match = findBestMatch(scripts, scriptName, MAX_DISTANCE);
468
- if (match) {
469
- logFix(`${command} run ${scriptName}`, `${command} run ${match.match}`);
470
- return run(command, ['run', match.match, ...args.slice(2)]);
471
- }
472
- return null;
473
- }
474
-
475
- function getGitBranches() {
476
- const result = spawnSync('git', ['branch', '--format=%(refname:short)'], { encoding: 'utf8' });
477
- if (result.status !== 0) return [];
478
- return (result.stdout || '')
479
- .split(/\r?\n/)
480
- .map((line) => line.trim())
481
- .filter(Boolean);
482
- }
483
-
484
- function tryGitBranchCorrection(command, args, combinedOutput) {
485
- if (command !== 'git') return null;
486
- if (args.length < 2) return null;
487
- const subcommand = args[0];
488
- if (subcommand !== 'checkout' && subcommand !== 'switch') return null;
489
- const branch = args[1];
490
- if (!branch || branch.startsWith('-')) return null;
491
- if (!GIT_PATHSPEC_PATTERN.test(combinedOutput)) return null;
492
- if (hasRiskyArgs(args)) return null;
493
-
494
- const branches = getGitBranches();
495
- const match = findBestMatch(branches, branch, MAX_DISTANCE);
496
- if (match) {
497
- logFix(`${command} ${subcommand} ${branch}`, `${command} ${subcommand} ${match.match}`);
498
- return run(command, [subcommand, match.match, ...args.slice(2)]);
499
- }
500
- return null;
501
- }
502
-
371
+
372
+ function damerauLevenshtein(a, b, maxDistance = 2) {
373
+ const aNorm = normalizeToken(a);
374
+ const bNorm = normalizeToken(b);
375
+ if (aNorm === bNorm) return 0;
376
+ if (Math.abs(aNorm.length - bNorm.length) > maxDistance) {
377
+ return maxDistance + 1;
378
+ }
379
+ const dp = Array.from({ length: aNorm.length + 1 }, () => new Array(bNorm.length + 1).fill(0));
380
+ for (let i = 0; i <= aNorm.length; i += 1) dp[i][0] = i;
381
+ for (let j = 0; j <= bNorm.length; j += 1) dp[0][j] = j;
382
+ for (let i = 1; i <= aNorm.length; i += 1) {
383
+ let rowMin = maxDistance + 1;
384
+ for (let j = 1; j <= bNorm.length; j += 1) {
385
+ const cost = aNorm[i - 1] === bNorm[j - 1] ? 0 : 1;
386
+ let value = Math.min(
387
+ dp[i - 1][j] + 1,
388
+ dp[i][j - 1] + 1,
389
+ dp[i - 1][j - 1] + cost
390
+ );
391
+ if (i > 1 && j > 1 && aNorm[i - 1] === bNorm[j - 2] && aNorm[i - 2] === bNorm[j - 1]) {
392
+ value = Math.min(value, dp[i - 2][j - 2] + 1);
393
+ }
394
+ dp[i][j] = value;
395
+ if (value < rowMin) rowMin = value;
396
+ }
397
+ if (rowMin > maxDistance) {
398
+ return maxDistance + 1;
399
+ }
400
+ }
401
+ return dp[aNorm.length][bNorm.length];
402
+ }
403
+
404
+ function collectPathCommands() {
405
+ const names = new Set();
406
+ const pathEntries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
407
+ const allowedExts = new Set(
408
+ (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
409
+ .toLowerCase()
410
+ .split(';')
411
+ .filter(Boolean)
412
+ );
413
+
414
+ for (const entry of pathEntries) {
415
+ try {
416
+ const items = fs.readdirSync(entry, { withFileTypes: true });
417
+ for (const item of items) {
418
+ if (item.isDirectory()) continue;
419
+ if (process.platform === 'win32') {
420
+ const ext = path.extname(item.name).toLowerCase();
421
+ const base = path.basename(item.name, ext);
422
+ if (!base) continue;
423
+ if (ext && !allowedExts.has(ext)) continue;
424
+ names.add(base);
425
+ } else {
426
+ names.add(item.name);
427
+ }
428
+ }
429
+ } catch (err) {
430
+ // Ignore unreadable PATH entries.
431
+ }
432
+ }
433
+ return names;
434
+ }
435
+
436
+ const PATH_COMMANDS = collectPathCommands();
437
+
438
+ function normalizePowerShellGetPrefix(command) {
439
+ if (!command) return command;
440
+ const lowered = normalizeToken(command);
441
+ if (!lowered.startsWith('get-')) return command;
442
+ if (PATH_COMMANDS.has(command) || PATH_COMMANDS.has(lowered)) return command;
443
+ const stripped = command.slice(4);
444
+ if (!stripped) return command;
445
+ if (PATH_COMMANDS.has(stripped)) return stripped;
446
+ const match = findBestMatch(PATH_COMMANDS, stripped, MAX_DISTANCE);
447
+ if (match) return stripped;
448
+ return command;
449
+ }
450
+
451
+ function findBestMatch(candidates, target, maxDistance = MAX_DISTANCE) {
452
+ if (!candidates || !target) return null;
453
+ let best = null;
454
+ let bestDistance = maxDistance + 1;
455
+ let ties = [];
456
+ for (const candidate of candidates || []) {
457
+ const dist = damerauLevenshtein(candidate, target, maxDistance);
458
+ if (dist < bestDistance) {
459
+ best = candidate;
460
+ bestDistance = dist;
461
+ ties = [candidate];
462
+ } else if (dist === bestDistance) {
463
+ ties.push(candidate);
464
+ }
465
+ }
466
+ if (!best || bestDistance > maxDistance) return null;
467
+ if (ties.length > 1) {
468
+ // Prefer a choice the user has accepted before for this exact typo.
469
+ const learned = learnedChoiceFor(target);
470
+ if (learned) {
471
+ const learnedHits = ties.filter((value) => normalizeToken(value) === learned);
472
+ if (learnedHits.length === 1) {
473
+ return { match: learnedHits[0], distance: bestDistance };
474
+ }
475
+ }
476
+ const preferred = ties.filter((value) => PRIORITY_BASES.has(normalizeToken(value)));
477
+ if (preferred.length === 1) {
478
+ return { match: preferred[0], distance: bestDistance };
479
+ }
480
+ return null;
481
+ }
482
+ return { match: best, distance: bestDistance };
483
+ }
484
+
485
+ function parseSuggestion(text) {
486
+ for (const pattern of suggestionPatterns) {
487
+ const match = text.match(pattern);
488
+ if (match && match[1]) {
489
+ return match[1];
490
+ }
491
+ }
492
+ return null;
493
+ }
494
+
495
+ // Resolve a bare command name to a real file on Windows using PATH + PATHEXT.
496
+ // Returns { file, ext } or null. On non-Windows we let the OS resolve it.
497
+ function resolveWindowsExecutable(cmd) {
498
+ if (process.platform !== 'win32' || !cmd) return null;
499
+ const exts = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
500
+ .split(';')
501
+ .map((e) => e.trim())
502
+ .filter(Boolean);
503
+ const hasExt = path.extname(cmd) !== '';
504
+ // Explicit path: trust it as-is, report its extension.
505
+ if (cmd.includes('\\') || cmd.includes('/')) {
506
+ try {
507
+ if (fs.statSync(cmd).isFile()) return { file: cmd, ext: path.extname(cmd).toLowerCase() };
508
+ } catch (err) {
509
+ // fall through
510
+ }
511
+ return null;
512
+ }
513
+ const dirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
514
+ for (const dir of dirs) {
515
+ if (hasExt) {
516
+ const candidate = path.join(dir, cmd);
517
+ try {
518
+ if (fs.statSync(candidate).isFile()) return { file: candidate, ext: path.extname(cmd).toLowerCase() };
519
+ } catch (err) {
520
+ // keep looking
521
+ }
522
+ } else {
523
+ for (const ext of exts) {
524
+ const candidate = path.join(dir, cmd + ext);
525
+ try {
526
+ if (fs.statSync(candidate).isFile()) return { file: candidate, ext: ext.toLowerCase() };
527
+ } catch (err) {
528
+ // keep looking
529
+ }
530
+ }
531
+ }
532
+ }
533
+ return null;
534
+ }
535
+
536
+ // Quote a single argument for a Windows command line (CRT argv rules) so args
537
+ // with spaces or quotes survive being passed through cmd.exe.
538
+ function winQuoteArg(arg) {
539
+ const s = String(arg);
540
+ if (s === '') return '""';
541
+ if (!/[\s"]/.test(s)) return s;
542
+ const escaped = s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, '$1$1');
543
+ return `"${escaped}"`;
544
+ }
545
+
546
+ function run(cmd, args) {
547
+ let spawnCmd = cmd;
548
+ let spawnArgs = args;
549
+ const options = { encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'] };
550
+
551
+ const resolved = resolveWindowsExecutable(cmd);
552
+ if (resolved && (resolved.ext === '.cmd' || resolved.ext === '.bat')) {
553
+ // Batch shims (npm/yarn/pnpm and friends) can only run via the command
554
+ // processor; build a quoted command line and let the shell execute it.
555
+ spawnCmd = [winQuoteArg(resolved.file), ...args.map(winQuoteArg)].join(' ');
556
+ spawnArgs = [];
557
+ options.shell = true;
558
+ } else if (resolved) {
559
+ // Spawn the real executable directly for perfect argument fidelity.
560
+ spawnCmd = resolved.file;
561
+ }
562
+
563
+ const result = spawnSync(spawnCmd, spawnArgs, options);
564
+ return {
565
+ code: typeof result.status === 'number' ? result.status : result.error ? 1 : 0,
566
+ stdout: result.stdout || '',
567
+ stderr: result.stderr || '',
568
+ error: result.error
569
+ };
570
+ }
571
+
572
+ // Ask a yes/no question synchronously on the controlling terminal. Defaults to
573
+ // "yes" when we cannot prompt (no TTY), so non-interactive use is unaffected.
574
+ function confirm(question) {
575
+ if (!process.stdin.isTTY) return true;
576
+ process.stderr.write(`${question} ${dim('[Y/n]')} `);
577
+ const buf = Buffer.alloc(16);
578
+ try {
579
+ const bytes = fs.readSync(0, buf, 0, buf.length, null);
580
+ const answer = buf.toString('utf8', 0, bytes).trim().toLowerCase();
581
+ return answer === '' || answer === 'y' || answer === 'yes';
582
+ } catch (err) {
583
+ return true;
584
+ }
585
+ }
586
+
587
+ // Confidence-tiered: distance-1 fixes auto-run; weaker (distance >= 2) fixes ask
588
+ // first. FUZZRUN_PROMPT=1 forces a prompt for every fix; FUZZRUN_YES=1 never asks.
589
+ function shouldPrompt(distance) {
590
+ if (DRY_RUN) return false;
591
+ if (process.env.FUZZRUN_YES === '1') return false;
592
+ if (process.env.FUZZRUN_PROMPT === '1') return true;
593
+ return typeof distance === 'number' && distance >= 2;
594
+ }
595
+
596
+ function announceFix(fromDisplay, toDisplay) {
597
+ const verb = DRY_RUN ? 'would correct' : 'auto-correcting';
598
+ const arrow = cyan('->');
599
+ process.stderr.write(
600
+ `${BOLT}${dim('fuzzrun:')} ${verb} ${dim(fromDisplay)} ${arrow} ${bold(green(toDisplay))}\n`
601
+ );
602
+ }
603
+
604
+ // Single chokepoint for applying a correction: announces it, honors dry-run and
605
+ // the confidence prompt, records the fix for learning/stats, then runs it.
606
+ function executeCorrection(command, args, meta) {
607
+ announceFix(meta.fromDisplay, meta.toDisplay);
608
+ if (DRY_RUN) {
609
+ return { code: 0, stdout: '', stderr: '', dryRun: true };
610
+ }
611
+ if (shouldPrompt(meta.distance) && !confirm('Run it?')) {
612
+ process.stderr.write(`${dim('fuzzrun: skipped.')}\n`);
613
+ return { code: 130, stdout: '', stderr: '', declined: true };
614
+ }
615
+ recordFix(meta.kind, meta.fromTok, meta.toTok);
616
+ return run(command, args);
617
+ }
618
+
619
+ function hasRiskyArgs(args) {
620
+ return args.some((arg) => RISKY_ARG_PATTERNS.some((pattern) => pattern.test(arg)));
621
+ }
622
+
623
+ function tryBaseCorrection(command, args) {
624
+ if (hasRiskyArgs(args)) return null;
625
+ const suggestion = findBestMatch(PATH_COMMANDS, command, MAX_DISTANCE);
626
+ if (suggestion && !DANGEROUS_BASE.has(suggestion.match) && suggestion.match !== command) {
627
+ const result = executeCorrection(suggestion.match, args, {
628
+ kind: 'base',
629
+ fromDisplay: command,
630
+ toDisplay: suggestion.match,
631
+ fromTok: command,
632
+ toTok: suggestion.match,
633
+ distance: suggestion.distance
634
+ });
635
+ return {
636
+ command: suggestion.match,
637
+ args,
638
+ result
639
+ };
640
+ }
641
+ return null;
642
+ }
643
+
644
+ function trySubcommandCorrection(command, args, combinedOutput) {
645
+ if (!SAFE_SUBCOMMAND_BASES.has(command) && !ALLOW_ANY_SUBCOMMANDS) return null;
646
+ if (!args.length) return null;
647
+ const attemptedSub = args[0];
648
+ if (attemptedSub.startsWith('-')) return null;
649
+ if (hasRiskyArgs(args)) return null;
650
+ const fromOutput = parseSuggestion(combinedOutput);
651
+ const candidates = COMMON_SUBCOMMANDS[command] || [];
652
+ const fromDict = findBestMatch(candidates, attemptedSub, MAX_DISTANCE);
653
+ const outputDistance = fromOutput
654
+ ? damerauLevenshtein(fromOutput, attemptedSub, MAX_DISTANCE)
655
+ : MAX_DISTANCE + 1;
656
+ const choice = outputDistance <= MAX_DISTANCE ? fromOutput : fromDict ? fromDict.match : null;
657
+ const distance = outputDistance <= MAX_DISTANCE ? outputDistance : fromDict ? fromDict.distance : undefined;
658
+
659
+ if (choice && choice !== attemptedSub && damerauLevenshtein(choice, attemptedSub, MAX_DISTANCE) <= MAX_DISTANCE) {
660
+ return executeCorrection(command, [choice, ...args.slice(1)], {
661
+ kind: 'subcommand',
662
+ fromDisplay: `${command} ${attemptedSub}`,
663
+ toDisplay: `${command} ${choice}`,
664
+ fromTok: attemptedSub,
665
+ toTok: choice,
666
+ distance
667
+ });
668
+ }
669
+ return null;
670
+ }
671
+
672
+ function isFlagError(output) {
673
+ return FLAG_ERROR_PATTERNS.some((pattern) => pattern.test(output));
674
+ }
675
+
676
+ // Correct a single mistyped long flag (e.g. `git commit --mesage` -> `--message`).
677
+ function tryFlagCorrection(command, args, combinedOutput) {
678
+ if (!SAFE_FLAG_BASES.has(command)) return null;
679
+ if (!isFlagError(combinedOutput)) return null;
680
+ if (hasRiskyArgs(args)) return null;
681
+ const candidates = (COMMON_FLAGS[command] || []).filter(
682
+ (flag) => !RISKY_ARG_PATTERNS.some((pattern) => pattern.test(flag))
683
+ );
684
+ for (let i = 0; i < args.length; i += 1) {
685
+ const arg = args[i];
686
+ if (!arg.startsWith('--')) continue;
687
+ const name = arg.split('=')[0];
688
+ if (candidates.includes(name)) continue;
689
+ if (RISKY_ARG_PATTERNS.some((pattern) => pattern.test(name))) continue;
690
+ const match = findBestMatch(candidates, name, MAX_DISTANCE);
691
+ if (match) {
692
+ const fixedArgs = args.slice();
693
+ fixedArgs[i] = arg.includes('=')
694
+ ? `${match.match}=${arg.slice(arg.indexOf('=') + 1)}`
695
+ : match.match;
696
+ return executeCorrection(command, fixedArgs, {
697
+ kind: 'flag',
698
+ fromDisplay: `${command} ${name}`,
699
+ toDisplay: `${command} ${match.match}`,
700
+ fromTok: name,
701
+ toTok: match.match,
702
+ distance: match.distance
703
+ });
704
+ }
705
+ }
706
+ return null;
707
+ }
708
+
709
+ function findPackageJson(startDir) {
710
+ let current = startDir;
711
+ while (current && current !== path.dirname(current)) {
712
+ const candidate = path.join(current, 'package.json');
713
+ if (fs.existsSync(candidate)) return candidate;
714
+ current = path.dirname(current);
715
+ }
716
+ return null;
717
+ }
718
+
719
+ function getPackageScripts(cwd) {
720
+ const pkgPath = findPackageJson(cwd);
721
+ if (!pkgPath) return [];
722
+ try {
723
+ const raw = fs.readFileSync(pkgPath, 'utf8');
724
+ const parsed = JSON.parse(raw);
725
+ return Object.keys(parsed.scripts || {});
726
+ } catch (err) {
727
+ return [];
728
+ }
729
+ }
730
+
731
+ function isScriptError(output) {
732
+ return SCRIPT_ERROR_PATTERNS.some((pattern) => pattern.test(output));
733
+ }
734
+
735
+ function tryScriptCorrection(command, args, combinedOutput) {
736
+ if (!SCRIPT_BASES.has(command)) return null;
737
+ if (args.length < 2) return null;
738
+ if (args[0] !== 'run') return null;
739
+ const scriptName = args[1];
740
+ if (!scriptName || scriptName.startsWith('-')) return null;
741
+ if (!isScriptError(combinedOutput)) return null;
742
+ if (hasRiskyArgs(args)) return null;
743
+
744
+ const scripts = getPackageScripts(process.cwd());
745
+ const match = findBestMatch(scripts, scriptName, MAX_DISTANCE);
746
+ if (match) {
747
+ return executeCorrection(command, ['run', match.match, ...args.slice(2)], {
748
+ kind: 'script',
749
+ fromDisplay: `${command} run ${scriptName}`,
750
+ toDisplay: `${command} run ${match.match}`,
751
+ fromTok: scriptName,
752
+ toTok: match.match,
753
+ distance: match.distance
754
+ });
755
+ }
756
+ return null;
757
+ }
758
+
759
+ function getGitBranches() {
760
+ const result = spawnSync('git', ['branch', '--format=%(refname:short)'], { encoding: 'utf8' });
761
+ if (result.status !== 0) return [];
762
+ return (result.stdout || '')
763
+ .split(/\r?\n/)
764
+ .map((line) => line.trim())
765
+ .filter(Boolean);
766
+ }
767
+
768
+ function tryGitBranchCorrection(command, args, combinedOutput) {
769
+ if (command !== 'git') return null;
770
+ if (args.length < 2) return null;
771
+ const subcommand = args[0];
772
+ if (subcommand !== 'checkout' && subcommand !== 'switch') return null;
773
+ const branch = args[1];
774
+ if (!branch || branch.startsWith('-')) return null;
775
+ if (!GIT_PATHSPEC_PATTERN.test(combinedOutput)) return null;
776
+ if (hasRiskyArgs(args)) return null;
777
+
778
+ const branches = getGitBranches();
779
+ const match = findBestMatch(branches, branch, MAX_DISTANCE);
780
+ if (match) {
781
+ return executeCorrection(command, [subcommand, match.match, ...args.slice(2)], {
782
+ kind: 'branch',
783
+ fromDisplay: `${command} ${subcommand} ${branch}`,
784
+ toDisplay: `${command} ${subcommand} ${match.match}`,
785
+ fromTok: branch,
786
+ toTok: match.match,
787
+ distance: match.distance
788
+ });
789
+ }
790
+ return null;
791
+ }
792
+
793
+ function formatDuration(fromIso) {
794
+ if (!fromIso) return null;
795
+ const start = Date.parse(fromIso);
796
+ if (Number.isNaN(start)) return null;
797
+ const days = Math.max(0, Math.floor((Date.now() - start) / 86400000));
798
+ if (days === 0) return 'today';
799
+ if (days === 1) return '1 day';
800
+ return `${days} days`;
801
+ }
802
+
803
+ function printStats() {
804
+ const state = readState() || {};
805
+ const total = state.totalFixes || 0;
806
+ const heading = bold(cyan(`${BOLT}FuzzRun stats`));
807
+ if (!total) {
808
+ process.stdout.write(`${heading}\n No corrections recorded yet. Go make some typos!\n`);
809
+ return;
810
+ }
811
+ const since = formatDuration(state.firstFixAt);
812
+ const lines = [heading];
813
+ lines.push(` ${bold(String(total))} commands rescued${since ? dim(` over the last ${since}`) : ''}.`);
814
+
815
+ const byKind = state.byKind || {};
816
+ const kinds = Object.keys(byKind).sort((a, b) => byKind[b] - byKind[a]);
817
+ if (kinds.length) {
818
+ lines.push(' ' + dim('by type: ') + kinds.map((k) => `${k} ${bold(String(byKind[k]))}`).join(', '));
819
+ }
820
+
821
+ const history = state.history || {};
822
+ const top = Object.values(history)
823
+ .sort((a, b) => b.count - a.count)
824
+ .slice(0, 5);
825
+ if (top.length) {
826
+ lines.push(' ' + dim('top typos:'));
827
+ for (const entry of top) {
828
+ lines.push(
829
+ ` ${dim(entry.from || '?')} ${cyan('->')} ${green(entry.to || '?')} ${dim(`x${entry.count}`)}`
830
+ );
831
+ }
832
+ }
833
+ process.stdout.write(lines.join('\n') + '\n');
834
+ }
835
+
503
836
  function main() {
504
- const argv = process.argv.slice(2);
837
+ let argv = process.argv.slice(2);
505
838
  if (!argv.length) {
506
839
  process.stderr.write('Usage: fuzzrun <command> [args...]\n');
507
840
  process.exit(1);
@@ -509,7 +842,19 @@ function main() {
509
842
 
510
843
  showInstallBannerOnce();
511
844
 
512
- const action = argv[0];
845
+ let action = argv[0];
846
+
847
+ // `fuzzrun explain <command...>` = dry-run: show the fix without running it.
848
+ if (action === 'explain' || action === '--dry-run') {
849
+ DRY_RUN = true;
850
+ argv = argv.slice(1);
851
+ if (!argv.length) {
852
+ process.stderr.write('Usage: fuzzrun explain <command> [args...]\n');
853
+ process.exit(1);
854
+ }
855
+ action = argv[0];
856
+ }
857
+
513
858
  if (action === 'enable') {
514
859
  const results = installer.enable({});
515
860
  const updated = results.some((item) => item.updated);
@@ -525,13 +870,17 @@ function main() {
525
870
  process.exit(0);
526
871
  }
527
872
  if (action === 'status') {
528
- const results = installer.status();
529
- for (const item of results) {
530
- process.stdout.write(`${item.enabled ? 'enabled' : 'disabled'}: ${item.path}\n`);
531
- }
532
- process.exit(0);
533
- }
534
-
873
+ const results = installer.status();
874
+ for (const item of results) {
875
+ process.stdout.write(`${item.enabled ? 'enabled' : 'disabled'}: ${item.path}\n`);
876
+ }
877
+ process.exit(0);
878
+ }
879
+ if (action === 'stats') {
880
+ printStats();
881
+ process.exit(0);
882
+ }
883
+
535
884
  const state = readState() || {};
536
885
  if (!state.disabled && process.env.FUZZRUN_SKIP_ENABLE !== '1') {
537
886
  try {
@@ -540,88 +889,107 @@ function main() {
540
889
  if (!anyEnabled) {
541
890
  const results = installer.enable({});
542
891
  const updated = results.some((item) => item.updated);
543
- if (updated) {
544
- process.stdout.write('FuzzRun auto-enabled. Restart your shell to apply changes.\n');
545
- }
546
- }
547
- } catch (err) {
548
- process.stderr.write(`fuzzrun: auto-enable failed: ${err.message}\n`);
549
- }
550
- }
551
-
552
- let baseCommand = argv[0];
553
- const rest = argv.slice(1);
554
- baseCommand = normalizePowerShellGetPrefix(baseCommand);
555
- const firstRun = run(baseCommand, rest);
556
-
557
- if (firstRun.error && firstRun.error.code === 'ENOENT') {
558
- const corrected = tryBaseCorrection(baseCommand, rest);
559
- if (corrected) {
560
- const { result } = corrected;
561
- if (result.code !== 0) {
562
- const combinedOutput = `${result.stderr}\n${result.stdout}`;
563
- const correctedSub = trySubcommandCorrection(corrected.command, corrected.args, combinedOutput);
564
- if (correctedSub) {
565
- process.stdout.write(correctedSub.stdout);
566
- process.stderr.write(correctedSub.stderr);
567
- process.exit(correctedSub.code);
568
- }
569
- const correctedScript = tryScriptCorrection(corrected.command, corrected.args, combinedOutput);
570
- if (correctedScript) {
571
- process.stdout.write(correctedScript.stdout);
572
- process.stderr.write(correctedScript.stderr);
573
- process.exit(correctedScript.code);
574
- }
575
- const correctedBranch = tryGitBranchCorrection(corrected.command, corrected.args, combinedOutput);
576
- if (correctedBranch) {
577
- process.stdout.write(correctedBranch.stdout);
578
- process.stderr.write(correctedBranch.stderr);
579
- process.exit(correctedBranch.code);
580
- }
581
- }
582
- process.stdout.write(result.stdout);
583
- process.stderr.write(result.stderr);
584
- process.exit(result.code);
585
- }
586
- process.stderr.write(firstRun.error.message ? `${firstRun.error.message}\n` : `fuzzrun: command not found: ${baseCommand}\n`);
587
- process.exit(firstRun.code);
588
- }
589
-
590
- if (firstRun.code === 0) {
591
- process.stdout.write(firstRun.stdout);
592
- process.stderr.write(firstRun.stderr);
593
- process.exit(0);
594
- }
595
-
596
- const combinedOutput = `${firstRun.stderr}\n${firstRun.stdout}`;
597
- const correctedSub = trySubcommandCorrection(baseCommand, rest, combinedOutput);
598
- if (correctedSub) {
599
- process.stdout.write(correctedSub.stdout);
600
- process.stderr.write(correctedSub.stderr);
601
- process.exit(correctedSub.code);
602
- }
603
-
604
- const correctedScript = tryScriptCorrection(baseCommand, rest, combinedOutput);
605
- if (correctedScript) {
606
- process.stdout.write(correctedScript.stdout);
607
- process.stderr.write(correctedScript.stderr);
608
- process.exit(correctedScript.code);
609
- }
610
-
611
- const correctedBranch = tryGitBranchCorrection(baseCommand, rest, combinedOutput);
612
- if (correctedBranch) {
613
- process.stdout.write(correctedBranch.stdout);
614
- process.stderr.write(correctedBranch.stderr);
615
- process.exit(correctedBranch.code);
616
- }
617
-
618
- process.stdout.write(firstRun.stdout);
619
- process.stderr.write(firstRun.stderr);
620
- process.exit(firstRun.code);
621
- }
622
-
623
- if (require.main === module) {
624
- main();
625
- }
626
-
627
- module.exports = { main };
892
+ if (updated) {
893
+ process.stdout.write('FuzzRun auto-enabled. Restart your shell to apply changes.\n');
894
+ }
895
+ }
896
+ } catch (err) {
897
+ process.stderr.write(`fuzzrun: auto-enable failed: ${err.message}\n`);
898
+ }
899
+ }
900
+
901
+ let baseCommand = argv[0];
902
+ const rest = argv.slice(1);
903
+ baseCommand = normalizePowerShellGetPrefix(baseCommand);
904
+ const firstRun = run(baseCommand, rest);
905
+
906
+ if (firstRun.error && firstRun.error.code === 'ENOENT') {
907
+ const corrected = tryBaseCorrection(baseCommand, rest);
908
+ if (corrected) {
909
+ const { result } = corrected;
910
+ if (result.declined) {
911
+ process.exit(result.code);
912
+ }
913
+ if (result.dryRun) {
914
+ process.exit(0);
915
+ }
916
+ if (result.code !== 0) {
917
+ const combinedOutput = `${result.stderr}\n${result.stdout}`;
918
+ const correctedSub = trySubcommandCorrection(corrected.command, corrected.args, combinedOutput);
919
+ if (correctedSub) {
920
+ process.stdout.write(correctedSub.stdout);
921
+ process.stderr.write(correctedSub.stderr);
922
+ process.exit(correctedSub.code);
923
+ }
924
+ const correctedScript = tryScriptCorrection(corrected.command, corrected.args, combinedOutput);
925
+ if (correctedScript) {
926
+ process.stdout.write(correctedScript.stdout);
927
+ process.stderr.write(correctedScript.stderr);
928
+ process.exit(correctedScript.code);
929
+ }
930
+ const correctedFlag = tryFlagCorrection(corrected.command, corrected.args, combinedOutput);
931
+ if (correctedFlag) {
932
+ process.stdout.write(correctedFlag.stdout);
933
+ process.stderr.write(correctedFlag.stderr);
934
+ process.exit(correctedFlag.code);
935
+ }
936
+ const correctedBranch = tryGitBranchCorrection(corrected.command, corrected.args, combinedOutput);
937
+ if (correctedBranch) {
938
+ process.stdout.write(correctedBranch.stdout);
939
+ process.stderr.write(correctedBranch.stderr);
940
+ process.exit(correctedBranch.code);
941
+ }
942
+ }
943
+ process.stdout.write(result.stdout);
944
+ process.stderr.write(result.stderr);
945
+ process.exit(result.code);
946
+ }
947
+ process.stderr.write(`fuzzrun: command not found: ${baseCommand}\n`);
948
+ process.exit(firstRun.code);
949
+ }
950
+
951
+ if (firstRun.code === 0) {
952
+ process.stdout.write(firstRun.stdout);
953
+ process.stderr.write(firstRun.stderr);
954
+ process.exit(0);
955
+ }
956
+
957
+ const combinedOutput = `${firstRun.stderr}\n${firstRun.stdout}`;
958
+ const correctedSub = trySubcommandCorrection(baseCommand, rest, combinedOutput);
959
+ if (correctedSub) {
960
+ process.stdout.write(correctedSub.stdout);
961
+ process.stderr.write(correctedSub.stderr);
962
+ process.exit(correctedSub.code);
963
+ }
964
+
965
+ const correctedScript = tryScriptCorrection(baseCommand, rest, combinedOutput);
966
+ if (correctedScript) {
967
+ process.stdout.write(correctedScript.stdout);
968
+ process.stderr.write(correctedScript.stderr);
969
+ process.exit(correctedScript.code);
970
+ }
971
+
972
+ const correctedFlag = tryFlagCorrection(baseCommand, rest, combinedOutput);
973
+ if (correctedFlag) {
974
+ process.stdout.write(correctedFlag.stdout);
975
+ process.stderr.write(correctedFlag.stderr);
976
+ process.exit(correctedFlag.code);
977
+ }
978
+
979
+ const correctedBranch = tryGitBranchCorrection(baseCommand, rest, combinedOutput);
980
+ if (correctedBranch) {
981
+ process.stdout.write(correctedBranch.stdout);
982
+ process.stderr.write(correctedBranch.stderr);
983
+ process.exit(correctedBranch.code);
984
+ }
985
+
986
+ process.stdout.write(firstRun.stdout);
987
+ process.stderr.write(firstRun.stderr);
988
+ process.exit(firstRun.code);
989
+ }
990
+
991
+ if (require.main === module) {
992
+ main();
993
+ }
994
+
995
+ module.exports = { main };