Skip to content

Commit 3c62ee2

Browse files
fix: harden Windows compatibility validation
1 parent f025cfb commit 3c62ee2

8 files changed

Lines changed: 135 additions & 77 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,5 @@ jobs:
3434
- name: Smoke test - help
3535
run: bun run src/cli.ts --help
3636

37-
- name: Smoke test - dry-run (Unix)
38-
if: runner.os != 'Windows'
39-
run: bun run src/cli.ts --dir ${{ github.workspace }} --dry-run --hide-errors
40-
41-
- name: Smoke test - dry-run (Windows)
42-
if: runner.os == 'Windows'
43-
run: bun run src/cli.ts --dir ${{ github.workspace }} --dry-run --hide-errors
44-
shell: pwsh
37+
- name: Smoke test - dry-run
38+
run: bun run src/cli.ts --dir "${{ github.workspace }}" --dry-run --hide-errors

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ jobs:
2121
run: bun install
2222

2323
- name: Run tests
24-
run: bun test
24+
run: bun run test
2525

2626
- name: Build package
2727
run: bun run build
2828

2929
- name: Publish to npm
3030
run: bun publish
3131
env:
32-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
32+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,14 @@ Search filters the already loaded list, so you can quickly narrow large result s
114114
| Platform | Status |
115115
|---|---|
116116
| macOS | ✅ Tested |
117-
| Linux | ⚠️ Not tested yet |
118-
| Windows 10/11 |Tested (Windows Terminal recommended) |
117+
| Linux | ✅ CI smoke tested |
118+
| Windows 10/11 |CI smoke tested (Windows Terminal recommended) |
119119

120120
> **Windows note:** Interactive mode requires a terminal that supports ANSI escape codes and raw mode.
121121
> [Windows Terminal](https://aka.ms/terminal) works well. The legacy `cmd.exe` prompt is not supported.
122122
123+
CI covers help output, type checking, builds, and dry-run scanning. Manual interactive terminal testing is still useful across terminals.
124+
123125
## Performance
124126

125127
Recent real scan on `--dir /Users/himanshum`:

benchmark/three-way-benchmark.ts

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* This is now JS/Bun-only.
77
*/
88

9-
import { join } from "node:path";
9+
import { join, resolve } from "node:path";
1010
import { scan } from "../src/scanner.ts";
1111

1212
const args = Bun.argv.slice(2);
@@ -34,20 +34,46 @@ function shouldSkip(p: string): boolean {
3434
);
3535
}
3636

37+
function shellQuote(value: string): string {
38+
return `'${value.replaceAll("'", "'\\''")}'`;
39+
}
40+
3741
/** Convert a Windows absolute path to a WSL /mnt/... path. */
3842
function toWslPath(winPath: string): string {
39-
// e.g. C:\Users\foo → /mnt/c/Users/foo
40-
return winPath
41-
.replace(/^([A-Za-z]):\\/, (_, drive) => `/mnt/${drive.toLowerCase()}/`)
42-
.replaceAll("\\", "/");
43+
const normalizedPath = resolve(winPath).replaceAll("\\", "/");
44+
const driveMatch = /^([A-Za-z]):\/(.*)$/.exec(normalizedPath);
45+
if (!driveMatch?.[1]) {
46+
return normalizedPath;
47+
}
48+
49+
const drive = driveMatch[1].toLowerCase();
50+
const rest = driveMatch[2] ?? "";
51+
return `/mnt/${drive}/${rest}`;
52+
}
53+
54+
async function hasWslNpkill(): Promise<boolean> {
55+
const probe = Bun.spawn({
56+
cmd: [
57+
"wsl",
58+
"bash",
59+
"-lc",
60+
"command -v script >/dev/null && command -v npkill >/dev/null",
61+
],
62+
stdout: "ignore",
63+
stderr: "ignore",
64+
});
65+
66+
return await probe.exited === 0;
4367
}
4468

