Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
18ef72d
Updated docs to include the .env local environmental variables
devinpearson Mar 4, 2025
612f7ce
Added options for credentials to all commands
devinpearson Mar 5, 2025
54922a2
Updated the credential properties casing to be more consistent
devinpearson Mar 5, 2025
7f9c271
Updated missing credential param casing
devinpearson Mar 5, 2025
08ec7fa
Added inline credential details to readme
devinpearson Mar 5, 2025
a5a6aea
Added a number of improvements for credentials
devinpearson Mar 5, 2025
2997692
added credentials-file option
devinpearson Mar 6, 2025
6301d74
Update README.md
devinpearson Mar 6, 2025
053c0a0
Merge pull request #14 from devinpearson/credential-file
devinpearson Mar 6, 2025
725e2fc
Added details on using custom .credentials file
devinpearson Mar 7, 2025
138c8fa
Added reference functions
devinpearson Mar 7, 2025
19350dc
Corrected copilot suggestions
devinpearson Mar 8, 2025
5cb5958
Merge pull request #19 from devinpearson/references
devinpearson Mar 8, 2025
1e51679
updated tapes and added script to generate them
devinpearson Mar 8, 2025
34c8caf
Added reference functions to README
devinpearson Mar 8, 2025
8f82705
Added the online simulate command
devinpearson Mar 17, 2025
d69b0d4
Added tape for simulate and corrected readme
devinpearson Mar 17, 2025
7776cbc
Corrected response from simulator as docs are incorrect
devinpearson Mar 17, 2025
3d86267
Refactored api and reduced duplicate code
devinpearson Mar 17, 2025
306cf79
fixed disable command output and recreated gifs
devinpearson Mar 18, 2025
344e565
Added additional error handling per command
devinpearson Mar 18, 2025
8577c2f
New command (#29)
devinpearson Mar 26, 2025
0a0e1fa
Updated package version
devinpearson Mar 26, 2025
62a234d
Merge branch 'dev' of https://github.com/devinpearson/ipb into dev
devinpearson Mar 26, 2025
e9b5f7a
Updated new command
devinpearson Mar 26, 2025
b802981
Merge branch 'main' into dev
devinpearson Mar 26, 2025
6baddf1
Updated output for new command and added code review improvements
devinpearson Mar 26, 2025
83fb92a
Merge branch 'dev' of https://github.com/devinpearson/ipb into dev
devinpearson Mar 26, 2025
d44b482
cleaned up error handling
devinpearson Mar 26, 2025
3a437d7
Merge branch 'main' into dev
devinpearson Apr 17, 2025
2005e0c
release: 0.9.0 — fetch, pay/transfer amounts, tapes (#39)
devinpearson Aug 3, 2026
b127007
Merge branch 'main' into dev
devinpearson Aug 3, 2026
45053bc
ci: bump GitHub Actions to Node 24 runtimes
devinpearson Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
Expand All @@ -40,9 +40,9 @@ jobs:
packaging-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Use Node.js 24.x
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 24.x
cache: "npm"
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Use Node.js 24.x
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 24.x
registry-url: "https://registry.npmjs.org/"
package-manager-cache: false
- run: npm ci
- run: npm run build --if-present
- name: Verify package contents
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 24.x

Expand Down Expand Up @@ -108,7 +108,7 @@ jobs:
sha256sum ${{ matrix.binary-name }} > ${{ matrix.binary-name }}.sha256

- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.binary-name }}
path: |
Expand All @@ -124,10 +124,10 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
path: artifacts

Expand Down Expand Up @@ -246,7 +246,7 @@ jobs:
VERSION: ${{ steps.tag_version.outputs.version }}

- name: Create GitHub Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v3
with:
files: release-assets/*
name: Release v${{ steps.tag_version.outputs.version }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/snap-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Node.js 24.x
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 24.x
cache: npm
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
echo "Snap file: $SNAP_FILE"

- name: Upload snap artifact to GitHub Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v3
with:
files: ${{ steps.snap_file.outputs.file }}
env:
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ User-facing README download examples should use the same version as `package.jso

```sh
IPB_NO_UPDATE_CHECK=1 npm run tapes
# runs scripts/tapes.sh — requires VHS and a built CLI
# builds, npm links local ipb, then runs scripts/tapes.sh — requires VHS
```

Regenerate assets under `assets/` when UX of recorded commands changes meaningfully.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,28 +137,28 @@ macOS:

```sh
# Apple Silicon
curl -L https://github.com/devinpearson/ipb/releases/download/v0.8.4/ipb-macos-arm64 -o ipb
curl -L https://github.com/devinpearson/ipb/releases/download/v0.9.0/ipb-macos-arm64 -o ipb
chmod +x ipb
sudo mv ipb /usr/local/bin/

# Intel
curl -L https://github.com/devinpearson/ipb/releases/download/v0.8.4/ipb-macos-x64 -o ipb
curl -L https://github.com/devinpearson/ipb/releases/download/v0.9.0/ipb-macos-x64 -o ipb
chmod +x ipb
sudo mv ipb /usr/local/bin/
```

Linux (.deb):

```sh
wget https://github.com/devinpearson/ipb/releases/download/v0.8.4/ipb_0.8.4_amd64.deb
sudo dpkg -i ipb_0.8.4_amd64.deb
wget https://github.com/devinpearson/ipb/releases/download/v0.9.0/ipb_0.9.0_amd64.deb
sudo dpkg -i ipb_0.9.0_amd64.deb
sudo apt-get install -f
```

Linux binary:

```sh
curl -L https://github.com/devinpearson/ipb/releases/download/v0.8.4/ipb-linux-x64 -o ipb
curl -L https://github.com/devinpearson/ipb/releases/download/v0.9.0/ipb-linux-x64 -o ipb
# or: ipb-linux-arm64
chmod +x ipb
sudo mv ipb /usr/local/bin/
Expand Down
Binary file modified assets/accounts.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/balances.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/beneficiaries.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/cards.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/deploy.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/env.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/fetch.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/logs.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/new.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/pay.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/publish.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/published.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/run.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/simulate.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/toggle.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/transactions.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/transfer.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/upload-env.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/upload.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "investec-ipb",
"version": "0.8.4",
"version": "0.9.0",
"main": "bin/index.js",
"bin": {
"ipb": "./bin/index.js"
Expand All @@ -9,7 +9,7 @@
"scripts": {
"build": "npm run clean && tsc && npm run copy-files",
"clean": "rimraf ./bin",
"copy-files": "cp -r ./templates/ ./bin/templates/ && cp -r ./assets/ ./bin/assets/ && cp instructions.txt ./bin/instructions.txt",
"copy-files": "cp -r ./templates/ ./bin/templates/ && cp -r ./assets/ ./bin/assets/ && cp instructions.txt ./bin/instructions.txt && chmod +x ./bin/index.js",
"test": "vitest",
"test:run": "vitest --run",
"lint": "biome check .",
Expand All @@ -23,7 +23,7 @@
"ci": "npm run build && npm run type-check && npm run lint && npm run lint:md && npm run format:check && npm run test:run && npm audit",
"verify:ci": "npm run build && npm run test:run && npm run bundle",
"dev": "vitest",
"tapes": "./scripts/tapes.sh",
"tapes": "npm run build && chmod +x ./bin/index.js && npm link && ./scripts/tapes.sh",
"docs": "npm run build && node bin/index.js docs",
"docs:source": "node --loader tsx src/cmds/docs.ts",
"prepublishOnly": "npm run build && npm run docs",
Expand Down
67 changes: 48 additions & 19 deletions scripts/tapes.sh
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail

# Ctrl+C / SIGTERM should stop the whole suite, not just the current vhs.
trap 'printf "\nInterrupted — stopping tape generation.\n" >&2; exit 130' INT TERM

export DEBUG=true
vhs tapes/cards.tape
vhs tapes/deploy.tape
vhs tapes/env.tape
vhs tapes/fetch.tape
vhs tapes/logs.tape
vhs tapes/new.tape
vhs tapes/publish.tape
vhs tapes/published.tape
vhs tapes/run.tape
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
export IPB_NO_UPDATE_CHECK="${IPB_NO_UPDATE_CHECK:-1}"

# npm link points the global `ipb` at ./bin/index.js; tsc emits without +x.
if [[ ! -x ./bin/index.js ]]; then
chmod +x ./bin/index.js
fi
if ! command -v ipb >/dev/null 2>&1; then
printf 'ipb is not on PATH. Run npm link after building.\n' >&2
exit 1
fi
if ! ipb --version >/dev/null 2>&1; then
printf 'Linked ipb is not executable (permission denied). Fixing ./bin/index.js and retrying.\n' >&2
chmod +x ./bin/index.js
ipb --version >/dev/null
fi

tapes=(
cards
deploy
env
fetch
logs
new
publish
published
run
simulate
toggle
upload-env
upload
accounts
beneficiaries
balances
transactions
pay
transfer
)

for name in "${tapes[@]}"; do
printf 'Recording tapes/%s.tape...\n' "$name"
# Run in foreground process group so SIGINT reaches vhs.
vhs "tapes/${name}.tape"
done
24 changes: 3 additions & 21 deletions src/cmds/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface Options extends CommonOptions {
/**
* Fetches saved code from a card and saves it to a file.
* @param options - CLI options including card key, filename, and API credentials
* @throws {CliError} When card key is missing, API doesn't support fetching, or file operations fail
* @throws {CliError} When card key is missing, API response is unexpected, or file operations fail
*/
export async function fetchCommand(options: Options) {
const cardKey = normalizeCardKey(options.cardKey, credentials.cardKey);
Expand All @@ -36,17 +36,7 @@ export async function fetchCommand(options: Options) {
let normalizedFilename = '';
await withSpinner(spinner, spinnerEnabled, async () => {
const api = await initializeApi(credentials, options);

// The api object may not have a getCode method; use getSavedCode if available, or handle gracefully
// biome-ignore lint/suspicious/noExplicitAny: API interface may not include all methods
if (typeof (api as any).getSavedCode !== 'function') {
throw new CliError(
ERROR_CODES.UNSUPPORTED_OPERATION,
'API client does not support fetching saved code (getSavedCode missing)'
);
}
// biome-ignore lint/suspicious/noExplicitAny: API interface may not include all methods
const result = await (api as any).getSavedCode(cardKey);
const result = await api.getCode(cardKey);

if (
!result ||
Expand All @@ -60,15 +50,7 @@ export async function fetchCommand(options: Options) {
);
}

const fetchedCode = result.data.result.code;
if (typeof fetchedCode !== 'string') {
throw new CliError(
ERROR_CODES.INVESTEC_API_ERROR,
'Failed to fetch code: Unexpected API response'
);
}

code = fetchedCode;
code = result.data.result.code;
normalizedFilename = await validateFilePathForWrite(options.filename, ['.js']);
});

Expand Down
8 changes: 4 additions & 4 deletions src/cmds/pay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type { CommonOptions } from './types.js';
export async function payCommand(
accountId: string,
beneficiaryId: string,
amount: number,
amount: number | string,
reference: string,
options: CommonOptions
) {
Expand All @@ -45,11 +45,11 @@ export async function payCommand(
throw new CliError(ERROR_CODES.INVALID_INPUT, 'Beneficiary ID is required');
}

// Commander passes args as strings; coerce before validation/formatting.
if (!amount) {
const amt = await input({ message: 'Enter amount (in rands):' });
amount = parseFloat(amt);
amount = await input({ message: 'Enter amount (in rands):' });
}
validateAmount(amount);
amount = validateAmount(amount);

if (!reference) {
reference = await input({ message: 'Enter reference for the payment:' });
Expand Down
8 changes: 4 additions & 4 deletions src/cmds/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type { CommonOptions } from './types.js';
export async function transferCommand(
accountId: string,
beneficiaryAccountId: string,
amount: number,
amount: number | string,
reference: string,
options: CommonOptions
) {
Expand All @@ -42,11 +42,11 @@ export async function transferCommand(
}
validateAccountId(beneficiaryAccountId);

// Commander passes args as strings; coerce before validation/formatting.
if (!amount) {
const amt = await input({ message: 'Enter amount (in rands):' });
amount = parseFloat(amt);
amount = await input({ message: 'Enter amount (in rands):' });
}
validateAmount(amount);
amount = validateAmount(amount);

if (!reference) {
reference = await input({ message: 'Enter reference for the transfer:' });
Expand Down
25 changes: 20 additions & 5 deletions src/utils/input-validation.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
import { CliError, ERROR_CODES } from '../errors.js';

export function validateAmount(amount: number, maxDecimals = 2): void {
if (Number.isNaN(amount)) {
/**
* Validates a monetary amount and returns it as a number.
* Accepts CLI string args (Commander always passes strings) or numbers.
* @param amount - Amount in rands
* @param maxDecimals - Maximum allowed fractional digits
* @returns Parsed finite positive amount
*/
export function validateAmount(amount: number | string, maxDecimals = 2): number {
if (typeof amount === 'string' && amount.trim() === '') {
throw new CliError(ERROR_CODES.INVALID_INPUT, 'Amount must be a valid number');
}

if (amount <= 0) {
const value = typeof amount === 'string' ? Number(amount.trim()) : Number(amount);

if (Number.isNaN(value)) {
throw new CliError(ERROR_CODES.INVALID_INPUT, 'Amount must be a valid number');
}

if (value <= 0) {
throw new CliError(ERROR_CODES.INVALID_INPUT, 'Amount must be positive');
}

if (!Number.isFinite(amount)) {
if (!Number.isFinite(value)) {
throw new CliError(ERROR_CODES.INVALID_INPUT, 'Amount must be a finite number');
}

const decimalPlaces = (amount.toString().split('.')[1] || '').length;
const decimalPlaces = (value.toString().split('.')[1] || '').length;
if (decimalPlaces > maxDecimals) {
throw new CliError(
ERROR_CODES.INVALID_INPUT,
`Amount can have at most ${maxDecimals} decimal place${maxDecimals === 1 ? '' : 's'}. Found ${decimalPlaces} decimal place${decimalPlaces === 1 ? '' : 's'}.`
);
}

return value;
}

export function validateAccountId(accountId: string): void {
Expand Down
Loading