mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 00:03:09 +00:00
Add project host setup CLI commands
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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<string, CommandHandler> {
|
||||
const groups = [
|
||||
CORE_HANDLERS,
|
||||
AUTOMATION_HANDLERS,
|
||||
PROJECT_HANDLERS,
|
||||
REPO_HANDLERS,
|
||||
WORKTREE_HANDLERS,
|
||||
FILE_HANDLERS,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, string | boolean>): 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<string, CommandHandler> = {
|
||||
'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)
|
||||
}
|
||||
}
|
||||
@@ -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<string, CommandHandler> = {
|
||||
'repo list': async ({ client, json }) => {
|
||||
@@ -37,7 +12,7 @@ export const REPO_HANDLERS: Record<string, CommandHandler> = {
|
||||
'repo add': async ({ flags, client, cwd, json }) => {
|
||||
const repoPath = getRequiredStringFlag(flags, 'path')
|
||||
const result = await client.call<{ repo: Record<string, unknown> }>('repo.add', {
|
||||
path: resolveRepoAddPath(repoPath, cwd, client.isRemote)
|
||||
path: resolveRepoPathArgument(repoPath, cwd, client.isRemote, 'Remote repo add')
|
||||
})
|
||||
printResult(result, json, formatRepoShow)
|
||||
},
|
||||
|
||||
@@ -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 <handle>] [--direction horizontal|vertical] [--json]
|
||||
orca terminal switch [--terminal <handle>] [--json]
|
||||
orca terminal close [--terminal <handle>] [--json]
|
||||
orca project list [--json]
|
||||
orca project setups [--project <id>] [--host <host-id>] [--json]
|
||||
orca project setup-existing-folder --project <id> --host <host-id> --path <path> [--kind git|folder] [--display-name <name>] [--json]
|
||||
orca repo list [--json]
|
||||
orca repo add --path <path> [--json]
|
||||
orca repo show --repo <selector> [--json]
|
||||
|
||||
+220
-15
@@ -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 () => {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <id>] [--host <host-id>] [--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 <id> --host <host-id> --path <path> [--kind git|folder] [--display-name <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'
|
||||
]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user