4569
async function npkillScan(dir: string, useWsl = false): Promise<number> {
46-
// Use login shell so node/npkill are in PATH inside WSL.
47-
// Only silence stderr (2>/dev/null) so stdout flows to proc.stdout for parsing.
48-
// Terminal corruption from npkill's \r sequences is handled by printRow's \r\x1b[2K.
70+
if (useWsl && !(await hasWslNpkill())) {
71+
return -1;
72+
}
73+
74+
const wslCommand = `npkill -d ${shellQuote(toWslPath(dir))} -nu --hide-errors`;
4975
const scriptCmd = useWsl
50-
? ["wsl", "bash", "-lc", `script -q /dev/null -c 'npkill -d "${toWslPath(dir)}" -nu --hide-errors' 2>/dev/null`]
76+
? ["wsl", "bash", "-lc", `script -q /dev/null -c ${shellQuote(wslCommand)} 2>/dev/null`]
5177
: ["script", "-q", "/dev/null", "npkill", "-d", dir, "-nu", "--hide-errors"];
5278

5379
try {
@@ -76,6 +102,10 @@ async function npkillScan(dir: string, useWsl = false): Promise<number> {
76102
clearTimeout(killTimer);
77103
try { proc.kill(); } catch {}
78104

105+
if (await proc.exited !== 0) {
106+
return -1;
107+
}
108+
79109
const raw = Buffer.concat(chunks).toString("utf8");
80110
const clean = raw.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\r/g, "");
81111

@@ -99,22 +129,23 @@ async function bunGlobScan(dir: string): Promise<number> {
99129
onlyFiles: false,
100130
followSymlinks: false,
101131
})) {
102-
// Normalize to forward slashes for consistent checks
103132
const normalized = relative.replaceAll("\\", "/");
104133
const fullPath = join(dir, relative);
105134
const fullNormalized = fullPath.replaceAll("\\", "/");
106135

107-
// Skip nested node_modules (same as bunkill)
108-
if (fullNormalized.includes("/node_modules/")) continue;
136+
const targetSegments = normalized.split("/")
137+
.filter((segment) => segment === "node_modules");
138+
if (targetSegments.length > 1) continue;
109139
if (shouldSkip(fullNormalized)) continue;
110140

111141
const depth = normalized.split("/").filter(Boolean).length;
112142
if (depth > MAX_DEPTH) continue;
113143

114-
// Require a package.json in the parent directory (same as bunkill)
115-
const parentPath = fullPath.slice(0, fullPath.length - "/node_modules".length);
116-
const pkgJson = Bun.file(join(parentPath, "package.json"));
117-
if (!(await pkgJson.exists())) continue;
144+
const projectPath = join(fullPath, "..");
145+
const packageJson = await Bun.file(join(projectPath, "package.json"))
146+
.json()
147+
.catch(() => null);
148+
if (!packageJson) continue;
118149

119150
results.push(fullPath);
120151
}
@@ -219,17 +250,19 @@ const npkillPath = isWindows
219250
? await Bun.$.nothrow()`where npkill`.text().then((s) => s.trim().split("\n")[0]?.trim() ?? "").catch(() => "")
220251
: await Bun.$.nothrow()`which npkill`.text().then((s) => s.trim()).catch(() => "");
221252

222-
// Detect WSL (Windows only)
223253
const wslAvailable = isWindows
224254
? await Bun.$.nothrow()`where wsl`.text().then((s) => s.trim().length > 0).catch(() => false)
225255
: false;
256+
const wslNpkillAvailable = isWindows && wslAvailable
257+
? await hasWslNpkill()
258+
: false;
226259

227260
const npkillStatus = !npkillPath
228261
? "NOT FOUND (npm install -g npkill)"
229-
: isWindows && wslAvailable
262+
: isWindows && wslNpkillAvailable
230263
? `${npkillPath} (via WSL)`
231264
: isWindows
232-
? `${npkillPath} (skipped: WSL not found — install WSL to enable)`
265+
? `${npkillPath} (skipped: WSL npkill/script unavailable)`
233266
: npkillPath;
234267

235268
console.log(` npkill : ${npkillStatus}`);
@@ -244,13 +277,13 @@ const [globResult, bunkillResult] = await Promise.all([
244277
]);
245278

246279
// On Windows: run npkill via WSL if available; on Unix: run directly
247-
const canRunNpkill = npkillPath && (!isWindows || wslAvailable);
280+
const canRunNpkill = npkillPath && (!isWindows || wslNpkillAvailable);
248281
const npkillResult = canRunNpkill
249282
? await bench("npkill", (d) => npkillScan(d, isWindows && wslAvailable), SCAN_DIR, 1)
250283
: {
251284
label: "npkill",
252285
avgMs: 0, minMs: 0, maxMs: 0, found: -1, available: false,
253-
note: !npkillPath ? "not installed" : "WSL not found (install WSL to enable)",
286+
note: !npkillPath ? "not installed" : "WSL npkill/script unavailable",
254287
};
255288

256289
const available = [npkillResult, globResult, bunkillResult].filter((r) => r.available);

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
},
2323
"devDependencies": {
2424
"@types/bun": "^1.3.10",
25-
"@types/node": "^24.5.2"
25+
"@types/node": "^24.5.2",
26+
"typescript": "^6.0.3"
2627
},
2728
"scripts": {
2829
"test": "bun run src/cli.ts --help",

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const APP_CONFIG = {
1212
: 8;
1313
return Math.max(4, Math.min(16, cpuCount || 8));
1414
})(),
15+
defaultDeleteConcurrency: 8,
1516
} as const;
1617

1718
export const SCAN_PATHS = {

0 commit comments

Comments
 (0)