fuzzrunx 0.1.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/README.md ADDED
@@ -0,0 +1,58 @@
1
+ ## FuzzRun (prototype)
2
+
3
+ Auto-correct mistyped commands/subcommands and re-run them automatically (no prompt) when the fix is high-confidence (edit distance 1 or the CLI provides a single suggestion). Base command corrections skip dangerous commands like `rm` or `mv`.
4
+
5
+ ### Quick run
6
+
7
+ ```
8
+ node bin/fuzzrun.js git commmmit -m "msg"
9
+ ```
10
+
11
+ Install (npm):
12
+
13
+ ```
14
+ npm i -g fuzzrunx
15
+ ```
16
+
17
+ ### Bash/Zsh hook (auto-run on typos)
18
+
19
+ Add to your shell rc:
20
+
21
+ ```bash
22
+ FUZZRUN_BIN="/absolute/path/to/bin/fuzzrun.js"
23
+ fuzzrun() { node "$FUZZRUN_BIN" "$@"; }
24
+ command_not_found_handle() { fuzzrun "$@"; }
25
+ git() { fuzzrun git "$@"; } # optional: wrap git to auto-fix subcommands
26
+ ```
27
+
28
+ Notes: `command_not_found_handle` is bash-only; on zsh use `command_not_found_handler`. Keep `FUZZRUN_BIN` absolute.
29
+
30
+ ### PowerShell hook
31
+
32
+ Append to `Documents\PowerShell\Microsoft.PowerShell_profile.ps1`:
33
+
34
+ ```powershell
35
+ $fuzzrun = "C:\Users\HP\fuzzRun\bin\fuzzrun.js" # update path
36
+ function global:fuzzrun { node $fuzzrun @args }
37
+ $ExecutionContext.InvokeCommand.CommandNotFoundAction = {
38
+ param($commandName, $eventArgs)
39
+ fuzzrun $commandName @($eventArgs.Arguments)
40
+ }
41
+ function global:git { fuzzrun git @args } # optional git wrapper
42
+ ```
43
+
44
+ ### How it works
45
+ - Runs the command once; if it fails with "command not found" or "unknown subcommand", tries a one-edit-away fix or the CLI's own suggestion and re-runs automatically.
46
+ - Uses Damerau-Levenshtein (handles transposed letters) and refuses ambiguous matches.
47
+ - Skips auto-run when risky flags are present (`--force`, `--hard`, `-rf`, etc.) and blocks dangerous bases (`rm`, `mv`, `dd`, etc.).
48
+ - Subcommand suggestions are preloaded for popular CLIs (git, npm/yarn/pnpm, pip, docker, kubectl, gh) plus "did you mean" parsing.
49
+ - Context-aware fixes for `git checkout/switch <branch>` and `npm/yarn/pnpm run <script>` after a failure.
50
+
51
+ ### Config
52
+ - `FUZZRUN_MAX_DISTANCE=1` (set to 2 if you want more aggressive matching)
53
+ - `FUZZRUN_ALLOW_ANY_SUBCOMMANDS=1` (allow subcommand fixes for any base that prints suggestions)
54
+ - `FUZZRUN_PREFER_BASES=git,npm,docker` (breaks ties in favor of preferred commands)
55
+
56
+ ### Limits
57
+ - Only one retry; only unique matches; no prompt.
58
+ - For safety, does not auto-correct to dangerous bases (`rm`, `mv`, `dd`, etc.).
package/bin/fuzzrun.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { main } = require('../src/cli');
5
+
6
+ main();
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "fuzzrunx",
3
+ "version": "0.1.0",
4
+ "description": "Auto-correct mistyped commands and subcommands and re-run them safely.",
5
+ "bin": {
6
+ "fuzzrun": "bin/fuzzrun.js"
7
+ },
8
+ "type": "commonjs",
9
+ "license": "MIT",
10
+ "scripts": {
11
+ "start": "node bin/fuzzrun.js"
12
+ }
13
+ }
package/src/cli.js ADDED
@@ -0,0 +1,524 @@
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
+ const { spawnSync } = require('child_process');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const MAX_DISTANCE = Number.isFinite(Number(process.env.FUZZRUN_MAX_DISTANCE))
11
+ ? Math.max(1, Number(process.env.FUZZRUN_MAX_DISTANCE))
12
+ : 1;
13
+
14
+ const DEFAULT_PRIORITY_BASES = [
15
+ 'git',
16
+ 'npm',
17
+ 'yarn',
18
+ 'pnpm',
19
+ 'node',
20
+ 'python',
21
+ 'python3',
22
+ 'pip',
23
+ 'pip3',
24
+ 'docker',
25
+ 'kubectl',
26
+ 'gh',
27
+ 'go',
28
+ 'cargo',
29
+ 'dotnet',
30
+ 'java',
31
+ 'mvn',
32
+ 'gradle'
33
+ ];
34
+ const ENV_PRIORITY_BASES = (process.env.FUZZRUN_PREFER_BASES || '')
35
+ .split(',')
36
+ .map((value) => normalizeToken(value).trim())
37
+ .filter(Boolean);
38
+ const PRIORITY_BASES = new Set([...DEFAULT_PRIORITY_BASES, ...ENV_PRIORITY_BASES]);
39
+
40
+ const DANGEROUS_BASE = new Set(['rm', 'mv', 'dd', 'shutdown', 'reboot', 'halt', 'poweroff']);
41
+
42
+ const COMMON_SUBCOMMANDS = {
43
+ git: [
44
+ 'add',
45
+ 'bisect',
46
+ 'branch',
47
+ 'checkout',
48
+ 'clone',
49
+ 'commit',
50
+ 'diff',
51
+ 'fetch',
52
+ 'init',
53
+ 'log',
54
+ 'merge',
55
+ 'mv',
56
+ 'pull',
57
+ 'push',
58
+ 'rebase',
59
+ 'revert',
60
+ 'rm',
61
+ 'show',
62
+ 'stash',
63
+ 'status',
64
+ 'switch',
65
+ 'tag'
66
+ ],
67
+ npm: [
68
+ 'install',
69
+ 'init',
70
+ 'run',
71
+ 'test',
72
+ 'publish',
73
+ 'link',
74
+ 'login',
75
+ 'logout',
76
+ 'ci',
77
+ 'config',
78
+ 'cache',
79
+ 'start',
80
+ 'stop',
81
+ 'restart',
82
+ 'update',
83
+ 'outdated',
84
+ 'list',
85
+ 'prune',
86
+ 'exec',
87
+ 'root',
88
+ 'pack',
89
+ 'uninstall'
90
+ ],
91
+ yarn: [
92
+ 'add',
93
+ 'install',
94
+ 'remove',
95
+ 'run',
96
+ 'test',
97
+ 'init',
98
+ 'upgrade',
99
+ 'global',
100
+ 'dlx',
101
+ 'config',
102
+ 'list'
103
+ ],
104
+ pnpm: [
105
+ 'add',
106
+ 'install',
107
+ 'update',
108
+ 'remove',
109
+ 'run',
110
+ 'exec',
111
+ 'list',
112
+ 'publish',
113
+ 'install-test',
114
+ 'fetch'
115
+ ],
116
+ pip: ['install', 'uninstall', 'list', 'freeze', 'show', 'search', 'cache', 'config'],
117
+ docker: [
118
+ 'build',
119
+ 'commit',
120
+ 'compose',
121
+ 'cp',
122
+ 'create',
123
+ 'diff',
124
+ 'events',
125
+ 'exec',
126
+ 'images',
127
+ 'info',
128
+ 'inspect',
129
+ 'kill',
130
+ 'load',
131
+ 'logs',
132
+ 'pause',
133
+ 'port',
134
+ 'ps',
135
+ 'pull',
136
+ 'push',
137
+ 'rename',
138
+ 'restart',
139
+ 'rm',
140
+ 'rmi',
141
+ 'run',
142
+ 'save',
143
+ 'start',
144
+ 'stats',
145
+ 'stop',
146
+ 'tag',
147
+ 'top',
148
+ 'unpause',
149
+ 'update',
150
+ 'version'
151
+ ],
152
+ kubectl: [
153
+ 'apply',
154
+ 'get',
155
+ 'describe',
156
+ 'delete',
157
+ 'logs',
158
+ 'exec',
159
+ 'create',
160
+ 'edit',
161
+ 'explain',
162
+ 'expose',
163
+ 'port-forward',
164
+ 'top',
165
+ 'cp',
166
+ 'scale',
167
+ 'rollout',
168
+ 'set',
169
+ 'explain',
170
+ 'label',
171
+ 'annotate',
172
+ 'cordon',
173
+ 'drain',
174
+ 'uncordon'
175
+ ],
176
+ gh: ['auth', 'repo', 'issue', 'pr', 'gist', 'alias', 'api', 'search', 'run', 'workflow', 'status', 'label']
177
+ };
178
+
179
+ const SAFE_SUBCOMMAND_BASES = new Set(Object.keys(COMMON_SUBCOMMANDS));
180
+ const ALLOW_ANY_SUBCOMMANDS = process.env.FUZZRUN_ALLOW_ANY_SUBCOMMANDS === '1';
181
+ const SCRIPT_BASES = new Set(['npm', 'yarn', 'pnpm']);
182
+ const RISKY_ARG_PATTERNS = [
183
+ /^-f$/,
184
+ /^-rf$/,
185
+ /^-fr$/,
186
+ /^--force$/i,
187
+ /^--hard$/i,
188
+ /^--delete$/i,
189
+ /^--purge$/i,
190
+ /^--no-preserve-root$/i
191
+ ];
192
+ const SCRIPT_ERROR_PATTERNS = [
193
+ /missing script/i,
194
+ /unknown script/i,
195
+ /script.*not found/i,
196
+ /couldn'?t find.*script/i,
197
+ /command ".*" not found/i
198
+ ];
199
+ const GIT_PATHSPEC_PATTERN = /pathspec .* did not match/i;
200
+
201
+ const suggestionPatterns = [
202
+ /The most similar command is\s+([^\s]+)/i,
203
+ /The most similar commands are:\s*\n\s*([^\s]+)/i,
204
+ /Did you mean\s+['"]?([A-Za-z0-9:_-]+)['"]?\??/i,
205
+ /Unknown command\s+['"]?([A-Za-z0-9:_-]+)['"]?\??/i,
206
+ /Perhaps you meant\s+['"]?([A-Za-z0-9:_-]+)['"]?\??/i
207
+ ];
208
+
209
+ function normalizeToken(value) {
210
+ return String(value || '').toLowerCase();
211
+ }
212
+
213
+ function damerauLevenshtein(a, b, maxDistance = 2) {
214
+ const aNorm = normalizeToken(a);
215
+ const bNorm = normalizeToken(b);
216
+ if (aNorm === bNorm) return 0;
217
+ if (Math.abs(aNorm.length - bNorm.length) > maxDistance) {
218
+ return maxDistance + 1;
219
+ }
220
+ const dp = Array.from({ length: aNorm.length + 1 }, () => new Array(bNorm.length + 1).fill(0));
221
+ for (let i = 0; i <= aNorm.length; i += 1) dp[i][0] = i;
222
+ for (let j = 0; j <= bNorm.length; j += 1) dp[0][j] = j;
223
+ for (let i = 1; i <= aNorm.length; i += 1) {
224
+ let rowMin = maxDistance + 1;
225
+ for (let j = 1; j <= bNorm.length; j += 1) {
226
+ const cost = aNorm[i - 1] === bNorm[j - 1] ? 0 : 1;
227
+ let value = Math.min(
228
+ dp[i - 1][j] + 1,
229
+ dp[i][j - 1] + 1,
230
+ dp[i - 1][j - 1] + cost
231
+ );
232
+ if (i > 1 && j > 1 && aNorm[i - 1] === bNorm[j - 2] && aNorm[i - 2] === bNorm[j - 1]) {
233
+ value = Math.min(value, dp[i - 2][j - 2] + 1);
234
+ }
235
+ dp[i][j] = value;
236
+ if (value < rowMin) rowMin = value;
237
+ }
238
+ if (rowMin > maxDistance) {
239
+ return maxDistance + 1;
240
+ }
241
+ }
242
+ return dp[aNorm.length][bNorm.length];
243
+ }
244
+
245
+ function collectPathCommands() {
246
+ const names = new Set();
247
+ const pathEntries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
248
+ const allowedExts = new Set(
249
+ (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
250
+ .toLowerCase()
251
+ .split(';')
252
+ .filter(Boolean)
253
+ );
254
+
255
+ for (const entry of pathEntries) {
256
+ try {
257
+ const items = fs.readdirSync(entry, { withFileTypes: true });
258
+ for (const item of items) {
259
+ if (item.isDirectory()) continue;
260
+ if (process.platform === 'win32') {
261
+ const ext = path.extname(item.name).toLowerCase();
262
+ const base = path.basename(item.name, ext);
263
+ if (!base) continue;
264
+ if (ext && !allowedExts.has(ext)) continue;
265
+ names.add(base);
266
+ } else {
267
+ names.add(item.name);
268
+ }
269
+ }
270
+ } catch (err) {
271
+ // Ignore unreadable PATH entries.
272
+ }
273
+ }
274
+ return names;
275
+ }
276
+
277
+ const PATH_COMMANDS = collectPathCommands();
278
+
279
+ function findBestMatch(candidates, target, maxDistance = MAX_DISTANCE) {
280
+ if (!candidates || !target) return null;
281
+ let best = null;
282
+ let bestDistance = maxDistance + 1;
283
+ let ties = [];
284
+ for (const candidate of candidates || []) {
285
+ const dist = damerauLevenshtein(candidate, target, maxDistance);
286
+ if (dist < bestDistance) {
287
+ best = candidate;
288
+ bestDistance = dist;
289
+ ties = [candidate];
290
+ } else if (dist === bestDistance) {
291
+ ties.push(candidate);
292
+ }
293
+ }
294
+ if (!best || bestDistance > maxDistance) return null;
295
+ if (ties.length > 1) {
296
+ const preferred = ties.filter((value) => PRIORITY_BASES.has(normalizeToken(value)));
297
+ if (preferred.length === 1) {
298
+ return { match: preferred[0], distance: bestDistance };
299
+ }
300
+ return null;
301
+ }
302
+ return { match: best, distance: bestDistance };
303
+ }
304
+
305
+ function parseSuggestion(text) {
306
+ for (const pattern of suggestionPatterns) {
307
+ const match = text.match(pattern);
308
+ if (match && match[1]) {
309
+ return match[1];
310
+ }
311
+ }
312
+ return null;
313
+ }
314
+
315
+ function run(cmd, args) {
316
+ const result = spawnSync(cmd, args, {
317
+ encoding: 'utf8',
318
+ stdio: ['inherit', 'pipe', 'pipe']
319
+ });
320
+ return {
321
+ code: typeof result.status === 'number' ? result.status : result.error ? 1 : 0,
322
+ stdout: result.stdout || '',
323
+ stderr: result.stderr || '',
324
+ error: result.error
325
+ };
326
+ }
327
+
328
+ function logFix(from, to) {
329
+ process.stderr.write(`fuzzrun: auto-correcting "${from}" -> "${to}"\n`);
330
+ }
331
+
332
+ function hasRiskyArgs(args) {
333
+ return args.some((arg) => RISKY_ARG_PATTERNS.some((pattern) => pattern.test(arg)));
334
+ }
335
+
336
+ function tryBaseCorrection(command, args) {
337
+ if (hasRiskyArgs(args)) return null;
338
+ const suggestion = findBestMatch(PATH_COMMANDS, command, MAX_DISTANCE);
339
+ if (suggestion && !DANGEROUS_BASE.has(suggestion.match) && suggestion.match !== command) {
340
+ logFix(command, suggestion.match);
341
+ return {
342
+ command: suggestion.match,
343
+ args,
344
+ result: run(suggestion.match, args)
345
+ };
346
+ }
347
+ return null;
348
+ }
349
+
350
+ function trySubcommandCorrection(command, args, combinedOutput) {
351
+ if (!SAFE_SUBCOMMAND_BASES.has(command) && !ALLOW_ANY_SUBCOMMANDS) return null;
352
+ if (!args.length) return null;
353
+ const attemptedSub = args[0];
354
+ if (attemptedSub.startsWith('-')) return null;
355
+ if (hasRiskyArgs(args)) return null;
356
+ const fromOutput = parseSuggestion(combinedOutput);
357
+ const candidates = COMMON_SUBCOMMANDS[command] || [];
358
+ const fromDict = findBestMatch(candidates, attemptedSub, MAX_DISTANCE);
359
+ const outputDistance = fromOutput
360
+ ? damerauLevenshtein(fromOutput, attemptedSub, MAX_DISTANCE)
361
+ : MAX_DISTANCE + 1;
362
+ const choice = outputDistance <= MAX_DISTANCE ? fromOutput : fromDict ? fromDict.match : null;
363
+
364
+ if (choice && choice !== attemptedSub && damerauLevenshtein(choice, attemptedSub, MAX_DISTANCE) <= MAX_DISTANCE) {
365
+ logFix(`${command} ${attemptedSub}`, `${command} ${choice}`);
366
+ return run(command, [choice, ...args.slice(1)]);
367
+ }
368
+ return null;
369
+ }
370
+
371
+ function findPackageJson(startDir) {
372
+ let current = startDir;
373
+ while (current && current !== path.dirname(current)) {
374
+ const candidate = path.join(current, 'package.json');
375
+ if (fs.existsSync(candidate)) return candidate;
376
+ current = path.dirname(current);
377
+ }
378
+ return null;
379
+ }
380
+
381
+ function getPackageScripts(cwd) {
382
+ const pkgPath = findPackageJson(cwd);
383
+ if (!pkgPath) return [];
384
+ try {
385
+ const raw = fs.readFileSync(pkgPath, 'utf8');
386
+ const parsed = JSON.parse(raw);
387
+ return Object.keys(parsed.scripts || {});
388
+ } catch (err) {
389
+ return [];
390
+ }
391
+ }
392
+
393
+ function isScriptError(output) {
394
+ return SCRIPT_ERROR_PATTERNS.some((pattern) => pattern.test(output));
395
+ }
396
+
397
+ function tryScriptCorrection(command, args, combinedOutput) {
398
+ if (!SCRIPT_BASES.has(command)) return null;
399
+ if (args.length < 2) return null;
400
+ if (args[0] !== 'run') return null;
401
+ const scriptName = args[1];
402
+ if (!scriptName || scriptName.startsWith('-')) return null;
403
+ if (!isScriptError(combinedOutput)) return null;
404
+ if (hasRiskyArgs(args)) return null;
405
+
406
+ const scripts = getPackageScripts(process.cwd());
407
+ const match = findBestMatch(scripts, scriptName, MAX_DISTANCE);
408
+ if (match) {
409
+ logFix(`${command} run ${scriptName}`, `${command} run ${match.match}`);
410
+ return run(command, ['run', match.match, ...args.slice(2)]);
411
+ }
412
+ return null;
413
+ }
414
+
415
+ function getGitBranches() {
416
+ const result = spawnSync('git', ['branch', '--format=%(refname:short)'], { encoding: 'utf8' });
417
+ if (result.status !== 0) return [];
418
+ return (result.stdout || '')
419
+ .split(/\r?\n/)
420
+ .map((line) => line.trim())
421
+ .filter(Boolean);
422
+ }
423
+
424
+ function tryGitBranchCorrection(command, args, combinedOutput) {
425
+ if (command !== 'git') return null;
426
+ if (args.length < 2) return null;
427
+ const subcommand = args[0];
428
+ if (subcommand !== 'checkout' && subcommand !== 'switch') return null;
429
+ const branch = args[1];
430
+ if (!branch || branch.startsWith('-')) return null;
431
+ if (!GIT_PATHSPEC_PATTERN.test(combinedOutput)) return null;
432
+ if (hasRiskyArgs(args)) return null;
433
+
434
+ const branches = getGitBranches();
435
+ const match = findBestMatch(branches, branch, MAX_DISTANCE);
436
+ if (match) {
437
+ logFix(`${command} ${subcommand} ${branch}`, `${command} ${subcommand} ${match.match}`);
438
+ return run(command, [subcommand, match.match, ...args.slice(2)]);
439
+ }
440
+ return null;
441
+ }
442
+
443
+ function main() {
444
+ const argv = process.argv.slice(2);
445
+ if (!argv.length) {
446
+ process.stderr.write('Usage: fuzzrun <command> [args...]\n');
447
+ process.exit(1);
448
+ }
449
+
450
+ const baseCommand = argv[0];
451
+ const rest = argv.slice(1);
452
+ const firstRun = run(baseCommand, rest);
453
+
454
+ if (firstRun.error && firstRun.error.code === 'ENOENT') {
455
+ const corrected = tryBaseCorrection(baseCommand, rest);
456
+ if (corrected) {
457
+ const { result } = corrected;
458
+ if (result.code !== 0) {
459
+ const combinedOutput = `${result.stderr}\n${result.stdout}`;
460
+ const correctedSub = trySubcommandCorrection(corrected.command, corrected.args, combinedOutput);
461
+ if (correctedSub) {
462
+ process.stdout.write(correctedSub.stdout);
463
+ process.stderr.write(correctedSub.stderr);
464
+ process.exit(correctedSub.code);
465
+ }
466
+ const correctedScript = tryScriptCorrection(corrected.command, corrected.args, combinedOutput);
467
+ if (correctedScript) {
468
+ process.stdout.write(correctedScript.stdout);
469
+ process.stderr.write(correctedScript.stderr);
470
+ process.exit(correctedScript.code);
471
+ }
472
+ const correctedBranch = tryGitBranchCorrection(corrected.command, corrected.args, combinedOutput);
473
+ if (correctedBranch) {
474
+ process.stdout.write(correctedBranch.stdout);
475
+ process.stderr.write(correctedBranch.stderr);
476
+ process.exit(correctedBranch.code);
477
+ }
478
+ }
479
+ process.stdout.write(result.stdout);
480
+ process.stderr.write(result.stderr);
481
+ process.exit(result.code);
482
+ }
483
+ process.stderr.write(firstRun.error.message ? `${firstRun.error.message}\n` : `fuzzrun: command not found: ${baseCommand}\n`);
484
+ process.exit(firstRun.code);
485
+ }
486
+
487
+ if (firstRun.code === 0) {
488
+ process.stdout.write(firstRun.stdout);
489
+ process.stderr.write(firstRun.stderr);
490
+ process.exit(0);
491
+ }
492
+
493
+ const combinedOutput = `${firstRun.stderr}\n${firstRun.stdout}`;
494
+ const correctedSub = trySubcommandCorrection(baseCommand, rest, combinedOutput);
495
+ if (correctedSub) {
496
+ process.stdout.write(correctedSub.stdout);
497
+ process.stderr.write(correctedSub.stderr);
498
+ process.exit(correctedSub.code);
499
+ }
500
+
501
+ const correctedScript = tryScriptCorrection(baseCommand, rest, combinedOutput);
502
+ if (correctedScript) {
503
+ process.stdout.write(correctedScript.stdout);
504
+ process.stderr.write(correctedScript.stderr);
505
+ process.exit(correctedScript.code);
506
+ }
507
+
508
+ const correctedBranch = tryGitBranchCorrection(baseCommand, rest, combinedOutput);
509
+ if (correctedBranch) {
510
+ process.stdout.write(correctedBranch.stdout);
511
+ process.stderr.write(correctedBranch.stderr);
512
+ process.exit(correctedBranch.code);
513
+ }
514
+
515
+ process.stdout.write(firstRun.stdout);
516
+ process.stderr.write(firstRun.stderr);
517
+ process.exit(firstRun.code);
518
+ }
519
+
520
+ if (require.main === module) {
521
+ main();
522
+ }
523
+
524
+ module.exports = { main };