feat: add Linux support (dev + build)#25
Conversation
- Add Linux electron-builder targets (AppImage, tar.gz) - Move platform-specific codex binaries to optionalDependencies - Add platform detection for resume-cli (xdg-terminal-exec on Linux) - Use spawn with args array instead of execSync string interpolation
There was a problem hiding this comment.
Pull request overview
This PR adds Linux support to the Electron app’s install/build workflow and adjusts runtime behavior to better handle platform-specific native binaries and terminal launching.
Changes:
- Add Linux electron-builder targets (AppImage + tar.gz) and introduce Linux-specific build script.
- Move platform-specific
acp-extension-codex-*native packages tooptionalDependenciesto prevent cross-platform install failures. - Update the
spool:resume-cliIPC handler to use Linux terminal launching viaxdg-terminal-exec(and addspawnusage).
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
pnpm-lock.yaml |
Reflects optionalDependencies move for platform codex packages. |
packages/app/src/main/index.ts |
Adds platform branching for spool:resume-cli and uses spawn on non-mac platforms. |
packages/app/package.json |
Adds Linux build targets + scripts and declares codex platform binaries as optional deps. |
package.json |
Updates pnpm onlyBuiltDependencies allowlist for Linux codex binary package. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| execSync(`osascript -e '${script}'`) | ||
| } | ||
| } else if (source === 'claude') { | ||
| spawn('xdg-terminal-exec', ['claude', '--resume', sessionUuid], { stdio: 'ignore', detached: true }).unref() |
There was a problem hiding this comment.
On non-mac platforms, spawn('xdg-terminal-exec', ...) errors (e.g., ENOENT when the command isn’t available) are emitted asynchronously on the ChildProcess and won’t be caught by this try/catch. Without an .on('error', ...) handler, this can crash the main process via an unhandled 'error' event. Handle the child error event (and ideally return { ok: false } when it fails) before calling .unref().
| spawn('xdg-terminal-exec', ['claude', '--resume', sessionUuid], { stdio: 'ignore', detached: true }).unref() | |
| const child = spawn('xdg-terminal-exec', ['claude', '--resume', sessionUuid], { | |
| stdio: 'ignore', | |
| detached: true, | |
| }) | |
| child.on('error', (error) => { | |
| console.error('Failed to launch xdg-terminal-exec for spool:resume-cli:', error) | |
| }) | |
| child.unref() |
| } else if (source === 'claude') { | ||
| spawn('xdg-terminal-exec', ['claude', '--resume', sessionUuid], { stdio: 'ignore', detached: true }).unref() |
There was a problem hiding this comment.
This handler treats any non-darwin platform the same, so on Windows it will attempt to run xdg-terminal-exec for source==='claude'. If Windows isn’t supported, it would be safer to explicitly check process.platform === 'linux' and return a clear { ok: false, error: 'unsupported platform' } for others to avoid accidental crashes/undefined behavior.
| } else if (source === 'claude') { | |
| spawn('xdg-terminal-exec', ['claude', '--resume', sessionUuid], { stdio: 'ignore', detached: true }).unref() | |
| } else if (process.platform === 'linux' && source === 'claude') { | |
| spawn('xdg-terminal-exec', ['claude', '--resume', sessionUuid], { stdio: 'ignore', detached: true }).unref() | |
| } else { | |
| return { ok: false, error: 'unsupported platform' } |
| } else if (source === 'claude') { | ||
| spawn('xdg-terminal-exec', ['claude', '--resume', sessionUuid], { stdio: 'ignore', detached: true }).unref() | ||
| } | ||
| return { ok: true } |
There was a problem hiding this comment.
For non-darwin platforms, source !== 'claude' becomes a no-op but still returns { ok: true }. Previously the non-claude path activated Terminal on macOS; if codex/opencli sessions are expected to still “resume” by opening a terminal, Linux should likely open the user’s terminal too (or return { ok: false } so the UI can reflect that resume isn’t supported for that source/platform).
| if (process.platform === 'darwin') { | ||
| if (source === 'claude') { | ||
| const script = `tell application "Terminal" to do script "claude --resume ${sessionUuid}"` | ||
| execSync(`osascript -e '${script}'`) | ||
| } else { | ||
| const script = `tell application "Terminal" to activate` | ||
| execSync(`osascript -e '${script}'`) |
There was a problem hiding this comment.
The macOS path still uses execSync with shell string interpolation around an AppleScript that includes sessionUuid. Since this IPC can be invoked from the renderer, a crafted sessionUuid containing quotes can break out of the quoting and lead to command injection, and execSync also blocks the main process. Prefer spawn/execFile for osascript with an args array (no shell) and validate/escape sessionUuid before embedding it into the AppleScript.
| if (process.platform === 'darwin') { | |
| if (source === 'claude') { | |
| const script = `tell application "Terminal" to do script "claude --resume ${sessionUuid}"` | |
| execSync(`osascript -e '${script}'`) | |
| } else { | |
| const script = `tell application "Terminal" to activate` | |
| execSync(`osascript -e '${script}'`) | |
| if (source === 'claude') { | |
| // Restrict sessionUuid to a safe character set before embedding in AppleScript/commands | |
| if (!/^[A-Za-z0-9_-]+$/.test(sessionUuid)) { | |
| throw new Error('Invalid sessionUuid') | |
| } | |
| } | |
| if (process.platform === 'darwin') { | |
| if (source === 'claude') { | |
| const script = `tell application "Terminal" to do script "claude --resume ${sessionUuid}"` | |
| spawn('osascript', ['-e', script], { stdio: 'ignore', detached: true }).unref() | |
| } else { | |
| const script = `tell application "Terminal" to activate` | |
| spawn('osascript', ['-e', script], { stdio: 'ignore', detached: true }).unref() |
| "build": "electron-vite build && electron-builder --publish never", | ||
| "build:mac": "electron-vite build && electron-builder --mac --arm64", | ||
| "build:linux": "electron-vite build && electron-builder --linux", | ||
| "build:electron": "electron-vite build", |
There was a problem hiding this comment.
build:linux runs electron-builder --linux without constraining the arch. On an arm64 Linux build machine this will likely produce an arm64 artifact, but the app only declares/asar-unpacks acp-extension-codex-linux-x64, so codex resolution (acp-extension-codex-${process.platform}-${process.arch}) will fail at runtime. Either pin the Linux build to --x64 (if that’s the only supported arch) or add acp-extension-codex-linux-arm64 to optionalDependencies and asarUnpack.
| "node_modules/acp-extension-claude/**", | ||
| "node_modules/acp-extension-codex/**", | ||
| "node_modules/acp-extension-codex-darwin-arm64/**", | ||
| "node_modules/acp-extension-codex-linux-x64/**", |
There was a problem hiding this comment.
The electron-builder asarUnpack list includes acp-extension-codex-linux-x64, but there’s no entry for acp-extension-codex-linux-arm64. Since the runtime code resolves codex binaries based on process.arch, packaging for arm64 Linux (or running on arm64 with a compatible build) will likely miss the unpacked binary. Add the arm64 package here as well if arm64 Linux is intended to work.
| "node_modules/acp-extension-codex-linux-x64/**", | |
| "node_modules/acp-extension-codex-linux-x64/**", | |
| "node_modules/acp-extension-codex-linux-arm64/**", |
| }, | ||
| "optionalDependencies": { | ||
| "acp-extension-codex-darwin-arm64": "^0.10.0", | ||
| "acp-extension-codex-linux-x64": "^0.10.0" |
There was a problem hiding this comment.
optionalDependencies adds acp-extension-codex-linux-x64 but not acp-extension-codex-linux-arm64, even though the codex resolver uses process.platform/process.arch and the lockfile already contains the linux-arm64 package. If Linux support is meant to include arm64, add the linux-arm64 optional dependency too (and keep asarUnpack in sync).
| "acp-extension-codex-linux-x64": "^0.10.0" | |
| "acp-extension-codex-linux-x64": "^0.10.0", | |
| "acp-extension-codex-linux-arm64": "^0.10.0" |
doodlewind
left a comment
There was a problem hiding this comment.
Thanks for the contribution! Linux support is definitely needed. I reviewed the changes and have a few concerns — some blocking, some suggestions.
Must Fix
1. Rebase needed — PR conflicts with main
main has been refactored since this PR was branched: the inline osascript/execSync calls in index.ts have been replaced by openTerminal() from a new terminal.ts module. The PR currently modifies the old inline code, which causes merge conflicts (mergeable_state: dirty).
Please rebase onto current main.
2. terminal.ts needs Linux support
After rebasing, the real work is in packages/app/src/main/terminal.ts — it's 100% macOS-only:
osascriptfor terminal detection and AppleScript executionopen -afor kitty/Alacritty/WezTerm/Applications/*.apppaths inisInstalled()- All supported terminals are macOS apps (Terminal.app, iTerm2, Warp)
On Linux, calling openTerminal() will hit osascript and crash. The xdg-terminal-exec approach in this PR is good — it just needs to be integrated into terminal.ts with a process.platform check, e.g.:
// In openTerminal():
if (process.platform !== 'darwin') {
if (command) {
spawn('xdg-terminal-exec', ['sh', '-c', command], { stdio: 'ignore', detached: true }).unref()
}
return
}For bonus points, kitty/Alacritty/WezTerm have cross-platform CLIs that work on Linux too (without open -a).
3. No ARM Linux (linux-arm64) support
Only acp-extension-codex-linux-x64 is added. The linux-arm64 variant exists in npm but is missing from:
optionalDependenciesasarUnpackonlyBuiltDependencies(rootpackage.json)
This means Spool won't work on ARM Linux devices (e.g., Raspberry Pi, ARM servers, Ampere cloud instances). Please add acp-extension-codex-linux-arm64 alongside the x64 variant.
Should Fix
4. build:linux has no arch flags
"build:linux": "electron-vite build && electron-builder --linux"This defaults to the host architecture only. Consider:
"build:linux": "electron-vite build && electron-builder --linux --x64 --arm64"Or provide separate build:linux-x64 / build:linux-arm64 scripts if cross-compilation isn't set up.
5. Verify resources/icon.png exists
The linux config references resources/icon.png. macOS uses resources/icon.icns. Make sure the PNG icon actually exists in the repo — electron-builder needs it for Linux builds.
Nice to Have
- Consider adding
.debtarget alongside AppImage + tar.gz — it's the most common format for Debian/Ubuntu users - The non-claude
sourcecase on Linux silently does nothing (noelsebranch). On macOS it at least activates the terminal. Consider opening a blank terminal on Linux too:spawn('xdg-terminal-exec', [], { stdio: 'ignore', detached: true }).unref()
What's Good
- Moving platform binaries to
optionalDependencies— correct pattern ✅ - Using
spawnwith args array instead of string interpolation — prevents command injection ✅ xdg-terminal-execas the Linux terminal launcher — freedesktop standard ✅- Splitting
buildintobuild:mac/build:linux— clean separation ✅ --publish neveron defaultbuild— safe default ✅
Generated by Claude Code
- Add Linux electron-builder targets (AppImage + tar.gz) - Move platform-specific codex native binaries to optionalDependencies (darwin-arm64, linux-x64, linux-arm64) - Add Linux platform gate in terminal.ts: uses xdg-terminal-exec (freedesktop standard) instead of macOS-only osascript/AppleScript - Add build:mac and build:linux scripts, default build uses --publish never - Add linux-arm64 support alongside linux-x64 Based on PR #25 by @2725244134, rebased onto current main and extended with linux-arm64 support and terminal.ts integration. https://claude.ai/code/session_01N5XGgU4C5pNqiQMEhRsPRj
- Add Linux electron-builder targets (AppImage + tar.gz) - Move platform-specific codex native binaries to optionalDependencies (darwin-arm64, linux-x64, linux-arm64) - Add Linux platform gate in terminal.ts: uses xdg-terminal-exec (freedesktop standard) instead of macOS-only osascript/AppleScript - Add build:mac and build:linux scripts, default build uses --publish never - Add linux-arm64 support alongside linux-x64 Based on PR #25 by @2725244134, rebased onto current main and extended with linux-arm64 support and terminal.ts integration. Co-authored-by: baiye <2725244134@qq.com> https://claude.ai/code/session_01N5XGgU4C5pNqiQMEhRsPRj
- Add Linux electron-builder targets (AppImage + tar.gz) - Move platform-specific codex native binaries to optionalDependencies - Add Linux terminal support via xdg-terminal-exec in terminal.ts - Use spawn with args array for Linux to avoid blocking and command injection Co-Authored-By: baiye <2725244134@qq.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Merged via squash to main as 2f543bc with the following adjustments:
All other changes (Linux build targets, optionalDependencies, lockfile) landed as-is. Tests pass on both macOS and Linux. Thanks for the contribution! |
Summary
darwin-arm64,linux-x64) tooptionalDependenciesso install doesn't fail on either platformresume-clihandler: usesxdg-terminal-execon Linux (user's default terminal)spawnwith args array instead ofexecSyncstring interpolation to avoid blocking the main process and prevent command injectionTested on
pnpm devworks,pnpm buildproduces AppImage + tar.gz,pnpm testpassesNotes
xdg-terminal-execis the freedesktop standard for launching the user's preferred terminalELECTRON_OZONE_PLATFORM_HINT=autocan be set by users for native Wayland support (no code change needed)Test plan
pnpm installsucceeds on Linuxpnpm devstarts Electron apppnpm buildproduces AppImage and tar.gzpnpm testpasses (4/4)