@rehearsal-db/core 0.1.0-beta.7 → 0.1.0-beta.9

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 (64) hide show
  1. package/CHANGELOG.md +77 -1
  2. package/COMPATIBILITY.md +15 -3
  3. package/README.md +31 -13
  4. package/docs/README.md +29 -0
  5. package/docs/adapters.md +15 -6
  6. package/docs/architecture.md +58 -0
  7. package/docs/baselines.md +22 -1
  8. package/docs/commands.md +81 -14
  9. package/docs/configuration.md +144 -11
  10. package/docs/getting-started.md +12 -2
  11. package/docs/glossary.md +4 -0
  12. package/docs/production-source.md +176 -33
  13. package/docs/roadmap.md +30 -28
  14. package/docs/runtime-policies.md +191 -0
  15. package/docs/sanitization.md +24 -8
  16. package/docs/security-model.md +29 -8
  17. package/docs/standalone-workflow.md +107 -0
  18. package/docs/troubleshooting.md +8 -0
  19. package/package.json +25 -22
  20. package/scripts/runtime/manage_database.mjs +18 -0
  21. package/src/README.md +17 -0
  22. package/src/application/session.mjs +313 -0
  23. package/{scripts/lib/rehearsal/baseline_artifact.mjs → src/baseline/artifact.mjs} +110 -14
  24. package/{scripts/lib/rehearsal/baseline_builder.mjs → src/baseline/builder.mjs} +8 -4
  25. package/{scripts/lib/rehearsal/baseline_preparation.mjs → src/baseline/preparation.mjs} +11 -4
  26. package/src/baseline/privacy_engine.mjs +413 -0
  27. package/{scripts/lib/rehearsal → src/baseline}/sanitization_policy.mjs +27 -7
  28. package/{scripts/lib/rehearsal → src/baseline}/schema_snapshot.mjs +1 -1
  29. package/src/cli/arguments.mjs +120 -0
  30. package/src/cli/guided.mjs +807 -0
  31. package/src/cli/rehearsal.mjs +933 -0
  32. package/src/cli/renderers.mjs +584 -0
  33. package/src/cli/runtime_commands.mjs +553 -0
  34. package/src/cli/source_commands.mjs +326 -0
  35. package/src/cli/terminal.mjs +275 -0
  36. package/src/identity/claim.mjs +975 -0
  37. package/src/identity/storage.mjs +165 -0
  38. package/{scripts/lib/rehearsal → src/project}/configuration.d.mts +34 -0
  39. package/{scripts/lib/rehearsal → src/project}/configuration.mjs +353 -3
  40. package/{scripts/lib/rehearsal → src/project}/setup.mjs +1 -1
  41. package/{scripts/lib/rehearsal → src/project}/support_report.mjs +5 -2
  42. package/{scripts/lib/rehearsal → src/runtime}/cleanup.mjs +3 -3
  43. package/{scripts/lib/rehearsal → src/runtime}/plan.mjs +5 -5
  44. package/src/runtime/policy.mjs +438 -0
  45. package/{scripts/lib/rehearsal/runtime_restore.mjs → src/runtime/restore.mjs} +40 -25
  46. package/src/runtime/topology.mjs +177 -0
  47. package/{scripts/lib/rehearsal → src/shared}/diagnostics.mjs +1 -1
  48. package/src/shared/operation_guard.mjs +162 -0
  49. package/{scripts/lib/rehearsal → src/shared}/process_environment.mjs +5 -1
  50. package/src/source/access.mjs +578 -0
  51. package/src/source/asset_transfer.mjs +177 -0
  52. package/src/source/baseline.mjs +446 -0
  53. package/src/source/postgresql_access.mjs +480 -0
  54. package/{scripts/lib/runtime/postgresql_runtime.mjs → src/targets/postgresql.mjs} +49 -13
  55. package/{scripts/lib/runtime/supabase_runtime.mjs → src/targets/supabase.mjs} +54 -16
  56. package/{scripts/lib/environment/local_supabase.mjs → src/targets/supabase_environment.mjs} +52 -20
  57. package/{scripts/lib/runtime/runtime_target.mjs → src/targets/target.mjs} +27 -2
  58. package/scripts/operations/database/manage_rehearsal_database.mjs +0 -11
  59. package/scripts/operations/rehearsal/rehearsal_cli.mjs +0 -2166
  60. /package/{scripts/lib/rehearsal → src/baseline}/input_discovery.mjs +0 -0
  61. /package/{scripts/lib/rehearsal → src/baseline}/policy_review.mjs +0 -0
  62. /package/{scripts/lib/rehearsal → src/runtime}/migration_history.mjs +0 -0
  63. /package/{scripts/lib/rehearsal → src/runtime}/service_environment.mjs +0 -0
  64. /package/{scripts/lib/rehearsal → src/shared}/human_output.mjs +0 -0
