@rehearsal-db/core 0.1.0-rc.2 → 0.1.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -1
- package/README.md +4 -1
- package/dist/package.json +4 -1
- package/dist/scripts/runtime/manage_database.mjs +17 -8
- package/dist/src/baseline/artifact.d.mts +14 -0
- package/dist/src/baseline/artifact.mjs +50 -0
- package/dist/src/cli/application_command_handlers.mjs +9 -4
- package/dist/src/cli/basic_command_handlers.mjs +2 -0
- package/dist/src/cli/inspection_command_handlers.mjs +45 -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 +71 -7
- package/dist/src/cli/runtime_commands.mjs +99 -36
- package/dist/src/cli/source_commands.d.mts +12 -2
- package/dist/src/cli/source_commands.mjs +24 -15
- package/dist/src/index.d.mts +2 -2
- package/dist/src/index.mjs +1 -1
- package/dist/src/project/configuration.d.mts +2 -0
- package/dist/src/project/configuration.mjs +1 -0
- package/dist/src/project/configuration_contract.d.mts +10 -0
- package/dist/src/project/configuration_contract.mjs +4 -0
- 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 +14 -0
- 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/project/project_discovery.d.mts +15 -0
- package/dist/src/project/project_discovery.mjs +350 -0
- package/dist/src/project/project_discovery_application.d.mts +16 -0
- package/dist/src/project/project_discovery_application.mjs +190 -0
- package/dist/src/project/project_discovery_contract.d.mts +78 -0
- package/dist/src/project/project_discovery_contract.mjs +43 -0
- package/dist/src/project/project_discovery_metadata.d.mts +14 -0
- package/dist/src/project/project_discovery_metadata.mjs +138 -0
- package/dist/src/runtime/lifecycle_engine.d.mts +6 -0
- package/dist/src/runtime/lifecycle_engine.mjs +24 -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 +18 -0
- package/dist/src/runtime/plan.mjs +27 -1
- package/dist/src/runtime/status.d.mts +32 -0
- package/dist/src/runtime/status.mjs +146 -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 +40 -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 +24 -0
- package/dist/src/targets/postgresql_status.mjs +64 -0
- package/dist/src/targets/supabase.mjs +34 -16
- package/dist/src/targets/supabase_environment.mjs +2 -0
- 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 +15 -0
- package/dist/src/targets/supabase_status.mjs +66 -0
- package/dist/src/targets/target.d.mts +3 -1
- package/dist/src/targets/target.mjs +3 -1
- package/docs/README.md +1 -0
- package/docs/architecture.md +8 -0
- package/docs/commands.md +188 -12
- package/docs/configuration.md +12 -0
- package/docs/runtime-lifecycle.md +31 -5
- package/docs/typescript.md +11 -0
- package/docs/vscode.md +53 -0
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,58 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## [0.1.0-rc.4] - 2026-10-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `discover --json` exposes a versioned, read-only Project Profile for editor clients,
|
|
13
|
+
separating proven facts, evidence-backed suggestions, and decisions that remain with
|
|
14
|
+
the user.
|
|
15
|
+
- Project discovery reports bounded application, package-manager, migration, target,
|
|
16
|
+
runtime, baseline-retention, configuration, and local-address metadata without reading
|
|
17
|
+
environment files, contacting databases, starting containers, or executing project code.
|
|
18
|
+
- Public TypeScript contracts now cover discovery results, retained baseline generations,
|
|
19
|
+
application readiness, cleanup policy, and structured PostgreSQL and Supabase runtime
|
|
20
|
+
state consumed by the Rehearsal VS Code extension.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Plans and status results expose the additional bounded metadata required for a
|
|
25
|
+
project-aware editor experience while retaining core ownership of all safety checks and
|
|
26
|
+
mutating operations.
|
|
27
|
+
- Documentation now includes the supported VS Code integration boundary, privacy model,
|
|
28
|
+
compatibility floor, and safe automatic-refresh behavior.
|
|
29
|
+
|
|
30
|
+
## [0.1.0-rc.3] - 2026-10-10
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `status --json` now exposes bounded, target-scoped runtime state without requiring
|
|
35
|
+
editor clients to parse human-formatted process output.
|
|
36
|
+
- `activity --json` reports idle, busy, stale, invalid, and unavailable project-operation
|
|
37
|
+
state without exposing lock tokens, process arguments, credentials, or private paths.
|
|
38
|
+
- Supported mutating operations now have an explicit cancellation contract: owned child
|
|
39
|
+
groups receive one bounded shutdown request, rollback and lock cleanup complete before
|
|
40
|
+
exit `130`, and unrelated processes and Docker resources remain untouched.
|
|
41
|
+
- Root configuration can now choose `stop-after-run` for bounded automated checks and
|
|
42
|
+
`stop-on-application-exit` for bounded hands-on sessions. Both modes stop only the
|
|
43
|
+
configured Rehearsal targets while preserving database and Storage volumes, applied
|
|
44
|
+
migrations, and sandbox edits for the next `start` or `open`.
|
|
45
|
+
- Plans, JSON results, terminal receipts, generated configuration comments, and the
|
|
46
|
+
lifecycle guide now report the selected persistence mode and resulting runtime state.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Transient application and helper processes are cleaned up before an opted-in runtime
|
|
51
|
+
stop after success, proof failure, readiness timeout, or supported interruption. The
|
|
52
|
+
compatibility default remains `keep-until-stop` for both `run` and `open`.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- Read-only runtime inspection now works when a VS Code extension launches the CLI
|
|
57
|
+
through Electron's executable in Node mode. That mode is preserved only for
|
|
58
|
+
Rehearsal's nested runtime manager and is not exposed to project-owned commands.
|
|
59
|
+
|
|
8
60
|
## [0.1.0-rc.2] - 2026-10-09
|
|
9
61
|
|
|
10
62
|
### Changed
|
|
@@ -575,7 +627,9 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
575
627
|
publication uses short-lived trusted OIDC, and every release tag must already exist on
|
|
576
628
|
protected `main`.
|
|
577
629
|
|
|
578
|
-
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.
|
|
630
|
+
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.4...HEAD
|
|
631
|
+
[0.1.0-rc.4]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.3...v0.1.0-rc.4
|
|
632
|
+
[0.1.0-rc.3]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.2...v0.1.0-rc.3
|
|
579
633
|
[0.1.0-rc.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.1...v0.1.0-rc.2
|
|
580
634
|
[0.1.0-rc.1]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.18...v0.1.0-rc.1
|
|
581
635
|
[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
|
|
@@ -164,6 +166,7 @@ Start here:
|
|
|
164
166
|
|
|
165
167
|
- [Documentation home](docs/index.md) — choose the shortest path for your goal
|
|
166
168
|
- [Getting started](docs/getting-started.md) — set up your own project
|
|
169
|
+
- [VS Code extension](docs/vscode.md) — use the project-aware visual workbench
|
|
167
170
|
- [Safe hands-on tutorial](docs/tutorial.md) — try the full flow in a disposable project
|
|
168
171
|
- [Troubleshooting](docs/troubleshooting.md) — fix common setup problems
|
|
169
172
|
- [Next steps](docs/roadmap.md) — see the current product sequence
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehearsal-db/core",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.4",
|
|
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": {
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"bin": {
|
|
23
23
|
"rehearsal": "dist/src/cli/rehearsal.mjs"
|
|
24
24
|
},
|
|
25
|
+
"rehearsalContracts": {
|
|
26
|
+
"projectDiscovery": 1
|
|
27
|
+
},
|
|
25
28
|
"exports": {
|
|
26
29
|
".": {
|
|
27
30
|
"types": "./types/configuration.d.mts",
|
|
@@ -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
|
+
}
|
|
@@ -66,6 +66,17 @@ export interface BaselinePrunePlan {
|
|
|
66
66
|
readonly retained: string[];
|
|
67
67
|
readonly removed: string[];
|
|
68
68
|
}
|
|
69
|
+
export interface BaselineGenerationSummary {
|
|
70
|
+
readonly generationId: string;
|
|
71
|
+
readonly createdAt: string | null;
|
|
72
|
+
readonly active: boolean;
|
|
73
|
+
readonly formatVersion: 1 | 2;
|
|
74
|
+
readonly rowCount: number;
|
|
75
|
+
readonly tableCount: number;
|
|
76
|
+
readonly migrationCount: number;
|
|
77
|
+
readonly migrationCutoff: string;
|
|
78
|
+
readonly storageAssetCount: number;
|
|
79
|
+
}
|
|
69
80
|
export declare const createBaselineGenerationId: ({ now, }?: {
|
|
70
81
|
now?: Date;
|
|
71
82
|
}) => string;
|
|
@@ -91,6 +102,9 @@ export declare const removeStagedBaselineGeneration: ({ artifactRoot, generation
|
|
|
91
102
|
export declare const resolveActiveBaselinePaths: ({ artifactRoot, }: {
|
|
92
103
|
artifactRoot: string;
|
|
93
104
|
}) => Promise<ActiveBaselinePaths>;
|
|
105
|
+
export declare const inspectBaselineGenerations: ({ artifactRoot, }: {
|
|
106
|
+
artifactRoot: string;
|
|
107
|
+
}) => Promise<readonly BaselineGenerationSummary[]>;
|
|
94
108
|
export declare const verifyActiveBaseline: ({ artifactRoot, }: {
|
|
95
109
|
artifactRoot: string;
|
|
96
110
|
}) => Promise<BaselineManifest>;
|
|
@@ -46,6 +46,12 @@ const assertGenerationId = (value) => {
|
|
|
46
46
|
}
|
|
47
47
|
return value;
|
|
48
48
|
};
|
|
49
|
+
const generationCreatedAt = (generationId) => {
|
|
50
|
+
const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z-/u.exec(generationId);
|
|
51
|
+
return match
|
|
52
|
+
? `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}Z`
|
|
53
|
+
: null;
|
|
54
|
+
};
|
|
49
55
|
const assertArtifactRoot = (artifactRoot) => {
|
|
50
56
|
const resolved = resolve(artifactRoot);
|
|
51
57
|
if (basename(resolved) !== ".rehearsal" || resolved === sep) {
|
|
@@ -364,6 +370,50 @@ export const resolveActiveBaselinePaths = async ({ artifactRoot, }) => {
|
|
|
364
370
|
schemaPath: join(generationDirectory, "production-schema.sql"),
|
|
365
371
|
};
|
|
366
372
|
};
|
|
373
|
+
export const inspectBaselineGenerations = async ({ artifactRoot, }) => {
|
|
374
|
+
const root = assertArtifactRoot(artifactRoot);
|
|
375
|
+
const active = basename((await resolveActiveBaselinePaths({ artifactRoot: root }))
|
|
376
|
+
.generationDirectory);
|
|
377
|
+
const generationsRoot = join(root, generationsDirectoryName);
|
|
378
|
+
await verifyNoSymlink(generationsRoot, "The Rehearsal generations directory");
|
|
379
|
+
const entries = await readdir(generationsRoot, { withFileTypes: true });
|
|
380
|
+
if (entries.length > 1_000) {
|
|
381
|
+
throw new Error("The Rehearsal baseline generation limit was exceeded.");
|
|
382
|
+
}
|
|
383
|
+
const summaries = [];
|
|
384
|
+
for (const entry of entries.sort((left, right) => right.name.localeCompare(left.name))) {
|
|
385
|
+
if (!entry.isDirectory() || !generationPattern.test(entry.name))
|
|
386
|
+
continue;
|
|
387
|
+
const generationDirectory = assertInsideRoot(root, join(generationsRoot, entry.name));
|
|
388
|
+
await verifyNoSymlink(generationDirectory, "A Rehearsal baseline generation");
|
|
389
|
+
const manifest = parseManifest(await readFile(join(generationDirectory, "baseline-manifest.json"), "utf8"));
|
|
390
|
+
if (manifest.generationId !== entry.name ||
|
|
391
|
+
![1, 2].includes(manifest.formatVersion) ||
|
|
392
|
+
!Number.isSafeInteger(manifest.rowCount) ||
|
|
393
|
+
manifest.rowCount < 0 ||
|
|
394
|
+
!isObject(manifest.tableCounts) ||
|
|
395
|
+
!isObject(manifest.migrations) ||
|
|
396
|
+
!isObject(manifest.storageAssets) ||
|
|
397
|
+
typeof manifest.migrationCutoff !== "string") {
|
|
398
|
+
throw new Error(`Rehearsal baseline generation ${entry.name} has an invalid manifest.`);
|
|
399
|
+
}
|
|
400
|
+
summaries.push({
|
|
401
|
+
generationId: entry.name,
|
|
402
|
+
createdAt: generationCreatedAt(entry.name),
|
|
403
|
+
active: entry.name === active,
|
|
404
|
+
formatVersion: manifest.formatVersion,
|
|
405
|
+
rowCount: manifest.rowCount,
|
|
406
|
+
tableCount: Object.keys(manifest.tableCounts).length,
|
|
407
|
+
migrationCount: Object.keys(manifest.migrations).length,
|
|
408
|
+
migrationCutoff: manifest.migrationCutoff,
|
|
409
|
+
storageAssetCount: Object.keys(manifest.storageAssets).length,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (!summaries.some(({ active: isActive }) => isActive)) {
|
|
413
|
+
throw new Error("The active Rehearsal baseline generation is unavailable.");
|
|
414
|
+
}
|
|
415
|
+
return Object.freeze(summaries.map((summary) => Object.freeze(summary)));
|
|
416
|
+
};
|
|
367
417
|
export const verifyActiveBaseline = async ({ artifactRoot, }) => {
|
|
368
418
|
const paths = await resolveActiveBaselinePaths({ artifactRoot });
|
|
369
419
|
const { generationDirectory } = paths;
|
|
@@ -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
|
}),
|
|
@@ -20,7 +20,9 @@ Commands:
|
|
|
20
20
|
identity claim --identity= --confirm-identity= Apply the exact local association
|
|
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
|
+
discover Inspect project metadata and explain setup suggestions
|
|
23
24
|
support Print a safe, copy-ready support report
|
|
25
|
+
activity Report idle, busy, stale, or invalid operation state
|
|
24
26
|
explain Show the immutable execution plan
|
|
25
27
|
run --dry-run Alias the exact explain plan without mutations
|
|
26
28
|
run --confirm-candidates= Execute reset, migration, and verification locally
|
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
/** Read-only readiness, support, planning, and inspection command handlers. */
|
|
2
2
|
import { collectRehearsalSupportReport } from "../project/support_report.mjs";
|
|
3
|
+
import { discoverProjectProfile } from "../project/project_discovery.mjs";
|
|
3
4
|
import { inspectRehearsalBaseline, inspectRehearsalMigrations, } from "../runtime/plan.mjs";
|
|
4
5
|
import { buildRuntimeStackPlan, runRuntimeStackDoctor, topologyCandidateSummary, } from "./runtime_commands.mjs";
|
|
5
6
|
import { renderBaseline, renderCandidates, renderDoctor, renderMigrations, renderPlan, renderSupportReport, } from "./renderers.mjs";
|
|
6
7
|
import { exactCommands, renderedCommand } from "./command_contract.mjs";
|
|
8
|
+
import { inspectProjectOperationState } from "../shared/operation_guard.mjs";
|
|
7
9
|
export const createInspectionCommandHandlers = () => [
|
|
10
|
+
exactCommands("discover", ["discover"], async ({ planOptions }) => {
|
|
11
|
+
const data = await discoverProjectProfile({
|
|
12
|
+
projectRoot: planOptions.projectRoot ?? process.cwd(),
|
|
13
|
+
});
|
|
14
|
+
return renderedCommand({
|
|
15
|
+
data,
|
|
16
|
+
render: (result) => [
|
|
17
|
+
`PROJECT PROFILE — READ ONLY`,
|
|
18
|
+
`Detected: ${result.detected.length}`,
|
|
19
|
+
`Suggested: ${result.suggested.length}`,
|
|
20
|
+
`Your decision: ${result.decisions.length}`,
|
|
21
|
+
`Configuration: ${result.configuration.state.replaceAll("_", " ")}`,
|
|
22
|
+
"",
|
|
23
|
+
"Run rehearsal discover --json for evidence and proposed configuration fields.",
|
|
24
|
+
].join("\n"),
|
|
25
|
+
});
|
|
26
|
+
}),
|
|
27
|
+
exactCommands("activity", ["activity"], async ({ planOptions }) => {
|
|
28
|
+
const data = await inspectProjectOperationState({
|
|
29
|
+
projectRoot: planOptions.projectRoot ?? process.cwd(),
|
|
30
|
+
});
|
|
31
|
+
return renderedCommand({
|
|
32
|
+
data,
|
|
33
|
+
render: (result) => {
|
|
34
|
+
if (result.state === "idle")
|
|
35
|
+
return "Rehearsal activity: idle.";
|
|
36
|
+
if (result.state === "invalid") {
|
|
37
|
+
return "Rehearsal activity: invalid lock state. No lock was changed; finish or diagnose the owning operation before retrying a mutation.";
|
|
38
|
+
}
|
|
39
|
+
const label = result.state === "busy" ? "busy" : "stale";
|
|
40
|
+
return [
|
|
41
|
+
`Rehearsal activity: ${label}.`,
|
|
42
|
+
`Operation: ${result.operation.kind}`,
|
|
43
|
+
`Started: ${result.operation.startedAt}`,
|
|
44
|
+
result.operation.rehearsalVersion
|
|
45
|
+
? `Rehearsal: ${result.operation.rehearsalVersion}`
|
|
46
|
+
: null,
|
|
47
|
+
]
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join("\n");
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}),
|
|
8
53
|
exactCommands("doctor", ["doctor"], async ({ guided, flags, planOptions }) => {
|
|
9
54
|
const data = await runRuntimeStackDoctor(planOptions);
|
|
10
55
|
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
|
]
|