Skip to content

feat: add Linux support (dev + build)#25

Closed
2725244134 wants to merge 1 commit intospool-lab:mainfrom
2725244134:feat/linux-support
Closed

feat: add Linux support (dev + build)#25
2725244134 wants to merge 1 commit intospool-lab:mainfrom
2725244134:feat/linux-support

Conversation

@2725244134
Copy link
Copy Markdown
Contributor

Summary

  • Add Linux electron-builder targets (AppImage + tar.gz)
  • Move platform-specific codex native binaries (darwin-arm64, linux-x64) to optionalDependencies so install doesn't fail on either platform
  • Add platform detection for resume-cli handler: uses xdg-terminal-exec on Linux (user's default terminal)
  • Use spawn with args array instead of execSync string interpolation to avoid blocking the main process and prevent command injection

Tested on

  • Arch Linux (CachyOS), Wayland, x86_64
  • pnpm dev works, pnpm build produces AppImage + tar.gz, pnpm test passes

Notes

  • macOS behavior is unchanged — all existing code paths are preserved
  • xdg-terminal-exec is the freedesktop standard for launching the user's preferred terminal
  • ELECTRON_OZONE_PLATFORM_HINT=auto can be set by users for native Wayland support (no code change needed)

Test plan

  • pnpm install succeeds on Linux
  • pnpm dev starts Electron app
  • pnpm build produces AppImage and tar.gz
  • pnpm test passes (4/4)
  • Verify macOS build is unaffected (no macOS machine to test)

- 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
Copilot AI review requested due to automatic review settings March 31, 2026 07:08
Copy link
Copy Markdown

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 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 to optionalDependencies to prevent cross-platform install failures.
  • Update the spool:resume-cli IPC handler to use Linux terminal launching via xdg-terminal-exec (and add spawn usage).

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()
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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().

Suggested change
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()

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +173
} else if (source === 'claude') {
spawn('xdg-terminal-exec', ['claude', '--resume', sessionUuid], { stdio: 'ignore', detached: true }).unref()
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
} 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' }

Copilot uses AI. Check for mistakes.
Comment on lines +172 to 175
} else if (source === 'claude') {
spawn('xdg-terminal-exec', ['claude', '--resume', sessionUuid], { stdio: 'ignore', detached: true }).unref()
}
return { ok: true }
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +170
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}'`)
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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()

Copilot uses AI. Check for mistakes.
Comment thread packages/app/package.json
Comment on lines +9 to 12
"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",
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread packages/app/package.json
"node_modules/acp-extension-claude/**",
"node_modules/acp-extension-codex/**",
"node_modules/acp-extension-codex-darwin-arm64/**",
"node_modules/acp-extension-codex-linux-x64/**",
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
"node_modules/acp-extension-codex-linux-x64/**",
"node_modules/acp-extension-codex-linux-x64/**",
"node_modules/acp-extension-codex-linux-arm64/**",

Copilot uses AI. Check for mistakes.
Comment thread packages/app/package.json
},
"optionalDependencies": {
"acp-extension-codex-darwin-arm64": "^0.10.0",
"acp-extension-codex-linux-x64": "^0.10.0"
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
"acp-extension-codex-linux-x64": "^0.10.0"
"acp-extension-codex-linux-x64": "^0.10.0",
"acp-extension-codex-linux-arm64": "^0.10.0"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@doodlewind doodlewind left a comment

Choose a reason for hiding this comment

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

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:

  • osascript for terminal detection and AppleScript execution
  • open -a for kitty/Alacritty/WezTerm
  • /Applications/*.app paths in isInstalled()
  • 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:

  • optionalDependencies
  • asarUnpack
  • onlyBuiltDependencies (root package.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 .deb target alongside AppImage + tar.gz — it's the most common format for Debian/Ubuntu users
  • The non-claude source case on Linux silently does nothing (no else branch). 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 spawn with args array instead of string interpolation — prevents command injection ✅
  • xdg-terminal-exec as the Linux terminal launcher — freedesktop standard ✅
  • Splitting build into build:mac / build:linux — clean separation ✅
  • --publish never on default build — safe default ✅

Generated by Claude Code

doodlewind pushed a commit that referenced this pull request Apr 1, 2026
- 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
doodlewind pushed a commit that referenced this pull request Apr 1, 2026
- 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
doodlewind added a commit that referenced this pull request Apr 3, 2026
- 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>
@doodlewind
Copy link
Copy Markdown
Contributor

Merged via squash to main as 2f543bc with the following adjustments:

  • Rebased onto latest main (18 commits behind, had conflicts in index.ts)
  • Conflict resolution: main already has openTerminal() abstraction in terminal.ts (from feat: auto-detect terminal for session resume with settings override #26), so the inline execSync/spawn changes to index.ts were dropped
  • Linux terminal support added to terminal.ts instead — uses xdg-terminal-exec via spawn (matching your original approach) with sh -c wrapper for cwd support

All other changes (Linux build targets, optionalDependencies, lockfile) landed as-is. Tests pass on both macOS and Linux. Thanks for the contribution!

@doodlewind doodlewind closed this Apr 3, 2026
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