fuzzrunx 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fuzzrunx",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Auto-correct mistyped commands and subcommands and re-run them safely.",
5
5
  "bin": {
6
6
  "fuzzrun": "bin/fuzzrun.js"
@@ -1,17 +1,36 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
3
6
  const installer = require('../src/installer');
4
7
 
5
8
  const skip = process.env.FUZZRUN_SKIP_ENABLE === '1';
6
9
 
10
+ function getStatePath() {
11
+ return path.join(os.homedir(), '.fuzzrun', 'state.json');
12
+ }
13
+
14
+ function writeState(next) {
15
+ const filePath = getStatePath();
16
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
17
+ fs.writeFileSync(filePath, JSON.stringify(next, null, 2), 'utf8');
18
+ }
19
+
7
20
  if (skip) {
8
- process.stdout.write('fuzzrun: auto-enable skipped\n');
21
+ process.stderr.write('fuzzrun: auto-enable skipped\n');
9
22
  process.exit(0);
10
23
  }
11
24
 
25
+ const foregroundScripts =
26
+ process.env.npm_config_foreground_scripts === 'true' ||
27
+ process.env.npm_config_foreground_scripts === '1';
28
+
12
29
  try {
13
30
  installer.enable({});
14
- process.stdout.write('FuzzRun is automatically enabled. Run "fuzzrun disable" to deactivate.\n');
31
+ process.stderr.write('FuzzRun is automatically enabled. Run "fuzzrun disable" to deactivate.\n');
32
+ writeState({ bannerShown: foregroundScripts, enableSucceeded: true });
15
33
  } catch (err) {
16
- process.stdout.write(`fuzzrun: auto-enable failed: ${err.message}\n`);
34
+ process.stderr.write(`fuzzrun: auto-enable failed: ${err.message}\n`);
35
+ writeState({ bannerShown: false, enableSucceeded: false, lastError: err.message });
17
36
  }
package/src/cli.js CHANGED
@@ -5,40 +5,41 @@
5
5
 
6
6
  const { spawnSync } = require('child_process');
7
7
  const fs = require('fs');
8
+ const os = require('os');
8
9
  const path = require('path');
9
10
  const installer = require('./installer');
10
-
11
- const MAX_DISTANCE = Number.isFinite(Number(process.env.FUZZRUN_MAX_DISTANCE))
12
- ? Math.max(1, Number(process.env.FUZZRUN_MAX_DISTANCE))
13
- : 1;
14
-
15
- const DEFAULT_PRIORITY_BASES = [
16
- 'git',
17
- 'npm',
18
- 'yarn',
19
- 'pnpm',
20
- 'node',
21
- 'python',
22
- 'python3',
23
- 'pip',
24
- 'pip3',
25
- 'docker',
26
- 'kubectl',
27
- 'gh',
28
- 'go',
29
- 'cargo',
30
- 'dotnet',
31
- 'java',
32
- 'mvn',
33
- 'gradle'
34
- ];
35
- const ENV_PRIORITY_BASES = (process.env.FUZZRUN_PREFER_BASES || '')
36
- .split(',')
37
- .map((value) => normalizeToken(value).trim())
38
- .filter(Boolean);
39
- const PRIORITY_BASES = new Set([...DEFAULT_PRIORITY_BASES, ...ENV_PRIORITY_BASES]);
40
-
41
- const DANGEROUS_BASE = new Set(['rm', 'mv', 'dd', 'shutdown', 'reboot', 'halt', 'poweroff']);
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']);
42
43
 
43
44
  const COMMON_SUBCOMMANDS = {
44
45
  git: [
@@ -174,31 +175,31 @@ const COMMON_SUBCOMMANDS = {
174
175
  'drain',
175
176
  'uncordon'
176
177
  ],
177
- gh: ['auth', 'repo', 'issue', 'pr', 'gist', 'alias', 'api', 'search', 'run', 'workflow', 'status', 'label']
178
- };
179
-
180
- const SAFE_SUBCOMMAND_BASES = new Set(Object.keys(COMMON_SUBCOMMANDS));
181
- const ALLOW_ANY_SUBCOMMANDS = process.env.FUZZRUN_ALLOW_ANY_SUBCOMMANDS === '1';
182
- const SCRIPT_BASES = new Set(['npm', 'yarn', 'pnpm']);
183
- const RISKY_ARG_PATTERNS = [
184
- /^-f$/,
185
- /^-rf$/,
186
- /^-fr$/,
187
- /^--force$/i,
188
- /^--hard$/i,
189
- /^--delete$/i,
190
- /^--purge$/i,
191
- /^--no-preserve-root$/i
192
- ];
193
- const SCRIPT_ERROR_PATTERNS = [
194
- /missing script/i,
195
- /unknown script/i,
196
- /script.*not found/i,
197
- /couldn'?t find.*script/i,
198
- /command ".*" not found/i
199
- ];
200
- const GIT_PATHSPEC_PATTERN = /pathspec .* did not match/i;
201
-
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
+
202
203
  const suggestionPatterns = [
203
204
  /The most similar command is\s+([^\s]+)/i,
204
205
  /The most similar commands are:\s*\n\s*([^\s]+)/i,
@@ -207,41 +208,79 @@ const suggestionPatterns = [
207
208
  /Perhaps you meant\s+['"]?([A-Za-z0-9:_-]+)['"]?\??/i
208
209
  ];
209
210
 
210
- function normalizeToken(value) {
211
- return String(value || '').toLowerCase();
211
+ function getStatePath() {
212
+ return path.join(os.homedir(), '.fuzzrun', 'state.json');
212
213
  }
213
214
 
214
- function damerauLevenshtein(a, b, maxDistance = 2) {
215
- const aNorm = normalizeToken(a);
216
- const bNorm = normalizeToken(b);
217
- if (aNorm === bNorm) return 0;
218
- if (Math.abs(aNorm.length - bNorm.length) > maxDistance) {
219
- return maxDistance + 1;
215
+ function readState() {
216
+ try {
217
+ const raw = fs.readFileSync(getStatePath(), 'utf8');
218
+ return JSON.parse(raw);
219
+ } catch (err) {
220
+ return null;
220
221
  }
221
- const dp = Array.from({ length: aNorm.length + 1 }, () => new Array(bNorm.length + 1).fill(0));
222
- for (let i = 0; i <= aNorm.length; i += 1) dp[i][0] = i;
223
- for (let j = 0; j <= bNorm.length; j += 1) dp[0][j] = j;
224
- for (let i = 1; i <= aNorm.length; i += 1) {
225
- let rowMin = maxDistance + 1;
226
- for (let j = 1; j <= bNorm.length; j += 1) {
227
- const cost = aNorm[i - 1] === bNorm[j - 1] ? 0 : 1;
228
- let value = Math.min(
229
- dp[i - 1][j] + 1,
230
- dp[i][j - 1] + 1,
231
- dp[i - 1][j - 1] + cost
232
- );
233
- if (i > 1 && j > 1 && aNorm[i - 1] === bNorm[j - 2] && aNorm[i - 2] === bNorm[j - 1]) {
234
- value = Math.min(value, dp[i - 2][j - 2] + 1);
235
- }
236
- dp[i][j] = value;
237
- if (value < rowMin) rowMin = value;
238
- }
239
- if (rowMin > maxDistance) {
240
- return maxDistance + 1;
241
- }
222
+ }
223
+
224
+ function writeState(next) {
225
+ try {
226
+ const filePath = getStatePath();
227
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
228
+ fs.writeFileSync(filePath, JSON.stringify(next, null, 2), 'utf8');
229
+ } catch (err) {
230
+ // Best-effort only.
242
231
  }
243
- return dp[aNorm.length][bNorm.length];
244
232
  }
233
+
234
+ function showInstallBannerOnce() {
235
+ const state = readState() || {};
236
+ if (state.bannerShown) return;
237
+ const message =
238
+ state.enableSucceeded === false
239
+ ? 'FuzzRun auto-enable failed during install. Run "fuzzrun enable" to activate.\n'
240
+ : 'FuzzRun is automatically enabled. Run "fuzzrun disable" to deactivate.\n';
241
+ process.stderr.write(message);
242
+ state.bannerShown = true;
243
+ if (typeof state.enableSucceeded === 'undefined') {
244
+ state.enableSucceeded = true;
245
+ }
246
+ writeState(state);
247
+ }
248
+
249
+ function normalizeToken(value) {
250
+ return String(value || '').toLowerCase();
251
+ }
252
+
253
+ function damerauLevenshtein(a, b, maxDistance = 2) {
254
+ const aNorm = normalizeToken(a);
255
+ const bNorm = normalizeToken(b);
256
+ if (aNorm === bNorm) return 0;
257
+ if (Math.abs(aNorm.length - bNorm.length) > maxDistance) {
258
+ return maxDistance + 1;
259
+ }
260
+ const dp = Array.from({ length: aNorm.length + 1 }, () => new Array(bNorm.length + 1).fill(0));
261
+ for (let i = 0; i <= aNorm.length; i += 1) dp[i][0] = i;
262
+ for (let j = 0; j <= bNorm.length; j += 1) dp[0][j] = j;
263
+ for (let i = 1; i <= aNorm.length; i += 1) {
264
+ let rowMin = maxDistance + 1;
265
+ for (let j = 1; j <= bNorm.length; j += 1) {
266
+ const cost = aNorm[i - 1] === bNorm[j - 1] ? 0 : 1;
267
+ let value = Math.min(
268
+ dp[i - 1][j] + 1,
269
+ dp[i][j - 1] + 1,
270
+ dp[i - 1][j - 1] + cost
271
+ );
272
+ if (i > 1 && j > 1 && aNorm[i - 1] === bNorm[j - 2] && aNorm[i - 2] === bNorm[j - 1]) {
273
+ value = Math.min(value, dp[i - 2][j - 2] + 1);
274
+ }
275
+ dp[i][j] = value;
276
+ if (value < rowMin) rowMin = value;
277
+ }
278
+ if (rowMin > maxDistance) {
279
+ return maxDistance + 1;
280
+ }
281
+ }
282
+ return dp[aNorm.length][bNorm.length];
283
+ }
245
284
 
246
285
  function collectPathCommands() {
247
286
  const names = new Set();
@@ -275,33 +314,46 @@ function collectPathCommands() {
275
314
  return names;
276
315
  }
277
316
 
278
- const PATH_COMMANDS = collectPathCommands();
279
-
280
- function findBestMatch(candidates, target, maxDistance = MAX_DISTANCE) {
281
- if (!candidates || !target) return null;
282
- let best = null;
283
- let bestDistance = maxDistance + 1;
284
- let ties = [];
285
- for (const candidate of candidates || []) {
286
- const dist = damerauLevenshtein(candidate, target, maxDistance);
287
- if (dist < bestDistance) {
288
- best = candidate;
289
- bestDistance = dist;
290
- ties = [candidate];
291
- } else if (dist === bestDistance) {
292
- ties.push(candidate);
293
- }
294
- }
295
- if (!best || bestDistance > maxDistance) return null;
296
- if (ties.length > 1) {
297
- const preferred = ties.filter((value) => PRIORITY_BASES.has(normalizeToken(value)));
298
- if (preferred.length === 1) {
299
- return { match: preferred[0], distance: bestDistance };
300
- }
301
- return null;
302
- }
303
- return { match: best, distance: bestDistance };
304
- }
317
+ const PATH_COMMANDS = collectPathCommands();
318
+
319
+ function normalizePowerShellGetPrefix(command) {
320
+ if (!command) return command;
321
+ const lowered = normalizeToken(command);
322
+ if (!lowered.startsWith('get-')) return command;
323
+ if (PATH_COMMANDS.has(command) || PATH_COMMANDS.has(lowered)) return command;
324
+ const stripped = command.slice(4);
325
+ if (!stripped) return command;
326
+ if (PATH_COMMANDS.has(stripped)) return stripped;
327
+ const match = findBestMatch(PATH_COMMANDS, stripped, MAX_DISTANCE);
328
+ if (match) return stripped;
329
+ return command;
330
+ }
331
+
332
+ function findBestMatch(candidates, target, maxDistance = MAX_DISTANCE) {
333
+ if (!candidates || !target) return null;
334
+ let best = null;
335
+ let bestDistance = maxDistance + 1;
336
+ let ties = [];
337
+ for (const candidate of candidates || []) {
338
+ const dist = damerauLevenshtein(candidate, target, maxDistance);
339
+ if (dist < bestDistance) {
340
+ best = candidate;
341
+ bestDistance = dist;
342
+ ties = [candidate];
343
+ } else if (dist === bestDistance) {
344
+ ties.push(candidate);
345
+ }
346
+ }
347
+ if (!best || bestDistance > maxDistance) return null;
348
+ if (ties.length > 1) {
349
+ const preferred = ties.filter((value) => PRIORITY_BASES.has(normalizeToken(value)));
350
+ if (preferred.length === 1) {
351
+ return { match: preferred[0], distance: bestDistance };
352
+ }
353
+ return null;
354
+ }
355
+ return { match: best, distance: bestDistance };
356
+ }
305
357
 
306
358
  function parseSuggestion(text) {
307
359
  for (const pattern of suggestionPatterns) {
@@ -326,121 +378,121 @@ function run(cmd, args) {
326
378
  };
327
379
  }
328
380
 
329
- function logFix(from, to) {
330
- process.stderr.write(`fuzzrun: auto-correcting "${from}" -> "${to}"\n`);
331
- }
332
-
333
- function hasRiskyArgs(args) {
334
- return args.some((arg) => RISKY_ARG_PATTERNS.some((pattern) => pattern.test(arg)));
335
- }
336
-
337
- function tryBaseCorrection(command, args) {
338
- if (hasRiskyArgs(args)) return null;
339
- const suggestion = findBestMatch(PATH_COMMANDS, command, MAX_DISTANCE);
340
- if (suggestion && !DANGEROUS_BASE.has(suggestion.match) && suggestion.match !== command) {
341
- logFix(command, suggestion.match);
342
- return {
343
- command: suggestion.match,
344
- args,
345
- result: run(suggestion.match, args)
346
- };
347
- }
348
- return null;
349
- }
350
-
351
- function trySubcommandCorrection(command, args, combinedOutput) {
352
- if (!SAFE_SUBCOMMAND_BASES.has(command) && !ALLOW_ANY_SUBCOMMANDS) return null;
353
- if (!args.length) return null;
354
- const attemptedSub = args[0];
355
- if (attemptedSub.startsWith('-')) return null;
356
- if (hasRiskyArgs(args)) return null;
357
- const fromOutput = parseSuggestion(combinedOutput);
358
- const candidates = COMMON_SUBCOMMANDS[command] || [];
359
- const fromDict = findBestMatch(candidates, attemptedSub, MAX_DISTANCE);
360
- const outputDistance = fromOutput
361
- ? damerauLevenshtein(fromOutput, attemptedSub, MAX_DISTANCE)
362
- : MAX_DISTANCE + 1;
363
- const choice = outputDistance <= MAX_DISTANCE ? fromOutput : fromDict ? fromDict.match : null;
364
-
365
- if (choice && choice !== attemptedSub && damerauLevenshtein(choice, attemptedSub, MAX_DISTANCE) <= MAX_DISTANCE) {
366
- logFix(`${command} ${attemptedSub}`, `${command} ${choice}`);
367
- return run(command, [choice, ...args.slice(1)]);
368
- }
369
- return null;
370
- }
371
-
372
- function findPackageJson(startDir) {
373
- let current = startDir;
374
- while (current && current !== path.dirname(current)) {
375
- const candidate = path.join(current, 'package.json');
376
- if (fs.existsSync(candidate)) return candidate;
377
- current = path.dirname(current);
378
- }
379
- return null;
380
- }
381
-
382
- function getPackageScripts(cwd) {
383
- const pkgPath = findPackageJson(cwd);
384
- if (!pkgPath) return [];
385
- try {
386
- const raw = fs.readFileSync(pkgPath, 'utf8');
387
- const parsed = JSON.parse(raw);
388
- return Object.keys(parsed.scripts || {});
389
- } catch (err) {
390
- return [];
391
- }
392
- }
393
-
394
- function isScriptError(output) {
395
- return SCRIPT_ERROR_PATTERNS.some((pattern) => pattern.test(output));
396
- }
397
-
398
- function tryScriptCorrection(command, args, combinedOutput) {
399
- if (!SCRIPT_BASES.has(command)) return null;
400
- if (args.length < 2) return null;
401
- if (args[0] !== 'run') return null;
402
- const scriptName = args[1];
403
- if (!scriptName || scriptName.startsWith('-')) return null;
404
- if (!isScriptError(combinedOutput)) return null;
405
- if (hasRiskyArgs(args)) return null;
406
-
407
- const scripts = getPackageScripts(process.cwd());
408
- const match = findBestMatch(scripts, scriptName, MAX_DISTANCE);
409
- if (match) {
410
- logFix(`${command} run ${scriptName}`, `${command} run ${match.match}`);
411
- return run(command, ['run', match.match, ...args.slice(2)]);
412
- }
413
- return null;
414
- }
415
-
416
- function getGitBranches() {
417
- const result = spawnSync('git', ['branch', '--format=%(refname:short)'], { encoding: 'utf8' });
418
- if (result.status !== 0) return [];
419
- return (result.stdout || '')
420
- .split(/\r?\n/)
421
- .map((line) => line.trim())
422
- .filter(Boolean);
423
- }
424
-
425
- function tryGitBranchCorrection(command, args, combinedOutput) {
426
- if (command !== 'git') return null;
427
- if (args.length < 2) return null;
428
- const subcommand = args[0];
429
- if (subcommand !== 'checkout' && subcommand !== 'switch') return null;
430
- const branch = args[1];
431
- if (!branch || branch.startsWith('-')) return null;
432
- if (!GIT_PATHSPEC_PATTERN.test(combinedOutput)) return null;
433
- if (hasRiskyArgs(args)) return null;
434
-
435
- const branches = getGitBranches();
436
- const match = findBestMatch(branches, branch, MAX_DISTANCE);
437
- if (match) {
438
- logFix(`${command} ${subcommand} ${branch}`, `${command} ${subcommand} ${match.match}`);
439
- return run(command, [subcommand, match.match, ...args.slice(2)]);
440
- }
441
- return null;
442
- }
443
-
381
+ function logFix(from, to) {
382
+ process.stderr.write(`fuzzrun: auto-correcting "${from}" -> "${to}"\n`);
383
+ }
384
+
385
+ function hasRiskyArgs(args) {
386
+ return args.some((arg) => RISKY_ARG_PATTERNS.some((pattern) => pattern.test(arg)));
387
+ }
388
+
389
+ function tryBaseCorrection(command, args) {
390
+ if (hasRiskyArgs(args)) return null;
391
+ const suggestion = findBestMatch(PATH_COMMANDS, command, MAX_DISTANCE);
392
+ if (suggestion && !DANGEROUS_BASE.has(suggestion.match) && suggestion.match !== command) {
393
+ logFix(command, suggestion.match);
394
+ return {
395
+ command: suggestion.match,
396
+ args,
397
+ result: run(suggestion.match, args)
398
+ };
399
+ }
400
+ return null;
401
+ }
402
+
403
+ function trySubcommandCorrection(command, args, combinedOutput) {
404
+ if (!SAFE_SUBCOMMAND_BASES.has(command) && !ALLOW_ANY_SUBCOMMANDS) return null;
405
+ if (!args.length) return null;
406
+ const attemptedSub = args[0];
407
+ if (attemptedSub.startsWith('-')) return null;
408
+ if (hasRiskyArgs(args)) return null;
409
+ const fromOutput = parseSuggestion(combinedOutput);
410
+ const candidates = COMMON_SUBCOMMANDS[command] || [];
411
+ const fromDict = findBestMatch(candidates, attemptedSub, MAX_DISTANCE);
412
+ const outputDistance = fromOutput
413
+ ? damerauLevenshtein(fromOutput, attemptedSub, MAX_DISTANCE)
414
+ : MAX_DISTANCE + 1;
415
+ const choice = outputDistance <= MAX_DISTANCE ? fromOutput : fromDict ? fromDict.match : null;
416
+
417
+ if (choice && choice !== attemptedSub && damerauLevenshtein(choice, attemptedSub, MAX_DISTANCE) <= MAX_DISTANCE) {
418
+ logFix(`${command} ${attemptedSub}`, `${command} ${choice}`);
419
+ return run(command, [choice, ...args.slice(1)]);
420
+ }
421
+ return null;
422
+ }
423
+
424
+ function findPackageJson(startDir) {
425
+ let current = startDir;
426
+ while (current && current !== path.dirname(current)) {
427
+ const candidate = path.join(current, 'package.json');
428
+ if (fs.existsSync(candidate)) return candidate;
429
+ current = path.dirname(current);
430
+ }
431
+ return null;
432
+ }
433
+
434
+ function getPackageScripts(cwd) {
435
+ const pkgPath = findPackageJson(cwd);
436
+ if (!pkgPath) return [];
437
+ try {
438
+ const raw = fs.readFileSync(pkgPath, 'utf8');
439
+ const parsed = JSON.parse(raw);
440
+ return Object.keys(parsed.scripts || {});
441
+ } catch (err) {
442
+ return [];
443
+ }
444
+ }
445
+
446
+ function isScriptError(output) {
447
+ return SCRIPT_ERROR_PATTERNS.some((pattern) => pattern.test(output));
448
+ }
449
+
450
+ function tryScriptCorrection(command, args, combinedOutput) {
451
+ if (!SCRIPT_BASES.has(command)) return null;
452
+ if (args.length < 2) return null;
453
+ if (args[0] !== 'run') return null;
454
+ const scriptName = args[1];
455
+ if (!scriptName || scriptName.startsWith('-')) return null;
456
+ if (!isScriptError(combinedOutput)) return null;
457
+ if (hasRiskyArgs(args)) return null;
458
+
459
+ const scripts = getPackageScripts(process.cwd());
460
+ const match = findBestMatch(scripts, scriptName, MAX_DISTANCE);
461
+ if (match) {
462
+ logFix(`${command} run ${scriptName}`, `${command} run ${match.match}`);
463
+ return run(command, ['run', match.match, ...args.slice(2)]);
464
+ }
465
+ return null;
466
+ }
467
+
468
+ function getGitBranches() {
469
+ const result = spawnSync('git', ['branch', '--format=%(refname:short)'], { encoding: 'utf8' });
470
+ if (result.status !== 0) return [];
471
+ return (result.stdout || '')
472
+ .split(/\r?\n/)
473
+ .map((line) => line.trim())
474
+ .filter(Boolean);
475
+ }
476
+
477
+ function tryGitBranchCorrection(command, args, combinedOutput) {
478
+ if (command !== 'git') return null;
479
+ if (args.length < 2) return null;
480
+ const subcommand = args[0];
481
+ if (subcommand !== 'checkout' && subcommand !== 'switch') return null;
482
+ const branch = args[1];
483
+ if (!branch || branch.startsWith('-')) return null;
484
+ if (!GIT_PATHSPEC_PATTERN.test(combinedOutput)) return null;
485
+ if (hasRiskyArgs(args)) return null;
486
+
487
+ const branches = getGitBranches();
488
+ const match = findBestMatch(branches, branch, MAX_DISTANCE);
489
+ if (match) {
490
+ logFix(`${command} ${subcommand} ${branch}`, `${command} ${subcommand} ${match.match}`);
491
+ return run(command, [subcommand, match.match, ...args.slice(2)]);
492
+ }
493
+ return null;
494
+ }
495
+
444
496
  function main() {
445
497
  const argv = process.argv.slice(2);
446
498
  if (!argv.length) {
@@ -448,115 +500,118 @@ function main() {
448
500
  process.exit(1);
449
501
  }
450
502
 
451
- const action = argv[0];
452
- if (action === 'enable') {
453
- const results = installer.enable({});
454
- const updated = results.some((item) => item.updated);
455
- process.stdout.write(updated ? 'FuzzRun enabled. Restart your shell to apply changes.\n' : 'FuzzRun already enabled.\n');
456
- process.exit(0);
457
- }
458
- if (action === 'disable') {
459
- const results = installer.disable();
460
- const updated = results.some((item) => item.updated);
461
- process.stdout.write(updated ? 'FuzzRun disabled. Restart your shell to apply changes.\n' : 'FuzzRun already disabled.\n');
462
- process.exit(0);
463
- }
464
- if (action === 'status') {
465
- const results = installer.status();
466
- for (const item of results) {
467
- process.stdout.write(`${item.enabled ? 'enabled' : 'disabled'}: ${item.path}\n`);
468
- }
469
- process.exit(0);
470
- }
471
-
472
- if (process.env.FUZZRUN_SKIP_ENABLE !== '1') {
473
- try {
474
- const status = installer.status();
475
- const anyEnabled = status.some((item) => item.enabled);
476
- if (!anyEnabled) {
477
- const results = installer.enable({});
478
- const updated = results.some((item) => item.updated);
479
- if (updated) {
480
- process.stdout.write('FuzzRun auto-enabled. Restart your shell to apply changes.\n');
481
- }
482
- }
483
- } catch (err) {
484
- process.stderr.write(`fuzzrun: auto-enable failed: ${err.message}\n`);
485
- }
486
- }
503
+ showInstallBannerOnce();
487
504
 
488
- const baseCommand = argv[0];
489
- const rest = argv.slice(1);
490
- const firstRun = run(baseCommand, rest);
491
-
492
- if (firstRun.error && firstRun.error.code === 'ENOENT') {
493
- const corrected = tryBaseCorrection(baseCommand, rest);
494
- if (corrected) {
495
- const { result } = corrected;
496
- if (result.code !== 0) {
497
- const combinedOutput = `${result.stderr}\n${result.stdout}`;
498
- const correctedSub = trySubcommandCorrection(corrected.command, corrected.args, combinedOutput);
499
- if (correctedSub) {
500
- process.stdout.write(correctedSub.stdout);
501
- process.stderr.write(correctedSub.stderr);
502
- process.exit(correctedSub.code);
503
- }
504
- const correctedScript = tryScriptCorrection(corrected.command, corrected.args, combinedOutput);
505
- if (correctedScript) {
506
- process.stdout.write(correctedScript.stdout);
507
- process.stderr.write(correctedScript.stderr);
508
- process.exit(correctedScript.code);
509
- }
510
- const correctedBranch = tryGitBranchCorrection(corrected.command, corrected.args, combinedOutput);
511
- if (correctedBranch) {
512
- process.stdout.write(correctedBranch.stdout);
513
- process.stderr.write(correctedBranch.stderr);
514
- process.exit(correctedBranch.code);
515
- }
516
- }
517
- process.stdout.write(result.stdout);
518
- process.stderr.write(result.stderr);
519
- process.exit(result.code);
520
- }
521
- process.stderr.write(firstRun.error.message ? `${firstRun.error.message}\n` : `fuzzrun: command not found: ${baseCommand}\n`);
522
- process.exit(firstRun.code);
523
- }
505
+ const action = argv[0];
506
+ if (action === 'enable') {
507
+ const results = installer.enable({});
508
+ const updated = results.some((item) => item.updated);
509
+ process.stdout.write(updated ? 'FuzzRun enabled. Restart your shell to apply changes.\n' : 'FuzzRun already enabled.\n');
510
+ process.exit(0);
511
+ }
512
+ if (action === 'disable') {
513
+ const results = installer.disable();
514
+ const updated = results.some((item) => item.updated);
515
+ process.stdout.write(updated ? 'FuzzRun disabled. Restart your shell to apply changes.\n' : 'FuzzRun already disabled.\n');
516
+ process.exit(0);
517
+ }
518
+ if (action === 'status') {
519
+ const results = installer.status();
520
+ for (const item of results) {
521
+ process.stdout.write(`${item.enabled ? 'enabled' : 'disabled'}: ${item.path}\n`);
522
+ }
523
+ process.exit(0);
524
+ }
524
525
 
525
- if (firstRun.code === 0) {
526
- process.stdout.write(firstRun.stdout);
527
- process.stderr.write(firstRun.stderr);
528
- process.exit(0);
529
- }
530
-
531
- const combinedOutput = `${firstRun.stderr}\n${firstRun.stdout}`;
532
- const correctedSub = trySubcommandCorrection(baseCommand, rest, combinedOutput);
533
- if (correctedSub) {
534
- process.stdout.write(correctedSub.stdout);
535
- process.stderr.write(correctedSub.stderr);
536
- process.exit(correctedSub.code);
537
- }
538
-
539
- const correctedScript = tryScriptCorrection(baseCommand, rest, combinedOutput);
540
- if (correctedScript) {
541
- process.stdout.write(correctedScript.stdout);
542
- process.stderr.write(correctedScript.stderr);
543
- process.exit(correctedScript.code);
544
- }
545
-
546
- const correctedBranch = tryGitBranchCorrection(baseCommand, rest, combinedOutput);
547
- if (correctedBranch) {
548
- process.stdout.write(correctedBranch.stdout);
549
- process.stderr.write(correctedBranch.stderr);
550
- process.exit(correctedBranch.code);
551
- }
552
-
553
- process.stdout.write(firstRun.stdout);
554
- process.stderr.write(firstRun.stderr);
555
- process.exit(firstRun.code);
556
- }
526
+ if (process.env.FUZZRUN_SKIP_ENABLE !== '1') {
527
+ try {
528
+ const status = installer.status();
529
+ const anyEnabled = status.some((item) => item.enabled);
530
+ if (!anyEnabled) {
531
+ const results = installer.enable({});
532
+ const updated = results.some((item) => item.updated);
533
+ if (updated) {
534
+ process.stdout.write('FuzzRun auto-enabled. Restart your shell to apply changes.\n');
535
+ }
536
+ }
537
+ } catch (err) {
538
+ process.stderr.write(`fuzzrun: auto-enable failed: ${err.message}\n`);
539
+ }
540
+ }
557
541
 
558
- if (require.main === module) {
559
- main();
560
- }
561
-
562
- module.exports = { main };
542
+ let baseCommand = argv[0];
543
+ const rest = argv.slice(1);
544
+ baseCommand = normalizePowerShellGetPrefix(baseCommand);
545
+ const firstRun = run(baseCommand, rest);
546
+
547
+ if (firstRun.error && firstRun.error.code === 'ENOENT') {
548
+ const corrected = tryBaseCorrection(baseCommand, rest);
549
+ if (corrected) {
550
+ const { result } = corrected;
551
+ if (result.code !== 0) {
552
+ const combinedOutput = `${result.stderr}\n${result.stdout}`;
553
+ const correctedSub = trySubcommandCorrection(corrected.command, corrected.args, combinedOutput);
554
+ if (correctedSub) {
555
+ process.stdout.write(correctedSub.stdout);
556
+ process.stderr.write(correctedSub.stderr);
557
+ process.exit(correctedSub.code);
558
+ }
559
+ const correctedScript = tryScriptCorrection(corrected.command, corrected.args, combinedOutput);
560
+ if (correctedScript) {
561
+ process.stdout.write(correctedScript.stdout);
562
+ process.stderr.write(correctedScript.stderr);
563
+ process.exit(correctedScript.code);
564
+ }
565
+ const correctedBranch = tryGitBranchCorrection(corrected.command, corrected.args, combinedOutput);
566
+ if (correctedBranch) {
567
+ process.stdout.write(correctedBranch.stdout);
568
+ process.stderr.write(correctedBranch.stderr);
569
+ process.exit(correctedBranch.code);
570
+ }
571
+ }
572
+ process.stdout.write(result.stdout);
573
+ process.stderr.write(result.stderr);
574
+ process.exit(result.code);
575
+ }
576
+ process.stderr.write(firstRun.error.message ? `${firstRun.error.message}\n` : `fuzzrun: command not found: ${baseCommand}\n`);
577
+ process.exit(firstRun.code);
578
+ }
579
+
580
+ if (firstRun.code === 0) {
581
+ process.stdout.write(firstRun.stdout);
582
+ process.stderr.write(firstRun.stderr);
583
+ process.exit(0);
584
+ }
585
+
586
+ const combinedOutput = `${firstRun.stderr}\n${firstRun.stdout}`;
587
+ const correctedSub = trySubcommandCorrection(baseCommand, rest, combinedOutput);
588
+ if (correctedSub) {
589
+ process.stdout.write(correctedSub.stdout);
590
+ process.stderr.write(correctedSub.stderr);
591
+ process.exit(correctedSub.code);
592
+ }
593
+
594
+ const correctedScript = tryScriptCorrection(baseCommand, rest, combinedOutput);
595
+ if (correctedScript) {
596
+ process.stdout.write(correctedScript.stdout);
597
+ process.stderr.write(correctedScript.stderr);
598
+ process.exit(correctedScript.code);
599
+ }
600
+
601
+ const correctedBranch = tryGitBranchCorrection(baseCommand, rest, combinedOutput);
602
+ if (correctedBranch) {
603
+ process.stdout.write(correctedBranch.stdout);
604
+ process.stderr.write(correctedBranch.stderr);
605
+ process.exit(correctedBranch.code);
606
+ }
607
+
608
+ process.stdout.write(firstRun.stdout);
609
+ process.stderr.write(firstRun.stderr);
610
+ process.exit(firstRun.code);
611
+ }
612
+
613
+ if (require.main === module) {
614
+ main();
615
+ }
616
+
617
+ module.exports = { main };
package/src/installer.js CHANGED
@@ -58,15 +58,64 @@ function buildPowerShellSnippet(binPath) {
58
58
  const lines = [
59
59
  MARKER_START,
60
60
  `$fuzzrun = "${binPath}"`,
61
+ '$global:FuzzRunLastLine = $null',
62
+ 'if (Get-Module -ListAvailable -Name PSReadLine) {',
63
+ ' try {',
64
+ ' Import-Module PSReadLine -ErrorAction SilentlyContinue | Out-Null',
65
+ ' Set-PSReadLineKeyHandler -Key Enter -ScriptBlock {',
66
+ ' param($key, $arg)',
67
+ ' $line = $null',
68
+ ' $cursor = $null',
69
+ ' [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)',
70
+ ' $global:FuzzRunLastLine = $line',
71
+ ' [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()',
72
+ ' }',
73
+ ' } catch {}',
74
+ '}',
61
75
  'function global:fuzzrun { node $fuzzrun @args }',
62
76
  '$ExecutionContext.InvokeCommand.CommandNotFoundAction = {',
63
77
  ' param($commandName, $eventArgs)',
64
- ' fuzzrun $commandName @($eventArgs.Arguments)',
78
+ ' $cmd = $commandName',
79
+ ' $fzArgs = @($eventArgs.Arguments)',
80
+ ' $line = $global:FuzzRunLastLine',
81
+ ' $global:FuzzRunLastLine = $null',
82
+ ' if (-not $line -and $eventArgs.CommandLine) {',
83
+ ' $line = $eventArgs.CommandLine',
84
+ ' }',
85
+ ' if (-not $line -and $eventArgs.CommandScriptBlock) {',
86
+ ' $line = $eventArgs.CommandScriptBlock.ToString()',
87
+ ' }',
88
+ ' if (-not $line) {',
89
+ ' $history = Get-History -Count 1 -ErrorAction SilentlyContinue',
90
+ ' if ($history) { $line = $history.CommandLine }',
91
+ ' }',
92
+ ' $argv = @()',
93
+ ' if ($line) {',
94
+ ' $tokens = [System.Management.Automation.PSParser]::Tokenize($line, [ref]$null)',
95
+ " foreach ($token in $tokens) {",
96
+ " if ($token.Type -in @('Command','CommandArgument','CommandParameter','String','Number')) {",
97
+ ' $argv += $token.Content',
98
+ ' }',
99
+ ' }',
100
+ ' }',
101
+ ' if ($argv.Count -gt 0) {',
102
+ ' $cmd = $argv[0]',
103
+ ' }',
104
+ ' if ($argv.Count -gt 1) {',
105
+ ' $fzArgs = $argv[1..($argv.Count - 1)]',
106
+ ' }',
107
+ ' $eventArgs.CommandScriptBlock = { fuzzrun $cmd @fzArgs }.GetNewClosure()',
108
+ ' $eventArgs.StopSearch = $true',
65
109
  '}'
66
110
  ];
67
- for (const base of WRAP_BASES) {
68
- lines.push(`function global:${base} { fuzzrun ${base} @args }`);
69
- }
111
+ lines.push(`$__fuzzrunBases = @(${WRAP_BASES.map((base) => `'${base}'`).join(', ')})`);
112
+ lines.push('foreach ($base in $__fuzzrunBases) {');
113
+ lines.push(' $resolved = Get-Command $base -ErrorAction SilentlyContinue | Where-Object { $_.CommandType -eq "Application" } | Select-Object -First 1');
114
+ lines.push(' if ($resolved) {');
115
+ lines.push(' $cmdName = $base');
116
+ lines.push(' Set-Item -Path ("Function:$cmdName") -Value { fuzzrun $cmdName @args }.GetNewClosure()');
117
+ lines.push(' }');
118
+ lines.push('}');
70
119
  lines.push(MARKER_END, '');
71
120
  return lines.join('\n');
72
121
  }