@rehearsal-db/core 0.1.0-rc.2 → 0.1.0-rc.4

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 (75) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +4 -1
  3. package/dist/package.json +4 -1
  4. package/dist/scripts/runtime/manage_database.mjs +17 -8
  5. package/dist/src/baseline/artifact.d.mts +14 -0
  6. package/dist/src/baseline/artifact.mjs +50 -0
  7. package/dist/src/cli/application_command_handlers.mjs +9 -4
  8. package/dist/src/cli/basic_command_handlers.mjs +2 -0
  9. package/dist/src/cli/inspection_command_handlers.mjs +45 -0
  10. package/dist/src/cli/project_commands.d.mts +9 -1
  11. package/dist/src/cli/project_commands.mjs +2 -2
  12. package/dist/src/cli/rehearsal.mjs +77 -65
  13. package/dist/src/cli/runtime_action_handlers.mjs +36 -9
  14. package/dist/src/cli/runtime_commands.d.mts +71 -7
  15. package/dist/src/cli/runtime_commands.mjs +99 -36
  16. package/dist/src/cli/source_commands.d.mts +12 -2
  17. package/dist/src/cli/source_commands.mjs +24 -15
  18. package/dist/src/index.d.mts +2 -2
  19. package/dist/src/index.mjs +1 -1
  20. package/dist/src/project/configuration.d.mts +2 -0
  21. package/dist/src/project/configuration.mjs +1 -0
  22. package/dist/src/project/configuration_contract.d.mts +10 -0
  23. package/dist/src/project/configuration_contract.mjs +4 -0
  24. package/dist/src/project/configuration_normalization.d.mts +4 -0
  25. package/dist/src/project/configuration_normalization.mjs +11 -1
  26. package/dist/src/project/configuration_rendering.mjs +14 -0
  27. package/dist/src/project/configuration_resolution.d.mts +4 -0
  28. package/dist/src/project/configuration_schema.d.mts +1 -0
  29. package/dist/src/project/configuration_schema.mjs +4 -0
  30. package/dist/src/project/project_discovery.d.mts +15 -0
  31. package/dist/src/project/project_discovery.mjs +350 -0
  32. package/dist/src/project/project_discovery_application.d.mts +16 -0
  33. package/dist/src/project/project_discovery_application.mjs +190 -0
  34. package/dist/src/project/project_discovery_contract.d.mts +78 -0
  35. package/dist/src/project/project_discovery_contract.mjs +43 -0
  36. package/dist/src/project/project_discovery_metadata.d.mts +14 -0
  37. package/dist/src/project/project_discovery_metadata.mjs +138 -0
  38. package/dist/src/runtime/lifecycle_engine.d.mts +6 -0
  39. package/dist/src/runtime/lifecycle_engine.mjs +24 -2
  40. package/dist/src/runtime/persistence.d.mts +19 -0
  41. package/dist/src/runtime/persistence.mjs +40 -0
  42. package/dist/src/runtime/plan.d.mts +18 -0
  43. package/dist/src/runtime/plan.mjs +27 -1
  44. package/dist/src/runtime/status.d.mts +32 -0
  45. package/dist/src/runtime/status.mjs +146 -0
  46. package/dist/src/shared/cancellation.d.mts +18 -0
  47. package/dist/src/shared/cancellation.mjs +80 -0
  48. package/dist/src/shared/diagnostics.d.mts +1 -0
  49. package/dist/src/shared/diagnostics.mjs +1 -0
  50. package/dist/src/shared/operation_guard.d.mts +12 -0
  51. package/dist/src/shared/operation_guard.mjs +91 -2
  52. package/dist/src/shared/owned_process.d.mts +30 -0
  53. package/dist/src/shared/owned_process.mjs +117 -0
  54. package/dist/src/targets/postgresql.d.mts +0 -4
  55. package/dist/src/targets/postgresql.mjs +40 -12
  56. package/dist/src/targets/postgresql_process.d.mts +8 -0
  57. package/dist/src/targets/postgresql_process.mjs +34 -0
  58. package/dist/src/targets/postgresql_status.d.mts +24 -0
  59. package/dist/src/targets/postgresql_status.mjs +64 -0
  60. package/dist/src/targets/supabase.mjs +34 -16
  61. package/dist/src/targets/supabase_environment.mjs +2 -0
  62. package/dist/src/targets/supabase_process.d.mts +7 -0
  63. package/dist/src/targets/supabase_process.mjs +21 -0
  64. package/dist/src/targets/supabase_status.d.mts +15 -0
  65. package/dist/src/targets/supabase_status.mjs +66 -0
  66. package/dist/src/targets/target.d.mts +3 -1
  67. package/dist/src/targets/target.mjs +3 -1
  68. package/docs/README.md +1 -0
  69. package/docs/architecture.md +8 -0
  70. package/docs/commands.md +188 -12
  71. package/docs/configuration.md +12 -0
  72. package/docs/runtime-lifecycle.md +31 -5
  73. package/docs/typescript.md +11 -0
  74. package/docs/vscode.md +53 -0
  75. package/package.json +4 -1
