Skip to content

Add Windows support to Claude Code Usage (ccusage) extension#28449

Draft
owendavidprice wants to merge 4 commits into
raycast:mainfrom
owendavidprice:ext/ccusage-windows-support
Draft

Add Windows support to Claude Code Usage (ccusage) extension#28449
owendavidprice wants to merge 4 commits into
raycast:mainfrom
owendavidprice:ext/ccusage-windows-support

Conversation

@owendavidprice

Copy link
Copy Markdown
Contributor

Description

Adds Windows support to the Claude Code Usage (ccusage) extension so it runs on the Raycast for Windows beta. No new features — purely platform-compatibility fixes, and macOS behaviour is unchanged.

The extension shells out to npx ccusage via Raycast's useExec, which calls child_process.spawn directly. Three things broke on Windows:

  • spawn("npx", …) throws ENOENT — Windows needs the npx.cmd shim, which is only found when the command runs through a shell. Now passes shell: true on Windows so cmd.exe resolves it via PATHEXT.
  • PATH was Unix-only — built with the : separator and hard-coded Homebrew/nvm paths. Now uses path.delimiter and os.homedir(), with a Windows branch that adds %APPDATA%\npm.
  • Relied on $HOME (unset on Windows) — switched to os.homedir() throughout.

Unix-only version-manager (nvm/fnm/n/volta) probing and env vars are gated to non-Windows. keychain-access already fell back to reading ~/.claude/.credentials.json (works on Windows via homedir()), and the usage-limits client uses fetch, so both were already cross-platform.

Changes

  • src/utils/exec-options.tsshell: true on Windows; path.delimiter; os.homedir(); gate nvm/fnm env vars to Unix
  • src/utils/node-path-resolver.ts — Windows PATH branch (%APPDATA%\npm, nodejs dir); path.delimiter / homedir(); skip Unix version-manager probing on Windows
  • package.json — add Windows to platforms
  • CHANGELOG.md — new entry

Checklist

  • I read the extension guidelines
  • npm run build succeeds (verified on Windows)
  • Verified ray develop loads the extension into Raycast on Windows and shows live ccusage data across all views
  • Updated CHANGELOG.md with a new entry using the {PR_MERGE_DATE} placeholder
  • No macOS regressions — all platform-specific code is gated behind process.platform === "win32"

The extension shells out to `npx ccusage` via Raycast's useExec, which
calls child_process.spawn directly. Three things broke on the Raycast
for Windows beta:

- spawn("npx", ...) throws ENOENT because Windows needs the npx.cmd
  shim, only found when run through a shell. Pass shell: true on
  Windows so cmd.exe resolves it via PATHEXT.
- The PATH was built with the Unix ':' separator and hard-coded
  Homebrew/nvm paths. Use path.delimiter and os.homedir(); add a
  Windows branch that includes %APPDATA%\npm.
- It relied on $HOME (unset on Windows). Use os.homedir() throughout.

Unix version-manager (nvm/fnm/n/volta) probing and env vars are gated
to non-Windows. Declares Windows in the platforms array.

Verified: ray build succeeds and ray develop loads the extension into
Raycast on Windows, showing live ccusage data across all views.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@raycastbot raycastbot added extension fix / improvement Label for PRs with extension's fix improvements extension: ccusage Issues related to the ccusage extension AI Extension platform: macOS platform: Windows labels May 31, 2026
@raycastbot

raycastbot commented May 31, 2026

Copy link
Copy Markdown
Collaborator

Thank you for your contribution! 🎉

🔔 @nyatinte @zhuravel @GarrickZ2 @pernielsentikaer @raulgg @ridemountainpig @bendrucker you might want to have a look.

You can use this guide to learn how to check out the Pull Request locally in order to test it.

📋 Quick checkout commands
BRANCH="ext/ccusage-windows-support"
FORK_URL="https://github.com/owendavidprice/extensions.git"
EXTENSION_NAME="ccusage"
REPO_NAME="extensions"

