Skip to content

feat(cli): add orca update and orca version commands#9406

Open
scastanoh21 wants to merge 1 commit into
stablyai:mainfrom
scastanoh21:scastanoh21/cli-update-command
Open

feat(cli): add orca update and orca version commands#9406
scastanoh21 wants to merge 1 commit into
stablyai:mainfrom
scastanoh21:scastanoh21/cli-update-command

Conversation

@scastanoh21

Copy link
Copy Markdown

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 orca CLI.

The orca CLI 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-tested electron-updater flow over the runtime RPC the CLI already speaks:

  • orca version — reports the installed version.
  • orca update [--check] [--prerelease] [--json]--check checks and reports without installing; plain update checks → downloads → installs (via the existing quitAndInstall) 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 existing src/main/updater.ts functions and app.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. --json output is a structured object per state.

Testing

Checks run in this worktree (see note on full-suite caveats below):

  • pnpm lint — changed-file oxlint + react-doctor + exhaustive-switch + max-lines ratchet pass. Full-repo pnpm lint is red on a pre-existing, unrelated error (skill-freshness-group.tsx exhaustive-switch) that exists on main and is not touched by this PR.
  • pnpm typecheckpnpm tc:cli and pnpm tc:node both pass (exit 0).
  • Tests — scoped vitest over the changed/added specs passes (RPC methods, CLI handlers, typed client, formatters). Full pnpm test was not run green due to unrelated Node-26 localStorage/zero-test-worker failures independent of this change.
  • pnpm buildpnpm build:cli passes.
  • Added high-quality tests: RPC method params/handlers, CLI handler paths (app-unreachable, error, not-available, prerelease), bounded polling, TTY vs non-TTY progress, and a --json contract 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).

  • Correctness: traced the full path CLI handler → typed runtime client → RPC method → 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; --check does not install.
  • Cross-platform / SSH: RPC-driven (works over the SSH relay against the host's Orca); no hardcoded paths or platform assumptions; the brew upgrade --cask hint is correctly gated on process.platform === 'darwin'. The \r/isTTY progress handling behaves identically on macOS/Linux/Windows terminals.
  • App-not-running: handled gracefully with actionable next steps (no stack trace); covered by a dedicated test.
  • Result: final verdict SHIP / SHIP-WITH-NITS. The one actioned nit was the in-place progress line; remaining nits are cosmetic (untested timeout-close branch, no erase-to-EOL on \r, minor copy inconsistency).

Security Audit

  • The CLI only triggers the existing updater over RPC; install goes through quitAndInstall. No new remote download/exec path, and electron-updater signature verification is not weakened or bypassed.
  • IPC/exposure: the new updater.* methods are deliberately not added to the mobile RPC allowlist, so a paired mobile client cannot trigger updater.install; only the local CLI and the full-trust SSH relay can. Verified against the default-deny gate in runtime-rpc.ts.
  • Input handling: updater.check accepts only an optional { includePrerelease } boolean; invalid params are rejected (tested). No secrets, no command execution from user input.

Notes

  • CLI-only change; no UI. In a dev/unpackaged build the updater reports not-available (expected).
  • Behavior is identical local vs SSH/remote (acts on the Orca instance executing the CLI).

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.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds version and update CLI commands backed by new updater RPC methods. The runtime client exposes version, status, check, download, and install operations. Update handlers support check-only mode, prerelease checks, polling, downloads, installation, TTY progress output, timeout/error exit codes, and runtime recovery guidance. CLI formatting, help text, command registration, argument handling, and unit tests are expanded accordingly.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main CLI command additions.
Description check ✅ Passed The description covers all required sections and provides substantial implementation, testing, security, and platform notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8e8d6fc2-a300-4839-9d37-d903fdbf804e

📥 Commits

Reviewing files that changed from the base of the PR and between ae1c943 and 2f3566a.

📒 Files selected for processing (16)
  • src/cli/args.ts
  • src/cli/dispatch.ts
  • src/cli/format.test.ts
  • src/cli/format.ts
  • src/cli/handlers/updater.test.ts
  • src/cli/handlers/updater.ts
  • src/cli/help.ts
  • src/cli/runtime/client-updater.test.ts
  • src/cli/runtime/client.ts
  • src/cli/specs/index.ts
  • src/cli/specs/updater.ts
  • src/cli/update-format.test.ts
  • src/cli/update-format.ts
  • src/main/runtime/rpc/methods/index.ts
  • src/main/runtime/rpc/methods/updater.test.ts
  • src/main/runtime/rpc/methods/updater.ts

Comment on lines +36 to +58
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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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

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