Files
orca/src/cli/worktree-project-target.ts
T
Neil 0bf5209ac8 fix(cli): infer the repo for worktree create when the parent is host-scoped by identity (#29909)
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.
2026-10-16 19:02:47 -07:00

143 lines
5.2 KiB
TypeScript

import { normalizeExecutionHostId, type ParsedExecutionHost } from '../shared/execution-host'
import type { ProjectHostSetup } from '../shared/project-types'
import { hostFilterMatchesHostId, resolveHostFlagTarget } from './execution-host-flag'
import type { RuntimeClient } from './runtime-client'
import { RuntimeClientError } from './runtime-client'
export type ProjectCreateTarget = {
repoSelector: string
setup: ProjectHostSetup
}
function getPresentStringFlag(
flags: Map<string, string | boolean>,
name: string
): string | undefined {
if (!flags.has(name)) {
return undefined
}
const value = flags.get(name)
if (typeof value === 'string' && value.length > 0) {
return value
}
throw new RuntimeClientError('invalid_argument', `Missing value for --${name}`)
}
export function hasWorkspaceProjectTarget(flags: Map<string, string | boolean>): boolean {
return flags.has('project') || flags.has('host') || flags.has('project-host-setup')
}
export function assertWorkspaceTargetFlagsCompatible(flags: Map<string, string | boolean>): void {
const hasProjectTarget = hasWorkspaceProjectTarget(flags)
if (flags.has('repo') && hasProjectTarget) {
throw new RuntimeClientError(
'invalid_argument',
'Choose either --repo or project target flags, not both.'
)
}
if (flags.has('host') && !flags.has('project') && !flags.has('project-host-setup')) {
throw new RuntimeClientError(
'invalid_argument',
'--host requires --project unless --project-host-setup is provided.'
)
}
}
export async function resolveProjectCreateRepoSelector(
flags: Map<string, string | boolean>,
client: RuntimeClient
): Promise<string | undefined> {
return (await resolveProjectCreateTarget(flags, client))?.repoSelector
}
// Why: the routed runtime can hold both an exact `runtime:<id>` row and a `local` row for the
// same project; the exact stamp is the one the caller named, so it wins the selection.
function findReadySetupOnHost(
setups: readonly ProjectHostSetup[],
projectId: string | undefined,
host: ParsedExecutionHost | undefined
): ProjectHostSetup | undefined {
const candidates = setups.filter((candidate) => candidate.projectId === projectId)
if (!host) {
return candidates[0]
}
return (
candidates.find((candidate) => normalizeExecutionHostId(candidate.hostId) === host.id) ??
candidates.find((candidate) => hostFilterMatchesHostId(host, candidate.hostId))
)
}
export async function resolveProjectCreateTarget(
flags: Map<string, string | boolean>,
client: RuntimeClient
): Promise<ProjectCreateTarget | undefined> {
const projectHostSetupId = getPresentStringFlag(flags, 'project-host-setup')
const projectId = getPresentStringFlag(flags, 'project')
const host = await resolveHostFlagTarget(flags, client)
if (!projectHostSetupId && !projectId && !host) {
return undefined
}
let result: Awaited<ReturnType<typeof client.call<{ setups: ProjectHostSetup[] }>>>
try {
result = await client.call<{ setups: ProjectHostSetup[] }>('projectHostSetup.list')
} catch (error) {
// Why: --host runtime:<id> routes here, so an older server is reachable without the caller
// meaning to; name the version gap rather than surfacing a raw method_not_found.
if (error instanceof RuntimeClientError && error.code === 'method_not_found') {
throw new RuntimeClientError(
'incompatible_runtime',
'This Orca server does not support project host setup yet. Update Orca on the server and try again.'
)
}
throw error
}
const ready = result.result.setups.filter((candidate) => candidate.setupState === 'ready')
const setup = projectHostSetupId
? ready.find((candidate) => candidate.id === projectHostSetupId)
: findReadySetupOnHost(ready, projectId, host)
if (!setup) {
throw new RuntimeClientError(
'invalid_argument',
projectHostSetupId
? `Project host setup is not ready or was not found: ${projectHostSetupId}`
: `Project is not set up on the selected host: ${projectId}${host ? ` on ${host.id}` : ''}`
)
}
return {
repoSelector: `id:${setup.repoId}`,
setup
}
}
// Why the worktree id, not the selector: a host-scoped `identity:` selector carries no repo id.
function getRepoSelectorFromWorktreeId(worktreeId: string | undefined): string | undefined {
const separatorIndex = worktreeId?.indexOf('::') ?? -1
if (!worktreeId || separatorIndex <= 0) {
return undefined
}
return `id:${worktreeId.slice(0, separatorIndex)}`
}
export async function getCreateRepoSelector(
flags: Map<string, string | boolean>,
cwdParentWorktree: { worktreeId: string } | undefined,
client: RuntimeClient
): Promise<string> {
const projectRepoSelector = await resolveProjectCreateRepoSelector(flags, client)
if (projectRepoSelector) {
return projectRepoSelector
}
const explicitRepo = getPresentStringFlag(flags, 'repo')
if (explicitRepo) {
return explicitRepo
}
const inferredRepo = getRepoSelectorFromWorktreeId(cwdParentWorktree?.worktreeId)
if (inferredRepo) {
return inferredRepo
}
throw new RuntimeClientError(
'invalid_argument',
'Missing repo selector. Pass --repo or run from inside an Orca-managed worktree.'
)
}