feat(linux): add RPM package target and rename CLI to orca-ide

Adds an RPM Linux package target and renames the Linux CLI command to orca-ide to avoid shadowing GNOME Orca, while preserving macOS and Windows CLI command behavior.\n\nFollow-up hardening keeps the Linux launcher executable, removes only old Orca-managed Linux/WSL orca launchers during migration, preserves AppImage/deb artifact names, and updates package/release tests for the RPM asset.
This commit is contained in:
Emad Fussi 2026-05-30 02:57:56 +03:00 committed by GitHub
parent 80a2b6195b
commit 7337cab2d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 303 additions and 134 deletions

View File

@ -548,9 +548,11 @@ jobs:
# Why: `pnpm build:release` verifies the Linux computer-use provider by
# importing AT-SPI bindings, which are runtime package deps but are not
# present on stock GitHub Ubuntu release runners.
# Why: `rpm` is needed by electron-builder's fpm backend to produce the
# .rpm artifact. Stock Ubuntu runners do not ship it.
- name: Install Linux computer-use provider dependencies
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y python3-gi gir1.2-atspi-2.0 at-spi2-core xclip xdotool
run: sudo apt-get update && sudo apt-get install -y python3-gi gir1.2-atspi-2.0 at-spi2-core xclip xdotool rpm
- name: Verify macOS signing environment
if: matrix.platform == 'mac'

View File

