@rehearsal-db/core 0.1.0-rc.1 → 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 (62) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/README.md +39 -9
  3. package/dist/package.json +2 -2
  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 +11 -1
  17. package/dist/src/project/configuration_contract.mjs +5 -1
  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 +20 -4
  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 +13 -2
  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 +33 -16
  46. package/dist/src/targets/supabase_environment.d.mts +6 -1
  47. package/dist/src/targets/supabase_environment.mjs +27 -3
  48. package/dist/src/targets/supabase_process.d.mts +7 -0
  49. package/dist/src/targets/supabase_process.mjs +21 -0
  50. package/dist/src/targets/supabase_status.d.mts +12 -0
  51. package/dist/src/targets/supabase_status.mjs +56 -0
  52. package/dist/src/targets/target.d.mts +3 -1
  53. package/dist/src/targets/target.mjs +3 -1
  54. package/docs/README.md +10 -2
  55. package/docs/commands.md +127 -2
  56. package/docs/configuration.md +26 -6
  57. package/docs/getting-started.md +1 -1
  58. package/docs/index.md +27 -8
  59. package/docs/runtime-lifecycle.md +125 -0
  60. package/docs/stability.md +5 -4
  61. package/docs/troubleshooting.md +2 -2
  62. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,57 @@ 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
+
38
+ ## [0.1.0-rc.2] - 2026-10-09
39
+
40
+ ### Changed
41
+
42
+ - Container-engine startup is now explicit by default. Rehearsal starts Colima only
43
+ when a project deliberately enables `containerRuntime.autoStartColima`, reports that
44
+ action, and never silently stops the shared engine.
45
+ - Package-managed Supabase containers use Docker's non-resuming restart policy after
46
+ each start, preventing stopped Rehearsal stacks from waking merely because Docker or
47
+ Colima restarts. Exact Supabase project labels keep the change inside the selected
48
+ disposable target.
49
+ - Runtime diagnostics and a new lifecycle guide explain which commands need Docker,
50
+ what remains running, and how to finish a low-memory local session without pruning or
51
+ deleting persistent data.
52
+
53
+ ### Fixed
54
+
55
+ - A stopped Colima VM no longer restarts from a default Rehearsal configuration.
56
+ - Independent Test Lab acceptance now reaps exact workspace-owned interactive children
57
+ after success, failure, interruption, and timeout.
58
+
8
59
  ## [0.1.0-rc.1] - 2026-10-08
9
60
 
10
61
  ### Changed
@@ -554,7 +605,9 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
554
605
  publication uses short-lived trusted OIDC, and every release tag must already exist on
555
606
  protected `main`.
556
607
 
