run402 1.53.1 → 1.54.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.
Files changed (52) hide show
  1. package/core-dist/keystore.js +65 -21
  2. package/lib/agent.mjs +4 -2
  3. package/lib/ai.mjs +18 -4
  4. package/lib/allowance.mjs +53 -15
  5. package/lib/apps.mjs +4 -2
  6. package/lib/auth.mjs +22 -7
  7. package/lib/billing.mjs +33 -17
  8. package/lib/blob.mjs +3 -4
  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 +15 -5
  17. package/lib/image.mjs +8 -2
  18. package/lib/init.mjs +30 -6
  19. package/lib/message.mjs +4 -2
  20. package/lib/projects.mjs +64 -13
  21. package/lib/sdk-errors.mjs +66 -10
  22. package/lib/secrets.mjs +8 -2
  23. package/lib/sender-domain.mjs +11 -5
  24. package/lib/sites.mjs +9 -7
  25. package/lib/status.mjs +1 -1
  26. package/lib/subdomains.mjs +24 -9
  27. package/lib/tier.mjs +8 -2
  28. package/lib/webhooks.mjs +27 -13
  29. package/package.json +1 -1
  30. package/sdk/core-dist/keystore.js +65 -21
  31. package/sdk/dist/errors.d.ts +18 -0
  32. package/sdk/dist/errors.d.ts.map +1 -1
  33. package/sdk/dist/errors.js +34 -6
  34. package/sdk/dist/errors.js.map +1 -1
  35. package/sdk/dist/index.d.ts.map +1 -1
  36. package/sdk/dist/index.js +13 -0
  37. package/sdk/dist/index.js.map +1 -1
  38. package/sdk/dist/namespaces/auth.d.ts.map +1 -1
  39. package/sdk/dist/namespaces/auth.js +10 -1
  40. package/sdk/dist/namespaces/auth.js.map +1 -1
  41. package/sdk/dist/namespaces/deploy.d.ts +5 -4
  42. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  43. package/sdk/dist/namespaces/deploy.js +13 -7
  44. package/sdk/dist/namespaces/deploy.js.map +1 -1
  45. package/sdk/dist/namespaces/subdomains.d.ts +2 -1
  46. package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
  47. package/sdk/dist/namespaces/subdomains.js +22 -17
  48. package/sdk/dist/namespaces/subdomains.js.map +1 -1
  49. package/core-dist/wallet-auth.js +0 -62
  50. package/core-dist/wallet.js +0 -25
  51. package/sdk/core-dist/wallet-auth.js +0 -62
  52. package/sdk/core-dist/wallet.js +0 -25
package/lib/init.mjs CHANGED
@@ -10,12 +10,23 @@ const TEMPO_RPC = "https://rpc.moderato.tempo.xyz/";
10
10
  const HELP = `run402 init — Set up allowance, funding, and check tier status
11
11
 
12
12
  Usage:
13
- run402 init Set up with x402 (Base Sepolia) — default
14
- run402 init mpp Set up with MPP (Tempo Moderato)
15
- run402 init --json Same as init, but emit a JSON summary on stdout
16
- (human lines go to stderr for agent automation)
13
+ run402 init Set up with x402 (Base Sepolia) — default
14
+ run402 init mpp Set up with MPP (Tempo Moderato)
15
+ run402 init <rail> --switch-rail
16
+ Switch the persisted payment rail to <rail>.
17
+ Required when an allowance already exists on
18
+ the other rail; protects scripted re-runs from
19
+ silently flipping billing networks.
20
+ run402 init --json Same as init, but emit a JSON summary on stdout
21
+ (human lines go to stderr — for agent automation)
17
22
 
18
- Steps (idempotent — safe to re-run):
23
+ Options:
24
+ --switch-rail Confirm switching the persisted payment rail. Re-running
25
+ init with the SAME rail as the existing allowance is always
26
+ idempotent and does not need this flag.
27
+ --json Emit a structured JSON summary on stdout.
28
+
29
+ Steps (idempotent when re-run with the same rail; pass --switch-rail to change rails):
19
30
  1. Creates config directory (~/.config/run402)
20
31
  2. Creates agent allowance if none exists
21
32
  3. Checks on-chain balance; requests faucet if zero
@@ -32,6 +43,19 @@ export async function run(args = []) {
32
43
  if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
33
44
  const jsonMode = args.includes("--json");
34
45
  const isMpp = args[0] === "mpp";
46
+ const requestedRail = isMpp ? "mpp" : "x402";
47
+ const switchRailConfirmed = args.includes("--switch-rail");
48
+
49
+ const existingAllowance = readAllowance();
50
+ if (existingAllowance?.rail && existingAllowance.rail !== requestedRail && !switchRailConfirmed) {
51
+ console.error(JSON.stringify({
52
+ status: "error",
53
+ code: "RAIL_SWITCH_REQUIRES_CONFIRM",
54
+ message: `Already on rail '${existingAllowance.rail}'. Pass --switch-rail to switch to '${requestedRail}'.`,
55
+ details: { current_rail: existingAllowance.rail, requested_rail: requestedRail },
56
+ }));
57
+ process.exit(1);
58
+ }
35
59
 
36
60
  // In --json mode, human-readable lines go to stderr so stdout stays clean for
37
61
  // agents. We also collect structured data for the final JSON emit.
@@ -55,7 +79,7 @@ export async function run(args = []) {
55
79
  line("Config", CONFIG_DIR);
56
80
 
57
81
  // 2. Allowance
58
- let allowance = readAllowance();
82
+ let allowance = existingAllowance;
59
83
  const previousRail = allowance?.rail;
60
84
  if (!allowance) {
61
85
  const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
package/lib/message.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { allowanceAuthHeaders } 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 message — Send messages to Run402 developers
6
6
 
@@ -16,7 +16,9 @@ Examples:
16
16
  `;
