{{ message }}
Repository navigation
Expand file tree
/
Copy pathexecution-host-flag.ts
More file actions
319 lines (308 loc) · 11.6 KB
/
Copy pathexecution-host-flag.ts
File metadata and controls
319 lines (308 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import {
LOCAL_EXECUTION_HOST_ID,
normalizeExecutionHostId,
parseExecutionHostId,
toSshExecutionHostId,
type ParsedExecutionHost
} from '../shared/execution-host'
import {
ambiguousEnvironments,
ambiguousSshTargets,
crossKindNextSteps,
findEnvironmentByName,
findSshTargetByName,
fetchSshTargetSummaries,
LOCAL_HOST_LIST_NAME,
normalizeTypedName,
resolveSshHostTargetId,
stripWrappingQuotes,
type EnvironmentSummary,
type SshTargetSummary
} from './host-selector-alternatives'
import type { RuntimeClient } from './runtime-client'
import { RuntimeClientError } from './runtime/types'
export type HostFlagRoutingSelection = {
// Enumerating SSH targets needs the running host; defer lookup until error recovery.
listSshTargets: () => Promise<SshTargetSummary[]>
// Fail on unreadable targets: routing a bare name requires ruling out SSH collisions.
verifySshTargets: () => Promise<SshTargetSummary[]>
pairingCode: string | null
// Preserve the selection source, including ORCA_ENVIRONMENT, for conflict messages.
environmentSelector: { value: string; label: string } | null
}
// Host labels need async catalog lookup before they can resolve to a machine.
export type HostFlagAlias = { kind: 'alias'; alias: string }
export type HostFlagSelection = ParsedExecutionHost | HostFlagAlias
function isLocalHostListName(alias: string): boolean {
return normalizeTypedName(alias) === LOCAL_HOST_LIST_NAME
}
export function parseHostFlag(flags: Map<string, string | boolean>): HostFlagSelection | undefined {
if (!flags.has('host')) {
return undefined
}
const raw = flags.get('host')
if (typeof raw !== 'string' || raw.length === 0) {
throw new RuntimeClientError('invalid_argument', 'Missing value for --host')
}
const value = stripWrappingQuotes(raw.trim())
const parsed = parseExecutionHostId(value)
if (parsed) {
return parsed
}
const alias = value.trim()
// Prefixed values must parse as IDs, never aliases; malformed IDs could rebind downstream.
if (!alias || alias.startsWith('ssh:') || alias.startsWith('runtime:')) {
throw new RuntimeClientError(
'invalid_argument',
`Invalid --host value: ${raw}. Expected local, ssh:<target-id>, runtime:<environment-id>, or a host name from \`orca host list\`.`
)
}
return { kind: 'alias', alias }
}
// Pairing IDs are local to this machine; route to the paired host before applying its filters.
export async function resolveHostFlagEnvironmentId(
flags: Map<string, string | boolean>,
selection: HostFlagRoutingSelection
): Promise<string | null> {
const host = parseHostFlag(flags)
if (host?.kind !== 'runtime' && host?.kind !== 'alias') {
return null
}
const [{ listEnvironments, resolveEnvironment }, { getDefaultUserDataPath }] = await Promise.all([
import('./runtime/environments.js'),
import('./runtime-client.js')
])
const userDataPath = getDefaultUserDataPath()
const environments = listEnvironments(userDataPath).map((candidate) => ({
id: candidate.id,
name: candidate.name
}))
const wanted = host.kind === 'runtime' ? host.environmentId : host.alias
const flagSpelling = host.kind === 'runtime' ? `--host ${host.id}` : `--host ${host.alias}`
// An explicit server selection scopes bare aliases to that server's local/SSH hosts.
if (host.kind === 'alias' && (selection.environmentSelector || selection.pairingCode)) {
return null
}
// Accept environment names but canonicalize them to IDs for stored-host comparisons.
const environment = findEnvironmentByName(environments, wanted)
if (!environment) {
assertEnvironmentNameUnambiguous(environments, wanted, flagSpelling)
// A missing server alias may still name the local machine or one of its SSH targets.
if (host.kind === 'alias') {
return null
}
const sshTargets = await selection.listSshTargets()
throw new RuntimeClientError(
'invalid_argument',
`Unknown Orca server in ${flagSpelling}: no paired Orca server is named or has id ${wanted}.`,
{
knownEnvironments: environments,
knownSshTargets: sshTargets,
nextSteps: [
...crossKindNextSteps(wanted, { environments, sshTargets }, 'environment'),
'Run `orca environment list` to see paired Orca servers.',
'Use --host local to target this machine.'
]
}
)
}
if (host.kind === 'alias') {
if (isLocalHostListName(host.alias)) {
throw new RuntimeClientError(
'invalid_argument',
`Ambiguous --host ${host.alias}: it names both this machine and a paired Orca server.`,
{
knownEnvironments: [environment],
nextSteps: [
'Use --host local for this machine.',
`Use --host runtime:${environment.id} for the paired Orca server named ${environment.name}.`
]
}
)
}
await assertAliasNamesOneHost(host.alias, environment, selection)
}
if (selection.pairingCode) {
throw new RuntimeClientError(
'invalid_argument',
`${flagSpelling} already selects a paired Orca server; use either --host runtime:<id> or --pairing-code, not both.`
)
}
if (selection.environmentSelector) {
const { value } = selection.environmentSelector
const selected =
findEnvironmentByName(environments, value) ?? resolveEnvironment(userDataPath, value)
if (selected.id !== environment.id) {
throw new RuntimeClientError(
'invalid_argument',
`${flagSpelling} and ${selection.environmentSelector.label} ${selection.environmentSelector.value} name different Orca servers.`
)
}
}
return environment.id
}
// An alias naming both a paired server and an SSH target must not choose either silently.
async function assertAliasNamesOneHost(
alias: string,
environment: EnvironmentSummary,
selection: HostFlagRoutingSelection
): Promise<void> {
let sshTargets: SshTargetSummary[]
try {
sshTargets = await selection.verifySshTargets()
} catch {
throw new RuntimeClientError(
'invalid_argument',
`Cannot verify --host ${alias}: it names a paired Orca server, but this machine's SSH targets could not be read to rule out one with the same name.`,
{
knownEnvironments: [environment],
nextSteps: [
`Use --host runtime:${environment.id} for the paired Orca server named ${environment.name}.`
]
}
)
}
// Duplicate SSH labels are ambiguous, not evidence that no target matches.
const matched = findSshTargetByName(sshTargets, alias)
const conflicts = matched ? [matched] : ambiguousSshTargets(sshTargets, alias)
if (conflicts.length === 0) {
return
}
const sshCount =
conflicts.length === 1 ? 'an SSH target' : `${conflicts.length} SSH targets sharing that label`
throw new RuntimeClientError(
'invalid_argument',
`Ambiguous --host ${alias}: it names both a paired Orca server and ${sshCount} on this machine.`,
{
knownEnvironments: [environment],
knownSshTargets: conflicts,
nextSteps: [
`Use --host runtime:${environment.id} for the paired Orca server named ${environment.name}.`,
...conflicts.map(
(target) => `Use --host ssh:${target.id} for the SSH target labeled ${target.label}.`
)
]
}
)
}
// After routing, local and runtime:<id> setups both belong to the selected server.
export function hostFilterMatchesHostId(
filter: ParsedExecutionHost,
candidateHostId: string | null | undefined
): boolean {
const candidate = normalizeExecutionHostId(candidateHostId)
if (candidate === filter.id) {
return true
}
return filter.kind === 'runtime' && candidate === LOCAL_EXECUTION_HOST_ID
}
// Validate SSH IDs against the connected runtime, not this machine's pairing store.
export async function resolveHostFlagTarget(
flags: Map<string, string | boolean>,
client: RuntimeClient
): Promise<ParsedExecutionHost | undefined> {
const host = parseHostFlag(flags)
if (host?.kind !== 'ssh' && host?.kind !== 'alias') {
return host
}
const [{ listEnvironments }, { getDefaultUserDataPath }] = await Promise.all([
import('./runtime/environments.js'),
import('./runtime-client.js')
])
const environments = listEnvironments(getDefaultUserDataPath()).map((candidate) => ({
id: candidate.id,
name: candidate.name
}))
if (host.kind === 'alias' && isLocalHostListName(host.alias)) {
return resolveLocalHostListName(host.alias, client)
}
// Server lookup already missed; SSH is the remaining alias scope.
const wanted = host.kind === 'ssh' ? host.targetId : host.alias
const flagSpelling = host.kind === 'ssh' ? `--host ${host.id}` : `--host ${host.alias}`
const targetId = await resolveSshHostTargetId(
client,
wanted,
environments,
flagSpelling,
host.kind === 'alias'
)
const sshHostId = toSshExecutionHostId(targetId)
return parseExecutionHostId(sshHostId) ?? { kind: 'ssh', id: sshHostId, targetId }
}
async function resolveLocalHostListName(
alias: string,
client: RuntimeClient
): Promise<ParsedExecutionHost> {
// Strict listing: an unreadable target list cannot rule out a clash.
const { targets: sshTargets } = await fetchSshTargetSummaries<SshTargetSummary>(client)
const clashing = sshTargets.filter(
(target) => normalizeTypedName(target.label) === LOCAL_HOST_LIST_NAME
)
if (clashing.length > 0) {
throw new RuntimeClientError(
'invalid_argument',
`Ambiguous --host ${alias}: it names both this machine and an SSH target.`,
{
knownSshTargets: clashing,
nextSteps: [
'Use --host local for this machine.',
...clashing.map((target) => `Use --host ssh:${target.id} for ${target.label}.`)
]
}
)
}
return { kind: 'local', id: LOCAL_EXECUTION_HOST_ID }
}
// Preserve the store's refusal to guess between duplicate environment names.
function assertEnvironmentNameUnambiguous(
environments: readonly { id: string; name: string }[],
name: string,
flag: string
): void {
const ambiguous = ambiguousEnvironments(environments, name)
if (ambiguous.length === 0) {
return
}
throw new RuntimeClientError(
'invalid_argument',
`Ambiguous Orca server in ${flag}: ${ambiguous.length} paired servers are named ${name}. Use the environment id.`,
{
knownEnvironments: ambiguous,
nextSteps: ambiguous.map(
(candidate) => `Use --host runtime:${candidate.id} for the server named ${candidate.name}.`
)
}
)
}
// Resolve here so cross-host suggestions survive store errors that discard structured data.
export async function resolveEnvironmentSelectorForStore(
selector: string,
listSshTargetsForSuggestion: () => Promise<SshTargetSummary[]>
): Promise<string> {
const [{ listEnvironments }, { getDefaultUserDataPath }] = await Promise.all([
import('./runtime/environments.js'),
import('./runtime-client.js')
])
const environments = listEnvironments(getDefaultUserDataPath()).map((candidate) => ({
id: candidate.id,
name: candidate.name
}))
const found = findEnvironmentByName(environments, selector)
if (found) {
return found.id === selector || found.name === selector ? selector : found.id
}
assertEnvironmentNameUnambiguous(environments, selector, `--environment ${selector}`)
const sshTargets = await listSshTargetsForSuggestion()
throw new RuntimeClientError(
'invalid_argument',
`Unknown Orca server in --environment ${selector}: no paired Orca server is named or has id ${selector}.`,
{
knownEnvironments: environments,
knownSshTargets: sshTargets,
nextSteps: [
...crossKindNextSteps(selector, { environments, sshTargets }, 'environment'),
'Run `orca host list` to see every machine you can target and the flag for each.'
]
}
)
}
You can’t perform that action at this time.
