@rehearsal-db/core 0.1.0-beta.13 → 0.1.0-beta.15

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 (169) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +4 -1
  3. package/dist/package.json +100 -0
  4. package/dist/scripts/install/create_root_config.d.mts +3 -0
  5. package/dist/scripts/install/create_root_config.mjs +13 -0
  6. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  7. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  8. package/dist/src/application/session.d.mts +96 -0
  9. package/dist/src/application/session.mjs +380 -0
  10. package/dist/src/baseline/artifact.d.mts +119 -0
  11. package/dist/src/baseline/artifact.mjs +522 -0
  12. package/dist/src/baseline/builder.d.mts +19 -0
  13. package/dist/src/baseline/builder.mjs +142 -0
  14. package/dist/src/baseline/input_discovery.d.mts +13 -0
  15. package/dist/src/baseline/input_discovery.mjs +149 -0
  16. package/dist/src/baseline/policy_review.d.mts +56 -0
  17. package/dist/src/baseline/policy_review.mjs +73 -0
  18. package/dist/src/baseline/preparation.d.mts +63 -0
  19. package/dist/src/baseline/preparation.mjs +295 -0
  20. package/dist/src/baseline/privacy_engine.d.mts +162 -0
  21. package/dist/src/baseline/privacy_engine.mjs +1162 -0
  22. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  23. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  24. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  25. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  26. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  27. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  28. package/dist/src/cli/arguments.d.mts +43 -0
  29. package/dist/src/cli/arguments.mjs +135 -0
  30. package/dist/src/cli/guided.d.mts +51 -0
  31. package/dist/src/cli/guided.mjs +713 -0
  32. package/dist/src/cli/init_command.d.mts +21 -0
  33. package/dist/src/cli/init_command.mjs +90 -0
  34. package/dist/src/cli/project_commands.d.mts +157 -0
  35. package/dist/src/cli/project_commands.mjs +183 -0
  36. package/dist/src/cli/rehearsal.d.mts +7 -0
  37. package/dist/src/cli/rehearsal.mjs +706 -0
  38. package/dist/src/cli/renderers.d.mts +299 -0
  39. package/dist/src/cli/renderers.mjs +554 -0
  40. package/dist/src/cli/runtime_commands.d.mts +265 -0
  41. package/dist/src/cli/runtime_commands.mjs +599 -0
  42. package/dist/src/cli/source_commands.d.mts +432 -0
  43. package/dist/src/cli/source_commands.mjs +299 -0
  44. package/dist/src/cli/terminal.d.mts +59 -0
  45. package/dist/src/cli/terminal.mjs +230 -0
  46. package/dist/src/identity/claim.d.mts +144 -0
  47. package/dist/src/identity/claim.mjs +947 -0
  48. package/dist/src/identity/provider_configuration.d.mts +37 -0
  49. package/dist/src/identity/provider_configuration.mjs +132 -0
  50. package/dist/src/identity/storage.d.mts +21 -0
  51. package/dist/src/identity/storage.mjs +122 -0
  52. package/dist/src/index.d.mts +4 -0
  53. package/dist/src/index.mjs +3 -0
  54. package/dist/src/project/configuration.d.mts +181 -0
  55. package/dist/src/project/configuration.mjs +958 -0
  56. package/dist/src/project/install_scaffold.d.mts +42 -0
  57. package/dist/src/project/install_scaffold.mjs +263 -0
  58. package/dist/src/project/setup.d.mts +87 -0
  59. package/dist/src/project/setup.mjs +414 -0
  60. package/dist/src/project/support_report.d.mts +88 -0
  61. package/dist/src/project/support_report.mjs +83 -0
  62. package/dist/src/runtime/cleanup.d.mts +85 -0
  63. package/dist/src/runtime/cleanup.mjs +311 -0
  64. package/dist/src/runtime/migration_history.d.mts +51 -0
  65. package/dist/src/runtime/migration_history.mjs +170 -0
  66. package/dist/src/runtime/plan.d.mts +134 -0
  67. package/dist/src/runtime/plan.mjs +636 -0
  68. package/dist/src/runtime/policy.d.mts +66 -0
  69. package/dist/src/runtime/policy.mjs +369 -0
  70. package/dist/src/runtime/restore.d.mts +22 -0
  71. package/dist/src/runtime/restore.mjs +276 -0
  72. package/dist/src/runtime/service_environment.d.mts +11 -0
  73. package/dist/src/runtime/service_environment.mjs +58 -0
  74. package/dist/src/runtime/topology.d.mts +51 -0
  75. package/dist/src/runtime/topology.mjs +128 -0
  76. package/dist/src/shared/diagnostics.d.mts +97 -0
  77. package/dist/src/shared/diagnostics.mjs +170 -0
  78. package/dist/src/shared/human_output.d.mts +2 -0
  79. package/{src → dist/src}/shared/human_output.mjs +1 -3
  80. package/dist/src/shared/node_runtime.d.mts +14 -0
  81. package/dist/src/shared/node_runtime.mjs +25 -0
  82. package/dist/src/shared/operation_guard.d.mts +22 -0
  83. package/dist/src/shared/operation_guard.mjs +150 -0
  84. package/dist/src/shared/process_environment.d.mts +15 -0
  85. package/dist/src/shared/process_environment.mjs +50 -0
  86. package/dist/src/source/access.d.mts +165 -0
  87. package/dist/src/source/access.mjs +602 -0
  88. package/dist/src/source/asset_transfer.d.mts +27 -0
  89. package/dist/src/source/asset_transfer.mjs +202 -0
  90. package/dist/src/source/baseline.d.mts +66 -0
  91. package/dist/src/source/baseline.mjs +372 -0
  92. package/dist/src/source/external_reader.d.mts +42 -0
  93. package/dist/src/source/external_reader.mjs +278 -0
  94. package/dist/src/source/postgresql_access.d.mts +86 -0
  95. package/dist/src/source/postgresql_access.mjs +454 -0
  96. package/dist/src/source/privacy_paths.d.mts +10 -0
  97. package/dist/src/source/privacy_paths.mjs +27 -0
  98. package/dist/src/targets/driver.d.mts +2 -0
  99. package/dist/src/targets/driver.mjs +13 -0
  100. package/dist/src/targets/postgresql.d.mts +5 -0
  101. package/dist/src/targets/postgresql.mjs +641 -0
  102. package/dist/src/targets/supabase.d.mts +8 -0
  103. package/dist/src/targets/supabase.mjs +659 -0
  104. package/dist/src/targets/supabase_environment.d.mts +49 -0
  105. package/dist/src/targets/supabase_environment.mjs +162 -0
  106. package/dist/src/targets/target.d.mts +33 -0
  107. package/dist/src/targets/target.mjs +39 -0
  108. package/docs/README.md +1 -0
  109. package/docs/architecture.md +17 -9
  110. package/docs/configuration.md +49 -8
  111. package/docs/getting-started.md +28 -0
  112. package/docs/index.md +1 -0
  113. package/docs/production-source.md +43 -7
  114. package/docs/releasing.md +5 -3
  115. package/docs/roadmap.md +3 -2
  116. package/docs/sanitization.md +415 -14
  117. package/docs/standalone-workflow.md +12 -6
  118. package/docs/troubleshooting.md +5 -1
  119. package/docs/tutorial.md +5 -0
  120. package/docs/typescript.md +87 -0
  121. package/package.json +35 -29
  122. package/scripts/install/create_root_config.mjs +0 -36
  123. package/src/README.md +0 -17
  124. package/src/application/session.mjs +0 -435
  125. package/src/baseline/artifact.mjs +0 -684
  126. package/src/baseline/builder.mjs +0 -164
  127. package/src/baseline/input_discovery.mjs +0 -155
  128. package/src/baseline/policy_review.mjs +0 -92
  129. package/src/baseline/preparation.mjs +0 -323
  130. package/src/baseline/privacy_engine.mjs +0 -952
  131. package/src/baseline/sanitization_policy.mjs +0 -299
  132. package/src/baseline/schema_snapshot.mjs +0 -113
  133. package/src/cli/arguments.mjs +0 -126
  134. package/src/cli/guided.mjs +0 -814
  135. package/src/cli/init_command.mjs +0 -94
  136. package/src/cli/rehearsal.mjs +0 -970
  137. package/src/cli/renderers.mjs +0 -626
  138. package/src/cli/runtime_commands.mjs +0 -707
  139. package/src/cli/source_commands.mjs +0 -347
  140. package/src/cli/terminal.mjs +0 -275
  141. package/src/identity/claim.mjs +0 -1163
  142. package/src/identity/provider_configuration.mjs +0 -177
  143. package/src/identity/storage.mjs +0 -165
  144. package/src/project/configuration.mjs +0 -1337
  145. package/src/project/install_scaffold.mjs +0 -291
  146. package/src/project/setup.mjs +0 -466
  147. package/src/project/support_report.mjs +0 -105
  148. package/src/runtime/cleanup.mjs +0 -381
  149. package/src/runtime/migration_history.mjs +0 -220
  150. package/src/runtime/plan.mjs +0 -722
  151. package/src/runtime/policy.mjs +0 -438
  152. package/src/runtime/restore.mjs +0 -337
  153. package/src/runtime/service_environment.mjs +0 -82
  154. package/src/runtime/topology.mjs +0 -177
  155. package/src/shared/diagnostics.mjs +0 -207
  156. package/src/shared/node_runtime.mjs +0 -31
  157. package/src/shared/operation_guard.mjs +0 -162
  158. package/src/shared/process_environment.mjs +0 -68
  159. package/src/source/access.mjs +0 -753
  160. package/src/source/asset_transfer.mjs +0 -207
  161. package/src/source/baseline.mjs +0 -482
  162. package/src/source/external_reader.mjs +0 -255
  163. package/src/source/postgresql_access.mjs +0 -581
  164. package/src/source/privacy_paths.mjs +0 -38
  165. package/src/targets/postgresql.mjs +0 -808
  166. package/src/targets/supabase.mjs +0 -840
  167. package/src/targets/supabase_environment.mjs +0 -229
  168. package/src/targets/target.mjs +0 -66
  169. /package/{src/project → types}/configuration.d.mts +0 -0
