mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
fix(cli): reject runtime selectors on host list and environment list
`orca host list --environment m4air` was not ignoring the flag — it was applying it to half the answer. `shouldIgnoreRemoteSelection` never pinned the `host` family, so the SSH-target lookup was routed to m4air while paired servers were still read from this machine's own pairing store, and the handler stamped the envelope `_meta.runtimeId: "local"` regardless. The result was one listing describing two hosts: the openclaw row silently disappeared, which reads as "m4air has no SSH targets". `environment list --environment X` had the pin but no guard, so the flag vanished with no signal at all. Reject rather than route. `host list` answers "what can this machine target and with what flag"; its paired-server half comes from a client-local store and cannot be routed at all, so any routed answer is necessarily half-substituted — rule 1 of docs/reference/ssh-execution-boundary.md. `environment list` is entirely client-local, so there is no other host to ask. This matches the `account` and `artifacts` precedent, the only two pinned families that already paired the pin with a rejection guard. - pin the `host` family so an ambient ORCA_ENVIRONMENT cannot produce the same two-machine listing with no flag to reject; `runtimeId: "local"` is now true - extract the duplicated `rejectRemoteSelectionFlags` from account.ts and artifacts.ts into src/cli/remote-selection-flag-rejection.ts - `environment show` / `environment rm` / `environment add` are untouched: there `--environment` and `--pairing-code` name the row to act on, not a route
This commit is contained in:
@@ -7,6 +7,7 @@ import type { CommandHandler, HandlerContext } from '../dispatch'
|
||||
import { printResult } from '../format'
|
||||
import { RuntimeClientError } from '../runtime-client'
|
||||
import { stripElectronRunAsNode } from '../runtime/launch'
|
||||
import { rejectRemoteSelectionFlags } from '../remote-selection-flag-rejection'
|
||||
import {
|
||||
deleteActiveClaudeKeychainCredentialsStrict,
|
||||
readActiveClaudeKeychainCredentialsStrict,
|
||||
@@ -276,15 +277,11 @@ async function addCodexAccount({ client, json }: HandlerContext): Promise<void>
|
||||
* mistake this feature exists to avoid. A `--help` note does not reach someone who
|
||||
* already typed the flag.
|
||||
*/
|
||||
function rejectRemoteSelectionFlags(ctx: HandlerContext, command: string): void {
|
||||
for (const flag of ['environment', 'pairing-code']) {
|
||||
if (ctx.flags.has(flag)) {
|
||||
throw new RuntimeClientError(
|
||||
'invalid_argument',
|
||||
`\`--${flag}\` does not retarget \`${command}\`. Run it on the host whose accounts you want to manage.`
|
||||
)
|
||||
}
|
||||
}
|
||||
function rejectAccountRemoteSelectionFlags(ctx: HandlerContext, command: string): void {
|
||||
rejectRemoteSelectionFlags(
|
||||
ctx.flags,
|
||||
`\`${command}\`. Run it on the host whose accounts you want to manage.`
|
||||
)
|
||||
}
|
||||
|
||||
async function assertAccountImportSupported({ client }: HandlerContext): Promise<void> {
|
||||
@@ -316,14 +313,14 @@ export const ACCOUNT_HANDLERS: Record<string, CommandHandler> = {
|
||||
`Unsupported --agent "${agent}". Use "claude" or "codex".`
|
||||
)
|
||||
}
|
||||
rejectRemoteSelectionFlags(ctx, 'orca account add')
|
||||
rejectAccountRemoteSelectionFlags(ctx, 'orca account add')
|
||||
// Why: fail on runtime version skew before burning a full OAuth round trip.
|
||||
await assertAccountImportSupported(ctx)
|
||||
await ctx.client.call('accounts.list', { refreshUsage: false })
|
||||
await (agent === 'claude' ? addClaudeAccount(ctx) : addCodexAccount(ctx))
|
||||
},
|
||||
'account list': async (ctx) => {
|
||||
rejectRemoteSelectionFlags(ctx, 'orca account list')
|
||||
rejectAccountRemoteSelectionFlags(ctx, 'orca account list')
|
||||
const { client, json } = ctx
|
||||
// Why: this command renders no usage numbers, so skip the forced provider
|
||||
// refresh — it is one serial network round-trip per managed account.
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ARTIFACT_SHARING_DISABLED_NEXT_STEPS
|
||||
} from '../../shared/artifact-sharing-gate'
|
||||
import type { CommandHandler, HandlerContext } from '../dispatch'
|
||||
import { rejectRemoteSelectionFlags } from '../remote-selection-flag-rejection'
|
||||
import { RuntimeClientError } from '../runtime-client'
|
||||
import { formatArtifactListPage, formatArtifactShared } from '../artifact-format'
|
||||
import { printResult } from '../format'
|
||||
@@ -44,15 +45,11 @@ function cloudOptions(ctx: HandlerContext): ArtifactCloudOptions {
|
||||
}
|
||||
}
|
||||
|
||||
function rejectRemoteSelectionFlags(ctx: HandlerContext): void {
|
||||
for (const flag of ['environment', 'pairing-code']) {
|
||||
if (ctx.flags.has(flag)) {
|
||||
throw new RuntimeClientError(
|
||||
'invalid_argument',
|
||||
`\`--${flag}\` does not retarget artifact commands; artifacts use the signed-in desktop account.`
|
||||
)
|
||||
}
|
||||
}
|
||||
function rejectArtifactRemoteSelectionFlags(ctx: HandlerContext): void {
|
||||
rejectRemoteSelectionFlags(
|
||||
ctx.flags,
|
||||
'artifact commands; artifacts use the signed-in desktop account.'
|
||||
)
|
||||
}
|
||||
|
||||
function artifactContentType(path: string): ArtifactWriteRequest['contentType'] | null {
|
||||
@@ -165,7 +162,7 @@ function requireOperation<T>(operation: ArtifactCloudOperation<T>): T {
|
||||
|
||||
export const ARTIFACT_HANDLERS: Record<string, CommandHandler> = {
|
||||
'artifacts list': async (ctx) => {
|
||||
rejectRemoteSelectionFlags(ctx)
|
||||
rejectArtifactRemoteSelectionFlags(ctx)
|
||||
const cursor = stringFlag(ctx, 'cursor')
|
||||
const response = await ctx.client.call<ArtifactCloudOperation<ArtifactListPage>>(
|
||||
'artifacts.list',
|
||||
@@ -178,7 +175,7 @@ export const ARTIFACT_HANDLERS: Record<string, CommandHandler> = {
|
||||
printResult({ ...response, result: value }, ctx.json, formatArtifactListPage)
|
||||
},
|
||||
'artifacts share': async (ctx) => {
|
||||
rejectRemoteSelectionFlags(ctx)
|
||||
rejectArtifactRemoteSelectionFlags(ctx)
|
||||
const response = await ctx.client.call<ArtifactCloudOperation<ArtifactListItem>>(
|
||||
'artifacts.share',
|
||||
await readArtifactRequest(ctx)
|
||||
@@ -187,7 +184,7 @@ export const ARTIFACT_HANDLERS: Record<string, CommandHandler> = {
|
||||
printResult({ ...response, result: value }, ctx.json, formatArtifactShared)
|
||||
},
|
||||
'artifacts update': async (ctx) => {
|
||||
rejectRemoteSelectionFlags(ctx)
|
||||
rejectArtifactRemoteSelectionFlags(ctx)
|
||||
const response = await ctx.client.call<ArtifactCloudOperation<ArtifactListItem>>(
|
||||
'artifacts.update',
|
||||
await readArtifactRequest(ctx)
|
||||
@@ -196,7 +193,7 @@ export const ARTIFACT_HANDLERS: Record<string, CommandHandler> = {
|
||||
printResult({ ...response, result: value }, ctx.json, formatArtifactShared)
|
||||
},
|
||||
'artifacts unshare': async (ctx) => {
|
||||
rejectRemoteSelectionFlags(ctx)
|
||||
rejectArtifactRemoteSelectionFlags(ctx)
|
||||
const remoteInput = parseRemoteArtifactInput(process.env[REMOTE_ARTIFACT_INPUT_ENV])
|
||||
const sourceKey = remoteInput?.sourceKey ?? resolve(ctx.cwd, requireStringFlag(ctx, 'file'))
|
||||
const response = await ctx.client.call<ArtifactCloudOperation<void>>('artifacts.unshare', {
|
||||
@@ -207,7 +204,7 @@ export const ARTIFACT_HANDLERS: Record<string, CommandHandler> = {
|
||||
printResult({ ...response, result: { deleted: true } }, ctx.json, () => 'Artifact deleted.')
|
||||
},
|
||||
'artifacts delete': async (ctx) => {
|
||||
rejectRemoteSelectionFlags(ctx)
|
||||
rejectArtifactRemoteSelectionFlags(ctx)
|
||||
const response = await ctx.client.call<ArtifactCloudOperation<void>>('artifacts.delete', {
|
||||
id: requireStringFlag(ctx, 'id'),
|
||||
...cloudOptions(ctx)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { formatEnvironment, formatEnvironmentList, formatHostList, printResult }
|
||||
import { listSshTargets } from '../host-selector-alternatives'
|
||||
import { getDefaultUserDataPath, RuntimeClientError } from '../runtime-client'
|
||||
import type { RuntimeRpcSuccess } from '../runtime-client'
|
||||
import { rejectRemoteSelectionFlags } from '../remote-selection-flag-rejection'
|
||||
import { redactRuntimeEnvironment } from '../../shared/runtime-environments'
|
||||
import {
|
||||
addEnvironmentFromPairingCode,
|
||||
@@ -33,7 +34,12 @@ export const ENVIRONMENT_HANDLERS: Record<string, CommandHandler> = {
|
||||
// Why: an agent told "run it on <name>" had nowhere to look. `orca environment list` showed
|
||||
// paired servers only, and nothing in the CLI listed SSH targets at all, so the wrong-axis
|
||||
// guess was the only move available. This is the one place that answers both.
|
||||
'host list': async ({ client, json }) => {
|
||||
'host list': async ({ client, flags, json }) => {
|
||||
rejectLocalPairingStoreRetargeting(
|
||||
flags,
|
||||
'`orca host list`. It answers from this machine\u2019s own pairing store, so a routed answer would name servers paired with a different machine.',
|
||||
'Run `orca host list` on that machine to see the SSH targets registered there.'
|
||||
)
|
||||
const environments = listEnvironments(getDefaultUserDataPath()).map((environment) => ({
|
||||
kind: 'environment' as const,
|
||||
name: environment.name,
|
||||
@@ -53,7 +59,12 @@ export const ENVIRONMENT_HANDLERS: Record<string, CommandHandler> = {
|
||||
]
|
||||
printResult(localSuccess({ hosts }), json, formatHostList)
|
||||
},
|
||||
'environment list': async ({ json }) => {
|
||||
'environment list': async ({ flags, json }) => {
|
||||
rejectLocalPairingStoreRetargeting(
|
||||
flags,
|
||||
'`orca environment list`. Paired servers are stored on this machine, so there is no other host to ask.',
|
||||
'Run `orca environment list` on that machine to see the servers paired with it.'
|
||||
)
|
||||
const environments = listEnvironments(getDefaultUserDataPath()).map(redactRuntimeEnvironment)
|
||||
printResult(localSuccess({ environments }), json, formatEnvironmentList)
|
||||
},
|
||||
@@ -78,6 +89,23 @@ export const ENVIRONMENT_HANDLERS: Record<string, CommandHandler> = {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These two listings are pinned local by `shouldIgnoreRemoteSelection`, so a runtime selector is
|
||||
* dropped for routing. It used to still reach the SSH half of `host list` through the routed
|
||||
* client, producing a listing whose SSH rows came from the named server and whose paired-server
|
||||
* rows came from this machine — one answer describing two hosts, stamped `runtimeId: local`.
|
||||
* Failing is the only answer that is true of a single machine.
|
||||
*/
|
||||
function rejectLocalPairingStoreRetargeting(
|
||||
flags: Map<string, string | boolean>,
|
||||
suffix: string,
|
||||
crossHostNextStep: string
|
||||
): void {
|
||||
rejectRemoteSelectionFlags(flags, suffix, {
|
||||
nextSteps: [crossHostNextStep, 'Drop the flag to answer for this machine.']
|
||||
})
|
||||
}
|
||||
|
||||
function getRequiredStringFlag(flags: Map<string, string | boolean>, name: string): string {
|
||||
const value = flags.get(name)
|
||||
if (typeof value !== 'string' || value.length === 0) {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
callMock,
|
||||
runtimeClientConstructorMock,
|
||||
serveOrcaAppMock,
|
||||
getDefaultUserDataPathMock,
|
||||
addEnvironmentFromPairingCodeMock,
|
||||
listEnvironmentsMock,
|
||||
removeEnvironmentMock,
|
||||
resolveEnvironmentMock,
|
||||
spawnMock
|
||||
} = vi.hoisted(() => ({
|
||||
callMock: vi.fn(),
|
||||
runtimeClientConstructorMock: vi.fn(),
|
||||
serveOrcaAppMock: vi.fn(),
|
||||
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
|
||||
addEnvironmentFromPairingCodeMock: vi.fn(),
|
||||
listEnvironmentsMock: vi.fn(),
|
||||
removeEnvironmentMock: vi.fn(),
|
||||
resolveEnvironmentMock: vi.fn(),
|
||||
spawnMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./runtime-client', async () => {
|
||||
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
|
||||
return createRuntimeClientModuleMock({
|
||||
callMock,
|
||||
runtimeClientConstructorMock,
|
||||
serveOrcaAppMock,
|
||||
getDefaultUserDataPathMock
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock('./runtime/environments', () => ({
|
||||
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
|
||||
listEnvironments: listEnvironmentsMock,
|
||||
removeEnvironment: removeEnvironmentMock,
|
||||
resolveEnvironment: resolveEnvironmentMock
|
||||
}))
|
||||
|
||||
vi.mock('child_process', async () => {
|
||||
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
|
||||
return createChildProcessModuleMock(spawnMock)
|
||||
})
|
||||
|
||||
import { main } from './index'
|
||||
import { okFixture, queueFixtures } from './test-fixtures'
|
||||
import { pairRuntimeEnvironment, useWorktreeAwarenessEnvironment } from './index-test-harness'
|
||||
|
||||
const SSH_TARGET = { id: 'ssh-1777360569033-yvz2mp', label: 'openclaw' }
|
||||
|
||||
/** Every SSH-target lookup answers with the one target only this machine's runtime knows about. */
|
||||
function queueSshTargetLookups(count: number): void {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
...Array.from({ length: count }, () => okFixture('req_ssh_targets', { targets: [SSH_TARGET] }))
|
||||
)
|
||||
}
|
||||
|
||||
describe('runtime-selector flags on locally pinned CLI commands', () => {
|
||||
useWorktreeAwarenessEnvironment({
|
||||
callMock,
|
||||
serveOrcaAppMock,
|
||||
getDefaultUserDataPathMock,
|
||||
addEnvironmentFromPairingCodeMock,
|
||||
listEnvironmentsMock,
|
||||
spawnMock
|
||||
})
|
||||
|
||||
it('answers `host list` from this machine and stamps the runtime that actually answered', async () => {
|
||||
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
|
||||
queueSshTargetLookups(1)
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['host', 'list', '--json'], '/tmp/repo')
|
||||
|
||||
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
|
||||
expect(printed._meta.runtimeId).toBe('local')
|
||||
expect(printed.result.hosts.map((host: { id: string }) => host.id)).toEqual([
|
||||
'local',
|
||||
SSH_TARGET.id,
|
||||
'env-m4air'
|
||||
])
|
||||
// The tell: `runtimeId: local` is only honest if no routed client was ever built.
|
||||
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, null)
|
||||
})
|
||||
|
||||
it('rejects `host list --environment` instead of answering with a half-routed listing', async () => {
|
||||
// Why: pre-fix this routed the SSH lookup to m4air while reading paired servers from this
|
||||
// machine, dropped the openclaw row, and still stamped `_meta.runtimeId: "local"` — one
|
||||
// listing describing two hosts, which reads as "m4air has no SSH targets".
|
||||
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['host', 'list', '--environment', 'm4air', '--json'], '/tmp/repo')
|
||||
|
||||
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
|
||||
expect(printed.ok).toBe(false)
|
||||
expect(printed.error.code).toBe('invalid_argument')
|
||||
expect(printed.error.message).toContain('`--environment` does not retarget `orca host list`')
|
||||
expect(process.exitCode).toBe(1)
|
||||
expect(callMock).not.toHaveBeenCalled()
|
||||
expect(runtimeClientConstructorMock).not.toHaveBeenCalledWith(null, 'm4air')
|
||||
process.exitCode = 0
|
||||
})
|
||||
|
||||
it('rejects `environment list --environment` rather than repeating the local answer', async () => {
|
||||
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['environment', 'list', '--environment', 'm4air', '--json'], '/tmp/repo')
|
||||
|
||||
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
|
||||
expect(printed.ok).toBe(false)
|
||||
expect(printed.error.code).toBe('invalid_argument')
|
||||
expect(printed.error.message).toContain(
|
||||
'`--environment` does not retarget `orca environment list`'
|
||||
)
|
||||
process.exitCode = 0
|
||||
})
|
||||
|
||||
it('rejects `--pairing-code` on both listings for the same reason', async () => {
|
||||
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['host', 'list', '--pairing-code', 'orca://pair?code=x', '--json'], '/tmp/repo')
|
||||
await main(
|
||||
['environment', 'list', '--pairing-code', 'orca://pair?code=x', '--json'],
|
||||
'/tmp/repo'
|
||||
)
|
||||
|
||||
for (const call of logSpy.mock.calls) {
|
||||
const printed = JSON.parse(String(call[0]))
|
||||
expect(printed.ok).toBe(false)
|
||||
expect(printed.error.message).toContain('`--pairing-code` does not retarget')
|
||||
}
|
||||
expect(callMock).not.toHaveBeenCalled()
|
||||
process.exitCode = 0
|
||||
})
|
||||
|
||||
it('keeps `host list` local when ORCA_ENVIRONMENT is set ambiently', async () => {
|
||||
// Why: the ambient variable produced the same two-machine listing as the explicit flag, with
|
||||
// no flag to reject. Pinning the family is what makes `runtimeId: local` true in both cases.
|
||||
process.env.ORCA_ENVIRONMENT = 'm4air'
|
||||
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
|
||||
queueSshTargetLookups(1)
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['host', 'list', '--json'], '/tmp/repo')
|
||||
|
||||
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
|
||||
expect(printed.ok).toBe(true)
|
||||
expect(printed.result.hosts.some((host: { id: string }) => host.id === SSH_TARGET.id)).toBe(
|
||||
true
|
||||
)
|
||||
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, null)
|
||||
expect(runtimeClientConstructorMock).not.toHaveBeenCalledWith(undefined, undefined)
|
||||
})
|
||||
|
||||
it('still treats --environment as the selector argument on `environment show` and `rm`', async () => {
|
||||
// Why: the guard must not fire where the flag names the row to act on rather than a route.
|
||||
const environment = {
|
||||
id: 'env-m4air',
|
||||
name: 'm4air',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastUsedAt: null,
|
||||
runtimeId: null,
|
||||
endpoints: [],
|
||||
preferredEndpointId: null
|
||||
}
|
||||
resolveEnvironmentMock.mockReturnValue(environment)
|
||||
removeEnvironmentMock.mockReturnValue(environment)
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['environment', 'show', '--environment', 'm4air', '--json'], '/tmp/repo')
|
||||
await main(['environment', 'rm', '--environment', 'm4air', '--json'], '/tmp/repo')
|
||||
|
||||
for (const call of logSpy.mock.calls) {
|
||||
expect(JSON.parse(String(call[0])).ok).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -31,6 +31,10 @@ function shouldIgnoreRemoteSelection(commandPath: string[]): boolean {
|
||||
commandPath[0] === 'account' ||
|
||||
commandPath[0] === 'artifacts' ||
|
||||
commandPath[0] === 'environment' ||
|
||||
// Why: `host list` answers "what can this machine target, and with what flag". Half of that
|
||||
// answer (paired servers) is read from this machine's own pairing store and cannot be routed,
|
||||
// so routing the other half produced one listing describing two machines at once.
|
||||
commandPath[0] === 'host' ||
|
||||
commandPath[0] === 'serve' ||
|
||||
commandPath[0] === 'agent' ||
|
||||
commandPath[0] === 'vm' ||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { RuntimeClientError } from './runtime/types'
|
||||
|
||||
/**
|
||||
* The flags that pick which runtime answers a command. `shouldIgnoreRemoteSelection`
|
||||
* in `src/cli/index.ts` pins some command families to the local runtime, which drops
|
||||
* these silently — so every pinned family pairs the pin with this rejection instead.
|
||||
*/
|
||||
export const REMOTE_SELECTION_FLAGS = ['environment', 'pairing-code'] as const
|
||||
|
||||
/**
|
||||
* Fails a pinned command that was given a runtime selector, rather than answering
|
||||
* for a machine the caller did not name. `suffix` completes "`--<flag>` does not
|
||||
* retarget …" and should say what the command answers for and where to run it.
|
||||
*/
|
||||
export function rejectRemoteSelectionFlags(
|
||||
flags: ReadonlyMap<string, string | boolean>,
|
||||
suffix: string,
|
||||
data?: Record<string, unknown>
|
||||
): void {
|
||||
for (const flag of REMOTE_SELECTION_FLAGS) {
|
||||
if (flags.has(flag)) {
|
||||
throw new RuntimeClientError(
|
||||
'invalid_argument',
|
||||
`\`--${flag}\` does not retarget ${suffix}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ export const ENVIRONMENT_COMMAND_SPECS: CommandSpec[] = [
|
||||
notes: [
|
||||
'Answers "what can I target and what do I pass" in one place: this machine, the SSH targets registered on it, and the Orca servers paired with it.',
|
||||
'The three kinds are reached differently. A paired Orca server is a connection, selected with --environment <name>. An SSH target is a machine the connected Orca host reaches, selected with --host ssh:<id>. Passing one where the other belongs is the most common way to get an empty or missing-host answer.',
|
||||
"SSH targets are read from the Orca host you are currently connected to, so this lists that host's targets and not another server's."
|
||||
"SSH targets are read from this machine's own Orca runtime, so this lists that machine's targets and not another server's. Run `orca host list` on the other machine to see the targets registered there.",
|
||||
'--environment and --pairing-code are rejected rather than ignored: paired servers come from this machine\u2019s pairing store, so a routed answer would describe two machines at once.'
|
||||
],
|
||||
examples: ['orca host list', 'orca host list --json']
|
||||
},
|
||||
@@ -25,7 +26,10 @@ export const ENVIRONMENT_COMMAND_SPECS: CommandSpec[] = [
|
||||
path: ['environment', 'list'],
|
||||
summary: 'List saved Orca runtime environments',
|
||||
usage: 'orca environment list [--json]',
|
||||
allowedFlags: [...GLOBAL_FLAGS]
|
||||
allowedFlags: [...GLOBAL_FLAGS],
|
||||
notes: [
|
||||
'Answers from this machine\u2019s pairing store. --environment and --pairing-code are rejected rather than ignored, because there is no other host that could answer.'
|
||||
]
|
||||
},
|
||||
{
|
||||
path: ['environment', 'show'],
|
||||
|
||||
Reference in New Issue
Block a user