Skip to content

fix: Windows compatibility for delete, size, and path separators#7

Open
vindiarputra wants to merge 3 commits intocodingstark-dev:mainfrom
vindiarputra:main
Open

fix: Windows compatibility for delete, size, and path separators#7
vindiarputra wants to merge 3 commits intocodingstark-dev:mainfrom
vindiarputra:main

Conversation

@vindiarputra
Copy link

@vindiarputra vindiarputra commented Mar 21, 2026

Summary by CodeRabbit

  • Chores

    • Added CI workflow running tests and CLI smoke checks across Ubuntu, macOS, and Windows (including dry-run/help checks).
  • Documentation

    • Expanded README with explicit Bun requirement, platform-specific install/usage examples, platform support table, and Windows terminal guidance.
  • Bug Fixes

    • Improved cross-platform path normalization, hidden/nesting detection, directory sizing on Windows, deletion robustness, and WSL-related behavior.
  • Refactor

    • Unified path-normalization used for CLI metadata and project-path derivation.

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

📝 Walkthrough

Walkthrough

Adds a GitHub Actions CI workflow for multi-OS testing, documents Bun and platform requirements in README, and implements cross-platform path normalization and safer filesystem operations in scanner with related CLI and benchmark updates for Windows/WSL support.

Changes

Cohort / File(s) Summary
CI workflow
/.github/workflows/ci.yml
Adds CI workflow (on push/pull_request → main) with OS matrix (ubuntu/macos/windows); checks out repo, installs Bun, runs bun install, bun run check, bun run build, and performs CLI smoke tests including an OS-aware dry-run step (PowerShell on Windows).
Documentation
README.md
Clarifies Bun requirement and supported platforms (macOS, Linux, Windows 10/11), adds platform-specific Bun install commands (including PowerShell), updates --dir examples per OS, converts platform status to a table, and documents Windows terminal requirements.
CLI integration
src/cli.ts
Replaces inline node_modules trimming with normalizeProjectPath(...) imported from src/scanner.ts when computing project path for git -C ... status.
Scanner & filesystem logic
src/scanner.ts
Introduces cross-platform separator normalization (IS_WINDOWS, normalizeSep()), exports normalizeProjectPath(nmPath, target), normalizes skip/match/case-insensitive checks, improves hidden/root/nesting detection, enables dotfile globbing, avoids du -sk on Windows, and replaces rm -rf subprocesses with fs/promises.rm(...).
Benchmark & platform handling
benchmark/three-way-benchmark.ts
Adds Windows/WSL detection and optional WSL-backed npkill invocation (with Windows→WSL path conversion), normalizes paths for glob/filter checks, requires parent package.json for matches, and changes row output to process.stdout.write with explicit clearing.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant GH as GitHub Actions
  participant Runner as Runner (ubuntu/macos/windows)
  participant Repo as Repository
  participant Bun as Bun runtime
  participant CLI as Project CLI

  GH->>Runner: start job (matrix OS)
  Runner->>Repo: checkout
  Runner->>Bun: install Bun (bun-version: latest)
  Runner->>Repo: run `bun install`
  Runner->>Repo: run `bun run check`
  Runner->>Repo: run `bun run build`
  Runner->>CLI: run `bun run src/cli.ts --help`
  Runner->>CLI: run `bun run src/cli.ts --dir $GITHUB_WORKSPACE --dry-run --hide-errors` 
  alt Windows
    Runner->>CLI: run dry-run using PowerShell (pwsh)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🐇 I hopped through slashes, back and forth,

Polished paths from south to north,
CI lights blink on every OS,
Bun boots up, checks run, no mess,
A rabbit hums: “All systems floss!”

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main changes: Windows compatibility fixes for delete operations (fs/promises.rm replacement), size calculations (du handling), and path separators (normalization across platforms).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 114-118: Update the Linux status row in the README table that
currently reads "Linux | ⚠️ Not tested yet" to reflect the new CI coverage
introduced by this PR; replace that cell with a concise phrase such as "✅ Tested
(CI)" or "✅ Tested (CI runs)" so the Linux entry matches the CI matrix and
avoids contradiction with the added CI runs.

