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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +3 -1
  3. package/dist/package.json +1 -1
  4. package/dist/scripts/runtime/manage_database.mjs +17 -8
  5. package/dist/src/cli/application_command_handlers.mjs +9 -4
  6. package/dist/src/cli/basic_command_handlers.mjs +1 -0
  7. package/dist/src/cli/inspection_command_handlers.mjs +27 -0
  8. package/dist/src/cli/project_commands.d.mts +9 -1
  9. package/dist/src/cli/project_commands.mjs +2 -2
  10. package/dist/src/cli/rehearsal.mjs +77 -65
  11. package/dist/src/cli/runtime_action_handlers.mjs +36 -9
  12. package/dist/src/cli/runtime_commands.d.mts +26 -7
  13. package/dist/src/cli/runtime_commands.mjs +94 -36
  14. package/dist/src/cli/source_commands.d.mts +12 -2
  15. package/dist/src/cli/source_commands.mjs +24 -15
  16. package/dist/src/project/configuration_contract.d.mts +10 -0
  17. package/dist/src/project/configuration_contract.mjs +4 -0
  18. package/dist/src/project/configuration_normalization.d.mts +4 -0
  19. package/dist/src/project/configuration_normalization.mjs +11 -1
  20. package/dist/src/project/configuration_rendering.mjs +14 -0
  21. package/dist/src/project/configuration_resolution.d.mts +4 -0
  22. package/dist/src/project/configuration_schema.d.mts +1 -0
  23. package/dist/src/project/configuration_schema.mjs +4 -0
  24. package/dist/src/runtime/lifecycle_engine.mjs +10 -2
  25. package/dist/src/runtime/persistence.d.mts +19 -0
  26. package/dist/src/runtime/persistence.mjs +40 -0
  27. package/dist/src/runtime/plan.d.mts +5 -0
  28. package/dist/src/runtime/plan.mjs +8 -0
  29. package/dist/src/runtime/status.d.mts +30 -0
  30. package/dist/src/runtime/status.mjs +141 -0
  31. package/dist/src/shared/cancellation.d.mts +18 -0
  32. package/dist/src/shared/cancellation.mjs +80 -0
  33. package/dist/src/shared/diagnostics.d.mts +1 -0
  34. package/dist/src/shared/diagnostics.mjs +1 -0
  35. package/dist/src/shared/operation_guard.d.mts +12 -0
  36. package/dist/src/shared/operation_guard.mjs +91 -2
  37. package/dist/src/shared/owned_process.d.mts +30 -0
  38. package/dist/src/shared/owned_process.mjs +117 -0
  39. package/dist/src/targets/postgresql.d.mts +0 -4
  40. package/dist/src/targets/postgresql.mjs +37 -12
  41. package/dist/src/targets/postgresql_process.d.mts +8 -0
  42. package/dist/src/targets/postgresql_process.mjs +34 -0
  43. package/dist/src/targets/postgresql_status.d.mts +21 -0
  44. package/dist/src/targets/postgresql_status.mjs +51 -0
  45. package/dist/src/targets/supabase.mjs +31 -16
  46. package/dist/src/targets/supabase_environment.mjs +2 -0
  47. package/dist/src/targets/supabase_process.d.mts +7 -0
  48. package/dist/src/targets/supabase_process.mjs +21 -0
  49. package/dist/src/targets/supabase_status.d.mts +12 -0
  50. package/dist/src/targets/supabase_status.mjs +56 -0
  51. package/dist/src/targets/target.d.mts +3 -1
  52. package/dist/src/targets/target.mjs +3 -1
  53. package/docs/commands.md +127 -2
  54. package/docs/configuration.md +12 -0
  55. package/docs/runtime-lifecycle.md +31 -5
  56. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## [0.1.0-rc.3] - 2026-10-10
