Skip to content

Commit e35a88c

Browse files
refactor(images): expose factory commands and migrate to apiClient
Convert the get/list/upload subcommands into createGet/createList/ createUpload factories and add a top-level createImagesCommand so the same images CLI surface can be reused under sf nodes images and sf vm images. Swap the remaining nodesClient and raw fetch calls for the typed apiClient against preview/v2, and regenerate schema types (adds revoked upload_status, etc.).
1 parent 6e5bd5e commit e35a88c

6 files changed

Lines changed: 14980 additions & 9744 deletions

File tree

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/images/get.tsx

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,27 @@ import timezone from "dayjs/plugin/timezone";
77
import utc from "dayjs/plugin/utc";
88
import { Box, render, Text } from "ink";
99
import Link from "ink-link";
10-
import { getAuthToken, loadConfig } from "../../helpers/config.ts";
10+
import { apiClient } from "../../apiClient.ts";
11+
import { logAndQuit } from "../../helpers/errors.ts";
1112
import { formatDate } from "../../helpers/format-time.ts";
12-
import { handleNodesError, nodesClient } from "../../nodesClient.ts";
13+
import type { components } from "../../schema.ts";
1314
import { Row } from "../Row.tsx";
1415

1516
dayjs.extend(utc);
1617
dayjs.extend(advanced);
1718
dayjs.extend(timezone);
1819

