fuzzrunx 0.1.6 → 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/installer.js CHANGED
@@ -1,201 +1,201 @@
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
- const roots = new Set([path.join(home, 'Documents')]);
31
- const oneDriveRoots = [
32
- process.env.OneDrive,
33
- process.env.OneDriveConsumer,
34
- process.env.OneDriveCommercial
35
- ].filter(Boolean);
36
- for (const root of oneDriveRoots) {
37
- roots.add(path.basename(root).toLowerCase() === 'documents' ? root : path.join(root, 'Documents'));
38
- }
39
- const targets = [];
40
- for (const root of roots) {
41
- targets.push(path.join(root, 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
42
- targets.push(path.join(root, 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
43
- }
44
- return [...new Set(targets)];
45
- }
46
- return [path.join(home, '.bashrc'), path.join(home, '.zshrc')];
47
- }
48
-
49
- function ensureDir(filePath) {
50
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
- }
52
-
53
- function stripBlock(content) {
54
- return content.replace(BLOCK_REGEX, '').trimEnd();
55
- }
56
-
57
- function buildPowerShellSnippet(binPath) {
58
- const lines = [
59
- MARKER_START,
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
- '}',
75
- 'function global:fuzzrun { node $fuzzrun @args }',
76
- '$ExecutionContext.InvokeCommand.CommandNotFoundAction = {',
77
- ' param($commandName, $eventArgs)',
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',
109
- '}'
110
- ];
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('}');
119
- lines.push(MARKER_END, '');
120
- return lines.join('\n');
121
- }
122
-
123
- function buildUnixSnippet(binPath) {
124
- const lines = [
125
- MARKER_START,
126
- `FUZZRUN_BIN="${binPath}"`,
127
- 'fuzzrun() { node "$FUZZRUN_BIN" "$@"; }',
128
- 'command_not_found_handle() { fuzzrun "$@"; }',
129
- 'command_not_found_handler() { fuzzrun "$@"; }'
130
- ];
131
- for (const base of WRAP_BASES) {
132
- lines.push(`${base}() { fuzzrun ${base} "$@"; }`);
133
- }
134
- lines.push(MARKER_END, '');
135
- return lines.join('\n');
136
- }
137
-
138
- function updateProfile(filePath, snippet) {
139
- const exists = fs.existsSync(filePath);
140
- const content = exists ? fs.readFileSync(filePath, 'utf8') : '';
141
- const cleaned = stripBlock(content);
142
- const spacer = cleaned.length ? '\n\n' : '';
143
- const nextContent = `${cleaned}${spacer}${snippet}`;
144
- ensureDir(filePath);
145
- fs.writeFileSync(filePath, nextContent, 'utf8');
146
- return { updated: content !== nextContent, path: filePath };
147
- }
148
-
149
- function removeProfileSnippet(filePath) {
150
- if (!fs.existsSync(filePath)) {
151
- return { updated: false, path: filePath };
152
- }
153
- const content = fs.readFileSync(filePath, 'utf8');
154
- const cleaned = stripBlock(content);
155
- if (cleaned === content) {
156
- return { updated: false, path: filePath };
157
- }
158
- ensureDir(filePath);
159
- fs.writeFileSync(filePath, cleaned ? `${cleaned}\n` : '', 'utf8');
160
- return { updated: true, path: filePath };
161
- }
162
-
163
- function pickTargets() {
164
- const targets = getProfileTargets();
165
- if (process.platform === 'win32') {
166
- return targets;
167
- }
168
- const existing = targets.filter((target) => fs.existsSync(target));
169
- if (existing.length) return existing;
170
- return [targets[0]];
171
- }
172
-
173
- function enable({ packageRoot } = {}) {
174
- const root = getPackageRoot(packageRoot);
175
- const binPath = getBinPath(root);
176
- const targets = pickTargets();
177
- const snippet = process.platform === 'win32' ? buildPowerShellSnippet(binPath) : buildUnixSnippet(binPath);
178
- return targets.map((target) => updateProfile(target, snippet));
179
- }
180
-
181
- function disable() {
182
- const targets = getProfileTargets().filter((target) => fs.existsSync(target));
183
- return targets.map((target) => removeProfileSnippet(target));
184
- }
185
-
186
- function status() {
187
- const targets = getProfileTargets();
188
- return targets.map((target) => {
189
- if (!fs.existsSync(target)) {
190
- return { path: target, enabled: false };
191
- }
192
- const content = fs.readFileSync(target, 'utf8');
193
- return { path: target, enabled: content.includes(MARKER_START) && content.includes(MARKER_END) };
194
- });
195
- }
196
-
197
- module.exports = {
198
- enable,
199
- disable,
200
- status
201
- };
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
+ const roots = new Set([path.join(home, 'Documents')]);
31
+ const oneDriveRoots = [
32
+ process.env.OneDrive,
33
+ process.env.OneDriveConsumer,
34
+ process.env.OneDriveCommercial
35
+ ].filter(Boolean);
36
+ for (const root of oneDriveRoots) {
37
+ roots.add(path.basename(root).toLowerCase() === 'documents' ? root : path.join(root, 'Documents'));
38
+ }
39
+ const targets = [];
40
+ for (const root of roots) {
41
+ targets.push(path.join(root, 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
42
+ targets.push(path.join(root, 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
43
+ }
44
+ return [...new Set(targets)];
45
+ }
46
+ return [path.join(home, '.bashrc'), path.join(home, '.zshrc')];
47
+ }
48
+
49
+ function ensureDir(filePath) {
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ }
52
+
53
+ function stripBlock(content) {
54
+ return content.replace(BLOCK_REGEX, '').trimEnd();
55
+ }
56
+
57
+ function buildPowerShellSnippet(binPath) {
58
+ const lines = [
59
+ MARKER_START,
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
+ '}',
75
+ 'function global:fuzzrun { node $fuzzrun @args }',
76
+ '$ExecutionContext.InvokeCommand.CommandNotFoundAction = {',
77
+ ' param($commandName, $eventArgs)',
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',
109
+ '}'
110
+ ];
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('}');
119
+ lines.push(MARKER_END, '');
120
+ return lines.join('\n');
121
+ }
122
+
123
+ function buildUnixSnippet(binPath) {
124
+ const lines = [
125
+ MARKER_START,
126
+ `FUZZRUN_BIN="${binPath}"`,
127
+ 'fuzzrun() { node "$FUZZRUN_BIN" "$@"; }',
128
+ 'command_not_found_handle() { fuzzrun "$@"; }',
129
+ 'command_not_found_handler() { fuzzrun "$@"; }'
130
+ ];
131
+ for (const base of WRAP_BASES) {
132
+ lines.push(`${base}() { fuzzrun ${base} "$@"; }`);
133
+ }
134
+ lines.push(MARKER_END, '');
135
+ return lines.join('\n');
136
+ }
137
+
138
+ function updateProfile(filePath, snippet) {
139
+ const exists = fs.existsSync(filePath);
140
+ const content = exists ? fs.readFileSync(filePath, 'utf8') : '';
141
+ const cleaned = stripBlock(content);
142
+ const spacer = cleaned.length ? '\n\n' : '';
143
+ const nextContent = `${cleaned}${spacer}${snippet}`;
144
+ ensureDir(filePath);
145
+ fs.writeFileSync(filePath, nextContent, 'utf8');
146
+ return { updated: content !== nextContent, path: filePath };
147
+ }
148
+
149
+ function removeProfileSnippet(filePath) {
150
+ if (!fs.existsSync(filePath)) {
151
+ return { updated: false, path: filePath };
152
+ }
153
+ const content = fs.readFileSync(filePath, 'utf8');
154
+ const cleaned = stripBlock(content);
155
+ if (cleaned === content) {
156
+ return { updated: false, path: filePath };
157
+ }
158
+ ensureDir(filePath);
159
+ fs.writeFileSync(filePath, cleaned ? `${cleaned}\n` : '', 'utf8');
160
+ return { updated: true, path: filePath };
161
+ }
162
+
163
+ function pickTargets() {
164
+ const targets = getProfileTargets();
165
+ if (process.platform === 'win32') {
166
+ return targets;
167
+ }
168
+ const existing = targets.filter((target) => fs.existsSync(target));
169
+ if (existing.length) return existing;
170
+ return [targets[0]];
171
+ }
172
+
173
+ function enable({ packageRoot } = {}) {
174
+ const root = getPackageRoot(packageRoot);
175
+ const binPath = getBinPath(root);
176
+ const targets = pickTargets();
177
+ const snippet = process.platform === 'win32' ? buildPowerShellSnippet(binPath) : buildUnixSnippet(binPath);
178
+ return targets.map((target) => updateProfile(target, snippet));
179
+ }
180
+
181
+ function disable() {
182
+ const targets = getProfileTargets().filter((target) => fs.existsSync(target));
183
+ return targets.map((target) => removeProfileSnippet(target));
184
+ }
185
+
186
+ function status() {
187
+ const targets = getProfileTargets();
188
+ return targets.map((target) => {
189
+ if (!fs.existsSync(target)) {
190
+ return { path: target, enabled: false };
191
+ }
192
+ const content = fs.readFileSync(target, 'utf8');
193
+ return { path: target, enabled: content.includes(MARKER_START) && content.includes(MARKER_END) };
194
+ });
195
+ }
196
+
197
+ module.exports = {
198
+ enable,
199
+ disable,
200
+ status
201
+ };
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { spawnSync } = require('node:child_process');
6
+ const fs = require('node:fs');
7
+ const os = require('node:os');
8
+ const path = require('node:path');
9
+
10
+ const BIN_PATH = path.resolve(__dirname, '..', 'bin', 'fuzzrun.js');
11
+
12
+ function makeTempHome() {
13
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'fuzzrun-test-'));
14
+ }
15
+
16
+ function runFuzzrun(args, envOverrides) {
17
+ const env = { ...process.env, ...envOverrides };
18
+ return spawnSync(process.execPath, [BIN_PATH, ...args], {
19
+ encoding: 'utf8',
20
+ env
21
+ });
22
+ }
23
+
24
+ test('skips install banner when FUZZRUN_SKIP_ENABLE=1', () => {
25
+ const home = makeTempHome();
26
+ try {
27
+ const result = runFuzzrun(
28
+ [process.execPath, '-e', "process.stdout.write('ok')"],
29
+ {
30
+ FUZZRUN_SKIP_ENABLE: '1',
31
+ HOME: home,
32
+ USERPROFILE: home
33
+ }
34
+ );
35
+
36
+ assert.equal(result.status, 0);
37
+ assert.equal(result.stdout, 'ok');
38
+ assert.ok(!result.stderr.includes('FuzzRun is automatically enabled'));
39
+ } finally {
40
+ fs.rmSync(home, { recursive: true, force: true });
41
+ }
42
+ });
43
+
44
+ test('prints a friendly message when the command is missing', () => {
45
+ const home = makeTempHome();
46
+ const missing = 'fuzzrun-missing-command-zz9';
47
+ try {
48
+ const result = runFuzzrun([missing], {
49
+ FUZZRUN_SKIP_ENABLE: '1',
50
+ HOME: home,
51
+ USERPROFILE: home,
52
+ PATH: ''
53
+ });
54
+
55
+ assert.notEqual(result.status, 0);
56
+ assert.ok(result.stderr.includes(`fuzzrun: command not found: ${missing}`));
57
+ assert.ok(!result.stderr.includes('spawnSync'));
58
+ } finally {
59
+ fs.rmSync(home, { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ test('stats reports nothing recorded on a fresh state', () => {
64
+ const home = makeTempHome();
65
+ try {
66
+ const result = runFuzzrun(['stats'], {
67
+ FUZZRUN_SKIP_ENABLE: '1',
68
+ HOME: home,
69
+ USERPROFILE: home
70
+ });
71
+
72
+ assert.equal(result.status, 0);
73
+ assert.ok(result.stdout.includes('No corrections recorded yet'));
74
+ } finally {
75
+ fs.rmSync(home, { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ test('explain previews a base fix without running it and records nothing', () => {
80
+ const home = makeTempHome();
81
+ try {
82
+ const result = runFuzzrun(['explain', 'nodee', '-e', "process.stdout.write('SHOULD_NOT_RUN')"], {
83
+ FUZZRUN_SKIP_ENABLE: '1',
84
+ HOME: home,
85
+ USERPROFILE: home
86
+ });
87
+
88
+ assert.ok(result.stderr.includes('would correct'));
89
+ assert.ok(result.stderr.includes('nodee'));
90
+ assert.ok(result.stderr.includes('node'));
91
+ // Dry-run must neither run the command nor persist any fix.
92
+ assert.ok(!result.stdout.includes('SHOULD_NOT_RUN'));
93
+ const statePath = path.join(home, '.fuzzrun', 'state.json');
94
+ if (fs.existsSync(statePath)) {
95
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
96
+ assert.ok(!state.totalFixes);
97
+ }
98
+ } finally {
99
+ fs.rmSync(home, { recursive: true, force: true });
100
+ }
101
+ });
102
+
103
+ test('runs a Windows .cmd shim via the shell instead of failing with ENOENT', { skip: process.platform !== 'win32' }, () => {
104
+ const home = makeTempHome();
105
+ const binDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fuzzrun-bin-'));
106
+ fs.writeFileSync(path.join(binDir, 'frhello.cmd'), '@echo off\r\necho HELLO_FROM_CMD %*\r\n');
107
+ try {
108
+ const result = runFuzzrun(['frhello', 'world'], {
109
+ FUZZRUN_SKIP_ENABLE: '1',
110
+ HOME: home,
111
+ USERPROFILE: home,
112
+ PATH: `${binDir}${path.delimiter}${process.env.PATH}`
113
+ });
114
+
115
+ assert.equal(result.status, 0);
116
+ assert.ok(result.stdout.includes('HELLO_FROM_CMD'));
117
+ assert.ok(result.stdout.includes('world'));
118
+ } finally {
119
+ fs.rmSync(home, { recursive: true, force: true });
120
+ fs.rmSync(binDir, { recursive: true, force: true });
121
+ }
122
+ });
123
+
124
+ test('auto-corrects a mistyped base command and records it for stats', () => {
125
+ const home = makeTempHome();
126
+ try {
127
+ const env = { FUZZRUN_SKIP_ENABLE: '1', HOME: home, USERPROFILE: home, FUZZRUN_YES: '1' };
128
+ const result = runFuzzrun(['nodee', '-e', "process.stdout.write('CORRECTED_OK')"], env);
129
+
130
+ assert.ok(result.stderr.includes('auto-correcting'));
131
+ assert.ok(result.stdout.includes('CORRECTED_OK'));
132
+
133
+ const stats = runFuzzrun(['stats'], env);
134
+ assert.ok(stats.stdout.includes('rescued'));
135
+ assert.ok(stats.stdout.includes('nodee'));
136
+ } finally {
137
+ fs.rmSync(home, { recursive: true, force: true });
138
+ }
139
+ });