mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
When two hosts share a worktree id, the caller's cwd resolves to an identity: selector, which carries no repo id, so worktree create reported "Missing repo selector". Keep the selected worktree id alongside the selector and infer the repo from it.
334 lines
11 KiB
TypeScript
334 lines
11 KiB
TypeScript
import { resolve as resolvePath } from 'node:path'
|
|
import type { ComputerAppQuery, RuntimeWorktreeListResult } from '../shared/runtime-types'
|
|
import {
|
|
isWslUncPathForCallerLinuxPath,
|
|
isWslUncPathForLinuxMountedPath
|
|
} from '../shared/cross-platform-path'
|
|
import { parseWslUncPath } from '../shared/wsl-paths'
|
|
import type { RuntimeClient } from './runtime-client'
|
|
import { RuntimeClientError } from './runtime/types'
|
|
import { getOptionalStringFlag, getRequiredStringFlag } from './flags'
|
|
import {
|
|
getCallerExecutionHostId,
|
|
resolveCallerPaneWorktreeSelector,
|
|
selectEnclosingWorktreeSelectionOnHost,
|
|
type HostScopedWorktreeSelection
|
|
} from './caller-host-worktree-selection'
|
|
|
|
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
|
|
}
|
|
|
|
/**
|
|
* Rewrites the Linux path a WSL shell prints into the UNC path the runtime stored (#16628).
|
|
*
|
|
* Why here and not in the runtime: only the CLI sits in the distro, so only it can
|
|
* prove which distro the typed path belongs to. Translating once at this chokepoint
|
|
* covers `worktree show`, `terminal list --worktree` and `worktree rm --worktree`.
|
|
*/
|
|
export async function resolveCallerDistroPathSelector(
|
|
selector: string,
|
|
cwd: string,
|
|
client: RuntimeClient
|
|
): Promise<string> {
|
|
// Why not WSL_DISTRO_NAME: it is also set for a Linux-native CLI whose runtime stores
|
|
// POSIX paths, so it would name a distro for a caller that has none. ORCA_CLI_CWD, which
|
|
// the WSL launcher always sets and which arrives here as the invocation cwd, proves it.
|
|
const callerDistro = parseWslUncPath(cwd)?.distro
|
|
const linuxPath = selector.startsWith('path:') ? selector.slice(5) : ''
|
|
const isLinuxMountedPath = /^\/mnt\/[A-Za-z](?:\/|$)/.test(linuxPath)
|
|
if (
|
|
(!callerDistro && !isLinuxMountedPath) ||
|
|
client.isRemote ||
|
|
!linuxPath.startsWith('/') ||
|
|
linuxPath.startsWith('//') ||
|
|
// Backslash is a legal Linux filename character but a separator once a path reads as UNC.
|
|
linuxPath.includes('\\')
|
|
) {
|
|
return selector
|
|
}
|
|
const worktrees = await client.call<RuntimeWorktreeListResult>('worktree.list', {
|
|
limit: 10_000
|
|
})
|
|
const match = worktrees.result.worktrees.find((worktree) =>
|
|
isLinuxMountedPath
|
|
? isWslUncPathForLinuxMountedPath(worktree.path, linuxPath)
|
|
: isWslUncPathForCallerLinuxPath(worktree.path, linuxPath, callerDistro!)
|
|
)
|
|
// Why the stored spelling rather than a synthesized UNC path: an unmatched selector must
|
|
// reach the runtime verbatim and fail as the caller typed it, never as a guessed distro.
|
|
return match ? `path:${match.path}` : selector
|
|
}
|
|
|
|
export async function normalizeWorktreeSelectorForCaller(
|
|
selector: string,
|
|
cwd: string,
|
|
client: RuntimeClient
|
|
): Promise<string> {
|
|
return await resolveCallerDistroPathSelector(
|
|
normalizeWorktreeSelector(selector, cwd),
|
|
cwd,
|
|
client
|
|
)
|
|
}
|
|
|
|
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 identity:<identity>, id:<repo-id>::<path>, name:<displayName>, branch:<branch>, issue:<number>, or path:<absolute-server-path>.`
|
|
)
|
|
}
|
|
|
|
export async function resolveCurrentWorktreeSelector(
|
|
cwd: string,
|
|
client: RuntimeClient
|
|
): Promise<string> {
|
|
return (await resolveCurrentWorktreeSelection(cwd, client)).selector
|
|
}
|
|
|
|
export async function resolveCurrentWorktreeSelection(
|
|
cwd: string,
|
|
client: RuntimeClient
|
|
): Promise<HostScopedWorktreeSelection> {
|
|
assertLocalCwdWorktreeSelector('current', client)
|
|
|
|
const worktrees = await client.call<RuntimeWorktreeListResult>('worktree.list', {
|
|
limit: 10_000
|
|
})
|
|
// Why: users expect "active/current" to mean the enclosing managed worktree
|
|
// even from nested subdirectories. Resolve to the concrete runtime row here:
|
|
// duplicate repo registrations can expose the same Git worktree path, and a
|
|
// path selector would throw selector_ambiguous after losing the repo id.
|
|
const selection = selectEnclosingWorktreeSelectionOnHost(
|
|
worktrees.result.worktrees,
|
|
cwd,
|
|
getCallerExecutionHostId()
|
|
)
|
|
if (!selection) {
|
|
throw new RuntimeClientError(
|
|
'selector_not_found',
|
|
`No Orca-managed worktree contains the current directory: ${resolvePath(cwd)}`
|
|
)
|
|
}
|
|
return selection
|
|
}
|
|
|
|
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 await normalizeWorktreeSelectorForCaller(value, cwd, client)
|
|
}
|
|
|
|
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 await normalizeWorktreeSelectorForCaller(value, cwd, client)
|
|
}
|
|
|
|
// 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 await normalizeWorktreeSelectorForCaller(value, cwd, client)
|
|
}
|
|
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,
|
|
options: { requireUnambiguous?: boolean } = {}
|
|
): 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,
|
|
...(options.requireUnambiguous ? { requireUnambiguous: true } : {})
|
|
})
|
|
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: await normalizeWorktreeSelectorForCaller(explicitWorktree, cwd, client)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Match browser targeting: workspace by default, explicit device/emulator/worktree overrides.
|
|
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
|
|
}
|
|
const terminalWorktreeId = process.env.ORCA_WORKTREE_ID
|
|
if (terminalWorktreeId?.trim()) {
|
|
const scoped = await resolveCallerPaneWorktreeSelector(terminalWorktreeId, client)
|
|
if (scoped) {
|
|
return scoped
|
|
}
|
|
}
|
|
const folderWorkspaceId = process.env.ORCA_WORKSPACE_ID?.trim()
|
|
if (folderWorkspaceId?.startsWith('folder:')) {
|
|
return folderWorkspaceId
|
|
}
|
|
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 }
|
|
}
|