git clone -n --depth=1 --filter=tree:0 -b $BRANCH $FORK_URL
cd $REPO_NAME
git sparse-checkout set --no-cone "extensions/$EXTENSION_NAME"
git checkout
cd "extensions/$EXTENSION_NAME"
npm install && npm run dev

We're currently experiencing a high volume of incoming requests. As a result, the initial review may take up to 10-15 business days.

@greptile-apps

greptile-apps Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Windows support to the ccusage extension by fixing three platform-specific spawn failures with no changes to macOS behavior. The approach is well-scoped: a new run-ccusage.ts helper centralizes CLI invocation via execa (eliminating the old ad-hoc execAsync + string-building pattern), while exec-options.ts and node-path-resolver.ts gain the platform awareness needed by the underlying useExec calls in the views.

  • Shell resolution fix: exec-options.ts now returns shell: true on Windows so child_process.spawn (used by useExec) resolves .cmd shims via PATHEXT; execa (used by the AI tools) tolerates the option without needing it, since cross-spawn handles shim resolution natively.
  • PATH construction fix: path-key resolves the correct case for the PATH variable on Windows (conventionally Path), preventing a duplicate ambiguous entry; path.delimiter replaces the hard-coded : separator throughout.
  • Home directory fix: os.homedir() replaces process.env.HOME everywhere; nvm/fnm/volta env-var injection is gated to non-Windows; the Windows branch adds only %APPDATA%\npm to PATH.

Confidence Score: 5/5

Safe to merge; all platform-specific code is gated behind process.platform === win32 and macOS behaviour is unchanged.

The changes are surgical and well-contained: every Windows-specific branch is guarded by isWindows, dependency version choices (execa@5, path-key@3) correctly target the last CJS-compatible releases for this non-ESM project, and the consolidation of CLI invocation into run-ccusage.ts removes duplication without altering logic. No regressions were identified on the macOS path.

No files require special attention.

Important Files Changed

Filename Overview
extensions/ccusage/src/utils/exec-options.ts Adds Windows-aware PATH key resolution via path-key, path.delimiter for cross-platform separators, os.homedir() instead of process.env.HOME, gates nvm/fnm env vars to non-Windows, and returns shell: true on Windows so useExec can resolve .cmd shims
extensions/ccusage/src/utils/node-path-resolver.ts Now accepts basePath parameter instead of reading process.env.PATH directly; adds Windows branch that prepends %APPDATA%/npm; switches to path.delimiter and os.homedir(); Unix version-manager probing is still only reached on non-Windows paths
extensions/ccusage/src/utils/run-ccusage.ts New file: consolidates CLI invocation into a single helper using execa with an args array, replacing the old ad-hoc execAsync + string-building pattern used across tools and claude-code-stats
extensions/ccusage/src/claude-code-stats.tsx Drops the local buildCommand helper and execAsync in favour of runCcusage; logic and output handling are unchanged
extensions/ccusage/package.json Adds execa@^5.1.1 and path-key@^3.1.1 dependencies (last CJS-compatible versions, correct for a non-ESM Raycast extension), and appends Windows to the platforms array
extensions/ccusage/src/utils/exec-async.ts Deleted: the thin promisify(exec) wrapper is superseded by execa in run-ccusage.ts
extensions/ccusage/CHANGELOG.md New [Windows support] - {PR_MERGE_DATE} entry added at the top with Added/Changed/Fixed sections; follows the required placeholder convention

Reviews (4): Last reviewed commit: "fix(ccusage): enable shell on Windows so..." | Re-trigger Greptile

child_process.exec (used by the no-view tools and claude-code-stats)
types ExecOptions.shell as string only, so shell: true failed tsc.
exec already runs via a shell; only useExec's spawn needed the hint.
Point both at process.env.ComSpec (a string) on Windows, which is
type-compatible with exec and still resolves the npx.cmd shim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@bendrucker bendrucker left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Windows changes here mostly re-implement, on a second code path, behavior that a dependency already in the tree provides. The inline comment describes that in more detail. I'm not a Windows user so any references to behavior there is just from public sources, please do verify them.

There are other code quality comments I'd leave but most of them will fall away when cross-platform details are delegated to dependencies.


