run402 4.0.1 → 4.0.3

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 (53) hide show
  1. package/README.md +7 -1
  2. package/cli.mjs +21 -0
  3. package/lib/branches.mjs +113 -0
  4. package/lib/core.mjs +2 -1
  5. package/lib/deploy-v2.mjs +143 -4
  6. package/lib/deploy.mjs +3 -1
  7. package/lib/projects.mjs +11 -0
  8. package/lib/snapshots.mjs +137 -0
  9. package/lib/up.mjs +146 -0
  10. package/package.json +1 -1
  11. package/sdk/dist/actions.d.ts +7 -1
  12. package/sdk/dist/actions.d.ts.map +1 -1
  13. package/sdk/dist/app-up.d.ts +14 -3
  14. package/sdk/dist/app-up.d.ts.map +1 -1
  15. package/sdk/dist/app-up.js +1 -0
  16. package/sdk/dist/app-up.js.map +1 -1
  17. package/sdk/dist/credentials.d.ts +1 -1
  18. package/sdk/dist/credentials.d.ts.map +1 -1
  19. package/sdk/dist/index.d.ts +8 -0
  20. package/sdk/dist/index.d.ts.map +1 -1
  21. package/sdk/dist/index.js +8 -0
  22. package/sdk/dist/index.js.map +1 -1
  23. package/sdk/dist/namespaces/branches.d.ts +11 -0
  24. package/sdk/dist/namespaces/branches.d.ts.map +1 -0
  25. package/sdk/dist/namespaces/branches.js +92 -0
  26. package/sdk/dist/namespaces/branches.js.map +1 -0
  27. package/sdk/dist/namespaces/branches.types.d.ts +48 -0
  28. package/sdk/dist/namespaces/branches.types.d.ts.map +1 -0
  29. package/sdk/dist/namespaces/branches.types.js +2 -0
  30. package/sdk/dist/namespaces/branches.types.js.map +1 -0
  31. package/sdk/dist/namespaces/deploy.d.ts +2 -1
  32. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  33. package/sdk/dist/namespaces/deploy.js +34 -0
  34. package/sdk/dist/namespaces/deploy.js.map +1 -1
  35. package/sdk/dist/namespaces/deploy.types.d.ts +98 -0
  36. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  37. package/sdk/dist/namespaces/deploy.types.js +24 -2
  38. package/sdk/dist/namespaces/deploy.types.js.map +1 -1
  39. package/sdk/dist/namespaces/snapshots.d.ts +13 -0
  40. package/sdk/dist/namespaces/snapshots.d.ts.map +1 -0
  41. package/sdk/dist/namespaces/snapshots.js +115 -0
  42. package/sdk/dist/namespaces/snapshots.js.map +1 -0
  43. package/sdk/dist/namespaces/snapshots.types.d.ts +79 -0
  44. package/sdk/dist/namespaces/snapshots.types.d.ts.map +1 -0
  45. package/sdk/dist/namespaces/snapshots.types.js +2 -0
  46. package/sdk/dist/namespaces/snapshots.types.js.map +1 -0
  47. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  48. package/sdk/dist/node/actions-node.js +460 -26
  49. package/sdk/dist/node/actions-node.js.map +1 -1
  50. package/sdk/dist/scoped.d.ts +28 -1
  51. package/sdk/dist/scoped.d.ts.map +1 -1
  52. package/sdk/dist/scoped.js +54 -0
  53. package/sdk/dist/scoped.js.map +1 -1
package/README.md CHANGED
@@ -41,10 +41,13 @@ run402 up --manifest run402.deploy.ts --check
41
41
  run402 up --manifest run402.deploy.ts --plan
42
42
  run402 up --manifest run402.deploy.ts --require-plan plan_...
43
43
  run402 up --manifest run402.deploy.json --project prj_...
44
+ run402 up verify --project prj_...
44
45
  ```
45
46
 
46
47
  `up` is a thin CLI shim over the Node SDK action runner. It discovers `run402.deploy.json` then `app.json`, validates the deploy input before any mutation, resolves the project as `--project` → `.run402/project.json` → manifest `project_id` → approved creation from `--name` → approved active-project fallback, then applies the manifest. `--name` is creation/link metadata only; it is not a manifest field and never renames an existing project. When everything is already configured, plain `run402 up` deploys. Non-interactive recursive prerequisites/local writes require `-y/--yes`.
47
48
 
49
+ For app manifests with `verify.http[]`, `up` runs HTTP checks after deploy. Fresh Run402 edge sentinel misses (`x-run402-edge` or sentinel JSON bodies) become `propagation_pending` with diagnostics instead of hard failure while the host binding is still converging. Use `--propagation-budget-s <seconds>` to tune the default 120 second wait, `--no-propagation-wait` to return immediately, and `run402 up verify` to rerun the same checks without upload, deploy, project creation, or resource mutation.
50
+
48
51
  For typed `run402.deploy.ts` configs, pass `--manifest` explicitly because TypeScript/JavaScript configs execute local code. Use `--check` for local-only import/normalize/file validation, `--print-spec` to inspect the normalized `ReleaseSpec`, `--plan` for a gateway-reviewed non-deploying plan, and `--require-plan <plan_id>` to apply only that reviewed intent. Warning flags are not used with `--require-plan`; the reviewed plan binds the exact warning/destructive set. Run402 Core skips Cloud allowance/tier prerequisites and fails closed when no Core project is selected.
49
52
 
50
53
  ### Allowance
@@ -71,6 +74,7 @@ run402 projects schema <id> # introspect tables + R
71
74
  ```bash
