Files
orca/src/cli/help.ts
T
352e69f09c feat(cli): select and remove any saved agent account from orca account (#38944)
* feat(cli): select and remove any saved agent account from `orca account`

`orca account select` and `orca account rm` accepted only OpenCode and
Devin. They now cover every provider with saved accounts — Claude, Codex,
OpenCode, Devin and Antigravity — through the runtime methods the
status-bar switcher and Settings already use (accounts.selectClaude,
selectCodex/selectCodexForTarget, selectData, antigravitySelect and the
matching remove methods). No runtime or wire change.

`--account` takes an id, an email/label (case-insensitive) or `system`.
A name shared by several accounts is refused with their ids rather than
guessing which subscription to bill. `system` is refused for Antigravity
(no system default), for Claude from the WSL bridge (the RPC has no
target and would reset the Windows host), and always for `rm`.

`orca account list` now prints each account's id and a `system` row,
accepts `--agent` for every provider, and adds the Antigravity block when
the host can read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cli): assert the host refusal for Antigravity accounts from WSL

The host rejects every client-selected WSL target for Antigravity account
management, reads included, so the test now mirrors that refusal and checks
that no select call is made instead of asserting an impossible success path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cli): keep only the account-selection tests that guard this change

Moves the guards into account.test.ts (the handler layer that owns them) and
drops layer-duplicating, copy-pinning, Cartesian and mock-only cases per
AGENTS.md test rules; un-exports resolveAccountId (test-only export).

---------

Co-authored-by: Maxime AUBURTIN <m@hellomax.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:03:51 -08:00

211 lines
8.7 KiB
TypeScript

