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/blob.mjs CHANGED
@@ -36,7 +36,8 @@ import { pipeline } from "node:stream/promises";
36
36
 
37
37
  import { resolveProject, resolveProjectId, API } from "./config.mjs";
38
38
  import { getSdk } from "./sdk.mjs";
39
- import { reportSdkError } from "./sdk-errors.mjs";
39
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
40
+ import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag } from "./argparse.mjs";
40
41
 
41
42
  const HELP = `run402 blob — Direct-to-S3 blob storage
42
43
 
@@ -62,13 +63,13 @@ Options:
62
63
  --ttl <seconds> Signed-URL TTL (sign only; default 3600, max 604800)
63
64
 
64
65
  Examples:
65
- run402 blob put ./artifact.tgz --project abc123
66
- run402 blob put ./dist/**/*.png --project abc123 --key assets/
67
- run402 blob put huge.bin --project abc123 --immutable
68
- run402 blob get images/logo.png --output /tmp/logo.png --project abc123
69
- run402 blob ls --project abc123 --prefix images/
70
- run402 blob rm images/logo.png --project abc123
71
- run402 blob sign images/logo.png --project abc123 --ttl 600
66
+ run402 blob put ./artifact.tgz --project prj_abc123
67
+ run402 blob put ./dist/**/*.png --project prj_abc123 --key assets/
68
+ run402 blob put huge.bin --project prj_abc123 --immutable
69
+ run402 blob get images/logo.png --output /tmp/logo.png --project prj_abc123
70
+ run402 blob ls --project prj_abc123 --prefix images/
71
+ run402 blob rm images/logo.png --project prj_abc123
72
+ run402 blob sign images/logo.png --project prj_abc123 --ttl 600
72
73
  `;
73
74
 
