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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/cli.mjs +3 -3
  3. package/lib/deploy-v2.mjs +5 -11
  4. package/lib/deploy.mjs +33 -364
  5. package/lib/domains.mjs +11 -8
  6. package/lib/init.mjs +2 -2
  7. package/lib/manifest.mjs +0 -29
  8. package/lib/manifest.test.mjs +1 -39
  9. package/lib/sites.mjs +2 -3
  10. package/lib/subdomains.mjs +21 -16
  11. package/package.json +1 -1
  12. package/sdk/dist/namespaces/apps.d.ts +2 -75
  13. package/sdk/dist/namespaces/apps.d.ts.map +1 -1
  14. package/sdk/dist/namespaces/apps.js +2 -243
  15. package/sdk/dist/namespaces/apps.js.map +1 -1
  16. package/sdk/dist/namespaces/deploy.d.ts +0 -16
  17. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  18. package/sdk/dist/namespaces/deploy.js +12 -5
  19. package/sdk/dist/namespaces/deploy.js.map +1 -1
  20. package/sdk/dist/namespaces/deploy.types.d.ts +1 -2
  21. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  22. package/sdk/dist/namespaces/deploy.types.js.map +1 -1
  23. package/sdk/dist/namespaces/projects.types.d.ts +0 -7
  24. package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
  25. package/sdk/dist/namespaces/sites.d.ts +0 -11
  26. package/sdk/dist/namespaces/sites.d.ts.map +1 -1
  27. package/sdk/dist/namespaces/sites.js +0 -4
  28. package/sdk/dist/namespaces/sites.js.map +1 -1
  29. package/sdk/dist/node/index.d.ts +1 -1
  30. package/sdk/dist/node/index.d.ts.map +1 -1
  31. package/sdk/dist/node/index.js.map +1 -1
  32. package/sdk/dist/node/sites-node.d.ts +5 -34
  33. package/sdk/dist/node/sites-node.d.ts.map +1 -1
  34. package/sdk/dist/node/sites-node.js +6 -55
  35. package/sdk/dist/node/sites-node.js.map +1 -1
  36. package/sdk/dist/scoped.d.ts +1 -2
  37. package/sdk/dist/scoped.d.ts.map +1 -1
  38. package/sdk/dist/scoped.js +2 -4
  39. package/sdk/dist/scoped.js.map +1 -1
  40. package/core-dist/client.js +0 -42
  41. package/core-dist/wallet-auth.js +0 -62
  42. package/core-dist/wallet.js +0 -25
  43. package/sdk/core-dist/client.js +0 -42
  44. package/sdk/core-dist/wallet-auth.js +0 -62
  45. 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 # one-call full stack deploy
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 Deploy a full-stack app or static site (requires active tier)
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
- * — same shape used by `bundle_deploy`. UTF-8 is the default; binary files
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. This mirrors the GH-185 guard already in place for the
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(opts.releaseId, sdkOpts);
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 { readFileSync } from "fs";
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 — Deploy to an existing project on Run402
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
- Options:
16
- --manifest <file> Path to manifest JSON file (default: read from stdin)
17
- --project <id> Project ID to deploy to (default: active project)
18
- --help, -h Show this help message
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
- Subcommands (recommended for new manifests):
21
- run402 deploy apply --manifest <file> unified deploy primitive (v1.34+)
22
- run402 deploy resume <operation_id> resume a stuck operation
23
- run402 deploy list [--project <id>] list recent deploy operations
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 format (JSON, v2 ReleaseSpec — recommended):
22
+ Manifest sketch:
30
23
  {
31
- "project_id": "prj_...",
32
24
  "database": {
33
- "migrations": [
34
- { "id": "001_init", "sql": "CREATE TABLE IF NOT EXISTS items (...)" }
35
- ],
36
- "expose": {
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": { "data": "export default async (req) => new Response('ok')" }
34
+ "source": { "path": "api.mjs" }
49
35
  }
50
36
  }
51
37
  },
52
- "site": {
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
- const opts = { manifest: null, project: null };
330
- for (let i = 0; i < args.length; i++) {
331
- if (args[i] === "--help" || args[i] === "-h") { console.log(HELP); process.exit(0); }
332
- if (args[i] === "--manifest" && args[i + 1]) opts.manifest = args[++i];
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 [<id>] | list --project <id> List custom domains for a project
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
- // Either --project <id> or a positional id is accepted; --project wins
131
- // when both are supplied. Falls back to the active project when neither
132
- // is given. Keeps backward-compat with the legacy `domains list <id>`
133
- // form (GH-209).
134
- const projectId = resolveProjectId(project || rest[0]);
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
  *
@@ -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, resolveMigrationsFile } from "./manifest.mjs";
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
- });