run402 1.67.0 → 1.68.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.
- package/README.md +1 -1
- package/cli.mjs +3 -3
- package/lib/deploy-v2.mjs +5 -11
- package/lib/deploy.mjs +33 -364
- package/lib/domains.mjs +11 -8
- package/lib/init.mjs +2 -2
- package/lib/manifest.mjs +0 -29
- package/lib/manifest.test.mjs +1 -39
- package/lib/sites.mjs +2 -3
- package/lib/subdomains.mjs +21 -16
- package/package.json +1 -1
- package/sdk/dist/namespaces/apps.d.ts +2 -75
- package/sdk/dist/namespaces/apps.d.ts.map +1 -1
- package/sdk/dist/namespaces/apps.js +2 -243
- package/sdk/dist/namespaces/apps.js.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts +0 -16
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +12 -5
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +1 -2
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.types.js.map +1 -1
- package/sdk/dist/namespaces/projects.types.d.ts +0 -7
- package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/sites.d.ts +0 -11
- package/sdk/dist/namespaces/sites.d.ts.map +1 -1
- package/sdk/dist/namespaces/sites.js +0 -4
- package/sdk/dist/namespaces/sites.js.map +1 -1
- package/sdk/dist/node/index.d.ts +1 -1
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/node/sites-node.d.ts +5 -34
- package/sdk/dist/node/sites-node.d.ts.map +1 -1
- package/sdk/dist/node/sites-node.js +6 -55
- package/sdk/dist/node/sites-node.js.map +1 -1
- package/sdk/dist/scoped.d.ts +1 -2
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +2 -4
- package/sdk/dist/scoped.js.map +1 -1
- package/core-dist/client.js +0 -42
- package/core-dist/wallet-auth.js +0 -62
- package/core-dist/wallet.js +0 -25
- package/sdk/core-dist/client.js +0 -42
- package/sdk/core-dist/wallet-auth.js +0 -62
- package/sdk/core-dist/wallet.js +0 -25
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ run402 projects schema <id> # introspect tables + R
|
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
59
|
run402 sites deploy-dir ./dist # incremental upload (plan/commit transport)
|
|
60
|
-
run402 deploy --manifest app.json
|
|
60
|
+
run402 deploy apply --manifest app.json # one-call full stack deploy
|
|
61
61
|
run402 deploy release active # inspect current-live release inventory
|
|
62
62
|
run402 deploy release diff --from empty --to active
|
|
63
63
|
run402 deploy diagnose --project prj_123 https://example.com/events --method GET
|
package/cli.mjs
CHANGED
|
@@ -25,7 +25,7 @@ Commands:
|
|
|
25
25
|
allowance Manage your agent allowance (create, fund, balance, status)
|
|
26
26
|
tier Manage tier subscription (status, set)
|
|
27
27
|
projects Manage projects (provision, list, query, inspect, delete)
|
|
28
|
-
deploy
|
|
28
|
+
deploy Unified deploy operations (requires active tier)
|
|
29
29
|
ci Link GitHub Actions OIDC deploy bindings
|
|
30
30
|
functions Manage serverless functions (deploy, invoke, logs, list, delete)
|
|
31
31
|
secrets Manage project secrets (set, list, delete)
|
|
@@ -51,7 +51,7 @@ Run 'run402 <command> --help' for detailed usage of each command.
|
|
|
51
51
|
Examples:
|
|
52
52
|
run402 allowance create
|
|
53
53
|
run402 allowance fund
|
|
54
|
-
run402 deploy --manifest app.json
|
|
54
|
+
run402 deploy apply --manifest app.json
|
|
55
55
|
run402 projects list
|
|
56
56
|
run402 projects sql <project_id> "SELECT * FROM users LIMIT 5"
|
|
57
57
|
run402 functions deploy <project_id> my-fn --file handler.ts
|
|
@@ -62,7 +62,7 @@ Getting started:
|
|
|
62
62
|
run402 init Set up with x402 (Base Sepolia)
|
|
63
63
|
run402 init mpp Set up with MPP (Tempo Moderato)
|
|
64
64
|
run402 tier set prototype Subscribe to a tier
|
|
65
|
-
run402 deploy --manifest app.json
|
|
65
|
+
run402 deploy apply --manifest app.json
|
|
66
66
|
run402 ci link github --project prj_... --manifest run402.deploy.json
|
|
67
67
|
`;
|
|
68
68
|
|
package/lib/deploy-v2.mjs
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
* `run402 deploy apply` and `run402 deploy resume` — CLI wrappers over the
|
|
3
3
|
* unified deploy primitive (`r.deploy.apply` / `r.deploy.resume`).
|
|
4
4
|
*
|
|
5
|
-
* The legacy `run402 deploy --manifest …` command is preserved in
|
|
6
|
-
* `cli/lib/deploy.mjs` and continues to work; this file adds the new
|
|
7
|
-
* subcommand surface.
|
|
8
|
-
*
|
|
9
5
|
* Manifest format mirrors the MCP `deploy` tool's input schema:
|
|
10
6
|
* {
|
|
11
7
|
* "project_id": "...",
|
|
@@ -19,9 +15,8 @@
|
|
|
19
15
|
* "idempotency_key": "..."
|
|
20
16
|
* }
|
|
21
17
|
*
|
|
22
|
-
* File entries: `{ "data": "...", "encoding": "utf-8" | "base64", "contentType": "..." }
|
|
23
|
-
*
|
|
24
|
-
* pass `"encoding": "base64"`.
|
|
18
|
+
* File entries: `{ "data": "...", "encoding": "utf-8" | "base64", "contentType": "..." }`.
|
|
19
|
+
* UTF-8 is the default; binary files pass `"encoding": "base64"`.
|
|
25
20
|
*/
|
|
26
21
|
|
|
27
22
|
import { readFileSync } from "node:fs";
|
|
@@ -315,8 +310,7 @@ async function applyCmd(args) {
|
|
|
315
310
|
// GH-232: Reject empty specs client-side. Without this guard,
|
|
316
311
|
// `run402 deploy apply --spec '{}'` (and `--manifest <empty>`) would silently
|
|
317
312
|
// send an empty ReleaseSpec to /deploy/v2/plans with no signal that nothing
|
|
318
|
-
// was deployed.
|
|
319
|
-
// legacy `run402 deploy --manifest` path.
|
|
313
|
+
// was deployed.
|
|
320
314
|
//
|
|
321
315
|
// `deploy apply` is v2-only — only meaningful keys are the v2 ReleaseSpec
|
|
322
316
|
// shape (database, site, functions, secrets, subdomains, domains).
|
|
@@ -797,9 +791,9 @@ async function releaseGetCmd(args) {
|
|
|
797
791
|
const project = resolveProjectId(opts.project);
|
|
798
792
|
|
|
799
793
|
try {
|
|
800
|
-
const sdkOpts = { project };
|
|
794
|
+
const sdkOpts = { project, releaseId: opts.releaseId };
|
|
801
795
|
if (opts.siteLimit !== null) sdkOpts.siteLimit = opts.siteLimit;
|
|
802
|
-
const release = await getSdk().deploy.getRelease(
|
|
796
|
+
const release = await getSdk().deploy.getRelease(sdkOpts);
|
|
803
797
|
console.log(JSON.stringify({ status: "ok", release }, null, 2));
|
|
804
798
|
} catch (err) {
|
|
805
799
|
reportSdkError(err);
|
package/lib/deploy.mjs
CHANGED
|
@@ -1,317 +1,52 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, resolve } from "path";
|
|
3
|
-
import { resolveProjectId } from "./config.mjs";
|
|
4
|
-
import { resolveFilePathsInManifest, resolveMigrationsFile } from "./manifest.mjs";
|
|
5
|
-
import { getSdk } from "./sdk.mjs";
|
|
6
|
-
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
7
|
-
import { normalizeDeployManifest } from "#sdk/node";
|
|
1
|
+
import { fail } from "./sdk-errors.mjs";
|
|
8
2
|
|
|
9
|
-
const HELP = `run402 deploy —
|
|
3
|
+
const HELP = `run402 deploy — Unified deploy operations
|
|
10
4
|
|
|
11
5
|
Usage:
|
|
12
|
-
run402 deploy [options]
|
|
13
|
-
cat manifest.json | run402 deploy [options]
|
|
6
|
+
run402 deploy <subcommand> [options]
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
--manifest <file>
|
|
17
|
-
|
|
18
|
-
--
|
|
8
|
+
Subcommands:
|
|
9
|
+
apply --manifest <file> Apply a v2 ReleaseSpec manifest
|
|
10
|
+
resume <operation_id> Resume a stuck operation
|
|
11
|
+
list [--project <id>] List recent deploy operations
|
|
12
|
+
events <operation_id> Fetch event stream for an operation
|
|
13
|
+
diagnose <url> Diagnose public URL routing
|
|
14
|
+
resolve --url <url> Low-level resolve diagnostics
|
|
15
|
+
release ... Inspect release inventory and diffs
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
run402 deploy apply --manifest
|
|
22
|
-
run402 deploy resume
|
|
23
|
-
run402 deploy
|
|
24
|
-
run402 deploy events <operation_id> fetch event stream for an operation
|
|
25
|
-
run402 deploy diagnose <url> diagnose public URL routing
|
|
26
|
-
run402 deploy resolve --url <url> low-level resolve diagnostics
|
|
27
|
-
run402 deploy release ... inspect release inventory and diffs
|
|
17
|
+
Examples:
|
|
18
|
+
run402 deploy apply --manifest app.json
|
|
19
|
+
run402 deploy resume op_123
|
|
20
|
+
run402 deploy release active --project prj_123
|
|
28
21
|
|
|
29
|
-
Manifest
|
|
22
|
+
Manifest sketch:
|
|
30
23
|
{
|
|
31
|
-
"project_id": "prj_...",
|
|
32
24
|
"database": {
|
|
33
|
-
"migrations": [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"version": "1",
|
|
38
|
-
"tables": [
|
|
39
|
-
{ "name": "items", "expose": true, "policy": "public_read_authenticated_write" }
|
|
40
|
-
]
|
|
41
|
-
}
|
|
25
|
+
"migrations": [{ "id": "001_init", "sql_path": "schema.sql" }]
|
|
26
|
+
},
|
|
27
|
+
"site": {
|
|
28
|
+
"replace": { "index.html": { "path": "dist/index.html" } }
|
|
42
29
|
},
|
|
43
|
-
"secrets": { "require": ["OPENAI_API_KEY"], "delete": ["OLD_KEY"] },
|
|
44
30
|
"functions": {
|
|
45
31
|
"replace": {
|
|
46
32
|
"api": {
|
|
47
33
|
"runtime": "node22",
|
|
48
|
-
"source": { "
|
|
34
|
+
"source": { "path": "api.mjs" }
|
|
49
35
|
}
|
|
50
36
|
}
|
|
51
37
|
},
|
|
52
|
-
"
|
|
53
|
-
"replace": {
|
|
54
|
-
"index.html": { "data": "<!doctype html><html>...</html>" },
|
|
55
|
-
"assets/logo.png": { "data": "iVBORw0KGgo...", "encoding": "base64" }
|
|
56
|
-
}
|
|
57
|
-
},
|
|
38
|
+
"secrets": { "require": ["OPENAI_API_KEY"] },
|
|
58
39
|
"subdomains": { "set": ["my-app"] }
|
|
59
40
|
}
|
|
60
|
-
|
|
61
|
-
project_id is required (provision first with 'run402 provision').
|
|
62
|
-
All other fields are optional. Top-level absence = "leave untouched".
|
|
63
|
-
|
|
64
|
-
Source of truth for the v2 ReleaseSpec shape:
|
|
65
|
-
https://run402.com/llms-cli.txt (search for "Unified Deploy")
|
|
66
|
-
|
|
67
|
-
Replace vs patch semantics per resource:
|
|
68
|
-
"site": { "replace": {...} } whole-site (omitted files removed)
|
|
69
|
-
"site": { "patch": { "put": {...}, "delete": [...] } } surgical updates
|
|
70
|
-
Same for "functions". Secrets are value-free declarations:
|
|
71
|
-
"secrets": { "require": ["OPENAI_API_KEY"], "delete": ["OLD_KEY"] }
|
|
72
|
-
Secret values must be set outside deploy manifests with:
|
|
73
|
-
run402 secrets set prj_... OPENAI_API_KEY --file ./.secrets/openai-key
|
|
74
|
-
Migrations are always additive (each is keyed by id; re-shipping the same
|
|
75
|
-
id+sql is a registry noop, same id with different sql is a hard
|
|
76
|
-
MIGRATION_CHECKSUM_MISMATCH error).
|
|
77
|
-
|
|
78
|
-
File entries accept inline "data", a local "path", or a "sql_path"
|
|
79
|
-
(migrations only) — paths are resolved relative to the manifest file's
|
|
80
|
-
directory. Binary files (images, fonts, PDFs) take "encoding": "base64";
|
|
81
|
-
text defaults to UTF-8.
|
|
82
|
-
|
|
83
|
-
Authorization (database.expose):
|
|
84
|
-
Tables are dark by default — anon/authenticated can't read them until
|
|
85
|
-
you declare them via "database.expose". Per-table policies:
|
|
86
|
-
user_owns_rows users see only their own rows.
|
|
87
|
-
Requires "owner_column"; with
|
|
88
|
-
"force_owner_on_insert": true the
|
|
89
|
-
gateway sets it from auth.uid()
|
|
90
|
-
automatically.
|
|
91
|
-
public_read_authenticated_write anyone reads; any authenticated
|
|
92
|
-
user can INSERT/UPDATE/DELETE any
|
|
93
|
-
row (not just their own).
|
|
94
|
-
public_read_write_UNRESTRICTED ⚠ fully open — anon_key reads AND
|
|
95
|
-
writes. REQUIRES
|
|
96
|
-
"i_understand_this_is_unrestricted":
|
|
97
|
-
true on the table entry.
|
|
98
|
-
custom escape hatch. Provide "custom_sql"
|
|
99
|
-
with CREATE POLICY statements.
|
|
100
|
-
Schema for the expose section: https://run402.com/schemas/manifest.v1.json
|
|
101
|
-
|
|
102
|
-
⚠️ Without an "expose" entry, tables are unreachable via anon_key.
|
|
103
|
-
|
|
104
|
-
Legacy v1 bundle format (still accepted via compatibility shim):
|
|
105
|
-
Existing manifests with top-level "migrations" (string), "functions" (array),
|
|
106
|
-
"files" (array), "subdomain" (string), and the
|
|
107
|
-
"files[].file/data/path" + inline "manifest.json" entry continue to work —
|
|
108
|
-
the SDK translates them into a v2 ReleaseSpec under the hood. Prefer the
|
|
109
|
-
v2 shape above for new manifests; the legacy form is preserved for the
|
|
110
|
-
deprecation window so existing scripts don't break. Legacy file manifests
|
|
111
|
-
with secret values no longer deploy: run 'run402 secrets set' first, then
|
|
112
|
-
use 'run402 deploy apply' with 'secrets.require'.
|
|
113
|
-
|
|
114
|
-
"migrations_file": "setup.sql" (legacy convenience) reads SQL from disk
|
|
115
|
-
relative to the manifest file. Useful when JSONB literals make inline
|
|
116
|
-
strings painful. Still supported on the legacy code path.
|
|
117
|
-
|
|
118
|
-
Examples:
|
|
119
|
-
run402 deploy --manifest app.json
|
|
120
|
-
run402 deploy --manifest app.json --project prj_123_1
|
|
121
|
-
cat app.json | run402 deploy
|
|
122
|
-
run402 deploy apply --manifest app.json # unified primitive (recommended)
|
|
123
|
-
|
|
124
|
-
Prerequisites:
|
|
125
|
-
- run402 init Set up allowance and funding
|
|
126
|
-
- run402 tier set prototype Subscribe to a tier
|
|
127
|
-
- run402 provision Provision a project first
|
|
128
|
-
|
|
129
|
-
Notes:
|
|
130
|
-
- Routes through the unified deploy primitive (POST /deploy/v2/plans);
|
|
131
|
-
bytes ride through the CAS substrate, only changed files get uploaded.
|
|
132
|
-
- Requires an active tier subscription (run402 tier set <tier>)
|
|
133
|
-
- Provision a project first with 'run402 provision', then deploy to it
|
|
134
|
-
- Use 'run402 projects list' to see all provisioned projects
|
|
135
41
|
`;
|
|
136
42
|
|
|
137
|
-
async function readStdin() {
|
|
138
|
-
const chunks = [];
|
|
139
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
140
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Load + parse the manifest from --manifest file or stdin, and resolve any
|
|
145
|
-
* referenced files[].path / migrations_file against the manifest's directory.
|
|
146
|
-
*
|
|
147
|
-
* Returns the parsed manifest on success. On any fs / parse failure, calls
|
|
148
|
-
* `fail()` (which writes the canonical error envelope to stderr and exits 1).
|
|
149
|
-
*/
|
|
150
|
-
async function loadManifest(opts) {
|
|
151
|
-
let raw;
|
|
152
|
-
let baseDir = null;
|
|
153
|
-
|
|
154
|
-
if (opts.manifest) {
|
|
155
|
-
const manifestAbs = resolve(opts.manifest);
|
|
156
|
-
baseDir = dirname(manifestAbs);
|
|
157
|
-
try {
|
|
158
|
-
raw = readFileSync(opts.manifest, "utf-8");
|
|
159
|
-
} catch (err) {
|
|
160
|
-
if (err && err.code === "ENOENT") {
|
|
161
|
-
fail({
|
|
162
|
-
code: "BAD_USAGE",
|
|
163
|
-
message: `File not found: ${manifestAbs}`,
|
|
164
|
-
hint: "Check that --manifest points to an existing JSON file.",
|
|
165
|
-
details: { field: "manifest", path: manifestAbs },
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
fail({
|
|
169
|
-
code: "BAD_USAGE",
|
|
170
|
-
message: err && err.message ? err.message : String(err),
|
|
171
|
-
details: { field: "manifest", path: manifestAbs, ...(err && err.code ? { syscall_code: err.code } : {}) },
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
} else {
|
|
175
|
-
raw = await readStdin();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
let manifest;
|
|
179
|
-
try {
|
|
180
|
-
manifest = JSON.parse(raw);
|
|
181
|
-
} catch (err) {
|
|
182
|
-
fail({
|
|
183
|
-
code: "BAD_USAGE",
|
|
184
|
-
message: `Manifest is not valid JSON: ${err.message}`,
|
|
185
|
-
details: {
|
|
186
|
-
field: opts.manifest ? "manifest" : "stdin",
|
|
187
|
-
...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
|
|
188
|
-
parse_error: err.message,
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// GH-185: Reject empty manifests client-side. Without this guard,
|
|
194
|
-
// `echo '{}' | run402 deploy` silently succeeds against the gateway with
|
|
195
|
-
// no signal that nothing was deployed. The MCP `deploy` tool was hardened
|
|
196
|
-
// for the same class of bug in #133; this is the CLI-side analog.
|
|
197
|
-
//
|
|
198
|
-
// "Meaningful" = at least one of these keys exists with non-empty content.
|
|
199
|
-
// We accept both shapes because this CLI path receives v1 manifests
|
|
200
|
-
// (translated by the bundleDeploy shim) and may also receive v2 manifests.
|
|
201
|
-
// v1: migrations, migrations_file, secrets, functions, files, subdomain
|
|
202
|
-
// v2: database, site, functions, secrets, subdomains, domains
|
|
203
|
-
// For object-typed v2 sections (site, database, functions, secrets,
|
|
204
|
-
// subdomains, domains) the "container is non-empty" check isn't enough —
|
|
205
|
-
// `site:{replace:{}}` has one key but ships nothing. We recurse one level
|
|
206
|
-
// so any object whose own values are all empty containers is still empty.
|
|
207
|
-
const meaningfulV1 = ["migrations", "migrations_file", "secrets", "functions", "files", "subdomain"];
|
|
208
|
-
const meaningfulV2 = ["database", "site", "functions", "secrets", "subdomains", "domains"];
|
|
209
|
-
const meaningful = [...new Set([...meaningfulV1, ...meaningfulV2])];
|
|
210
|
-
|
|
211
|
-
function hasContent(v) {
|
|
212
|
-
if (v == null) return false;
|
|
213
|
-
if (Array.isArray(v)) return v.length > 0;
|
|
214
|
-
if (typeof v === "object") {
|
|
215
|
-
const keys = Object.keys(v);
|
|
216
|
-
if (keys.length === 0) return false;
|
|
217
|
-
return keys.some((k) => hasContent(v[k]));
|
|
218
|
-
}
|
|
219
|
-
if (typeof v === "string") return v.length > 0;
|
|
220
|
-
return true;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const hasMeaningfulContent = manifest && typeof manifest === "object" && !Array.isArray(manifest) && meaningful.some((key) => hasContent(manifest[key]));
|
|
224
|
-
if (!hasMeaningfulContent) {
|
|
225
|
-
fail({
|
|
226
|
-
code: "MANIFEST_EMPTY",
|
|
227
|
-
message: `Manifest contains no deployable sections. Expected at least one of: ${meaningful.join(", ")}`,
|
|
228
|
-
hint: "Did you mean to write a 'site.replace' or 'database.migrations' block? See https://run402.com/schemas/manifest.v1.json",
|
|
229
|
-
details: {
|
|
230
|
-
field: opts.manifest ? "manifest" : "stdin",
|
|
231
|
-
...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
|
|
232
|
-
meaningful_keys: meaningful,
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
rejectUnsafeSecretManifest(manifest, {
|
|
238
|
-
source: opts.manifest ? "manifest" : "stdin",
|
|
239
|
-
...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
if (opts.manifest) {
|
|
243
|
-
try {
|
|
244
|
-
resolveMigrationsFile(manifest, baseDir);
|
|
245
|
-
resolveFilePathsInManifest(manifest, baseDir);
|
|
246
|
-
} catch (err) {
|
|
247
|
-
if (err && err.code === "ENOENT") {
|
|
248
|
-
fail({
|
|
249
|
-
code: "BAD_USAGE",
|
|
250
|
-
message: `File not found: ${err.absPath || err.path || "<unknown>"}`,
|
|
251
|
-
hint: `Paths in manifest.${err.field || "files[].path"} are resolved relative to the manifest file's directory (${baseDir}).`,
|
|
252
|
-
details: {
|
|
253
|
-
field: err.field || "manifest",
|
|
254
|
-
...(err.absPath || err.path ? { path: err.absPath || err.path } : {}),
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
fail({
|
|
259
|
-
code: "BAD_USAGE",
|
|
260
|
-
message: err && err.message ? err.message : String(err),
|
|
261
|
-
details: {
|
|
262
|
-
...(err && err.field ? { field: err.field } : {}),
|
|
263
|
-
...(err && (err.absPath || err.path) ? { path: err.absPath || err.path } : {}),
|
|
264
|
-
...(err && err.code ? { syscall_code: err.code } : {}),
|
|
265
|
-
},
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return manifest;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function rejectUnsafeSecretManifest(manifest, details) {
|
|
274
|
-
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return;
|
|
275
|
-
const secrets = manifest.secrets;
|
|
276
|
-
if (secrets === undefined) return;
|
|
277
|
-
if (Array.isArray(secrets) && secrets.length > 0) {
|
|
278
|
-
fail({
|
|
279
|
-
code: "UNSAFE_SECRET_MANIFEST",
|
|
280
|
-
message: "Deploy manifests must not contain secret values. Legacy top-level secrets arrays are no longer supported.",
|
|
281
|
-
hint: "Run `run402 secrets set <project> <KEY> --file <path>` first, then use `run402 deploy apply` with `\"secrets\": { \"require\": [\"KEY\"] }`.",
|
|
282
|
-
details: { ...details, field: "secrets", legacy_shape: "array" },
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
if (!secrets || typeof secrets !== "object" || Array.isArray(secrets)) return;
|
|
286
|
-
if (Object.prototype.hasOwnProperty.call(secrets, "set")) {
|
|
287
|
-
fail({
|
|
288
|
-
code: "UNSAFE_SECRET_MANIFEST",
|
|
289
|
-
message: "Deploy manifests must not use secrets.set. Secret values are write-only and must be set outside deploy specs.",
|
|
290
|
-
hint: "Run `run402 secrets set <project> <KEY> --file <path>` first, then deploy with `\"secrets\": { \"require\": [\"KEY\"] }`.",
|
|
291
|
-
details: { ...details, field: "secrets.set" },
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
if (Object.prototype.hasOwnProperty.call(secrets, "replace_all")) {
|
|
295
|
-
fail({
|
|
296
|
-
code: "UNSAFE_SECRET_MANIFEST",
|
|
297
|
-
message: "Deploy manifests must not use secrets.replace_all. Exact replacement is not representable in the value-free deploy contract.",
|
|
298
|
-
hint: "Use `secrets.require` for keys that must exist and `secrets.delete` for explicit removals.",
|
|
299
|
-
details: { ...details, field: "secrets.replace_all" },
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
43
|
export async function run(args) {
|
|
305
|
-
// Subcommand dispatch (v1.34+):
|
|
306
|
-
// run402 deploy apply ... → unified deploy primitive (deploy.apply)
|
|
307
|
-
// run402 deploy resume <op> → resume an activation_pending operation
|
|
308
|
-
// run402 deploy list → list recent deploy operations
|
|
309
|
-
// run402 deploy events <op> → fetch recorded event stream for an operation
|
|
310
|
-
// run402 deploy diagnose ... → URL-first public diagnostics
|
|
311
|
-
// run402 deploy resolve ... → lower-level resolve endpoint parity
|
|
312
|
-
// run402 deploy release ... → release inventory/diff observability
|
|
313
|
-
// run402 deploy --manifest … → legacy bundle deploy (routes through v2)
|
|
314
44
|
const sub = args[0];
|
|
45
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
46
|
+
console.log(HELP);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
315
50
|
switch (sub) {
|
|
316
51
|
case "apply":
|
|
317
52
|
case "resume":
|
|
@@ -324,78 +59,12 @@ export async function run(args) {
|
|
|
324
59
|
await runDeployV2(sub, args.slice(1));
|
|
325
60
|
return;
|
|
326
61
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const manifest = await loadManifest(opts);
|
|
337
|
-
|
|
338
|
-
// If both sources set project_id and they disagree, refuse to deploy rather
|
|
339
|
-
// than silently shipping to the wrong target.
|
|
340
|
-
if (opts.project && manifest.project_id && opts.project !== manifest.project_id) {
|
|
341
|
-
fail({
|
|
342
|
-
code: "BAD_USAGE",
|
|
343
|
-
message: `project_id conflict: manifest.project_id=${manifest.project_id} but --project=${opts.project}`,
|
|
344
|
-
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).",
|
|
345
|
-
details: {
|
|
346
|
-
manifest_project_id: manifest.project_id,
|
|
347
|
-
flag_project_id: opts.project,
|
|
348
|
-
},
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (opts.project) manifest.project_id = opts.project;
|
|
353
|
-
if (!manifest.project_id) {
|
|
354
|
-
manifest.project_id = resolveProjectId(null);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const projectId = manifest.project_id;
|
|
358
|
-
delete manifest.name;
|
|
359
|
-
|
|
360
|
-
if (isV2Manifest(manifest)) {
|
|
361
|
-
try {
|
|
362
|
-
const normalized = await normalizeDeployManifest(manifest, {
|
|
363
|
-
baseDir: opts.manifest ? dirname(resolve(opts.manifest)) : process.cwd(),
|
|
364
|
-
});
|
|
365
|
-
const result = await getSdk().deploy.apply(normalized.spec, {
|
|
366
|
-
idempotencyKey: normalized.idempotencyKey,
|
|
62
|
+
default:
|
|
63
|
+
fail({
|
|
64
|
+
code: "BAD_USAGE",
|
|
65
|
+
message: `Unknown deploy subcommand: ${sub}`,
|
|
66
|
+
hint: "Use `run402 deploy apply --manifest <file>` for deployments.",
|
|
67
|
+
details: { subcommand: sub },
|
|
367
68
|
});
|
|
368
|
-
console.log(JSON.stringify({ project_id: projectId, ...result }, null, 2));
|
|
369
|
-
} catch (err) {
|
|
370
|
-
reportSdkError(err);
|
|
371
|
-
}
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Strip fields that aren't part of the bundleDeploy contract.
|
|
376
|
-
delete manifest.project_id;
|
|
377
|
-
delete manifest.migrations_file;
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
const result = await getSdk().apps.bundleDeploy(projectId, manifest);
|
|
381
|
-
console.log(JSON.stringify(result, null, 2));
|
|
382
|
-
} catch (err) {
|
|
383
|
-
reportSdkError(err);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function isV2Manifest(manifest) {
|
|
388
|
-
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return false;
|
|
389
|
-
if (manifest.database !== undefined) return true;
|
|
390
|
-
if (manifest.site !== undefined) return true;
|
|
391
|
-
if (manifest.subdomains !== undefined) return true;
|
|
392
|
-
if (manifest.routes !== undefined) return true;
|
|
393
|
-
if (manifest.checks !== undefined) return true;
|
|
394
|
-
if (manifest.secrets && typeof manifest.secrets === "object" && !Array.isArray(manifest.secrets)) {
|
|
395
|
-
return true;
|
|
396
|
-
}
|
|
397
|
-
if (manifest.functions && typeof manifest.functions === "object" && !Array.isArray(manifest.functions)) {
|
|
398
|
-
return true;
|
|
399
69
|
}
|
|
400
|
-
return false;
|
|
401
70
|
}
|
package/lib/domains.mjs
CHANGED
|
@@ -9,7 +9,7 @@ Usage:
|
|
|
9
9
|
|
|
10
10
|
Subcommands:
|
|
11
11
|
add <domain> <subdomain_name> [--project <id>] Register a custom domain
|
|
12
|
-
list [
|
|
12
|
+
list [--project <id>] List custom domains for a project
|
|
13
13
|
status <domain> [--project <id>] Check domain DNS/SSL status
|
|
14
14
|
delete <domain> --confirm [--project <id>] Release a custom domain. Requires --confirm.
|
|
15
15
|
|
|
@@ -51,14 +51,14 @@ Examples:
|
|
|
51
51
|
list: `run402 domains list — List custom domains for a project
|
|
52
52
|
|
|
53
53
|
Usage:
|
|
54
|
-
run402 domains list [<id>]
|
|
54
|
+
run402 domains list [--project <id>]
|
|
55
55
|
|
|
56
56
|
Arguments:
|
|
57
57
|
<id> Project ID (defaults to the active project)
|
|
58
58
|
|
|
59
59
|
Examples:
|
|
60
60
|
run402 domains list
|
|
61
|
-
run402 domains list prj_abc123
|
|
61
|
+
run402 domains list --project prj_abc123
|
|
62
62
|
`,
|
|
63
63
|
status: `run402 domains status — Check DNS/SSL status of a custom domain
|
|
64
64
|
|
|
@@ -127,11 +127,14 @@ async function add(args) {
|
|
|
127
127
|
async function list(args) {
|
|
128
128
|
const argList = Array.isArray(args) ? args : [];
|
|
129
129
|
const { project, rest } = parseProjectFlag(argList);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
if (rest.length > 0) {
|
|
131
|
+
fail({
|
|
132
|
+
code: "BAD_USAGE",
|
|
133
|
+
message: `Unexpected argument for domains list: ${rest[0]}`,
|
|
134
|
+
hint: "Use `run402 domains list --project <id>`.",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const projectId = resolveProjectId(project);
|
|
135
138
|
try {
|
|
136
139
|
const data = await getSdk().domains.list(projectId);
|
|
137
140
|
console.log(JSON.stringify(data, null, 2));
|
package/lib/init.mjs
CHANGED
|
@@ -243,11 +243,11 @@ export async function run(args = []) {
|
|
|
243
243
|
write("");
|
|
244
244
|
const nextStep = (!tierInfo || !tierInfo.tier || !tierInfo.active)
|
|
245
245
|
? "run402 tier set prototype"
|
|
246
|
-
: "run402 deploy --manifest app.json";
|
|
246
|
+
: "run402 deploy apply --manifest app.json";
|
|
247
247
|
if (!tierInfo || !tierInfo.tier || !tierInfo.active) {
|
|
248
248
|
write(" Next: run402 tier set prototype");
|
|
249
249
|
} else {
|
|
250
|
-
write(" Ready to deploy. Run: run402 deploy --manifest app.json");
|
|
250
|
+
write(" Ready to deploy. Run: run402 deploy apply --manifest app.json");
|
|
251
251
|
}
|
|
252
252
|
write("");
|
|
253
253
|
summary.next_step = nextStep;
|
package/lib/manifest.mjs
CHANGED
|
@@ -6,35 +6,6 @@ const TEXT_EXTS = new Set([
|
|
|
6
6
|
".json", ".svg", ".xml", ".txt", ".md", ".yaml", ".yml", ".toml", ".csv",
|
|
7
7
|
]);
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* If the manifest has `migrations_file` instead of (or in addition to) `migrations`,
|
|
11
|
-
* read the SQL from that file path and set `migrations` to its contents.
|
|
12
|
-
* `migrations_file` is resolved relative to `baseDir`.
|
|
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
|
-
*
|
|
20
|
-
* @param {object} manifest Parsed manifest JSON (mutated in place)
|
|
21
|
-
* @param {string} baseDir Directory to resolve relative paths from
|
|
22
|
-
* @returns {object} The same manifest object
|
|
23
|
-
*/
|
|
24
|
-
export function resolveMigrationsFile(manifest, baseDir) {
|
|
25
|
-
if (!manifest.migrations_file) return manifest;
|
|
26
|
-
const abs = resolve(baseDir, manifest.migrations_file);
|
|
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
|
-
}
|
|
34
|
-
delete manifest.migrations_file;
|
|
35
|
-
return manifest;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
9
|
/**
|
|
39
10
|
* Resolve `path` fields in a manifest's files array.
|
|
40
11
|
*
|
package/lib/manifest.test.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { resolveFilePathsInManifest
|
|
6
|
+
import { resolveFilePathsInManifest } from "./manifest.mjs";
|
|
7
7
|
|
|
8
8
|
let tempDir;
|
|
9
9
|
|
|
@@ -13,7 +13,6 @@ before(() => {
|
|
|
13
13
|
writeFileSync(join(tempDir, "index.html"), "<!DOCTYPE html><html><body>Hello</body></html>");
|
|
14
14
|
writeFileSync(join(tempDir, "style.css"), "body { margin: 0; }");
|
|
15
15
|
writeFileSync(join(tempDir, "logo.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])); // PNG header
|
|
16
|
-
writeFileSync(join(tempDir, "setup.sql"), "CREATE TABLE items (id serial PRIMARY KEY, data jsonb);\nINSERT INTO items (data) VALUES ('[{\"x\":0.5}]');");
|
|
17
16
|
});
|
|
18
17
|
|
|
19
18
|
after(() => {
|
|
@@ -89,40 +88,3 @@ describe("resolveFilePathsInManifest", () => {
|
|
|
89
88
|
);
|
|
90
89
|
});
|
|
91
90
|
});
|
|
92
|
-
|
|
93
|
-
describe("resolveMigrationsFile", () => {
|
|
94
|
-
it("reads SQL from migrations_file and sets migrations", () => {
|
|
95
|
-
const manifest = { migrations_file: "setup.sql" };
|
|
96
|
-
resolveMigrationsFile(manifest, tempDir);
|
|
97
|
-
assert.ok(manifest.migrations.includes("CREATE TABLE items"));
|
|
98
|
-
assert.ok(manifest.migrations.includes('[{"x":0.5}]'), "should preserve JSON literals without escaping issues");
|
|
99
|
-
assert.equal(manifest.migrations_file, undefined, "migrations_file should be removed");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("overwrites inline migrations when migrations_file is present", () => {
|
|
103
|
-
const manifest = { migrations: "SELECT 1", migrations_file: "setup.sql" };
|
|
104
|
-
resolveMigrationsFile(manifest, tempDir);
|
|
105
|
-
assert.ok(manifest.migrations.includes("CREATE TABLE items"));
|
|
106
|
-
assert.equal(manifest.migrations_file, undefined);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("leaves manifest untouched when no migrations_file", () => {
|
|
110
|
-
const manifest = { migrations: "SELECT 1" };
|
|
111
|
-
resolveMigrationsFile(manifest, tempDir);
|
|
112
|
-
assert.equal(manifest.migrations, "SELECT 1");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("handles manifest with neither migrations nor migrations_file", () => {
|
|
116
|
-
const manifest = { files: [] };
|
|
117
|
-
resolveMigrationsFile(manifest, tempDir);
|
|
118
|
-
assert.equal(manifest.migrations, undefined);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("throws on missing migrations file", () => {
|
|
122
|
-
const manifest = { migrations_file: "does-not-exist.sql" };
|
|
123
|
-
assert.throws(
|
|
124
|
-
() => resolveMigrationsFile(manifest, tempDir),
|
|
125
|
-
/ENOENT/,
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
});
|