@@ -0,0 +1,706 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Purpose: Run the versioned, project-configured Rehearsal developer CLI for
4
+ * initialization, readiness, planning, inspection, and verified local execution.
5
+ * Run: `npm run rehearsal -- doctor` or `npm run rehearsal -- explain`.
6
+ */
7
+ import { readFile } from "node:fs/promises";
8
+ import { join, relative } from "node:path";
9
+ import { performance } from "node:perf_hooks";
10
+ import { parseEnv } from "node:util";
11
+ import { fileURLToPath } from "node:url";
12
+ import * as prompts from "@clack/prompts";
13
+ import { createRehearsalResult, normalizeRehearsalError, REHEARSAL_VERSION, renderHumanError, serializeRehearsalError, } from "../shared/diagnostics.mjs";
14
+ import { inspectRehearsalBaseline, inspectRehearsalMigrations, runRehearsalDoctor, } from "../runtime/plan.mjs";
15
+ import { createSyntheticBaselineFromFiles } from "../baseline/builder.mjs";
16
+ import { inspectBaselineInputFiles } from "../baseline/preparation.mjs";
17
+ import { formatCount } from "../shared/human_output.mjs";
18
+ import { collectRehearsalSupportReport } from "../project/support_report.mjs";
19
+ import { createPrivacyKey } from "../baseline/privacy_engine.mjs";
20
+ import { createSourceAccessPlan } from "../source/access.mjs";
21
+ import { buildApplicationEnvironment, runHttpProofs, startApplicationSession, } from "../application/session.mjs";
22
+ import { applyIdentityClaim } from "../identity/claim.mjs";
23
+ import { createSupabaseStorageTransfer } from "../identity/storage.mjs";
24
+ import { acquireProjectOperation, assertInstalledPackageFingerprint, createInstalledPackageFingerprint, } from "../shared/operation_guard.mjs";
25
+ import { commandMutatesProject, parseArguments, resetGuidedFlags, } from "./arguments.mjs";
26
+ import { formatDuration, installGuidedExitShortcut, isHumanTerminal, terminalStyle, useStyledPrompts, } from "./terminal.mjs";
27
+ import { createGuidedHome } from "./guided.mjs";
28
+ import { buildRefreshWorkflowPlan, createRefreshWorkflow, loadPreparationContext, runSourceAccess, runSourceBaselineRefresh, runSourceRetirement, } from "./source_commands.mjs";
29
+ import { buildRuntimeStackPlan, createRuntimeCommands, runRuntimeStackDoctor, topologyCandidateSummary, topologyCommandEnvironment, topologyEnvironmentFiles, } from "./runtime_commands.mjs";
30
+ import { renderBaseline, renderBaselineInputInspection, renderBaselinePreparation, renderCandidates, renderCleanup, renderDoctor, formatBytes, renderIdentityPlan, renderInit, renderMigrations, renderPlan, renderRefreshWorkflow, renderSetup, renderSourceAccessPlan, renderSourceRetirement, renderSupportReport, } from "./renderers.mjs";
31
+ import { runRehearsalInit } from "./init_command.mjs";
32
+ import { createProjectCommands } from "./project_commands.mjs";
33
+ const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
34
+ const projectRoot = process.cwd();
35
+ let activePackageFingerprint = null;
36
+ const commandStartedAt = new Date();
37
+ const commandStartedMs = performance.now();
38
+ const managerPath = join(packageRoot, "dist/scripts/runtime/manage_database.mjs");
39
+ let lastGuidedDetails;
40
+ const isRuntimeAction = (command) => [
41
+ "run",
42
+ "start",
43
+ "migrate",
44
+ "reset",
45
+ "status",
46
+ "stop",
47
+ "discard",
48
+ "verify",
49
+ ].includes(command);
50
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
51
+ const { openRuntimeApplication, prepareCandidateConfirmation, prepareDependentTargets, proveRuntimeStack, runManager, runRuntimeStack, } = createRuntimeCommands({
52
+ packageRoot,
53
+ projectRoot,
54
+ managerPath,
55
+ getActivePackageFingerprint: () => activePackageFingerprint ?? undefined,
56
+ });
57
+ const { loadIdentityClaimPlan, planRuntimeStackCleanup, runBaselinePreparation, runCleanup, runSetup, } = createProjectCommands({ projectRoot, runManager });
58
+ const emit = ({ command, data, flags, render, status = "success", }) => {
59
+ const result = createRehearsalResult({
60
+ command,
61
+ status,
62
+ data,
63
+ startedAt: commandStartedAt,
64
+ durationMs: Math.round((performance.now() - commandStartedMs) * 100) / 100,
65
+ });
66
+ if (flags.json)
67
+ console.log(JSON.stringify(result, null, 2));
68
+ else {
69
+ const renderOutput = render;
70
+ console.log(renderOutput(data, flags.verbosity));
71
+ }
72
+ };
73
+ const usage = () => `Usage: rehearsal <command> [options]
74
+
75
+ Commands:
76
+ version Print the installed Rehearsal version
77
+ guide Open the interactive, state-aware home screen
78
+ setup [--target=] [--write] Preview or create safe first-run scaffolding
79
+ init [--write] Preview or explicitly write safe starter config
80
+ baseline prepare --records= --ledger= [--write] Create a fail-closed policy draft
81
+ baseline create --records= --ledger= [--assets=] Create a baseline from safe local inputs
82
+ privacy key [--write] Preview or create the owner-only pseudonym key
83
+ source plan Preview exact, temporary source access
84
+ source apply --confirm-source-access= Apply the reviewed source-access plan
85
+ source retire [--confirm-source-retirement=] Preview or retire exact source access
86
+ baseline refresh Replace only the baseline from a reviewed source
87
+ refresh [--confirm-refresh=] Replace the database copy, reset locally, and remove old copies
88
+ identity plan --identity= Preview an approved local identity association
89
+ identity claim --identity= --confirm-identity= Apply the exact local association
90
+ doctor Check whether Rehearsal is safe and ready
91
+ support Print a safe, copy-ready support report
92
+ explain Show the immutable execution plan
93
+ run --dry-run Alias the exact explain plan without mutations
94
+ run --confirm-candidates= Execute reset, migration, and verification locally
95
+ open Start the runtime and keep the application open
96
+ candidates Show the exact pending migration digest
97
+ inspect baseline Show verified baseline provenance
98
+ inspect migrations Classify represented, applied, and candidate migrations
99
+ start Start an existing verified local runtime
100
+ migrate --confirm-candidates= Apply the exact candidate suffix without resetting
101
+ reset Restore and verify the immutable local baseline
102
+ status Report the disposable local runtime state
103
+ stop Stop only this project's local runtime
104
+ discard Remove only this project's disposable runtime
105
+ cleanup [--include-runtime] [--include-images] [--write] [--confirm-cleanup=]
106
+ Preview or apply conservative disk cleanup
107
+ verify Verify the current local Rehearsal runtime
108
+
109
+ Options: --help, -h --version, -V --json --verbose --debug --plain
110
+ --config=<path> --target=supabase|postgresql
111
+ --confirm-source-access=<digest> --confirm-source-retirement=<digest>
112
+ --confirm-refresh=<digest>`;
113
+ const runRefreshWorkflow = createRefreshWorkflow({
114
+ runRuntimeStack,
115
+ prepareDependentTargets,
116
+ topologyCommandEnvironment,
117
+ });
118
+ const runGuidedHome = createGuidedHome({
119
+ projectRoot,
120
+ topologyCandidateSummary,
121
+ buildRefreshWorkflowPlan,
122
+ planRuntimeStackCleanup,
123
+ getLastGuidedDetails: () => lastGuidedDetails,
124
+ });
125
+ const executeCommand = async ({ command, flags, planOptions, guided, }) => {
126
+ if (command === "version" || flags.version) {
127
+ if (flags.json) {
128
+ console.log(JSON.stringify({ name: "@rehearsal-db/core", version: REHEARSAL_VERSION }, null, 2));
129
+ }
130
+ else
131
+ console.log(REHEARSAL_VERSION);
132
+ return;
133
+ }
134
+ if (command === "help" || flags.help) {
135
+ console.log(usage());
136
+ return;
137
+ }
138
+ if (command === "setup") {
139
+ const data = await runSetup({ flags, planOptions });
140
+ emit({ command, data, flags, render: renderSetup });
141
+ return;
142
+ }
143
+ if (command === "init") {
144
+ const data = await runRehearsalInit({ projectRoot, flags });
145
+ emit({ command, data, flags, render: renderInit });
146
+ return;
147
+ }
148
+ if (command === "baseline prepare") {
149
+ const data = await runBaselinePreparation({ flags, planOptions });
150
+ emit({
151
+ command,
152
+ data,
153
+ flags,
154
+ render: (result) => renderBaselinePreparation(result, flags),
155
+ });
156
+ return;
157
+ }
158
+ if (command === "baseline create") {
159
+ if (!flags.recordsPath || !flags.ledgerPath) {
160
+ throw new Error("baseline create requires --records and --ledger input paths.");
161
+ }
162
+ const baselineInput = {
163
+ ...planOptions,
164
+ recordsPath: flags.recordsPath,
165
+ ledgerPath: flags.ledgerPath,
166
+ ...(flags.assetsPath === undefined
167
+ ? {}
168
+ : { assetsPath: flags.assetsPath }),
169
+ };
170
+ await inspectBaselineInputFiles(baselineInput);
171
+ const data = await createSyntheticBaselineFromFiles(baselineInput);
172
+ emit({
173
+ command,
174
+ data,
175
+ flags,
176
+ render: (baseline) => [
177
+ `Activated synthetic baseline ${baseline.generationId}: ${formatCount(baseline.rowCount, "row")} across ${formatCount(baseline.tableCount, "table")}; ${formatCount(baseline.migrationCount, "migration")} through ${baseline.migrationCutoff}.`,
178
+ "",
179
+ flags.guided
180
+ ? "Next: choose Run a rehearsal when you are ready."
181
+ : "Next: run rehearsal doctor to check readiness.",
182
+ ].join("\n"),
183
+ });
184
+ return;
185
+ }
186
+ if (command === "privacy key") {
187
+ const { loaded } = await loadPreparationContext(planOptions);
188
+ if (!loaded.paths.privacyKey) {
189
+ throw new Error("No config.preparation.privacyKey is configured.");
190
+ }
191
+ const privacyKeyPath = loaded.paths.privacyKey;
192
+ const data = flags.write
193
+ ? {
194
+ mode: "written",
195
+ path: relative(loaded.projectRoot, privacyKeyPath),
196
+ ...(await createPrivacyKey(privacyKeyPath)),
197
+ }
198
+ : {
199
+ mode: "preview",
200
+ path: relative(loaded.projectRoot, privacyKeyPath),
201
+ };
202
+ emit({
203
+ command,
204
+ data,
205
+ flags,
206
+ render: (result) => result.mode === "preview"
207
+ ? `PRIVACY KEY — PREVIEW\n\nWould create ${result.path} with owner-only permissions.\nNothing was written. Rerun with --write after reviewing the path.`
208
+ : `Created owner-only privacy key at ${result.path}.\nFingerprint: ${result.fingerprint}\nThe key is excluded from source control and must not be shared.`,
209
+ });
210
+ return;
211
+ }
212
+ if (command === "source plan") {
213
+ const { sourcePolicy } = await loadPreparationContext(planOptions);
214
+ const data = createSourceAccessPlan({ policy: sourcePolicy });
215
+ emit({ command, data, flags, render: renderSourceAccessPlan });
216
+ return;
217
+ }
218
+ if (command === "source apply") {
219
+ const data = await runSourceAccess({ flags, planOptions });
220
+ emit({
221
+ command,
222
+ data,
223
+ flags,
224
+ render: (result) => result.mode === "preview"
225
+ ? renderSourceAccessPlan(result.plan)
226
+ : [
227
+ "SOURCE ACCESS — READY",
228
+ `${result.receipt.accessMode === "external" ? "External" : "Temporary"} reader expires: ${result.receipt.expiresAt}`,
229
+ result.receipt.accessMode === "external"
230
+ ? "Rehearsal made no source-side changes."
231
+ : "Rehearsal created only the reviewed temporary source access.",
232
+ `Local credential: ${result.credentialFile}`,
233
+ "",
234
+ "Next: run rehearsal baseline refresh, then retire source access.",
235
+ ].join("\n"),
236
+ });
237
+ return;
238
+ }
239
+ if (command === "source retire") {
240
+ const data = await runSourceRetirement({ flags, planOptions });
241
+ emit({ command, data, flags, render: renderSourceRetirement });
242
+ return;
243
+ }
244
+ if (command === "baseline refresh") {
245
+ const data = await runSourceBaselineRefresh({ planOptions });
246
+ emit({
247
+ command,
248
+ data,
249
+ flags,
250
+ render: (result) => [
251
+ `Activated refreshed baseline ${result.generationId}.`,
252
+ `${formatCount(result.manifest.rowCount, "sanitized row")} across ${formatCount(Object.keys(result.manifest.tableCounts).length, "table")}.`,
253
+ `Source estimate: ${formatCount(result.estimate.rows, "row")}, ${formatBytes(result.estimate.bytes)}.`,
254
+ "",
255
+ "Next: run rehearsal source retire, then rehearsal doctor.",
256
+ ].join("\n"),
257
+ });
258
+ return;
259
+ }
260
+ if (command === "refresh") {
261
+ const data = await runRefreshWorkflow({ flags, planOptions });
262
+ emit({ command, data, flags, render: renderRefreshWorkflow });
263
+ return;
264
+ }
265
+ if (command === "identity plan" || command === "identity claim") {
266
+ const { loaded, plan } = await loadIdentityClaimPlan({
267
+ flags,
268
+ planOptions,
269
+ });
270
+ if (command === "identity plan" || !flags.identityConfirmation) {
271
+ emit({
272
+ command,
273
+ data: { mode: "preview", plan },
274
+ flags,
275
+ render: renderIdentityPlan,
276
+ });
277
+ return;
278
+ }
279
+ const runtimeEnvironment = parseEnv(await readFile(loaded.paths.applicationEnvironment, "utf8"));
280
+ if (!runtimeEnvironment.DATABASE_URL) {
281
+ throw new Error("The local runtime environment has no DATABASE_URL.");
282
+ }
283
+ const needsStorageTransfer = plan.identity.assets.some((asset) => asset.rewritePath);
284
+ if (needsStorageTransfer &&
285
+ (!runtimeEnvironment.SUPABASE_URL ||
286
+ !runtimeEnvironment.SUPABASE_SERVICE_ROLE_KEY)) {
287
+ throw new Error("Identity asset transfer requires local Supabase URL and service-role credentials.");
288
+ }
289
+ const storageTransfer = needsStorageTransfer
290
+ ? createSupabaseStorageTransfer({
291
+ url: runtimeEnvironment.SUPABASE_URL,
292
+ serviceRoleKey: runtimeEnvironment.SUPABASE_SERVICE_ROLE_KEY,
293
+ })
294
+ : undefined;
295
+ const result = await applyIdentityClaim({
296
+ plan,
297
+ confirmation: flags.identityConfirmation,
298
+ connectionString: runtimeEnvironment.DATABASE_URL,
299
+ ...(storageTransfer === undefined ? {} : { storageTransfer }),
300
+ });
301
+ emit({
302
+ command,
303
+ data: { mode: "written", plan, result },
304
+ flags,
305
+ render: renderIdentityPlan,
306
+ });
307
+ return;
308
+ }
309
+ if (command === "doctor") {
310
+ const data = await runRuntimeStackDoctor(planOptions);
311
+ emit({
312
+ command,
313
+ data,
314
+ flags,
315
+ render: renderDoctor,
316
+ status: data.state === "READY" ? "success" : "not_ready",
317
+ });
318
+ if (data.state !== "READY" && !guided)
319
+ process.exitCode = 1;
320
+ return;
321
+ }
322
+ if (command === "support") {
323
+ const data = await collectRehearsalSupportReport(planOptions, {
324
+ runDoctor: async (options) => {
325
+ const report = await runRuntimeStackDoctor(options);
326
+ const targetAliases = new Map();
327
+ const aliasFor = (name) => {
328
+ if (!targetAliases.has(name)) {
329
+ targetAliases.set(name, `runtime-${targetAliases.size + 1}`);
330
+ }
331
+ return targetAliases.get(name);
332
+ };
333
+ return {
334
+ ...report,
335
+ checks: report.checks.map((check) => {
336
+ const separator = check.id.indexOf(":");
337
+ if (separator === -1)
338
+ return check;
339
+ const target = check.id.slice(0, separator);
340
+ const alias = aliasFor(target);
341
+ return {
342
+ ...check,
343
+ id: `${alias}:${check.id.slice(separator + 1)}`,
344
+ label: check.label.replace(`${target}:`, `${alias}:`),
345
+ };
346
+ }),
347
+ };
348
+ },
349
+ });
350
+ emit({ command, data, flags, render: renderSupportReport });
351
+ return;
352
+ }
353
+ if (command === "cleanup") {
354
+ const data = await runCleanup({ flags, planOptions });
355
+ emit({ command, data, flags, render: renderCleanup });
356
+ return;
357
+ }
358
+ if (command === "explain" || (command === "run" && flags.dryRun)) {
359
+ const data = await buildRuntimeStackPlan(planOptions);
360
+ emit({ command, data, flags, render: renderPlan });
361
+ return;
362
+ }
363
+ if (command === "inspect baseline") {
364
+ const data = await inspectRehearsalBaseline(planOptions);
365
+ emit({ command, data, flags, render: renderBaseline });
366
+ return;
367
+ }
368
+ if (command === "inspect migrations") {
369
+ const data = await inspectRehearsalMigrations(planOptions);
370
+ emit({ command, data, flags, render: renderMigrations });
371
+ return;
372
+ }
373
+ if (command === "candidates") {
374
+ const { topology: _topology, ...data } = await topologyCandidateSummary(planOptions);
375
+ emit({ command, data, flags, render: renderCandidates });
376
+ return;
377
+ }
378
+ if (command === "open") {
379
+ if (flags.dryRun) {
380
+ throw new Error("--dry-run is supported only by rehearsal run.");
381
+ }
382
+ const data = await openRuntimeApplication({
383
+ flags,
384
+ planOptions,
385
+ onReady: (ready) => {
386
+ const lines = [
387
+ `Application: ${ready.url}`,
388
+ "The verified database runtime stays running and keeps your local changes.",
389
+ guided
390
+ ? "Press Ctrl+C to close the app and return here, or Ctrl+Z to exit Rehearsal."
391
+ : "Press Ctrl+C or Ctrl+Z to close the app. Rehearsal will leave the database runtime running.",
392
+ ];
393
+ if (useStyledPrompts(flags)) {
394
+ prompts.note(lines.join("\n"), "SANDBOX READY");
395
+ }
396
+ else if (!flags.json) {
397
+ console.log(["", "SANDBOX READY", ...lines, ""].join("\n"));
398
+ }
399
+ },
400
+ });
401
+ if (guided && data.application.stoppedBy === "SIGTSTP") {
402
+ flags.exitGuidedSession = true;
403
+ }
404
+ if (guided) {
405
+ lastGuidedDetails = [
406
+ data.started.output,
407
+ data.verified.output,
408
+ ...data.preparations.map((preparation) => preparation.output),
409
+ ]
410
+ .filter(Boolean)
411
+ .join("\n");
412
+ }
413
+ emit({
414
+ command,
415
+ data,
416
+ flags,
417
+ render: (result) => [
418
+ "SANDBOX APP CLOSED",
419
+ `Application stopped after ${result.application.stoppedBy}.`,
420
+ "The database runtime and its local database and Storage changes were preserved.",
421
+ "Run rehearsal open to return, rehearsal verify to check it, or rehearsal stop to stop the databases.",
422
+ ].join("\n"),
423
+ });
424
+ return;
425
+ }
426
+ if (isRuntimeAction(command)) {
427
+ if (flags.dryRun && command !== "run") {
428
+ throw new Error("--dry-run is supported only by rehearsal run.");
429
+ }
430
+ if (!(await prepareCandidateConfirmation({ command, flags, planOptions }))) {
431
+ return;
432
+ }
433
+ const stack = await runRuntimeStack({ command, flags, planOptions });
434
+ const { runtime, topology } = stack;
435
+ const stackEnvironment = topologyCommandEnvironment(topology);
436
+ const preparations = ["run", "reset", "migrate"].includes(command)
437
+ ? prepareDependentTargets({
438
+ topology,
439
+ environment: stackEnvironment,
440
+ })
441
+ : [];
442
+ let applicationProof;
443
+ let dependentProofs = [];
444
+ let httpProofResults = [];
445
+ let applicationReadiness;
446
+ if (command === "run") {
447
+ const applicationConfig = topology.primary.config.application;
448
+ const mappedApplicationEnvironment = Object.keys(applicationConfig.environmentVariables).length
449
+ ? await buildApplicationEnvironment({
450
+ files: topologyEnvironmentFiles(topology),
451
+ mappings: applicationConfig.environmentVariables,
452
+ })
453
+ : {};
454
+ const proofEnvironment = {
455
+ ...stackEnvironment,
456
+ ...mappedApplicationEnvironment,
457
+ };
458
+ const proofLabel = topology.targets.length > 1
459
+ ? "project-owned dependent and application proofs"
460
+ : "project-owned application proof";
461
+ const proofSpinner = useStyledPrompts(flags) ? prompts.spinner() : null;
462
+ if (proofSpinner) {
463
+ proofSpinner.start(`Running the ${proofLabel}.`);
464
+ }
465
+ else if (isHumanTerminal(flags)) {
466
+ console.log(`${terminalStyle(flags, "36", "→")} Running the ${proofLabel}.`);
467
+ }
468
+ let applicationSession;
469
+ try {
470
+ if (applicationConfig.readiness) {
471
+ applicationSession = await startApplicationSession({
472
+ command: applicationConfig.startCommand,
473
+ cwd: topology.projectRoot,
474
+ files: topologyEnvironmentFiles(topology),
475
+ mappings: applicationConfig.environmentVariables,
476
+ readiness: applicationConfig.readiness,
477
+ });
478
+ applicationReadiness = applicationSession.ready;
479
+ }
480
+ ({ applicationProof, dependentProofs } = await proveRuntimeStack({
481
+ topology,
482
+ planOptions,
483
+ environment: proofEnvironment,
484
+ }));
485
+ if (applicationConfig.httpProofs.length) {
486
+ httpProofResults = await runHttpProofs({
487
+ checks: applicationConfig.httpProofs,
488
+ });
489
+ }
490
+ }
491
+ catch (error) {
492
+ if (proofSpinner)
493
+ proofSpinner.stop("A project proof failed.");
494
+ throw error;
495
+ }
496
+ finally {
497
+ await applicationSession?.stop();
498
+ }
499
+ if (proofSpinner) {
500
+ proofSpinner.stop("Project proofs passed.");
501
+ }
502
+ else if (isHumanTerminal(flags)) {
503
+ console.log(`${terminalStyle(flags, "32", "✓")} Project proofs passed.`);
504
+ }
505
+ }
506
+ const data = command === "run"
507
+ ? {
508
+ runtime,
509
+ preparations,
510
+ dependentProofs,
511
+ applicationProof,
512
+ applicationReadiness,
513
+ httpProofResults,
514
+ }
515
+ : preparations.length
516
+ ? { ...runtime, preparations }
517
+ : runtime;
518
+ if (guided) {
519
+ lastGuidedDetails = [
520
+ runtime.output,
521
+ ...preparations.map((preparation) => preparation.output),
522
+ ...dependentProofs.map((proof) => proof.output),
523
+ applicationProof?.output,
524
+ ]
525
+ .filter(Boolean)
526
+ .join("\n");
527
+ }
528
+ emit({
529
+ command,
530
+ data,
531
+ flags,
532
+ render: (value) => {
533
+ const runtimeResult = "runtime" in value ? value.runtime : value;
534
+ const renderedPreparations = "preparations" in value ? value.preparations : [];
535
+ const renderedDependentProofs = "dependentProofs" in value ? value.dependentProofs : [];
536
+ const renderedApplicationProof = "applicationProof" in value ? value.applicationProof : undefined;
537
+ const renderedApplicationReadiness = "applicationReadiness" in value
538
+ ? value.applicationReadiness
539
+ : undefined;
540
+ const renderedHttpProofResults = "httpProofResults" in value ? value.httpProofResults : [];
541
+ const nextAction = {
542
+ run: "Next: run rehearsal open for hands-on testing, then rehearsal verify.",
543
+ start: "Next: run rehearsal open for hands-on testing or rehearsal status.",
544
+ migrate: "Next: run rehearsal verify to prove the current runtime.",
545
+ reset: "Next: run rehearsal open or rehearsal verify.",
546
+ status: "Next: run rehearsal verify, reset, stop, or discard.",
547
+ stop: "Next: run rehearsal start when you want to resume.",
548
+ discard: "The disposable runtime was removed; the immutable baseline remains.",
549
+ verify: "Next: continue testing, reset to the baseline, or stop the runtime.",
550
+ }[runtimeResult.action];
551
+ if (guided) {
552
+ const title = runtimeResult.action === "run"
553
+ ? "REHEARSAL PASSED"
554
+ : "ACTION COMPLETE";
555
+ return [
556
+ title,
557
+ `✓ ${runtimeResult.action} completed in ${formatDuration(runtimeResult.durationMs)}`,
558
+ ...renderedPreparations.map((preparation) => `✓ ${preparation.target} preparation passed: ${preparation.command}`),
559
+ ...renderedDependentProofs.map((proof) => `✓ ${proof.target} proof passed: ${proof.command}`),
560
+ renderedApplicationProof
561
+ ? `✓ Application proof passed: ${renderedApplicationProof.command}`
562
+ : null,
563
+ renderedApplicationReadiness
564
+ ? `✓ Application became ready at ${renderedApplicationReadiness.url}`
565
+ : null,
566
+ ...renderedHttpProofResults.map((proof) => `✓ ${proof.kind} HTTP proof passed: ${proof.name}`),
567
+ "",
568
+ "Technical output is available from Show details in the guide.",
569
+ ]
570
+ .filter(Boolean)
571
+ .join("\n");
572
+ }
573
+ return [
574
+ runtimeResult.output ||
575
+ `Rehearsal ${runtimeResult.action} completed.`,
576
+ ...renderedPreparations.map((preparation) => `${preparation.target} preparation passed: ${preparation.command}`),
577
+ ...renderedDependentProofs.map((proof) => `${proof.target} proof passed: ${proof.command}`),
578
+ renderedApplicationProof
579
+ ? `Application proof passed: ${renderedApplicationProof.command}`
580
+ : null,
581
+ renderedApplicationReadiness
582
+ ? `Application readiness passed: ${renderedApplicationReadiness.url}`
583
+ : null,
584
+ ...renderedHttpProofResults.map((proof) => `${proof.kind} HTTP proof passed: ${proof.name}`),
585
+ nextAction ? "" : null,
586
+ nextAction,
587
+ ]
588
+ .filter((entry) => entry !== null)
589
+ .join("\n");
590
+ },
591
+ });
592
+ return;
593
+ }
594
+ throw new Error(`Unknown Rehearsal command: ${command}.\n\n${usage()}`);
595
+ };
596
+ const main = async () => {
597
+ const { flags, positionals } = parseArguments(process.argv.slice(2));
598
+ const command = positionals.join(" ") || "help";
599
+ const planOptions = {
600
+ projectRoot,
601
+ ...(flags.configPath === undefined ? {} : { configPath: flags.configPath }),
602
+ };
603
+ const wantsAutomaticGuide = positionals.length === 0 &&
604
+ !flags.help &&
605
+ !flags.json &&
606
+ process.stdin.isTTY &&
607
+ process.stdout.isTTY;
608
+ const guided = command === "guide" || wantsAutomaticGuide;
609
+ const mutating = commandMutatesProject({ command, flags, guided });
610
+ const operation = mutating
611
+ ? await acquireProjectOperation({ projectRoot, command })
612
+ : null;
613
+ if (operation) {
614
+ activePackageFingerprint = createInstalledPackageFingerprint({
615
+ packageRoot,
616
+ });
617
+ }
618
+ try {
619
+ if (!guided) {
620
+ await executeCommand({ command, flags, planOptions, guided: false });
621
+ return;
622
+ }
623
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
624
+ throw new Error("The guided home screen requires an interactive terminal. Run rehearsal --help to list scriptable commands.");
625
+ }
626
+ flags.guided = true;
627
+ const installExitShortcut = () => installGuidedExitShortcut(flags, {
628
+ beforeExit: () => operation?.releaseSync(),
629
+ });
630
+ let removeGuidedExitShortcut = installExitShortcut();
631
+ if (useStyledPrompts(flags)) {
632
+ prompts.intro("REHEARSAL · Safe local migration testing");
633
+ }
634
+ try {
635
+ while (true) {
636
+ resetGuidedFlags(flags);
637
+ const selected = await runGuidedHome({ flags, planOptions });
638
+ if (!selected)
639
+ break;
640
+ if (selected === "home")
641
+ continue;
642
+ const opensApplication = selected === "open";
643
+ if (opensApplication) {
644
+ removeGuidedExitShortcut?.();
645
+ removeGuidedExitShortcut = null;
646
+ }
647
+ try {
648
+ await executeCommand({
649
+ command: selected,
650
+ flags,
651
+ planOptions,
652
+ guided: true,
653
+ });
654
+ }
655
+ finally {
656
+ if (opensApplication) {
657
+ removeGuidedExitShortcut = installExitShortcut();
658
+ }
659
+ }
660
+ if (flags.exitGuidedSession)
661
+ break;
662
+ }
663
+ }
664
+ finally {
665
+ removeGuidedExitShortcut?.();
666
+ }
667
+ if (useStyledPrompts(flags)) {
668
+ prompts.outro("See you at the next rehearsal.");
669
+ }
670
+ else {
671
+ console.log("See you at the next rehearsal.");
672
+ }
673
+ }
674
+ finally {
675
+ if (operation) {
676
+ try {
677
+ assertInstalledPackageFingerprint({
678
+ packageRoot,
679
+ expected: activePackageFingerprint,
680
+ });
681
+ }
682
+ finally {
683
+ activePackageFingerprint = null;
684
+ await operation.release();
685
+ }
686
+ }
687
+ }
688
+ };
689
+ try {
690
+ await main();
691
+ }
692
+ catch (error) {
693
+ const failure = normalizeRehearsalError(error, {
694
+ expected: "a safe, versioned, local-only Rehearsal operation",
695
+ actual: errorMessage(error),
696
+ refused: "No further Rehearsal action was performed.",
697
+ suggestions: ["Run rehearsal doctor for actionable readiness checks."],
698
+ });
699
+ const wantsJson = process.argv.includes("--json");
700
+ const debug = process.argv.includes("--debug");
701
+ if (wantsJson)
702
+ console.log(JSON.stringify(serializeRehearsalError(failure, { debug }), null, 2));
703
+ else
704
+ console.error(renderHumanError(failure, { debug }));
705
+ process.exitCode = failure.exitCode;
706
+ }