@rehearsal-db/core 0.1.0-beta.14 → 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 (162) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -0
  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 +9 -3
  111. package/docs/index.md +1 -0
  112. package/docs/typescript.md +87 -0
  113. package/package.json +35 -29
  114. package/scripts/install/create_root_config.mjs +0 -19
  115. package/src/README.md +0 -17
  116. package/src/application/session.mjs +0 -435
  117. package/src/baseline/artifact.mjs +0 -684
  118. package/src/baseline/builder.mjs +0 -164
  119. package/src/baseline/input_discovery.mjs +0 -155
  120. package/src/baseline/policy_review.mjs +0 -92
  121. package/src/baseline/preparation.mjs +0 -323
  122. package/src/baseline/privacy_engine.mjs +0 -1399
  123. package/src/baseline/privacy_structured_json.mjs +0 -554
  124. package/src/baseline/sanitization_policy.mjs +0 -299
  125. package/src/baseline/schema_snapshot.mjs +0 -113
  126. package/src/cli/arguments.mjs +0 -126
  127. package/src/cli/guided.mjs +0 -814
  128. package/src/cli/init_command.mjs +0 -94
  129. package/src/cli/rehearsal.mjs +0 -972
  130. package/src/cli/renderers.mjs +0 -638
  131. package/src/cli/runtime_commands.mjs +0 -707
  132. package/src/cli/source_commands.mjs +0 -355
  133. package/src/cli/terminal.mjs +0 -275
  134. package/src/identity/claim.mjs +0 -1165
  135. package/src/identity/provider_configuration.mjs +0 -177
  136. package/src/identity/storage.mjs +0 -165
  137. package/src/project/configuration.mjs +0 -1337
  138. package/src/project/install_scaffold.mjs +0 -325
  139. package/src/project/setup.mjs +0 -474
  140. package/src/project/support_report.mjs +0 -105
  141. package/src/runtime/cleanup.mjs +0 -381
  142. package/src/runtime/migration_history.mjs +0 -220
  143. package/src/runtime/plan.mjs +0 -722
  144. package/src/runtime/policy.mjs +0 -438
  145. package/src/runtime/restore.mjs +0 -337
  146. package/src/runtime/service_environment.mjs +0 -82
  147. package/src/runtime/topology.mjs +0 -177
  148. package/src/shared/diagnostics.mjs +0 -207
  149. package/src/shared/node_runtime.mjs +0 -31
  150. package/src/shared/operation_guard.mjs +0 -162
  151. package/src/shared/process_environment.mjs +0 -68
  152. package/src/source/access.mjs +0 -798
  153. package/src/source/asset_transfer.mjs +0 -259
  154. package/src/source/baseline.mjs +0 -482
  155. package/src/source/external_reader.mjs +0 -368
  156. package/src/source/postgresql_access.mjs +0 -638
  157. package/src/source/privacy_paths.mjs +0 -38
  158. package/src/targets/postgresql.mjs +0 -808
  159. package/src/targets/supabase.mjs +0 -840
  160. package/src/targets/supabase_environment.mjs +0 -229
  161. package/src/targets/target.mjs +0 -66
  162. /package/{src/project → types}/configuration.d.mts +0 -0
