run402 1.54.0 → 1.54.2

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.
Files changed (64) hide show
  1. package/lib/agent.mjs +4 -2
  2. package/lib/ai.mjs +24 -10
  3. package/lib/allowance.mjs +53 -15
  4. package/lib/apps.mjs +13 -11
  5. package/lib/argparse.mjs +147 -0
  6. package/lib/auth.mjs +24 -9
  7. package/lib/billing.mjs +33 -17
  8. package/lib/blob.mjs +58 -26
  9. package/lib/cdn.mjs +3 -4
  10. package/lib/config.mjs +32 -8
  11. package/lib/contracts.mjs +38 -21
  12. package/lib/deploy-v2.mjs +38 -23
  13. package/lib/deploy.mjs +43 -44
  14. package/lib/domains.mjs +24 -8
  15. package/lib/email.mjs +38 -29
  16. package/lib/functions.mjs +77 -34
  17. package/lib/image.mjs +8 -2
  18. package/lib/message.mjs +4 -2
  19. package/lib/projects.mjs +115 -40
  20. package/lib/sdk-errors.mjs +66 -10
  21. package/lib/secrets.mjs +14 -8
  22. package/lib/sender-domain.mjs +11 -5
  23. package/lib/sites.mjs +9 -7
  24. package/lib/status.mjs +5 -2
  25. package/lib/subdomains.mjs +26 -11
  26. package/lib/tier.mjs +8 -2
  27. package/lib/webhooks.mjs +27 -13
  28. package/package.json +1 -1
  29. package/sdk/dist/index.d.ts +1 -0
  30. package/sdk/dist/index.d.ts.map +1 -1
  31. package/sdk/dist/index.js +5 -0
  32. package/sdk/dist/index.js.map +1 -1
  33. package/sdk/dist/namespaces/auth.d.ts +7 -0
  34. package/sdk/dist/namespaces/auth.d.ts.map +1 -1
  35. package/sdk/dist/namespaces/auth.js +24 -0
  36. package/sdk/dist/namespaces/auth.js.map +1 -1
  37. package/sdk/dist/namespaces/billing.d.ts +3 -0
  38. package/sdk/dist/namespaces/billing.d.ts.map +1 -1
  39. package/sdk/dist/namespaces/billing.js +6 -0
  40. package/sdk/dist/namespaces/billing.js.map +1 -1
  41. package/sdk/dist/namespaces/contracts.d.ts +3 -0
  42. package/sdk/dist/namespaces/contracts.d.ts.map +1 -1
  43. package/sdk/dist/namespaces/contracts.js +6 -0
  44. package/sdk/dist/namespaces/contracts.js.map +1 -1
  45. package/sdk/dist/namespaces/email.d.ts +4 -0
  46. package/sdk/dist/namespaces/email.d.ts.map +1 -1
  47. package/sdk/dist/namespaces/email.js +8 -0
  48. package/sdk/dist/namespaces/email.js.map +1 -1
  49. package/sdk/dist/namespaces/projects.d.ts +14 -0
  50. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  51. package/sdk/dist/namespaces/projects.js +72 -0
  52. package/sdk/dist/namespaces/projects.js.map +1 -1
  53. package/sdk/dist/namespaces/sender-domain.d.ts +2 -0
  54. package/sdk/dist/namespaces/sender-domain.d.ts.map +1 -1
  55. package/sdk/dist/namespaces/sender-domain.js +4 -0
  56. package/sdk/dist/namespaces/sender-domain.js.map +1 -1
  57. package/sdk/dist/scoped.d.ts +8 -1
  58. package/sdk/dist/scoped.d.ts.map +1 -1
  59. package/sdk/dist/scoped.js +21 -0
  60. package/sdk/dist/scoped.js.map +1 -1
  61. package/core-dist/wallet-auth.js +0 -62
  62. package/core-dist/wallet.js +0 -25
  63. package/sdk/core-dist/wallet-auth.js +0 -62
  64. package/sdk/core-dist/wallet.js +0 -25
package/lib/projects.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { findProject, loadKeyStore, API, allowanceAuthHeaders, resolveProjectId, getActiveProjectId } from "./config.mjs";
3
3
  import { getSdk } from "./sdk.mjs";
4
- import { reportSdkError } from "./sdk-errors.mjs";
4
+ import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
5
+ import { assertKnownFlags, failBadProjectId, hasHelp, normalizeArgv, positionalArgs } from "./argparse.mjs";
5
6
 
