From 0a4a4ddbd19eda03035633b8348e296e827f16af Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 10 Jun 2026 15:53:40 -0700 Subject: [PATCH] Add project host setup CLI commands Co-authored-by: Orca --- src/cli/args.ts | 2 + src/cli/dispatch.ts | 2 + src/cli/format.ts | 5 + src/cli/handlers/project.ts | 61 +++++++++ src/cli/handlers/repo.ts | 29 +--- src/cli/help.ts | 8 ++ src/cli/index.test.ts | 235 ++++++++++++++++++++++++++++++--- src/cli/project-format.ts | 41 ++++++ src/cli/repo-path-arguments.ts | 31 +++++ src/cli/specs/index.ts | 2 + src/cli/specs/project.ts | 36 +++++ 11 files changed, 410 insertions(+), 42 deletions(-) create mode 100644 src/cli/handlers/project.ts create mode 100644 src/cli/project-format.ts create mode 100644 src/cli/repo-path-arguments.ts create mode 100644 src/cli/specs/project.ts diff --git a/src/cli/args.ts b/src/cli/args.ts index e2a015b50e6..43925e6c3c1 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -77,6 +77,7 @@ export function supportsBrowserPageFlag(commandPath: string[]): boolean { if ( [ 'automations', + 'project', 'repo', 'worktree', 'terminal', @@ -105,6 +106,7 @@ export function isCommandGroup(commandPath: string[]): boolean { (commandPath.length === 1 && [ 'automations', + 'project', 'repo', 'worktree', 'terminal', diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index bbec78dac26..36c6537f6ee 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -2,6 +2,7 @@ import type { RuntimeClient } from './runtime-client' import { RuntimeClientError } from './runtime-client' import { CORE_HANDLERS } from './handlers/core' import { AUTOMATION_HANDLERS } from './handlers/automations' +import { PROJECT_HANDLERS } from './handlers/project' import { REPO_HANDLERS } from './handlers/repo' import { WORKTREE_HANDLERS } from './handlers/worktree' import { FILE_HANDLERS } from './handlers/file' @@ -36,6 +37,7 @@ function buildHandlers(): Map { const groups = [ CORE_HANDLERS, AUTOMATION_HANDLERS, + PROJECT_HANDLERS, REPO_HANDLERS, WORKTREE_HANDLERS, FILE_HANDLERS, diff --git a/src/cli/format.ts b/src/cli/format.ts index 500c5ca55d0..cc476a1af92 100644 --- a/src/cli/format.ts +++ b/src/cli/format.ts @@ -22,6 +22,11 @@ export { formatListWindows } from './computer-format' export type { ComputerActionFollowUpTarget } from './computer-format' +export { + formatProjectHostSetupList, + formatProjectHostSetupResult, + formatProjectList +} from './project-format' export { formatTerminalClose, formatTerminalCreate, diff --git a/src/cli/handlers/project.ts b/src/cli/handlers/project.ts new file mode 100644 index 00000000000..504d7a08daf --- /dev/null +++ b/src/cli/handlers/project.ts @@ -0,0 +1,61 @@ +import type { + Project, + ProjectHostSetup, + ProjectHostSetupExistingFolderArgs, + ProjectHostSetupResult, + RepoKind +} from '../../shared/types' +import type { CommandHandler } from '../dispatch' +import { + formatProjectHostSetupList, + formatProjectHostSetupResult, + formatProjectList, + printResult +} from '../format' +import { getOptionalStringFlag, getRequiredStringFlag } from '../flags' +import { resolveRepoPathArgument } from '../repo-path-arguments' +import { RuntimeClientError } from '../runtime-client' + +function getOptionalRepoKind(flags: Map): RepoKind | undefined { + const kind = getOptionalStringFlag(flags, 'kind') + if (kind === undefined) { + return undefined + } + if (kind === 'git' || kind === 'folder') { + return kind + } + throw new RuntimeClientError('invalid_argument', '--kind must be git or folder') +} + +export const PROJECT_HANDLERS: Record = { + 'project list': async ({ client, json }) => { + const result = await client.call<{ projects: Project[] }>('project.list') + printResult(result, json, formatProjectList) + }, + 'project setups': async ({ flags, client, json }) => { + const projectFilter = getOptionalStringFlag(flags, 'project') + const hostFilter = getOptionalStringFlag(flags, 'host') + const result = await client.call<{ setups: ProjectHostSetup[] }>('projectHostSetup.list') + const setups = result.result.setups.filter( + (setup) => + (projectFilter === undefined || setup.projectId === projectFilter) && + (hostFilter === undefined || setup.hostId === hostFilter) + ) + printResult({ ...result, result: { setups } }, json, formatProjectHostSetupList) + }, + 'project setup-existing-folder': async ({ flags, client, cwd, json }) => { + const rawPath = getRequiredStringFlag(flags, 'path') + const args: ProjectHostSetupExistingFolderArgs = { + projectId: getRequiredStringFlag(flags, 'project'), + hostId: getRequiredStringFlag(flags, 'host') as ProjectHostSetupExistingFolderArgs['hostId'], + path: resolveRepoPathArgument(rawPath, cwd, client.isRemote, 'Remote project setup'), + kind: getOptionalRepoKind(flags), + displayName: getOptionalStringFlag(flags, 'display-name') + } + const result = await client.call<{ result: ProjectHostSetupResult }>( + 'projectHostSetup.setupExistingFolder', + args + ) + printResult(result, json, formatProjectHostSetupResult) + } +} diff --git a/src/cli/handlers/repo.ts b/src/cli/handlers/repo.ts index fc4c9f22809..e0432cc9bc6 100644 --- a/src/cli/handlers/repo.ts +++ b/src/cli/handlers/repo.ts @@ -1,33 +1,8 @@ -import { resolve as resolvePath } from 'path' import type { RuntimeRepoList, RuntimeRepoSearchRefs } from '../../shared/runtime-types' import type { CommandHandler } from '../dispatch' import { formatRepoList, formatRepoRefs, formatRepoShow, printResult } from '../format' import { getOptionalPositiveIntegerFlag, getRequiredStringFlag } from '../flags' -import { RuntimeClientError } from '../runtime-client' - -function isAbsoluteServerPath(value: string): boolean { - return ( - value.startsWith('/') || - /^[A-Za-z]:[\\/]/.test(value) || - value.startsWith('\\\\') || - value.startsWith('//') - ) -} - -function resolveRepoAddPath(inputPath: string, cwd: string, isRemote: boolean): string { - if (!isRemote) { - return resolvePath(cwd, inputPath) - } - // Why: the local CLI cwd is unrelated to a paired runtime's filesystem. - // Relative remote paths would silently target the wrong machine. - if (!isAbsoluteServerPath(inputPath)) { - throw new RuntimeClientError( - 'invalid_argument', - 'Remote repo add requires --path to be an absolute path on the remote server.' - ) - } - return inputPath -} +import { resolveRepoPathArgument } from '../repo-path-arguments' export const REPO_HANDLERS: Record = { 'repo list': async ({ client, json }) => { @@ -37,7 +12,7 @@ export const REPO_HANDLERS: Record = { 'repo add': async ({ flags, client, cwd, json }) => { const repoPath = getRequiredStringFlag(flags, 'path') const result = await client.call<{ repo: Record }>('repo.add', { - path: resolveRepoAddPath(repoPath, cwd, client.isRemote) + path: resolveRepoPathArgument(repoPath, cwd, client.isRemote, 'Remote repo add') }) printResult(result, json, formatRepoShow) }, diff --git a/src/cli/help.ts b/src/cli/help.ts index 06124a1007c..46731378483 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -29,6 +29,11 @@ Automations: automations run Run an Orca automation now automations runs List automation run history +Projects: + project list List durable projects known to Orca + project setups List project host setups + project setup-existing-folder Make a project available on a host by importing an existing folder + Repos: repo list List repos registered in Orca repo add Add a project to Orca by filesystem path @@ -201,6 +206,9 @@ Common Commands: orca terminal split [--terminal ] [--direction horizontal|vertical] [--json] orca terminal switch [--terminal ] [--json] orca terminal close [--terminal ] [--json] + orca project list [--json] + orca project setups [--project ] [--host ] [--json] + orca project setup-existing-folder --project --host --path [--kind git|folder] [--display-name ] [--json] orca repo list [--json] orca repo add --path [--json] orca repo show --repo [--json] diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index deb5472b784..adfb191501d 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -141,6 +141,9 @@ describe('orca root help', () => { expect(logSpy.mock.calls[0][0]).toContain( 'computer press-key Press a single key such as Return or Escape' ) + expect(logSpy.mock.calls[0][0]).toContain( + 'project setup-existing-folder Make a project available on a host by importing an existing folder' + ) expect(callMock).not.toHaveBeenCalled() }) }) @@ -247,7 +250,9 @@ describe('orca cli worktree awareness', () => { await main(['worktree', 'current', '--json'], '/tmp/repo/feature/src') - expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { limit: 10_000 }) + expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { + limit: 10_000 + }) expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', { worktree: 'id:repo::/tmp/repo/feature' }) @@ -996,6 +1001,174 @@ describe('orca cli worktree awareness', () => { }) }) + it('lists projects through the project-first runtime API', async () => { + queueFixtures( + callMock, + okFixture('req_project_list', { + projects: [ + { + id: 'github:stablyai/orca', + displayName: 'Orca', + badgeColor: '#7c3aed', + providerIdentity: { + provider: 'github', + owner: 'stablyai', + repo: 'orca' + }, + sourceRepoIds: ['repo-1'], + createdAt: 1, + updatedAt: 1 + } + ] + }) + ) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + await main(['project', 'list', '--json'], '/tmp/repo') + + expect(callMock).toHaveBeenCalledWith('project.list') + }) + + it('filters project host setups locally after fetching setup compatibility state', async () => { + queueFixtures( + callMock, + okFixture('req_project_setups', { + setups: [ + { + id: 'setup-local', + projectId: 'github:stablyai/orca', + hostId: 'local', + repoId: 'repo-local', + path: '/tmp/orca', + displayName: 'Orca', + setupState: 'ready', + setupMethod: 'legacy-repo', + createdAt: 1, + updatedAt: 1 + }, + { + id: 'setup-remote', + projectId: 'github:stablyai/orca', + hostId: 'runtime:gpu', + repoId: 'repo-remote', + path: '/srv/orca', + displayName: 'Orca', + setupState: 'ready', + setupMethod: 'legacy-repo', + createdAt: 1, + updatedAt: 1 + } + ] + }) + ) + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + await main( + ['project', 'setups', '--project', 'github:stablyai/orca', '--host', 'runtime:gpu'], + '/tmp/repo' + ) + + expect(callMock).toHaveBeenCalledWith('projectHostSetup.list') + expect(logSpy.mock.calls[0]?.[0]).toContain('setup-remote') + expect(logSpy.mock.calls[0]?.[0]).not.toContain('setup-local') + }) + + it('sets up an existing project folder with a path resolved against the local cli cwd', async () => { + queueFixtures( + callMock, + okFixture('req_project_setup', { + result: { + project: { + id: 'github:stablyai/orca', + displayName: 'Orca', + badgeColor: '#7c3aed', + sourceRepoIds: ['repo-1'], + createdAt: 1, + updatedAt: 1 + }, + setup: { + id: 'setup-local', + projectId: 'github:stablyai/orca', + hostId: 'local', + repoId: 'repo-1', + path: path.resolve('/tmp/orca'), + displayName: 'Orca', + setupState: 'ready', + setupMethod: 'imported-existing-folder', + createdAt: 1, + updatedAt: 1 + }, + repo: { + id: 'repo-1', + path: path.resolve('/tmp/orca'), + displayName: 'Orca', + badgeColor: '#7c3aed', + addedAt: 1 + } + } + }) + ) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + await main( + [ + 'project', + 'setup-existing-folder', + '--project', + 'github:stablyai/orca', + '--host', + 'local', + '--path', + '..', + '--kind', + 'git', + '--display-name', + 'Orca', + '--json' + ], + '/tmp/orca/worktrees/feature' + ) + + expect(callMock).toHaveBeenCalledWith('projectHostSetup.setupExistingFolder', { + projectId: 'github:stablyai/orca', + hostId: 'local', + path: path.resolve('/tmp/orca/worktrees'), + kind: 'git', + displayName: 'Orca' + }) + }) + + it('rejects remote project setup relative paths instead of resolving against client cwd', async () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const priorExitCode = process.exitCode + + await main( + [ + 'project', + 'setup-existing-folder', + '--project', + 'github:stablyai/orca', + '--host', + 'runtime:gpu', + '--path', + './orca', + '--pairing-code', + 'remote-runtime', + '--json' + ], + '/tmp/repo' + ) + + expect(callMock).not.toHaveBeenCalled() + expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain( + 'Remote project setup requires --path to be an absolute path on the remote server.' + ) + expect(process.exitCode).toBe(1) + + process.exitCode = priorExitCode + }) + it('rejects remote repo.add relative paths instead of resolving against client cwd', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -2118,7 +2291,9 @@ describe('orca cli worktree awareness', () => { await main(['tab', 'current', '--pairing-code', 'remote-runtime', '--json'], '/tmp/client/src') expect(callMock).toHaveBeenCalledTimes(1) - expect(callMock).toHaveBeenCalledWith('browser.tabCurrent', { worktree: undefined }) + expect(callMock).toHaveBeenCalledWith('browser.tabCurrent', { + worktree: undefined + }) }) it('passes emulator gesture points through to the runtime', async () => { @@ -2254,7 +2429,9 @@ describe('orca cli worktree awareness', () => { '/tmp/repo/feature/src' ) - expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { limit: 10_000 }) + expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { + limit: 10_000 + }) expect(callMock).toHaveBeenNthCalledWith(2, 'automation.create', { name: 'Daily review', prompt: 'Review open changes', @@ -2442,8 +2619,12 @@ describe('orca cli worktree awareness', () => { queueFixtures( callMock, worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo', 'abc', 'repo-1')]), - okFixture('req_create', { automation: { id: 'auto-1', name: 'Daily review' } }), - okFixture('req_edit', { automation: { id: 'auto-1', name: 'Daily review' } }) + okFixture('req_create', { + automation: { id: 'auto-1', name: 'Daily review' } + }), + okFixture('req_edit', { + automation: { id: 'auto-1', name: 'Daily review' } + }) ) vi.spyOn(console, 'log').mockImplementation(() => {}) @@ -2468,7 +2649,9 @@ describe('orca cli worktree awareness', () => { ) await main(['automations', 'edit', 'auto-1', '--fresh-session', '--json'], '/tmp/repo') - expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { limit: 10_000 }) + expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { + limit: 10_000 + }) expect(callMock).toHaveBeenNthCalledWith( 2, 'automation.create', @@ -2532,8 +2715,16 @@ describe('orca cli worktree awareness', () => { }) it.each([ - { flag: 'enabled', value: 'false', message: '--enabled does not take a value' }, - { flag: 'disabled', value: 'false', message: '--disabled does not take a value' } + { + flag: 'enabled', + value: 'false', + message: '--enabled does not take a value' + }, + { + flag: 'disabled', + value: 'false', + message: '--disabled does not take a value' + } ])('rejects automation create --$flag with a string value', async ({ flag, value, message }) => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -2571,7 +2762,9 @@ describe('orca cli worktree awareness', () => { queueFixtures( callMock, worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo', 'abc', 'repo-1')]), - okFixture('req_automation_create', { automation: { id: 'auto-1', name: 'Daily review' } }) + okFixture('req_automation_create', { + automation: { id: 'auto-1', name: 'Daily review' } + }) ) vi.spyOn(console, 'log').mockImplementation(() => {}) @@ -2594,7 +2787,9 @@ describe('orca cli worktree awareness', () => { '/tmp/repo/feature/src' ) - expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { limit: 10_000 }) + expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { + limit: 10_000 + }) expect(callMock).toHaveBeenNthCalledWith(2, 'automation.create', { name: 'Daily review', prompt: 'Review open changes', @@ -2615,7 +2810,9 @@ describe('orca cli worktree awareness', () => { queueFixtures( callMock, worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo', 'abc', 'repo-1')]), - okFixture('req_edit', { automation: { id: 'auto-1', name: 'Daily review' } }) + okFixture('req_edit', { + automation: { id: 'auto-1', name: 'Daily review' } + }) ) vi.spyOn(console, 'log').mockImplementation(() => {}) @@ -2624,7 +2821,9 @@ describe('orca cli worktree awareness', () => { '/tmp/repo/feature/src' ) - expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { limit: 10_000 }) + expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', { + limit: 10_000 + }) expect(callMock).toHaveBeenNthCalledWith(2, 'automation.update', { id: 'auto-1', updates: { @@ -2692,9 +2891,15 @@ describe('orca cli worktree awareness', () => { missedRunGraceMinutes: undefined } }) - expect(callMock).toHaveBeenNthCalledWith(2, 'automation.delete', { id: 'auto-1' }) - expect(callMock).toHaveBeenNthCalledWith(3, 'automation.runNow', { id: 'auto-1' }) - expect(callMock).toHaveBeenNthCalledWith(4, 'automation.show', { id: 'auto-1' }) + expect(callMock).toHaveBeenNthCalledWith(2, 'automation.delete', { + id: 'auto-1' + }) + expect(callMock).toHaveBeenNthCalledWith(3, 'automation.runNow', { + id: 'auto-1' + }) + expect(callMock).toHaveBeenNthCalledWith(4, 'automation.show', { + id: 'auto-1' + }) }) it('rejects ambiguous positional and flag automation ids before dispatch', async () => { diff --git a/src/cli/project-format.ts b/src/cli/project-format.ts new file mode 100644 index 00000000000..926b57fceaf --- /dev/null +++ b/src/cli/project-format.ts @@ -0,0 +1,41 @@ +import type { Project, ProjectHostSetup, ProjectHostSetupResult } from '../shared/types' + +export function formatProjectList(result: { projects: Project[] }): string { + if (result.projects.length === 0) { + return 'No projects found.' + } + return result.projects + .map((project) => { + const identity = project.providerIdentity + ? `${project.providerIdentity.provider}:${project.providerIdentity.owner}/${project.providerIdentity.repo}` + : 'no-provider' + return `${project.id} ${project.displayName} ${identity}` + }) + .join('\n') +} + +export function formatProjectHostSetupList(result: { setups: ProjectHostSetup[] }): string { + if (result.setups.length === 0) { + return 'No project host setups found.' + } + return result.setups + .map( + (setup) => + `${setup.id} project:${setup.projectId} host:${setup.hostId} ${setup.setupState} ${setup.path}` + ) + .join('\n') +} + +export function formatProjectHostSetupResult(result: { result: ProjectHostSetupResult }): string { + const { project, setup, repo } = result.result + return [ + `projectId: ${project.id}`, + `project: ${project.displayName}`, + `setupId: ${setup.id}`, + `hostId: ${setup.hostId}`, + `path: ${setup.path}`, + `state: ${setup.setupState}`, + `method: ${setup.setupMethod}`, + `repoId: ${repo.id}` + ].join('\n') +} diff --git a/src/cli/repo-path-arguments.ts b/src/cli/repo-path-arguments.ts new file mode 100644 index 00000000000..efc0148eff0 --- /dev/null +++ b/src/cli/repo-path-arguments.ts @@ -0,0 +1,31 @@ +import { resolve as resolvePath } from 'path' +import { RuntimeClientError } from './runtime-client' + +function isAbsoluteServerPath(value: string): boolean { + return ( + value.startsWith('/') || + /^[A-Za-z]:[\\/]/.test(value) || + value.startsWith('\\\\') || + value.startsWith('//') + ) +} + +export function resolveRepoPathArgument( + inputPath: string, + cwd: string, + isRemote: boolean, + remotePathSubject = 'Remote repo path' +): string { + if (!isRemote) { + return resolvePath(cwd, inputPath) + } + // Why: the local CLI cwd is unrelated to a paired runtime's filesystem. + // Relative remote paths would silently target the wrong machine. + if (!isAbsoluteServerPath(inputPath)) { + throw new RuntimeClientError( + 'invalid_argument', + `${remotePathSubject} requires --path to be an absolute path on the remote server.` + ) + } + return inputPath +} diff --git a/src/cli/specs/index.ts b/src/cli/specs/index.ts index 333cf42077a..3cffe9fb73f 100644 --- a/src/cli/specs/index.ts +++ b/src/cli/specs/index.ts @@ -4,6 +4,7 @@ import { BROWSER_BASIC_COMMAND_SPECS } from './browser-basic' import { AUTOMATION_COMMAND_SPECS } from './automations' import { CORE_COMMAND_SPECS } from './core' import { FILE_COMMAND_SPECS } from './file' +import { PROJECT_COMMAND_SPECS } from './project' import { ORCHESTRATION_COMMAND_SPECS } from './orchestration' import { COMPUTER_COMMAND_SPECS } from './computer' import { ENVIRONMENT_COMMAND_SPECS } from './environment' @@ -13,6 +14,7 @@ import { EMULATOR_COMMAND_SPECS } from './emulator' export const COMMAND_SPECS: CommandSpec[] = [ ...CORE_COMMAND_SPECS, + ...PROJECT_COMMAND_SPECS, ...FILE_COMMAND_SPECS, ...AUTOMATION_COMMAND_SPECS, ...BROWSER_BASIC_COMMAND_SPECS, diff --git a/src/cli/specs/project.ts b/src/cli/specs/project.ts new file mode 100644 index 00000000000..1f6ef6755d3 --- /dev/null +++ b/src/cli/specs/project.ts @@ -0,0 +1,36 @@ +import type { CommandSpec } from '../args' +import { GLOBAL_FLAGS } from '../args' + +export const PROJECT_COMMAND_SPECS: CommandSpec[] = [ + { + path: ['project', 'list'], + summary: 'List durable projects known to Orca', + usage: 'orca project list [--json]', + allowedFlags: [...GLOBAL_FLAGS], + examples: ['orca project list', 'orca project list --json'] + }, + { + path: ['project', 'setups'], + summary: 'List project host setups', + usage: 'orca project setups [--project ] [--host ] [--json]', + allowedFlags: [...GLOBAL_FLAGS, 'project', 'host'], + notes: ['A setup means a project is available on a host at a concrete filesystem path.'], + examples: [ + 'orca project setups', + 'orca project setups --project github:stablyai/orca', + 'orca project setups --host local' + ] + }, + { + path: ['project', 'setup-existing-folder'], + summary: 'Make a project available on a host by importing an existing folder', + usage: + 'orca project setup-existing-folder --project --host --path [--kind git|folder] [--display-name ] [--json]', + allowedFlags: [...GLOBAL_FLAGS, 'project', 'host', 'path', 'kind', 'display-name'], + notes: ['For remote runtimes, --path must be an absolute path on the remote server.'], + examples: [ + 'orca project setup-existing-folder --project github:stablyai/orca --host local --path ~/orca', + 'orca project setup-existing-folder --project github:stablyai/orca --host runtime:gpu --path /home/me/orca --kind git --json' + ] + } +]