@@ -1,707 +0,0 @@
1
- /**
2
- * CLI orchestration for one or more isolated database runtimes and their
3
- * project-owned proof commands.
4
- */
5
-
6
- import { spawnSync } from "node:child_process";
7
- import { access } from "node:fs/promises";
8
- import { join } from "node:path";
9
- import { performance } from "node:perf_hooks";
10
- import * as prompts from "@clack/prompts";
11
- import {
12
- holdApplicationSession,
13
- startApplicationSession,
14
- summarizeProjectCommandFailure,
15
- } from "../application/session.mjs";
16
- import { loadRehearsalConfig } from "../project/configuration.mjs";
17
- import { buildRehearsalPlan, runRehearsalDoctor } from "../runtime/plan.mjs";
18
- import {
19
- inspectRuntimeTopologyCandidates,
20
- loadRuntimeTopology,
21
- } from "../runtime/topology.mjs";
22
- import {
23
- RehearsalError,
24
- normalizeRehearsalError,
25
- redactDiagnosticValue,
26
- } from "../shared/diagnostics.mjs";
27
- import { assertInstalledPackageFingerprint } from "../shared/operation_guard.mjs";
28
- import { buildRuntimeManagerArguments } from "../targets/target.mjs";
29
- import {
30
- formatDuration,
31
- isHumanTerminal,
32
- promptForConfirmation,
33
- terminalStyle,
34
- useStyledPrompts,
35
- } from "./terminal.mjs";
36
-
37
- export const buildRuntimeStackPlan = async (planOptions) => {
38
- const topology = await loadRuntimeTopology(planOptions);
39
- if (topology.targets.length === 1) return buildRehearsalPlan(planOptions);
40
- const targets = [];
41
- for (const target of topology.targets) {
42
- targets.push({
43
- name: target.name,
44
- declaration: target.declaration,
45
- plan: await buildRehearsalPlan({
46
- projectRoot: topology.projectRoot,
47
- configPath: target.configPath,
48
- }),
49
- });
50
- }
51
- const candidates = await inspectRuntimeTopologyCandidates(planOptions);
52
- return {
53
- targets,
54
- migrations: {
55
- candidateCount: candidates.candidateCount,
56
- candidateSha256: candidates.candidateSha256,
57
- },
58
- execution: [
59
- "verify every immutable baseline and migration prefix",
60
- "restore and migrate the primary runtime",
61
- "restore and migrate each dependent runtime in declared order",
62
- ...topology.dependents
63
- .filter((target) => target.declaration.prepareCommand)
64
- .map(
65
- (target) =>
66
- `run ${target.name} preparation: ${target.declaration.prepareCommand}`,
67
- ),
68
- ...topology.dependents.map(
69
- (target) =>
70
- `run ${target.name} proof: ${target.config.application.proofCommand}`,
71
- ),
72
- `run primary proof: ${topology.primary.config.application.proofCommand}`,
73
- ],
74
- guarantee:
75
- "Every database target is isolated by its own project ID, ports, artifact directory, and local-only safety policy. No production resources will be contacted.",
76
- };
77
- };
78
-
79
- export const runRuntimeStackDoctor = async (planOptions) => {
80
- let topology;
81
- try {
82
- topology = await loadRuntimeTopology(planOptions);
83
- } catch (error) {
84
- if (
85
- String(error?.message ?? error).startsWith(
86
- "No Rehearsal configuration was found",
87
- )
88
- ) {
89
- return runRehearsalDoctor(planOptions);
90
- }
91
- return {
92
- state: "NOT READY",
93
- checks: [
94
- {
95
- id: "runtime-topology",
96
- label: "Dependent runtime isolation",
97
- status: "fail",
98
- detail: redactDiagnosticValue(String(error?.message ?? error)),
99
- remediation:
100
- "Give every target a readable config with unique ports, project ID, environment file, and .rehearsal artifact directory.",
101
- },
102
- ],
103
- ambientHostedVariables: {
104
- presentButQuarantined: [],
105
- note: "Topology validation stopped before any runtime action.",
106
- },
107
- };
108
- }
109
- if (topology.targets.length === 1) return runRehearsalDoctor(planOptions);
110
- const reports = [];
111
- for (const target of topology.targets) {
112
- reports.push({
113
- target: target.name,
114
- report: await runRehearsalDoctor({
115
- projectRoot: topology.projectRoot,
116
- configPath: target.configPath,
117
- }),
118
- });
119
- }
120
- return {
121
- state: reports.every(({ report }) => report.state === "READY")
122
- ? "READY"
123
- : "NOT READY",
124
- checks: reports.flatMap(({ target, report }) =>
125
- report.checks.map((check) => ({
126
- ...check,
127
- id: `${target}:${check.id}`,
128
- label: `${target}: ${check.label}`,
129
- })),
130
- ),
131
- ambientHostedVariables: {
132
- presentButQuarantined: [
133
- ...new Set(
134
- reports.flatMap(
135
- ({ report }) =>
136
- report.ambientHostedVariables?.presentButQuarantined ?? [],
137
- ),
138
- ),
139
- ],
140
- note: "Blocked ambient values are not inherited by Rehearsal child processes.",
141
- },
142
- };
143
- };
144
-
145
- export const topologyCandidateSummary = async (planOptions) => {
146
- const inspection = await inspectRuntimeTopologyCandidates(planOptions);
147
- const targets = inspection.targets.map((target) => ({
148
- name: target.name,
149
- configPath: target.configPath,
150
- baselineGenerationId: target.summary.baselineGenerationId,
151
- candidateSha256: target.summary.candidateSha256,
152
- candidateCount: target.summary.candidateCount,
153
- candidates: target.summary.candidates,
154
- }));
155
- return {
156
- baselineGenerationId: targets[0].baselineGenerationId,
157
- candidateSha256: inspection.candidateSha256,
158
- candidateCount: inspection.candidateCount,
159
- candidates: targets.flatMap((target) =>
160
- target.candidates.map((candidate) => ({
161
- ...candidate,
162
- target: target.name,
163
- })),
164
- ),
165
- targets,
166
- topology: inspection.topology,
167
- };
168
- };
169
-
170
- const parseCommand = (source) => {
171
- const values = [];
172
- let current = "";
173
- let quote = null;
174
- for (const character of source) {
175
- if (quote) {
176
- if (character === quote) quote = null;
177
- else current += character;
178
- } else if (character === '"' || character === "'") quote = character;
179
- else if (/\s/u.test(character)) {
180
- if (current) values.push(current);
181
- current = "";
182
- } else current += character;
183
- }
184
- if (quote)
185
- throw new Error("Application proof command contains an unmatched quote.");
186
- if (current) values.push(current);
187
- if (values.length === 0)
188
- throw new Error("Application proof command is empty.");
189
- return values;
190
- };
191
-
192
- const projectCommandEnvironment = (additional = {}) => ({
193
- ...Object.fromEntries(
194
- [
195
- "CI",
196
- "COLORTERM",
197
- "FORCE_COLOR",
198
- "HOME",
199
- "LANG",
200
- "LC_ALL",
201
- "NO_COLOR",
202
- "PATH",
203
- "SHELL",
204
- "TERM",
205
- "TMPDIR",
206
- "USER",
207
- ].flatMap((key) =>
208
- process.env[key] === undefined ? [] : [[key, process.env[key]]],
209
- ),
210
- ),
211
- ...additional,
212
- });
213
-
214
- const RUNTIME_STARTUP_FAILURE_PATTERN =
215
- /container\b.*\b(?:is not ready|starting|unhealthy)|health(?:check)?\b.*\b(?:failed|starting|unhealthy)|out of memory|\boom\b|no space left|resource temporarily unavailable/iu;
216
-
217
- export const classifyManagerFailure = ({
218
- action,
219
- output,
220
- inferredCategory,
221
- }) => {
222
- if (RUNTIME_STARTUP_FAILURE_PATTERN.test(output)) {
223
- return "runtime_dependency_failure";
224
- }
225
- if (
226
- action === "migrate" ||
227
- (action === "run" &&
228
- /migrateRuntime|candidate migration|migration up/iu.test(output))
229
- ) {
230
- return "migration_candidate_failure";
231
- }
232
- return inferredCategory;
233
- };
234
-
235
- export const parseDockerCapacity = (output) => {
236
- const [cpuText, memoryText, runningText, totalText] = output
237
- .trim()
238
- .split(/\s+/u);
239
- const values = [cpuText, memoryText, runningText, totalText].map(Number);
240
- if (!values.every(Number.isFinite)) return null;
241
- const [cpus, memoryBytes, runningContainers, containers] = values;
242
- return { cpus, memoryBytes, runningContainers, containers };
243
- };
244
-
245
- export const parseColimaMemory = (output) => {
246
- const line = output
247
- .split("\n")
248
- .map((entry) => entry.trim())
249
- .find((entry) => entry.startsWith("Mem:"));
250
- if (!line) return null;
251
- const columns = line.split(/\s+/u);
252
- const totalBytes = Number(columns[1]);
253
- const availableBytes = Number(columns[6]);
254
- if (![totalBytes, availableBytes].every(Number.isFinite)) return null;
255
- return { totalBytes, availableBytes };
256
- };
257
-
258
- const diagnosticCommand = (command, args) =>
259
- spawnSync(command, args, {
260
- encoding: "utf8",
261
- env: projectCommandEnvironment(),
262
- stdio: ["ignore", "pipe", "ignore"],
263
- timeout: 5_000,
264
- });
265
-
266
- const inspectContainerCapacity = () => {
267
- const docker = diagnosticCommand("docker", [
268
- "info",
269
- "--format",
270
- "{{.NCPU}} {{.MemTotal}} {{.ContainersRunning}} {{.Containers}}",
271
- ]);
272
- const capacity =
273
- docker.status === 0 ? parseDockerCapacity(docker.stdout) : null;
274
- if (!capacity) return undefined;
275
-
276
- const context = diagnosticCommand("docker", ["context", "show"]);
277
- if (context.status !== 0 || context.stdout.trim() !== "colima") {
278
- return { engine: "docker-compatible", ...capacity };
279
- }
280
- const memory = diagnosticCommand("colima", ["ssh", "--", "free", "-b"]);
281
- return {
282
- engine: "colima",
283
- ...capacity,
284
- ...(memory.status === 0
285
- ? { runtimeMemory: parseColimaMemory(memory.stdout) ?? undefined }
286
- : {}),
287
- };
288
- };
289
-
290
- export const topologyCommandEnvironment = (topology) =>
291
- Object.fromEntries(
292
- topology.targets.map((target) => [
293
- target.primary
294
- ? "REHEARSAL_PRIMARY_ENV_FILE"
295
- : `REHEARSAL_DEPENDENT_${target.name.toUpperCase().replaceAll("-", "_")}_ENV_FILE`,
296
- target.paths.applicationEnvironment,
297
- ]),
298
- );
299
-
300
- export const topologyEnvironmentFiles = (topology) =>
301
- Object.fromEntries(
302
- topology.targets.map((target) => [
303
- target.primary ? "primary" : target.name,
304
- target.paths.applicationEnvironment,
305
- ]),
306
- );
307
-
308
- const runProjectCommand = ({
309
- source,
310
- configuredRoot,
311
- label,
312
- project,
313
- environment = {},
314
- }) => {
315
- const [command, ...args] = parseCommand(source);
316
- const result = spawnSync(command, args, {
317
- cwd: configuredRoot,
318
- encoding: "utf8",
319
- env: projectCommandEnvironment(environment),
320
- stdio: ["ignore", "pipe", "pipe"],
321
- maxBuffer: 16 * 1024 * 1024,
322
- });
323
- if (result.error || result.status !== 0) {
324
- throw new RehearsalError({
325
- category: "application_proof_failure",
326
- code: "PROJECT_COMMAND_FAILED",
327
- message: `The configured ${label} did not pass.`,
328
- expected: `${source} exits successfully`,
329
- actual: summarizeProjectCommandFailure(result),
330
- context: { project, label },
331
- refused:
332
- "Rehearsal did not report the local runtime stack as fully verified.",
333
- suggestions: [
334
- "Run the command directly in the same project and correct the failure.",
335
- ],
336
- cause: result.error,
337
- });
338
- }
339
- return {
340
- command: source,
341
- output: redactDiagnosticValue(String(result.stdout ?? "").trim()),
342
- };
343
- };
344
-
345
- export const createRuntimeCommands = ({
346
- packageRoot,
347
- projectRoot,
348
- managerPath,
349
- getActivePackageFingerprint,
350
- }) => {
351
- const runManager = ({
352
- action,
353
- flags,
354
- configPath = flags.configPath,
355
- confirmation = flags.confirmation,
356
- targetName,
357
- }) => {
358
- const fingerprint = getActivePackageFingerprint();
359
- if (fingerprint) {
360
- assertInstalledPackageFingerprint({
361
- packageRoot,
362
- expected: fingerprint,
363
- });
364
- }
365
- const args = buildRuntimeManagerArguments({
366
- managerPath,
367
- action,
368
- configPath,
369
- confirmation,
370
- });
371
- const runtimeStartedMs = performance.now();
372
- const actionDescription = {
373
- run: "Restoring the baseline and applying reviewed migrations",
374
- start: "Starting the disposable local runtime",
375
- migrate: "Applying reviewed migrations to the current runtime",
376
- reset: "Restoring the immutable baseline",
377
- status: "Inspecting the disposable local runtime",
378
- stop: "Stopping the disposable local runtime",
379
- discard: "Removing the disposable local runtime",
380
- verify: "Verifying the current local runtime",
381
- }[action];
382
- const describedAction = targetName
383
- ? `${actionDescription} for ${targetName}`
384
- : actionDescription;
385
- const spinner = useStyledPrompts(flags) ? prompts.spinner() : null;
386
- if (spinner) {
387
- spinner.start(`${describedAction}. This can take a moment.`);
388
- } else if (isHumanTerminal(flags)) {
389
- console.log(
390
- `\n${terminalStyle(flags, "1;36", action === "run" ? "REHEARSING" : "REHEARSAL")}\n${terminalStyle(flags, "36", "→")} ${describedAction}. This can take a moment.`,
391
- );
392
- }
393
- const result = spawnSync(process.execPath, args, {
394
- cwd: projectRoot,
395
- encoding: "utf8",
396
- env: projectCommandEnvironment(),
397
- stdio: ["inherit", "pipe", "pipe"],
398
- maxBuffer: 16 * 1024 * 1024,
399
- });
400
- const currentFingerprint = getActivePackageFingerprint();
401
- if (currentFingerprint) {
402
- assertInstalledPackageFingerprint({
403
- packageRoot,
404
- expected: currentFingerprint,
405
- });
406
- }
407
- if (result.error) {
408
- if (spinner) spinner.stop(`${describedAction} did not complete.`);
409
- throw result.error;
410
- }
411
- if (result.status !== 0) {
412
- if (spinner) spinner.stop(`${describedAction} did not complete.`);
413
- const completeOutput = [
414
- String(result.stdout ?? ""),
415
- String(result.stderr ?? ""),
416
- ]
417
- .filter(Boolean)
418
- .join("\n")
419
- .trim();
420
- const inferred = normalizeRehearsalError(
421
- new Error(completeOutput || `Rehearsal ${action} failed.`),
422
- );
423
- const category = classifyManagerFailure({
424
- action,
425
- output: completeOutput,
426
- inferredCategory: inferred.category,
427
- });
428
- const startupFailure = category === "runtime_dependency_failure";
429
- const engineCapacity = startupFailure
430
- ? inspectContainerCapacity()
431
- : undefined;
432
- const conciseOutput = completeOutput
433
- .replaceAll(String.fromCodePoint(27), "")
434
- .split("\n")
435
- .map((line) => line.trim())
436
- .filter(Boolean)
437
- .slice(-8)
438
- .join("\n")
439
- .slice(-2_000);
440
- throw new RehearsalError({
441
- category,
442
- code: `MANAGER_${action.toUpperCase()}_FAILED`,
443
- message: `The local Rehearsal ${action} operation did not complete.`,
444
- expected: "the isolated runtime operation to finish and verify",
445
- actual: conciseOutput || `exit ${result.status}`,
446
- context: { action, ...(engineCapacity ? { engineCapacity } : {}) },
447
- refused: "The runtime was not reported as trusted.",
448
- suggestions: [
449
- "Review the concise failure above, then rerun rehearsal doctor before retrying.",
450
- ...(startupFailure
451
- ? [
452
- "Stop unrelated local stacks or increase Docker/Colima capacity, then retry. Rehearsal will not change engine settings or bypass service health checks.",
453
- "Run docker stats --no-stream to identify active containers using the shared engine.",
454
- ]
455
- : []),
456
- "Use --debug only when the safe diagnostic detail is needed.",
457
- ],
458
- cause: new Error(completeOutput || `exit ${result.status}`),
459
- });
460
- }
461
- const durationMs = performance.now() - runtimeStartedMs;
462
- if (spinner) {
463
- spinner.stop(
464
- `${describedAction} completed in ${formatDuration(durationMs)}.`,
465
- );
466
- } else if (isHumanTerminal(flags)) {
467
- console.log(
468
- `${terminalStyle(flags, "32", "✓")} Local runtime step completed in ${formatDuration(durationMs)}.`,
469
- );
470
- }
471
- return {
472
- action,
473
- target: targetName ?? "primary",
474
- durationMs: Math.round(durationMs * 100) / 100,
475
- output: redactDiagnosticValue(String(result.stdout ?? "").trim()),
476
- };
477
- };
478
-
479
- const runRuntimeStack = async ({ command, flags, planOptions }) => {
480
- const topology =
481
- flags.runtimeTopology ?? (await loadRuntimeTopology(planOptions));
482
- const reverse = ["stop", "discard"].includes(command);
483
- const targets = reverse
484
- ? [...topology.targets].reverse()
485
- : topology.targets;
486
- const results = targets.map((target) =>
487
- runManager({
488
- action: command,
489
- flags,
490
- configPath: target.configPath,
491
- confirmation: flags.targetConfirmations?.get(target.configPath),
492
- targetName: topology.targets.length > 1 ? target.name : undefined,
493
- }),
494
- );
495
- if (results.length === 1) return { topology, runtime: results[0] };
496
- return {
497
- topology,
498
- runtime: {
499
- action: command,
500
- durationMs:
501
- Math.round(
502
- results.reduce((total, result) => total + result.durationMs, 0) *
503
- 100,
504
- ) / 100,
505
- output: results
506
- .map((result) => `[${result.target}]\n${result.output}`)
507
- .join("\n\n"),
508
- targets: results,
509
- },
510
- };
511
- };
512
-
513
- const runApplicationProof = async (planOptions, options = {}) => {
514
- const { config, projectRoot: configuredRoot } =
515
- await loadRehearsalConfig(planOptions);
516
- return runProjectCommand({
517
- source: config.application.proofCommand,
518
- configuredRoot,
519
- label: options.targetName
520
- ? `application proof for ${options.targetName}`
521
- : "application proof",
522
- project: config.project.name,
523
- environment: options.environment,
524
- });
525
- };
526
-
527
- const prepareDependentTargets = ({ topology, environment }) =>
528
- topology.dependents.flatMap((target) => {
529
- if (!target.declaration.prepareCommand) return [];
530
- return [
531
- {
532
- target: target.name,
533
- ...runProjectCommand({
534
- source: target.declaration.prepareCommand,
535
- configuredRoot: topology.projectRoot,
536
- label: `preparation command for ${target.name}`,
537
- project: topology.primary.config.project.name,
538
- environment,
539
- }),
540
- },
541
- ];
542
- });
543
-
544
- const proveRuntimeStack = async ({ topology, planOptions, environment }) => {
545
- const dependentProofs = [];
546
- for (const target of topology.dependents) {
547
- dependentProofs.push({
548
- target: target.name,
549
- ...(await runApplicationProof(
550
- { ...planOptions, configPath: target.configPath },
551
- { targetName: target.name, environment },
552
- )),
553
- });
554
- }
555
- const applicationProof = await runApplicationProof(planOptions, {
556
- targetName: topology.targets.length > 1 ? "primary" : undefined,
557
- environment,
558
- });
559
- return { applicationProof, dependentProofs };
560
- };
561
-
562
- const openRuntimeApplication = async ({
563
- flags,
564
- planOptions,
565
- onReady = () => undefined,
566
- }) => {
567
- const topology = await loadRuntimeTopology(planOptions);
568
- const application = topology.primary.config.application;
569
- if (!application.readiness) {
570
- throw new Error(
571
- "rehearsal open requires config.application.readiness so it can confirm the local application is usable.",
572
- );
573
- }
574
- const runtimeReceipts = await Promise.all(
575
- topology.targets.map((target) =>
576
- access(join(target.paths.runtimeWorkdir, "baseline.json"))
577
- .then(() => true)
578
- .catch((error) => {
579
- if (error?.code === "ENOENT") return false;
580
- throw error;
581
- }),
582
- ),
583
- );
584
- if (runtimeReceipts.some((present) => !present)) {
585
- throw new Error(
586
- "rehearsal open requires an existing verified runtime. Run rehearsal run first.",
587
- );
588
- }
589
- flags.runtimeTopology = topology;
590
- const started = await runRuntimeStack({
591
- command: "start",
592
- flags,
593
- planOptions,
594
- });
595
- const verified = await runRuntimeStack({
596
- command: "verify",
597
- flags,
598
- planOptions,
599
- });
600
- const preparations = prepareDependentTargets({
601
- topology,
602
- environment: topologyCommandEnvironment(topology),
603
- });
604
- const session = await startApplicationSession({
605
- command: application.startCommand,
606
- cwd: topology.projectRoot,
607
- files: topologyEnvironmentFiles(topology),
608
- mappings: application.environmentVariables,
609
- readiness: application.readiness,
610
- });
611
- const closed = await holdApplicationSession({ session, onReady });
612
- return {
613
- action: "open",
614
- started: started.runtime,
615
- verified: verified.runtime,
616
- preparations,
617
- application: {
618
- command: application.startCommand,
619
- readiness: session.ready,
620
- stoppedBy: closed.signal,
621
- diagnostics: closed.diagnostics,
622
- },
623
- };
624
- };
625
-
626
- const prepareCandidateConfirmation = async ({
627
- command,
628
- flags,
629
- planOptions,
630
- }) => {
631
- if (!["run", "migrate"].includes(command) || flags.dryRun) return true;
632
- const summary = await topologyCandidateSummary(planOptions);
633
- flags.runtimeTopology = summary.topology;
634
- flags.targetConfirmations = new Map(
635
- summary.targets.map((target) => [
636
- target.configPath,
637
- target.candidateSha256,
638
- ]),
639
- );
640
- if (summary.candidateCount === 0) return true;
641
- if (flags.confirmation === summary.candidateSha256) return true;
642
- const interactive =
643
- !flags.json && process.stdin.isTTY && process.stdout.isTTY;
644
- if (!interactive || flags.confirmation) {
645
- throw new RehearsalError({
646
- category: "migration_candidate_failure",
647
- code: "CANDIDATE_CONFIRMATION_REQUIRED",
648
- message: flags.confirmation
649
- ? "The candidate migration confirmation does not match the current candidate set."
650
- : "Candidate migration confirmation is required in noninteractive use.",
651
- expected: summary.candidateSha256,
652
- actual: flags.confirmation ?? "missing",
653
- context: {
654
- command,
655
- candidateCount: summary.candidateCount,
656
- candidates: summary.candidates.map(
657
- (migration) => `${migration.target}: ${migration.filename}`,
658
- ),
659
- },
660
- refused:
661
- "Rehearsal did not reset, start, or modify the disposable runtime.",
662
- suggestions: [
663
- "Run rehearsal candidates, review every file, then pass the current digest with --confirm-candidates=.",
664
- ],
665
- });
666
- }
667
- console.log(
668
- [
669
- "",
670
- terminalStyle(
671
- flags,
672
- "1",
673
- `${summary.candidateCount} candidate migration${summary.candidateCount === 1 ? "" : "s"}`,
674
- ),
675
- "",
676
- ...summary.candidates.map((migration) =>
677
- summary.targets.length > 1
678
- ? ` → ${migration.target}: ${migration.filename}`
679
- : ` → ${migration.filename}`,
680
- ),
681
- "",
682
- `Candidate set: ${summary.candidateSha256.slice(0, 12)}`,
683
- "",
684
- `Rehearsal will apply only this exact set to ${summary.targets.length === 1 ? "the disposable local runtime" : "the isolated local runtime stack"}.`,
685
- ].join("\n"),
686
- );
687
- const accepted = await promptForConfirmation(
688
- `Apply exactly ${summary.candidateCount === 1 ? "this migration" : "these migrations"}?`,
689
- flags,
690
- );
691
- if (!accepted) {
692
- console.log("No changes made.");
693
- return false;
694
- }
695
- flags.confirmation = summary.candidateSha256;
696
- return true;
697
- };
698
-
699
- return {
700
- openRuntimeApplication,
701
- prepareCandidateConfirmation,
702
- prepareDependentTargets,
703
- proveRuntimeStack,
704
- runManager,
705
- runRuntimeStack,
706
- };
707
- };