Files
orca/src/cli/selectors.ts
T
NeilandOrca 46646d7ff1 chore(lint): upgrade oxlint to 1.71 + enable 7 new rules (autofixed backlog) (#6841)
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules

Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.

Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):

error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol        (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse            (19: copy-then-reverse -> toReversed)

warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type  (aliasing footgun guard)
- typescript/no-unsafe-function-type         (bans bare Function type)
- unicorn/prefer-array-flat-map              (map().flat() -> flatMap())
- unicorn/prefer-regexp-test                 (.match() in bool ctx -> .test())

mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.

Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.

* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse

mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).

Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-29 22:38:29 -07:00

265 lines
8.2 KiB
TypeScript

import { isAbsolute, relative, resolve as resolvePath } from 'node:path'
import type {
ComputerAppQuery,
RuntimeWorktreeListResult,
RuntimeWorktreeRecord
} from '../shared/runtime-types'
import { isPathInsideOrEqual } from '../shared/cross-platform-path'
import type { RuntimeClient } from './runtime-client'
import { RuntimeClientError } from './runtime-client'
import { getOptionalStringFlag, getRequiredStringFlag } from './flags'
export type BrowserCliTarget = {
worktree?: string
page?: string
}
export type ComputerCliTarget = {
session?: string
worktree?: string
app: ComputerAppQuery
}
export function buildCurrentWorktreeSelector(cwd: string): string {
return `path:${resolvePath(cwd)}`
}
export function normalizeWorktreeSelector(selector: string, cwd: string): string {
if (selector === 'active' || selector === 'current') {
return buildCurrentWorktreeSelector(cwd)
}
return selector
}
function assertLocalCwdWorktreeSelector(selector: string, client: RuntimeClient): void {
if (!client.isRemote) {
return
}
// Why: a paired CLI's cwd belongs to the client machine, not the runtime
// server, so cwd-derived worktree selectors are only valid locally.
throw new RuntimeClientError(
'invalid_argument',
`${selector} is a local cwd shortcut and cannot be resolved against a remote runtime. Pass an explicit server-side worktree selector such as id:<id>, name:<displayName>, branch:<branch>, issue:<number>, or path:<absolute-server-path>.`
)
}
function isWithinPath(parentPath: string, childPath: string): boolean {
if (isPathInsideOrEqual(parentPath, childPath)) {
return true
}
const relativePath = relative(parentPath, childPath)
return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))
}
export async function resolveCurrentWorktreeSelector(
cwd: string,
client: RuntimeClient
): Promise<string> {
assertLocalCwdWorktreeSelector('current', client)
const currentPath = resolvePath(cwd)
const worktrees = await client.call<RuntimeWorktreeListResult>('worktree.list', {
limit: 10_000
})
let enclosingWorktree: RuntimeWorktreeRecord | undefined
let enclosingPathLength = -1
for (const worktree of worktrees.result.worktrees) {
const worktreePath = resolvePath(worktree.path)
if (!isWithinPath(worktreePath, currentPath) || worktreePath.length <= enclosingPathLength) {
continue
}
enclosingWorktree = worktree
enclosingPathLength = worktreePath.length
}
if (!enclosingWorktree) {
throw new RuntimeClientError(
'selector_not_found',
`No Orca-managed worktree contains the current directory: ${currentPath}`
)
}
// Why: users expect "active/current" to mean the enclosing managed worktree
// even from nested subdirectories. Resolve to the concrete runtime id here:
// duplicate repo registrations can expose the same Git worktree path, and a
// path selector would throw selector_ambiguous after losing the repo id.
return `id:${enclosingWorktree.id}`
}
export async function getOptionalWorktreeSelector(
flags: Map<string, string | boolean>,
name: string,
cwd: string,
client: RuntimeClient
): Promise<string | undefined> {
const value = getOptionalStringFlag(flags, name)
if (!value) {
return undefined
}
if (value === 'active' || value === 'current') {
assertLocalCwdWorktreeSelector(value, client)
return await resolveCurrentWorktreeSelector(cwd, client)
}
return normalizeWorktreeSelector(value, cwd)
}
export async function getRequiredWorktreeSelector(
flags: Map<string, string | boolean>,
name: string,
cwd: string,
client: RuntimeClient
): Promise<string> {
const value = getRequiredStringFlag(flags, name)
if (value === 'active' || value === 'current') {
assertLocalCwdWorktreeSelector(value, client)
return await resolveCurrentWorktreeSelector(cwd, client)
}
return normalizeWorktreeSelector(value, cwd)
}
// Why: local browser commands default to the current worktree by auto-resolving
// from cwd. Remote commands omit worktree so the runtime uses server-side focus.
export async function getBrowserWorktreeSelector(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient
): Promise<string | undefined> {
const value = getOptionalStringFlag(flags, 'worktree')
if (value === 'all') {
return undefined
}
if (value) {
if (value === 'active' || value === 'current') {
assertLocalCwdWorktreeSelector(value, client)
return await resolveCurrentWorktreeSelector(cwd, client)
}
return normalizeWorktreeSelector(value, cwd)
}
if (client.isRemote) {
return undefined
}
// Default: auto-resolve from cwd
try {
return await resolveCurrentWorktreeSelector(cwd, client)
} catch {
// Not inside a managed worktree — no filter
return undefined
}
}
// Why: mirrors browser's implicit active-tab targeting. When --terminal is
// omitted, resolve the active terminal in the current worktree so commands
// like `orca terminal send --text "hello" --enter` Just Work.
export async function getTerminalHandle(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient
): Promise<string> {
const explicit = getOptionalStringFlag(flags, 'terminal')
if (explicit) {
return explicit
}
const worktree = await getBrowserWorktreeSelector(flags, cwd, client)
const response = await client.call<{ handle: string }>('terminal.resolveActive', { worktree })
return response.result.handle
}
export async function getBrowserCommandTarget(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient
): Promise<BrowserCliTarget> {
const page = getOptionalStringFlag(flags, 'page')
if (!page) {
return {
worktree: await getBrowserWorktreeSelector(flags, cwd, client)
}
}
const explicitWorktree = getOptionalStringFlag(flags, 'worktree')
if (!explicitWorktree || explicitWorktree === 'all') {
return { page }
}
if (explicitWorktree === 'active' || explicitWorktree === 'current') {
assertLocalCwdWorktreeSelector(explicitWorktree, client)
return {
page,
worktree: await resolveCurrentWorktreeSelector(cwd, client)
}
}
return {
page,
worktree: normalizeWorktreeSelector(explicitWorktree, cwd)
}
}
export async function getComputerCommandTarget(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient
): Promise<ComputerCliTarget> {
const app = getRequiredStringFlag(flags, 'app')
const session = getOptionalStringFlag(flags, 'session')
const worktree = getOptionalStringFlag(flags, 'worktree')
if (session && worktree) {
throw new RuntimeClientError(
'invalid_argument',
'Computer-use targeting accepts either --session or --worktree, not both'
)
}
if (session) {
return { session, app }
}
return {
app,
worktree: await getBrowserWorktreeSelector(flags, cwd, client)
}
}
// Mirror getBrowserCommandTarget / getBrowserWorktreeSelector for emulator (workspace scoped by default + explicit --device/--emulator/--worktree; active from bridge for unqualified).
export type EmulatorCliTarget = {
worktree?: string
device?: string
emulator?: string // Orca id from list
}
export async function getEmulatorWorktreeSelector(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient
): Promise<string | undefined> {
const explicit = getOptionalStringFlag(flags, 'worktree')
if (explicit === 'all') {
return undefined
}
if (explicit) {
if (explicit === 'active' || explicit === 'current') {
assertLocalCwdWorktreeSelector(explicit, client)
return resolveCurrentWorktreeSelector(cwd, client)
}
return explicit
}
if (client.isRemote) {
return undefined
}
try {
return await resolveCurrentWorktreeSelector(cwd, client)
} catch {
return undefined
}
}
export async function getEmulatorCommandTarget(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient
): Promise<EmulatorCliTarget> {
const device = getOptionalStringFlag(flags, 'device')
const emulator = getOptionalStringFlag(flags, 'emulator')
const worktree = await getEmulatorWorktreeSelector(flags, cwd, client)
if (device || emulator) {
return { device: device || undefined, emulator: emulator || undefined, worktree }
}
return { worktree }
}