run402 1.35.1 → 1.35.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.
package/lib/blob.mjs CHANGED
@@ -34,7 +34,7 @@ import { basename, dirname, join, resolve as resolvePath } from "node:path";
34
34
  import { homedir } from "node:os";
35
35
  import { pipeline } from "node:stream/promises";
36
36
 
37
- import { findProject, API } from "./config.mjs";
37
+ import { resolveProject, API } from "./config.mjs";
38
38
 
39
39
  const HELP = `run402 blob — Direct-to-S3 blob storage
40
40
 
@@ -97,13 +97,6 @@ function parseArgs(args) {
97
97
  return out;
98
98
  }
99
99
 
100
- function resolveProject(projectId) {
101
- if (!projectId) die("--project is required (or run 'run402 projects use <id>' to set default)");
102
- const p = findProject(projectId);
103
- if (!p) die(`Project not found: ${projectId}`);
104
- return p;
105
- }
106
-
107
100
  async function sha256File(filePath) {
108
101
  const h = createHash("sha256");
109
102
  const stream = createReadStream(filePath);
package/lib/deploy.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { dirname, resolve } from "path";
3
3
  import { Agent, fetch as undiciFetch } from "undici";
4
- import { API, allowanceAuthHeaders, findProject } from "./config.mjs";
4
+ import { API, allowanceAuthHeaders, resolveProjectId } from "./config.mjs";
5
5
  import { resolveFilePathsInManifest, resolveMigrationsFile } from "./manifest.mjs";
6
6
 
7
7
  // Custom undici dispatcher with longer timeouts for large-batch deploys.
@@ -158,6 +158,91 @@ async function readStdin() {
158
158
  return Buffer.concat(chunks).toString("utf-8");
159
159
  }
160
160
 
161
+ /**
162
+ * Load + parse the manifest from --manifest file or stdin, and resolve any
163
+ * referenced files[].path / migrations_file against the manifest's directory.
164
+ *
165
+ * Returns { manifest } on success, or { error } with a structured error object
166
+ * on any fs / parse failure. Never throws.
167
+ *
168
+ * The returned error shape (GH-44):
169
+ * { status: "error", message, field, path?, hint? }
170
+ * where `field` is one of: "manifest", "stdin", "migrations_file", "files[<i>].path".
171
+ */
172
+ async function loadManifest(opts) {
173
+ let raw;
174
+ let baseDir = null;
175
+
176
+ // Step 1: read the manifest source.
177
+ if (opts.manifest) {
178
+ const manifestAbs = resolve(opts.manifest);
179
+ baseDir = dirname(manifestAbs);
180
+ try {
181
+ raw = readFileSync(opts.manifest, "utf-8");
182
+ } catch (err) {
183
+ if (err && err.code === "ENOENT") {
184
+ return { error: {
185
+ status: "error",
186
+ message: `File not found: ${manifestAbs}`,
187
+ field: "manifest",
188
+ path: manifestAbs,
189
+ hint: "Check that --manifest points to an existing JSON file.",
190
+ } };
191
+ }
192
+ return { error: {
193
+ status: "error",
194
+ message: err && err.message ? err.message : String(err),
195
+ field: "manifest",
196
+ path: manifestAbs,
197
+ ...(err && err.code ? { code: err.code } : {}),
198
+ } };
199
+ }
200
+ } else {
201
+ raw = await readStdin();
202
+ }
203
+
204
+ // Step 2: parse JSON.
205
+ let manifest;
206
+ try {
207
+ manifest = JSON.parse(raw);
208
+ } catch (err) {
209
+ return { error: {
210
+ status: "error",
211
+ message: `Manifest is not valid JSON: ${err.message}`,
212
+ field: opts.manifest ? "manifest" : "stdin",
213
+ ...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
214
+ } };
215
+ }
216
+
217
+ // Step 3: resolve file paths (only when reading from a manifest file — we
218
+ // can't resolve relative paths without a baseDir).
219
+ if (opts.manifest) {
220
+ try {
221
+ resolveMigrationsFile(manifest, baseDir);
222
+ resolveFilePathsInManifest(manifest, baseDir);
223
+ } catch (err) {
224
+ if (err && err.code === "ENOENT") {
225
+ return { error: {
226
+ status: "error",
227
+ message: `File not found: ${err.absPath || err.path || "<unknown>"}`,
228
+ field: err.field || "manifest",
229
+ ...(err.absPath || err.path ? { path: err.absPath || err.path } : {}),
230
+ hint: `Paths in manifest.${err.field || "files[].path"} are resolved relative to the manifest file's directory (${baseDir}).`,
231
+ } };
232
+ }
233
+ return { error: {
234
+ status: "error",
235
+ message: err && err.message ? err.message : String(err),
236
+ ...(err && err.field ? { field: err.field } : {}),
237
+ ...(err && (err.absPath || err.path) ? { path: err.absPath || err.path } : {}),
238
+ ...(err && err.code ? { code: err.code } : {}),
239
+ } };
240
+ }
241
+ }
242
+
243
+ return { manifest };
244
+ }
245
+
161
246
  export async function run(args) {
162
247
  const opts = { manifest: null, project: null };
163
248
  for (let i = 0; i < args.length; i++) {
@@ -166,21 +251,42 @@ export async function run(args) {
166
251
  if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
167
252
  }
168
253
 
169
- const raw = opts.manifest ? readFileSync(opts.manifest, "utf-8") : await readStdin();
170
- const manifest = JSON.parse(raw);
171
- if (opts.manifest) {
172
- const baseDir = dirname(resolve(opts.manifest));
173
- resolveMigrationsFile(manifest, baseDir);
174
- resolveFilePathsInManifest(manifest, baseDir);
254
+ // Load + parse the manifest. Errors here (missing --manifest path, malformed
255
+ // JSON, or any referenced files[].path / migrations_file that doesn't exist)
256
+ // must be surfaced as structured JSON on stderr — never as a raw Node stack
257
+ // trace (GH-44). The CLI is agent-first; stack traces break JSON consumers.
258
+ const manifestResult = await loadManifest(opts);
259
+ if (manifestResult.error) {
260
+ console.error(JSON.stringify(manifestResult.error));
261
+ process.exit(1);
262
+ }
263
+ const manifest = manifestResult.manifest;
264
+
265
+ // If both sources set project_id and they disagree, refuse to deploy rather
266
+ // than silently shipping to the wrong target. Agents and humans should be
267
+ // forced to be explicit when the two sources conflict (GH-42).
268
+ if (opts.project && manifest.project_id && opts.project !== manifest.project_id) {
269
+ const err = {
270
+ status: "error",
271
+ message: `project_id conflict: manifest.project_id=${manifest.project_id} but --project=${opts.project}`,
272
+ manifest_project_id: manifest.project_id,
273
+ flag_project_id: opts.project,
274
+ 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).",
275
+ };
276
+ console.error(JSON.stringify(err));
277
+ process.exit(1);
175
278
  }
176
279
 
177
- // --project flag overrides manifest's project_id
280
+ // --project flag fills in manifest's project_id when the manifest doesn't
281
+ // specify one. (When both are set they must already agree — enforced above.)
178
282
  if (opts.project) manifest.project_id = opts.project;
179
283
 
180
- // If no project_id in manifest, use active project
284
+ // If no project_id in manifest, fall back to the active project.
285
+ // resolveProjectId() returns the active project id when its argument is
286
+ // falsy, and emits a clear error + exits non-zero when no active project
287
+ // is set either.
181
288
  if (!manifest.project_id) {
182
- const { id } = findProject(null);
183
- manifest.project_id = id;
289
+ manifest.project_id = resolveProjectId(null);
184
290
  }
185
291
 
186
292
  // Remove legacy 'name' field if present
package/lib/manifest.mjs CHANGED
@@ -11,6 +11,12 @@ const TEXT_EXTS = new Set([
11
11
  * read the SQL from that file path and set `migrations` to its contents.
12
12
  * `migrations_file` is resolved relative to `baseDir`.
13
13
  *
14
+ * On read failure, re-throws the underlying fs error with additional context
15
+ * attached:
16
+ * err.field = "migrations_file"
17
+ * err.absPath = <absolute path that was attempted>
18
+ * (the original Error.code / Error.message / Error.path are preserved).
19
+ *
14
20
  * @param {object} manifest Parsed manifest JSON (mutated in place)
15
21
  * @param {string} baseDir Directory to resolve relative paths from
16
22
  * @returns {object} The same manifest object
@@ -18,7 +24,13 @@ const TEXT_EXTS = new Set([
18
24
  export function resolveMigrationsFile(manifest, baseDir) {
19
25
  if (!manifest.migrations_file) return manifest;
20
26
  const abs = resolve(baseDir, manifest.migrations_file);
21
- manifest.migrations = readFileSync(abs, "utf-8");
27
+ try {
28
+ manifest.migrations = readFileSync(abs, "utf-8");
29
+ } catch (err) {
30
+ err.field = "migrations_file";
31
+ err.absPath = abs;
32
+ throw err;
33
+ }
22
34
  delete manifest.migrations_file;
23
35
  return manifest;
24
36
  }
@@ -31,6 +43,12 @@ export function resolveMigrationsFile(manifest, baseDir) {
31
43
  *
32
44
  * Entries with `data` already set are left untouched.
33
45
  *
46
+ * On read failure, re-throws the underlying fs error with additional context
47
+ * attached:
48
+ * err.field = `files[<i>].path`
49
+ * err.absPath = <absolute path that was attempted>
50
+ * (the original Error.code / Error.message / Error.path are preserved).
51
+ *
34
52
  * @param {object} manifest Parsed manifest JSON (mutated in place)
35
53
  * @param {string} baseDir Directory to resolve relative paths from
36
54
  * @returns {object} The same manifest object
@@ -38,18 +56,25 @@ export function resolveMigrationsFile(manifest, baseDir) {
38
56
  export function resolveFilePathsInManifest(manifest, baseDir) {
39
57
  if (!Array.isArray(manifest.files)) return manifest;
40
58
 
41
- for (const entry of manifest.files) {
59
+ for (let i = 0; i < manifest.files.length; i++) {
60
+ const entry = manifest.files[i];
42
61
  if (!entry.path || entry.data !== undefined) continue;
43
62
 
44
63
  const abs = resolve(baseDir, entry.path);
45
64
  const ext = extname(abs).toLowerCase();
46
65
  const isText = TEXT_EXTS.has(ext);
47
66
 
48
- if (isText) {
49
- entry.data = readFileSync(abs, "utf-8");
50
- } else {
51
- entry.data = readFileSync(abs).toString("base64");
52
- entry.encoding = "base64";
67
+ try {
68
+ if (isText) {
69
+ entry.data = readFileSync(abs, "utf-8");
70
+ } else {
71
+ entry.data = readFileSync(abs).toString("base64");
72
+ entry.encoding = "base64";
73
+ }
74
+ } catch (err) {
75
+ err.field = `files[${i}].path`;
76
+ err.absPath = abs;
77
+ throw err;
53
78
  }
54
79
 
55
80
  // If no explicit file (deploy target name), use the path value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.35.1",
3
+ "version": "1.35.2",
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": {