@ -200,8 +200,8 @@ module.exports = {
extraResources: [
relayExtraResource,
{
from: 'resources/linux/bin/orca',
to: 'bin/orca'
from: 'resources/linux/bin/orca-ide',
to: 'bin/orca-ide'
},
{
from: 'node_modules/agent-browser/bin/agent-browser-linux-${arch}',
@ -213,7 +213,7 @@ module.exports = {
},
featureWallResources
],
target: ['AppImage', 'deb'],
target: ['AppImage', 'deb', 'rpm'],
maintainer: 'stablyai',
category: 'Utility'
},
@ -225,6 +225,11 @@ module.exports = {
artifactName: 'orca-ide_${version}_${arch}.${ext}',
depends: ['python3', 'python3-gi', 'gir1.2-atspi-2.0', 'at-spi2-core', 'xdotool', 'xclip']
},
rpm: {
packageName: 'orca-ide',
artifactName: 'orca-ide-${version}.${arch}.${ext}',
depends: ['python3', 'python3-gobject', 'at-spi2-core', 'xdotool', 'xclip']
},
// Why: must be true so that electron-builder rebuilds native modules
// (node-pty) for each target architecture when producing dual-arch macOS
// builds (x64 + arm64). With npmRebuild disabled, CI on an arm64 runner

View File

@ -8,4 +8,14 @@ describe('electron-builder config', () => {
it('uses the multi-size icon source for Linux packages', () => {
expect(electronBuilderConfig.linux.icon).toBe('resources/build/icon.icns')
})
it('builds RPMs without changing existing Linux artifact names', () => {
expect(electronBuilderConfig.linux.target).toEqual(['AppImage', 'deb', 'rpm'])
expect(electronBuilderConfig.appImage.artifactName).toBe('orca-linux.${ext}')
expect(electronBuilderConfig.deb.artifactName).toBe('orca-ide_${version}_${arch}.${ext}')
expect(electronBuilderConfig.rpm).toMatchObject({
packageName: 'orca-ide',
artifactName: 'orca-ide-${version}.${arch}.${ext}'
})
})
})

View File

@ -12,6 +12,7 @@ export function getRequiredReleaseAssetNames(tag) {
'latest.yml',
'orca-linux.AppImage',
`orca-ide_${version}_amd64.deb`,
`orca-ide-${version}.x86_64.rpm`,
'orca-windows-setup.exe',
'orca-windows-setup.exe.blockmap',
`Orca-${version}-mac.zip`,

View File

@ -44,6 +44,16 @@ describe('getRequiredReleaseAssetNames', () => {
])
)
})
it('includes the Linux RPM alongside the existing AppImage and deb names', () => {
expect(getRequiredReleaseAssetNames('v1.4.27')).toEqual(
expect.arrayContaining([
'orca-linux.AppImage',
'orca-ide_1.4.27_amd64.deb',
'orca-ide-1.4.27.x86_64.rpm'
])
)
})
})
describe('extractManifestAssetNames', () => {

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# Why: invoked via ~/.local/bin/orca symlink (from CliInstaller); resolve it
# Why: invoked via ~/.local/bin/orca-ide symlink (from CliInstaller); resolve it
# so APP_DIR points at the real install, not the symlink's parent.
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do

View File

@ -20,9 +20,13 @@ description: >-
Use this skill when the task should go through Orca's control plane rather than directly through `git`, shell PTYs, or ad hoc filesystem access.
## Platform Note
On Linux, the CLI command is `orca-ide` (not `orca`) to avoid conflicting with GNOME Orca, the accessibility screen reader. Everywhere this document says `orca <subcommand>`, Linux users should substitute `orca-ide <subcommand>`. macOS and Windows are unaffected.
## When To Use
Use `orca` for:
Use `orca` (or `orca-ide` on Linux) for:
- worktree orchestration inside a running Orca app
- updating the current worktree comment with meaningful progress checkpoints
@ -30,7 +34,7 @@ Use `orca` for:
- stopping or waiting on Orca-managed terminals
- creating and managing scheduled Orca automations
- accessing repos known to Orca
Do not use `orca` when plain shell tools are simpler and Orca state does not matter.
Do not use `orca` / `orca-ide` when plain shell tools are simpler and Orca state does not matter.
Examples:
@ -42,27 +46,30 @@ Examples:
## Preconditions
- Prefer the public `orca` command first
- Prefer the public `orca` command first (`orca-ide` on Linux)
- Orca editor/runtime should already be running, or the agent should start it with `orca open`
- Do not begin by inspecting Orca source files just to decide how to invoke the CLI. The first step is to check whether the installed `orca` command exists.
- Do not begin by inspecting Orca source files just to decide how to invoke the CLI. The first step is to check whether the installed `orca` / `orca-ide` command exists.
- Do not assume a generic shell environment variable proves the agent is "inside Orca". For normal agent flows, the public CLI is the supported surface, but avoid wasting a round trip on probe-only checks when a direct Orca action would answer the question.
First verify the public CLI is installed:
```bash
# macOS / Windows
command -v orca
# Linux
command -v orca-ide
```
Then use the public command:
```bash
orca status --json
orca status --json # or orca-ide on Linux
```
If the task is about Orca worktrees or Orca terminals, do this before any codebase exploration:
```bash
command -v orca
command -v orca # or orca-ide on Linux
orca status --json
```
@ -72,7 +79,7 @@ If the agent truly needs to confirm that the current directory is inside an Orca
orca worktree current --json
```
If `orca` is not on PATH, say so explicitly and stop or ask the user to install/register the CLI before continuing.
If `orca` / `orca-ide` is not on PATH, say so explicitly and stop or ask the user to install/register the CLI before continuing.
## Core Workflow
@ -237,7 +244,7 @@ Why: `--direction horizontal` splits the pane **left and right** (new pane appea
- Treat `orca worktree set --worktree active --comment ... --json` as a default coding-agent behavior whenever the agent reaches a meaningful checkpoint in the current Orca-managed worktree; the user does not need to explicitly ask for each update.
- Update the worktree comment at significant checkpoints, not every trivial command. Good checkpoints include reproducing a bug, confirming a hypothesis, starting a risky migration, finishing a meaningful implementation slice, switching from investigation to fix, or blocking on external input.
- Write comments as short status snapshots of the current state, for example `debugging AWS CLI profile resolution`, `confirmed flaky test is caused by temp-dir race`, or `fix implemented; running integration tests`.
- Prefer optimistic execution over probe-first flows for checkpoint updates: if `orca` is on `PATH`, call `orca worktree set --worktree active --comment ... --json` directly at the checkpoint instead of spending an extra cycle on `orca worktree current`.
- Prefer optimistic execution over probe-first flows for checkpoint updates: if `orca` (or `orca-ide` on Linux) is on `PATH`, call `orca worktree set --worktree active --comment ... --json` directly at the checkpoint instead of spending an extra cycle on `orca worktree current`.
- If that direct update fails because Orca is unavailable or the shell is not inside an Orca-managed worktree, continue the main task and treat the comment update as best-effort unless the user explicitly made Orca state part of the task.
- Use `orca worktree current --json` only when the agent actually needs the worktree identity for later logic, not as a preflight before every comment update.
- Orca only injects `ORCA_WORKTREE_PATH`-style variables for some setup-hook flows, so they are not a general detection contract for agents.
@ -249,12 +256,12 @@ Why: `--direction horizontal` splits the pane **left and right** (new pane appea
- Use `terminal create` to spin up new terminal tabs programmatically, optionally with a `--command` for startup (e.g. `--command "claude"` to launch Claude Code) and `--title` for labeling. In local Orca sessions, `--command "codex"` is routed through Orca's visible terminal path automatically so Codex does not start as a headless/background PTY. After creating a `--command` terminal, use `terminal wait --for tui-idle` to wait for the agent to boot before dispatching.
- Use `terminal split` to create split panes within an existing terminal tab. Pass `--command` to run a command in the new pane.
- Prefer Orca worktree selectors over hardcoded paths when Orca identity already exists.
- If the user asks for CLI UX feedback, test the public `orca` command first. Only inspect `src/cli` or use `node out/cli/index.js` if the public command is missing or the task is explicitly about implementation internals.
- If a command fails, prefer retrying with the public `orca` command before concluding the CLI is broken, unless the failure already came from `orca` itself.
- If the user asks for CLI UX feedback, test the public `orca` / `orca-ide` command first. Only inspect `src/cli` or use `node out/cli/index.js` if the public command is missing or the task is explicitly about implementation internals.
- If a command fails, prefer retrying with the public `orca` / `orca-ide` command before concluding the CLI is broken, unless the failure already came from the CLI itself.
## Browser Automation
The `orca` CLI also drives the built-in Orca browser. The core workflow is a **snapshot-interact-re-snapshot** loop:
The `orca` CLI (or `orca-ide` on Linux) also drives the built-in Orca browser. The core workflow is a **snapshot-interact-re-snapshot** loop:
1. **Snapshot** the page to see interactive elements and their refs.
2. **Interact** using refs (`@e1`, `@e3`, etc.) to click, fill, or select.
@ -626,7 +633,7 @@ When `orca tab create` opens a new tab, it is automatically set as the active ta
- Terminal handles are ephemeral and tied to the current Orca runtime. If Orca restarts, handles change.
- `terminal wait` supports `--for exit` (wait for process exit) and `--for tui-idle` (wait for a recognized agent CLI like Claude Code, Gemini, or Codex to finish its current task, detected via OSC title transitions). `tui-idle` defaults to a 5-minute timeout if `--timeout-ms` is not specified. Real coding tasks routinely take 15-60 minutes — always pass `--timeout-ms` explicitly.
- Orca is the source of truth for worktree/terminal state; do not duplicate that state with manual assumptions.
- The public `orca` command is the interface users experience. Agents should validate and use that surface, not repo-local implementation entrypoints.
- The public `orca` command (`orca-ide` on Linux) is the interface users experience. Agents should validate and use that surface, not repo-local implementation entrypoints.
- The default bounded `terminal read` preview is for status monitoring. For retained transcript extraction, use `terminal read --json` with `oldestCursor`/`nextCursor`, `--cursor`, and `--limit`.
## References

View File

@ -28,7 +28,7 @@ Use `orca-cli` instead for ordinary terminal control, shell commands, browser au
## Preconditions
- Orca must be running (`orca status --json` should return `runtime: true`).
- The `orca` CLI must be on PATH (installed via Settings > Browser > Enable Orca CLI).
- The `orca` CLI must be on PATH (`orca-ide` on Linux; installed via Settings > Browser > Enable Orca CLI).
- The orchestration experimental feature must be enabled in Settings > Experimental.
- All `orca orchestration` commands are RPC calls to the running Orca runtime — they require an active Orca session.
@ -92,7 +92,7 @@ orca orchestration dispatch --task <task_id> --to <handle> [--from <handle>] [--
orca orchestration dispatch-show --task <task_id> [--json]
```
Why: `--inject` sends a preamble that teaches the agent how to use `orca orchestration send --type worker_done` to report completion. All agents have `orca` on PATH and can execute shell commands. The preamble maximizes structured feedback but the system works without it (coordinator falls back to idle detection + output reading).
Why: `--inject` sends a preamble that teaches the agent how to use `orca orchestration send --type worker_done` to report completion. All agents have `orca` (or `orca-ide` on Linux) on PATH and can execute shell commands. The preamble maximizes structured feedback but the system works without it (coordinator falls back to idle detection + output reading).
Why: `--inject` requires a recognized agent CLI (e.g. Claude Code) running in the target terminal. If the terminal is a bare shell, omit `--inject` and send the prompt manually with `terminal send`.

View File

@ -1,4 +1,4 @@
import { mkdtemp, mkdir, readFile, symlink, writeFile } from 'node:fs/promises'
import { lstat, mkdtemp, mkdir, readFile, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
@ -33,60 +33,98 @@ describe('CliInstaller', () => {
})
// Why: this test creates Unix symlinks and shell scripts that only apply on macOS.
it.skipIf(process.platform === 'win32')('creates a dev launcher and installs a macOS symlink in the requested path', async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, 'bin', 'orca')
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
commandPathOverride: installPath,
processPathEnv: join(fixture.root, 'bin')
})
it.skipIf(process.platform === 'win32')(
'creates a dev launcher and installs a macOS symlink in the requested path',
async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, 'bin', 'orca')
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
commandPathOverride: installPath,
processPathEnv: join(fixture.root, 'bin')
})
const initial = await installer.getStatus()
expect(initial.state).toBe('not_installed')
expect(initial.launcherPath).toContain(join('userData', 'cli', 'bin', 'orca'))
const initial = await installer.getStatus()
expect(initial.state).toBe('not_installed')
expect(initial.launcherPath).toContain(join('userData', 'cli', 'bin', 'orca'))
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.pathConfigured).toBe(true)
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.pathConfigured).toBe(true)
const launcherContent = await readFile(installed.launcherPath as string, 'utf8')
expect(launcherContent).toContain('ELECTRON_RUN_AS_NODE=1')
expect(launcherContent).toContain(join(fixture.appPath, 'out', 'cli', 'index.js'))
const launcherContent = await readFile(installed.launcherPath as string, 'utf8')
expect(launcherContent).toContain('ELECTRON_RUN_AS_NODE=1')
expect(launcherContent).toContain(join(fixture.appPath, 'out', 'cli', 'index.js'))
const removed = await installer.remove()
expect(removed.state).toBe('not_installed')
})
const removed = await installer.remove()
expect(removed.state).toBe('not_installed')
}
)
// Why: this test creates Unix symlinks and shell scripts that only apply on Linux.
it.skipIf(process.platform === 'win32')('creates a linux symlink under the requested path and warns when PATH is missing', async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, '.local', 'bin', 'orca')
const installer = new CliInstaller({
platform: 'linux',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/opt/Orca/orca',
appPath: fixture.appPath,
commandPathOverride: installPath,
processPathEnv: '/usr/bin'
})
it.skipIf(process.platform === 'win32')(
'creates a linux symlink under the requested path and warns when PATH is missing',
async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, '.local', 'bin', 'orca-ide')
const installer = new CliInstaller({
platform: 'linux',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/opt/Orca/orca-ide',
appPath: fixture.appPath,
commandPathOverride: installPath,
processPathEnv: '/usr/bin'
})
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.pathConfigured).toBe(false)
expect(installed.detail).toContain('.local')
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.commandName).toBe('orca-ide')
expect(installed.pathConfigured).toBe(false)
expect(installed.detail).toContain('.local')
const launcherContent = await readFile(installed.launcherPath as string, 'utf8')
expect(launcherContent).toContain('ELECTRON_RUN_AS_NODE=1')
const launcherContent = await readFile(installed.launcherPath as string, 'utf8')
expect(launcherContent).toContain('ELECTRON_RUN_AS_NODE=1')
const removed = await installer.remove()
expect(removed.state).toBe('not_installed')
})
const removed = await installer.remove()
expect(removed.state).toBe('not_installed')
}
)
// Why: Linux renamed the public command to avoid shadowing GNOME Orca, so
// upgrading must clean up only the old symlink owned by prior Orca installs.
it.skipIf(process.platform === 'win32')(
'removes the old managed linux orca symlink when installing orca-ide',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const commandDir = join(homePath, '.local', 'bin')
const oldLauncherPath = join(fixture.userDataPath, 'cli', 'bin', 'orca')
const legacyCommandPath = join(commandDir, 'orca')
await mkdir(commandDir, { recursive: true })
await mkdir(join(fixture.userDataPath, 'cli', 'bin'), { recursive: true })
await writeFile(oldLauncherPath, '#!/usr/bin/env bash\n', 'utf8')
await symlink(oldLauncherPath, legacyCommandPath)
const installer = new CliInstaller({
platform: 'linux',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/opt/Orca/orca-ide',
appPath: fixture.appPath,
homePath,
processPathEnv: commandDir
})
const installed = await installer.install()
expect(installed.commandPath).toBe(join(commandDir, 'orca-ide'))
await expect(lstat(legacyCommandPath)).rejects.toMatchObject({ code: 'ENOENT' })
}
)
it('creates a windows wrapper and updates the user PATH', async () => {
const fixture = await makeFixture()
@ -120,24 +158,27 @@ describe('CliInstaller', () => {
})
// Why: this test creates a Unix symlink to /tmp/not-orca, which only applies on macOS/Linux.
it.skipIf(process.platform === 'win32')('reports stale when a different symlink already exists', async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, 'bin', 'orca')
await mkdir(join(fixture.root, 'bin'), { recursive: true })
await symlink('/tmp/not-orca', installPath)
it.skipIf(process.platform === 'win32')(
'reports stale when a different symlink already exists',
async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, 'bin', 'orca')
await mkdir(join(fixture.root, 'bin'), { recursive: true })
await symlink('/tmp/not-orca', installPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
commandPathOverride: installPath
})
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
commandPathOverride: installPath
})
await expect(installer.getStatus()).resolves.toMatchObject({
state: 'stale',
supported: true
})
})
await expect(installer.getStatus()).resolves.toMatchObject({
state: 'stale',
supported: true
})
}
)
})

