mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Add current worktree shortcuts to Orca CLI (#301)
This commit is contained in:
@@ -34,6 +34,7 @@ The following commands are implemented against the running Orca app:
|
||||
- `orca repo search-refs`
|
||||
- `orca worktree list`
|
||||
- `orca worktree show`
|
||||
- `orca worktree current`
|
||||
- `orca worktree create`
|
||||
- `orca worktree set`
|
||||
- `orca worktree rm`
|
||||
@@ -50,14 +51,15 @@ The following commands are implemented against the running Orca app:
|
||||
Focused v1 supports the complete agent loop for worktree orchestration:
|
||||
|
||||
1. Inspect current Orca runtime availability.
|
||||
2. Discover repos indirectly through existing worktrees and summary views.
|
||||
3. Create a new worktree in a chosen repo.
|
||||
4. Attach or update worktree metadata like display name, linked issue, and comment.
|
||||
5. Inspect many worktrees at once with `worktree ps`.
|
||||
6. Discover live terminal handles in a worktree.
|
||||
7. Read terminal output with bounded token-efficient reads.
|
||||
8. Send input back to the terminal.
|
||||
9. Stop live terminals for a worktree when needed.
|
||||
2. Discover the enclosing Orca-managed worktree from the current shell directory.
|
||||
3. Discover repos indirectly through existing worktrees and summary views.
|
||||
4. Create a new worktree in a chosen repo.
|
||||
5. Attach or update worktree metadata like display name, linked issue, and comment.
|
||||
6. Inspect many worktrees at once with `worktree ps`.
|
||||
7. Discover live terminal handles in a worktree.
|
||||
8. Read terminal output with bounded token-efficient reads.
|
||||
9. Send input back to the terminal.
|
||||
10. Stop live terminals for a worktree when needed.
|
||||
|
||||
It also covers the adjacent setup tasks needed to make worktree creation usable:
|
||||
|
||||
|
||||
@@ -375,6 +375,20 @@ Status:
|
||||
orca worktree show --worktree branch:feature/foo --json
|
||||
```
|
||||
|
||||
Focused v1 also accepts `active` / `current` as CLI-only shortcuts for worktree
|
||||
selectors. The CLI resolves them from the caller's current directory and sends a
|
||||
`path:` selector to the runtime.
|
||||
|
||||
## `orca worktree current`
|
||||
|
||||
Status:
|
||||
|
||||
- `v1-now`
|
||||
|
||||
```bash
|
||||
orca worktree current --json
|
||||
```
|
||||
|
||||
## `orca worktree create`
|
||||
|
||||
Status:
|
||||
@@ -404,6 +418,7 @@ Status:
|
||||
|
||||
```bash
|
||||
orca worktree set --worktree branch:feature/foo --display-name "Parser" --issue 123 --comment "parser work" --json
|
||||
orca worktree set --worktree active --comment "parser work" --json
|
||||
```
|
||||
|
||||
## `orca worktree rm`
|
||||
|
||||
@@ -97,9 +97,11 @@ orca repo search-refs --repo id:<repoId> --query main --limit 10 --json
|
||||
```bash
|
||||
orca worktree list --repo id:<repoId> --json
|
||||
orca worktree ps --json
|
||||
orca worktree current --json
|
||||
orca worktree show --worktree id:<worktreeId> --json
|
||||
orca worktree create --repo id:<repoId> --name my-task --issue 123 --comment "seed" --json
|
||||
orca worktree set --worktree id:<worktreeId> --display-name "My Task" --json
|
||||
orca worktree set --worktree active --comment "waiting on review" --json
|
||||
orca worktree rm --worktree id:<worktreeId> --force --json
|
||||
```
|
||||
|
||||
@@ -109,6 +111,7 @@ Worktree selectors supported in focused v1:
|
||||
- `path:<absolute-path>`
|
||||
- `branch:<branch-name>`
|
||||
- `issue:<number>`
|
||||
- `active` / `current` to resolve the enclosing Orca-managed worktree from the shell `cwd`
|
||||
|
||||
### Terminal
|
||||
|
||||
@@ -131,6 +134,7 @@ Why: terminal handles are runtime-scoped and may go stale after reloads. If Orca
|
||||
- Treat Orca as the source of truth for Orca worktree and terminal tasks. Do not mix Orca-managed state with ad hoc git worktree commands unless Orca explicitly cannot perform the requested action.
|
||||
- Prefer `--json` for all machine-driven use.
|
||||
- Use `worktree ps` as the first summary view when many worktrees may exist.
|
||||
- Use `worktree current` or `--worktree active` when the agent is already running inside the target worktree.
|
||||
- Use `terminal list` to reacquire handles after Orca reloads.
|
||||
- Use `terminal read` before `terminal send` unless the next input is obvious.
|
||||
- Use `terminal wait --for exit` only when the task actually depends on process completion.
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const callMock = vi.fn()
|
||||
|
||||
vi.mock('./runtime-client', () => {
|
||||
class RuntimeClient {
|
||||
call = callMock
|
||||
getCliStatus = vi.fn()
|
||||
openOrca = vi.fn()
|
||||
}
|
||||
|
||||
class RuntimeClientError extends Error {
|
||||
readonly code: string
|
||||
|
||||
constructor(code: string, message: string) {
|
||||
super(message)
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
class RuntimeRpcFailureError extends RuntimeClientError {
|
||||
readonly response: unknown
|
||||
|
||||
constructor(response: unknown) {
|
||||
super('runtime_error', 'runtime_error')
|
||||
this.response = response
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
RuntimeClient,
|
||||
RuntimeClientError,
|
||||
RuntimeRpcFailureError
|
||||
}
|
||||
})
|
||||
|
||||
import { buildCurrentWorktreeSelector, main, normalizeWorktreeSelector } from './index'
|
||||
|
||||
describe('orca cli worktree awareness', () => {
|
||||
beforeEach(() => {
|
||||
callMock.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('builds the current worktree selector from cwd', () => {
|
||||
expect(buildCurrentWorktreeSelector('/tmp/repo/feature')).toBe('path:/tmp/repo/feature')
|
||||
})
|
||||
|
||||
it('normalizes active/current worktree selectors to cwd', () => {
|
||||
expect(normalizeWorktreeSelector('active', '/tmp/repo/feature')).toBe('path:/tmp/repo/feature')
|
||||
expect(normalizeWorktreeSelector('current', '/tmp/repo/feature')).toBe('path:/tmp/repo/feature')
|
||||
expect(normalizeWorktreeSelector('branch:feature/foo', '/tmp/repo/feature')).toBe(
|
||||
'branch:feature/foo'
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the enclosing worktree for `worktree current`', async () => {
|
||||
callMock
|
||||
.mockResolvedValueOnce({
|
||||
id: 'req_list',
|
||||
ok: true,
|
||||
result: {
|
||||
worktrees: [
|
||||
{
|
||||
id: 'repo::/tmp/repo/feature',
|
||||
repoId: 'repo',
|
||||
path: '/tmp/repo/feature',
|
||||
branch: 'feature/foo',
|
||||
linkedIssue: null,
|
||||
git: {
|
||||
path: '/tmp/repo/feature',
|
||||
head: 'abc',
|
||||
branch: 'feature/foo',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
},
|
||||
displayName: '',
|
||||
comment: ''
|
||||
}
|
||||
],
|
||||
totalCount: 1,
|
||||
truncated: false
|
||||
},
|
||||
_meta: {
|
||||
runtimeId: 'runtime-1'
|
||||
}
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'req_1',
|
||||
ok: true,
|
||||
result: {
|
||||
worktree: {
|
||||
id: 'repo::/tmp/repo/feature',
|
||||
branch: 'feature/foo',
|
||||
path: '/tmp/repo/feature'
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
runtimeId: 'runtime-1'
|
||||
}
|
||||
})
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['worktree', 'current', '--json'], '/tmp/repo/feature/src')
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', {
|
||||
limit: 10_000
|
||||
})
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
|
||||
worktree: 'path:/tmp/repo/feature'
|
||||
})
|
||||
expect(logSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('uses cwd when active is passed to worktree.set', async () => {
|
||||
callMock
|
||||
.mockResolvedValueOnce({
|
||||
id: 'req_list',
|
||||
ok: true,
|
||||
result: {
|
||||
worktrees: [
|
||||
{
|
||||
id: 'repo::/tmp/repo',
|
||||
repoId: 'repo',
|
||||
path: '/tmp/repo',
|
||||
branch: 'main',
|
||||
linkedIssue: null,
|
||||
git: {
|
||||
path: '/tmp/repo',
|
||||
head: 'aaa',
|
||||
branch: 'main',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
},
|
||||
displayName: '',
|
||||
comment: ''
|
||||
},
|
||||
{
|
||||
id: 'repo::/tmp/repo/feature',
|
||||
repoId: 'repo',
|
||||
path: '/tmp/repo/feature',
|
||||
branch: 'feature/foo',
|
||||
linkedIssue: null,
|
||||
git: {
|
||||
path: '/tmp/repo/feature',
|
||||
head: 'abc',
|
||||
branch: 'feature/foo',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
},
|
||||
displayName: '',
|
||||
comment: ''
|
||||
}
|
||||
],
|
||||
totalCount: 2,
|
||||
truncated: false
|
||||
},
|
||||
_meta: {
|
||||
runtimeId: 'runtime-1'
|
||||
}
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'req_1',
|
||||
ok: true,
|
||||
result: {
|
||||
worktree: {
|
||||
id: 'repo::/tmp/repo/feature',
|
||||
branch: 'feature/foo',
|
||||
path: '/tmp/repo/feature',
|
||||
comment: 'hello'
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
runtimeId: 'runtime-1'
|
||||
}
|
||||
})
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(
|
||||
['worktree', 'set', '--worktree', 'active', '--comment', 'hello', '--json'],
|
||||
'/tmp/repo/feature/src'
|
||||
)
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.set', {
|
||||
worktree: 'path:/tmp/repo/feature',
|
||||
displayName: undefined,
|
||||
linkedIssue: undefined,
|
||||
comment: 'hello'
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the resolved enclosing worktree for other worktree consumers', async () => {
|
||||
callMock
|
||||
.mockResolvedValueOnce({
|
||||
id: 'req_list',
|
||||
ok: true,
|
||||
result: {
|
||||
worktrees: [
|
||||
{
|
||||
id: 'repo::/tmp/repo/feature',
|
||||
repoId: 'repo',
|
||||
path: '/tmp/repo/feature',
|
||||
branch: 'feature/foo',
|
||||
linkedIssue: null,
|
||||
git: {
|
||||
path: '/tmp/repo/feature',
|
||||
head: 'abc',
|
||||
branch: 'feature/foo',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
},
|
||||
displayName: '',
|
||||
comment: ''
|
||||
}
|
||||
],
|
||||
totalCount: 1,
|
||||
truncated: false
|
||||
},
|
||||
_meta: {
|
||||
runtimeId: 'runtime-1'
|
||||
}
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'req_show',
|
||||
ok: true,
|
||||
result: {
|
||||
worktree: {
|
||||
id: 'repo::/tmp/repo/feature',
|
||||
branch: 'feature/foo',
|
||||
path: '/tmp/repo/feature'
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
runtimeId: 'runtime-1'
|
||||
}
|
||||
})
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['worktree', 'show', '--worktree', 'current', '--json'], '/tmp/repo/feature/src')
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
|
||||
worktree: 'path:/tmp/repo/feature'
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the resolved enclosing worktree for terminal consumers', async () => {
|
||||
callMock
|
||||
.mockResolvedValueOnce({
|
||||
id: 'req_list',
|
||||
ok: true,
|
||||
result: {
|
||||
worktrees: [
|
||||
{
|
||||
id: 'repo::/tmp/repo/feature',
|
||||
repoId: 'repo',
|
||||
path: '/tmp/repo/feature',
|
||||
branch: 'feature/foo',
|
||||
linkedIssue: null,
|
||||
git: {
|
||||
path: '/tmp/repo/feature',
|
||||
head: 'abc',
|
||||
branch: 'feature/foo',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
},
|
||||
displayName: '',
|
||||
comment: ''
|
||||
}
|
||||
],
|
||||
totalCount: 1,
|
||||
truncated: false
|
||||
},
|
||||
_meta: {
|
||||
runtimeId: 'runtime-1'
|
||||
}
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'req_term',
|
||||
ok: true,
|
||||
result: {
|
||||
terminals: [],
|
||||
totalCount: 0,
|
||||
truncated: false
|
||||
},
|
||||
_meta: {
|
||||
runtimeId: 'runtime-1'
|
||||
}
|
||||
})
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['terminal', 'list', '--worktree', 'active', '--json'], '/tmp/repo/feature/src')
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.list', {
|
||||
worktree: 'path:/tmp/repo/feature',
|
||||
limit: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
+107
-15
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable max-lines -- Why: the public CLI entrypoint keeps command dispatch in one place so the bundled shell command and development fallback stay behaviorally identical. */
|
||||
|
||||
import { isAbsolute, relative, resolve as resolvePath } from 'path'
|
||||
import type {
|
||||
CliStatusResult,
|
||||
RuntimeRepoList,
|
||||
@@ -95,6 +96,16 @@ const COMMAND_SPECS: CommandSpec[] = [
|
||||
usage: 'orca worktree show --worktree <selector> [--json]',
|
||||
allowedFlags: [...GLOBAL_FLAGS, 'worktree']
|
||||
},
|
||||
{
|
||||
path: ['worktree', 'current'],
|
||||
summary: 'Show the Orca-managed worktree for the current directory',
|
||||
usage: 'orca worktree current [--json]',
|
||||
allowedFlags: [...GLOBAL_FLAGS],
|
||||
notes: [
|
||||
'Resolves the current shell directory to a path: selector so agents can target the enclosing Orca worktree without spelling out $PWD.'
|
||||
],
|
||||
examples: ['orca worktree current', 'orca worktree current --json']
|
||||
},
|
||||
{
|
||||
path: ['worktree', 'create'],
|
||||
summary: 'Create a new Orca-managed worktree',
|
||||
@@ -161,8 +172,8 @@ const COMMAND_SPECS: CommandSpec[] = [
|
||||
}
|
||||
]
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const parsed = parseArgs(process.argv.slice(2))
|
||||
export async function main(argv = process.argv.slice(2), cwd = process.cwd()): Promise<void> {
|
||||
const parsed = parseArgs(argv)
|
||||
const helpPath = resolveHelpPath(parsed)
|
||||
if (helpPath !== null) {
|
||||
printHelp(helpPath)
|
||||
@@ -237,7 +248,7 @@ async function main(): Promise<void> {
|
||||
|
||||
if (matches(commandPath, ['terminal', 'list'])) {
|
||||
const result = await client.call<RuntimeTerminalListResult>('terminal.list', {
|
||||
worktree: getOptionalStringFlag(parsed.flags, 'worktree'),
|
||||
worktree: await getOptionalWorktreeSelector(parsed.flags, 'worktree', cwd, client),
|
||||
limit: getOptionalPositiveIntegerFlag(parsed.flags, 'limit')
|
||||
})
|
||||
return printResult(result, json, formatTerminalList)
|
||||
@@ -288,7 +299,7 @@ async function main(): Promise<void> {
|
||||
|
||||
if (matches(commandPath, ['terminal', 'stop'])) {
|
||||
const result = await client.call<{ stopped: number }>('terminal.stop', {
|
||||
worktree: getRequiredStringFlag(parsed.flags, 'worktree')
|
||||
worktree: await getRequiredWorktreeSelector(parsed.flags, 'worktree', cwd, client)
|
||||
})
|
||||
return printResult(result, json, (value) => `Stopped ${value.stopped} terminals.`)
|
||||
}
|
||||
@@ -310,7 +321,14 @@ async function main(): Promise<void> {
|
||||
|
||||
if (matches(commandPath, ['worktree', 'show'])) {
|
||||
const result = await client.call<{ worktree: RuntimeWorktreeRecord }>('worktree.show', {
|
||||
worktree: getRequiredStringFlag(parsed.flags, 'worktree')
|
||||
worktree: await getRequiredWorktreeSelector(parsed.flags, 'worktree', cwd, client)
|
||||
})
|
||||
return printResult(result, json, formatWorktreeShow)
|
||||
}
|
||||
|
||||
if (matches(commandPath, ['worktree', 'current'])) {
|
||||
const result = await client.call<{ worktree: RuntimeWorktreeRecord }>('worktree.show', {
|
||||
worktree: await resolveCurrentWorktreeSelector(cwd, client)
|
||||
})
|
||||
return printResult(result, json, formatWorktreeShow)
|
||||
}
|
||||
@@ -328,7 +346,7 @@ async function main(): Promise<void> {
|
||||
|
||||
if (matches(commandPath, ['worktree', 'set'])) {
|
||||
const result = await client.call<{ worktree: RuntimeWorktreeRecord }>('worktree.set', {
|
||||
worktree: getRequiredStringFlag(parsed.flags, 'worktree'),
|
||||
worktree: await getRequiredWorktreeSelector(parsed.flags, 'worktree', cwd, client),
|
||||
displayName: getOptionalStringFlag(parsed.flags, 'display-name'),
|
||||
linkedIssue: getOptionalNullableNumberFlag(parsed.flags, 'issue'),
|
||||
comment: getOptionalStringFlag(parsed.flags, 'comment')
|
||||
@@ -338,7 +356,7 @@ async function main(): Promise<void> {
|
||||
|
||||
if (matches(commandPath, ['worktree', 'rm'])) {
|
||||
const result = await client.call<{ removed: boolean }>('worktree.rm', {
|
||||
worktree: getRequiredStringFlag(parsed.flags, 'worktree'),
|
||||
worktree: await getRequiredWorktreeSelector(parsed.flags, 'worktree', cwd, client),
|
||||
force: parsed.flags.get('force') === true
|
||||
})
|
||||
return printResult(result, json, (value) => `removed: ${value.removed}`)
|
||||
@@ -370,7 +388,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): ParsedArgs {
|
||||
export function parseArgs(argv: string[]): ParsedArgs {
|
||||
const commandPath: string[] = []
|
||||
const flags = new Map<string, string | boolean>()
|
||||
|
||||
@@ -394,7 +412,7 @@ function parseArgs(argv: string[]): ParsedArgs {
|
||||
return { commandPath, flags }
|
||||
}
|
||||
|
||||
function resolveHelpPath(parsed: ParsedArgs): string[] | null {
|
||||
export function resolveHelpPath(parsed: ParsedArgs): string[] | null {
|
||||
if (parsed.commandPath[0] === 'help') {
|
||||
return parsed.commandPath.slice(1)
|
||||
}
|
||||
@@ -404,7 +422,7 @@ function resolveHelpPath(parsed: ParsedArgs): string[] | null {
|
||||
return null
|
||||
}
|
||||
|
||||
function validateCommandAndFlags(parsed: ParsedArgs): void {
|
||||
export function validateCommandAndFlags(parsed: ParsedArgs): void {
|
||||
const spec = findCommandSpec(parsed.commandPath)
|
||||
if (!spec) {
|
||||
throw new RuntimeClientError(
|
||||
@@ -423,7 +441,7 @@ function validateCommandAndFlags(parsed: ParsedArgs): void {
|
||||
}
|
||||
}
|
||||
|
||||
function findCommandSpec(commandPath: string[]): CommandSpec | undefined {
|
||||
export function findCommandSpec(commandPath: string[]): CommandSpec | undefined {
|
||||
return COMMAND_SPECS.find((spec) => matches(spec.path, commandPath))
|
||||
}
|
||||
|
||||
@@ -447,6 +465,74 @@ function getOptionalStringFlag(
|
||||
return typeof value === 'string' && value.length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
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 isWithinPath(parentPath: string, childPath: string): boolean {
|
||||
const relativePath = relative(parentPath, childPath)
|
||||
return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))
|
||||
}
|
||||
|
||||
async function resolveCurrentWorktreeSelector(cwd: string, client: RuntimeClient): Promise<string> {
|
||||
const currentPath = resolvePath(cwd)
|
||||
const worktrees = await client.call<RuntimeWorktreeListResult>('worktree.list', {
|
||||
limit: 10_000
|
||||
})
|
||||
const enclosingWorktree = worktrees.result.worktrees
|
||||
.filter((worktree) => isWithinPath(resolvePath(worktree.path), currentPath))
|
||||
.sort((left, right) => right.path.length - left.path.length)[0]
|
||||
|
||||
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. The CLI resolves that shell-local concept
|
||||
// to the deepest matching worktree root, then hands the runtime a normal
|
||||
// path selector so selector semantics stay centralized in one layer.
|
||||
return buildCurrentWorktreeSelector(enclosingWorktree.path)
|
||||
}
|
||||
|
||||
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') {
|
||||
return await resolveCurrentWorktreeSelector(cwd, client)
|
||||
}
|
||||
return normalizeWorktreeSelector(value, cwd)
|
||||
}
|
||||
|
||||
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') {
|
||||
return await resolveCurrentWorktreeSelector(cwd, client)
|
||||
}
|
||||
return normalizeWorktreeSelector(value, cwd)
|
||||
}
|
||||
|
||||
function getOptionalNumberFlag(
|
||||
flags: Map<string, string | boolean>,
|
||||
name: string
|
||||
@@ -487,7 +573,7 @@ function getOptionalNullableNumberFlag(
|
||||
return getOptionalNumberFlag(flags, name)
|
||||
}
|
||||
|
||||
function matches(actual: string[], expected: string[]): boolean {
|
||||
export function matches(actual: string[], expected: string[]): boolean {
|
||||
return (
|
||||
actual.length === expected.length && actual.every((value, index) => value === expected[index])
|
||||
)
|
||||
@@ -685,6 +771,7 @@ Repos:
|
||||
Worktrees:
|
||||
worktree list List Orca-managed worktrees
|
||||
worktree show Show one worktree
|
||||
worktree current Show the Orca-managed worktree for the current directory
|
||||
worktree create Create a new Orca-managed worktree
|
||||
worktree set Update Orca metadata for a worktree
|
||||
worktree rm Remove a worktree from Orca and git
|
||||
@@ -704,6 +791,7 @@ Common Commands:
|
||||
orca worktree list [--repo <selector>] [--limit <n>] [--json]
|
||||
orca worktree create --repo <selector> --name <name> [--base-branch <ref>] [--issue <number>] [--comment <text>] [--json]
|
||||
orca worktree show --worktree <selector> [--json]
|
||||
orca worktree current [--json]
|
||||
orca worktree set --worktree <selector> [--display-name <name>] [--issue <number|null>] [--comment <text>] [--json]
|
||||
orca worktree rm --worktree <selector> [--force] [--json]
|
||||
orca worktree ps [--limit <n>] [--json]
|
||||
@@ -721,7 +809,7 @@ Common Commands:
|
||||
|
||||
Selectors:
|
||||
--repo <selector> Registered repo selector such as id:<id>, name:<name>, or path:<path>
|
||||
--worktree <selector> Worktree selector such as id:<id>, branch:<branch>, issue:<number>, or path:<path>
|
||||
--worktree <selector> Worktree selector such as id:<id>, branch:<branch>, issue:<number>, path:<path>, or active/current
|
||||
--terminal <handle> Runtime-issued terminal handle returned by \`orca terminal list --json\`
|
||||
|
||||
Terminal Send Options:
|
||||
@@ -747,6 +835,8 @@ Examples:
|
||||
$ orca repo list
|
||||
$ orca worktree create --repo name:orca --name cli-test-1 --issue 273
|
||||
$ orca worktree show --worktree branch:Jinwoo-H/cli
|
||||
$ orca worktree current
|
||||
$ orca worktree set --worktree active --comment "waiting on review"
|
||||
$ orca worktree ps --limit 10
|
||||
$ orca terminal list --worktree path:/Users/me/orca/workspaces/orca/cli-test-1 --json
|
||||
$ orca terminal send --terminal term_123 --text "hi" --enter
|
||||
@@ -812,10 +902,12 @@ function formatFlagHelp(flag: string): string {
|
||||
text: '--text <text> Text to send to the terminal',
|
||||
'timeout-ms': '--timeout-ms <ms> Maximum wait time before timing out',
|
||||
worktree:
|
||||
'--worktree <selector> Worktree selector such as id:<id>, branch:<branch>, issue:<number>, or path:<path>'
|
||||
'--worktree <selector> Worktree selector such as id:<id>, branch:<branch>, issue:<number>, path:<path>, or active/current'
|
||||
}
|
||||
|
||||
return helpByFlag[flag] ?? `--${flag}`
|
||||
}
|
||||
|
||||
void main()
|
||||
if (require.main === module) {
|
||||
void main()
|
||||
}
|
||||
|
||||
@@ -261,6 +261,12 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('does not interpret active as a runtime-global worktree selector', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
|
||||
await expect(runtime.showManagedWorktree('active')).rejects.toThrow('selector_not_found')
|
||||
})
|
||||
|
||||
it('reads bounded terminal output and writes through the PTY controller', async () => {
|
||||
const writes: string[] = []
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
|
||||
Reference in New Issue
Block a user