fix: use cli-wrapper.cjs to launch claude-code instead of native binary (#14430)

### What this PR does

Before this PR:

Cherry Studio launches `claude-code` by having Bun execute
`bin/claude.exe`. Since `@anthropic-ai/claude-code` switched to native
binary distribution, `bin/claude.exe` is now a Mach-O/ELF/PE native
binary (or a shell placeholder if postinstall didn't run). Bun parses it
as JavaScript and fails on all platforms:
- **macOS/Linux**: `error: Expected ";" but found ...` (Bun treats
binary bytes as JS)
- **Windows**: `error: could not create process — Bun failed to remap
this bin`

After this PR:

Cherry Studio uses `cli-wrapper.cjs` (the official JS launcher shipped
with `@anthropic-ai/claude-code`) to launch claude-code. Bun executes
`cli-wrapper.cjs`, which locates and `spawnSync`s the correct
platform-specific native binary. This works reliably on all platforms
regardless of whether `postinstall` ran.

Fixes #14426, fixes #14367, fixes #14422

### Why we need it and why it was done in this way

`@anthropic-ai/claude-code` changed its distribution model: the main
package's `bin` field points to a native binary (`bin/claude.exe`), with
platform-specific binaries in `optionalDependencies`. The official
`cli-wrapper.cjs` is provided as a JS fallback launcher that finds and
spawns the correct native binary.

The following tradeoffs were made:

- We always construct the `bun cli-wrapper.cjs` command path even before
install (for first-install flow). This works because `cli-wrapper.cjs`
is a static file in the npm package that exists as soon as `bun install`
completes, independent of `postinstall`.
- A fallback to direct binary execution is provided in case
`cli-wrapper.cjs` is not found (e.g., older package versions).

The following alternatives were considered:

- **Execute the native binary directly** (without Bun): Would work on
macOS/Linux after `postinstall`, but fails if `postinstall` didn't run,
and doesn't integrate with Cherry Studio's Bun-based execution model.
- **Upgrade Bun**: Would not fix the root cause — Bun cannot and should
not execute native binaries as JS.

### Breaking changes

None.

### Special notes for your reviewer

- Three code paths were updated: `getClaudeCodeCommand()` (new helper),
`getVersionInfo()`, and `run()` (baseCommand construction).
- `isPackageInstalled()` still checks `~/.cherrystudio/bin/claude`
existence, which remains correct — Bun creates this symlink during
`install -g`.

**Local reproduction:**
```bash
# Bug (old path): Bun tries to parse native binary as JS
~/.cherrystudio/bin/bun ~/.cherrystudio/install/global/node_modules/@anthropic-ai/claude-code/bin/claude.exe --version
# → error: Unexpected �

# Fix (new path): Bun executes cli-wrapper.cjs which spawns native binary
~/.cherrystudio/bin/bun ~/.cherrystudio/install/global/node_modules/@anthropic-ai/claude-code/cli-wrapper.cjs --version
# → 2.1.114 (Claude Code)
```

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: [Write code that humans can
understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans)
and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy
Scout
Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [ ] Documentation: A [user-guide update](https://docs.cherry-ai.com)
was considered and is present (link) or not required. Check this only
when the PR introduces or changes a user-facing feature or behavior.
- [x] Self-review: I have reviewed my own code (e.g., via
[`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`,
or GitHub UI) before requesting review from others

### Release note

```release-note
Fix: Claude Code fails to launch on all platforms due to @anthropic-ai/claude-code switching to native binary distribution. Cherry Studio now uses the official cli-wrapper.cjs launcher instead of executing the native binary via Bun.
```

Signed-off-by: raymond <13162938362@163.com>
Co-authored-by: SuYao <sy20010504@gmail.com>
This commit is contained in:
Qin Lingguang
2026-04-22 14:20:36 +08:00
committed by GitHub
parent 8e3faf0743
commit 4c0ce8f799

View File

@@ -129,6 +129,37 @@ class CodeToolsService {
}
}
/**
* Get the command to execute claude-code.
*
* Since @anthropic-ai/claude-code ships a native binary (bin/claude.exe) instead of
* a JavaScript file, it cannot be executed via Bun. The official cli-wrapper.cjs is
* a JS launcher that locates and spawns the correct platform-specific binary.
* We use Bun to run cli-wrapper.cjs, which works on all platforms.
*/
private async getClaudeCodeCommand(bunPath: string): Promise<string> {
const globalInstallDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'install', 'global')
const cliWrapperPath = path.join(
globalInstallDir,
'node_modules',
'@anthropic-ai',
'claude-code',
'cli-wrapper.cjs'
)
if (fs.existsSync(cliWrapperPath)) {
logger.debug(`Using cli-wrapper.cjs for claude-code: ${cliWrapperPath}`)
return `"${bunPath}" "${cliWrapperPath}"`
}
// Fallback: try to execute the binary directly (works if postinstall ran correctly)
const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
const executableName = await this.getCliExecutableName(codeTools.claudeCode)
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
logger.warn(`cli-wrapper.cjs not found at ${cliWrapperPath}, falling back to direct execution: ${executablePath}`)
return `"${executablePath}"`
}
/**
* Generate opencode.json config file for OpenCode CLI
* Merge approach:
@@ -653,11 +684,21 @@ class CodeToolsService {
if (isInstalled) {
logger.info(`${cliTool} is installed, getting current version`)
try {
const executableName = await this.getCliExecutableName(cliTool)
const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
let versionCommand: string
const { stdout } = await execAsync(`"${executablePath}" --version`, {
// claude-code ships a native binary that cannot be executed via Bun.
// Use cli-wrapper.cjs (via Bun) to run --version reliably on all platforms.
if (cliTool === codeTools.claudeCode) {
const bunPath = await this.getBunPath()
versionCommand = await this.getClaudeCodeCommand(bunPath)
} else {
const executableName = await this.getCliExecutableName(cliTool)
const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
versionCommand = `"${executablePath}"`
}
const { stdout } = await execAsync(`${versionCommand} --version`, {
timeout: 10000
})
// Extract version number from output (format may vary by tool)
@@ -919,7 +960,17 @@ class CodeToolsService {
}
}
let baseCommand = isWin ? `"${executablePath}"` : `"${bunPath}" "${executablePath}"`
let baseCommand: string
// claude-code ships a native binary that cannot be executed via Bun.
// Use cli-wrapper.cjs (via Bun) on all platforms for reliable execution.
if (cliTool === codeTools.claudeCode) {
baseCommand = await this.getClaudeCodeCommand(bunPath)
} else if (isWin) {
baseCommand = `"${executablePath}"`
} else {
baseCommand = `"${bunPath}" "${executablePath}"`
}
// Special handling for kimi-cli: use uvx instead of bun
if (cliTool === codeTools.kimiCli) {