From 8a37662c0f8e6cfd96b142dc4896d536676c0638 Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 10:59:48 -0700 Subject: [PATCH 01/10] isLoggedIn() --- package.json | 1 - src/helpers/config.ts | 6 +++ src/helpers/errors.ts | 41 +++++++++++++++ src/helpers/urls.ts | 18 +++---- src/index.ts | 6 ++- src/lib/balance.ts | 9 ++-- src/lib/buy.ts | 9 ++-- src/lib/contracts.ts | 7 ++- src/lib/instances.ts | 7 ++- src/lib/orders.ts | 7 ++- src/lib/sell.ts | 11 ++-- src/lib/tokens.ts | 114 ++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 197 insertions(+), 39 deletions(-) create mode 100644 src/lib/tokens.ts diff --git a/package.json b/package.json index 2bb93d89..00ec1dd1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "lint": "biome lint --write ./src", "check": "biome check ./src", "dev": "IS_DEVELOPMENT_CLI_ENV=true bun run src/index.ts", - "pack": "bun build src/index.ts --outfile dist/cli.js", "release": "bun run src/scripts/release.ts", "prod": "bun run src/index.ts" }, diff --git a/src/helpers/config.ts b/src/helpers/config.ts index de042f2b..9008df56 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -23,6 +23,8 @@ const ConfigDefaults = process.env.IS_DEVELOPMENT_CLI_ENV ? DevelopmentConfigDefaults : ProductionConfigDefaults; +// -- + export async function saveConfig(config: Partial): Promise { const configPath = getConfigPath(); const configData = JSON.stringify(config, null, 2); @@ -104,3 +106,7 @@ export async function getAuthorizationHeader() { const token = await getAuthToken(); return { Authorization: `Bearer ${token}` }; } + +export function isLoggedIn() { + return !!getAuthToken(); +} diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 3b5ed27e..9c96ab85 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -1,3 +1,44 @@ +export const ApiErrorCode = { + Base: { + InvalidRequest: "invalid_request", + NotAuthenticated: "not_authenticated", + Unauthorized: "unauthorized", + NotFound: "not_found", + RouteNotFound: "route_not_found", + TooManyRequests: "too_many_requests", + InternalServer: "internal_server", + }, + Accounts: { + NotFound: "account.not_found", + }, + Orders: { + InvalidId: "order.invalid_id", + InvalidPrice: "order.invalid_price", + InvalidQuantity: "order.invalid_quantity", + InvalidStart: "order.invalid_start", + InvalidDuration: "order.invalid_duration", + InsufficientFunds: "order.insufficient_funds", + NotFound: "order.not_found", + }, + Tokens: { + TokenNotFound: "token.not_found", + InvalidTokenCreateOriginClient: "token.invalid_token_create_origin_client", + InvalidTokenExpirationDuration: "token.invalid_token_expiration_duration", + MaxTokenLimitReached: "token.max_token_limit_reached", + }, +}; + +export const HTTPStatusCode = { + BadRequest: 400, + Unauthorized: 401, + Forbidden: 403, + NotFound: 404, + TooManyRequests: 429, + InternalServerError: 500, +}; + +// -- + export function logAndQuit(message: string) { console.error(message); process.exit(1); diff --git a/src/helpers/urls.ts b/src/helpers/urls.ts index 694e8e28..24768689 100644 --- a/src/helpers/urls.ts +++ b/src/helpers/urls.ts @@ -21,13 +21,14 @@ const apiPaths = { contracts_get: ({ id }: { id: string }) => `/v0/contracts/${id}`, balance_get: "/v0/balance", + + tokens_create: "/v0/tokens", + tokens_list: "/v0/tokens", + tokens_delete: ({ id }: { id: string }) => `/v0/tokens/${id}`, }; -export async function getWebAppUrl< - K extends keyof typeof webPaths, - V extends Extract<(typeof webPaths)[K], (...args: any) => any>, ->(key: K, params: Parameters[0]): Promise; -export async function getWebAppUrl(key: keyof typeof webPaths): Promise; +// -- + export async function getWebAppUrl( key: keyof typeof webPaths, params?: any, @@ -37,14 +38,10 @@ export async function getWebAppUrl( if (typeof path === "function") { return config.webapp_url + path(params); } + return config.webapp_url + path; } -export async function getApiUrl< - K extends keyof typeof apiPaths, - V extends Extract<(typeof apiPaths)[K], (...args: any) => any>, ->(key: K, params: Parameters[0]): Promise; -export async function getApiUrl(key: keyof typeof apiPaths): Promise; export async function getApiUrl( key: keyof typeof apiPaths, params?: any, @@ -54,5 +51,6 @@ export async function getApiUrl( if (typeof path === "function") { return config.api_url + path(params); } + return config.api_url + path; } diff --git a/src/index.ts b/src/index.ts index 6f13d2d6..899c5da9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,13 +11,14 @@ import { registerLogin } from "./lib/login"; import { registerOrders } from "./lib/orders"; import { registerSell } from "./lib/sell"; import { registerSSH } from "./lib/ssh"; +import { registerTokens } from "./lib/tokens"; import { registerUpgrade } from "./lib/upgrade"; const program = new Command(); program .name("sf") - .description("San Francisco Compute command line tool.") + .description("The San Francisco Compute command line tool.") .version(version); // commands @@ -29,9 +30,10 @@ registerInstances(program); registerSSH(program); registerSell(program); registerBalance(program); +registerTokens(program); registerUpgrade(program); -// (only development commands) +// (development commands) registerDev(program); program.parse(Bun.argv); diff --git a/src/lib/balance.ts b/src/lib/balance.ts index e639b11e..f18f1cfb 100644 --- a/src/lib/balance.ts +++ b/src/lib/balance.ts @@ -1,7 +1,7 @@ import chalk from "chalk"; import Table from "cli-table3"; import type { Command } from "commander"; -import { loadConfig } from "../helpers/config"; +import { isLoggedIn, loadConfig } from "../helpers/config"; import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; import type { Centicents } from "../helpers/units"; import { getApiUrl } from "../helpers/urls"; @@ -71,14 +71,15 @@ async function getBalance(): Promise<{ available: { centicents: Centicents; whole: number }; reserved: { centicents: Centicents; whole: number }; }> { - const config = await loadConfig(); - if (!config.auth_token) { + if (!isLoggedIn()) { logLoginMessageAndQuit(); + return { available: { centicents: 0, whole: 0 }, reserved: { centicents: 0, whole: 0 }, }; } + const config = await loadConfig(); const response = await fetch(await getApiUrl("balance_get"), { method: "GET", @@ -91,6 +92,7 @@ async function getBalance(): Promise<{ if (!response.ok) { if (response.status === 401) { logLoginMessageAndQuit(); + return { available: { centicents: 0, whole: 0 }, reserved: { centicents: 0, whole: 0 }, @@ -98,6 +100,7 @@ async function getBalance(): Promise<{ } logAndQuit(`Failed to fetch balance: ${response.statusText}`); + return { available: { centicents: 0, whole: 0 }, reserved: { centicents: 0, whole: 0 }, diff --git a/src/lib/buy.ts b/src/lib/buy.ts index 0dd47962..4c86e402 100644 --- a/src/lib/buy.ts +++ b/src/lib/buy.ts @@ -6,7 +6,7 @@ import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import relativeTime from "dayjs/plugin/relativeTime"; import parseDuration from "parse-duration"; -import { loadConfig } from "../helpers/config"; +import { getAuthToken, isLoggedIn } from "../helpers/config"; import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; import { getApiUrl } from "../helpers/urls"; import { @@ -56,7 +56,7 @@ function confirmPlaceOrderParametersMessage(params: PlaceOrderParameters) { const fromNowTime = dayjs(startDate).fromNow(); const humanReadableStartAt = dayjs(startDate).format("MM/DD/YYYY hh:mm A"); - const centicentsAsDollars = (price / 10000).toFixed(2); + const centicentsAsDollars = (price / 10_000).toFixed(2); const durationHumanReadable = formatDuration(duration * 1000); const topLine = `${c.green(quantity)} ${c.green(instance_type)} nodes for ${c.green(durationHumanReadable)} starting ${c.green(humanReadableStartAt)} (${c.green(fromNowTime)})`; @@ -96,8 +96,7 @@ interface PlaceBuyOrderArguments { async function placeBuyOrder(props: PlaceBuyOrderArguments) { const { type, duration, price, quantity, start } = props; - const config = await loadConfig(); - if (!config.auth_token) { + if (!isLoggedIn()) { return logLoginMessageAndQuit(); } @@ -134,7 +133,7 @@ async function placeBuyOrder(props: PlaceBuyOrderArguments) { body: JSON.stringify(params), headers: { "Content-Type": "application/json", - Authorization: `Bearer ${config.auth_token}`, + Authorization: `Bearer ${await getAuthToken()}`, }, }); diff --git a/src/lib/contracts.ts b/src/lib/contracts.ts index 071508f2..0ab49760 100644 --- a/src/lib/contracts.ts +++ b/src/lib/contracts.ts @@ -1,6 +1,6 @@ import Table from "cli-table3"; import { Command } from "commander"; -import { loadConfig } from "../helpers/config"; +import { getAuthToken, isLoggedIn } from "../helpers/config"; import { logLoginMessageAndQuit } from "../helpers/errors"; import { getApiUrl } from "../helpers/urls"; @@ -70,8 +70,7 @@ export function registerContracts(program: Command) { } async function listContracts() { - const config = await loadConfig(); - if (!config.auth_token) { + if (!isLoggedIn()) { return logLoginMessageAndQuit(); } @@ -79,7 +78,7 @@ async function listContracts() { method: "GET", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${config.auth_token}`, + Authorization: `Bearer ${await getAuthToken()}`, }, }); diff --git a/src/lib/instances.ts b/src/lib/instances.ts index 23980294..a28e77ea 100644 --- a/src/lib/instances.ts +++ b/src/lib/instances.ts @@ -1,7 +1,7 @@ import chalk, { type ChalkInstance } from "chalk"; import Table from "cli-table3"; import type { Command } from "commander"; -import { loadConfig } from "../helpers/config"; +import { getAuthToken, isLoggedIn } from "../helpers/config"; import { logLoginMessageAndQuit } from "../helpers/errors"; import { getApiUrl } from "../helpers/urls"; @@ -148,8 +148,7 @@ const colorInstanceType = (instanceType: InstanceType) => async function getInstances({ clusterId, }: { clusterId?: string }): Promise> { - const config = await loadConfig(); - if (!config.auth_token) { + if (!isLoggedIn()) { logLoginMessageAndQuit(); } @@ -161,7 +160,7 @@ async function getInstances({ method: "GET", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${config.auth_token}`, + Authorization: `Bearer ${await getAuthToken()}`, }, }); diff --git a/src/lib/orders.ts b/src/lib/orders.ts index 44d42eb5..721544e6 100644 --- a/src/lib/orders.ts +++ b/src/lib/orders.ts @@ -3,7 +3,7 @@ import type { Command } from "commander"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import relativeTime from "dayjs/plugin/relativeTime"; -import { loadConfig } from "../helpers/config"; +import { getAuthToken, isLoggedIn } from "../helpers/config"; import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; import { getApiUrl } from "../helpers/urls"; import type { ListResponseBody, Order } from "./types"; @@ -157,8 +157,7 @@ export async function getOrders(props: { side?: "buy" | "sell"; include_public?: boolean; }) { - const config = await loadConfig(); - if (!config.auth_token) { + if (!isLoggedIn()) { logLoginMessageAndQuit(); } @@ -175,7 +174,7 @@ export async function getOrders(props: { method: "GET", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${config.auth_token}`, + Authorization: `Bearer ${await getAuthToken()}`, }, }); diff --git a/src/lib/sell.ts b/src/lib/sell.ts index 7d1621d3..05cfa82f 100644 --- a/src/lib/sell.ts +++ b/src/lib/sell.ts @@ -1,7 +1,7 @@ import * as chrono from "chrono-node"; import type { Command } from "commander"; import parseDuration from "parse-duration"; -import { loadConfig } from "../helpers/config"; +import { getAuthToken, isLoggedIn } from "../helpers/config"; import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; import { getApiUrl } from "../helpers/urls"; import type { PlaceSellOrderParameters } from "./orders"; @@ -39,8 +39,7 @@ async function placeSellOrder(options: { duration: string; flags?: Record; }) { - const config = await loadConfig(); - if (!config.auth_token) { + if (!isLoggedIn()) { return logLoginMessageAndQuit(); } @@ -66,7 +65,7 @@ async function placeSellOrder(options: { ...flags, }; - const res = await postSellOrder(config.auth_token, params); + const res = await postSellOrder(params); if (!res.ok) { return logAndQuit("Failed to place sell order"); } @@ -74,13 +73,13 @@ async function placeSellOrder(options: { console.log(data); } -async function postSellOrder(token: string, params: PlaceSellOrderParameters) { +async function postSellOrder(params: PlaceSellOrderParameters) { return await fetch(await getApiUrl("orders_create"), { method: "POST", body: JSON.stringify(params), headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${await getAuthToken()}`, }, }); } diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts new file mode 100644 index 00000000..4065d6cd --- /dev/null +++ b/src/lib/tokens.ts @@ -0,0 +1,114 @@ +import chalk from "chalk"; +import Table from "cli-table3"; +import type { Command } from "commander"; +import { loadConfig } from "../helpers/config"; +import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; +import type { Centicents } from "../helpers/units"; +import { getApiUrl } from "../helpers/urls"; + +export function registerTokens(program: Command) { + program + .command("balance") + .description("Get account balance") + .option("--json", "Output in JSON format") + .action(async (options) => { + const { + available: { whole: availableWhole, centicents: availableCenticents }, + reserved: { whole: reservedWhole, centicents: reservedCenticents }, + } = await getBalance(); + + if (options.json) { + const jsonOutput = { + available: { + whole: availableWhole, + centicents: availableCenticents, + }, + reserved: { + whole: reservedWhole, + centicents: reservedCenticents, + }, + }; + console.log(JSON.stringify(jsonOutput, null, 2)); + } else { + const formattedAvailable = usdFormatter.format(availableWhole); + const formattedReserved = usdFormatter.format(reservedWhole); + + const table = new Table({ + head: [ + chalk.gray("Type"), + chalk.gray("Amount"), + chalk.gray("Centicents (1/100th of a cent)"), + ], + colWidths: [15, 15, 35], + }); + + table.push( + [ + "Available", + chalk.green(formattedAvailable), + chalk.green(availableCenticents.toLocaleString()), + ], + [ + "Reserved", + chalk.gray(formattedReserved), + chalk.gray(reservedCenticents.toLocaleString()), + ], + ); + + console.log(table.toString() + "\n"); + } + + process.exit(0); + }); +} + +async function getBalance(): Promise<{ + available: { centicents: Centicents; whole: number }; + reserved: { centicents: Centicents; whole: number }; +}> { + const config = await loadConfig(); + if (!config.auth_token) { + logLoginMessageAndQuit(); + return { + available: { centicents: 0, whole: 0 }, + reserved: { centicents: 0, whole: 0 }, + }; + } + + const response = await fetch(await getApiUrl("balance_get"), { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${config.auth_token}`, + }, + }); + + if (!response.ok) { + if (response.status === 401) { + logLoginMessageAndQuit(); + return { + available: { centicents: 0, whole: 0 }, + reserved: { centicents: 0, whole: 0 }, + }; + } + + logAndQuit(`Failed to fetch balance: ${response.statusText}`); + return { + available: { centicents: 0, whole: 0 }, + reserved: { centicents: 0, whole: 0 }, + }; + } + + const data = await response.json(); + + return { + available: { + centicents: data.available.amount, + whole: data.available.amount / 10_000, + }, + reserved: { + centicents: data.reserved.amount, + whole: data.reserved.amount / 10_000, + }, + }; +} From 2681a93e5b074e2043c448e39534bd565b240c20 Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 11:30:50 -0700 Subject: [PATCH 02/10] await --- src/helpers/config.ts | 5 +- src/lib/balance.ts | 5 +- src/lib/buy.ts | 5 +- src/lib/contracts.ts | 3 +- src/lib/instances.ts | 3 +- src/lib/orders.ts | 3 +- src/lib/sell.ts | 3 +- src/lib/tokens.ts | 156 +++++++++++++++++------------------------- 8 files changed, 78 insertions(+), 105 deletions(-) diff --git a/src/helpers/config.ts b/src/helpers/config.ts index 9008df56..0eab40ad 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -107,6 +107,7 @@ export async function getAuthorizationHeader() { return { Authorization: `Bearer ${token}` }; } -export function isLoggedIn() { - return !!getAuthToken(); +export async function isLoggedIn() { + const authToken = await getAuthToken(); + return !!authToken; } diff --git a/src/lib/balance.ts b/src/lib/balance.ts index f18f1cfb..b4a376a9 100644 --- a/src/lib/balance.ts +++ b/src/lib/balance.ts @@ -1,7 +1,7 @@ import chalk from "chalk"; import Table from "cli-table3"; import type { Command } from "commander"; -import { isLoggedIn, loadConfig } from "../helpers/config"; +import { isLoggedIn, isLoggedIn, loadConfig } from "../helpers/config"; import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; import type { Centicents } from "../helpers/units"; import { getApiUrl } from "../helpers/urls"; @@ -71,7 +71,8 @@ async function getBalance(): Promise<{ available: { centicents: Centicents; whole: number }; reserved: { centicents: Centicents; whole: number }; }> { - if (!isLoggedIn()) { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { logLoginMessageAndQuit(); return { diff --git a/src/lib/buy.ts b/src/lib/buy.ts index 4c86e402..b0475070 100644 --- a/src/lib/buy.ts +++ b/src/lib/buy.ts @@ -95,10 +95,11 @@ interface PlaceBuyOrderArguments { } async function placeBuyOrder(props: PlaceBuyOrderArguments) { - const { type, duration, price, quantity, start } = props; - if (!isLoggedIn()) { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { return logLoginMessageAndQuit(); } + const { type, duration, price, quantity, start } = props; const orderQuantity = quantity ? Number(quantity) : 1; const durationMs = parseDuration(duration); diff --git a/src/lib/contracts.ts b/src/lib/contracts.ts index 0ab49760..f1e7782a 100644 --- a/src/lib/contracts.ts +++ b/src/lib/contracts.ts @@ -70,7 +70,8 @@ export function registerContracts(program: Command) { } async function listContracts() { - if (!isLoggedIn()) { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { return logLoginMessageAndQuit(); } diff --git a/src/lib/instances.ts b/src/lib/instances.ts index a28e77ea..d04fd9ab 100644 --- a/src/lib/instances.ts +++ b/src/lib/instances.ts @@ -148,7 +148,8 @@ const colorInstanceType = (instanceType: InstanceType) => async function getInstances({ clusterId, }: { clusterId?: string }): Promise> { - if (!isLoggedIn()) { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { logLoginMessageAndQuit(); } diff --git a/src/lib/orders.ts b/src/lib/orders.ts index 721544e6..f2a000d2 100644 --- a/src/lib/orders.ts +++ b/src/lib/orders.ts @@ -157,7 +157,8 @@ export async function getOrders(props: { side?: "buy" | "sell"; include_public?: boolean; }) { - if (!isLoggedIn()) { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { logLoginMessageAndQuit(); } diff --git a/src/lib/sell.ts b/src/lib/sell.ts index 05cfa82f..608e22c9 100644 --- a/src/lib/sell.ts +++ b/src/lib/sell.ts @@ -39,7 +39,8 @@ async function placeSellOrder(options: { duration: string; flags?: Record; }) { - if (!isLoggedIn()) { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { return logLoginMessageAndQuit(); } diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index 4065d6cd..dcae0bc3 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -1,114 +1,80 @@ -import chalk from "chalk"; -import Table from "cli-table3"; import type { Command } from "commander"; -import { loadConfig } from "../helpers/config"; -import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; -import type { Centicents } from "../helpers/units"; -import { getApiUrl } from "../helpers/urls"; +import { isLoggedIn } from "../helpers/config"; +import { logLoginMessageAndQuit } from "../helpers/errors"; export function registerTokens(program: Command) { - program - .command("balance") - .description("Get account balance") - .option("--json", "Output in JSON format") - .action(async (options) => { - const { - available: { whole: availableWhole, centicents: availableCenticents }, - reserved: { whole: reservedWhole, centicents: reservedCenticents }, - } = await getBalance(); + const tokens = program + .command("tokens") + .description("Manage account access tokens."); - if (options.json) { - const jsonOutput = { - available: { - whole: availableWhole, - centicents: availableCenticents, - }, - reserved: { - whole: reservedWhole, - centicents: reservedCenticents, - }, - }; - console.log(JSON.stringify(jsonOutput, null, 2)); - } else { - const formattedAvailable = usdFormatter.format(availableWhole); - const formattedReserved = usdFormatter.format(reservedWhole); + tokens + .command("create") + .description("Create a new access token") + .action(createTokenAction); - const table = new Table({ - head: [ - chalk.gray("Type"), - chalk.gray("Amount"), - chalk.gray("Centicents (1/100th of a cent)"), - ], - colWidths: [15, 15, 35], - }); + // tokens + // .command("ls") + // .description("List all tokens") + // .option("--include-system, -is", "Include system tokens") + // .action(listTokensAction); - table.push( - [ - "Available", - chalk.green(formattedAvailable), - chalk.green(availableCenticents.toLocaleString()), - ], - [ - "Reserved", - chalk.gray(formattedReserved), - chalk.gray(reservedCenticents.toLocaleString()), - ], - ); + // tokens + // .command("delete") + // .description("Delete a token") + // .option("--name ", "Specify the token name") + // .option("--id ", "Specify the token ID") + // .action(deleteTokenAction); +} + +// -- - console.log(table.toString() + "\n"); - } +async function createTokenAction() { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { + logLoginMessageAndQuit(); + } - process.exit(0); - }); + // const response = await fetch(await getApiUrl("tokens_create"), { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // Authorization: `Bearer ${config.auth_token}`, + // }, + // }); + + console.log("Creating token..."); } -async function getBalance(): Promise<{ - available: { centicents: Centicents; whole: number }; - reserved: { centicents: Centicents; whole: number }; -}> { - const config = await loadConfig(); - if (!config.auth_token) { +async function listTokensAction() { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { logLoginMessageAndQuit(); - return { - available: { centicents: 0, whole: 0 }, - reserved: { centicents: 0, whole: 0 }, - }; } - const response = await fetch(await getApiUrl("balance_get"), { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${config.auth_token}`, - }, - }); + // const response = await fetch(await getApiUrl("tokens_create"), { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // Authorization: `Bearer ${config.auth_token}`, + // }, + // }); - if (!response.ok) { - if (response.status === 401) { - logLoginMessageAndQuit(); - return { - available: { centicents: 0, whole: 0 }, - reserved: { centicents: 0, whole: 0 }, - }; - } + console.log("Listing tokens..."); +} - logAndQuit(`Failed to fetch balance: ${response.statusText}`); - return { - available: { centicents: 0, whole: 0 }, - reserved: { centicents: 0, whole: 0 }, - }; +async function deleteTokenAction() { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { + logLoginMessageAndQuit(); } - const data = await response.json(); + // const response = await fetch(await getApiUrl("tokens_create"), { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // Authorization: `Bearer ${config.auth_token}`, + // }, + // }); - return { - available: { - centicents: data.available.amount, - whole: data.available.amount / 10_000, - }, - reserved: { - centicents: data.reserved.amount, - whole: data.reserved.amount / 10_000, - }, - }; + console.log("Deleting tokens..."); } From 47ff15e264416ad56391e23df02b0f0fe19ad5f8 Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 12:48:02 -0700 Subject: [PATCH 03/10] consistent prompting --- src/lib/buy.ts | 30 ++++++++---------------------- src/lib/tokens.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/lib/buy.ts b/src/lib/buy.ts index b0475070..4c05321a 100644 --- a/src/lib/buy.ts +++ b/src/lib/buy.ts @@ -1,4 +1,3 @@ -import readline from "node:readline"; import c from "chalk"; import * as chrono from "chrono-node"; import type { Command } from "commander"; @@ -14,6 +13,7 @@ import { formatDuration, priceToCenticents, } from "./orders"; +import { confirm } from "@inquirer/prompts"; dayjs.extend(relativeTime); dayjs.extend(duration); @@ -33,22 +33,6 @@ export function registerBuy(program: Command) { }); } -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, -}); - -async function prompt(msg: string) { - const answer = await new Promise((resolve) => - rl.question(msg, (ans) => { - rl.close(); - resolve(ans); - }), - ); - - return answer; -} - function confirmPlaceOrderParametersMessage(params: PlaceOrderParameters) { const { quantity, price, instance_type, duration, start_at } = params; @@ -96,7 +80,7 @@ interface PlaceBuyOrderArguments { async function placeBuyOrder(props: PlaceBuyOrderArguments) { const loggedIn = await isLoggedIn(); - if (!loggedIn) { + if (loggedIn) { return logLoginMessageAndQuit(); } const { type, duration, price, quantity, start } = props; @@ -120,11 +104,13 @@ async function placeBuyOrder(props: PlaceBuyOrderArguments) { start_at: startDate.toISOString(), }; - const msg = confirmPlaceOrderParametersMessage(params); - if (!props.yes) { - const answer = await prompt(msg); - if (answer !== "y") { + const placeBuyOrderConfirmed = await confirm({ + message: confirmPlaceOrderParametersMessage(params), + default: false, + }); + + if (!placeBuyOrderConfirmed) { return logAndQuit("Order cancelled"); } } diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index dcae0bc3..306258bb 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -1,6 +1,18 @@ import type { Command } from "commander"; import { isLoggedIn } from "../helpers/config"; import { logLoginMessageAndQuit } from "../helpers/errors"; +import { input } from "@inquirer/prompts"; +import ora from "ora"; +import chalk from "chalk"; + +export const TOKEN_EXPIRATION_SECONDS = { + IN_7_DAYS: 7 * 24 * 60 * 60, + IN_14_DAYS: 14 * 24 * 60 * 60, + IN_30_DAYS: 30 * 24 * 60 * 60, + IN_60_DAYS: 60 * 24 * 60 * 60, + IN_90_DAYS: 90 * 24 * 60 * 60, + IN_100_YEARS: 100 * 365 * 24 * 60 * 60, +}; export function registerTokens(program: Command) { const tokens = program @@ -34,6 +46,35 @@ async function createTokenAction() { logLoginMessageAndQuit(); } + // collect duration + const expiresInSeconds = await select({ + message: "Select token expiration:", + default: TOKEN_EXPIRATION_SECONDS.IN_100_YEARS, + choices: [ + { name: "1 week", value: TOKEN_EXPIRATION_SECONDS.IN_7_DAYS }, + { name: "2 weeks", value: TOKEN_EXPIRATION_SECONDS.IN_14_DAYS }, + { name: "1 month", value: TOKEN_EXPIRATION_SECONDS.IN_30_DAYS }, + { name: "2 months", value: TOKEN_EXPIRATION_SECONDS.IN_60_DAYS }, + { name: "3 months", value: TOKEN_EXPIRATION_SECONDS.IN_90_DAYS }, + { + name: "Never Expire", + value: TOKEN_EXPIRATION_SECONDS.IN_100_YEARS, + }, + ], + }); + + // collect name & description + const name = await input({ + message: `Name your token ${chalk.gray("(optional, ↵ to skip)")}:`, + default: "", + }); + const description = await input({ + message: `Description for your token ${chalk.gray("(optional, ↵ to skip)")}:`, + default: "", + }); + + const loadingSpinner = ora("Generating token\n").start(); + // const response = await fetch(await getApiUrl("tokens_create"), { // method: "POST", // headers: { @@ -42,7 +83,9 @@ async function createTokenAction() { // }, // }); - console.log("Creating token..."); + // console.log(expiresInSeconds); + console.log(name); + process.exit(0); } async function listTokensAction() { From 2a06c87b3dcf435304879663428049d533aa3068 Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 14:55:08 -0700 Subject: [PATCH 04/10] create token --- bun.lockb | Bin 44888 -> 45212 bytes package.json | 2 +- src/helpers/command.ts | 3 ++ src/helpers/errors.ts | 7 ++-- src/helpers/urls.ts | 3 ++ src/lib/balance.ts | 2 +- src/lib/tokens.ts | 91 +++++++++++++++++++++++++++++++++++------ 7 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 src/helpers/command.ts diff --git a/bun.lockb b/bun.lockb index 0a8b7c112af38e6c7ac24a9129ec0ad43be75032..004236af88ba7d6faa765b61b61384d3dfb2a1ad 100755 GIT binary patch delta 2392 zcmZ9O3s6->7{_-HT)7uGDhkNujc-k^$bg{8y^soWFCeINMDu}4;sZfJ1=K)85rR;^ z(bUQ?o6KSyozpa6<1_P>SxwoXjWt%|M<-MzQ# z2j$J1$~MOczj#M?_sI|a#rZ4m1Xi8>Yvj56rJ096zN6+Ij7i_uG1mWxFv`m9T9gu7 zezdAYNncb{y|e;qQPq+%wh_TX^cAA64{UGPUa&S;uK_~%!j>*6EskBZK-`1g2l08b zd({?phCS|e8(saNcAGJtFh-ddfsvt4LWQQfQLrJf!LS&pt~*=^JM1l3OjLITmfJh@ zyIy6AHTTl7gx{KW`s?$1{oPOc(?*|RlooE&8|_|_vh5S(x6~NwQeuPYQm9i|1LNbg z9mBxmF@`s7_f1mN0n`-cly15h=8_x97VffL#Y~gXK^h(GA=DJ^wB%#jG*bOjatAqO23>@_DO3m- z%a+qUo{dwGj>o+yATr64jtCD%q*&_VC6XGLBF|7(v`gs~PEFBHnMN0*U2+52VqBK> zNLV4MF)5a-@CpoXWTX&N4X+xW$MDX=%Qw8BC?RqTZvni={f@#j`YG0E-92cpB87fq zNP`#5oDpsIw2T{V|5kqQ$osB%LR|aipFjU5vT@H2|CaxLnSD(^e(;s>^%LTb6;M*(0Y?;be`h z87dt|luxz{jXDsOW_aXu>O@qMsZvCyM;6eMOpQXOsPqLQjqFo2x`1fI6pt*Vi-@YS zREo{=$k|k#rO|M=N;eSAp%HG4x)8OvJ#rphL$uYSQmV%zi>T3~QF69QcMvV4aoHOE zis-Fuk1VF!h<4|wl#}C;i)l}eCQIlp>LrwwtI4IbAN4Z&2laBwpQ_1HI)r)!$vjP# zQ6cJb{b*jMmA3f=(kUNrdN9*!t;9a&(?c@9O>Zx(52C*LV|>fYjbkX@ucYy1sX-4P zJuWQH0uuD1?PdK_ag>ZQ8#Sw5Ks6L0UNkjt*v+Ok$J8vaTj(Z6XfRvmnJqrBTTN}g zsaatgO|8h(uxQalW6Jy2zG}8CG+TI0&8D`<)cj#vOs&||c%3z-_L8aLLJ`A(0u}?F z1(%2z3Ak`x_&sp}i9mo-D^{XrF#zC58%bDYYW-k!z=dxwPEda^1MmXY0DhJSO8~rp zwSX581QG#j)qwl)_+tTUym*QE`9cy&rnt@!jmt?So7#F)!v!G5nc4l4)6kTz(p!|;MHsbEaF@nz=(RltBwNu0B3`@0&CIWZNOy< zU@Zo$2AmDv7(CuUz?tK0@Fw644g$*nXQP(CHH(8m3E*tB0@j8A{1szim-yq4ap7^+{WD3Nl#9=qBjeLMK;qg<{*)4%}UaJ&&0f)_xJ-nr&S zIV2ly9+(PpK>?T!^1(DP11zAiiyFPMmoMWsbkxtaa z+ISxrMZZ^b*e|^2C%<)1X2~C6ABcsi|nNR&34&MpENHH>%eEk7OHE%9-Mgke(u-aa*QL{5s#U!P-=@ktmntR z&!4aFigJG0_N2X=R<_uc;0S7N2?^`@ui%P3WLE>XQ>M%c|ZFPnpf$NA#xG&IHo__Gnsu;1MMsNYIzP Hd&l>GyPnfJ delta 2237 zcmZ9NYfKbZ6vyu!SaFfX2M<|x7g*6MwICW1D6&hd$YWR@RXSY^7(gqi2v)0rSg?X( zTR?jn6@qO^YNAa`Yv)TfO|*{>kg8N;jH0&Hq=|emZEa#OO&|X=&PLjQ0Fn_AUc=ds&=pAGMO|Ana|!@_7y zhqZVmwefUwoswDIT;I4I>O_6@m z?fx#u<53eBIt}Wm8RH+uEDK#i;MXw&9a0^WP9b7o@4{lDj!{@U>{qa}VF%27r~W9Y zQPG>GOwOW{v8nnod$6QivCEZ5Q55U&C@)9Tc!kuYgi2>3W@Y`$}cX})?tRRP6VOg@0{4TF`5HZgp>5em9xfVm?E{`=D%VN<= z)luoPl>BjC%VB6Ms4^}~xoW41D36>)_IR(QI93QR%a)#iXT1RLCHy^zs^iivHtZdL zbY)pe;iZu)GE4SSe}Y%J;-K*aj|?Nb+bavm@Ag{qQ7b=Hy0a`p@X8JEA-od9%Z(F4 zHM~Rc$_(!gJfGpE#0SRhfoF_UuEy&Fi35uMLsFd7E9Td**Ken&Tvb20qCrwau8L}% z$<^rVTpPJosj`IXS7}tY%0?Fvm61J9qnJD!?a5Q+Y8pi}f+#s(mF3iuuTgWpjcy~- zD5*fB1qC+hDNv=KZXmjjD7#RVYw2L2M%{%rx{s)mGF6Q-R2v;vRk@BP5Zy!MD^g`O z^%rS$tjI?HAgZMTpGNsU8-4CmKhw2;33V?FZ&lphc-7F2SW^Qr}U<5YHYG6 z;c+%v1$qe-U^5gU)|y%nY!{twipbt?W-85$1@?fcRhilp*ltr>XKEp^2T5*=$bQGn zRGS%`DDkeT)tDL|tM^Q;*3@{NJyeDnTFngai&xC6U10jJ!}5$c+TwY@jrYvsaGXUX z4Q-2P$8j?Dxy{VXgw+AJHvsQ<7FYwqz?*<)hyY6gUvVekT1A31z*;llad@YQWq`F7 zz#3mO9YAXryag6UnQS3G~oKlf#YHg;51Iee+CKEW7>+*#T#Zh^8cvt^2yp8HtR(=h2v>uj2fm0K@avVU z!S|jkI0tM7Tn)a%tR;dvUKy987qB=N@QveY90sh-1M2}-;{(9jbD$bk? ztl!$PUg|4%=F8BNeE-qDX$d4}@^i-9r~&1mNFUwxi*)C~Edj;A2i5?-XelTItLb7( z{5)RotH2Ll0~+9E@&5S67#i7H7q#;U&T=N;^T>eztbKYMSK#UVvsbD`o{tq*KGC#W zVH>~}fZIvuT5aJQ;s4Qs4(&k?U2FX_gzJe$mv$#xW!2)8#r)HQ+lHq(1K-%b{=B*e zj!zdUvTA8cYD%gQH>2p%ff!{XitZg)BF~ef+pdg7)3WX*lJxrSJd4C@r^hZw>BpXp zj!xV>JU1PmjYp?_`>^us+cDT_p9LNT9Y){?orWaX<8bs3EWb diff --git a/package.json b/package.json index 00ec1dd1..6998f261 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "chrono-node": "^2.7.6", "cli-table3": "^0.6.5", "commander": "^12.1.0", - "dayjs": "^1.11.11", + "dayjs": "^1.11.12", "dotenv": "^16.4.5", "inquirer": "^10.1.2", "node-fetch": "^3.3.2", diff --git a/src/helpers/command.ts b/src/helpers/command.ts new file mode 100644 index 00000000..2bf64442 --- /dev/null +++ b/src/helpers/command.ts @@ -0,0 +1,3 @@ +export function getCommandBase() { + return process.env.IS_DEVELOPMENT_CLI_ENV ? "bun dev" : "sf"; +} diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 9c96ab85..4cdebc25 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -1,3 +1,5 @@ +import { getCommandBase } from "./command"; + export const ApiErrorCode = { Base: { InvalidRequest: "invalid_request", @@ -45,9 +47,8 @@ export function logAndQuit(message: string) { } export function logLoginMessageAndQuit() { - const loginCommand = process.env.IS_DEVELOPMENT_CLI_ENV - ? "bun dev login" - : "sf login"; + const base = getCommandBase(); + const loginCommand = `${base} login`; logAndQuit(`You need to login first.\n\n\t$ ${loginCommand}\n`); } diff --git a/src/helpers/urls.ts b/src/helpers/urls.ts index 24768689..84f20da5 100644 --- a/src/helpers/urls.ts +++ b/src/helpers/urls.ts @@ -7,6 +7,9 @@ const webPaths = { }; const apiPaths = { + index: "/", + ping: "/v0/ping", + orders_create: "/v0/orders", orders_list: "/v0/orders", orders_get: ({ id }: { id: string }) => `/v0/orders/${id}`, diff --git a/src/lib/balance.ts b/src/lib/balance.ts index b4a376a9..373ec7ab 100644 --- a/src/lib/balance.ts +++ b/src/lib/balance.ts @@ -1,7 +1,7 @@ import chalk from "chalk"; import Table from "cli-table3"; import type { Command } from "commander"; -import { isLoggedIn, isLoggedIn, loadConfig } from "../helpers/config"; +import { isLoggedIn, loadConfig } from "../helpers/config"; import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; import type { Centicents } from "../helpers/units"; import { getApiUrl } from "../helpers/urls"; diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index 306258bb..fa0d5f0f 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -1,9 +1,12 @@ import type { Command } from "commander"; -import { isLoggedIn } from "../helpers/config"; +import { getAuthToken, isLoggedIn } from "../helpers/config"; import { logLoginMessageAndQuit } from "../helpers/errors"; -import { input } from "@inquirer/prompts"; +import { input, select } from "@inquirer/prompts"; import ora from "ora"; import chalk from "chalk"; +import { getApiUrl } from "../helpers/urls"; +import { getCommandBase } from "../helpers/command"; +import Table from "cli-table3"; export const TOKEN_EXPIRATION_SECONDS = { IN_7_DAYS: 7 * 24 * 60 * 60, @@ -40,6 +43,13 @@ export function registerTokens(program: Command) { // -- +interface PostTokenRequestBody { + expires_in_seconds: number; + name?: string; + description?: string; + origin_client: string; +} + async function createTokenAction() { const loggedIn = await isLoggedIn(); if (!loggedIn) { @@ -73,21 +83,78 @@ async function createTokenAction() { default: "", }); - const loadingSpinner = ora("Generating token\n").start(); + // generate token + console.log("\n"); + const loadingSpinner = ora("Generating token").start(); + + const response = await fetch(await getApiUrl("tokens_create"), { + method: "POST", + body: JSON.stringify({ + expires_in_seconds: expiresInSeconds, + name, + description, + origin_client: "cli", + } as PostTokenRequestBody), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${await getAuthToken()}`, + }, + }); + if (!response.ok) { + loadingSpinner.fail("Failed to create token"); - // const response = await fetch(await getApiUrl("tokens_create"), { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // Authorization: `Bearer ${config.auth_token}`, - // }, - // }); + process.exit(1); + } + + // display token to user + const data = await response.json(); + loadingSpinner.succeed(chalk.gray("Access token created 🎉")); + console.log(chalk.green(data.token) + "\n"); + + // tell them they will set this in the Authorization header + console.log( + `${chalk.gray(`Pass this in the 'Authorization' header of API requests:`)}`, + ); + console.log( + [ + chalk.gray("{ "), + chalk.white("Authorization"), + chalk.gray(": "), + chalk.green('"Bearer '), + chalk.magenta(""), + chalk.green('"'), + chalk.gray(" }"), + ].join(""), + ); + console.log("\n"); + + // give them a sample curl + const pingUrl = await getApiUrl("ping"); + console.log(`${chalk.gray("Here is a sample curl to get your started:")}`); + console.log( + chalk.white(`curl --request GET \\ + --url ${pingUrl} \\ + --header 'Authorization: Bearer ${data.token}'`), + ); + console.log("\n"); + + // tip user on other commands + const base = getCommandBase(); + + const table = new Table({ + colWidths: [20, 30], + }); + table.push(["View All Tokens", chalk.magenta(`${base} tokens list`)]); + table.push(["Delete a Token", chalk.magenta(`${base} tokens delete`)]); + + console.log(`${chalk.gray("And other commands you can try:")}`); + console.log(table.toString()); - // console.log(expiresInSeconds); - console.log(name); process.exit(0); } +// -- + async function listTokensAction() { const loggedIn = await isLoggedIn(); if (!loggedIn) { From 0872e364b46492f034fa98abe707f7f2f123af3f Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 15:11:48 -0700 Subject: [PATCH 05/10] token create --- src/helpers/config.ts | 18 +++++++++++++++--- src/helpers/errors.ts | 15 ++++++--------- src/lib/balance.ts | 8 ++++++-- src/lib/tokens.ts | 11 ++++++++++- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/helpers/config.ts b/src/helpers/config.ts index 0eab40ad..f30e7329 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -25,15 +25,18 @@ const ConfigDefaults = process.env.IS_DEVELOPMENT_CLI_ENV // -- -export async function saveConfig(config: Partial): Promise { +export async function saveConfig( + config: Partial, +): Promise<{ success: boolean }> { const configPath = getConfigPath(); const configData = JSON.stringify(config, null, 2); try { await Bun.write(configPath, configData); - console.log("Config saved successfully."); + + return { success: true }; } catch (error) { - console.error("Failed to save config:", error); + return { success: false }; } } @@ -43,6 +46,15 @@ export async function loadConfig(): Promise { return { ...ConfigDefaults, ...configFileData }; } +export async function clearAuthFromConfig() { + const config = await loadConfig(); + + await saveConfig({ + ...config, + auth_token: undefined, + }); +} + // only for development export async function deleteConfig() { const exists = await configFileExists(); diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 4cdebc25..b14475df 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -1,4 +1,5 @@ import { getCommandBase } from "./command"; +import { clearAuthFromConfig } from "./config"; export const ApiErrorCode = { Base: { @@ -30,15 +31,6 @@ export const ApiErrorCode = { }, }; -export const HTTPStatusCode = { - BadRequest: 400, - Unauthorized: 401, - Forbidden: 403, - NotFound: 404, - TooManyRequests: 429, - InternalServerError: 500, -}; - // -- export function logAndQuit(message: string) { @@ -52,3 +44,8 @@ export function logLoginMessageAndQuit() { logAndQuit(`You need to login first.\n\n\t$ ${loginCommand}\n`); } + +export async function logSessionTokenExpiredAndQuit() { + await clearAuthFromConfig(); + logAndQuit("\nYour session has expired. Please login again."); +} diff --git a/src/lib/balance.ts b/src/lib/balance.ts index 373ec7ab..024864a7 100644 --- a/src/lib/balance.ts +++ b/src/lib/balance.ts @@ -2,7 +2,11 @@ import chalk from "chalk"; import Table from "cli-table3"; import type { Command } from "commander"; import { isLoggedIn, loadConfig } from "../helpers/config"; -import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; +import { + logAndQuit, + logLoginMessageAndQuit, + logSessionTokenExpiredAndQuit, +} from "../helpers/errors"; import type { Centicents } from "../helpers/units"; import { getApiUrl } from "../helpers/urls"; @@ -92,7 +96,7 @@ async function getBalance(): Promise<{ if (!response.ok) { if (response.status === 401) { - logLoginMessageAndQuit(); + logSessionTokenExpiredAndQuit(); return { available: { centicents: 0, whole: 0 }, diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index fa0d5f0f..a3b40eb8 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -1,6 +1,9 @@ import type { Command } from "commander"; import { getAuthToken, isLoggedIn } from "../helpers/config"; -import { logLoginMessageAndQuit } from "../helpers/errors"; +import { + logLoginMessageAndQuit, + logSessionTokenExpiredAndQuit, +} from "../helpers/errors"; import { input, select } from "@inquirer/prompts"; import ora from "ora"; import chalk from "chalk"; @@ -101,6 +104,12 @@ async function createTokenAction() { }, }); if (!response.ok) { + if (response.status === 401) { + await logSessionTokenExpiredAndQuit(); + } + + const data = await response.json(); + console.log(data); loadingSpinner.fail("Failed to create token"); process.exit(1); From bd0e77b8ab30a8ad08ce830ee275885501cd59da Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 15:47:54 -0700 Subject: [PATCH 06/10] tokens list --- src/lib/tokens.ts | 111 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 16 deletions(-) diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index a3b40eb8..b7c87668 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -10,6 +10,7 @@ import chalk from "chalk"; import { getApiUrl } from "../helpers/urls"; import { getCommandBase } from "../helpers/command"; import Table from "cli-table3"; +import dayjs from "dayjs"; export const TOKEN_EXPIRATION_SECONDS = { IN_7_DAYS: 7 * 24 * 60 * 60, @@ -30,11 +31,11 @@ export function registerTokens(program: Command) { .description("Create a new access token") .action(createTokenAction); - // tokens - // .command("ls") - // .description("List all tokens") - // .option("--include-system, -is", "Include system tokens") - // .action(listTokensAction); + tokens + .command("list") + .alias("ls") + .description("List all tokens") + .action(listTokensAction); // tokens // .command("delete") @@ -46,6 +47,19 @@ export function registerTokens(program: Command) { // -- +interface TokenObject { + id: string; + token?: string; + name?: string; + description?: string; + is_sandbox: boolean; + created_at: string; + last_active_at: string; + expires_at: string; + origin_client: string; + is_system: boolean; +} + interface PostTokenRequestBody { expires_in_seconds: number; name?: string; @@ -108,10 +122,9 @@ async function createTokenAction() { await logSessionTokenExpiredAndQuit(); } - const data = await response.json(); - console.log(data); - loadingSpinner.fail("Failed to create token"); + // TODO: handle specific errors + loadingSpinner.fail("Failed to create token"); process.exit(1); } @@ -170,17 +183,83 @@ async function listTokensAction() { logLoginMessageAndQuit(); } - // const response = await fetch(await getApiUrl("tokens_create"), { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // Authorization: `Bearer ${config.auth_token}`, - // }, - // }); + const loadingSpinner = ora("Fetching tokens...").start(); + + // fetch tokens + const tokensListUrl = await getApiUrl("tokens_list"); + const response = await fetch(tokensListUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${await getAuthToken()}`, + }, + }); + if (!response.ok) { + if (response.status === 401) { + await logSessionTokenExpiredAndQuit(); + } + + // TODO: handle specific errors + + loadingSpinner.fail("Failed to fetch tokens"); + process.exit(1); + } + loadingSpinner.stop(); // hide spinner + + // show account tokens + const responseBody = await response.json(); + const tokens = responseBody.data as Array; + + // show empty table if no tokens + if (tokens.length === 0) { + const table = new Table({ + head: [chalk.gray("Access Tokens")], + colWidths: [50], + }); + table.push([ + { colSpan: 1, content: "No access tokens found", hAlign: "center" }, + ]); + console.log(table.toString() + "\n"); + + // prompt user that they can generate one + const base = getCommandBase(); + console.log( + chalk.gray("Generate your first token with: ") + + chalk.magenta(`${base} tokens create`), + ); - console.log("Listing tokens..."); + process.exit(0); + } + + // display table + const tokensTable = new Table({ + head: [ + chalk.gray("Token ID"), + chalk.gray("Last Active At"), + chalk.gray("Expires"), + chalk.gray("Created At"), + ], + colWidths: [40, 25, 25, 25], + }); + for (const token of tokens) { + tokensTable.push([ + token.id, + chalk.green(formatDate(token.last_active_at)), + chalk.red(formatDate(token.expires_at)), + chalk.gray(formatDate(token.created_at)), + ]); + } + console.log(tokensTable.toString()); + + process.exit(0); +} + +function formatDate(isoString: string): string { + return dayjs(isoString).format("MMM D, YYYY [at] h:mma").toLowerCase(); } +// -- + async function deleteTokenAction() { const loggedIn = await isLoggedIn(); if (!loggedIn) { From 68278ef8daf42ded5b9f57066c248fb9b523b7ca Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 15:51:55 -0700 Subject: [PATCH 07/10] list tokens --- src/lib/tokens.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index b7c87668..0ec93cc2 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -235,18 +235,18 @@ async function listTokensAction() { const tokensTable = new Table({ head: [ chalk.gray("Token ID"), + chalk.gray("Name"), chalk.gray("Last Active At"), chalk.gray("Expires"), - chalk.gray("Created At"), ], - colWidths: [40, 25, 25, 25], + colWidths: [40, 15, 25, 25], }); for (const token of tokens) { tokensTable.push([ - token.id, + chalk.gray(token.id), + token.name ? token.name : chalk.gray("(empty)"), chalk.green(formatDate(token.last_active_at)), - chalk.red(formatDate(token.expires_at)), - chalk.gray(formatDate(token.created_at)), + chalk.white(formatDate(token.expires_at)), ]); } console.log(tokensTable.toString()); From b957c73b3c7a823c6b46b8ab74690d88bc140a14 Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 16:20:14 -0700 Subject: [PATCH 08/10] delete token --- src/helpers/errors.ts | 7 ++++ src/helpers/urls.ts | 2 +- src/lib/tokens.ts | 83 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index b14475df..5ff16b0b 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -1,6 +1,13 @@ import { getCommandBase } from "./command"; import { clearAuthFromConfig } from "./config"; +export interface ApiError { + object: "error"; + code: string; + message: string; + details: Record; +} + export const ApiErrorCode = { Base: { InvalidRequest: "invalid_request", diff --git a/src/helpers/urls.ts b/src/helpers/urls.ts index 84f20da5..7a8eeb45 100644 --- a/src/helpers/urls.ts +++ b/src/helpers/urls.ts @@ -27,7 +27,7 @@ const apiPaths = { tokens_create: "/v0/tokens", tokens_list: "/v0/tokens", - tokens_delete: ({ id }: { id: string }) => `/v0/tokens/${id}`, + tokens_delete_by_id: ({ id }: { id: string }) => `/v0/tokens/${id}`, }; // -- diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index 0ec93cc2..a1a0bf0f 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -1,8 +1,10 @@ import type { Command } from "commander"; import { getAuthToken, isLoggedIn } from "../helpers/config"; import { + ApiErrorCode, logLoginMessageAndQuit, logSessionTokenExpiredAndQuit, + type ApiError, } from "../helpers/errors"; import { input, select } from "@inquirer/prompts"; import ora from "ora"; @@ -10,6 +12,7 @@ import chalk from "chalk"; import { getApiUrl } from "../helpers/urls"; import { getCommandBase } from "../helpers/command"; import Table from "cli-table3"; +import { confirm } from "@inquirer/prompts"; import dayjs from "dayjs"; export const TOKEN_EXPIRATION_SECONDS = { @@ -37,12 +40,12 @@ export function registerTokens(program: Command) { .description("List all tokens") .action(listTokensAction); - // tokens - // .command("delete") - // .description("Delete a token") - // .option("--name ", "Specify the token name") - // .option("--id ", "Specify the token ID") - // .action(deleteTokenAction); + tokens + .command("delete") + .description("Delete a token") + .requiredOption("--id ", "Specify the token ID") + .option("--force", "Force delete the token, skipping confirmation") + .action(deleteTokenAction); } // -- @@ -260,19 +263,69 @@ function formatDate(isoString: string): string { // -- -async function deleteTokenAction() { +async function deleteTokenAction({ + id, + force, +}: { id: string; force?: boolean }) { const loggedIn = await isLoggedIn(); if (!loggedIn) { logLoginMessageAndQuit(); } - // const response = await fetch(await getApiUrl("tokens_create"), { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // Authorization: `Bearer ${config.auth_token}`, - // }, - // }); + if (force) { + await deleteTokenById(id); + } + + const deleteTokenConfirmed = await confirm({ + 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)"), + default: false, + }); + + if (!verySureConfirmed) { + process.exit(0); + } else { + await deleteTokenById(id); + } + } +} +async function deleteTokenById(id: string) { + const deleteTokenByIdUrl = await getApiUrl("tokens_delete_by_id", { id }); + const loadingSpinner = ora("Deleting token...").start(); - console.log("Deleting tokens..."); + const response = await fetch(deleteTokenByIdUrl, { + method: "DELETE", + headers: { + Authorization: `Bearer ${await getAuthToken()}`, + }, + }); + if (!response.ok) { + if (response.status === 401) { + await logSessionTokenExpiredAndQuit(); + } + + const error = (await response.json()) as ApiError; + if (error.code === ApiErrorCode.Tokens.TokenNotFound) { + loadingSpinner.fail("Token not found"); + process.exit(1); + } + + // TODO: handle more specific errors + + // generic catch-all + loadingSpinner.fail("Failed to delete token"); + process.exit(1); + } + + loadingSpinner.stop(); + console.log(chalk.gray("Token deleted. 🧼")); + + process.exit(0); } From 5b4be039317fd023249b7a6926c1f7dd32acc80a Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 16:21:53 -0700 Subject: [PATCH 09/10] rm alias --- src/lib/tokens.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index a1a0bf0f..c6be7c11 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -42,6 +42,7 @@ export function registerTokens(program: Command) { tokens .command("delete") + .command("rm") .description("Delete a token") .requiredOption("--id ", "Specify the token ID") .option("--force", "Force delete the token, skipping confirmation") From c85a0c5eb45cabc8f6d1d39665c355ec2227d9f1 Mon Sep 17 00:00:00 2001 From: Benyam Ephrem Date: Fri, 9 Aug 2024 16:22:29 -0700 Subject: [PATCH 10/10] bun check --- src/lib/buy.ts | 2 +- src/lib/tokens.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib/buy.ts b/src/lib/buy.ts index 4c05321a..e5bff1ab 100644 --- a/src/lib/buy.ts +++ b/src/lib/buy.ts @@ -1,3 +1,4 @@ +import { confirm } from "@inquirer/prompts"; import c from "chalk"; import * as chrono from "chrono-node"; import type { Command } from "commander"; @@ -13,7 +14,6 @@ import { formatDuration, priceToCenticents, } from "./orders"; -import { confirm } from "@inquirer/prompts"; dayjs.extend(relativeTime); dayjs.extend(duration); diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index c6be7c11..1f9f8e2b 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -1,19 +1,19 @@ +import { input, select } from "@inquirer/prompts"; +import { confirm } from "@inquirer/prompts"; +import chalk from "chalk"; +import Table from "cli-table3"; import type { Command } from "commander"; +import dayjs from "dayjs"; +import ora from "ora"; +import { getCommandBase } from "../helpers/command"; import { getAuthToken, isLoggedIn } from "../helpers/config"; import { + type ApiError, ApiErrorCode, logLoginMessageAndQuit, logSessionTokenExpiredAndQuit, - type ApiError, } from "../helpers/errors"; -import { input, select } from "@inquirer/prompts"; -import ora from "ora"; -import chalk from "chalk"; import { getApiUrl } from "../helpers/urls"; -import { getCommandBase } from "../helpers/command"; -import Table from "cli-table3"; -import { confirm } from "@inquirer/prompts"; -import dayjs from "dayjs"; export const TOKEN_EXPIRATION_SECONDS = { IN_7_DAYS: 7 * 24 * 60 * 60,