17
17
 
18
18
  async function send(text) {
19
- if (!text) { console.error(JSON.stringify({ status: "error", message: "Missing message text" })); process.exit(1); }
19
+ if (!text) {
20
+ fail({ code: "BAD_USAGE", message: "Missing message text." });
21
+ }
20
22
  // Preserve the aggressive early exit when no allowance is configured.
21
23
  allowanceAuthHeaders("/message/v1");
22
24
 
package/lib/projects.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "fs";
2
- import { findProject, loadKeyStore, API, allowanceAuthHeaders, resolveProjectId } from "./config.mjs";
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
5
 
6
6
  const HELP = `run402 projects — Manage your deployed Run402 projects
7
7
 
@@ -128,9 +128,16 @@ async function provision(args) {
128
128
  // gives the user a more specific prompt than the SDK's 401/402 path.
129
129
  allowanceAuthHeaders("/projects/v1");
130
130
 
131
+ const activeBefore = getActiveProjectId();
131
132
  try {
132
133
  const data = await getSdk().projects.provision({ tier: opts.tier, name: opts.name });
133
- console.log(JSON.stringify(data, null, 2));
134
+ const activeAfter = getActiveProjectId();
135
+ const out = { ...data };
136
+ if (activeBefore && activeAfter && activeBefore !== activeAfter) {
137
+ out.note = `active project changed: ${activeBefore} -> ${activeAfter}`;
138
+ out.previous_active_project_id = activeBefore;
139
+ }
140
+ console.log(JSON.stringify(out, null, 2));
134
141
  } catch (err) {
135
142
  reportSdkError(err);
136
143
  }
@@ -146,12 +153,21 @@ async function applyExpose(projectId, args = []) {
146
153
  }
147
154
  const raw = file ? readFileSync(file, "utf-8") : inline;
148
155
  if (!raw) {
149
- console.error(JSON.stringify({ status: "error", message: "Missing manifest. Provide inline JSON or use --file <path>" }));
150
- process.exit(1);
156
+ fail({
157
+ code: "BAD_USAGE",
158
+ message: "Missing manifest.",
159
+ hint: "Provide inline JSON or use --file <path>",
160
+ });
151
161
  }
152
162
  let manifest;
153
163
  try { manifest = JSON.parse(raw); }
154
- catch { console.error(JSON.stringify({ status: "error", message: "Invalid JSON for manifest" })); process.exit(1); }
164
+ catch (err) {
165
+ fail({
166
+ code: "BAD_USAGE",
167
+ message: "Invalid JSON for manifest",
168
+ details: { parse_error: err.message },
169
+ });
170
+ }
155
171
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/expose`, {
156
172
  method: "POST",
157
173
  headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
@@ -215,11 +231,22 @@ async function sqlCmd(projectId, args = []) {
215
231
  else if (!query && !args[i].startsWith("--")) { query = args[i]; }
216
232
  }
217
233
  const sql = file ? readFileSync(file, "utf-8") : query;
218
- if (!sql) { console.error(JSON.stringify({ status: "error", message: "Missing SQL query. Provide inline or use --file <path>" })); process.exit(1); }
234
+ if (!sql) {
235
+ fail({
236
+ code: "BAD_USAGE",
237
+ message: "Missing SQL query.",
238
+ hint: "Provide inline or use --file <path>",
239
+ });
240
+ }
219
241
  let params;
220
242
  if (paramsRaw) {
221
- 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); }
222
- if (!Array.isArray(params)) { console.error(JSON.stringify({ status: "error", message: "--params must be a JSON array, e.g. '[42, \"hello\"]'" })); process.exit(1); }
243
+ params = parseFlagJson("--params", paramsRaw);
244
+ if (!Array.isArray(params)) {
245
+ fail({
246
+ code: "BAD_USAGE",
247
+ message: "--params must be a JSON array, e.g. '[42, \"hello\"]'",
248
+ });
249
+ }
223
250
  }