@@ -8,6 +8,7 @@ import { buildRehearsalPlan } from "../runtime/plan.mjs";
8
8
  import { loadRuntimeTopology } from "../runtime/topology.mjs";
9
9
  import type { RuntimeTopologyTarget } from "../runtime/topology.mjs";
10
10
  import type { InstalledPackageFingerprint } from "../shared/operation_guard.mjs";
11
+ import type { RuntimeStatusSnapshot } from "../runtime/status.mjs";
11
12
  import type { RehearsalCliFlags } from "./arguments.mjs";
12
13
  import type { RehearsalErrorCategory } from "../shared/diagnostics.mjs";
13
14
  import type { CliSessionState } from "./command_contract.mjs";
@@ -18,6 +19,7 @@ interface ManagerReceipt {
18
19
  readonly target: string;
19
20
  readonly durationMs: number;
20
21
  readonly output: string;
22
+ readonly runtimeStatus?: RuntimeStatusSnapshot;
21
23
  }
22
24
  interface RuntimeStackResult {
23
25
  readonly topology: RuntimeTopology;
@@ -73,6 +75,23 @@ export declare const buildRuntimeStackPlan: (planOptions: RehearsalConfigPathOpt
73
75
  sha256: string;
74
76
  }[];
75
77
  };
78
+ lifecycle: {
79
+ run: "keep-until-stop" | "stop-after-run";
80
+ open: "keep-until-stop" | "stop-on-application-exit";
81
+ dataOnStop: string;
82
+ };
83
+ application: {
84
+ startCommand: string;
85
+ proofCommand: string;
86
+ readiness: {
87
+ url: string;
88
+ expectedStatus: number;
89
+ timeoutSeconds: number;
90
+ } | null;
91
+ };
92
+ cleanup: {
93
+ retainBaselineGenerations: number;
94
+ };
76
95
  execution: string[];
77
96
  guarantee: string;
78
97
  paths: {
@@ -81,6 +100,39 @@ export declare const buildRuntimeStackPlan: (planOptions: RehearsalConfigPathOpt
81
100
  sanitizationPolicy: string;
82
101
  };
83
102
  } | {
103
+ config: {
104
+ schemaVersion: 1;
105
+ path: string;
106
+ project: string;
107
+ };
108
+ environment: {
109
+ target: "postgresql" | "supabase";
110
+ kind: string;
111
+ applicationUrl: string;
112
+ projectId: string;
113
+ ports: Readonly<{
114
+ api?: number;
115
+ database: number;
116
+ studio?: number;
117
+ }>;
118
+ hostedAccess: "disabled";
119
+ outboundNetwork: "deny";
120
+ authenticationProviders: readonly string[];
121
+ authenticationCallback: string | null;
122
+ barriers: string[];
123
+ };
124
+ application: {
125
+ startCommand: string;
126
+ proofCommand: string;
127
+ readiness: {
128
+ url: string;
129
+ expectedStatus: number;
130
+ timeoutSeconds: number;
131
+ } | null;
132
+ };
133
+ cleanup: {
134
+ retainBaselineGenerations: number;
135
+ };
84
136
  targets: {
85
137
  name: string;
86
138
  declaration: RuntimeTopologyTarget["declaration"];
@@ -90,6 +142,11 @@ export declare const buildRuntimeStackPlan: (planOptions: RehearsalConfigPathOpt
90
142
  candidateCount: number;
91
143
  candidateSha256: string;
92
144
  };
145
+ lifecycle: {
146
+ run: "keep-until-stop" | "stop-after-run";
147
+ open: "keep-until-stop" | "stop-on-application-exit";
148
+ dataOnStop: string;
149
+ };
93
150
  execution: string[];
94
151
  guarantee: string;
95
152
  }>;
@@ -209,9 +266,9 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
209
266
  targets: readonly ManagerReceipt[];
210
267
  }>;
211
268
  preparations: {
269
+ target: string;
212
270
  command: string;
213
271
  output: string;
214
- target: string;
215
272
  }[];
216
273
  application: {
217
274
  command: string;
@@ -222,11 +279,18 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
222
279
  stoppedBy: string;
223
280
  diagnostics: import("../application/session.mjs").ApplicationDiagnostics;
224
281
  };
282
+ lifecycle: import("../runtime/persistence.mjs").RuntimePersistenceOutcome<ManagerReceipt | Readonly<{
283
+ action: RuntimeAction;
284
+ durationMs: number;
285
+ output: string;
286
+ targets: readonly ManagerReceipt[];
287
+ }>>;
225
288
  }>;
