run402 1.38.0 → 1.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * CLI-side SDK error translator.
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_`.
10
+ * - HTML / non-JSON error bodies → `body_preview` field (first 500 chars),
11
+ * matching GH-84 behavior.
12
+ * - Network errors → `{status: "error", message: "..."}`.
13
+ */
14
+
15
+ export function reportSdkError(err) {
16
+ if (err?.name === "ProjectNotFound") {
17
+ const id = err.projectId || "";
18
+ 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);
23
+ }
24
+
25
+ const payload = { status: "error" };
26
+
27
+ if (err?.status !== undefined && err?.status !== null) {
28
+ payload.http = err.status;
29
+ if (err.body && typeof err.body === "object") {
30
+ Object.assign(payload, err.body);
31
+ } else if (typeof err.body === "string") {
32
+ payload.body_preview = err.body.slice(0, 500);
33
+ }
34
+ } else {
35
+ payload.message = err?.message || String(err);
36
+ }
37
+
38
+ // Keep `status: "error"` as the outer envelope even if the response body
39
+ // happened to contain its own `status` field (e.g. `{"status":"degraded"}`
40
+ // from /health 503 responses). Downstream scripts match on this sentinel.
41
+ payload.status = "error";
42
+
43
+ console.error(JSON.stringify(payload));
44
+ process.exit(1);
45
+ }
package/lib/sdk.mjs ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CLI-side SDK getter.
3
+ *
4
+ * Constructs a fresh Node-flavored SDK on each call. Each CLI invocation is
5
+ * typically a single process, so a fresh instance per subcommand is cheap
6
+ * and sidesteps stale-env issues in tests that mutate RUN402_CONFIG_DIR /
7
+ * RUN402_API_BASE between runs.
8
+ */
9
+
10
+ import { run402 } from "../../sdk/dist/node/index.js";
11
+
12
+ export function getSdk() {
13
+ return run402();
14
+ }
package/lib/secrets.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "fs";
2
- import { findProject, API } from "./config.mjs";
2
+ import { getSdk } from "./sdk.mjs";
3
+ import { reportSdkError } from "./sdk-errors.mjs";
3
4
 
4
5
  const HELP = `run402 secrets — Manage project secrets
5
6
 
@@ -48,7 +49,6 @@ Examples:
48
49
  };
49
50
 
50
51
  async function set(projectId, key, args = []) {
51
- const p = findProject(projectId);
52
52
  let file = null;
53
53
  let value = null;
54
54
  for (let i = 0; i < args.length; i++) {
@@ -57,37 +57,29 @@ async function set(projectId, key, args = []) {
57
57
  }
58
58
  const val = file ? readFileSync(file, "utf-8") : value;
59
59
  if (!val) { console.error(JSON.stringify({ status: "error", message: "Missing secret value. Provide inline or use --file <path>" })); process.exit(1); }
60
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/secrets`, {
61
- method: "POST",
62
- headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
63
- body: JSON.stringify({ key, value: val }),
64
- });
65
- const data = await res.json();
66
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
67
- console.log(JSON.stringify({ status: "ok", message: `Secret '${key}' set for project ${projectId}.` }));
60
+ try {
61
+ await getSdk().secrets.set(projectId, key, val);
62
+ console.log(JSON.stringify({ status: "ok", message: `Secret '${key}' set for project ${projectId}.` }));
63
+ } catch (err) {
64
+ reportSdkError(err);
65
+ }
68
66
  }
69
67
 
70
68
  async function list(projectId) {
71
- const p = findProject(projectId);
72
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/secrets`, {
73
- headers: { "Authorization": `Bearer ${p.service_key}` },
74
- });
75
- const data = await res.json();
76
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
77
- console.log(JSON.stringify(data, null, 2));
69
+ try {
70
+ const data = await getSdk().secrets.list(projectId);
71
+ console.log(JSON.stringify(data, null, 2));
72
+ } catch (err) {
73
+ reportSdkError(err);
74
+ }
78
75
  }
79
76
 
