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
@@ -1,7 +1,34 @@
1
- import { readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync } from "node:fs";
1
+ import { readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync, rmdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { getKeystorePath } from "./config.js";
5
+ function withFileLock(path, fn, { retries = 200, delayMs = 20 } = {}) {
6
+ const lockDir = path + ".lock";
7
+ mkdirSync(dirname(path), { recursive: true });
8
+ for (let i = 0; i < retries; i++) {
9
+ try {
10
+ mkdirSync(lockDir, { mode: 0o700 });
11
+ }
12
+ catch (e) {
13
+ const code = e.code;
14
+ if (code !== "EEXIST")
15
+ throw e;
16
+ const until = Date.now() + delayMs;
17
+ while (Date.now() < until) { /* spin */ }
18
+ continue;
19
+ }
20
+ try {
21
+ return fn();
22
+ }
23
+ finally {
24
+ try {
25
+ rmdirSync(lockDir);
26
+ }
27
+ catch { /* best-effort */ }
28
+ }
29
+ }
30
+ throw new Error(`Could not acquire keystore lock after ${retries} retries: ${lockDir}`);
31
+ }
5
32
  /**
6
33
  * Load the keystore from disk.
7
34
  * Auto-migrates legacy formats:
@@ -13,7 +40,6 @@ export function loadKeyStore(path) {
13
40
  try {
14
41
  const data = readFileSync(p, "utf-8");
15
42
  const parsed = JSON.parse(data);
16
- // Auto-migrate array format (CLI legacy) to object format
17
43
  if (Array.isArray(parsed)) {
18
44
  const projects = {};
19
45
  for (const item of parsed) {
@@ -29,7 +55,6 @@ export function loadKeyStore(path) {
29
55
  return { projects };
30
56
  }
31
57
  if (parsed && typeof parsed === "object" && parsed.projects) {
32
- // Strip legacy fields (tier, lease_expires_at, expires_at) from projects
33
58
  for (const proj of Object.values(parsed.projects)) {
34
59
  const rec = proj;
35
60
  delete rec.tier;
@@ -38,6 +63,7 @@ export function loadKeyStore(path) {
38
63
  }
39
64
  return {
40
65
  ...(parsed.active_project_id && { active_project_id: parsed.active_project_id }),
66
+ ...(parsed.previous_active_project_id && { previous_active_project_id: parsed.previous_active_project_id }),
41
67
  projects: parsed.projects,
42
68
  };
43
69
  }
@@ -62,27 +88,40 @@ export function getProject(projectId, path) {
62
88
  }
63
89
  export function saveProject(projectId, project, path) {
64
90
  const p = path ?? getKeystorePath();
65
- const store = loadKeyStore(p);
66
- store.projects[projectId] = project;
67
- saveKeyStore(store, p);
91
+ withFileLock(p, () => {
92
+ const store = loadKeyStore(p);
93
+ store.projects[projectId] = project;
94
+ saveKeyStore(store, p);
95
+ });
68
96
  }
69
97
  export function updateProject(projectId, update, path) {
70
98
  const p = path ?? getKeystorePath();
71
- const store = loadKeyStore(p);
72
- const existing = store.projects[projectId];
73
- if (existing) {
74
- store.projects[projectId] = { ...existing, ...update };
75
- saveKeyStore(store, p);
76
- }
99
+ withFileLock(p, () => {
100
+ const store = loadKeyStore(p);
101
+ const existing = store.projects[projectId];
102
+ if (existing) {
103
+ store.projects[projectId] = { ...existing, ...update };
104
+ saveKeyStore(store, p);
105
+ }
106
+ });
77
107
  }
78
108
  export function removeProject(projectId, path) {
79
109
  const p = path ?? getKeystorePath();
80
- const store = loadKeyStore(p);
81
- delete store.projects[projectId];
82
- if (store.active_project_id === projectId) {
83
- delete store.active_project_id;
84
- }
85
- saveKeyStore(store, p);
110
+ withFileLock(p, () => {
111
+ const store = loadKeyStore(p);
112
+ delete store.projects[projectId];
113
+ if (store.active_project_id === projectId) {
114
+ const fallback = store.previous_active_project_id;
115
+ if (fallback && fallback !== projectId && store.projects[fallback]) {
116
+ store.active_project_id = fallback;
117
+ }
118
+ else {
119
+ delete store.active_project_id;
120
+ }
121
+ delete store.previous_active_project_id;
122
+ }
123
+ saveKeyStore(store, p);
124
+ });
86
125
  }
87
126
  export function getActiveProjectId(path) {
88
127
  const store = loadKeyStore(path);
@@ -90,8 +129,13 @@ export function getActiveProjectId(path) {
90
129
  }
91
130
  export function setActiveProjectId(projectId, path) {
92
131
  const p = path ?? getKeystorePath();
93
- const store = loadKeyStore(p);
94
- store.active_project_id = projectId;
95
- saveKeyStore(store, p);
132
+ withFileLock(p, () => {
133
+ const store = loadKeyStore(p);
134
+ if (store.active_project_id && store.active_project_id !== projectId) {
135
+ store.previous_active_project_id = store.active_project_id;
136
+ }
137
+ store.active_project_id = projectId;
138
+ saveKeyStore(store, p);
139
+ });
96
140
  }
97
141
  //# sourceMappingURL=keystore.js.map
package/lib/agent.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 agent — Manage agent identity
6
6
 
@@ -24,7 +24,9 @@ async function contact(args) {
24
24
  if (args[i] === "--email" && args[i + 1]) email = args[++i];
25
25
  if (args[i] === "--webhook" && args[i + 1]) webhook = args[++i];
26
26
  }
27
- if (!name) { console.error(JSON.stringify({ status: "error", message: "Missing --name <name>" })); process.exit(1); }
27
+ if (!name) {
28
+ fail({ code: "BAD_USAGE", message: "Missing --name <name>" });
29
+ }
28
30
  // Preserve the aggressive early exit when no allowance is configured.
29
31
  allowanceAuthHeaders("/agent/v1/contact");
30
32
 
package/lib/ai.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 ai — AI translation and moderation tools
6
6
 
@@ -76,8 +76,16 @@ async function translate(args) {
76
76
  const from = parseFlag(args, "--from");
77
77
  const context = parseFlag(args, "--context");
78
78
 
79
- if (!text) { console.error(JSON.stringify({ status: "error", message: "Text required. Usage: run402 ai translate <project_id> <text> --to <lang>" })); process.exit(1); }
80
- if (!to) { console.error(JSON.stringify({ status: "error", message: "--to <lang> is required" })); process.exit(1); }
79
+ if (!text) {
80
+ fail({
81
+ code: "BAD_USAGE",
82
+ message: "Text required.",
83
+ hint: "run402 ai translate <project_id> <text> --to <lang>",
84
+ });
85
+ }
86
+ if (!to) {
87
+ fail({ code: "BAD_USAGE", message: "--to <lang> is required" });
88
+ }
81
89
 
82
90
  try {
83
91
  const data = await getSdk().ai.translate(projectId, { text, to, from: from ?? undefined, context: context ?? undefined });
@@ -101,7 +109,13 @@ async function moderate(args) {
101
109
  const projectId = resolveProjectId(projectOpt || positional[0]);
102
110
  text = positional[1] || null;
103
111
 
104
- if (!text) { console.error(JSON.stringify({ status: "error", message: "Text required. Usage: run402 ai moderate <project_id> <text>" })); process.exit(1); }
112
+ if (!text) {
113
+ fail({
114
+ code: "BAD_USAGE",
115
+ message: "Text required.",
116
+ hint: "run402 ai moderate <project_id> <text>",
117
+ });
118
+ }
105
119
 
106
120
  try {
107
121
  const data = await getSdk().ai.moderate(projectId, text);
package/lib/allowance.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readAllowance, saveAllowance, ALLOWANCE_FILE, API } 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 allowance — Manage your agent allowance
6
6
 
@@ -82,7 +82,7 @@ async function status() {
82
82
  const data = await getSdk().allowance.status();
83
83
  if (!data.configured) {
84
84
  console.log(JSON.stringify({ status: "no_wallet", message: "No agent allowance found. Run: run402 allowance create" }));
85
- return;
85
+ process.exit(1);
86
86
  }
87
87
  // Preserve CLI's rail field (SDK doesn't surface it; read from local allowance).
88
88
  const w = readAllowance();
@@ -115,8 +115,11 @@ async function create() {
115
115
  } catch (err) {
116
116
  const msg = (err instanceof Error) ? err.message : String(err);
117
117
  if (/already exists/i.test(msg)) {
118
- console.log(JSON.stringify({ status: "error", message: "Agent allowance already exists. Use 'status' to check it." }));
119
- process.exit(1);
118
+ fail({
119
+ code: "ALLOWANCE_EXISTS",
120
+ message: "Agent allowance already exists.",
121
+ hint: "Use 'status' to check it.",
122
+ });
120
123
  }
121
124
  reportSdkError(err);
122
125
  }
@@ -124,7 +127,13 @@ async function create() {
124
127
 
125
128
  async function fund() {
126
129
  const w = readAllowance();
127
- if (!w) { console.log(JSON.stringify({ status: "error", message: "No agent allowance. Run: run402 allowance create" })); process.exit(1); }
130
+ if (!w) {
131
+ fail({
132
+ code: "NO_ALLOWANCE",
133
+ message: "No agent allowance.",
134
+ hint: "Run: run402 allowance create",
135
+ });
136
+ }
128
137
 
129
138
  if (w.rail === "mpp") {
130
139
  // Tempo Moderato faucet — instant, no polling needed
@@ -139,8 +148,11 @@ async function fund() {
139
148
  });
140
149
  const data = await res.json();
141
150
  if (data.error) {
142
- console.log(JSON.stringify({ status: "error", message: data.error.message || "Tempo faucet failed" }));
143
- process.exit(1);
151
+ fail({
152
+ code: "FAUCET_FAILED",
153
+ message: data.error.message || "Tempo faucet failed",
154
+ details: { rail: "mpp" },
155
+ });
144
156
  }
145
157
 
146
158
  // Re-read balance once (instant confirmation)
@@ -164,8 +176,11 @@ async function fund() {
164
176
  const res = await fetch(`${API}/faucet/v1`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address: w.address }) });
165
177
  const data = await res.json();
166
178
  if (!res.ok) {
167
- console.log(JSON.stringify({ status: "error", ...data }));
168
- process.exit(1);
179
+ fail({
180
+ code: data?.code || "FAUCET_FAILED",
181
+ message: data?.message || "Faucet request failed",
182
+ details: { http: res.status, ...data },
183
+ });
169
184
  }
170
185
 
171
186
  const MAX_WAIT = 30;
@@ -196,7 +211,13 @@ async function readUsdcBalance(client, usdc, address) {
196
211
 
197
212
  async function balance() {
198
213
  const w = readAllowance();
199
- if (!w) { console.log(JSON.stringify({ status: "error", message: "No agent allowance. Run: run402 allowance create" })); process.exit(1); }
214
+ if (!w) {
215
+ fail({
216
+ code: "NO_ALLOWANCE",
217
+ message: "No agent allowance.",
218
+ hint: "Run: run402 allowance create",
219
+ });
220
+ }
200
221
 
201
222
  const { createPublicClient, http, base, baseSepolia, tempoModerato } = await loadDeps();
202
223
  const mainnetClient = createPublicClient({ chain: base, transport: http() });
@@ -229,19 +250,30 @@ async function exportAddr() {
229
250
  const address = await getSdk().allowance.export();
230
251
  console.log(address);
231
252
  } catch {
232
- console.log(JSON.stringify({ status: "error", message: "No agent allowance." }));
233
- process.exit(1);
253
+ fail({ code: "NO_ALLOWANCE", message: "No agent allowance." });
234
254
  }
235
255
  }
236
256
 
237
257
  async function checkout(args) {
238
258
  const w = readAllowance();
239
- if (!w) { console.log(JSON.stringify({ status: "error", message: "No agent allowance. Run: run402 allowance create" })); process.exit(1); }
259
+ if (!w) {
260
+ fail({
261
+ code: "NO_ALLOWANCE",
262
+ message: "No agent allowance.",
263
+ hint: "Run: run402 allowance create",
264
+ });
265
+ }
240
266
  let amount = null;
241
267
  for (let i = 0; i < args.length; i++) {
242
268
  if (args[i] === "--amount" && args[i + 1]) amount = parseInt(args[++i], 10);
243
269
  }
244
- if (!amount) { console.error(JSON.stringify({ status: "error", message: "Missing --amount <usd_micros> (e.g. --amount 5000000 for $5)" })); process.exit(1); }
270
+ if (!amount) {
271
+ fail({
272
+ code: "BAD_USAGE",
273
+ message: "Missing --amount <usd_micros>",
274
+ hint: "e.g. --amount 5000000 for $5",
275
+ });
276
+ }
245
277
  const res = await fetch(`${API}/billing/v1/checkouts`, {
246
278
  method: "POST",
247
279
  headers: { "Content-Type": "application/json" },
@@ -254,7 +286,13 @@ async function checkout(args) {
254
286
 
255
287
  async function history(args) {
256
288
  const w = readAllowance();
257
- if (!w) { console.log(JSON.stringify({ status: "error", message: "No agent allowance. Run: run402 allowance create" })); process.exit(1); }
289
+ if (!w) {
290
+ fail({
291
+ code: "NO_ALLOWANCE",
292
+ message: "No agent allowance.",
293
+ hint: "Run: run402 allowance create",
294
+ });
295
+ }
258
296
  let limit = 20;
259
297
  for (let i = 0; i < args.length; i++) {
260
298
  if (args[i] === "--limit" && args[i + 1]) limit = parseInt(args[++i], 10);
package/lib/apps.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { allowanceAuthHeaders, saveProject } 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 apps — Browse and manage the app marketplace
6
6
 
@@ -177,7 +177,9 @@ async function versions(projectId) {
177
177
  }
178
178
 
179
179
  async function inspect(versionId) {
180
- if (!versionId) { console.error(JSON.stringify({ status: "error", message: "Missing version ID" })); process.exit(1); }
180
+ if (!versionId) {
181
+ fail({ code: "BAD_USAGE", message: "Missing version ID" });
182
+ }
181
183
  try {
182
184
  const data = await getSdk().apps.getApp(versionId);
183
185
  console.log(JSON.stringify(data, null, 2));
package/lib/auth.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { findProject, resolveProjectId, API } 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 auth — Manage project user authentication
6
6
 
@@ -121,8 +121,12 @@ async function magicLink(args) {
121
121
  const redirect = parseFlag(args, "--redirect");
122
122
  const projectId = resolveProjectId(parseFlag(args, "--project"));
123
123
 
124
- if (!email) { console.error(JSON.stringify({ status: "error", message: "Missing --email" })); process.exit(1); }
125
- if (!redirect) { console.error(JSON.stringify({ status: "error", message: "Missing --redirect <url>" })); process.exit(1); }
124
+ if (!email) {
125
+ fail({ code: "BAD_USAGE", message: "Missing --email" });
126
+ }
127
+ if (!redirect) {
128
+ fail({ code: "BAD_USAGE", message: "Missing --redirect <url>" });
129
+ }
126
130
 
127
131
  try {
128
132
  await getSdk().auth.requestMagicLink(projectId, { email, redirectUrl: redirect });
@@ -136,7 +140,9 @@ async function verify(args) {
136
140
  const token = parseFlag(args, "--token");
137
141
  const projectId = resolveProjectId(parseFlag(args, "--project"));
138
142
 
139
- if (!token) { console.error(JSON.stringify({ status: "error", message: "Missing --token" })); process.exit(1); }
143
+ if (!token) {
144
+ fail({ code: "BAD_USAGE", message: "Missing --token" });
145
+ }
140
146
 
141
147
  try {
142
148
  const data = await getSdk().auth.verifyMagicLink(projectId, token);
@@ -152,8 +158,12 @@ async function setPassword(args) {
152
158
  const currentPassword = parseFlag(args, "--current");
153
159
  const projectId = resolveProjectId(parseFlag(args, "--project"));
154
160
 
155
- if (!accessToken) { console.error(JSON.stringify({ status: "error", message: "Missing --token <bearer_token>" })); process.exit(1); }
156
- if (!newPassword) { console.error(JSON.stringify({ status: "error", message: "Missing --new <password>" })); process.exit(1); }
161
+ if (!accessToken) {
162
+ fail({ code: "BAD_USAGE", message: "Missing --token <bearer_token>" });
163
+ }
164
+ if (!newPassword) {
165
+ fail({ code: "BAD_USAGE", message: "Missing --new <password>" });
166
+ }
157
167
 
158
168
  try {
159
169
  await getSdk().auth.setUserPassword(projectId, {
@@ -171,7 +181,12 @@ async function settings(args) {
171
181
  const allowPasswordSet = parseFlag(args, "--allow-password-set");
172
182
  const projectId = resolveProjectId(parseFlag(args, "--project"));
173
183
 
174
- if (allowPasswordSet === null) { console.error(JSON.stringify({ status: "error", message: "Missing --allow-password-set <true|false>" })); process.exit(1); }
184
+ if (allowPasswordSet === null) {
185
+ fail({
186
+ code: "BAD_USAGE",
187
+ message: "Missing --allow-password-set <true|false>",
188
+ });
189
+ }
175
190
 
176
191
  try {
177
192
  await getSdk().auth.settings(projectId, { allow_password_set: allowPasswordSet === "true" });
package/lib/billing.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { API } 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 billing — Email billing accounts, Stripe tier checkout, email packs
6
6
 
@@ -97,8 +97,11 @@ function parseFlag(args, flag) {
97
97
  async function createEmail(args) {
98
98
  const email = args[0];
99
99
  if (!email) {
100
- console.error(JSON.stringify({ status: "error", message: "Missing email. Usage: run402 billing create-email <email>" }));
101
- process.exit(1);
100
+ fail({
101
+ code: "BAD_USAGE",
102
+ message: "Missing email.",
103
+ hint: "run402 billing create-email <email>",
104
+ });
102
105
  }
103
106
  try {
104
107
  const data = await getSdk().billing.createEmailAccount(email);
@@ -112,8 +115,11 @@ async function linkWallet(args) {
112
115
  const accountId = args[0];
113
116
  const wallet = args[1];
114
117
  if (!accountId || !wallet) {
115
- console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing link-wallet <account_id> <wallet>" }));
116
- process.exit(1);
118
+ fail({
119
+ code: "BAD_USAGE",
120
+ message: "Missing <account_id> and/or <wallet>.",
121
+ hint: "run402 billing link-wallet <account_id> <wallet>",
122
+ });
117
123
  }
118
124
  try {
119
125
  await getSdk().billing.linkWallet(accountId, wallet);
@@ -126,14 +132,16 @@ async function linkWallet(args) {
126
132
  async function tierCheckout(args) {
127
133
  const tier = args[0];
128
134
  if (!tier) {
129
- console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing tier-checkout <tier> [--email <e> | --wallet <w>]" }));
130
- process.exit(1);
135
+ fail({
136
+ code: "BAD_USAGE",
137
+ message: "Missing <tier>.",
138
+ hint: "run402 billing tier-checkout <tier> [--email <e> | --wallet <w>]",
139
+ });
131
140
  }
132
141
  const email = parseFlag(args, "--email");
133
142
  const wallet = parseFlag(args, "--wallet");
134
143
  if (!email && !wallet) {
135
- console.error(JSON.stringify({ status: "error", message: "Must provide --email or --wallet" }));
136
- process.exit(1);
144
+ fail({ code: "BAD_USAGE", message: "Must provide --email or --wallet" });
137
145
  }
138
146
  try {
139
147
  const data = await getSdk().billing.tierCheckout(tier, { email: email ?? undefined, wallet: wallet ?? undefined });
@@ -147,8 +155,7 @@ async function buyPack(args) {
147
155
  const email = parseFlag(args, "--email");
148
156
  const wallet = parseFlag(args, "--wallet");
149
157
  if (!email && !wallet) {
150
- console.error(JSON.stringify({ status: "error", message: "Must provide --email or --wallet" }));
151
- process.exit(1);
158
+ fail({ code: "BAD_USAGE", message: "Must provide --email or --wallet" });
152
159
  }
153
160
  try {
154
161
  const data = await getSdk().billing.buyEmailPack({ email: email ?? undefined, wallet: wallet ?? undefined });
@@ -162,8 +169,11 @@ async function autoRecharge(args) {
162
169
  const accountId = args[0];
163
170
  const state = args[1];
164
171
  if (!accountId || !state || !["on", "off"].includes(state)) {
165
- console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing auto-recharge <account_id> <on|off> [--threshold <n>]" }));
166
- process.exit(1);
172
+ fail({
173
+ code: "BAD_USAGE",
174
+ message: "Missing <account_id> and/or <on|off>.",
175
+ hint: "run402 billing auto-recharge <account_id> <on|off> [--threshold <n>]",
176
+ });
167
177
  }
168
178
  const thresholdStr = parseFlag(args, "--threshold");
169
179
  try {
@@ -182,8 +192,11 @@ async function balance(args) {
182
192
  // Accepts email OR wallet — SDK only models wallet, so keep direct fetch.
183
193
  const id = args[0];
184
194
  if (!id) {
185
- console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing balance <email-or-wallet>" }));
186
- process.exit(1);
195
+ fail({
196
+ code: "BAD_USAGE",
197
+ message: "Missing <email-or-wallet>.",
198
+ hint: "run402 billing balance <email-or-wallet>",
199
+ });
187
200
  }
188
201
  const res = await fetch(`${API}/billing/v1/accounts/${encodeURIComponent(id)}`);
189
202
  const data = await res.json();
@@ -194,8 +207,11 @@ async function balance(args) {
194
207
  async function history(args) {
195
208
  const id = args[0];
196
209
  if (!id) {
197
- console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing history <email-or-wallet> [--limit <n>]" }));
198
- process.exit(1);
210
+ fail({
211
+ code: "BAD_USAGE",
212
+ message: "Missing <email-or-wallet>.",
213
+ hint: "run402 billing history <email-or-wallet> [--limit <n>]",
214
+ });
199
215
  }
200
216
  const limit = parseFlag(args, "--limit") || "50";
201
217
  const res = await fetch(`${API}/billing/v1/accounts/${encodeURIComponent(id)}/history?limit=${encodeURIComponent(limit)}`);
package/lib/blob.mjs CHANGED
@@ -36,7 +36,7 @@ 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
40
 
41
41
  const HELP = `run402 blob — Direct-to-S3 blob storage
42
42
 
@@ -182,9 +182,8 @@ Examples:
182
182
 
183
183
  const UPLOAD_STATE_DIR = join(homedir(), ".run402", "uploads");
184
184
 
185
- function die(msg, code = 1) {
186
- console.error(JSON.stringify({ status: "error", message: msg }));
187
- process.exit(code);
185
+ function die(msg, exit_code = 1) {
186
+ fail({ code: "BAD_USAGE", message: msg, exit_code });
188
187
  }
189
188
 
190
189
  function parseArgs(args) {
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