In `@src/scanner.ts`:
- Around line 58-60: normalizeProjectPath currently builds a dynamic RegExp from
the user-controlled target which can throw for malformed patterns and cause
semaphore leaks; replace the RegExp with safe string operations: inside
normalizeProjectPath (and using normalizeSep), normalize the target (trim any
trailing slashes), construct the suffix `"/" + normalizedTarget`, then if the
normalized path endsWith that suffix remove it with slice; this avoids RegExp
construction and eliminates the crash risk that occurs after
metaSemaphore.acquire().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ee18e38a-d5b6-4646-9f85-f5f835a5fa8e

📥 Commits

Reviewing files that changed from the base of the PR and between 91676da and b74e056.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • README.md
  • src/cli.ts
  • src/scanner.ts

Comment on lines +114 to +118
| Platform | Status |
|---|---|
| macOS | ✅ Tested |
| Linux | ⚠️ Not tested yet |
| Windows 10/11 | ✅ Tested (Windows Terminal recommended) |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 114 - 118, Update the Linux status row in the README
table that currently reads "Linux | ⚠️ Not tested yet" to reflect the new CI
coverage introduced by this PR; replace that cell with a concise phrase such as
"✅ Tested (CI)" or "✅ Tested (CI runs)" so the Linux entry matches the CI matrix
and avoids contradiction with the added CI runs.

@vindiarputra
Copy link
Author

vindiarputra commented Mar 21, 2026

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

@vindiarputra Sure! I'll start an incremental review of the PR right away.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

✅ Actions performed

Full review triggered.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves cross-platform (especially Windows) behavior in BunKill’s scanning, sizing, and deletion logic, and adds CI coverage to validate the tool on multiple OSes.

Changes:

  • Normalize path separators and project-path derivation to work on Windows (scanner + CLI).
  • Replace shell rm -rf deletion with fs.promises.rm() for cross-platform deletes; adjust sizing fallback to include dotfiles.
  • Add GitHub Actions CI matrix for Ubuntu/macOS/Windows and update README with Windows usage notes.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/scanner.ts Adds Windows-aware path normalization, improves nesting/hidden checks, adjusts sizing and deletion implementations for cross-platform support.
src/cli.ts Reuses shared normalizeProjectPath to avoid hardcoded /node_modules path assumptions.
README.md Documents Bun requirement, Windows install/usage, and platform support notes.
.github/workflows/ci.yml Adds OS-matrix CI with typecheck/build and CLI smoke tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +114 to +118
| Platform | Status |
|---|---|
| macOS | ✅ Tested |
| Linux | ⚠️ Not tested yet |
| Windows 10/11 | ✅ Tested (Windows Terminal recommended) |
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README now claims Linux is "⚠️ Not tested yet", but this PR adds a CI job that runs typecheck/build and CLI smoke tests on ubuntu-latest. Either update the table to reflect that Linux is tested (at least via CI smoke tests) or adjust the workflow/wording so they’re consistent.

Copilot uses AI. Check for mistakes.

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
Copy link

Copilot AI Mar 21, 2026

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 for latest if you want early warning.

Suggested change
bun-version: latest
bun-version: '1.0.0'

Copilot uses AI. Check for mistakes.
@codingstark-dev
Copy link
Owner

codingstark-dev commented Mar 21, 2026

@vindiarputra Thanks for the contribution. Can you please run the test or run the benchmark and paste the result here? I just want to see if this is properly working. I don't have window to test :(

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
benchmark/three-way-benchmark.ts (1)

113-117: Package.json check may cause result mismatch with scanner.

Per context snippet 2, the scanner does NOT validate package.json existence at discovery time—it only filters later in processModuleMeta. This means bunKillScan may count directories that bunGlobScan now excludes, leading to different found counts in benchmark output.

