diff --git a/src/cli/format.ts b/src/cli/format.ts index cae419094d2..1487a69eea0 100644 --- a/src/cli/format.ts +++ b/src/cli/format.ts @@ -245,6 +245,9 @@ function formatHostConnection(host: HostListEntry): string { if (host.kind !== 'ssh') { return '' } + if (host.connected === undefined) { + return `connection unknown${host.connectionStatus ? ` (${host.connectionStatus})` : ''}` + } return host.connected ? `connected${host.connectionStatus ? ` (${host.connectionStatus})` : ''}` : `not connected${host.connectionStatus ? ` (${host.connectionStatus})` : ''}` diff --git a/src/cli/handlers/environment.ts b/src/cli/handlers/environment.ts index a597df8669e..181b2947f98 100644 --- a/src/cli/handlers/environment.ts +++ b/src/cli/handlers/environment.ts @@ -51,7 +51,7 @@ export const ENVIRONMENT_HANDLERS: Record = { name: target.label, id: target.id, selector: `--host ssh:${target.id}`, - connected: target.connected ?? false, + ...(target.connected === undefined ? {} : { connected: target.connected }), ...(target.connectionStatus ? { connectionStatus: target.connectionStatus } : {}), ...(target.remotePlatform ? { platform: target.remotePlatform } : {}) })) diff --git a/src/cli/host-selector-alternatives.test.ts b/src/cli/host-selector-alternatives.test.ts index bc4fadd05c1..9460931a083 100644 --- a/src/cli/host-selector-alternatives.test.ts +++ b/src/cli/host-selector-alternatives.test.ts @@ -118,6 +118,24 @@ describe('listSshTargets', () => { expect(call).toHaveBeenCalledWith('ssh.listTargets') }) + it('enriches legacy target rows from host-owned connection state', async () => { + const { RuntimeClientError } = await import('./runtime/types.js') + const call = vi.fn(async (method: string) => { + if (method === 'ssh.listTargetSummaries') { + throw new RuntimeClientError('method_not_found', 'Unknown method') + } + if (method === 'ssh.getState') { + return { result: { state: { status: 'connected', remotePlatform: 'win32' } } } + } + return { result: { targets: SSH_TARGETS } } + }) + + await expect(listSshTargets({ call } as unknown as RuntimeClient)).resolves.toEqual([ + { ...SSH_TARGETS[0], connected: true, connectionStatus: 'connected', remotePlatform: 'win32' } + ]) + expect(call).toHaveBeenCalledWith('ssh.getState', { targetId: SSH_TARGETS[0].id }) + }) + // Why: this only ever runs to enrich an error we are already reporting; a failure here must // not replace that error with a confusing one about SSH enumeration. it('returns nothing rather than masking the error it was enriching', async () => { diff --git a/src/cli/host-selector-alternatives.ts b/src/cli/host-selector-alternatives.ts index 564fbef5bd9..f42fec88aee 100644 --- a/src/cli/host-selector-alternatives.ts +++ b/src/cli/host-selector-alternatives.ts @@ -112,7 +112,7 @@ export async function listSshTargets(client: RuntimeClient): Promise('ssh.listTargets') - return legacy.result.targets + return await enrichLegacySshTargetStates(client, legacy.result.targets) } catch { return [] } @@ -121,6 +121,34 @@ export async function listSshTargets(client: RuntimeClient): Promise { + 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:` 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--`), the label a caller actually