6
7
  const HELP = `run402 projects — Manage your deployed Run402 projects
7
8
 
@@ -33,17 +34,17 @@ Examples:
33
34
  run402 projects provision --tier hobby --name my-app
34
35
  run402 projects use prj_abc123
35
36
  run402 projects list
36
- run402 projects info abc123
37
- run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
38
- run402 projects sql abc123 "SELECT * FROM users WHERE id = $1" --params '[42]'
39
- run402 projects sql abc123 --file setup.sql
40
- run402 projects rest abc123 users "limit=10&select=id,name"
41
- run402 projects usage abc123
42
- run402 projects schema abc123
43
- run402 projects apply-expose abc123 --file manifest.json
44
- run402 projects get-expose abc123
45
- run402 projects keys abc123
46
- run402 projects delete abc123 --confirm
37
+ run402 projects info prj_abc123
38
+ run402 projects sql prj_abc123 "SELECT * FROM users LIMIT 5"
39
+ run402 projects sql prj_abc123 "SELECT * FROM users WHERE id = $1" --params '[42]'
40
+ run402 projects sql prj_abc123 --file setup.sql
41
+ run402 projects rest prj_abc123 users "limit=10&select=id,name"
42
+ run402 projects usage prj_abc123
43
+ run402 projects schema prj_abc123
44
+ run402 projects apply-expose prj_abc123 --file manifest.json
45
+ run402 projects get-expose prj_abc123
46
+ run402 projects keys prj_abc123
47
+ run402 projects delete prj_abc123 --confirm
47
48
 
48
49
  Notes:
49
50
  - <id> is the project_id shown in 'run402 projects list' (prefix: 'prj_')
@@ -103,9 +104,9 @@ Options:
103
104
  --params '<json>' JSON array of parameters for a parameterized query
104
105
 
105
106
  Examples:
106
- run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
107
- run402 projects sql abc123 "SELECT * FROM users WHERE id = $1" --params '[42]'
108
- run402 projects sql abc123 --file setup.sql
107
+ run402 projects sql prj_abc123 "SELECT * FROM users LIMIT 5"
108
+ run402 projects sql prj_abc123 "SELECT * FROM users WHERE id = $1" --params '[42]'
109
+ run402 projects sql prj_abc123 --file setup.sql
109
110
  `,
110
111
  };
111
112
 
@@ -153,12 +154,21 @@ async function applyExpose(projectId, args = []) {
153
154
  }
154
155
  const raw = file ? readFileSync(file, "utf-8") : inline;
155
156
  if (!raw) {
156
- console.error(JSON.stringify({ status: "error", message: "Missing manifest. Provide inline JSON or use --file <path>" }));
157
- process.exit(1);
157
+ fail({
158
+ code: "BAD_USAGE",
159
+ message: "Missing manifest.",
160
+ hint: "Provide inline JSON or use --file <path>",
161
+ });
158
162
  }
159
163
  let manifest;
160
164
  try { manifest = JSON.parse(raw); }
