fuzzrunx 0.1.4 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fuzzrunx",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Auto-correct mistyped commands and subcommands and re-run them safely.",
5
5
  "bin": {
6
6
  "fuzzrun": "bin/fuzzrun.js"
package/src/cli.js CHANGED
@@ -211,6 +211,22 @@ function normalizeToken(value) {
211
211
  return String(value || '').toLowerCase();
212
212
  }
213
213
 
214
+ function commandKey(command) {
215
+ const base = path.basename(String(command || ''));
216
+ const ext = path.extname(base);
217
+ return normalizeToken(ext ? base.slice(0, -ext.length) : base);
218
+ }
219
+
220
+ function isInteractiveCommand(command, args) {
221
+ const key = commandKey(command);
222
+ if (INTERACTIVE_BASES.has(key)) return true;
223
+ const subcommand = args && args.length ? normalizeToken(args[0]) : '';
224
+ if (subcommand && INTERACTIVE_SUBCOMMANDS[key] && INTERACTIVE_SUBCOMMANDS[key].has(subcommand)) {
225
+ return true;
226
+ }
227
+ return false;
228
+ }
229
+
214
230
  function damerauLevenshtein(a, b, maxDistance = 2) {
215
231
  const aNorm = normalizeToken(a);
216
232
  const bNorm = normalizeToken(b);
@@ -276,6 +292,76 @@ function collectPathCommands() {
276
292
  }
277
293
 
278
294
  const PATH_COMMANDS = collectPathCommands();
295
+ const RESOLVE_CACHE = new Map();
296
+ const PATHEXT = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
297
+ .split(';')
298
+ .map((value) => value.toLowerCase())
299
+ .filter(Boolean);
300
+ const INTERACTIVE_BASES = new Set(
301
+ (process.env.FUZZRUN_INTERACTIVE_BASES || 'npm,npx,yarn,pnpm')
302
+ .split(',')
303
+ .map((value) => normalizeToken(value).trim())
304
+ .filter(Boolean)
305
+ );
306
+ const INTERACTIVE_SUBCOMMANDS = {
307
+ npm: new Set(['publish', 'login', 'adduser']),
308
+ yarn: new Set(['publish', 'login']),
309
+ pnpm: new Set(['publish', 'login'])
310
+ };
311
+
312
+ function looksLikePath(value) {
313
+ if (!value) return false;
314
+ return value.includes('/') || value.includes('\\') || value.includes(':');
315
+ }
316
+
317
+ function resolveCommand(command) {
318
+ if (!command || looksLikePath(command)) return command;
319
+ if (RESOLVE_CACHE.has(command)) return RESOLVE_CACHE.get(command);
320
+
321
+ let resolved = null;
322
+ if (process.platform === 'win32') {
323
+ const result = spawnSync('where', [command], { encoding: 'utf8' });
324
+ if (result.status === 0) {
325
+ const lines = String(result.stdout || '')
326
+ .split(/\r?\n/)
327
+ .map((value) => value.trim())
328
+ .filter(Boolean);
329
+ const preferred = lines.find((line) => PATHEXT.includes(path.extname(line).toLowerCase()));
330
+ resolved = preferred || lines[0] || null;
331
+ if (resolved && !path.extname(resolved)) {
332
+ for (const ext of PATHEXT) {
333
+ const candidate = `${resolved}${ext.toLowerCase()}`;
334
+ if (fs.existsSync(candidate)) {
335
+ resolved = candidate;
336
+ break;
337
+ }
338
+ }
339
+ }
340
+ }
341
+ } else {
342
+ const result = spawnSync('which', [command], { encoding: 'utf8' });
343
+ if (result.status === 0) {
344
+ const line = String(result.stdout || '').trim();
345
+ if (line) resolved = line;
346
+ }
347
+ }
348
+
349
+ RESOLVE_CACHE.set(command, resolved || command);
350
+ return resolved || command;
351
+ }
352
+
353
+ function normalizePowerShellGetPrefix(command) {
354
+ if (!command) return command;
355
+ const lowered = normalizeToken(command);
356
+ if (!lowered.startsWith('get-')) return command;
357
+ if (PATH_COMMANDS.has(command) || PATH_COMMANDS.has(lowered)) return command;
358
+ const stripped = command.slice(4);
359
+ if (!stripped) return command;
360
+ if (PATH_COMMANDS.has(stripped)) return stripped;
361
+ const match = findBestMatch(PATH_COMMANDS, stripped, MAX_DISTANCE);
362
+ if (match) return stripped;
363
+ return command;
364
+ }
279
365
 
280
366
  function findBestMatch(candidates, target, maxDistance = MAX_DISTANCE) {
281
367
  if (!candidates || !target) return null;
@@ -313,18 +399,35 @@ function parseSuggestion(text) {
313
399
  return null;
314
400
  }
315
401
 
316
- function run(cmd, args) {
317
- const result = spawnSync(cmd, args, {
318
- encoding: 'utf8',
319
- stdio: ['inherit', 'pipe', 'pipe']
320
- });
321
- return {
322
- code: typeof result.status === 'number' ? result.status : result.error ? 1 : 0,
323
- stdout: result.stdout || '',
324
- stderr: result.stderr || '',
325
- error: result.error
326
- };
327
- }
402
+ function run(cmd, args) {
403
+ const interactive = isInteractiveCommand(cmd, args);
404
+ const result = spawnSync(cmd, args, {
405
+ encoding: 'utf8',
406
+ stdio: interactive ? 'inherit' : ['inherit', 'pipe', 'pipe']
407
+ });
408
+ if (result.error && result.error.code === 'ENOENT') {
409
+ const resolved = resolveCommand(cmd);
410
+ if (resolved && resolved !== cmd) {
411
+ const retryInteractive = isInteractiveCommand(resolved, args);
412
+ const retry = spawnSync(resolved, args, {
413
+ encoding: 'utf8',
414
+ stdio: retryInteractive ? 'inherit' : ['inherit', 'pipe', 'pipe']
415
+ });
416
+ return {
417
+ code: typeof retry.status === 'number' ? retry.status : retry.error ? 1 : 0,
418
+ stdout: retryInteractive ? '' : retry.stdout || '',
419
+ stderr: retryInteractive ? '' : retry.stderr || '',
420
+ error: retry.error
421
+ };
422
+ }
423
+ }
424
+ return {
425
+ code: typeof result.status === 'number' ? result.status : result.error ? 1 : 0,
426
+ stdout: interactive ? '' : result.stdout || '',
427
+ stderr: interactive ? '' : result.stderr || '',
428
+ error: result.error
429
+ };
430
+ }
328
431
 
329
432
  function logFix(from, to) {
330
433
  process.stderr.write(`fuzzrun: auto-correcting "${from}" -> "${to}"\n`);
@@ -485,8 +588,9 @@ function main() {
485
588
  }
486
589
  }
487
590
 
488
- const baseCommand = argv[0];
591
+ let baseCommand = argv[0];
489
592
  const rest = argv.slice(1);
593
+ baseCommand = normalizePowerShellGetPrefix(baseCommand);
490
594
  const firstRun = run(baseCommand, rest);
491
595
 
492
596
  if (firstRun.error && firstRun.error.code === 'ENOENT') {
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
  }