226
- startRuntimeApplication: ({ flags, planOptions, session: commandSession, }: {
289
+ startRuntimeApplication: ({ flags, planOptions, session: commandSession, onRuntimeStarted, }: {
227
290
  flags: RehearsalCliFlags;
228
291
  planOptions: RehearsalConfigPathOptions;
229
292
  session: CliSessionState;
293
+ onRuntimeStarted?: () => void;
230
294
  }) => Promise<{
231
295
  started: ManagerReceipt | Readonly<{
232
296
  action: RuntimeAction;
@@ -241,9 +305,9 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
241
305
  targets: readonly ManagerReceipt[];
242
306
  }>;
243
307
  preparations: {
308
+ target: string;
244
309
  command: string;
245
310
  output: string;
246
- target: string;
247
311
  }[];
248
312
  command: string;
249
313
  session: import("../application/session.mjs").ApplicationSession;
@@ -257,11 +321,11 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
257
321
  prepareDependentTargets: ({ topology, environment, }: {
258
322
  topology: RuntimeTopology;
259
323
  environment: NodeJS.ProcessEnv;
260
- }) => {
324
+ }) => Promise<{
325
+ target: string;
261
326
  command: string;
262
327
  output: string;
263
- target: string;
264
- }[];
328
+ }[]>;
265
329
  proveRuntimeStack: ({ topology, planOptions, environment, }: {
266
330
  topology: RuntimeTopology;
267
331
  planOptions: RehearsalConfigPathOptions;
@@ -283,7 +347,7 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
283
347
  configPath?: string;
284
348
  confirmation?: string;
285
349
  targetName?: string;
286
- }) => ManagerReceipt;
350
+ }) => Promise<ManagerReceipt>;
287
351
  runRuntimeStack: ({ command, flags, planOptions, session, }: {
288
352
  command: RuntimeAction;
289
353
  flags: RehearsalCliFlags;
@@ -13,7 +13,11 @@ import { buildRehearsalPlan, runRehearsalDoctor } from "../runtime/plan.mjs";
13
13
  import { inspectRuntimeTopologyCandidates, loadRuntimeTopology, } from "../runtime/topology.mjs";
14
14
  import { RehearsalError, normalizeRehearsalError, redactDiagnosticValue, } from "../shared/diagnostics.mjs";
15
15
  import { assertInstalledPackageFingerprint } from "../shared/operation_guard.mjs";
16
+ import { getOperationCancellationSignal, throwIfOperationCancelled, } from "../shared/cancellation.mjs";
17
+ import { runOwnedProcess } from "../shared/owned_process.mjs";
16
18
  import { buildRuntimeManagerArguments } from "../targets/target.mjs";
19
+ import { parseRuntimeStatusSnapshot, renderRuntimeStatusSnapshot, } from "../runtime/status.mjs";
20
+ import { applyRuntimePersistence } from "../runtime/persistence.mjs";
17
21
  import { formatDuration, isHumanTerminal, promptForConfirmation, terminalStyle, useStyledPrompts, } from "./terminal.mjs";
18
22
  const errorMessage = (error) => error instanceof Error ? error.message : String(error);
19
23
  export const buildRuntimeStackPlan = async (planOptions) => {
@@ -32,12 +36,22 @@ export const buildRuntimeStackPlan = async (planOptions) => {
32
36
  });
33
37
  }
34
38
  const candidates = await inspectRuntimeTopologyCandidates(planOptions);
39
+ const primaryPlan = targets[0].plan;
35
40
  return {
41
+ config: primaryPlan.config,
42
+ environment: primaryPlan.environment,
43
+ application: primaryPlan.application,
44
+ cleanup: primaryPlan.cleanup,
36
45
  targets,
37
46
  migrations: {
38
47
  candidateCount: candidates.candidateCount,
39
48
  candidateSha256: candidates.candidateSha256,
40
49
  },
50
+ lifecycle: {
51
+ run: topology.primary.config.lifecycle.run,
52
+ open: topology.primary.config.lifecycle.open,
53
+ dataOnStop: "preserved",
54
+ },
41
55
  execution: [
42
56
  "verify every immutable baseline and migration prefix",
43
57
  "restore and migrate the primary runtime",
@@ -47,6 +61,9 @@ export const buildRuntimeStackPlan = async (planOptions) => {
47
61
  .map((target) => `run ${target.name} preparation: ${target.declaration.prepareCommand}`),
48
62
  ...topology.dependents.map((target) => `run ${target.name} proof: ${target.config.application.proofCommand}`),
49
63
  `run primary proof: ${topology.primary.config.application.proofCommand}`,
64
+ topology.primary.config.lifecycle.run === "stop-after-run"
65
+ ? "stop every database target after proofs while preserving its data"
66
+ : "keep every database target available until an explicit stop",
50
67
  ],
51
68
  guarantee: "Every database target is isolated by its own project ID, ports, artifact directory, and local-only safety policy. No production resources will be contacted.",
52
69
  };
@@ -256,15 +273,18 @@ export const topologyEnvironmentFiles = (topology) => ({
256
273
  target.paths.applicationEnvironment,
257
274
  ])),
258
275
  });