161
- catch { console.error(JSON.stringify({ status: "error", message: "Invalid JSON for manifest" })); process.exit(1); }
165
+ catch (err) {
166
+ fail({
167
+ code: "BAD_USAGE",
168
+ message: "Invalid JSON for manifest",
169
+ details: { parse_error: err.message },
170
+ });
171
+ }
162
172
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/expose`, {
163
173
  method: "POST",
164
174
  headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
@@ -222,11 +232,22 @@ async function sqlCmd(projectId, args = []) {
222
232
  else if (!query && !args[i].startsWith("--")) { query = args[i]; }
223
233
  }
224
234
  const sql = file ? readFileSync(file, "utf-8") : query;
225
- if (!sql) { console.error(JSON.stringify({ status: "error", message: "Missing SQL query. Provide inline or use --file <path>" })); process.exit(1); }
235
+ if (!sql) {
236
+ fail({
237
+ code: "BAD_USAGE",
238
+ message: "Missing SQL query.",
239
+ hint: "Provide inline or use --file <path>",
240
+ });
241
+ }
226
242
  let params;
227
243
  if (paramsRaw) {
228
- try { params = JSON.parse(paramsRaw); } catch { console.error(JSON.stringify({ status: "error", message: "Invalid JSON for --params. Expected a JSON array, e.g. '[42, \"hello\"]'" })); process.exit(1); }
229
- if (!Array.isArray(params)) { console.error(JSON.stringify({ status: "error", message: "--params must be a JSON array, e.g. '[42, \"hello\"]'" })); process.exit(1); }
244
+ params = parseFlagJson("--params", paramsRaw);
245
+ if (!Array.isArray(params)) {
246
+ fail({
247
+ code: "BAD_USAGE",
248
+ message: "--params must be a JSON array, e.g. '[42, \"hello\"]'",
249
+ });
250
+ }
230
251
  }
231
252
  const useParams = params && params.length > 0;
232
253
  const headers = { "Authorization": `Bearer ${p.service_key}`, "Content-Type": useParams ? "application/json" : "text/plain" };
@@ -264,7 +285,13 @@ async function schema(projectId) {
264
285
  }
265
286
 
266
287
  async function use(projectId) {
267
- if (!projectId) { console.error("Usage: run402 projects use <project_id>"); process.exit(1); }
288
+ if (!projectId) {
289
+ fail({
290
+ code: "BAD_USAGE",
291
+ message: "Missing <project_id>.",
292
+ hint: "run402 projects use <project_id>",
293
+ });
294
+ }
268
295
  try {
269
296
  await getSdk().projects.use(projectId);
270
297
  console.log(JSON.stringify({ status: "ok", active_project_id: projectId }));
@@ -274,7 +301,13 @@ async function use(projectId) {
274
301
  }
275
302
 
276
303
  async function pin(projectId) {
277
- if (!projectId) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 projects pin <project_id>" })); process.exit(1); }
304
+ if (!projectId) {
305
+ fail({
306
+ code: "BAD_USAGE",
307
+ message: "Missing <project_id>.",
308
+ hint: "run402 projects pin <project_id>",
309
+ });
310
+ }
278
311
  try {
279
312
  const data = await getSdk().projects.pin(projectId);
280
313
  console.log(JSON.stringify(data, null, 2));
@@ -284,7 +317,13 @@ async function pin(projectId) {
284
317
  }
285
318
 
286
319
  async function promoteUser(projectId, email) {
287
- if (!email) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 projects promote-user <project_id> <email>" })); process.exit(1); }
320
+ if (!email) {
321
+ fail({
322
+ code: "BAD_USAGE",
323
+ message: "Missing <email>.",
324
+ hint: "run402 projects promote-user <project_id> <email>",
325
+ });
326
+ }
288
327
  const p = findProject(projectId);
289
328
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/promote-user`, {
290
329
  method: "POST",
@@ -297,7 +336,13 @@ async function promoteUser(projectId, email) {
297
336
  }
298
337
 
299
338
  async function demoteUser(projectId, email) {
300
- if (!email) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 projects demote-user <project_id> <email>" })); process.exit(1); }
339
+ if (!email) {
340
+ fail({
341
+ code: "BAD_USAGE",
342
+ message: "Missing <email>.",
343
+ hint: "run402 projects demote-user <project_id> <email>",
344
+ });
345
+ }
301
346
  const p = findProject(projectId);
302
347
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/demote-user`, {
303
348
  method: "POST",
@@ -329,42 +374,72 @@ async function deleteProject(projectId, args = []) {
329
374
  }
330
375
 
331
376
  // Resolve a positional project_id argument with active-project fallback (GH-102).
332
- // Heuristic: real project IDs start with "prj_". If args[0] is missing OR
333
- // doesn't start with "prj_", fall back to the active project and return the
334
- // full args array as remaining positionals. Otherwise consume args[0] as the
335
- // project_id and return args.slice(1) as remaining positionals.
336
- function resolvePositionalProject(args) {
377
+ // Callers can tighten the legacy shorthand when a bare non-prj positional is
378
+ // more likely a mistyped project id than an argument for the active project.
379
+ function resolvePositionalProject(args, opts = {}) {
337
380
  const first = Array.isArray(args) ? args[0] : undefined;
338
381
  if (typeof first === "string" && first.startsWith("prj_")) {
339
382
  return { projectId: first, rest: args.slice(1) };
340
383
  }
384
+ if (
385
+ typeof first === "string" &&
386
+ first.length > 0 &&
387
+ !first.startsWith("-") &&
388
+ Array.isArray(opts.rejectBareFirstWhenFlagPresent) &&
389
+ opts.rejectBareFirstWhenFlagPresent.some((flag) => args.includes(flag))
390
+ ) {
391
+ failBadProjectId(first);
392
+ }
393
+ if (typeof first === "string" && first.length > 0 && !first.startsWith("-") && opts.rejectBareFirst) {
394
+ failBadProjectId(first);
395
+ }
396
+ if (typeof first === "string" && first.length > 0 && !first.startsWith("-") && opts.maxBarePositionals !== undefined) {
397
+ const bare = positionalArgs(args, opts.valueFlags ?? []);
398
+ if (bare.length > opts.maxBarePositionals) {
399
+ failBadProjectId(first);
400
+ }
401
+ }
341
402
  return { projectId: resolveProjectId(null), rest: Array.isArray(args) ? args : [] };
342
403
  }
343
404
 
405
+ const FLAGS_BY_SUB = {
406
+ provision: { known: ["--tier", "--name"], values: ["--tier", "--name"] },
407
+ sql: { known: ["--file", "--params"], values: ["--file", "--params"] },
408
+ "apply-expose": { known: ["--file"], values: ["--file"] },
409
+ delete: { known: ["--confirm"], values: [] },
410
+ };
411
+
412
+ function validateFlags(sub, args) {
413
+ const spec = FLAGS_BY_SUB[sub] ?? { known: [], values: [] };
414
+ assertKnownFlags(args, [...spec.known, "--help", "-h"], spec.values);
415
+ }
416
+
344
417
  export async function run(sub, args) {
345
418
  if (!sub || sub === '--help' || sub === '-h') {
346
419
  console.log(HELP);
347
420
  process.exit(0);
348
421
  }
349
- if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
422
+ args = normalizeArgv(args);
423
+ if (Array.isArray(args) && hasHelp(args)) {
350
424
  console.log(SUB_HELP[sub] || HELP);
351
425
  process.exit(0);
352
426
  }
427
+ validateFlags(sub, args);
353
428
  switch (sub) {
354
429
  case "quote": await quote(); break;
355
430
  case "provision": await provision(args); break;
356
431
  case "use": await use(args[0]); break;
357
432
  case "list": await list(); break;
358
- case "info": { const { projectId } = resolvePositionalProject(args); await info(projectId); break; }
359
- case "keys": { const { projectId } = resolvePositionalProject(args); await keys(projectId); break; }
360
- case "sql": { const { projectId, rest } = resolvePositionalProject(args); await sqlCmd(projectId, rest); break; }
433
+ case "info": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await info(projectId); break; }
434
+ case "keys": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await keys(projectId); break; }
435
+ case "sql": { const { projectId, rest } = resolvePositionalProject(args, { maxBarePositionals: 1, valueFlags: FLAGS_BY_SUB.sql.values, rejectBareFirstWhenFlagPresent: ["--file"] }); await sqlCmd(projectId, rest); break; }
361
436
  case "rest": { const { projectId, rest: restArgs } = resolvePositionalProject(args); await rest(projectId, restArgs[0], restArgs[1]); break; }
362
- case "usage": { const { projectId } = resolvePositionalProject(args); await usage(projectId); break; }
363
- case "schema": { const { projectId } = resolvePositionalProject(args); await schema(projectId); break; }
364
- case "apply-expose": { const { projectId, rest } = resolvePositionalProject(args); await applyExpose(projectId, rest); break; }
365
- case "get-expose": { const { projectId } = resolvePositionalProject(args); await getExpose(projectId); break; }
366
- case "delete": { const { projectId, rest } = resolvePositionalProject(args); await deleteProject(projectId, rest); break; }
367
- case "pin": { const { projectId } = resolvePositionalProject(args); await pin(projectId); break; }
437
+ case "usage": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await usage(projectId); break; }
438
+ case "schema": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await schema(projectId); break; }
439
+ case "apply-expose": { const { projectId, rest } = resolvePositionalProject(args, { maxBarePositionals: 1, valueFlags: FLAGS_BY_SUB["apply-expose"].values, rejectBareFirstWhenFlagPresent: ["--file"] }); await applyExpose(projectId, rest); break; }
440
+ case "get-expose": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await getExpose(projectId); break; }
441
+ case "delete": { const { projectId, rest } = resolvePositionalProject(args, { rejectBareFirst: true }); await deleteProject(projectId, rest); break; }
442
+ case "pin": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await pin(projectId); break; }
368
443
  case "promote-user": { const { projectId, rest } = resolvePositionalProject(args); await promoteUser(projectId, rest[0]); break; }
369
444
  case "demote-user": { const { projectId, rest } = resolvePositionalProject(args); await demoteUser(projectId, rest[0]); break; }
370
445
  default:
@@ -1,25 +1,81 @@
1
1
  /**
2
2
  * CLI-side SDK error translator.
3
3
  *
4
- * Maps SDK `Run402Error` subclasses into the CLI's existing error output
5
- * format: `{status: "error", http: ?, ...bodyFields}` on stderr, `process.exit(1)`.
6
- * Preserves specific behaviors:
7
- * - `ProjectNotFound` → plain-text "Project <id> not found in local registry"
8
- * with the "Hint: project IDs start with prj_" guidance when the id
9
- * doesn't start with `prj_`.
4
+ * Maps SDK `Run402Error` subclasses into the CLI's canonical error envelope:
5
+ * `{status: "error", code, message, retryable, safe_to_retry, ...}` on stderr,
6
+ * `process.exit(1)`. Preserves specific behaviors:
7
+ * - `ProjectNotFound` → canonical envelope with `code: "PROJECT_NOT_FOUND"`
8
+ * and `details.source: "local_registry"` so callers can distinguish the
9
+ * local-registry miss from a gateway 404.
10
10
  * - HTML / non-JSON error bodies → `body_preview` field (first 500 chars),
11
11
  * matching GH-84 behavior.
12
12
  * - Network errors → `{status: "error", message: "..."}`.
13
+ *
14
+ * For client-side validation failures (missing flags, bad JSON, no-op
15
+ * environments), use `fail()` instead — `reportSdkError` is strictly for
16
+ * thrown `Run402Error` instances.
13
17
  */
14
18
 
19
+ /**
20
+ * Canonical client-side failure emitter.
21
+ *
22
+ * Writes a single JSON envelope to stderr and exits with `exit_code` (default 1).
23
+ * The envelope shape matches the gateway's structured error contract so callers
24
+ * branching on `code` / `retryable` / `safe_to_retry` work uniformly across
25
+ * client-side validation errors and SDK-thrown errors.
26
+ *
27
+ * `code` defaults to "BAD_USAGE" so the helper is safe to call without one.
28
+ * `retryable: false` and `safe_to_retry: true` are sane defaults for client-side
29
+ * validation: the call wasn't sent, so retrying is safe and won't help unless
30
+ * the user fixes input.
31
+ */
32
+ export function fail({ message, code, hint, details, next_actions, retryable = false, safe_to_retry = true, exit_code = 1 } = {}) {
33
+ const envelope = {
34
+ status: "error",
35
+ code: code ?? "BAD_USAGE",
36
+ message,
37
+ retryable,
38
+ safe_to_retry,
39
+ };
40
+ if (hint !== undefined) envelope.hint = hint;
41
+ if (details !== undefined) envelope.details = details;
42
+ envelope.next_actions = Array.isArray(next_actions) ? next_actions : [];
43
+ envelope.trace_id = null;
44
+ console.error(JSON.stringify(envelope));
45
+ process.exit(exit_code);
46
+ }
47
+
48
+ /**
49
+ * Parse a JSON-bearing CLI flag value, naming the flag in the failure envelope.
50
+ *
51
+ * Wraps `JSON.parse` so the failure says which flag was bad and includes a
52
+ * truncated value preview, instead of leaking a raw V8 `JSON.parse` message
53
+ * that doesn't tell the caller which flag failed.
54
+ */
55
+ export function parseFlagJson(name, value) {
56
+ try {
57
+ return JSON.parse(value);
58
+ } catch (e) {
59
+ fail({
60
+ code: "BAD_JSON_FLAG",
61
+ message: `${name} value is not valid JSON`,
62
+ details: { flag: name, value_preview: String(value).slice(0, 32), parse_error: e.message },
63
+ });
64
+ }
65
+ }
66
+
15
67
  export function reportSdkError(err) {
16
68
  if (err?.name === "ProjectNotFound") {
17
69
  const id = err.projectId || "";
18
70
  const hint = id && !String(id).startsWith("prj_")
19
- ? ` Hint: project IDs start with "prj_". Check that the argument order is <project_id> <name>.`
20
- : "";
21
- console.error(`Project ${id} not found in local registry.${hint}`);
22
- process.exit(1);
71
+ ? `project IDs start with "prj_". Check that the argument order is <project_id> <name>.`
72
+ : undefined;
73
+ fail({
74
+ code: "PROJECT_NOT_FOUND",
75
+ message: `Project ${id} not found in local registry.`,
76
+ hint,
77
+ details: { project_id: id, source: "local_registry" },
78
+ });
23
79
  }
24
80
 
25
81
  const payload = { status: "error" };
package/lib/secrets.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { getSdk } from "./sdk.mjs";
3
- import { reportSdkError } from "./sdk-errors.mjs";
3
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
4
4
 
5
5
  const HELP = `run402 secrets — Manage project secrets
6
6
 
@@ -13,10 +13,10 @@ Subcommands:
13
13
  delete <id> <key> Delete a secret from a project
14
14
 
15
15
  Examples:
16
- run402 secrets set abc123 STRIPE_KEY sk-1234
17
- run402 secrets set abc123 TLS_CERT --file cert.pem
18
- run402 secrets list abc123
19
- run402 secrets delete abc123 STRIPE_KEY
16
+ run402 secrets set prj_abc123 STRIPE_KEY sk-1234
17
+ run402 secrets set prj_abc123 TLS_CERT --file cert.pem
18
+ run402 secrets list prj_abc123
19
+ run402 secrets delete prj_abc123 STRIPE_KEY
20
20
 
21
21
  Notes:
22
22
  - Secrets are injected as process.env in serverless functions
@@ -43,8 +43,8 @@ Notes:
43
43
  - Values are write-only; 'list' returns a value_hash for verification
44
44
 
45
45
  Examples:
46
- run402 secrets set abc123 STRIPE_KEY sk-1234
47
- run402 secrets set abc123 TLS_CERT --file cert.pem
46
+ run402 secrets set prj_abc123 STRIPE_KEY sk-1234
47
+ run402 secrets set prj_abc123 TLS_CERT --file cert.pem
48
48
  `,
49
49
  };
50
50
 
@@ -56,7 +56,13 @@ async function set(projectId, key, args = []) {
56
56
  else if (!value && !args[i].startsWith("--")) { value = args[i]; }
57
57
  }
58
58
  const val = file ? readFileSync(file, "utf-8") : value;
59
- if (!val) { console.error(JSON.stringify({ status: "error", message: "Missing secret value. Provide inline or use --file <path>" })); process.exit(1); }
59
+ if (!val) {
60
+ fail({
61
+ code: "BAD_USAGE",
62
+ message: "Missing secret value.",
63
+ hint: "Provide inline or use --file <path>",
64
+ });
65
+ }
60
66
  try {
61
67
  await getSdk().secrets.set(projectId, key, val);
62
68
  console.log(JSON.stringify({ status: "ok", message: `Secret '${key}' set for project ${projectId}.` }));
@@ -1,6 +1,6 @@
1
1
  import { resolveProjectId } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
- import { reportSdkError } from "./sdk-errors.mjs";
3
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
4
4
 
5
5
  const HELP = `run402 sender-domain — Manage custom email sender domain
6
6
 
@@ -39,8 +39,11 @@ async function register(args) {
39
39
  const projectId = resolveProjectId(projectOpt);
40
40
 
41
41
  if (!domain) {
42
- console.error(JSON.stringify({ status: "error", message: "Missing domain. Usage: run402 sender-domain register <domain>" }));
43
- process.exit(1);
42
+ fail({
43
+ code: "BAD_USAGE",
44
+ message: "Missing domain.",
45
+ hint: "run402 sender-domain register <domain>",
46
+ });
44
47
  }
45
48
 
46
49
  try {
@@ -81,8 +84,11 @@ async function inboundToggle(action, args) {
81
84
  const projectId = resolveProjectId(projectOpt);
82
85
 
83
86
  if (!domain) {
84
- console.error(JSON.stringify({ status: "error", message: `Missing domain. Usage: run402 sender-domain inbound-${action} <domain>` }));
85
- process.exit(1);
87
+ fail({
88
+ code: "BAD_USAGE",
89
+ message: "Missing domain.",
90
+ hint: `run402 sender-domain inbound-${action} <domain>`,
91
+ });
86
92
  }
87
93
 
88
94
  try {
package/lib/sites.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileSetFromDir } from "#sdk/node";
5
5
  import { allowanceAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
6
6
  import { resolveFilePathsInManifest } from "./manifest.mjs";
7
7
  import { getSdk } from "./sdk.mjs";
8
- import { reportSdkError } from "./sdk-errors.mjs";
8
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
9
9
 
10
10
  const SMALL_DIR_THRESHOLD = 5;
11
11
 
@@ -244,17 +244,19 @@ async function deployDir(args) {
244
244
  if (args[i] === "--dry-run") { opts.dryRun = true; continue; }
245
245
  if (args[i] === "--confirm-prune") { opts.confirmPrune = true; continue; }
246
246
  if (args[i] === "--inherit") {
247
- console.error(JSON.stringify({
248
- status: "error",
247
+ fail({
248
+ code: "BAD_USAGE",
249
249
  message: "--inherit is removed; the SDK now uploads only changed files automatically.",
250
- }));
251
- process.exit(1);
250
+ });
252
251
  }
253
252
  if (!args[i].startsWith("-") && opts.dir === null) { opts.dir = args[i]; continue; }
254
253
  }
255
254
  if (!opts.dir) {
256
- console.error(JSON.stringify({ status: "error", message: "Missing <path>. Usage: run402 sites deploy-dir <path> --project <id>" }));
257
- process.exit(1);
255
+ fail({
256
+ code: "BAD_USAGE",
257
+ message: "Missing <path>.",
258
+ hint: "run402 sites deploy-dir <path> --project <id>",
259
+ });
258
260
  }
259
261
  const projectId = resolveProjectId(opts.project);
260
262
 
package/lib/status.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readAllowance, loadKeyStore, getActiveProjectId, API } from "./config.mjs";
2
2
  import { getAllowanceAuthHeaders } from "../core-dist/allowance-auth.js";
3
+ import { assertKnownFlags, hasHelp, normalizeArgv } from "./argparse.mjs";
3
4
 
4
5
  const HELP = `run402 status — Show full account state in one shot
5
6
 
@@ -75,11 +76,13 @@ function normalizeProject(raw) {
75
76
  }
76
77
 
77
78
  export async function run(args = []) {
78
- if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
79
+ args = normalizeArgv(args);
80
+ if (hasHelp(args)) { console.log(HELP); process.exit(0); }
81
+ assertKnownFlags(args, ["--help", "-h"]);
79
82
  const allowance = readAllowance();
80
83
  if (!allowance) {
81
84
  console.log(JSON.stringify({ status: "no_allowance", message: "No agent allowance found. Run: run402 init" }));
82
- return;
85
+ process.exit(1);
83
86
  }
84
87
 
85
88
  const wallet = allowance.address.toLowerCase();
@@ -1,6 +1,6 @@
1
1
  import { resolveProject, resolveProjectId } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
- import { reportSdkError } from "./sdk-errors.mjs";
3
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
4
4
 
5
5
  const HELP = `run402 subdomains — Manage custom subdomains
6
6
 
@@ -17,7 +17,7 @@ Legacy syntax 'claim <deployment_id> <name>' is still supported.
17
17
 
18
18
  Examples:
19
19
  run402 subdomains claim myapp
20
- run402 subdomains claim myapp --deployment dpl_abc123 --project proj123
20
+ run402 subdomains claim myapp --deployment dpl_abc123 --project prj_abc123
21
21
  run402 subdomains delete myapp --confirm
22
22
  run402 subdomains list
23
23
 
@@ -47,7 +47,7 @@ Notes:
47
47
 
48
48
  Examples:
49
49
  run402 subdomains claim myapp
50
- run402 subdomains claim myapp --deployment dpl_abc123 --project proj123
50
+ run402 subdomains claim myapp --deployment dpl_abc123 --project prj_abc123
51
51
  `,
52
52
  };
53
53
 
@@ -64,11 +64,25 @@ async function claim(positionalArgs, flagArgs) {
64
64
  } else if (positionalArgs.length === 1) {
65
65
  name = positionalArgs[0];
66
66
  }
67
- if (!name) { console.error("Usage: run402 subdomains claim <name> [--project <id>] [--deployment <id>]"); process.exit(1); }
67
+ if (!name) {
68
+ fail({
69
+ code: "BAD_USAGE",
70
+ message: "Missing <name>.",
71
+ hint: "run402 subdomains claim <name> [--project <id>] [--deployment <id>]",
72
+ });
73
+ }
68
74
  const projectId = resolveProjectId(opts.project);
69
75
  const p = resolveProject(opts.project);
70
76
  deploymentId = opts.deployment || deploymentId || p.last_deployment_id;
71
- if (!deploymentId) { console.error("Error: no deployment_id specified and no recent deployment found. Deploy a site first or pass --deployment <id>."); process.exit(1); }
77
+ if (!deploymentId) {
78
+ fail({
79
+ code: "NO_DEPLOYMENT",
80
+ message: "no deployment_id specified and no recent deployment found.",
81
+ hint: "Deploy a site first or pass --deployment <id>.",
82
+ details: { project_id: projectId },
83
+ next_actions: [{ action: "deploy_site_first" }],
84
+ });
85
+ }
72
86
  try {
73
87
  const data = await getSdk().subdomains.claim(name, deploymentId, { projectId });
74
88
  console.log(JSON.stringify(data, null, 2));
@@ -86,17 +100,18 @@ async function deleteSubdomain(allArgs) {
86
100
  else if (!argList[i].startsWith("--") && !name) { name = argList[i]; }
87
101
  }
88
102
  if (!name) {
89
- console.error(JSON.stringify({ status: "error", message: "Usage: run402 subdomains delete <name> --confirm [--project <id>]" }));
90
- process.exit(1);
103
+ fail({
104
+ code: "BAD_USAGE",
105
+ message: "Missing <name>.",
106
+ hint: "run402 subdomains delete <name> --confirm [--project <id>]",
107
+ });
91
108
  }
92
109
  if (!argList.includes("--confirm")) {
93
- console.error(JSON.stringify({
94
- status: "error",
110
+ fail({
95
111
  code: "CONFIRMATION_REQUIRED",
96
112
  message: `Destructive: releasing subdomain '${name}' makes it available for any other project to claim. This is irreversible. Re-run with --confirm to proceed.`,
97
113
  details: { name },
98
- }));
99
- process.exit(1);
114
+ });
100
115
  }
101
116
  const projectId = resolveProjectId(opts.project);
102
117
  try {
package/lib/tier.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getSdk } from "./sdk.mjs";
2
- import { reportSdkError } from "./sdk-errors.mjs";
2
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
3
3
 
4
4
  const HELP = `run402 tier — Manage your Run402 tier subscription
5
5
 
@@ -34,7 +34,13 @@ async function status() {
34
34
  }
35
35
 
36
36
  async function set(tierName) {
37
- if (!tierName) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 tier set <prototype|hobby|team>" })); process.exit(1); }
37
+ if (!tierName) {
38
+ fail({
39
+ code: "BAD_USAGE",
40
+ message: "Missing <tier>.",
41
+ hint: "run402 tier set <prototype|hobby|team>",
42
+ });
43
+ }
38
44
  try {
39
45
  const data = await getSdk().tier.set(tierName);
40
46
  console.log(JSON.stringify(data, null, 2));
package/lib/webhooks.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { resolveProjectId } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
- import { reportSdkError } from "./sdk-errors.mjs";
3
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
4
4
 
5
5
  const HELP = `run402 email webhooks — Manage mailbox webhooks
6
6
 
@@ -62,8 +62,11 @@ async function get(args) {
62
62
  }
63
63
  const projectId = resolveProjectId(projectOpt);
64
64
  if (!webhookId) {
65
- console.error(JSON.stringify({ status: "error", message: "Missing webhook_id. Usage: run402 email webhooks get <webhook_id>" }));
66
- process.exit(1);
65
+ fail({
66
+ code: "BAD_USAGE",
67
+ message: "Missing webhook_id.",
68
+ hint: "run402 email webhooks get <webhook_id>",
69
+ });
67
70
  }
68
71
  try {
69
72
  const data = await getSdk().email.webhooks.get(projectId, webhookId);
@@ -82,8 +85,11 @@ async function del(args) {
82
85
  }
83
86
  const projectId = resolveProjectId(projectOpt);
84
87
  if (!webhookId) {
85
- console.error(JSON.stringify({ status: "error", message: "Missing webhook_id. Usage: run402 email webhooks delete <webhook_id>" }));
86
- process.exit(1);
88
+ fail({
89
+ code: "BAD_USAGE",
90
+ message: "Missing webhook_id.",
91
+ hint: "run402 email webhooks delete <webhook_id>",
92
+ });
87
93
  }
88
94
  try {
89
95
  await getSdk().email.webhooks.delete(projectId, webhookId);
@@ -106,12 +112,14 @@ async function update(args) {
106
112
  }
107
113
  const projectId = resolveProjectId(projectOpt);
108
114
  if (!webhookId) {
109
- console.error(JSON.stringify({ status: "error", message: "Missing webhook_id. Usage: run402 email webhooks update <webhook_id> [--url <url>] [--events <e1,e2>]" }));
110
- process.exit(1);
115
+ fail({
116
+ code: "BAD_USAGE",
117
+ message: "Missing webhook_id.",
118
+ hint: "run402 email webhooks update <webhook_id> [--url <url>] [--events <e1,e2>]",
119
+ });
111
120
  }
112
121
  if (!url && !eventsRaw) {
113
- console.error(JSON.stringify({ status: "error", message: "Provide at least --url or --events" }));
114
- process.exit(1);
122
+ fail({ code: "BAD_USAGE", message: "Provide at least --url or --events" });
115
123
  }
116
124
 
117
125
  try {
@@ -132,12 +140,18 @@ async function register(args) {
132
140
  const projectId = resolveProjectId(projectOpt);
133
141
 
134
142
  if (!url) {
135
- console.error(JSON.stringify({ status: "error", message: "Missing --url. Usage: run402 email webhooks register --url <url> --events <e1,e2>" }));
136
- process.exit(1);
143
+ fail({
144
+ code: "BAD_USAGE",
145
+ message: "Missing --url.",
146
+ hint: "run402 email webhooks register --url <url> --events <e1,e2>",
147
+ });
137
148
  }
138
149
  if (!eventsRaw) {
139
- console.error(JSON.stringify({ status: "error", message: "Missing --events. Valid events: delivery, bounced, complained, reply_received" }));
140
- process.exit(1);
150
+ fail({
151
+ code: "BAD_USAGE",
152
+ message: "Missing --events.",
153
+ hint: "Valid events: delivery, bounced, complained, reply_received",
154
+ });
141
155
  }
142
156
 
143
157
  const events = eventsRaw.split(",").map((e) => e.trim());