View File

@ -10,6 +10,8 @@ import type { CliInstallMethod, CliInstallStatus } from '../../shared/cli-instal
const execFileAsync = promisify(execFile)
const DEFAULT_MAC_COMMAND_PATH = '/usr/local/bin/orca'
const LINUX_COMMAND_NAME = 'orca-ide'
const LEGACY_LINUX_COMMAND_NAME = 'orca'
const DEV_LAUNCHER_DIR = ['cli', 'bin']
type CliInstallerOptions = {
@ -48,6 +50,11 @@ export class CliInstaller {
private readonly userPathReader: () => Promise<string | null>
private readonly userPathWriter: (value: string) => Promise<void>
// Why: Linux uses `orca-ide` to avoid shadowing GNOME Orca's /usr/bin/orca.
private get commandName(): string {
return this.platform === 'linux' ? LINUX_COMMAND_NAME : 'orca'
}
constructor(options: CliInstallerOptions = {}) {
this.platform = options.platform ?? process.platform
this.isPackaged = options.isPackaged ?? app.isPackaged
@ -73,7 +80,7 @@ export class CliInstaller {
if (!spec) {
return {
platform: this.platform,
commandName: 'orca',
commandName: this.commandName,
commandPath: null,
pathDirectory: null,
pathConfigured: false,
@ -91,7 +98,7 @@ export class CliInstaller {
if (!launcherPath) {
return {
platform: this.platform,
commandName: 'orca',
commandName: this.commandName,
commandPath: spec.commandPath,
pathDirectory: dirname(spec.commandPath),
pathConfigured: false,
@ -130,6 +137,7 @@ export class CliInstaller {
// eslint-disable-next-line unicorn/prefer-ternary -- Why: the install path performs async side effects and is easier to audit as an explicit branch than as an awaited ternary.
if (status.installMethod === 'symlink') {
await this.installSymlink(status)
await this.removeLegacyLinuxCommandIfManaged(status.launcherPath)
} else {
await this.installWindowsWrapper(status.commandPath, status.launcherPath)
}
@ -150,6 +158,7 @@ export class CliInstaller {
return status
}
if (status.state === 'not_installed') {
await this.removeLegacyLinuxCommandIfManaged(status.launcherPath)
if (this.platform === 'win32') {
await this.removeWindowsPathEntry(dirname(status.commandPath))
return this.getStatus()
@ -165,6 +174,7 @@ export class CliInstaller {
if (status.installMethod === 'symlink') {
await this.removeSymlink(status.commandPath)
await this.removeLegacyLinuxCommandIfManaged(status.launcherPath)
} else {
await unlink(status.commandPath)
await this.removeWindowsPathEntry(dirname(status.commandPath))
@ -209,7 +219,10 @@ export class CliInstaller {
// Why: Linux does not have a single privileged global shell-command flow
// equivalent to macOS's /usr/local/bin integration. ~/.local/bin is the
// least surprising user-scoped location that many distros already expose.
return join(this.homePath, '.local', 'bin', 'orca')
// Why `orca-ide`: GNOME Orca (the screen reader) ships /usr/bin/orca on
// most Linux distros. Using `orca-ide` avoids shadowing that system
// command, matching the executableName already used for the Electron binary.
return join(this.homePath, '.local', 'bin', LINUX_COMMAND_NAME)
}
if (this.platform === 'win32') {
@ -275,6 +288,36 @@ export class CliInstaller {
}
}
private async removeLegacyLinuxCommandIfManaged(launcherPath: string | null): Promise<void> {
if (this.platform !== 'linux' || this.commandPathOverride || !launcherPath) {
return
}
const legacyCommandPath = join(this.homePath, '.local', 'bin', LEGACY_LINUX_COMMAND_NAME)
try {
const stats = await lstat(legacyCommandPath)
if (!stats.isSymbolicLink()) {
return
}
const currentTarget = await readlink(legacyCommandPath)
const resolvedCurrentTarget = resolve(dirname(legacyCommandPath), currentTarget)
const legacyLauncherPath = resolve(dirname(launcherPath), LEGACY_LINUX_COMMAND_NAME)
if (resolvedCurrentTarget !== legacyLauncherPath) {
return
}
// Why: after the Linux command rename, the old Orca-owned `orca` symlink
// would keep shadowing GNOME Orca even though the new command is installed.
await unlink(legacyCommandPath)
} catch (error) {
if (isMissingError(error)) {
return
}
throw error
}
}
private async installWindowsWrapper(commandPath: string, launcherPath: string): Promise<void> {
await writeFile(commandPath, buildWindowsForwarder(launcherPath), 'utf8')
}
@ -387,7 +430,7 @@ export class CliInstaller {
}): CliInstallStatus {
return {
platform: this.platform,
commandName: 'orca',
commandName: this.commandName,
commandPath: args.commandPath,
pathDirectory: dirname(args.commandPath),
pathConfigured: false,
@ -480,7 +523,7 @@ async function ensureDevLauncher(args: {
const launcherPath = join(
args.userDataPath,
...DEV_LAUNCHER_DIR,
args.platform === 'win32' ? 'orca.cmd' : 'orca'
args.platform === 'win32' ? 'orca.cmd' : args.platform === 'linux' ? LINUX_COMMAND_NAME : 'orca'
)
await mkdir(dirname(launcherPath), { recursive: true })
@ -623,9 +666,12 @@ export function getBundledLauncherPath(
platform: NodeJS.Platform,
resourcesPath: string
): string | null {
if (platform === 'darwin' || platform === 'linux') {
if (platform === 'darwin') {
return join(resourcesPath, 'bin', 'orca')
}
if (platform === 'linux') {
return join(resourcesPath, 'bin', LINUX_COMMAND_NAME)
}
if (platform === 'win32') {
return join(resourcesPath, 'bin', 'orca.cmd')
}

View File

@ -1,5 +1,5 @@
import { execFile } from 'node:child_process'
import { copyFile, chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
import { copyFile, chmod, mkdir, mkdtemp, rm, stat, symlink, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@ -12,7 +12,7 @@ const itRunsUnixShell = process.platform === 'win32' ? it.skip : it
const builderConfig = require('../../../config/electron-builder.config.cjs') as {
asarUnpack?: string[]
}
const linuxLauncherAsset = new URL('../../../resources/linux/bin/orca', import.meta.url)
const linuxLauncherAsset = new URL('../../../resources/linux/bin/orca-ide', import.meta.url)
describe('packaged CLI assets', () => {
it('unpacks runtime dependencies used before Electron asar integration is available', () => {
@ -26,6 +26,11 @@ describe('packaged CLI assets', () => {
)
})
itRunsUnixShell('keeps the Linux launcher executable in packaged resources', async () => {
const launcherStats = await stat(linuxLauncherAsset)
expect(launcherStats.mode & 0o111).not.toBe(0)
})
itRunsUnixShell(
'runs the Linux launcher from its packaged path and installed symlink',
async () => {
@ -35,7 +40,7 @@ describe('packaged CLI assets', () => {
const resourcesDir = join(appDir, 'resources')
const launcherDir = join(resourcesDir, 'bin')
const cliDir = join(resourcesDir, 'app.asar.unpacked', 'out', 'cli')
const launcherPath = join(launcherDir, 'orca')
const launcherPath = join(launcherDir, 'orca-ide')
const electronPath = join(appDir, 'orca-ide')
const cliPath = join(cliDir, 'index.js')
@ -62,7 +67,7 @@ printf 'arg=%s\\n' "$@"
const homeDir = join(root, 'home')
const commandDir = join(homeDir, '.local', 'bin')
const commandPath = join(commandDir, 'orca')
const commandPath = join(commandDir, 'orca-ide')
await mkdir(commandDir, { recursive: true })
await mkdir(join(homeDir, 'orca'), { recursive: true })
await symlink(launcherPath, commandPath)

View File

@ -20,7 +20,7 @@ function makeHostStatus(launcherPath = 'C:\\Users\\me\\AppData\\Local\\Orca\\bin
}
function createWslRunner(initialFile: string | null = null, pathIncludesLocalBin = true) {
const commandPath = '/home/alice/.local/bin/orca'
const commandPath = '/home/alice/.local/bin/orca-ide'
const bridgePath = '/home/alice/.local/share/orca/orca-wsl-bridge.ps1'
const files = new Map<string, string>()
if (initialFile !== null) {
@ -91,7 +91,7 @@ describe('WslCliInstaller', () => {
await expect(installer.getStatus()).resolves.toMatchObject({
state: 'not_installed',
commandPath: '/home/alice/.local/bin/orca'
commandPath: '/home/alice/.local/bin/orca-ide'
})
const installed = await installer.install()
@ -108,6 +108,18 @@ describe('WslCliInstaller', () => {
)
)
expect(wsl.getBridge()).toBe(_internals.buildWslBridgeScript())
const installCommand = wsl.calls.find((command) => command.includes('cat > "$command_tmp"'))
expect(installCommand).toContain("legacy_command_path='/home/alice/.local/bin/orca'")
expect(installCommand).toContain('rm -f "$legacy_command_path"')
})
it('derives the shared WSL bridge path for current and legacy command names', () => {
expect(_internals.getBridgePathFromCommandPath('/home/alice/.local/bin/orca-ide')).toBe(
'/home/alice/.local/share/orca/orca-wsl-bridge.ps1'
)
expect(_internals.getBridgePathFromCommandPath('/home/alice/.local/bin/orca')).toBe(
'/home/alice/.local/share/orca/orca-wsl-bridge.ps1'
)
})
it('reports installed WSL launchers whose bin directory is missing from PATH', async () => {

View File

@ -21,6 +21,8 @@ import {
const execFileAsync = promisify(execFile)
const MANAGED_MARKER = getWslLauncherMarker()
const BRIDGE_MANAGED_MARKER = getWslBridgeMarker()
const WSL_COMMAND_NAME = 'orca-ide'
const LEGACY_WSL_COMMAND_NAME = 'orca'
type WslCliInstallerOptions = {
platform?: NodeJS.Platform
@ -137,6 +139,9 @@ export class WslCliInstaller {
`mkdir -p ${quoteShell(getPosixDirname(getBridgePathFromCommandPath(status.commandPath)))}`,
`command_tmp=${quoteShell(`${status.commandPath}.tmp`)}.$$`,
`bridge_path=${quoteShell(getBridgePathFromCommandPath(status.commandPath))}`,
`legacy_command_path=${quoteShell(
`${getPosixDirname(status.commandPath)}/${LEGACY_WSL_COMMAND_NAME}`
)}`,
'bridge_tmp="${bridge_path}.tmp.$$"',
'cleanup() { rm -f "$command_tmp" "$bridge_tmp"; }',
'trap cleanup EXIT',
@ -158,6 +163,9 @@ export class WslCliInstaller {
getBridgePathFromCommandPath(status.commandPath),
BRIDGE_MANAGED_MARKER
),
// Why: the command was renamed to avoid GNOME Orca; remove only the
// old Orca-managed WSL wrapper so unmanaged `orca` commands survive.
`if [ -f "$legacy_command_path" ] && grep -Fq ${quoteShell(MANAGED_MARKER)} "$legacy_command_path"; then rm -f "$legacy_command_path"; fi`,
`mv -f "$bridge_tmp" ${quoteShell(getBridgePathFromCommandPath(status.commandPath))}`,
`mv -f "$command_tmp" ${quoteShell(status.commandPath)}`,
'trap - EXIT'
@ -240,7 +248,8 @@ export class WslCliInstaller {
}
const pathDirectory = `${home}/.local/bin`
const commandPath = `${pathDirectory}/orca`
// Why: matches the Linux CLI rename to `orca-ide` (avoids GNOME Orca conflict).
const commandPath = `${pathDirectory}/${WSL_COMMAND_NAME}`
const pathConfigured =
(
await this.run(
@ -296,9 +305,9 @@ export class WslCliInstaller {
}): CliInstallStatus {
return {
platform: 'linux',
commandName: 'orca',
commandName: WSL_COMMAND_NAME,
commandPath: args.commandPath,
pathDirectory: args.commandPath.replace(/\/orca$/, ''),
pathDirectory: getPosixDirname(args.commandPath),
pathConfigured: args.pathConfigured,
launcherPath: args.launcherPath,
installMethod: 'wrapper',
@ -308,7 +317,7 @@ export class WslCliInstaller {
unsupportedReason: null,
detail:
args.state === 'installed' && !args.pathConfigured
? `${args.commandPath} is registered, but ${args.commandPath.replace(/\/orca$/, '')} is not on PATH in ${args.distro}.`
? `${args.commandPath} is registered, but ${getPosixDirname(args.commandPath)} is not on PATH in ${args.distro}.`
: args.detail
}
}
@ -319,7 +328,7 @@ export class WslCliInstaller {
): CliInstallStatus {
return {
platform: 'linux',
commandName: 'orca',
commandName: WSL_COMMAND_NAME,
commandPath: null,
pathDirectory: null,
pathConfigured: false,
@ -360,5 +369,6 @@ function buildEncodedWslBashCommand(command: string): string {
export const _internals = {
buildEncodedWslBashCommand,
buildWslBridgeScript,
buildWslLauncher
buildWslLauncher,
getBridgePathFromCommandPath
}

View File

@ -44,7 +44,9 @@ try {
}
export function getBridgePathFromCommandPath(commandPath: string): string {
return `${commandPath.replace(/\/\.local\/bin\/orca$/, '/.local/share/orca')}/orca-wsl-bridge.ps1`
// Why: both the current Linux command and the legacy pre-rename command
// share one WSL bridge under ~/.local/share/orca.
return `${commandPath.replace(/\/\.local\/bin\/(?:orca|orca-ide)$/, '/.local/share/orca')}/orca-wsl-bridge.ps1`
}
export function buildSafeReplaceGuard(path: string, managedMarker: string): string {

View File

@ -61,10 +61,10 @@ export function FloatingTerminalOrchestrationDialog({
const cliInstalled = isOrcaCliAvailableOnPath(cliStatus)
const cliSupported = cliStatus?.supported ?? false
const cliLabel = cliInstalled
? 'orca is on PATH'
? 'Orca CLI is on PATH'
: cliLoading
? 'Checking CLI status...'
: (cliStatus?.detail ?? 'Register orca so agents can call Orca from a terminal.')
: (cliStatus?.detail ?? 'Register the Orca CLI so agents can call Orca from a terminal.')
const handleInstallCli = async (): Promise<void> => {
setCliBusy(true)
@ -77,7 +77,7 @@ export function FloatingTerminalOrchestrationDialog({
onSetupStateChange()
}
if (isOrcaCliAvailableOnPath(next)) {
toast.success('Registered `orca` in PATH.')
toast.success('Registered the Orca CLI in PATH.')
}
} finally {
setCliBusy(false)

View File

@ -31,14 +31,14 @@ const FEATURE_SETUP_ROWS: readonly FeatureSetupRow[] = [
id: 'computerUse',
title: 'Computer Use',
description: 'Agents can inspect app windows and operate local apps when you ask.',
setupSummary: 'Registers `orca`, opens permissions, and prepares the skill.',
setupSummary: 'Registers the Orca CLI, opens permissions, and prepares the skill.',
icon: <MonitorCog className="size-4" />
},
{
id: 'orchestration',
title: 'Agent Orchestration',
description: 'Agents can message each other, take tasks, and coordinate handoffs.',
setupSummary: 'Registers `orca`, enables orchestration, and prepares the skill.',
setupSummary: 'Registers the Orca CLI, enables orchestration, and prepares the skill.',
icon: <Workflow className="size-4" />
}
]

View File

@ -124,7 +124,7 @@ export function BrowserUseSetup({
onStatusChange: setCliStatus
})
if (isOrcaCliAvailableOnPath(next)) {
toast.success('Registered `orca` in PATH.')
toast.success('Registered the Orca CLI in PATH.')
}
} finally {
setCliBusy(false)
@ -254,7 +254,7 @@ export function BrowserUseSetup({
{showStep1 ? (
<SearchableSetting
title="Enable Orca CLI"
description="Register the orca shell command so agents can drive the browser."
description="Register the Orca CLI so agents can drive the browser."
keywords={BROWSER_USE_PANE_SEARCH_ENTRIES[0].keywords}
className="rounded-xl border border-border/60 bg-card/50 p-4"
>
@ -266,8 +266,8 @@ export function BrowserUseSetup({
<div className="min-w-0 flex-1 space-y-1">
<p className="text-sm font-medium">Enable Orca CLI</p>
<p className="text-xs text-muted-foreground">
Registers the <code className="rounded bg-muted px-1 py-0.5 text-[11px]">orca</code>{' '}
command so agents can orchestrate the browser from their shell.
Registers the Orca CLI command so agents can orchestrate the browser from their
shell.
</p>
{cliStatus?.commandPath && cliEnabled ? (
<p className="text-[11px] text-muted-foreground">

View File

@ -47,7 +47,7 @@ function getInstallDescription(platform: string): string {
return 'Register `orca` in /usr/local/bin.'
}
if (platform === 'linux') {
return 'Register `orca` in ~/.local/bin.'
return 'Register `orca-ide` in ~/.local/bin.'
}
if (platform === 'win32') {
return 'Register `orca` in your user PATH.'
@ -55,6 +55,10 @@ function getInstallDescription(platform: string): string {
return 'CLI registration is not yet available on this platform.'
}
function getFallbackCommandName(platform: string): string {
return platform === 'linux' ? 'orca-ide' : 'orca'
}
export function CliSection({ currentPlatform }: CliSectionProps): React.JSX.Element {
const [status, setStatus] = useState<CliInstallStatus | null>(null)
const [loading, setLoading] = useState(true)
@ -88,6 +92,7 @@ export function CliSection({ currentPlatform }: CliSectionProps): React.JSX.Elem
const isSupported = status?.supported ?? false
const isBrowserManaged = status?.unsupportedReason === 'launch_mode_unavailable'
const revealLabel = getRevealLabel(currentPlatform)
const commandName = status?.commandName ?? getFallbackCommandName(currentPlatform)
const canRevealCommandPath =
status?.commandPath != null && ['installed', 'stale', 'conflict'].includes(status.state)
@ -97,9 +102,11 @@ export function CliSection({ currentPlatform }: CliSectionProps): React.JSX.Elem
const next = await window.api.cli.install()
setStatus(next)
setDialogOpen(false)
toast.success('Registered `orca` in PATH.')
toast.success(`Registered \`${next.commandName}\` in PATH.`)
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to register `orca` in PATH.')
toast.error(
error instanceof Error ? error.message : `Failed to register \`${commandName}\` in PATH.`
)
} finally {
setBusyAction(null)
}
@ -111,9 +118,11 @@ export function CliSection({ currentPlatform }: CliSectionProps): React.JSX.Elem
const next = await window.api.cli.remove()
setStatus(next)
setDialogOpen(false)
toast.success('Removed `orca` from PATH.')
toast.success(`Removed \`${next.commandName}\` from PATH.`)
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to remove `orca` from PATH.')
toast.error(
error instanceof Error ? error.message : `Failed to remove \`${commandName}\` from PATH.`
)
} finally {
setBusyAction(null)
}
@ -253,12 +262,14 @@ export function CliSection({ currentPlatform }: CliSectionProps): React.JSX.Elem
<DialogContent>
<DialogHeader>
<DialogTitle>
{isEnabled ? 'Remove `orca` from PATH?' : 'Register `orca` in PATH?'}
{isEnabled
? `Remove \`${commandName}\` from PATH?`
: `Register \`${commandName}\` in PATH?`}
</DialogTitle>
<DialogDescription>
{isEnabled
? 'This removes the shell command symlink. Orca itself remains installed.'
: `Orca will register ${status?.commandPath ?? '`orca`'} so the command works from your terminal.`}
: `Orca will register ${status?.commandPath ?? commandName} so the command works from your terminal.`}
</DialogDescription>
</DialogHeader>
{status?.commandPath ? (

View File

@ -52,6 +52,7 @@ export function WslCliRegistration({
const isEnabled = status?.state === 'installed'
const isSupported = status?.supported ?? false
const commandName = status?.commandName ?? 'orca-ide'
const handleInstall = async (): Promise<void> => {
setBusyAction('install')
@ -59,9 +60,11 @@ export function WslCliRegistration({
const next = await window.api.cli.installWsl()
setStatus(next)
setDialogOpen(false)
toast.success('Registered `orca` in WSL.')
toast.success(`Registered \`${next.commandName}\` in WSL.`)
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to register `orca` in WSL.')
toast.error(
error instanceof Error ? error.message : `Failed to register \`${commandName}\` in WSL.`
)
} finally {
setBusyAction(null)
}
@ -73,9 +76,11 @@ export function WslCliRegistration({
const next = await window.api.cli.removeWsl()
setStatus(next)
setDialogOpen(false)
toast.success('Removed `orca` from WSL.')
toast.success(`Removed \`${next.commandName}\` from WSL.`)
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to remove `orca` from WSL.')
toast.error(
error instanceof Error ? error.message : `Failed to remove \`${commandName}\` from WSL.`
)
} finally {
setBusyAction(null)
}
@ -90,7 +95,7 @@ export function WslCliRegistration({
<p className="text-xs text-muted-foreground">
{loading
? 'Checking WSL CLI registration...'
: (status?.detail ?? 'Register `orca` in ~/.local/bin inside WSL.')}
: (status?.detail ?? 'Register `orca-ide` in ~/.local/bin inside WSL.')}
</p>
</div>
<div className="flex items-center gap-2">
@ -148,12 +153,14 @@ export function WslCliRegistration({
<DialogContent>
<DialogHeader>
<DialogTitle>
{isEnabled ? 'Remove `orca` from WSL?' : 'Register `orca` in WSL?'}
{isEnabled
? `Remove \`${commandName}\` from WSL?`
: `Register \`${commandName}\` in WSL?`}
</DialogTitle>
<DialogDescription>
{isEnabled
? 'This removes the WSL shell command. Orca itself remains installed on Windows.'
: `Orca will register ${status?.commandPath ?? '`orca`'} so the command works from WSL terminals.`}
: `Orca will register ${status?.commandPath ?? commandName} so the command works from WSL terminals.`}
</DialogDescription>
</DialogHeader>
{status?.commandPath ? (

View File

@ -3,7 +3,7 @@ import type { SettingsSearchEntry } from './settings-search'
export const BROWSER_USE_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Enable Orca CLI',
description: 'Register the orca shell command so agents can drive the browser.',
description: 'Register the Orca CLI so agents can drive the browser.',
keywords: ['browser use', 'cli', 'orca', 'path', 'command', 'shell', 'enable', 'setup']
},
{

View File

@ -81,14 +81,14 @@ export const GENERAL_NAVIGATION_SEARCH_ENTRIES: SettingsSearchEntry[] = [
export const GENERAL_CLI_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Orca CLI',
description: 'Register or remove the orca shell command.',
description: 'Register or remove the Orca CLI command.',
keywords: ['cli', 'path', 'terminal', 'command', 'shell command'],
cmdJKeywords: ['cli', 'path', 'command', 'shell command'],
targetSectionId: 'cli'
},
{
title: 'Agent skill',
description: 'Install the Orca skill so agents know to use the orca CLI.',
description: 'Install the Orca skill so agents know to use the Orca CLI.',
keywords: ['skill', 'agents', 'npx']
}
]

View File

@ -7,11 +7,11 @@ type EnsureOrcaCliAvailableOptions = {
}
export const AGENT_SKILL_CLI_PREREQUISITE_NOTICE =
'Before opening setup, Orca may show a system prompt to register the orca command on PATH.'
'Before opening setup, Orca may show a system prompt to register the Orca CLI command on PATH.'
export const CLI_PREREQUISITE_REGISTRATION_TOAST = 'Orca needs to register its CLI on PATH.'
export const CLI_PREREQUISITE_REGISTRATION_TOAST_DESCRIPTION =
'Approve the system prompt so skill setup can use the orca command.'
'Approve the system prompt so skill setup can use the Orca CLI command.'
export function isOrcaCliAvailableOnPath(status: CliInstallStatus | null | undefined): boolean {
return status?.state === 'installed' && status.pathConfigured
@ -42,7 +42,7 @@ export async function ensureOrcaCliAvailableForAgentSkillTerminal({
return status
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to register `orca` in PATH.')
toast.error(error instanceof Error ? error.message : 'Failed to register the Orca CLI in PATH.')
return null
}
}
@ -78,8 +78,8 @@ function showCliPrerequisiteWarning(status: CliInstallStatus): void {
if (!status.pathConfigured) {
// Why: the skill installer opens a real shell; agents only get the expected
// Orca affordances when that shell can resolve the `orca` command.
toast.warning('`orca` is not visible on PATH yet', {
// Orca affordances when that shell can resolve the Orca CLI command.
toast.warning('Orca CLI is not visible on PATH yet', {
description:
status.detail ?? 'Restart your shell or add the Orca CLI directory to PATH before setup.'
})

View File

@ -1675,7 +1675,7 @@ function createPreflightApi(): NonNullable<Partial<PreloadApi>['preflight']> {
function createCliApi(): NonNullable<Partial<PreloadApi>['cli']> {
const status = {
platform: getBrowserPlatform(),
commandName: 'orca',
commandName: getBrowserPlatform() === 'linux' ? 'orca-ide' : 'orca',
commandPath: null,
pathDirectory: null,
pathConfigured: false,