@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.
- package/CHANGELOG.md +54 -1
- package/README.md +39 -9
- package/dist/package.json +2 -2
- package/dist/scripts/runtime/manage_database.mjs +17 -8
- package/dist/src/cli/application_command_handlers.mjs +9 -4
- package/dist/src/cli/basic_command_handlers.mjs +1 -0
- package/dist/src/cli/inspection_command_handlers.mjs +27 -0
- package/dist/src/cli/project_commands.d.mts +9 -1
- package/dist/src/cli/project_commands.mjs +2 -2
- package/dist/src/cli/rehearsal.mjs +77 -65
- package/dist/src/cli/runtime_action_handlers.mjs +36 -9
- package/dist/src/cli/runtime_commands.d.mts +26 -7
- package/dist/src/cli/runtime_commands.mjs +94 -36
- package/dist/src/cli/source_commands.d.mts +12 -2
- package/dist/src/cli/source_commands.mjs +24 -15
- package/dist/src/project/configuration_contract.d.mts +11 -1
- package/dist/src/project/configuration_contract.mjs +5 -1
- package/dist/src/project/configuration_normalization.d.mts +4 -0
- package/dist/src/project/configuration_normalization.mjs +11 -1
- package/dist/src/project/configuration_rendering.mjs +20 -4
- package/dist/src/project/configuration_resolution.d.mts +4 -0
- package/dist/src/project/configuration_schema.d.mts +1 -0
- package/dist/src/project/configuration_schema.mjs +4 -0
- package/dist/src/runtime/lifecycle_engine.mjs +10 -2
- package/dist/src/runtime/persistence.d.mts +19 -0
- package/dist/src/runtime/persistence.mjs +40 -0
- package/dist/src/runtime/plan.d.mts +5 -0
- package/dist/src/runtime/plan.mjs +13 -2
- package/dist/src/runtime/status.d.mts +30 -0
- package/dist/src/runtime/status.mjs +141 -0
- package/dist/src/shared/cancellation.d.mts +18 -0
- package/dist/src/shared/cancellation.mjs +80 -0
- package/dist/src/shared/diagnostics.d.mts +1 -0
- package/dist/src/shared/diagnostics.mjs +1 -0
- package/dist/src/shared/operation_guard.d.mts +12 -0
- package/dist/src/shared/operation_guard.mjs +91 -2
- package/dist/src/shared/owned_process.d.mts +30 -0
- package/dist/src/shared/owned_process.mjs +117 -0
- package/dist/src/targets/postgresql.d.mts +0 -4
- package/dist/src/targets/postgresql.mjs +37 -12
- package/dist/src/targets/postgresql_process.d.mts +8 -0
- package/dist/src/targets/postgresql_process.mjs +34 -0
- package/dist/src/targets/postgresql_status.d.mts +21 -0
- package/dist/src/targets/postgresql_status.mjs +51 -0
- package/dist/src/targets/supabase.mjs +33 -16
- package/dist/src/targets/supabase_environment.d.mts +6 -1
- package/dist/src/targets/supabase_environment.mjs +27 -3
- package/dist/src/targets/supabase_process.d.mts +7 -0
- package/dist/src/targets/supabase_process.mjs +21 -0
- package/dist/src/targets/supabase_status.d.mts +12 -0
- package/dist/src/targets/supabase_status.mjs +56 -0
- package/dist/src/targets/target.d.mts +3 -1
- package/dist/src/targets/target.mjs +3 -1
- package/docs/README.md +10 -2
- package/docs/commands.md +127 -2
- package/docs/configuration.md +26 -6
- package/docs/getting-started.md +1 -1
- package/docs/index.md +27 -8
- package/docs/runtime-lifecycle.md +125 -0
- package/docs/stability.md +5 -4
- package/docs/troubleshooting.md +2 -2
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
5
|
+
<h1 align="center">Rehearsal</h1>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
<p align="center"><strong>Your migration passed on an empty database. Production will not be empty.</strong></p>
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
>
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.1.0-rc.3",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
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
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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({
|
|
93
|
+
? await acquireProjectOperation({
|
|
94
|
+
projectRoot,
|
|
95
|
+
command: guided ? "guide" : command,
|
|
96
|
+
})
|
|
93
97
|
: null;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
planOptions,
|
|
134
|
-
guided: true,
|
|
164
|
+
assertInstalledPackageFingerprint({
|
|
165
|
+
packageRoot,
|
|
166
|
+
expected: activePackageFingerprint,
|
|
135
167
|
});
|
|
136
168
|
}
|
|
137
169
|
finally {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
170
|
+
activePackageFingerprint = null;
|
|
171
|
+
await operation.release();
|
|
141
172
|
}
|
|
142
|
-
if (flags.exitGuidedSession)
|
|
143
|
-
break;
|
|
144
173
|
}
|
|
145
174
|
}
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
]
|