9
+
10
+ ### Added
11
+
12
+ - `status --json` now exposes bounded, target-scoped runtime state without requiring
13
+ editor clients to parse human-formatted process output.
14
+ - `activity --json` reports idle, busy, stale, invalid, and unavailable project-operation
15
+ state without exposing lock tokens, process arguments, credentials, or private paths.
16
+ - Supported mutating operations now have an explicit cancellation contract: owned child
17
+ groups receive one bounded shutdown request, rollback and lock cleanup complete before
18
+ exit `130`, and unrelated processes and Docker resources remain untouched.
19
+ - Root configuration can now choose `stop-after-run` for bounded automated checks and
20
+ `stop-on-application-exit` for bounded hands-on sessions. Both modes stop only the
21
+ configured Rehearsal targets while preserving database and Storage volumes, applied
22
+ migrations, and sandbox edits for the next `start` or `open`.
23
+ - Plans, JSON results, terminal receipts, generated configuration comments, and the
24
+ lifecycle guide now report the selected persistence mode and resulting runtime state.
25
+
26
+ ### Changed
27
+
28
+ - Transient application and helper processes are cleaned up before an opted-in runtime
29
+ stop after success, proof failure, readiness timeout, or supported interruption. The
30
+ compatibility default remains `keep-until-stop` for both `run` and `open`.
31
+
32
+ ### Fixed
33
+
34
+ - Read-only runtime inspection now works when a VS Code extension launches the CLI
35
+ through Electron's executable in Node mode. That mode is preserved only for
36
+ Rehearsal's nested runtime manager and is not exposed to project-owned commands.
37
+
8
38
  ## [0.1.0-rc.2] - 2026-10-09
9
39
 
10
40
  ### Changed
@@ -575,7 +605,8 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
575
605
  publication uses short-lived trusted OIDC, and every release tag must already exist on
576
606
  protected `main`.
577
607
 
