-
Notifications
You must be signed in to change notification settings - Fork 5
fix: Windows compatibility for delete, size, and path separators #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
61e3bfb
38e9978
f025cfb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
|
|
||
| jobs: | ||
| test: | ||
| name: Test on ${{ matrix.os }} | ||
| runs-on: ${{ matrix.os }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, macos-latest, windows-latest] | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: oven-sh/setup-bun@v2 | ||
| with: | ||
| bun-version: latest | ||
|
|
||
| - name: Install dependencies | ||
| run: bun install | ||
|
|
||
| - name: Type check | ||
| run: bun run check | ||
|
|
||
| - name: Build | ||
| run: bun run build | ||
|
|
||
| - name: Smoke test - help | ||
| run: bun run src/cli.ts --help | ||
|
|
||
| - name: Smoke test - dry-run (Unix) | ||
| if: runner.os != 'Windows' | ||
| run: bun run src/cli.ts --dir ${{ github.workspace }} --dry-run --hide-errors | ||
|
|
||
| - name: Smoke test - dry-run (Windows) | ||
| if: runner.os == 'Windows' | ||
| run: bun run src/cli.ts --dir ${{ github.workspace }} --dry-run --hide-errors | ||
| shell: pwsh | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,15 +29,22 @@ BunKill scans large directory trees, calculates folder sizes, and lets you delet | |
|
|
||
| ## Requirements | ||
|
|
||
| - Bun is required at runtime | ||
| - macOS is the only platform tested so far | ||
| - [Bun](https://bun.sh) runtime is required | ||
| - Supported platforms: **macOS**, **Linux**, **Windows 10/11** | ||
| - Windows: requires [Windows Terminal](https://aka.ms/terminal) for best interactive UI experience | ||
|
|
||
| Install Bun if needed: | ||
| **Install Bun:** | ||
|
|
||
| macOS / Linux: | ||
| ```bash | ||
| curl -fsSL https://bun.sh/install | bash | ||
| ``` | ||
|
|
||
| Windows (PowerShell): | ||
| ```powershell | ||
| powershell -c "irm bun.sh/install.ps1 | iex" | ||
| ``` | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
|
|
@@ -56,9 +63,12 @@ bun install -g bunkill | |
| # interactive scan in current directory | ||
| bunkill | ||
|
|
||
| # scan a specific directory | ||
| # scan a specific directory (macOS / Linux) | ||
| bunkill --dir ~/Projects | ||
|
|
||
| # scan a specific directory (Windows) | ||
| bunkill --dir "C:\Users\YourName\Projects" | ||
|
|
||
| # preview only | ||
| bunkill --dir ~/Projects --dry-run | ||
|
|
||
|
|
@@ -101,13 +111,14 @@ Search filters the already loaded list, so you can quickly narrow large result s | |
|
|
||
| ## Platform status | ||
|
|
||
| - macOS: tested | ||
| - Linux: not tested yet | ||
| - Windows: not tested yet | ||
|
|
||
| Linux and Windows may work, but they have not been validated in this repo yet. | ||
| | Platform | Status | | ||
| |---|---| | ||
| | macOS | ✅ Tested | | ||
| | Linux | ⚠️ Not tested yet | | ||
| | Windows 10/11 | ✅ Tested (Windows Terminal recommended) | | ||
|
Comment on lines
+114
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align Linux platform status with the new CI matrix. Line 117 currently says Linux is “Not tested yet”, but this PR adds Linux CI runs. Please update wording to avoid contradictory platform claims. 🤖 Prompt for AI Agents
Comment on lines
+114
to
+118
|
||
|
|
||
| Contributions for Linux and Windows testing or fixes are welcome. | ||
| > **Windows note:** Interactive mode requires a terminal that supports ANSI escape codes and raw mode. | ||
| > [Windows Terminal](https://aka.ms/terminal) works well. The legacy `cmd.exe` prompt is not supported. | ||
|
|
||
| ## Performance | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -34,10 +34,25 @@ function shouldSkip(p: string): boolean { | |||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| async function npkillScan(dir: string): Promise<number> { | ||||||||||||||||||||
| /** Convert a Windows absolute path to a WSL /mnt/... path. */ | ||||||||||||||||||||
| function toWslPath(winPath: string): string { | ||||||||||||||||||||
| // e.g. C:\Users\foo → /mnt/c/Users/foo | ||||||||||||||||||||
| return winPath | ||||||||||||||||||||
| .replace(/^([A-Za-z]):\\/, (_, drive) => `/mnt/${drive.toLowerCase()}/`) | ||||||||||||||||||||
| .replaceAll("\\", "/"); | ||||||||||||||||||||
|
Comment on lines
+37
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's see the actual code around lines 37-50 in the file
cat -n benchmark/three-way-benchmark.ts | head -60 | tail -35Repository: codingstark-dev/bunkill Length of output: 1591 🏁 Script executed: # Search for where toWslPath is called and how dir parameter is passed
rg "toWslPath|npkillScan" benchmark/three-way-benchmark.ts -A 2 -B 2Repository: codingstark-dev/bunkill Length of output: 992 🏁 Script executed: # Find where SCAN_DIR is defined
rg "SCAN_DIR|--dir" benchmark/three-way-benchmark.ts -B 2 -A 2Repository: codingstark-dev/bunkill Length of output: 862 🏁 Script executed: # Verify the path resolution behavior by testing the proposed fix logic
python3 << 'EOF'
import re
# Current implementation
def toWslPath_current(winPath: str) -> str:
result = winPath
# Regex only matches backslashes
result = re.sub(r'^([A-Za-z]):\\', lambda m: f"/mnt/{m.group(1).lower()}/", result)
result = result.replace("\\", "/")
return result
# Proposed implementation (simulated on Unix)
def toWslPath_proposed(winPath: str) -> str:
# Simulate resolve() by converting mixed slashes to forward slashes
normalized = winPath.replace("\\", "/")
# Now match forward slash pattern
result = re.sub(r'^([A-Za-z]):/', lambda m: f"/mnt/{m.group(1).lower()}/", normalized)
return result
# Test cases
test_cases = [
".",
"./repo",
"C:\\Users\\foo",
"C:/Users/foo",
"C:\\Users\\foo's\\bar",
]
print("Testing path conversions:")
print("-" * 60)
for case in test_cases:
current = toWslPath_current(case)
proposed = toWslPath_proposed(case)
print(f"Input: {case!r}")
print(f" Current: {current!r}")
print(f" Proposed: {proposed!r}")
print()
EOFRepository: codingstark-dev/bunkill Length of output: 530 🏁 Script executed: # Also verify the shell escaping issue with a simple test
python3 << 'EOF'
def escapeForSingleQuotedShell(value: str) -> str:
return value.replace("'", "'\"'\"'")
# Test with a problematic path
path_with_quote = "/mnt/c/User's Files"
escaped = escapeForSingleQuotedShell(path_with_quote)
print("Shell escaping test:")
print(f"Original path: {path_with_quote!r}")
print(f"Escaped path: {escaped!r}")
print()
print("Bash command construction:")
print(f"Unescaped: bash -lc 'npkill -d \"{path_with_quote}\" -nu'")
print(f"Escaped: bash -lc 'npkill -d \"{escaped}\" -nu'")
EOFRepository: codingstark-dev/bunkill Length of output: 316 Resolve relative paths and escape shell metacharacters in WSL path conversion. The
Use Fix-import { join } from "node:path";
+import { join, resolve } from "node:path";
+
+function escapeForSingleQuotedShell(value: string): string {
+ return value.replaceAll("'", `'"'"'`);
+}
/** Convert a Windows absolute path to a WSL /mnt/... path. */
function toWslPath(winPath: string): string {
- // e.g. C:\Users\foo → /mnt/c/Users/foo
- return winPath
- .replace(/^([A-Za-z]):\\/, (_, drive) => `/mnt/${drive.toLowerCase()}/`)
- .replaceAll("\\", "/");
+ const normalized = resolve(winPath).replaceAll("\\", "/");
+ return normalized.replace(/^([A-Za-z]):\//, (_, drive) => `/mnt/${drive.toLowerCase()}/`);
}
async function npkillScan(dir: string, useWsl = false): Promise<number> {
const scriptCmd = useWsl
- ? ["wsl", "bash", "-lc", `script -q /dev/null -c 'npkill -d "${toWslPath(dir)}" -nu --hide-errors' 2>/dev/null`]
+ ? ["wsl", "bash", "-lc", `script -q /dev/null -c 'npkill -d "${escapeForSingleQuotedShell(toWslPath(dir))}" -nu --hide-errors' 2>/dev/null`]
: ["script", "-q", "/dev/null", "npkill", "-d", dir, "-nu", "--hide-errors"];🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| async function npkillScan(dir: string, useWsl = false): Promise<number> { | ||||||||||||||||||||
| // Use login shell so node/npkill are in PATH inside WSL. | ||||||||||||||||||||
| // Only silence stderr (2>/dev/null) so stdout flows to proc.stdout for parsing. | ||||||||||||||||||||
| // Terminal corruption from npkill's \r sequences is handled by printRow's \r\x1b[2K. | ||||||||||||||||||||
| const scriptCmd = useWsl | ||||||||||||||||||||
| ? ["wsl", "bash", "-lc", `script -q /dev/null -c 'npkill -d "${toWslPath(dir)}" -nu --hide-errors' 2>/dev/null`] | ||||||||||||||||||||
|
||||||||||||||||||||
| ? ["wsl", "bash", "-lc", `script -q /dev/null -c 'npkill -d "${toWslPath(dir)}" -nu --hide-errors' 2>/dev/null`] | |
| ? [ | |
| "wsl", | |
| "bash", | |
| "-lc", | |
| 'script -q /dev/null -c "npkill -d \\"$1\\" -nu --hide-errors" 2>/dev/null', | |
| "--", | |
| toWslPath(dir), | |
| ] |
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cd benchmark && wc -l three-way-benchmark.tsRepository: codingstark-dev/bunkill
Length of output: 93
🏁 Script executed:
cd benchmark && sed -n '40,90p' three-way-benchmark.tsRepository: codingstark-dev/bunkill
Length of output: 1697
🏁 Script executed:
cd benchmark && sed -n '210,260p' three-way-benchmark.tsRepository: codingstark-dev/bunkill
Length of output: 2058
🏁 Script executed:
cd benchmark && sed -n '75,95p' three-way-benchmark.tsRepository: codingstark-dev/bunkill
Length of output: 682
🏁 Script executed:
cd benchmark && grep -n "exitCode\|exitStatus\|exit\|proc\." three-way-benchmark.ts | head -20Repository: codingstark-dev/bunkill
Length of output: 381
🏁 Script executed:
web_search
Bun.spawn exit code status checkingRepository: codingstark-dev/bunkill
Length of output: 165
🌐 Web query:
Bun.spawn exit code checking proc.exited
💡 Result:
To check the exit code of a process spawned with Bun.spawn, use the proc.exited Promise and the proc.exitCode property on the returned Subprocess object. Key methods: 1. Await proc.exited to wait for the process to exit, which resolves to the exit code: const proc = Bun.spawn(["bun", "--version"]); const exitCode = await proc.exited; // e.g. 0 for success console.log(exitCode); 2. After awaiting exited (or if already exited), read proc.exitCode synchronously (null if not exited): await proc.exited; console.log(proc.exitCode); // number | null 3. Use the onExit option in spawn options for a callback: const proc = Bun.spawn(["echo", "hello"], { onExit(subprocess, exitCode, signalCode, error) { console.log(Exited with code ${exitCode}); }, }); proc.exited is a Promise that resolves to the exit code when the process exits. Bun waits for all child processes to exit by default unless unref is called. This matches Node.js child_process behavior but uses ReadableStreams for stdio.
Citations:
- 1: https://bun.com/reference/bun/Spawn/SpawnSyncOptions/onExit
- 2: https://bun.com/docs/api/spawn
- 3: https://bun.com/docs/guides/process/spawn
- 4: https://bun.com/docs/runtime/child-process
- 5: https://bun.com/reference/bun/SpawnOptions/OptionsObject/onExit
🏁 Script executed:
cd benchmark && rg -A 5 -B 5 "proc\.exited" three-way-benchmark.tsRepository: codingstark-dev/bunkill
Length of output: 420
🏁 Script executed:
cd benchmark && grep -n "available" three-way-benchmark.ts | tail -10Repository: codingstark-dev/bunkill
Length of output: 706
Check WSL tool availability before running benchmarks and inspect process exit codes.
where npkill and where wsl only confirm the Windows shim and WSL binary exist—they do not prove npkill or script are available inside the WSL environment. The code runs wsl bash -lc "script... npkill..." at line 49, but if either tool is missing in WSL, the process fails silently.
Additionally, Promise.race([collectPromise, proc.exited]) at line 75 awaits the exit but never inspects proc.exitCode. A failed command with no output returns found: 0 indistinguishably from a successful scan with zero packages, allowing the benchmark to appear available and successful when the underlying tools are unavailable.
Probe inside WSL before enabling npkill (e.g., wsl bash -lc "command -v script && command -v npkill"), or check proc.exitCode after line 75 and return -1 if non-zero before parsing.
Also applies to: 218–224, 246–254, 79–87
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@benchmark/three-way-benchmark.ts` around lines 45 - 55, npkillScan currently
assumes npkill/script exist inside WSL and treats any no-output run as success;
fix by probing the WSL environment for required binaries before running and by
checking the process exit code after the Promise.race. Specifically, before
constructing or invoking the wsl command in npkillScan, run a quick probe like
`wsl bash -lc "command -v script >/dev/null && command -v npkill >/dev/null"`
(or equivalent spawn) and bail out (return -1) if the probe fails; after
awaiting Promise.race([collectPromise, proc.exited]) inspect proc.exitCode (or
proc.success) and return -1 / throw if non-zero before parsing stdout. Apply the
same pre-check + exit-code inspection pattern to the other similar call sites
noted (the sections around lines 79–87, 218–224, 246–254) so missing tools in
WSL are detected rather than silently producing zero results.
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parentPath is derived by slicing with the hard-coded string length of "/node_modules". On Windows fullPath uses backslashes (e.g. ...\node_modules), so this slice removes the wrong number of characters and can produce an invalid parent directory (breaking the package.json existence check). Prefer using path.dirname(fullPath) (or compute based on the actual separator/normalized string) instead of string-length slicing.
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,7 @@ import { stat } from "node:fs/promises"; | |||||||||
| import { basename, join, resolve } from "node:path"; | ||||||||||
| import { filesize } from "filesize"; | ||||||||||
| import { APP_CONFIG } from "./config.ts"; | ||||||||||
| import { deleteModules, scan as scanEngine } from "./scanner.ts"; | ||||||||||
| import { deleteModules, normalizeProjectPath, scan as scanEngine } from "./scanner.ts"; | ||||||||||
| import type { NodeModule, ScanOptions } from "./types.ts"; | ||||||||||
|
|
||||||||||
| const LOGO = ` | ||||||||||
|
|
@@ -558,7 +558,7 @@ class BunKill { | |||||||||
| } | ||||||||||
|
|
||||||||||
| this.pendingUiMeta.add(module.path); | ||||||||||
| const projectPath = module.path.replace(/\/node_modules$/, ""); | ||||||||||
| const projectPath = normalizeProjectPath(module.path); | ||||||||||
|
||||||||||
| const projectPath = normalizeProjectPath(module.path); | |
| const projectPath = | |
| (module as any).projectRoot ?? | |
| normalizeProjectPath(module.path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CI uses
bun-version: latest, which can make builds non-deterministic and introduce sudden breakages when Bun releases. Consider pinning to a specific Bun version (or at least the minimum supported in package.json engines) and optionally adding a separate scheduled job forlatestif you want early warning.