const isWindows = process.platform === "win32";

export const getExecOptions = () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The extension has two exec paths that behave differently on Windows. The views call useExec, which wraps execa and uses cross-spawn to resolve .cmd / PATHEXT shims and to read PATH case-insensitively via path-key. The AI tools in src/tools/ call raw child_process.exec, which goes through none of that. Every Windows branch in this PR exists to hand-build, on the second path, what the first path already gets from the dependency.

I'd suggest routing the AI tools through the same execa call the views use (execa("npx", ["ccusage@latest", ...]), or cross-spawn directly) instead of child_process.exec with a manually assembled environment. That removes the divergence at the source. Concretely, it lets this PR drop:

  • The shell: cmd.exe option (line 49). child_process.exec already runs through cmd.exe via ComSpec, and execa resolves .cmd shims through PATHEXT, so forcing a shell is redundant and only adds cmd.exe quoting risk (cmd command reference).
  • The { ...process.env, PATH } casing hazard (line 9). On Windows the variable is conventionally Path, names are case-insensitive, and the environment block is sorted case-insensitively, so spreading process.env and then adding a second PATH key leaves two entries for one logical variable with ambiguous precedence (environment variable names are case-insensitive on Windows, the environment block is sorted case-insensitively). cross-spawn handles this for you; if you still inject PATH yourself, write it back to the existing key (for example via path-key) rather than adding a new one.
  • The isWindows branches in node-path-resolver.ts and the getWindowsNodePaths helper, including its ProgramFiles\nodejs entry (which the inherited PATH already covers, and which the helper's own comment does not mention).

The one Windows detail likely worth keeping is %APPDATA%\npm, the npm global prefix where global .cmd shims live, if Raycast does not inherit the full user PATH. If you find that's needed, prepend just that one directory rather than reconstructing the PATH per platform.

I'd want the env-casing behavior confirmed on a real Windows box regardless of which approach we take, since it determines whether the tools resolve ccusage.cmd at all.

@owendavidprice

Copy link
Copy Markdown
Contributor Author

Heads-up for anyone skimming the run history: the first CI run on this PR went red on a TypeScript error (shell: true isn't assignable to child_process.exec's ExecOptions.shell, which is string-only). That was resolved in the follow-up commit 9828148c, which sets the shell to the cmd.exe path (process.env.ComSpec, a string) — type-compatible with both exec and useExec, and functionally equivalent on Windows.

All checks are green on the current head commit (build, ESLint, Prettier, tsc, Socket Security, Greptile). The earlier red run is superseded and can be ignored.

Addresses @bendrucker's review. The AI tools and claude-code-stats used
child_process.exec, a second exec path that didn't get the cross-platform
handling useExec already has. They now go through execa (which useExec
also uses): cross-spawn resolves .cmd shims via PATHEXT, so the manual
Windows handling is removed.

- New runCcusage() helper invokes the CLI via execa with an args array,
  honouring useDirectCcusageCommand and customNpxPath; tools and
  claude-code-stats use it. Removes child_process.exec usage.
- Drops the shell option entirely (execa needs no shell; exec already
  ran through cmd.exe).
- Fixes the PATH-casing hazard: writes PATH to its real key via path-key
  instead of spreading process.env and adding a second PATH entry that
  on Windows (conventionally Path) left two ambiguous entries.
- node-path-resolver Windows branch reduced to just the npm global prefix
  (%APPDATA%\npm); removes getWindowsNodePaths and the ProgramFiles entry.

Verified on Windows: execa resolves npx.cmd with no shell; tsc, ray
build, eslint, prettier, and the jest suite (15 tests) all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@owendavidprice

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — you're right that the cleaner fix is to remove the second exec path rather than re-implement cross-platform behaviour on it. Pushed 12324bf6 doing that.

What changed

  • The AI tools (src/tools/*) and claude-code-stats now call a small runCcusage() helper that invokes the CLI through execa (the same library useExec uses) with an args array, instead of child_process.exec with a hand-assembled command string. cross-spawn handles .cmd/PATHEXT resolution, so both exec paths now behave identically.
  • Dropped the shell option entirely. As you noted, exec already ran through cmd.exe and execa resolves shims via PATHEXT, so forcing a shell was redundant.
  • Fixed the PATH-casing hazard. getExecOptions now resolves the real key with path-key and writes back to it, instead of spreading process.env and adding a second PATH entry. This was the bug you flagged — on Windows the inherited variable is Path, so the old code left two entries with ambiguous precedence.
  • Reduced the node-path-resolver Windows branch to just the npm global prefix (%APPDATA%\npm), the one location you identified as worth keeping. Removed getWindowsNodePaths and its ProgramFiles\nodejs entry (already covered by the inherited PATH).

execa and path-key were already in the tree via @raycast/utils; they're now direct dependencies at the same versions (execa@5.1.1, path-key@3.1.1).

Verified on a real Windows box (the env-casing behaviour you asked about specifically): with execa and no shell, path-key resolves the PATH key and npx ccusage@latest ... --json resolves npx.cmd and returns data correctly. tsc, ray build, ESLint, Prettier and the Jest suite (15 tests) all pass; the extension loads via ray develop and the views render live data.

You mentioned other code-quality points that would fall away once this was delegated to the dependency — happy to take those on if any remain.

Real-world testing on the Raycast for Windows beta showed the views
still failing with 'ccusage command failed'. Runtime diagnostics
proved: Raycast does inherit the full user PATH (npx resolves) and the
command runs fine via execa — but the views use useExec, which spawns
through child_process.spawn. On Windows raw spawn throws ENOENT for
the npx.cmd/ccusage.cmd shims unless run through a shell (PATHEXT).

Enable shell on Windows in getExecOptions. Verified: the views now load
live data in Raycast. tsc/build/eslint/prettier and 15 jest tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@owendavidprice

Copy link
Copy Markdown
Contributor Author

Update after testing on a real Windows box (Raycast for Windows beta) — there's a wrinkle worth flagging.

After the execa refactor, the views still failed with "ccusage command failed", so I added runtime diagnostics inside Raycast's extension process to capture exactly what happens. Findings:

  1. Raycast does inherit the full user PATH (the Node dir was present), and npx ccusage@latest daily --json runs fine via execa from that process. So the AI tools (now on execa) work, and the PATH/case handling is sufficient — no registry/PATH reconstruction needed. 👍

  2. But the views use useExec, and in the @raycast/utils version here useExec spawns through raw child_process.spawn, not cross-spawn. On Windows that throws ENOENT for the npx.cmd shim unless run through a shell. Minimal repro on the same box:

    child_process.spawn("npx", ["--version"])               // → ENOENT
    child_process.spawn("npx", ["--version"], {shell:true}) // → exit 0

So the cross-spawn delegation we were counting on for useExec doesn't hold in this version — the views genuinely need a shell on Windows. The latest commit (c665895a) therefore:

  • Enables shell on Windows only in getExecOptions (fixes the views; execa tolerates it, and the args are fixed tokens + validated dates, so no quoting risk).
  • Keeps the execa refactor for the AI tools / claude-code-stats.
  • Drops the manual PATH/casing reconstruction beyond path-key + %APPDATA%\npm, since the diagnostics confirmed it wasn't needed.

If you'd prefer the shell flag scoped to just the useExec call sites rather than shared getExecOptions (the execa tools don't strictly need it), I'm happy to split it — let me know which you prefer. Verified end to end: the views now render live data in Raycast on Windows, and tsc/ray build/ESLint/Prettier/15 Jest tests pass.

@bendrucker

Copy link
Copy Markdown
Contributor

Hmm seems like this could be worth fixing upstream versus working around in the extension, even in this narrower way

@pernielsentikaer

Copy link
Copy Markdown
Collaborator

Drafting this until we have time to look into the other PR you opened @bendrucker

@pernielsentikaer pernielsentikaer marked this pull request as draft June 15, 2026 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Extension extension: ccusage Issues related to the ccusage extension extension fix / improvement Label for PRs with extension's fix improvements platform: macOS platform: Windows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants