diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6aaaee7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "singleQuote": false, + "jsxSingleQuote": false, + "trailingComma": "es5", + "tabWidth": 2, + "printWidth": 80, + "arrowParens": "avoid", + "endOfLine": "lf", + "plugins": [] +} diff --git a/README.md b/README.md index 93d78e8..e72f71d 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,26 @@ Then, you can run the cli: ```bash sf --version # 0.1.0 ``` + +## Local Development / Contributing + +### Setup + +- Install [Deno](https://docs.deno.com/runtime/) +- Install dependencies `deno install` + - Use same mental model as `npm install` +- Auth your CLI with `deno run prod login` + +### Development Loop + +- Make code changes +- Test changes with + - `deno run devv` to test against local API + - `deno run prod` to test against production API + - The `deno run ` is an alias to the user facing `sf` command. So if you wanted to run `sf login` locally against the local API, run `deno run devv login` + +## New Release + +This is ran locally + +- `deno run release ` diff --git a/deno.lock b/deno.lock index 3856096..0f8c210 100644 --- a/deno.lock +++ b/deno.lock @@ -26,6 +26,7 @@ "npm:openapi-fetch@~0.11.1": "0.11.3", "npm:ora@^8.1.0": "8.1.1", "npm:parse-duration@^1.1.0": "1.1.0", + "npm:prettier@^3.4.2": "3.4.2", "npm:react@^18.3.1": "18.3.1", "npm:semver@^7.6.3": "7.6.3", "npm:tiny-invariant@^1.3.3": "1.3.3", @@ -751,6 +752,9 @@ "patch-console@2.0.0": { "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==" }, + "prettier@3.4.2": { + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==" + }, "prop-types@15.8.1": { "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dependencies": [ @@ -1071,6 +1075,7 @@ "npm:openapi-fetch@~0.11.1", "npm:ora@^8.1.0", "npm:parse-duration@^1.1.0", + "npm:prettier@^3.4.2", "npm:react@^18.3.1", "npm:semver@^7.6.3", "npm:tiny-invariant@^1.3.3", diff --git a/package.json b/package.json index 41cbd88..9a4a85c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "devv": "IS_DEVELOPMENT_CLI_ENV=true deno run --allow-all src/index.ts", "release": "deno run --allow-all src/scripts/release.ts", "prod": "deno run --allow-all src/index.ts", - "schema": "npx openapi-typescript https://api.sfcompute.com/docs/json -o src/schema.ts" + "schema": "npx openapi-typescript https://api.sfcompute.com/docs/json -o src/schema.ts", + "lint": "prettier --write ." }, "dependencies": { "@inquirer/prompts": "^5.1.2", @@ -26,6 +27,7 @@ "openapi-fetch": "^0.11.1", "ora": "^8.1.0", "parse-duration": "^1.1.0", + "prettier": "^3.4.2", "react": "^18.3.1", "semver": "^7.6.3", "tiny-invariant": "^1.3.3", @@ -40,5 +42,5 @@ "peerDependencies": { "typescript": "^5.6.2" }, - "version": "0.1.10" + "version": "0.1.13" } \ No newline at end of file diff --git a/src/checkVersion.ts b/src/checkVersion.ts index f89ebcf..2fc7475 100644 --- a/src/checkVersion.ts +++ b/src/checkVersion.ts @@ -6,7 +6,7 @@ import semver from "semver"; async function checkProductionCLIVersion() { try { const response = await fetch( - "https://raw.githubusercontent.com/sfcompute/cli/refs/heads/main/package.json", + "https://raw.githubusercontent.com/sfcompute/cli/refs/heads/main/package.json" ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -39,7 +39,7 @@ Run 'sf upgrade' to update to the latest version padding: 1, borderColor: "yellow", borderStyle: "round", - }), + }) ); } } diff --git a/src/helpers/config.ts b/src/helpers/config.ts index a0b5d61..f5732cd 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -26,7 +26,7 @@ const ConfigDefaults = process.env.IS_DEVELOPMENT_CLI_ENV // -- export async function saveConfig( - config: Partial, + config: Partial ): Promise<{ success: boolean }> { const configPath = getConfigPath(); const configDir = join(homedir(), ".sfcompute"); diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 814ca12..91e1f83 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -20,7 +20,7 @@ export async function logSessionTokenExpiredAndQuit(): Promise { export function failedToConnect(): never { logAndQuit( - "Failed to connect to the server. Please check your internet connection and try again.", + "Failed to connect to the server. Please check your internet connection and try again." ); } diff --git a/src/helpers/price.ts b/src/helpers/price.ts index 9052354..602beb9 100644 --- a/src/helpers/price.ts +++ b/src/helpers/price.ts @@ -4,7 +4,7 @@ export function pricePerGPUHourToTotalPriceCents( pricePerGPUHourCents: Cents, durationSeconds: number, nodes: number, - gpusPerNode: number, + gpusPerNode: number ): Cents { const totalGPUs = nodes * gpusPerNode; const totalHours = durationSeconds / 3600; @@ -16,7 +16,7 @@ export function totalPriceToPricePerGPUHour( priceCents: number, durationSeconds: number, nodes: number, - gpusPerNode: number, + gpusPerNode: number ): Cents { const totalGPUs = nodes * gpusPerNode; const totalHours = durationSeconds / 3600; diff --git a/src/helpers/units.ts b/src/helpers/units.ts index 2bfff7b..fe2646f 100644 --- a/src/helpers/units.ts +++ b/src/helpers/units.ts @@ -29,11 +29,10 @@ export function roundStartDate(startDate: Date): Date { export function computeApproximateDurationSeconds( startDate: Date | "NOW", - endDate: Date, + endDate: Date ): number { - const startEpoch = startDate === "NOW" - ? currentEpoch() - : dateToEpoch(startDate); + const startEpoch = + startDate === "NOW" ? currentEpoch() : dateToEpoch(startDate); const endEpoch = dateToEpoch(endDate); return dayjs(epochToDate(endEpoch)).diff(dayjs(epochToDate(startEpoch)), "s"); } @@ -58,7 +57,7 @@ interface PriceWholeToCentsReturn { invalid: boolean; } export function priceWholeToCents( - price: string | number, + price: string | number ): PriceWholeToCentsReturn { if ( price === null || diff --git a/src/helpers/urls.ts b/src/helpers/urls.ts index 2a3c887..1b9aca1 100644 --- a/src/helpers/urls.ts +++ b/src/helpers/urls.ts @@ -38,7 +38,7 @@ const apiPaths = { export async function getWebAppUrl( key: keyof typeof webPaths, - params?: any, + params?: any ): Promise { const config = await loadConfig(); const path = webPaths[key]; @@ -51,7 +51,7 @@ export async function getWebAppUrl( export async function getApiUrl( key: keyof typeof apiPaths, - params?: any, + params?: any ): Promise { const config = await loadConfig(); const path = apiPaths[key]; diff --git a/src/helpers/waitingForOrder.ts b/src/helpers/waitingForOrder.ts index 3ae85c8..4b2a73f 100644 --- a/src/helpers/waitingForOrder.ts +++ b/src/helpers/waitingForOrder.ts @@ -5,7 +5,7 @@ import { getOrder } from "./fetchers.ts"; export async function waitForOrderToNotBePending(orderId: string) { const spinner = ora( - `Order ${orderId} - pending (this can take a moment)`, + `Order ${orderId} - pending (this can take a moment)` ).start(); // 1 minute @@ -18,7 +18,7 @@ export async function waitForOrderToNotBePending(orderId: string) { spinner.succeed(); return order; } - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise(resolve => setTimeout(resolve, 500)); } spinner.fail(); diff --git a/src/lib/ConfirmInput.tsx b/src/lib/ConfirmInput.tsx index 7ec534a..2f2c7e4 100644 --- a/src/lib/ConfirmInput.tsx +++ b/src/lib/ConfirmInput.tsx @@ -22,9 +22,12 @@ const ConfirmInput: React.FC = ({ value = "", ...props }) => { - const handleSubmit = useCallback((newValue: string) => { - onSubmit(yn(newValue, { default: isChecked })); - }, [isChecked, onSubmit]); + const handleSubmit = useCallback( + (newValue: string) => { + onSubmit(yn(newValue, { default: isChecked })); + }, + [isChecked, onSubmit] + ); return ( { + .action(async options => { const { available: { whole: availableWhole, cents: availableCents }, reserved: { whole: reservedWhole, cents: reservedCents }, @@ -57,7 +57,7 @@ export function registerBalance(program: Command) { "Reserved", chalk.gray(formattedReserved), chalk.gray(reservedCents.toLocaleString()), - ], + ] ); console.log(table.toString() + "\n"); @@ -98,7 +98,7 @@ export async function getBalance(): Promise { if (!data) { return logAndQuit( - `Failed to get balance: Unexpected response from server: ${response}`, + `Failed to get balance: Unexpected response from server: ${response}` ); } diff --git a/src/lib/buy/index.tsx b/src/lib/buy/index.tsx index 47bd458..db40fb6 100644 --- a/src/lib/buy/index.tsx +++ b/src/lib/buy/index.tsx @@ -47,14 +47,14 @@ export function registerBuy(program: Command) { .option("-p, --price ", "The price in dollars, per GPU hour") .option( "-s, --start ", - "Specify the start date. Can be a date, relative time like '+1d', or the string 'NOW'", + "Specify the start date. Can be a date, relative time like '+1d', or the string 'NOW'" ) .option("-y, --yes", "Automatically confirm the order") .option( "-colo, --colocate ", "Colocate with existing contracts", - (value) => value.split(","), - [], + value => value.split(","), + [] ) .option("--quote", "Only provide a quote for the order") .action(buyOrderAction); @@ -117,11 +117,7 @@ function parsePricePerGpuHour(price?: string) { return Number.parseFloat(priceWithoutDollar) * 100; } -function QuoteComponent( - props: { - options: SfBuyOptions; - }, -) { +function QuoteComponent(props: { options: SfBuyOptions }) { const [quote, setQuote] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -136,16 +132,16 @@ function QuoteComponent( })(); }, [props.options]); - return isLoading - ? ( + return isLoading ? ( + + - - - Getting quote... - + Getting quote... - ) - : ; + + ) : ( + + ); } /* @@ -163,7 +159,7 @@ async function buyOrderAction(options: SfBuyOptions) { const nodes = parseAccelerators(options.accelerators); if (!Number.isInteger(nodes)) { return logAndQuit( - `You can only buy whole nodes, or 8 GPUs at a time. Got: ${options.accelerators}`, + `You can only buy whole nodes, or 8 GPUs at a time. Got: ${options.accelerators}` ); } @@ -171,11 +167,7 @@ async function buyOrderAction(options: SfBuyOptions) { } } -function QuoteAndBuy( - props: { - options: SfBuyOptions; - }, -) { +function QuoteAndBuy(props: { options: SfBuyOptions }) { const [orderProps, setOrderProps] = useState(null); // submit a quote request, handle loading state @@ -185,7 +177,7 @@ function QuoteAndBuy( // Grab the price per GPU hour, either let pricePerGpuHour: number | null = parsePricePerGpuHour( - props.options.price, + props.options.price ); if (!pricePerGpuHour) { const quote = await getQuoteFromParsedSfBuyOptions(props.options); @@ -199,7 +191,7 @@ function QuoteAndBuy( const duration = parseDuration(props.options.duration); const startDate = parseStartAsDate(props.options.start); const endsAt = roundEndDate( - dayjs(startDate).add(duration, "seconds").toDate(), + dayjs(startDate).add(duration, "seconds").toDate() ).toDate(); setOrderProps({ @@ -213,16 +205,16 @@ function QuoteAndBuy( })(); }, []); - return orderProps === null - ? ( + return orderProps === null ? ( + + - - - Getting quote... - + Getting quote... - ) - : ; + + ) : ( + + ); } function roundEndDate(endDate: Date) { @@ -242,20 +234,18 @@ function roundEndDate(endDate: Date) { function getTotalPrice( pricePerGpuHour: number, size: number, - durationInHours: number, + durationInHours: number ) { return Math.ceil(pricePerGpuHour * size * GPUS_PER_NODE * durationInHours); } -function BuyOrderPreview( - props: { - price: number; - size: number; - startAt: Date | "NOW"; - endsAt: Date; - type: string; - }, -) { +function BuyOrderPreview(props: { + price: number; + size: number; + startAt: Date | "NOW"; + endsAt: Date; + type: string; +}) { const startDate = props.startAt === "NOW" ? dayjs() : dayjs(props.startAt); const start = startDate.format("MMM D h:mm a").toLowerCase(); @@ -272,8 +262,8 @@ function BuyOrderPreview( const realDurationHours = realDuration / 3600 / 1000; const realDurationString = ms(realDuration); - const totalPrice = getTotalPrice(props.price, props.size, realDurationHours) / - 100; + const totalPrice = + getTotalPrice(props.price, props.size, realDurationHours) / 100; return ( @@ -326,25 +316,22 @@ type BuyOrderProps = { type: string; colocate?: Array; }; -function BuyOrder( - props: BuyOrderProps, -) { +function BuyOrder(props: BuyOrderProps) { const [isLoading, setIsLoading] = useState(false); const [value, setValue] = useState(""); const { exit } = useApp(); const [order, setOrder] = useState(null); const intervalRef = useRef | null>(null); const [loadingMsg, setLoadingMsg] = useState( - "Placing order...", + "Placing order..." ); async function submitOrder() { const endsAt = roundEndDate(props.endsAt); - const startAt = props.startAt === "NOW" - ? parseStartAsDate(props.startAt) - : props.startAt; - const realDurationInHours = dayjs(endsAt).diff(dayjs(startAt)) / 1000 / - 3600; + const startAt = + props.startAt === "NOW" ? parseStartAsDate(props.startAt) : props.startAt; + const realDurationInHours = + dayjs(endsAt).diff(dayjs(startAt)) / 1000 / 3600; setIsLoading(true); const order = await placeBuyOrder({ @@ -352,7 +339,7 @@ function BuyOrder( totalPriceInCents: getTotalPrice( props.price, props.size, - realDurationInHours, + realDurationInHours ), startsAt: props.startAt, endsAt: endsAt.toDate(), @@ -363,18 +350,21 @@ function BuyOrder( } const [resultMessage, setResultMessage] = useState(null); - const handleSubmit = useCallback((submitValue: boolean) => { - if (submitValue === false) { - setIsLoading(false); - setResultMessage("Order not placed, use 'y' to confirm"); - setTimeout(() => { - exit(); - }, 0); - return; - } + const handleSubmit = useCallback( + (submitValue: boolean) => { + if (submitValue === false) { + setIsLoading(false); + setResultMessage("Order not placed, use 'y' to confirm"); + setTimeout(() => { + exit(); + }, 0); + return; + } - submitOrder(); - }, [exit, setIsLoading]); + submitOrder(); + }, + [exit, setIsLoading] + ); useEffect(() => { if (isLoading && intervalRef.current == null) { @@ -386,7 +376,7 @@ function BuyOrder( const o = await getOrder(order.id); if (!o) { setLoadingMsg( - "Can't find order. This could be a network issue, try ctrl-c and running 'sf orders ls' to see if it was placed.", + "Can't find order. This could be a network issue, try ctrl-c and running 'sf orders ls' to see if it was placed." ); return; } @@ -466,24 +456,22 @@ function BuyOrder( ); } -export async function placeBuyOrder( - options: { - instanceType: string; - totalPriceInCents: number; - startsAt: Date | "NOW"; - endsAt: Date; - colocateWith: Array; - numberNodes: number; - }, -) { +export async function placeBuyOrder(options: { + instanceType: string; + totalPriceInCents: number; + startsAt: Date | "NOW"; + endsAt: Date; + colocateWith: Array; + numberNodes: number; +}) { invariant( options.totalPriceInCents === Math.ceil(options.totalPriceInCents), - "totalPriceInCents must be a whole number", + "totalPriceInCents must be a whole number" ); invariant(options.numberNodes > 0, "numberNodes must be greater than 0"); invariant( options.numberNodes === Math.ceil(options.numberNodes), - "numberNodes must be a whole number", + "numberNodes must be a whole number" ); const api = await apiClient(); @@ -493,9 +481,10 @@ export async function placeBuyOrder( instance_type: options.instanceType, quantity: options.numberNodes, // round start date again because the user might take a long time to confirm - start_at: options.startsAt === "NOW" - ? "NOW" - : roundStartDate(options.startsAt).toISOString(), + start_at: + options.startsAt === "NOW" + ? "NOW" + : roundStartDate(options.startsAt).toISOString(), end_at: options.endsAt.toISOString(), price: options.totalPriceInCents, colocate_with: options.colocateWith, @@ -506,7 +495,7 @@ export async function placeBuyOrder( switch (response.status) { case 400: return logAndQuit( - `Bad Request: ${error?.message}; ${JSON.stringify(error, null, 2)}`, + `Bad Request: ${error?.message}; ${JSON.stringify(error, null, 2)}` ); case 401: return await logSessionTokenExpiredAndQuit(); @@ -519,7 +508,7 @@ export async function placeBuyOrder( if (!data) { return logAndQuit( - `Failed to place order: Unexpected response from server: ${response}`, + `Failed to place order: Unexpected response from server: ${response}` ); } @@ -528,7 +517,7 @@ export async function placeBuyOrder( function getPricePerGpuHourFromQuote(quote: NonNullable) { const durationSeconds = dayjs(quote.end_at).diff( - parseStartAsDate(quote.start_at), + parseStartAsDate(quote.start_at) ); const durationHours = durationSeconds / 3600 / 1000; @@ -560,12 +549,10 @@ export async function getQuote(options: QuoteOptions) { instance_type: options.instanceType, quantity: options.quantity, duration: options.durationSeconds, - min_start_date: options.startsAt === "NOW" - ? "NOW" - : options.startsAt.toISOString(), - max_start_date: options.startsAt === "NOW" - ? "NOW" - : options.startsAt.toISOString(), + min_start_date: + options.startsAt === "NOW" ? "NOW" : options.startsAt.toISOString(), + max_start_date: + options.startsAt === "NOW" ? "NOW" : options.startsAt.toISOString(), }, }, // timeout after 600 seconds @@ -587,7 +574,7 @@ export async function getQuote(options: QuoteOptions) { if (!data) { return logAndQuit( - `Failed to get quote: Unexpected response from server: ${response}`, + `Failed to get quote: Unexpected response from server: ${response}` ); } diff --git a/src/lib/clusters/clusters.tsx b/src/lib/clusters/clusters.tsx index 5abcb18..81a2690 100644 --- a/src/lib/clusters/clusters.tsx +++ b/src/lib/clusters/clusters.tsx @@ -28,7 +28,7 @@ export function registerClusters(program: Command) { .description("List clusters") .option("--json", "Output in JSON format") .option("--token ", "API token") - .action(async (options) => { + .action(async options => { await listClustersAction({ returnJson: options.json, token: options.token, @@ -48,7 +48,7 @@ export function registerClusters(program: Command) { .option("--json", "Output in JSON format") .option("--token ", "API token") .option("--print", "Print the kubeconfig instead of syncing to file") - .action(async (options) => { + .action(async options => { await addClusterUserAction({ clusterName: options.cluster, username: options.user, @@ -75,7 +75,7 @@ export function registerClusters(program: Command) { .alias("ls") .description("List users in a cluster") .option("--token ", "API token") - .action(async (options) => { + .action(async options => { await listClusterUsers({ token: options.token }); }); @@ -84,7 +84,7 @@ export function registerClusters(program: Command) { .description("Generate or sync kubeconfig") .option("--token ", "API token") .option("--print", "Print the config instead of syncing to file") - .action(async (options) => { + .action(async options => { await kubeconfigAction({ token: options.token, print: options.print, @@ -92,16 +92,18 @@ export function registerClusters(program: Command) { }); } -function ClusterDisplay( - { clusters }: { - clusters: Array< - { name: string; kubernetes_api_url: string; kubernetes_namespace: string } - >; - }, -) { +function ClusterDisplay({ + clusters, +}: { + clusters: Array<{ + name: string; + kubernetes_api_url: string; + kubernetes_namespace: string; + }>; +}) { return ( - {clusters.map((cluster) => ( + {clusters.map(cluster => ( {cluster.name} @@ -122,9 +124,13 @@ function ClusterDisplay( ); } -async function listClustersAction( - { returnJson, token }: { returnJson?: boolean; token?: string }, -) { +async function listClustersAction({ + returnJson, + token, +}: { + returnJson?: boolean; + token?: string; +}) { const api = await apiClient(token); const { data, error, response } = await api.GET("/v0/clusters"); @@ -136,7 +142,7 @@ async function listClustersAction( if (!data) { console.error(error); return logAndQuit( - `Failed to get clusters: Unexpected response from server: ${response}`, + `Failed to get clusters: Unexpected response from server: ${response}` ); } @@ -145,24 +151,24 @@ async function listClustersAction( } else { render( ({ + clusters={data.data.map(cluster => ({ name: cluster.name, kubernetes_api_url: cluster.kubernetes_api_url || "", kubernetes_namespace: cluster.kubernetes_namespace || "", }))} - />, + /> ); } } -function ClusterUserDisplay( - { users }: { - users: Array<{ name: string; is_usable: boolean; cluster: string }>; - }, -) { +function ClusterUserDisplay({ + users, +}: { + users: Array<{ name: string; is_usable: boolean; cluster: string }>; +}) { return ( - {users.map((user) => ( + {users.map(user => ( {user.name} @@ -183,8 +189,8 @@ async function isCredentialReady(id: string) { const api = await apiClient(); const { data } = await api.GET("/v0/credentials"); - const cred = data?.data.find((credential) => - credential.id === id && credential.object === "k8s_credential" + const cred = data?.data.find( + credential => credential.id === id && credential.object === "k8s_credential" ); if (!cred) { @@ -210,19 +216,19 @@ async function listClusterUsers({ token }: { token?: string }) { if (!data) { console.error(error); return logAndQuit( - `Failed to get users in cluster: Unexpected response from server: ${response}`, + `Failed to get users in cluster: Unexpected response from server: ${response}` ); } - const k8s = data.data.filter((credential) => - credential.object === "k8s_credential" + const k8s = data.data.filter( + credential => credential.object === "k8s_credential" ); const users: Array<{ name: string; is_usable: boolean; cluster: string }> = []; for (const k of k8s) { const is_usable: boolean = Boolean( - k.encrypted_token && k.nonce && k.ephemeral_pubkey, + k.encrypted_token && k.nonce && k.ephemeral_pubkey ); users.push({ name: k.username || "", @@ -341,7 +347,7 @@ async function addClusterUserAction({ if (!data) { console.error(error); return logAndQuit( - `Failed to add user to cluster: Unexpected response from server: ${response}`, + `Failed to add user to cluster: Unexpected response from server: ${response}` ); } @@ -349,9 +355,13 @@ async function addClusterUserAction({ render(); } -async function removeClusterUserAction( - { id, token }: { id: string; token?: string }, -) { +async function removeClusterUserAction({ + id, + token, +}: { + id: string; + token?: string; +}) { const api = await apiClient(token); const { data, error, response } = await api.DELETE("/v0/credentials/{id}", { @@ -364,37 +374,41 @@ async function removeClusterUserAction( if (!response.ok) { return logAndQuit( - `Failed to remove user from cluster: ${response.statusText}`, + `Failed to remove user from cluster: ${response.statusText}` ); } if (!data) { console.error(error); return logAndQuit( - `Failed to remove user from cluster: Unexpected response from server: ${response}`, + `Failed to remove user from cluster: Unexpected response from server: ${response}` ); } console.log(data); } -async function kubeconfigAction( - { token, print }: { token?: string; print?: boolean }, -) { +async function kubeconfigAction({ + token, + print, +}: { + token?: string; + print?: boolean; +}) { const api = await apiClient(token); const { data, error, response } = await api.GET("/v0/credentials"); if (!response.ok) { return logAndQuit( - `Failed to list users in cluster: ${response.statusText}`, + `Failed to list users in cluster: ${response.statusText}` ); } if (!data) { console.error(error); return logAndQuit( - `Failed to list users in cluster: Unexpected response from server: ${response}`, + `Failed to list users in cluster: Unexpected response from server: ${response}` ); } @@ -404,14 +418,12 @@ async function kubeconfigAction( } const { privateKey } = await getKeys(); - const clusters: Array< - { - name: string; - certificateAuthorityData: string; - kubernetesApiUrl: string; - namespace?: string; - } - > = []; + const clusters: Array<{ + name: string; + certificateAuthorityData: string; + kubernetesApiUrl: string; + namespace?: string; + }> = []; const users: Array<{ name: string; token: string }> = []; for (const item of data.data) { if (item.object !== "k8s_credential") { @@ -434,7 +446,6 @@ async function kubeconfigAction( continue; } - if (!item.cluster) { continue; } diff --git a/src/lib/clusters/keys.tsx b/src/lib/clusters/keys.tsx index 2ea1ea7..5ca8dda 100644 --- a/src/lib/clusters/keys.tsx +++ b/src/lib/clusters/keys.tsx @@ -4,12 +4,14 @@ import * as path from "node:path"; import * as os from "node:os"; import { Buffer } from "node:buffer"; -export async function getKeys(): Promise< - { publicKey: string; privateKey: string } -> { +export async function getKeys(): Promise<{ + publicKey: string; + privateKey: string; +}> { const keys = await loadKeys(); if ( - keys && typeof keys.privateKey === "string" && + keys && + typeof keys.privateKey === "string" && typeof keys.publicKey === "string" ) { return { @@ -39,20 +41,18 @@ function generateKeyPair() { }; } -export function decryptSecret( - props: { - encrypted: string; - secretKey: string; - nonce: string; - ephemeralPublicKey: string; - }, -) { +export function decryptSecret(props: { + encrypted: string; + secretKey: string; + nonce: string; + ephemeralPublicKey: string; +}) { // Generate nonce and message from encrypted secret const decrypted = nacl.default.box.open( util.decodeBase64(props.encrypted), util.decodeBase64(props.nonce), util.decodeBase64(props.ephemeralPublicKey), - util.decodeBase64(props.secretKey), + util.decodeBase64(props.secretKey) ); if (!decrypted) { diff --git a/src/lib/clusters/kubeconfig.test.ts b/src/lib/clusters/kubeconfig.test.ts index 5187f44..8763ab4 100644 --- a/src/lib/clusters/kubeconfig.test.ts +++ b/src/lib/clusters/kubeconfig.test.ts @@ -48,12 +48,12 @@ Deno.test("Merges clusters without overwriting unique entries", () => { assertEquals(mergedConfig.clusters.length, 2); assert( - mergedConfig.clusters.some((cluster) => cluster.name === "cluster1"), - "cluster1 should exist in merged clusters", + mergedConfig.clusters.some(cluster => cluster.name === "cluster1"), + "cluster1 should exist in merged clusters" ); assert( - mergedConfig.clusters.some((cluster) => cluster.name === "cluster2"), - "cluster2 should exist in merged clusters", + mergedConfig.clusters.some(cluster => cluster.name === "cluster2"), + "cluster2 should exist in merged clusters" ); }); @@ -275,7 +275,7 @@ Deno.test( assertEquals(mergedConfig.apiVersion, "v1"); assertEquals(mergedConfig.kind, "Config"); assertEquals(mergedConfig["current-context"], "context1"); - }, + } ); Deno.test("Handles optional fields like namespace correctly", () => { @@ -323,7 +323,7 @@ Deno.test("Handles optional fields like namespace correctly", () => { assertEquals( mergedConfig.contexts[0].context.namespace, "namespace2", - "Namespace should be updated from config2", + "Namespace should be updated from config2" ); }); diff --git a/src/lib/clusters/kubeconfig.ts b/src/lib/clusters/kubeconfig.ts index 811f27b..f46ad44 100644 --- a/src/lib/clusters/kubeconfig.ts +++ b/src/lib/clusters/kubeconfig.ts @@ -53,14 +53,14 @@ export function createKubeconfig(props: { apiVersion: "v1", kind: "Config", preferences: {}, - clusters: clusters.map((cluster) => ({ + clusters: clusters.map(cluster => ({ name: cluster.name, cluster: { server: cluster.kubernetesApiUrl, "certificate-authority-data": cluster.certificateAuthorityData, }, })), - users: users.map((user) => ({ + users: users.map(user => ({ name: user.name, user: { token: user.token, @@ -71,9 +71,9 @@ export function createKubeconfig(props: { }; // Generate contexts automatically by matching clusters and users by name - kubeconfig.contexts = clusters.map((cluster) => { + kubeconfig.contexts = clusters.map(cluster => { // Try to find a user with the same name as the cluster - let user = users.find((u) => u.name === cluster.name); + let user = users.find(u => u.name === cluster.name); // If no matching user, default to the first user if (!user) { @@ -94,8 +94,7 @@ export function createKubeconfig(props: { // Set current context based on provided cluster and user names if (currentContext) { - const contextName = - `${currentContext.clusterName}@${currentContext.userName}`; + const contextName = `${currentContext.clusterName}@${currentContext.userName}`; kubeconfig["current-context"] = contextName; } else if (kubeconfig.contexts.length > 0) { kubeconfig["current-context"] = kubeconfig.contexts[0].name; @@ -106,7 +105,7 @@ export function createKubeconfig(props: { export function mergeNamedItems( items1: T[], - items2: T[], + items2: T[] ): T[] { const map = new Map(); for (const item of items1) { @@ -120,7 +119,7 @@ export function mergeNamedItems( export function mergeKubeconfigs( oldConfig: Kubeconfig, - newConfig?: Kubeconfig, + newConfig?: Kubeconfig ): Kubeconfig { if (!newConfig) { return oldConfig; @@ -130,15 +129,15 @@ export function mergeKubeconfigs( apiVersion: newConfig.apiVersion || oldConfig.apiVersion, clusters: mergeNamedItems( oldConfig.clusters || [], - newConfig.clusters || [], + newConfig.clusters || [] ), contexts: mergeNamedItems( oldConfig.contexts || [], - newConfig.contexts || [], + newConfig.contexts || [] ), users: mergeNamedItems(oldConfig.users || [], newConfig.users || []), - "current-context": newConfig["current-context"] || - oldConfig["current-context"], + "current-context": + newConfig["current-context"] || oldConfig["current-context"], kind: newConfig.kind || oldConfig.kind, preferences: { ...oldConfig.preferences, ...newConfig.preferences }, }; diff --git a/src/lib/contracts/ContractDisplay.tsx b/src/lib/contracts/ContractDisplay.tsx index 638744c..1325478 100644 --- a/src/lib/contracts/ContractDisplay.tsx +++ b/src/lib/contracts/ContractDisplay.tsx @@ -27,17 +27,20 @@ export function ContractDisplay(props: { contract: Contract }) { 0 - ? props.contract.colocate_with.join(", ") - : "-"} + value={ + props.contract.colocate_with.length > 0 + ? props.contract.colocate_with.join(", ") + : "-" + } /> - {props.contract.shape.intervals.slice(0, -1).map((interval) => { + {props.contract.shape.intervals.slice(0, -1).map(interval => { const start = new Date(interval); const next = new Date( - props.contract.shape - .intervals[props.contract.shape.intervals.indexOf(interval) + 1], + props.contract.shape.intervals[ + props.contract.shape.intervals.indexOf(interval) + 1 + ] ); const duration = next.getTime() - start.getTime(); @@ -45,15 +48,16 @@ export function ContractDisplay(props: { contract: Contract }) { const nextString = dayjs(next).format("MMM D h:mm a").toLowerCase(); const durationString = ms(duration); - const quantity = props.contract.shape - .quantities[props.contract.shape.intervals.indexOf(interval)]; + const quantity = + props.contract.shape.quantities[ + props.contract.shape.intervals.indexOf(interval) + ]; return ( - {quantity * GPUS_PER_NODE} x {props.contract.instance_type} - {" "} + {quantity * GPUS_PER_NODE} x {props.contract.instance_type}{" "} (gpus) @@ -88,7 +92,7 @@ export function ContractList(props: { contracts: Contract[] }) { return ( - {props.contracts.map((contract) => ( + {props.contracts.map(contract => ( ))} diff --git a/src/lib/contracts/index.tsx b/src/lib/contracts/index.tsx index b2d05e7..fe58c57 100644 --- a/src/lib/contracts/index.tsx +++ b/src/lib/contracts/index.tsx @@ -23,7 +23,7 @@ export function registerContracts(program: Command) { .alias("ls") .option("--json", "Output in JSON format") .description("List all contracts") - .action(async (options) => { + .action(async options => { if (options.json) { console.log(await listContracts()); } else { @@ -32,7 +32,7 @@ export function registerContracts(program: Command) { render(); } process.exit(0); - }), + }) ); } @@ -59,7 +59,7 @@ async function listContracts(): Promise { if (!data) { return logAndQuit( - `Failed to get contracts: Unexpected response from server: ${response}`, + `Failed to get contracts: Unexpected response from server: ${response}` ); } diff --git a/src/lib/dev.ts b/src/lib/dev.ts index 7c29dbe..318861a 100644 --- a/src/lib/dev.ts +++ b/src/lib/dev.ts @@ -31,7 +31,7 @@ export function registerDev(program: Command) { const unixEpochSecondsNow = dayjs().unix(); console.log(unixEpochSecondsNow); console.log( - chalk.green(dayjs().utc().format("dddd, MMMM D, YYYY h:mm:ss A")), + chalk.green(dayjs().utc().format("dddd, MMMM D, YYYY h:mm:ss A")) ); process.exit(0); @@ -57,7 +57,7 @@ function registerConfig(program: Command) { // sf config // sf config [-rm, --remove] - configCmd.action(async (options) => { + configCmd.action(async options => { if (options.remove) { await removeConfigAction(); } else { @@ -112,11 +112,9 @@ function registerEpoch(program: Command) { timestamps.forEach((epochTimestamp, i) => { const date = epochToDate(Number.parseInt(epochTimestamp)); console.log( - `${colorDiffedEpochs[i]} | ${ - chalk.yellow( - dayjs(date).format("hh:mm A MM-DD-YYYY"), - ) - } Local`, + `${colorDiffedEpochs[i]} | ${chalk.yellow( + dayjs(date).format("hh:mm A MM-DD-YYYY") + )} Local` ); }); } @@ -135,14 +133,14 @@ function registerEpoch(program: Command) { } function colorDiffEpochs(epochStrings: string[]): string[] { - const minLength = Math.min(...epochStrings.map((num) => num.length)); + const minLength = Math.min(...epochStrings.map(num => num.length)); // function to find the common prefix between all numbers const findCommonPrefix = (arr: string[]): string => { let prefix = ""; for (let i = 0; i < minLength; i++) { const currentChar = arr[0][i]; - if (arr.every((num) => num[i] === currentChar)) { + if (arr.every(num => num[i] === currentChar)) { prefix += currentChar; } else { break; @@ -155,7 +153,7 @@ function colorDiffEpochs(epochStrings: string[]): string[] { // find the common prefix for all numbers const commonPrefix = findCommonPrefix(epochStrings); - return epochStrings.map((num) => { + return epochStrings.map(num => { const prefix = num.startsWith(commonPrefix) ? commonPrefix : ""; const rest = num.slice(prefix.length); diff --git a/src/lib/login.ts b/src/lib/login.ts index 9488ce3..81fab88 100644 --- a/src/lib/login.ts +++ b/src/lib/login.ts @@ -29,7 +29,7 @@ export function registerLogin(program: Command) { clearScreen(); console.log(`\n\n Click here to login:\n ${url}\n\n`); console.log( - ` Do these numbers match your browser window?\n ${validation}\n\n`, + ` Do these numbers match your browser window?\n ${validation}\n\n` ); const checkSession = async () => { @@ -47,11 +47,7 @@ export function registerLogin(program: Command) { }); } -async function createSession({ - validation, -}: { - validation: string; -}) { +async function createSession({ validation }: { validation: string }) { const url = await getWebAppUrl("cli_session_create"); try { @@ -63,7 +59,7 @@ async function createSession({ "Content-Type": "application/json", }, maxRedirects: 5, - }, + } ); return response.data as { @@ -76,11 +72,7 @@ async function createSession({ } } -async function getSession({ - token, -}: { - token: string; -}) { +async function getSession({ token }: { token: string }) { try { const url = await getWebAppUrl("cli_session_get", { token }); const response = await axios.get(url, { diff --git a/src/lib/orders/OrderDisplay.tsx b/src/lib/orders/OrderDisplay.tsx index 37db27f..4dff309 100644 --- a/src/lib/orders/OrderDisplay.tsx +++ b/src/lib/orders/OrderDisplay.tsx @@ -1,22 +1,25 @@ -import { Box, Text } from "ink"; -import type { HydratedOrder } from "./types.ts"; -import { GPUS_PER_NODE } from "../constants.ts"; +import { Box, measureElement, Text, useInput } from "ink"; import dayjs from "npm:dayjs@1.11.13"; -import { formatDuration } from "./index.tsx"; +import React, { useEffect } from "react"; +import { GPUS_PER_NODE } from "../constants.ts"; import { Row } from "../Row.tsx"; -import React from "react"; +import { formatDuration } from "./index.tsx"; +import type { HydratedOrder } from "./types.ts"; function orderDetails(order: HydratedOrder) { const duration = dayjs(order.end_at).diff(order.start_at); const durationInHours = duration === 0 ? 1 : duration / 1000 / 60 / 60; - const pricePerGPUHour = order.price * order.quantity / - GPUS_PER_NODE / durationInHours / 100; + const pricePerGPUHour = + (order.price * order.quantity) / GPUS_PER_NODE / durationInHours / 100; const durationFormatted = formatDuration(duration); let executedPricePerGPUHour; if (order.execution_price) { - executedPricePerGPUHour = order.execution_price * order.quantity / - GPUS_PER_NODE / durationInHours / 100; + executedPricePerGPUHour = + (order.execution_price * order.quantity) / + GPUS_PER_NODE / + durationInHours / + 100; } return { @@ -26,6 +29,9 @@ function orderDetails(order: HydratedOrder) { }; } +const formatDateTime = (date: string) => + dayjs(date).format("MMM D h:mm a").toLowerCase(); + function Order(props: { order: HydratedOrder }) { const { pricePerGPUHour, durationFormatted } = orderDetails(props.order); @@ -71,55 +77,88 @@ function Order(props: { order: HydratedOrder }) { ); } -function OrderMinimal(props: { order: HydratedOrder }) { +function OrderMinimal(props: { + order: HydratedOrder; + activeTab: "all" | "sell" | "buy"; +}) { const { pricePerGPUHour, durationFormatted, executedPricePerGPUHour } = orderDetails(props.order); return ( - - - {props.order.side === "buy" ? "↑" : "↓"} - + {props.order.side} - + - ${pricePerGPUHour.toFixed(2)}/gpu/hr + ${pricePerGPUHour.toFixed(2)} + /gpu/hr {executedPricePerGPUHour && ( ${executedPricePerGPUHour.toFixed(2)} )} - - - {dayjs(props.order.start_at).format("MMM D h:mm a").toLowerCase()} → - {" "} - {dayjs(props.order.end_at).format("MMM D h:mm a").toLowerCase()} - - - - {durationFormatted} + + {durationFormatted} + + + {formatDateTime(props.order.start_at)} + + + + + + + {dayjs(props.order.start_at).isSame(props.order.end_at, "day") + ? dayjs(props.order.end_at).format("h:mm a").toLowerCase() + : formatDateTime(props.order.end_at)} + + + - - ({props.order.status}) + + {props.order.status} - {props.order.id} + {props.order.id} ); } -export function OrderDisplay( - props: { orders: HydratedOrder[]; expanded?: boolean }, -) { +const NUMBER_OF_ORDERS_TO_DISPLAY = 20; + +export function OrderDisplay(props: { + orders: HydratedOrder[]; + expanded?: boolean; +}) { + const [activeTab, setActiveTab] = React.useState<"all" | "sell" | "buy">( + "all" + ); + + useInput((input, key) => { + if (key.escape || input === "q") { + process.exit(0); + } + if (input === "a") { + setActiveTab("all"); + } + + if (input === "s") { + setActiveTab("sell"); + } + + if (input === "b") { + setActiveTab("buy"); + } + }); + if (props.orders.length === 0) { return ( @@ -138,9 +177,263 @@ export function OrderDisplay( ); } - return props.orders.map((order) => { - return props.expanded - ? - : ; + const orders = + activeTab === "all" + ? props.orders + : props.orders.filter(order => order.side === activeTab); + + const { sellOrdersCount, buyOrdersCount } = React.useMemo(() => { + return { + sellOrdersCount: props.orders.filter(order => order.side === "sell") + .length, + buyOrdersCount: props.orders.filter(order => order.side === "buy").length, + }; + }, [props.orders]); + + return ( + <> + + {orders.map(order => { + return props.expanded ? ( + + ) : ( + + ); + })} + + {orders.length === 0 && ( + + + There are 0 outstanding {activeTab === "all" ? "" : activeTab}{" "} + orders right now. + + + )} + + + ); +} + +interface ScrollState { + innerHeight: number; + height: number; + scrollTop: number; +} + +type ScrollAction = + | { type: "SET_INNER_HEIGHT"; innerHeight: number } + | { type: "SCROLL_DOWN" } + | { type: "SCROLL_DOWN_BULK" } + | { type: "SCROLL_UP" } + | { type: "SCROLL_UP_BULK" } + | { type: "SCROLL_TO_TOP" } + | { type: "SCROLL_TO_BOTTOM" } + | { type: "SWITCHED_TAB" }; + +const reducer = (state: ScrollState, action: ScrollAction): ScrollState => { + switch (action.type) { + case "SET_INNER_HEIGHT": + return { + ...state, + innerHeight: action.innerHeight, + }; + + case "SCROLL_DOWN": + return { + ...state, + scrollTop: Math.min( + state.innerHeight - state.height, + state.scrollTop + 1 + ), + }; + + case "SCROLL_DOWN_BULK": + return { + ...state, + scrollTop: Math.min( + state.innerHeight - state.height, + state.scrollTop + NUMBER_OF_ORDERS_TO_DISPLAY + ), + }; + + case "SCROLL_UP": + return { + ...state, + scrollTop: Math.max(0, state.scrollTop - 1), + }; + + case "SCROLL_UP_BULK": + return { + ...state, + scrollTop: Math.max(0, state.scrollTop - NUMBER_OF_ORDERS_TO_DISPLAY), + }; + + case "SCROLL_TO_TOP": + return { + ...state, + scrollTop: 0, + }; + + case "SCROLL_TO_BOTTOM": + return { + ...state, + scrollTop: state.innerHeight - state.height, + }; + + case "SWITCHED_TAB": { + return { + ...state, + scrollTop: 0, + }; + } + + default: + return state; + } +}; + +export function ScrollArea({ + height, + children, + orders, + activeTab, + sellOrdersCount, + buyOrdersCount, +}: { + height: number; + children: React.ReactNode; + orders: HydratedOrder[]; + activeTab: "all" | "sell" | "buy"; + sellOrdersCount: number; + buyOrdersCount: number; +}) { + const [state, dispatch] = React.useReducer< + React.Reducer + >(reducer, { + height, + scrollTop: 0, + innerHeight: 0, + }); + + const innerRef = React.useRef(null); + const canScrollUp = state.scrollTop > 0 && orders.length > 0; + const numberOfOrdersAboveScrollArea = state.scrollTop; + const dateRangeAboveScrollArea = + orders.length > 0 + ? `${formatDateTime(orders[0].start_at)} → ${formatDateTime(orders[numberOfOrdersAboveScrollArea - 1]?.end_at || "0")}` + : ""; + const numberOfOrdersBelowScrollArea = + orders.length - (state.scrollTop + state.height); + const dateRangeBelowScrollArea = + orders.length > 0 + ? `${formatDateTime(orders[state.scrollTop + state.height]?.start_at || "0")} → ${formatDateTime(orders[orders.length - 1].end_at)}` + : ""; + const canScrollDown = + state.scrollTop + state.height < state.innerHeight && + numberOfOrdersBelowScrollArea >= 0; + + useEffect(() => { + if (!innerRef.current) { + return; + } + + const dimensions = measureElement(innerRef.current); + + dispatch({ + type: "SET_INNER_HEIGHT", + innerHeight: dimensions.height, + }); + }, []); + + useEffect(() => { + dispatch({ type: "SWITCHED_TAB" }); + }, [activeTab]); + + useInput((input, key) => { + if (key.downArrow || input === "j") { + dispatch({ + type: "SCROLL_DOWN", + }); + } + + if (key.upArrow || input === "k") { + dispatch({ + type: "SCROLL_UP", + }); + } + + if (input === "u") { + dispatch({ + type: "SCROLL_UP_BULK", + }); + } + + if (input === "d") { + dispatch({ + type: "SCROLL_DOWN_BULK", + }); + } + + if (input === "g") { + dispatch({ + type: "SCROLL_TO_TOP", + }); + } + + if (input === "G") { + dispatch({ + type: "SCROLL_TO_BOTTOM", + }); + } }); + + return ( + + + + + {canScrollUp + ? `↑ ${numberOfOrdersAboveScrollArea.toLocaleString()} more (${dateRangeAboveScrollArea})` + : " "} + + + + [a]ll {sellOrdersCount + buyOrdersCount} + + + [s]ell {sellOrdersCount} + + + [b]uy {buyOrdersCount} + + + + + + + {children} + + + + + + {canScrollDown + ? `↓ ${numberOfOrdersBelowScrollArea.toLocaleString()} more (${dateRangeBelowScrollArea})` + : " "} + + + + + ); } diff --git a/src/lib/orders/index.tsx b/src/lib/orders/index.tsx index e4e2b6c..a66e960 100644 --- a/src/lib/orders/index.tsx +++ b/src/lib/orders/index.tsx @@ -1,7 +1,9 @@ import type { Command } from "commander"; import dayjs from "dayjs"; +import { render } from "ink"; import duration from "npm:dayjs@1.11.13/plugin/duration.js"; import relativeTime from "npm:dayjs@1.11.13/plugin/relativeTime.js"; +import React from "react"; import { getAuthToken, isLoggedIn } from "../../helpers/config.ts"; import { logAndQuit, @@ -10,11 +12,9 @@ import { } from "../../helpers/errors.ts"; import { fetchAndHandleErrors } from "../../helpers/fetch.ts"; import { getApiUrl } from "../../helpers/urls.ts"; -import { render, Text } from "ink"; +import { parseStartAsDate } from "../buy/index.tsx"; import { OrderDisplay } from "./OrderDisplay.tsx"; import type { HydratedOrder, ListResponseBody } from "./types.ts"; -import React from "react"; -import { parseStartAsDate } from "../buy/index.tsx"; dayjs.extend(relativeTime); dayjs.extend(duration); @@ -44,7 +44,10 @@ export function formatDuration(ms: number) { } export function registerOrders(program: Command) { - const ordersCommand = program.command("orders").alias("o").alias("order") + const ordersCommand = program + .command("orders") + .alias("o") + .alias("order") .description("Manage orders"); ordersCommand @@ -58,67 +61,67 @@ export function registerOrders(program: Command) { .option("--max-price ", "Filter by maximum price (in cents)") .option( "--min-start ", - "Filter by minimum start date (ISO 8601 datestring)", + "Filter by minimum start date (ISO 8601 datestring)" ) .option( "--max-start ", - "Filter by maximum start date (ISO 8601 datestring)", + "Filter by maximum start date (ISO 8601 datestring)" ) .option( "--min-duration ", - "Filter by minimum duration (in seconds)", + "Filter by minimum duration (in seconds)" ) .option( "--max-duration ", - "Filter by maximum duration (in seconds)", + "Filter by maximum duration (in seconds)" ) .option("--min-quantity ", "Filter by minimum quantity") .option("--max-quantity ", "Filter by maximum quantity") .option( "--contract-id ", - "Filter by contract ID (only for sell orders)", + "Filter by contract ID (only for sell orders)" ) .option("--only-open", "Show only open orders") .option("--exclude-filled", "Exclude filled orders") .option("--only-filled", "Show only filled orders") .option( "--min-filled-at ", - "Filter by minimum filled date (ISO 8601 datestring)", + "Filter by minimum filled date (ISO 8601 datestring)" ) .option( "--max-filled-at ", - "Filter by maximum filled date (ISO 8601 datestring)", + "Filter by maximum filled date (ISO 8601 datestring)" ) .option( "--min-fill-price ", - "Filter by minimum fill price (in cents)", + "Filter by minimum fill price (in cents)" ) .option( "--max-fill-price ", - "Filter by maximum fill price (in cents)", + "Filter by maximum fill price (in cents)" ) .option("--include-cancelled", "Include cancelled orders") .option("--only-cancelled", "Show only cancelled orders") .option( "--min-cancelled-at ", - "Filter by minimum cancelled date (ISO 8601 datestring)", + "Filter by minimum cancelled date (ISO 8601 datestring)" ) .option( "--max-cancelled-at ", - "Filter by maximum cancelled date (ISO 8601 datestring)", + "Filter by maximum cancelled date (ISO 8601 datestring)" ) .option( "--min-placed-at ", - "Filter by minimum placed date (ISO 8601 datestring)", + "Filter by minimum placed date (ISO 8601 datestring)" ) .option( "--max-placed-at ", - "Filter by maximum placed date (ISO 8601 datestring)", + "Filter by maximum placed date (ISO 8601 datestring)" ) .option("--limit ", "Limit the number of results") .option("--offset ", "Offset the results (for pagination)") .option("--json", "Output in JSON format") - .action(async (options) => { + .action(async options => { const orders = await getOrders({ side: options.side, instance_type: options.type, @@ -166,11 +169,16 @@ export function registerOrders(program: Command) { if (options.json) { console.log(JSON.stringify(sortedOrders, null, 2)); + process.exit(0); } else { render(); - } - process.exit(0); + // Automatically exit the process if there are no orders + // Otherwise leave the process running so the user can interact (scroll) to see the orders + if (sortedOrders.length === 0) { + process.exit(0); + } + } }); ordersCommand @@ -247,7 +255,7 @@ export async function getOrders(props: { } export async function submitOrderCancellationByIdAction( - orderId: string, + orderId: string ): Promise { const loggedIn = await isLoggedIn(); if (!loggedIn) { diff --git a/src/lib/sell.ts b/src/lib/sell.ts index dcafd18..d5e2217 100644 --- a/src/lib/sell.ts +++ b/src/lib/sell.ts @@ -32,9 +32,9 @@ export function registerSell(program: Command) { .option( "-f, --flags ", "Specify additional flags as JSON", - JSON.parse, + JSON.parse ) - .action(async (options) => { + .action(async options => { await placeSellOrder(options); }); } @@ -54,7 +54,7 @@ function contractStartAndEnd(contract: { }) { const startDate = dayjs(contract.shape.intervals[0]).toDate(); const endDate = dayjs( - contract.shape.intervals[contract.shape.intervals.length - 1], + contract.shape.intervals[contract.shape.intervals.length - 1] ).toDate(); return { startDate, endDate }; @@ -84,15 +84,14 @@ async function placeSellOrder(options: { if (contract?.status === "pending") { return logAndQuit( - `Contract ${options.contractId} is currently pending. Please try again in a few seconds.`, + `Contract ${options.contractId} is currently pending. Please try again in a few seconds.` ); } if (options.accelerators % GPUS_PER_NODE !== 0) { - const exampleCommand = - `sf sell -n ${GPUS_PER_NODE} -c ${options.contractId}`; + const exampleCommand = `sf sell -n ${GPUS_PER_NODE} -c ${options.contractId}`; return logAndQuit( - `At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}`, + `At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}` ); } @@ -134,7 +133,7 @@ async function placeSellOrder(options: { priceCents, totalDurationSecs, nodes, - GPUS_PER_NODE, + GPUS_PER_NODE ); const params: PlaceSellOrderParameters = { @@ -155,13 +154,11 @@ async function placeSellOrder(options: { switch (response.status) { case 400: return logAndQuit( - `Bad Request: ${error?.message}: ${ - JSON.stringify( - error?.details, - null, - 2, - ) - }`, + `Bad Request: ${error?.message}: ${JSON.stringify( + error?.details, + null, + 2 + )}` ); // return logAndQuit(`Bad Request: ${error?.message}`); case 401: diff --git a/src/lib/sell/index.tsx b/src/lib/sell/index.tsx index 180091f..52b198e 100644 --- a/src/lib/sell/index.tsx +++ b/src/lib/sell/index.tsx @@ -47,13 +47,13 @@ export function registerSell(program: Command) { .option("-n, --accelerators ", "Specify the number of GPUs", "8") .option( "-s, --start ", - "Specify the start date. Can be a date, relative time like '+1d', or the string 'NOW'", + "Specify the start date. Can be a date, relative time like '+1d', or the string 'NOW'" ) .option("-d, --duration ", "Specify the duration", "1h") .option( "-f, --flags ", "Specify additional flags as JSON", - JSON.parse, + JSON.parse ) .option("-y, --yes", "Automatically confirm the order") .action(sellOrderAction); @@ -123,7 +123,7 @@ function roundEndDate(endDate: Date) { function getTotalPrice( pricePerGpuHour: number, size: number, - durationInHours: number, + durationInHours: number ) { return Math.ceil(pricePerGpuHour * size * GPUS_PER_NODE * durationInHours); } @@ -151,7 +151,7 @@ async function sellOrderAction(options: SfSellOptions) { const size = parseAccelerators(options.accelerators); if (isNaN(size) || size <= 0) { return logAndQuit( - `Invalid number of accelerators: ${options.accelerators}`, + `Invalid number of accelerators: ${options.accelerators}` ); } @@ -166,7 +166,7 @@ async function sellOrderAction(options: SfSellOptions) { } const endDate = roundEndDate( - dayjs(startDate).add(durationSeconds, "seconds").toDate(), + dayjs(startDate).add(durationSeconds, "seconds").toDate() ).toDate(); // Fetch contract details @@ -214,7 +214,7 @@ function SellOrder(props: { submitOrder(); }, - [exit], + [exit] ); async function submitOrder() { @@ -340,8 +340,8 @@ function SellOrderPreview(props: { const realDurationHours = realDuration / 3600 / 1000; const realDurationString = ms(realDuration); - const totalPrice = getTotalPrice(props.price, props.size, realDurationHours) / - 100; + const totalPrice = + getTotalPrice(props.price, props.size, realDurationHours) / 100; return ( @@ -391,19 +391,20 @@ export async function placeSellOrder(options: { endsAt: Date; flags?: Record; }) { - const realDurationHours = dayjs(options.endsAt).diff( - dayjs(options.startAt === "NOW" ? new Date() : options.startAt), - ) / + const realDurationHours = + dayjs(options.endsAt).diff( + dayjs(options.startAt === "NOW" ? new Date() : options.startAt) + ) / 3600 / 1000; const totalPrice = getTotalPrice( options.price, options.quantity, - realDurationHours, + realDurationHours ); invariant( totalPrice == Math.ceil(totalPrice), - "totalPrice must be a whole number", + "totalPrice must be a whole number" ); const api = await apiClient(); @@ -413,9 +414,8 @@ export async function placeSellOrder(options: { price: totalPrice, contract_id: options.contractId, quantity: options.quantity, - start_at: options.startAt === "NOW" - ? "NOW" - : options.startAt.toISOString(), + start_at: + options.startAt === "NOW" ? "NOW" : options.startAt.toISOString(), end_at: options.endsAt.toISOString(), flags: options.flags || {}, }, @@ -436,7 +436,7 @@ export async function placeSellOrder(options: { if (!data) { return logAndQuit( - `Failed to place order: Unexpected response from server: ${response}`, + `Failed to place order: Unexpected response from server: ${response}` ); } diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index 04004a0..98d31c7 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -98,9 +98,9 @@ async function createTokenAction() { default: "", }); const description = await input({ - message: `Description for your token ${ - chalk.gray("(optional, ↵ to skip)") - }:`, + message: `Description for your token ${chalk.gray( + "(optional, ↵ to skip)" + )}:`, default: "", }); @@ -139,7 +139,7 @@ async function createTokenAction() { // tell them they will set this in the Authorization header console.log( - `${chalk.gray(`Pass this in the 'Authorization' header of API requests:`)}`, + `${chalk.gray(`Pass this in the 'Authorization' header of API requests:`)}` ); console.log( [ @@ -150,7 +150,7 @@ async function createTokenAction() { chalk.magenta(""), chalk.green('"'), chalk.gray(" }"), - ].join(""), + ].join("") ); console.log("\n"); @@ -160,7 +160,7 @@ async function createTokenAction() { console.log( chalk.white(`curl --request GET \\ --url ${pingUrl} \\ - --header 'Authorization: Bearer ${data.token}'`), + --header 'Authorization: Bearer ${data.token}'`) ); console.log("\n"); @@ -229,7 +229,7 @@ async function listTokensAction() { const base = getCommandBase(); console.log( chalk.gray("Generate your first token with: ") + - chalk.magenta(`${base} tokens create`), + chalk.magenta(`${base} tokens create`) ); process.exit(0); @@ -267,7 +267,10 @@ function formatDate(isoString: string): string { async function deleteTokenAction({ id, force, -}: { id: string; force?: boolean }) { +}: { + id: string; + force?: boolean; +}) { const loggedIn = await isLoggedIn(); if (!loggedIn) { logLoginMessageAndQuit(); @@ -278,17 +281,17 @@ async function deleteTokenAction({ } const deleteTokenConfirmed = await confirm({ - message: `Are you sure you want to delete this token? ${ - chalk.gray("(it will stop working immediately.)") - }`, + message: `Are you sure you want to delete this token? ${chalk.gray( + "(it will stop working immediately.)" + )}`, default: false, }); if (!deleteTokenConfirmed) { process.exit(0); } else { const verySureConfirmed = await confirm({ - message: chalk.red("Very sure?") + " " + - chalk.gray("(just double-checking)"), + message: + chalk.red("Very sure?") + " " + chalk.gray("(just double-checking)"), default: false, }); diff --git a/src/lib/updown.tsx b/src/lib/updown.tsx index 665c023..f560ac0 100644 --- a/src/lib/updown.tsx +++ b/src/lib/updown.tsx @@ -19,7 +19,10 @@ export function registerScale(program: Command) { const scale = program .command("scale") .description("Scale GPUs or show current procurement details") - .option("-n, --accelerators ", "Set number of GPUs (0 to turn off)") + .option( + "-n, --accelerators ", + "Set number of GPUs (0 to turn off)" + ) .option("-t, --type ", "Specify node type", "h100i") .option("-d, --duration ", "Minimum duration", "2h") .option("-p, --price ", "Max price per GPU hour, in dollars") @@ -30,12 +33,12 @@ export function registerScale(program: Command) { .command("show") .description("Show current procurement details") .option("-t, --type ", "Specify node type", "h100i") - .action((options) => { + .action(options => { render(); }); // Default action when running "fly scale" without "show" - scale.action((options) => { + scale.action(options => { // If -n is provided, attempt to scale if (options.accelerators !== undefined) { render(); @@ -57,8 +60,10 @@ function ScaleCommand(props: { const [isLoading, setIsLoading] = useState(false); const [value, setValue] = useState(""); const [error, setError] = useState(null); - const [confirmationMessage, setConfirmationMessage] = useState(null); - const [balanceLowMessage, setBalanceLowMessage] = useState(null); + const [confirmationMessage, setConfirmationMessage] = + useState(null); + const [balanceLowMessage, setBalanceLowMessage] = + useState(null); const [procurementResult, setProcurementResult] = useState(null); const [ displayedPricePerNodeHourInCents, @@ -89,7 +94,8 @@ function ScaleCommand(props: { } = await getDefaultProcurementOptions(props); setDisplayedPricePerNodeHourInCents(pricePerNodeHourInCents); - const pricePerGpuHourInCents = Math.ceil(pricePerNodeHourInCents) / GPUS_PER_NODE; + const pricePerGpuHourInCents = + Math.ceil(pricePerNodeHourInCents) / GPUS_PER_NODE; if (durationHours < 1) { setError("Minimum duration is 1 hour"); @@ -100,7 +106,9 @@ function ScaleCommand(props: { if (balance.available.cents < totalPriceInCents) { setBalanceLowMessage( - You can't afford this. Available: ${(balance.available.cents / 100).toFixed(2)}, Needed: ${(totalPriceInCents / 100).toFixed(2)} + You can't afford this. Available: $ + {(balance.available.cents / 100).toFixed(2)}, Needed: $ + {(totalPriceInCents / 100).toFixed(2)} ); return; @@ -170,11 +178,8 @@ function ScaleCommand(props: { return; } - const { - durationHours, - nodesRequired, - type, - } = getProcurementOptions(props); + const { durationHours, nodesRequired, type } = + getProcurementOptions(props); if (!displayedPricePerNodeHourInCents) { throw new Error("Price per node hour not set."); @@ -291,7 +296,9 @@ function ShowCommand(props: { type: string }) { const duration = info.min_duration_in_hours; const quantity = info.quantity * GPUS_PER_NODE; const pricePerNodeHourInCents = info.max_price_per_node_hour; - const pricePerGpuHourInCents = Math.ceil(pricePerNodeHourInCents / GPUS_PER_NODE); + const pricePerGpuHourInCents = Math.ceil( + pricePerNodeHourInCents / GPUS_PER_NODE + ); return ( @@ -307,7 +314,9 @@ function ShowCommand(props: { type: string }) { head="Min Duration" value={formatDuration(duration * 3600 * 1000)} /> - Current procurement details fetched successfully. + + Current procurement details fetched successfully. + ); } @@ -326,7 +335,11 @@ function ConfirmationMessage(props: { start GPUs - + { + .action(async version => { const spinner = ora(); if (version) { spinner.start(`Checking if version ${version} exists`); - const url = - `https://github.com/sfcompute/cli/archive/refs/tags/${version}.zip`; + const url = `https://github.com/sfcompute/cli/archive/refs/tags/${version}.zip`; const response = await fetch(url, { method: "HEAD" }); if (response.status === 404) { @@ -25,7 +24,7 @@ export function registerUpgrade(program: Command) { // Fetch the install script spinner.start("Downloading install script"); const scriptResponse = await fetch( - "https://www.sfcompute.com/cli/install", + "https://www.sfcompute.com/cli/install" ); if (!scriptResponse.ok) { diff --git a/src/schema.ts b/src/schema.ts index 9c32265..75969be 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -401,108 +401,114 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": { - /** @constant */ - object: "quote"; - /** @constant */ - side: "buy"; - quote: { - /** @description Price in cents (1 = $0.01) */ - price: number; - /** @description The number of nodes. */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description The instance type. */ - instance_type: string; - } | null; - } | { - /** @constant */ - object: "quote"; - /** @constant */ - side: "sell"; - quote: { - /** @description Price in cents (1 = $0.01) */ - price: number; - /** @description The number of nodes. */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - contract_id: string; - } | null; - }; - "multipart/form-data": { - /** @constant */ - object: "quote"; - /** @constant */ - side: "buy"; - quote: { - /** @description Price in cents (1 = $0.01) */ - price: number; - /** @description The number of nodes. */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description The instance type. */ - instance_type: string; - } | null; - } | { - /** @constant */ - object: "quote"; - /** @constant */ - side: "sell"; - quote: { - /** @description Price in cents (1 = $0.01) */ - price: number; - /** @description The number of nodes. */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - contract_id: string; - } | null; - }; - "text/plain": { - /** @constant */ - object: "quote"; - /** @constant */ - side: "buy"; - quote: { - /** @description Price in cents (1 = $0.01) */ - price: number; - /** @description The number of nodes. */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description The instance type. */ - instance_type: string; - } | null; - } | { - /** @constant */ - object: "quote"; - /** @constant */ - side: "sell"; - quote: { - /** @description Price in cents (1 = $0.01) */ - price: number; - /** @description The number of nodes. */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ - start_at: string; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - contract_id: string; - } | null; - }; + "application/json": + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "buy"; + quote: { + /** @description Price in cents (1 = $0.01) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description The instance type. */ + instance_type: string; + } | null; + } + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "sell"; + quote: { + /** @description Price in cents (1 = $0.01) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + contract_id: string; + } | null; + }; + "multipart/form-data": + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "buy"; + quote: { + /** @description Price in cents (1 = $0.01) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description The instance type. */ + instance_type: string; + } | null; + } + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "sell"; + quote: { + /** @description Price in cents (1 = $0.01) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + contract_id: string; + } | null; + }; + "text/plain": + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "buy"; + quote: { + /** @description Price in cents (1 = $0.01) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description The instance type. */ + instance_type: string; + } | null; + } + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "sell"; + quote: { + /** @description Price in cents (1 = $0.01) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + contract_id: string; + } | null; + }; }; }; 401: { @@ -794,168 +800,174 @@ export interface operations { }; requestBody: { content: { - "application/json": { - /** @constant */ - side: "buy"; - /** @description The instance type. */ - instance_type: string; - /** @description The number of nodes. */ - quantity: number; - start_at: string | "NOW"; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number; - flags?: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - colocate_with?: string[]; - } | { - /** @constant */ - side: "sell"; - contract_id: string; - /** @description The number of nodes. */ - quantity: number; - start_at: string | "NOW"; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number; - flags?: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - reprice?: { - /** - * @description Adjust this order's price linearly from adjustment start to end. - * @constant - */ - strategy: "linear"; - /** @description For sell orders, the floor (lowest) price the order can be adjusted to, in cents. For buy orders, the ceiling (highest) price the order can be adjusted to. */ - limit: number; - /** @description When to start adjusting the order’s price. If this date is in the past, it will be clamped such that the adjustment starts immediately. */ - start_at?: string; - /** @description When to stop adjusting the order’s price. If this date is past the order’s end time, it will be clamped such that the adjustment ends at the order’s end time. */ - end_at?: string; - }; - }; - "multipart/form-data": { - /** @constant */ - side: "buy"; - /** @description The instance type. */ - instance_type: string; - /** @description The number of nodes. */ - quantity: number; - start_at: string | "NOW"; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number; - flags?: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - colocate_with?: string[]; - } | { - /** @constant */ - side: "sell"; - contract_id: string; - /** @description The number of nodes. */ - quantity: number; - start_at: string | "NOW"; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number; - flags?: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - reprice?: { - /** - * @description Adjust this order's price linearly from adjustment start to end. - * @constant - */ - strategy: "linear"; - /** @description For sell orders, the floor (lowest) price the order can be adjusted to, in cents. For buy orders, the ceiling (highest) price the order can be adjusted to. */ - limit: number; - /** @description When to start adjusting the order’s price. If this date is in the past, it will be clamped such that the adjustment starts immediately. */ - start_at?: string; - /** @description When to stop adjusting the order’s price. If this date is past the order’s end time, it will be clamped such that the adjustment ends at the order’s end time. */ - end_at?: string; - }; - }; - "text/plain": { - /** @constant */ - side: "buy"; - /** @description The instance type. */ - instance_type: string; - /** @description The number of nodes. */ - quantity: number; - start_at: string | "NOW"; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number; - flags?: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - colocate_with?: string[]; - } | { - /** @constant */ - side: "sell"; - contract_id: string; - /** @description The number of nodes. */ - quantity: number; - start_at: string | "NOW"; - /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ - end_at: string; - /** @description Price in cents (1 = $0.01) */ - price: number; - flags?: { - /** @description If true, this will be a market order. */ - market?: boolean; - /** @description If true, this is a post-only order. */ - post_only?: boolean; - /** @description If true, this is an immediate-or-cancel order. */ - ioc?: boolean; - }; - reprice?: { - /** - * @description Adjust this order's price linearly from adjustment start to end. - * @constant - */ - strategy: "linear"; - /** @description For sell orders, the floor (lowest) price the order can be adjusted to, in cents. For buy orders, the ceiling (highest) price the order can be adjusted to. */ - limit: number; - /** @description When to start adjusting the order’s price. If this date is in the past, it will be clamped such that the adjustment starts immediately. */ - start_at?: string; - /** @description When to stop adjusting the order’s price. If this date is past the order’s end time, it will be clamped such that the adjustment ends at the order’s end time. */ - end_at?: string; - }; - }; + "application/json": + | { + /** @constant */ + side: "buy"; + /** @description The instance type. */ + instance_type: string; + /** @description The number of nodes. */ + quantity: number; + start_at: string | "NOW"; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in cents (1 = $0.01) */ + price: number; + flags?: { + /** @description If true, this will be a market order. */ + market?: boolean; + /** @description If true, this is a post-only order. */ + post_only?: boolean; + /** @description If true, this is an immediate-or-cancel order. */ + ioc?: boolean; + }; + colocate_with?: string[]; + } + | { + /** @constant */ + side: "sell"; + contract_id: string; + /** @description The number of nodes. */ + quantity: number; + start_at: string | "NOW"; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in cents (1 = $0.01) */ + price: number; + flags?: { + /** @description If true, this will be a market order. */ + market?: boolean; + /** @description If true, this is a post-only order. */ + post_only?: boolean; + /** @description If true, this is an immediate-or-cancel order. */ + ioc?: boolean; + }; + reprice?: { + /** + * @description Adjust this order's price linearly from adjustment start to end. + * @constant + */ + strategy: "linear"; + /** @description For sell orders, the floor (lowest) price the order can be adjusted to, in cents. For buy orders, the ceiling (highest) price the order can be adjusted to. */ + limit: number; + /** @description When to start adjusting the order’s price. If this date is in the past, it will be clamped such that the adjustment starts immediately. */ + start_at?: string; + /** @description When to stop adjusting the order’s price. If this date is past the order’s end time, it will be clamped such that the adjustment ends at the order’s end time. */ + end_at?: string; + }; + }; + "multipart/form-data": + | { + /** @constant */ + side: "buy"; + /** @description The instance type. */ + instance_type: string; + /** @description The number of nodes. */ + quantity: number; + start_at: string | "NOW"; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in cents (1 = $0.01) */ + price: number; + flags?: { + /** @description If true, this will be a market order. */ + market?: boolean; + /** @description If true, this is a post-only order. */ + post_only?: boolean; + /** @description If true, this is an immediate-or-cancel order. */ + ioc?: boolean; + }; + colocate_with?: string[]; + } + | { + /** @constant */ + side: "sell"; + contract_id: string; + /** @description The number of nodes. */ + quantity: number; + start_at: string | "NOW"; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in cents (1 = $0.01) */ + price: number; + flags?: { + /** @description If true, this will be a market order. */ + market?: boolean; + /** @description If true, this is a post-only order. */ + post_only?: boolean; + /** @description If true, this is an immediate-or-cancel order. */ + ioc?: boolean; + }; + reprice?: { + /** + * @description Adjust this order's price linearly from adjustment start to end. + * @constant + */ + strategy: "linear"; + /** @description For sell orders, the floor (lowest) price the order can be adjusted to, in cents. For buy orders, the ceiling (highest) price the order can be adjusted to. */ + limit: number; + /** @description When to start adjusting the order’s price. If this date is in the past, it will be clamped such that the adjustment starts immediately. */ + start_at?: string; + /** @description When to stop adjusting the order’s price. If this date is past the order’s end time, it will be clamped such that the adjustment ends at the order’s end time. */ + end_at?: string; + }; + }; + "text/plain": + | { + /** @constant */ + side: "buy"; + /** @description The instance type. */ + instance_type: string; + /** @description The number of nodes. */ + quantity: number; + start_at: string | "NOW"; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in cents (1 = $0.01) */ + price: number; + flags?: { + /** @description If true, this will be a market order. */ + market?: boolean; + /** @description If true, this is a post-only order. */ + post_only?: boolean; + /** @description If true, this is an immediate-or-cancel order. */ + ioc?: boolean; + }; + colocate_with?: string[]; + } + | { + /** @constant */ + side: "sell"; + contract_id: string; + /** @description The number of nodes. */ + quantity: number; + start_at: string | "NOW"; + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in cents (1 = $0.01) */ + price: number; + flags?: { + /** @description If true, this will be a market order. */ + market?: boolean; + /** @description If true, this is a post-only order. */ + post_only?: boolean; + /** @description If true, this is an immediate-or-cancel order. */ + ioc?: boolean; + }; + reprice?: { + /** + * @description Adjust this order's price linearly from adjustment start to end. + * @constant + */ + strategy: "linear"; + /** @description For sell orders, the floor (lowest) price the order can be adjusted to, in cents. For buy orders, the ceiling (highest) price the order can be adjusted to. */ + limit: number; + /** @description When to start adjusting the order’s price. If this date is in the past, it will be clamped such that the adjustment starts immediately. */ + start_at?: string; + /** @description When to stop adjusting the order’s price. If this date is past the order’s end time, it will be clamped such that the adjustment ends at the order’s end time. */ + end_at?: string; + }; + }; }; }; responses: { @@ -1826,91 +1838,100 @@ export interface operations { }; content: { "application/json": { - data: ({ - /** @constant */ - object: "ssh_credential"; - id: string; - pubkey: string; - username: string; - } | { - /** @constant */ - object: "k8s_credential"; - id: string; - username?: string; - label?: string; - pubkey: string; - cluster?: { - /** @constant */ - object: "kubernetes_cluster"; - kubernetes_api_url?: string; - name: string; - kubernetes_namespace: string; - kubernetes_ca_cert?: string; - }; - encrypted_token?: string; - nonce?: string; - ephemeral_pubkey?: string; - })[]; + data: ( + | { + /** @constant */ + object: "ssh_credential"; + id: string; + pubkey: string; + username: string; + } + | { + /** @constant */ + object: "k8s_credential"; + id: string; + username?: string; + label?: string; + pubkey: string; + cluster?: { + /** @constant */ + object: "kubernetes_cluster"; + kubernetes_api_url?: string; + name: string; + kubernetes_namespace: string; + kubernetes_ca_cert?: string; + }; + encrypted_token?: string; + nonce?: string; + ephemeral_pubkey?: string; + } + )[]; has_more: boolean; /** @constant */ object: "list"; }; "multipart/form-data": { - data: ({ - /** @constant */ - object: "ssh_credential"; - id: string; - pubkey: string; - username: string; - } | { - /** @constant */ - object: "k8s_credential"; - id: string; - username?: string; - label?: string; - pubkey: string; - cluster?: { - /** @constant */ - object: "kubernetes_cluster"; - kubernetes_api_url?: string; - name: string; - kubernetes_namespace: string; - kubernetes_ca_cert?: string; - }; - encrypted_token?: string; - nonce?: string; - ephemeral_pubkey?: string; - })[]; + data: ( + | { + /** @constant */ + object: "ssh_credential"; + id: string; + pubkey: string; + username: string; + } + | { + /** @constant */ + object: "k8s_credential"; + id: string; + username?: string; + label?: string; + pubkey: string; + cluster?: { + /** @constant */ + object: "kubernetes_cluster"; + kubernetes_api_url?: string; + name: string; + kubernetes_namespace: string; + kubernetes_ca_cert?: string; + }; + encrypted_token?: string; + nonce?: string; + ephemeral_pubkey?: string; + } + )[]; has_more: boolean; /** @constant */ object: "list"; }; "text/plain": { - data: ({ - /** @constant */ - object: "ssh_credential"; - id: string; - pubkey: string; - username: string; - } | { - /** @constant */ - object: "k8s_credential"; - id: string; - username?: string; - label?: string; - pubkey: string; - cluster?: { - /** @constant */ - object: "kubernetes_cluster"; - kubernetes_api_url?: string; - name: string; - kubernetes_namespace: string; - kubernetes_ca_cert?: string; - }; - encrypted_token?: string; - nonce?: string; - ephemeral_pubkey?: string; - })[]; + data: ( + | { + /** @constant */ + object: "ssh_credential"; + id: string; + pubkey: string; + username: string; + } + | { + /** @constant */ + object: "k8s_credential"; + id: string; + username?: string; + label?: string; + pubkey: string; + cluster?: { + /** @constant */ + object: "kubernetes_cluster"; + kubernetes_api_url?: string; + name: string; + kubernetes_namespace: string; + kubernetes_ca_cert?: string; + }; + encrypted_token?: string; + nonce?: string; + ephemeral_pubkey?: string; + } + )[]; has_more: boolean; /** @constant */ object: "list"; @@ -1993,45 +2014,51 @@ export interface operations { }; requestBody: { content: { - "application/json": { - pubkey: string; - username: string; - /** @constant */ - object?: "ssh_credential"; - } | { - username: string; - label?: string; - cluster: string; - /** @constant */ - object: "k8s_credential"; - pubkey: string; - }; - "multipart/form-data": { - pubkey: string; - username: string; - /** @constant */ - object?: "ssh_credential"; - } | { - username: string; - label?: string; - cluster: string; - /** @constant */ - object: "k8s_credential"; - pubkey: string; - }; - "text/plain": { - pubkey: string; - username: string; - /** @constant */ - object?: "ssh_credential"; - } | { - username: string; - label?: string; - cluster: string; - /** @constant */ - object: "k8s_credential"; - pubkey: string; - }; + "application/json": + | { + pubkey: string; + username: string; + /** @constant */ + object?: "ssh_credential"; + } + | { + username: string; + label?: string; + cluster: string; + /** @constant */ + object: "k8s_credential"; + pubkey: string; + }; + "multipart/form-data": + | { + pubkey: string; + username: string; + /** @constant */ + object?: "ssh_credential"; + } + | { + username: string; + label?: string; + cluster: string; + /** @constant */ + object: "k8s_credential"; + pubkey: string; + }; + "text/plain": + | { + pubkey: string; + username: string; + /** @constant */ + object?: "ssh_credential"; + } + | { + username: string; + label?: string; + cluster: string; + /** @constant */ + object: "k8s_credential"; + pubkey: string; + }; }; }; responses: { @@ -2040,81 +2067,87 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": { - /** @constant */ - object: "ssh_credential"; - id: string; - pubkey: string; - username: string; - } | { - /** @constant */ - object: "k8s_credential"; - id: string; - username?: string; - label?: string; - pubkey: string; - cluster?: { - /** @constant */ - object: "kubernetes_cluster"; - kubernetes_api_url?: string; - name: string; - kubernetes_namespace: string; - kubernetes_ca_cert?: string; - }; - encrypted_token?: string; - nonce?: string; - ephemeral_pubkey?: string; - }; - "multipart/form-data": { - /** @constant */ - object: "ssh_credential"; - id: string; - pubkey: string; - username: string; - } | { - /** @constant */ - object: "k8s_credential"; - id: string; - username?: string; - label?: string; - pubkey: string; - cluster?: { - /** @constant */ - object: "kubernetes_cluster"; - kubernetes_api_url?: string; - name: string; - kubernetes_namespace: string; - kubernetes_ca_cert?: string; - }; - encrypted_token?: string; - nonce?: string; - ephemeral_pubkey?: string; - }; - "text/plain": { - /** @constant */ - object: "ssh_credential"; - id: string; - pubkey: string; - username: string; - } | { - /** @constant */ - object: "k8s_credential"; - id: string; - username?: string; - label?: string; - pubkey: string; - cluster?: { - /** @constant */ - object: "kubernetes_cluster"; - kubernetes_api_url?: string; - name: string; - kubernetes_namespace: string; - kubernetes_ca_cert?: string; - }; - encrypted_token?: string; - nonce?: string; - ephemeral_pubkey?: string; - }; + "application/json": + | { + /** @constant */ + object: "ssh_credential"; + id: string; + pubkey: string; + username: string; + } + | { + /** @constant */ + object: "k8s_credential"; + id: string; + username?: string; + label?: string; + pubkey: string; + cluster?: { + /** @constant */ + object: "kubernetes_cluster"; + kubernetes_api_url?: string; + name: string; + kubernetes_namespace: string; + kubernetes_ca_cert?: string; + }; + encrypted_token?: string; + nonce?: string; + ephemeral_pubkey?: string; + }; + "multipart/form-data": + | { + /** @constant */ + object: "ssh_credential"; + id: string; + pubkey: string; + username: string; + } + | { + /** @constant */ + object: "k8s_credential"; + id: string; + username?: string; + label?: string; + pubkey: string; + cluster?: { + /** @constant */ + object: "kubernetes_cluster"; + kubernetes_api_url?: string; + name: string; + kubernetes_namespace: string; + kubernetes_ca_cert?: string; + }; + encrypted_token?: string; + nonce?: string; + ephemeral_pubkey?: string; + }; + "text/plain": + | { + /** @constant */ + object: "ssh_credential"; + id: string; + pubkey: string; + username: string; + } + | { + /** @constant */ + object: "k8s_credential"; + id: string; + username?: string; + label?: string; + pubkey: string; + cluster?: { + /** @constant */ + object: "kubernetes_cluster"; + kubernetes_api_url?: string; + name: string; + kubernetes_namespace: string; + kubernetes_ca_cert?: string; + }; + encrypted_token?: string; + nonce?: string; + ephemeral_pubkey?: string; + }; }; }; 401: { @@ -2341,88 +2374,97 @@ export interface operations { }; content: { "application/json": { - data: ({ - /** @constant */ - object: "contract"; - /** @constant */ - status: "active"; - id: string; - /** Format: date-time */ - created_at: string; - /** @description The instance type. */ - instance_type: string; - /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ - shape: { - intervals: string[]; - quantities: number[]; - }; - colocate_with?: string[]; - cluster_id?: string; - } | { - /** @constant */ - object: "contract"; - /** @constant */ - status: "pending"; - id: string; - })[]; + data: ( + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + } + )[]; has_more: boolean; /** @constant */ object: "list"; }; "multipart/form-data": { - data: ({ - /** @constant */ - object: "contract"; - /** @constant */ - status: "active"; - id: string; - /** Format: date-time */ - created_at: string; - /** @description The instance type. */ - instance_type: string; - /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ - shape: { - intervals: string[]; - quantities: number[]; - }; - colocate_with?: string[]; - cluster_id?: string; - } | { - /** @constant */ - object: "contract"; - /** @constant */ - status: "pending"; - id: string; - })[]; + data: ( + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + } + )[]; has_more: boolean; /** @constant */ object: "list"; }; "text/plain": { - data: ({ - /** @constant */ - object: "contract"; - /** @constant */ - status: "active"; - id: string; - /** Format: date-time */ - created_at: string; - /** @description The instance type. */ - instance_type: string; - /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ - shape: { - intervals: string[]; - quantities: number[]; - }; - colocate_with?: string[]; - cluster_id?: string; - } | { - /** @constant */ - object: "contract"; - /** @constant */ - status: "pending"; - id: string; - })[]; + data: ( + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + } + )[]; has_more: boolean; /** @constant */ object: "list"; @@ -2512,78 +2554,84 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": { - /** @constant */ - object: "contract"; - /** @constant */ - status: "active"; - id: string; - /** Format: date-time */ - created_at: string; - /** @description The instance type. */ - instance_type: string; - /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ - shape: { - intervals: string[]; - quantities: number[]; - }; - colocate_with?: string[]; - cluster_id?: string; - } | { - /** @constant */ - object: "contract"; - /** @constant */ - status: "pending"; - id: string; - }; - "multipart/form-data": { - /** @constant */ - object: "contract"; - /** @constant */ - status: "active"; - id: string; - /** Format: date-time */ - created_at: string; - /** @description The instance type. */ - instance_type: string; - /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ - shape: { - intervals: string[]; - quantities: number[]; - }; - colocate_with?: string[]; - cluster_id?: string; - } | { - /** @constant */ - object: "contract"; - /** @constant */ - status: "pending"; - id: string; - }; - "text/plain": { - /** @constant */ - object: "contract"; - /** @constant */ - status: "active"; - id: string; - /** Format: date-time */ - created_at: string; - /** @description The instance type. */ - instance_type: string; - /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ - shape: { - intervals: string[]; - quantities: number[]; - }; - colocate_with?: string[]; - cluster_id?: string; - } | { - /** @constant */ - object: "contract"; - /** @constant */ - status: "pending"; - id: string; - }; + "application/json": + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; + "multipart/form-data": + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; + "text/plain": + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "active"; + id: string; + /** Format: date-time */ + created_at: string; + /** @description The instance type. */ + instance_type: string; + /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0 if not empty. */ + shape: { + intervals: string[]; + quantities: number[]; + }; + colocate_with?: string[]; + cluster_id?: string; + } + | { + /** @constant */ + object: "contract"; + /** @constant */ + status: "pending"; + id: string; + }; }; }; 401: { diff --git a/src/scripts/release.ts b/src/scripts/release.ts index 3d2f0ee..22fd839 100644 --- a/src/scripts/release.ts +++ b/src/scripts/release.ts @@ -1,4 +1,3 @@ -import fs from "node:fs"; import { Command } from "commander"; const program = new Command(); @@ -10,12 +9,12 @@ function logAndError(msg: string) { function bumpVersion( version: string, - type: "major" | "minor" | "patch" | "prerelease", + type: "major" | "minor" | "patch" | "prerelease" ) { - const [major, minor, patch] = version.split(".").map((v) => + const [major, minor, patch] = version.split(".").map(v => Number.parseInt( // Remove everything after the - if there is one - v.includes("-") ? v.split("-")[0] : v, + v.includes("-") ? v.split("-")[0] : v ) ); switch (type) { @@ -98,9 +97,9 @@ async function createRelease(version: string) { // Verify zip files are valid before creating release const distFiles = Array.from(Deno.readDirSync("./dist")); const zipFiles = distFiles - .filter((entry) => entry.isFile) - .filter((entry) => entry.name.endsWith(".zip")) - .map((entry) => `./dist/${entry.name}`); + .filter(entry => entry.isFile) + .filter(entry => entry.name.endsWith(".zip")) + .map(entry => `./dist/${entry.name}`); console.log(zipFiles); @@ -127,6 +126,15 @@ async function createRelease(version: string) { releaseFlag, ]); if (result.exitCode !== 0) { + console.log( + "GitHub release creation failed with exit code:", + result.exitCode + ); + console.log("Common failure reasons:"); + console.log("- GitHub CLI not installed or not authenticated"); + console.log("- Release tag already exists"); + console.log("- No write permissions to repository"); + console.log("- Network connectivity issues"); logAndError(`Failed to create GitHub release for version ${version}`); } console.log(`✅ Created GitHub release for version ${version}`); @@ -168,10 +176,10 @@ async function cleanDist() { program .name("release") .description( - "A github release tool for the project. Valid types are: major, minor, patch, prerelease", + "A github release tool for the project. Valid types are: major, minor, patch, prerelease" ) .arguments("[type]") - .action(async (type) => { + .action(async type => { try { if (!type || type === "") { program.help(); @@ -181,11 +189,9 @@ program const validTypes = ["major", "minor", "patch", "prerelease"]; if (!validTypes.includes(type)) { console.error( - `Invalid release type: ${type}. Valid types are: ${ - validTypes.join( - ", ", - ) - }`, + `Invalid release type: ${type}. Valid types are: ${validTypes.join( + ", " + )}` ); process.exit(1); } @@ -200,14 +206,14 @@ program $ brew install gh - `, + ` ); process.exit(1); } process.on("SIGINT", () => { console.log( - "\nRelease process interrupted. Please confirm to exit (ctrl-c again to confirm).", + "\nRelease process interrupted. Please confirm to exit (ctrl-c again to confirm)." ); process.once("SIGINT", () => { console.log("Exiting...");