run402 1.54.3 → 1.54.4

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/argparse.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { existsSync, statSync } from "node:fs";
1
2
  import { fail } from "./sdk-errors.mjs";
2
3
  import { resolveProjectId } from "./config.mjs";
3
4
 
@@ -143,6 +144,46 @@ export function validateWebhookUrl(url, fieldName = "--url") {
143
144
  }
144
145
  }
145
146
 
147
+ /**
148
+ * Validate that a CLI flag pointing at a filesystem path resolves to an
149
+ * existing regular file. Replaces the GH-195 inline pattern that was
150
+ * duplicated across `functions deploy`, `secrets set`, `projects sql`,
151
+ * and `projects apply-expose` (GH-233).
152
+ *
153
+ * Without this guard, `readFileSync` against a missing path leaks a raw
154
+ * `node:fs` ENOENT/EISDIR stack to stderr (with the V8 source pointer),
155
+ * which violates the CLI's structured-error contract.
156
+ *
157
+ * No-op: this helper is meant to be called only when the flag is set.
158
+ * Callers handle the optional/required dichotomy themselves.
159
+ *
160
+ * On failure: `fail()` writes a `FILE_NOT_FOUND` or `NOT_A_FILE` envelope
161
+ * to stderr and exits 1.
162
+ *
163
+ * @param {string} path - The filesystem path captured from the flag.
164
+ * @param {string} fieldName - The flag name for the envelope (default "--file").
165
+ */
166
+ export function validateRegularFile(path, fieldName = "--file") {
167
+ if (!existsSync(path)) {
168
+ fail({
169
+ code: "FILE_NOT_FOUND",
170
+ message: `File not found: ${path}`,
171
+ field: fieldName,
172
+ path,
173
+ hint: `Check that ${fieldName} points to an existing file.`,
174
+ });
175
+ }
176
+ const stat = statSync(path);
177
+ if (!stat.isFile()) {
178
+ fail({
179
+ code: "NOT_A_FILE",
180
+ message: `${fieldName} points to a ${stat.isDirectory() ? "directory" : "non-regular file"}: ${path}`,
181
+ field: fieldName,
182
+ path,
183
+ });
184
+ }
185
+ }
186
+
146
187
  export function positionalArgs(args = [], flagsWithValues = []) {
147
188
  const valueFlags = new Set(flagsWithValues);
148
189
  const out = [];
package/lib/deploy-v2.mjs CHANGED
@@ -166,6 +166,43 @@ async function applyCmd(args) {
166
166
  });
167
167
  }
168
168
 
169
+ // GH-232: Reject empty specs client-side. Without this guard,
170
+ // `run402 deploy apply --spec '{}'` (and `--manifest <empty>`) would silently
171
+ // send an empty ReleaseSpec to /deploy/v2/plans with no signal that nothing
172
+ // was deployed. This mirrors the GH-185 guard already in place for the
173
+ // legacy `run402 deploy --manifest` path.
174
+ //
175
+ // `deploy apply` is v2-only — only meaningful keys are the v2 ReleaseSpec
176
+ // shape (database, site, functions, secrets, subdomains, domains).
177
+ // For object-typed sections the "container is non-empty" check isn't enough
178
+ // — `site:{replace:{}}` has one key but ships nothing. We recurse one level
179
+ // so any object whose own values are all empty containers is still empty.
180
+ const meaningful = ["database", "site", "functions", "secrets", "subdomains", "domains"];
181
+ function hasContent(v) {
182
+ if (v == null) return false;
183
+ if (Array.isArray(v)) return v.length > 0;
184
+ if (typeof v === "object") {
185
+ const keys = Object.keys(v);
186
+ if (keys.length === 0) return false;
187
+ return keys.some((k) => hasContent(v[k]));
188
+ }
189
+ if (typeof v === "string") return v.length > 0;
190
+ return true;
191
+ }
192
+ const hasMeaningfulContent = spec && typeof spec === "object" && !Array.isArray(spec) && meaningful.some((key) => hasContent(spec[key]));
193
+ if (!hasMeaningfulContent) {
194
+ fail({
195
+ code: "MANIFEST_EMPTY",
196
+ message: `Manifest contains no deployable sections. Expected at least one of: ${meaningful.join(", ")}`,
197
+ hint: "Did you mean to write a 'site.replace' or 'database.migrations' block? See https://run402.com/schemas/manifest.v1.json",
198
+ details: {
199
+ field: opts.manifest ? "manifest" : opts.spec ? "spec" : "stdin",
200
+ ...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
201
+ meaningful_keys: meaningful,
202
+ },
203
+ });
204
+ }
205
+
169
206
  if (opts.manifest) resolveFileDataPaths(spec, dirname(resolve(opts.manifest)));
170
207
 
171
208
  if (opts.project && spec.project_id && spec.project_id !== opts.project) {
package/lib/functions.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { readFileSync, existsSync, statSync } from "fs";
1
+ import { readFileSync } from "fs";
2
2
  import { findProject, API } from "./config.mjs";
3
3
  import { getSdk } from "./sdk.mjs";
4
4
  import { reportSdkError, fail } from "./sdk-errors.mjs";
5
- import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag } from "./argparse.mjs";
5
+ import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag, validateRegularFile } from "./argparse.mjs";
6
6
 
7
7
  const HELP = `run402 functions — Manage serverless functions
8
8
 
@@ -181,24 +181,7 @@ async function deploy(projectId, name, args) {
181
181
  if (!opts.file) {
182
182
  fail({ code: "BAD_USAGE", message: "Missing --file <file>" });
183
183
  }
184
- if (!existsSync(opts.file)) {
185
- fail({
186
- code: "FILE_NOT_FOUND",
187
- message: `File not found: ${opts.file}`,
188
- field: "--file",
189
- path: opts.file,
190
- hint: "Check that --file points to an existing source file.",
191
- });
192
- }
193
- const stat = statSync(opts.file);
194
- if (!stat.isFile()) {
195
- fail({
196
- code: "NOT_A_FILE",
197
- message: `--file points to a ${stat.isDirectory() ? "directory" : "non-regular file"}: ${opts.file}`,
198
- field: "--file",
199
- path: opts.file,
200
- });
201
- }
184
+ validateRegularFile(opts.file, "--file");
202
185
  const code = readFileSync(opts.file, "utf-8");
203
186
 
204
187
  const deployOpts = { name, code };
package/lib/projects.mjs CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from "fs";
2
2
  import { findProject, loadKeyStore, API, allowanceAuthHeaders, resolveProjectId, getActiveProjectId } from "./config.mjs";
3
3
  import { getSdk } from "./sdk.mjs";
4
4
  import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
5
- import { assertKnownFlags, failBadProjectId, hasHelp, normalizeArgv, positionalArgs, resolvePositionalProject } from "./argparse.mjs";
5
+ import { assertKnownFlags, failBadProjectId, hasHelp, normalizeArgv, positionalArgs, resolvePositionalProject, validateRegularFile } from "./argparse.mjs";
6
6
 
7
7
  const HELP = `run402 projects — Manage your deployed Run402 projects
8
8
 
@@ -175,13 +175,14 @@ async function provision(args) {
175
175
  }
176
176
 
