run402 1.36.0 → 1.36.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/lib/ai.mjs CHANGED
@@ -22,6 +22,32 @@ Notes:
22
22
  - usage shows translation word quota for the current billing period
23
23
  `;
24
24
 
25
+ const SUB_HELP = {
26
+ translate: `run402 ai translate — Translate text to another language
27
+
28
+ Usage:
29
+ run402 ai translate <project_id> <text> --to <lang> [--from <lang>] [--context <hint>]
30
+
31
+ Arguments:
32
+ <project_id> Project ID (defaults to the active project if omitted)
33
+ <text> Text to translate (quote it to preserve spaces)
34
+
35
+ Options:
36
+ --to <lang> Target language code (required, e.g. es, ja, fr)
37
+ --from <lang> Source language code (optional; auto-detected if omitted)
38
+ --context <hint> Optional translation hint (e.g. "formal business email")
39
+
40
+ Notes:
41
+ - Requires the AI Translation add-on on the project
42
+ - Counts against the project's translation word quota
43
+
44
+ Examples:
45
+ run402 ai translate proj-001 "Hello world" --to es
46
+ run402 ai translate proj-001 "Hello" --to ja --from en \\
47
+ --context "formal business email"
48
+ `,
49
+ };
50
+
25
51
  function parseFlag(args, flag) {
26
52
  for (let i = 0; i < args.length; i++) {
27
53
  if (args[i] === flag && args[i + 1]) return args[i + 1];
@@ -119,6 +145,7 @@ async function usage(args) {
119
145
 
120
146
  export async function run(sub, args) {
121
147
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
148
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
122
149
  switch (sub) {
123
150
  case "translate": await translate(args); break;
124
151
  case "moderate": await moderate(args); break;
package/lib/allowance.mjs CHANGED
@@ -29,6 +29,33 @@ Examples:
29
29
  run402 allowance history --limit 10
30
30
  `;
31
31
 
32
+ const SUB_HELP = {
33
+ checkout: `run402 allowance checkout — Create a billing checkout session
34
+
35
+ Usage:
36
+ run402 allowance checkout --amount <usd_micros>
37
+
38
+ Options:
39
+ --amount <n> Amount in USD micros (required; e.g. 5000000 for $5)
40
+
41
+ Examples:
42
+ run402 allowance checkout --amount 5000000
43
+ run402 allowance checkout --amount 10000000
44
+ `,
45
+ history: `run402 allowance history — View billing transaction history
46
+
47
+ Usage:
48
+ run402 allowance history [--limit <n>]
49
+
50
+ Options:
51
+ --limit <n> Max entries to return (default: 20)
52
+
53
+ Examples:
54
+ run402 allowance history
55
+ run402 allowance history --limit 10
56
+ `,
57
+ };
58
+
32
59
  const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
33
60
  const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
34
61
  const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
@@ -213,6 +240,10 @@ export async function run(sub, args) {
213
240
  console.log(HELP);
214
241
  process.exit(0);
215
242
  }
