11import console from "node:console" ;
22import { Command } from "@commander-js/extra-typings" ;
3- import type SFCNodes from "@sfcompute/nodes-sdk-alpha" ;
43import chalk from "chalk" ;
54import dayjs from "dayjs" ;
65import advanced from "dayjs/plugin/advancedFormat" ;
76import timezone from "dayjs/plugin/timezone" ;
87import utc from "dayjs/plugin/utc" ;
98import { Box , render , Text } from "ink" ;
109import Link from "ink-link" ;
10+ import { apiClient } from "../../../apiClient.ts" ;
11+ import { logAndQuit } from "../../../helpers/errors.ts" ;
1112import { formatDate } from "../../../helpers/format-time.ts" ;
12- import { handleNodesError , nodesClient } from "../../../nodesClient.ts" ;
1313import { Row } from "../../Row.tsx" ;
1414
1515dayjs . extend ( utc ) ;
@@ -18,20 +18,25 @@ dayjs.extend(timezone);
1818
1919export function ImageDisplay ( {
2020 image,
21+ download,
2122} : {
22- image : SFCNodes . VMs . ImageGetResponse ;
23+ image : {
24+ name : string ;
25+ id : string ;
26+ upload_status : string ;
27+ } ;
28+ download : { url : string ; expires_at : number } | null ;
2329} ) {
24- const expiresAt = image . expires_at ? new Date ( image . expires_at ) : null ;
30+ const expiresAt = download ?. expires_at
31+ ? new Date ( download . expires_at * 1000 )
32+ : null ;
2533 const isExpired = expiresAt ? expiresAt < new Date ( ) : false ;
2634
27- const statusColor = isExpired ? "red" : "green" ;
28- const statusText = isExpired ? "Expired" : "Ready" ;
29-
3035 return (
3136 < Box flexDirection = "column" padding = { 0 } width = { 80 } >
3237 < Box borderStyle = "single" borderColor = "cyan" paddingX = { 1 } >
3338 < Text color = "cyan" bold >
34- Image: { image . name } ({ image . image_id } )
39+ Image: { image . name } ({ image . id } )
3540 </ Text >
3641 </ Box >
3742
@@ -40,62 +45,113 @@ export function ImageDisplay({
4045 head = "Status: "
4146 value = {
4247 < Box gap = { 1 } >
43- < Text color = { statusColor } > { statusText } </ Text >
48+ < Text color = { formatStatusColor ( image . upload_status ) } >
49+ { formatStatusText ( image . upload_status ) }
50+ </ Text >
4451 </ Box >
4552 }
4653 />
47- < Row
48- head = "URL: "
49- value = {
50- < Box flexDirection = "column" paddingRight = { 1 } >
51- < Text color = "cyan" > Use curl or wget to download.</ Text >
52- < Link url = { image . download_url } fallback = { false } >
53- { image . download_url }
54- </ Link >
55- </ Box >
56- }
57- />
58- { expiresAt && (
59- < Row
60- head = "URL Expiry: "
61- value = {
62- < Box gap = { 1 } >
63- < Text color = { isExpired ? "red" : undefined } >
64- { expiresAt . toISOString ( ) } { " " }
65- { chalk . blackBright (
66- `(${ formatDate ( dayjs ( expiresAt ) . toDate ( ) ) } ${ dayjs (
67- expiresAt ,
68- ) . format ( "z" ) } )`,
69- ) }
70- </ Text >
71- { isExpired && < Text dimColor > (Expired)</ Text > }
72- </ Box >
73- }
74- />
54+ { download && (
55+ < >
56+ < Row
57+ head = "URL: "
58+ value = {
59+ < Box flexDirection = "column" paddingRight = { 1 } >
60+ < Text color = "cyan" > Use curl or wget to download.</ Text >
61+ < Link url = { download . url } fallback = { false } >
62+ { download . url }
63+ </ Link >
64+ </ Box >
65+ }
66+ />
67+ { expiresAt && (
68+ < Row
69+ head = "URL Expiry: "
70+ value = {
71+ < Box gap = { 1 } >
72+ < Text color = { isExpired ? "red" : undefined } >
73+ { expiresAt . toISOString ( ) } { " " }
74+ { chalk . blackBright (
75+ `(${ formatDate ( dayjs ( expiresAt ) . toDate ( ) ) } ${ dayjs (
76+ expiresAt ,
77+ ) . format ( "z" ) } )`,
78+ ) }
79+ </ Text >
80+ { isExpired && < Text dimColor > (Expired)</ Text > }
81+ </ Box >
82+ }
83+ />
84+ ) }
85+ </ >
7586 ) }
7687 </ Box >
7788 </ Box >
7889 ) ;
7990}
8091
92+ function formatStatusColor ( status : string ) : string {
93+ switch ( status ) {
94+ case "started" :
95+ return "green" ;
96+ case "uploading" :
97+ return "yellow" ;
98+ case "completed" :
99+ return "cyan" ;
100+ case "failed" :
101+ return "red" ;
102+ default :
103+ return "gray" ;
104+ }
105+ }
106+
107+ function formatStatusText ( status : string ) : string {
108+ switch ( status ) {
109+ case "started" :
110+ return "Started" ;
111+ case "uploading" :
112+ return "Uploading" ;
113+ case "completed" :
114+ return "Completed" ;
115+ case "failed" :
116+ return "Failed" ;
117+ default :
118+ return "Unknown" ;
119+ }
120+ }
121+
81122const show = new Command ( "show" )
82123 . description ( "Show VM image details and download URL" )
83124 . argument ( "<image-id>" , "ID of the image" )
84125 . option ( "--json" , "Output JSON" )
85126 . action ( async ( imageId , opts ) => {
86- try {
87- const client = await nodesClient ( ) ;
88- const data = await client . vms . images . get ( imageId ) ;
127+ const client = await apiClient ( ) ;
89128
90- if ( opts . json ) {
91- console . log ( JSON . stringify ( data , null , 2 ) ) ;
92- return ;
129+ const { data : image , response } = await client . GET ( "/v2/images/{id}" , {
130+ params : { path : { id : imageId } } ,
131+ } ) ;
132+ if ( ! response . ok || ! image ) {
133+ logAndQuit (
134+ `Failed to get image: ${ response . status } ${ response . statusText } ` ,
135+ ) ;
136+ }
137+
138+ let download = null ;
139+ if ( image . upload_status === "completed" ) {
140+ const { data : downloadData , response : downloadResponse } =
141+ await client . GET ( "/v2/images/{id}/download" , {
142+ params : { path : { id : imageId } } ,
143+ } ) ;
144+ if ( downloadResponse . ok && downloadData ) {
145+ download = downloadData ;
93146 }
147+ }
94148
95- render ( < ImageDisplay image = { data } /> ) ;
96- } catch ( err ) {
97- handleNodesError ( err ) ;
149+ if ( opts . json ) {
150+ console . log ( JSON . stringify ( { ... image , download } , null , 2 ) ) ;
151+ return ;
98152 }
153+
154+ render ( < ImageDisplay image = { image } download = { download } /> ) ;
99155 } ) ;
100156
101157export default show ;
0 commit comments