72
75
  run402 sites deploy-dir ./dist # incremental upload (plan/commit transport)
73
76
  run402 deploy apply --manifest app.json # one-call full stack deploy
77
+ run402 apply --manifest app.json --rehearse --json
74
78
  run402 deploy release active # inspect current-live release inventory
75
79
  run402 deploy release diff --from empty --to active
76
80
  run402 deploy diagnose --project prj_123 https://example.com/events --method GET
@@ -79,7 +83,9 @@ run402 subdomains claim my-app # → my-app.run402.com (auto-reas
79
83
  ```
80
84
 
81
85
  `deploy-dir` hashes each file client-side and only uploads bytes the gateway doesn't already have. Re-deploying an unchanged tree returns immediately with `bytes_uploaded: 0`. Progress events stream to stderr.
82
- Release inspection commands print `{ release: ... }` or `{ diff: ... }` (raw payload, no envelope — see the "Output Contract" section in [llms-cli.txt](llms-cli.txt)); use them after deploys to compare release inventory without starting another mutation. Inventories include `release_generation`, `static_manifest_sha256`, and nullable `static_manifest_metadata`; diffs include `static_assets` counters such as unchanged/changed/added/removed and CAS byte reuse. `deploy diagnose` / `deploy resolve --url` print URL-first diagnostics with `would_serve`, `diagnostic_status`, `match`, warnings, and next steps; host misses are successful diagnostic calls with `would_serve: false`. Stable-host resolve fields can include `authorization_result`, `cas_object`, `response_variant`, `allow`, `route_pattern`, `target_type`, `target_name`, and `target_file`.
86
+ Release inspection commands print `{ release: ... }` or `{ diff: ... }` (raw payload, no envelope — see the "Output Contract" section in [llms-cli.txt](llms-cli.txt)); use them after deploys to compare release inventory without starting another mutation. Inventories include `release_generation`, `static_manifest_sha256`, and nullable `static_manifest_metadata`; diffs include `static_assets` counters such as unchanged/changed/added/removed and CAS byte reuse. `deploy diagnose` / `deploy resolve --url` print URL-first diagnostics with `would_serve`, `diagnostic_status`, `match`, warnings, `edge_propagation`, and next steps; host misses are successful diagnostic calls with `would_serve: false`. Stable-host resolve fields can include `authorization_result`, `cas_object`, `response_variant`, `allow`, `route_pattern`, `target_type`, `target_name`, `target_file`, and `edge_propagation` (`settled`, `propagating`, or `sync_pending`).
87
+
88
+ For database-bearing changes, use `run402 apply --manifest app.json --rehearse --json` before commit. It creates a contained branch, applies the candidate plan there, runs checks, and exits nonzero on a failed rehearsal. Manual restore points live under `run402 snapshots create|list|get|restore|delete`; temporary data branches live under `run402 branches create|list|renew|delete`.
83
89
 
84
90
  ### GitHub Actions OIDC deploys
85
91
 
package/cli.mjs CHANGED
@@ -28,6 +28,9 @@ Commands:
28
28
  allowance Manage your agent allowance (create, fund, balance, status)
29
29
  tier Manage tier subscription (status, set)
30
30
  projects Manage projects (provision, list, query, inspect, delete)
31
+ apply Alias for deploy apply; supports --rehearse for migration rehearsal
32
+ snapshots Create/list/restore/delete project data snapshots
33
+ branches Create/list/renew/delete contained project branches
31
34
  admin Platform-admin operations (lease-perpetual, archive, reactivate)
32
35
  cloud Cloud portability archive export (archives create/download/status)
33
36
  archives Inspect and verify portable project archives locally
@@ -73,6 +76,9 @@ Examples:
73
76
  run402 allowance create
74
77
  run402 allowance fund
75
78
  run402 deploy apply --manifest app.json
79
+ run402 apply --manifest app.json --rehearse --json
80
+ run402 snapshots list prj_...
81
+ run402 branches create prj_... --ttl-days 7 --json
76
82
  run402 cloud archives create prj_... --wait --output ./project.r402ar --json
77
83
  run402 core projects import ./project.r402ar --name imported-project --env-file ./required.env --json
78
84
  run402 jobs submit --file job.json
@@ -175,6 +181,21 @@ switch (cmd) {
175
181
  await run(sub, rest);
176
182
  break;
177
183
  }
184
+ case "apply": {
185
+ const { runDeployV2 } = await import("./lib/deploy-v2.mjs");
186
+ await runDeployV2("apply", [sub, ...rest].filter(Boolean));
187
+ break;
188
+ }
189
+ case "snapshots": {
190
+ const { run } = await import("./lib/snapshots.mjs");
191
+ await run(sub, rest);
192
+ break;
193
+ }
194
+ case "branches": {
195
+ const { run } = await import("./lib/branches.mjs");
196
+ await run(sub, rest);
197
+ break;
198
+ }
178
199
  case "admin": {
179
200
  const { run } = await import("./lib/admin.mjs");
180
201
  await run(sub, rest);
@@ -0,0 +1,113 @@
1
+ import { getSdk } from "./sdk.mjs";
2
+ import { fail, reportSdkError } from "./sdk-errors.mjs";
3
+ import {
4
+ assertAllowedValue,
5
+ assertKnownFlags,
6
+ flagValue,
7
+ hasHelp,
8
+ normalizeArgv,
9
+ parseIntegerFlag,
10
+ positionalArgs,
11
+ } from "./argparse.mjs";
12
+ import { resolveProjectId } from "./config.mjs";
13
+
14
+ const HELP = `run402 branches — Contained project data branches
15
+
16
+ Usage:
17
+ run402 branches create [project-id] [--from-snapshot <snapshot-id>] [--name <label>] [--email-mode sandbox|off] [--enable-cron] [--ttl-days <n>] [--json]
18
+ run402 branches list [project-id] [--json]
19
+ run402 branches renew [project-id] <branch-project-id> [--ttl-days <n>] [--json]
20
+ run402 branches delete [project-id] <branch-project-id> [--json]
21
+ `;
22
+
23
+ const FLAG_VALUES = ["--from-snapshot", "--name", "--email-mode", "--ttl-days"];
24
+ const FLAGS = new Set([...FLAG_VALUES, "--enable-cron", "--json", "--help", "-h"]);
25
+
26
+ export async function run(sub, args = []) {
27
+ const all = [sub, ...args].filter(Boolean);
28
+ if (!sub || hasHelp(all)) {
29
+ console.log(HELP);
30
+ return;
31
+ }
32
+ const rest = normalizeArgv(args);
33
+ assertKnownFlags(rest, [...FLAGS], FLAG_VALUES);
34
+ switch (sub) {
35
+ case "create": return create(rest);
36
+ case "list": return list(rest);
37
+ case "renew": return renew(rest);
38
+ case "delete": return deleteBranch(rest);
39
+ default:
40
+ fail({
41
+ code: "UNKNOWN_SUBCOMMAND",
42
+ message: `Unknown branches subcommand: ${sub}`,
43
+ hint: "Run `run402 branches --help` for usage.",
44
+ details: { command: "branches", subcommand: sub },
45
+ });
46
+ }
47
+ }
48
+
49
+ async function create(args) {
50
+ const projectId = resolveOptionalProject(positionalArgs(args, FLAG_VALUES)[0]);
51
+ const emailMode = flagValue(args, "--email-mode") ?? undefined;
52
+ if (emailMode !== undefined) assertAllowedValue(emailMode, ["sandbox", "off"], "--email-mode");
53
+ const ttlDaysFlag = flagValue(args, "--ttl-days");
54
+ const ttlDays = ttlDaysFlag === null ? undefined : parseIntegerFlag("--ttl-days", ttlDaysFlag, { min: 1, max: 30 });
55
+ try {
56
+ const branch = await getSdk().branches.create(projectId, {
57
+ ...(flagValue(args, "--from-snapshot") ? { fromSnapshotId: flagValue(args, "--from-snapshot") } : {}),
58
+ ...(flagValue(args, "--name") ? { name: flagValue(args, "--name") } : {}),
59
+ ...(emailMode ? { emailMode } : {}),
60
+ ...(args.includes("--enable-cron") ? { enableCron: true } : {}),
61
+ ...(ttlDays !== undefined ? { ttlDays } : {}),
62
+ });
63
+ console.log(JSON.stringify({ ok: true, branch }, null, 2));
64
+ } catch (err) {
65
+ reportSdkError(err);
66
+ }
67
+ }
68
+
69
+ async function list(args) {
70
+ const projectId = resolveOptionalProject(positionalArgs(args, FLAG_VALUES)[0]);
71
+ try {
72
+ const result = await getSdk().branches.list(projectId);
73
+ console.log(JSON.stringify({ project_id: projectId, ...result }, null, 2));
74
+ } catch (err) {
75
+ reportSdkError(err);
76
+ }
77
+ }
78
+
79
+ async function renew(args) {
80
+ const { projectId, branchProjectId } = resolveProjectAndBranch(args, "run402 branches renew [project-id] <branch-project-id>");
81
+ const ttlDaysFlag = flagValue(args, "--ttl-days");
82
+ const ttlDays = ttlDaysFlag === null ? undefined : parseIntegerFlag("--ttl-days", ttlDaysFlag, { min: 1, max: 30 });
83
+ try {
84
+ const branch = await getSdk().branches.renew(projectId, branchProjectId, {
85
+ ...(ttlDays !== undefined ? { ttlDays } : {}),
86
+ });
87
+ console.log(JSON.stringify({ ok: true, branch }, null, 2));
88
+ } catch (err) {
89
+ reportSdkError(err);
90
+ }
91
+ }
92
+
93
+ async function deleteBranch(args) {
94
+ const { projectId, branchProjectId } = resolveProjectAndBranch(args, "run402 branches delete [project-id] <branch-project-id>");
95
+ try {
96
+ await getSdk().branches.delete(projectId, branchProjectId);
97
+ console.log(JSON.stringify({ ok: true, project_id: projectId, branch_project_id: branchProjectId, deleted: true }, null, 2));
98
+ } catch (err) {
99
+ reportSdkError(err);
100
+ }
101
+ }
102
+
103
+ function resolveOptionalProject(value) {
104
+ if (value && String(value).startsWith("prj_")) return value;
105
+ return resolveProjectId(null);
106
+ }
107
+
108
+ function resolveProjectAndBranch(args, usage) {
109
+ const pos = positionalArgs(args, FLAG_VALUES);
110
+ if (pos.length === 1) return { projectId: resolveProjectId(null), branchProjectId: pos[0] };
111
+ if (pos.length === 2 && pos[0].startsWith("prj_")) return { projectId: pos[0], branchProjectId: pos[1] };
112
+ fail({ code: "BAD_USAGE", message: `Usage: ${usage}` });
113
+ }
package/lib/core.mjs CHANGED
@@ -6,6 +6,7 @@ const HELP = `run402 core — Local Run402 Core commands
6
6
 