74
75
  const SUB_HELP = {
@@ -90,9 +91,9 @@ Options:
90
91
  --json Emit NDJSON progress events on stdout (for agent consumption)
91
92
 
92
93
  Examples:
93
- run402 blob put ./artifact.tgz --project abc123
94
- run402 blob put ./dist/**/*.png --project abc123 --key assets/
95
- run402 blob put huge.bin --project abc123 --immutable --concurrency 8
94
+ run402 blob put ./artifact.tgz --project prj_abc123
95
+ run402 blob put ./dist/**/*.png --project prj_abc123 --key assets/
96
+ run402 blob put huge.bin --project prj_abc123 --immutable --concurrency 8
96
97
  `,
97
98
  get: `run402 blob get — Download a blob by key
98
99
 
@@ -107,7 +108,7 @@ Options:
107
108
  --project <id> Project ID (defaults to active project)
108
109
 
109
110
  Examples:
110
- run402 blob get images/logo.png --output /tmp/logo.png --project abc123
111
+ run402 blob get images/logo.png --output /tmp/logo.png --project prj_abc123
111
112
  `,
112
113
  ls: `run402 blob ls — List blob keys in a project
113
114
 
@@ -120,8 +121,8 @@ Options:
120
121
  --limit <n> Max results (default 100, max 1000)
121
122
 
122
123
  Examples:
123
- run402 blob ls --project abc123
124
- run402 blob ls --project abc123 --prefix images/ --limit 500
124
+ run402 blob ls --project prj_abc123
125
+ run402 blob ls --project prj_abc123 --prefix images/ --limit 500
125
126
  `,
126
127
  rm: `run402 blob rm — Delete a blob
127
128
 
@@ -135,7 +136,7 @@ Options:
135
136
  --project <id> Project ID (defaults to active project)
136
137
 
137
138
  Examples:
138
- run402 blob rm images/logo.png --project abc123
139
+ run402 blob rm images/logo.png --project prj_abc123
139
140
  `,
140
141
  sign: `run402 blob sign — Create a presigned download URL for a blob
141
142
 
@@ -150,7 +151,7 @@ Options:
150
151
  --ttl <seconds> Signed-URL TTL (default 3600, max 604800)
151
152
 
152
153
  Examples:
153
- run402 blob sign reports/2025-q4.pdf --project abc123 --ttl 600
154
+ run402 blob sign reports/2025-q4.pdf --project prj_abc123 --ttl 600
154
155
  `,
155
156
  diagnose: `run402 blob diagnose — Inspect the live CDN state for a public blob URL
156
157
 
@@ -182,12 +183,42 @@ Examples:
182
183
 
183
184
  const UPLOAD_STATE_DIR = join(homedir(), ".run402", "uploads");
184
185
 
185
- function die(msg, code = 1) {
186
- console.error(JSON.stringify({ status: "error", message: msg }));
187
- process.exit(code);
186
+ function die(msg, exit_code = 1) {
187
+ fail({ code: "BAD_USAGE", message: msg, exit_code });
188
188
  }
189
189
 
190
- function parseArgs(args) {
190
+ function dieApiFailure(prefix, http, body) {
191
+ if (body && typeof body === "object" && !Array.isArray(body)) {
192
+ const envelope = { status: "error", http, ...body };
193
+ if (!envelope.message && envelope.error) envelope.message = envelope.error;
194
+ console.error(JSON.stringify(envelope));
195
+ process.exit(1);
196
+ }
197
+ fail({
198
+ message: `${prefix}: HTTP ${http}${typeof body === "string" && body ? `: ${body.slice(0, 500)}` : ""}`,
199
+ details: { http },
200
+ });
201
+ }
202
+
203
+ function parseArgs(rawArgs) {
204
+ const args = normalizeArgv(rawArgs);
205
+ const valueFlags = ["--project", "--key", "--concurrency", "--prefix", "--limit", "--output", "-o", "--ttl"];
206
+ assertKnownFlags(args, [
207
+ "--project",
208
+ "--key",
209
+ "--private",
210
+ "--immutable",
211
+ "--concurrency",
212
+ "--no-resume",
213
+ "--json",
214
+ "--prefix",
215
+ "--limit",
216
+ "--output",
217
+ "-o",
218
+ "--ttl",
219
+ "--help",
220
+ "-h",
221
+ ], valueFlags);
191
222
  const out = { positional: [], project: null, key: null, private: false, immutable: false,
192
223
  concurrency: 4, resume: true, json: false, prefix: null, limit: null,
193
224
  output: null, ttl: null };
@@ -197,13 +228,13 @@ function parseArgs(args) {
197
228
  else if (a === "--key") out.key = args[++i];
198
229
  else if (a === "--private") out.private = true;
199
230
  else if (a === "--immutable") out.immutable = true;
200
- else if (a === "--concurrency") out.concurrency = parseInt(args[++i], 10);
231
+ else if (a === "--concurrency") out.concurrency = parseIntegerFlag("--concurrency", args[++i], { min: 1 });
201
232
  else if (a === "--no-resume") out.resume = false;
202
233
  else if (a === "--json") out.json = true;
203
234
  else if (a === "--prefix") out.prefix = args[++i];
204
- else if (a === "--limit") out.limit = parseInt(args[++i], 10);
235
+ else if (a === "--limit") out.limit = parseIntegerFlag("--limit", args[++i], { min: 1, max: 1000 });
205
236
  else if (a === "--output" || a === "-o") out.output = args[++i];
206
- else if (a === "--ttl") out.ttl = parseInt(args[++i], 10);
237
+ else if (a === "--ttl") out.ttl = parseIntegerFlag("--ttl", args[++i], { min: 1, max: 604800 });
207
238
  else if (!a.startsWith("--")) out.positional.push(a);
208
239
  }
209
240
  return out;
@@ -285,7 +316,7 @@ async function putOne(project, filePath, opts) {
285
316
  immutable: opts.immutable,
286
317
  sha256,
287
318
  });
288
- if (init.status !== 201) die(`Init failed: HTTP ${init.status}: ${JSON.stringify(init.body)}`);
319
+ if (init.status !== 201) dieApiFailure("Init failed", init.status, init.body);
289
320
  initRes = init.body;
290
321
  saveState({
291
322
  upload_id: initRes.upload_id,
@@ -331,7 +362,7 @@ async function putOne(project, filePath, opts) {
331
362
  ? { parts: etags.map((e, i) => ({ part_number: i + 1, etag: e.etag })) }
332
363
  : {};
333
364
  const complete = await apiFetch(`${API}/storage/v1/uploads/${state.upload_id}/complete`, "POST", project, body);
334
- if (complete.status !== 200) die(`Complete failed: HTTP ${complete.status}: ${JSON.stringify(complete.body)}`);
365
+ if (complete.status !== 200) dieApiFailure("Complete failed", complete.status, complete.body);
335
366
 
336
367
  removeState(state.upload_id);
337
368
  log(opts, { event: "done", ...complete.body });
@@ -566,7 +597,8 @@ export async function run(sub, args) {
566
597
  console.log(HELP);
567
598
  process.exit(0);
568
599
  }
569
- if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
600
+ args = normalizeArgv(args);
601
+ if (Array.isArray(args) && hasHelp(args)) {
570
602
  console.log(SUB_HELP[sub] || HELP);
571
603
  process.exit(0);
572
604
  }
package/lib/cdn.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  import { resolveProjectId } from "./config.mjs";
17
17
  import { getSdk } from "./sdk.mjs";
18
- import { reportSdkError } from "./sdk-errors.mjs";
18
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
19
19
 
20
20
  const HELP = `run402 cdn — CloudFront CDN diagnostics for public blob URLs
21
21
 
@@ -68,9 +68,8 @@ Examples:
68
68
  `,
69
69
  };
70
70
 
71
- function die(msg, code = 1) {
72
- console.error(JSON.stringify({ status: "error", message: msg }));
73
- process.exit(code);
71
+ function die(msg, exit_code = 1) {
72
+ fail({ code: "BAD_USAGE", message: msg, exit_code });
74
73
  }
75
74
 
76
75
  function parseArgs(args) {
package/lib/config.mjs CHANGED
@@ -7,6 +7,7 @@ import { getApiBase, getConfigDir, getKeystorePath, getAllowancePath } from "../
7
7
  import { readAllowance as coreReadAllowance, saveAllowance as coreSaveAllowance } from "../core-dist/allowance.js";
8
8
  import { loadKeyStore, getProject, saveProject, updateProject, removeProject, saveKeyStore, getActiveProjectId, setActiveProjectId } from "../core-dist/keystore.js";
9
9
  import { getAllowanceAuthHeaders as coreGetAllowanceAuthHeaders } from "../core-dist/allowance-auth.js";
10
+ import { fail } from "./sdk-errors.mjs";
10
11
 
11
12
  export const CONFIG_DIR = getConfigDir();
12
13
  export const ALLOWANCE_FILE = getAllowancePath();
@@ -23,31 +24,54 @@ export function saveAllowance(data) {
23
24
 
24
25
  export function allowanceAuthHeaders(path) {
25
26
  const headers = coreGetAllowanceAuthHeaders(path);
26
- if (!headers) { console.error(JSON.stringify({ status: "error", message: "No agent allowance found. Run: run402 allowance create" })); process.exit(1); }
27
+ if (!headers) {
28
+ fail({
29
+ code: "NO_ALLOWANCE",
30
+ message: "No agent allowance found.",
31
+ hint: "Run: run402 allowance create",
32
+ });
33
+ }
27
34
  return headers;
28
35
  }
29
36
 
30
37
  export function findProject(id) {
31
38
  const p = getProject(id);
32
39
  if (!p) {
33
- const hint = id && !id.startsWith("prj_")
34
- ? ` Hint: project IDs start with "prj_". Check that the argument order is <project_id> <name>.`
35
- : "";
36
- console.error(`Project ${id} not found in local registry.${hint}`);
37
- process.exit(1);
40
+ const idStr = id ?? "";
41
+ const hint = idStr && !String(idStr).startsWith("prj_")
42
+ ? `project IDs start with "prj_". Check that the argument order is <project_id> <name>.`
43
+ : undefined;
44
+ fail({
45
+ code: "PROJECT_NOT_FOUND",
46
+ message: `Project ${idStr} not found in local registry.`,
47
+ hint,
48
+ details: { project_id: idStr, source: "local_registry" },
49
+ });
38
50
  }
39
51
  return p;
40
52
  }
41
53
 
42
54
  export function resolveProject(id) {
43
55
  const projectId = id || getActiveProjectId();
44
- if (!projectId) { console.error("Error: no project specified and no active project set. Run: run402 projects provision"); process.exit(1); }
56
+ if (!projectId) {
57
+ fail({
58
+ code: "NO_ACTIVE_PROJECT",
59
+ message: "no project specified and no active project set.",
60
+ hint: "Run: run402 projects provision",
61
+ });
62
+ }
45
63
  return findProject(projectId);
46
64
  }
47
65
 
48
66
  export function resolveProjectId(id) {
49
67
  const projectId = id || getActiveProjectId();
50
- if (!projectId) { console.error("Error: no project specified and no active project set. Run: run402 projects provision"); process.exit(1); }
68
+ if (!projectId) {
69
+ fail({
70
+ code: "NO_ACTIVE_PROJECT",
71
+ message: "no project specified and no active project set.",
72
+ hint: "Run: run402 projects provision",
73
+ });
74
+ }
51
75
  return projectId;
52
76
  }
53
77
 
package/lib/contracts.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { findProject, API } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
- import { reportSdkError } from "./sdk-errors.mjs";
3
+ import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
4
4
 
5
5
  const HELP = `run402 contracts — KMS-backed Ethereum wallets for smart-contract calls
6
6
 
@@ -100,8 +100,10 @@ async function provisionWallet(projectId, args) {
100
100
  const p = findProject(projectId);
101
101
  const chain = parseFlag(args, "--chain");
102
102
  if (!chain) {
103
- console.error(JSON.stringify({ status: "error", message: "Missing --chain (base-mainnet or base-sepolia)" }));
104
- process.exit(1);
103
+ fail({
104
+ code: "BAD_USAGE",
105
+ message: "Missing --chain (base-mainnet or base-sepolia)",
106
+ });
105
107
  }
106
108
  const recovery = parseFlag(args, "--recovery");
107
109
  // Soft default of one wallet — confirm if project already has one. This
@@ -115,8 +117,11 @@ async function provisionWallet(projectId, args) {
115
117
  const list = await listRes.json();
116
118
  const active = (list.wallets || []).filter((w) => w.status === "active");
117
119
  if (active.length >= 1 && !hasFlag(args, "--yes")) {
118
- console.error(`This project already has ${active.length} active wallet(s). Adding another costs $0.04/day each ($1.20/month). Re-run with --yes to confirm.`);
119
- process.exit(1);
120
+ fail({
121
+ code: "CONFIRMATION_REQUIRED",
122
+ message: `This project already has ${active.length} active wallet(s). Adding another costs $0.04/day each ($1.20/month). Re-run with --yes to confirm.`,
123
+ details: { active_wallets: active.length },
124
+ });
120
125
  }
121
126
  }
122
127
  } catch { /* best-effort */ }
@@ -154,8 +159,10 @@ async function setRecovery(projectId, walletId, args) {
154
159
  const clear = hasFlag(args, "--clear");
155
160
  const address = parseFlag(args, "--address");
156
161
  if (!clear && !address) {
157
- console.error(JSON.stringify({ status: "error", message: "Provide --address 0x... or --clear" }));
158
- process.exit(1);
162
+ fail({
163
+ code: "BAD_USAGE",
164
+ message: "Provide --address 0x... or --clear",
165
+ });
159
166
  }
160
167
  try {
161
168
  await getSdk().contracts.setRecovery(projectId, walletId, clear ? null : address);
@@ -168,8 +175,7 @@ async function setRecovery(projectId, walletId, args) {
168
175
  async function setAlert(projectId, walletId, args) {
169
176
  const threshold = parseFlag(args, "--threshold-wei");
170
177
  if (!threshold) {
171
- console.error(JSON.stringify({ status: "error", message: "Missing --threshold-wei <n>" }));
172
- process.exit(1);
178
+ fail({ code: "BAD_USAGE", message: "Missing --threshold-wei <n>" });
173
179
  }
174
180
  try {
175
181
  await getSdk().contracts.setLowBalanceAlert(projectId, walletId, threshold);
@@ -188,17 +194,22 @@ async function call(projectId, walletId, args) {
188
194
  const chain = parseFlag(args, "--chain") || "base-mainnet";
189
195
  const idempotency = parseFlag(args, "--idempotency-key");
190
196
  if (!to || !abi || !fn || !argsJson) {
191
- console.error(JSON.stringify({ status: "error", message: "Required flags: --to, --abi, --fn, --args. Cost: chain gas + $0.000005 KMS sign fee." }));
192
- process.exit(1);
197
+ fail({
198
+ code: "BAD_USAGE",
199
+ message: "Required flags: --to, --abi, --fn, --args.",
200
+ hint: "Cost: chain gas + $0.000005 KMS sign fee.",
201
+ });
193
202
  }
203
+ const abiFragment = parseFlagJson("--abi", abi);
204
+ const callArgs = parseFlagJson("--args", argsJson);
194
205
  try {
195
206
  const data = await getSdk().contracts.call(projectId, {
196
207
  walletId,
197
208
  chain,
198
209
  contractAddress: to,
199
- abiFragment: JSON.parse(abi),
210
+ abiFragment,
200
211
  functionName: fn,
201
- args: JSON.parse(argsJson),
212
+ args: callArgs,
202
213
  value: value ?? undefined,
203
214
  idempotencyKey: idempotency ?? undefined,
204
215
  });
@@ -215,16 +226,20 @@ async function read(args) {
215
226
  const fn = parseFlag(args, "--fn");
216
227
  const argsJson = parseFlag(args, "--args");
217
228
  if (!chain || !to || !abi || !fn || !argsJson) {
218
- console.error(JSON.stringify({ status: "error", message: "Required flags: --chain, --to, --abi, --fn, --args" }));
219
- process.exit(1);
229
+ fail({
230
+ code: "BAD_USAGE",
231
+ message: "Required flags: --chain, --to, --abi, --fn, --args",
232
+ });
220
233
  }
234
+ const abiFragment = parseFlagJson("--abi", abi);
235
+ const callArgs = parseFlagJson("--args", argsJson);
221
236
  try {
222
237
  const data = await getSdk().contracts.read({
223
238
  chain,
224
239
  contractAddress: to,
225
- abiFragment: JSON.parse(abi),
240
+ abiFragment,
226
241
  functionName: fn,
227
- args: JSON.parse(argsJson),
242
+ args: callArgs,
228
243
  });
229
244
  console.log(JSON.stringify(data, null, 2));
230
245
  } catch (err) {
@@ -244,8 +259,11 @@ async function status(projectId, callId) {
244
259
  async function drain(projectId, walletId, args) {
245
260
  const to = parseFlag(args, "--to");
246
261
  if (!to || !hasFlag(args, "--confirm")) {
247
- console.error(JSON.stringify({ status: "error", message: "Required: --to 0x... and --confirm. Cost: chain gas + $0.000005 KMS sign fee." }));
248
- process.exit(1);
262
+ fail({
263
+ code: "BAD_USAGE",
264
+ message: "Required: --to 0x... and --confirm.",
265
+ hint: "Cost: chain gas + $0.000005 KMS sign fee.",
266
+ });
249
267
  }
250
268
  try {
251
269
  const data = await getSdk().contracts.drain(projectId, walletId, to);
@@ -257,8 +275,7 @@ async function drain(projectId, walletId, args) {
257
275
 
258
276
  async function deleteWallet(projectId, walletId, args) {
259
277
  if (!hasFlag(args, "--confirm")) {
260
- console.error(JSON.stringify({ status: "error", message: "Required: --confirm" }));
261
- process.exit(1);
278
+ fail({ code: "BAD_USAGE", message: "Required: --confirm" });
262
279
  }
263
280
  try {
264
281
  const data = await getSdk().contracts.deleteWallet(projectId, walletId);
package/lib/deploy-v2.mjs CHANGED
@@ -26,7 +26,7 @@
26
26
  import { readFileSync } from "node:fs";
27
27
  import { resolve, dirname, isAbsolute, join } from "node:path";
28
28
  import { getSdk } from "./sdk.mjs";
29
- import { reportSdkError } from "./sdk-errors.mjs";
29
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
30
30
  import { allowanceAuthHeaders, resolveProjectId } from "./config.mjs";
31
31
 
32
32
  const APPLY_HELP = `run402 deploy apply — Unified deploy primitive (v1.34+)
@@ -107,8 +107,11 @@ export async function runDeployV2(sub, args) {
107
107
  if (sub === "resume") return await resumeCmd(args);
108
108
  if (sub === "list") return await listCmd(args);
109
109
  if (sub === "events") return await eventsCmd(args);
110
- console.error(JSON.stringify({ status: "error", message: `Unknown deploy subcommand: ${sub}` }));
111
- process.exit(1);
110
+ fail({
111
+ code: "BAD_USAGE",
112
+ message: `Unknown deploy subcommand: ${sub}`,
113
+ details: { subcommand: sub },
114
+ });
112
115
  }
113
116
 
114
117
  async function readStdin() {
@@ -142,8 +145,11 @@ async function applyCmd(args) {
142
145
  const manifestPath = isAbsolute(opts.manifest) ? opts.manifest : resolve(process.cwd(), opts.manifest);
143
146
  raw = readFileSync(manifestPath, "utf-8");
144
147
  } catch (err) {
145
- console.error(JSON.stringify({ status: "error", message: `Failed to read manifest: ${err.message}` }));
146
- process.exit(1);
148
+ fail({
149
+ code: "BAD_USAGE",
150
+ message: `Failed to read manifest: ${err.message}`,
151
+ details: { flag: "--manifest", path: opts.manifest },
152
+ });
147
153
  }
148
154
  } else {
149
155
  raw = await readStdin();
@@ -153,18 +159,21 @@ async function applyCmd(args) {
153
159
  try {
154
160
  spec = JSON.parse(raw);
155
161
  } catch (err) {
156
- console.error(JSON.stringify({ status: "error", message: `Manifest is not valid JSON: ${err.message}` }));
157
- process.exit(1);
162
+ fail({
163
+ code: "BAD_USAGE",
164
+ message: `Manifest is not valid JSON: ${err.message}`,
165
+ details: { source: opts.manifest ? "manifest" : opts.spec ? "spec" : "stdin", parse_error: err.message },
166
+ });
158
167
  }
159
168
 
160
169
  if (opts.manifest) resolveFileDataPaths(spec, dirname(resolve(opts.manifest)));
161
170
 
162
171
  if (opts.project && spec.project_id && spec.project_id !== opts.project) {
163
- console.error(JSON.stringify({
164
- status: "error",
172
+ fail({
173
+ code: "BAD_USAGE",
165
174
  message: `project_id conflict: spec.project_id=${spec.project_id} but --project=${opts.project}`,
166
- }));
167
- process.exit(1);
175
+ details: { spec_project_id: spec.project_id, flag_project_id: opts.project },
176
+ });
168
177
  }
169
178
  if (opts.project) spec.project_id = opts.project;
170
179
  if (!spec.project_id) spec.project_id = resolveProjectId(null);
@@ -198,8 +207,11 @@ async function resumeCmd(args) {
198
207
  if (!args[i].startsWith("-") && !opts.operationId) opts.operationId = args[i];
199
208
  }
200
209
  if (!opts.operationId) {
201
- console.error(JSON.stringify({ status: "error", message: "Usage: run402 deploy resume <operation_id>" }));
202
- process.exit(1);
210
+ fail({
211
+ code: "BAD_USAGE",
212
+ message: "Missing <operation_id>.",
213
+ hint: "run402 deploy resume <operation_id>",
214
+ });
203
215
  }
204
216
 
205
217
  allowanceAuthHeaders("/deploy/v2/operations");
@@ -243,8 +255,11 @@ async function eventsCmd(args) {
243
255
  if (!args[i].startsWith("-") && !opts.operationId) opts.operationId = args[i];
244
256
  }
245
257
  if (!opts.operationId) {
246
- console.error(JSON.stringify({ status: "error", message: "Usage: run402 deploy events <operation_id>" }));
247
- process.exit(1);
258
+ fail({
259
+ code: "BAD_USAGE",
260
+ message: "Missing <operation_id>.",
261
+ hint: "run402 deploy events <operation_id>",
262
+ });
248
263
  }
249
264
 
250
265
  const project = resolveProjectId(opts.project);
@@ -383,11 +398,11 @@ function resolveFileDataPaths(spec, baseDir) {
383
398
  m.sql = readFileSync(p, "utf-8");
384
399
  delete m.sql_path;
385
400
  } catch (err) {
386
- console.error(JSON.stringify({
387
- status: "error",
401
+ fail({
402
+ code: "BAD_USAGE",
388
403
  message: `Failed to read migration sql_path '${m.sql_path}': ${err.message}`,
389
- }));
390
- process.exit(1);
404
+ details: { migration_id: m.id, sql_path: m.sql_path },
405
+ });
391
406
  }
392
407
  }
393
408
  }
@@ -421,10 +436,10 @@ function readFileEntry(entry, baseDir) {
421
436
  if (entry.contentType) out.contentType = entry.contentType;
422
437
  return out;
423
438
  } catch (err) {
424
- console.error(JSON.stringify({
425
- status: "error",
439
+ fail({
440
+ code: "BAD_USAGE",
426
441
  message: `Failed to read file '${entry.path}': ${err.message}`,
427
- }));
428
- process.exit(1);
442
+ details: { path: entry.path },
443
+ });
429
444
  }
430
445
  }
package/lib/deploy.mjs CHANGED
@@ -3,7 +3,7 @@ import { dirname, resolve } from "path";
3
3
  import { resolveProjectId } from "./config.mjs";
4
4
  import { resolveFilePathsInManifest, resolveMigrationsFile } from "./manifest.mjs";
5
5
  import { getSdk } from "./sdk.mjs";
6
- import { reportSdkError } from "./sdk-errors.mjs";
6
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
7
7
 
8
8
  const HELP = `run402 deploy — Deploy to an existing project on Run402
9
9
 
@@ -111,8 +111,8 @@ async function readStdin() {
111
111
  * Load + parse the manifest from --manifest file or stdin, and resolve any
112
112
  * referenced files[].path / migrations_file against the manifest's directory.
113
113
  *
114
- * Returns { manifest } on success, or { error } with a structured error object
115
- * on any fs / parse failure. Never throws.
114
+ * Returns the parsed manifest on success. On any fs / parse failure, calls
115
+ * `fail()` (which writes the canonical error envelope to stderr and exits 1).
116
116
  */
117
117
  async function loadManifest(opts) {
118
118
  let raw;
@@ -125,21 +125,18 @@ async function loadManifest(opts) {
125
125
  raw = readFileSync(opts.manifest, "utf-8");
126
126
  } catch (err) {
127
127
  if (err && err.code === "ENOENT") {
128
- return { error: {
129
- status: "error",
128
+ fail({
129
+ code: "BAD_USAGE",
130
130
  message: `File not found: ${manifestAbs}`,
131
- field: "manifest",
132
- path: manifestAbs,
133
131
  hint: "Check that --manifest points to an existing JSON file.",
134
- } };
132
+ details: { field: "manifest", path: manifestAbs },
133
+ });
135
134
  }
136
- return { error: {
137
- status: "error",
135
+ fail({
136
+ code: "BAD_USAGE",
138
137
  message: err && err.message ? err.message : String(err),
139
- field: "manifest",
140
- path: manifestAbs,
141
- ...(err && err.code ? { code: err.code } : {}),
142
- } };
138
+ details: { field: "manifest", path: manifestAbs, ...(err && err.code ? { syscall_code: err.code } : {}) },
139
+ });
143
140
  }
144
141
  } else {
145
142
  raw = await readStdin();
@@ -149,12 +146,15 @@ async function loadManifest(opts) {
149
146
  try {
150
147
  manifest = JSON.parse(raw);
151
148
  } catch (err) {
152
- return { error: {
153
- status: "error",
149
+ fail({
150
+ code: "BAD_USAGE",
154
151
  message: `Manifest is not valid JSON: ${err.message}`,
155
- field: opts.manifest ? "manifest" : "stdin",
156
- ...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
157
- } };
152
+ details: {
153
+ field: opts.manifest ? "manifest" : "stdin",
154
+ ...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
155
+ parse_error: err.message,
156
+ },
157
+ });
158
158
  }
159
159
 
160
160
  if (opts.manifest) {
@@ -163,25 +163,29 @@ async function loadManifest(opts) {
163
163
  resolveFilePathsInManifest(manifest, baseDir);
164
164
  } catch (err) {
165
165
  if (err && err.code === "ENOENT") {
166
- return { error: {
167
- status: "error",
166
+ fail({
167
+ code: "BAD_USAGE",
168
168
  message: `File not found: ${err.absPath || err.path || "<unknown>"}`,
169
- field: err.field || "manifest",
170
- ...(err.absPath || err.path ? { path: err.absPath || err.path } : {}),
171
169
  hint: `Paths in manifest.${err.field || "files[].path"} are resolved relative to the manifest file's directory (${baseDir}).`,
172
- } };
170
+ details: {
171
+ field: err.field || "manifest",
172
+ ...(err.absPath || err.path ? { path: err.absPath || err.path } : {}),
173
+ },
174
+ });
173
175
  }
174
- return { error: {
175
- status: "error",
176
+ fail({
177
+ code: "BAD_USAGE",
176
178
  message: err && err.message ? err.message : String(err),
177
- ...(err && err.field ? { field: err.field } : {}),
178
- ...(err && (err.absPath || err.path) ? { path: err.absPath || err.path } : {}),
179
- ...(err && err.code ? { code: err.code } : {}),
180
- } };
179
+ details: {
180
+ ...(err && err.field ? { field: err.field } : {}),
181
+ ...(err && (err.absPath || err.path) ? { path: err.absPath || err.path } : {}),
182
+ ...(err && err.code ? { syscall_code: err.code } : {}),
183
+ },
184
+ });
181
185
  }
182
186
  }