@@ -0,0 +1,553 @@
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 { performance } from "node:perf_hooks";
8
+ import * as prompts from "@clack/prompts";
9
+ import { summarizeProjectCommandFailure } from "../application/session.mjs";
10
+ import { loadRehearsalConfig } from "../project/configuration.mjs";
11
+ import { buildRehearsalPlan, runRehearsalDoctor } from "../runtime/plan.mjs";
12
+ import {
13
+ inspectRuntimeTopologyCandidates,
14
+ loadRuntimeTopology,
15
+ } from "../runtime/topology.mjs";
16
+ import {
17
+ RehearsalError,
18
+ normalizeRehearsalError,
19
+ redactDiagnosticValue,
20
+ } from "../shared/diagnostics.mjs";
21
+ import { assertInstalledPackageFingerprint } from "../shared/operation_guard.mjs";
22
+ import { buildRuntimeManagerArguments } from "../targets/target.mjs";
23
+ import {
24
+ formatDuration,
25
+ isHumanTerminal,
26
+ promptForConfirmation,
27
+ terminalStyle,
28
+ useStyledPrompts,
29
+ } from "./terminal.mjs";
30
+
31
+ export const buildRuntimeStackPlan = async (planOptions) => {
32
+ const topology = await loadRuntimeTopology(planOptions);
33
+ if (topology.targets.length === 1) return buildRehearsalPlan(planOptions);
34
+ const targets = [];
35
+ for (const target of topology.targets) {
36
+ targets.push({
37
+ name: target.name,
38
+ declaration: target.declaration,
39
+ plan: await buildRehearsalPlan({
40
+ projectRoot: topology.projectRoot,
41
+ configPath: target.configPath,
42
+ }),
43
+ });
44
+ }
45
+ const candidates = await inspectRuntimeTopologyCandidates(planOptions);
46
+ return {
47
+ targets,
48
+ migrations: {
49
+ candidateCount: candidates.candidateCount,
50
+ candidateSha256: candidates.candidateSha256,
51
+ },
52
+ execution: [
53
+ "verify every immutable baseline and migration prefix",
54
+ "restore and migrate the primary runtime",
55
+ "restore and migrate each dependent runtime in declared order",
56
+ ...topology.dependents
57
+ .filter((target) => target.declaration.prepareCommand)
58
+ .map(
59
+ (target) =>
60
+ `run ${target.name} preparation: ${target.declaration.prepareCommand}`,
61
+ ),
62
+ ...topology.dependents.map(
63
+ (target) =>
64
+ `run ${target.name} proof: ${target.config.application.proofCommand}`,
65
+ ),
66
+ `run primary proof: ${topology.primary.config.application.proofCommand}`,
67
+ ],
68
+ guarantee:
69
+ "Every database target is isolated by its own project ID, ports, artifact directory, and local-only safety policy. No production resources will be contacted.",
70
+ };
71
+ };
72
+
73
+ export const runRuntimeStackDoctor = async (planOptions) => {
74
+ let topology;
75
+ try {
76
+ topology = await loadRuntimeTopology(planOptions);
77
+ } catch (error) {
78
+ if (
79
+ String(error?.message ?? error).startsWith(
80
+ "No Rehearsal configuration was found",
81
+ )
82
+ ) {
83
+ return runRehearsalDoctor(planOptions);
84
+ }
85
+ return {
86
+ state: "NOT READY",
87
+ checks: [
88
+ {
89
+ id: "runtime-topology",
90
+ label: "Dependent runtime isolation",
91
+ status: "fail",
92
+ detail: redactDiagnosticValue(String(error?.message ?? error)),
93
+ remediation:
94
+ "Give every target a readable config with unique ports, project ID, environment file, and .rehearsal artifact directory.",
95
+ },
96
+ ],
97
+ ambientHostedVariables: {
98
+ presentButQuarantined: [],
99
+ note: "Topology validation stopped before any runtime action.",
100
+ },
101
+ };
102
+ }
103
+ if (topology.targets.length === 1) return runRehearsalDoctor(planOptions);
104
+ const reports = [];
105
+ for (const target of topology.targets) {
106
+ reports.push({
107
+ target: target.name,
108
+ report: await runRehearsalDoctor({
109
+ projectRoot: topology.projectRoot,
110
+ configPath: target.configPath,
111
+ }),
112
+ });
113
+ }
114
+ return {
115
+ state: reports.every(({ report }) => report.state === "READY")
116
+ ? "READY"
117
+ : "NOT READY",
118
+ checks: reports.flatMap(({ target, report }) =>
119
+ report.checks.map((check) => ({
120
+ ...check,
121
+ id: `${target}:${check.id}`,
122
+ label: `${target}: ${check.label}`,
123
+ })),
124
+ ),
125
+ ambientHostedVariables: {
126
+ presentButQuarantined: [
127
+ ...new Set(
128
+ reports.flatMap(
129
+ ({ report }) =>
130
+ report.ambientHostedVariables?.presentButQuarantined ?? [],
131
+ ),
132
+ ),
133
+ ],
134
+ note: "Blocked ambient values are not inherited by Rehearsal child processes.",
135
+ },
136
+ };
137
+ };
138
+
139
+ export const topologyCandidateSummary = async (planOptions) => {
140
+ const inspection = await inspectRuntimeTopologyCandidates(planOptions);
141
+ const targets = inspection.targets.map((target) => ({
142
+ name: target.name,
143
+ configPath: target.configPath,
144
+ baselineGenerationId: target.summary.baselineGenerationId,
145
+ candidateSha256: target.summary.candidateSha256,
146
+ candidateCount: target.summary.candidateCount,
147
+ candidates: target.summary.candidates,
148
+ }));
149
+ return {
150
+ baselineGenerationId: targets[0].baselineGenerationId,
151
+ candidateSha256: inspection.candidateSha256,
152
+ candidateCount: inspection.candidateCount,
153
+ candidates: targets.flatMap((target) =>
154
+ target.candidates.map((candidate) => ({
155
+ ...candidate,
156
+ target: target.name,
157
+ })),
158
+ ),
159
+ targets,
160
+ topology: inspection.topology,
161
+ };
162
+ };
163
+
164
+ const parseCommand = (source) => {
165
+ const values = [];
166
+ let current = "";
167
+ let quote = null;
168
+ for (const character of source) {
169
+ if (quote) {
170
+ if (character === quote) quote = null;
171
+ else current += character;
172
+ } else if (character === '"' || character === "'") quote = character;
173
+ else if (/\s/u.test(character)) {
174
+ if (current) values.push(current);
175
+ current = "";
176
+ } else current += character;
177
+ }
178
+ if (quote)
179
+ throw new Error("Application proof command contains an unmatched quote.");
180
+ if (current) values.push(current);
181
+ if (values.length === 0)
182
+ throw new Error("Application proof command is empty.");
183
+ return values;
184
+ };
185
+
186
+ const projectCommandEnvironment = (additional = {}) => ({
187
+ ...Object.fromEntries(
188
+ [
189
+ "CI",
190
+ "COLORTERM",
191
+ "FORCE_COLOR",
192
+ "HOME",
193
+ "LANG",
194
+ "LC_ALL",
195
+ "NO_COLOR",
196
+ "PATH",
197
+ "SHELL",
198
+ "TERM",
199
+ "TMPDIR",
200
+ "USER",
201
+ ].flatMap((key) =>
202
+ process.env[key] === undefined ? [] : [[key, process.env[key]]],
203
+ ),
204
+ ),
205
+ ...additional,
206
+ });
207
+
208
+ export const topologyCommandEnvironment = (topology) =>
209
+ Object.fromEntries(
210
+ topology.targets.map((target) => [
211
+ target.primary
212
+ ? "REHEARSAL_PRIMARY_ENV_FILE"
213
+ : `REHEARSAL_DEPENDENT_${target.name.toUpperCase().replaceAll("-", "_")}_ENV_FILE`,
214
+ target.paths.applicationEnvironment,
215
+ ]),
216
+ );
217
+
218
+ export const topologyEnvironmentFiles = (topology) =>
219
+ Object.fromEntries(
220
+ topology.targets.map((target) => [
221
+ target.primary ? "primary" : target.name,
222
+ target.paths.applicationEnvironment,
223
+ ]),
224
+ );
225
+
226
+ const runProjectCommand = ({
227
+ source,
228
+ configuredRoot,
229
+ label,
230
+ project,
231
+ environment = {},
232
+ }) => {
233
+ const [command, ...args] = parseCommand(source);
234
+ const result = spawnSync(command, args, {
235
+ cwd: configuredRoot,
236
+ encoding: "utf8",
237
+ env: projectCommandEnvironment(environment),
238
+ stdio: ["ignore", "pipe", "pipe"],
239
+ maxBuffer: 16 * 1024 * 1024,
240
+ });
241
+ if (result.error || result.status !== 0) {
242
+ throw new RehearsalError({
243
+ category: "application_proof_failure",
244
+ code: "PROJECT_COMMAND_FAILED",
245
+ message: `The configured ${label} did not pass.`,
246
+ expected: `${source} exits successfully`,
247
+ actual: summarizeProjectCommandFailure(result),
248
+ context: { project, label },
249
+ refused:
250
+ "Rehearsal did not report the local runtime stack as fully verified.",
251
+ suggestions: [
252
+ "Run the command directly in the same project and correct the failure.",
253
+ ],
254
+ cause: result.error,
255
+ });
256
+ }
257
+ return {
258
+ command: source,
259
+ output: redactDiagnosticValue(String(result.stdout ?? "").trim()),
260
+ };
261
+ };
262
+
263
+ export const createRuntimeCommands = ({
264
+ packageRoot,
265
+ projectRoot,
266
+ managerPath,
267
+ getActivePackageFingerprint,
268
+ }) => {
269
+ const runManager = ({
270
+ action,
271
+ flags,
272
+ configPath = flags.configPath,
273
+ confirmation = flags.confirmation,
274
+ targetName,
275
+ }) => {
276
+ const fingerprint = getActivePackageFingerprint();
277
+ if (fingerprint) {
278
+ assertInstalledPackageFingerprint({
279
+ packageRoot,
280
+ expected: fingerprint,
281
+ });
282
+ }
283
+ const args = buildRuntimeManagerArguments({
284
+ managerPath,
285
+ action,
286
+ configPath,
287
+ confirmation,
288
+ });
289
+ const runtimeStartedMs = performance.now();
290
+ const actionDescription = {
291
+ run: "Restoring the baseline and applying reviewed migrations",
292
+ start: "Starting the disposable local runtime",
293
+ migrate: "Applying reviewed migrations to the current runtime",
294
+ reset: "Restoring the immutable baseline",
295
+ status: "Inspecting the disposable local runtime",
296
+ stop: "Stopping the disposable local runtime",
297
+ discard: "Removing the disposable local runtime",
298
+ verify: "Verifying the current local runtime",
299
+ }[action];
300
+ const describedAction = targetName
301
+ ? `${actionDescription} for ${targetName}`
302
+ : actionDescription;
303
+ const spinner = useStyledPrompts(flags) ? prompts.spinner() : null;
304
+ if (spinner) {
305
+ spinner.start(`${describedAction}. This can take a moment.`);
306
+ } else if (isHumanTerminal(flags)) {
307
+ console.log(
308
+ `\n${terminalStyle(flags, "1;36", action === "run" ? "REHEARSING" : "REHEARSAL")}\n${terminalStyle(flags, "36", "→")} ${describedAction}. This can take a moment.`,
309
+ );
310
+ }
311
+ const result = spawnSync(process.execPath, args, {
312
+ cwd: projectRoot,
313
+ encoding: "utf8",
314
+ env: projectCommandEnvironment(),
315
+ stdio: ["inherit", "pipe", "pipe"],
316
+ maxBuffer: 16 * 1024 * 1024,
317
+ });
318
+ const currentFingerprint = getActivePackageFingerprint();
319
+ if (currentFingerprint) {
320
+ assertInstalledPackageFingerprint({
321
+ packageRoot,
322
+ expected: currentFingerprint,
323
+ });
324
+ }
325
+ if (result.error) {
326
+ if (spinner) spinner.stop(`${describedAction} did not complete.`);
327
+ throw result.error;
328
+ }
329
+ if (result.status !== 0) {
330
+ if (spinner) spinner.stop(`${describedAction} did not complete.`);
331
+ const completeOutput = [
332
+ String(result.stdout ?? ""),
333
+ String(result.stderr ?? ""),
334
+ ]
335
+ .filter(Boolean)
336
+ .join("\n")
337
+ .trim();
338
+ const inferred = normalizeRehearsalError(
339
+ new Error(completeOutput || `Rehearsal ${action} failed.`),
340
+ );
341
+ const category =
342
+ action === "migrate" ||
343
+ (action === "run" &&
344
+ /migrateRuntime|candidate migration|migration up/iu.test(
345
+ completeOutput,
346
+ ))
347
+ ? "migration_candidate_failure"
348
+ : inferred.category;
349
+ const conciseOutput = completeOutput
350
+ .replaceAll(String.fromCodePoint(27), "")
351
+ .split("\n")
352
+ .map((line) => line.trim())
353
+ .filter(Boolean)
354
+ .slice(-8)
355
+ .join("\n")
356
+ .slice(-2_000);
357
+ throw new RehearsalError({
358
+ category,
359
+ code: `MANAGER_${action.toUpperCase()}_FAILED`,
360
+ message: `The local Rehearsal ${action} operation did not complete.`,
361
+ expected: "the isolated runtime operation to finish and verify",
362
+ actual: conciseOutput || `exit ${result.status}`,
363
+ context: { action },
364
+ refused: "The runtime was not reported as trusted.",
365
+ suggestions: [
366
+ "Review the concise failure above, then rerun rehearsal doctor before retrying.",
367
+ "Use --debug only when the safe diagnostic detail is needed.",
368
+ ],
369
+ cause: new Error(completeOutput || `exit ${result.status}`),
370
+ });
371
+ }
372
+ const durationMs = performance.now() - runtimeStartedMs;
373
+ if (spinner) {
374
+ spinner.stop(
375
+ `${describedAction} completed in ${formatDuration(durationMs)}.`,
376
+ );
377
+ } else if (isHumanTerminal(flags)) {
378
+ console.log(
379
+ `${terminalStyle(flags, "32", "✓")} Local runtime step completed in ${formatDuration(durationMs)}.`,
380
+ );
381
+ }
382
+ return {
383
+ action,
384
+ target: targetName ?? "primary",
385
+ durationMs: Math.round(durationMs * 100) / 100,
386
+ output: redactDiagnosticValue(String(result.stdout ?? "").trim()),
387
+ };
388
+ };
389
+
390
+ const runRuntimeStack = async ({ command, flags, planOptions }) => {
391
+ const topology =
392
+ flags.runtimeTopology ?? (await loadRuntimeTopology(planOptions));
393
+ const reverse = ["stop", "discard"].includes(command);
394
+ const targets = reverse
395
+ ? [...topology.targets].reverse()
396
+ : topology.targets;
397
+ const results = targets.map((target) =>
398
+ runManager({
399
+ action: command,
400
+ flags,
401
+ configPath: target.configPath,
402
+ confirmation: flags.targetConfirmations?.get(target.configPath),
403
+ targetName: topology.targets.length > 1 ? target.name : undefined,
404
+ }),
405
+ );
406
+ if (results.length === 1) return { topology, runtime: results[0] };
407
+ return {
408
+ topology,
409
+ runtime: {
410
+ action: command,
411
+ durationMs:
412
+ Math.round(
413
+ results.reduce((total, result) => total + result.durationMs, 0) *
414
+ 100,
415
+ ) / 100,
416
+ output: results
417
+ .map((result) => `[${result.target}]\n${result.output}`)
418
+ .join("\n\n"),
419
+ targets: results,
420
+ },
421
+ };
422
+ };
423
+
424
+ const runApplicationProof = async (planOptions, options = {}) => {
425
+ const { config, projectRoot: configuredRoot } =
426
+ await loadRehearsalConfig(planOptions);
427
+ return runProjectCommand({
428
+ source: config.application.proofCommand,
429
+ configuredRoot,
430
+ label: options.targetName
431
+ ? `application proof for ${options.targetName}`
432
+ : "application proof",
433
+ project: config.project.name,
434
+ environment: options.environment,
435
+ });
436
+ };
437
+
438
+ const prepareDependentTargets = ({ topology, environment }) =>
439
+ topology.dependents.flatMap((target) => {
440
+ if (!target.declaration.prepareCommand) return [];
441
+ return [
442
+ {
443
+ target: target.name,
444
+ ...runProjectCommand({
445
+ source: target.declaration.prepareCommand,
446
+ configuredRoot: topology.projectRoot,
447
+ label: `preparation command for ${target.name}`,
448
+ project: topology.primary.config.project.name,
449
+ environment,
450
+ }),
451
+ },
452
+ ];
453
+ });
454
+
455
+ const proveRuntimeStack = async ({ topology, planOptions, environment }) => {
456
+ const dependentProofs = [];
457
+ for (const target of topology.dependents) {
458
+ dependentProofs.push({
459
+ target: target.name,
460
+ ...(await runApplicationProof(
461
+ { ...planOptions, configPath: target.configPath },
462
+ { targetName: target.name, environment },
463
+ )),
464
+ });
465
+ }
466
+ const applicationProof = await runApplicationProof(planOptions, {
467
+ targetName: topology.targets.length > 1 ? "primary" : undefined,
468
+ environment,
469
+ });
470
+ return { applicationProof, dependentProofs };
471
+ };
472
+
473
+ const prepareCandidateConfirmation = async ({
474
+ command,
475
+ flags,
476
+ planOptions,
477
+ }) => {
478
+ if (!["run", "migrate"].includes(command) || flags.dryRun) return true;
479
+ const summary = await topologyCandidateSummary(planOptions);
480
+ flags.runtimeTopology = summary.topology;
481
+ flags.targetConfirmations = new Map(
482
+ summary.targets.map((target) => [
483
+ target.configPath,
484
+ target.candidateSha256,
485
+ ]),
486
+ );
487
+ if (summary.candidateCount === 0) return true;
488
+ if (flags.confirmation === summary.candidateSha256) return true;
489
+ const interactive =
490
+ !flags.json && process.stdin.isTTY && process.stdout.isTTY;
491
+ if (!interactive || flags.confirmation) {
492
+ throw new RehearsalError({
493
+ category: "migration_candidate_failure",
494
+ code: "CANDIDATE_CONFIRMATION_REQUIRED",
495
+ message: flags.confirmation
496
+ ? "The candidate migration confirmation does not match the current candidate set."
497
+ : "Candidate migration confirmation is required in noninteractive use.",
498
+ expected: summary.candidateSha256,
499
+ actual: flags.confirmation ?? "missing",
500
+ context: {
501
+ command,
502
+ candidateCount: summary.candidateCount,
503
+ candidates: summary.candidates.map(
504
+ (migration) => `${migration.target}: ${migration.filename}`,
505
+ ),
506
+ },
507
+ refused:
508
+ "Rehearsal did not reset, start, or modify the disposable runtime.",
509
+ suggestions: [
510
+ "Run rehearsal candidates, review every file, then pass the current digest with --confirm-candidates=.",
511
+ ],
512
+ });
513
+ }
514
+ console.log(
515
+ [
516
+ "",
517
+ terminalStyle(
518
+ flags,
519
+ "1",
520
+ `${summary.candidateCount} candidate migration${summary.candidateCount === 1 ? "" : "s"}`,
521
+ ),
522
+ "",
523
+ ...summary.candidates.map((migration) =>
524
+ summary.targets.length > 1
525
+ ? ` → ${migration.target}: ${migration.filename}`
526
+ : ` → ${migration.filename}`,
527
+ ),
528
+ "",
529
+ `Candidate set: ${summary.candidateSha256.slice(0, 12)}`,
530
+ "",
531
+ `Rehearsal will apply only this exact set to ${summary.targets.length === 1 ? "the disposable local runtime" : "the isolated local runtime stack"}.`,
532
+ ].join("\n"),
533
+ );
534
+ const accepted = await promptForConfirmation(
535
+ `Apply exactly ${summary.candidateCount === 1 ? "this migration" : "these migrations"}?`,
536
+ flags,
537
+ );
538
+ if (!accepted) {
539
+ console.log("No changes made.");
540
+ return false;
541
+ }
542
+ flags.confirmation = summary.candidateSha256;
543
+ return true;
544
+ };
545
+
546
+ return {
547
+ prepareCandidateConfirmation,
548
+ prepareDependentTargets,
549
+ proveRuntimeStack,
550
+ runManager,
551
+ runRuntimeStack,
552
+ };
553
+ };