@rehearsal-db/core 0.1.0-rc.2 → 0.1.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/README.md +3 -1
- package/dist/package.json +1 -1
- 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 +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/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 +8 -0
- 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 +31 -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 +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/commands.md +127 -2
- package/docs/configuration.md +12 -0
- package/docs/runtime-lifecycle.md +31 -5
- package/package.json +1 -1
|
@@ -13,7 +13,11 @@ import { buildRehearsalPlan, runRehearsalDoctor } from "../runtime/plan.mjs";
|
|
|
13
13
|
import { inspectRuntimeTopologyCandidates, loadRuntimeTopology, } from "../runtime/topology.mjs";
|
|
14
14
|
import { RehearsalError, normalizeRehearsalError, redactDiagnosticValue, } from "../shared/diagnostics.mjs";
|
|
15
15
|
import { assertInstalledPackageFingerprint } from "../shared/operation_guard.mjs";
|
|
16
|
+
import { getOperationCancellationSignal, throwIfOperationCancelled, } from "../shared/cancellation.mjs";
|
|
17
|
+
import { runOwnedProcess } from "../shared/owned_process.mjs";
|
|
16
18
|
import { buildRuntimeManagerArguments } from "../targets/target.mjs";
|
|
19
|
+
import { parseRuntimeStatusSnapshot, renderRuntimeStatusSnapshot, } from "../runtime/status.mjs";
|
|
20
|
+
import { applyRuntimePersistence } from "../runtime/persistence.mjs";
|
|
17
21
|
import { formatDuration, isHumanTerminal, promptForConfirmation, terminalStyle, useStyledPrompts, } from "./terminal.mjs";
|
|
18
22
|
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
19
23
|
export const buildRuntimeStackPlan = async (planOptions) => {
|
|
@@ -38,6 +42,11 @@ export const buildRuntimeStackPlan = async (planOptions) => {
|
|
|
38
42
|
candidateCount: candidates.candidateCount,
|
|
39
43
|
candidateSha256: candidates.candidateSha256,
|
|
40
44
|
},
|
|
45
|
+
lifecycle: {
|
|
46
|
+
run: topology.primary.config.lifecycle.run,
|
|
47
|
+
open: topology.primary.config.lifecycle.open,
|
|
48
|
+
dataOnStop: "preserved",
|
|
49
|
+
},
|
|
41
50
|
execution: [
|
|
42
51
|
"verify every immutable baseline and migration prefix",
|
|
43
52
|
"restore and migrate the primary runtime",
|
|
@@ -47,6 +56,9 @@ export const buildRuntimeStackPlan = async (planOptions) => {
|
|
|
47
56
|
.map((target) => `run ${target.name} preparation: ${target.declaration.prepareCommand}`),
|
|
48
57
|
...topology.dependents.map((target) => `run ${target.name} proof: ${target.config.application.proofCommand}`),
|
|
49
58
|
`run primary proof: ${topology.primary.config.application.proofCommand}`,
|
|
59
|
+
topology.primary.config.lifecycle.run === "stop-after-run"
|
|
60
|
+
? "stop every database target after proofs while preserving its data"
|
|
61
|
+
: "keep every database target available until an explicit stop",
|
|
50
62
|
],
|
|
51
63
|
guarantee: "Every database target is isolated by its own project ID, ports, artifact directory, and local-only safety policy. No production resources will be contacted.",
|
|
52
64
|
};
|
|
@@ -256,15 +268,18 @@ export const topologyEnvironmentFiles = (topology) => ({
|
|
|
256
268
|
target.paths.applicationEnvironment,
|
|
257
269
|
])),
|
|
258
270
|
});
|
|
259
|
-
const runProjectCommand = ({ source, configuredRoot, label, project, environment = {}, }) => {
|
|
271
|
+
const runProjectCommand = async ({ source, configuredRoot, label, project, environment = {}, }) => {
|
|
260
272
|
const [command, ...args] = parseCommand(source);
|
|
261
|
-
const
|
|
273
|
+
const cancellationSignal = getOperationCancellationSignal();
|
|
274
|
+
const result = await runOwnedProcess({
|
|
275
|
+
command,
|
|
276
|
+
args,
|
|
262
277
|
cwd: configuredRoot,
|
|
263
|
-
encoding: "utf8",
|
|
264
278
|
env: projectCommandEnvironment(environment),
|
|
265
|
-
|
|
266
|
-
|
|
279
|
+
stdin: "ignore",
|
|
280
|
+
...(cancellationSignal ? { signal: cancellationSignal } : {}),
|
|
267
281
|
});
|
|
282
|
+
throwIfOperationCancelled();
|
|
268
283
|
if (result.error || result.status !== 0) {
|
|
269
284
|
throw new RehearsalError({
|
|
270
285
|
category: "application_proof_failure",
|
|
@@ -286,7 +301,7 @@ const runProjectCommand = ({ source, configuredRoot, label, project, environment
|
|
|
286
301
|
};
|
|
287
302
|
};
|
|
288
303
|
export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, getActivePackageFingerprint, }) => {
|
|
289
|
-
const runManager = ({ action, flags, configPath = flags.configPath, confirmation = flags.confirmation, targetName, }) => {
|
|
304
|
+
const runManager = async ({ action, flags, configPath = flags.configPath, confirmation = flags.confirmation, targetName, }) => {
|
|
290
305
|
const fingerprint = getActivePackageFingerprint();
|
|
291
306
|
if (fingerprint) {
|
|
292
307
|
assertInstalledPackageFingerprint({
|
|
@@ -299,6 +314,7 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
|
|
|
299
314
|
action,
|
|
300
315
|
...(configPath === undefined ? {} : { configPath }),
|
|
301
316
|
...(confirmation === undefined ? {} : { confirmation }),
|
|
317
|
+
structuredResult: action === "status",
|
|
302
318
|
});
|
|
303
319
|
const runtimeStartedMs = performance.now();
|
|
304
320
|
const actionDescription = {
|
|
@@ -321,13 +337,25 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
|
|
|
321
337
|
else if (isHumanTerminal(flags)) {
|
|
322
338
|
console.log(`\n${terminalStyle(flags, "1;36", action === "run" ? "REHEARSING" : "REHEARSAL")}\n${terminalStyle(flags, "36", "→")} ${describedAction}. This can take a moment.`);
|
|
323
339
|
}
|
|
324
|
-
const
|
|
340
|
+
const cancellationSignal = getOperationCancellationSignal();
|
|
341
|
+
// VS Code extensions launch the package CLI through Electron's executable in
|
|
342
|
+
// Node mode. Preserve that mode only for Rehearsal's nested Node manager;
|
|
343
|
+
// project-owned application commands continue to receive the reduced
|
|
344
|
+
// project environment above.
|
|
345
|
+
const managerEnvironment = {
|
|
346
|
+
...(cancellationSignal ? { REHEARSAL_CANCELLABLE_OPERATION: "1" } : {}),
|
|
347
|
+
...(process.env.ELECTRON_RUN_AS_NODE === "1"
|
|
348
|
+
? { ELECTRON_RUN_AS_NODE: "1" }
|
|
349
|
+
: {}),
|
|
350
|
+
};
|
|
351
|
+
const result = await runOwnedProcess({
|
|
352
|
+
command: process.execPath,
|
|
353
|
+
args,
|
|
325
354
|
cwd: projectRoot,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
stdio: ["inherit", "pipe", "pipe"],
|
|
329
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
355
|
+
env: projectCommandEnvironment(managerEnvironment),
|
|
356
|
+
...(cancellationSignal ? { signal: cancellationSignal } : {}),
|
|
330
357
|
});
|
|
358
|
+
throwIfOperationCancelled();
|
|
331
359
|
const currentFingerprint = getActivePackageFingerprint();
|
|
332
360
|
if (currentFingerprint) {
|
|
333
361
|
assertInstalledPackageFingerprint({
|
|
@@ -396,11 +424,17 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
|
|
|
396
424
|
else if (isHumanTerminal(flags)) {
|
|
397
425
|
console.log(`${terminalStyle(flags, "32", "✓")} Local runtime step completed in ${formatDuration(durationMs)}.`);
|
|
398
426
|
}
|
|
427
|
+
const runtimeStatus = action === "status"
|
|
428
|
+
? parseRuntimeStatusSnapshot(JSON.parse(String(result.stdout ?? "")))
|
|
429
|
+
: undefined;
|
|
399
430
|
return {
|
|
400
431
|
action,
|
|
401
432
|
target: targetName ?? "primary",
|
|
402
433
|
durationMs: Math.round(durationMs * 100) / 100,
|
|
403
|
-
output:
|
|
434
|
+
output: runtimeStatus
|
|
435
|
+
? renderRuntimeStatusSnapshot(runtimeStatus)
|
|
436
|
+
: String(redactDiagnosticValue(String(result.stdout ?? "").trim())),
|
|
437
|
+
...(runtimeStatus ? { runtimeStatus } : {}),
|
|
404
438
|
};
|
|
405
439
|
};
|
|
406
440
|
const runRuntimeStack = async ({ command, flags, planOptions, session, }) => {
|
|
@@ -410,16 +444,17 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
|
|
|
410
444
|
const targets = reverse
|
|
411
445
|
? [...topology.targets].reverse()
|
|
412
446
|
: topology.targets;
|
|
413
|
-
const results =
|
|
447
|
+
const results = [];
|
|
448
|
+
for (const target of targets) {
|
|
414
449
|
const confirmation = session.targetConfirmations?.get(target.configPath);
|
|
415
|
-
|
|
450
|
+
results.push(await runManager({
|
|
416
451
|
action: command,
|
|
417
452
|
flags,
|
|
418
453
|
configPath: target.configPath,
|
|
419
454
|
...(confirmation === undefined ? {} : { confirmation }),
|
|
420
455
|
...(topology.targets.length > 1 ? { targetName: target.name } : {}),
|
|
421
|
-
});
|
|
422
|
-
}
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
423
458
|
if (results.length === 1)
|
|
424
459
|
return { topology, runtime: results[0] };
|
|
425
460
|
return {
|
|
@@ -449,22 +484,24 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
|
|
|
449
484
|
: { environment: options.environment }),
|
|
450
485
|
});
|
|
451
486
|
};
|
|
452
|
-
const prepareDependentTargets = ({ topology, environment, }) =>
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
487
|
+
const prepareDependentTargets = async ({ topology, environment, }) => {
|
|
488
|
+
const preparations = [];
|
|
489
|
+
for (const target of topology.dependents) {
|
|
490
|
+
if (!target.declaration?.prepareCommand)
|
|
491
|
+
continue;
|
|
492
|
+
preparations.push({
|
|
457
493
|
target: target.name,
|
|
458
|
-
...runProjectCommand({
|
|
494
|
+
...(await runProjectCommand({
|
|
459
495
|
source: target.declaration.prepareCommand,
|
|
460
496
|
configuredRoot: topology.projectRoot,
|
|
461
497
|
label: `preparation command for ${target.name}`,
|
|
462
498
|
project: topology.primary.config.project.name,
|
|
463
499
|
environment,
|
|
464
|
-
}),
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
500
|
+
})),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return preparations;
|
|
504
|
+
};
|
|
468
505
|
const proveRuntimeStack = async ({ topology, planOptions, environment, }) => {
|
|
469
506
|
const dependentProofs = [];
|
|
470
507
|
for (const target of topology.dependents) {
|
|
@@ -479,7 +516,7 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
|
|
|
479
516
|
});
|
|
480
517
|
return { applicationProof, dependentProofs };
|
|
481
518
|
};
|
|
482
|
-
const startRuntimeApplication = async ({ flags, planOptions, session: commandSession, }) => {
|
|
519
|
+
const startRuntimeApplication = async ({ flags, planOptions, session: commandSession, onRuntimeStarted = () => undefined, }) => {
|
|
483
520
|
const topology = await loadRuntimeTopology(planOptions);
|
|
484
521
|
const application = topology.primary.config.application;
|
|
485
522
|
if (!application.readiness) {
|
|
@@ -505,13 +542,14 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
|
|
|
505
542
|
planOptions,
|
|
506
543
|
session: commandSession,
|
|
507
544
|
});
|
|
545
|
+
onRuntimeStarted();
|
|
508
546
|
const verified = await runRuntimeStack({
|
|
509
547
|
command: "verify",
|
|
510
548
|
flags,
|
|
511
549
|
planOptions,
|
|
512
550
|
session: commandSession,
|
|
513
551
|
});
|
|
514
|
-
const preparations = prepareDependentTargets({
|
|
552
|
+
const preparations = await prepareDependentTargets({
|
|
515
553
|
topology,
|
|
516
554
|
environment: topologyCommandEnvironment(topology),
|
|
517
555
|
});
|
|
@@ -531,15 +569,34 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
|
|
|
531
569
|
};
|
|
532
570
|
};
|
|
533
571
|
const openRuntimeApplication = async ({ flags, planOptions, session: commandSession, onReady = () => undefined, }) => {
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
572
|
+
const topology = await loadRuntimeTopology(planOptions);
|
|
573
|
+
let runtimeStarted = false;
|
|
574
|
+
const { result, lifecycle } = await applyRuntimePersistence({
|
|
575
|
+
mode: topology.primary.config.lifecycle.open,
|
|
576
|
+
canStop: () => runtimeStarted,
|
|
577
|
+
operation: async () => {
|
|
578
|
+
const active = await startRuntimeApplication({
|
|
579
|
+
flags,
|
|
580
|
+
planOptions,
|
|
581
|
+
session: commandSession,
|
|
582
|
+
onRuntimeStarted: () => {
|
|
583
|
+
runtimeStarted = true;
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
const closed = await holdApplicationSession({
|
|
587
|
+
session: active.session,
|
|
588
|
+
onReady,
|
|
589
|
+
});
|
|
590
|
+
return { active, closed };
|
|
591
|
+
},
|
|
592
|
+
stop: async () => (await runRuntimeStack({
|
|
593
|
+
command: "stop",
|
|
594
|
+
flags,
|
|
595
|
+
planOptions,
|
|
596
|
+
session: commandSession,
|
|
597
|
+
})).runtime,
|
|
542
598
|
});
|
|
599
|
+
const { active, closed } = result;
|
|
543
600
|
return {
|
|
544
601
|
action: "open",
|
|
545
602
|
started: active.started,
|
|
@@ -551,6 +608,7 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
|
|
|
551
608
|
stoppedBy: closed.signal,
|
|
552
609
|
diagnostics: closed.diagnostics,
|
|
553
610
|
},
|
|
611
|
+
lifecycle,
|
|
554
612
|
};
|
|
555
613
|
};
|
|
556
614
|
const prepareCandidateConfirmation = async ({ command, flags, planOptions, session, }) => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* CLI orchestration for temporary production-source access and safe baseline
|
|
3
3
|
* replacement. Domain validation remains in src/source and src/baseline.
|
|
4
4
|
*/
|
|
5
|
+
import { activateBaselineGeneration } from "../baseline/artifact.mjs";
|
|
5
6
|
import type { RehearsalConfigPathOptions } from "../project/configuration.mjs";
|
|
6
7
|
import type { RehearsalCliFlags } from "./arguments.mjs";
|
|
7
8
|
import type { CliSessionState } from "./command_contract.mjs";
|
|
@@ -53,6 +54,10 @@ export declare const loadPreparationContext: (planOptions: RehearsalConfigPathOp
|
|
|
53
54
|
containerRuntime: Readonly<{
|
|
54
55
|
autoStartColima: boolean;
|
|
55
56
|
}>;
|
|
57
|
+
lifecycle: Readonly<{
|
|
58
|
+
run: "keep-until-stop" | "stop-after-run";
|
|
59
|
+
open: "keep-until-stop" | "stop-on-application-exit";
|
|
60
|
+
}>;
|
|
56
61
|
cleanup: Readonly<{
|
|
57
62
|
retainBaselineGenerations: number;
|
|
58
63
|
}>;
|
|
@@ -201,6 +206,10 @@ export declare const buildRefreshWorkflowPlan: (planOptions: RehearsalConfigPath
|
|
|
201
206
|
containerRuntime: Readonly<{
|
|
202
207
|
autoStartColima: boolean;
|
|
203
208
|
}>;
|
|
209
|
+
lifecycle: Readonly<{
|
|
210
|
+
run: "keep-until-stop" | "stop-after-run";
|
|
211
|
+
open: "keep-until-stop" | "stop-on-application-exit";
|
|
212
|
+
}>;
|
|
204
213
|
cleanup: Readonly<{
|
|
205
214
|
retainBaselineGenerations: number;
|
|
206
215
|
}>;
|
|
@@ -323,7 +332,7 @@ export declare const buildRefreshWorkflowPlan: (planOptions: RehearsalConfigPath
|
|
|
323
332
|
digest: string;
|
|
324
333
|
};
|
|
325
334
|
}>;
|
|
326
|
-
export declare const createRefreshWorkflow: ({ runRuntimeStack, prepareDependentTargets, topologyCommandEnvironment, buildPlan, refreshBaseline, }: {
|
|
335
|
+
export declare const createRefreshWorkflow: ({ runRuntimeStack, prepareDependentTargets, topologyCommandEnvironment, buildPlan, refreshBaseline, activateBaseline, }: {
|
|
327
336
|
runRuntimeStack: (input: {
|
|
328
337
|
command: "reset";
|
|
329
338
|
flags: RehearsalCliFlags;
|
|
@@ -336,10 +345,11 @@ export declare const createRefreshWorkflow: ({ runRuntimeStack, prepareDependent
|
|
|
336
345
|
prepareDependentTargets: (input: {
|
|
337
346
|
topology: RuntimeTopology;
|
|
338
347
|
environment: NodeJS.ProcessEnv;
|
|
339
|
-
}) => unknown[]
|
|
348
|
+
}) => Promise<unknown[]>;
|
|
340
349
|
topologyCommandEnvironment: (topology: RuntimeTopology) => NodeJS.ProcessEnv;
|
|
341
350
|
buildPlan?: (options: RehearsalConfigPathOptions) => Promise<RefreshPlanContext>;
|
|
342
351
|
refreshBaseline?: typeof runSourceBaselineRefresh;
|
|
352
|
+
activateBaseline?: typeof activateBaselineGeneration;
|
|
343
353
|
}) => ({ flags, planOptions, session, }: SourceCommandInput & {
|
|
344
354
|
session: CliSessionState;
|
|
345
355
|
}) => Promise<{
|
|
@@ -14,6 +14,7 @@ import { refreshPostgresqlBaseline } from "../source/baseline.mjs";
|
|
|
14
14
|
import { resolvePrivacyMappedAssets } from "../source/privacy_paths.mjs";
|
|
15
15
|
import { applyPostgresqlSourceAccess, planPostgresqlSourceAccessRetirement, resolvePostgresqlSourceStatePaths, retirePostgresqlSourceAccess, } from "../source/postgresql_access.mjs";
|
|
16
16
|
import { loadRuntimeTopology } from "../runtime/topology.mjs";
|
|
17
|
+
import { isOperationCancelled, throwIfOperationCancelled, withOperationCancellationShield, } from "../shared/cancellation.mjs";
|
|
17
18
|
const parseSourceAccessReceipt = (source) => {
|
|
18
19
|
const parsed = JSON.parse(source);
|
|
19
20
|
if (typeof parsed.targetFingerprint !== "string" ||
|
|
@@ -224,7 +225,7 @@ export const buildRefreshWorkflowPlan = async (planOptions) => {
|
|
|
224
225
|
},
|
|
225
226
|
};
|
|
226
227
|
};
|
|
227
|
-
export const createRefreshWorkflow = ({ runRuntimeStack, prepareDependentTargets, topologyCommandEnvironment, buildPlan = buildRefreshWorkflowPlan, refreshBaseline = runSourceBaselineRefresh, }) => async ({ flags, planOptions, session, }) => {
|
|
228
|
+
export const createRefreshWorkflow = ({ runRuntimeStack, prepareDependentTargets, topologyCommandEnvironment, buildPlan = buildRefreshWorkflowPlan, refreshBaseline = runSourceBaselineRefresh, activateBaseline = activateBaselineGeneration, }) => async ({ flags, planOptions, session, }) => {
|
|
228
229
|
const context = await buildPlan(planOptions);
|
|
229
230
|
if (!flags.refreshConfirmation) {
|
|
230
231
|
return { mode: "preview", plan: context.plan };
|
|
@@ -236,40 +237,48 @@ export const createRefreshWorkflow = ({ runRuntimeStack, prepareDependentTargets
|
|
|
236
237
|
let runtime;
|
|
237
238
|
let preparations = [];
|
|
238
239
|
try {
|
|
240
|
+
throwIfOperationCancelled();
|
|
239
241
|
refreshed = await refreshBaseline({ planOptions });
|
|
242
|
+
throwIfOperationCancelled();
|
|
240
243
|
const stack = await runRuntimeStack({
|
|
241
244
|
command: "reset",
|
|
242
245
|
flags,
|
|
243
246
|
planOptions,
|
|
244
247
|
session,
|
|
245
248
|
});
|
|
249
|
+
throwIfOperationCancelled();
|
|
246
250
|
runtime = stack.runtime;
|
|
247
|
-
preparations = prepareDependentTargets({
|
|
251
|
+
preparations = await prepareDependentTargets({
|
|
248
252
|
topology: stack.topology,
|
|
249
253
|
environment: topologyCommandEnvironment(stack.topology),
|
|
250
254
|
});
|
|
255
|
+
throwIfOperationCancelled();
|
|
251
256
|
}
|
|
252
257
|
catch (error) {
|
|
253
258
|
if (refreshed) {
|
|
254
|
-
await activateBaselineGeneration({
|
|
255
|
-
artifactRoot: context.loaded.paths.artifactDirectory,
|
|
256
|
-
generationId: context.plan.review.currentBaseline,
|
|
257
|
-
});
|
|
258
259
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
260
|
+
await withOperationCancellationShield(async () => {
|
|
261
|
+
await activateBaseline({
|
|
262
|
+
artifactRoot: context.loaded.paths.artifactDirectory,
|
|
263
|
+
generationId: context.plan.review.currentBaseline,
|
|
264
|
+
});
|
|
265
|
+
const rollback = await runRuntimeStack({
|
|
266
|
+
command: "reset",
|
|
267
|
+
flags,
|
|
268
|
+
planOptions,
|
|
269
|
+
session,
|
|
270
|
+
});
|
|
271
|
+
await prepareDependentTargets({
|
|
272
|
+
topology: rollback.topology,
|
|
273
|
+
environment: topologyCommandEnvironment(rollback.topology),
|
|
274
|
+
});
|
|
268
275
|
});
|
|
269
276
|
}
|
|
270
277
|
catch (rollbackError) {
|
|
271
278
|
throw new AggregateError([error, rollbackError], "The refreshed runtime failed and the previous local runtime could not be restored automatically. The previous immutable baseline is active; run rehearsal reset after correcting the reported problem.");
|
|
272
279
|
}
|
|
280
|
+
if (isOperationCancelled(error))
|
|
281
|
+
throw error;
|
|
273
282
|
throw new Error("The refreshed runtime failed verification. Rehearsal restored the previous baseline and local runtime.", { cause: error });
|
|
274
283
|
}
|
|
275
284
|
throw error;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Public configuration types, format version, and stable defaults. */
|
|
2
2
|
export type RehearsalConfigVersion = 1;
|
|
3
|
+
export type RunPersistenceMode = "keep-until-stop" | "stop-after-run";
|
|
4
|
+
export type OpenPersistenceMode = "keep-until-stop" | "stop-on-application-exit";
|
|
3
5
|
export interface RehearsalConfig {
|
|
4
6
|
schemaVersion: RehearsalConfigVersion;
|
|
5
7
|
project: {
|
|
@@ -38,6 +40,10 @@ export interface RehearsalConfig {
|
|
|
38
40
|
containerRuntime?: {
|
|
39
41
|
autoStartColima?: boolean;
|
|
40
42
|
};
|
|
43
|
+
lifecycle?: {
|
|
44
|
+
run?: RunPersistenceMode;
|
|
45
|
+
open?: OpenPersistenceMode;
|
|
46
|
+
};
|
|
41
47
|
cleanup?: {
|
|
42
48
|
retainBaselineGenerations?: number;
|
|
43
49
|
};
|
|
@@ -108,6 +114,10 @@ export declare const REHEARSAL_DEFAULTS: Readonly<{
|
|
|
108
114
|
containerRuntime: Readonly<{
|
|
109
115
|
autoStartColima: false;
|
|
110
116
|
}>;
|
|
117
|
+
lifecycle: Readonly<{
|
|
118
|
+
run: RunPersistenceMode;
|
|
119
|
+
open: OpenPersistenceMode;
|
|
120
|
+
}>;
|
|
111
121
|
cleanup: Readonly<{
|
|
112
122
|
retainBaselineGenerations: 2;
|
|
113
123
|
}>;
|
|
@@ -3,6 +3,10 @@ export const REHEARSAL_CONFIG_VERSION = 1;
|
|
|
3
3
|
export const REHEARSAL_DEFAULTS = Object.freeze({
|
|
4
4
|
baseline: Object.freeze({ artifactDirectory: ".rehearsal" }),
|
|
5
5
|
containerRuntime: Object.freeze({ autoStartColima: false }),
|
|
6
|
+
lifecycle: Object.freeze({
|
|
7
|
+
run: "keep-until-stop",
|
|
8
|
+
open: "keep-until-stop",
|
|
9
|
+
}),
|
|
6
10
|
cleanup: Object.freeze({ retainBaselineGenerations: 2 }),
|
|
7
11
|
runtime: Object.freeze({
|
|
8
12
|
applicationUrl: "http://localhost:5175",
|
|
@@ -41,6 +41,10 @@ export declare const normalizeRehearsalConfig: (input: unknown) => Readonly<{
|
|
|
41
41
|
containerRuntime: Readonly<{
|
|
42
42
|
autoStartColima: boolean;
|
|
43
43
|
}>;
|
|
44
|
+
lifecycle: Readonly<{
|
|
45
|
+
run: "keep-until-stop" | "stop-after-run";
|
|
46
|
+
open: "keep-until-stop" | "stop-on-application-exit";
|
|
47
|
+
}>;
|
|
44
48
|
cleanup: Readonly<{
|
|
45
49
|
retainBaselineGenerations: number;
|
|
46
50
|
}>;
|
|
@@ -8,6 +8,12 @@ import { validateConfigurationSchema } from "./configuration_schema.mjs";
|
|
|
8
8
|
import { assertBoolean, assertCommand, assertDatabaseIdentifier, assertEnvironmentKey, assertEnvironmentReference, assertHttpStatus, assertIdentifier, assertKnownKeys, assertLoopbackUrl, assertNonEmptyString, assertPlainObject, assertPort, assertPostgresImage, assertPositiveInteger, assertRelativePath, assertStringArray, } from "./configuration_validation.mjs";
|
|
9
9
|
const LOOPBACK_HOSTS = new Set(REHEARSAL_DEFAULTS.safety.allowedHosts);
|
|
10
10
|
const SUPPORTED_SUPABASE_OAUTH_PROVIDERS = new Set(["github", "google"]);
|
|
11
|
+
const assertChoice = (value, choices, path) => {
|
|
12
|
+
if (typeof value !== "string" || !choices.includes(value)) {
|
|
13
|
+
throw new Error(`${path} must be one of: ${choices.join(", ")}.`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
11
17
|
const normalizeApplicationEnvironmentMappings = (value = {}) => {
|
|
12
18
|
const mappings = assertPlainObject(value, "config.application.environmentVariables");
|
|
13
19
|
return Object.freeze(Object.fromEntries(Object.entries(mappings).map(([key, reference]) => {
|
|
@@ -17,7 +23,7 @@ const normalizeApplicationEnvironmentMappings = (value = {}) => {
|
|
|
17
23
|
})));
|
|
18
24
|
};
|
|
19
25
|
export const normalizeRehearsalConfig = (input) => {
|
|
20
|
-
const { root, project, runtime, runtimeTarget, supabase, postgresql, baseline, containerRuntime, cleanup, preparation, dependentTargets, application, readiness, httpProofs, safety, verification, authentication, } = validateConfigurationSchema(input);
|
|
26
|
+
const { root, project, runtime, runtimeTarget, supabase, postgresql, baseline, containerRuntime, lifecycle, cleanup, preparation, dependentTargets, application, readiness, httpProofs, safety, verification, authentication, } = validateConfigurationSchema(input);
|
|
21
27
|
const projectName = assertIdentifier(project.name, "config.project.name");
|
|
22
28
|
const normalizedDependentTargets = dependentTargets.map((target, index) => {
|
|
23
29
|
const path = `config.dependentTargets[${index}]`;
|
|
@@ -190,6 +196,10 @@ export const normalizeRehearsalConfig = (input) => {
|
|
|
190
196
|
autoStartColima: assertBoolean(containerRuntime.autoStartColima ??
|
|
191
197
|
REHEARSAL_DEFAULTS.containerRuntime.autoStartColima, "config.containerRuntime.autoStartColima"),
|
|
192
198
|
}),
|
|
199
|
+
lifecycle: Object.freeze({
|
|
200
|
+
run: assertChoice(lifecycle.run ?? REHEARSAL_DEFAULTS.lifecycle.run, ["keep-until-stop", "stop-after-run"], "config.lifecycle.run"),
|
|
201
|
+
open: assertChoice(lifecycle.open ?? REHEARSAL_DEFAULTS.lifecycle.open, ["keep-until-stop", "stop-on-application-exit"], "config.lifecycle.open"),
|
|
202
|
+
}),
|
|
193
203
|
cleanup: Object.freeze({
|
|
194
204
|
retainBaselineGenerations: assertPositiveInteger(cleanup.retainBaselineGenerations ??
|
|
195
205
|
REHEARSAL_DEFAULTS.cleanup.retainBaselineGenerations, "config.cleanup.retainBaselineGenerations"),
|
|
@@ -67,6 +67,13 @@ export default defineRehearsalConfig({
|
|
|
67
67
|
autoStartColima: false,
|
|
68
68
|
},
|
|
69
69
|
|
|
70
|
+
// Choose when Rehearsal stops its database targets. Stopping preserves all
|
|
71
|
+
// database and Storage changes; only reset or discard removes runtime state.
|
|
72
|
+
lifecycle: {
|
|
73
|
+
run: "keep-until-stop", // Or "stop-after-run" for bounded automated checks.
|
|
74
|
+
open: "keep-until-stop", // Or "stop-on-application-exit" after Ctrl+C/Ctrl+Z.
|
|
75
|
+
},
|
|
76
|
+
|
|
70
77
|
// Keep this many baseline generations, always including the active one.
|
|
71
78
|
cleanup: {
|
|
72
79
|
retainBaselineGenerations: 2,
|
|
@@ -173,6 +180,13 @@ export default defineRehearsalConfig({
|
|
|
173
180
|
autoStartColima: false,
|
|
174
181
|
},
|
|
175
182
|
|
|
183
|
+
// Choose when Rehearsal stops its database targets. Stopping preserves all
|
|
184
|
+
// database changes; only reset or discard removes runtime state.
|
|
185
|
+
lifecycle: {
|
|
186
|
+
run: "keep-until-stop", // Or "stop-after-run" for bounded automated checks.
|
|
187
|
+
open: "keep-until-stop", // Or "stop-on-application-exit" after Ctrl+C/Ctrl+Z.
|
|
188
|
+
},
|
|
189
|
+
|
|
176
190
|
// Keep this many baseline generations, always including the active one.
|
|
177
191
|
cleanup: {
|
|
178
192
|
retainBaselineGenerations: 2,
|
|
@@ -41,6 +41,10 @@ export declare const loadRehearsalConfig: ({ projectRoot, configPath, }?: Rehear
|
|
|
41
41
|
containerRuntime: Readonly<{
|
|
42
42
|
autoStartColima: boolean;
|
|
43
43
|
}>;
|
|
44
|
+
lifecycle: Readonly<{
|
|
45
|
+
run: "keep-until-stop" | "stop-after-run";
|
|
46
|
+
open: "keep-until-stop" | "stop-on-application-exit";
|
|
47
|
+
}>;
|
|
44
48
|
cleanup: Readonly<{
|
|
45
49
|
retainBaselineGenerations: number;
|
|
46
50
|
}>;
|
|
@@ -9,6 +9,7 @@ export interface ValidatedConfigurationSchema {
|
|
|
9
9
|
postgresql: PlainObject | null;
|
|
10
10
|
baseline: PlainObject;
|
|
11
11
|
containerRuntime: PlainObject;
|
|
12
|
+
lifecycle: PlainObject;
|
|
12
13
|
cleanup: PlainObject;
|
|
13
14
|
preparation: PlainObject | null;
|
|
14
15
|
dependentTargets: PlainObject[];
|
|
@@ -16,6 +16,7 @@ export const validateConfigurationSchema = (input) => {
|
|
|
16
16
|
"postgresql",
|
|
17
17
|
"baseline",
|
|
18
18
|
"containerRuntime",
|
|
19
|
+
"lifecycle",
|
|
19
20
|
"cleanup",
|
|
20
21
|
"preparation",
|
|
21
22
|
"runtimePolicy",
|
|
@@ -71,6 +72,8 @@ export const validateConfigurationSchema = (input) => {
|
|
|
71
72
|
assertKnownKeys(baseline, ["artifactDirectory", "sanitizationPolicy"], "config.baseline");
|
|
72
73
|
const containerRuntime = assertPlainObject(root.containerRuntime ?? {}, "config.containerRuntime");
|
|
73
74
|
assertKnownKeys(containerRuntime, ["autoStartColima"], "config.containerRuntime");
|
|
75
|
+
const lifecycle = assertPlainObject(root.lifecycle ?? {}, "config.lifecycle");
|
|
76
|
+
assertKnownKeys(lifecycle, ["run", "open"], "config.lifecycle");
|
|
74
77
|
const cleanup = assertPlainObject(root.cleanup ?? {}, "config.cleanup");
|
|
75
78
|
assertKnownKeys(cleanup, ["retainBaselineGenerations"], "config.cleanup");
|
|
76
79
|
const preparation = root.preparation === undefined
|
|
@@ -147,6 +150,7 @@ export const validateConfigurationSchema = (input) => {
|
|
|
147
150
|
postgresql,
|
|
148
151
|
baseline,
|
|
149
152
|
containerRuntime,
|
|
153
|
+
lifecycle,
|
|
150
154
|
cleanup,
|
|
151
155
|
preparation,
|
|
152
156
|
dependentTargets,
|
|
@@ -4,6 +4,7 @@ import { resolveActiveBaselinePaths, verifyActiveBaseline, } from "../baseline/a
|
|
|
4
4
|
import { readBoundRuntimeSanitizationPolicy } from "../baseline/sanitization_policy.mjs";
|
|
5
5
|
import { readMigrationFileInventory } from "./migration_history.mjs";
|
|
6
6
|
import { createCandidateMigrationReceipt } from "./restore.mjs";
|
|
7
|
+
import { reachOperationCancellationCheckpoint, withOperationCancellationShield, } from "../shared/cancellation.mjs";
|
|
7
8
|
export const readActiveRuntimeInput = async ({ artifactRoot, manifestPath, validate, }) => {
|
|
8
9
|
const [baseline, paths, policyBytes] = await Promise.all([
|
|
9
10
|
verifyActiveBaseline({ artifactRoot }),
|
|
@@ -70,10 +71,17 @@ export const readMatchingRuntimeMarker = async ({ path, expected, mismatchMessag
|
|
|
70
71
|
};
|
|
71
72
|
export const withOwnedRuntimeRollback = async ({ operation, rollback, }) => {
|
|
72
73
|
try {
|
|
73
|
-
|
|
74
|
+
const result = await operation();
|
|
75
|
+
await reachOperationCancellationCheckpoint();
|
|
76
|
+
return result;
|
|
74
77
|
}
|
|
75
78
|
catch (error) {
|
|
76
|
-
|
|
79
|
+
try {
|
|
80
|
+
await withOperationCancellationShield(rollback);
|
|
81
|
+
}
|
|
82
|
+
catch (rollbackError) {
|
|
83
|
+
throw new AggregateError([error, rollbackError], "The runtime operation failed and its owned runtime could not be removed automatically.");
|
|
84
|
+
}
|
|
77
85
|
throw error;
|
|
78
86
|
}
|
|
79
87
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Explicit, non-destructive persistence policy for project-owned runtimes. */
|
|
2
|
+
import type { OpenPersistenceMode, RunPersistenceMode } from "../project/configuration_contract.mjs";
|
|
3
|
+
export type RuntimePersistenceMode = RunPersistenceMode | OpenPersistenceMode;
|
|
4
|
+
export interface RuntimePersistenceOutcome<StopResult> {
|
|
5
|
+
readonly mode: RuntimePersistenceMode;
|
|
6
|
+
readonly runtimeState: "running" | "stopped";
|
|
7
|
+
readonly dataState: "preserved";
|
|
8
|
+
readonly stop: StopResult | null;
|
|
9
|
+
}
|
|
10
|
+
export declare const shouldStopRuntime: (mode: RuntimePersistenceMode) => boolean;
|
|
11
|
+
export declare const applyRuntimePersistence: <Result, StopResult>({ mode, operation, stop, canStop, }: {
|
|
12
|
+
mode: RuntimePersistenceMode;
|
|
13
|
+
operation: () => Promise<Result>;
|
|
14
|
+
stop: () => Promise<StopResult>;
|
|
15
|
+
canStop?: () => boolean;
|
|
16
|
+
}) => Promise<{
|
|
17
|
+
result: Result;
|
|
18
|
+
lifecycle: RuntimePersistenceOutcome<StopResult>;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Explicit, non-destructive persistence policy for project-owned runtimes. */
|
|
2
|
+
import { withOperationCancellationShield } from "../shared/cancellation.mjs";
|
|
3
|
+
export const shouldStopRuntime = (mode) => mode === "stop-after-run" || mode === "stop-on-application-exit";
|
|
4
|
+
export const applyRuntimePersistence = async ({ mode, operation, stop, canStop = () => true, }) => {
|
|
5
|
+
let result;
|
|
6
|
+
let operationFailed = false;
|
|
7
|
+
let operationError;
|
|
8
|
+
try {
|
|
9
|
+
result = await operation();
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
operationFailed = true;
|
|
13
|
+
operationError = error;
|
|
14
|
+
}
|
|
15
|
+
let stopResult = null;
|
|
16
|
+
let stopped = false;
|
|
17
|
+
if (shouldStopRuntime(mode) && canStop()) {
|
|
18
|
+
try {
|
|
19
|
+
stopResult = await withOperationCancellationShield(stop);
|
|
20
|
+
stopped = true;
|
|
21
|
+
}
|
|
22
|
+
catch (stopError) {
|
|
23
|
+
if (operationFailed) {
|
|
24
|
+
throw new AggregateError([operationError, stopError], "The Rehearsal operation failed and its owned runtime could not be stopped automatically.");
|
|
25
|
+
}
|
|
26
|
+
throw stopError;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (operationFailed)
|
|
30
|
+
throw operationError;
|
|
31
|
+
return {
|
|
32
|
+
result: result,
|
|
33
|
+
lifecycle: Object.freeze({
|
|
34
|
+
mode,
|
|
35
|
+
runtimeState: stopped ? "stopped" : "running",
|
|
36
|
+
dataState: "preserved",
|
|
37
|
+
stop: stopResult,
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -49,6 +49,11 @@ export declare const buildRehearsalPlan: (options?: RehearsalConfigPathOptions)
|
|
|
49
49
|
sha256: string;
|
|
50
50
|
}[];
|
|
51
51
|
};
|
|
52
|
+
lifecycle: {
|
|
53
|
+
run: "keep-until-stop" | "stop-after-run";
|
|
54
|
+
open: "keep-until-stop" | "stop-on-application-exit";
|
|
55
|
+
dataOnStop: string;
|
|
56
|
+
};
|
|
52
57
|
execution: string[];
|
|
53
58
|
guarantee: string;
|
|
54
59
|
paths: {
|