If the intent is benchmark fairness (comparing equivalent logic), either remove this check from bunGlobScan, or document that the glob baseline uses stricter filtering.

🤖 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 113 - 117, The package.json
existence check in bunGlobScan (the parentPath / pkgJson logic) causes
bunGlobScan to exclude modules that bunKillScan/processModuleMeta would include
later; to make the benchmark fair, remove the pkgJson.exists() early check (the
const parentPath / const pkgJson and its await pkgJson.exists() conditional) so
discovery behavior matches the scanner, or alternatively move that filtering
into the same later step (processModuleMeta) used by bunKillScan; update
references in bunGlobScan accordingly so counts align with processModuleMeta.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmark/three-way-benchmark.ts`:
- Around line 48-50: The WSL branch of scriptCmd currently appends "> /dev/null"
which discards stdout so proc.stdout (where results are read) is empty; update
the WSL command in scriptCmd (the array built when useWsl is true, which uses
toWslPath(dir) and runs npkill) to stop redirecting stdout — either remove the
trailing "> /dev/null" entirely or replace it with "2>/dev/null" to only silence
stderr — so that proc.stdout receives the npkill output for parsing.

---

Nitpick comments:
In `@benchmark/three-way-benchmark.ts`:
- Around line 113-117: The package.json existence check in bunGlobScan (the
parentPath / pkgJson logic) causes bunGlobScan to exclude modules that
bunKillScan/processModuleMeta would include later; to make the benchmark fair,
remove the pkgJson.exists() early check (the const parentPath / const pkgJson
and its await pkgJson.exists() conditional) so discovery behavior matches the
scanner, or alternatively move that filtering into the same later step
(processModuleMeta) used by bunKillScan; update references in bunGlobScan
accordingly so counts align with processModuleMeta.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1310069d-3c63-4134-a77d-a32591b5d603

📥 Commits

Reviewing files that changed from the base of the PR and between 61e3bfb and 38e9978.

📒 Files selected for processing (1)
  • benchmark/three-way-benchmark.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
benchmark/three-way-benchmark.ts (1)

102-117: Reuse the scanner’s filtering helpers here instead of re-encoding them.

This block has already diverged from src/scanner.ts: separator normalization is unconditional here, and package.json existence is treated as sufficient, while readPackageMetadata() / processModuleMeta() exclude unreadable or invalid manifests. That means bunGlobScan() can count a different project set than scan(), which weakens the benchmark comparison.

🤖 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 102 - 117, The benchmark is
re-implementing scanner logic and diverging from scan(); update
three-way-benchmark.ts to reuse the scanner helpers instead of custom checks:
import and call the same normalization/filtering utilities and shouldSkip logic
used by scan(), and replace the manual package.json existence check with
readPackageMetadata() (and/or processModuleMeta()) to ensure manifests are
readable and valid before counting a project; this ensures bunGlobScan() uses
identical filtering/normalization as scan() and prevents the benchmark from
counting different project sets.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmark/three-way-benchmark.ts`:
- Around line 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.
- Around line 37-42: The toWslPath function must resolve relative paths to
absolute ones, handle both back- and forward-slash Windows drive prefixes, and
shell-escape single quotes before returning; fix it by first calling
path.resolve(winPath) to normalize and resolve relative inputs, update the
drive-regex in toWslPath to accept either backslash or forward slash after the
drive letter (e.g., /^([A-Za-z]):[\\/]/), convert Windows separators to POSIX
with replaceAll("\\", "/") (or equivalent) so C:/... becomes /mnt/c/..., and
finally escape single quotes in the produced WSL path (replace ' with '\'' or
the equivalent safe shell-escaping) so the path is safe to interpolate into the
bash -lc command. Ensure changes are made inside the toWslPath function and that
callers (where the command is built with bash -lc) receive the escaped WSL path.

---