183
187
 
184
- return { manifest };
188
+ return manifest;
185
189
  }
186
190
 
187
191
  export async function run(args) {
@@ -210,25 +214,20 @@ export async function run(args) {
210
214
  if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
211
215
  }
212
216
 
213
- const manifestResult = await loadManifest(opts);
214
- if (manifestResult.error) {
215
- console.error(JSON.stringify(manifestResult.error));
216
- process.exit(1);
217
- }
218
- const manifest = manifestResult.manifest;
217
+ const manifest = await loadManifest(opts);
219
218
 
220
219
  // If both sources set project_id and they disagree, refuse to deploy rather
221
220
  // than silently shipping to the wrong target.
222
221
  if (opts.project && manifest.project_id && opts.project !== manifest.project_id) {
223
- const err = {
224
- status: "error",
222
+ fail({
223
+ code: "BAD_USAGE",
225
224
  message: `project_id conflict: manifest.project_id=${manifest.project_id} but --project=${opts.project}`,
226
- manifest_project_id: manifest.project_id,
227
- flag_project_id: opts.project,
228
225
  hint: "Remove one of them or make them match. The --project flag and manifest.project_id must agree (or only one of them must be set).",
229
- };
230
- console.error(JSON.stringify(err));
231
- process.exit(1);
226
+ details: {
227
+ manifest_project_id: manifest.project_id,
228
+ flag_project_id: opts.project,
229
+ },
230
+ });
232
231
  }
233
232
 
234
233
  if (opts.project) manifest.project_id = opts.project;