20+
type Image = components["schemas"]["sfc-api_ImageListEntry"];
21+
type Download = components["schemas"]["sfc-api_ImageDownloadResponse"];
22+
1923
function ImageDisplay({
2024
image,
2125
download,
2226
}: {
23-
image: {
24-
name: string;
25-
id: string;
26-
upload_status: string;
27-
sha256: string | null;
28-
};
29-
download: { url: string; expires_at: number } | null;
27+
image: Image;
28+
download: Download | null;
3029
}) {
31-
const expiresAt = download?.expires_at
32-
? new Date(download.expires_at * 1000)
33-
: null;
30+
const expiresAt = download ? new Date(download.expires_at * 1000) : null;
3431
const isExpired = expiresAt ? expiresAt < new Date() : false;
3532

3633
return (
@@ -92,36 +89,40 @@ function formatStatusInk(status: string): React.ReactElement {
9289
return <Text color="cyan">Completed</Text>;
9390
case "failed":
9491
return <Text color="red">Failed</Text>;
92+
case "revoked":
93+
return <Text color="red">Revoked</Text>;
9594
default:
9695
return <Text dimColor>Unknown</Text>;
9796
}
9897
}
9998

100-
const get = new Command("get")
101-
.description("Get image details and download URL")
102-
.argument("<id>", "Image ID or name")
103-
.option("--json", "Output JSON")
104-
.action(async (id, opts) => {
105-
try {
106-
const client = await nodesClient();
107-
const image = await client.vms.images.get(id);
99+
export function createGet() {
100+
return new Command("get")
101+
.alias("show")
102+
.description("Get image details and download URL")
103+
.argument("<id>", "Image ID or name")
104+
.option("--json", "Output JSON")
105+
.action(async (id, opts) => {
106+
const client = await apiClient();
107+
108+
const { data: image, response } = await client.GET(
109+
"/preview/v2/images/{id}",
110+
{ params: { path: { id } } },
111+
);
112+
if (!response.ok || !image) {
113+
logAndQuit(
114+
`Failed to get image: ${response.status} ${response.statusText}`,
115+
);
116+
}
108117

109-
// Fetch download URL if image is completed
110-
let download: { url: string; expires_at: number } | null = null;
118+
let download: Download | null = null;
111119
if (image.upload_status === "completed") {
112-
const config = await loadConfig();
113-
const token = await getAuthToken();
114-
const downloadResponse = await fetch(
115-
`${config.api_url}/preview/v2/images/${encodeURIComponent(id)}/download`,
116-
{
117-
headers: { Authorization: `Bearer ${token}` },
118-
},
120+
const { data: downloadData } = await client.GET(
121+
"/preview/v2/images/{id}/download",
122+
{ params: { path: { id } } },
119123
);
120-
if (downloadResponse.ok) {
121-
download = (await downloadResponse.json()) as {
122-
url: string;
123-
expires_at: number;
124-
};
124+
if (downloadData) {
125+
download = downloadData;
125126
}
126127
}
127128

@@ -130,15 +131,6 @@ const get = new Command("get")
130131
return;
131132
}
132133

133-
render(
134-
<ImageDisplay
135-
image={{ ...image, sha256: image.sha256 ?? null }}
136-
download={download}
137-
/>,
138-
);
139-
} catch (err) {
140-
handleNodesError(err);
141-
}
142-
});
143-
144-
export default get;
134+
render(<ImageDisplay image={image} download={download} />);
135+
});
136+
}

src/lib/images/index.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import type { Command } from "@commander-js/extra-typings";
2-
import get from "./get.tsx";
3-
import list from "./list.ts";
4-
import upload from "./upload.ts";
1+
import { Command } from "@commander-js/extra-typings";
2+
import { createGet } from "./get.tsx";
3+
import { createList } from "./list.ts";
4+
import { createUpload } from "./upload.ts";
55

6-
export function registerImages(program: Command) {
7-
const images = program
8-
.command("images")
6+
export function createImagesCommand() {
7+
const images = new Command("images")
98
.alias("image")
109
.description("Manage images")
1110
.showHelpAfterError()
@@ -23,10 +22,15 @@ Examples:\n
2322
$ sf images get <image-id>
2423
`,
2524
)
26-
.addCommand(list)
27-
.addCommand(upload)
28-
.addCommand(get)
25+
.addCommand(createList())
26+
.addCommand(createUpload())
27+
.addCommand(createGet())
2928
.action(() => {
3029
images.help();
3130
});
31+
return images;
32+
}
33+
34+
export function registerImages(program: Command) {
35+
program.addCommand(createImagesCommand());
3236
}

src/lib/images/list.ts

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ import { Command } from "@commander-js/extra-typings";
33
import chalk from "chalk";
44
import Table from "cli-table3";
55
import ora from "ora";
6+
import { apiClient } from "../../apiClient.ts";
7+
import { logAndQuit } from "../../helpers/errors.ts";
68
import { formatDate } from "../../helpers/format-time.ts";
7-
import { handleNodesError, nodesClient } from "../../nodesClient.ts";
89
import { getDefaultWorkspace } from "./utils.ts";
910

10-
const list = new Command("list")
11-
.alias("ls")
12-
.description("List images")
13-
.showHelpAfterError()
14-
.option("--json", "Output in JSON format")
15-
.addHelpText(
16-
"after",
17-
`
11+
export function createList() {
12+
return new Command("list")
13+
.alias("ls")
14+
.description("List images")
15+
.showHelpAfterError()
16+
.option("--json", "Output in JSON format")
17+
.addHelpText(
18+
"after",
19+
`
1820
Examples:\n
1921
\x1b[2m# List all images\x1b[0m
2022
$ sf images list
@@ -25,22 +27,29 @@ Examples:\n
2527
\x1b[2m# List images in JSON format\x1b[0m
2628
$ sf images list --json
2729
`,
28-
)
29-
.action(async (options) => {
30-
try {
31-
const client = await nodesClient();
30+
)
31+
.action(async (options) => {
32+
const client = await apiClient();
3233
const workspace = await getDefaultWorkspace();
3334

3435
const spinner = ora("Fetching images...").start();
35-
const result = await client.vms.images.list({ workspace });
36+
const { data, response } = await client.GET("/preview/v2/images", {
37+
params: { query: { workspace } },
38+
});
3639
spinner.stop();
3740

41+
if (!response.ok || !data) {
42+
logAndQuit(
43+
`Failed to list images: ${response.status} ${response.statusText}`,
44+
);
45+
}
46+
3847
if (options.json) {
39-
console.log(JSON.stringify(result, null, 2));
48+
console.log(JSON.stringify(data, null, 2));
4049
return;
4150
}
4251

43-
const images = result.data;
52+
const images = data.data;
4453

4554
if (images.length === 0) {
4655
console.log("No images found.");
@@ -49,10 +58,9 @@ Examples:\n
4958
return;
5059
}
5160

52-
// Sort images by created_at (newest first)
53-
const sortedImages = [...images].sort((a, b) => {
54-
return (b.created_at || 0) - (a.created_at || 0);
55-
});
61+
const sortedImages = [...images].sort(
62+
(a, b) => (b.created_at || 0) - (a.created_at || 0),
63+
);
5664
const imagesToShow = sortedImages.slice(0, 5);
5765

5866
const table = new Table({
@@ -62,20 +70,19 @@ Examples:\n
6270
chalk.cyan("STATUS"),
6371
chalk.cyan("CREATED"),
6472
],
65-
style: {
66-
head: [],
67-
border: ["gray"],
68-
},
73+
style: { head: [], border: ["gray"] },
6974
});
7075

7176
for (const image of imagesToShow) {
7277
const createdAt = image.created_at
7378
? formatDate(new Date(image.created_at * 1000))
7479
: "Unknown";
75-
76-
const status = formatStatus(image.upload_status);
77-
78-
table.push([image.name, image.id, status, createdAt]);
80+
table.push([
81+
image.name,
82+
image.id,
83+
formatStatus(image.upload_status),
84+
createdAt,
85+
]);
7986
}
8087

8188
if (images.length > 5) {
@@ -98,10 +105,18 @@ Examples:\n
98105
if (firstImage) {
99106
console.log(` sf images get ${chalk.cyan(firstImage.id)}`);
100107
}
101-
} catch (err) {
102-
handleNodesError(err);
103-
}
104-
});
108+
const firstCompletedImage = sortedImages.find(
109+
(image) => image.upload_status === "completed",
110+
);
111+
if (firstCompletedImage) {
112+
console.log(
113+
` sf nodes create -z hayesvalley -d 2h -p 13.50 --image ${chalk.cyan(
114+
firstCompletedImage.id,
115+
)}`,
116+
);
117+
}
118+
});
119+
}
105120

106121
function formatStatus(status: string): string {
107122
switch (status) {
@@ -113,9 +128,9 @@ function formatStatus(status: string): string {
113128
return chalk.cyan("Completed");
114129
case "failed":
115130
return chalk.red("Failed");
131+
case "revoked":
132+
return chalk.red("Revoked");
116133
default:
117134
return chalk.gray("Unknown");
118135
}
119136
}
120-
121-
export default list;

0 commit comments

Comments
 (0)