Skip to content

Commit df7178d

Browse files
feat: [PR-1697] sf nodes ssh supports v2/nodes (#249)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a1a7064 commit df7178d

1 file changed

Lines changed: 63 additions & 59 deletions

File tree

src/lib/nodes/ssh.ts

Lines changed: 63 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import chalk from "chalk";
66
import ora from "ora";
77
import { Shescape } from "shescape";
88

9-
import { getAuthToken } from "../../helpers/config.ts";
9+
import { apiClient } from "../../apiClient.ts";
10+
import { getAuthToken, loadConfig } from "../../helpers/config.ts";
1011
import {
1112
logAndQuit,
1213
logSessionTokenExpiredAndQuit,
1314
} from "../../helpers/errors.ts";
14-
import { getApiUrl } from "../../helpers/urls.ts";
1515
import { handleNodesError, nodesClient } from "../../nodesClient.ts";
16+
import type { components } from "../../schema.ts";
1617
import { jsonOption } from "./utils.ts";
1718

19+
type SshInfo = components["schemas"]["vmorch_GetSshResponse"];
20+
1821
const ssh = new Command("ssh")
1922
.description(`SSH into a VM on a node.
2023
@@ -46,7 +49,7 @@ Examples:
4649
4750
\x1b[2m# SSH with a specific username\x1b[0m
4851
$ sf nodes ssh jenson@my-node
49-
52+
5053
\x1b[2m# SSH directly to a VM ID\x1b[0m
5154
$ sf nodes ssh root@vm_xxxxxxxxxxxxxxxxxxxxx
5255
`,
@@ -67,75 +70,76 @@ Examples:
6770
logAndQuit(`Invalid SSH destination string: ${destination}`);
6871
}
6972

70-
let vmId: string;
73+
const sshSpinner = ora("Fetching SSH information...").start();
74+
const config = await loadConfig();
75+
const token = await getAuthToken();
76+
77+
let hostKeyAlias = "";
78+
let data: SshInfo | undefined;
7179

72-
// If the ID doesn't start with vm_, assume it's a node name/ID
80+
// Try v2 endpoint for non-vm_ IDs
7381
if (!nodeOrVmId.startsWith("vm_")) {
74-
const client = await nodesClient();
75-
const spinner = ora("Fetching node information...").start();
82+
const v2Response = await fetch(
83+
`${config.api_url}/v2/nodes/${nodeOrVmId}/ssh`,
84+
{
85+
method: "GET",
86+
headers: { Authorization: `Bearer ${token}` },
87+
},
88+
);
7689

77-
try {
78-
const node = await client.nodes.get(nodeOrVmId);
79-
spinner.succeed(`Node found for name ${chalk.cyan(nodeOrVmId)}.`);
80-
81-
if (!node?.current_vm) {
82-
spinner.fail(
83-
`Node ${chalk.cyan(
84-
nodeOrVmId,
85-
)} does not have a current VM. VMs can take up to 5-10 minutes to spin up.`,
86-
);
87-
process.exit(1);
88-
}
90+
if (v2Response.ok) {
91+
data = await v2Response.json();
92+
hostKeyAlias = `${nodeOrVmId}.v2.nodes.sfcompute.dev`;
93+
}
94+
}
8995

90-
vmId = node.current_vm.id;
91-
} catch {
92-
spinner.info(
93-
`No node found for name ${chalk.cyan(
94-
nodeOrVmId,
95-
)}. Interpreting as VM ID...`,
96-
);
96+
// Fall back to v0 flow if v2 didn't resolve
97+
if (!data) {
98+
let vmId: string;
99+
100+
if (!nodeOrVmId.startsWith("vm_")) {
101+
const client = await nodesClient();
102+
try {
103+
const node = await client.nodes.get(nodeOrVmId);
104+
if (!node?.current_vm) {
105+
sshSpinner.fail(
106+
`Node ${chalk.cyan(
107+
nodeOrVmId,
108+
)} does not have a current VM. VMs can take up to 5-10 minutes to spin up.`,
109+
);
110+
process.exit(1);
111+
}
112+
vmId = node.current_vm.id;
113+
} catch {
114+
vmId = nodeOrVmId;
115+
}
116+
} else {
97117
vmId = nodeOrVmId;
98118
}
99-
} else {
100-
vmId = nodeOrVmId;
101-
}
102119

103-
const sshSpinner = ora("Fetching SSH information...").start();
104-
const baseUrl = await getApiUrl("vms_ssh_get");
105-
const params = new URLSearchParams();
106-
params.append("vm_id", vmId);
107-
const url = `${baseUrl}?${params.toString()}`;
108-
const response = await fetch(url, {
109-
method: "GET",
110-
headers: {
111-
Authorization: `Bearer ${await getAuthToken()}`,
112-
},
113-
});
120+
const client = await apiClient(token);
121+
const { response, data: sshData } = await client.GET("/v0/vms/ssh", {
122+
params: { query: { vm_id: vmId } },
123+
});
114124

115-
if (!response.ok) {
116125
if (response.status === 401) {
117126
sshSpinner.stop();
118127
logSessionTokenExpiredAndQuit();
119128
}
120129

121-
sshSpinner.fail(
122-
`Failed to retrieve SSH information for ${chalk.cyan(
123-
vmId,
124-
)}: ${response.statusText}`,
125-
);
126-
process.exit(1);
130+
if (!response.ok || !sshData) {
131+
sshSpinner.fail(
132+
`Failed to retrieve SSH information for ${chalk.cyan(
133+
vmId,
134+
)}: ${response.statusText}`,
135+
);
136+
process.exit(1);
137+
}
138+
139+
data = sshData;
140+
hostKeyAlias = `${vmId}.vms.sfcompute.dev`;
127141
}
128142

129-
const data = (await response.json()) as {
130-
ssh_hostname: string;
131-
ssh_port: number;
132-
ssh_host_keys:
133-
| {
134-
key_type: string;
135-
base64_encoded_key: string;
136-
}[]
137-
| undefined;
138-
};
139143
sshSpinner.succeed("SSH information fetched successfully.");
140144

141145
if (options.json) {
@@ -162,7 +166,7 @@ Examples:
162166
let knownHostsCommand = ["/usr/bin/env", "printf", "%s %s %s\\n"];
163167
for (const sshHostKey of sshHostKeys) {
164168
knownHostsCommand = knownHostsCommand.concat([
165-
`${vmId}.vms.sfcompute.dev`,
169+
hostKeyAlias,
166170
sshHostKey.key_type,
167171
sshHostKey.base64_encoded_key,
168172
]);
@@ -177,7 +181,7 @@ Examples:
177181
cmd = cmd.concat(["-o", `KnownHostsCommand=${knownHostsCommand_str}`]);
178182
}
179183

180-
cmd = cmd.concat(["-o", `HostKeyAlias=${vmId}.vms.sfcompute.dev`]);
184+
cmd = cmd.concat(["-o", `HostKeyAlias=${hostKeyAlias}`]);
181185
cmd = cmd.concat([sshDestination]);
182186

183187
let shescape: undefined | Shescape;

0 commit comments

Comments
 (0)