578
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.2...HEAD
608
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.3...HEAD
609
+ [0.1.0-rc.3]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.2...v0.1.0-rc.3
579
610
  [0.1.0-rc.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.1...v0.1.0-rc.2
580
611
  [0.1.0-rc.1]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.18...v0.1.0-rc.1
581
612
  [0.1.0-beta.18]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.17...v0.1.0-beta.18
package/README.md CHANGED
@@ -116,7 +116,9 @@ source-owner approval; see [Standalone workflow](docs/standalone-workflow.md).
116
116
 
117
117
  `npx rehearsal open` starts and verifies the existing local runtime, keeps the configured
118
118
  app available until you press `Ctrl+C`, and preserves database and Storage changes for
119
- the next session.
119
+ the next session. Set `lifecycle.open` to `stop-on-application-exit` when you also want
120
+ its database targets stopped afterward; `lifecycle.run: "stop-after-run"` gives automated
121
+ checks the same bounded, non-destructive behavior.
120
122
 
121
123
  For an approved production-shaped copy, the optional sequence is `source plan`, `source
122
124
  apply`, `refresh`, and `source retire`. `refresh` verifies the replacement before it
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-rc.2",
3
+ "version": "0.1.0-rc.3",
4
4
  "private": false,
5
5
  "description": "Catch PostgreSQL and Supabase migrations that pass on empty databases but fail on real data—before deploy.",
6
6
  "repository": {
@@ -6,11 +6,20 @@
6
6
  import { loadRehearsalConfig } from "../../src/project/configuration.mjs";
7
7
  import { parseRuntimeInvocation } from "../../src/targets/target.mjs";
8
8
  import { runRuntimeTarget } from "../../src/targets/driver.mjs";
9
- const invocation = parseRuntimeInvocation();
10
- const { config } = await loadRehearsalConfig({
11
- projectRoot: process.cwd(),
12
- ...(invocation.configPath === undefined
13
- ? {}
14
- : { configPath: invocation.configPath }),
15
- });
16
- await runRuntimeTarget(config.runtime.target);
9
+ import { runWithOperationCancellation } from "../../src/shared/cancellation.mjs";
10
+ const run = async () => {
11
+ const invocation = parseRuntimeInvocation();
12
+ const { config } = await loadRehearsalConfig({
13
+ projectRoot: process.cwd(),
14
+ ...(invocation.configPath === undefined
15
+ ? {}
16
+ : { configPath: invocation.configPath }),
17
+ });
18
+ await runRuntimeTarget(config.runtime.target);
19
+ };
20
+ if (process.env.REHEARSAL_CANCELLABLE_OPERATION === "1") {
21
+ await runWithOperationCancellation({ task: run });
22
+ }
23
+ else {
24
+ await run();
25
+ }
@@ -14,10 +14,10 @@ export const createApplicationCommandHandlers = ({ openRuntimeApplication, }) =>
14
14
  onReady: (ready) => {
15
15
  const lines = [
16
16
  `Application: ${ready.url}`,
17
- "The verified database runtime stays running and keeps your local changes.",
17
+ "The configured lifecycle policy will preserve your local database and Storage changes.",
18
18
  guided
19
19
  ? "Press Ctrl+C to close the app and return here, or Ctrl+Z to exit Rehearsal."
20
- : "Press Ctrl+C or Ctrl+Z to close the app. Rehearsal will leave the database runtime running.",
20
+ : "Press Ctrl+C or Ctrl+Z to close the app. Rehearsal will then apply the configured lifecycle policy.",
21
21
  ];
22
22
  if (useStyledPrompts(flags)) {
23
23
  prompts.note(lines.join("\n"), "SANDBOX READY");
@@ -35,6 +35,7 @@ export const createApplicationCommandHandlers = ({ openRuntimeApplication, }) =>
35
35
  data.started.output,
36
36
  data.verified.output,
37
37
  ...data.preparations.map((preparation) => preparation.output),
38
+ data.lifecycle.stop?.output,
38
39
  ]
39
40
  .filter(Boolean)
40
41
  .join("\n");
@@ -44,8 +45,12 @@ export const createApplicationCommandHandlers = ({ openRuntimeApplication, }) =>
44
45
  render: (result) => [
45
46
  "SANDBOX APP CLOSED",
46
47
  `Application stopped after ${result.application.stoppedBy}.`,
47
- "The database runtime and its local database and Storage changes were preserved.",
48
- "Run rehearsal open to return, rehearsal verify to check it, or rehearsal stop to stop the databases.",
48
+ result.lifecycle.runtimeState === "stopped"
49
+ ? `Lifecycle ${result.lifecycle.mode} stopped the database runtime; its local database and Storage changes were preserved.`
50
+ : `Lifecycle ${result.lifecycle.mode} left the database runtime running; its local database and Storage changes were preserved.`,
51
+ result.lifecycle.runtimeState === "stopped"
52
+ ? "Run rehearsal start or rehearsal open to resume."
53
+ : "Run rehearsal open to return, rehearsal verify to check it, or rehearsal stop to stop the databases.",
49
54
  ].join("\n"),
50
55
  });
51
56
  }),
@@ -21,6 +21,7 @@ Commands:
21
21
  identity connect [--identity=] Keep the app open, detect sign-in, and connect once
22
22
  doctor Check whether Rehearsal is safe and ready
23
23
  support Print a safe, copy-ready support report
24
+ activity Report idle, busy, stale, or invalid operation state
24
25
  explain Show the immutable execution plan
25
26
  run --dry-run Alias the exact explain plan without mutations
26
27
  run --confirm-candidates= Execute reset, migration, and verification locally
@@ -4,7 +4,34 @@ import { inspectRehearsalBaseline, inspectRehearsalMigrations, } from "../runtim
4
4
  import { buildRuntimeStackPlan, runRuntimeStackDoctor, topologyCandidateSummary, } from "./runtime_commands.mjs";