Nitpick comments:
In `@benchmark/three-way-benchmark.ts`:
- Around line 102-117: The benchmark is re-implementing scanner logic and
diverging from scan(); update three-way-benchmark.ts to reuse the scanner
helpers instead of custom checks: import and call the same
normalization/filtering utilities and shouldSkip logic used by scan(), and
replace the manual package.json existence check with readPackageMetadata()
(and/or processModuleMeta()) to ensure manifests are readable and valid before
counting a project; this ensures bunGlobScan() uses identical
filtering/normalization as scan() and prevents the benchmark from counting
different project sets.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eb95b884-64dd-4307-b62f-99f8cb8647b9

📥 Commits

Reviewing files that changed from the base of the PR and between 38e9978 and f025cfb.

📒 Files selected for processing (1)
  • benchmark/three-way-benchmark.ts

Comment on lines +37 to +42
/** 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("\\", "/");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -35

Repository: 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 2

Repository: 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 2

Repository: 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()
EOF

Repository: 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'")
EOF

Repository: codingstark-dev/bunkill

Length of output: 316


Resolve relative paths and escape shell metacharacters in WSL path conversion.

The toWslPath() function has three bugs:

  1. Relative paths (e.g., --dir .) are not resolved to absolute paths, so WSL scans the wrong directory
  2. Forward-slash Windows paths (e.g., --dir C:/repo) are not converted to /mnt/ form due to the backslash-only regex
  3. Paths containing single quotes break the bash -lc string interpolation

Use resolve() to normalize the input, update the regex to handle forward slashes, and shell-escape the final WSL path before building the command.

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
Verify each finding against the current code and only fix it if needed.

In `@benchmark/three-way-benchmark.ts` around lines 37 - 42, The toWslPath
function must resolve relative paths to absolute ones, handle both back- and
forward-slash Windows drive prefixes, and shell-escape single quotes before
returning; fix it by first calling path.resolve(winPath) to normalize and
resolve relative inputs, update the drive-regex in toWslPath to accept either
backslash or forward slash after the drive letter (e.g., /^([A-Za-z]):[\\/]/),
convert Windows separators to POSIX with replaceAll("\\", "/") (or equivalent)
so C:/... becomes /mnt/c/..., and finally escape single quotes in the produced
WSL path (replace ' with '\'' or the equivalent safe shell-escaping) so the path
is safe to interpolate into the bash -lc command. Ensure changes are made inside
the toWslPath function and that callers (where the command is built with bash
-lc) receive the escaped WSL path.

Comment on lines +45 to +55
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`]
: ["script", "-q", "/dev/null", "npkill", "-d", dir, "-nu", "--hide-errors"];

try {
const proc = Bun.spawn({
cmd: ["script", "-q", "/dev/null", "npkill", "-d", dir, "-nu", "--hide-errors"],
cmd: scriptCmd,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd benchmark && wc -l three-way-benchmark.ts

Repository: codingstark-dev/bunkill

Length of output: 93


🏁 Script executed:

cd benchmark && sed -n '40,90p' three-way-benchmark.ts

Repository: codingstark-dev/bunkill

Length of output: 1697


🏁 Script executed:

cd benchmark && sed -n '210,260p' three-way-benchmark.ts

Repository: codingstark-dev/bunkill

Length of output: 2058


🏁 Script executed:

cd benchmark && sed -n '75,95p' three-way-benchmark.ts

Repository: codingstark-dev/bunkill

Length of output: 682


🏁 Script executed:

cd benchmark && grep -n "exitCode\|exitStatus\|exit\|proc\." three-way-benchmark.ts | head -20

Repository: codingstark-dev/bunkill

Length of output: 381


🏁 Script executed:

web_search
Bun.spawn exit code status checking

Repository: 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:


🏁 Script executed:

cd benchmark && rg -A 5 -B 5 "proc\.exited" three-way-benchmark.ts

Repository: codingstark-dev/bunkill

Length of output: 420


🏁 Script executed:

cd benchmark && grep -n "available" three-way-benchmark.ts | tail -10

Repository: 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.

@vindiarputra
Copy link
Author

vindiarputra commented Mar 21, 2026

@vindiarputra Thanks for the contribution. Can you please run the test or run the benchmark and paste the result here? I just want to see if this is properly working. I don't have window to test :(

I made some improvements to benchmark/three-way-benchmark.ts before running:

  1. Fixed baseline fairness: bun glob walk was counting all node_modules including nested ones (82 dirs), while bunkill only counts top-level project dirs. Applied the same nesting filter + package.json check to make the comparison apple-to-apple.

  2. WSL Support: npkill requires a TTY which Windows lacks natively. The script now auto-runs npkill via wsl bash -lc to get a proper Linux TTY, while piping valid output (2>/dev/null) back to Bun.

Here are the benchmark results on Windows 11 with WSL:
Three-way traversal benchmark (bun run bench:traversal --dir --runs 1):

Runs : 1 (+ 1 warm-up)

npkill : C:\nvm4w\nodejs\npkill (via WSL)
Bun.Glob : baseline glob walk
bunkill : current scanner.ts implementation

Running benchmarks...

Results:

npkill avg= 10.71s min= 10.71s max= 10.71s found= 0 baseline
bun glob walk avg= 6.95s min= 6.95s max= 6.95s found= 6 1.5x faster
bunkill current avg= 9.15s min= 9.15s max= 9.15s found= 6 1.2x faster


Installed vs local benchmark (bun run bench:installed-vs-local --dir --runs 1):

Runs : 1 (+ 2 warm-ups)
Timeout : 900.00s

Warming up...

installed bunkill 6 found, reported 11.97s
local bundled dist 6 found, reported 7.29s
local source cli 6 found, reported 7.44s

Running timed benchmark...

run 1 installed bunkill wall= 8.77s reported= 8.52s found= 6
run 1 local bundled dist wall= 7.94s reported= 7.74s found= 6
run 1 local source cli wall= 10.07s reported= 9.71s found= 6

Summary:

installed bunkill avg wall= 8.77s avg reported= 8.52s found= 6
local bundled dist avg wall= 7.94s avg reported= 7.74s found= 6
local source cli avg wall= 10.07s avg reported= 9.71s found= 6

Fastest: local bundled dist
Speedup: 1.25x vs local source clist

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +114 to +117
// Require a package.json in the parent directory (same as bunkill)
const parentPath = fullPath.slice(0, fullPath.length - "/node_modules".length);
const pkgJson = Bun.file(join(parentPath, "package.json"));
if (!(await pkgJson.exists())) continue;
Copy link

Copilot AI Mar 21, 2026

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.

Copilot uses AI. Check for mistakes.

this.pendingUiMeta.add(module.path);
const projectPath = module.path.replace(/\/node_modules$/, "");
const projectPath = normalizeProjectPath(module.path);
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeProjectPath(module.path) uses the default target (node_modules). If the user runs the CLI with --target <name>, module.path will end with that custom target, and this call won’t strip it—so git -C will run inside the target directory instead of the project root (often causing git status to fail / mark as non-repo). Consider persisting the scan target on the BunKill instance and passing it here (or storing the derived project root on NodeModule).

Suggested change
const projectPath = normalizeProjectPath(module.path);
const projectPath =
(module as any).projectRoot ??
normalizeProjectPath(module.path);

Copilot uses AI. Check for mistakes.
// 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`]
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When useWsl is true, the bash -lc command string interpolates dir into a shell script (... "${toWslPath(dir)}" ...). If --dir contains characters like ", $, backticks, or ', this can break quoting and potentially lead to shell injection or unexpected behavior. Prefer passing the path as a positional parameter to bash -lc (e.g. bash -lc '... "$1" ...' -- <path>) or otherwise escaping the value safely.

Suggested change
? ["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),
]

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants