From f330191b432bdc75e075d396c0d237ebd46ae0bc Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Wed, 11 Feb 2026 19:34:35 -0800 Subject: [PATCH 1/4] feat: [PR-1697] `sf nodes ssh` supports v2/nodes Try the v2 nodes SSH endpoint first, falling back to the v0/vms/ssh path. Extract SshInfo type, use apiClient for the v0 fallback, and use distinct host key aliases per endpoint version. Co-Authored-By: Claude Opus 4.6 --- src/lib/nodes/ssh.ts | 128 ++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 61 deletions(-) diff --git a/src/lib/nodes/ssh.ts b/src/lib/nodes/ssh.ts index 98ac9a1..6659536 100644 --- a/src/lib/nodes/ssh.ts +++ b/src/lib/nodes/ssh.ts @@ -6,15 +6,24 @@ import chalk from "chalk"; import ora from "ora"; import { Shescape } from "shescape"; -import { getAuthToken } from "../../helpers/config.ts"; +import { apiClient } from "../../apiClient.ts"; +import { getAuthToken, loadConfig } from "../../helpers/config.ts"; import { logAndQuit, logSessionTokenExpiredAndQuit, } from "../../helpers/errors.ts"; -import { getApiUrl } from "../../helpers/urls.ts"; import { handleNodesError, nodesClient } from "../../nodesClient.ts"; import { jsonOption } from "./utils.ts"; +type SshInfo = { + ssh_hostname: string; + ssh_port: number; + ssh_host_keys?: { + key_type: string; + base64_encoded_key: string; + }[]; +}; + const ssh = new Command("ssh") .description(`SSH into a VM on a node. @@ -46,7 +55,7 @@ Examples: \x1b[2m# SSH with a specific username\x1b[0m $ sf nodes ssh jenson@my-node - + \x1b[2m# SSH directly to a VM ID\x1b[0m $ sf nodes ssh root@vm_xxxxxxxxxxxxxxxxxxxxx `, @@ -67,75 +76,72 @@ Examples: logAndQuit(`Invalid SSH destination string: ${destination}`); } - let vmId: string; - - // If the ID doesn't start with vm_, assume it's a node name/ID - if (!nodeOrVmId.startsWith("vm_")) { - const client = await nodesClient(); - const spinner = ora("Fetching node information...").start(); + const sshSpinner = ora("Fetching SSH information...").start(); + const config = await loadConfig(); + const token = await getAuthToken(); + + let hostKeyAlias: string; + let data: SshInfo; + + // Try v2 endpoint first + const v2Response = await fetch( + `${config.api_url}/v2/nodes/${nodeOrVmId}/ssh`, + { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }, + ); - try { - const node = await client.nodes.get(nodeOrVmId); - spinner.succeed(`Node found for name ${chalk.cyan(nodeOrVmId)}.`); - - if (!node?.current_vm) { - spinner.fail( - `Node ${chalk.cyan( - nodeOrVmId, - )} does not have a current VM. VMs can take up to 5-10 minutes to spin up.`, - ); - process.exit(1); + if (v2Response.ok) { + data = await v2Response.json(); + hostKeyAlias = `${nodeOrVmId}.v2.nodes.sfcompute.dev`; + } else { + // Fall back to v1 flow: resolve node name/ID to VM ID, then use v0 endpoint + let vmId: string; + + if (!nodeOrVmId.startsWith("vm_")) { + const client = await nodesClient(); + try { + const node = await client.nodes.get(nodeOrVmId); + if (!node?.current_vm) { + sshSpinner.fail( + `Node ${chalk.cyan( + nodeOrVmId, + )} does not have a current VM. VMs can take up to 5-10 minutes to spin up.`, + ); + process.exit(1); + } + vmId = node.current_vm.id; + } catch { + vmId = nodeOrVmId; } - - vmId = node.current_vm.id; - } catch { - spinner.info( - `No node found for name ${chalk.cyan( - nodeOrVmId, - )}. Interpreting as VM ID...`, - ); + } else { vmId = nodeOrVmId; } - } else { - vmId = nodeOrVmId; - } - const sshSpinner = ora("Fetching SSH information...").start(); - const baseUrl = await getApiUrl("vms_ssh_get"); - const params = new URLSearchParams(); - params.append("vm_id", vmId); - const url = `${baseUrl}?${params.toString()}`; - const response = await fetch(url, { - method: "GET", - headers: { - Authorization: `Bearer ${await getAuthToken()}`, - }, - }); + const client = await apiClient(token); + const { response, data: sshData } = await client.GET("/v0/vms/ssh", { + params: { query: { vm_id: vmId } }, + }); - if (!response.ok) { if (response.status === 401) { sshSpinner.stop(); logSessionTokenExpiredAndQuit(); } - sshSpinner.fail( - `Failed to retrieve SSH information for ${chalk.cyan( - vmId, - )}: ${response.statusText}`, - ); - process.exit(1); + if (!response.ok || !sshData) { + sshSpinner.fail( + `Failed to retrieve SSH information for ${chalk.cyan( + vmId, + )}: ${response.statusText}`, + ); + process.exit(1); + } + + data = sshData; + hostKeyAlias = `${vmId}.vms.sfcompute.dev`; } - const data = (await response.json()) as { - ssh_hostname: string; - ssh_port: number; - ssh_host_keys: - | { - key_type: string; - base64_encoded_key: string; - }[] - | undefined; - }; sshSpinner.succeed("SSH information fetched successfully."); if (options.json) { @@ -162,7 +168,7 @@ Examples: let knownHostsCommand = ["/usr/bin/env", "printf", "%s %s %s\\n"]; for (const sshHostKey of sshHostKeys) { knownHostsCommand = knownHostsCommand.concat([ - `${vmId}.vms.sfcompute.dev`, + hostKeyAlias, sshHostKey.key_type, sshHostKey.base64_encoded_key, ]); @@ -177,7 +183,7 @@ Examples: cmd = cmd.concat(["-o", `KnownHostsCommand=${knownHostsCommand_str}`]); } - cmd = cmd.concat(["-o", `HostKeyAlias=${vmId}.vms.sfcompute.dev`]); + cmd = cmd.concat(["-o", `HostKeyAlias=${hostKeyAlias}`]); cmd = cmd.concat([sshDestination]); let shescape: undefined | Shescape; From 0656e128e09605e65dc0fd4b2220e103046ccb11 Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Wed, 11 Feb 2026 19:47:10 -0800 Subject: [PATCH 2/4] refactor: derive `SshInfo` from OAPI schema Co-Authored-By: Claude Opus 4.6 --- src/lib/nodes/ssh.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/lib/nodes/ssh.ts b/src/lib/nodes/ssh.ts index 6659536..f308d79 100644 --- a/src/lib/nodes/ssh.ts +++ b/src/lib/nodes/ssh.ts @@ -13,16 +13,10 @@ import { logSessionTokenExpiredAndQuit, } from "../../helpers/errors.ts"; import { handleNodesError, nodesClient } from "../../nodesClient.ts"; +import type { components } from "../../schema.ts"; import { jsonOption } from "./utils.ts"; -type SshInfo = { - ssh_hostname: string; - ssh_port: number; - ssh_host_keys?: { - key_type: string; - base64_encoded_key: string; - }[]; -}; +type SshInfo = components["schemas"]["vmorch_GetSshResponse"]; const ssh = new Command("ssh") .description(`SSH into a VM on a node. From 955114963725f0fa05e0d3824c016cb3e83e363f Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Wed, 11 Feb 2026 19:54:25 -0800 Subject: [PATCH 3/4] perf: skip v2 call for vm_ prefixed IDs vm_ IDs are always legacy VMs, so go straight to the v0 endpoint to avoid an unnecessary round-trip to v2. Co-Authored-By: Claude Opus 4.6 --- src/lib/nodes/ssh.ts | 93 +++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/src/lib/nodes/ssh.ts b/src/lib/nodes/ssh.ts index f308d79..50a4503 100644 --- a/src/lib/nodes/ssh.ts +++ b/src/lib/nodes/ssh.ts @@ -77,23 +77,46 @@ Examples: let hostKeyAlias: string; let data: SshInfo; - // Try v2 endpoint first - const v2Response = await fetch( - `${config.api_url}/v2/nodes/${nodeOrVmId}/ssh`, - { - method: "GET", - headers: { Authorization: `Bearer ${token}` }, - }, - ); - - if (v2Response.ok) { - data = await v2Response.json(); - hostKeyAlias = `${nodeOrVmId}.v2.nodes.sfcompute.dev`; + if (nodeOrVmId.startsWith("vm_")) { + // vm_ prefix means this is a legacy VM ID; skip v2 and go straight to v0 + const client = await apiClient(token); + const { response, data: sshData } = await client.GET("/v0/vms/ssh", { + params: { query: { vm_id: nodeOrVmId } }, + }); + + if (response.status === 401) { + sshSpinner.stop(); + logSessionTokenExpiredAndQuit(); + } + + if (!response.ok || !sshData) { + sshSpinner.fail( + `Failed to retrieve SSH information for ${chalk.cyan( + nodeOrVmId, + )}: ${response.statusText}`, + ); + process.exit(1); + } + + data = sshData; + hostKeyAlias = `${nodeOrVmId}.vms.sfcompute.dev`; } else { - // Fall back to v1 flow: resolve node name/ID to VM ID, then use v0 endpoint - let vmId: string; + // Try v2 endpoint first + const v2Response = await fetch( + `${config.api_url}/v2/nodes/${nodeOrVmId}/ssh`, + { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }, + ); + + if (v2Response.ok) { + data = await v2Response.json(); + hostKeyAlias = `${nodeOrVmId}.v2.nodes.sfcompute.dev`; + } else { + // Fall back to v0 flow: resolve node name/ID to VM ID + let vmId: string; - if (!nodeOrVmId.startsWith("vm_")) { const client = await nodesClient(); try { const node = await client.nodes.get(nodeOrVmId); @@ -109,31 +132,29 @@ Examples: } catch { vmId = nodeOrVmId; } - } else { - vmId = nodeOrVmId; - } - const client = await apiClient(token); - const { response, data: sshData } = await client.GET("/v0/vms/ssh", { - params: { query: { vm_id: vmId } }, - }); + const apiCli = await apiClient(token); + const { response, data: sshData } = await apiCli.GET("/v0/vms/ssh", { + params: { query: { vm_id: vmId } }, + }); - if (response.status === 401) { - sshSpinner.stop(); - logSessionTokenExpiredAndQuit(); - } + if (response.status === 401) { + sshSpinner.stop(); + logSessionTokenExpiredAndQuit(); + } - if (!response.ok || !sshData) { - sshSpinner.fail( - `Failed to retrieve SSH information for ${chalk.cyan( - vmId, - )}: ${response.statusText}`, - ); - process.exit(1); - } + if (!response.ok || !sshData) { + sshSpinner.fail( + `Failed to retrieve SSH information for ${chalk.cyan( + vmId, + )}: ${response.statusText}`, + ); + process.exit(1); + } - data = sshData; - hostKeyAlias = `${vmId}.vms.sfcompute.dev`; + data = sshData; + hostKeyAlias = `${vmId}.vms.sfcompute.dev`; + } } sshSpinner.succeed("SSH information fetched successfully."); From 6d0c8957cd62830781debbb1bf29a96992731c27 Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Wed, 11 Feb 2026 19:55:54 -0800 Subject: [PATCH 4/4] refactor: deduplicate v0 fallback and vm_ prefix logic Co-Authored-By: Claude Opus 4.6 --- src/lib/nodes/ssh.ts | 81 +++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/src/lib/nodes/ssh.ts b/src/lib/nodes/ssh.ts index 50a4503..773fe53 100644 --- a/src/lib/nodes/ssh.ts +++ b/src/lib/nodes/ssh.ts @@ -74,34 +74,11 @@ Examples: const config = await loadConfig(); const token = await getAuthToken(); - let hostKeyAlias: string; - let data: SshInfo; + let hostKeyAlias = ""; + let data: SshInfo | undefined; - if (nodeOrVmId.startsWith("vm_")) { - // vm_ prefix means this is a legacy VM ID; skip v2 and go straight to v0 - const client = await apiClient(token); - const { response, data: sshData } = await client.GET("/v0/vms/ssh", { - params: { query: { vm_id: nodeOrVmId } }, - }); - - if (response.status === 401) { - sshSpinner.stop(); - logSessionTokenExpiredAndQuit(); - } - - if (!response.ok || !sshData) { - sshSpinner.fail( - `Failed to retrieve SSH information for ${chalk.cyan( - nodeOrVmId, - )}: ${response.statusText}`, - ); - process.exit(1); - } - - data = sshData; - hostKeyAlias = `${nodeOrVmId}.vms.sfcompute.dev`; - } else { - // Try v2 endpoint first + // Try v2 endpoint for non-vm_ IDs + if (!nodeOrVmId.startsWith("vm_")) { const v2Response = await fetch( `${config.api_url}/v2/nodes/${nodeOrVmId}/ssh`, { @@ -113,10 +90,14 @@ Examples: if (v2Response.ok) { data = await v2Response.json(); hostKeyAlias = `${nodeOrVmId}.v2.nodes.sfcompute.dev`; - } else { - // Fall back to v0 flow: resolve node name/ID to VM ID - let vmId: string; + } + } + // Fall back to v0 flow if v2 didn't resolve + if (!data) { + let vmId: string; + + if (!nodeOrVmId.startsWith("vm_")) { const client = await nodesClient(); try { const node = await client.nodes.get(nodeOrVmId); @@ -132,29 +113,31 @@ Examples: } catch { vmId = nodeOrVmId; } + } else { + vmId = nodeOrVmId; + } - const apiCli = await apiClient(token); - const { response, data: sshData } = await apiCli.GET("/v0/vms/ssh", { - params: { query: { vm_id: vmId } }, - }); - - if (response.status === 401) { - sshSpinner.stop(); - logSessionTokenExpiredAndQuit(); - } + const client = await apiClient(token); + const { response, data: sshData } = await client.GET("/v0/vms/ssh", { + params: { query: { vm_id: vmId } }, + }); - if (!response.ok || !sshData) { - sshSpinner.fail( - `Failed to retrieve SSH information for ${chalk.cyan( - vmId, - )}: ${response.statusText}`, - ); - process.exit(1); - } + if (response.status === 401) { + sshSpinner.stop(); + logSessionTokenExpiredAndQuit(); + } - data = sshData; - hostKeyAlias = `${vmId}.vms.sfcompute.dev`; + if (!response.ok || !sshData) { + sshSpinner.fail( + `Failed to retrieve SSH information for ${chalk.cyan( + vmId, + )}: ${response.statusText}`, + ); + process.exit(1); } + + data = sshData; + hostKeyAlias = `${vmId}.vms.sfcompute.dev`; } sshSpinner.succeed("SSH information fetched successfully.");