5
5
  import { renderBaseline, renderCandidates, renderDoctor, renderMigrations, renderPlan, renderSupportReport, } from "./renderers.mjs";
6
6
  import { exactCommands, renderedCommand } from "./command_contract.mjs";
7
+ import { inspectProjectOperationState } from "../shared/operation_guard.mjs";
7
8
  export const createInspectionCommandHandlers = () => [
9
+ exactCommands("activity", ["activity"], async ({ planOptions }) => {
10
+ const data = await inspectProjectOperationState({
11
+ projectRoot: planOptions.projectRoot ?? process.cwd(),
12
+ });
13
+ return renderedCommand({
14
+ data,
15
+ render: (result) => {
16
+ if (result.state === "idle")
17
+ return "Rehearsal activity: idle.";
18
+ if (result.state === "invalid") {
19
+ return "Rehearsal activity: invalid lock state. No lock was changed; finish or diagnose the owning operation before retrying a mutation.";
20
+ }
21
+ const label = result.state === "busy" ? "busy" : "stale";
22
+ return [
23
+ `Rehearsal activity: ${label}.`,
24
+ `Operation: ${result.operation.kind}`,
25
+ `Started: ${result.operation.startedAt}`,
26
+ result.operation.rehearsalVersion
27
+ ? `Rehearsal: ${result.operation.rehearsalVersion}`
28
+ : null,
29
+ ]
30
+ .filter(Boolean)
31
+ .join("\n");
32
+ },
33
+ });
34
+ }),
8
35
  exactCommands("doctor", ["doctor"], async ({ guided, flags, planOptions }) => {
9
36
  const data = await runRuntimeStackDoctor(planOptions);
10
37
  return renderedCommand({
@@ -17,7 +17,7 @@ type RunManager = (input: {
17
17
  flags: RehearsalCliFlags;
18
18
  configPath?: string;
19
19
  targetName?: string;
20
- }) => unknown;
20
+ }) => Promise<unknown>;
21
21
  interface ProjectCommandInput {
22
22
  readonly flags: RehearsalCliFlags;
23
23
  readonly planOptions: RehearsalConfigPathOptions;
@@ -67,6 +67,10 @@ export declare const createProjectCommands: ({ projectRoot, runManager, }: {
67
67
  containerRuntime: Readonly<{
68
68
  autoStartColima: boolean;
69
69
  }>;
70
+ lifecycle: Readonly<{
71
+ run: "keep-until-stop" | "stop-after-run";
72
+ open: "keep-until-stop" | "stop-on-application-exit";
73
+ }>;
70
74
  cleanup: Readonly<{
71
75
  retainBaselineGenerations: number;
72
76
  }>;
@@ -191,6 +195,10 @@ export declare const createProjectCommands: ({ projectRoot, runManager, }: {
191
195
  containerRuntime: Readonly<{
192
196
  autoStartColima: boolean;
193
197
  }>;
198
+ lifecycle: Readonly<{
199
+ run: "keep-until-stop" | "stop-after-run";
200
+ open: "keep-until-stop" | "stop-on-application-exit";
201
+ }>;
194
202
  cleanup: Readonly<{
195
203
  retainBaselineGenerations: number;
196
204
  }>;
@@ -128,7 +128,7 @@ export const createProjectCommands = ({ projectRoot, runManager, }) => {
128
128
  projectRoot,
129
129
  configPath: target.configPath,
130
130
  removeRuntime: async () => {
131
- runManager({
131
+ await runManager({
132
132
  action: "discard",
133
133
  flags,
134
134
  configPath: target.configPath,
@@ -151,7 +151,7 @@ export const createProjectCommands = ({ projectRoot, runManager, }) => {
151
151
  : { confirmation: flags.cleanupConfirmation }),
152
152
  ...planOptions,
153
153
  removeRuntime: async () => {
154
- runManager({ action: "discard", flags });
154
+ await runManager({ action: "discard", flags });
155
155
  },
156
156
  });
157
157
  return { mode: "written", plan, applied };
@@ -20,6 +20,7 @@ import { createProjectCommands } from "./project_commands.mjs";
20
20
  import { buildRuntimeStackPlan, createRuntimeCommands, runRuntimeStackDoctor, topologyCandidateSummary, topologyCommandEnvironment, } from "./runtime_commands.mjs";
21
21
  import { buildRefreshWorkflowPlan, createRefreshWorkflow, } from "./source_commands.mjs";
22
22
  import { installGuidedExitShortcut, useStyledPrompts } from "./terminal.mjs";
23
+ import { runWithOperationCancellation, supportsOperationCancellation, } from "../shared/cancellation.mjs";
23
24
  const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
24
25
  const projectRoot = process.cwd();
25
26
  const managerPath = join(packageRoot, "dist/scripts/runtime/manage_database.mjs");
@@ -89,83 +90,94 @@ const main = async () => {
89
90
  const guided = command === "guide" || wantsAutomaticGuide;
90
91
  const mutating = commandMutatesProject({ command, flags, guided });
91
92
  const operation = mutating
92
- ? await acquireProjectOperation({ projectRoot, command })
93
+ ? await acquireProjectOperation({
94
+ projectRoot,
95
+ command: guided ? "guide" : command,
96
+ })
93
97
  : null;
94
- if (operation) {
95
- activePackageFingerprint = createInstalledPackageFingerprint({
96
- packageRoot,
97
- });
98
- }
99
- try {
100
- if (!guided) {
101
- await executeCommand({ command, flags, planOptions, guided: false });
102
- return;
103
- }
104
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
105
- throw new Error("The guided home screen requires an interactive terminal. Run rehearsal --help to list scriptable commands.");
106
- }
107
- flags.guided = true;
108
- const installExitShortcut = () => installGuidedExitShortcut(flags, {
109
- beforeExit: () => operation?.releaseSync(),
110
- });
111
- let removeGuidedExitShortcut = installExitShortcut();
112
- if (useStyledPrompts(flags)) {
113
- prompts.intro("REHEARSAL · Safe local migration testing");
98
+ const run = async () => {
99
+ if (operation) {
100
+ activePackageFingerprint = createInstalledPackageFingerprint({
101
+ packageRoot,
102
+ });
114
103
  }
115
104
  try {
116
- while (true) {
117
- resetGuidedFlags(flags);
118
- resetCommandSessionState(state);
119
- const selected = await runGuidedHome({ flags, planOptions });
120
- if (!selected)
121
- break;
122
- if (selected === "home")
123
- continue;
124
- const opensApplication = selected === "open";
125
- if (opensApplication) {
126
- removeGuidedExitShortcut?.();
127
- removeGuidedExitShortcut = null;
105
+ if (!guided) {
106
+ await executeCommand({ command, flags, planOptions, guided: false });
107
+ return;
108
+ }
109
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
110
+ throw new Error("The guided home screen requires an interactive terminal. Run rehearsal --help to list scriptable commands.");
111
+ }
112
+ flags.guided = true;
113
+ const installExitShortcut = () => installGuidedExitShortcut(flags, {
114
+ beforeExit: () => operation?.releaseSync(),
115
+ });
116
+ let removeGuidedExitShortcut = installExitShortcut();
117
+ if (useStyledPrompts(flags)) {
118
+ prompts.intro("REHEARSAL · Safe local migration testing");
119
+ }
120
+ try {
121
+ while (true) {
122
+ resetGuidedFlags(flags);
123
+ resetCommandSessionState(state);
124
+ const selected = await runGuidedHome({ flags, planOptions });
125
+ if (!selected)
126
+ break;
127
+ if (selected === "home")
128
+ continue;
129
+ const opensApplication = selected === "open";
130
+ if (opensApplication) {
131
+ removeGuidedExitShortcut?.();
132
+ removeGuidedExitShortcut = null;
133
+ }
134
+ try {
135
+ await executeCommand({
136
+ command: selected,
137
+ flags,
138
+ planOptions,
139
+ guided: true,
140
+ });
141
+ }
142
+ finally {
143
+ if (opensApplication) {
144
+ removeGuidedExitShortcut = installExitShortcut();
145
+ }
146
+ }
147
+ if (flags.exitGuidedSession)
148
+ break;
128
149
  }
150
+ }
151
+ finally {
152
+ removeGuidedExitShortcut?.();
153
+ }
154
+ if (useStyledPrompts(flags)) {
155
+ prompts.outro("See you at the next rehearsal.");
156
+ }
157
+ else {
158
+ console.log("See you at the next rehearsal.");
159
+ }
160
+ }
161
+ finally {
162
+ if (operation) {
129
163
  try {
130
- await executeCommand({
131
- command: selected,
132
- flags,
133
- planOptions,
134
- guided: true,
164
+ assertInstalledPackageFingerprint({
165
+ packageRoot,
166
+ expected: activePackageFingerprint,
135
167
  });
136
168
  }
137
169
  finally {
138
- if (opensApplication) {
139
- removeGuidedExitShortcut = installExitShortcut();
140
- }
170
+ activePackageFingerprint = null;
171
+ await operation.release();
141
172
  }
142
- if (flags.exitGuidedSession)
143
- break;
144
173
  }
145
174
  }
146
- finally {
147
- removeGuidedExitShortcut?.();
148
- }
149
- if (useStyledPrompts(flags)) {
150
- prompts.outro("See you at the next rehearsal.");
151
- }
152
- else {
153
- console.log("See you at the next rehearsal.");
154
- }
175
+ };
176
+ if (operation && !guided && supportsOperationCancellation(command)) {
177
+ await runWithOperationCancellation({ task: run });
155
178
  }
156
- finally {
157
- if (operation) {
158
- try {
159
- assertInstalledPackageFingerprint({
160
- packageRoot,
161
- expected: activePackageFingerprint,
162
- });
163
- }
164
- finally {
165
- activePackageFingerprint = null;
166
- await operation.release();
167
- }
168
- }
179
+ else {
180
+ await run();
169
181
  }
170
182
  };
171
183
  try {
@@ -1,6 +1,7 @@
1
1
  /** Database runtime action handlers shared by guided and scripted CLI flows. */
2
2
  import * as prompts from "@clack/prompts";
3
3
  import { buildApplicationEnvironment, runHttpProofs, startApplicationSession, } from "../application/session.mjs";
4
+ import { applyRuntimePersistence } from "../runtime/persistence.mjs";
4
5
  import { exactCommands, renderedCommand, silentCommand, } from "./command_contract.mjs";
5
6
  import { topologyCommandEnvironment, topologyEnvironmentFiles, } from "./runtime_commands.mjs";
6
7
  import { formatDuration, isHumanTerminal, terminalStyle, useStyledPrompts, } from "./terminal.mjs";
@@ -42,17 +43,20 @@ export const createRuntimeActionHandlers = ({ prepareCandidateConfirmation, prep
42
43
  });
43
44
  const { runtime, topology } = stack;
44
45
  const stackEnvironment = topologyCommandEnvironment(topology);
45
- const preparations = ["run", "reset", "migrate"].includes(command)
46
- ? prepareDependentTargets({
47
- topology,
48
- environment: stackEnvironment,
49
- })
50
- : [];
46
+ let preparations = [];
51
47
  let applicationProof;
52
48
  let dependentProofs = [];
53
49
  let httpProofResults = [];
54
50
  let applicationReadiness;
55
- if (command === "run") {
51
+ const completeFollowups = async () => {
52
+ preparations = ["run", "reset", "migrate"].includes(command)
53
+ ? await prepareDependentTargets({
54
+ topology,
55
+ environment: stackEnvironment,
56
+ })
57
+ : [];
58
+ if (command !== "run")
59
+ return;
56
60
  const applicationConfig = topology.primary.config.application;
57
61
  const mappedApplicationEnvironment = Object.keys(applicationConfig.environmentVariables).length
58
62
  ? await buildApplicationEnvironment({
@@ -111,7 +115,19 @@ export const createRuntimeActionHandlers = ({ prepareCandidateConfirmation, prep
111
115
  else if (isHumanTerminal(flags)) {
112
116
  console.log(`${terminalStyle(flags, "32", "✓")} Project proofs passed.`);
113
117
  }
114
- }
118
+ };
119
+ const persistence = command === "run"
120
+ ? await applyRuntimePersistence({
121
+ mode: topology.primary.config.lifecycle.run,
122
+ operation: completeFollowups,
123
+ stop: async () => (await runRuntimeStack({
124
+ command: "stop",
125
+ flags,
126
+ planOptions,
127
+ session: state,
128
+ })).runtime,
129
+ })
130
+ : (await completeFollowups(), null);
115
131
  const data = command === "run"
116
132
  ? {
117
133
  runtime,
@@ -120,6 +136,7 @@ export const createRuntimeActionHandlers = ({ prepareCandidateConfirmation, prep
120
136
  applicationProof,
121
137
  applicationReadiness,
122
138
  httpProofResults,
139
+ lifecycle: persistence.lifecycle,
123
140
  }
124
141
  : preparations.length
125
142
  ? { ...runtime, preparations }
@@ -130,6 +147,7 @@ export const createRuntimeActionHandlers = ({ prepareCandidateConfirmation, prep
130
147
  ...preparations.map((preparation) => preparation.output),
131
148
  ...dependentProofs.map((proof) => proof.output),
132
149
  applicationProof?.output,
150
+ persistence?.lifecycle.stop?.output,
133
151
  ]
134
152
  .filter(Boolean)
135
153
  .join("\n");
@@ -145,8 +163,11 @@ export const createRuntimeActionHandlers = ({ prepareCandidateConfirmation, prep
145
163
  ? value.applicationReadiness
146
164
  : undefined;
147
165
  const renderedHttpProofResults = "httpProofResults" in value ? value.httpProofResults : [];
166
+ const renderedLifecycle = "lifecycle" in value ? value.lifecycle : undefined;
148
167
  const nextAction = {
149
- run: "Next: run rehearsal open for hands-on testing, then rehearsal verify.",
168
+ run: renderedLifecycle?.runtimeState === "stopped"
169
+ ? "The runtime was stopped with its database and Storage changes preserved. Run rehearsal start or rehearsal open to resume."
170
+ : "Next: run rehearsal open for hands-on testing, then rehearsal verify.",
150
171
  start: "Next: run rehearsal open for hands-on testing or rehearsal status.",
151
172
  migrate: "Next: run rehearsal verify to prove the current runtime.",
152
173
  reset: "Next: run rehearsal open or rehearsal verify.",
@@ -171,6 +192,9 @@ export const createRuntimeActionHandlers = ({ prepareCandidateConfirmation, prep
171
192
  ? `✓ Application became ready at ${renderedApplicationReadiness.url}`
172
193
  : null,
173
194
  ...renderedHttpProofResults.map((proof) => `✓ ${proof.kind} HTTP proof passed: ${proof.name}`),
195
+ renderedLifecycle
196
+ ? `✓ Lifecycle ${renderedLifecycle.mode}: runtime ${renderedLifecycle.runtimeState}; data preserved`
197
+ : null,
174
198
  "",
175
199
  "Technical output is available from Show details in the guide.",
176
200
  ]
@@ -189,6 +213,9 @@ export const createRuntimeActionHandlers = ({ prepareCandidateConfirmation, prep
189
213
  ? `Application readiness passed: ${renderedApplicationReadiness.url}`
190
214
  : null,
191
215
  ...renderedHttpProofResults.map((proof) => `${proof.kind} HTTP proof passed: ${proof.name}`),
216
+ renderedLifecycle
217
+ ? `Lifecycle ${renderedLifecycle.mode}: runtime ${renderedLifecycle.runtimeState}; data preserved.`
218
+ : null,
192
219
  nextAction ? "" : null,
193
220
  nextAction,
194
221
  ]
@@ -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,11 @@ 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
+ };
76
83
  execution: string[];
77
84
  guarantee: string;
78
85
  paths: {
@@ -90,6 +97,11 @@ export declare const buildRuntimeStackPlan: (planOptions: RehearsalConfigPathOpt
90
97
  candidateCount: number;
91
98
  candidateSha256: string;
92
99
  };
100
+ lifecycle: {
101
+ run: "keep-until-stop" | "stop-after-run";
102
+ open: "keep-until-stop" | "stop-on-application-exit";
103
+ dataOnStop: string;
104
+ };
93
105
  execution: string[];
94
106
  guarantee: string;
95
107
  }>;
@@ -209,9 +221,9 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
209
221
  targets: readonly ManagerReceipt[];
210
222
  }>;
211
223
  preparations: {
224
+ target: string;
212
225
  command: string;
213
226
  output: string;
214
- target: string;
215
227
  }[];
216
228
  application: {
217
229
  command: string;
@@ -222,11 +234,18 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
222
234
  stoppedBy: string;
223
235
  diagnostics: import("../application/session.mjs").ApplicationDiagnostics;
224
236
  };
237
+ lifecycle: import("../runtime/persistence.mjs").RuntimePersistenceOutcome<ManagerReceipt | Readonly<{
238
+ action: RuntimeAction;
239
+ durationMs: number;
240
+ output: string;
241
+ targets: readonly ManagerReceipt[];
242
+ }>>;
225
243
  }>;
226
- startRuntimeApplication: ({ flags, planOptions, session: commandSession, }: {
244
+ startRuntimeApplication: ({ flags, planOptions, session: commandSession, onRuntimeStarted, }: {
227
245
  flags: RehearsalCliFlags;
228
246
  planOptions: RehearsalConfigPathOptions;
229
247
  session: CliSessionState;
248
+ onRuntimeStarted?: () => void;
230
249
  }) => Promise<{
231
250
  started: ManagerReceipt | Readonly<{
232
251
  action: RuntimeAction;
@@ -241,9 +260,9 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
241
260
  targets: readonly ManagerReceipt[];
242
261
  }>;
243
262
  preparations: {
263
+ target: string;
244
264
  command: string;
245
265
  output: string;
246
- target: string;
247
266
  }[];
248
267
  command: string;
249
268
  session: import("../application/session.mjs").ApplicationSession;
@@ -257,11 +276,11 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
257
276
  prepareDependentTargets: ({ topology, environment, }: {
258
277
  topology: RuntimeTopology;
259
278
  environment: NodeJS.ProcessEnv;
260
- }) => {
279
+ }) => Promise<{
280
+ target: string;
261
281
  command: string;
262
282
  output: string;
263
- target: string;
264
- }[];
283
+ }[]>;
265
284
  proveRuntimeStack: ({ topology, planOptions, environment, }: {
266
285
  topology: RuntimeTopology;
267
286
  planOptions: RehearsalConfigPathOptions;
@@ -283,7 +302,7 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
283
302
  configPath?: string;
284
303
  confirmation?: string;
285
304
  targetName?: string;
286
- }) => ManagerReceipt;
305
+ }) => Promise<ManagerReceipt>;
287
306
  runRuntimeStack: ({ command, flags, planOptions, session, }: {
288
307
  command: RuntimeAction;
289
308
  flags: RehearsalCliFlags;