557
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.1...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
610
+ [0.1.0-rc.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.1...v0.1.0-rc.2
558
611
  [0.1.0-rc.1]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.18...v0.1.0-rc.1
559
612
  [0.1.0-beta.18]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.17...v0.1.0-beta.18
560
613
  [0.1.0-beta.17]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.16...v0.1.0-beta.17
package/README.md CHANGED
@@ -1,12 +1,39 @@
1
- # Rehearsal
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/Ddupasquier/rehearsal-db/main/docs/site/logo-mark.svg" width="104" height="104" alt="Rehearsal logo" />
3
+ </p>
2
4
 
3
- [![Monthly npm downloads](https://img.shields.io/npm/dm/%40rehearsal-db%2Fcore?style=flat-square&logo=npm&logoColor=white&label=npm%20downloads&labelColor=1c2a34&color=287b5c)](https://www.npmjs.com/package/@rehearsal-db/core)
5
+ <h1 align="center">Rehearsal</h1>
4
6
 
5
- [Read the documentation](https://ddupasquier.github.io/rehearsal-db/) · [View on npm](https://www.npmjs.com/package/@rehearsal-db/core) · [Report a problem](https://github.com/Ddupasquier/rehearsal-db/issues)
7
+ <p align="center"><strong>Your migration passed on an empty database. Production will not be empty.</strong></p>
6
8
 
7
- Rehearsal tests PostgreSQL and Supabase migrations on your computer before you run them
8
- anywhere important. It restores safe test data into a disposable local database, applies
9
- only the migrations you approve, and runs your project's own test command.
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@rehearsal-db/core"><img alt="npm version" src="https://img.shields.io/npm/v/%40rehearsal-db%2Fcore?style=flat-square&logo=npm&logoColor=white&labelColor=1c2a34&color=287b5c" /></a>
11
+ <a href="https://www.npmjs.com/package/@rehearsal-db/core"><img alt="Monthly npm downloads" src="https://img.shields.io/npm/dm/%40rehearsal-db%2Fcore?style=flat-square&logo=npm&logoColor=white&label=downloads&labelColor=1c2a34&color=287b5c" /></a>
12
+ <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/npm/l/%40rehearsal-db%2Fcore?style=flat-square&labelColor=1c2a34&color=287b5c" /></a>
13
+ <a href="https://ddupasquier.github.io/rehearsal-db/"><img alt="Read the documentation" src="https://img.shields.io/badge/docs-read_the_guide-269fab?style=flat-square&labelColor=1c2a34" /></a>
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="https://ddupasquier.github.io/rehearsal-db/">Read the documentation</a> ·
18
+ <a href="https://www.npmjs.com/package/@rehearsal-db/core">View on npm</a> ·
19
+ <a href="https://github.com/Ddupasquier/rehearsal-db/issues">Report a problem</a>
20
+ </p>
21
+
22
+ Most migration tests start with an empty database. Production does not.
23
+
24
+ Existing rows, relationships, authentication identities, stored files, and migration
25
+ history are where risky changes break. A migration can build successfully and still fail
26
+ against the data and application behavior you actually have.
27
+
28
+ **Rehearsal catches those problems before deployment.** It restores a reviewed,
29
+ sanitized baseline into a disposable local PostgreSQL or Supabase environment, applies
30
+ only the exact migrations you approve, and runs your application's own proof.
31
+
32
+ | An empty database cannot prove… | Rehearsal lets you verify… |
33
+ | ---------------------------------------------------- | ------------------------------------------------------------ |
34
+ | old rows still satisfy new constraints | the migration against a reviewed, production-shaped copy |
35
+ | relationships, identities, and stored files survive | database, Auth, and Storage behavior in one local sandbox |
36
+ | the application still works after the schema changes | your own tests and hands-on flows before anything is shipped |
10
37
 
11
38
  Projects with a separate publication or read-model database can declare it as a
12
39
  dependent target. Rehearsal then manages the complete isolated runtime stack.
@@ -17,7 +44,7 @@ it through a reviewed privacy policy, and retire the access again. It is never p
17
44
  `run`, `reset`, `migrate`, or application launch. Rehearsal is not a backup system or a
18
45
  production deployment tool.
19
46
 
20
- > Rehearsal is in public beta. Use it on a branch and keep a working backup of your
47
+ > **Pre-1.0 safety note:** use Rehearsal on a branch and keep a working backup of your
21
48
  > project.
22
49
 
23
50
  ## What you need
@@ -37,7 +64,7 @@ your migration is intended to change.
37
64
  From your project directory:
38
65
 
39
66
  ```bash
40
- npm install --save-dev @rehearsal-db/core@beta
67
+ npm install --save-dev @rehearsal-db/core
41
68
  npx rehearsal
42
69
  ```
43
70
 
@@ -89,7 +116,9 @@ source-owner approval; see [Standalone workflow](docs/standalone-workflow.md).
89
116
 
90
117
  `npx rehearsal open` starts and verifies the existing local runtime, keeps the configured
91
118
  app available until you press `Ctrl+C`, and preserves database and Storage changes for
92
- 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.
93
122
 
94
123
  For an approved production-shaped copy, the optional sequence is `source plan`, `source
95
124
  apply`, `refresh`, and `source retire`. `refresh` verifies the replacement before it
@@ -145,6 +174,7 @@ Reference:
145
174
 
146
175
  - [CLI commands](docs/commands.md)
147
176
  - [Configuration](docs/configuration.md)
177
+ - [Container runtime lifecycle](docs/runtime-lifecycle.md) — keep Docker and Supabase use on demand
148
178
  - [TypeScript types](docs/typescript.md)
149
179
  - [Baselines](docs/baselines.md)
150
180
  - [Sanitization](docs/sanitization.md)
package/dist/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.3",
4
4
  "private": false,
5
- "description": "Safely rehearse PostgreSQL and Supabase migrations against sanitized, production-shaped data.",
5
+ "description": "Catch PostgreSQL and Supabase migrations that pass on empty databases but fail on real data—before deploy.",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/Ddupasquier/rehearsal-db.git"
@@ -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
  ]