80
77
  async function deleteSecret(projectId, key) {
81
- const p = findProject(projectId);
82
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/secrets/${encodeURIComponent(key)}`, {
83
- method: "DELETE",
84
- headers: { "Authorization": `Bearer ${p.service_key}` },
85
- });
86
- if (res.status === 204 || res.ok) {
78
+ try {
79
+ await getSdk().secrets.delete(projectId, key);
87
80
  console.log(JSON.stringify({ status: "ok", message: `Secret '${key}' deleted.` }));
88
- } else {
89
- const data = await res.json().catch(() => ({}));
90
- console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
81
+ } catch (err) {
82
+ reportSdkError(err);
91
83
  }
92
84
  }
93
85
 
@@ -1,4 +1,6 @@
1
- import { findProject, resolveProjectId, API } from "./config.mjs";
1
+ import { resolveProjectId } from "./config.mjs";
2
+ import { getSdk } from "./sdk.mjs";
3
+ import { reportSdkError } from "./sdk-errors.mjs";
2
4
 
3
5
  const HELP = `run402 sender-domain — Manage custom email sender domain
4
6
 
@@ -35,55 +37,38 @@ async function register(args) {
35
37
  else if (!args[i].startsWith("--") && !domain) { domain = args[i]; }
36
38
  }
37
39
  const projectId = resolveProjectId(projectOpt);
38
- const p = findProject(projectId);
39
40
 
40
41
  if (!domain) {
41
42
  console.error(JSON.stringify({ status: "error", message: "Missing domain. Usage: run402 sender-domain register <domain>" }));
42
43
  process.exit(1);
43
44
  }
44
45
 
45
- const res = await fetch(`${API}/email/v1/domains`, {
46
- method: "POST",
47
- headers: { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" },
48
- body: JSON.stringify({ domain }),
49
- });
50
- const data = await res.json();
51
- if (!res.ok) {
52
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
53
- process.exit(1);
46
+ try {
47
+ const data = await getSdk().senderDomain.register(projectId, domain);
48
+ console.log(JSON.stringify(data, null, 2));
49
+ } catch (err) {
50
+ reportSdkError(err);
54
51
  }
55
- console.log(JSON.stringify(data, null, 2));
56
52
  }
57
53
 
58
54
  async function status(args) {
59
55
  const projectId = resolveProjectId(parseFlag(args, "--project"));
60
- const p = findProject(projectId);
61
-
62
- const res = await fetch(`${API}/email/v1/domains`, {
63
- headers: { Authorization: `Bearer ${p.service_key}` },
64
- });
65
- const data = await res.json();
66
- if (!res.ok) {
67
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
68
- process.exit(1);
56
+ try {
57
+ const data = await getSdk().senderDomain.status(projectId);
58
+ console.log(JSON.stringify(data, null, 2));
59
+ } catch (err) {
60
+ reportSdkError(err);
69
61
  }
70
- console.log(JSON.stringify(data, null, 2));
71
62
  }
72
63
 
73
64
  async function remove(args) {
74
65
  const projectId = resolveProjectId(parseFlag(args, "--project"));
75
- const p = findProject(projectId);
76
-
77
- const res = await fetch(`${API}/email/v1/domains`, {
78
- method: "DELETE",
79
- headers: { Authorization: `Bearer ${p.service_key}` },
80
- });
81
- const data = await res.json();
82
- if (!res.ok) {
83
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
84
- process.exit(1);
66
+ try {
67
+ await getSdk().senderDomain.remove(projectId);
68
+ console.log(JSON.stringify({ status: "ok" }));
69
+ } catch (err) {
70
+ reportSdkError(err);
85
71
  }
86
- console.log(JSON.stringify(data));
87
72
  }
88
73
 
89
74
  async function inboundToggle(action, args) {
@@ -94,25 +79,23 @@ async function inboundToggle(action, args) {
94
79
  else if (!args[i].startsWith("--") && !domain) { domain = args[i]; }
95
80
  }
96
81
  const projectId = resolveProjectId(projectOpt);
97
- const p = findProject(projectId);
98
82
 
99
83
  if (!domain) {
100
84
  console.error(JSON.stringify({ status: "error", message: `Missing domain. Usage: run402 sender-domain inbound-${action} <domain>` }));
101
85
  process.exit(1);
102
86
  }
103
87
 
104
- const method = action === "enable" ? "POST" : "DELETE";
105
- const res = await fetch(`${API}/email/v1/domains/inbound`, {
106
- method,
107
- headers: { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" },
108
- body: JSON.stringify({ domain }),
109
- });
110
- const data = await res.json();
111
- if (!res.ok) {
112
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
113
- process.exit(1);
88
+ try {
89
+ if (action === "enable") {
90
+ const data = await getSdk().senderDomain.enableInbound(projectId, domain);
91
+ console.log(JSON.stringify(data, null, 2));
92
+ } else {
93
+ await getSdk().senderDomain.disableInbound(projectId, domain);
94
+ console.log(JSON.stringify({ status: "ok", domain }));
95
+ }
96
+ } catch (err) {
97
+ reportSdkError(err);
114
98
  }
115
- console.log(JSON.stringify(data, null, 2));
116
99
  }
117
100
 
118
101
  export async function run(sub, args) {
package/lib/service.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { API } from "./config.mjs";
1
+ import { getSdk } from "./sdk.mjs";
2
+ import { reportSdkError } from "./sdk-errors.mjs";
2
3
 
3
4
  const HELP = `run402 service — Run402 service health and availability
4
5
 
@@ -12,22 +13,22 @@ Notes:
12
13
  balance, tier, projects), use 'run402 status'.
13
14
  `;
14
15
 
15
- async function fetchAndEmit(path) {
16
- let res;
16
+ async function status() {
17
17
  try {
18
- res = await fetch(`${API}${path}`);
18
+ const data = await getSdk().service.status();
19
+ console.log(JSON.stringify(data, null, 2));
19
20
  } catch (err) {
20
- console.error(JSON.stringify({ status: "error", message: err?.message || String(err) }));
21
- process.exit(1);
21
+ reportSdkError(err);
22
22
  }
23
- const text = await res.text();
24
- let body;
25
- try { body = JSON.parse(text); } catch { body = text; }
26
- if (!res.ok) {
27
- console.error(JSON.stringify({ status: "error", http: res.status, body }));
28
- process.exit(1);
23
+ }
24
+
25
+ async function health() {
26
+ try {
27
+ const data = await getSdk().service.health();
28
+ console.log(JSON.stringify(data, null, 2));
29
+ } catch (err) {
30
+ reportSdkError(err);
29
31
  }
30
- console.log(JSON.stringify(body, null, 2));
31
32
  }
32
33
 
33
34
  export async function run(sub, args) {
@@ -37,12 +38,8 @@ export async function run(sub, args) {
37
38
  process.exit(0);
38
39
  }
39
40
  switch (sub) {
40
- case "status":
41
- await fetchAndEmit("/status");
42
- return;
43
- case "health":
44
- await fetchAndEmit("/health");
45
- return;
41
+ case "status": await status(); break;
42
+ case "health": await health(); break;
46
43
  default:
47
44
  console.error(`Unknown subcommand: ${sub}\n`);
48
45
  console.log(HELP);
package/lib/sites.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { dirname, resolve } from "path";
3
- import { API, allowanceAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
3
+ import { allowanceAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
4
4
  import { resolveFilePathsInManifest } from "./manifest.mjs";
5
+ import { getSdk } from "./sdk.mjs";
6
+ import { reportSdkError } from "./sdk-errors.mjs";
5
7
 
6
8
  const HELP = `run402 sites — Deploy and manage static sites
7
9
 
@@ -99,22 +101,22 @@ async function deploy(args) {
99
101
  const raw = opts.manifest ? readFileSync(opts.manifest, "utf-8") : await readStdin();
100
102
  const manifest = JSON.parse(raw);
101
103
  if (opts.manifest) resolveFilePathsInManifest(manifest, dirname(resolve(opts.manifest)));
102
- const body = { files: manifest.files, project: projectId };
103
- if (opts.target) body.target = opts.target;
104
- if (opts.inherit) body.inherit = true;
105
-
106
- const authHeaders = allowanceAuthHeaders("/deployments/v1");
107
- const res = await fetch(`${API}/deployments/v1`, {
108
- method: "POST",
109
- headers: { "Content-Type": "application/json", ...authHeaders },
110
- body: JSON.stringify(body),
111
- });
112
- const data = await res.json();
113
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
114
- if (data.deployment_id) {
115
- updateProject(projectId, { last_deployment_id: data.deployment_id });
104
+
105
+ // Preserve the aggressive early exit when no allowance is configured.
106
+ allowanceAuthHeaders("/deployments/v1");
107
+
108
+ try {
109
+ const data = await getSdk().sites.deploy(projectId, manifest.files, {
110
+ target: opts.target,
111
+ inherit: opts.inherit,
112
+ });
113
+ if (data.deployment_id) {
114
+ updateProject(projectId, { last_deployment_id: data.deployment_id });
115
+ }
116
+ console.log(JSON.stringify(data, null, 2));
117
+ } catch (err) {
118
+ reportSdkError(err);
116
119
  }
117
- console.log(JSON.stringify(data, null, 2));
118
120
  }
119
121
 
120
122
  async function status(args) {
@@ -123,10 +125,12 @@ async function status(args) {
123
125
  if (!args[i].startsWith("-")) { deploymentId = args[i]; break; }
124
126
  }
125
127
  if (!deploymentId) { console.error(JSON.stringify({ status: "error", message: "Missing deployment ID" })); process.exit(1); }
126
- const res = await fetch(`${API}/deployments/v1/${deploymentId}`);
127
- const data = await res.json();
128
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
129
- console.log(JSON.stringify(data, null, 2));
128
+ try {
129
+ const data = await getSdk().sites.getDeployment(deploymentId);
130
+ console.log(JSON.stringify(data, null, 2));
131
+ } catch (err) {
132
+ reportSdkError(err);
133
+ }
130
134
  }
131
135
 
132
136
  export async function run(sub, args) {
@@ -1,4 +1,6 @@
1
- import { resolveProject, resolveProjectId, API } from "./config.mjs";
1
+ import { resolveProject, resolveProjectId } from "./config.mjs";
2
+ import { getSdk } from "./sdk.mjs";
3
+ import { reportSdkError } from "./sdk-errors.mjs";
2
4
 
3
5
  const HELP = `run402 subdomains — Manage custom subdomains
4
6
 
@@ -55,7 +57,6 @@ async function claim(positionalArgs, flagArgs) {
55
57
  if (flagArgs[i] === "--project" && flagArgs[i + 1]) opts.project = flagArgs[++i];
56
58
  if (flagArgs[i] === "--deployment" && flagArgs[i + 1]) opts.deployment = flagArgs[++i];
57
59
  }
58
- // positional: [name] or [deployment_id, name]
59
60
  let name, deploymentId;
60
61
  if (positionalArgs.length >= 2) {
61
62
  deploymentId = positionalArgs[0];
@@ -68,15 +69,12 @@ async function claim(positionalArgs, flagArgs) {
68
69
  const p = resolveProject(opts.project);
69
70
  deploymentId = opts.deployment || deploymentId || p.last_deployment_id;
70
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); }
71
- const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${p.service_key}` };
72
- const res = await fetch(`${API}/subdomains/v1`, {
73
- method: "POST",
74
- headers,
75
- body: JSON.stringify({ name, deployment_id: deploymentId }),
76
- });
77
- const data = await res.json();
78
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
79
- console.log(JSON.stringify(data, null, 2));
72
+ try {
73
+ const data = await getSdk().subdomains.claim(name, deploymentId, { projectId });
74
+ console.log(JSON.stringify(data, null, 2));
75
+ } catch (err) {
76
+ reportSdkError(err);
77
+ }
80
78
  }
81
79
 
82
80
  async function deleteSubdomain(name, args) {
@@ -84,28 +82,23 @@ async function deleteSubdomain(name, args) {
84
82
  for (let i = 0; i < args.length; i++) {
85
83
  if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
86
84
  }
87
- const p = resolveProject(opts.project);
88
- const headers = { "Authorization": `Bearer ${p.service_key}` };
89
- const res = await fetch(`${API}/subdomains/v1/${encodeURIComponent(name)}`, {
90
- method: "DELETE",
91
- headers,
92
- });
93
- if (res.status === 204 || res.ok) {
85
+ const projectId = resolveProjectId(opts.project);
86
+ try {
87
+ await getSdk().subdomains.delete(name, { projectId });
94
88
  console.log(JSON.stringify({ status: "ok", message: `Subdomain '${name}' released.` }));
95
- } else {
96
- const data = await res.json().catch(() => ({}));
97
- console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
89
+ } catch (err) {
90
+ reportSdkError(err);
98
91
  }
99
92
  }
100
93
 
101
- async function list(projectId) {
102
- const p = resolveProject(projectId);
103
- const res = await fetch(`${API}/subdomains/v1`, {
104
- headers: { "Authorization": `Bearer ${p.service_key}` },
105
- });
106
- const data = await res.json();
107
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
108
- console.log(JSON.stringify(data, null, 2));
94
+ async function list(projectIdArg) {
95
+ const projectId = resolveProjectId(projectIdArg);
96
+ try {
97
+ const data = await getSdk().subdomains.list(projectId);
98
+ console.log(JSON.stringify(data, null, 2));
99
+ } catch (err) {
100
+ reportSdkError(err);
101
+ }
109
102
  }
110
103
 
111
104
  export async function run(sub, args) {
package/lib/tier.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { readAllowance, ALLOWANCE_FILE, API, allowanceAuthHeaders } from "./config.mjs";
2
- import { setupPaidFetch } from "./paid-fetch.mjs";
1
+ import { getSdk } from "./sdk.mjs";
2
+ import { reportSdkError } from "./sdk-errors.mjs";
3
3
 
4
4
  const HELP = `run402 tier — Manage your Run402 tier subscription
5
5
 
@@ -25,36 +25,22 @@ Examples:
25
25
  `;
26
26
 
27
27
  async function status() {
28
- const authHeaders = allowanceAuthHeaders("/tiers/v1/status");
29
- const res = await fetch(`${API}/tiers/v1/status`, {
30
- headers: { ...authHeaders },
31
- });
32
- const text = await res.text();
33
- let data;
34
28
  try {
35
- data = JSON.parse(text);
36
- } catch {
37
- console.error(JSON.stringify({ status: "error", http: res.status, message: "Non-JSON response from server", body: text.slice(0, 500) }));
38
- process.exit(1);
29
+ const data = await getSdk().tier.status();
30
+ console.log(JSON.stringify(data, null, 2));
31
+ } catch (err) {
32
+ reportSdkError(err);
39
33
  }
40
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
41
- console.log(JSON.stringify(data, null, 2));
42
34
  }
43
35
 
44
36
  async function set(tierName) {
45
37
  if (!tierName) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 tier set <prototype|hobby|team>" })); process.exit(1); }
46
- const fetchPaid = await setupPaidFetch();
47
- const res = await fetchPaid(`${API}/tiers/v1/${tierName}`, { method: "POST", headers: { "Content-Type": "application/json" } });
48
- const text = await res.text();
49
- let data;
50
38
  try {
51
- data = JSON.parse(text);
52
- } catch {
53
- console.error(JSON.stringify({ status: "error", http: res.status, message: "Non-JSON response from server", body: text.slice(0, 500) }));
54
- process.exit(1);
39
+ const data = await getSdk().tier.set(tierName);
40
+ console.log(JSON.stringify(data, null, 2));
41
+ } catch (err) {
42
+ reportSdkError(err);
55
43
  }
56
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
57
- console.log(JSON.stringify(data, null, 2));
58
44
  }
59
45
 
60
46
  export async function run(sub, args) {