diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1eb0f4e..a73ace6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,3 +9,7 @@ jobs: steps: - uses: actions/checkout@v2 + - uses: denoland/setup-deno@v2 + - run: deno install + - run: deno fmt --check + - run: deno check ./src/index.ts diff --git a/README.md b/README.md index e72f71d..7e8d7ca 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ sf --version # 0.1.0 - 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` + - 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 diff --git a/deno.json b/deno.json index 5b320c2..b08b783 100644 --- a/deno.json +++ b/deno.json @@ -4,5 +4,11 @@ }, "imports": { "@std/assert": "jsr:@std/assert@1" + }, + "lint": { + "exclude": ["src/schema.ts"] + }, + "fmt": { + "exclude": ["src/schema.ts"] } } diff --git a/package.json b/package.json index 1c6f2a6..9f43184 100644 --- a/package.json +++ b/package.json @@ -45,4 +45,4 @@ "typescript": "^5.6.2" }, "version": "0.1.35" -} \ No newline at end of file +} diff --git a/src/checkVersion.ts b/src/checkVersion.ts index 9347128..4ca280e 100644 --- a/src/checkVersion.ts +++ b/src/checkVersion.ts @@ -68,7 +68,7 @@ async function checkProductionCLIVersion() { // Fetch from network 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}`); @@ -102,7 +102,7 @@ export async function checkVersion() { if (isPatchUpdate) { console.log( - chalk.cyan(`Automatically upgrading ${version} → ${latestVersion}`) + chalk.cyan(`Automatically upgrading ${version} → ${latestVersion}`), ); try { execSync("sf upgrade", { stdio: "inherit" }); @@ -130,7 +130,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 2d52e98..4bc4f89 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -27,7 +27,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 6c17172..e606a48 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -16,13 +16,13 @@ export function logLoginMessageAndQuit(): never { export async function logSessionTokenExpiredAndQuit(): Promise { await clearAuthFromConfig(); logAndQuit( - `\nYour session has expired. Please login again.\n\n\t$ ${loginCommand}\n` + `\nYour session has expired. Please login again.\n\n\t$ ${loginCommand}\n`, ); } 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 602beb9..9052354 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 17507ec..7b6e227 100644 --- a/src/helpers/units.ts +++ b/src/helpers/units.ts @@ -29,10 +29,11 @@ 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"); } @@ -57,7 +58,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 1b9aca1..2a3c887 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 4b2a73f..3ae85c8 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/index.ts b/src/index.ts index 772a824..730c16c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ import { registerDev } from "./lib/dev.ts"; import { registerLogin } from "./lib/login.ts"; import { registerMe } from "./lib/me.ts"; import { registerOrders } from "./lib/orders/index.tsx"; -import { IS_TRACKING_DISABLED, analytics } from "./lib/posthog.ts"; +import { analytics, IS_TRACKING_DISABLED } from "./lib/posthog.ts"; import { registerSell } from "./lib/sell.ts"; import { registerTokens } from "./lib/tokens.ts"; import { registerScale } from "./lib/updown.tsx"; @@ -68,7 +68,7 @@ const main = async () => { } } - program.exitOverride(error => { + program.exitOverride((error) => { let isError = true; switch (error.code) { @@ -91,8 +91,8 @@ const main = async () => { const nextArg = arr[i + 1]; if (nextArg && !nextArg.startsWith("-")) { (acc as Record)[key] = isNaN( - Number(nextArg) - ) + Number(nextArg), + ) ? nextArg : Number(nextArg); } else { @@ -103,7 +103,9 @@ const main = async () => { }, {}); analytics.track({ - event: `${process.argv[2] || "unknown"}${process.argv[3] ? "_" + process.argv[3] : ""}`, + event: `${process.argv[2] || "unknown"}${ + process.argv[3] ? "_" + process.argv[3] : "" + }`, properties: { ...args, shell: process.env.SHELL, diff --git a/src/lib/ConfirmInput.tsx b/src/lib/ConfirmInput.tsx index 2f2c7e4..bf10e05 100644 --- a/src/lib/ConfirmInput.tsx +++ b/src/lib/ConfirmInput.tsx @@ -26,7 +26,7 @@ const ConfirmInput: React.FC = ({ (newValue: string) => { onSubmit(yn(newValue, { default: isChecked })); }, - [isChecked, onSubmit] + [isChecked, onSubmit], ); return ( diff --git a/src/lib/Quote.tsx b/src/lib/Quote.tsx index 08b9961..e1c98e0 100644 --- a/src/lib/Quote.tsx +++ b/src/lib/Quote.tsx @@ -40,17 +40,17 @@ export default function QuoteDisplay(props: { quote: Quote }) { export type Quote = | { - price: number; - quantity: number; - start_at: string; - end_at: string; - instance_type: string; - } + price: number; + quantity: number; + start_at: string; + end_at: string; + instance_type: string; + } | { - price: number; - quantity: number; - start_at: string; - end_at: string; - contract_id: string; - } + price: number; + quantity: number; + start_at: string; + end_at: string; + contract_id: string; + } | null; diff --git a/src/lib/balance.ts b/src/lib/balance.ts index bd94013..53cad02 100644 --- a/src/lib/balance.ts +++ b/src/lib/balance.ts @@ -20,7 +20,7 @@ export function registerBalance(program: Command) { .command("balance") .description("Get account balance") .option("--json", "Output in JSON format") - .action(async options => { + .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"); @@ -91,7 +91,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 9c9eda8..b018f17 100644 --- a/src/lib/buy/index.tsx +++ b/src/lib/buy/index.tsx @@ -1,6 +1,6 @@ import { parseDate } from "chrono-node"; import type { Command } from "commander"; -import { Box, Text, render, useApp } from "ink"; +import { Box, render, Text, useApp } from "ink"; import Spinner from "ink-spinner"; import ms from "ms"; import dayjs from "npm:dayjs@1.11.13"; @@ -46,14 +46,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); @@ -136,16 +136,16 @@ function QuoteComponent(props: { options: SfBuyOptions }) { })(); }, []); - return isLoading ? ( - - + return isLoading + ? ( - Getting quote... + + + Getting quote... + - - ) : ( - - ); + ) + : ; } /* @@ -163,7 +163,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}`, ); } @@ -179,7 +179,7 @@ function QuoteAndBuy(props: { options: SfBuyOptions }) { (async () => { // Grab the price per GPU hour, either let pricePerGpuHour: number | null = parsePricePerGpuHour( - props.options.price + props.options.price, ); let startAt = parseStart(props.options.start); @@ -195,16 +195,15 @@ function QuoteAndBuy(props: { options: SfBuyOptions }) { const quote = await getQuoteFromParsedSfBuyOptions(props.options); if (!quote) { return logAndQuit( - "No quote found for the desired order. Try with a different start date, duration, or price." + "No quote found for the desired order. Try with a different start date, duration, or price.", ); } pricePerGpuHour = getPricePerGpuHourFromQuote(quote); - startAt = - quote.start_at === "NOW" - ? ("NOW" as const) - : parseStartAsDate(quote.start_at); + startAt = quote.start_at === "NOW" + ? ("NOW" as const) + : parseStartAsDate(quote.start_at); endsAt = dayjs(quote.end_at).toDate(); @@ -222,16 +221,16 @@ function QuoteAndBuy(props: { options: SfBuyOptions }) { })(); }, []); - return orderProps === null ? ( - - + return orderProps === null + ? ( - Getting quote... + + + Getting quote... + - - ) : ( - - ); + ) + : ; } function roundEndDate(endDate: Date) { @@ -251,7 +250,7 @@ function roundEndDate(endDate: Date) { export function getTotalPrice( pricePerGpuHour: number, size: number, - durationInHours: number + durationInHours: number, ) { return Math.ceil(pricePerGpuHour * size * GPUS_PER_NODE * durationInHours); } @@ -279,8 +278,8 @@ function BuyOrderPreview(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 ( @@ -341,15 +340,16 @@ function BuyOrder(props: BuyOrderProps) { const [order, setOrder] = useState(null); const intervalRef = useRef | null>(null); const [loadingMsg, setLoadingMsg] = useState( - "Placing order..." + "Placing order...", ); async function submitOrder() { const endsAt = 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({ @@ -357,7 +357,7 @@ function BuyOrder(props: BuyOrderProps) { totalPriceInCents: getTotalPrice( props.price, props.size, - realDurationInHours + realDurationInHours, ), startsAt: props.startAt, endsAt, @@ -371,16 +371,15 @@ function BuyOrder(props: BuyOrderProps) { const handleSubmit = useCallback( (submitValue: boolean) => { const endsAt = 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; const totalPriceInCents = getTotalPrice( props.price, props.size, - realDurationInHours + realDurationInHours, ); analytics.track({ @@ -427,7 +426,7 @@ function BuyOrder(props: BuyOrderProps) { }); submitOrder(); }, - [exit, setIsLoading] + [exit, setIsLoading], ); useEffect(() => { @@ -440,7 +439,7 @@ function BuyOrder(props: BuyOrderProps) { 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; } @@ -536,12 +535,12 @@ export async function placeBuyOrder(options: { }) { 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(); @@ -551,10 +550,9 @@ export async function placeBuyOrder(options: { 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, @@ -565,7 +563,7 @@ export async function placeBuyOrder(options: { 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(); @@ -578,7 +576,7 @@ export async function placeBuyOrder(options: { if (!data) { return logAndQuit( - `Failed to place order: Unexpected response from server: ${response}` + `Failed to place order: Unexpected response from server: ${response}`, ); } @@ -587,7 +585,7 @@ export async function placeBuyOrder(options: { export function getPricePerGpuHourFromQuote(quote: NonNullable) { const durationSeconds = dayjs(quote.end_at).diff( - parseStartAsDate(quote.start_at) + parseStartAsDate(quote.start_at), ); const durationHours = durationSeconds / 3600 / 1000; @@ -613,11 +611,11 @@ async function getQuoteFromParsedSfBuyOptions(options: SfBuyOptions) { const minDurationSeconds = Math.max( 1, - durationSeconds - Math.ceil(durationSeconds * 0.1) + durationSeconds - Math.ceil(durationSeconds * 0.1), ); const maxDurationSeconds = Math.max( durationSeconds + 3600, - durationSeconds + Math.ceil(durationSeconds * 0.1) + durationSeconds + Math.ceil(durationSeconds * 0.1), ); return await getQuote({ @@ -647,14 +645,12 @@ export async function getQuote(options: QuoteOptions) { side: "buy", instance_type: options.instanceType, quantity: options.quantity, - min_start_date: - options.minStartTime === "NOW" - ? ("NOW" as const) - : options.minStartTime.toISOString(), - max_start_date: - options.maxStartTime === "NOW" - ? ("NOW" as const) - : options.maxStartTime.toISOString(), + min_start_date: options.minStartTime === "NOW" + ? ("NOW" as const) + : options.minStartTime.toISOString(), + max_start_date: options.maxStartTime === "NOW" + ? ("NOW" as const) + : options.maxStartTime.toISOString(), min_duration: options.minDurationSeconds, max_duration: options.maxDurationSeconds, }, @@ -678,7 +674,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 2d05c2d..aaf790d 100644 --- a/src/lib/clusters/clusters.tsx +++ b/src/lib/clusters/clusters.tsx @@ -1,5 +1,5 @@ import type { Command } from "commander"; -import { Box, Text, render, useApp } from "ink"; +import { Box, render, Text, useApp } from "ink"; import Spinner from "ink-spinner"; import React, { useEffect, useState } from "react"; import yaml from "yaml"; @@ -8,8 +8,8 @@ import { logAndQuit } from "../../helpers/errors.ts"; import { Row } from "../Row.tsx"; import { decryptSecret, getKeys, regenerateKeys } from "./keys.tsx"; import { - KUBECONFIG_PATH, createKubeconfig, + KUBECONFIG_PATH, syncKubeconfig, } from "./kubeconfig.ts"; @@ -27,7 +27,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, @@ -47,7 +47,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, @@ -74,7 +74,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 }); }); @@ -83,7 +83,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, @@ -139,7 +139,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}`, ); } @@ -154,12 +154,12 @@ 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 || "", }))} - /> + />, ); } } @@ -171,7 +171,7 @@ function ClusterUserDisplay({ }) { return ( - {users.map(user => ( + {users.map((user) => ( {user.name} @@ -193,7 +193,8 @@ async function isCredentialReady(id: string) { const { data } = await api.GET("/v0/credentials"); const cred = data?.data.find( - credential => credential.id === id && credential.object === "k8s_credential" + (credential) => + credential.id === id && credential.object === "k8s_credential", ); if (!cred) { @@ -219,19 +220,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" + (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 || "", @@ -350,7 +351,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}`, ); } @@ -375,19 +376,19 @@ async function removeClusterUserAction({ id, }, }, - } + }, ); 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}`, ); } @@ -407,14 +408,14 @@ async function kubeconfigAction({ 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}`, ); } diff --git a/src/lib/clusters/keys.tsx b/src/lib/clusters/keys.tsx index 5ca8dda..a9aed05 100644 --- a/src/lib/clusters/keys.tsx +++ b/src/lib/clusters/keys.tsx @@ -52,7 +52,7 @@ export function decryptSecret(props: { 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 8763ab4..5187f44 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 f46ad44..811f27b 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,7 +94,8 @@ 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; @@ -105,7 +106,7 @@ export function createKubeconfig(props: { export function mergeNamedItems( items1: T[], - items2: T[] + items2: T[], ): T[] { const map = new Map(); for (const item of items1) { @@ -119,7 +120,7 @@ export function mergeNamedItems( export function mergeKubeconfigs( oldConfig: Kubeconfig, - newConfig?: Kubeconfig + newConfig?: Kubeconfig, ): Kubeconfig { if (!newConfig) { return oldConfig; @@ -129,15 +130,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 1325478..feb98d6 100644 --- a/src/lib/contracts/ContractDisplay.tsx +++ b/src/lib/contracts/ContractDisplay.tsx @@ -27,20 +27,18 @@ 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 - ] + ], ); const duration = next.getTime() - start.getTime(); @@ -48,16 +46,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) @@ -92,7 +90,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 e3d2184..684b2a1 100644 --- a/src/lib/contracts/index.tsx +++ b/src/lib/contracts/index.tsx @@ -22,7 +22,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 { @@ -31,7 +31,7 @@ export function registerContracts(program: Command) { render(); } // process.exit(0); - }) + }), ); } @@ -58,7 +58,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 f551321..1b84866 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,9 +112,11 @@ 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`, ); }); } @@ -133,14 +135,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; @@ -153,7 +155,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 a724780..39ae596 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 () => { @@ -59,7 +59,7 @@ async function createSession({ validation }: { validation: string }) { "Content-Type": "application/json", }, maxRedirects: 5, - } + }, ); return response.data as { diff --git a/src/lib/orders/OrderDisplay.tsx b/src/lib/orders/OrderDisplay.tsx index ee2a25f..16a9c68 100644 --- a/src/lib/orders/OrderDisplay.tsx +++ b/src/lib/orders/OrderDisplay.tsx @@ -1,4 +1,4 @@ -import { Box, Text, measureElement, useInput } from "ink"; +import { Box, measureElement, Text, useInput } from "ink"; import dayjs from "npm:dayjs@1.11.13"; import React, { useEffect } from "react"; import { Row } from "../Row.tsx"; @@ -9,14 +9,13 @@ 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 * durationInHours * GPUS_PER_NODE) / 100; + const pricePerGPUHour = order.price / + (order.quantity * durationInHours * GPUS_PER_NODE) / 100; const durationFormatted = formatDuration(duration); let executedPricePerGPUHour; if (order.execution_price) { - executedPricePerGPUHour = - (order.execution_price * order.quantity) / + executedPricePerGPUHour = (order.execution_price * order.quantity) / GPUS_PER_NODE / durationInHours / 100; @@ -105,7 +104,7 @@ function OrderMinimal(props: { )} - {durationFormatted} + {durationFormatted} {formatDateTime(props.order.start_at)} @@ -139,7 +138,7 @@ export function OrderDisplay(props: { expanded?: boolean; }) { const [activeTab, setActiveTab] = React.useState<"all" | "sell" | "buy">( - "all" + "all", ); useInput((input, key) => { @@ -183,16 +182,17 @@ export function OrderDisplay(props: { ); } - const orders = - activeTab === "all" - ? props.orders - : props.orders.filter(order => order.side === activeTab); + 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") + sellOrdersCount: props.orders.filter((order) => order.side === "sell") .length, - buyOrdersCount: props.orders.filter(order => order.side === "buy").length, + buyOrdersCount: props.orders.filter((order) => + order.side === "buy" + ).length, }; }, [props.orders]); @@ -205,18 +205,23 @@ export function OrderDisplay(props: { sellOrdersCount={sellOrdersCount} buyOrdersCount={buyOrdersCount} > - {orders.map(order => { - return props.expanded ? ( - - ) : ( - - ); + {orders.map((order) => { + return props.expanded + ? + : ( + + ); })} {orders.length === 0 && ( - There are 0 outstanding {activeTab === "all" ? "" : activeTab}{" "} + There are 0 outstanding {activeTab === "all" ? "" : activeTab} + {" "} orders right now. @@ -255,7 +260,7 @@ const reducer = (state: ScrollState, action: ScrollAction): ScrollState => { ...state, scrollTop: Math.min( state.innerHeight - state.height, - state.scrollTop + 1 + state.scrollTop + 1, ), }; @@ -264,7 +269,7 @@ const reducer = (state: ScrollState, action: ScrollAction): ScrollState => { ...state, scrollTop: Math.min( state.innerHeight - state.height, - state.scrollTop + NUMBER_OF_ORDERS_TO_DISPLAY + state.scrollTop + NUMBER_OF_ORDERS_TO_DISPLAY, ), }; @@ -330,18 +335,19 @@ export function ScrollArea({ 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 && + 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(() => { diff --git a/src/lib/orders/index.tsx b/src/lib/orders/index.tsx index 132d4de..d367ecc 100644 --- a/src/lib/orders/index.tsx +++ b/src/lib/orders/index.tsx @@ -1,4 +1,5 @@ import type { Command } from "commander"; +import { Option } from "commander"; import dayjs from "dayjs"; import { render } from "ink"; import duration from "npm:dayjs@1.11.13/plugin/duration.js"; @@ -57,75 +58,101 @@ export function registerOrders(program: Command) { .description("List orders") .option("--side ", "Filter by order side (buy or sell)") .option("-t, --type ", "Filter by instance type") - .option("--public", "Include public orders") + .addOption( + new Option( + "--public", + "Include public orders. Only includes open orders.", + ) + .conflicts(["onlyFilled", "onlyCancelled"]) + .implies({ + onlyOpen: true, + }), + ) .option("--min-price ", "Filter by minimum price (in cents)") .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)", + ) + .addOption( + new Option("--only-open", "Show only open orders") + .conflicts(["onlyFilled", "onlyCancelled"]), + ) + .addOption( + new Option("--exclude-filled", "Exclude filled orders") + .conflicts(["onlyFilled"]), + ) + .addOption( + new Option("--only-filled", "Show only filled orders") + .conflicts(["excludeFilled", "onlyCancelled", "onlyOpen", "public"]), ) - .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", + ) + .addOption( + new Option("--only-cancelled", "Show only cancelled orders") + .conflicts(["onlyFilled", "onlyOpen", "public"]) + .implies({ + includeCancelled: true, + }), ) - .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 minDuration = parseDurationArgument(options.minDuration); const maxDuration = parseDurationArgument(options.maxDuration); - const orders = await getOrders({ side: options.side, instance_type: options.type, @@ -263,7 +290,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/posthog.ts b/src/lib/posthog.ts index 69a296b..beac24c 100644 --- a/src/lib/posthog.ts +++ b/src/lib/posthog.ts @@ -8,7 +8,7 @@ const postHogClient = new PostHog( host: "https://us.posthog.com", flushAt: 1, flushInterval: 0, - } + }, ); // Uncomment this out to see Posthog debugging logs. // postHogClient.debug(); diff --git a/src/lib/sell.ts b/src/lib/sell.ts index f0cbc66..5070858 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,14 +84,15 @@ 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}`, ); } @@ -133,7 +134,7 @@ async function placeSellOrder(options: { priceCents, totalDurationSecs, nodes, - GPUS_PER_NODE + GPUS_PER_NODE, ); const params: PlaceSellOrderParameters = { @@ -154,11 +155,13 @@ 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 52b198e..180091f 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,20 +391,19 @@ 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(); @@ -414,8 +413,9 @@ 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 5898999..0bdb5c3 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -97,9 +97,11 @@ 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: "", }); @@ -138,7 +140,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( [ @@ -149,7 +151,7 @@ async function createTokenAction() { chalk.magenta(""), chalk.green('"'), chalk.gray(" }"), - ].join("") + ].join(""), ); console.log("\n"); @@ -159,7 +161,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"); @@ -228,7 +230,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); @@ -280,17 +282,19 @@ 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 a0da3a6..6168226 100644 --- a/src/lib/updown.tsx +++ b/src/lib/updown.tsx @@ -21,7 +21,7 @@ export function registerScale(program: Command) { .description("Scale GPUs or show current procurement details") .option( "-n, --accelerators ", - "Set number of GPUs (0 to turn off)" + "Set number of GPUs (0 to turn off)", ) .option("-t, --type ", "Specify node type", "h100i") .option("-d, --duration ", "Minimum duration", "2h") @@ -33,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(); @@ -60,10 +60,12 @@ 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< + React.ReactNode + >(null); + const [balanceLowMessage, setBalanceLowMessage] = useState( + null, + ); const [procurementResult, setProcurementResult] = useState(null); const [ displayedPricePerNodeHourInCents, @@ -94,8 +96,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"); @@ -109,7 +111,7 @@ function ScaleCommand(props: { You can't afford this. Available: $ {(balance.available.cents / 100).toFixed(2)}, Needed: $ {(totalPriceInCents / 100).toFixed(2)} - + , ); return; } @@ -121,7 +123,7 @@ function ScaleCommand(props: { accelerators={accelerators} totalPriceInCents={totalPriceInCents} type={type} - /> + />, ); if (props.yes) { @@ -168,7 +170,7 @@ function ScaleCommand(props: { exit(); } }, - [props.duration, props.price, exit] + [props.duration, props.price, exit], ); const handleSubmit = useCallback( @@ -178,8 +180,9 @@ 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."); @@ -192,7 +195,7 @@ function ScaleCommand(props: { pricePerNodeHourInCents: displayedPricePerNodeHourInCents, }); }, - [submitProcurement, displayedPricePerNodeHourInCents, exit] + [submitProcurement, displayedPricePerNodeHourInCents, exit], ); return ( @@ -246,12 +249,12 @@ function ShowCommand(props: { type: string }) { const procurements = await client.GET("/v0/procurements"); if (!procurements.response.ok) { throw new Error( - procurements.error?.message || "Failed to list procurements" + procurements.error?.message || "Failed to list procurements", ); } const current = procurements.data?.data.find( - (p: any) => p.instance_type === props.type + (p: any) => p.instance_type === props.type, ); if (!current) { setInfo(null); @@ -297,7 +300,7 @@ function ShowCommand(props: { type: string }) { const quantity = info.quantity * GPUS_PER_NODE; const pricePerNodeHourInCents = info.max_price_per_node_hour; const pricePerGpuHourInCents = Math.ceil( - pricePerNodeHourInCents / GPUS_PER_NODE + pricePerNodeHourInCents / GPUS_PER_NODE, ); return ( @@ -353,7 +356,9 @@ function ConfirmationMessage(props: { ); @@ -415,7 +420,7 @@ async function getDefaultProcurementOptions(props: { let quotePricePerNodeHourInCents: number; if (quote) { quotePricePerNodeHourInCents = Math.ceil( - quote.price / (durationHours * nodesRequired) + quote.price / (durationHours * nodesRequired), ); } else { quotePricePerNodeHourInCents = DEFAULT_PRICE_PER_GPU_HOUR_IN_CENTS; @@ -423,8 +428,8 @@ async function getDefaultProcurementOptions(props: { pricePerNodeHourInCents = quotePricePerNodeHourInCents; } - const totalPriceInCents = - pricePerNodeHourInCents * nodesRequired * durationHours; + const totalPriceInCents = pricePerNodeHourInCents * nodesRequired * + durationHours; return { durationHours, @@ -452,12 +457,12 @@ async function scaleToCount({ const procurements = await client.GET("/v0/procurements"); if (!procurements.response.ok) { throw new Error( - procurements.error?.message || "Failed to list procurements" + procurements.error?.message || "Failed to list procurements", ); } const existingProcurement = procurements.data?.data.find( - (p: any) => p.instance_type === type + (p: any) => p.instance_type === type, ); if (existingProcurement) { @@ -495,7 +500,7 @@ async function scaleDown(type: string) { const procurements = await client.GET("/v0/procurements"); if (!procurements.response.ok) { throw new Error( - procurements.error?.message || "Failed to list procurements" + procurements.error?.message || "Failed to list procurements", ); } diff --git a/src/lib/upgrade.ts b/src/lib/upgrade.ts index b82b79f..9811883 100644 --- a/src/lib/upgrade.ts +++ b/src/lib/upgrade.ts @@ -28,13 +28,14 @@ export function registerUpgrade(program: Command) { .command("upgrade") .argument("[version]", "The version to upgrade to") .description("Upgrade to the latest version or a specific version") - .action(async version => { + .action(async (version) => { const spinner = ora(); const currentVersion = program.version(); 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) { @@ -53,7 +54,7 @@ export function registerUpgrade(program: Command) { const isOnLatestVersion = await getIsOnLatestVersion(currentVersion); if (isOnLatestVersion) { spinner.succeed( - `You are already on the latest version (${currentVersion}).` + `You are already on the latest version (${currentVersion}).`, ); process.exit(0); return; @@ -62,7 +63,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/scripts/release.ts b/src/scripts/release.ts index 22fd839..c28d3aa 100644 --- a/src/scripts/release.ts +++ b/src/scripts/release.ts @@ -9,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) { @@ -97,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); @@ -128,7 +128,7 @@ async function createRelease(version: string) { if (result.exitCode !== 0) { console.log( "GitHub release creation failed with exit code:", - result.exitCode + result.exitCode, ); console.log("Common failure reasons:"); console.log("- GitHub CLI not installed or not authenticated"); @@ -176,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(); @@ -189,9 +189,11 @@ 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); } @@ -206,14 +208,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...");