import type { RuntimeWorktreeListResult, RuntimeWorktreePsResult, RuntimeWorktreeRecord, RuntimeWorktreeCreateResult, RuntimeWorktreeRemoveResult } from '../../shared/runtime-types' import type { CommandHandler } from '../dispatch' import { formatWorktreeList, formatWorktreePs, formatWorktreeShow, printResult } from '../format' import { RuntimeClientError } from '../runtime-client' import { getOptionalNullableNumberFlag, getOptionalNumberFlag, getOptionalPositiveIntegerFlag, getOptionalStringFlag, getRequiredStringFlag } from '../flags' import { getOptionalWorktreeSelector, getRequiredWorktreeSelector, resolveCurrentWorktreeSelector } from '../selectors' import { isTuiAgent } from '../../shared/tui-agent-config' import { isWorkspaceKey, worktreeWorkspaceKey } from '../../shared/workspace-scope' import { printLineageSummary } from './worktree-lineage-summary' import { assertWorkspaceTargetFlagsCompatible, hasWorkspaceProjectTarget, resolveProjectCreateRepoSelector } from '../worktree-project-target' import { assertCreateParentFlagsCompatible, resolveCreateParentSelector } from './worktree-create-parent-selector' import { getOptionalLinearIssueLinkFlag } from './worktree-linear-issue-link' type HookWarningResult = { warning?: string } type PreservedBranchResult = { preservedBranch?: { branchName: string } } function printHookWarning(result: HookWarningResult, json: boolean): void { if (!json && result.warning) { console.error(`warning: ${result.warning}`) } } function printPreservedBranchWarning(result: PreservedBranchResult, json: boolean): void { if (!json && result.preservedBranch) { console.error( `warning: local branch "${result.preservedBranch.branchName}" was kept because Git could not safely delete it` ) } } function assertParentWorktreeFlagsCompatible(flags: Map): void { if (flags.has('parent-worktree') && flags.get('no-parent') === true) { throw new RuntimeClientError( 'invalid_argument', 'Choose either --parent-worktree or --no-parent, not both.' ) } const parentWorktree = flags.get('parent-worktree') if ( flags.has('parent-worktree') && (typeof parentWorktree !== 'string' || parentWorktree === '') ) { throw new RuntimeClientError('invalid_argument', 'Missing required --parent-worktree') } } function getEnvParentWorkspace(): string | undefined { const workspaceId = process.env.ORCA_WORKSPACE_ID if (typeof workspaceId === 'string' && isWorkspaceKey(workspaceId)) { return workspaceId } const worktreeId = process.env.ORCA_WORKTREE_ID if (typeof worktreeId === 'string' && worktreeId.length > 0) { return isWorkspaceKey(worktreeId) ? worktreeId : worktreeWorkspaceKey(worktreeId) } return undefined } function getPresentStringFlag( flags: Map, name: string, options: { allowEmpty?: boolean } = {} ): string | undefined { if (!flags.has(name)) { return undefined } const value = flags.get(name) if (typeof value === 'string' && (options.allowEmpty || value.length > 0)) { return value } throw new RuntimeClientError('invalid_argument', `Missing value for --${name}`) } function getOptionalStartupAgent(flags: Map): string | undefined { const agent = getPresentStringFlag(flags, 'agent') if (agent === undefined) { if (flags.has('prompt')) { throw new RuntimeClientError('invalid_argument', '--prompt requires --agent') } return undefined } if (!isTuiAgent(agent)) { throw new RuntimeClientError('invalid_argument', `Unknown TUI agent "${agent}"`) } return agent } function getOptionalSetupDecision( flags: Map ): 'run' | 'skip' | 'inherit' | undefined { const setup = getPresentStringFlag(flags, 'setup') if (setup !== undefined && setup !== 'run' && setup !== 'skip' && setup !== 'inherit') { throw new RuntimeClientError('invalid_argument', '--setup must be one of: run, skip, inherit') } if (flags.get('run-hooks') === true) { if (setup !== undefined && setup !== 'run') { throw new RuntimeClientError( 'invalid_argument', 'Choose either --run-hooks or --setup run, not contradictory setup flags.' ) } return setup } return setup } function getRepoSelectorFromWorktreeSelector(selector: string | undefined): string | undefined { if (!selector?.startsWith('id:')) { return undefined } const worktreeId = selector.slice('id:'.length) const separatorIndex = worktreeId.indexOf('::') if (separatorIndex <= 0) { return undefined } return `id:${worktreeId.slice(0, separatorIndex)}` } async function getCreateRepoSelector( flags: Map, cwdParentWorktree: string | undefined, client: Parameters[0]['client'] ): Promise { const projectRepoSelector = await resolveProjectCreateRepoSelector(flags, client) if (projectRepoSelector) { return projectRepoSelector } const explicitRepo = getPresentStringFlag(flags, 'repo') if (explicitRepo) { return explicitRepo } const inferredRepo = getRepoSelectorFromWorktreeSelector(cwdParentWorktree) if (inferredRepo) { return inferredRepo } throw new RuntimeClientError( 'invalid_argument', 'Missing repo selector. Pass --repo or run from inside an Orca-managed worktree.' ) } export const WORKTREE_HANDLERS: Record = { 'worktree ps': async ({ flags, client, json }) => { const result = await client.call('worktree.ps', { limit: getOptionalPositiveIntegerFlag(flags, 'limit') }) printResult(result, json, formatWorktreePs) }, 'worktree list': async ({ flags, client, json }) => { const result = await client.call('worktree.list', { repo: getOptionalStringFlag(flags, 'repo'), limit: getOptionalPositiveIntegerFlag(flags, 'limit') }) printResult(result, json, formatWorktreeList) }, 'worktree show': async ({ flags, client, cwd, json }) => { const result = await client.call<{ worktree: RuntimeWorktreeRecord }>('worktree.show', { worktree: await getRequiredWorktreeSelector(flags, 'worktree', cwd, client) }) printResult(result, json, formatWorktreeShow) }, 'worktree current': async ({ client, cwd, json }) => { const result = await client.call<{ worktree: RuntimeWorktreeRecord }>('worktree.show', { worktree: await resolveCurrentWorktreeSelector(cwd, client) }) printResult(result, json, formatWorktreeShow) }, 'worktree create': async ({ flags, client, cwd, json }) => { assertCreateParentFlagsCompatible(flags) assertWorkspaceTargetFlagsCompatible(flags) const callerTerminalHandle = typeof process.env.ORCA_TERMINAL_HANDLE === 'string' && process.env.ORCA_TERMINAL_HANDLE.length > 0 ? process.env.ORCA_TERMINAL_HANDLE : undefined const explicitParent = await resolveCreateParentSelector(flags, cwd, client) const explicitParentWorktree = explicitParent.parentWorktree const explicitParentWorkspace = explicitParent.parentWorkspace const startupAgent = getOptionalStartupAgent(flags) const setupDecision = getOptionalSetupDecision(flags) const noParent = flags.get('no-parent') === true const envParentWorkspace = !noParent && !explicitParentWorkspace && !explicitParentWorktree ? getEnvParentWorkspace() : undefined let cwdParentWorktree: string | undefined const needsCwdRepoInference = !flags.has('repo') && !hasWorkspaceProjectTarget(flags) if ( (!explicitParentWorktree && !explicitParentWorkspace && !noParent) || needsCwdRepoInference ) { try { // Why: agent shells can lose ORCA_TERMINAL_HANDLE while still running // inside an Orca worktree. Cwd keeps CLI-created children nestable and // lets create infer the repo for the common current-workspace case. cwdParentWorktree = await resolveCurrentWorktreeSelector(cwd, client) } catch { cwdParentWorktree = undefined } } const linearIssueLink = getOptionalLinearIssueLinkFlag(flags, 'linear-issue') const result = await client.call('worktree.create', { repo: await getCreateRepoSelector(flags, cwdParentWorktree, client), name: getRequiredStringFlag(flags, 'name'), baseBranch: getOptionalStringFlag(flags, 'base-branch'), linkedIssue: getOptionalNumberFlag(flags, 'issue'), ...linearIssueLink, comment: getOptionalStringFlag(flags, 'comment'), runHooks: flags.get('run-hooks') === true, activate: flags.get('activate') === true || flags.get('run-hooks') === true, ...(setupDecision ? { setupDecision } : {}), parentWorktree: explicitParentWorktree, ...(explicitParentWorkspace ? { parentWorkspace: explicitParentWorkspace } : {}), ...(envParentWorkspace ? { envParentWorkspace } : {}), ...(cwdParentWorktree ? { cwdParentWorktree } : {}), noParent, callerTerminalHandle, // Why: marks the workspace as CLI-created so the sidebar can badge and // filter it. Sent on every `worktree create` — hand-typed or agent-run. cliProvenanceRequest: callerTerminalHandle ? { callerTerminalHandle } : {}, ...(startupAgent ? { startupAgent, startupPrompt: getPresentStringFlag(flags, 'prompt', { allowEmpty: true }) ?? '' } : {}) }) printHookWarning(result.result, json) printLineageSummary(result.result, json) printResult(result, json, formatWorktreeShow) }, 'worktree set': async ({ flags, client, cwd, json }) => { assertParentWorktreeFlagsCompatible(flags) const linearIssueLink = getOptionalLinearIssueLinkFlag(flags, 'linear-issue', { allowNull: true }) const result = await client.call<{ worktree: RuntimeWorktreeRecord }>('worktree.set', { worktree: await getRequiredWorktreeSelector(flags, 'worktree', cwd, client), displayName: getOptionalStringFlag(flags, 'display-name'), linkedIssue: getOptionalNullableNumberFlag(flags, 'issue'), ...linearIssueLink, comment: getOptionalStringFlag(flags, 'comment'), workspaceStatus: getOptionalStringFlag(flags, 'workspace-status'), parentWorktree: await getOptionalWorktreeSelector(flags, 'parent-worktree', cwd, client), noParent: flags.get('no-parent') === true }) printResult(result, json, formatWorktreeShow) }, 'worktree rm': async ({ flags, client, cwd, json }) => { const result = await client.call('worktree.rm', { worktree: await getRequiredWorktreeSelector(flags, 'worktree', cwd, client), force: flags.get('force') === true, runHooks: flags.get('run-hooks') === true }) printHookWarning(result.result, json) printPreservedBranchWarning(result.result, json) printResult(result, json, (value) => `removed: ${value.removed}`) } }