run402 1.36.0 → 1.37.0

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/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.37.0",
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": {