@progyai/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @progyai/cli
2
+
3
+ Customer-side CLI for [progy](https://github.com/whatl3y/progy) — the permissionless AI proxy. Works against any progy deployment without cloning the server.
4
+
5
+ ## First-time setup (2 commands)
6
+
7
+ ```bash
8
+ npx @progyai/cli init # mint a key, save to ~/.progy/credentials
9
+ npx @progyai/cli topup --amount 10 # fund it
10
+ ```
11
+
12
+ That's it. `progy balance`, `progy usage`, and provider requests through the proxy now Just Work because the saved key is picked up automatically.
13
+
14
+ ## Install
15
+
16
+ Use ad-hoc with `npx` (recommended — always picks up the latest):
17
+
18
+ ```bash
19
+ npx @progyai/cli <command>
20
+ ```
21
+
22
+ Or install globally if you use it often:
23
+
24
+ ```bash
25
+ npm i -g @progyai/cli
26
+ progy <command>
27
+ ```
28
+
29
+ > The x402 rail pulls in on-chain signing libraries (`viem` for EVM, `@solana/kit` for Solana) via the `@x402/*` packages, so the install footprint is larger than a typical CLI. This is required to sign payments locally without ever sending your key to the server.
30
+
31
+ ## Configuration
32
+
33
+ The CLI resolves the URL and API key from (most-explicit wins):
34
+
35
+ 1. CLI flags (`--url`, `--key`)
36
+ 2. Env vars (`PROGY_URL`, `PROGY_API_KEY`)
37
+ 3. `~/.progy/credentials` for the active profile (written by `progy init`)
38
+ 4. Default URL `https://x.progy.ai` (key has no default)
39
+
40
+ The credentials file is JSON with one entry per profile (`{ default: {...}, staging: {...}, ... }`), POSIX perms `0600` on the file and `0700` on the directory. Safe to delete at any time — `progy init` will recreate it.
41
+
42
+ | Env | Flag | What |
43
+ |---|---|---|
44
+ | `PROGY_URL` | `--url` | the progy instance to talk to (default `https://x.progy.ai`) |
45
+ | `PROGY_API_KEY` | `--key` | your `progy_sk_…` customer key |
46
+ | `PROGY_PROFILE` | `--profile` | credentials profile to use (default `default`) |
47
+ | `PROGY_TOPUP_PM_ID` | `--pm` | (topup) Stripe PaymentMethod id, e.g. `pm_card_visa` in test mode |
48
+ | `PROGY_EVM_PRIVATE_KEY` | — | (topup x402) EVM wallet private key (`0x…`) that signs the on-chain payment |
49
+ | `PROGY_SVM_PRIVATE_KEY` | — | (topup x402) Solana wallet secret key (base58) that signs the on-chain payment |
50
+ | `PROGY_SVM_RPC_URL` | — | (topup x402) optional Solana RPC URL (defaults to a public RPC per network) |
51
+
52
+ ### Multi-environment usage
53
+
54
+ The `--profile` flag (or `PROGY_PROFILE` env) lets a single user juggle several progy deployments — e.g. local + staging + prod — without re-running `init` each time:
55
+
56
+ ```bash
57
+ progy init --profile prod --url https://progy.example.com
58
+ progy init --profile staging --url https://staging.progy.example.com
59
+
60
+ progy balance --profile prod
61
+ PROGY_PROFILE=staging progy whoami
62
+ ```
63
+
64
+ Profile names must be lowercase ASCII with optional dashes (e.g. `prod-eu-west-1`). The `default` profile is what's used when no profile is specified.
65
+
66
+ ## Commands
67
+
68
+ ### Setup
69
+
70
+ #### `progy init` *(alias: `progy keygen`)*
71
+
72
+ Mint a fresh progy API key and save it to `~/.progy/credentials`. The new key has zero balance — top it up before making provider requests.
73
+
74
+ Idempotent: if a key is already configured (env or file), prints what's there and exits 0. Pass `--force` to mint a new one anyway (which replaces the saved one).
75
+
76
+ ```bash
77
+ npx @progyai/cli init # local default
78
+ npx @progyai/cli init --url https://progy.example.com
79
+ npx @progyai/cli init --force # mint a new key over the saved one
80
+ ```
81
+
82
+ #### `progy whoami`
83
+
84
+ Show which account the CLI is configured to use and where the credentials came from (flag, env, or file). Best-effort pings `/v1/balance` for account_id and balance; falls back to local-only output if the proxy is unreachable.
85
+
86
+ ```bash
87
+ npx @progyai/cli whoami
88
+ ```
89
+
90
+ #### `progy logout`
91
+
92
+ Delete saved credentials. With no flags, removes the default profile (and the whole file if no other profiles remain); with `--profile <name>`, removes just that one profile and leaves siblings intact. Exit 0 whether or not the profile existed (safe in cleanup scripts). The server-side account is untouched — only the local copy of the key is removed.
93
+
94
+ ```bash
95
+ npx @progyai/cli logout
96
+ npx @progyai/cli logout --profile staging # remove just that one profile
97
+ ```
98
+
99
+ #### `progy config`
100
+
101
+ View or update the saved credentials for a profile. Unlike `init --force` (which mints a brand-new key), this **merges** — so you can repoint the URL while keeping your existing key, or swap the key while keeping the URL.
102
+
103
+ ```bash
104
+ npx @progyai/cli config # show saved url + key prefix
105
+ npx @progyai/cli config --url https://x.progy.ai # repoint the saved profile (keeps the key)
106
+ npx @progyai/cli config --key progy_sk_… # replace the saved key (keeps the url)
107
+ ```
108
+
109
+ ### Account
110
+
111
+ #### `progy topup --amount <usd>`
112
+
113
+ Credit your balance via either rail. `--rail` is auto-picked from what the server advertises (and whether you supplied a Stripe PM); pass it explicitly to force one.
114
+
115
+ **Stripe (card)** — requires a Stripe PaymentMethod id:
116
+
117
+ ```bash
118
+ PROGY_TOPUP_PM_ID=pm_… \
119
+ npx @progyai/cli topup --amount 10 --rail stripe
120
+ ```
121
+
122
+ **x402 (on-chain USDC)** — signs the payment locally with your wallet and settles via the proxy's facilitator. Works on EVM chains (Base, Polygon, …) and Solana. Pick the chain with `--network`:
123
+
124
+ ```bash
125
+ # EVM (Base) — needs an EVM private key
126
+ PROGY_EVM_PRIVATE_KEY=0x… \
127
+ npx @progyai/cli topup --amount 10 --rail x402 --network base
128
+
129
+ # Solana — needs a base58 secret key (and optionally a dedicated RPC)
130
+ PROGY_SVM_PRIVATE_KEY=… \
131
+ PROGY_SVM_RPC_URL=https://… \
132
+ npx @progyai/cli topup --amount 10 --rail x402 --network solana
133
+ ```
134
+
135
+ If the wallet key env var isn't set, the CLI prompts for it securely (hidden input) — nothing is written to disk. The key signs an EIP-3009 authorization (EVM) or an SPL-USDC transfer (Solana); the proxy never sees your private key. Built on the x402 v2 protocol (`PAYMENT-SIGNATURE` header) via the `@x402/*` packages.
136
+
137
+ #### `progy balance`
138
+
139
+ Show your current balance and the last 20 ledger entries.
140
+
141
+ #### `progy usage [--since <Nd|Nh|Nm>]`
142
+
143
+ Show your last 50 per-request usage records, optionally filtered to a recent window (e.g. `--since 7d`, `--since 3h`).
144
+
145
+ ### `progy --help` / `progy <command> --help`
146
+
147
+ Print the dispatch help, or per-command help.
148
+
149
+ ## Versioning
150
+
151
+ This CLI is **decoupled from the progy server** — it talks to whatever progy instance `PROGY_URL` points at, over the documented HTTP contract.
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.balanceCommand = balanceCommand;
4
+ const http_1 = require("../lib/http");
5
+ const format_1 = require("../lib/format");
6
+ async function balanceCommand(flags) {
7
+ const ctx = (0, http_1.resolveClient)(flags);
8
+ const body = (await (0, http_1.progyFetch)(ctx, '/v1/balance'));
9
+ console.log(`Account: #${body.account_id}${body.wallet_address ? ` (${body.wallet_address})` : ''}`);
10
+ console.log(`Balance: ${(0, format_1.usd)(body.balance_usd)}`);
11
+ const rows = body.recent_ledger.map((e) => [
12
+ (0, format_1.ts)(e.created_at),
13
+ e.type,
14
+ (0, format_1.usd)(e.amount_usd, true),
15
+ `→ ${(0, format_1.usd)(e.balance_after_usd)}`,
16
+ (0, format_1.shortId)(e.request_id),
17
+ ]);
18
+ if (rows.length === 0) {
19
+ console.log('\nNo ledger activity yet.');
20
+ return;
21
+ }
22
+ console.log('\nRecent activity:');
23
+ console.log((0, format_1.table)(rows, ['when', 'type', 'amount', 'balance after', 'request']));
24
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configCommand = configCommand;
4
+ const flags_1 = require("../lib/flags");
5
+ const http_1 = require("../lib/http");
6
+ const credentials_1 = require("../lib/credentials");
7
+ /**
8
+ * View or update the saved credentials for a profile.
9
+ *
10
+ * progy config # print the saved url + key prefix
11
+ * progy config --url https://x.progy.ai # repoint the saved profile (keeps the key)
12
+ * progy config --key progy_sk_… # replace the saved key (keeps the url)
13
+ *
14
+ * Updates are merged into the profile, so changing the url leaves the api_key
15
+ * intact and vice versa — unlike `init --force`, which mints a brand-new key.
16
+ */
17
+ async function configCommand(flags) {
18
+ const profile = (0, http_1.resolveProfile)(flags);
19
+ const profileSuffix = profile === credentials_1.DEFAULT_PROFILE ? '' : ` --profile ${profile}`;
20
+ // `--url` / `--key` double as the connection flags; here they mean "save this".
21
+ const newUrl = (0, flags_1.readFlag)(flags, 'url');
22
+ const newKey = (0, flags_1.readFlag)(flags, 'key');
23
+ // No setters → just show the current saved config.
24
+ if (!newUrl && !newKey) {
25
+ const saved = (0, credentials_1.loadCredentials)(profile);
26
+ if (!saved || (!saved.url && !saved.api_key)) {
27
+ console.log(`No saved credentials for profile "${profile}".`);
28
+ console.log(`Mint one with: progy init${profileSuffix}`);
29
+ return;
30
+ }
31
+ console.log(`Profile: ${profile}`);
32
+ console.log(`File: ${(0, credentials_1.credentialsPath)()}`);
33
+ if (saved.url)
34
+ console.log(`URL: ${saved.url}`);
35
+ if (saved.api_key)
36
+ console.log(`Key: ${saved.api_key.slice(0, 18)}…`);
37
+ console.log(`\nUpdate with: progy config${profileSuffix} --url <url> [--key <key>]`);
38
+ return;
39
+ }
40
+ const update = {};
41
+ if (newUrl)
42
+ update.url = newUrl.replace(/\/$/, '');
43
+ if (newKey)
44
+ update.api_key = newKey;
45
+ (0, credentials_1.saveCredentials)(update, profile);
46
+ const saved = (0, credentials_1.loadCredentials)(profile);
47
+ console.log(`Updated profile "${profile}" at ${(0, credentials_1.credentialsPath)()}:`);
48
+ if (update.url)
49
+ console.log(` URL: ${saved?.url}`);
50
+ if (update.api_key)
51
+ console.log(` Key: ${saved?.api_key?.slice(0, 18)}…`);
52
+ if (update.url && !saved?.api_key) {
53
+ console.log(`\nNote: no key is saved for this profile yet — run \`progy init${profileSuffix}\` or set one with --key.`);
54
+ }
55
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initCommand = initCommand;
4
+ const flags_1 = require("../lib/flags");
5
+ const http_1 = require("../lib/http");
6
+ const credentials_1 = require("../lib/credentials");
7
+ async function initCommand(flags) {
8
+ const force = flags.force === true;
9
+ const profile = (0, http_1.resolveProfile)(flags);
10
+ const profileSuffix = profile === credentials_1.DEFAULT_PROFILE ? '' : ` --profile ${profile}`;
11
+ const url = ((0, flags_1.readFlag)(flags, 'url', 'PROGY_URL') || 'https://x.progy.ai').replace(/\/$/, '');
12
+ if (!force) {
13
+ // Idempotent check — env wins over file (both signal "you already have a key").
14
+ const envKey = process.env.PROGY_API_KEY;
15
+ const saved = (0, credentials_1.loadCredentials)(profile);
16
+ if (envKey) {
17
+ const prefix = envKey.slice(0, 18);
18
+ console.log(`You already have a progy API key configured via PROGY_API_KEY (prefix: ${prefix}).`);
19
+ console.log(`To mint a new one anyway: progy init${profileSuffix} --force`);
20
+ return;
21
+ }
22
+ if (saved?.api_key) {
23
+ const prefix = saved.api_key.slice(0, 18);
24
+ console.log(`You already have a progy account saved at ${(0, credentials_1.credentialsPath)()} (profile: ${profile}).`);
25
+ console.log(` prefix: ${prefix}`);
26
+ if (saved.url)
27
+ console.log(` url: ${saved.url}`);
28
+ console.log(`\nTo mint a new one anyway: progy init${profileSuffix} --force`);
29
+ return;
30
+ }
31
+ }
32
+ // The mint endpoint is unauthenticated by design — no key required to call
33
+ // it (this is how you get one). Pass an empty `_` key to bypass the
34
+ // resolveClient guard; the bearer header gets ignored server-side for this
35
+ // public route, and a stub value keeps the fetch wrapper happy.
36
+ const body = (await (0, http_1.progyFetch)({ url, key: 'unauthenticated' }, '/v1/accounts', { method: 'POST', body: {} }));
37
+ (0, credentials_1.saveCredentials)({ url, api_key: body.api_key }, profile);
38
+ console.log('Created progy account.');
39
+ console.log('');
40
+ console.log(`Your API key (also saved to ${(0, credentials_1.credentialsPath)()} under profile "${profile}"):`);
41
+ console.log('');
42
+ console.log(` ${body.api_key}`);
43
+ console.log('');
44
+ console.log(`Account #${body.account_id} · prefix ${body.prefix} · balance $${body.balance_usd}`);
45
+ console.log('');
46
+ console.log('Next steps:');
47
+ console.log(` progy topup${profileSuffix} --amount 10 # fund the new key`);
48
+ console.log(` progy balance${profileSuffix} # confirm the balance`);
49
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logoutCommand = logoutCommand;
4
+ const http_1 = require("../lib/http");
5
+ const credentials_1 = require("../lib/credentials");
6
+ async function logoutCommand(flags) {
7
+ const profile = (0, http_1.resolveProfile)(flags);
8
+ const removed = (0, credentials_1.deleteCredentials)(profile);
9
+ const target = profile === credentials_1.DEFAULT_PROFILE
10
+ ? `default profile at ${(0, credentials_1.credentialsPath)()}`
11
+ : `profile "${profile}" at ${(0, credentials_1.credentialsPath)()}`;
12
+ if (removed) {
13
+ console.log(`Removed ${target}.`);
14
+ }
15
+ else {
16
+ console.log(`Nothing to remove for ${target}.`);
17
+ }
18
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.topupCommand = topupCommand;
4
+ const flags_1 = require("../lib/flags");
5
+ const http_1 = require("../lib/http");
6
+ const x402_1 = require("../lib/x402");
7
+ const format_1 = require("../lib/format");
8
+ function b64(s) {
9
+ return Buffer.from(s).toString('base64');
10
+ }
11
+ async function topupCommand(flags) {
12
+ const ctx = (0, http_1.resolveClient)(flags);
13
+ const amount = Number((0, flags_1.readFlag)(flags, 'amount'));
14
+ if (!Number.isFinite(amount) || amount <= 0)
15
+ throw new Error('missing/invalid --amount (USD)');
16
+ const rail = String((0, flags_1.readFlag)(flags, 'rail') ?? '').toLowerCase();
17
+ const network = (0, flags_1.readFlag)(flags, 'network');
18
+ const pm = (0, flags_1.readFlag)(flags, 'pm', 'PROGY_TOPUP_PM_ID');
19
+ // 1) Combined challenge — learns which rails the server has configured.
20
+ console.log(`Requesting challenge for ${(0, format_1.usd)(amount)} from ${ctx.url}/v1/topup …`);
21
+ const challenge = await (0, http_1.progyFetch)(ctx, '/v1/topup', {
22
+ method: 'POST',
23
+ body: { amountUsd: amount },
24
+ }).catch((err) => {
25
+ if (err.status === 402)
26
+ return err.body;
27
+ throw err;
28
+ });
29
+ const accepts = (challenge.accepts ?? []);
30
+ const railNames = accepts.map((a) => a.scheme).filter(Boolean);
31
+ const hasStripe = accepts.some((a) => a.scheme === 'stripe');
32
+ const x402Nets = (0, x402_1.offeredNetworks)(accepts);
33
+ console.log(`Configured rails: ${railNames.join(', ') || '(none)'}`);
34
+ if (x402Nets.length)
35
+ console.log(`x402 networks: ${x402Nets.join(', ')}`);
36
+ // 2) Pick the rail. Explicit --rail wins; otherwise prefer stripe when a PM is
37
+ // available, else fall back to x402 when an on-chain network is offered.
38
+ let chosen = rail;
39
+ if (!chosen) {
40
+ if (hasStripe && pm)
41
+ chosen = 'stripe';
42
+ else if (x402Nets.length)
43
+ chosen = 'x402';
44
+ else if (hasStripe)
45
+ chosen = 'stripe';
46
+ else
47
+ chosen = '';
48
+ }
49
+ if (chosen === 'x402') {
50
+ if (x402Nets.length === 0) {
51
+ throw new Error('x402 rail not advertised by the server for this top-up');
52
+ }
53
+ const result = await (0, x402_1.topupViaX402)(ctx, amount, { network });
54
+ console.log(`\n✓ Top-up settled on ${result.network}. New balance: ${(0, format_1.usd)(result.balanceUsd)}\n`);
55
+ return;
56
+ }
57
+ if (chosen !== 'stripe' || !hasStripe) {
58
+ throw new Error(`no usable rail (auto-picked "${chosen || 'none'}"). Configured: [${railNames.join(', ')}]. ` +
59
+ 'For x402 set a wallet key (PROGY_EVM_PRIVATE_KEY / PROGY_SVM_PRIVATE_KEY) and pass --rail x402; ' +
60
+ 'for stripe set STRIPE_SECRET_KEY + STRIPE_PUBLISHABLE_KEY on the proxy and provide --pm.');
61
+ }
62
+ if (!pm) {
63
+ throw new Error('missing PROGY_TOPUP_PM_ID (or --pm) — a saved Stripe PaymentMethod id ' +
64
+ '(e.g. pm_card_visa for test mode, or a real pm_… from the Stripe dashboard).');
65
+ }
66
+ // Stripe settle: base64-encoded {paymentMethodId, topUpAmountUsd} on a `payment` header.
67
+ const paymentHeader = b64(JSON.stringify({ paymentMethodId: pm, topUpAmountUsd: amount }));
68
+ console.log(`Charging ${(0, format_1.usd)(amount)} via Stripe (pm=${pm}) …`);
69
+ const result = await (0, http_1.progyFetch)(ctx, '/v1/topup', {
70
+ method: 'POST',
71
+ body: { amountUsd: amount },
72
+ extraHeaders: { payment: paymentHeader },
73
+ });
74
+ console.log(`\n✓ Top-up settled. New balance: ${(0, format_1.usd)(result?.balance_usd)}\n`);
75
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usageCommand = usageCommand;
4
+ const flags_1 = require("../lib/flags");
5
+ const http_1 = require("../lib/http");
6
+ const format_1 = require("../lib/format");
7
+ async function usageCommand(flags) {
8
+ const ctx = (0, http_1.resolveClient)(flags);
9
+ const sinceMs = (0, format_1.parseDuration)((0, flags_1.readFlag)(flags, 'since'));
10
+ const body = (await (0, http_1.progyFetch)(ctx, '/v1/usage'));
11
+ let records = body.usage;
12
+ if (sinceMs !== undefined) {
13
+ const threshold = Date.now() - sinceMs;
14
+ records = records.filter((r) => new Date(r.created_at).getTime() >= threshold);
15
+ }
16
+ console.log(`Account: #${body.account_id} (${records.length} record${records.length === 1 ? '' : 's'})`);
17
+ if (records.length === 0)
18
+ return;
19
+ const rows = records.map((r) => [
20
+ (0, format_1.ts)(r.created_at),
21
+ r.provider,
22
+ r.model,
23
+ `${r.input_tokens}→${r.output_tokens}`,
24
+ (0, format_1.usd)(r.cost_usd),
25
+ (0, format_1.shortId)(r.request_id),
26
+ ]);
27
+ console.log();
28
+ console.log((0, format_1.table)(rows, ['when', 'provider', 'model', 'tokens (in→out)', 'cost', 'request']));
29
+ const total = records.reduce((sum, r) => sum + Number(r.cost_usd || 0), 0);
30
+ console.log(`\nTotal cost: ${(0, format_1.usd)(total)}`);
31
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.whoamiCommand = whoamiCommand;
4
+ const flags_1 = require("../lib/flags");
5
+ const http_1 = require("../lib/http");
6
+ const credentials_1 = require("../lib/credentials");
7
+ async function whoamiCommand(flags) {
8
+ const profile = (0, http_1.resolveProfile)(flags);
9
+ const profileSuffix = profile === credentials_1.DEFAULT_PROFILE ? '' : ` --profile ${profile}`;
10
+ const flagKey = (0, flags_1.readFlag)(flags, 'key', undefined);
11
+ const envKey = process.env.PROGY_API_KEY;
12
+ const saved = (0, credentials_1.loadCredentials)(profile);
13
+ let source;
14
+ let key;
15
+ if (flagKey) {
16
+ source = '--key flag';
17
+ key = flagKey;
18
+ }
19
+ else if (envKey) {
20
+ source = 'PROGY_API_KEY env';
21
+ key = envKey;
22
+ }
23
+ else if (saved?.api_key) {
24
+ source = `${(0, credentials_1.credentialsPath)()} [profile: ${profile}]`;
25
+ key = saved.api_key;
26
+ }
27
+ else {
28
+ source = '(none)';
29
+ }
30
+ const url = ((0, flags_1.readFlag)(flags, 'url', 'PROGY_URL') ||
31
+ saved?.url ||
32
+ 'https://x.progy.ai').replace(/\/$/, '');
33
+ if (!key) {
34
+ console.log(`No progy API key is configured for profile "${profile}".`);
35
+ console.log(`URL would resolve to: ${url}`);
36
+ const others = (0, credentials_1.listProfiles)().filter((p) => p !== profile);
37
+ if (others.length > 0) {
38
+ console.log(`\nOther saved profiles: ${others.join(', ')}`);
39
+ }
40
+ console.log(`\nMint and save one with: progy init${profileSuffix}`);
41
+ return;
42
+ }
43
+ const prefix = key.slice(0, 18);
44
+ console.log(`Profile: ${profile}`);
45
+ console.log(`Key prefix: ${prefix}`);
46
+ console.log(`Source: ${source}`);
47
+ console.log(`URL: ${url}`);
48
+ // Best-effort account lookup so the user sees account_id + balance without
49
+ // a second command. Network failures degrade gracefully.
50
+ try {
51
+ const body = (await (0, http_1.progyFetch)({ url, key }, '/v1/balance'));
52
+ console.log(`Account: #${body.account_id}${body.wallet_address ? ` (${body.wallet_address})` : ''}`);
53
+ console.log(`Balance: $${body.balance_usd}`);
54
+ }
55
+ catch (err) {
56
+ console.log(`Account: (could not verify — ${err.message})`);
57
+ }
58
+ }
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_PROFILE = void 0;
4
+ exports.credentialsPath = credentialsPath;
5
+ exports.loadCredentials = loadCredentials;
6
+ exports.saveCredentials = saveCredentials;
7
+ exports.deleteCredentials = deleteCredentials;
8
+ exports.listProfiles = listProfiles;
9
+ const os_1 = require("os");
10
+ const path_1 = require("path");
11
+ const fs_1 = require("fs");
12
+ exports.DEFAULT_PROFILE = 'default';
13
+ const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
14
+ function credentialsPath() {
15
+ return (0, path_1.join)((0, os_1.homedir)(), '.progy', 'credentials');
16
+ }
17
+ function credentialsDir() {
18
+ return (0, path_1.dirname)(credentialsPath());
19
+ }
20
+ function assertValidProfile(name) {
21
+ if (!PROFILE_NAME_RE.test(name)) {
22
+ throw new Error(`invalid profile name "${name}" — must be lowercase ASCII letters, digits, or dashes (max 32 chars, must start with letter or digit)`);
23
+ }
24
+ }
25
+ /** Load a profile from the credentials file, or `undefined` if missing/unreadable. */
26
+ function loadCredentials(profile = exports.DEFAULT_PROFILE) {
27
+ assertValidProfile(profile);
28
+ const path = credentialsPath();
29
+ if (!(0, fs_1.existsSync)(path))
30
+ return undefined;
31
+ let raw;
32
+ try {
33
+ raw = (0, fs_1.readFileSync)(path, 'utf8');
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ if (!raw.trim())
39
+ return undefined;
40
+ try {
41
+ const parsed = JSON.parse(raw);
42
+ return parsed[profile];
43
+ }
44
+ catch {
45
+ // Corrupt or hand-edited file — surface as "no credentials" rather than
46
+ // crashing the CLI; the user can recover by re-running `progy init`.
47
+ return undefined;
48
+ }
49
+ }
50
+ /**
51
+ * Persist credentials to the named profile (default: 'default'). Creates
52
+ * `~/.progy/` with 0700 and the file with 0600 if they don't already exist.
53
+ * Merges with any existing profiles so writes to one profile don't clobber
54
+ * siblings.
55
+ */
56
+ function saveCredentials(creds, profile = exports.DEFAULT_PROFILE) {
57
+ assertValidProfile(profile);
58
+ const dir = credentialsDir();
59
+ const path = credentialsPath();
60
+ if (!(0, fs_1.existsSync)(dir)) {
61
+ (0, fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
62
+ }
63
+ else {
64
+ // Existed already — tighten perms in case a previous run created it loose.
65
+ try {
66
+ (0, fs_1.chmodSync)(dir, 0o700);
67
+ }
68
+ catch {
69
+ // POSIX-only; ignore on platforms (e.g. Windows) where chmod is a no-op
70
+ // or unsupported.
71
+ }
72
+ }
73
+ let existing = {};
74
+ if ((0, fs_1.existsSync)(path)) {
75
+ try {
76
+ const raw = (0, fs_1.readFileSync)(path, 'utf8');
77
+ if (raw.trim())
78
+ existing = JSON.parse(raw);
79
+ }
80
+ catch {
81
+ existing = {};
82
+ }
83
+ }
84
+ const merged = {
85
+ ...existing,
86
+ [profile]: { ...existing[profile], ...creds },
87
+ };
88
+ (0, fs_1.writeFileSync)(path, JSON.stringify(merged, null, 2) + '\n', {
89
+ mode: 0o600,
90
+ encoding: 'utf8',
91
+ });
92
+ try {
93
+ (0, fs_1.chmodSync)(path, 0o600);
94
+ }
95
+ catch {
96
+ // Same rationale as above.
97
+ }
98
+ }
99
+ /**
100
+ * Delete a single profile from the file (the whole file is removed when the
101
+ * last profile is gone, so the directory stays clean). When called with the
102
+ * default profile and no other profiles exist, behaves identically to the
103
+ * pre-multi-profile behavior. Returns true if something was removed.
104
+ */
105
+ function deleteCredentials(profile = exports.DEFAULT_PROFILE) {
106
+ assertValidProfile(profile);
107
+ const path = credentialsPath();
108
+ if (!(0, fs_1.existsSync)(path))
109
+ return false;
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
113
+ }
114
+ catch {
115
+ // Corrupt file — easiest recovery is to nuke it.
116
+ try {
117
+ (0, fs_1.unlinkSync)(path);
118
+ return true;
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ }
124
+ if (!(profile in parsed))
125
+ return false;
126
+ delete parsed[profile];
127
+ const remaining = Object.keys(parsed).filter((k) => parsed[k] !== undefined);
128
+ if (remaining.length === 0) {
129
+ try {
130
+ (0, fs_1.unlinkSync)(path);
131
+ return true;
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ (0, fs_1.writeFileSync)(path, JSON.stringify(parsed, null, 2) + '\n', {
138
+ mode: 0o600,
139
+ encoding: 'utf8',
140
+ });
141
+ return true;
142
+ }
143
+ /** List the names of all profiles currently stored in the file. */
144
+ function listProfiles() {
145
+ const path = credentialsPath();
146
+ if (!(0, fs_1.existsSync)(path))
147
+ return [];
148
+ try {
149
+ const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
150
+ return Object.keys(parsed)
151
+ .filter((k) => parsed[k] !== undefined)
152
+ .sort();
153
+ }
154
+ catch {
155
+ return [];
156
+ }
157
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readFlag = readFlag;
4
+ /** Read a string-typed flag with env fallback; throws with a clear hint if missing and required. */
5
+ function readFlag(flags, name, envVar, required = false) {
6
+ const v = flags[name];
7
+ if (typeof v === 'string')
8
+ return v;
9
+ if (envVar) {
10
+ const env = process.env[envVar];
11
+ if (env)
12
+ return env;
13
+ }
14
+ if (required) {
15
+ const hint = envVar ? ` (or set ${envVar})` : '';
16
+ throw new Error(`missing --${name}${hint}`);
17
+ }
18
+ return undefined;
19
+ }