diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..f157f98 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,11 @@ +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. + +The project uses commanderjs for the command line interface, and it is designed to be run in a Node.js environment. The code is structured to allow for easy addition of new commands and features. + +Commands can be found in src/cmds and the entry point is in src/index.ts. The project also includes a configuration file for managing settings and options. + +The project is distributed via npm and can be installed globally or used as a local dependency in other projects. + +It is compiled using TypeScript, and the source code is organized into modules for better maintainability. The project also includes unit tests to ensure the functionality of the commands and features. this is done via npm run build. diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..a7fc288 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,30 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node.js CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x, 22.x, 24.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + - run: npm ci + - run: npm run build --if-present + - run: npm test --if-present diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..09170ec --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,24 @@ +name: Publish to npm + +on: + push: + tags: + - "v*.*.*" + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + registry-url: "https://registry.npmjs.org/" + - run: npm ci + - run: npm run build --if-present + - name: Verify package contents + run: npm pack --dry-run + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 5ed4ce0..43e4f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ data .DS_Store executions.json /main.js +published.js env.json gen.js ai-generated.js diff --git a/README.md b/README.md index d3f01b1..5b040b1 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,14 @@ This repository is crafted with ā¤ļø by our talented community members. It's a - [Fetch Published Code](#fetch-published-code) - [Publish Code](#publish-code) - [Simulate Code](#simulate-code) + - [Accounts](#accounts) + - [Balances](#balances) + - [Transfer](#transfer) + - [Pay](#pay) + - [Transactions](#transactions) + - [Beneficiaries](#beneficiaries) + - [Config](#config) + - [Bank](#bank) - [Development](#development) - [Contributing](#contributing) - [License](#license) @@ -338,6 +346,86 @@ This command is ideal for testing your code in a production-like environment bef ![simulate command](assets/simulate.gif) +### Accounts + +Get a list of your accounts: + +```sh +ipb accounts +``` + +This command retrieves all your Investec accounts linked to your credentials. + +### Balances + +Get balances for a specific account: + +```sh +ipb balances +``` + +This command fetches the balance for the given account ID. + +### Transfer + +Transfer between your accounts: + +```sh +ipb transfer +``` + +Transfers the specified amount (in rands, e.g. 100.00) from one account to another with a reference. + +### Pay + +Pay a beneficiary from your account: + +```sh +ipb pay +``` + +Pays a beneficiary from your account with the specified amount and reference. + +### Transactions + +Get transactions for a specific account: + +```sh +ipb transactions +``` + +Fetches the transaction history for the given account ID. + +### Beneficiaries + +Get your list of beneficiaries: + +```sh +ipb beneficiaries +``` + +Lists all beneficiaries linked to your Investec profile. + +### Config + +Set authentication credentials for the CLI: + +```sh +ipb config --client-id --client-secret --api-key +``` + +You can also set card key, OpenAI key, and sandbox key using additional options. + +### Bank + +Use the LLM to call your bank with a natural language prompt: + +```sh +ipb bank "Show me my last 5 transactions" +``` + +This command uses AI to interpret your prompt and interact with your bank data. + --- ## Development diff --git a/assets/accounts.gif b/assets/accounts.gif new file mode 100644 index 0000000..27ecdc2 Binary files /dev/null and b/assets/accounts.gif differ diff --git a/assets/balances.gif b/assets/balances.gif new file mode 100644 index 0000000..4e3fd58 Binary files /dev/null and b/assets/balances.gif differ diff --git a/assets/beneficiaries.gif b/assets/beneficiaries.gif new file mode 100644 index 0000000..8453694 Binary files /dev/null and b/assets/beneficiaries.gif differ diff --git a/assets/cards.gif b/assets/cards.gif index 895327b..276f894 100644 Binary files a/assets/cards.gif and b/assets/cards.gif differ diff --git a/assets/deploy.gif b/assets/deploy.gif index 5d5b395..714e4c1 100644 Binary files a/assets/deploy.gif and b/assets/deploy.gif differ diff --git a/assets/env.gif b/assets/env.gif index 0b68be1..b477240 100644 Binary files a/assets/env.gif and b/assets/env.gif differ diff --git a/assets/fetch.gif b/assets/fetch.gif index c5077d0..54490c8 100644 Binary files a/assets/fetch.gif and b/assets/fetch.gif differ diff --git a/assets/logs.gif b/assets/logs.gif index 3fb27cd..adeccbc 100644 Binary files a/assets/logs.gif and b/assets/logs.gif differ diff --git a/assets/new.gif b/assets/new.gif index a98707d..c6c4e8b 100644 Binary files a/assets/new.gif and b/assets/new.gif differ diff --git a/assets/pay.gif b/assets/pay.gif new file mode 100644 index 0000000..f8b6c2d Binary files /dev/null and b/assets/pay.gif differ diff --git a/assets/publish.gif b/assets/publish.gif index 11e988d..56c2e7d 100644 Binary files a/assets/publish.gif and b/assets/publish.gif differ diff --git a/assets/published.gif b/assets/published.gif index 73741e9..ce40558 100644 Binary files a/assets/published.gif and b/assets/published.gif differ diff --git a/assets/run.gif b/assets/run.gif index 1e8e2dc..fccd2ce 100644 Binary files a/assets/run.gif and b/assets/run.gif differ diff --git a/assets/simulate.gif b/assets/simulate.gif index 149723b..37a1069 100644 Binary files a/assets/simulate.gif and b/assets/simulate.gif differ diff --git a/assets/toggle.gif b/assets/toggle.gif index c527d5c..1459471 100644 Binary files a/assets/toggle.gif and b/assets/toggle.gif differ diff --git a/assets/transactions.gif b/assets/transactions.gif new file mode 100644 index 0000000..c54b562 Binary files /dev/null and b/assets/transactions.gif differ diff --git a/assets/transfer.gif b/assets/transfer.gif new file mode 100644 index 0000000..760aa40 Binary files /dev/null and b/assets/transfer.gif differ diff --git a/assets/upload-env.gif b/assets/upload-env.gif index 2289283..b6bfd49 100644 Binary files a/assets/upload-env.gif and b/assets/upload-env.gif differ diff --git a/assets/upload.gif b/assets/upload.gif index 1330192..206af27 100644 Binary files a/assets/upload.gif and b/assets/upload.gif differ diff --git a/package-lock.json b/package-lock.json index aa6dd46..35a2c68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,20 @@ { "name": "investec-ipb", - "version": "0.8.0", + "version": "0.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "investec-ipb", - "version": "0.8.0", + "version": "0.8.1", "license": "MIT", "dependencies": { + "@inquirer/prompts": "^7.5.3", "chalk": "^4.1.2", "commander": "^13.1.0", "dotenv": "^16.3.1", "investec-card-api": "^0.2.0", + "investec-pb-api": "^0.3.6", "node-fetch": "^3.3.2", "openai": "^4.96.0", "programmable-card-code-emulator": "^1.4.2", @@ -533,6 +535,384 @@ "node": ">=18" } }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz", + "integrity": "sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/checkbox/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/confirm": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.12.tgz", + "integrity": "sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.15.tgz", + "integrity": "sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.12.tgz", + "integrity": "sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.15.tgz", + "integrity": "sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.15.tgz", + "integrity": "sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password/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/prompts": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.3.tgz", + "integrity": "sha512-8YL0WiV7J86hVAxrh3fE5mDCzcTDe1670unmJRz6ArDgN+DBK1a0+rbnNWp4DUB5rPMwqD5ZP6YHl9KK1mbZRg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.8", + "@inquirer/confirm": "^5.1.12", + "@inquirer/editor": "^4.2.13", + "@inquirer/expand": "^4.0.15", + "@inquirer/input": "^4.1.12", + "@inquirer/number": "^3.0.15", + "@inquirer/password": "^4.0.15", + "@inquirer/rawlist": "^4.1.3", + "@inquirer/search": "^3.0.15", + "@inquirer/select": "^4.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.3.tgz", + "integrity": "sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz", + "integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.3.tgz", + "integrity": "sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select/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/type": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1209,6 +1589,12 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -1305,6 +1691,15 @@ "@colors/colors": "1.5.0" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1442,7 +1837,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/emojilib": { @@ -1597,10 +1991,24 @@ "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.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1888,6 +2296,18 @@ "ms": "^2.0.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/investec-card-api": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/investec-card-api/-/investec-card-api-0.2.0.tgz", @@ -1897,11 +2317,19 @@ "node-fetch": "^3.3.2" } }, + "node_modules/investec-pb-api": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/investec-pb-api/-/investec-pb-api-0.3.6.tgz", + "integrity": "sha512-5b6avJ0tfBMtWd7jUcTE1V9ZucWAwFrMC1r5eUSLWCnmBLf+hrRk3h+1RDi4OneAybv7SWM8nTe6UFameLkBaw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^3.3.2" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2076,6 +2504,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -2235,6 +2672,15 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "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", @@ -2487,6 +2933,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -2534,7 +2986,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -2584,7 +3035,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -2615,7 +3065,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2652,7 +3101,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2725,13 +3173,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { @@ -2771,12 +3219,36 @@ "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", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.6.1-rc", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.1-rc.tgz", @@ -2831,18 +3303,18 @@ } }, "node_modules/vite": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", - "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", - "tinyglobby": "^0.2.12" + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -3130,6 +3602,18 @@ "node": ">=10" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.24.3", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", diff --git a/package.json b/package.json index 4b95059..b4788ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "investec-ipb", - "version": "0.8.0", + "version": "0.8.1", "main": "bin/index.js", "bin": { "ipb": "./bin/index.js" @@ -11,9 +11,10 @@ "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", "check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm", "check-format": "prettier --check .", - "ci": "npm run build && npm run check-format && npm run check-exports && npm run test", + "ci": "npm run build && npm run check-format && npm run lint && npm run test && npm audit", "dev": "vitest", "format": "prettier --write .", "tapes": "./scripts/tapes.sh" @@ -30,10 +31,12 @@ "license": "MIT", "description": "A cli application to manage programmable banking cards", "dependencies": { + "@inquirer/prompts": "^7.5.3", "chalk": "^4.1.2", "commander": "^13.1.0", "dotenv": "^16.3.1", "investec-card-api": "^0.2.0", + "investec-pb-api": "^0.3.6", "node-fetch": "^3.3.2", "openai": "^4.96.0", "programmable-card-code-emulator": "^1.4.2", diff --git a/published.js b/published.js deleted file mode 100644 index c80ab0a..0000000 --- a/published.js +++ /dev/null @@ -1,13 +0,0 @@ -// This function runs before a transaction. -const beforeTransaction = async (authorization) => { - console.log(authorization); - }; - // This function runs after a transaction was successful. - const afterTransaction = async (transaction) => { - console.log(transaction); - }; - // This function runs after a transaction was declined. - const afterDecline = async (transaction) => { - console.log(transaction); - }; - \ No newline at end of file diff --git a/scripts/tapes.sh b/scripts/tapes.sh index 9fbe866..9eaf7b1 100755 --- a/scripts/tapes.sh +++ b/scripts/tapes.sh @@ -13,3 +13,9 @@ vhs tapes/simulate.tape vhs tapes/toggle.tape vhs tapes/upload-env.tape vhs tapes/upload.tape +vhs tapes/accounts.tape +vhs tapes/beneficiaries.tape +vhs tapes/balances.tape +vhs tapes/transactions.tape +vhs tapes/pay.tape +vhs tapes/transfer.tape diff --git a/src/cmds/accounts.ts b/src/cmds/accounts.ts new file mode 100644 index 0000000..d58b62f --- /dev/null +++ b/src/cmds/accounts.ts @@ -0,0 +1,40 @@ +import { credentials, initializePbApi } from "../index.js"; +import { handleCliError, printTable } from "../utils.js"; +import type { CommonOptions } from "./types.js"; + +interface Options extends CommonOptions { + json?: boolean; +} + +/** + * Fetch and display Investec accounts. + * @param options CLI options + */ +export async function accountsCommand(options: Options) { + try { + const api = await initializePbApi(credentials, options); + if (options.verbose) console.log("šŸ’³ fetching accounts..."); + const result = await api.getAccounts(); + const accounts = result.data.accounts; + if (!accounts || accounts.length === 0) { + console.log("No accounts found"); + return; + } + if (options.json) { + console.log(JSON.stringify(accounts, null, 2)); + } else { + const simpleAccounts = accounts.map( + ({ accountId, accountNumber, referenceName, productName }) => ({ + accountId, + accountNumber, + referenceName, + productName, + }), + ); + printTable(simpleAccounts); + console.log(`\n${accounts.length} account(s) found.`); + } + } catch (error: any) { + handleCliError(error, options, "fetch accounts"); + } +} diff --git a/src/cmds/ai.ts b/src/cmds/ai.ts index 3dcdbe0..be71778 100644 --- a/src/cmds/ai.ts +++ b/src/cmds/ai.ts @@ -4,6 +4,13 @@ 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"; + +const agent = new https.Agent({ + rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", +}); const instructions = `- You are a coding assistant that creates code snippets for users. - The purpose is to create a code snippet that helps the user control their credit card transactions and taking action if the transaction declines or if it is approved. @@ -85,6 +92,10 @@ export async function aiCommand(prompt: string, options: Options) { try { const envFilename = ".env.ai"; printTitleBox(); + // Prompt for prompt if not provided + if (!prompt) { + prompt = await input({ message: "Enter your AI code prompt:" }); + } // if (!credentials.openaiKey) { // throw new Error("OPENAI_API_KEY is not set"); // } @@ -145,13 +156,8 @@ export async function aiCommand(prompt: string, options: Options) { `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}'`, ); } - console.log(""); } catch (error: any) { - console.error(chalk.redBright("Failed to fetch cards:"), error.message); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "AI generation"); } } @@ -163,8 +169,9 @@ async function generateCode( let openai = new OpenAI({ apiKey: credentials.openaiKey, }); - if (credentials.openaiKey === "") { + if (credentials.openaiKey === "" || credentials.openaiKey === undefined) { openai = new OpenAI({ + httpAgent: agent, apiKey: credentials.sandboxKey, baseURL: "https://ipb.sandboxpay.co.za/proxy/v1", }); diff --git a/src/cmds/balances.ts b/src/cmds/balances.ts new file mode 100644 index 0000000..627e5aa --- /dev/null +++ b/src/cmds/balances.ts @@ -0,0 +1,29 @@ +import { credentials, initializePbApi } from "../index.js"; +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions {} + +export async function balancesCommand(accountId: string, options: Options) { + try { + const api = await initializePbApi(credentials, options); + + console.log("šŸ’³ fetching balances"); + const result = await api.getAccountBalances(accountId); + //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}`); + } 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 balances"); + } + } +} diff --git a/src/cmds/bank.ts b/src/cmds/bank.ts new file mode 100644 index 0000000..1af6494 --- /dev/null +++ b/src/cmds/bank.ts @@ -0,0 +1,201 @@ +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"; + +const agent = new https.Agent({ + rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", +}); + +let openai: OpenAI | undefined = 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`; + +interface Options { + // host: string; // will change this to openai compatible host + credentialsFile: string; // will allow the openai api key to be set in the file as well as its host + filename: string; + verbose: boolean; +} + +export async function bankCommand(prompt: string, options: Options) { + try { + printTitleBox(); + // Prompt for prompt if not provided + if (!prompt) { + prompt = await input({ message: "Enter your banking prompt:" }); + } + + openai = new OpenAI({ + apiKey: credentials.openaiKey, + }); + + if (credentials.openaiKey === "" || credentials.openaiKey === undefined) { + openai = new OpenAI({ + httpAgent: agent, + apiKey: credentials.sandboxKey, + baseURL: "https://ipb.sandboxpay.co.za/proxy/v1", + }); + } + if (!openai) { + throw new Error("OpenAI client is not initialized"); + } + // if (!credentials.openaiKey) { + // throw new Error("OPENAI_API_KEY is not set"); + // } + // if (!fs.existsSync("./instructions.txt")) { + // throw new Error("instructions.txt does not exist"); + // } + + // tell the user we are loading the instructions + 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(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(response); + } else { + 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"); + } +} + +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 }, + ]; + + if (!openai) { + throw new Error("OpenAI client is not initialized"); + } + + const completion = await openai.chat.completions.create({ + model: "gpt-4.1", + temperature: 0.2, + messages, + tools, + }); + //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"); + + if (message.tool_calls) { + return await toolCall(message, tools, messages); + } else if (message.content) { + const content = message.content; + return content; + } + throw new Error("Invalid response format from OpenAI"); + } catch (error) { + console.error("Error generating code:", error); + return null; + } +} + +async function secondCall( + functionResponse: string, + messages: OpenAI.ChatCompletionMessageParam[], + toolCaller: OpenAI.ChatCompletionMessageToolCall, + tools: OpenAI.ChatCompletionTool[], +) { + if (!openai) { + 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 + // Ensure the tool_call_id in the tool message matches the tool_call_id in the assistant message's tool_calls array + const followupMessages: OpenAI.ChatCompletionMessageParam[] = [ + messages[0] as OpenAI.ChatCompletionMessageParam, // system + messages[1] as OpenAI.ChatCompletionMessageParam, // user + { + role: "assistant", + content: null, + tool_calls: [ + toolCaller, // tool call from the assistant message + ], + } as OpenAI.ChatCompletionMessageParam, + { + role: "tool", + tool_call_id: toolCaller.id, + content: + typeof functionResponse === "string" + ? functionResponse + : JSON.stringify(functionResponse), + } as OpenAI.ChatCompletionToolMessageParam, + ]; + + const response2 = await openai.chat.completions.create({ + 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"); + if (message.tool_calls) { + return await toolCall(message, tools, messages); + } + if (message.content) { + const content = message.content; + return content; + } + return null; +} + +// Fix: toolCall should be async and return Promise +async function toolCall( + message: OpenAI.ChatCompletionMessage, + tools: OpenAI.ChatCompletionTool[], + 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; + if (!toolCalls) { + throw new Error("No tool_calls found in message"); + } + + for (const toolCall of toolCalls) { + const functionName = toolCall.function.name; + const functionToCall = availableFunctions[functionName]; + if (!functionToCall) continue; // skip unknown tools + const functionArgs = JSON.parse(toolCall.function.arguments); + const functionResponse = await functionToCall(functionArgs); + return await secondCall(functionResponse, messages, toolCall, tools); + } + throw new Error("Invalid response format from OpenAI"); +} diff --git a/src/cmds/beneficiaries.ts b/src/cmds/beneficiaries.ts new file mode 100644 index 0000000..8eb3960 --- /dev/null +++ b/src/cmds/beneficiaries.ts @@ -0,0 +1,39 @@ +import { credentials, initializePbApi } from "../index.js"; +import { handleCliError, printTable } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions {} + +export async function beneficiariesCommand(options: Options) { + try { + const api = await initializePbApi(credentials, options); + + console.log("šŸ’³ fetching beneficiaries"); + const result = await api.getBeneficiaries(); + const beneficiaries = result.data; + console.log(""); + if (!beneficiaries) { + console.log("No beneficiaries found"); + return; + } + const simpleBeneficiaries = beneficiaries.map( + ({ + beneficiaryId, + accountNumber, + beneficiaryName, + lastPaymentDate, + lastPaymentAmount, + referenceName, + }) => ({ + beneficiaryId, + accountNumber, + beneficiaryName, + lastPaymentDate, + lastPaymentAmount, + referenceName, + }), + ); + printTable(simpleBeneficiaries); + } catch (error: any) { + handleCliError(error, options, "fetch beneficiaries"); + } +} diff --git a/src/cmds/cards.ts b/src/cmds/cards.ts index f8d4a7b..185c11d 100644 --- a/src/cmds/cards.ts +++ b/src/cmds/cards.ts @@ -1,13 +1,7 @@ -import chalk from "chalk"; import { credentials, initializeApi } from "../index.js"; -interface Options { - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; -} +import { handleCliError, printTable } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions {} export async function cardsCommand(options: Options) { try { @@ -21,22 +15,16 @@ export async function cardsCommand(options: Options) { console.log("No cards found"); return; } - console.log("Card Key \tCard Number \t\tCode Enabled"); - for (let i = 0; i < cards.length; i++) { - if (cards[i]) { - console.log( - chalk.greenBright(`${cards[i]?.CardKey ?? "N/A"}\t\t`) + - chalk.blueBright(`${cards[i]?.CardNumber ?? "N/A"}\t\t`) + - chalk.redBright(`${cards[i]?.IsProgrammable ?? "N/A"}`), - ); - } - } - console.log(""); + + const simpleCards = cards.map( + ({ CardKey, CardNumber, IsProgrammable }) => ({ + CardKey, + CardNumber, + IsProgrammable, + }), + ); + printTable(simpleCards); } catch (error: any) { - console.error(chalk.redBright("Failed to fetch cards:"), error.message); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "fetch cards"); } } diff --git a/src/cmds/countries.ts b/src/cmds/countries.ts index 9b541d6..cd40234 100644 --- a/src/cmds/countries.ts +++ b/src/cmds/countries.ts @@ -1,13 +1,7 @@ -import chalk from "chalk"; import { credentials, initializeApi } from "../index.js"; -interface Options { - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; -} +import { handleCliError, printTable } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions {} export async function countriesCommand(options: Options) { try { const api = await initializeApi(credentials, options); @@ -20,22 +14,10 @@ export async function countriesCommand(options: Options) { console.log("No countries found"); return; } - console.log("Code \t\t Name"); - for (let i = 0; i < countries.length; i++) { - if (countries[i]) { - console.log( - chalk.greenBright(`${countries[i]?.Code ?? "N/A"}`) + - ` \t ` + - chalk.blueBright(`${countries[i]?.Name ?? "N/A"}`), - ); - } - } - console.log(""); + + const simpleCountries = countries.map(({ Code, Name }) => ({ Code, Name })); + printTable(simpleCountries); } catch (error: any) { - console.error(chalk.redBright("Failed to fetch countries:"), error.message); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "fetch countries"); } } diff --git a/src/cmds/currencies.ts b/src/cmds/currencies.ts index 45f1238..e9b6a6b 100644 --- a/src/cmds/currencies.ts +++ b/src/cmds/currencies.ts @@ -1,13 +1,7 @@ -import chalk from "chalk"; import { credentials, initializeApi } from "../index.js"; -interface Options { - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; -} +import { handleCliError, printTable } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions {} export async function currenciesCommand(options: Options) { try { const api = await initializeApi(credentials, options); @@ -20,25 +14,13 @@ export async function currenciesCommand(options: Options) { console.log("No currencies found"); return; } - console.log("Code \t Name"); - for (let i = 0; i < currencies.length; i++) { - if (currencies[i]) { - console.log( - chalk.greenBright(`${currencies[i]?.Code ?? "N/A"}`) + - ` \t ` + - chalk.blueBright(`${currencies[i]?.Name ?? "N/A"}`), - ); - } - } - console.log(""); + + const simpleCurrencies = currencies.map(({ Code, Name }) => ({ + Code, + Name, + })); + printTable(simpleCurrencies); } catch (error: any) { - console.error( - chalk.redBright("Failed to fetch currencies:"), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "fetch currencies"); } } diff --git a/src/cmds/deploy.ts b/src/cmds/deploy.ts index 855aa2c..973e074 100644 --- a/src/cmds/deploy.ts +++ b/src/cmds/deploy.ts @@ -1,51 +1,50 @@ import fs from "fs"; import dotenv from "dotenv"; import { credentials, initializeApi } from "../index.js"; -import chalk from "chalk"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; filename: string; env: string; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - //verbose: boolean; } + export async function deployCommand(options: Options) { - let envObject = {}; - if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new Error("card-key is required"); + try { + let envObject = {}; + if (options.cardKey === undefined) { + if (credentials.cardKey === "") { + throw new Error("card-key is required"); + } + options.cardKey = Number(credentials.cardKey); } - options.cardKey = Number(credentials.cardKey); - } - const api = await initializeApi(credentials, options); + const api = await initializeApi(credentials, options); - if (options.env) { - if (!fs.existsSync(`.env.${options.env}`)) { - throw new Error("Env does not exist"); - } - envObject = dotenv.parse(fs.readFileSync(`.env.${options.env}`)); + if (options.env) { + if (!fs.existsSync(`.env.${options.env}`)) { + throw new Error("Env does not exist"); + } + envObject = dotenv.parse(fs.readFileSync(`.env.${options.env}`)); - await api.uploadEnv(options.cardKey, { variables: envObject }); - console.log("šŸ“¦ env deployed"); - } - console.log("šŸš€ 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, - ); - if (result.data.result.codeId) { - console.log("šŸŽ‰ code deployed"); + await api.uploadEnv(options.cardKey, { variables: envObject }); + console.log("šŸ“¦ env deployed"); + } + console.log("šŸš€ 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, + ); + if (result.data.result.codeId) { + console.log("šŸŽ‰ code deployed"); + } + } catch (error: any) { + handleCliError(error, { verbose: (options as any).verbose }, "deploy code"); } - console.log(""); } diff --git a/src/cmds/disable.ts b/src/cmds/disable.ts index d22b0e0..7530bcf 100644 --- a/src/cmds/disable.ts +++ b/src/cmds/disable.ts @@ -1,19 +1,14 @@ import { credentials, initializeApi } from "../index.js"; -import chalk from "chalk"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; } export async function disableCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { - throw new Error("cardkey is required"); + throw new Error("card-key is required"); } options.cardKey = Number(credentials.cardKey); } @@ -27,12 +22,7 @@ export async function disableCommand(options: Options) { } else { console.log("āŒ code disable failed"); } - console.log(""); } catch (error: any) { - console.error(chalk.redBright("Failed to disable:"), error.message); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "disable card code"); } } diff --git a/src/cmds/env.ts b/src/cmds/env.ts index f6069c5..6519662 100644 --- a/src/cmds/env.ts +++ b/src/cmds/env.ts @@ -1,16 +1,12 @@ import fs from "fs"; import { credentials, initializeApi } from "../index.js"; -import chalk from "chalk"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; filename: string; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; } + export async function envCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { @@ -30,15 +26,7 @@ export async function envCommand(options: Options) { console.log(`šŸ’¾ saving to file: ${options.filename}`); fs.writeFileSync(options.filename, JSON.stringify(envs, null, 4)); console.log("šŸŽ‰ envs saved to file"); - console.log(""); } catch (error: any) { - console.error( - chalk.redBright("Failed to fetch environment variables: "), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "fetch environment variables"); } } diff --git a/src/cmds/fetch.ts b/src/cmds/fetch.ts index 3ac3263..f8bfbec 100644 --- a/src/cmds/fetch.ts +++ b/src/cmds/fetch.ts @@ -1,16 +1,12 @@ import fs from "fs"; import { credentials, initializeApi } from "../index.js"; -import chalk from "chalk"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; filename: string; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; } + export async function fetchCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { @@ -30,15 +26,7 @@ export async function fetchCommand(options: Options) { console.log(`šŸ’¾ saving to file: ${options.filename}`); await fs.writeFileSync(options.filename, code); console.log("šŸŽ‰ code saved to file"); - console.log(""); } catch (error: any) { - console.error( - chalk.redBright("Failed to fetch saved code:"), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "fetch saved code"); } } diff --git a/src/cmds/index.ts b/src/cmds/index.ts index 46a61fe..c0a7333 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -16,6 +16,7 @@ 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"; export { cardsCommand, @@ -36,4 +37,5 @@ export { merchantsCommand, newCommand, aiCommand as generateCommand, + bankCommand, }; diff --git a/src/cmds/login.ts b/src/cmds/login.ts index 9235096..b38b4d9 100644 --- a/src/cmds/login.ts +++ b/src/cmds/login.ts @@ -1,6 +1,13 @@ -import chalk from "chalk"; 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"; + +const agent = new https.Agent({ + rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", +}); interface Options { email: string; @@ -16,14 +23,30 @@ interface LoginResponse { created_at: number; } -export async function loginCommand(options: Options) { +export async function loginCommand(options: any) { try { printTitleBox(); + if (!options.email) { + options.email = await input({ + 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.", + }); + } if (!options.email || !options.password) { throw new Error("Email and password are required"); } console.log("šŸ’³ logging into account"); const result = await fetch("https://ipb.sandboxpay.co.za/auth/login", { + agent, method: "POST", headers: { "Content-Type": "application/json", @@ -47,16 +70,20 @@ export async function loginCommand(options: Options) { openaiKey: "", sandboxKey: "", }; + if (fs.existsSync(credentialLocation.filename)) { + cred = JSON.parse(fs.readFileSync(credentialLocation.filename, "utf8")); + } else { + if (!fs.existsSync(credentialLocation.folder)) { + fs.mkdirSync(credentialLocation.folder, { recursive: true }); + } + + await fs.writeFileSync(credentialLocation.filename, JSON.stringify(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"); - console.log(""); } catch (error: any) { - console.error(chalk.redBright("Failed to login:"), error.message); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "login"); } } diff --git a/src/cmds/logs.ts b/src/cmds/logs.ts index 6d615e4..40e0b92 100644 --- a/src/cmds/logs.ts +++ b/src/cmds/logs.ts @@ -1,27 +1,24 @@ import chalk from "chalk"; import fs from "fs"; import { credentials, initializeApi } from "../index.js"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; filename: string; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; } + export async function logsCommand(options: Options) { - if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new Error("cardkey is required"); - } - options.cardKey = Number(credentials.cardKey); - } - if (options.filename === undefined || options.filename === "") { - throw new Error("filename is required"); - } try { + if (options.cardKey === undefined) { + 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"); + } const api = await initializeApi(credentials, options); console.log("šŸ“Š fetching execution items"); @@ -34,15 +31,7 @@ export async function logsCommand(options: Options) { JSON.stringify(result.data.result.executionItems, null, 4), ); console.log("šŸŽ‰ " + chalk.greenBright("logs saved to file")); - console.log(""); } catch (error: any) { - console.error( - chalk.redBright("Failed to fetch execution logs:"), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "fetch execution logs"); } } diff --git a/src/cmds/merchants.ts b/src/cmds/merchants.ts index d2a6d67..ab42ab7 100644 --- a/src/cmds/merchants.ts +++ b/src/cmds/merchants.ts @@ -1,13 +1,8 @@ -import chalk from "chalk"; import { credentials, initializeApi } from "../index.js"; -interface Options { - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; -} +import { handleCliError, printTable } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions {} + export async function merchantsCommand(options: Options) { try { const api = await initializeApi(credentials, options); @@ -21,22 +16,9 @@ export async function merchantsCommand(options: Options) { return; } - console.log("Code \t Name"); - for (let i = 0; i < merchants.length; i++) { - if (merchants[i]) { - console.log( - chalk.greenBright(`${merchants[i]?.Code ?? "N/A"}`) + - ` \t ` + - chalk.blueBright(`${merchants[i]?.Name ?? "N/A"}`), - ); - } - } - console.log(""); + const simpleMerchants = merchants.map(({ Code, Name }) => ({ Code, Name })); + printTable(simpleMerchants); } catch (error: any) { - console.error(chalk.redBright("Failed to fetch merchants:"), error.message); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "fetch merchants"); } } diff --git a/src/cmds/new.ts b/src/cmds/new.ts index 65dc472..6dae7a0 100644 --- a/src/cmds/new.ts +++ b/src/cmds/new.ts @@ -2,6 +2,7 @@ import chalk from "chalk"; import fs from "fs"; import path from "path"; import { printTitleBox } from "../index.js"; +import { handleCliError } from "../utils.js"; interface Options { template: string; @@ -48,13 +49,6 @@ export async function newCommand(name: string, options: Options) { `- 🧪 Test your code with: ${chalk.green(`ipb run -f ${name}/main.js`)}`, ); } catch (error: any) { - console.error( - chalk.redBright("Failed to create from template:"), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "create new project"); } } diff --git a/src/cmds/pay.ts b/src/cmds/pay.ts new file mode 100644 index 0000000..05b114f --- /dev/null +++ b/src/cmds/pay.ts @@ -0,0 +1,70 @@ +import { credentials, initializePbApi } from "../index.js"; +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +import { input, password } from "@inquirer/prompts"; + +interface Options extends CommonOptions {} + +export async function payCommand( + accountId: string, + beneficiaryId: string, + amount: number, + reference: string, + options: Options, +) { + try { + // Prompt for missing arguments interactively + if (!accountId) { + accountId = await input({ message: "Enter your account ID:" }); + } + if (!beneficiaryId) { + beneficiaryId = await input({ message: "Enter beneficiary ID:" }); + } + if (!amount) { + 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 (!reference) { + reference = await input({ message: "Enter reference for the payment:" }); + } + + const api = await initializePbApi(credentials, options); + + +( + // Show transaction summary and require confirmation + console.log(`\nTransaction Summary:`) + ); + console.log(`Account: ${accountId}`); + console.log(`Beneficiary: ${beneficiaryId}`); + console.log(`Amount: R${amount.toFixed(2)}`); + console.log(`Reference: ${reference}\n`); + + const confirmPayment = await input({ + message: "Type 'CONFIRM' to proceed with this payment:", + }); + if (confirmPayment !== "CONFIRM") { + console.log("Payment cancelled."); + return; + } + + console.log("šŸ’³ paying"); + const result = await api.payMultiple(accountId, [ + { + beneficiaryId: beneficiaryId, + amount: amount.toString(), + myReference: reference, + theirReference: reference, + }, + ]); + for (const transfer of result.data.TransferResponses) { + console.log( + `Transfer to ${transfer.BeneficiaryAccountId}, reference ${transfer.PaymentReferenceNumber} was successful.`, + ); + } + } catch (error: any) { + handleCliError(error, options, "pay beneficiary"); + } +} diff --git a/src/cmds/publish.ts b/src/cmds/publish.ts index 460da72..3d692db 100644 --- a/src/cmds/publish.ts +++ b/src/cmds/publish.ts @@ -1,28 +1,24 @@ import fs from "fs"; import { credentials, initializeApi } from "../index.js"; -import chalk from "chalk"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; filename: string; codeId: string; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; } + export async function publishCommand(options: Options) { - if (!fs.existsSync(options.filename)) { - throw new Error("File does not exist"); - } - if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new Error("card-key is required"); - } - options.cardKey = Number(credentials.cardKey); - } try { + if (!fs.existsSync(options.filename)) { + throw new Error("File does not exist"); + } + if (options.cardKey === undefined) { + if (credentials.cardKey === "") { + throw new Error("card-key is required"); + } + options.cardKey = Number(credentials.cardKey); + } const api = await initializeApi(credentials, options); console.log("šŸš€ publishing code..."); @@ -33,12 +29,7 @@ export async function publishCommand(options: Options) { code, ); console.log(`šŸŽ‰ code published with codeId: ${result.data.result.codeId}`); - console.log(""); } catch (error: any) { - console.error(chalk.redBright("Failed to publish code:"), error.message); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "publish code"); } } diff --git a/src/cmds/published.ts b/src/cmds/published.ts index 82c7f1c..28f39f8 100644 --- a/src/cmds/published.ts +++ b/src/cmds/published.ts @@ -1,16 +1,12 @@ import fs from "fs"; import { credentials, initializeApi } from "../index.js"; -import chalk from "chalk"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; filename: string; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; } + export async function publishedCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { @@ -27,15 +23,7 @@ export async function publishedCommand(options: Options) { console.log(`šŸ’¾ saving to file: ${options.filename}`); await fs.writeFileSync(options.filename, code); console.log("šŸŽ‰ code saved to file"); - console.log(""); } catch (error: any) { - console.error( - chalk.redBright("Failed to publish saved code:"), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "fetch published code"); } } diff --git a/src/cmds/register.ts b/src/cmds/register.ts index 619f8ec..f89ad38 100644 --- a/src/cmds/register.ts +++ b/src/cmds/register.ts @@ -1,19 +1,43 @@ -import chalk from "chalk"; import { printTitleBox } from "../index.js"; -interface Options { +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"; + +const agent = new https.Agent({ + rejectUnauthorized: process.env.REJECT_UNAUTHORIZED !== "false", +}); +interface Options extends CommonOptions { email: string; password: string; - credentialsFile: string; } export async function registerCommand(options: Options) { try { printTitleBox(); + // 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.", + }); + } + 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.", + }); + } if (!options.email || !options.password) { throw new Error("Email and password are required"); } console.log("šŸ’³ registering account"); const result = await fetch("https://ipb.sandboxpay.co.za/auth/register", { + agent, method: "POST", headers: { "Content-Type": "application/json", @@ -29,9 +53,7 @@ export async function registerCommand(options: Options) { } console.log("Account registered successfully"); - console.log(""); } catch (error: any) { - console.error(chalk.redBright("Failed to register:"), error.message); - console.log(""); + handleCliError(error, { verbose: options.verbose }, "register"); } } diff --git a/src/cmds/run.ts b/src/cmds/run.ts index 74da1c7..7760f46 100644 --- a/src/cmds/run.ts +++ b/src/cmds/run.ts @@ -3,6 +3,7 @@ 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"; interface Options { filename: string; env: string; @@ -12,72 +13,78 @@ interface Options { merchant: string; city: string; country: string; - // verbose: boolean; + verbose: boolean; } export async function runCommand(options: Options) { printTitleBox(); - if (!fs.existsSync(options.filename)) { - throw new Error("File does not exist"); - } - 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, - ); - 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), - ); - // 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 Error("Env does not exist"); + try { + if (!fs.existsSync(options.filename)) { + throw new Error("File does not exist"); } + 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, + ); + 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), + ); + // Read the template env.json file and replace the values with the process.env values - const data = fs.readFileSync(`.env.${options.env}`, "utf8"); - let lines = data.split("\n"); + let environmentvariables: { [key: string]: string } = {}; + if (options.env) { + if (!fs.existsSync(`.env.${options.env}`)) { + throw new Error("Env does not exist"); + } - 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", - ); - // Run the code - const executionItems = await run( - transaction, - code, - environmentvariablesString, - ); - executionItems.forEach((item) => { - console.log("\nšŸ’» ", chalk.green(item.type)); - item.logs.forEach((log) => { - console.log("\n", chalk.yellow(log.level), chalk.white(log.content)); + const data = fs.readFileSync(`.env.${options.env}`, "utf8"); + let 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", + ); + // Run the code + const executionItems = await run( + transaction, + code, + environmentvariablesString, + ); + executionItems.forEach((item) => { + console.log("\nšŸ’» ", chalk.green(item.type)); + item.logs.forEach((log) => { + console.log("\n", chalk.yellow(log.level), chalk.white(log.content)); + }); }); - }); - console.log(""); + } catch (error: any) { + handleCliError(error, { verbose: options.verbose }, "run code"); + } } function convertToJson(arr: string[]) { diff --git a/src/cmds/set.ts b/src/cmds/set.ts index 26a360f..8b635d9 100644 --- a/src/cmds/set.ts +++ b/src/cmds/set.ts @@ -1,5 +1,7 @@ import fs from "fs"; import { credentialLocation } from "../index.js"; +import { handleCliError } from "../utils.js"; + interface Options { clientId: string; clientSecret: string; @@ -45,12 +47,7 @@ export async function configCommand(options: Options) { } await fs.writeFileSync(credentialLocation.filename, JSON.stringify(cred)); console.log("šŸ”‘ credentials saved"); - console.log(""); } catch (error: any) { - console.error("Failed to save credentials:", error.message); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "set config"); } } diff --git a/src/cmds/simulate.ts b/src/cmds/simulate.ts index 7989869..65a208c 100644 --- a/src/cmds/simulate.ts +++ b/src/cmds/simulate.ts @@ -2,6 +2,8 @@ import chalk from "chalk"; import fs from "fs"; import { createTransaction } from "programmable-card-code-emulator"; import { credentials, initializeApi } from "../index.js"; +import { handleCliError } from "../utils.js"; + interface Options { cardKey: number; filename: string; @@ -19,16 +21,16 @@ interface Options { verbose: boolean; } export async function simulateCommand(options: Options) { - if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new Error("card-key is required"); - } - options.cardKey = Number(credentials.cardKey); - } - if (!fs.existsSync(options.filename)) { - throw new Error("File does not exist"); - } try { + if (options.cardKey === undefined) { + if (credentials.cardKey === "") { + throw new Error("card-key is required"); + } + options.cardKey = Number(credentials.cardKey); + } + if (!fs.existsSync(options.filename)) { + throw new Error("File does not exist"); + } const api = await initializeApi(credentials, options); console.log("šŸš€ uploading code & running simulation"); @@ -76,15 +78,7 @@ export async function simulateCommand(options: Options) { console.log("\n", chalk.yellow(log.level), chalk.white(log.content)); }); }); - console.log(""); } catch (error: any) { - console.error( - chalk.redBright("Failed to simulate code online:"), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "simulate code"); } } diff --git a/src/cmds/toggle.ts b/src/cmds/toggle.ts index 8d5445a..b6bad48 100644 --- a/src/cmds/toggle.ts +++ b/src/cmds/toggle.ts @@ -1,14 +1,10 @@ import { credentials, initializeApi } from "../index.js"; -import chalk from "chalk"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; } + export async function enableCommand(options: Options) { if (options.cardKey === undefined) { if (credentials.cardKey === "") { @@ -26,15 +22,7 @@ export async function enableCommand(options: Options) { } else { console.log("āŒ code enable failed"); } - console.log(""); } catch (error: any) { - console.error( - chalk.redBright("Failed to enable card code:"), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "enable card code"); } } diff --git a/src/cmds/transactions.ts b/src/cmds/transactions.ts new file mode 100644 index 0000000..9b0e7e0 --- /dev/null +++ b/src/cmds/transactions.ts @@ -0,0 +1,57 @@ +import { credentials, initializePbApi } from "../index.js"; +import { handleCliError, printTable } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions {} + +/** + * Minimal transaction type for CLI display. + */ +type Transaction = { + uuid: string; + amount: number; + transactionDate: string; + description: string; + // ...other fields can be added as needed +}; + +/** + * Fetch and display transactions for a given account. + * @param accountId - The account ID to fetch transactions for. + * @param options - CLI options. + */ +export async function transactionsCommand(accountId: string, options: Options) { + try { + const api = await initializePbApi(credentials, options); + + console.log("šŸ’³ fetching transactions"); + const result = await api.getAccountTransactions( + accountId, + null, + null, + null, + ); + const transactions = result.data.transactions; + console.log(""); + if (!transactions) { + console.log("No transactions found"); + return; + } + + const simpleTransactions = transactions.map( + ({ uuid, amount, transactionDate, description }: Transaction) => ({ + uuid, + amount, + transactionDate, + description, + }), + ); + printTable(simpleTransactions); + } 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"); + } + } +} diff --git a/src/cmds/transfer.ts b/src/cmds/transfer.ts new file mode 100644 index 0000000..1e0e00d --- /dev/null +++ b/src/cmds/transfer.ts @@ -0,0 +1,55 @@ +import { credentials, initializePbApi } from "../index.js"; +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +import { input, password } from "@inquirer/prompts"; + +interface Options extends CommonOptions {} + +export async function transferCommand( + accountId: string, + beneficiaryAccountId: string, + amount: number, + reference: string, + options: Options, +) { + try { + // Prompt for missing arguments interactively + if (!accountId) { + accountId = await input({ message: "Enter your account ID:" }); + } + if (!beneficiaryAccountId) { + beneficiaryAccountId = await input({ + message: "Enter beneficiary account ID:", + }); + } + if (!amount) { + 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 (!reference) { + reference = await input({ message: "Enter reference for the transfer:" }); + } + + const api = await initializePbApi(credentials, options); + + console.log("šŸ’³ transfering"); + const result = await api.transferMultiple(accountId, [ + { + beneficiaryAccountId: beneficiaryAccountId, + amount: amount.toString(), + myReference: reference, + theirReference: reference, + }, + ]); + for (const transfer of result.data.TransferResponses) { + console.log( + `Transfer to ${transfer.BeneficiaryAccountId}, reference ${transfer.PaymentReferenceNumber} was successful.`, + ); + } + } catch (error: any) { + handleCliError(error, options, "transfer"); + } +} diff --git a/src/cmds/types.ts b/src/cmds/types.ts new file mode 100644 index 0000000..530edd0 --- /dev/null +++ b/src/cmds/types.ts @@ -0,0 +1,27 @@ +// Common options shared by most CLI commands +export interface CommonOptions { + host: string; + apiKey: string; + clientId: string; + clientSecret: string; + credentialsFile: string; + verbose: boolean; +} + +export interface Credentials { + host: string; + clientId: string; + clientSecret: string; + apiKey: string; + cardKey: string; + openaiKey: string; + sandboxKey: string; +} + +export interface BasicOptions { + host: string; + apiKey: string; + clientId: string; + clientSecret: string; + credentialsFile: string; +} diff --git a/src/cmds/upload-env.ts b/src/cmds/upload-env.ts index ca0f9d4..d9d3484 100644 --- a/src/cmds/upload-env.ts +++ b/src/cmds/upload-env.ts @@ -1,16 +1,12 @@ import fs from "fs"; import { credentials, initializeApi } from "../index.js"; -import chalk from "chalk"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; filename: string; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; } + export async function uploadEnvCommand(options: Options) { if (!fs.existsSync(options.filename)) { throw new Error("File does not exist"); @@ -30,15 +26,7 @@ export async function uploadEnvCommand(options: Options) { raw.variables = JSON.parse(variables); const result = await api.uploadEnv(options.cardKey, raw); console.log(`šŸŽ‰ env uploaded`); - console.log(""); } catch (error: any) { - console.error( - chalk.redBright("Failed to upload environment variables: "), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "upload environment variables"); } } diff --git a/src/cmds/upload.ts b/src/cmds/upload.ts index b747c5f..109d91d 100644 --- a/src/cmds/upload.ts +++ b/src/cmds/upload.ts @@ -1,27 +1,23 @@ import fs from "fs"; import { credentials, initializeApi } from "../index.js"; -import chalk from "chalk"; -interface Options { +import { handleCliError } from "../utils.js"; +import type { CommonOptions } from "./types.js"; +interface Options extends CommonOptions { cardKey: number; filename: string; - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; - verbose: boolean; } + export async function uploadCommand(options: Options) { - if (!fs.existsSync(options.filename)) { - throw new Error("File does not exist"); - } - if (options.cardKey === undefined) { - if (credentials.cardKey === "") { - throw new Error("card-key is required"); - } - options.cardKey = Number(credentials.cardKey); - } try { + if (!fs.existsSync(options.filename)) { + throw new Error("File does not exist"); + } + if (options.cardKey === undefined) { + if (credentials.cardKey === "") { + throw new Error("card-key is required"); + } + options.cardKey = Number(credentials.cardKey); + } const api = await initializeApi(credentials, options); console.log("šŸš€ uploading code"); @@ -30,15 +26,7 @@ export async function uploadCommand(options: Options) { raw.code = code; const result = await api.uploadCode(options.cardKey, raw); console.log(`šŸŽ‰ code uploaded with codeId: ${result.data.result.codeId}`); - console.log(""); } catch (error: any) { - console.error( - chalk.redBright("Failed to upload to saved code:"), - error.message, - ); - console.log(""); - if (options.verbose) { - console.error(error); - } + handleCliError(error, options, "upload code"); } } diff --git a/src/function-calls.ts b/src/function-calls.ts new file mode 100644 index 0000000..3203ffc --- /dev/null +++ b/src/function-calls.ts @@ -0,0 +1,202 @@ +import OpenAI from "openai"; +import { credentials, initializePbApi } from "./index.js"; +import type { BasicOptions } from "./cmds/types.js"; +import type { + AccountBalance, + AccountTransaction, + Transfer, + TransferMultiple, + TransferResponse, +} from "investec-pb-api"; + +export const getWeatherFunctionCall: OpenAI.ChatCompletionTool = { + type: "function", + function: { + name: "get_weather", + description: "Get current temperature for provided coordinates in celsius.", + parameters: { + type: "object", + properties: { + latitude: { type: "number" }, + longitude: { type: "number" }, + }, + 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; +} + +interface Options extends BasicOptions { + verbose: boolean; +} + +export const getAccountsFunctionCall: OpenAI.ChatCompletionTool = { + type: "function", + function: { + name: "get_accounts", + description: "Get a list of your accounts.", + }, +}; + +export const getBalanceFunctionCall: OpenAI.ChatCompletionTool = { + type: "function", + function: { + name: "get_balance", + description: "Get the balance for a specific account.", + parameters: { + type: "object", + properties: { + accountId: { type: "string" }, + }, + required: ["accountId"], + additionalProperties: false, + }, + }, +}; + +export const getAccountTransactionFunctionCall: OpenAI.ChatCompletionTool = { + type: "function", + function: { + name: "get_transactions", + description: "Get the transactions for a specific account.", + parameters: { + type: "object", + properties: { + accountId: { type: "string" }, + fromDate: { type: "string", format: "date" }, + toDate: { type: "string", format: "date" }, + }, + required: ["accountId", "fromDate"], + additionalProperties: false, + }, + }, +}; + +export const getBeneficiariesFunctionCall: OpenAI.ChatCompletionTool = { + type: "function", + function: { + name: "get_beneficiaries", + description: + "Get a list of your external beneficiaries for making payments.", + }, +}; + +export const transferMultipleFunctionCall: OpenAI.ChatCompletionTool = { + type: "function", + function: { + name: "transfer_multiple", + description: + "Transfer money between accounts. the beneficiaryAccountId is the account you are transferring to.", + parameters: { + type: "object", + properties: { + accountId: { type: "string" }, + beneficiaryAccountId: { type: "string" }, + amount: { type: "string" }, + myReference: { type: "string" }, + theirReference: { type: "string" }, + }, + 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 { + const api = await initializePbApi(credentials, {} as Options); + const result = await api.getAccounts(); + console.log("šŸ’³ fetching accounts"); + const accounts = result.data.accounts; + return accounts; +} + +export async function getAccountBalances(options: { + accountId: string; +}): Promise { + const api = await initializePbApi(credentials, {} as Options); + console.log(`šŸ’³ fetching balances for account ${options.accountId}`); + const result = await api.getAccountBalances(options.accountId); + const accounts = result.data; + return accounts; +} + +// thin out responses as they use too many tokens +export async function getAccountTransactions(options: { + accountId: string; + fromDate: string; + toDate: string; +}): Promise { + 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, + ); + const transactions = result.data.transactions; + return transactions; +} + +export async function getBeneficiaries(): Promise { + const api = await initializePbApi(credentials, {} as Options); + const result = await api.getBeneficiaries(); + 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 { + 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 + myReference: options.myReference, + theirReference: options.theirReference, + }; + // Fix: always pass as array to match type signature + const result = await api.transferMultiple(options.accountId, [transfer]); + const transferResponse = result.data.TransferResponses; + return transferResponse; +} + +export const tools: OpenAI.ChatCompletionTool[] = [ + getAccountsFunctionCall, + getBalanceFunctionCall, + getAccountTransactionFunctionCall, + getBeneficiariesFunctionCall, + transferMultipleFunctionCall, +]; + +export const availableFunctions: Record any> = { + get_accounts: getAccounts, + get_balance: getAccountBalances, + get_transactions: getAccountTransactions, + get_beneficiaries: getBeneficiaries, + transfer_multiple: transferMultiple, +}; diff --git a/src/index.ts b/src/index.ts index 53409a2..bfdfb83 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,15 @@ #!/usr/bin/env node +// File: src/index.ts +// Main entry point for the Investec Programmable Banking CLI +// 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 { cardsCommand, configCommand, @@ -21,34 +29,39 @@ import { merchantsCommand, newCommand, generateCommand, + bankCommand, } from "./cmds/index.js"; -import { homedir } from "os"; -import { Command, Option } from "commander"; -import chalk from "chalk"; import { simulateCommand } from "./cmds/simulate.js"; -import { InvestecCardApi } from "investec-card-api"; -import { CardApi } from "./mock-card.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"; -const version = "0.8.0"; +const version = "0.8.1-rc.3"; const program = new Command(); + +// Only export what is needed outside this file export const credentialLocation = { folder: `${homedir()}/.ipb`, filename: `${homedir()}/.ipb/.credentials.json`, }; + +// Print CLI title (used in some commands) export async function printTitleBox() { - // const v = await checkLatestVersion() console.log(""); console.log("šŸ¦“ Investec Programmable Banking CLI"); - console.log("šŸ”® " + chalk.blueBright(`v${version}`)); - // if (v !== version) { - // console.log("šŸ”„ " + chalk.redBright(`v${v} is available`)) - // }; + // console.log("šŸ”® " + chalk.blueBright(`v${version}`)); console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); console.log(""); } +// Load credentials from file if present let cred = { clientId: "", clientSecret: "", @@ -65,119 +78,15 @@ if (fs.existsSync(credentialLocation.filename)) { if (err instanceof Error) { console.error( chalk.red(`šŸ™€ Invalid credentials file format: ${err.message}`), - console.log(""), ); + console.log(""); } else { console.error(chalk.red("šŸ™€ Invalid credentials file format")); console.log(""); } } } -export interface Credentials { - host: string; - clientId: string; - clientSecret: string; - apiKey: string; - cardKey: string; - openaiKey: string; - sandboxKey: string; -} - -export interface BasicOptions { - host: string; - apiKey: string; - clientId: string; - clientSecret: string; - credentialsFile: string; -} -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')); - api = new CardApi( - credentials.clientId, - credentials.clientSecret, - credentials.apiKey, - credentials.host, - ); - } else { - 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 optionCredentials( - options: BasicOptions, - credentials: any, -) { - if (options.credentialsFile) { - credentials = await loadcredentialsFile( - credentials, - options.credentialsFile, - ); - } - if (options.apiKey) { - credentials.apiKey = options.apiKey; - } - if (options.clientId) { - credentials.clientId = options.clientId; - } - if (options.clientSecret) { - credentials.clientSecret = options.clientSecret; - } - if (options.host) { - credentials.host = options.host; - } - return credentials; -} -export async function loadcredentialsFile( - credentials: Credentials, - credentialsFile: string, -) { - if (credentialsFile) { - const file = await import("file://" + credentialsFile, { - with: { type: "json" }, - }); - if (file.host) { - credentials.host = file.host; - } - if (file.apiKey) { - credentials.apiKey = file.apiKey; - } - if (file.clientId) { - credentials.clientId = file.clientId; - } - if (file.clientSecret) { - credentials.clientSecret = file.clientSecret; - } - if (file.openaiKey) { - credentials.openaiKey = file.openaiKey; - } - if (file.sandboxKey) { - credentials.sandboxKey = file.sandboxKey; - } - } - return credentials; -} export const credentials: Credentials = { host: process.env.INVESTEC_HOST || "https://openapi.investec.com", clientId: process.env.INVESTEC_CLIENT_ID || cred.clientId, @@ -187,15 +96,10 @@ export const credentials: Credentials = { openaiKey: process.env.OPENAI_API_KEY || cred.openaiKey, sandboxKey: process.env.SANDBOX_KEY || cred.sandboxKey, }; -async function main() { - program - .name("ipb") - .description("CLI to manage Investec Programmable Banking") - .version(version); - program - .command("cards") - .description("Gets a list of your cards") +// 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( @@ -207,21 +111,28 @@ async function main() { "--credentials-file ", "Set a custom credentials file", ) - .option("-v,--verbose", "additional debugging information") - .action(cardsCommand); + .option("-v,--verbose", "additional debugging information"); +} +// Show help if no arguments are provided +if (process.argv.length <= 2) { + program.outputHelp(); + process.exit(0); +} + +async function main() { program - .command("config") - .description("set auth credentials") - .option("--api-key ", "Sets your api key for the Investec API") - .option( - "--client-id ", - "Sets your client Id for the Investec API", - ) - .option( - "--client-secret ", - "Sets your client secret for the Investec API", - ) + .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 ", @@ -231,48 +142,20 @@ async function main() { "--sandbox-key ", "Sets your sandbox key for the AI generation", ) - .option("-v,--verbose", "additional debugging information") .action(configCommand); - - program - .command("deploy") - .description("deploy code to card") + 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") - .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("-v,--verbose", "additional debugging information") .action(deployCommand); - - program - .command("logs") - .description("fetches logs from the api") + addApiCredentialOptions( + program.command("logs").description("fetches logs from the api"), + ) .requiredOption("-f,--filename ", "the filename") .option("-c,--card-key ", "the cardkey") - .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("-v,--verbose", "additional debugging information") .action(logsCommand); - program .command("run") .description("runs the code locally") @@ -286,120 +169,44 @@ async function main() { .option("-o,--country ", "country code", "ZA") .option("-v,--verbose", "additional debugging information") .action(runCommand); - - program - .command("fetch") - .description("fetches the saved code") + addApiCredentialOptions( + program.command("fetch").description("fetches the saved code"), + ) .requiredOption("-f,--filename ", "the filename") .option("-c,--card-key ", "the cardkey") - .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("-v,--verbose", "additional debugging information") .action(fetchCommand); - - program - .command("upload") - .description("uploads to saved code") + addApiCredentialOptions( + program.command("upload").description("uploads to saved code"), + ) .requiredOption("-f,--filename ", "the filename") .option("-c,--card-key ", "the cardkey") - .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("-v,--verbose", "additional debugging information") .action(uploadCommand); - - program - .command("env") - .description("downloads to env to a local file") + addApiCredentialOptions( + program.command("env").description("downloads to env to a local file"), + ) .requiredOption("-f,--filename ", "the filename") .option("-c,--card-key ", "the cardkey") - .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("-v,--verbose", "additional debugging information") .action(envCommand); - - program - .command("upload-env") - .description("uploads env to the card") + addApiCredentialOptions( + program.command("upload-env").description("uploads env to the card"), + ) .requiredOption("-f,--filename ", "the filename") .option("-c,--card-key ", "the cardkey") - .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("-v,--verbose", "additional debugging information") .action(uploadEnvCommand); - - program - .command("published") - .description("downloads to published code to a local file") + addApiCredentialOptions( + program + .command("published") + .description("downloads to published code to a local file"), + ) .requiredOption("-f,--filename ", "the filename") .option("-c,--card-key ", "the cardkey") - .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("-v,--verbose", "additional debugging information") .action(publishedCommand); - - program - .command("publish") - .description("publishes code to the card") + 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") - .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("-v,--verbose", "additional debugging information") .action(publishCommand); program .command("simulate") @@ -415,93 +222,63 @@ async function main() { .option("-o,--country ", "country code", "ZA") .option("-v,--verbose", "additional debugging information") .action(simulateCommand); - program - .command("enable") - .description("enables code to be used on card") + addApiCredentialOptions( + program.command("enable").description("enables code to be used on card"), + ) .option("-c,--card-key ", "the cardkey") - .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("-v,--verbose", "additional debugging information") .action(enableCommand); - - program - .command("disable") - .description("disables code to be used on card") + addApiCredentialOptions( + program.command("disable").description("disables code to be used on card"), + ) .option("-c,--card-key ", "the cardkey") - .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("-v,--verbose", "additional debugging information") .action(disableCommand); - - program - .command("currencies") - .description("Gets a list of supported currencies") - .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("-v,--verbose", "additional debugging information") - .action(currenciesCommand); - - program - .command("countries") - .description("Gets a list of countries") - .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("-v,--verbose", "additional debugging information") - .action(countriesCommand); - - program - .command("merchants") - .description("Gets a list of merchants") - .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("-v,--verbose", "additional debugging information") - .action(merchantsCommand); - + addApiCredentialOptions( + program + .command("currencies") + .description("Gets a list of supported currencies"), + ).action(currenciesCommand); + addApiCredentialOptions( + program.command("countries").description("Gets a list of countries"), + ).action(countriesCommand); + addApiCredentialOptions( + 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") + .action(accountsCommand); + addApiCredentialOptions( + program.command("balances").description("Gets your account balances"), + ) + .argument("", "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") + .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") + .action(payCommand); + addApiCredentialOptions( + program + .command("transactions") + .description("Gets your account transactions"), + ) + .argument("", "accountId of the account to fetch balances for") + .action(transactionsCommand); + addApiCredentialOptions( + program.command("beneficiaries").description("Gets your beneficiaries"), + ).action(beneficiariesCommand); program .command("new") .description("Sets up scaffoldings for a new project") @@ -514,7 +291,6 @@ async function main() { .choices(["default", "petro"]), ) .action(newCommand); - program .command("ai") .description("Generates card code using an LLM") @@ -523,14 +299,18 @@ async function main() { .option("-v,--verbose", "additional debugging information") .option("--force", "force overwrite existing files") .action(generateCommand); - + program + .command("bank") + .description("Uses the LLM to call your bank") + .argument("", "prompt for the LLM") + .option("-v,--verbose", "additional debugging information") + .action(bankCommand); program .command("register") .description("registers with the server for LLM generation") .option("-e,--email ", "your email") .option("-p,--password ", "your password") .action(registerCommand); - program .command("login") .description("login with the server for LLM generation") @@ -540,31 +320,104 @@ async function main() { try { await program.parseAsync(process.argv); + console.log(""); // Add a newline after command execution } catch (err) { - if (err instanceof Error) { - console.log("šŸ™€ Error encountered: " + chalk.red(err.message)); - console.log(""); - } else { - console.log( - "šŸ™€ Error encountered: " + chalk.red("An unknown error occurred"), - ); - console.log(""); - } + // Use handleCliError with fallback context and options + handleCliError(err, { verbose: true }, "run CLI"); + process.exit(1); } } -export async function checkLatestVersion() { - const response = await fetch("https://registry.npmjs.org/investec-ipb", { - method: "GET", - headers: { - Accept: "application/vnd.npm.install-v1+json", - }, - }); +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; +} - const data = (await response.json()) as { "dist-tags": { latest: string } }; - const latestVersion = data["dist-tags"].latest; +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; +} - return latestVersion; +export async function optionCredentials( + options: BasicOptions, + credentials: any, +) { + if (options.credentialsFile) { + credentials = await loadCredentialsFile( + credentials, + options.credentialsFile, + ); + } + if (options.apiKey) { + credentials.apiKey = options.apiKey; + } + if (options.clientId) { + credentials.clientId = options.clientId; + } + if (options.clientSecret) { + credentials.clientSecret = options.clientSecret; + } + if (options.host) { + credentials.host = options.host; + } + return credentials; } -main(); +main().catch((err) => { + handleCliError(err, { verbose: true }, "run CLI"); + process.exit(1); +}); diff --git a/src/mock-pb.ts b/src/mock-pb.ts new file mode 100644 index 0000000..086b425 --- /dev/null +++ b/src/mock-pb.ts @@ -0,0 +1,208 @@ +import type { + AuthResponse, + AccountResponse, + AccountTransactionResponse, + BeneficiaryResponse, + TransferResponse, + AccountTransaction, + Beneficiary, + Transfer, +} from "investec-pb-api"; + +// Inline types copied from investec-pb-api (not exported) + +export interface IPbApi { + host: string; + clientId: string; + clientSecret: string; + apiKey: string; + token: string; + expiresIn: Date; + + getToken(): Promise; + getAccessToken(): Promise; + getAccountBalances(accountId: string): Promise; + getAccountTransactions( + accountId: string, + ): Promise; + getBeneficiaries(): Promise; + transfer( + accountId: string, + beneficiaryAccountId: string, + amount: number, + reference: string, + ): Promise; +} + +export class PbApi implements IPbApi { + host: string; + clientId: string; + clientSecret: string; + apiKey: string; + token: string; + expiresIn: Date; + constructor( + clientId: string, + clientSecret: string, + apiKey: string, + host?: string, + ) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.apiKey = apiKey; + this.host = host || "https://openapi.investec.com"; + this.token = ""; + this.expiresIn = new Date(); + } + async getToken(): Promise { + return Promise.resolve("MOCK_PB_TOKEN"); + } + async getAccessToken(): Promise { + return Promise.resolve({ + access_token: "MOCK_PB_ACCESS_TOKEN_FOR_TESTING", + token_type: "Bearer", + expires_in: 1799, + scope: "accounts balances transactions beneficiaries transfer pay", + }); + } + async getAccountBalances(accountId: string): Promise { + // Return a single AccountBalance object as expected by the CLI + return Promise.resolve({ + data: { + accountId, + currentBalance: 1000.0, + availableBalance: 900.0, + budgetBalance: 800.0, + straightBalance: 700.0, + cashBalance: 600.0, + currency: "ZAR", + }, + }) as unknown as Promise; + } + async getAccounts(): Promise { + const account = { + accountId: "mock-account-id", + accountNumber: "123456", + accountName: "Mock Account", + referenceName: "Main", + productName: "Cheque", + kycCompliant: true, + profileId: "profile1", + profileName: "Personal", + }; + return Promise.resolve({ + data: { + accounts: [account], + }, + }) as unknown as Promise; + } + async getAccountTransactions( + accountId: string, + ): Promise { + const transaction: AccountTransaction = { + accountId, + type: "credit", + transactionType: "deposit", + status: "posted", + description: "Test Transaction", + cardNumber: null, + postedOrder: 1, + postingDate: new Date().toISOString(), + valueDate: new Date().toISOString(), + actionDate: new Date().toISOString(), + transactionDate: new Date().toISOString(), + amount: 100.0, + runningBalance: 1000.0, + uuid: "t1", + }; + return Promise.resolve({ + data: { + transactions: [transaction], + }, + }); + } + async getBeneficiaries(): Promise { + const beneficiary: Beneficiary = { + beneficiaryId: "b1", + accountNumber: "111111", + code: "INV", + bank: "Investec", + beneficiaryName: "John Doe", + lastPaymentAmount: "100.00", + lastPaymentDate: new Date().toISOString(), + cellNo: "0820000000", + emailAddress: "john@example.com", + name: "John Doe", + referenceAccountNumber: "123456", + referenceName: "Main", + categoryId: "cat1", + profileId: "profile1", + fasterPaymentAllowed: true, + }; + return Promise.resolve({ + data: [beneficiary], + links: { self: "mock" }, + meta: { totalPages: 1 }, + }); + } + async transfer( + accountId: string, + beneficiaryAccountId: string, + amount: number, + reference: string, + ): Promise { + const transfer: Transfer = { + PaymentReferenceNumber: "PRN123", + PaymentDate: new Date().toISOString(), + Status: "success", + BeneficiaryName: "John Doe", + BeneficiaryAccountId: beneficiaryAccountId, + AuthorisationRequired: false, + }; + return Promise.resolve({ + data: { + TransferResponses: [transfer], + }, + }); + } + async payMultiple( + accountId: string, + payments: any[] | any, + ): Promise { + // Mock payMultiple for CLI compatibility + return Promise.resolve({ + data: { + TransferResponses: payments.map((p: any, i: number) => ({ + PaymentReferenceNumber: `PRN${i + 1}`, + PaymentDate: new Date().toISOString(), + Status: "success", + BeneficiaryName: "John Doe", + BeneficiaryAccountId: p.beneficiaryId, + AuthorisationRequired: false, + })), + }, + }); + } + + async transferMultiple( + accountId: string, + transfers: any[] | any, + ): Promise { + // Mock transferMultiple for CLI compatibility + return Promise.resolve({ + data: { + TransferResponses: (Array.isArray(transfers) + ? transfers + : [transfers] + ).map((t: any, i: number) => ({ + PaymentReferenceNumber: `PRN${i + 1}`, + PaymentDate: new Date().toISOString(), + Status: "success", + BeneficiaryName: "John Doe", + BeneficiaryAccountId: t.beneficiaryAccountId, + AuthorisationRequired: false, + })), + }, + }); + } +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..5ced54b --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,105 @@ +import chalk from "chalk"; +import type { Credentials } from "./cmds/types.js"; + +export function handleCliError( + error: any, + options: { verbose?: boolean }, + context: string, +) { + console.error(chalk.redBright(`Failed to ${context}:`), error.message); + console.log(""); + if (options.verbose) { + console.error(error); + } +} + +export async function checkLatestVersion() { + try { + const response = await fetch("https://registry.npmjs.org/investec-ipb", { + method: "GET", + headers: { + Accept: "application/vnd.npm.install-v1+json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch version: ${response.statusText}`); + } + + const data = (await response.json()) as { "dist-tags": { latest: string } }; + const latestVersion = data["dist-tags"].latest; + + return latestVersion; + } catch (error) { + console.warn("Failed to check latest version:", error); + return null; + } +} + +export interface TableRow { + [key: string]: string | number | boolean | null | undefined; +} + +export type TableData = TableRow[]; + +export function printTable(data: TableData): void { + if (!data || data.length === 0) { + console.log("No data to display."); + return; + } + + // Determine column widths based on header and data length + const headers: string[] = Object.keys(data[0] as TableRow); + const colWidths: number[] = headers.map((header) => + Math.max(header.length, ...data.map((row) => String(row[header]).length)), + ); + + // Print header row + const headerRow: string = headers + .map((header, index) => header.padEnd(colWidths[index] ?? 0)) + .join(" | "); + console.log(headerRow); + console.log("-".repeat(headerRow.length)); + + // Print data rows + data.forEach((row) => { + const dataRow: string = headers + .map((header, index) => String(row[header]).padEnd(colWidths[index] ?? 0)) + .join(" | "); + console.log(dataRow); + }); +} + +export async function loadCredentialsFile( + credentials: Credentials, + credentialsFile: string, +) { + if (credentialsFile) { + try { + const file = await import("file://" + credentialsFile, { + with: { type: "json" }, + }); + + // Only copy known credential properties + const credentialKeys: (keyof Credentials)[] = [ + "host", + "apiKey", + "clientId", + "clientSecret", + "openaiKey", + "sandboxKey", + "cardKey", + ]; + + credentialKeys.forEach((key) => { + if (file[key] !== undefined) { + credentials[key] = file[key]; + } + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to load credentials file: ${message}`); + } + } + return credentials; +} diff --git a/tapes/accounts.tape b/tapes/accounts.tape new file mode 100644 index 0000000..611152f --- /dev/null +++ b/tapes/accounts.tape @@ -0,0 +1,6 @@ +Output assets/accounts.gif +Source tapes/config.tape + +Type "ipb accounts" Sleep 500ms Enter + +Sleep 2s \ No newline at end of file diff --git a/tapes/balances.tape b/tapes/balances.tape new file mode 100644 index 0000000..e64c31c --- /dev/null +++ b/tapes/balances.tape @@ -0,0 +1,6 @@ +Output assets/balances.gif +Source tapes/config.tape + +Type "ipb balances mock-account-id" Sleep 500ms Enter + +Sleep 2s diff --git a/tapes/beneficiaries.tape b/tapes/beneficiaries.tape new file mode 100644 index 0000000..88a790a --- /dev/null +++ b/tapes/beneficiaries.tape @@ -0,0 +1,6 @@ +Output assets/beneficiaries.gif +Source tapes/config.tape + +Type "ipb beneficiaries" Sleep 500ms Enter + +Sleep 2s diff --git a/tapes/deploy.tape b/tapes/deploy.tape index e9833d2..cfd95c3 100644 --- a/tapes/deploy.tape +++ b/tapes/deploy.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb deploy -f main.js -e prod -c 1111111" Sleep 500ms Enter -Sleep 15s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/env.tape b/tapes/env.tape index 41fe681..b291de4 100644 --- a/tapes/env.tape +++ b/tapes/env.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb env -f env.json -c 1111111" Sleep 500ms Enter -Sleep 10s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/fetch.tape b/tapes/fetch.tape index ff78a84..f07d07d 100644 --- a/tapes/fetch.tape +++ b/tapes/fetch.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb fetch -f main.js -c 1111111" Sleep 500ms Enter -Sleep 10s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/pay.tape b/tapes/pay.tape new file mode 100644 index 0000000..850f7bd --- /dev/null +++ b/tapes/pay.tape @@ -0,0 +1,6 @@ +Output assets/pay.gif +Source tapes/config.tape + +Type "ipb pay mock-account-id b1 100.00 test-payment" Sleep 500ms Enter + +Sleep 2s diff --git a/tapes/publish.tape b/tapes/publish.tape index 2bf7e54..9da8fc5 100644 --- a/tapes/publish.tape +++ b/tapes/publish.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb publish -f main.js --code-id 2b388c8a-daaf-44f1-bcf9-ca3482d641f3 -c 1111111" Sleep 500ms Enter -Sleep 10s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/published.tape b/tapes/published.tape index a2dd6b2..a09fac6 100644 --- a/tapes/published.tape +++ b/tapes/published.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb published -f published.js -c 1111111" Sleep 500ms Enter -Sleep 10s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/run.tape b/tapes/run.tape index cc70bb5..bada56a 100644 --- a/tapes/run.tape +++ b/tapes/run.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb run -f main.js -e prod --amount 10000 --currency zar --mcc 5933 --merchant 'Second chance' --city 'Cape Town' --country ZA" Sleep 500ms Enter -Sleep 10s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/set.tape b/tapes/set.tape index 81d092a..8dff591 100644 --- a/tapes/set.tape +++ b/tapes/set.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb config --client-id 1234 --client-secret 678776 --api-key 3738373 --card-key 78768" Sleep 500ms Enter -Sleep 10s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/simulate.tape b/tapes/simulate.tape index e5150e4..1e4daa2 100644 --- a/tapes/simulate.tape +++ b/tapes/simulate.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb simulate -f main.js -c 1111111 --amount 10000 --currency zar --mcc 5933 --merchant 'Second chance' --city 'Cape Town' --country ZA" Sleep 500ms Enter -Sleep 10s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/toggle.tape b/tapes/toggle.tape index 3d5dec2..6e40e98 100644 --- a/tapes/toggle.tape +++ b/tapes/toggle.tape @@ -7,4 +7,4 @@ Sleep 5s Type "ipb disable -c 1111111" Sleep 500ms Enter -Sleep 7s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/transactions.tape b/tapes/transactions.tape new file mode 100644 index 0000000..861ecde --- /dev/null +++ b/tapes/transactions.tape @@ -0,0 +1,6 @@ +Output assets/transactions.gif +Source tapes/config.tape + +Type "ipb transactions mock-account-id" Sleep 500ms Enter + +Sleep 2s diff --git a/tapes/transfer.tape b/tapes/transfer.tape new file mode 100644 index 0000000..fc95c03 --- /dev/null +++ b/tapes/transfer.tape @@ -0,0 +1,6 @@ +Output assets/transfer.gif +Source tapes/config.tape + +Type "ipb transfer mock-account-id mock-account-id 100.00 test-transfer" Sleep 500ms Enter + +Sleep 2s diff --git a/tapes/upload-env.tape b/tapes/upload-env.tape index 97afa75..93745c1 100644 --- a/tapes/upload-env.tape +++ b/tapes/upload-env.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb upload-env -f env.json -c 1111111" Sleep 500ms Enter -Sleep 10s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/tapes/upload.tape b/tapes/upload.tape index 8ec6405..69e9838 100644 --- a/tapes/upload.tape +++ b/tapes/upload.tape @@ -3,4 +3,4 @@ Source tapes/config.tape Type "ipb upload -f main.js -c 1111111" Sleep 500ms Enter -Sleep 10s \ No newline at end of file +Sleep 5s \ No newline at end of file diff --git a/test/cmds/cards.test.ts b/test/cmds/cards.test.ts index 0b3b658..7b8b801 100644 --- a/test/cmds/cards.test.ts +++ b/test/cmds/cards.test.ts @@ -48,17 +48,16 @@ describe("cardsCommand", () => { expect(console.log).toHaveBeenCalledWith("šŸ’³ fetching cards"); expect(console.log).toHaveBeenCalledWith(""); expect(console.log).toHaveBeenCalledWith( - "Card Key \tCard Number \t\tCode Enabled", + "CardKey | CardNumber | IsProgrammable", ); expect(console.log).toHaveBeenCalledWith( - chalk.greenBright("123\t\t") + - chalk.blueBright("4567 8901 2345 6789\t\t") + - chalk.redBright("true"), + "----------------------------------------------", ); expect(console.log).toHaveBeenCalledWith( - chalk.greenBright("456\t\t") + - chalk.blueBright("9876 5432 1098 7654\t\t") + - chalk.redBright("false"), + "123 | 4567 8901 2345 6789 | true ", + ); + expect(console.log).toHaveBeenCalledWith( + "456 | 9876 5432 1098 7654 | false ", ); expect(console.log).toHaveBeenCalledWith(""); });