feat(cli): add orca update and orca version commands#9406
Conversation
The `orca` CLI ships inside the Electron app bundle (its version is the
app version), so it cannot self-update independently. Instead these
commands drive the app's existing electron-updater over the runtime RPC
the CLI already speaks.
- New RPC methods `updater.{getVersion,getStatus,check,download,install}`
(src/main/runtime/rpc/methods/updater.ts, registered in the method
index). They delegate to the existing src/main/updater.ts functions and
app.getVersion(); no hand-rolled installer, and signature verification
is untouched. Not added to the mobile RPC allowlist, so paired mobile
clients cannot trigger an install (local CLI and full-trust SSH relay
can).
- `orca version` reports the installed version.
- `orca update [--check] [--prerelease] [--json]`: bounded polling until a
terminal updater state; `--check` never installs; plain `update` checks
then downloads then installs only when an update is available. Human
download progress renders on a single in-place TTY line and falls back
to newline output when stdout is not a TTY; the --json contract is
unchanged.
- Graceful handling when the app is not running: actionable next steps
instead of a stack trace (brew upgrade --cask on macOS, download link).
- Unit/integration tests for the RPC methods, CLI handlers (app
unreachable, error, not-available, TTY vs non-TTY), the typed runtime
client, and output formatting.
📝 WalkthroughWalkthroughAdds 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 8e8d6fc2-a300-4839-9d37-d903fdbf804e
📒 Files selected for processing (16)
src/cli/args.tssrc/cli/dispatch.tssrc/cli/format.test.tssrc/cli/format.tssrc/cli/handlers/updater.test.tssrc/cli/handlers/updater.tssrc/cli/help.tssrc/cli/runtime/client-updater.test.tssrc/cli/runtime/client.tssrc/cli/specs/index.tssrc/cli/specs/updater.tssrc/cli/update-format.test.tssrc/cli/update-format.tssrc/main/runtime/rpc/methods/index.tssrc/main/runtime/rpc/methods/updater.test.tssrc/main/runtime/rpc/methods/updater.ts
| const check = await pollForStatus( | ||
| client, | ||
| initialCheck, | ||
| (status) => | ||
| status.state === 'available' || | ||
| status.state === 'not-available' || | ||
| status.state === 'error', | ||
| CHECK_POLL_ATTEMPTS | ||
| ) | ||
|
|
||
| if (checkOnly || check.timedOut || check.response.result.state !== 'available') { | ||
| finishUpdateCommand(check.response, json, { | ||
| operation: checkOnly ? 'check' : 'update', | ||
| installRequested: false, | ||
| timedOut: check.timedOut | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| if (!json) { | ||
| console.log(`Update available: Orca ${check.response.result.version}. Downloading...`) | ||
| } | ||
| const initialDownload = await withUpdaterRecovery(() => client.downloadUpdate()) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Support attaching to an already-downloading update.
If orca update is executed while the desktop app is already downloading an update (e.g., initiated via the UI), the check might return a downloading or downloaded state. Because these states aren't in the check phase's terminal condition, the CLI will poll for 30 seconds, time out, and exit with an error instead of seamlessly attaching to the existing download progress.
Consider treating these states as terminal for the initial check to jump straight into the download phase.
💡 Proposed fix to attach to an in-progress download
const check = await pollForStatus(
client,
initialCheck,
(status) =>
status.state === 'available' ||
status.state === 'not-available' ||
- status.state === 'error',
+ status.state === 'error' ||
+ (!checkOnly && (status.state === 'downloading' || status.state === 'downloaded')),
CHECK_POLL_ATTEMPTS
)
- if (checkOnly || check.timedOut || check.response.result.state !== 'available') {
+ if (
+ checkOnly ||
+ check.timedOut ||
+ (check.response.result.state !== 'available' &&
+ check.response.result.state !== 'downloading' &&
+ check.response.result.state !== 'downloaded')
+ ) {
finishUpdateCommand(check.response, json, {
operation: checkOnly ? 'check' : 'update',
installRequested: false,
timedOut: check.timedOut
})
return
}
- if (!json) {
+ if (!json && check.response.result.state === 'available') {
console.log(`Update available: Orca ${check.response.result.version}. Downloading...`)
}
- const initialDownload = await withUpdaterRecovery(() => client.downloadUpdate())
+ const initialDownload =
+ check.response.result.state === 'available'
+ ? await withUpdaterRecovery(() => client.downloadUpdate())
+ : check.response📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const check = await pollForStatus( | |
| client, | |
| initialCheck, | |
| (status) => | |
| status.state === 'available' || | |
| status.state === 'not-available' || | |
| status.state === 'error', | |
| CHECK_POLL_ATTEMPTS | |
| ) | |
| if (checkOnly || check.timedOut || check.response.result.state !== 'available') { | |
| finishUpdateCommand(check.response, json, { | |
| operation: checkOnly ? 'check' : 'update', | |
| installRequested: false, | |
| timedOut: check.timedOut | |
| }) | |
| return | |
| } | |
| if (!json) { | |
| console.log(`Update available: Orca ${check.response.result.version}. Downloading...`) | |
| } | |
| const initialDownload = await withUpdaterRecovery(() => client.downloadUpdate()) | |
| const check = await pollForStatus( | |
| client, | |
| initialCheck, | |
| (status) => | |
| status.state === 'available' || | |
| status.state === 'not-available' || | |
| status.state === 'error' || | |
| (!checkOnly && (status.state === 'downloading' || status.state === 'downloaded')), | |
| CHECK_POLL_ATTEMPTS | |
| ) | |
| if ( | |
| checkOnly || | |
| check.timedOut || | |
| (check.response.result.state !== 'available' && | |
| check.response.result.state !== 'downloading' && | |
| check.response.result.state !== 'downloaded') | |
| ) { | |
| finishUpdateCommand(check.response, json, { | |
| operation: checkOnly ? 'check' : 'update', | |
| installRequested: false, | |
| timedOut: check.timedOut | |
| }) | |
| return | |
| } | |
| if (!json && check.response.result.state === 'available') { | |
| console.log(`Update available: Orca ${check.response.result.version}. Downloading...`) | |
| } | |
| const initialDownload = | |
| check.response.result.state === 'available' | |
| ? await withUpdaterRecovery(() => client.downloadUpdate()) | |
| : check.response |
Summary
Adds two CLI commands so users (and agents) can update Orca and check its version from the terminal — there was previously no way to do either from the
orcaCLI.The
orcaCLI ships bundled inside the Electron app (its version is the app version), so it cannot self-update as an independent binary. Instead these commands drive the app's existing, battle-testedelectron-updaterflow over the runtime RPC the CLI already speaks:orca version— reports the installed version.orca update [--check] [--prerelease] [--json]—--checkchecks and reports without installing; plainupdatechecks → downloads → installs (via the existingquitAndInstall) only when an update is available.New RPC methods
updater.{getVersion,getStatus,check,download,install}(src/main/runtime/rpc/methods/updater.ts, registered in the method index) delegate to the existingsrc/main/updater.tsfunctions andapp.getVersion(). No hand-rolled installer; signature verification is untouched.Screenshots
No visual change (CLI only). Terminal behavior: human download progress renders on a single in-place line on a TTY (
Downloading Orca <version>… NN%), and falls back to newline output when stdout is not a TTY.--jsonoutput is a structured object per state.Testing
Checks run in this worktree (see note on full-suite caveats below):
pnpm lint— changed-fileoxlint+ react-doctor + exhaustive-switch + max-lines ratchet pass. Full-repopnpm lintis red on a pre-existing, unrelated error (skill-freshness-group.tsxexhaustive-switch) that exists onmainand is not touched by this PR.pnpm typecheck—pnpm tc:cliandpnpm tc:nodeboth pass (exit 0).vitestover the changed/added specs passes (RPC methods, CLI handlers, typed client, formatters). Fullpnpm testwas not run green due to unrelated Node-26localStorage/zero-test-worker failures independent of this change.pnpm build—pnpm build:clipasses.--jsoncontract test.AI Review Report
Reviewed across two rounds with an AI reviewer (Fable 5), each independently re-running typecheck, the scoped test files, and
oxlint(not trusting the implementer's claims).src/main/updater.ts; method names/param/return shapes match on both sides. Polling is genuinely bounded (60×500ms check / 1200×500ms download) with all terminal states handled;--checkdoes not install.brew upgrade --caskhint is correctly gated onprocess.platform === 'darwin'. The\r/isTTYprogress handling behaves identically on macOS/Linux/Windows terminals.\r, minor copy inconsistency).Security Audit
quitAndInstall. No new remote download/exec path, and electron-updater signature verification is not weakened or bypassed.updater.*methods are deliberately not added to the mobile RPC allowlist, so a paired mobile client cannot triggerupdater.install; only the local CLI and the full-trust SSH relay can. Verified against the default-deny gate inruntime-rpc.ts.updater.checkaccepts only an optional{ includePrerelease }boolean; invalid params are rejected (tested). No secrets, no command execution from user input.Notes
not-available(expected).