import type { CommandSpec } from './args'
import { findCommandSpec, isCommandGroup, matches, supportsBrowserPageFlag } from './args'
import { unknownCommandData } from './command-suggestion'
import { formatCommandScopedFlagHelp } from './command-scoped-flag-help'
import { FLAG_HELP_TEXT } from './flag-help-text'
import { ROOT_HELP_TEXT_PRIMARY } from './root-help-text-primary'
import { ROOT_HELP_TEXT_SECONDARY } from './root-help-text-secondary'
const ROOT_HELP_TEXT = [ROOT_HELP_TEXT_PRIMARY, ROOT_HELP_TEXT_SECONDARY].join('\n')
export function printHelp(specs: CommandSpec[], commandPath: string[] = []): void {
const exactSpec = findCommandSpec(specs, commandPath)
if (exactSpec) {
console.log(formatCommandHelp(exactSpec))
return
}
if (isCommandGroup(specs, commandPath)) {
console.log(formatGroupHelp(specs, commandPath))
return
}
if (commandPath.length > 0) {
const { nextSteps } = unknownCommandData(specs, commandPath)
const recovery = nextSteps.map((step) => `Next step: ${step}`).join('\n')
console.log(`Unknown command: ${commandPath.join(' ')}${recovery ? `\n${recovery}` : ''}\n`)
}
console.log(ROOT_HELP_TEXT)
}
export function formatCommandHelp(spec: CommandSpec): string {
const lines = [`orca ${spec.path.join(' ')}`, '', `Usage: ${spec.usage}`, '', spec.summary]
const displayedFlags =
spec.argumentMode === 'passthrough'
? []
: supportsBrowserPageFlag(spec)
? [...spec.allowedFlags, 'page']
: spec.allowedFlags
if (displayedFlags.length > 0) {
lines.push('', 'Options:')
for (const flag of displayedFlags) {
lines.push(` ${formatCommandFlagHelp(flag, spec.path)}`)
}
}
if (spec.notes && spec.notes.length > 0) {
lines.push('', 'Notes:')
for (const note of spec.notes) {
lines.push(` ${note}`)
}
}
if (spec.examples && spec.examples.length > 0) {
lines.push('', 'Examples:')
for (const example of spec.examples) {
lines.push(` $ ${example}`)
}
}
return lines.join('\n')
}
export function formatGroupHelp(specs: CommandSpec[], groupPath: string[]): string {
const group = groupPath.join(' ')
const lines = [`orca ${group}`, '', `Usage: orca ${group} <command> [options]`, '', 'Commands:']
for (const spec of specs) {
if (
spec.hidden === true ||
spec.path.length <= groupPath.length ||
!matches(spec.path.slice(0, groupPath.length), groupPath)
) {
continue
}
lines.push(` ${spec.path.slice(groupPath.length).join(' ').padEnd(18)} ${spec.summary}`)
}
lines.push('', `Run \`orca ${group} <command> --help\` for command-specific usage.`)
return lines.join('\n')
}
function formatCommandFlagHelp(flag: string, commandPath: string[]): string {
const command = commandPath.join(' ')
const scopedHelp = formatCommandScopedFlagHelp(command, flag)
if (scopedHelp) {
return scopedHelp
}
if (command === 'terminal close' && flag === 'tab') {
return '--tab Close the whole tab and wait for durable persistence'
}
if (command === 'terminal set-pane-title' && flag === 'title') {
return '--title <text> Custom title for one pane (pass "" to reset)'
}
if (command === 'linear issue' && flag === 'id') {
return '--id <id> Linear issue key, id, or URL'
}
if ((command === 'linear comment edit' || command === 'linear comment delete') && flag === 'id') {
return '--id <commentId> Linear comment id (a UUID)'
}
if (command === 'linear issue' && flag === 'workspace') {
return '--workspace <id> Connected Linear workspace id'
}
if (command === 'linear search' && flag === 'query') {
return '--query <text> Text to search across Linear issues'
}
if (command === 'linear search' && flag === 'workspace') {
return '--workspace <id|all> Connected Linear workspace id, or all'
}
if (command === 'linear list-issues' && flag === 'cursor') {
return '--cursor <cursor> Opaque cursor from a previous list-issues page; issued cursors bind the workspace, raw Linear cursors need --workspace'
}
if (command === 'linear list-issues' && flag === 'priority') {
return '--priority <0-4> 0=none, 1=urgent, 2=high, 3=medium, 4=low'
}
if (command === 'linear list-issues' && flag === 'limit') {
return '--limit <n> Max issues to return; omit to return every match'
}
if (command === 'artifacts list' && flag === 'cursor') {
return '--cursor <cursor> Opaque cursor returned by a previous artifacts page'
}
if (command === 'orchestration worker-read' && flag === 'cursor') {
return '--cursor <cursor> Opaque cursor returned by a previous worker-read page'
}
if (command === 'orchestration worker-list' && flag === 'cursor') {
return '--cursor <cursor> Opaque page cursor copied from nextCursor'
}
if (command === 'orchestration worker-list' && flag === 'terminal-state') {
return '--terminal-state <state> Terminal accounting filter: active, reclaimable, retained, release_pending, release_unknown, or released'
}
if (command.startsWith('orchestration ') && flag === 'full') {
return '--full Print the full record Orca returned instead of the concise view'
}
if (command === 'skills get' && flag === 'full') {
return '--full Print the full guide with bundled references'
}
if (command === 'orchestration worker-list' && flag === 'include-remote') {
return '--include-remote Include connected-server worker observations'
}
if (command === 'linear list-issues' && flag === 'workspace') {
return '--workspace <id|all> Connected Linear workspace id, or all'
}
if (command.startsWith('linear ') && flag === 'workspace') {
return '--workspace <id> Connected Linear workspace id'
}
if (command.startsWith('linear ') && flag === 'body') {
return '--body <text> Linear comment or issue body'
}
if (command.startsWith('linear ') && flag === 'body-file') {
return '--body-file <path|-> Read Linear body from a file or stdin'
}
if (command.startsWith('linear ') && flag === 'write-id') {
return '--write-id <uuid> Retry id from linear_write_unconfirmed'
}
if (command.startsWith('linear ') && flag === 'to') {
return '--to <state> Exact Linear workflow state name'
}
if (command === 'linear comment add' && flag === 'reply-to') {
return '--reply-to <id> Comment id to reply to'
}
if (command === 'linear attach' && flag === 'url') {
return '--url <url> Absolute http(s) link to attach'
}
if (command === 'linear attach' && flag === 'title') {
return '--title <text> Attachment title'
}
if (command === 'linear create' && flag === 'title') {
return '--title <text> New Linear issue title'
}
if (command === 'linear create' && flag === 'team') {
return '--team <key> Linear team key'
}
if (command === 'linear create' && flag === 'parent') {
return '--parent <id> Parent Linear issue key, id, or URL'
}
if (command === 'linear create' && flag === 'parent-current') {
return '--parent-current Use the current linked issue as parent'
}
if (command === 'worktree create' && flag === 'parent-worktree') {
return '--parent-worktree <selector> Parent selector such as identity:<identity>, active/current, id:<repo-id>::<path>, branch:<branch>, issue:<number>, path:<path>, folder:<id>, or worktree:<worktreeId>'
}
if (command === 'orchestration task-create' && flag === 'task-title') {
return '--task-title <text> Concise title for the orchestration task'
}
if (command === 'orchestration task-create' && flag === 'display-name') {
return '--display-name <text> UI label shown for dispatched worker rows'
}
if (command === 'account add' && flag === 'agent') {
return '--agent <id> Account provider: claude, codex, opencode, or devin (default claude)'
}
if (command.startsWith('account ') && flag === 'agent') {
return '--agent <id> Account provider: claude, codex, opencode, devin, or antigravity'
}
if (command === 'chat rename' && flag === 'session') {
return '--session <id> Chat session id (defaults to the chat this runs in)'
}
if (command === 'chat rename' && flag === 'name') {
return '--name <text> New name for the chat'
}
if (flag === 'key' && command === 'computer hotkey') {
return '--key <key-combo> Modifier chord with one key, e.g. CmdOrCtrl+A'
}
if (flag === 'key' && command === 'computer press-key') {
return '--key <key> Single key, e.g. Return, Escape, Tab, Left, or PageUp'
}
return formatFlagHelp(flag)
}
export function formatFlagHelp(flag: string): string {
return FLAG_HELP_TEXT[flag] ?? `--${flag}`
}