243
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
244
+ console.log(SUB_HELP[sub] || HELP);
245
+ process.exit(0);
246
+ }
216
247
  switch (sub) {
217
248
  case "status": await status(); break;
218
249
  case "create": await create(); break;
package/lib/apps.mjs CHANGED
@@ -28,6 +28,78 @@ Examples:
28
28
  run402 apps delete proj123 ver_abc123
29
29
  `;
30
30
 
31
+ const SUB_HELP = {
32
+ browse: `run402 apps browse — Browse public apps in the marketplace
33
+
34
+ Usage:
35
+ run402 apps browse [--tag <tag>]
36
+
37
+ Options:
38
+ --tag <tag> Filter by tag; repeat the flag to filter on multiple tags
39
+
40
+ Examples:
41
+ run402 apps browse
42
+ run402 apps browse --tag auth
43
+ run402 apps browse --tag todo --tag auth
44
+ `,
45
+ fork: `run402 apps fork — Fork a published app into your own project
46
+
47
+ Usage:
48
+ run402 apps fork <version_id> <name> [options]
49
+
50
+ Arguments:
51
+ <version_id> Published version ID (e.g. ver_abc123)
52
+ <name> Name for the forked project
53
+
54
+ Options:
55
+ --tier <tier> Tier for the new project (default: prototype)
56
+ --subdomain <name> Claim a subdomain for the forked project
57
+
58
+ Examples:
59
+ run402 apps fork ver_abc123 my-todo
60
+ run402 apps fork ver_abc123 my-todo --tier hobby --subdomain todo-v2
61
+ `,
62
+ publish: `run402 apps publish — Publish a project as an app
63
+
64
+ Usage:
65
+ run402 apps publish <id> [options]
66
+
67
+ Arguments:
68
+ <id> Project ID to publish
69
+
70
+ Options:
71
+ --description <d> Human-readable description of the app
72
+ --tags <t1,t2> Comma-separated list of tags
73
+ --visibility <v> Visibility: 'public' or 'private'
74
+ --fork-allowed Allow other users to fork this app
75
+
76
+ Examples:
77
+ run402 apps publish proj123 --description "Todo app" --tags todo,auth
78
+ run402 apps publish proj123 --visibility public --fork-allowed
79
+ `,
80
+ update: `run402 apps update — Update a published version's metadata
81
+
82
+ Usage:
83
+ run402 apps update <project_id> <version_id> [options]
84
+
85
+ Arguments:
86
+ <project_id> Project ID that owns the version
87
+ <version_id> Published version ID to update
88
+
89
+ Options:
90
+ --description <d> New description
91
+ --tags <t1,t2> New comma-separated list of tags
92
+ --visibility <v> New visibility ('public' or 'private')
93
+ --fork-allowed Enable forking for this version
94
+ --no-fork Disable forking for this version
95
+
96
+ Examples:
97
+ run402 apps update proj123 ver_abc123 --description "Updated"
98
+ run402 apps update proj123 ver_abc123 --tags todo,auth --fork-allowed
99
+ run402 apps update proj123 ver_abc123 --no-fork
100
+ `,
101
+ };
102
+
31
103
  async function browse(args) {
32
104
  let url = `${API}/apps/v1`;
33
105
  const tags = [];
@@ -152,6 +224,7 @@ async function deleteVersion(projectId, versionId) {
152
224
 
153
225
  export async function run(sub, args) {
154
226
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
227
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
155
228
  switch (sub) {
156
229
  case "browse": await browse(args); break;
157
230
  case "fork": await fork(args[0], args[1], args.slice(2)); break;
package/lib/auth.mjs CHANGED
@@ -29,6 +29,84 @@ Examples:
29
29
  run402 auth providers
30
30
  `;
31
31
 
32
+ const SUB_HELP = {
33
+ "magic-link": `run402 auth magic-link — Send a passwordless login link
34
+
35
+ Usage:
36
+ run402 auth magic-link --email <addr> --redirect <url> [options]
37
+
38
+ Options:
39
+ --email <addr> Required: recipient email address
40
+ --redirect <url> Required: URL to redirect to after the user clicks
41
+ --project <id> Project ID (defaults to active project)
42
+
43
+ Notes:
44
+ Auto-creates the user on first use. Uses the project's anon_key.
45
+
46
+ Examples:
47
+ run402 auth magic-link --email user@example.com \\
48
+ --redirect https://myapp.run402.com/cb
49
+ `,
50
+ verify: `run402 auth verify — Exchange a magic-link token for session tokens
51
+
52
+ Usage:
53
+ run402 auth verify --token <token> [options]
54
+
55
+ Options:
56
+ --token <token> Required: the one-time magic-link token
57
+ --project <id> Project ID (defaults to active project)
58
+
59
+ Notes:
60
+ Returns an access_token + refresh_token pair on success.
61
+
62
+ Examples:
63
+ run402 auth verify --token abc123def456
64
+ `,
65
+ "set-password": `run402 auth set-password — Change, reset, or set a user's password
66
+
67
+ Usage:
68
+ run402 auth set-password --token <bearer> --new <password> [options]
69
+
70
+ Options:
71
+ --token <bearer> Required: the user's access_token (Bearer token)
72
+ --new <password> Required: new password
73
+ --current <pwd> Current password (required when one is already set)
74
+ --project <id> Project ID (defaults to active project)
75
+
76
+ Examples:
77
+ run402 auth set-password --token eyJ... --new "new-pass" \\
78
+ --current "old-pass"
79
+ `,
80
+ settings: `run402 auth settings — Update project auth settings
81
+
82
+ Usage:
83
+ run402 auth settings --allow-password-set <true|false> [options]
84
+
85
+ Options:
86
+ --allow-password-set <true|false> Required: toggle password-set flow
87
+ --project <id> Project ID (defaults to active project)
88
+
89
+ Notes:
90
+ Requires the project's service_key (admin-level).
91
+
92
+ Examples:
93
+ run402 auth settings --allow-password-set true
94
+ run402 auth settings --allow-password-set false --project abc123
95
+ `,
96
+ providers: `run402 auth providers — List available auth providers
97
+
98
+ Usage:
99
+ run402 auth providers [options]
100
+
101
+ Options:
102
+ --project <id> Project ID (defaults to active project)
103
+
104
+ Examples:
105
+ run402 auth providers
106
+ run402 auth providers --project abc123
107
+ `,
108
+ };
109
+
32
110
  function parseFlag(args, flag) {
33
111
  for (let i = 0; i < args.length; i++) {
34
112
  if (args[i] === flag && args[i + 1]) return args[i + 1];
@@ -90,6 +168,8 @@ async function setPassword(args) {
90
168
  const accessToken = parseFlag(args, "--token");
91
169
  const newPassword = parseFlag(args, "--new");
92
170
  const currentPassword = parseFlag(args, "--current");
171
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
172
+ const p = findProject(projectId);
93
173
 
94
174
  if (!accessToken) { console.error(JSON.stringify({ status: "error", message: "Missing --token <bearer_token>" })); process.exit(1); }
95
175
  if (!newPassword) { console.error(JSON.stringify({ status: "error", message: "Missing --new <password>" })); process.exit(1); }
@@ -97,9 +177,16 @@ async function setPassword(args) {
97
177
  const body = { new_password: newPassword };
98
178
  if (currentPassword) body.current_password = currentPassword;
99
179
 
180
+ // /auth/v1/* is gated by apikeyAuth middleware: the `apikey` header must be
181
+ // the project's anon_key. `Authorization: Bearer <access_token>` stays as
182
+ // the user's identity so the server knows whose password to change.
100
183
  const res = await fetch(`${API}/auth/v1/user/password`, {
101
184
  method: "PUT",
102
- headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json" },
185
+ headers: {
186
+ "apikey": p.anon_key,
187
+ "Authorization": `Bearer ${accessToken}`,
188
+ "Content-Type": "application/json",
189
+ },
103
190
  body: JSON.stringify(body),
104
191
  });
105
192
  const data = await res.json();
@@ -155,7 +242,7 @@ async function providers(args) {
155
242
  export async function run(sub, args) {
156
243
  if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
157
244
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
158
- console.log(HELP);
245
+ console.log(SUB_HELP[sub] || HELP);
159
246
  process.exit(0);
160
247
  }
161
248
  switch (sub) {
package/lib/billing.mjs CHANGED
@@ -22,6 +22,69 @@ Examples:
22
22
  run402 billing balance user@example.com
23
23
  `;
24
24
 
25
+ const SUB_HELP = {
26
+ "tier-checkout": `run402 billing tier-checkout — Create a Stripe tier checkout session
27
+
28
+ Usage:
29
+ run402 billing tier-checkout <tier> [--email <e> | --wallet <w>]
30
+
31
+ Arguments:
32
+ <tier> Tier name (e.g. hobby, pro)
33
+
34
+ Options:
35
+ --email <e> Email billing account to charge
36
+ --wallet <w> Wallet address (0x...) to associate with the checkout
37
+
38
+ Examples:
39
+ run402 billing tier-checkout hobby --email user@example.com
40
+ run402 billing tier-checkout pro --wallet 0x1234...
41
+ `,
42
+ "buy-email-pack": `run402 billing buy-email-pack — Buy a $5 email pack (10,000 emails)
43
+
44
+ Usage:
45
+ run402 billing buy-email-pack [--email <e> | --wallet <w>]
46
+
47
+ Options:
48
+ --email <e> Email billing account to charge
49
+ --wallet <w> Wallet address (0x...) to associate with the purchase
50
+
51
+ Examples:
52
+ run402 billing buy-email-pack --email user@example.com
53
+ run402 billing buy-email-pack --wallet 0x1234...
54
+ `,
55
+ "auto-recharge": `run402 billing auto-recharge — Toggle email-pack auto-recharge
56
+
57
+ Usage:
58
+ run402 billing auto-recharge <account_id> <on|off> [--threshold <n>]
59
+
60
+ Arguments:
61
+ <account_id> Billing account ID
62
+ <on|off> Enable or disable auto-recharge
63
+
64
+ Options:
65
+ --threshold <n> Remaining-email threshold that triggers auto-recharge
66
+
67
+ Examples:
68
+ run402 billing auto-recharge acct_abc on --threshold 2000
69
+ run402 billing auto-recharge acct_abc off
70
+ `,
71
+ history: `run402 billing history — Show ledger history for an email or wallet
72
+
73
+ Usage:
74
+ run402 billing history <identifier> [--limit <n>]
75
+
76
+ Arguments:
77
+ <identifier> Email address or wallet (0x...)
78
+
79
+ Options:
80
+ --limit <n> Max entries to return (default: 50)
81
+
82
+ Examples:
83
+ run402 billing history user@example.com
84
+ run402 billing history 0x1234... --limit 100
85
+ `,
86
+ };
87
+
25
88
  function parseFlag(args, flag) {
26
89
  for (let i = 0; i < args.length; i++) {
27
90
  if (args[i] === flag && args[i + 1]) return args[i + 1];
@@ -153,6 +216,7 @@ async function history(args) {
153
216
 
154
217
  export async function run(sub, args) {
155
218
  if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
219
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
156
220
  switch (sub) {
157
221
  case "create-email": await createEmail(args); break;
158
222
  case "link-wallet": await linkWallet(args); break;
package/lib/blob.mjs CHANGED
@@ -68,6 +68,89 @@ Examples:
68
68
  run402 blob sign images/logo.png --project abc123 --ttl 600
69
69
  `;
70
70
 
71
+ const SUB_HELP = {
72
+ put: `run402 blob put — Upload one or more files to blob storage
73
+
74
+ Usage:
75
+ run402 blob put <file> [files...] [options]
76
+
77
+ Arguments:
78
+ <file> Path to a file (or glob); pass multiple files to batch-upload
79
+
80
+ Options:
81
+ --project <id> Project ID (defaults to active project from 'run402 projects use')
82
+ --key <dest> Destination key; defaults to file basename. Use trailing '/' as prefix.
83
+ --private Upload as private (not served by CDN; apikey required to read)
84
+ --immutable Append content-hash suffix so overwrites produce distinct URLs
85
+ --concurrency N Concurrent part PUTs for multipart uploads (default 4)
86
+ --no-resume Ignore any cached resumable-upload state and start fresh
87
+ --json Emit NDJSON progress events on stdout (for agent consumption)
88
+
89
+ Examples:
90
+ run402 blob put ./artifact.tgz --project abc123
91
+ run402 blob put ./dist/**/*.png --project abc123 --key assets/
92
+ run402 blob put huge.bin --project abc123 --immutable --concurrency 8
93
+ `,
94
+ get: `run402 blob get — Download a blob by key
95
+
96
+ Usage:
97
+ run402 blob get <key> --output <file> [options]
98
+
99
+ Arguments:
100
+ <key> Blob key to download
101
+
102
+ Options:
103
+ --output <file> Local destination path (required)
104
+ --project <id> Project ID (defaults to active project)
105
+
106
+ Examples:
107
+ run402 blob get images/logo.png --output /tmp/logo.png --project abc123
108
+ `,
109
+ ls: `run402 blob ls — List blob keys in a project
110
+
111
+ Usage:
112
+ run402 blob ls [options]
113
+
114
+ Options:
115
+ --project <id> Project ID (defaults to active project)
116
+ --prefix <p> Only list keys starting with this prefix
117
+ --limit <n> Max results (default 100, max 1000)
118
+
119
+ Examples:
120
+ run402 blob ls --project abc123
121
+ run402 blob ls --project abc123 --prefix images/ --limit 500
122
+ `,
123
+ rm: `run402 blob rm — Delete a blob
124
+
125
+ Usage:
126
+ run402 blob rm <key> [options]
127
+
128
+ Arguments:
129
+ <key> Blob key to delete
130
+
131
+ Options:
132
+ --project <id> Project ID (defaults to active project)
133
+
134
+ Examples:
135
+ run402 blob rm images/logo.png --project abc123
136
+ `,
137
+ sign: `run402 blob sign — Create a presigned download URL for a blob
138
+
139
+ Usage:
140
+ run402 blob sign <key> [options]
141
+
142
+ Arguments:
143
+ <key> Blob key to sign
144
+
145
+ Options:
146
+ --project <id> Project ID (defaults to active project)
147
+ --ttl <seconds> Signed-URL TTL (default 3600, max 604800)
148
+
149
+ Examples:
150
+ run402 blob sign reports/2025-q4.pdf --project abc123 --ttl 600
151
+ `,
152
+ };
153
+
71
154
  const UPLOAD_STATE_DIR = join(homedir(), ".run402", "uploads");
72
155
 
73
156
  function die(msg, code = 1) {
@@ -428,7 +511,7 @@ export async function run(sub, args) {
428
511
  process.exit(0);
429
512
  }
430
513
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
431
- console.log(HELP);
514
+ console.log(SUB_HELP[sub] || HELP);
432
515
  process.exit(0);
433
516
  }
434
517
  const defaultProject = process.env.RUN402_PROJECT ?? null;
package/lib/contracts.mjs CHANGED
@@ -36,6 +36,144 @@ Examples:
36
36
  run402 contracts call proj_abc cwlt_xyz --to 0x1234... --abi '[{"type":"function","name":"ping","inputs":[],"outputs":[]}]' --fn ping --args '[]'
37
37
  `;
38
38
 
39
+ const SUB_HELP = {
40
+ "provision-wallet": `run402 contracts provision-wallet — Provision a KMS-backed wallet
41
+
42
+ Usage:
43
+ run402 contracts provision-wallet <project_id> --chain <chain> [options]
44
+
45
+ Arguments:
46
+ <project_id> Target project ID
47
+
48
+ Options:
49
+ --chain <chain> Required: base-mainnet or base-sepolia
50
+ --recovery 0x... Optional recovery address (can be set later)
51
+ --yes Skip confirmation when project already has a wallet
52
+
53
+ Pricing:
54
+ $0.04/day per wallet ($1.20/month). Creation requires $1.20 prepay
55
+ (30 days of rent). Non-custodial — see terms.html#non-custodial-kms-wallets.
56
+
57
+ Examples:
58
+ run402 contracts provision-wallet proj_abc --chain base-mainnet
59
+ run402 contracts provision-wallet proj_abc --chain base-sepolia --recovery 0xAbC...
60
+ `,
61
+ "set-recovery": `run402 contracts set-recovery — Set or clear the wallet recovery address
62
+
63
+ Usage:
64
+ run402 contracts set-recovery <project_id> <wallet_id> [options]
65
+
66
+ Arguments:
67
+ <project_id> Target project ID
68
+ <wallet_id> KMS wallet ID (cwlt_...)
69
+
70
+ Options:
71
+ --address 0x... New recovery address
72
+ --clear Clear the recovery address (mutually exclusive with --address)
73
+
74
+ Examples:
75
+ run402 contracts set-recovery proj_abc cwlt_xyz --address 0xAbC...
76
+ run402 contracts set-recovery proj_abc cwlt_xyz --clear
77
+ `,
78
+ "set-alert": `run402 contracts set-alert — Set the low-balance alert threshold
79
+
80
+ Usage:
81
+ run402 contracts set-alert <project_id> <wallet_id> --threshold-wei <n>
82
+
83
+ Arguments:
84
+ <project_id> Target project ID
85
+ <wallet_id> KMS wallet ID (cwlt_...)
86
+
87
+ Options:
88
+ --threshold-wei <n> Required: alert threshold in wei
89
+
90
+ Examples:
91
+ run402 contracts set-alert proj_abc cwlt_xyz --threshold-wei 1000000000000000
92
+ `,
93
+ call: `run402 contracts call — Submit a contract write call
94
+
95
+ Usage:
96
+ run402 contracts call <project_id> <wallet_id> --to 0x... --abi <json>
97
+ --fn <name> --args <json> [options]
98
+
99
+ Arguments:
100
+ <project_id> Target project ID
101
+ <wallet_id> KMS wallet ID (cwlt_...)
102
+
103
+ Options:
104
+ --to 0x... Required: contract address
105
+ --abi <json> Required: ABI fragment (JSON string)
106
+ --fn <name> Required: function name to invoke
107
+ --args <json> Required: function args (JSON array)
108
+ --value-wei <n> Native value to send (default 0)
109
+ --chain <chain> Chain override (default: base-mainnet)
110
+ --idempotency-key <k> Idempotency key for safe retries
111
+
112
+ Pricing:
113
+ Chain gas + $0.000005 KMS sign fee per call.
114
+
115
+ Examples:
116
+ run402 contracts call proj_abc cwlt_xyz --to 0x1234... \\
117
+ --abi '[{"type":"function","name":"ping","inputs":[],"outputs":[]}]' \\
118
+ --fn ping --args '[]'
119
+ `,
120
+ read: `run402 contracts read — Read-only contract call (free)
121
+
122
+ Usage:
123
+ run402 contracts read --chain <chain> --to 0x... --abi <json>
124
+ --fn <name> --args <json>
125
+
126
+ Options:
127
+ --chain <chain> Required: base-mainnet or base-sepolia
128
+ --to 0x... Required: contract address
129
+ --abi <json> Required: ABI fragment (JSON string)
130
+ --fn <name> Required: function name to invoke
131
+ --args <json> Required: function args (JSON array)
132
+
133
+ Examples:
134
+ run402 contracts read --chain base-mainnet --to 0x1234... \\
135
+ --abi '[{"type":"function","name":"balanceOf","inputs":[{"type":"address"}],"outputs":[{"type":"uint256"}]}]' \\
136
+ --fn balanceOf --args '["0xAbC..."]'
137
+ `,
138
+ drain: `run402 contracts drain — Drain native balance to a destination address
139
+
140
+ Usage:
141
+ run402 contracts drain <project_id> <wallet_id> --to 0x... --confirm
142
+
143
+ Arguments:
144
+ <project_id> Target project ID
145
+ <wallet_id> KMS wallet ID (cwlt_...)
146
+
147
+ Options:
148
+ --to 0x... Required: destination address
149
+ --confirm Required: explicit confirmation flag
150
+
151
+ Notes:
152
+ Works on suspended wallets. Cost: chain gas + $0.000005 KMS sign fee.
153
+
154
+ Examples:
155
+ run402 contracts drain proj_abc cwlt_xyz --to 0xAbC... --confirm
156
+ `,
157
+ delete: `run402 contracts delete — Schedule the KMS key for deletion
158
+
159
+ Usage:
160
+ run402 contracts delete <project_id> <wallet_id> --confirm
161
+
162
+ Arguments:
163
+ <project_id> Target project ID
164
+ <wallet_id> KMS wallet ID (cwlt_...)
165
+
166
+ Options:
167
+ --confirm Required: explicit confirmation flag
168
+
169
+ Notes:
170
+ Refused if wallet balance is greater than or equal to dust. Drain first.
171
+
172
+ Examples:
173
+ run402 contracts delete proj_abc cwlt_xyz --confirm
174
+ `,
175
+ };
176
+
39
177
  function parseFlag(args, flag) {
40
178
  for (let i = 0; i < args.length; i++) {
41
179
  if (args[i] === flag && args[i + 1]) return args[i + 1];
@@ -243,6 +381,7 @@ async function deleteWallet(projectId, walletId, args) {
243
381
 
244
382
  export async function run(sub, args) {
245
383
  if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
384
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
246
385
  switch (sub) {
247
386
  case "provision-wallet": await provisionWallet(args[0], args.slice(1)); break;
248
387
  case "get-wallet": await getWallet(args[0], args[1]); break;
package/lib/domains.mjs CHANGED
@@ -92,6 +92,7 @@ async function deleteDomain(args) {
92
92
 
93
93
  export async function run(sub, args) {
94
94
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
95
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(HELP); process.exit(0); }
95
96
  switch (sub) {
96
97
  case "add": await add(args); break;
97
98
  case "list": await list(args[0]); break;
package/lib/email.mjs CHANGED
@@ -56,6 +56,56 @@ Notes:
56
56
  - --project defaults to the active project
57
57
  `;
58
58
 
59
+ const SUB_HELP = {
60
+ send: `run402 email send — Send an email (template or raw HTML)
61
+
62
+ Usage:
63
+ run402 email send --to <email> --template <name> --var key=value [--var ...]
64
+ run402 email send --to <email> --subject "..." --html "..." [--text "..."]
65
+
66
+ Options:
67
+ --to <email> Recipient email address (required; single recipient)
68
+ --template <name> Template name (template mode): project_invite, magic_link,
69
+ notification
70
+ --var key=value Template variable (repeatable; required keys vary by
71
+ template)
72
+ --subject "..." Subject line (raw HTML mode)
73
+ --html "..." HTML body (raw HTML mode)
74
+ --text "..." Plain-text body (raw HTML mode; optional)
75
+ --from-name "..." Display name for the From header
76
+ --project <id> Project ID (defaults to the active project)
77
+
78
+ Templates:
79
+ project_invite --var project_name=... --var invite_url=...
80
+ magic_link --var project_name=... --var link_url=... --var expires_in=...
81
+ notification --var project_name=... --var message=... (max 500 chars)
82
+
83
+ Examples:
84
+ run402 email send --template project_invite --to user@example.com \\
85
+ --var project_name="My App" --var invite_url="https://example.com/invite/abc"
86
+ run402 email send --to user@example.com --subject "Welcome!" \\
87
+ --html "<h1>Hello</h1><p>Welcome aboard.</p>" --from-name "My App"
88
+ run402 email send --template notification --to admin@example.com \\
89
+ --var project_name="My App" --var message="Deploy complete"
90
+ `,
91
+ "get-raw": `run402 email get-raw — Fetch raw RFC-822 bytes for an inbound message
92
+
93
+ Usage:
94
+ run402 email get-raw <message_id> [--output <file>] [--project <id>]
95
+
96
+ Arguments:
97
+ <message_id> Message ID to fetch (inbound messages only)
98
+
99
+ Options:
100
+ --output <file> Write raw bytes to this file; omit to stream to stdout
101
+ --project <id> Project ID (defaults to the active project)
102
+
103
+ Examples:
104
+ run402 email get-raw msg_abc123 --output reply.eml
105
+ run402 email get-raw msg_abc123 > reply.eml
106
+ `,
107
+ };
108
+
59
109
  const SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
60
110
 
61
111
  function parseFlag(args, flag) {
@@ -327,6 +377,7 @@ async function status(args) {
327
377
 
328
378
  export async function run(sub, args) {
329
379
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
380
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h")) && sub !== "webhooks") { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
330
381
  switch (sub) {
331
382
  case "create": await create(args); break;
332
383
  case "status": await status(args); break;
package/lib/functions.mjs CHANGED
@@ -38,6 +38,95 @@ Notes:
38
38
  - Deploy may require payment if the project lease has expired
39
39
  `;
40
40
 
41
+ const SUB_HELP = {
42
+ deploy: `run402 functions deploy — Deploy a function to a project
43
+
44
+ Usage:
45
+ run402 functions deploy <project_id> <name> --file <file> [options]
46
+
47
+ Arguments:
48
+ <project_id> Target project ID
49
+ <name> Function name (used in the invoke URL path)
50
+
51
+ Options:
52
+ --file <file> Required: path to the function source file
53
+ --timeout <s> Runtime timeout in seconds
54
+ --memory <mb> Memory in MB
55
+ --deps <pkg,...> Comma-separated npm deps to bundle
56
+ --schedule <cron> Cron schedule; pass '' to clear an existing schedule
57
+
58
+ Notes:
59
+ Code must export a default async function:
60
+ export default async (req: Request) => Response
61
+ Deploy may require payment if the project lease has expired.
62
+
63
+ Examples:
64
+ run402 functions deploy abc123 stripe-webhook --file handler.ts
65
+ run402 functions deploy abc123 send-reminders --file remind.ts \\
66
+ --schedule '*/15 * * * *'
67
+ run402 functions deploy abc123 send-reminders --file remind.ts --schedule ''
68
+ `,
69
+ invoke: `run402 functions invoke — Invoke a deployed function
70
+
71
+ Usage:
72
+ run402 functions invoke <project_id> <name> [options]
73
+
74
+ Arguments:
75
+ <project_id> Target project ID
76
+ <name> Function name
77
+
78
+ Options:
79
+ --method <M> HTTP method (default POST)
80
+ --body <json> Request body (ignored for GET/HEAD)
81
+
82
+ Examples:
83
+ run402 functions invoke abc123 stripe-webhook --body '{"event":"test"}'
84
+ run402 functions invoke abc123 ping --method GET
85
+ `,
86
+ logs: `run402 functions logs — Fetch or tail function logs
87
+
88
+ Usage:
89
+ run402 functions logs <project_id> <name> [options]
90
+
91
+ Arguments:
92
+ <project_id> Target project ID
93
+ <name> Function name
94
+
95
+ Options:
96
+ --tail <n> Number of most-recent entries (default 50)
97
+ --since <ts> ISO timestamp or epoch ms; only entries after this
98
+ --follow Poll every 3s and stream new entries (Ctrl-C to stop)
99
+
100
+ Examples:
101
+ run402 functions logs abc123 stripe-webhook --tail 100
102
+ run402 functions logs abc123 stripe-webhook --since 2026-03-29T14:00:00Z
103
+ run402 functions logs abc123 stripe-webhook --follow
104
+ `,
105
+ update: `run402 functions update — Update function config without re-deploying
106
+
107
+ Usage:
108
+ run402 functions update <project_id> <name> [options]
109
+
110
+ Arguments:
111
+ <project_id> Target project ID
112
+ <name> Function name
113
+
114
+ Options:
115
+ --schedule <cron> New cron schedule (pass '' to clear)
116
+ --schedule-remove Explicitly remove the schedule
117
+ --timeout <s> Runtime timeout in seconds
118
+ --memory <mb> Memory in MB
119
+
120
+ Notes:
121
+ Must provide at least one of the options above.
122
+
123
+ Examples:
124
+ run402 functions update abc123 send-reminders --schedule '0 */4 * * *'
125
+ run402 functions update abc123 send-reminders --schedule-remove
126
+ run402 functions update abc123 my-func --timeout 15 --memory 256
127
+ `,
128
+ };
129
+
41
130
  async function deploy(projectId, name, args) {
42
131
  const p = findProject(projectId);
43
132
  const opts = { file: null, timeout: undefined, memory: undefined, deps: undefined, schedule: undefined };
@@ -213,7 +302,7 @@ async function deleteFunction(projectId, name) {
213
302
  export async function run(sub, args) {
214
303
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
215
304
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
216
- console.log(HELP);
305
+ console.log(SUB_HELP[sub] || HELP);
217
306
  process.exit(0);
218
307
  }
219
308
  switch (sub) {
package/lib/init.mjs CHANGED
@@ -112,16 +112,23 @@ export async function run(args = []) {
112
112
  });
113
113
  const data = await res.json();
114
114
  if (data.result) {
115
- // Tempo faucet is instant re-read balance once
116
- try {
117
- const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
118
- balance = Number(raw);
119
- } catch {}
115
+ // Tempo faucet is "instant" on-chain, but the client RPC read can be
116
+ // racy relative to faucet settlement — poll up to 30s (GH-81), mirroring
117
+ // the x402 path below.
118
+ for (let i = 0; i < 30; i++) {
119
+ await new Promise(r => setTimeout(r, 1000));
120
+ try {
121
+ const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
122
+ balance = Number(raw);
123
+ if (balance > 0) break;
124
+ } catch {}
125
+ }
120
126
  saveAllowance({ ...allowance, funded: true, lastFaucet: new Date().toISOString() });
127
+ summary.allowance.funded = true;
121
128
  if (balance > 0) {
122
129
  line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD (funded)`);
123
130
  } else {
124
- line("Balance", "faucet sent — checking balance...");
131
+ line("Balance", "faucet sent — not yet confirmed on-chain");
125
132
  }
126
133
  } else {
127
134
  line("Balance", `faucet failed: ${data.error?.message || "unknown error"}`);
@@ -131,6 +138,7 @@ export async function run(args = []) {
131
138
  }
132
139
  } else {
133
140
  line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD`);
141
+ summary.allowance.funded = balance > 0;
134
142
  }
135
143
  summary.balance = { symbol: "pathUSD", usd_micros: balance };
136
144
  } else {
@@ -162,6 +170,7 @@ export async function run(args = []) {
162
170
  } catch {}
163
171
  }
164
172
  saveAllowance({ ...allowance, funded: true, lastFaucet: new Date().toISOString() });
173
+ summary.allowance.funded = true;
165
174
  if (balance > 0) {
166
175
  line("Balance", `${(balance / 1e6).toFixed(2)} USDC (funded)`);
167
176
  } else {
@@ -174,6 +183,7 @@ export async function run(args = []) {
174
183
  }
175
184
  } else {
176
185
  line("Balance", `${(balance / 1e6).toFixed(2)} USDC`);
186
+ summary.allowance.funded = balance > 0;
177
187
  }
178
188
  summary.balance = { symbol: "USDC", usd_micros: balance };
179
189
  }
package/lib/projects.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "fs";
2
- import { findProject, loadKeyStore, saveProject, removeProject, API, allowanceAuthHeaders, setActiveProjectId, getActiveProjectId } from "./config.mjs";
2
+ import { findProject, loadKeyStore, saveProject, removeProject, API, allowanceAuthHeaders, setActiveProjectId, getActiveProjectId, resolveProjectId } from "./config.mjs";
3
3
 
4
4
  const HELP = `run402 projects — Manage your deployed Run402 projects
5
5
 
@@ -11,17 +11,17 @@ Subcommands:
11
11
  provision [--tier <tier>] [--name <n>] Provision a new Postgres project (pays via x402)
12
12
  use <id> Set the active project (used as default for other commands)
13
13
  list List all your projects (IDs, URLs, active marker)
14
- info <id> Show project details: REST URL, keys
15
- keys <id> Print anon_key and service_key as JSON
16
- sql <id> "<query>" [--file <path>] [--params '<json>'] Run a SQL query (supports parameterized queries)
17
- rest <id> <table> [params] Query a table via the REST API (PostgREST)
18
- usage <id> Show compute/storage usage for a project
19
- schema <id> Inspect the database schema
20
- rls <id> <template> <tables_json> Apply Row-Level Security policies
21
- delete <id> Immediately and irreversibly delete a project (cascade purge) and remove from local state
22
- pin <id> Pin a project (prevents expiry/GC)
23
- promote-user <id> <email> Promote a user to project_admin role
24
- demote-user <id> <email> Demote a user from project_admin role
14
+ info [id] Show project details: REST URL, keys
15
+ keys [id] Print anon_key and service_key as JSON
16
+ sql [id] "<query>" [--file <path>] [--params '<json>'] Run a SQL query (supports parameterized queries)
17
+ rest [id] <table> [params] Query a table via the REST API (PostgREST)
18
+ usage [id] Show compute/storage usage for a project
19
+ schema [id] Inspect the database schema
20
+ rls [id] <template> <tables_json> Apply Row-Level Security policies
21
+ delete [id] Immediately and irreversibly delete a project (cascade purge) and remove from local state
22
+ pin [id] Pin a project (prevents expiry/GC)
23
+ promote-user [id] <email> Promote a user to project_admin role
24
+ demote-user [id] <email> Demote a user from project_admin role
25
25
 
26
26
  Examples:
27
27
  run402 projects quote
@@ -41,8 +41,10 @@ Examples:
41
41
  run402 projects delete abc123
42
42
 
43
43
  Notes:
44
- - <id> is the project_id shown in 'run402 projects list'
45
- - Most commands that take <id> default to the active project if omitted
44
+ - <id> is the project_id shown in 'run402 projects list' (prefix: 'prj_')
45
+ - Most commands that take <id> default to the active project when omitted
46
+ (set it with 'run402 projects use <id>'). Project IDs start with 'prj_';
47
+ any first positional that doesn't is treated as the next argument instead.
46
48
  - 'rest' uses PostgREST query syntax (table name + optional query string)
47
49
  - 'provision' requires a funded allowance — payment is automatic via x402
48
50
  - RLS templates (prefer user_owns_rows for user-scoped data):
@@ -52,6 +54,48 @@ Notes:
52
54
  that includes "i_understand_this_is_unrestricted": true
53
55
  `;
54
56
 
57
+ const SUB_HELP = {
58
+ provision: `run402 projects provision — Provision a new Postgres project
59
+
60
+ Usage:
61
+ run402 projects provision [--tier <tier>] [--name <name>]
62
+
63
+ Options:
64
+ --tier <tier> Tier for the new project (default: prototype)
65
+ --name <name> Human-readable name for the project
66
+
67
+ Notes:
68
+ - Payment is automatic via x402; requires a funded allowance
69
+ - The new project becomes the active project after provisioning
70
+
71
+ Examples:
72
+ run402 projects provision
73
+ run402 projects provision --tier prototype
74
+ run402 projects provision --tier hobby --name my-app
75
+ `,
76
+ sql: `run402 projects sql — Run a SQL query against a project's database
77
+
78
+ Usage:
79
+ run402 projects sql [id] "<query>" [options]
80
+ run402 projects sql [id] --file <path> [options]
81
+
82
+ Arguments:
83
+ [id] Project ID (defaults to the active project if omitted;
84
+ must start with 'prj_' — any other first arg is treated
85
+ as the query instead)
86
+ <query> Inline SQL query (quote it to preserve spaces)
87
+
88
+ Options:
89
+ --file <path> Read SQL from a file instead of an inline query
90
+ --params '<json>' JSON array of parameters for a parameterized query
91
+
92
+ Examples:
93
+ run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
94
+ run402 projects sql abc123 "SELECT * FROM users WHERE id = $1" --params '[42]'
95
+ run402 projects sql abc123 --file setup.sql
96
+ `,
97
+ };
98
+
55
99
  async function quote() {
56
100
  const res = await fetch(`${API}/tiers/v1`);
57
101
  const data = await res.json();
@@ -73,8 +117,34 @@ async function provision(args) {
73
117
  headers: { "Content-Type": "application/json", ...authHeaders },
74
118
  body: JSON.stringify(body),
75
119
  });
76
- const data = await res.json();
77
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
120
+ // Content-type aware parsing: gateways (ALB, CloudFront, etc.) return HTML on
121
+ // 502/504/etc., which would otherwise crash res.json() with SyntaxError (GH-84).
122
+ const contentType = res.headers.get("content-type") || "";
123
+ let data = null;
124
+ let parseError = null;
125
+ let bodyText = null;
126
+ if (contentType.includes("application/json")) {
127
+ try {
128
+ data = await res.json();
129
+ } catch (e) {
130
+ parseError = e;
131
+ try { bodyText = await res.text(); } catch { bodyText = ""; }
132
+ }
133
+ } else {
134
+ try { bodyText = await res.text(); } catch { bodyText = ""; }
135
+ }
136
+ if (!res.ok || parseError || data === null) {
137
+ const err = { status: "error", http: res.status, content_type: contentType || null };
138
+ if (data && typeof data === "object") {
139
+ Object.assign(err, data);
140
+ } else {
141
+ const preview = typeof bodyText === "string" ? bodyText.slice(0, 500) : "";
142
+ err.body_preview = preview;
143
+ if (parseError) err.parse_error = "response body was not valid JSON";
144
+ }
145
+ console.error(JSON.stringify(err));
146
+ process.exit(1);
147
+ }
78
148
  // Save project credentials locally and set as active
79
149
  if (data.project_id) {
80
150
  saveProject(data.project_id, {
@@ -231,13 +301,26 @@ async function deleteProject(projectId) {
231
301
  }
232
302
  }
233
303
 
304
+ // Resolve a positional project_id argument with active-project fallback (GH-102).
305
+ // Heuristic: real project IDs start with "prj_". If args[0] is missing OR
306
+ // doesn't start with "prj_", fall back to the active project and return the
307
+ // full args array as remaining positionals. Otherwise consume args[0] as the
308
+ // project_id and return args.slice(1) as remaining positionals.
309
+ function resolvePositionalProject(args) {
310
+ const first = Array.isArray(args) ? args[0] : undefined;
311
+ if (typeof first === "string" && first.startsWith("prj_")) {
312
+ return { projectId: first, rest: args.slice(1) };
313
+ }
314
+ return { projectId: resolveProjectId(null), rest: Array.isArray(args) ? args : [] };
315
+ }
316
+
234
317
  export async function run(sub, args) {
235
318
  if (!sub || sub === '--help' || sub === '-h') {
236
319
  console.log(HELP);
237
320
  process.exit(0);
238
321
  }
239
322
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
240
- console.log(HELP);
323
+ console.log(SUB_HELP[sub] || HELP);
241
324
  process.exit(0);
242
325
  }
243
326
  switch (sub) {
@@ -245,17 +328,17 @@ export async function run(sub, args) {
245
328
  case "provision": await provision(args); break;
246
329
  case "use": await use(args[0]); break;
247
330
  case "list": await list(); break;
248
- case "info": await info(args[0]); break;
249
- case "keys": await keys(args[0]); break;
250
- case "sql": await sqlCmd(args[0], args.slice(1)); break;
251
- case "rest": await rest(args[0], args[1], args[2]); break;
252
- case "usage": await usage(args[0]); break;
253
- case "schema": await schema(args[0]); break;
254
- case "rls": await rls(args[0], args[1], args[2]); break;
255
- case "delete": await deleteProject(args[0]); break;
256
- case "pin": await pin(args[0]); break;
257
- case "promote-user": await promoteUser(args[0], args[1]); break;
258
- case "demote-user": await demoteUser(args[0], args[1]); break;
331
+ case "info": { const { projectId } = resolvePositionalProject(args); await info(projectId); break; }
332
+ case "keys": { const { projectId } = resolvePositionalProject(args); await keys(projectId); break; }
333
+ case "sql": { const { projectId, rest } = resolvePositionalProject(args); await sqlCmd(projectId, rest); break; }
334
+ case "rest": { const { projectId, rest: restArgs } = resolvePositionalProject(args); await rest(projectId, restArgs[0], restArgs[1]); break; }
335
+ case "usage": { const { projectId } = resolvePositionalProject(args); await usage(projectId); break; }
336
+ case "schema": { const { projectId } = resolvePositionalProject(args); await schema(projectId); break; }
337
+ case "rls": { const { projectId, rest } = resolvePositionalProject(args); await rls(projectId, rest[0], rest[1]); break; }
338
+ case "delete": { const { projectId } = resolvePositionalProject(args); await deleteProject(projectId); break; }
339
+ case "pin": { const { projectId } = resolvePositionalProject(args); await pin(projectId); break; }
340
+ case "promote-user": { const { projectId, rest } = resolvePositionalProject(args); await promoteUser(projectId, rest[0]); break; }
341
+ case "demote-user": { const { projectId, rest } = resolvePositionalProject(args); await demoteUser(projectId, rest[0]); break; }
259
342
  default:
260
343
  console.error(`Unknown subcommand: ${sub}\n`);
261
344
  console.log(HELP);
package/lib/secrets.mjs CHANGED
@@ -22,6 +22,31 @@ Notes:
22
22
  - Values are write-only — list returns keys with a value_hash (first 8 hex chars of SHA-256) for verifying the correct value was set
23
23
  `;
24
24
 
25
+ const SUB_HELP = {
26
+ set: `run402 secrets set — Set a secret on a project
27
+
28
+ Usage:
29
+ run402 secrets set <id> <key> <value> [--file <path>]
30
+ run402 secrets set <id> <key> --file <path>
31
+
32
+ Arguments:
33
+ <id> Project ID (from 'run402 projects list')
34
+ <key> Secret key name (exposed as process.env.<key>)
35
+ <value> Inline secret value (omit if using --file)
36
+
37
+ Options:
38
+ --file <path> Read the secret value from a file instead of inline
39
+
40
+ Notes:
41
+ - Secrets are injected as process.env in serverless functions
42
+ - Values are write-only; 'list' returns a value_hash for verification
43
+
44
+ Examples:
45
+ run402 secrets set abc123 STRIPE_KEY sk-1234
46
+ run402 secrets set abc123 TLS_CERT --file cert.pem
47
+ `,
48
+ };
49
+
25
50
  async function set(projectId, key, args = []) {
26
51
  const p = findProject(projectId);
27
52
  let file = null;
@@ -69,7 +94,7 @@ async function deleteSecret(projectId, key) {
69
94
  export async function run(sub, args) {
70
95
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
71
96
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
72
- console.log(HELP);
97
+ console.log(SUB_HELP[sub] || HELP);
73
98
  process.exit(0);
74
99
  }
75
100
  switch (sub) {
@@ -117,6 +117,7 @@ async function inboundToggle(action, args) {
117
117
 
118
118
  export async function run(sub, args) {
119
119
  if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
120
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(HELP); process.exit(0); }
120
121
  switch (sub) {
121
122
  case "register": await register(args); break;
122
123
  case "status": await status(args); break;
package/lib/service.mjs CHANGED
@@ -17,15 +17,15 @@ async function fetchAndEmit(path) {
17
17
  try {
18
18
  res = await fetch(`${API}${path}`);
19
19
  } catch (err) {
20
- console.log(JSON.stringify({ error: "fetch_failed", message: err?.message || String(err) }));
21
- return;
20
+ console.error(JSON.stringify({ status: "error", message: err?.message || String(err) }));
21
+ process.exit(1);
22
22
  }
23
23
  const text = await res.text();
24
24
  let body;
25
25
  try { body = JSON.parse(text); } catch { body = text; }
26
26
  if (!res.ok) {
27
- console.log(JSON.stringify({ error: "non_2xx", status: res.status, body }, null, 2));
28
- return;
27
+ console.error(JSON.stringify({ status: "error", http: res.status, body }));
28
+ process.exit(1);
29
29
  }
30
30
  console.log(JSON.stringify(body, null, 2));
31
31
  }
package/lib/sites.mjs CHANGED
@@ -45,6 +45,41 @@ Notes:
45
45
  - Free with active tier — requires allowance auth
46
46
  `;
47
47
 
48
+ const SUB_HELP = {
49
+ deploy: `run402 sites deploy — Deploy a static site from a manifest
50
+
51
+ Usage:
52
+ run402 sites deploy --manifest <file> [--project <id>] [--target <target>] [--inherit]
53
+ cat manifest.json | run402 sites deploy [--project <id>] [--target <target>]
54
+
55
+ Options:
56
+ --manifest <file> Path to manifest JSON file (or read from stdin)
57
+ --project <id> Project ID (defaults to the active project)
58
+ --target <target> Deployment target (e.g. 'production')
59
+ --inherit Copy unchanged files from the previous deployment
60
+ (only upload changed files)
61
+
62
+ Manifest format (JSON):
63
+ {
64
+ "files": [
65
+ { "file": "index.html", "data": "<html>...</html>" },
66
+ { "file": "style.css", "path": "./dist/style.css" }
67
+ ]
68
+ }
69
+ Paths are resolved relative to the manifest file's directory.
70
+ Binary files are auto-detected and base64-encoded.
71
+
72
+ Notes:
73
+ - Must include at least index.html in the files array
74
+ - Free with active tier — requires allowance auth
75
+
76
+ Examples:
77
+ run402 sites deploy --manifest site.json
78
+ run402 sites deploy --manifest site.json --target production --inherit
79
+ cat site.json | run402 sites deploy
80
+ `,
81
+ };
82
+
48
83
  async function readStdin() {
49
84
  const chunks = [];
50
85
  for await (const chunk of process.stdin) chunks.push(chunk);
@@ -96,6 +131,7 @@ async function status(args) {
96
131
 
97
132
  export async function run(sub, args) {
98
133
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
134
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
99
135
  switch (sub) {
100
136
  case "deploy": await deploy(args); break;
101
137
  case "status": await status(args); break;
package/lib/storage.mjs CHANGED
@@ -25,6 +25,29 @@ Notes:
25
25
  - Upload reads from --file or stdin if no --file is given
26
26
  `;
27
27
 
28
+ const SUB_HELP = {
29
+ upload: `run402 storage upload — Upload a file to a project's storage bucket
30
+
31
+ Usage:
32
+ run402 storage upload <id> <bucket> <path> [--file <local>] [--content-type <mime>]
33
+ echo "..." | run402 storage upload <id> <bucket> <path> [--content-type <mime>]
34
+
35
+ Arguments:
36
+ <id> Project ID (from 'run402 projects list')
37
+ <bucket> Target bucket name
38
+ <path> Destination path within the bucket
39
+
40
+ Options:
41
+ --file <local> Local file to upload; if omitted, content is read from stdin
42
+ --content-type <mime> MIME type of the upload (default: text/plain)
43
+
44
+ Examples:
45
+ run402 storage upload abc123 assets logo.png --file ./logo.png \\
46
+ --content-type image/png
47
+ echo "hello" | run402 storage upload abc123 data notes.txt
48
+ `,
49
+ };
50
+
28
51
  async function readStdin() {
29
52
  const chunks = [];
30
53
  for await (const chunk of process.stdin) chunks.push(chunk);
@@ -88,6 +111,7 @@ async function list(projectId, bucket) {
88
111
 
89
112
  export async function run(sub, args) {
90
113
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
114
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
91
115
  switch (sub) {
92
116
  case "upload": await upload(args[0], args[1], args[2], args.slice(3)); break;
93
117
  case "download": await download(args[0], args[1], args[2]); break;
@@ -24,6 +24,31 @@ Notes:
24
24
  - Creates <name>.run402.com pointing to the deployment
25
25
  `;
26
26
 
27
+ const SUB_HELP = {
28
+ claim: `run402 subdomains claim — Claim a custom subdomain for a deployment
29
+
30
+ Usage:
31
+ run402 subdomains claim <name> [--project <id>] [--deployment <id>]
32
+
33
+ Arguments:
34
+ <name> Subdomain name (3-63 chars, lowercase alphanumeric +
35
+ hyphens). Creates <name>.run402.com.
36
+
37
+ Options:
38
+ --project <id> Project ID (defaults to the active project)
39
+ --deployment <id> Deployment ID to point at (defaults to the project's
40
+ last deployment)
41
+
42
+ Notes:
43
+ - Legacy syntax 'claim <deployment_id> <name>' is still supported
44
+ - Deploy a site first (or pass --deployment) so there is a target to claim
45
+
46
+ Examples:
47
+ run402 subdomains claim myapp
48
+ run402 subdomains claim myapp --deployment dpl_abc123 --project proj123
49
+ `,
50
+ };
51
+
27
52
  async function claim(positionalArgs, flagArgs) {
28
53
  const opts = { project: null, deployment: null };
29
54
  for (let i = 0; i < flagArgs.length; i++) {
@@ -85,6 +110,7 @@ async function list(projectId) {
85
110
 
86
111
  export async function run(sub, args) {
87
112
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
113
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
88
114
  switch (sub) {
89
115
  case "claim": {
90
116
  const positional = [];
package/lib/tier.mjs CHANGED
@@ -29,7 +29,14 @@ async function status() {
29
29
  const res = await fetch(`${API}/tiers/v1/status`, {
30
30
  headers: { ...authHeaders },
31
31
  });
32
- const data = await res.json();
32
+ const text = await res.text();
33
+ let data;
34
+ try {
35
+ data = JSON.parse(text);
36
+ } catch {
37
+ console.error(JSON.stringify({ status: "error", http: res.status, message: "Non-JSON response from server", body: text.slice(0, 500) }));
38
+ process.exit(1);
39
+ }
33
40
  if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
34
41
  console.log(JSON.stringify(data, null, 2));
35
42
  }
package/lib/webhooks.mjs CHANGED
@@ -21,6 +21,47 @@ Examples:
21
21
  run402 email webhooks delete whk_123
22
22
  `;
23
23
 
24
+ const SUB_HELP = {
25
+ update: `run402 email webhooks update — Update an existing webhook
26
+
27
+ Usage:
28
+ run402 email webhooks update <webhook_id> [--url <url>] [--events <e1,e2>] [--project <id>]
29
+
30
+ Arguments:
31
+ <webhook_id> Webhook ID to update
32
+
33
+ Options:
34
+ --url <url> New delivery URL for the webhook
35
+ --events <e1,e2> Comma-separated event list to replace the current events
36
+ Valid: delivery, bounced, complained, reply_received
37
+ --project <id> Project ID (defaults to the active project)
38
+
39
+ Notes:
40
+ - Provide at least one of --url or --events
41
+
42
+ Examples:
43
+ run402 email webhooks update whk_123 --url https://new.example.com/hook
44
+ run402 email webhooks update whk_123 --events delivery,bounced
45
+ `,
46
+ register: `run402 email webhooks register — Register a new webhook
47
+
48
+ Usage:
49
+ run402 email webhooks register --url <url> --events <e1,e2> [--project <id>]
50
+
51
+ Options:
52
+ --url <url> Delivery URL for the webhook (required)
53
+ --events <e1,e2> Comma-separated event list (required)
54
+ Valid: delivery, bounced, complained, reply_received
55
+ --project <id> Project ID (defaults to the active project)
56
+
57
+ Examples:
58
+ run402 email webhooks register --url https://example.com/hook \\
59
+ --events delivery,bounced
60
+ run402 email webhooks register --url https://example.com/hook \\
61
+ --events reply_received --project proj123
62
+ `,
63
+ };
64
+
24
65
  function parseFlag(args, flag) {
25
66
  for (let i = 0; i < args.length; i++) {
26
67
  if (args[i] === flag && args[i + 1]) return args[i + 1];
@@ -207,6 +248,7 @@ async function register(args) {
207
248
 
208
249
  export async function run(sub, args) {
209
250
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
251
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
210
252
  switch (sub) {
211
253
  case "list": await list(args); break;
212
254
  case "get": await get(args); break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.36.0",
3
+ "version": "1.36.1",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {