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 +41 -0
- package/lib/deploy-v2.mjs +37 -0
- package/lib/functions.mjs +3 -20
- package/lib/projects.mjs +5 -3
- package/lib/secrets.mjs +2 -0
- package/lib/subdomains.mjs +20 -4
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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({
|
package/lib/subdomains.mjs
CHANGED
|
@@ -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>]
|
|
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
|
-
|
|
155
|
-
|
|
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
|
|
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