7
7
  Usage:
8
8
  run402 core projects import <archive-path> --name <project-name> [options]
9
+ run402 core projects apply <archive-path> --name <project-name> [options]
9
10
 
10
11
  Options:
11
12
  --name <name> New Core project name (default imported-project)
@@ -36,7 +37,7 @@ export async function run(sub, args = []) {
36
37
  });
37
38
  }
38
39
  const action = args[0];
39
- if (action === "import") return importProject(args.slice(1));
40
+ if (action === "import" || action === "apply") return importProject(args.slice(1));
40
41
  fail({
41
42
  code: "UNKNOWN_SUBCOMMAND",
42
43
  message: `Unknown core projects subcommand: ${action}`,
package/lib/deploy-v2.mjs CHANGED
@@ -43,8 +43,8 @@ import { createUpdateCheckScheduler, emitUpdateNotice } from "./update-check.mjs
43
43
  const APPLY_HELP = `run402 deploy apply — Unified deploy primitive (v1.34+)
44
44
 
45
45
  Usage:
46
- run402 deploy apply --manifest <path> [--project <id>] [--check|--print-spec|--plan|--require-plan <id>] [--quiet|--final-only]
47
- run402 deploy apply --spec '<json>' [--project <id>] [--check|--print-spec|--plan|--require-plan <id>] [--quiet|--final-only]
46
+ run402 deploy apply --manifest <path> [--project <id>] [--check|--print-spec|--plan|--rehearse|--require-plan <id>] [--quiet|--final-only] [--json]
47
+ run402 deploy apply --spec '<json>' [--project <id>] [--check|--print-spec|--plan|--rehearse|--require-plan <id>] [--quiet|--final-only] [--json]
48
48
  run402 deploy apply --dir <build-output> [--manifest <path>] [--project <id>]
49
49
  cat spec.json | run402 deploy apply [--project <id>]
50
50
 
@@ -95,7 +95,11 @@ Options:
95
95
  --check Validate and normalize locally. No gateway calls or uploads.
96
96
  --print-spec Print the normalized ReleaseSpec JSON. No gateway calls or uploads.
97
97
  --plan Ask the gateway for a reviewed plan. No upload or commit.
98
+ --rehearse Plan, upload missing content, run the plan against a contained branch, and print the rehearsal report. Does not commit unless --commit is also passed.
99
+ --teardown <policy> Rehearsal branch cleanup policy: keep (default), on_pass, or always.
100
+ --commit After a passing rehearsal, commit the original plan.
98
101
  --require-plan <id> Apply only if this reviewed plan still matches.
102
+ --json No-op compatibility flag; success output is always JSON.
99
103
  --quiet Suppress per-event JSON-line stderr (final result still on stdout)
100
104
  --final-only Alias for --quiet; final success/error envelope is still preserved
101
105
  --allow-warning <code> Continue past this reviewed warning code (repeatable)
@@ -186,6 +190,15 @@ Output:
186
190
  stdout: { "events": [...] }
187
191
  `;
188
192
 
193
+ const REHEARSE_HELP = `run402 deploy rehearse — Run a persisted plan on a contained branch
194
+
195
+ Usage:
196
+ run402 deploy rehearse <plan_id> [--project <id>] [--teardown keep|on_pass|always] [--json]
197
+
198
+ Use \`run402 apply --manifest app.json --rehearse --json\` for the canonical
199
+ one-shot plan → upload → rehearse report flow.
200
+ `;
201
+
189
202
  const RELEASE_HELP = `run402 deploy release — Inspect deploy release inventory and diffs
190
203
 
191
204
  Usage:
@@ -283,6 +296,7 @@ exit 0; inspect would_serve and diagnostic_status in the result payload.
283
296
 
284
297
  export async function runDeployV2(sub, args) {
285
298
  if (sub === "apply") return await applyCmd(args);
299
+ if (sub === "rehearse") return await rehearseCmd(args);
286
300
  if (sub === "promote") return await promoteCmd(args);
287
301
  if (sub === "resume") return await resumeCmd(args);
288
302
  if (sub === "list") return await listCmd(args);
@@ -297,6 +311,59 @@ export async function runDeployV2(sub, args) {
297
311
  });
298
312
  }
299
313
 
314
+ async function rehearseCmd(rawArgs) {
315
+ const args = normalizeArgv(rawArgs);
316
+ if (args.includes("--help") || args.includes("-h")) {
317
+ console.log(REHEARSE_HELP);
318
+ return;
319
+ }
320
+ const valueFlags = new Set(["--project", "--teardown"]);
321
+ const allowedFlags = ["--project", "--teardown", "--json", "--help", "-h"];
322
+ const positionals = [];
323
+ for (let i = 0; i < args.length; i += 1) {
324
+ const arg = args[i];
325
+ if (valueFlags.has(arg)) {
326
+ if (args[i + 1] === undefined || String(args[i + 1]).startsWith("--")) {
327
+ fail({ code: "BAD_USAGE", message: `${arg} requires a value`, details: { flag: arg } });
328
+ }
329
+ i += 1;
330
+ continue;
331
+ }
332
+ if (String(arg).startsWith("-")) {
333
+ if (!allowedFlags.includes(arg)) {
334
+ fail({ code: "BAD_USAGE", message: `Unknown flag for deploy rehearse: ${arg}`, details: { flag: arg, allowed_flags: allowedFlags } });
335
+ }
336
+ continue;
337
+ }
338
+ positionals.push(arg);
339
+ }
340
+ const planId = positionals[0];
341
+ if (!planId || positionals.length > 1) {
342
+ fail({ code: "BAD_USAGE", message: "Usage: run402 deploy rehearse <plan_id> [--project <id>] [--teardown keep|on_pass|always] [--json]" });
343
+ }
344
+ const teardown = flagValue(args, "--teardown") ?? "keep";
345
+ if (!["keep", "on_pass", "always"].includes(teardown)) {
346
+ fail({ code: "BAD_USAGE", message: "--teardown must be one of: keep, on_pass, always", details: { flag: "--teardown", value: teardown } });
347
+ }
348
+ const project = flagValue(args, "--project") ?? undefined;
349
+ if (!isCoreApiTarget() && !loadLiveControlPlaneSession()) {
350
+ allowanceAuthHeaders(`/apply/v1/plans/${planId}/rehearse`);
351
+ }
352
+ try {
353
+ const rehearsal = await withAutoApprove(() =>
354
+ getSdk()._applyEngine.rehearse(planId, { project, teardown }),
355
+ );
356
+ console.log(JSON.stringify({
357
+ ok: rehearsal.report.status === "passed",
358
+ rehearsal,
359
+ commit_command: `run402 deploy apply --require-plan ${planId}`,
360
+ }, null, 2));
361
+ if (rehearsal.report.status !== "passed") process.exit(1);
362
+ } catch (err) {
363
+ reportSdkError(err);
364
+ }
365
+ }
366
+
300
367
  const PROMOTE_HELP = `run402 deploy promote — Operator pointer-swap recovery (v1.58+)
301
368
 
302
369
  Usage:
@@ -520,8 +587,10 @@ function parseApplyArgs(args) {
520
587
  allowWarningCodes: [],
521
588
  mode: null,
522
589
  planFingerprint: null,
590
+ rehearsalTeardown: "keep",
591
+ commitAfterRehearse: false,
523
592
  };
524
- const allowedFlags = ["--manifest", "--spec", "--dir", "--project", "--quiet", "--final-only", "--allow-warning", "--allow-warnings", "--check", "--print-spec", "--plan", "--require-plan", "--plan-fingerprint", "--help", "-h"];
593
+ const allowedFlags = ["--manifest", "--spec", "--dir", "--project", "--quiet", "--final-only", "--json", "--allow-warning", "--allow-warnings", "--check", "--print-spec", "--plan", "--rehearse", "--teardown", "--commit", "--require-plan", "--plan-fingerprint", "--help", "-h"];
525
594
 
526
595
  for (let i = 0; i < args.length; i++) {
527
596
  const arg = args[i];
@@ -529,7 +598,7 @@ function parseApplyArgs(args) {
529
598
  console.log(APPLY_HELP);
530
599
  process.exit(0);
531
600
  }
532
- if (arg === "--manifest" || arg === "--spec" || arg === "--dir" || arg === "--project" || arg === "--allow-warning" || arg === "--require-plan" || arg === "--plan-fingerprint") {
601
+ if (arg === "--manifest" || arg === "--spec" || arg === "--dir" || arg === "--project" || arg === "--allow-warning" || arg === "--require-plan" || arg === "--plan-fingerprint" || arg === "--teardown") {
533
602
  const value = args[i + 1];
534
603
  if (value === undefined || (typeof value === "string" && value.startsWith("--"))) {
535
604
  fail({
@@ -571,6 +640,15 @@ function parseApplyArgs(args) {
571
640
  opts.allowWarningCodes.push(value);
572
641
  } else if (arg === "--require-plan") {
573
642
  setApplyMode(opts, { kind: "applyReviewed", planId: value }, "--require-plan");
643
+ } else if (arg === "--teardown") {
644
+ if (!["keep", "on_pass", "always"].includes(value)) {
645
+ fail({
646
+ code: "BAD_USAGE",
647
+ message: "--teardown must be one of: keep, on_pass, always",
648
+ details: { flag: "--teardown", value },
649
+ });
650
+ }
651
+ opts.rehearsalTeardown = value;
574
652
  } else {
575
653
  opts.planFingerprint = value;
576
654
  }
@@ -578,10 +656,13 @@ function parseApplyArgs(args) {
578
656
  continue;
579
657
  }
580
658
  if (arg === "--quiet" || arg === "--final-only") { opts.quiet = true; continue; }
659
+ if (arg === "--json") { continue; }
581
660
  if (arg === "--allow-warnings") { opts.allowWarnings = true; continue; }
582
661
  if (arg === "--check") { setApplyMode(opts, "check", "--check"); continue; }
583
662
  if (arg === "--print-spec") { setApplyMode(opts, "printSpec", "--print-spec"); continue; }
584
663
  if (arg === "--plan") { setApplyMode(opts, "plan", "--plan"); continue; }
664
+ if (arg === "--rehearse") { setApplyMode(opts, "rehearse", "--rehearse"); continue; }
665
+ if (arg === "--commit") { opts.commitAfterRehearse = true; continue; }
585
666
  if (typeof arg === "string" && arg.startsWith("-")) {
586
667
  fail({
587
668
  code: "BAD_USAGE",
@@ -612,6 +693,13 @@ function parseApplyArgs(args) {
612
693
  });
613
694
  }
614
695
  }
696
+ if (opts.commitAfterRehearse && opts.mode !== "rehearse") {
697
+ fail({
698
+ code: "BAD_USAGE",
699
+ message: "--commit can only be used with --rehearse.",
700
+ details: { flag: "--commit" },
701
+ });
702
+ }
615
703
 
616
704
  return opts;
617
705
  }
@@ -1027,6 +1115,56 @@ async function applyCmd(args) {
1027
1115
  console.log(JSON.stringify(result.plan, null, 2));
1028
1116
  return;
1029
1117
  }
1118
+ if (opts.mode === "rehearse") {
1119
+ const sdk = getSdk(sdkOpts);
1120
+ const planned = await sdk._applyEngine.plan(releaseSpec, { idempotencyKey });
1121
+ const planId = planned.plan.plan_id;
1122
+ if (!planId) {
1123
+ fail({
1124
+ code: "DRY_RUN_PLAN_NOT_COMMITTABLE",
1125
+ message: "Rehearsal requires a persisted plan_id, but the plan response did not include one.",
1126
+ details: { project_id: releaseSpec.project },
1127
+ });
1128
+ }
1129
+ await sdk._applyEngine.upload(planned.plan, {
1130
+ project: releaseSpec.project,
1131
+ byteReaders: planned.byteReaders,
1132
+ onEvent: makeStderrEventWriter(opts.quiet),
1133
+ });
1134
+ const rehearsal = await withAutoApprove(() =>
1135
+ sdk._applyEngine.rehearse(planId, {
1136
+ project: releaseSpec.project,
1137
+ teardown: opts.rehearsalTeardown,
1138
+ }),
1139
+ );
1140
+ if (rehearsal.report.status !== "passed") {
1141
+ console.log(JSON.stringify({
1142
+ ok: false,
1143
+ project_id: releaseSpec.project,
1144
+ plan_id: planId,
1145
+ rehearsal,
1146
+ commit_command: `run402 deploy apply --require-plan ${planId}${planned.plan.plan_fingerprint ? ` --plan-fingerprint ${planned.plan.plan_fingerprint}` : ""}`,
1147
+ }, null, 2));
1148
+ process.exit(1);
1149
+ }
1150
+ if (opts.commitAfterRehearse) {
1151
+ const committed = await sdk._applyEngine.commit(planId, {
1152
+ project: releaseSpec.project,
1153
+ onEvent: makeStderrEventWriter(opts.quiet),
1154
+ idempotencyKey,
1155
+ });
1156
+ console.log(JSON.stringify({ ok: true, project_id: releaseSpec.project, plan: planned.plan, rehearsal, commit: committed }, null, 2));
1157
+ return;
1158
+ }
1159
+ console.log(JSON.stringify({
1160
+ ok: true,
1161
+ project_id: releaseSpec.project,
1162
+ plan: planned.plan,
1163
+ rehearsal,
1164
+ commit_command: `run402 deploy apply --require-plan ${planId}${planned.plan.plan_fingerprint ? ` --plan-fingerprint ${planned.plan.plan_fingerprint}` : ""}`,
1165
+ }, null, 2));
1166
+ return;
1167
+ }
1030
1168
  const requiredPlan = opts.mode && opts.mode.kind === "applyReviewed"
1031
1169
  ? { planId: opts.mode.planId, ...(opts.mode.planFingerprint ? { planFingerprint: opts.mode.planFingerprint } : {}) }
1032
1170
  : undefined;
@@ -1677,6 +1815,7 @@ async function printResolveEnvelope(input) {
1677
1815
  summary: summary.summary,
1678
1816
  request,
1679
1817
  warnings: summary.warnings,
1818
+ edge_propagation: resolution.edge_propagation ?? null,
1680
1819
  resolution,
1681
1820
  next_steps: summary.next_steps,
1682
1821
  }, null, 2));
package/lib/deploy.mjs CHANGED
@@ -7,6 +7,7 @@ Usage:
7
7
 
8
8
  Subcommands:
9
9
  apply --manifest <file> Apply a v2 ReleaseSpec manifest
10
+ rehearse <plan_id> Rehearse a persisted plan on a contained branch
10
11
  resume <operation_id> Resume a stuck operation
11
12
  list [--project <id>] List recent deploy operations
12
13
  events <operation_id> Fetch event stream for an operation
@@ -48,7 +49,8 @@ export async function run(args) {
48
49
  }
49
50
 
50
51
  switch (sub) {
51
- case "apply":
52
+ case "apply":
53
+ case "rehearse":
52
54
  case "promote":
53
55
  case "resume":
54
56
  case "list":
package/lib/projects.mjs CHANGED
@@ -33,6 +33,7 @@ Subcommands:
33
33
  delete [id] --confirm Immediately and irreversibly delete a project (cascade purge) and remove from local state. Requires --confirm.
34
34
  promote-user [id] <email> Promote a user to project_admin role
35
35
  demote-user [id] <email> Demote a user from project_admin role
36
+ export [id] [archive options] Export a portable Run402 Core archive
36
37
 
37
38
  Examples:
38
39
  run402 projects quote
@@ -56,6 +57,7 @@ Examples:
56
57
  run402 projects apply-expose prj_abc123 --file manifest.json
57
58
  run402 projects get-expose prj_abc123
58
59
  run402 projects delete prj_abc123 --confirm
60
+ run402 projects export prj_abc123 --wait --output ./project.r402ar --json
59
61
 
60
62
  Global options (any command):
61
63
  --wallet <name> Use a named wallet (profile) for this command. Precedence:
@@ -799,6 +801,10 @@ const FLAGS_BY_SUB = {
799
801
  values: ["--file", "--migration-file", "--migration-sql"],
800
802
  },
801
803
  delete: { known: ["--confirm"], values: [] },
804
+ export: {
805
+ known: ["--scope", "--auth", "--consistency", "--idempotency-key", "--output", "--poll-interval", "--timeout", "--wait", "--json", "--json-stream"],
806
+ values: ["--scope", "--auth", "--consistency", "--idempotency-key", "--output", "--poll-interval", "--timeout"],
807
+ },
802
808
  };
803
809
 
804
810
  function validateFlags(sub, args) {
@@ -845,6 +851,11 @@ export async function run(sub, args) {
845
851
  case "validate-expose": await validateExpose(args); break;
846
852
  case "get-expose": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await getExpose(projectId); break; }
847
853
  case "delete": { const { projectId, rest } = resolvePositionalProject(args, { rejectBareFirst: true }); await deleteProject(projectId, rest); break; }
854
+ case "export": {
855
+ const { run } = await import("./cloud.mjs");
856
+ await run("archives", ["create", ...args]);
857
+ break;
858
+ }
848
859
  case "promote-user": { const { projectId, rest } = resolvePositionalProject(args); await promoteUser(projectId, rest[0]); break; }
849
860
  case "demote-user": { const { projectId, rest } = resolvePositionalProject(args); await demoteUser(projectId, rest[0]); break; }
850
861
  default:
@@ -0,0 +1,137 @@
1
+ import { getSdk } from "./sdk.mjs";
2
+ import { fail, reportSdkError } from "./sdk-errors.mjs";
3
+ import {
4
+ assertAllowedValue,
5
+ assertKnownFlags,
6
+ flagValue,
7
+ hasHelp,
8
+ normalizeArgv,
9
+ parseIntegerFlag,
10
+ positionalArgs,
11
+ } from "./argparse.mjs";
12
+ import { resolveProjectId } from "./config.mjs";
13
+
14
+ const HELP = `run402 snapshots — Project database restore points
15
+
16
+ Usage:
17
+ run402 snapshots create [project-id] [--json]
18
+ run402 snapshots list [project-id] [--kind <kind>] [--limit <n>] [--after <cursor>] [--json]
19
+ run402 snapshots get [project-id] <snapshot-id> [--json]
20
+ run402 snapshots restore [project-id] <snapshot-id> [--include-auth] [--confirm <token>] [--json]
21
+ run402 snapshots delete [project-id] <snapshot-id> [--json]
22
+
23
+ Restore is a two-step handshake. First call without --confirm to get a
24
+ restore_plan.confirm.token, then re-run with --confirm after reviewing the
25
+ data-loss statement.
26
+ `;
27
+
28
+ const FLAG_VALUES = ["--kind", "--limit", "--after", "--confirm"];
29
+ const FLAGS = new Set([...FLAG_VALUES, "--include-auth", "--json", "--help", "-h"]);
30
+ const SNAPSHOT_KINDS = ["manual", "pre_migration", "pre_restore", "scheduled"];
31
+
32
+ export async function run(sub, args = []) {
33
+ const all = [sub, ...args].filter(Boolean);
34
+ if (!sub || hasHelp(all)) {
35
+ console.log(HELP);
36
+ return;
37
+ }
38
+ const rest = normalizeArgv(args);
39
+ assertKnownFlags(rest, [...FLAGS], FLAG_VALUES);
40
+ switch (sub) {
41
+ case "create": return create(rest);
42
+ case "list": return list(rest);
43
+ case "get": return get(rest);
44
+ case "restore": return restore(rest);
45
+ case "delete": return deleteSnapshot(rest);
46
+ default:
47
+ fail({
48
+ code: "UNKNOWN_SUBCOMMAND",
49
+ message: `Unknown snapshots subcommand: ${sub}`,
50
+ hint: "Run `run402 snapshots --help` for usage.",
51
+ details: { command: "snapshots", subcommand: sub },
52
+ });
53
+ }
54
+ }
55
+
56
+ async function create(args) {
57
+ const projectId = resolveOptionalProject(positionalArgs(args, FLAG_VALUES)[0]);
58
+ try {
59
+ const snapshot = await getSdk().snapshots.create(projectId);
60
+ console.log(JSON.stringify({ ok: snapshot.status === "ready", snapshot }, null, 2));
61
+ } catch (err) {
62
+ reportSdkError(err);
63
+ }
64
+ }
65
+
66
+ async function list(args) {
67
+ const projectId = resolveOptionalProject(positionalArgs(args, FLAG_VALUES)[0]);
68
+ const kind = flagValue(args, "--kind") ?? undefined;
69
+ if (kind !== undefined) assertAllowedValue(kind, SNAPSHOT_KINDS, "--kind");
70
+ const limitFlag = flagValue(args, "--limit");
71
+ const limit = limitFlag === null ? undefined : parseIntegerFlag("--limit", limitFlag, { min: 1, max: 100 });
72
+ try {
73
+ const result = await getSdk().snapshots.list(projectId, {
74
+ ...(kind ? { kind } : {}),
75
+ ...(limit !== undefined ? { limit } : {}),
76
+ ...(flagValue(args, "--after") ? { after: flagValue(args, "--after") } : {}),
77
+ });
78
+ console.log(JSON.stringify({ project_id: projectId, ...result }, null, 2));
79
+ } catch (err) {
80
+ reportSdkError(err);
81
+ }
82
+ }
83
+
84
+ async function get(args) {
85
+ const { projectId, snapshotId } = resolveProjectAndSnapshot(args, "run402 snapshots get [project-id] <snapshot-id>");
86
+ try {
87
+ const snapshot = await getSdk().snapshots.get(projectId, snapshotId);
88
+ console.log(JSON.stringify({ snapshot }, null, 2));
89
+ } catch (err) {
90
+ reportSdkError(err);
91
+ }
92
+ }
93
+
94
+ async function restore(args) {
95
+ const { projectId, snapshotId } = resolveProjectAndSnapshot(args, "run402 snapshots restore [project-id] <snapshot-id> [--confirm <token>]");
96
+ const includeAuth = args.includes("--include-auth");
97
+ const confirm = flagValue(args, "--confirm");
98
+ try {
99
+ if (confirm) {
100
+ const result = await getSdk().snapshots.restore(projectId, snapshotId, confirm, { includeAuth });
101
+ console.log(JSON.stringify({ ok: result.status === "ready", restore: result }, null, 2));
102
+ return;
103
+ }
104
+ const plan = await getSdk().snapshots.restorePlan(projectId, snapshotId, { includeAuth });
105
+ console.log(JSON.stringify({
106
+ ok: true,
107
+ project_id: projectId,
108
+ snapshot_id: snapshotId,
109
+ ...plan,
110
+ confirm_command: `run402 snapshots restore ${projectId} ${snapshotId} --confirm ${JSON.stringify(plan.restore_plan.confirm.token)}${includeAuth ? " --include-auth" : ""} --json`,
111
+ }, null, 2));
112
+ } catch (err) {
113
+ reportSdkError(err);
114
+ }
115
+ }
116
+
117
+ async function deleteSnapshot(args) {
118
+ const { projectId, snapshotId } = resolveProjectAndSnapshot(args, "run402 snapshots delete [project-id] <snapshot-id>");
119
+ try {
120
+ await getSdk().snapshots.delete(projectId, snapshotId);
121
+ console.log(JSON.stringify({ ok: true, project_id: projectId, snapshot_id: snapshotId, deleted: true }, null, 2));
122
+ } catch (err) {
123
+ reportSdkError(err);
124
+ }
125
+ }
126
+
127
+ function resolveOptionalProject(value) {
128
+ if (value && String(value).startsWith("prj_")) return value;
129
+ return resolveProjectId(null);
130
+ }
131
+
132
+ function resolveProjectAndSnapshot(args, usage) {
133
+ const pos = positionalArgs(args, FLAG_VALUES);
134
+ if (pos.length === 1) return { projectId: resolveProjectId(null), snapshotId: pos[0] };
135
+ if (pos.length === 2 && pos[0].startsWith("prj_")) return { projectId: pos[0], snapshotId: pos[1] };
136
+ fail({ code: "BAD_USAGE", message: `Usage: ${usage}` });
137
+ }