259
- const runProjectCommand = ({ source, configuredRoot, label, project, environment = {}, }) => {
276
+ const runProjectCommand = async ({ source, configuredRoot, label, project, environment = {}, }) => {
260
277
  const [command, ...args] = parseCommand(source);
261
- const result = spawnSync(command, args, {
278
+ const cancellationSignal = getOperationCancellationSignal();
279
+ const result = await runOwnedProcess({
280
+ command,
281
+ args,
262
282
  cwd: configuredRoot,
263
- encoding: "utf8",
264
283
  env: projectCommandEnvironment(environment),
265
- stdio: ["ignore", "pipe", "pipe"],
266
- maxBuffer: 16 * 1024 * 1024,
284
+ stdin: "ignore",
285
+ ...(cancellationSignal ? { signal: cancellationSignal } : {}),
267
286
  });
287
+ throwIfOperationCancelled();
268
288
  if (result.error || result.status !== 0) {
269
289
  throw new RehearsalError({
270
290
  category: "application_proof_failure",
@@ -286,7 +306,7 @@ const runProjectCommand = ({ source, configuredRoot, label, project, environment
286
306
  };
287
307
  };
288
308
  export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, getActivePackageFingerprint, }) => {
289
- const runManager = ({ action, flags, configPath = flags.configPath, confirmation = flags.confirmation, targetName, }) => {
309
+ const runManager = async ({ action, flags, configPath = flags.configPath, confirmation = flags.confirmation, targetName, }) => {
290
310
  const fingerprint = getActivePackageFingerprint();
291
311
  if (fingerprint) {
292
312
  assertInstalledPackageFingerprint({
@@ -299,6 +319,7 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
299
319
  action,
300
320
  ...(configPath === undefined ? {} : { configPath }),
301
321
  ...(confirmation === undefined ? {} : { confirmation }),
322
+ structuredResult: action === "status",
302
323
  });
303
324
  const runtimeStartedMs = performance.now();
304
325
  const actionDescription = {
@@ -321,13 +342,25 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
321
342
  else if (isHumanTerminal(flags)) {
322
343
  console.log(`\n${terminalStyle(flags, "1;36", action === "run" ? "REHEARSING" : "REHEARSAL")}\n${terminalStyle(flags, "36", "→")} ${describedAction}. This can take a moment.`);
323
344
  }
324
- const result = spawnSync(process.execPath, args, {
345
+ const cancellationSignal = getOperationCancellationSignal();
346
+ // VS Code extensions launch the package CLI through Electron's executable in
347
+ // Node mode. Preserve that mode only for Rehearsal's nested Node manager;
348
+ // project-owned application commands continue to receive the reduced
349
+ // project environment above.
350
+ const managerEnvironment = {
351
+ ...(cancellationSignal ? { REHEARSAL_CANCELLABLE_OPERATION: "1" } : {}),
352
+ ...(process.env.ELECTRON_RUN_AS_NODE === "1"
353
+ ? { ELECTRON_RUN_AS_NODE: "1" }
354
+ : {}),
355
+ };
356
+ const result = await runOwnedProcess({
357
+ command: process.execPath,
358
+ args,
325
359
  cwd: projectRoot,
326
- encoding: "utf8",
327
- env: projectCommandEnvironment(),
328
- stdio: ["inherit", "pipe", "pipe"],
329
- maxBuffer: 16 * 1024 * 1024,
360
+ env: projectCommandEnvironment(managerEnvironment),
361
+ ...(cancellationSignal ? { signal: cancellationSignal } : {}),
330
362
  });
363
+ throwIfOperationCancelled();
331
364
  const currentFingerprint = getActivePackageFingerprint();
332
365
  if (currentFingerprint) {
333
366
  assertInstalledPackageFingerprint({
@@ -396,11 +429,17 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
396
429
  else if (isHumanTerminal(flags)) {
397
430
  console.log(`${terminalStyle(flags, "32", "✓")} Local runtime step completed in ${formatDuration(durationMs)}.`);
398
431
  }
432
+ const runtimeStatus = action === "status"
433
+ ? parseRuntimeStatusSnapshot(JSON.parse(String(result.stdout ?? "")))
434
+ : undefined;
399
435
  return {
400
436
  action,
401
437
  target: targetName ?? "primary",
402
438
  durationMs: Math.round(durationMs * 100) / 100,
403
- output: String(redactDiagnosticValue(String(result.stdout ?? "").trim())),
439
+ output: runtimeStatus
440
+ ? renderRuntimeStatusSnapshot(runtimeStatus)
441
+ : String(redactDiagnosticValue(String(result.stdout ?? "").trim())),
442
+ ...(runtimeStatus ? { runtimeStatus } : {}),
404
443
  };
405
444
  };
406
445
  const runRuntimeStack = async ({ command, flags, planOptions, session, }) => {
@@ -410,16 +449,17 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
410
449
  const targets = reverse
411
450
  ? [...topology.targets].reverse()
412
451
  : topology.targets;
413
- const results = targets.map((target) => {
452
+ const results = [];
453
+ for (const target of targets) {
414
454
  const confirmation = session.targetConfirmations?.get(target.configPath);
415
- return runManager({
455
+ results.push(await runManager({
416
456
  action: command,
417
457
  flags,
418
458
  configPath: target.configPath,
419
459
  ...(confirmation === undefined ? {} : { confirmation }),
420
460
  ...(topology.targets.length > 1 ? { targetName: target.name } : {}),
421
- });
422
- });
461
+ }));
462
+ }
423
463
  if (results.length === 1)
424
464
  return { topology, runtime: results[0] };
425
465
  return {
@@ -449,22 +489,24 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
449
489
  : { environment: options.environment }),
450
490
  });
451
491
  };
452
- const prepareDependentTargets = ({ topology, environment, }) => topology.dependents.flatMap((target) => {
453
- if (!target.declaration?.prepareCommand)
454
- return [];
455
- return [
456
- {
492
+ const prepareDependentTargets = async ({ topology, environment, }) => {
493
+ const preparations = [];
494
+ for (const target of topology.dependents) {
495
+ if (!target.declaration?.prepareCommand)
496
+ continue;
497
+ preparations.push({
457
498
  target: target.name,
458
- ...runProjectCommand({
499
+ ...(await runProjectCommand({
459
500
  source: target.declaration.prepareCommand,
460
501
  configuredRoot: topology.projectRoot,
461
502
  label: `preparation command for ${target.name}`,
462
503
  project: topology.primary.config.project.name,
463
504
  environment,
464
- }),
465
- },
466
- ];
467
- });
505
+ })),
506
+ });
507
+ }
508
+ return preparations;
509
+ };
468
510
  const proveRuntimeStack = async ({ topology, planOptions, environment, }) => {
469
511
  const dependentProofs = [];
470
512
  for (const target of topology.dependents) {
@@ -479,7 +521,7 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
479
521
  });
480
522
  return { applicationProof, dependentProofs };
481
523
  };
482
- const startRuntimeApplication = async ({ flags, planOptions, session: commandSession, }) => {
524
+ const startRuntimeApplication = async ({ flags, planOptions, session: commandSession, onRuntimeStarted = () => undefined, }) => {
483
525
  const topology = await loadRuntimeTopology(planOptions);
484
526
  const application = topology.primary.config.application;
485
527
  if (!application.readiness) {
@@ -505,13 +547,14 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
505
547
  planOptions,
506
548
  session: commandSession,
507
549
  });
550
+ onRuntimeStarted();
508
551
  const verified = await runRuntimeStack({
509
552
  command: "verify",
510
553
  flags,
511
554
  planOptions,
512
555
  session: commandSession,
513
556
  });
514
- const preparations = prepareDependentTargets({
557
+ const preparations = await prepareDependentTargets({
515
558
  topology,
516
559
  environment: topologyCommandEnvironment(topology),
517
560
  });
@@ -531,15 +574,34 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
531
574
  };
532
575
  };
533
576
  const openRuntimeApplication = async ({ flags, planOptions, session: commandSession, onReady = () => undefined, }) => {
534
- const active = await startRuntimeApplication({
535
- flags,
536
- planOptions,
537
- session: commandSession,
538
- });
539
- const closed = await holdApplicationSession({
540
- session: active.session,
541
- onReady,
577
+ const topology = await loadRuntimeTopology(planOptions);
578
+ let runtimeStarted = false;
579
+ const { result, lifecycle } = await applyRuntimePersistence({
580
+ mode: topology.primary.config.lifecycle.open,
581
+ canStop: () => runtimeStarted,
582
+ operation: async () => {
583
+ const active = await startRuntimeApplication({
584
+ flags,
585
+ planOptions,
586
+ session: commandSession,
587
+ onRuntimeStarted: () => {
588
+ runtimeStarted = true;
589
+ },
590
+ });
591
+ const closed = await holdApplicationSession({
592
+ session: active.session,
593
+ onReady,
594
+ });
595
+ return { active, closed };
596
+ },
597
+ stop: async () => (await runRuntimeStack({
598
+ command: "stop",
599
+ flags,
600
+ planOptions,
601
+ session: commandSession,
602
+ })).runtime,
542
603
  });
604
+ const { active, closed } = result;
543
605
  return {
544
606
  action: "open",
545
607
  started: active.started,
@@ -551,6 +613,7 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
551
613
  stoppedBy: closed.signal,
552
614
  diagnostics: closed.diagnostics,
553
615
  },
616
+ lifecycle,
554
617
  };
555
618
  };
556
619
  const prepareCandidateConfirmation = async ({ command, flags, planOptions, session, }) => {
@@ -2,6 +2,7 @@
2
2
  * CLI orchestration for temporary production-source access and safe baseline
3
3
  * replacement. Domain validation remains in src/source and src/baseline.
4
4
  */
5
+ import { activateBaselineGeneration } from "../baseline/artifact.mjs";
5
6
  import type { RehearsalConfigPathOptions } from "../project/configuration.mjs";
6
7
  import type { RehearsalCliFlags } from "./arguments.mjs";
7
8
  import type { CliSessionState } from "./command_contract.mjs";
@@ -53,6 +54,10 @@ export declare const loadPreparationContext: (planOptions: RehearsalConfigPathOp
53
54
  containerRuntime: Readonly<{
54
55
  autoStartColima: boolean;
55
56
  }>;
57
+ lifecycle: Readonly<{
58
+ run: "keep-until-stop" | "stop-after-run";
59
+ open: "keep-until-stop" | "stop-on-application-exit";
60
+ }>;
56
61
  cleanup: Readonly<{
57
62
  retainBaselineGenerations: number;
58
63
  }>;
@@ -201,6 +206,10 @@ export declare const buildRefreshWorkflowPlan: (planOptions: RehearsalConfigPath
201
206
  containerRuntime: Readonly<{
202
207
  autoStartColima: boolean;
203
208
  }>;
209
+ lifecycle: Readonly<{
210
+ run: "keep-until-stop" | "stop-after-run";
211
+ open: "keep-until-stop" | "stop-on-application-exit";
212
+ }>;
204
213
  cleanup: Readonly<{
205
214
  retainBaselineGenerations: number;
206
215
  }>;
@@ -323,7 +332,7 @@ export declare const buildRefreshWorkflowPlan: (planOptions: RehearsalConfigPath
323
332
  digest: string;
324
333
  };
325
334
  }>;
326
- export declare const createRefreshWorkflow: ({ runRuntimeStack, prepareDependentTargets, topologyCommandEnvironment, buildPlan, refreshBaseline, }: {
335
+ export declare const createRefreshWorkflow: ({ runRuntimeStack, prepareDependentTargets, topologyCommandEnvironment, buildPlan, refreshBaseline, activateBaseline, }: {
327
336
  runRuntimeStack: (input: {
328
337
  command: "reset";
329
338
  flags: RehearsalCliFlags;
@@ -336,10 +345,11 @@ export declare const createRefreshWorkflow: ({ runRuntimeStack, prepareDependent
336
345
  prepareDependentTargets: (input: {
337
346
  topology: RuntimeTopology;
338
347
  environment: NodeJS.ProcessEnv;
339
- }) => unknown[];
348
+ }) => Promise<unknown[]>;
340
349
  topologyCommandEnvironment: (topology: RuntimeTopology) => NodeJS.ProcessEnv;
341
350
  buildPlan?: (options: RehearsalConfigPathOptions) => Promise<RefreshPlanContext>;
342
351
  refreshBaseline?: typeof runSourceBaselineRefresh;
352
+ activateBaseline?: typeof activateBaselineGeneration;
343
353
  }) => ({ flags, planOptions, session, }: SourceCommandInput & {
344
354
  session: CliSessionState;
345
355
  }) => Promise<{
@@ -14,6 +14,7 @@ import { refreshPostgresqlBaseline } from "../source/baseline.mjs";
14
14
  import { resolvePrivacyMappedAssets } from "../source/privacy_paths.mjs";
15
15
  import { applyPostgresqlSourceAccess, planPostgresqlSourceAccessRetirement, resolvePostgresqlSourceStatePaths, retirePostgresqlSourceAccess, } from "../source/postgresql_access.mjs";
16
16
  import { loadRuntimeTopology } from "../runtime/topology.mjs";
17
+ import { isOperationCancelled, throwIfOperationCancelled, withOperationCancellationShield, } from "../shared/cancellation.mjs";
17
18
  const parseSourceAccessReceipt = (source) => {
18
19
  const parsed = JSON.parse(source);
19
20
  if (typeof parsed.targetFingerprint !== "string" ||
@@ -224,7 +225,7 @@ export const buildRefreshWorkflowPlan = async (planOptions) => {
224
225
  },
225
226
  };
226
227
  };
227
- export const createRefreshWorkflow = ({ runRuntimeStack, prepareDependentTargets, topologyCommandEnvironment, buildPlan = buildRefreshWorkflowPlan, refreshBaseline = runSourceBaselineRefresh, }) => async ({ flags, planOptions, session, }) => {
228
+ export const createRefreshWorkflow = ({ runRuntimeStack, prepareDependentTargets, topologyCommandEnvironment, buildPlan = buildRefreshWorkflowPlan, refreshBaseline = runSourceBaselineRefresh, activateBaseline = activateBaselineGeneration, }) => async ({ flags, planOptions, session, }) => {
228
229
  const context = await buildPlan(planOptions);
229
230
  if (!flags.refreshConfirmation) {
230
231
  return { mode: "preview", plan: context.plan };
@@ -236,40 +237,48 @@ export const createRefreshWorkflow = ({ runRuntimeStack, prepareDependentTargets
236
237
  let runtime;
237
238
  let preparations = [];
238
239
  try {
240
+ throwIfOperationCancelled();
239
241
  refreshed = await refreshBaseline({ planOptions });
242
+ throwIfOperationCancelled();
240
243
  const stack = await runRuntimeStack({
241
244
  command: "reset",
242
245
  flags,
243
246
  planOptions,
244
247
  session,
245
248
  });
249
+ throwIfOperationCancelled();
246
250
  runtime = stack.runtime;
247
- preparations = prepareDependentTargets({
251
+ preparations = await prepareDependentTargets({
248
252
  topology: stack.topology,
249
253
  environment: topologyCommandEnvironment(stack.topology),
250
254
  });
255
+ throwIfOperationCancelled();
251
256
  }
252
257
  catch (error) {
253
258
  if (refreshed) {
254
- await activateBaselineGeneration({
255
- artifactRoot: context.loaded.paths.artifactDirectory,
256
- generationId: context.plan.review.currentBaseline,
257
- });
258
259
  try {
259
- const rollback = await runRuntimeStack({
260
- command: "reset",
261
- flags,
262
- planOptions,
263
- session,
264
- });
265
- prepareDependentTargets({
266
- topology: rollback.topology,
267
- environment: topologyCommandEnvironment(rollback.topology),
260
+ await withOperationCancellationShield(async () => {
261
+ await activateBaseline({
262
+ artifactRoot: context.loaded.paths.artifactDirectory,
263
+ generationId: context.plan.review.currentBaseline,
264
+ });
265
+ const rollback = await runRuntimeStack({
266
+ command: "reset",
267
+ flags,
268
+ planOptions,
269
+ session,
270
+ });
271
+ await prepareDependentTargets({
272
+ topology: rollback.topology,
273
+ environment: topologyCommandEnvironment(rollback.topology),
274
+ });
268
275
  });
269
276
  }
270
277
  catch (rollbackError) {
271
278
  throw new AggregateError([error, rollbackError], "The refreshed runtime failed and the previous local runtime could not be restored automatically. The previous immutable baseline is active; run rehearsal reset after correcting the reported problem.");
272
279
  }
280
+ if (isOperationCancelled(error))
281
+ throw error;
273
282
  throw new Error("The refreshed runtime failed verification. Rehearsal restored the previous baseline and local runtime.", { cause: error });
274
283
  }
275
284
  throw error;
@@ -1,4 +1,4 @@
1
1
  /** Public Rehearsal package entry point. */
2
- export { REHEARSAL_CONFIG_VERSION, defineRehearsalConfig, findRehearsalConfigPath, inspectDetectedProject, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./project/configuration.mjs";
3
- export type { DetectedProject, NormalizedRehearsalConfig, RehearsalConfig, RehearsalConfigPathOptions, RehearsalConfigVersion, } from "./project/configuration.mjs";
2
+ export { REHEARSAL_CONFIG_VERSION, defineRehearsalConfig, discoverProjectProfile, findRehearsalConfigPath, inspectDetectedProject, PROJECT_DISCOVERY_SCHEMA_VERSION, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./project/configuration.mjs";
3
+ export type { DetectedProject, DecisionDiscoveryItem, DetectedDiscoveryItem, DiscoveredConfigurationField, DiscoveryConfidence, DiscoveryEvidence, DiscoveryValue, NormalizedRehearsalConfig, ProjectDiscoveryResult, RehearsalConfig, RehearsalConfigPathOptions, RehearsalConfigVersion, } from "./project/configuration.mjs";
4
4
  export { EXCLUDED_VALUE, SANITIZATION_ACTIONS, applySanitizationAction, normalizeSanitizationAction, readBoundRuntimeSanitizationPolicy, validateSanitizationCoverage, validateRuntimeSanitizationPolicy, } from "./baseline/sanitization_policy.mjs";
@@ -1,3 +1,3 @@
1
1
  /** Public Rehearsal package entry point. */
2
- export { REHEARSAL_CONFIG_VERSION, defineRehearsalConfig, findRehearsalConfigPath, inspectDetectedProject, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./project/configuration.mjs";
2
+ export { REHEARSAL_CONFIG_VERSION, defineRehearsalConfig, discoverProjectProfile, findRehearsalConfigPath, inspectDetectedProject, PROJECT_DISCOVERY_SCHEMA_VERSION, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./project/configuration.mjs";
3
3
  export { EXCLUDED_VALUE, SANITIZATION_ACTIONS, applySanitizationAction, normalizeSanitizationAction, readBoundRuntimeSanitizationPolicy, validateSanitizationCoverage, validateRuntimeSanitizationPolicy, } from "./baseline/sanitization_policy.mjs";
@@ -10,4 +10,6 @@ export { loadRehearsalConfig } from "./configuration_resolution.mjs";
10
10
  export type { NormalizedRehearsalConfig } from "./configuration_resolution.mjs";
11
11
  export { inspectDetectedProject } from "./project_detection.mjs";
12
12
  export type { DetectedProject } from "./project_detection.mjs";
13
+ export { discoverProjectProfile, probeContainerEngineWithoutStarting, PROJECT_DISCOVERY_SCHEMA_VERSION, } from "./project_discovery.mjs";
14
+ export type { ContainerEngineProbe, DecisionDiscoveryItem, DetectedDiscoveryItem, DiscoveredConfigurationField, DiscoveryConfidence, DiscoveryEvidence, DiscoveryValue, ProjectDiscoveryResult, SuggestedDiscoveryItem, } from "./project_discovery.mjs";
13
15
  export { renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./configuration_rendering.mjs";
@@ -6,4 +6,5 @@ export { defineRehearsalConfig, REHEARSAL_CONFIG_VERSION, REHEARSAL_DEFAULTS, }
6
6
  export { findRehearsalConfigPath } from "./configuration_discovery.mjs";
7
7
  export { loadRehearsalConfig } from "./configuration_resolution.mjs";
8
8
  export { inspectDetectedProject } from "./project_detection.mjs";
9
+ export { discoverProjectProfile, probeContainerEngineWithoutStarting, PROJECT_DISCOVERY_SCHEMA_VERSION, } from "./project_discovery.mjs";
9
10
  export { renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./configuration_rendering.mjs";
@@ -1,5 +1,7 @@
1
1
  /** Public configuration types, format version, and stable defaults. */
2
2
  export type RehearsalConfigVersion = 1;
3
+ export type RunPersistenceMode = "keep-until-stop" | "stop-after-run";
4
+ export type OpenPersistenceMode = "keep-until-stop" | "stop-on-application-exit";
3
5
  export interface RehearsalConfig {
4
6
  schemaVersion: RehearsalConfigVersion;
5
7
  project: {
@@ -38,6 +40,10 @@ export interface RehearsalConfig {
38
40
  containerRuntime?: {
39
41
  autoStartColima?: boolean;
40
42
  };
43
+ lifecycle?: {
44
+ run?: RunPersistenceMode;
45
+ open?: OpenPersistenceMode;
46
+ };
41
47
  cleanup?: {
42
48
  retainBaselineGenerations?: number;
43
49
  };
@@ -108,6 +114,10 @@ export declare const REHEARSAL_DEFAULTS: Readonly<{
108
114
  containerRuntime: Readonly<{
109
115
  autoStartColima: false;
110
116
  }>;
117
+ lifecycle: Readonly<{
118
+ run: RunPersistenceMode;
119
+ open: OpenPersistenceMode;
120
+ }>;
111
121
  cleanup: Readonly<{
112
122
  retainBaselineGenerations: 2;
113
123
  }>;
@@ -3,6 +3,10 @@ export const REHEARSAL_CONFIG_VERSION = 1;
3
3
  export const REHEARSAL_DEFAULTS = Object.freeze({
4
4
  baseline: Object.freeze({ artifactDirectory: ".rehearsal" }),
5
5
  containerRuntime: Object.freeze({ autoStartColima: false }),
6
+ lifecycle: Object.freeze({
7
+ run: "keep-until-stop",
8
+ open: "keep-until-stop",
9
+ }),
6
10
  cleanup: Object.freeze({ retainBaselineGenerations: 2 }),
7
11
  runtime: Object.freeze({
8
12
  applicationUrl: "http://localhost:5175",
@@ -41,6 +41,10 @@ export declare const normalizeRehearsalConfig: (input: unknown) => Readonly<{
41
41
  containerRuntime: Readonly<{
42
42
  autoStartColima: boolean;
43
43
  }>;
44
+ lifecycle: Readonly<{
45
+ run: "keep-until-stop" | "stop-after-run";
46
+ open: "keep-until-stop" | "stop-on-application-exit";
47
+ }>;
44
48
  cleanup: Readonly<{
45
49
  retainBaselineGenerations: number;
46
50
  }>;