From 8f84ab6d13701684cd76dcbf8598980653b6e016 Mon Sep 17 00:00:00 2001 From: Devin Date: Fri, 27 Jun 2025 09:12:08 +0200 Subject: [PATCH 01/47] updated errors with error codes --- src/cmds/balances.ts | 1 + src/cmds/beneficiaries.ts | 1 + src/cmds/cards.ts | 1 + src/cmds/countries.ts | 1 + src/cmds/currencies.ts | 1 + src/cmds/deploy.ts | 5 +++-- src/cmds/disable.ts | 3 ++- src/cmds/env.ts | 3 ++- src/cmds/fetch.ts | 3 ++- src/cmds/login.ts | 3 ++- src/cmds/logs.ts | 5 +++-- src/cmds/merchants.ts | 1 + src/cmds/new.ts | 9 ++++----- src/cmds/publish.ts | 5 +++-- src/cmds/published.ts | 3 ++- src/cmds/register.ts | 3 ++- src/cmds/run.ts | 5 +++-- src/cmds/simulate.ts | 5 +++-- src/cmds/toggle.ts | 3 ++- src/cmds/transactions.ts | 1 + src/cmds/upload-env.ts | 5 +++-- src/errors.ts | 41 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 30 +++++++++++++++------------- 23 files changed, 101 insertions(+), 37 deletions(-) create mode 100644 src/errors.ts diff --git a/src/cmds/balances.ts b/src/cmds/balances.ts index 08a6329..85e5797 100644 --- a/src/cmds/balances.ts +++ b/src/cmds/balances.ts @@ -2,6 +2,7 @@ import { credentials, initializePbApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions {} diff --git a/src/cmds/beneficiaries.ts b/src/cmds/beneficiaries.ts index cdf0d55..953be15 100644 --- a/src/cmds/beneficiaries.ts +++ b/src/cmds/beneficiaries.ts @@ -36,6 +36,7 @@ export async function beneficiariesCommand(options: Options) { }), ); printTable(simpleBeneficiaries); + console.log(`\n${beneficiaries.length} beneficiary(ies) found.`); } catch (error: any) { handleCliError(error, options, "fetch beneficiaries"); } diff --git a/src/cmds/cards.ts b/src/cmds/cards.ts index 7919c0e..30eb220 100644 --- a/src/cmds/cards.ts +++ b/src/cmds/cards.ts @@ -27,6 +27,7 @@ export async function cardsCommand(options: Options) { }), ); printTable(simpleCards); + console.log(`\n${cards.length} card(s) found.`); } catch (error: any) { handleCliError(error, options, "fetch cards"); } diff --git a/src/cmds/countries.ts b/src/cmds/countries.ts index 32a6067..729f032 100644 --- a/src/cmds/countries.ts +++ b/src/cmds/countries.ts @@ -20,6 +20,7 @@ export async function countriesCommand(options: Options) { const simpleCountries = countries.map(({ Code, Name }) => ({ Code, Name })); printTable(simpleCountries); + console.log(`\n${countries.length} country(ies) found.`); } catch (error: any) { handleCliError(error, options, "fetch countries"); } diff --git a/src/cmds/currencies.ts b/src/cmds/currencies.ts index 41038bf..d0fc3e1 100644 --- a/src/cmds/currencies.ts +++ b/src/cmds/currencies.ts @@ -23,6 +23,7 @@ export async function currenciesCommand(options: Options) { Name, })); printTable(simpleCurrencies); + console.log(`\n${currencies.length} currency(ies) found.`); } catch (error: any) { handleCliError(error, options, "fetch currencies"); } diff --git a/src/cmds/deploy.ts b/src/cmds/deploy.ts index c782754..b7e54b6 100644 --- a/src/cmds/deploy.ts +++ b/src/cmds/deploy.ts @@ -4,6 +4,7 @@ import { credentials, initializeApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -18,7 +19,7 @@ export async function deployCommand(options: Options) { let envObject = {}; if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } @@ -27,7 +28,7 @@ export async function deployCommand(options: Options) { if (options.env) { if (!fs.existsSync(`.env.${options.env}`)) { - throw new Error("Env does not exist"); + throw new CliError(ERROR_CODES.MISSING_ENV_FILE, `Env file .env.${options.env} does not exist`); } spinner.text = `📦 uploading env from .env.${options.env}`; envObject = dotenv.parse(fs.readFileSync(`.env.${options.env}`)); diff --git a/src/cmds/disable.ts b/src/cmds/disable.ts index e2a0723..9fd7510 100644 --- a/src/cmds/disable.ts +++ b/src/cmds/disable.ts @@ -2,6 +2,7 @@ import { credentials, initializeApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -10,7 +11,7 @@ interface Options extends CommonOptions { export async function disableCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } diff --git a/src/cmds/env.ts b/src/cmds/env.ts index 1dce9cc..537a47c 100644 --- a/src/cmds/env.ts +++ b/src/cmds/env.ts @@ -3,6 +3,7 @@ import { credentials, initializeApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -12,7 +13,7 @@ interface Options extends CommonOptions { export async function envCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } diff --git a/src/cmds/fetch.ts b/src/cmds/fetch.ts index 6d0b3ec..fe18bac 100644 --- a/src/cmds/fetch.ts +++ b/src/cmds/fetch.ts @@ -3,6 +3,7 @@ import { credentials, initializeApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -12,7 +13,7 @@ interface Options extends CommonOptions { export async function fetchCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } diff --git a/src/cmds/login.ts b/src/cmds/login.ts index b38b4d9..f99e900 100644 --- a/src/cmds/login.ts +++ b/src/cmds/login.ts @@ -4,6 +4,7 @@ import fetch from "node-fetch"; import https from "https"; import { handleCliError } from "../utils.js"; import { input, password } from "@inquirer/prompts"; +import { CliError, ERROR_CODES } from "../errors.js"; const agent = new https.Agent({ rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", @@ -42,7 +43,7 @@ export async function loginCommand(options: any) { }); } if (!options.email || !options.password) { - throw new Error("Email and password are required"); + throw new CliError(ERROR_CODES.INVALID_CREDENTIALS, "Email and password are required"); } console.log("💳 logging into account"); const result = await fetch("https://ipb.sandboxpay.co.za/auth/login", { diff --git a/src/cmds/logs.ts b/src/cmds/logs.ts index 946b9ec..9104528 100644 --- a/src/cmds/logs.ts +++ b/src/cmds/logs.ts @@ -4,6 +4,7 @@ import { credentials, initializeApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -14,12 +15,12 @@ export async function logsCommand(options: Options) { try { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } if (options.filename === undefined || options.filename === "") { - throw new Error("filename is required"); + throw new CliError("E4006", "filename is required"); } printTitleBox(); const spinner = ora("📊 fetching execution items...").start(); diff --git a/src/cmds/merchants.ts b/src/cmds/merchants.ts index a3ebd1c..ffcf69f 100644 --- a/src/cmds/merchants.ts +++ b/src/cmds/merchants.ts @@ -21,6 +21,7 @@ export async function merchantsCommand(options: Options) { const simpleMerchants = merchants.map(({ Code, Name }) => ({ Code, Name })); printTable(simpleMerchants); + console.log(`\n${merchants.length} merchant(s) found.`); } catch (error: any) { handleCliError(error, options, "fetch merchants"); } diff --git a/src/cmds/new.ts b/src/cmds/new.ts index 6dae7a0..3455958 100644 --- a/src/cmds/new.ts +++ b/src/cmds/new.ts @@ -3,6 +3,7 @@ import fs from "fs"; import path from "path"; import { printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options { template: string; @@ -20,13 +21,11 @@ export async function newCommand(name: string, options: Options) { console.log("📂 Finding template called " + chalk.green(options.template)); try { if (!fs.existsSync(uri)) { - throw new Error("💣 Template does not exist"); + throw new CliError(ERROR_CODES.TEMPLATE_NOT_FOUND, "💣 Template does not exist"); } // Validate project name if (!/^[a-zA-Z0-9-_]+$/.test(name)) { - throw new Error( - "💣 Project name contains invalid characters. Use only letters, numbers, hyphens, and underscores.", - ); + throw new CliError(ERROR_CODES.INVALID_PROJECT_NAME, "💣 Project name contains invalid characters. Use only letters, numbers, hyphens, and underscores."); } // Add a force option to the Options interface if (fs.existsSync(name) && options.force) { @@ -36,7 +35,7 @@ export async function newCommand(name: string, options: Options) { // Remove existing directory fs.rmSync(name, { recursive: true, force: true }); } else if (fs.existsSync(name)) { - throw new Error("💣 Project already exists"); + throw new CliError(ERROR_CODES.PROJECT_EXISTS, "💣 Project already exists"); } fs.cpSync(uri, name, { recursive: true }); console.log(`🚀 Created new project from template ${options.template}`); diff --git a/src/cmds/publish.ts b/src/cmds/publish.ts index d54f9ea..7c05ce4 100644 --- a/src/cmds/publish.ts +++ b/src/cmds/publish.ts @@ -3,6 +3,7 @@ import { credentials, initializeApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -13,11 +14,11 @@ interface Options extends CommonOptions { export async function publishCommand(options: Options) { try { if (!fs.existsSync(options.filename)) { - throw new Error("File does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "File does not exist"); } if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } diff --git a/src/cmds/published.ts b/src/cmds/published.ts index 67bab39..d883dc1 100644 --- a/src/cmds/published.ts +++ b/src/cmds/published.ts @@ -3,6 +3,7 @@ import { credentials, initializeApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -12,7 +13,7 @@ interface Options extends CommonOptions { export async function publishedCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } diff --git a/src/cmds/register.ts b/src/cmds/register.ts index f89ad38..edba138 100644 --- a/src/cmds/register.ts +++ b/src/cmds/register.ts @@ -4,6 +4,7 @@ import https from "https"; import { handleCliError } from "../utils.js"; import { input, password } from "@inquirer/prompts"; import type { CommonOptions } from "./types.js"; +import { CliError, ERROR_CODES } from "../errors.js"; const agent = new https.Agent({ rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", @@ -33,7 +34,7 @@ export async function registerCommand(options: Options) { }); } if (!options.email || !options.password) { - throw new Error("Email and password are required"); + throw new CliError(ERROR_CODES.MISSING_EMAIL_OR_PASSWORD, "Email and password are required"); } console.log("💳 registering account"); const result = await fetch("https://ipb.sandboxpay.co.za/auth/register", { diff --git a/src/cmds/run.ts b/src/cmds/run.ts index 7760f46..f6cd38d 100644 --- a/src/cmds/run.ts +++ b/src/cmds/run.ts @@ -4,6 +4,7 @@ import path from "path"; import { createTransaction, run } from "programmable-card-code-emulator"; import { printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options { filename: string; env: string; @@ -19,7 +20,7 @@ export async function runCommand(options: Options) { printTitleBox(); try { if (!fs.existsSync(options.filename)) { - throw new Error("File does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "File does not exist"); } console.log( chalk.white(`Running code:`), @@ -56,7 +57,7 @@ export async function runCommand(options: Options) { let environmentvariables: { [key: string]: string } = {}; if (options.env) { if (!fs.existsSync(`.env.${options.env}`)) { - throw new Error("Env does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "Env does not exist"); } const data = fs.readFileSync(`.env.${options.env}`, "utf8"); diff --git a/src/cmds/simulate.ts b/src/cmds/simulate.ts index 65a208c..da0d81b 100644 --- a/src/cmds/simulate.ts +++ b/src/cmds/simulate.ts @@ -3,6 +3,7 @@ import fs from "fs"; import { createTransaction } from "programmable-card-code-emulator"; import { credentials, initializeApi } from "../index.js"; import { handleCliError } from "../utils.js"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options { cardKey: number; @@ -24,12 +25,12 @@ export async function simulateCommand(options: Options) { try { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } if (!fs.existsSync(options.filename)) { - throw new Error("File does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "File does not exist"); } const api = await initializeApi(credentials, options); diff --git a/src/cmds/toggle.ts b/src/cmds/toggle.ts index 1ceb71b..cbf7244 100644 --- a/src/cmds/toggle.ts +++ b/src/cmds/toggle.ts @@ -1,3 +1,4 @@ +import { CliError, ERROR_CODES } from "../errors.js"; import { credentials, initializeApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; @@ -10,7 +11,7 @@ interface Options extends CommonOptions { export async function enableCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } diff --git a/src/cmds/transactions.ts b/src/cmds/transactions.ts index 403e0ac..ecde451 100644 --- a/src/cmds/transactions.ts +++ b/src/cmds/transactions.ts @@ -49,6 +49,7 @@ export async function transactionsCommand(accountId: string, options: Options) { }), ); printTable(simpleTransactions); + console.log(`\n${transactions.length} transaction(s) found for account ${accountId}.`); } catch (error: any) { if (error.message && error.message === "Bad Request") { console.log(""); diff --git a/src/cmds/upload-env.ts b/src/cmds/upload-env.ts index 5b182d4..1d8b65b 100644 --- a/src/cmds/upload-env.ts +++ b/src/cmds/upload-env.ts @@ -3,6 +3,7 @@ import { credentials, initializeApi, printTitleBox } from "../index.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; +import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -11,11 +12,11 @@ interface Options extends CommonOptions { export async function uploadEnvCommand(options: Options) { if (!fs.existsSync(options.filename)) { - throw new Error("File does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "File does not exist"); } if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("card-key is required"); + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); } options.cardKey = Number(credentials.cardKey); } diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..532f915 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,41 @@ +// Custom error class for CLI errors with codes and friendly messages +export class CliError extends Error { + code: string; + constructor(code: string, message: string) { + super(`Error (${code}): ${message}`); + this.code = code; + this.name = "CliError"; + } +} + +// Example error codes and messages +export const ERROR_CODES = { + MISSING_API_TOKEN: "E4002", + MISSING_CARD_KEY: "E4003", + MISSING_ENV_FILE: "E4004", + INVALID_CREDENTIALS: "E4005", + DEPLOY_FAILED: "E5001", + TEMPLATE_NOT_FOUND: "E4007", + INVALID_PROJECT_NAME: "E4008", + PROJECT_EXISTS: "E4009", + FILE_NOT_FOUND: "E4010", + MISSING_EMAIL_OR_PASSWORD: "E4011", // Added for register command + MISSING_ACCOUNT_ID: "E4012", // Added for balances command + // Add more as needed +}; + +// Helper to throw a custom error +export function throwCliError(code: string, message: string): never { + throw new CliError(code, message); +} + +// Helper to format error output (for use in catch blocks) +export function printCliError(error: unknown) { + if (error instanceof CliError) { + console.error(error.message); + } else if (error instanceof Error) { + console.error(`Error: ${error.message}`); + } else { + console.error("An unknown error occurred."); + } +} diff --git a/src/index.ts b/src/index.ts index 437051a..99b68e1 100755 --- a/src/index.ts +++ b/src/index.ts @@ -46,6 +46,10 @@ import type { Credentials, BasicOptions } from "./cmds/types.js"; const version = "0.8.3"; const program = new Command(); +// Improve error output for missing arguments/options +program.showHelpAfterError(); +program.showSuggestionAfterError(); + // Only export what is needed outside this file export const credentialLocation = { folder: `${homedir()}/.ipb`, @@ -251,30 +255,30 @@ async function main() { addApiCredentialOptions( program.command("balances").description("Gets your account balances"), ) - .argument("", "accountId of the account to fetch balances for") + .argument("accountId", "accountId of the account to fetch balances for") .action(balancesCommand); addApiCredentialOptions( program.command("transfer").description("Allows transfer between accounts"), ) - .argument("", "accountId of the account to transfer from") - .argument("", "beneficiaryAccountId of the account to transfer to") - .argument("", "amount to transfer in rands (e.g. 100.00)") - .argument("", "reference for the transfer") + .argument("accountId", "accountId of the account to transfer from") + .argument("beneficiaryAccountId", "beneficiaryAccountId of the account to transfer to") + .argument("amount", "amount to transfer in rands (e.g. 100.00)") + .argument("reference", "reference for the transfer") .action(transferCommand); addApiCredentialOptions( program.command("pay").description("Pay a beneficiary from your account"), ) - .argument("", "accountId of the account to transfer from") - .argument("", "beneficiaryId of the beneficiary to pay") - .argument("", "amount to transfer in rands (e.g. 100.00)") - .argument("", "reference for the payment") + .argument("accountId", "accountId of the account to transfer from") + .argument("beneficiaryId", "beneficiaryId of the beneficiary to pay") + .argument("amount", "amount to transfer in rands (e.g. 100.00)") + .argument("reference", "reference for the payment") .action(payCommand); addApiCredentialOptions( program .command("transactions") .description("Gets your account transactions"), ) - .argument("", "accountId of the account to fetch balances for") + .argument("accountId", "accountId of the account to fetch balances for") .action(transactionsCommand); addApiCredentialOptions( program.command("beneficiaries").description("Gets your beneficiaries"), @@ -282,7 +286,7 @@ async function main() { program .command("new") .description("Sets up scaffoldings for a new project") - .argument("", "name of the new project") + .argument("name", "name of the new project") .option("-v,--verbose", "additional debugging information") .option("--force", "force overwrite existing files") .addOption( @@ -294,7 +298,7 @@ async function main() { program .command("ai") .description("Generates card code using an LLM") - .argument("", "prompt for the LLM") + .argument("prompt", "prompt for the LLM") .option("-f,--filename ", "the filename", "ai-generated.js") .option("-v,--verbose", "additional debugging information") .option("--force", "force overwrite existing files") @@ -302,7 +306,7 @@ async function main() { program .command("bank") .description("Uses the LLM to call your bank") - .argument("", "prompt for the LLM") + .argument("prompt", "prompt for the LLM") .option("-v,--verbose", "additional debugging information") .action(bankCommand); program From 56e7a66aaeb09f5e98c3d135f8c6c426411dd1a5 Mon Sep 17 00:00:00 2001 From: Devin Date: Fri, 27 Jun 2025 14:57:05 +0200 Subject: [PATCH 02/47] made improvements to allow the disabling of the spinner animation --- src/cmds/accounts.ts | 18 ++++---- src/cmds/balances.ts | 41 ++++++++++------- src/cmds/beneficiaries.ts | 16 ++++--- src/cmds/cards.ts | 16 ++++--- src/cmds/countries.ts | 15 ++++--- src/cmds/currencies.ts | 15 ++++--- src/cmds/deploy.ts | 31 ++++++++----- src/cmds/disable.ts | 12 +++-- src/cmds/env.ts | 14 +++--- src/cmds/fetch.ts | 16 ++++--- src/cmds/login.ts | 5 ++- src/cmds/logs.ts | 20 +++++---- src/cmds/merchants.ts | 16 ++++--- src/cmds/new.ts | 15 +++++-- src/cmds/pay.ts | 9 ++-- src/cmds/publish.ts | 16 +++++-- src/cmds/published.ts | 12 +++-- src/cmds/register.ts | 5 ++- src/cmds/simulate.ts | 8 +++- src/cmds/toggle.ts | 12 +++-- src/cmds/transactions.ts | 28 ++++++------ src/cmds/transfer.ts | 17 ++++--- src/cmds/types.ts | 2 + src/cmds/upload-env.ts | 9 ++-- src/cmds/upload.ts | 9 ++-- src/function-calls.ts | 3 +- src/index.ts | 70 +++-------------------------- src/utils.ts | 93 ++++++++++++++++++++++++++++++++++++++- 28 files changed, 327 insertions(+), 216 deletions(-) diff --git a/src/cmds/accounts.ts b/src/cmds/accounts.ts index b733ccc..bf2f33d 100644 --- a/src/cmds/accounts.ts +++ b/src/cmds/accounts.ts @@ -1,20 +1,20 @@ -import { credentials, initializePbApi, printTitleBox } from "../index.js"; -import { handleCliError, printTable } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializePbApi } from "../utils.js"; +import { handleCliError, printTable, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; - -interface Options extends CommonOptions { - json?: boolean; -} /** * Fetch and display Investec accounts. * @param options CLI options */ -export async function accountsCommand(options: Options) { +export async function accountsCommand(options: CommonOptions) { try { printTitleBox(); - const spinner = ora("💳 fetching accounts...").start(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "💳 fetching accounts...", + ).start(); const api = await initializePbApi(credentials, options); if (options.verbose) console.log("💳 fetching accounts..."); const result = await api.getAccounts(); diff --git a/src/cmds/balances.ts b/src/cmds/balances.ts index 85e5797..024bcba 100644 --- a/src/cmds/balances.ts +++ b/src/cmds/balances.ts @@ -1,28 +1,37 @@ -import { credentials, initializePbApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializePbApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; -import { CliError, ERROR_CODES } from "../errors.js"; -interface Options extends CommonOptions {} - -export async function balancesCommand(accountId: string, options: Options) { +export async function balancesCommand( + accountId: string, + options: CommonOptions, +) { try { printTitleBox(); - const spinner = ora("💳 fetching balances...").start(); + const disableSpinner = options.spinner === true; + const spinner = createSpinner( + !disableSpinner, + "💳 fetching balances...", + ).start(); const api = await initializePbApi(credentials, options); const result = await api.getAccountBalances(accountId); spinner.stop(); //console.table(accounts) - console.log(`Account Id ${result.data.accountId}`); - console.log(`Currency: ${result.data.currency}`); - console.log("Balances:"); - console.log(`Current: ${result.data.currentBalance}`); - console.log(`Available: ${result.data.availableBalance}`); - console.log(`Budget: ${result.data.budgetBalance}`); - console.log(`Straight: ${result.data.straightBalance}`); - console.log(`Cash: ${result.data.cashBalance}`); + if (options.json) { + console.log(JSON.stringify(result.data, null, 2)); + return; + } else { + console.log(`Account Id ${result.data.accountId}`); + console.log(`Currency: ${result.data.currency}`); + console.log("Balances:"); + console.log(`Current: ${result.data.currentBalance}`); + console.log(`Available: ${result.data.availableBalance}`); + console.log(`Budget: ${result.data.budgetBalance}`); + console.log(`Straight: ${result.data.straightBalance}`); + console.log(`Cash: ${result.data.cashBalance}`); + } } catch (error: any) { if (error.message && error.message === "Bad Request") { console.log(""); diff --git a/src/cmds/beneficiaries.ts b/src/cmds/beneficiaries.ts index 953be15..fc1c231 100644 --- a/src/cmds/beneficiaries.ts +++ b/src/cmds/beneficiaries.ts @@ -1,14 +1,16 @@ -import { credentials, initializePbApi, printTitleBox } from "../index.js"; -import { handleCliError, printTable } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializePbApi } from "../utils.js"; +import { handleCliError, printTable, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; -interface Options extends CommonOptions {} - -export async function beneficiariesCommand(options: Options) { +export async function beneficiariesCommand(options: CommonOptions) { try { printTitleBox(); - const spinner = ora("💳 fetching beneficiaries...").start(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "💳 fetching beneficiaries...", + ).start(); const api = await initializePbApi(credentials, options); const result = await api.getBeneficiaries(); diff --git a/src/cmds/cards.ts b/src/cmds/cards.ts index 30eb220..c8dd037 100644 --- a/src/cmds/cards.ts +++ b/src/cmds/cards.ts @@ -1,14 +1,16 @@ -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError, printTable } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, printTable, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; -interface Options extends CommonOptions {} - -export async function cardsCommand(options: Options) { +export async function cardsCommand(options: CommonOptions) { try { printTitleBox(); - const spinner = ora("💳 fetching cards...").start(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "💳 fetching cards...", + ).start(); const api = await initializeApi(credentials, options); const result = await api.getCards(); diff --git a/src/cmds/countries.ts b/src/cmds/countries.ts index 729f032..2839040 100644 --- a/src/cmds/countries.ts +++ b/src/cmds/countries.ts @@ -1,13 +1,16 @@ -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError, printTable } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, printTable, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; -interface Options extends CommonOptions {} -export async function countriesCommand(options: Options) { +export async function countriesCommand(options: CommonOptions) { try { printTitleBox(); - const spinner = ora("💳 fetching countries...").start(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "💳 fetching countries...", + ).start(); const api = await initializeApi(credentials, options); const result = await api.getCountries(); diff --git a/src/cmds/currencies.ts b/src/cmds/currencies.ts index d0fc3e1..a58174e 100644 --- a/src/cmds/currencies.ts +++ b/src/cmds/currencies.ts @@ -1,13 +1,16 @@ -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError, printTable } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, printTable, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; -interface Options extends CommonOptions {} -export async function currenciesCommand(options: Options) { +export async function currenciesCommand(options: CommonOptions) { try { printTitleBox(); - const spinner = ora("💳 fetching currencies...").start(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "💳 fetching currencies...", + ).start(); const api = await initializeApi(credentials, options); const result = await api.getCurrencies(); diff --git a/src/cmds/deploy.ts b/src/cmds/deploy.ts index b7e54b6..ebdad59 100644 --- a/src/cmds/deploy.ts +++ b/src/cmds/deploy.ts @@ -1,9 +1,10 @@ import fs from "fs"; import dotenv from "dotenv"; -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; +import type { Spinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { @@ -15,11 +16,18 @@ interface Options extends CommonOptions { export async function deployCommand(options: Options) { try { printTitleBox(); - const spinner = ora("💳 starting deployment...").start(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "💳 starting deployment...", + ).start(); let envObject = {}; if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + throw new CliError( + ERROR_CODES.MISSING_CARD_KEY, + "card-key is required", + ); } options.cardKey = Number(credentials.cardKey); } @@ -28,7 +36,10 @@ export async function deployCommand(options: Options) { if (options.env) { if (!fs.existsSync(`.env.${options.env}`)) { - throw new CliError(ERROR_CODES.MISSING_ENV_FILE, `Env file .env.${options.env} does not exist`); + throw new CliError( + ERROR_CODES.MISSING_ENV_FILE, + `Env file .env.${options.env} does not exist`, + ); } spinner.text = `📦 uploading env from .env.${options.env}`; envObject = dotenv.parse(fs.readFileSync(`.env.${options.env}`)); @@ -50,10 +61,10 @@ export async function deployCommand(options: Options) { code, ); spinner.stop(); - if (result.data.result.codeId) { - console.log("🎉 code deployed"); - } + console.log( + `🎉 code deployed with codeId: ${saveResult.data.result.codeId}`, + ); } catch (error: any) { - handleCliError(error, { verbose: (options as any).verbose }, "deploy code"); + handleCliError(error, options, "deploy code"); } } diff --git a/src/cmds/disable.ts b/src/cmds/disable.ts index 9fd7510..64a6cfd 100644 --- a/src/cmds/disable.ts +++ b/src/cmds/disable.ts @@ -1,7 +1,7 @@ -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { @@ -17,9 +17,13 @@ export async function disableCommand(options: Options) { } try { printTitleBox(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "🍄 disabling code on card...", + ).start(); const api = await initializeApi(credentials, options); - const spinner = ora("🍄 disabling code on card...").start(); const result = await api.toggleCode(options.cardKey, false); spinner.stop(); if (!result.data.result.Enabled) { diff --git a/src/cmds/env.ts b/src/cmds/env.ts index 537a47c..d5c5fb2 100644 --- a/src/cmds/env.ts +++ b/src/cmds/env.ts @@ -1,8 +1,8 @@ import fs from "fs"; -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { @@ -19,10 +19,12 @@ export async function envCommand(options: Options) { } try { printTitleBox(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "💎 fetching envs...", + ).start(); const api = await initializeApi(credentials, options); - - const spinner = ora("💎 fetching envs...").start(); - const result = await api.getEnv(options.cardKey); const envs = result.data.result.variables; spinner.stop(); diff --git a/src/cmds/fetch.ts b/src/cmds/fetch.ts index fe18bac..2c55b51 100644 --- a/src/cmds/fetch.ts +++ b/src/cmds/fetch.ts @@ -1,9 +1,8 @@ import fs from "fs"; -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; -import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -13,15 +12,18 @@ interface Options extends CommonOptions { export async function fetchCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + throw new Error("card-key is required"); } options.cardKey = Number(credentials.cardKey); } try { printTitleBox(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "💳 fetching code...", + ).start(); const api = await initializeApi(credentials, options); - const spinner = ora("💳 fetching code...").start(); - const result = await api.getCode(options.cardKey); const code = result.data.result.code; diff --git a/src/cmds/login.ts b/src/cmds/login.ts index f99e900..14ee3b3 100644 --- a/src/cmds/login.ts +++ b/src/cmds/login.ts @@ -43,7 +43,10 @@ export async function loginCommand(options: any) { }); } if (!options.email || !options.password) { - throw new CliError(ERROR_CODES.INVALID_CREDENTIALS, "Email and password are required"); + throw new CliError( + ERROR_CODES.INVALID_CREDENTIALS, + "Email and password are required", + ); } console.log("💳 logging into account"); const result = await fetch("https://ipb.sandboxpay.co.za/auth/login", { diff --git a/src/cmds/logs.ts b/src/cmds/logs.ts index 9104528..3785686 100644 --- a/src/cmds/logs.ts +++ b/src/cmds/logs.ts @@ -1,10 +1,8 @@ -import chalk from "chalk"; import fs from "fs"; -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; -import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { cardKey: number; @@ -15,15 +13,19 @@ export async function logsCommand(options: Options) { try { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + throw new Error("card-key is required"); } options.cardKey = Number(credentials.cardKey); } if (options.filename === undefined || options.filename === "") { - throw new CliError("E4006", "filename is required"); + throw new Error("filename is required"); } printTitleBox(); - const spinner = ora("📊 fetching execution items...").start(); + const disableSpinner = options.spinner === true; + const spinner = createSpinner( + !disableSpinner, + "📊 fetching execution items...", + ).start(); const api = await initializeApi(credentials, options); const result = await api.getExecutions(options.cardKey); @@ -33,7 +35,7 @@ export async function logsCommand(options: Options) { options.filename, JSON.stringify(result.data.result.executionItems, null, 4), ); - console.log("🎉 " + chalk.greenBright("logs saved to file")); + console.log("🎉 " + "logs saved to file"); } catch (error: any) { handleCliError(error, options, "fetch execution logs"); } diff --git a/src/cmds/merchants.ts b/src/cmds/merchants.ts index ffcf69f..152c3c1 100644 --- a/src/cmds/merchants.ts +++ b/src/cmds/merchants.ts @@ -1,14 +1,16 @@ -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError, printTable } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, printTable, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; -interface Options extends CommonOptions {} - -export async function merchantsCommand(options: Options) { +export async function merchantsCommand(options: CommonOptions) { try { printTitleBox(); - const spinner = ora("🏪 fetching merchants...").start(); + const disableSpinner = options.spinner === true; + const spinner = createSpinner( + !disableSpinner, + "🏪 fetching merchants...", + ).start(); const api = await initializeApi(credentials, options); const result = await api.getMerchants(); diff --git a/src/cmds/new.ts b/src/cmds/new.ts index 3455958..cd96764 100644 --- a/src/cmds/new.ts +++ b/src/cmds/new.ts @@ -21,11 +21,17 @@ export async function newCommand(name: string, options: Options) { console.log("📂 Finding template called " + chalk.green(options.template)); try { if (!fs.existsSync(uri)) { - throw new CliError(ERROR_CODES.TEMPLATE_NOT_FOUND, "💣 Template does not exist"); + throw new CliError( + ERROR_CODES.TEMPLATE_NOT_FOUND, + "💣 Template does not exist", + ); } // Validate project name if (!/^[a-zA-Z0-9-_]+$/.test(name)) { - throw new CliError(ERROR_CODES.INVALID_PROJECT_NAME, "💣 Project name contains invalid characters. Use only letters, numbers, hyphens, and underscores."); + throw new CliError( + ERROR_CODES.INVALID_PROJECT_NAME, + "💣 Project name contains invalid characters. Use only letters, numbers, hyphens, and underscores.", + ); } // Add a force option to the Options interface if (fs.existsSync(name) && options.force) { @@ -35,7 +41,10 @@ export async function newCommand(name: string, options: Options) { // Remove existing directory fs.rmSync(name, { recursive: true, force: true }); } else if (fs.existsSync(name)) { - throw new CliError(ERROR_CODES.PROJECT_EXISTS, "💣 Project already exists"); + throw new CliError( + ERROR_CODES.PROJECT_EXISTS, + "💣 Project already exists", + ); } fs.cpSync(uri, name, { recursive: true }); console.log(`🚀 Created new project from template ${options.template}`); diff --git a/src/cmds/pay.ts b/src/cmds/pay.ts index bb4d447..4227a8d 100644 --- a/src/cmds/pay.ts +++ b/src/cmds/pay.ts @@ -1,16 +1,15 @@ -import { credentials, initializePbApi, printTitleBox } from "../index.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializePbApi } from "../utils.js"; import { handleCliError } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import { input, password } from "@inquirer/prompts"; - -interface Options extends CommonOptions {} +import { input } from "@inquirer/prompts"; export async function payCommand( accountId: string, beneficiaryId: string, amount: number, reference: string, - options: Options, + options: CommonOptions, ) { try { // Prompt for missing arguments interactively diff --git a/src/cmds/publish.ts b/src/cmds/publish.ts index 7c05ce4..190e254 100644 --- a/src/cmds/publish.ts +++ b/src/cmds/publish.ts @@ -1,6 +1,7 @@ import fs from "fs"; -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; import ora from "ora"; import { CliError, ERROR_CODES } from "../errors.js"; @@ -18,12 +19,19 @@ export async function publishCommand(options: Options) { } if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + throw new CliError( + ERROR_CODES.MISSING_CARD_KEY, + "card-key is required", + ); } options.cardKey = Number(credentials.cardKey); } printTitleBox(); - const spinner = ora("🚀 publishing code...").start(); + const disableSpinner = options.spinner === true; + const spinner = createSpinner( + !disableSpinner, + "🚀 publishing code...", + ).start(); const api = await initializeApi(credentials, options); const code = fs.readFileSync(options.filename).toString(); diff --git a/src/cmds/published.ts b/src/cmds/published.ts index d883dc1..b7c8294 100644 --- a/src/cmds/published.ts +++ b/src/cmds/published.ts @@ -1,8 +1,8 @@ import fs from "fs"; -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { @@ -19,7 +19,11 @@ export async function publishedCommand(options: Options) { } try { printTitleBox(); - const spinner = ora("🚀 fetching code...").start(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner( + !disableSpinner, + "🚀 fetching code...", + ).start(); const api = await initializeApi(credentials, options); const result = await api.getPublishedCode(options.cardKey); diff --git a/src/cmds/register.ts b/src/cmds/register.ts index edba138..0be5c60 100644 --- a/src/cmds/register.ts +++ b/src/cmds/register.ts @@ -34,7 +34,10 @@ export async function registerCommand(options: Options) { }); } if (!options.email || !options.password) { - throw new CliError(ERROR_CODES.MISSING_EMAIL_OR_PASSWORD, "Email and password are required"); + throw new CliError( + ERROR_CODES.MISSING_EMAIL_OR_PASSWORD, + "Email and password are required", + ); } console.log("💳 registering account"); const result = await fetch("https://ipb.sandboxpay.co.za/auth/register", { diff --git a/src/cmds/simulate.ts b/src/cmds/simulate.ts index da0d81b..043b0a9 100644 --- a/src/cmds/simulate.ts +++ b/src/cmds/simulate.ts @@ -1,7 +1,8 @@ import chalk from "chalk"; import fs from "fs"; import { createTransaction } from "programmable-card-code-emulator"; -import { credentials, initializeApi } from "../index.js"; +import { credentials } from "../index.js"; +import { initializeApi } from "../utils.js"; import { handleCliError } from "../utils.js"; import { CliError, ERROR_CODES } from "../errors.js"; @@ -25,7 +26,10 @@ export async function simulateCommand(options: Options) { try { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + throw new CliError( + ERROR_CODES.MISSING_CARD_KEY, + "card-key is required", + ); } options.cardKey = Number(credentials.cardKey); } diff --git a/src/cmds/toggle.ts b/src/cmds/toggle.ts index cbf7244..13170eb 100644 --- a/src/cmds/toggle.ts +++ b/src/cmds/toggle.ts @@ -1,8 +1,8 @@ import { CliError, ERROR_CODES } from "../errors.js"; -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; interface Options extends CommonOptions { cardKey: number; @@ -17,7 +17,11 @@ export async function enableCommand(options: Options) { } try { printTitleBox(); - const spinner = ora("🍄 enabling code on card...").start(); + const disableSpinner = options.spinner === true; + const spinner = createSpinner( + !disableSpinner, + "🍄 enabling code on card...", + ).start(); const api = await initializeApi(credentials, options); const result = await api.toggleCode(options.cardKey, true); diff --git a/src/cmds/transactions.ts b/src/cmds/transactions.ts index ecde451..714664b 100644 --- a/src/cmds/transactions.ts +++ b/src/cmds/transactions.ts @@ -1,9 +1,7 @@ -import { credentials, initializePbApi, printTitleBox } from "../index.js"; -import { handleCliError, printTable } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializePbApi } from "../utils.js"; +import { handleCliError, printTable, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; - -interface Options extends CommonOptions {} /** * Minimal transaction type for CLI display. @@ -21,10 +19,17 @@ type Transaction = { * @param accountId - The account ID to fetch transactions for. * @param options - CLI options. */ -export async function transactionsCommand(accountId: string, options: Options) { +export async function transactionsCommand( + accountId: string, + options: CommonOptions, +) { try { printTitleBox(); - const spinner = ora("💳 fetching transactions...").start(); + const disableSpinner = options.spinner === true; + const spinner = createSpinner( + !disableSpinner, + "💳 fetching transactions...", + ).start(); const api = await initializePbApi(credentials, options); const result = await api.getAccountTransactions( @@ -49,13 +54,8 @@ export async function transactionsCommand(accountId: string, options: Options) { }), ); printTable(simpleTransactions); - console.log(`\n${transactions.length} transaction(s) found for account ${accountId}.`); + console.log(`\n${transactions.length} transaction(s) found.`); } catch (error: any) { - if (error.message && error.message === "Bad Request") { - console.log(""); - console.error(`Account with ID ${accountId} not found.`); - } else { - handleCliError(error, options, "fetch transactions"); - } + handleCliError(error, options, "fetch transactions"); } } diff --git a/src/cmds/transfer.ts b/src/cmds/transfer.ts index 7c16f7b..7b7db75 100644 --- a/src/cmds/transfer.ts +++ b/src/cmds/transfer.ts @@ -1,17 +1,15 @@ -import { credentials, initializePbApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializePbApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import { input, password } from "@inquirer/prompts"; -import ora from "ora"; - -interface Options extends CommonOptions {} +import { input } from "@inquirer/prompts"; export async function transferCommand( accountId: string, beneficiaryAccountId: string, amount: number, reference: string, - options: Options, + options: CommonOptions, ) { try { // Prompt for missing arguments interactively @@ -34,7 +32,8 @@ export async function transferCommand( reference = await input({ message: "Enter reference for the transfer:" }); } printTitleBox(); - const spinner = ora("💳 transfering...").start(); + const disableSpinner = options.spinner === true; + const spinner = createSpinner(!disableSpinner, "💳 transfering..."); const api = await initializePbApi(credentials, options); const result = await api.transferMultiple(accountId, [ @@ -48,7 +47,7 @@ export async function transferCommand( spinner.stop(); for (const transfer of result.data.TransferResponses) { console.log( - `Transfer to ${transfer.BeneficiaryAccountId}, reference ${transfer.PaymentReferenceNumber} was successful.`, + `Transfer to ${transfer.BeneficiaryAccountId}: ${transfer.Status}`, ); } } catch (error: any) { diff --git a/src/cmds/types.ts b/src/cmds/types.ts index 530edd0..576348f 100644 --- a/src/cmds/types.ts +++ b/src/cmds/types.ts @@ -6,6 +6,8 @@ export interface CommonOptions { clientSecret: string; credentialsFile: string; verbose: boolean; + spinner?: boolean; // allow disabling spinner + json?: boolean; // output in JSON format } export interface Credentials { diff --git a/src/cmds/upload-env.ts b/src/cmds/upload-env.ts index 1d8b65b..8d697bb 100644 --- a/src/cmds/upload-env.ts +++ b/src/cmds/upload-env.ts @@ -1,8 +1,8 @@ import fs from "fs"; -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; import { CliError, ERROR_CODES } from "../errors.js"; interface Options extends CommonOptions { @@ -22,7 +22,8 @@ export async function uploadEnvCommand(options: Options) { } try { printTitleBox(); - const spinner = ora("🚀 uploading env...").start(); + const disableSpinner = options.spinner === true; + const spinner = createSpinner(!disableSpinner, "🚀 uploading env..."); const api = await initializeApi(credentials, options); const raw = { variables: {} }; diff --git a/src/cmds/upload.ts b/src/cmds/upload.ts index e433537..c8ccd5e 100644 --- a/src/cmds/upload.ts +++ b/src/cmds/upload.ts @@ -1,8 +1,8 @@ import fs from "fs"; -import { credentials, initializeApi, printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; +import { credentials, printTitleBox } from "../index.js"; +import { initializeApi } from "../utils.js"; +import { handleCliError, createSpinner } from "../utils.js"; import type { CommonOptions } from "./types.js"; -import ora from "ora"; interface Options extends CommonOptions { cardKey: number; @@ -21,7 +21,8 @@ export async function uploadCommand(options: Options) { options.cardKey = Number(credentials.cardKey); } printTitleBox(); - const spinner = ora("🚀 uploading code...").start(); + const disableSpinner = options.spinner === true; // default false + const spinner = createSpinner(!disableSpinner, "🚀 uploading code..."); const api = await initializeApi(credentials, options); const raw = { code: "" }; const code = fs.readFileSync(options.filename).toString(); diff --git a/src/function-calls.ts b/src/function-calls.ts index 3203ffc..6bb50ce 100644 --- a/src/function-calls.ts +++ b/src/function-calls.ts @@ -1,5 +1,6 @@ import OpenAI from "openai"; -import { credentials, initializePbApi } from "./index.js"; +import { credentials } from "./index.js"; +import { initializePbApi } from "./utils.js"; import type { BasicOptions } from "./cmds/types.js"; import type { AccountBalance, diff --git a/src/index.ts b/src/index.ts index 99b68e1..21a33c3 100755 --- a/src/index.ts +++ b/src/index.ts @@ -115,6 +115,7 @@ function addApiCredentialOptions(cmd: Command) { "--credentials-file ", "Set a custom credentials file", ) + .option("-s,--spinner", "disable spinner during command execution") .option("-v,--verbose", "additional debugging information"); } @@ -261,7 +262,10 @@ async function main() { program.command("transfer").description("Allows transfer between accounts"), ) .argument("accountId", "accountId of the account to transfer from") - .argument("beneficiaryAccountId", "beneficiaryAccountId of the account to transfer to") + .argument( + "beneficiaryAccountId", + "beneficiaryAccountId of the account to transfer to", + ) .argument("amount", "amount to transfer in rands (e.g. 100.00)") .argument("reference", "reference for the transfer") .action(transferCommand); @@ -332,70 +336,6 @@ async function main() { } } -export async function initializeApi( - credentials: Credentials, - options: BasicOptions, -) { - //printTitleBox(); - credentials = await optionCredentials(options, credentials); - let api; - if (process.env.DEBUG == "true") { - // console.log(chalk.yellow('Using mock API for debugging')); - const { CardApi } = await import("./mock-card.js"); - api = new CardApi( - credentials.clientId, - credentials.clientSecret, - credentials.apiKey, - credentials.host, - ); - } else { - const { InvestecCardApi } = await import("investec-card-api"); - api = new InvestecCardApi( - credentials.clientId, - credentials.clientSecret, - credentials.apiKey, - credentials.host, - ); - } - const accessResult = await api.getAccessToken(); - // if (accessResult.scope !== "cards") { - // console.log( - // chalk.redBright( - // "Scope is not only cards, please consider reducing the scopes", - // ), - // ); - // console.log(""); - // } - return api; -} - -export async function initializePbApi( - credentials: Credentials, - options: BasicOptions, -) { - credentials = await optionCredentials(options, credentials); - let api; - if (process.env.DEBUG == "true") { - const { PbApi } = await import("./mock-pb.js"); - api = new PbApi( - credentials.clientId, - credentials.clientSecret, - credentials.apiKey, - credentials.host, - ); - } else { - const { InvestecPbApi } = await import("investec-pb-api"); - api = new InvestecPbApi( - credentials.clientId, - credentials.clientSecret, - credentials.apiKey, - credentials.host, - ); - } - await api.getAccessToken(); - return api; -} - export async function optionCredentials( options: BasicOptions, credentials: any, diff --git a/src/utils.ts b/src/utils.ts index 5ced54b..fe42344 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import chalk from "chalk"; -import type { Credentials } from "./cmds/types.js"; +import type { BasicOptions, Credentials } from "./cmds/types.js"; export function handleCliError( error: any, @@ -103,3 +103,94 @@ export async function loadCredentialsFile( } return credentials; } + +import ora from "ora"; +import { optionCredentials } from "./index.js"; + +// Spinner abstraction for testability and control +export interface Spinner { + start: (text?: string) => Spinner; + stop: () => Spinner; + text?: string; +} + +// Default spinner factory (uses ora) +export function createSpinner(enabled: boolean, text: string): Spinner { + if (!enabled) { + // No-op spinner: logs start/stop messages but does not animate + return { + start(msg?: string) { + if (msg || text) console.log(msg || text); + return this; + }, + stop() { + // Optionally log stop if needed + return this; + }, + }; + } + // Real spinner + return ora(text); +} +export async function initializePbApi( + credentials: Credentials, + options: BasicOptions, +) { + credentials = await optionCredentials(options, credentials); + let api; + if (process.env.DEBUG == "true") { + const { PbApi } = await import("./mock-pb.js"); + api = new PbApi( + credentials.clientId, + credentials.clientSecret, + credentials.apiKey, + credentials.host, + ); + } else { + const { InvestecPbApi } = await import("investec-pb-api"); + api = new InvestecPbApi( + credentials.clientId, + credentials.clientSecret, + credentials.apiKey, + credentials.host, + ); + } + await api.getAccessToken(); + return api; +} +export async function initializeApi( + credentials: Credentials, + options: BasicOptions, +) { + //printTitleBox(); + credentials = await optionCredentials(options, credentials); + let api; + if (process.env.DEBUG == "true") { + // console.log(chalk.yellow('Using mock API for debugging')); + const { CardApi } = await import("./mock-card.js"); + api = new CardApi( + credentials.clientId, + credentials.clientSecret, + credentials.apiKey, + credentials.host, + ); + } else { + const { InvestecCardApi } = await import("investec-card-api"); + api = new InvestecCardApi( + credentials.clientId, + credentials.clientSecret, + credentials.apiKey, + credentials.host, + ); + } + const accessResult = await api.getAccessToken(); + // if (accessResult.scope !== "cards") { + // console.log( + // chalk.redBright( + // "Scope is not only cards, please consider reducing the scopes", + // ), + // ); + // console.log(""); + // } + return api; +} From 6b6ab559bc2d5d81a5202ca4fe1a53ec515de56e Mon Sep 17 00:00:00 2001 From: Devin Date: Tue, 4 Nov 2025 10:48:21 +0200 Subject: [PATCH 03/47] implemented changes to improve the codebase and upgrade packages --- .github/copilot-instructions.md | 2 + BIOME_MIGRATION.md | 64 +++++ REVIEW.md | 350 ++++++++++++++++++++++++++ biome.json | 43 ++++ package-lock.json | 355 ++++++++++++++++++-------- package.json | 13 +- prettier.config.ts | 7 - scripts/advanced-fix-tests.js | 0 scripts/fix-spinner-mock.js | 0 scripts/fix-tests.sh | 0 scripts/test-accounts.js | 0 scripts/verify-process-emitter.js | 0 src/cmds/accounts.ts | 22 +- src/cmds/ai.ts | 131 +++++----- src/cmds/balances.ts | 27 +- src/cmds/bank.ts | 104 ++++---- src/cmds/beneficiaries.ts | 20 +- src/cmds/cards.ts | 30 +-- src/cmds/countries.ts | 18 +- src/cmds/currencies.ts | 18 +- src/cmds/deploy.ts | 51 ++-- src/cmds/disable.ts | 26 +- src/cmds/env.ts | 26 +- src/cmds/fetch.ts | 38 +-- src/cmds/index.ts | 38 +-- src/cmds/login.ts | 76 +++--- src/cmds/logs.ts | 33 +-- src/cmds/merchants.ts | 18 +- src/cmds/new.ts | 48 ++-- src/cmds/pay.ts | 37 ++- src/cmds/publish.ts | 36 +-- src/cmds/published.ts | 26 +- src/cmds/register.ts | 52 ++-- src/cmds/run.ts | 83 +++--- src/cmds/scopes.ts | 0 src/cmds/set.ts | 33 +-- src/cmds/simulate.ts | 61 ++--- src/cmds/toggle.ts | 26 +- src/cmds/transactions.ts | 32 +-- src/cmds/transfer.ts | 33 ++- src/cmds/upload-env.ts | 27 +- src/cmds/upload.ts | 23 +- src/errors.ts | 26 +- src/function-calls.ts | 189 +++++++------- src/index.ts | 388 ++++++++++++----------------- src/mock-card.ts | 179 ++++++------- src/mock-pb.ts | 157 ++++++------ src/utils.ts | 131 +++++----- templates/default/main.js | 6 +- templates/petro/main.js | 8 +- test/README.md | 0 test/README.md.extra | 0 test/__mocks__/external-editor.js | 0 test/__mocks__/index.js | 0 test/__mocks__/ora.js | 0 test/__mocks__/tmp.js | 0 test/__mocks__/utils.js | 0 test/cmds/cards.test.ts | 91 ++++--- test/cmds/deploy.test.ts | 25 +- test/cmds/special-accounts.test.ts | 0 test/helpers.d.ts | 0 test/helpers.ts | 0 test/setup.js | 0 test/test-template.ts | 0 64 files changed, 1788 insertions(+), 1439 deletions(-) create mode 100644 BIOME_MIGRATION.md create mode 100644 REVIEW.md create mode 100644 biome.json delete mode 100644 prettier.config.ts create mode 100644 scripts/advanced-fix-tests.js create mode 100644 scripts/fix-spinner-mock.js create mode 100644 scripts/fix-tests.sh create mode 100644 scripts/test-accounts.js create mode 100644 scripts/verify-process-emitter.js create mode 100644 src/cmds/scopes.ts create mode 100644 test/README.md create mode 100644 test/README.md.extra create mode 100644 test/__mocks__/external-editor.js create mode 100644 test/__mocks__/index.js create mode 100644 test/__mocks__/ora.js create mode 100644 test/__mocks__/tmp.js create mode 100644 test/__mocks__/utils.js create mode 100644 test/cmds/special-accounts.test.ts create mode 100644 test/helpers.d.ts create mode 100644 test/helpers.ts create mode 100644 test/setup.js create mode 100644 test/test-template.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f157f98..ba4ed26 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,3 +1,5 @@ +# Investec Programmable Banking CLI + This project is a command line tool for managing and interacting with Investec banks API services. It provides various commands to perform operations such as getting a list of bank accounts, balances, and transactions. the service also provides a way to write js snippets that are loaded on to your bank card account and executed when you make a payment. This allows you to automate certain actions or perform custom logic when spending money. diff --git a/BIOME_MIGRATION.md b/BIOME_MIGRATION.md new file mode 100644 index 0000000..8ef7b92 --- /dev/null +++ b/BIOME_MIGRATION.md @@ -0,0 +1,64 @@ +# Biome Migration Complete + +## What Changed + +The project has been successfully migrated from ESLint + Prettier to **Biome**, a fast all-in-one linter and formatter. + +## Benefits + +1. **Faster** - Biome is significantly faster than ESLint + Prettier +2. **Simpler** - One tool instead of two +3. **Better DX** - Faster feedback in editors +4. **TypeScript Support** - Built-in TypeScript support without additional plugins + +## Configuration + +- **Config file**: `biome.json` +- **Style**: Matches previous Prettier config: + - Single quotes + - 2-space indentation + - 100 character line width + - ES5 trailing commas + - Always arrow parentheses + +## New Scripts + +- `npm run lint` - Check for linting issues (Biome) +- `npm run lint:fix` - Auto-fix linting issues +- `npm run format` - Format code (Biome) +- `npm run format:check` - Check formatting without fixing +- `npm run type-check` - TypeScript type checking (separate from linting) + +## Removed Scripts + +- `npm run check-format` - Replaced by `npm run format:check` + +## CI/CD + +The `npm run ci` script has been updated to use Biome instead of Prettier and ESLint. + +## Optional Cleanup + +You can optionally remove these dependencies (they're no longer needed): +- `eslint` +- `@typescript-eslint/eslint-plugin` +- `@typescript-eslint/parser` +- `eslint-config-prettier` +- `prettier` + +To remove: +```bash +npm uninstall eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier prettier +``` + +You can also remove: +- `.eslintrc.json` +- `.eslintignore` +- `prettier.config.ts` + +## Next Steps + +1. Run `npm run lint:fix` to auto-fix any remaining issues +2. Review and commit the changes +3. Optionally remove old ESLint/Prettier files and dependencies + diff --git a/REVIEW.md b/REVIEW.md new file mode 100644 index 0000000..8db2d83 --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,350 @@ +# Code Review & Improvement Suggestions + +## Executive Summary + +This is a well-structured CLI tool for managing Investec Programmable Banking. The codebase demonstrates good organization and modern TypeScript practices. However, there are several areas for improvement in type safety, error handling, testing, and code quality. + +--- + +## 🔴 Critical Issues + +### 1. Type Safety - Extensive Use of `any` + +**Issue**: The codebase uses `any` types extensively, which defeats TypeScript's type checking benefits. + +**Found in**: +- `src/utils.ts:5` - `error: any` in `handleCliError` +- `src/index.ts:341` - `credentials: any` in `optionCredentials` +- `src/cmds/login.ts:27` - `options: any` in `loginCommand` +- All catch blocks use `error: any` (36+ occurrences) +- `src/function-calls.ts:197` - Function definitions use `any[]` + +**Recommendation**: +```typescript +// Instead of: +export function handleCliError(error: any, ...) + +// Use: +export function handleCliError(error: unknown, ...) + +// In catch blocks: +} catch (error: unknown) { + if (error instanceof Error) { + // handle Error + } else if (error instanceof CliError) { + // handle CliError + } +} +``` + +**Priority**: High - Type safety is crucial for maintainability + +--- + +### 2. Inconsistent Error Handling + +**Issue**: Error handling is inconsistent across commands. Some use `CliError`, others use generic `Error`. + +**Examples**: +- `src/cmds/login.ts:65` - Uses generic `Error` instead of `CliError` +- `src/cmds/register.ts:56` - Uses generic `Error` instead of `CliError` +- Many commands catch `any` but don't check error types + +**Recommendation**: +- Standardize on `CliError` for all CLI-specific errors +- Create error type guards +- Use consistent error codes from `ERROR_CODES` + +**Priority**: High - Better error handling improves user experience + +--- + +### 3. Missing Error Code in ERROR_CODES + +**Issue**: `src/errors.ts:13` has a syntax error - missing comma after `MISSING_API_TOKEN: "E4002"` + +**Current**: +```typescript +export const ERROR_CODES = { + MISSING_API_TOKEN: "E4002" // Missing comma + MISSING_CARD_KEY: "E4003", +``` + +**Recommendation**: Fix the syntax error + +**Priority**: High - This is a syntax error that may cause issues + +--- + +## 🟡 Important Improvements + +### 4. Dead/Commented Code + +**Issue**: There's commented-out code in multiple files that should be removed or uncommented. + +**Found in**: +- `src/index.ts:60-65` - Entire `printTitleBox` function is commented +- `src/utils.ts:169` - Commented debug log +- `src/cmds/deploy.ts:49,57` - Commented console.log statements + +**Recommendation**: +- Remove commented code if not needed +- Use proper logging/debugging infrastructure if needed +- Consider using a debug library (like `debug` package) instead of commenting code + +**Priority**: Medium - Clean code is easier to maintain + +--- + +### 5. Inconsistent Type Definitions + +**Issue**: `loginCommand` accepts `options: any` but other similar commands use proper types. + +**Example**: +- `src/cmds/login.ts:27` - `options: any` +- `src/cmds/register.ts:17` - `options: Options` (properly typed) + +**Recommendation**: Type `loginCommand` options properly: +```typescript +interface LoginOptions extends CommonOptions { + email: string; + password: string; +} + +export async function loginCommand(options: LoginOptions) { + // ... +} +``` + +**Priority**: Medium - Consistency improves maintainability + +--- + +### 6. File System Operations Not Using Async/Await Consistently + +**Issue**: Mix of sync and async file operations. + +**Found in**: +- `src/cmds/login.ts:84,88` - Uses `await fs.writeFileSync` (sync function with await) +- `src/index.ts:79` - Uses `fs.readFileSync` (sync) + +**Recommendation**: +- Use `fs.promises.writeFile` instead of `fs.writeFileSync` +- Use `fs.promises.readFile` instead of `fs.readFileSync` +- Or use `import { readFile, writeFile } from 'fs/promises'` + +**Priority**: Medium - Prevents blocking the event loop + +--- + +### 7. Credential File Security + +**Issue**: Credential files are stored in plain text without file permissions being set. + +**Found in**: +- `src/cmds/login.ts:84,88` - Writes credentials without setting restrictive permissions +- `src/cmds/set.ts` - Similar issue + +**Recommendation**: +```typescript +import { chmod } from 'fs/promises'; + +await fs.promises.writeFile(credentialLocation.filename, JSON.stringify(cred), { + mode: 0o600 // Read/write for owner only +}); +await chmod(credentialLocation.filename, 0o600); +``` + +**Priority**: High - Security concern + +--- + +### 8. Missing ESLint Configuration + +**Issue**: No ESLint configuration found, but there's a Prettier config. + +**Recommendation**: +- Add ESLint with TypeScript support +- Use `@typescript-eslint/recommended` and `@typescript-eslint/strict` +- Configure to catch `any` usage +- Add to CI pipeline + +**Priority**: Medium - Helps catch issues early + +--- + +### 9. Test Coverage Gaps + +**Issue**: Limited test files and many untracked test files in git. + +**Found**: +- Only 6 test files in `test/cmds/` +- Many commands lack tests +- Untracked test files: `test/README.md`, `test/README.md.extra`, `test/__mocks__/`, etc. + +**Recommendation**: +- Add tests for all commands +- Track test files in git +- Add coverage reporting +- Consider adding integration tests for critical paths + +**Priority**: Medium - Tests improve confidence in changes + +--- + +### 10. Missing JSDoc Comments + +**Issue**: Many functions lack JSDoc comments, making it harder to understand their purpose. + +**Good Example**: `src/cmds/accounts.ts:6-9` has good JSDoc + +**Recommendation**: Add JSDoc to all exported functions: +```typescript +/** + * Fetches and displays Investec accounts. + * @param options - CLI options including API credentials + * @throws {CliError} When API credentials are invalid or API call fails + */ +``` + +**Priority**: Low - Documentation improves developer experience + +--- + +## 🟢 Minor Improvements + +### 11. String Comparison Inconsistency + +**Issue**: `src/utils.ts:141,168` uses `==` instead of `===` for string comparison. + +**Current**: +```typescript +if (process.env.DEBUG == "true") { +``` + +**Recommendation**: Use strict equality: +```typescript +if (process.env.DEBUG === "true") { +``` + +**Priority**: Low - Best practice + +--- + +### 12. Markdown Linter Warning + +**Issue**: `.github/copilot-instructions.md` has a linter warning about first line not being a heading. + +**Recommendation**: Add a heading at the top of the file + +**Priority**: Low - Cosmetic + +--- + +### 13. Duplicate Code in Credential Loading + +**Issue**: Similar credential loading logic appears in multiple places. + +**Found in**: +- `src/index.ts:69-92` - Credential loading +- `src/cmds/login.ts:69-88` - Similar credential loading + +**Recommendation**: Extract to a shared utility function + +**Priority**: Low - DRY principle + +--- + +### 14. Missing Input Validation + +**Issue**: Some commands don't validate required inputs before making API calls. + +**Example**: `src/cmds/deploy.ts:25` - Checks for `cardKey` but type is `number` when it should be `string | number` + +**Recommendation**: Add validation early and use proper types + +**Priority**: Medium - Prevents runtime errors + +--- + +### 15. Inconsistent Error Context Messages + +**Issue**: Error context messages in `handleCliError` calls are inconsistent. + +**Examples**: +- `"fetch accounts"` vs `"deploy code"` vs `"login"` + +**Recommendation**: Standardize format (e.g., all lowercase or all "action object") + +**Priority**: Low - Consistency + +--- + +## 📋 Recommended Action Plan + +### Phase 1: Critical Fixes (Week 1) +1. ✅ Fix syntax error in `ERROR_CODES` +2. ✅ Replace all `any` types with proper types (`unknown` for errors) +3. ✅ Standardize error handling to use `CliError` +4. ✅ Fix credential file security (set file permissions) + +### Phase 2: Important Improvements (Week 2) +5. ✅ Add ESLint configuration +6. ✅ Remove dead/commented code +7. ✅ Fix async file operations +8. ✅ Add proper types to `loginCommand` and other `any` types + +### Phase 3: Quality Improvements (Week 3-4) +9. ✅ Add JSDoc comments to all exported functions +10. ✅ Improve test coverage +11. ✅ Extract duplicate credential loading logic +12. ✅ Add input validation + +### Phase 4: Polish (Ongoing) +13. ✅ Fix markdown linter warnings +14. ✅ Standardize error messages +15. ✅ Code review and refactoring + +--- + +## 🎯 Quick Wins + +These can be implemented immediately: + +1. **Fix syntax error** in `src/errors.ts` (missing comma) +2. **Replace `==` with `===`** in `src/utils.ts` +3. **Add heading** to `.github/copilot-instructions.md` +4. **Set file permissions** on credential files +5. **Remove commented code** or uncomment if needed + +--- + +## 📊 Code Quality Metrics + +- **Type Safety**: ⚠️ Moderate (extensive `any` usage) +- **Error Handling**: ⚠️ Moderate (inconsistent patterns) +- **Test Coverage**: ⚠️ Low (limited tests) +- **Documentation**: ✅ Good (README is comprehensive) +- **Security**: ⚠️ Moderate (credential handling needs improvement) +- **Code Organization**: ✅ Good (clear structure) + +--- + +## 🔗 Additional Resources + +- [TypeScript Error Handling Best Practices](https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript) +- [Node.js File Permissions](https://nodejs.org/api/fs.html#file-system-flags) +- [ESLint TypeScript Rules](https://typescript-eslint.io/rules/) + +--- + +## Conclusion + +The codebase is well-structured and functional. The main areas for improvement are: +1. **Type safety** - Eliminate `any` types +2. **Error handling** - Standardize on `CliError` +3. **Security** - Secure credential files +4. **Testing** - Increase coverage + +Addressing these issues will significantly improve code quality, maintainability, and developer confidence. + diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..04bc572 --- /dev/null +++ b/biome.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.3/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "lineEnding": "lf" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "error" + }, + "correctness": { + "noUnusedVariables": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "always" + } + }, + "json": { + "formatter": { + "trailingCommas": "none" + } + } +} diff --git a/package-lock.json b/package-lock.json index ff58687..f3b87ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,10 +26,11 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.2", + "@biomejs/biome": "^2.3.3", "@commander-js/extra-typings": "^13.1.0", "@types/node": "^22.12.0", - "prettier": "3.0.3", "rimraf": "^6.0.1", + "typescript": "^5.9.3", "vitest": "^3.1.1" } }, @@ -90,6 +91,183 @@ "node": ">=18" } }, + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.6.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.1-rc.tgz", + "integrity": "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.3.tgz", + "integrity": "sha512-zn/P1pRBCpDdhi+VNSMnpczOz9DnqzOA2c48K8xgxjDODvi5O8gs3a2H233rck/5HXpkFj6TmyoqVvxirZUnvg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.3", + "@biomejs/cli-darwin-x64": "2.3.3", + "@biomejs/cli-linux-arm64": "2.3.3", + "@biomejs/cli-linux-arm64-musl": "2.3.3", + "@biomejs/cli-linux-x64": "2.3.3", + "@biomejs/cli-linux-x64-musl": "2.3.3", + "@biomejs/cli-win32-arm64": "2.3.3", + "@biomejs/cli-win32-x64": "2.3.3" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.3.tgz", + "integrity": "sha512-5+JtW6RKmjqL9un0UtHV0ezOslAyYBzyl5ZhYiu7GHesX2x8NCDl6tXYrenv9m7e1RLbkO5E5Kh04kseMtz6lw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.3.tgz", + "integrity": "sha512-UPmKRalkHicvIpeccuKqq+/gA2HYV8FUnAEDJnqYBlGlycKqe6xrovWqvWTE4TTNpIFf4UQyuaDzLkN6Kz6tbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.3.tgz", + "integrity": "sha512-zeiKwALNB/hax7+LLhCYqhqzlWdTfgE9BGkX2Z8S4VmCYnGFrf2fON/ec6KCos7mra5MDm6fYICsEWN2+HKZhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.3.tgz", + "integrity": "sha512-KhCDMV+V7Yu72v40ssGJTHuv/j0n7JQ6l0s/c+EMcX5zPYLMLr4XpmI+WXhp4Vfkz0T5Xnh5wbrTBI3f2UTpjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.3.tgz", + "integrity": "sha512-05CjPLbvVVU8J6eaO6iSEoA0FXKy2l6ddL+1h/VpiosCmIp3HxRKLOa1hhC1n+D13Z8g9b1DtnglGtM5U3sTag==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.3.tgz", + "integrity": "sha512-IyqQ+jYzU5MVy9CK5NV0U+NnUMPUAhYMrB/x4QgL/Dl1MqzBVc61bHeyhLnKM6DSEk73/TQYrk/8/QmVHudLdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.3.tgz", + "integrity": "sha512-NtlLs3pdFqFAQYZjlEHKOwJEn3GEaz7rtR2oCrzaLT2Xt3Cfd55/VvodQ5V+X+KepLa956QJagckJrNL+DmumQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.3.tgz", + "integrity": "sha512-klJKPPQvUk9Rlp0Dd56gQw/+Wt6uUprHdHWtbDC93f3Iv+knA2tLWpcYoOZJgPV+9s+RBmYv0DGy4mUlr20esg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -536,6 +714,15 @@ "node": ">=18" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", + "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/checkbox": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz", @@ -597,14 +784,14 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.13", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", - "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", + "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.1", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", @@ -623,21 +810,6 @@ } } }, - "node_modules/@inquirer/core/node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@inquirer/core/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -653,14 +825,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.13.tgz", - "integrity": "sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==", + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.21.tgz", + "integrity": "sha512-MjtjOGjr0Kh4BciaFShYpZ1s9400idOdvQ5D7u7lE6VztPFoyLcVNE5dXBmEEIQq5zi4B9h2kU+q7AVBxJMAkQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.13", - "@inquirer/type": "^3.0.7", - "external-editor": "^3.1.0" + "@inquirer/core": "^10.3.0", + "@inquirer/external-editor": "^1.0.2", + "@inquirer/type": "^3.0.9" }, "engines": { "node": ">=18" @@ -696,10 +868,31 @@ } } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", + "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/figures": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", - "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", + "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", "license": "MIT", "engines": { "node": ">=18" @@ -898,9 +1091,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", - "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", + "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", "license": "MIT", "engines": { "node": ">=18" @@ -1514,9 +1707,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1590,9 +1783,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "license": "MIT" }, "node_modules/check-error": { @@ -2018,20 +2211,6 @@ "node": ">=12.0.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fdir": { "version": "6.4.5", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", @@ -2095,14 +2274,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -2336,15 +2516,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/investec-card-api": { @@ -2875,15 +3059,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -3018,22 +3193,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/programmable-card-code-emulator": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/programmable-card-code-emulator/-/programmable-card-code-emulator-1.4.2.tgz", @@ -3450,18 +3609,6 @@ "node": ">=14.0.0" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -3481,9 +3628,9 @@ } }, "node_modules/typescript": { - "version": "5.6.1-rc", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.1-rc.tgz", - "integrity": "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3534,9 +3681,9 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f4c5212..7ae67d4 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,14 @@ "clean": "rimraf ./bin", "copy-files": "cp -r ./templates/ ./bin/templates/ && cp -r ./assets/ ./bin/assets/ && cp instructions.txt ./bin/instructions.txt", "test": "vitest", - "lint": "tsc", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "format:check": "biome format .", + "type-check": "tsc", "check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm", - "check-format": "prettier --check .", - "ci": "npm run build && npm run check-format && npm run lint && npm run test && npm audit", + "ci": "npm run build && npm run type-check && npm run lint && npm run format:check && npm run test && npm audit", "dev": "vitest", - "format": "prettier --write .", "tapes": "./scripts/tapes.sh" }, "keywords": [ @@ -45,10 +47,11 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.2", + "@biomejs/biome": "^2.3.3", "@commander-js/extra-typings": "^13.1.0", "@types/node": "^22.12.0", - "prettier": "3.0.3", "rimraf": "^6.0.1", + "typescript": "^5.9.3", "vitest": "^3.1.1" }, "files": [ diff --git a/prettier.config.ts b/prettier.config.ts deleted file mode 100644 index 08b99d7..0000000 --- a/prettier.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - arrowParens: "always", - printWidth: 100, - proseWrap: "never", - singleQuote: true, - trailingComma: "es5", -}; diff --git a/scripts/advanced-fix-tests.js b/scripts/advanced-fix-tests.js new file mode 100644 index 0000000..e69de29 diff --git a/scripts/fix-spinner-mock.js b/scripts/fix-spinner-mock.js new file mode 100644 index 0000000..e69de29 diff --git a/scripts/fix-tests.sh b/scripts/fix-tests.sh new file mode 100644 index 0000000..e69de29 diff --git a/scripts/test-accounts.js b/scripts/test-accounts.js new file mode 100644 index 0000000..e69de29 diff --git a/scripts/verify-process-emitter.js b/scripts/verify-process-emitter.js new file mode 100644 index 0000000..e69de29 diff --git a/src/cmds/accounts.ts b/src/cmds/accounts.ts index bf2f33d..5ef87ca 100644 --- a/src/cmds/accounts.ts +++ b/src/cmds/accounts.ts @@ -1,7 +1,6 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializePbApi } from "../utils.js"; -import { handleCliError, printTable, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializePbApi, printTable } from '../utils.js'; +import type { CommonOptions } from './types.js'; /** * Fetch and display Investec accounts. @@ -11,17 +10,14 @@ export async function accountsCommand(options: CommonOptions) { try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "💳 fetching accounts...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💳 fetching accounts...').start(); const api = await initializePbApi(credentials, options); - if (options.verbose) console.log("💳 fetching accounts..."); + if (options.verbose) console.log('💳 fetching accounts...'); const result = await api.getAccounts(); const accounts = result.data.accounts; if (!accounts || accounts.length === 0) { spinner.stop(); - console.log("No accounts found"); + console.log('No accounts found'); return; } if (options.json) { @@ -33,13 +29,13 @@ export async function accountsCommand(options: CommonOptions) { accountNumber, referenceName, productName, - }), + }) ); spinner.stop(); printTable(simpleAccounts); console.log(`\n${accounts.length} account(s) found.`); } - } catch (error: any) { - handleCliError(error, options, "fetch accounts"); + } catch (error: unknown) { + handleCliError(error, options, 'fetch accounts'); } } diff --git a/src/cmds/ai.ts b/src/cmds/ai.ts index be71778..e27a267 100644 --- a/src/cmds/ai.ts +++ b/src/cmds/ai.ts @@ -1,15 +1,15 @@ -import fs from "fs"; -import chalk from "chalk"; -import OpenAI from "openai"; -import { zodResponseFormat } from "openai/helpers/zod"; -import { z } from "zod"; -import { printTitleBox, credentials } from "../index.js"; -import https from "https"; -import { handleCliError } from "../utils.js"; -import { input } from "@inquirer/prompts"; +import fs from 'node:fs'; +import https from 'node:https'; +import { input } from '@inquirer/prompts'; +import chalk from 'chalk'; +import OpenAI from 'openai'; +import { zodResponseFormat } from 'openai/helpers/zod'; +import { z } from 'zod'; +import { credentials, printTitleBox } from '../index.js'; +import { handleCliError } from '../utils.js'; const agent = new https.Agent({ - rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", + rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== 'false', }); const instructions = `- You are a coding assistant that creates code snippets for users. @@ -44,35 +44,32 @@ async function afterDecline(transaction) { - Output must be Javascript format only`; // Define the desired output schema using Zod const outputSchema = z.object({ - code: z.string().describe("The code to be generated"), - env_variables: z - .array(z.string()) - .nullable() - .describe("Environment variables"), - description: z.string().describe("Description of the code and how to use it"), + code: z.string().describe('The code to be generated'), + env_variables: z.array(z.string()).nullable().describe('Environment variables'), + description: z.string().describe('Description of the code and how to use it'), example_transaction: z .object({ - accountNumber: z.string().describe("Account number"), - dateTime: z.string().describe("Date and time of the transaction"), - centsAmount: z.number().describe("Amount in cents"), - currencyCode: z.string().describe("Currency code"), - reference: z.string().describe("Reference string"), + accountNumber: z.string().describe('Account number'), + dateTime: z.string().describe('Date and time of the transaction'), + centsAmount: z.number().describe('Amount in cents'), + currencyCode: z.string().describe('Currency code'), + reference: z.string().describe('Reference string'), merchant: z .object({ - name: z.string().describe("Merchant name"), - city: z.string().describe("Merchant city"), - country: z.string().describe("Merchant country"), + name: z.string().describe('Merchant name'), + city: z.string().describe('Merchant city'), + country: z.string().describe('Merchant country'), category: z .object({ - key: z.string().describe("Category key"), - code: z.string().describe("Category code"), - name: z.string().describe("Category name"), + key: z.string().describe('Category key'), + code: z.string().describe('Category code'), + name: z.string().describe('Category name'), }) - .describe("Category object"), + .describe('Category object'), }) - .describe("Merchant object"), + .describe('Merchant object'), }) - .describe("Example transaction"), + .describe('Example transaction'), // instructions: z.array(z.string()).describe("Step-by-step instructions"), // prepTime: z.string().optional().describe("Preparation time (optional)"), }); @@ -90,11 +87,11 @@ interface Options { // node . 'allow transactions that USD or ZAR' export async function aiCommand(prompt: string, options: Options) { try { - const envFilename = ".env.ai"; + const envFilename = '.env.ai'; printTitleBox(); // Prompt for prompt if not provided if (!prompt) { - prompt = await input({ message: "Enter your AI code prompt:" }); + prompt = await input({ message: 'Enter your AI code prompt:' }); } // if (!credentials.openaiKey) { // throw new Error("OPENAI_API_KEY is not set"); @@ -104,100 +101,88 @@ export async function aiCommand(prompt: string, options: Options) { // } // tell the user we are loading the instructions - console.log(chalk.blueBright("Loading instructions from instructions.txt")); + console.log(chalk.blueBright('Loading instructions from instructions.txt')); // read the instructions from the file //const instructions = fs.readFileSync("./instructions.txt").toString(); - console.log( - chalk.blueBright("Calling OpenAI with the prompt and instructions"), - ); - console.log(chalk.blueBright("Prompt:")); + console.log(chalk.blueBright('Calling OpenAI with the prompt and instructions')); + console.log(chalk.blueBright('Prompt:')); console.log(prompt); const response = await generateCode(prompt, instructions); // mention calling open ai with the prompt and instructions if (options.verbose) { - console.log(""); - console.log(chalk.blueBright("Response from OpenAI:")); + console.log(''); + console.log(chalk.blueBright('Response from OpenAI:')); console.log(response); } else { - console.log(""); - console.log(chalk.blueBright("Response from OpenAI:")); - console.log(chalk.blueBright("Description:")); + console.log(''); + console.log(chalk.blueBright('Response from OpenAI:')); + console.log(chalk.blueBright('Description:')); console.log(response?.description); } - console.log(""); - var output = response?.code as string; + console.log(''); + const output = response?.code as string; // remove ```javascript // seems to only be needed if its not structured output // output = output.replace(/```javascript/g, ""); // remove ``` // output = output.replace(/```/g, ""); console.log(`💾 saving to file: ${chalk.greenBright(options.filename)}`); await fs.writeFileSync(options.filename, output); - console.log("🎉 generated code saved to file"); + console.log('🎉 generated code saved to file'); // write the env variables to a file if (response?.env_variables) { - console.log(""); - console.log( - `💾 saving env variables to file: ${chalk.greenBright(envFilename)}`, - ); + console.log(''); + console.log(`💾 saving env variables to file: ${chalk.greenBright(envFilename)}`); const envFile = fs.createWriteStream(envFilename); response.env_variables.forEach((envVar) => { envFile.write(`${envVar}=${process.env[envVar]}\n`); }); envFile.end(); - console.log("🎉 env variables saved to file"); + console.log('🎉 env variables saved to file'); } // show example call to ipb rub with example transaction if (response?.example_transaction) { - console.log(""); - console.log(chalk.blueBright("To test locally run:")); + console.log(''); + console.log(chalk.blueBright('To test locally run:')); console.log( - `ipb run -f ai-generated.js --env ai --currency ${response.example_transaction.currencyCode} --amount ${response.example_transaction.centsAmount} --mcc ${response.example_transaction.merchant.category.code} --merchant '${response.example_transaction.merchant.name}' --city '${response.example_transaction.merchant.city}' --country '${response.example_transaction.merchant.country}'`, + `ipb run -f ai-generated.js --env ai --currency ${response.example_transaction.currencyCode} --amount ${response.example_transaction.centsAmount} --mcc ${response.example_transaction.merchant.category.code} --merchant '${response.example_transaction.merchant.name}' --city '${response.example_transaction.merchant.city}' --country '${response.example_transaction.merchant.country}'` ); } - } catch (error: any) { - handleCliError(error, options, "AI generation"); + } catch (error: unknown) { + handleCliError(error, options, 'AI generation'); } } -async function generateCode( - prompt: string, - instructions: string, -): Promise { +async function generateCode(prompt: string, instructions: string): Promise { try { let openai = new OpenAI({ apiKey: credentials.openaiKey, }); - if (credentials.openaiKey === "" || credentials.openaiKey === undefined) { + if (credentials.openaiKey === '' || credentials.openaiKey === undefined) { openai = new OpenAI({ httpAgent: agent, apiKey: credentials.sandboxKey, - baseURL: "https://ipb.sandboxpay.co.za/proxy/v1", + baseURL: 'https://ipb.sandboxpay.co.za/proxy/v1', }); } const response = await openai.chat.completions.create({ - model: "gpt-4.1", + model: 'gpt-4.1', temperature: 0.2, messages: [ - { role: "system", content: instructions }, - { role: "user", content: prompt }, + { role: 'system', content: instructions }, + { role: 'user', content: prompt }, ], - response_format: zodResponseFormat(outputSchema, "output_schema"), + response_format: zodResponseFormat(outputSchema, 'output_schema'), }); - if ( - response.choices && - response.choices[0] && - response.choices[0].message && - response.choices[0].message.content - ) { + if (response.choices?.[0]?.message?.content) { const content = response.choices[0].message.content; return outputSchema.parse(JSON.parse(content)); } - throw new Error("Invalid response format from OpenAI"); + throw new Error('Invalid response format from OpenAI'); } catch (error) { - console.error("Error generating code:", error); + console.error('Error generating code:', error); return null; } } diff --git a/src/cmds/balances.ts b/src/cmds/balances.ts index 024bcba..4505ed6 100644 --- a/src/cmds/balances.ts +++ b/src/cmds/balances.ts @@ -1,19 +1,12 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializePbApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializePbApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; -export async function balancesCommand( - accountId: string, - options: CommonOptions, -) { +export async function balancesCommand(accountId: string, options: CommonOptions) { try { printTitleBox(); const disableSpinner = options.spinner === true; - const spinner = createSpinner( - !disableSpinner, - "💳 fetching balances...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💳 fetching balances...').start(); const api = await initializePbApi(credentials, options); const result = await api.getAccountBalances(accountId); @@ -25,19 +18,19 @@ export async function balancesCommand( } else { console.log(`Account Id ${result.data.accountId}`); console.log(`Currency: ${result.data.currency}`); - console.log("Balances:"); + console.log('Balances:'); console.log(`Current: ${result.data.currentBalance}`); console.log(`Available: ${result.data.availableBalance}`); console.log(`Budget: ${result.data.budgetBalance}`); console.log(`Straight: ${result.data.straightBalance}`); console.log(`Cash: ${result.data.cashBalance}`); } - } catch (error: any) { - if (error.message && error.message === "Bad Request") { - console.log(""); + } catch (error: unknown) { + if (error instanceof Error && error.message === 'Bad Request') { + console.log(''); console.error(`Account with ID ${accountId} not found.`); } else { - handleCliError(error, options, "fetch balances"); + handleCliError(error, options, 'fetch balances'); } } } diff --git a/src/cmds/bank.ts b/src/cmds/bank.ts index 1af6494..eca4c3a 100644 --- a/src/cmds/bank.ts +++ b/src/cmds/bank.ts @@ -1,17 +1,16 @@ -import fs from "fs"; -import chalk from "chalk"; -import OpenAI from "openai"; -import { printTitleBox, credentials } from "../index.js"; -import https from "https"; -import { availableFunctions, tools } from "../function-calls.js"; -import { handleCliError } from "../utils.js"; -import { input } from "@inquirer/prompts"; +import https from 'node:https'; +import { input } from '@inquirer/prompts'; +import chalk from 'chalk'; +import OpenAI from 'openai'; +import { availableFunctions, tools } from '../function-calls.js'; +import { credentials, printTitleBox } from '../index.js'; +import { handleCliError } from '../utils.js'; const agent = new https.Agent({ - rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", + rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== 'false', }); -let openai: OpenAI | undefined = undefined; +let openai: OpenAI | undefined; const instructions = `- You are a banking bot, enabling the user to access their investec accounts based on user input. -if fetching transactions only retrieve from 5 days ago`; @@ -27,22 +26,22 @@ export async function bankCommand(prompt: string, options: Options) { printTitleBox(); // Prompt for prompt if not provided if (!prompt) { - prompt = await input({ message: "Enter your banking prompt:" }); + prompt = await input({ message: 'Enter your banking prompt:' }); } openai = new OpenAI({ apiKey: credentials.openaiKey, }); - if (credentials.openaiKey === "" || credentials.openaiKey === undefined) { + if (credentials.openaiKey === '' || credentials.openaiKey === undefined) { openai = new OpenAI({ httpAgent: agent, apiKey: credentials.sandboxKey, - baseURL: "https://ipb.sandboxpay.co.za/proxy/v1", + baseURL: 'https://ipb.sandboxpay.co.za/proxy/v1', }); } if (!openai) { - throw new Error("OpenAI client is not initialized"); + throw new Error('OpenAI client is not initialized'); } // if (!credentials.openaiKey) { // throw new Error("OPENAI_API_KEY is not set"); @@ -52,51 +51,46 @@ export async function bankCommand(prompt: string, options: Options) { // } // tell the user we are loading the instructions - console.log(chalk.blueBright("Loading instructions from instructions.txt")); + console.log(chalk.blueBright('Loading instructions from instructions.txt')); // read the instructions from the file //const instructions = fs.readFileSync("./instructions.txt").toString(); - console.log( - chalk.blueBright("Calling OpenAI with the prompt and instructions"), - ); - console.log(chalk.blueBright("Prompt:")); + console.log(chalk.blueBright('Calling OpenAI with the prompt and instructions')); + console.log(chalk.blueBright('Prompt:')); console.log(prompt); const response = await generateResponse(prompt, instructions); // mention calling open ai with the prompt and instructions if (options.verbose) { - console.log(""); - console.log(chalk.blueBright("Response from OpenAI:")); + console.log(''); + console.log(chalk.blueBright('Response from OpenAI:')); console.log(response); } else { - console.log(""); - console.log(chalk.blueBright("Response from OpenAI:")); + console.log(''); + console.log(chalk.blueBright('Response from OpenAI:')); //console.log(chalk.blueBright("Description:")); console.log(response); } - } catch (error: any) { - handleCliError(error, options, "bank command"); + } catch (error: unknown) { + handleCliError(error, options, 'bank command'); } } -async function generateResponse( - prompt: string, - instructions: string, -): Promise { +async function generateResponse(prompt: string, instructions: string): Promise { try { // Use OpenAI chat completions API correctly const messages: OpenAI.ChatCompletionMessageParam[] = [ - { role: "system", content: instructions }, - { role: "user", content: prompt }, + { role: 'system', content: instructions }, + { role: 'user', content: prompt }, ]; if (!openai) { - throw new Error("OpenAI client is not initialized"); + throw new Error('OpenAI client is not initialized'); } const completion = await openai.chat.completions.create({ - model: "gpt-4.1", + model: 'gpt-4.1', temperature: 0.2, messages, tools, @@ -104,13 +98,8 @@ async function generateResponse( //console.log("OpenAI response received"); //console.log(completion.choices) // Defensive: check completion.choices[0] and .message - const message = - completion.choices && - completion.choices[0] && - completion.choices[0].message - ? completion.choices[0].message - : undefined; - if (!message) throw new Error("No message returned from OpenAI"); + const message = completion.choices?.[0]?.message ? completion.choices[0].message : undefined; + if (!message) throw new Error('No message returned from OpenAI'); if (message.tool_calls) { return await toolCall(message, tools, messages); @@ -118,21 +107,21 @@ async function generateResponse( const content = message.content; return content; } - throw new Error("Invalid response format from OpenAI"); + throw new Error('Invalid response format from OpenAI'); } catch (error) { - console.error("Error generating code:", error); + console.error('Error generating code:', error); return null; } } async function secondCall( - functionResponse: string, + functionResponse: unknown, messages: OpenAI.ChatCompletionMessageParam[], toolCaller: OpenAI.ChatCompletionMessageToolCall, - tools: OpenAI.ChatCompletionTool[], + tools: OpenAI.ChatCompletionTool[] ) { if (!openai) { - throw new Error("OpenAI client is not initialized"); + throw new Error('OpenAI client is not initialized'); } // Compose the correct message sequence for tool call follow-up // Only include the original system/user messages, then the assistant message with tool_calls, then the tool message @@ -141,32 +130,27 @@ async function secondCall( messages[0] as OpenAI.ChatCompletionMessageParam, // system messages[1] as OpenAI.ChatCompletionMessageParam, // user { - role: "assistant", + role: 'assistant', content: null, tool_calls: [ toolCaller, // tool call from the assistant message ], } as OpenAI.ChatCompletionMessageParam, { - role: "tool", + role: 'tool', tool_call_id: toolCaller.id, content: - typeof functionResponse === "string" - ? functionResponse - : JSON.stringify(functionResponse), + typeof functionResponse === 'string' ? functionResponse : JSON.stringify(functionResponse), } as OpenAI.ChatCompletionToolMessageParam, ]; const response2 = await openai.chat.completions.create({ - model: "gpt-4.1", + model: 'gpt-4.1', messages: followupMessages, tools, }); - const message = - response2.choices && response2.choices[0] && response2.choices[0].message - ? response2.choices[0].message - : undefined; - if (!message) throw new Error("No message returned from OpenAI"); + const message = response2.choices?.[0]?.message ? response2.choices[0].message : undefined; + if (!message) throw new Error('No message returned from OpenAI'); if (message.tool_calls) { return await toolCall(message, tools, messages); } @@ -181,12 +165,12 @@ async function secondCall( async function toolCall( message: OpenAI.ChatCompletionMessage, tools: OpenAI.ChatCompletionTool[], - messages: OpenAI.ChatCompletionMessageParam[], + messages: OpenAI.ChatCompletionMessageParam[] ): Promise { // Defensive: check if message has tool_calls property (should be on ChatCompletionMessage, not ChatCompletionToolMessageParam) - const toolCalls = (message as any).tool_calls; + const toolCalls = 'tool_calls' in message && message.tool_calls ? message.tool_calls : undefined; if (!toolCalls) { - throw new Error("No tool_calls found in message"); + throw new Error('No tool_calls found in message'); } for (const toolCall of toolCalls) { @@ -197,5 +181,5 @@ async function toolCall( const functionResponse = await functionToCall(functionArgs); return await secondCall(functionResponse, messages, toolCall, tools); } - throw new Error("Invalid response format from OpenAI"); + throw new Error('Invalid response format from OpenAI'); } diff --git a/src/cmds/beneficiaries.ts b/src/cmds/beneficiaries.ts index fc1c231..97bd467 100644 --- a/src/cmds/beneficiaries.ts +++ b/src/cmds/beneficiaries.ts @@ -1,23 +1,19 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializePbApi } from "../utils.js"; -import { handleCliError, printTable, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializePbApi, printTable } from '../utils.js'; +import type { CommonOptions } from './types.js'; export async function beneficiariesCommand(options: CommonOptions) { try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "💳 fetching beneficiaries...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💳 fetching beneficiaries...').start(); const api = await initializePbApi(credentials, options); const result = await api.getBeneficiaries(); const beneficiaries = result.data; spinner.stop(); if (!beneficiaries) { - console.log("No beneficiaries found"); + console.log('No beneficiaries found'); return; } const simpleBeneficiaries = beneficiaries.map( @@ -35,11 +31,11 @@ export async function beneficiariesCommand(options: CommonOptions) { lastPaymentDate, lastPaymentAmount, referenceName, - }), + }) ); printTable(simpleBeneficiaries); console.log(`\n${beneficiaries.length} beneficiary(ies) found.`); - } catch (error: any) { - handleCliError(error, options, "fetch beneficiaries"); + } catch (error: unknown) { + handleCliError(error, options, 'fetch beneficiaries'); } } diff --git a/src/cmds/cards.ts b/src/cmds/cards.ts index c8dd037..f697c09 100644 --- a/src/cmds/cards.ts +++ b/src/cmds/cards.ts @@ -1,36 +1,30 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, printTable, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi, printTable } from '../utils.js'; +import type { CommonOptions } from './types.js'; export async function cardsCommand(options: CommonOptions) { try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "💳 fetching cards...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💳 fetching cards...').start(); const api = await initializeApi(credentials, options); const result = await api.getCards(); const cards = result.data.cards; spinner.stop(); if (!cards) { - console.log("No cards found"); + console.log('No cards found'); return; } - const simpleCards = cards.map( - ({ CardKey, CardNumber, IsProgrammable }) => ({ - CardKey, - CardNumber, - IsProgrammable, - }), - ); + const simpleCards = cards.map(({ CardKey, CardNumber, IsProgrammable }) => ({ + CardKey, + CardNumber, + IsProgrammable, + })); printTable(simpleCards); console.log(`\n${cards.length} card(s) found.`); - } catch (error: any) { - handleCliError(error, options, "fetch cards"); + } catch (error: unknown) { + handleCliError(error, options, 'fetch cards'); } } diff --git a/src/cmds/countries.ts b/src/cmds/countries.ts index 2839040..48ed023 100644 --- a/src/cmds/countries.ts +++ b/src/cmds/countries.ts @@ -1,30 +1,26 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, printTable, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi, printTable } from '../utils.js'; +import type { CommonOptions } from './types.js'; export async function countriesCommand(options: CommonOptions) { try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "💳 fetching countries...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💳 fetching countries...').start(); const api = await initializeApi(credentials, options); const result = await api.getCountries(); spinner.stop(); const countries = result.data.result; if (!countries) { - console.log("No countries found"); + console.log('No countries found'); return; } const simpleCountries = countries.map(({ Code, Name }) => ({ Code, Name })); printTable(simpleCountries); console.log(`\n${countries.length} country(ies) found.`); - } catch (error: any) { - handleCliError(error, options, "fetch countries"); + } catch (error: unknown) { + handleCliError(error, options, 'fetch countries'); } } diff --git a/src/cmds/currencies.ts b/src/cmds/currencies.ts index a58174e..ebb38ba 100644 --- a/src/cmds/currencies.ts +++ b/src/cmds/currencies.ts @@ -1,23 +1,19 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, printTable, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi, printTable } from '../utils.js'; +import type { CommonOptions } from './types.js'; export async function currenciesCommand(options: CommonOptions) { try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "💳 fetching currencies...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💳 fetching currencies...').start(); const api = await initializeApi(credentials, options); const result = await api.getCurrencies(); spinner.stop(); const currencies = result.data.result; if (!currencies) { - console.log("No currencies found"); + console.log('No currencies found'); return; } @@ -27,7 +23,7 @@ export async function currenciesCommand(options: CommonOptions) { })); printTable(simpleCurrencies); console.log(`\n${currencies.length} currency(ies) found.`); - } catch (error: any) { - handleCliError(error, options, "fetch currencies"); + } catch (error: unknown) { + handleCliError(error, options, 'fetch currencies'); } } diff --git a/src/cmds/deploy.ts b/src/cmds/deploy.ts index ebdad59..3d85a75 100644 --- a/src/cmds/deploy.ts +++ b/src/cmds/deploy.ts @@ -1,11 +1,9 @@ -import fs from "fs"; -import dotenv from "dotenv"; -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { Spinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; -import { CliError, ERROR_CODES } from "../errors.js"; +import fs from 'node:fs'; +import dotenv from 'dotenv'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -17,17 +15,11 @@ export async function deployCommand(options: Options) { try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "💳 starting deployment...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💳 starting deployment...').start(); let envObject = {}; if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new CliError( - ERROR_CODES.MISSING_CARD_KEY, - "card-key is required", - ); + if (credentials.cardKey === '') { + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, 'card-key is required'); } options.cardKey = Number(credentials.cardKey); } @@ -38,33 +30,24 @@ export async function deployCommand(options: Options) { if (!fs.existsSync(`.env.${options.env}`)) { throw new CliError( ERROR_CODES.MISSING_ENV_FILE, - `Env file .env.${options.env} does not exist`, + `Env file .env.${options.env} does not exist` ); } spinner.text = `📦 uploading env from .env.${options.env}`; envObject = dotenv.parse(fs.readFileSync(`.env.${options.env}`)); await api.uploadEnv(options.cardKey, { variables: envObject }); - spinner.text = "📦 env uploaded"; - //console.log("📦 env deployed"); + spinner.text = '📦 env uploaded'; } - spinner.text = "🚀 deploying code"; - //console.log("🚀 deploying code"); - const raw = { code: "" }; + spinner.text = '🚀 deploying code'; + const raw = { code: '' }; const code = fs.readFileSync(options.filename).toString(); raw.code = code; const saveResult = await api.uploadCode(options.cardKey, raw); - // console.log(saveResult); - const result = await api.uploadPublishedCode( - options.cardKey, - saveResult.data.result.codeId, - code, - ); + await api.uploadPublishedCode(options.cardKey, saveResult.data.result.codeId, code); spinner.stop(); - console.log( - `🎉 code deployed with codeId: ${saveResult.data.result.codeId}`, - ); - } catch (error: any) { - handleCliError(error, options, "deploy code"); + console.log(`🎉 code deployed with codeId: ${saveResult.data.result.codeId}`); + } catch (error: unknown) { + handleCliError(error, options, 'deploy code'); } } diff --git a/src/cmds/disable.ts b/src/cmds/disable.ts index 64a6cfd..47a9021 100644 --- a/src/cmds/disable.ts +++ b/src/cmds/disable.ts @@ -1,8 +1,7 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; -import { CliError, ERROR_CODES } from "../errors.js"; +import { CliError, ERROR_CODES } from '../errors.js'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -10,28 +9,25 @@ interface Options extends CommonOptions { export async function disableCommand(options: Options) { if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + if (credentials.cardKey === '') { + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, 'card-key is required'); } options.cardKey = Number(credentials.cardKey); } try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "🍄 disabling code on card...", - ).start(); + const spinner = createSpinner(!disableSpinner, '🍄 disabling code on card...').start(); const api = await initializeApi(credentials, options); const result = await api.toggleCode(options.cardKey, false); spinner.stop(); if (!result.data.result.Enabled) { - console.log("✅ code disabled successfully"); + console.log('✅ code disabled successfully'); } else { - console.log("❌ code disable failed"); + console.log('❌ code disable failed'); } - } catch (error: any) { - handleCliError(error, options, "disable card code"); + } catch (error: unknown) { + handleCliError(error, options, 'disable card code'); } } diff --git a/src/cmds/env.ts b/src/cmds/env.ts index d5c5fb2..5434110 100644 --- a/src/cmds/env.ts +++ b/src/cmds/env.ts @@ -1,9 +1,8 @@ -import fs from "fs"; -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; -import { CliError, ERROR_CODES } from "../errors.js"; +import fs from 'node:fs'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -12,26 +11,23 @@ interface Options extends CommonOptions { export async function envCommand(options: Options) { if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + if (credentials.cardKey === '') { + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, 'card-key is required'); } options.cardKey = Number(credentials.cardKey); } try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "💎 fetching envs...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💎 fetching envs...').start(); const api = await initializeApi(credentials, options); const result = await api.getEnv(options.cardKey); const envs = result.data.result.variables; spinner.stop(); console.log(`💾 saving to file: ${options.filename}`); fs.writeFileSync(options.filename, JSON.stringify(envs, null, 4)); - console.log("🎉 envs saved to file"); - } catch (error: any) { - handleCliError(error, options, "fetch environment variables"); + console.log('🎉 envs saved to file'); + } catch (error: unknown) { + handleCliError(error, options, 'fetch environment variables'); } } diff --git a/src/cmds/fetch.ts b/src/cmds/fetch.ts index 2c55b51..a0e2392 100644 --- a/src/cmds/fetch.ts +++ b/src/cmds/fetch.ts @@ -1,8 +1,7 @@ -import fs from "fs"; -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import fs from 'node:fs'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -11,27 +10,36 @@ interface Options extends CommonOptions { export async function fetchCommand(options: Options) { if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new Error("card-key is required"); + if (credentials.cardKey === '') { + throw new Error('card-key is required'); } options.cardKey = Number(credentials.cardKey); } try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "💳 fetching code...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💳 fetching code...').start(); const api = await initializeApi(credentials, options); - const result = await api.getCode(options.cardKey); + + // The api object may not have a getCode method; use getSavedCode if available, or handle gracefully + if (typeof (api as any).getSavedCode !== 'function') { + spinner.stop(); + throw new Error('API client does not support fetching saved code (getSavedCode missing)'); + } + const result = await (api as any).getSavedCode(options.cardKey); + + if (!result || !result.data || !result.data.result || typeof result.data.result.code !== 'string') { + spinner.stop(); + throw new Error('Failed to fetch code: Unexpected API response'); + } + const code = result.data.result.code; spinner.stop(); console.log(`💾 saving to file: ${options.filename}`); await fs.writeFileSync(options.filename, code); - console.log("🎉 code saved to file"); - } catch (error: any) { - handleCliError(error, options, "fetch saved code"); + console.log('🎉 code saved to file'); + } catch (error: unknown) { + handleCliError(error, options, 'fetch saved code'); } } diff --git a/src/cmds/index.ts b/src/cmds/index.ts index c0a7333..9b8a94c 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -1,22 +1,22 @@ -import { cardsCommand } from "./cards.js"; -import { envCommand } from "./env.js"; -import { logsCommand } from "./logs.js"; -import { fetchCommand } from "./fetch.js"; -import { publishCommand } from "./publish.js"; -import { publishedCommand } from "./published.js"; -import { runCommand } from "./run.js"; -import { enableCommand } from "./toggle.js"; -import { disableCommand } from "./disable.js"; -import { uploadCommand } from "./upload.js"; -import { uploadEnvCommand } from "./upload-env.js"; -import { deployCommand } from "./deploy.js"; -import { configCommand } from "./set.js"; -import { currenciesCommand } from "./currencies.js"; -import { countriesCommand } from "./countries.js"; -import { merchantsCommand } from "./merchants.js"; -import { newCommand } from "./new.js"; -import { aiCommand } from "./ai.js"; -import { bankCommand } from "./bank.js"; +import { aiCommand } from './ai.js'; +import { bankCommand } from './bank.js'; +import { cardsCommand } from './cards.js'; +import { countriesCommand } from './countries.js'; +import { currenciesCommand } from './currencies.js'; +import { deployCommand } from './deploy.js'; +import { disableCommand } from './disable.js'; +import { envCommand } from './env.js'; +import { fetchCommand } from './fetch.js'; +import { logsCommand } from './logs.js'; +import { merchantsCommand } from './merchants.js'; +import { newCommand } from './new.js'; +import { publishCommand } from './publish.js'; +import { publishedCommand } from './published.js'; +import { runCommand } from './run.js'; +import { configCommand } from './set.js'; +import { enableCommand } from './toggle.js'; +import { uploadCommand } from './upload.js'; +import { uploadEnvCommand } from './upload-env.js'; export { cardsCommand, diff --git a/src/cmds/login.ts b/src/cmds/login.ts index 14ee3b3..f9eb1b3 100644 --- a/src/cmds/login.ts +++ b/src/cmds/login.ts @@ -1,13 +1,13 @@ -import { credentialLocation, printTitleBox } from "../index.js"; -import fs from "fs"; -import fetch from "node-fetch"; -import https from "https"; -import { handleCliError } from "../utils.js"; -import { input, password } from "@inquirer/prompts"; -import { CliError, ERROR_CODES } from "../errors.js"; +import fs, { promises as fsPromises } from 'node:fs'; +import https from 'node:https'; +import { input, password } from '@inquirer/prompts'; +import fetch from 'node-fetch'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { credentialLocation, printTitleBox } from '../index.js'; +import { handleCliError, writeCredentialsFile } from '../utils.js'; const agent = new https.Agent({ - rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", + rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== 'false', }); interface Options { @@ -24,36 +24,31 @@ interface LoginResponse { created_at: number; } -export async function loginCommand(options: any) { +export async function loginCommand(options: Options) { try { printTitleBox(); if (!options.email) { options.email = await input({ - message: "Enter your email:", - validate: (input: string) => - input.includes("@") || "Please enter a valid email.", + message: 'Enter your email:', + validate: (input: string) => input.includes('@') || 'Please enter a valid email.', }); } if (!options.password) { options.password = await password({ - message: "Enter your password:", - mask: "*", - validate: (input: string) => - input.length >= 6 || "Password must be at least 6 characters.", + message: 'Enter your password:', + mask: '*', + validate: (input: string) => input.length >= 6 || 'Password must be at least 6 characters.', }); } if (!options.email || !options.password) { - throw new CliError( - ERROR_CODES.INVALID_CREDENTIALS, - "Email and password are required", - ); + throw new CliError(ERROR_CODES.INVALID_CREDENTIALS, 'Email and password are required'); } - console.log("💳 logging into account"); - const result = await fetch("https://ipb.sandboxpay.co.za/auth/login", { + console.log('💳 logging into account'); + const result = await fetch('https://ipb.sandboxpay.co.za/auth/login', { agent, - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ email: options.email, @@ -62,32 +57,31 @@ export async function loginCommand(options: any) { }); if (!result.ok) { const body = await result.text(); - throw new Error(`Error: ${result.status} ${body}`); + throw new CliError(ERROR_CODES.INVALID_CREDENTIALS, `Login failed: ${result.status} ${body}`); } const loginResponse: LoginResponse = (await result.json()) as LoginResponse; - console.log("Login successful"); + console.log('Login successful'); let cred = { - clientId: "", - clientSecret: "", - apiKey: "", - cardKey: "", - openaiKey: "", - sandboxKey: "", + clientId: '', + clientSecret: '', + apiKey: '', + cardKey: '', + openaiKey: '', + sandboxKey: '', }; if (fs.existsSync(credentialLocation.filename)) { - cred = JSON.parse(fs.readFileSync(credentialLocation.filename, "utf8")); + const data = await fsPromises.readFile(credentialLocation.filename, 'utf8'); + cred = JSON.parse(data); } else { if (!fs.existsSync(credentialLocation.folder)) { - fs.mkdirSync(credentialLocation.folder, { recursive: true }); + await fsPromises.mkdir(credentialLocation.folder, { recursive: true }); } - - await fs.writeFileSync(credentialLocation.filename, JSON.stringify(cred)); + await writeCredentialsFile(credentialLocation.filename, cred); } - cred = JSON.parse(fs.readFileSync(credentialLocation.filename, "utf8")); cred.sandboxKey = loginResponse.access_token; - await fs.writeFileSync(credentialLocation.filename, JSON.stringify(cred)); - console.log("🔑 access token saved"); - } catch (error: any) { - handleCliError(error, options, "login"); + await writeCredentialsFile(credentialLocation.filename, cred); + console.log('🔑 access token saved'); + } catch (error: unknown) { + handleCliError(error, options, 'login'); } } diff --git a/src/cmds/logs.ts b/src/cmds/logs.ts index 3785686..21807f8 100644 --- a/src/cmds/logs.ts +++ b/src/cmds/logs.ts @@ -1,8 +1,7 @@ -import fs from "fs"; -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import fs from 'node:fs'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -12,31 +11,25 @@ interface Options extends CommonOptions { export async function logsCommand(options: Options) { try { if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new Error("card-key is required"); + if (credentials.cardKey === '') { + throw new Error('card-key is required'); } options.cardKey = Number(credentials.cardKey); } - if (options.filename === undefined || options.filename === "") { - throw new Error("filename is required"); + if (options.filename === undefined || options.filename === '') { + throw new Error('filename is required'); } printTitleBox(); const disableSpinner = options.spinner === true; - const spinner = createSpinner( - !disableSpinner, - "📊 fetching execution items...", - ).start(); + const spinner = createSpinner(!disableSpinner, '📊 fetching execution items...').start(); const api = await initializeApi(credentials, options); const result = await api.getExecutions(options.cardKey); spinner.stop(); console.log(`💾 saving to file: ${options.filename}`); - fs.writeFileSync( - options.filename, - JSON.stringify(result.data.result.executionItems, null, 4), - ); - console.log("🎉 " + "logs saved to file"); - } catch (error: any) { - handleCliError(error, options, "fetch execution logs"); + fs.writeFileSync(options.filename, JSON.stringify(result.data.result.executionItems, null, 4)); + console.log('🎉 ' + 'logs saved to file'); + } catch (error: unknown) { + handleCliError(error, options, 'fetch execution logs'); } } diff --git a/src/cmds/merchants.ts b/src/cmds/merchants.ts index 152c3c1..5429d27 100644 --- a/src/cmds/merchants.ts +++ b/src/cmds/merchants.ts @@ -1,30 +1,26 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, printTable, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi, printTable } from '../utils.js'; +import type { CommonOptions } from './types.js'; export async function merchantsCommand(options: CommonOptions) { try { printTitleBox(); const disableSpinner = options.spinner === true; - const spinner = createSpinner( - !disableSpinner, - "🏪 fetching merchants...", - ).start(); + const spinner = createSpinner(!disableSpinner, '🏪 fetching merchants...').start(); const api = await initializeApi(credentials, options); const result = await api.getMerchants(); const merchants = result.data.result; spinner.stop(); if (!merchants) { - console.log("No merchants found"); + console.log('No merchants found'); return; } const simpleMerchants = merchants.map(({ Code, Name }) => ({ Code, Name })); printTable(simpleMerchants); console.log(`\n${merchants.length} merchant(s) found.`); - } catch (error: any) { - handleCliError(error, options, "fetch merchants"); + } catch (error: unknown) { + handleCliError(error, options, 'fetch merchants'); } } diff --git a/src/cmds/new.ts b/src/cmds/new.ts index cd96764..3725297 100644 --- a/src/cmds/new.ts +++ b/src/cmds/new.ts @@ -1,9 +1,9 @@ -import chalk from "chalk"; -import fs from "fs"; -import path from "path"; -import { printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; -import { CliError, ERROR_CODES } from "../errors.js"; +import fs from 'node:fs'; +import path from 'node:path'; +import chalk from 'chalk'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { printTitleBox } from '../index.js'; +import { handleCliError } from '../utils.js'; interface Options { template: string; @@ -13,50 +13,36 @@ interface Options { export async function newCommand(name: string, options: Options) { printTitleBox(); - const uri = path.join( - import.meta.dirname, - "/../templates/", - options.template, - ); - console.log("📂 Finding template called " + chalk.green(options.template)); + const uri = path.join(import.meta.dirname, '/../templates/', options.template); + console.log(`📂 Finding template called ${chalk.green(options.template)}`); try { if (!fs.existsSync(uri)) { - throw new CliError( - ERROR_CODES.TEMPLATE_NOT_FOUND, - "💣 Template does not exist", - ); + throw new CliError(ERROR_CODES.TEMPLATE_NOT_FOUND, '💣 Template does not exist'); } // Validate project name if (!/^[a-zA-Z0-9-_]+$/.test(name)) { throw new CliError( ERROR_CODES.INVALID_PROJECT_NAME, - "💣 Project name contains invalid characters. Use only letters, numbers, hyphens, and underscores.", + '💣 Project name contains invalid characters. Use only letters, numbers, hyphens, and underscores.' ); } // Add a force option to the Options interface if (fs.existsSync(name) && options.force) { - console.log( - chalk.yellowBright(`Warning: Overwriting existing project ${name}`), - ); + console.log(chalk.yellowBright(`Warning: Overwriting existing project ${name}`)); // Remove existing directory fs.rmSync(name, { recursive: true, force: true }); } else if (fs.existsSync(name)) { - throw new CliError( - ERROR_CODES.PROJECT_EXISTS, - "💣 Project already exists", - ); + throw new CliError(ERROR_CODES.PROJECT_EXISTS, '💣 Project already exists'); } fs.cpSync(uri, name, { recursive: true }); console.log(`🚀 Created new project from template ${options.template}`); - console.log(""); + console.log(''); // Provide next steps - console.log("Next steps:"); + console.log('Next steps:'); console.log(`- 📂 Navigate to your project: ${chalk.green(`cd ${name}`)}`); console.log(`- 📝 Edit your code in ${chalk.green(`${name}/main.js`)}`); - console.log( - `- 🧪 Test your code with: ${chalk.green(`ipb run -f ${name}/main.js`)}`, - ); - } catch (error: any) { - handleCliError(error, options, "create new project"); + console.log(`- 🧪 Test your code with: ${chalk.green(`ipb run -f ${name}/main.js`)}`); + } catch (error: unknown) { + handleCliError(error, options, 'create new project'); } } diff --git a/src/cmds/pay.ts b/src/cmds/pay.ts index 4227a8d..e30a416 100644 --- a/src/cmds/pay.ts +++ b/src/cmds/pay.ts @@ -1,40 +1,39 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializePbApi } from "../utils.js"; -import { handleCliError } from "../utils.js"; -import type { CommonOptions } from "./types.js"; -import { input } from "@inquirer/prompts"; +import { input } from '@inquirer/prompts'; +import { credentials, printTitleBox } from '../index.js'; +import { handleCliError, initializePbApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; export async function payCommand( accountId: string, beneficiaryId: string, amount: number, reference: string, - options: CommonOptions, + options: CommonOptions ) { try { // Prompt for missing arguments interactively if (!accountId) { - accountId = await input({ message: "Enter your account ID:" }); + accountId = await input({ message: 'Enter your account ID:' }); } if (!beneficiaryId) { - beneficiaryId = await input({ message: "Enter beneficiary ID:" }); + beneficiaryId = await input({ message: 'Enter beneficiary ID:' }); } if (!amount) { - const amt = await input({ message: "Enter amount (in rands):" }); + const amt = await input({ message: 'Enter amount (in rands):' }); amount = parseFloat(amt); - if (isNaN(amount) || amount <= 0) { - throw new Error("Amount must be a positive number"); + if (Number.isNaN(amount) || amount <= 0) { + throw new Error('Amount must be a positive number'); } } if (!reference) { - reference = await input({ message: "Enter reference for the payment:" }); + reference = await input({ message: 'Enter reference for the payment:' }); } printTitleBox(); const api = await initializePbApi(credentials, options); // Show transaction summary and require confirmation console.log(`\nTransaction Summary:`); - console.log("-------------------------"); + console.log('-------------------------'); console.log(`Account: ${accountId}`); console.log(`Beneficiary: ${beneficiaryId}`); console.log(`Amount: R${amount.toFixed(2)}`); @@ -43,12 +42,12 @@ export async function payCommand( const confirmPayment = await input({ message: "Type 'CONFIRM' to proceed with this payment:", }); - if (confirmPayment !== "CONFIRM") { - console.log("Payment cancelled."); + if (confirmPayment !== 'CONFIRM') { + console.log('Payment cancelled.'); return; } - console.log("💳 paying"); + console.log('💳 paying'); const result = await api.payMultiple(accountId, [ { beneficiaryId: beneficiaryId, @@ -59,10 +58,10 @@ export async function payCommand( ]); for (const transfer of result.data.TransferResponses) { console.log( - `Transfer to ${transfer.BeneficiaryAccountId}, reference ${transfer.PaymentReferenceNumber} was successful.`, + `Transfer to ${transfer.BeneficiaryAccountId}, reference ${transfer.PaymentReferenceNumber} was successful.` ); } - } catch (error: any) { - handleCliError(error, options, "pay beneficiary"); + } catch (error: unknown) { + handleCliError(error, options, 'pay beneficiary'); } } diff --git a/src/cmds/publish.ts b/src/cmds/publish.ts index 190e254..7ae3403 100644 --- a/src/cmds/publish.ts +++ b/src/cmds/publish.ts @@ -1,10 +1,8 @@ -import fs from "fs"; -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; -import ora from "ora"; -import { CliError, ERROR_CODES } from "../errors.js"; +import fs from 'node:fs'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -15,34 +13,24 @@ interface Options extends CommonOptions { export async function publishCommand(options: Options) { try { if (!fs.existsSync(options.filename)) { - throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "File does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, 'File does not exist'); } if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new CliError( - ERROR_CODES.MISSING_CARD_KEY, - "card-key is required", - ); + if (credentials.cardKey === '') { + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, 'card-key is required'); } options.cardKey = Number(credentials.cardKey); } printTitleBox(); const disableSpinner = options.spinner === true; - const spinner = createSpinner( - !disableSpinner, - "🚀 publishing code...", - ).start(); + const spinner = createSpinner(!disableSpinner, '🚀 publishing code...').start(); const api = await initializeApi(credentials, options); const code = fs.readFileSync(options.filename).toString(); - const result = await api.uploadPublishedCode( - options.cardKey, - options.codeId, - code, - ); + const result = await api.uploadPublishedCode(options.cardKey, options.codeId, code); spinner.stop(); console.log(`🎉 code published with codeId: ${result.data.result.codeId}`); - } catch (error: any) { - handleCliError(error, options, "publish code"); + } catch (error: unknown) { + handleCliError(error, options, 'publish code'); } } diff --git a/src/cmds/published.ts b/src/cmds/published.ts index b7c8294..1378a74 100644 --- a/src/cmds/published.ts +++ b/src/cmds/published.ts @@ -1,9 +1,8 @@ -import fs from "fs"; -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; -import { CliError, ERROR_CODES } from "../errors.js"; +import fs from 'node:fs'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -12,18 +11,15 @@ interface Options extends CommonOptions { export async function publishedCommand(options: Options) { if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + if (credentials.cardKey === '') { + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, 'card-key is required'); } options.cardKey = Number(credentials.cardKey); } try { printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner( - !disableSpinner, - "🚀 fetching code...", - ).start(); + const spinner = createSpinner(!disableSpinner, '🚀 fetching code...').start(); const api = await initializeApi(credentials, options); const result = await api.getPublishedCode(options.cardKey); @@ -31,8 +27,8 @@ export async function publishedCommand(options: Options) { spinner.stop(); console.log(`💾 saving to file: ${options.filename}`); await fs.writeFileSync(options.filename, code); - console.log("🎉 code saved to file"); - } catch (error: any) { - handleCliError(error, options, "fetch published code"); + console.log('🎉 code saved to file'); + } catch (error: unknown) { + handleCliError(error, options, 'fetch published code'); } } diff --git a/src/cmds/register.ts b/src/cmds/register.ts index 0be5c60..bea13fc 100644 --- a/src/cmds/register.ts +++ b/src/cmds/register.ts @@ -1,13 +1,13 @@ -import { printTitleBox } from "../index.js"; -import fetch from "node-fetch"; -import https from "https"; -import { handleCliError } from "../utils.js"; -import { input, password } from "@inquirer/prompts"; -import type { CommonOptions } from "./types.js"; -import { CliError, ERROR_CODES } from "../errors.js"; +import https from 'node:https'; +import { input, password } from '@inquirer/prompts'; +import fetch from 'node-fetch'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { printTitleBox } from '../index.js'; +import { handleCliError } from '../utils.js'; +import type { CommonOptions } from './types.js'; const agent = new https.Agent({ - rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", + rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== 'false', }); interface Options extends CommonOptions { email: string; @@ -20,31 +20,26 @@ export async function registerCommand(options: Options) { // Prompt for email and password if not provided if (!options.email) { options.email = await input({ - message: "Enter your email:", - validate: (input: string) => - input.includes("@") || "Please enter a valid email.", + message: 'Enter your email:', + validate: (input: string) => input.includes('@') || 'Please enter a valid email.', }); } if (!options.password) { options.password = await password({ - message: "Enter your password:", - mask: "*", - validate: (input: string) => - input.length >= 6 || "Password must be at least 6 characters.", + message: 'Enter your password:', + mask: '*', + validate: (input: string) => input.length >= 6 || 'Password must be at least 6 characters.', }); } if (!options.email || !options.password) { - throw new CliError( - ERROR_CODES.MISSING_EMAIL_OR_PASSWORD, - "Email and password are required", - ); + throw new CliError(ERROR_CODES.MISSING_EMAIL_OR_PASSWORD, 'Email and password are required'); } - console.log("💳 registering account"); - const result = await fetch("https://ipb.sandboxpay.co.za/auth/register", { + console.log('💳 registering account'); + const result = await fetch('https://ipb.sandboxpay.co.za/auth/register', { agent, - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ email: options.email, @@ -53,11 +48,14 @@ export async function registerCommand(options: Options) { }); if (!result.ok) { const body = await result.text(); - throw new Error(`Error: ${result.status} ${body}`); + throw new CliError( + ERROR_CODES.INVALID_CREDENTIALS, + `Registration failed: ${result.status} ${body}` + ); } - console.log("Account registered successfully"); - } catch (error: any) { - handleCliError(error, { verbose: options.verbose }, "register"); + console.log('Account registered successfully'); + } catch (error: unknown) { + handleCliError(error, { verbose: options.verbose }, 'register'); } } diff --git a/src/cmds/run.ts b/src/cmds/run.ts index f6cd38d..6cb3624 100644 --- a/src/cmds/run.ts +++ b/src/cmds/run.ts @@ -1,10 +1,11 @@ -import chalk from "chalk"; -import fs from "fs"; -import path from "path"; -import { createTransaction, run } from "programmable-card-code-emulator"; -import { printTitleBox } from "../index.js"; -import { handleCliError } from "../utils.js"; -import { CliError, ERROR_CODES } from "../errors.js"; +import fs from 'node:fs'; +import path from 'node:path'; +import chalk from 'chalk'; +import { createTransaction, run } from 'programmable-card-code-emulator'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { printTitleBox } from '../index.js'; +import { handleCliError } from '../utils.js'; + interface Options { filename: string; env: string; @@ -20,85 +21,63 @@ export async function runCommand(options: Options) { printTitleBox(); try { if (!fs.existsSync(options.filename)) { - throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "File does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, 'File does not exist'); } - console.log( - chalk.white(`Running code:`), - chalk.blueBright(options.filename), - ); + console.log(chalk.white(`Running code:`), chalk.blueBright(options.filename)); const transaction = createTransaction( options.currency, options.amount, options.mcc, options.merchant, options.city, - options.country, + options.country ); console.log(chalk.blue(`currency:`), chalk.green(transaction.currencyCode)); console.log(chalk.blue(`amount:`), chalk.green(transaction.centsAmount)); - console.log( - chalk.blue(`merchant code:`), - chalk.green(transaction.merchant.category.code), - ); - console.log( - chalk.blue(`merchant name:`), - chalk.greenBright(transaction.merchant.name), - ); - console.log( - chalk.blue(`merchant city:`), - chalk.green(transaction.merchant.city), - ); - console.log( - chalk.blue(`merchant country:`), - chalk.green(transaction.merchant.country.code), - ); + console.log(chalk.blue(`merchant code:`), chalk.green(transaction.merchant.category.code)); + console.log(chalk.blue(`merchant name:`), chalk.greenBright(transaction.merchant.name)); + console.log(chalk.blue(`merchant city:`), chalk.green(transaction.merchant.city)); + console.log(chalk.blue(`merchant country:`), chalk.green(transaction.merchant.country.code)); // Read the template env.json file and replace the values with the process.env values let environmentvariables: { [key: string]: string } = {}; if (options.env) { if (!fs.existsSync(`.env.${options.env}`)) { - throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "Env does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, 'Env does not exist'); } - const data = fs.readFileSync(`.env.${options.env}`, "utf8"); - let lines = data.split("\n"); + const data = fs.readFileSync(`.env.${options.env}`, 'utf8'); + const lines = data.split('\n'); environmentvariables = convertToJson(lines); } // Convert the environmentvariables to a string - let environmentvariablesString = JSON.stringify(environmentvariables); - const code = fs.readFileSync( - path.join(path.resolve(), options.filename), - "utf8", - ); + const environmentvariablesString = JSON.stringify(environmentvariables); + const code = fs.readFileSync(path.join(path.resolve(), options.filename), 'utf8'); // Run the code - const executionItems = await run( - transaction, - code, - environmentvariablesString, - ); + const executionItems = await run(transaction, code, environmentvariablesString); executionItems.forEach((item) => { - console.log("\n💻 ", chalk.green(item.type)); + console.log('\n💻 ', chalk.green(item.type)); item.logs.forEach((log) => { - console.log("\n", chalk.yellow(log.level), chalk.white(log.content)); + console.log('\n', chalk.yellow(log.level), chalk.white(log.content)); }); }); - } catch (error: any) { - handleCliError(error, { verbose: options.verbose }, "run code"); + } catch (error: unknown) { + handleCliError(error, { verbose: options.verbose }, 'run code'); } } function convertToJson(arr: string[]) { - let output: { [key: string]: string } = {}; + const output: { [key: string]: string } = {}; for (let i = 0; i < arr.length; i++) { - let line = arr[i]; + const line = arr[i]; - if (line !== "\r") { - let txt = line?.trim(); + if (line !== '\r') { + const _txt = line?.trim(); if (line) { - let key = line.split("=")[0]?.trim(); - let value = line.split("=")[1]?.trim(); + const key = line.split('=')[0]?.trim(); + const value = line.split('=')[1]?.trim(); if (key && value) { output[key] = value; } diff --git a/src/cmds/scopes.ts b/src/cmds/scopes.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/cmds/set.ts b/src/cmds/set.ts index 8b635d9..557e952 100644 --- a/src/cmds/set.ts +++ b/src/cmds/set.ts @@ -1,6 +1,6 @@ -import fs from "fs"; -import { credentialLocation } from "../index.js"; -import { handleCliError } from "../utils.js"; +import fs, { promises as fsPromises } from 'node:fs'; +import { credentialLocation } from '../index.js'; +import { handleCliError, writeCredentialsFile } from '../utils.js'; interface Options { clientId: string; @@ -14,17 +14,20 @@ interface Options { export async function configCommand(options: Options) { try { let cred = { - clientId: "", - clientSecret: "", - apiKey: "", - cardKey: "", - openaiKey: "", - sandboxKey: "", + clientId: '', + clientSecret: '', + apiKey: '', + cardKey: '', + openaiKey: '', + sandboxKey: '', }; if (fs.existsSync(credentialLocation.filename)) { - cred = JSON.parse(fs.readFileSync(credentialLocation.filename, "utf8")); + const data = await fsPromises.readFile(credentialLocation.filename, 'utf8'); + cred = JSON.parse(data); } else { - fs.mkdirSync(credentialLocation.folder); + if (!fs.existsSync(credentialLocation.folder)) { + await fsPromises.mkdir(credentialLocation.folder, { recursive: true }); + } } if (options.clientId) { @@ -45,9 +48,9 @@ export async function configCommand(options: Options) { if (options.sandboxKey) { cred.sandboxKey = options.sandboxKey; } - await fs.writeFileSync(credentialLocation.filename, JSON.stringify(cred)); - console.log("🔑 credentials saved"); - } catch (error: any) { - handleCliError(error, options, "set config"); + await writeCredentialsFile(credentialLocation.filename, cred); + console.log('🔑 credentials saved'); + } catch (error: unknown) { + handleCliError(error, options, 'set config'); } } diff --git a/src/cmds/simulate.ts b/src/cmds/simulate.ts index 043b0a9..7125172 100644 --- a/src/cmds/simulate.ts +++ b/src/cmds/simulate.ts @@ -1,10 +1,9 @@ -import chalk from "chalk"; -import fs from "fs"; -import { createTransaction } from "programmable-card-code-emulator"; -import { credentials } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError } from "../utils.js"; -import { CliError, ERROR_CODES } from "../errors.js"; +import fs from 'node:fs'; +import chalk from 'chalk'; +import { createTransaction } from 'programmable-card-code-emulator'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { credentials } from '../index.js'; +import { handleCliError, initializeApi } from '../utils.js'; interface Options { cardKey: number; @@ -25,20 +24,17 @@ interface Options { export async function simulateCommand(options: Options) { try { if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new CliError( - ERROR_CODES.MISSING_CARD_KEY, - "card-key is required", - ); + if (credentials.cardKey === '') { + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, 'card-key is required'); } options.cardKey = Number(credentials.cardKey); } if (!fs.existsSync(options.filename)) { - throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "File does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, 'File does not exist'); } const api = await initializeApi(credentials, options); - console.log("🚀 uploading code & running simulation"); + console.log('🚀 uploading code & running simulation'); const code = fs.readFileSync(options.filename).toString(); const transaction = createTransaction( options.currency, @@ -46,44 +42,29 @@ export async function simulateCommand(options: Options) { options.mcc, options.merchant, options.city, - options.country, + options.country ); const result = await api.executeCode(code, transaction, options.cardKey); const executionItems = result.data.result; - console.log(""); - console.log( - chalk.white(`Simulated code:`), - chalk.blueBright(options.filename), - ); + console.log(''); + console.log(chalk.white(`Simulated code:`), chalk.blueBright(options.filename)); console.log(chalk.blue(`currency:`), chalk.green(transaction.currencyCode)); console.log(chalk.blue(`amount:`), chalk.green(transaction.centsAmount)); - console.log( - chalk.blue(`merchant code:`), - chalk.green(transaction.merchant.category.code), - ); - console.log( - chalk.blue(`merchant name:`), - chalk.greenBright(transaction.merchant.name), - ); - console.log( - chalk.blue(`merchant city:`), - chalk.green(transaction.merchant.city), - ); - console.log( - chalk.blue(`merchant country:`), - chalk.green(transaction.merchant.country.code), - ); + console.log(chalk.blue(`merchant code:`), chalk.green(transaction.merchant.category.code)); + console.log(chalk.blue(`merchant name:`), chalk.greenBright(transaction.merchant.name)); + console.log(chalk.blue(`merchant city:`), chalk.green(transaction.merchant.city)); + console.log(chalk.blue(`merchant country:`), chalk.green(transaction.merchant.country.code)); // Read the template env.json file and replace the values with the process.env values executionItems.forEach((item) => { - console.log("\n💻 ", chalk.green(item.type)); + console.log('\n💻 ', chalk.green(item.type)); item.logs.forEach((log) => { - console.log("\n", chalk.yellow(log.level), chalk.white(log.content)); + console.log('\n', chalk.yellow(log.level), chalk.white(log.content)); }); }); - } catch (error: any) { - handleCliError(error, options, "simulate code"); + } catch (error: unknown) { + handleCliError(error, options, 'simulate code'); } } diff --git a/src/cmds/toggle.ts b/src/cmds/toggle.ts index 13170eb..b13a2c1 100644 --- a/src/cmds/toggle.ts +++ b/src/cmds/toggle.ts @@ -1,8 +1,7 @@ -import { CliError, ERROR_CODES } from "../errors.js"; -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import { CliError, ERROR_CODES } from '../errors.js'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -10,28 +9,25 @@ interface Options extends CommonOptions { export async function enableCommand(options: Options) { if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + if (credentials.cardKey === '') { + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, 'card-key is required'); } options.cardKey = Number(credentials.cardKey); } try { printTitleBox(); const disableSpinner = options.spinner === true; - const spinner = createSpinner( - !disableSpinner, - "🍄 enabling code on card...", - ).start(); + const spinner = createSpinner(!disableSpinner, '🍄 enabling code on card...').start(); const api = await initializeApi(credentials, options); const result = await api.toggleCode(options.cardKey, true); spinner.stop(); if (result.data.result.Enabled) { - console.log("✅ code enabled"); + console.log('✅ code enabled'); } else { - console.log("❌ code enable failed"); + console.log('❌ code enable failed'); } - } catch (error: any) { - handleCliError(error, options, "enable card code"); + } catch (error: unknown) { + handleCliError(error, options, 'enable card code'); } } diff --git a/src/cmds/transactions.ts b/src/cmds/transactions.ts index 714664b..a2acf83 100644 --- a/src/cmds/transactions.ts +++ b/src/cmds/transactions.ts @@ -1,7 +1,6 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializePbApi } from "../utils.js"; -import { handleCliError, printTable, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializePbApi, printTable } from '../utils.js'; +import type { CommonOptions } from './types.js'; /** * Minimal transaction type for CLI display. @@ -19,29 +18,18 @@ type Transaction = { * @param accountId - The account ID to fetch transactions for. * @param options - CLI options. */ -export async function transactionsCommand( - accountId: string, - options: CommonOptions, -) { +export async function transactionsCommand(accountId: string, options: CommonOptions) { try { printTitleBox(); const disableSpinner = options.spinner === true; - const spinner = createSpinner( - !disableSpinner, - "💳 fetching transactions...", - ).start(); + const spinner = createSpinner(!disableSpinner, '💳 fetching transactions...').start(); const api = await initializePbApi(credentials, options); - const result = await api.getAccountTransactions( - accountId, - null, - null, - null, - ); + const result = await api.getAccountTransactions(accountId); const transactions = result.data.transactions; spinner.stop(); if (!transactions) { - console.log("No transactions found"); + console.log('No transactions found'); return; } @@ -51,11 +39,11 @@ export async function transactionsCommand( amount, transactionDate, description, - }), + }) ); printTable(simpleTransactions); console.log(`\n${transactions.length} transaction(s) found.`); - } catch (error: any) { - handleCliError(error, options, "fetch transactions"); + } catch (error: unknown) { + handleCliError(error, options, 'fetch transactions'); } } diff --git a/src/cmds/transfer.ts b/src/cmds/transfer.ts index 7b7db75..795aa7e 100644 --- a/src/cmds/transfer.ts +++ b/src/cmds/transfer.ts @@ -1,39 +1,38 @@ -import { credentials, printTitleBox } from "../index.js"; -import { initializePbApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; -import { input } from "@inquirer/prompts"; +import { input } from '@inquirer/prompts'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializePbApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; export async function transferCommand( accountId: string, beneficiaryAccountId: string, amount: number, reference: string, - options: CommonOptions, + options: CommonOptions ) { try { // Prompt for missing arguments interactively if (!accountId) { - accountId = await input({ message: "Enter your account ID:" }); + accountId = await input({ message: 'Enter your account ID:' }); } if (!beneficiaryAccountId) { beneficiaryAccountId = await input({ - message: "Enter beneficiary account ID:", + message: 'Enter beneficiary account ID:', }); } if (!amount) { - const amt = await input({ message: "Enter amount (in rands):" }); + const amt = await input({ message: 'Enter amount (in rands):' }); amount = parseFloat(amt); - if (isNaN(amount) || amount <= 0) { - throw new Error("Please enter a valid positive amount"); + if (Number.isNaN(amount) || amount <= 0) { + throw new Error('Please enter a valid positive amount'); } } if (!reference) { - reference = await input({ message: "Enter reference for the transfer:" }); + reference = await input({ message: 'Enter reference for the transfer:' }); } printTitleBox(); const disableSpinner = options.spinner === true; - const spinner = createSpinner(!disableSpinner, "💳 transfering..."); + const spinner = createSpinner(!disableSpinner, '💳 transfering...'); const api = await initializePbApi(credentials, options); const result = await api.transferMultiple(accountId, [ @@ -46,11 +45,9 @@ export async function transferCommand( ]); spinner.stop(); for (const transfer of result.data.TransferResponses) { - console.log( - `Transfer to ${transfer.BeneficiaryAccountId}: ${transfer.Status}`, - ); + console.log(`Transfer to ${transfer.BeneficiaryAccountId}: ${transfer.Status}`); } - } catch (error: any) { - handleCliError(error, options, "transfer"); + } catch (error: unknown) { + handleCliError(error, options, 'transfer'); } } diff --git a/src/cmds/upload-env.ts b/src/cmds/upload-env.ts index 8d697bb..5a03a6d 100644 --- a/src/cmds/upload-env.ts +++ b/src/cmds/upload-env.ts @@ -1,9 +1,8 @@ -import fs from "fs"; -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; -import { CliError, ERROR_CODES } from "../errors.js"; +import fs from 'node:fs'; +import { CliError, ERROR_CODES } from '../errors.js'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -12,27 +11,27 @@ interface Options extends CommonOptions { export async function uploadEnvCommand(options: Options) { if (!fs.existsSync(options.filename)) { - throw new CliError(ERROR_CODES.FILE_NOT_FOUND, "File does not exist"); + throw new CliError(ERROR_CODES.FILE_NOT_FOUND, 'File does not exist'); } if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new CliError(ERROR_CODES.MISSING_CARD_KEY, "card-key is required"); + if (credentials.cardKey === '') { + throw new CliError(ERROR_CODES.MISSING_CARD_KEY, 'card-key is required'); } options.cardKey = Number(credentials.cardKey); } try { printTitleBox(); const disableSpinner = options.spinner === true; - const spinner = createSpinner(!disableSpinner, "🚀 uploading env..."); + const spinner = createSpinner(!disableSpinner, '🚀 uploading env...'); const api = await initializeApi(credentials, options); const raw = { variables: {} }; - const variables = fs.readFileSync(options.filename, "utf8"); + const variables = fs.readFileSync(options.filename, 'utf8'); raw.variables = JSON.parse(variables); - const result = await api.uploadEnv(options.cardKey, raw); + const _result = await api.uploadEnv(options.cardKey, raw); spinner.stop(); console.log(`🎉 env uploaded`); - } catch (error: any) { - handleCliError(error, options, "upload environment variables"); + } catch (error: unknown) { + handleCliError(error, options, 'upload environment variables'); } } diff --git a/src/cmds/upload.ts b/src/cmds/upload.ts index c8ccd5e..8d36653 100644 --- a/src/cmds/upload.ts +++ b/src/cmds/upload.ts @@ -1,8 +1,7 @@ -import fs from "fs"; -import { credentials, printTitleBox } from "../index.js"; -import { initializeApi } from "../utils.js"; -import { handleCliError, createSpinner } from "../utils.js"; -import type { CommonOptions } from "./types.js"; +import fs from 'node:fs'; +import { credentials, printTitleBox } from '../index.js'; +import { createSpinner, handleCliError, initializeApi } from '../utils.js'; +import type { CommonOptions } from './types.js'; interface Options extends CommonOptions { cardKey: number; @@ -12,25 +11,25 @@ interface Options extends CommonOptions { export async function uploadCommand(options: Options) { try { if (!fs.existsSync(options.filename)) { - throw new Error("File does not exist"); + throw new Error('File does not exist'); } if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new Error("card-key is required"); + if (credentials.cardKey === '') { + throw new Error('card-key is required'); } options.cardKey = Number(credentials.cardKey); } printTitleBox(); const disableSpinner = options.spinner === true; // default false - const spinner = createSpinner(!disableSpinner, "🚀 uploading code..."); + const spinner = createSpinner(!disableSpinner, '🚀 uploading code...'); const api = await initializeApi(credentials, options); - const raw = { code: "" }; + const raw = { code: '' }; const code = fs.readFileSync(options.filename).toString(); raw.code = code; const result = await api.uploadCode(options.cardKey, raw); spinner.stop(); console.log(`🎉 code uploaded with codeId: ${result.data.result.codeId}`); - } catch (error: any) { - handleCliError(error, options, "upload code"); + } catch (error: unknown) { + handleCliError(error, options, 'upload code'); } } diff --git a/src/errors.ts b/src/errors.ts index 532f915..1a256a0 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,23 +4,23 @@ export class CliError extends Error { constructor(code: string, message: string) { super(`Error (${code}): ${message}`); this.code = code; - this.name = "CliError"; + this.name = 'CliError'; } } // Example error codes and messages export const ERROR_CODES = { - MISSING_API_TOKEN: "E4002", - MISSING_CARD_KEY: "E4003", - MISSING_ENV_FILE: "E4004", - INVALID_CREDENTIALS: "E4005", - DEPLOY_FAILED: "E5001", - TEMPLATE_NOT_FOUND: "E4007", - INVALID_PROJECT_NAME: "E4008", - PROJECT_EXISTS: "E4009", - FILE_NOT_FOUND: "E4010", - MISSING_EMAIL_OR_PASSWORD: "E4011", // Added for register command - MISSING_ACCOUNT_ID: "E4012", // Added for balances command + MISSING_API_TOKEN: 'E4002', + MISSING_CARD_KEY: 'E4003', + MISSING_ENV_FILE: 'E4004', + INVALID_CREDENTIALS: 'E4005', + DEPLOY_FAILED: 'E5001', + TEMPLATE_NOT_FOUND: 'E4007', + INVALID_PROJECT_NAME: 'E4008', + PROJECT_EXISTS: 'E4009', + FILE_NOT_FOUND: 'E4010', + MISSING_EMAIL_OR_PASSWORD: 'E4011', // Added for register command + MISSING_ACCOUNT_ID: 'E4012', // Added for balances command // Add more as needed }; @@ -36,6 +36,6 @@ export function printCliError(error: unknown) { } else if (error instanceof Error) { console.error(`Error: ${error.message}`); } else { - console.error("An unknown error occurred."); + console.error('An unknown error occurred.'); } } diff --git a/src/function-calls.ts b/src/function-calls.ts index 6bb50ce..f5b3b9c 100644 --- a/src/function-calls.ts +++ b/src/function-calls.ts @@ -1,40 +1,41 @@ -import OpenAI from "openai"; -import { credentials } from "./index.js"; -import { initializePbApi } from "./utils.js"; -import type { BasicOptions } from "./cmds/types.js"; import type { AccountBalance, + AccountResponse, AccountTransaction, + BeneficiaryResponse, Transfer, TransferMultiple, - TransferResponse, -} from "investec-pb-api"; +} from 'investec-pb-api'; +import type OpenAI from 'openai'; +import type { BasicOptions } from './cmds/types.js'; +import { credentials } from './index.js'; +import { initializePbApi } from './utils.js'; export const getWeatherFunctionCall: OpenAI.ChatCompletionTool = { - type: "function", + type: 'function', function: { - name: "get_weather", - description: "Get current temperature for provided coordinates in celsius.", + name: 'get_weather', + description: 'Get current temperature for provided coordinates in celsius.', parameters: { - type: "object", + type: 'object', properties: { - latitude: { type: "number" }, - longitude: { type: "number" }, + latitude: { type: 'number' }, + longitude: { type: 'number' }, }, - required: ["latitude", "longitude"], + required: ['latitude', 'longitude'], additionalProperties: false, }, }, }; -export async function getWeather(latitude: number, longitude: number) { - return "24C"; - const response = await fetch( - `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m`, - ); - const data = await response.json(); - // Type assertion to fix 'unknown' type error - return (data as any).current.temperature_2m; +export async function getWeather(_latitude: number, _longitude: number) { + // Mock implementation - real API call commented out + return '24C'; + // const response = await fetch( + // `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m` + // ); + // const data = await response.json(); + // return (data as { current: { temperature_2m: string } }).current.temperature_2m; } interface Options extends BasicOptions { @@ -42,95 +43,89 @@ interface Options extends BasicOptions { } export const getAccountsFunctionCall: OpenAI.ChatCompletionTool = { - type: "function", + type: 'function', function: { - name: "get_accounts", - description: "Get a list of your accounts.", + name: 'get_accounts', + description: 'Get a list of your accounts.', }, }; export const getBalanceFunctionCall: OpenAI.ChatCompletionTool = { - type: "function", + type: 'function', function: { - name: "get_balance", - description: "Get the balance for a specific account.", + name: 'get_balance', + description: 'Get the balance for a specific account.', parameters: { - type: "object", + type: 'object', properties: { - accountId: { type: "string" }, + accountId: { type: 'string' }, }, - required: ["accountId"], + required: ['accountId'], additionalProperties: false, }, }, }; export const getAccountTransactionFunctionCall: OpenAI.ChatCompletionTool = { - type: "function", + type: 'function', function: { - name: "get_transactions", - description: "Get the transactions for a specific account.", + name: 'get_transactions', + description: 'Get the transactions for a specific account.', parameters: { - type: "object", + type: 'object', properties: { - accountId: { type: "string" }, - fromDate: { type: "string", format: "date" }, - toDate: { type: "string", format: "date" }, + accountId: { type: 'string' }, + fromDate: { type: 'string', format: 'date' }, + toDate: { type: 'string', format: 'date' }, }, - required: ["accountId", "fromDate"], + required: ['accountId', 'fromDate'], additionalProperties: false, }, }, }; export const getBeneficiariesFunctionCall: OpenAI.ChatCompletionTool = { - type: "function", + type: 'function', function: { - name: "get_beneficiaries", - description: - "Get a list of your external beneficiaries for making payments.", + name: 'get_beneficiaries', + description: 'Get a list of your external beneficiaries for making payments.', }, }; export const transferMultipleFunctionCall: OpenAI.ChatCompletionTool = { - type: "function", + type: 'function', function: { - name: "transfer_multiple", + name: 'transfer_multiple', description: - "Transfer money between accounts. the beneficiaryAccountId is the account you are transferring to.", + 'Transfer money between accounts. the beneficiaryAccountId is the account you are transferring to.', parameters: { - type: "object", + type: 'object', properties: { - accountId: { type: "string" }, - beneficiaryAccountId: { type: "string" }, - amount: { type: "string" }, - myReference: { type: "string" }, - theirReference: { type: "string" }, + accountId: { type: 'string' }, + beneficiaryAccountId: { type: 'string' }, + amount: { type: 'string' }, + myReference: { type: 'string' }, + theirReference: { type: 'string' }, }, - required: [ - "accountId", - "beneficiaryAccountId", - "amount", - "myReference", - "theirReference", - ], + required: ['accountId', 'beneficiaryAccountId', 'amount', 'myReference', 'theirReference'], additionalProperties: false, }, }, }; -// If you want to avoid the error, use 'any[]' as the return type -export async function getAccounts(): Promise { +export async function getAccounts(_args?: unknown): Promise { const api = await initializePbApi(credentials, {} as Options); const result = await api.getAccounts(); - console.log("💳 fetching accounts"); + console.log('💳 fetching accounts'); const accounts = result.data.accounts; return accounts; } -export async function getAccountBalances(options: { - accountId: string; -}): Promise { +export async function getAccountBalances(args: unknown): Promise { + const options = args as { accountId: string }; + if (!options || typeof options !== 'object' || !('accountId' in options)) { + throw new Error('getAccountBalances requires { accountId: string }'); + } const api = await initializePbApi(credentials, {} as Options); console.log(`💳 fetching balances for account ${options.accountId}`); const result = await api.getAccountBalances(options.accountId); @@ -139,44 +134,66 @@ export async function getAccountBalances(options: { } // thin out responses as they use too many tokens -export async function getAccountTransactions(options: { - accountId: string; - fromDate: string; - toDate: string; -}): Promise { +export async function getAccountTransactions(args: unknown): Promise { + const options = args as { + accountId: string; + fromDate: string; + toDate: string; + }; + if ( + !options || + typeof options !== 'object' || + !('accountId' in options) || + !('fromDate' in options) || + !('toDate' in options) + ) { + throw new Error( + 'getAccountTransactions requires { accountId: string; fromDate: string; toDate: string }' + ); + } const api = await initializePbApi(credentials, {} as Options); console.log( - `💳 fetching transactions for account ${options.accountId}, fromDate: ${options.fromDate}, toDate: ${options.toDate}`, - ); - const result = await api.getAccountTransactions( - options.accountId, - "2025-05-24", - options.toDate, + `💳 fetching transactions for account ${options.accountId}, fromDate: ${options.fromDate}, toDate: ${options.toDate}` ); + const result = await api.getAccountTransactions(options.accountId, '2025-05-24', options.toDate); const transactions = result.data.transactions; return transactions; } -export async function getBeneficiaries(): Promise { +export async function getBeneficiaries(_args?: unknown): Promise { const api = await initializePbApi(credentials, {} as Options); const result = await api.getBeneficiaries(); - console.log("💳 fetching beneficiaries"); + console.log('💳 fetching beneficiaries'); const beneficiaries = result.data; return beneficiaries; } -export async function transferMultiple(options: { - accountId: string; - beneficiaryAccountId: string; - amount: string; - myReference: string; - theirReference: string; -}): Promise { +export async function transferMultiple(args: unknown): Promise { + const options = args as { + accountId: string; + beneficiaryAccountId: string; + amount: string; + myReference: string; + theirReference: string; + }; + if ( + !options || + typeof options !== 'object' || + !('accountId' in options) || + !('beneficiaryAccountId' in options) || + !('amount' in options) || + !('myReference' in options) || + !('theirReference' in options) + ) { + throw new Error( + 'transferMultiple requires { accountId: string; beneficiaryAccountId: string; amount: string; myReference: string; theirReference: string }' + ); + } const api = await initializePbApi(credentials, {} as Options); console.log(`💳 transfering for account ${options.accountId}`); const transfer: TransferMultiple = { beneficiaryAccountId: options.beneficiaryAccountId, - amount: "10", // hardcoded for testing + amount: '10', // hardcoded for testing myReference: options.myReference, theirReference: options.theirReference, }; @@ -194,7 +211,7 @@ export const tools: OpenAI.ChatCompletionTool[] = [ transferMultipleFunctionCall, ]; -export const availableFunctions: Record any> = { +export const availableFunctions: Record Promise> = { get_accounts: getAccounts, get_balance: getAccountBalances, get_transactions: getAccountTransactions, diff --git a/src/index.ts b/src/index.ts index 21a33c3..7657e51 100755 --- a/src/index.ts +++ b/src/index.ts @@ -4,46 +4,46 @@ // Sets up all CLI commands and shared options using Commander.js // For more information, see README.md -import "dotenv/config"; -import process from "process"; -import fs from "fs"; -import { homedir } from "os"; -import { Command, Option } from "commander"; -import chalk from "chalk"; +import 'dotenv/config'; +import fs from 'node:fs'; +import { homedir } from 'node:os'; +import process from 'node:process'; +import chalk from 'chalk'; +import { Command, Option } from 'commander'; +import { accountsCommand } from './cmds/accounts.js'; +import { balancesCommand } from './cmds/balances.js'; +import { beneficiariesCommand } from './cmds/beneficiaries.js'; import { + bankCommand, cardsCommand, configCommand, - logsCommand, + countriesCommand, + currenciesCommand, deployCommand, - fetchCommand, - uploadCommand, - envCommand, - uploadEnvCommand, - publishedCommand, - publishCommand, - enableCommand, disableCommand, - runCommand, - currenciesCommand, - countriesCommand, + enableCommand, + envCommand, + fetchCommand, + generateCommand, + logsCommand, merchantsCommand, newCommand, - generateCommand, - bankCommand, -} from "./cmds/index.js"; -import { simulateCommand } from "./cmds/simulate.js"; -import { registerCommand } from "./cmds/register.js"; -import { loginCommand } from "./cmds/login.js"; -import { accountsCommand } from "./cmds/accounts.js"; -import { balancesCommand } from "./cmds/balances.js"; -import { transactionsCommand } from "./cmds/transactions.js"; -import { transferCommand } from "./cmds/transfer.js"; -import { beneficiariesCommand } from "./cmds/beneficiaries.js"; -import { payCommand } from "./cmds/pay.js"; -import { handleCliError, loadCredentialsFile } from "./utils.js"; -import type { Credentials, BasicOptions } from "./cmds/types.js"; + publishCommand, + publishedCommand, + runCommand, + uploadCommand, + uploadEnvCommand, +} from './cmds/index.js'; +import { loginCommand } from './cmds/login.js'; +import { payCommand } from './cmds/pay.js'; +import { registerCommand } from './cmds/register.js'; +import { simulateCommand } from './cmds/simulate.js'; +import { transactionsCommand } from './cmds/transactions.js'; +import { transferCommand } from './cmds/transfer.js'; +import type { BasicOptions, Credentials } from './cmds/types.js'; +import { handleCliError, loadCredentialsFile } from './utils.js'; -const version = "0.8.3"; +const version = '0.8.3'; const program = new Command(); // Improve error output for missing arguments/options @@ -57,42 +57,37 @@ export const credentialLocation = { }; // Print CLI title (used in some commands) +// Currently unused - kept for potential future use export async function printTitleBox() { - // console.log(""); - // console.log("🦓 Investec Programmable Banking CLI"); - // // console.log("🔮 " + chalk.blueBright(`v${version}`)); - // console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); - // console.log(""); + // Function intentionally empty - can be implemented if needed } // Load credentials from file if present let cred = { - clientId: "", - clientSecret: "", - apiKey: "", - cardKey: "", - openaiKey: "", - sandboxKey: "", + clientId: '', + clientSecret: '', + apiKey: '', + cardKey: '', + openaiKey: '', + sandboxKey: '', }; if (fs.existsSync(credentialLocation.filename)) { try { - const data = fs.readFileSync(credentialLocation.filename, "utf8"); + const data = fs.readFileSync(credentialLocation.filename, 'utf8'); cred = JSON.parse(data); } catch (err) { if (err instanceof Error) { - console.error( - chalk.red(`🙀 Invalid credentials file format: ${err.message}`), - ); - console.log(""); + console.error(chalk.red(`🙀 Invalid credentials file format: ${err.message}`)); + console.log(''); } else { - console.error(chalk.red("🙀 Invalid credentials file format")); - console.log(""); + console.error(chalk.red('🙀 Invalid credentials file format')); + console.log(''); } } } export const credentials: Credentials = { - host: process.env.INVESTEC_HOST || "https://openapi.investec.com", + host: process.env.INVESTEC_HOST || 'https://openapi.investec.com', clientId: process.env.INVESTEC_CLIENT_ID || cred.clientId, clientSecret: process.env.INVESTEC_CLIENT_SECRET || cred.clientSecret, apiKey: process.env.INVESTEC_API_KEY || cred.apiKey, @@ -104,19 +99,13 @@ export const credentials: Credentials = { // Helper for shared API credential options function addApiCredentialOptions(cmd: Command) { return cmd - .option("--api-key ", "api key for the Investec API") - .option("--client-id ", "client Id for the Investec API") - .option( - "--client-secret ", - "client secret for the Investec API", - ) - .option("--host ", "Set a custom host for the Investec Sandbox API") - .option( - "--credentials-file ", - "Set a custom credentials file", - ) - .option("-s,--spinner", "disable spinner during command execution") - .option("-v,--verbose", "additional debugging information"); + .option('--api-key ', 'api key for the Investec API') + .option('--client-id ', 'client Id for the Investec API') + .option('--client-secret ', 'client secret for the Investec API') + .option('--host ', 'Set a custom host for the Investec Sandbox API') + .option('--credentials-file ', 'Set a custom credentials file') + .option('-s,--spinner', 'disable spinner during command execution') + .option('-v,--verbose', 'additional debugging information'); } // Show help if no arguments are provided @@ -126,225 +115,180 @@ if (process.argv.length <= 2) { } async function main() { - program - .name("ipb") - .description("CLI to manage Investec Programmable Banking") - .version(version); + program.name('ipb').description('CLI to manage Investec Programmable Banking').version(version); // Use shared options for most commands - addApiCredentialOptions( - program.command("cards").description("Gets a list of your cards"), - ).action(cardsCommand); - addApiCredentialOptions( - program.command("config").description("set auth credentials"), - ) - .option("--card-key ", "Sets your card key for the Investec API") - .option( - "--openai-key ", - "Sets your OpenAI key for the AI generation", - ) - .option( - "--sandbox-key ", - "Sets your sandbox key for the AI generation", - ) + addApiCredentialOptions(program.command('cards').description('Gets a list of your cards')).action( + cardsCommand + ); + addApiCredentialOptions(program.command('config').description('set auth credentials')) + .option('--card-key ', 'Sets your card key for the Investec API') + .option('--openai-key ', 'Sets your OpenAI key for the AI generation') + .option('--sandbox-key ', 'Sets your sandbox key for the AI generation') .action(configCommand); - addApiCredentialOptions( - program.command("deploy").description("deploy code to card"), - ) - .option("-f,--filename ", "the filename") - .option("-e,--env ", "env to run") - .option("-c,--card-key ", "the cardkey") + addApiCredentialOptions(program.command('deploy').description('deploy code to card')) + .option('-f,--filename ', 'the filename') + .option('-e,--env ', 'env to run') + .option('-c,--card-key ', 'the cardkey') .action(deployCommand); - addApiCredentialOptions( - program.command("logs").description("fetches logs from the api"), - ) - .requiredOption("-f,--filename ", "the filename") - .option("-c,--card-key ", "the cardkey") + addApiCredentialOptions(program.command('logs').description('fetches logs from the api')) + .requiredOption('-f,--filename ', 'the filename') + .option('-c,--card-key ', 'the cardkey') .action(logsCommand); program - .command("run") - .description("runs the code locally") - .option("-f,--filename ", "the filename") - .option("-e,--env ", "env to run") - .option("-a,--amount ", "amount in cents", "10000") - .option("-u,--currency ", "currency code", "zar") - .option("-z,--mcc ", "merchant category code", "0000") - .option("-m,--merchant ", "merchant name", "The Coders Bakery") - .option("-i,--city ", "city name", "Cape Town") - .option("-o,--country ", "country code", "ZA") - .option("-v,--verbose", "additional debugging information") + .command('run') + .description('runs the code locally') + .option('-f,--filename ', 'the filename') + .option('-e,--env ', 'env to run') + .option('-a,--amount ', 'amount in cents', '10000') + .option('-u,--currency ', 'currency code', 'zar') + .option('-z,--mcc ', 'merchant category code', '0000') + .option('-m,--merchant ', 'merchant name', 'The Coders Bakery') + .option('-i,--city ', 'city name', 'Cape Town') + .option('-o,--country ', 'country code', 'ZA') + .option('-v,--verbose', 'additional debugging information') .action(runCommand); - addApiCredentialOptions( - program.command("fetch").description("fetches the saved code"), - ) - .requiredOption("-f,--filename ", "the filename") - .option("-c,--card-key ", "the cardkey") + addApiCredentialOptions(program.command('fetch').description('fetches the saved code')) + .requiredOption('-f,--filename ', 'the filename') + .option('-c,--card-key ', 'the cardkey') .action(fetchCommand); - addApiCredentialOptions( - program.command("upload").description("uploads to saved code"), - ) - .requiredOption("-f,--filename ", "the filename") - .option("-c,--card-key ", "the cardkey") + addApiCredentialOptions(program.command('upload').description('uploads to saved code')) + .requiredOption('-f,--filename ', 'the filename') + .option('-c,--card-key ', 'the cardkey') .action(uploadCommand); - addApiCredentialOptions( - program.command("env").description("downloads to env to a local file"), - ) - .requiredOption("-f,--filename ", "the filename") - .option("-c,--card-key ", "the cardkey") + addApiCredentialOptions(program.command('env').description('downloads to env to a local file')) + .requiredOption('-f,--filename ', 'the filename') + .option('-c,--card-key ', 'the cardkey') .action(envCommand); - addApiCredentialOptions( - program.command("upload-env").description("uploads env to the card"), - ) - .requiredOption("-f,--filename ", "the filename") - .option("-c,--card-key ", "the cardkey") + addApiCredentialOptions(program.command('upload-env').description('uploads env to the card')) + .requiredOption('-f,--filename ', 'the filename') + .option('-c,--card-key ', 'the cardkey') .action(uploadEnvCommand); addApiCredentialOptions( - program - .command("published") - .description("downloads to published code to a local file"), + program.command('published').description('downloads to published code to a local file') ) - .requiredOption("-f,--filename ", "the filename") - .option("-c,--card-key ", "the cardkey") + .requiredOption('-f,--filename ', 'the filename') + .option('-c,--card-key ', 'the cardkey') .action(publishedCommand); - addApiCredentialOptions( - program.command("publish").description("publishes code to the card"), - ) - .requiredOption("-f,--filename ", "the filename") - .option("-c,--card-key ", "the cardkey") - .option("-i,--code-id ", "the code id of the save code") + addApiCredentialOptions(program.command('publish').description('publishes code to the card')) + .requiredOption('-f,--filename ', 'the filename') + .option('-c,--card-key ', 'the cardkey') + .option('-i,--code-id ', 'the code id of the save code') .action(publishCommand); program - .command("simulate") - .description("runs the code using the online simulator") - .requiredOption("-f,--filename ", "the filename") - .option("-c,--card-key ", "the cardkey") - .option("-e,--env ", "env to run", "development") - .option("-a,--amount ", "amount in cents", "10000") - .option("-u,--currency ", "currency code", "zar") - .option("-z,--mcc ", "merchant category code", "0000") - .option("-m,--merchant ", "merchant name", "The Coders Bakery") - .option("-i,--city ", "city name", "Cape Town") - .option("-o,--country ", "country code", "ZA") - .option("-v,--verbose", "additional debugging information") + .command('simulate') + .description('runs the code using the online simulator') + .requiredOption('-f,--filename ', 'the filename') + .option('-c,--card-key ', 'the cardkey') + .option('-e,--env ', 'env to run', 'development') + .option('-a,--amount ', 'amount in cents', '10000') + .option('-u,--currency ', 'currency code', 'zar') + .option('-z,--mcc ', 'merchant category code', '0000') + .option('-m,--merchant ', 'merchant name', 'The Coders Bakery') + .option('-i,--city ', 'city name', 'Cape Town') + .option('-o,--country ', 'country code', 'ZA') + .option('-v,--verbose', 'additional debugging information') .action(simulateCommand); - addApiCredentialOptions( - program.command("enable").description("enables code to be used on card"), - ) - .option("-c,--card-key ", "the cardkey") + addApiCredentialOptions(program.command('enable').description('enables code to be used on card')) + .option('-c,--card-key ', 'the cardkey') .action(enableCommand); addApiCredentialOptions( - program.command("disable").description("disables code to be used on card"), + program.command('disable').description('disables code to be used on card') ) - .option("-c,--card-key ", "the cardkey") + .option('-c,--card-key ', 'the cardkey') .action(disableCommand); addApiCredentialOptions( - program - .command("currencies") - .description("Gets a list of supported currencies"), + program.command('currencies').description('Gets a list of supported currencies') ).action(currenciesCommand); addApiCredentialOptions( - program.command("countries").description("Gets a list of countries"), + program.command('countries').description('Gets a list of countries') ).action(countriesCommand); addApiCredentialOptions( - program.command("merchants").description("Gets a list of merchants"), + program.command('merchants').description('Gets a list of merchants') ).action(merchantsCommand); - addApiCredentialOptions( - program.command("accounts").description("Gets a list of your accounts"), - ) - .option("--json", "output raw JSON") + addApiCredentialOptions(program.command('accounts').description('Gets a list of your accounts')) + .option('--json', 'output raw JSON') .action(accountsCommand); - addApiCredentialOptions( - program.command("balances").description("Gets your account balances"), - ) - .argument("accountId", "accountId of the account to fetch balances for") + addApiCredentialOptions(program.command('balances').description('Gets your account balances')) + .argument('accountId', 'accountId of the account to fetch balances for') .action(balancesCommand); addApiCredentialOptions( - program.command("transfer").description("Allows transfer between accounts"), + program.command('transfer').description('Allows transfer between accounts') ) - .argument("accountId", "accountId of the account to transfer from") - .argument( - "beneficiaryAccountId", - "beneficiaryAccountId of the account to transfer to", - ) - .argument("amount", "amount to transfer in rands (e.g. 100.00)") - .argument("reference", "reference for the transfer") + .argument('accountId', 'accountId of the account to transfer from') + .argument('beneficiaryAccountId', 'beneficiaryAccountId of the account to transfer to') + .argument('amount', 'amount to transfer in rands (e.g. 100.00)') + .argument('reference', 'reference for the transfer') .action(transferCommand); - addApiCredentialOptions( - program.command("pay").description("Pay a beneficiary from your account"), - ) - .argument("accountId", "accountId of the account to transfer from") - .argument("beneficiaryId", "beneficiaryId of the beneficiary to pay") - .argument("amount", "amount to transfer in rands (e.g. 100.00)") - .argument("reference", "reference for the payment") + addApiCredentialOptions(program.command('pay').description('Pay a beneficiary from your account')) + .argument('accountId', 'accountId of the account to transfer from') + .argument('beneficiaryId', 'beneficiaryId of the beneficiary to pay') + .argument('amount', 'amount to transfer in rands (e.g. 100.00)') + .argument('reference', 'reference for the payment') .action(payCommand); addApiCredentialOptions( - program - .command("transactions") - .description("Gets your account transactions"), + program.command('transactions').description('Gets your account transactions') ) - .argument("accountId", "accountId of the account to fetch balances for") + .argument('accountId', 'accountId of the account to fetch balances for') .action(transactionsCommand); addApiCredentialOptions( - program.command("beneficiaries").description("Gets your beneficiaries"), + program.command('beneficiaries').description('Gets your beneficiaries') ).action(beneficiariesCommand); program - .command("new") - .description("Sets up scaffoldings for a new project") - .argument("name", "name of the new project") - .option("-v,--verbose", "additional debugging information") - .option("--force", "force overwrite existing files") + .command('new') + .description('Sets up scaffoldings for a new project') + .argument('name', 'name of the new project') + .option('-v,--verbose', 'additional debugging information') + .option('--force', 'force overwrite existing files') .addOption( - new Option("--template