224
251
  const useParams = params && params.length > 0;
225
252
  const headers = { "Authorization": `Bearer ${p.service_key}`, "Content-Type": useParams ? "application/json" : "text/plain" };
@@ -257,7 +284,13 @@ async function schema(projectId) {
257
284
  }
258
285
 
259
286
  async function use(projectId) {
260
- if (!projectId) { console.error("Usage: run402 projects use <project_id>"); process.exit(1); }
287
+ if (!projectId) {
288
+ fail({
289
+ code: "BAD_USAGE",
290
+ message: "Missing <project_id>.",
291
+ hint: "run402 projects use <project_id>",
292
+ });
293
+ }
261
294
  try {
262
295
  await getSdk().projects.use(projectId);
263
296
  console.log(JSON.stringify({ status: "ok", active_project_id: projectId }));
@@ -267,7 +300,13 @@ async function use(projectId) {
267
300
  }
268
301
 
269
302
  async function pin(projectId) {
270
- if (!projectId) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 projects pin <project_id>" })); process.exit(1); }
303
+ if (!projectId) {
304
+ fail({
305
+ code: "BAD_USAGE",
306
+ message: "Missing <project_id>.",
307
+ hint: "run402 projects pin <project_id>",
308
+ });
309
+ }
271
310
  try {
272
311
  const data = await getSdk().projects.pin(projectId);
273
312
  console.log(JSON.stringify(data, null, 2));
@@ -277,7 +316,13 @@ async function pin(projectId) {
277
316
  }
278
317
 
279
318
  async function promoteUser(projectId, email) {
280
- if (!email) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 projects promote-user <project_id> <email>" })); process.exit(1); }
319
+ if (!email) {
320
+ fail({
321
+ code: "BAD_USAGE",
322
+ message: "Missing <email>.",
323
+ hint: "run402 projects promote-user <project_id> <email>",
324
+ });
325
+ }
281
326
  const p = findProject(projectId);
282
327
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/promote-user`, {
283
328
  method: "POST",
@@ -290,7 +335,13 @@ async function promoteUser(projectId, email) {
290
335
  }
291
336
 
292
337
  async function demoteUser(projectId, email) {
293
- if (!email) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 projects demote-user <project_id> <email>" })); process.exit(1); }
338
+ if (!email) {
339
+ fail({
340
+ code: "BAD_USAGE",
341
+ message: "Missing <email>.",
342
+ hint: "run402 projects demote-user <project_id> <email>",
343
+ });
344
+ }
294
345
  const p = findProject(projectId);
295
346
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/demote-user`, {
296
347
  method: "POST",
@@ -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
 
@@ -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
@@ -79,7 +79,7 @@ export async function run(args = []) {
79
79
  const allowance = readAllowance();
80
80
  if (!allowance) {
81
81
  console.log(JSON.stringify({ status: "no_allowance", message: "No agent allowance found. Run: run402 init" }));
82
- return;
82
+ process.exit(1);
83
83
  }
84
84
 
85
85
  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
 
@@ -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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.53.1",
3
+ "version": "1.54.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": {