fuzzrunx 0.1.0 → 0.1.1
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 +16 -6
- package/package.json +3 -2
- package/scripts/postinstall.js +18 -0
- package/src/cli.js +28 -6
- package/src/installer.js +138 -0
package/README.md
CHANGED
|
@@ -13,10 +13,15 @@ Install (npm):
|
|
|
13
13
|
```
|
|
14
14
|
npm i -g fuzzrunx
|
|
15
15
|
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
|
|
17
|
+
On global install, FuzzRun auto-enables shell hooks and will print:
|
|
18
|
+
`FuzzRun is automatically enabled. Run "fuzzrun disable" to deactivate.`
|
|
19
|
+
|
|
20
|
+
If you want to skip auto-enable, set `FUZZRUN_SKIP_ENABLE=1` during install.
|
|
21
|
+
|
|
22
|
+
### Bash/Zsh hook (auto-run on typos)
|
|
23
|
+
|
|
24
|
+
Add to your shell rc:
|
|
20
25
|
|
|
21
26
|
```bash
|
|
22
27
|
FUZZRUN_BIN="/absolute/path/to/bin/fuzzrun.js"
|
|
@@ -38,8 +43,13 @@ $ExecutionContext.InvokeCommand.CommandNotFoundAction = {
|
|
|
38
43
|
param($commandName, $eventArgs)
|
|
39
44
|
fuzzrun $commandName @($eventArgs.Arguments)
|
|
40
45
|
}
|
|
41
|
-
function global:git { fuzzrun git @args } # optional git wrapper
|
|
42
|
-
```
|
|
46
|
+
function global:git { fuzzrun git @args } # optional git wrapper
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Manage hooks
|
|
50
|
+
- `fuzzrun enable` (add hooks to your shell profile)
|
|
51
|
+
- `fuzzrun disable` (remove hooks)
|
|
52
|
+
- `fuzzrun status` (show which profiles are enabled)
|
|
43
53
|
|
|
44
54
|
### How it works
|
|
45
55
|
- 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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fuzzrunx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Auto-correct mistyped commands and subcommands and re-run them safely.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fuzzrun": "bin/fuzzrun.js"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"type": "commonjs",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"start": "node bin/fuzzrun.js"
|
|
11
|
+
"start": "node bin/fuzzrun.js",
|
|
12
|
+
"postinstall": "node scripts/postinstall.js"
|
|
12
13
|
}
|
|
13
14
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const installer = require('../src/installer');
|
|
4
|
+
|
|
5
|
+
const skip = process.env.FUZZRUN_SKIP_ENABLE === '1';
|
|
6
|
+
const isGlobal = process.env.npm_config_global === 'true' || process.env.npm_config_global === '1';
|
|
7
|
+
|
|
8
|
+
if (skip || !isGlobal) {
|
|
9
|
+
process.stdout.write('fuzzrun: auto-enable skipped\n');
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
installer.enable({});
|
|
15
|
+
process.stdout.write('FuzzRun is automatically enabled. Run "fuzzrun disable" to deactivate.\n');
|
|
16
|
+
} catch (err) {
|
|
17
|
+
process.stdout.write(`fuzzrun: auto-enable failed: ${err.message}\n`);
|
|
18
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const { spawnSync } = require('child_process');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const installer = require('./installer');
|
|
9
10
|
|
|
10
11
|
const MAX_DISTANCE = Number.isFinite(Number(process.env.FUZZRUN_MAX_DISTANCE))
|
|
11
12
|
? Math.max(1, Number(process.env.FUZZRUN_MAX_DISTANCE))
|
|
@@ -444,12 +445,33 @@ function main() {
|
|
|
444
445
|
const argv = process.argv.slice(2);
|
|
445
446
|
if (!argv.length) {
|
|
446
447
|
process.stderr.write('Usage: fuzzrun <command> [args...]\n');
|
|
447
|
-
process.exit(1);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
|
|
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
|
+
const baseCommand = argv[0];
|
|
473
|
+
const rest = argv.slice(1);
|
|
474
|
+
const firstRun = run(baseCommand, rest);
|
|
453
475
|
|
|
454
476
|
if (firstRun.error && firstRun.error.code === 'ENOENT') {
|
|
455
477
|
const corrected = tryBaseCorrection(baseCommand, rest);
|
package/src/installer.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const MARKER_START = '# >>> fuzzrun start';
|
|
8
|
+
const MARKER_END = '# <<< fuzzrun end';
|
|
9
|
+
|
|
10
|
+
const WRAP_BASES = ['git', 'npm', 'yarn', 'pnpm', 'pip', 'docker', 'kubectl', 'gh'];
|
|
11
|
+
|
|
12
|
+
function escapeRegex(value) {
|
|
13
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const BLOCK_REGEX = new RegExp(`${escapeRegex(MARKER_START)}[\\s\\S]*?${escapeRegex(MARKER_END)}\\s*`, 'g');
|
|
17
|
+
|
|
18
|
+
function getPackageRoot(explicitRoot) {
|
|
19
|
+
if (explicitRoot) return explicitRoot;
|
|
20
|
+
return path.resolve(__dirname, '..');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getBinPath(packageRoot) {
|
|
24
|
+
return path.resolve(packageRoot, 'bin', 'fuzzrun.js');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getProfileTargets() {
|
|
28
|
+
const home = os.homedir();
|
|
29
|
+
if (process.platform === 'win32') {
|
|
30
|
+
return [
|
|
31
|
+
path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
32
|
+
path.join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1')
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
return [path.join(home, '.bashrc'), path.join(home, '.zshrc')];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ensureDir(filePath) {
|
|
39
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripBlock(content) {
|
|
43
|
+
return content.replace(BLOCK_REGEX, '').trimEnd();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildPowerShellSnippet(binPath) {
|
|
47
|
+
const lines = [
|
|
48
|
+
MARKER_START,
|
|
49
|
+
`$fuzzrun = "${binPath}"`,
|
|
50
|
+
'function global:fuzzrun { node $fuzzrun @args }',
|
|
51
|
+
'$ExecutionContext.InvokeCommand.CommandNotFoundAction = {',
|
|
52
|
+
' param($commandName, $eventArgs)',
|
|
53
|
+
' fuzzrun $commandName @($eventArgs.Arguments)',
|
|
54
|
+
'}'
|
|
55
|
+
];
|
|
56
|
+
for (const base of WRAP_BASES) {
|
|
57
|
+
lines.push(`function global:${base} { fuzzrun ${base} @args }`);
|
|
58
|
+
}
|
|
59
|
+
lines.push(MARKER_END, '');
|
|
60
|
+
return lines.join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildUnixSnippet(binPath) {
|
|
64
|
+
const lines = [
|
|
65
|
+
MARKER_START,
|
|
66
|
+
`FUZZRUN_BIN="${binPath}"`,
|
|
67
|
+
'fuzzrun() { node "$FUZZRUN_BIN" "$@"; }',
|
|
68
|
+
'command_not_found_handle() { fuzzrun "$@"; }',
|
|
69
|
+
'command_not_found_handler() { fuzzrun "$@"; }'
|
|
70
|
+
];
|
|
71
|
+
for (const base of WRAP_BASES) {
|
|
72
|
+
lines.push(`${base}() { fuzzrun ${base} "$@"; }`);
|
|
73
|
+
}
|
|
74
|
+
lines.push(MARKER_END, '');
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateProfile(filePath, snippet) {
|
|
79
|
+
const exists = fs.existsSync(filePath);
|
|
80
|
+
const content = exists ? fs.readFileSync(filePath, 'utf8') : '';
|
|
81
|
+
const cleaned = stripBlock(content);
|
|
82
|
+
const spacer = cleaned.length ? '\n\n' : '';
|
|
83
|
+
const nextContent = `${cleaned}${spacer}${snippet}`;
|
|
84
|
+
ensureDir(filePath);
|
|
85
|
+
fs.writeFileSync(filePath, nextContent, 'utf8');
|
|
86
|
+
return { updated: content !== nextContent, path: filePath };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function removeProfileSnippet(filePath) {
|
|
90
|
+
if (!fs.existsSync(filePath)) {
|
|
91
|
+
return { updated: false, path: filePath };
|
|
92
|
+
}
|
|
93
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
94
|
+
const cleaned = stripBlock(content);
|
|
95
|
+
if (cleaned === content) {
|
|
96
|
+
return { updated: false, path: filePath };
|
|
97
|
+
}
|
|
98
|
+
ensureDir(filePath);
|
|
99
|
+
fs.writeFileSync(filePath, cleaned ? `${cleaned}\n` : '', 'utf8');
|
|
100
|
+
return { updated: true, path: filePath };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pickTargets() {
|
|
104
|
+
const targets = getProfileTargets();
|
|
105
|
+
const existing = targets.filter((target) => fs.existsSync(target));
|
|
106
|
+
if (existing.length) return existing;
|
|
107
|
+
return [targets[0]];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function enable({ packageRoot } = {}) {
|
|
111
|
+
const root = getPackageRoot(packageRoot);
|
|
112
|
+
const binPath = getBinPath(root);
|
|
113
|
+
const targets = pickTargets();
|
|
114
|
+
const snippet = process.platform === 'win32' ? buildPowerShellSnippet(binPath) : buildUnixSnippet(binPath);
|
|
115
|
+
return targets.map((target) => updateProfile(target, snippet));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function disable() {
|
|
119
|
+
const targets = getProfileTargets().filter((target) => fs.existsSync(target));
|
|
120
|
+
return targets.map((target) => removeProfileSnippet(target));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function status() {
|
|
124
|
+
const targets = getProfileTargets();
|
|
125
|
+
return targets.map((target) => {
|
|
126
|
+
if (!fs.existsSync(target)) {
|
|
127
|
+
return { path: target, enabled: false };
|
|
128
|
+
}
|
|
129
|
+
const content = fs.readFileSync(target, 'utf8');
|
|
130
|
+
return { path: target, enabled: content.includes(MARKER_START) && content.includes(MARKER_END) };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
enable,
|
|
136
|
+
disable,
|
|
137
|
+
status
|
|
138
|
+
};
|