177
177
  async function applyExpose(projectId, args = []) {
178
- const p = findProject(projectId);
179
178
  let file = null;
180
179
  let inline = null;
181
180
  for (let i = 0; i < args.length; i++) {
182
181
  if (args[i] === "--file" && args[i + 1]) { file = args[++i]; }
183
182
  else if (!inline && !args[i].startsWith("--")) { inline = args[i]; }
184
183
  }
184
+ if (file) validateRegularFile(file, "--file");
185
+ const p = findProject(projectId);
185
186
  const raw = file ? readFileSync(file, "utf-8") : inline;
186
187
  if (!raw) {
187
188
  fail({
@@ -252,7 +253,6 @@ async function keys(projectId) {
252
253
  }
253
254
 
254
255
  async function sqlCmd(projectId, args = []) {
255
- const p = findProject(projectId);
256
256
  let file = null;
257
257
  let query = null;
258
258
  let paramsRaw = null;
@@ -261,6 +261,8 @@ async function sqlCmd(projectId, args = []) {
261
261
  else if (args[i] === "--params" && args[i + 1]) { paramsRaw = args[++i]; }
262
262
  else if (!query && !args[i].startsWith("--")) { query = args[i]; }
263
263
  }
264
+ if (file) validateRegularFile(file, "--file");
265
+ const p = findProject(projectId);
264
266
  const sql = file ? readFileSync(file, "utf-8") : query;
265
267
  if (!sql) {
266
268
  fail({
package/lib/secrets.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
+ import { validateRegularFile } from "./argparse.mjs";
4
5
 
5
6
  const HELP = `run402 secrets — Manage project secrets
6
7
 
@@ -82,6 +83,7 @@ async function set(projectId, key, args = []) {
82
83
  if (args[i] === "--file" && args[i + 1]) { file = args[++i]; }
83
84
  else if (!value && !args[i].startsWith("--")) { value = args[i]; }
84
85
  }
86
+ if (file) validateRegularFile(file, "--file");
85
87
  const val = file ? readFileSync(file, "utf-8") : value;
86
88
  if (!val) {
87
89
  fail({
@@ -10,7 +10,7 @@ Usage:
10
10
  Subcommands:
11
11
  claim <name> [--project <id>] [--deployment <id>] Claim a subdomain
12
12
  delete <name> --confirm [--project <id>] Release a subdomain. Requires --confirm.
13
- list [<id>] List subdomains for a project
13
+ list [<id>] | list --project <id> List subdomains for a project
14
14
 
15
15
  Options default to the active project and its last deployment when omitted.
16
16
  Legacy syntax 'claim <deployment_id> <name>' is still supported.
@@ -151,8 +151,24 @@ async function deleteSubdomain(allArgs) {
151
151
  }
152
152
  }
153
153
 
154
- async function list(projectIdArg) {
155
- const projectId = resolveProjectId(projectIdArg);
154
+ function parseProjectFlag(args) {
155
+ let project = null;
156
+ const rest = [];
157
+ for (let i = 0; i < args.length; i++) {
158
+ if (args[i] === "--project" && args[i + 1]) { project = args[++i]; }
159
+ else { rest.push(args[i]); }
160
+ }
161
+ return { project, rest };
162
+ }
163
+
164
+ async function list(args) {
165
+ const argList = Array.isArray(args) ? args : [];
166
+ const { project, rest } = parseProjectFlag(argList);
167
+ // Either --project <id> or a positional id is accepted; --project wins
168
+ // when both are supplied. Falls back to the active project when neither
169
+ // is given. Keeps backward-compat with the legacy `subdomains list <id>`
170
+ // form (GH-231; mirrors the GH-209 fix for `domains list`).
171
+ const projectId = resolveProjectId(project || rest[0]);
156
172
  try {
157
173
  const data = await getSdk().subdomains.list(projectId);
158
174
  console.log(JSON.stringify(data, null, 2));
@@ -177,7 +193,7 @@ export async function run(sub, args) {
177
193
  break;
178
194
  }
179
195
  case "delete": await deleteSubdomain(args); break;
180
- case "list": await list(args[0]); break;
196
+ case "list": await list(args); break;
181
197
  default:
182
198
  console.error(`Unknown subcommand: ${sub}\n`);
183
199
  console.log(HELP);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.54.3",
3
+ "version": "1.54.4",
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": {