Files
orca/src/cli/host-selector-alternatives.ts
Neil 51eed5a1bc feat(cli): report SSH host platforms (#18896)
* feat(cli): report SSH host platforms

* feat(cli): include SSH connection status

* fix(cli): preserve unknown SSH connection state
2026-09-05 14:35:45 -07:00

194 lines
7.2 KiB
TypeScript

import type { RuntimeClient } from './runtime-client'
export type SshTargetSummary = {
id: string
label: string
remotePlatform?: 'linux' | 'darwin' | 'win32'
connected?: boolean
connectionStatus?: string
}
export type EnvironmentSummary = { id: string; name: string }
export type HostAlternatives = {
sshTargets: readonly SshTargetSummary[]
environments: readonly EnvironmentSummary[]
}
// Why: SSH target ids are machine-generated (`ssh-<timestamp>-<random>`), so the name a person
// or an agent actually knows is the label. Looking a name up as an id alone reports "not found"
// for a target that is sitting right there.
export function findSshTargetByName(
targets: readonly SshTargetSummary[],
name: string
): SshTargetSummary | undefined {
const byId = targets.find((target) => target.id === name)
if (byId) {
return byId
}
const byLabel = matchesByLabel(targets, name)
// Why: two targets can share a label. Picking the first would silently choose a machine for
// the caller — the failure this whole selector path exists to prevent — so an ambiguous name
// resolves to nothing and the caller is told to use an id.
return byLabel.length === 1 ? byLabel[0] : undefined
}
function matchesByLabel(targets: readonly SshTargetSummary[], name: string): SshTargetSummary[] {
const wanted = name.trim().toLowerCase()
return targets.filter((target) => target.label.trim().toLowerCase() === wanted)
}
export function ambiguousSshTargets(
targets: readonly SshTargetSummary[],
name: string
): SshTargetSummary[] {
const byLabel = matchesByLabel(targets, name)
return byLabel.length > 1 ? byLabel : []
}
export function findEnvironmentByName(
environments: readonly EnvironmentSummary[],
name: string
): EnvironmentSummary | undefined {
const byId = environments.find((environment) => environment.id === name)
if (byId) {
return byId
}
const byName = matchesByEnvironmentName(environments, name)
// Why: the environment store itself refuses an ambiguous name rather than guessing; resolving
// one here would quietly reintroduce the guess it exists to prevent.
return byName.length === 1 ? byName[0] : undefined
}
function matchesByEnvironmentName(
environments: readonly EnvironmentSummary[],
name: string
): EnvironmentSummary[] {
const wanted = name.trim().toLowerCase()
return environments.filter((environment) => environment.name.trim().toLowerCase() === wanted)
}
export function ambiguousEnvironments(
environments: readonly EnvironmentSummary[],
name: string
): EnvironmentSummary[] {
const byName = matchesByEnvironmentName(environments, name)
return byName.length > 1 ? byName : []
}
// Why: a paired Orca server and an SSH target are different machines reached different ways, but
// a caller only knows "the machine called X". When X misses on one axis, the useful answer is
// almost always that it exists on the other — so say which, and give the exact flag.
export function crossKindNextSteps(
name: string,
alternatives: HostAlternatives,
requested: 'environment' | 'ssh'
): string[] {
const steps: string[] = []
const ssh = findSshTargetByName(alternatives.sshTargets, name)
const environment = findEnvironmentByName(alternatives.environments, name)
if (requested !== 'ssh' && ssh) {
steps.push(
`"${name}" is an SSH target on this Orca host, not a paired server. Use --host ssh:${ssh.id}.`
)
}
if (requested !== 'environment' && environment) {
steps.push(
`"${name}" is a paired Orca server, not an SSH target. Use --environment ${environment.name}.`
)
}
return steps
}
// Why: only display identity crosses this boundary — the RPC deliberately withholds addresses
// and credentials — and an enumeration failure must never mask the error we are explaining.
export async function listSshTargets(client: RuntimeClient): Promise<SshTargetSummary[]> {
try {
const result = await client.call<{ targets: SshTargetSummary[] }>('ssh.listTargetSummaries')
return result.result.targets
} catch (error) {
// Why: hosts predating listTargetSummaries still answer listTargets, and both are served by
// the same summariser. Without this an old host looks like one with no SSH targets at all,
// which would reject a target id that is actually valid there.
if (error instanceof Error && 'code' in error && error.code === 'method_not_found') {
try {
const legacy = await client.call<{ targets: SshTargetSummary[] }>('ssh.listTargets')
return await enrichLegacySshTargetStates(client, legacy.result.targets)
} catch {
return []
}
}
return []
}
}
async function enrichLegacySshTargetStates(
client: RuntimeClient,
targets: SshTargetSummary[]
): Promise<SshTargetSummary[]> {
return Promise.all(
targets.map(async (target) => {
try {
const response = await client.call<{
state: {
status?: string
remotePlatform?: 'linux' | 'darwin' | 'win32'
} | null
}>('ssh.getState', { targetId: target.id })
const state = response.result.state
return {
...target,
...(state?.status === undefined
? {}
: { connected: state.status === 'connected', connectionStatus: state.status }),
...(state?.remotePlatform === undefined ? {} : { remotePlatform: state.remotePlatform })
}
} catch {
return target
}
})
)
}
// Why: `--host ssh:<id>` was never validated, so an unknown target answered ok:true with an
// empty list — the same silent wrong-machine answer that `runtime:` ids used to give. And since
// target ids are machine-generated (`ssh-<timestamp>-<random>`), the label a caller actually
// knows never matches one, so this fires on the common spelling rather than a rare typo.
// Resolving the label and naming the alternative is what makes the failure recoverable.
export async function resolveSshHostTargetId(
client: RuntimeClient,
targetId: string,
environments: readonly EnvironmentSummary[]
): Promise<string> {
const targets = await listSshTargets(client)
const matched = findSshTargetByName(targets, targetId)
if (matched) {
return matched.id
}
const { RuntimeClientError } = await import('./runtime/types.js')
const ambiguous = ambiguousSshTargets(targets, targetId)
if (ambiguous.length > 0) {
throw new RuntimeClientError(
'invalid_argument',
`Ambiguous SSH target in --host ssh:${targetId}: ${ambiguous.length} targets share that label. Use the target id.`,
{
knownSshTargets: ambiguous,
nextSteps: ambiguous.map((target) => `Use --host ssh:${target.id} for ${target.label}.`)
}
)
}
throw new RuntimeClientError(
'invalid_argument',
`Unknown SSH target in --host ssh:${targetId}: this Orca host has no SSH target named or with id ${targetId}.`,
{
knownSshTargets: targets,
knownEnvironments: environments,
nextSteps: [
...crossKindNextSteps(targetId, { sshTargets: targets, environments }, 'ssh'),
...(targets.length > 0
? [`Known SSH targets: ${targets.map((target) => target.label).join(', ')}.`]
: ['This Orca host has no SSH targets registered.'])
]
}
)
}