@rehearsal-db/core 0.1.0-beta.13 → 0.1.0-beta.15
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 +93 -0
- package/README.md +4 -1
- package/dist/package.json +100 -0
- package/dist/scripts/install/create_root_config.d.mts +3 -0
- package/dist/scripts/install/create_root_config.mjs +13 -0
- package/dist/scripts/runtime/manage_database.d.mts +6 -0
- package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
- package/dist/src/application/session.d.mts +96 -0
- package/dist/src/application/session.mjs +380 -0
- package/dist/src/baseline/artifact.d.mts +119 -0
- package/dist/src/baseline/artifact.mjs +522 -0
- package/dist/src/baseline/builder.d.mts +19 -0
- package/dist/src/baseline/builder.mjs +142 -0
- package/dist/src/baseline/input_discovery.d.mts +13 -0
- package/dist/src/baseline/input_discovery.mjs +149 -0
- package/dist/src/baseline/policy_review.d.mts +56 -0
- package/dist/src/baseline/policy_review.mjs +73 -0
- package/dist/src/baseline/preparation.d.mts +63 -0
- package/dist/src/baseline/preparation.mjs +295 -0
- package/dist/src/baseline/privacy_engine.d.mts +162 -0
- package/dist/src/baseline/privacy_engine.mjs +1162 -0
- package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
- package/dist/src/baseline/privacy_structured_json.mjs +464 -0
- package/dist/src/baseline/sanitization_policy.d.mts +69 -0
- package/dist/src/baseline/sanitization_policy.mjs +227 -0
- package/dist/src/baseline/schema_snapshot.d.mts +19 -0
- package/dist/src/baseline/schema_snapshot.mjs +79 -0
- package/dist/src/cli/arguments.d.mts +43 -0
- package/dist/src/cli/arguments.mjs +135 -0
- package/dist/src/cli/guided.d.mts +51 -0
- package/dist/src/cli/guided.mjs +713 -0
- package/dist/src/cli/init_command.d.mts +21 -0
- package/dist/src/cli/init_command.mjs +90 -0
- package/dist/src/cli/project_commands.d.mts +157 -0
- package/dist/src/cli/project_commands.mjs +183 -0
- package/dist/src/cli/rehearsal.d.mts +7 -0
- package/dist/src/cli/rehearsal.mjs +706 -0
- package/dist/src/cli/renderers.d.mts +299 -0
- package/dist/src/cli/renderers.mjs +554 -0
- package/dist/src/cli/runtime_commands.d.mts +265 -0
- package/dist/src/cli/runtime_commands.mjs +599 -0
- package/dist/src/cli/source_commands.d.mts +432 -0
- package/dist/src/cli/source_commands.mjs +299 -0
- package/dist/src/cli/terminal.d.mts +59 -0
- package/dist/src/cli/terminal.mjs +230 -0
- package/dist/src/identity/claim.d.mts +144 -0
- package/dist/src/identity/claim.mjs +947 -0
- package/dist/src/identity/provider_configuration.d.mts +37 -0
- package/dist/src/identity/provider_configuration.mjs +132 -0
- package/dist/src/identity/storage.d.mts +21 -0
- package/dist/src/identity/storage.mjs +122 -0
- package/dist/src/index.d.mts +4 -0
- package/dist/src/index.mjs +3 -0
- package/dist/src/project/configuration.d.mts +181 -0
- package/dist/src/project/configuration.mjs +958 -0
- package/dist/src/project/install_scaffold.d.mts +42 -0
- package/dist/src/project/install_scaffold.mjs +263 -0
- package/dist/src/project/setup.d.mts +87 -0
- package/dist/src/project/setup.mjs +414 -0
- package/dist/src/project/support_report.d.mts +88 -0
- package/dist/src/project/support_report.mjs +83 -0
- package/dist/src/runtime/cleanup.d.mts +85 -0
- package/dist/src/runtime/cleanup.mjs +311 -0
- package/dist/src/runtime/migration_history.d.mts +51 -0
- package/dist/src/runtime/migration_history.mjs +170 -0
- package/dist/src/runtime/plan.d.mts +134 -0
- package/dist/src/runtime/plan.mjs +636 -0
- package/dist/src/runtime/policy.d.mts +66 -0
- package/dist/src/runtime/policy.mjs +369 -0
- package/dist/src/runtime/restore.d.mts +22 -0
- package/dist/src/runtime/restore.mjs +276 -0
- package/dist/src/runtime/service_environment.d.mts +11 -0
- package/dist/src/runtime/service_environment.mjs +58 -0
- package/dist/src/runtime/topology.d.mts +51 -0
- package/dist/src/runtime/topology.mjs +128 -0
- package/dist/src/shared/diagnostics.d.mts +97 -0
- package/dist/src/shared/diagnostics.mjs +170 -0
- package/dist/src/shared/human_output.d.mts +2 -0
- package/{src → dist/src}/shared/human_output.mjs +1 -3
- package/dist/src/shared/node_runtime.d.mts +14 -0
- package/dist/src/shared/node_runtime.mjs +25 -0
- package/dist/src/shared/operation_guard.d.mts +22 -0
- package/dist/src/shared/operation_guard.mjs +150 -0
- package/dist/src/shared/process_environment.d.mts +15 -0
- package/dist/src/shared/process_environment.mjs +50 -0
- package/dist/src/source/access.d.mts +165 -0
- package/dist/src/source/access.mjs +602 -0
- package/dist/src/source/asset_transfer.d.mts +27 -0
- package/dist/src/source/asset_transfer.mjs +202 -0
- package/dist/src/source/baseline.d.mts +66 -0
- package/dist/src/source/baseline.mjs +372 -0
- package/dist/src/source/external_reader.d.mts +42 -0
- package/dist/src/source/external_reader.mjs +278 -0
- package/dist/src/source/postgresql_access.d.mts +86 -0
- package/dist/src/source/postgresql_access.mjs +454 -0
- package/dist/src/source/privacy_paths.d.mts +10 -0
- package/dist/src/source/privacy_paths.mjs +27 -0
- package/dist/src/targets/driver.d.mts +2 -0
- package/dist/src/targets/driver.mjs +13 -0
- package/dist/src/targets/postgresql.d.mts +5 -0
- package/dist/src/targets/postgresql.mjs +641 -0
- package/dist/src/targets/supabase.d.mts +8 -0
- package/dist/src/targets/supabase.mjs +659 -0
- package/dist/src/targets/supabase_environment.d.mts +49 -0
- package/dist/src/targets/supabase_environment.mjs +162 -0
- package/dist/src/targets/target.d.mts +33 -0
- package/dist/src/targets/target.mjs +39 -0
- package/docs/README.md +1 -0
- package/docs/architecture.md +17 -9
- package/docs/configuration.md +49 -8
- package/docs/getting-started.md +28 -0
- package/docs/index.md +1 -0
- package/docs/production-source.md +43 -7
- package/docs/releasing.md +5 -3
- package/docs/roadmap.md +3 -2
- package/docs/sanitization.md +415 -14
- package/docs/standalone-workflow.md +12 -6
- package/docs/troubleshooting.md +5 -1
- package/docs/tutorial.md +5 -0
- package/docs/typescript.md +87 -0
- package/package.json +35 -29
- package/scripts/install/create_root_config.mjs +0 -36
- package/src/README.md +0 -17
- package/src/application/session.mjs +0 -435
- package/src/baseline/artifact.mjs +0 -684
- package/src/baseline/builder.mjs +0 -164
- package/src/baseline/input_discovery.mjs +0 -155
- package/src/baseline/policy_review.mjs +0 -92
- package/src/baseline/preparation.mjs +0 -323
- package/src/baseline/privacy_engine.mjs +0 -952
- package/src/baseline/sanitization_policy.mjs +0 -299
- package/src/baseline/schema_snapshot.mjs +0 -113
- package/src/cli/arguments.mjs +0 -126
- package/src/cli/guided.mjs +0 -814
- package/src/cli/init_command.mjs +0 -94
- package/src/cli/rehearsal.mjs +0 -970
- package/src/cli/renderers.mjs +0 -626
- package/src/cli/runtime_commands.mjs +0 -707
- package/src/cli/source_commands.mjs +0 -347
- package/src/cli/terminal.mjs +0 -275
- package/src/identity/claim.mjs +0 -1163
- package/src/identity/provider_configuration.mjs +0 -177
- package/src/identity/storage.mjs +0 -165
- package/src/project/configuration.mjs +0 -1337
- package/src/project/install_scaffold.mjs +0 -291
- package/src/project/setup.mjs +0 -466
- package/src/project/support_report.mjs +0 -105
- package/src/runtime/cleanup.mjs +0 -381
- package/src/runtime/migration_history.mjs +0 -220
- package/src/runtime/plan.mjs +0 -722
- package/src/runtime/policy.mjs +0 -438
- package/src/runtime/restore.mjs +0 -337
- package/src/runtime/service_environment.mjs +0 -82
- package/src/runtime/topology.mjs +0 -177
- package/src/shared/diagnostics.mjs +0 -207
- package/src/shared/node_runtime.mjs +0 -31
- package/src/shared/operation_guard.mjs +0 -162
- package/src/shared/process_environment.mjs +0 -68
- package/src/source/access.mjs +0 -753
- package/src/source/asset_transfer.mjs +0 -207
- package/src/source/baseline.mjs +0 -482
- package/src/source/external_reader.mjs +0 -255
- package/src/source/postgresql_access.mjs +0 -581
- package/src/source/privacy_paths.mjs +0 -38
- package/src/targets/postgresql.mjs +0 -808
- package/src/targets/supabase.mjs +0 -840
- package/src/targets/supabase_environment.mjs +0 -229
- package/src/targets/target.mjs +0 -66
- /package/{src/project → types}/configuration.d.mts +0 -0
|
@@ -1,707 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI orchestration for one or more isolated database runtimes and their
|
|
3
|
-
* project-owned proof commands.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
import { access } from "node:fs/promises";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { performance } from "node:perf_hooks";
|
|
10
|
-
import * as prompts from "@clack/prompts";
|
|
11
|
-
import {
|
|
12
|
-
holdApplicationSession,
|
|
13
|
-
startApplicationSession,
|
|
14
|
-
summarizeProjectCommandFailure,
|
|
15
|
-
} from "../application/session.mjs";
|
|
16
|
-
import { loadRehearsalConfig } from "../project/configuration.mjs";
|
|
17
|
-
import { buildRehearsalPlan, runRehearsalDoctor } from "../runtime/plan.mjs";
|
|
18
|
-
import {
|
|
19
|
-
inspectRuntimeTopologyCandidates,
|
|
20
|
-
loadRuntimeTopology,
|
|
21
|
-
} from "../runtime/topology.mjs";
|
|
22
|
-
import {
|
|
23
|
-
RehearsalError,
|
|
24
|
-
normalizeRehearsalError,
|
|
25
|
-
redactDiagnosticValue,
|
|
26
|
-
} from "../shared/diagnostics.mjs";
|
|
27
|
-
import { assertInstalledPackageFingerprint } from "../shared/operation_guard.mjs";
|
|
28
|
-
import { buildRuntimeManagerArguments } from "../targets/target.mjs";
|
|
29
|
-
import {
|
|
30
|
-
formatDuration,
|
|
31
|
-
isHumanTerminal,
|
|
32
|
-
promptForConfirmation,
|
|
33
|
-
terminalStyle,
|
|
34
|
-
useStyledPrompts,
|
|
35
|
-
} from "./terminal.mjs";
|
|
36
|
-
|
|
37
|
-
export const buildRuntimeStackPlan = async (planOptions) => {
|
|
38
|
-
const topology = await loadRuntimeTopology(planOptions);
|
|
39
|
-
if (topology.targets.length === 1) return buildRehearsalPlan(planOptions);
|
|
40
|
-
const targets = [];
|
|
41
|
-
for (const target of topology.targets) {
|
|
42
|
-
targets.push({
|
|
43
|
-
name: target.name,
|
|
44
|
-
declaration: target.declaration,
|
|
45
|
-
plan: await buildRehearsalPlan({
|
|
46
|
-
projectRoot: topology.projectRoot,
|
|
47
|
-
configPath: target.configPath,
|
|
48
|
-
}),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
const candidates = await inspectRuntimeTopologyCandidates(planOptions);
|
|
52
|
-
return {
|
|
53
|
-
targets,
|
|
54
|
-
migrations: {
|
|
55
|
-
candidateCount: candidates.candidateCount,
|
|
56
|
-
candidateSha256: candidates.candidateSha256,
|
|
57
|
-
},
|
|
58
|
-
execution: [
|
|
59
|
-
"verify every immutable baseline and migration prefix",
|
|
60
|
-
"restore and migrate the primary runtime",
|
|
61
|
-
"restore and migrate each dependent runtime in declared order",
|
|
62
|
-
...topology.dependents
|
|
63
|
-
.filter((target) => target.declaration.prepareCommand)
|
|
64
|
-
.map(
|
|
65
|
-
(target) =>
|
|
66
|
-
`run ${target.name} preparation: ${target.declaration.prepareCommand}`,
|
|
67
|
-
),
|
|
68
|
-
...topology.dependents.map(
|
|
69
|
-
(target) =>
|
|
70
|
-
`run ${target.name} proof: ${target.config.application.proofCommand}`,
|
|
71
|
-
),
|
|
72
|
-
`run primary proof: ${topology.primary.config.application.proofCommand}`,
|
|
73
|
-
],
|
|
74
|
-
guarantee:
|
|
75
|
-
"Every database target is isolated by its own project ID, ports, artifact directory, and local-only safety policy. No production resources will be contacted.",
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export const runRuntimeStackDoctor = async (planOptions) => {
|
|
80
|
-
let topology;
|
|
81
|
-
try {
|
|
82
|
-
topology = await loadRuntimeTopology(planOptions);
|
|
83
|
-
} catch (error) {
|
|
84
|
-
if (
|
|
85
|
-
String(error?.message ?? error).startsWith(
|
|
86
|
-
"No Rehearsal configuration was found",
|
|
87
|
-
)
|
|
88
|
-
) {
|
|
89
|
-
return runRehearsalDoctor(planOptions);
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
state: "NOT READY",
|
|
93
|
-
checks: [
|
|
94
|
-
{
|
|
95
|
-
id: "runtime-topology",
|
|
96
|
-
label: "Dependent runtime isolation",
|
|
97
|
-
status: "fail",
|
|
98
|
-
detail: redactDiagnosticValue(String(error?.message ?? error)),
|
|
99
|
-
remediation:
|
|
100
|
-
"Give every target a readable config with unique ports, project ID, environment file, and .rehearsal artifact directory.",
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
ambientHostedVariables: {
|
|
104
|
-
presentButQuarantined: [],
|
|
105
|
-
note: "Topology validation stopped before any runtime action.",
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
if (topology.targets.length === 1) return runRehearsalDoctor(planOptions);
|
|
110
|
-
const reports = [];
|
|
111
|
-
for (const target of topology.targets) {
|
|
112
|
-
reports.push({
|
|
113
|
-
target: target.name,
|
|
114
|
-
report: await runRehearsalDoctor({
|
|
115
|
-
projectRoot: topology.projectRoot,
|
|
116
|
-
configPath: target.configPath,
|
|
117
|
-
}),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
state: reports.every(({ report }) => report.state === "READY")
|
|
122
|
-
? "READY"
|
|
123
|
-
: "NOT READY",
|
|
124
|
-
checks: reports.flatMap(({ target, report }) =>
|
|
125
|
-
report.checks.map((check) => ({
|
|
126
|
-
...check,
|
|
127
|
-
id: `${target}:${check.id}`,
|
|
128
|
-
label: `${target}: ${check.label}`,
|
|
129
|
-
})),
|
|
130
|
-
),
|
|
131
|
-
ambientHostedVariables: {
|
|
132
|
-
presentButQuarantined: [
|
|
133
|
-
...new Set(
|
|
134
|
-
reports.flatMap(
|
|
135
|
-
({ report }) =>
|
|
136
|
-
report.ambientHostedVariables?.presentButQuarantined ?? [],
|
|
137
|
-
),
|
|
138
|
-
),
|
|
139
|
-
],
|
|
140
|
-
note: "Blocked ambient values are not inherited by Rehearsal child processes.",
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
export const topologyCandidateSummary = async (planOptions) => {
|
|
146
|
-
const inspection = await inspectRuntimeTopologyCandidates(planOptions);
|
|
147
|
-
const targets = inspection.targets.map((target) => ({
|
|
148
|
-
name: target.name,
|
|
149
|
-
configPath: target.configPath,
|
|
150
|
-
baselineGenerationId: target.summary.baselineGenerationId,
|
|
151
|
-
candidateSha256: target.summary.candidateSha256,
|
|
152
|
-
candidateCount: target.summary.candidateCount,
|
|
153
|
-
candidates: target.summary.candidates,
|
|
154
|
-
}));
|
|
155
|
-
return {
|
|
156
|
-
baselineGenerationId: targets[0].baselineGenerationId,
|
|
157
|
-
candidateSha256: inspection.candidateSha256,
|
|
158
|
-
candidateCount: inspection.candidateCount,
|
|
159
|
-
candidates: targets.flatMap((target) =>
|
|
160
|
-
target.candidates.map((candidate) => ({
|
|
161
|
-
...candidate,
|
|
162
|
-
target: target.name,
|
|
163
|
-
})),
|
|
164
|
-
),
|
|
165
|
-
targets,
|
|
166
|
-
topology: inspection.topology,
|
|
167
|
-
};
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const parseCommand = (source) => {
|
|
171
|
-
const values = [];
|
|
172
|
-
let current = "";
|
|
173
|
-
let quote = null;
|
|
174
|
-
for (const character of source) {
|
|
175
|
-
if (quote) {
|
|
176
|
-
if (character === quote) quote = null;
|
|
177
|
-
else current += character;
|
|
178
|
-
} else if (character === '"' || character === "'") quote = character;
|
|
179
|
-
else if (/\s/u.test(character)) {
|
|
180
|
-
if (current) values.push(current);
|
|
181
|
-
current = "";
|
|
182
|
-
} else current += character;
|
|
183
|
-
}
|
|
184
|
-
if (quote)
|
|
185
|
-
throw new Error("Application proof command contains an unmatched quote.");
|
|
186
|
-
if (current) values.push(current);
|
|
187
|
-
if (values.length === 0)
|
|
188
|
-
throw new Error("Application proof command is empty.");
|
|
189
|
-
return values;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const projectCommandEnvironment = (additional = {}) => ({
|
|
193
|
-
...Object.fromEntries(
|
|
194
|
-
[
|
|
195
|
-
"CI",
|
|
196
|
-
"COLORTERM",
|
|
197
|
-
"FORCE_COLOR",
|
|
198
|
-
"HOME",
|
|
199
|
-
"LANG",
|
|
200
|
-
"LC_ALL",
|
|
201
|
-
"NO_COLOR",
|
|
202
|
-
"PATH",
|
|
203
|
-
"SHELL",
|
|
204
|
-
"TERM",
|
|
205
|
-
"TMPDIR",
|
|
206
|
-
"USER",
|
|
207
|
-
].flatMap((key) =>
|
|
208
|
-
process.env[key] === undefined ? [] : [[key, process.env[key]]],
|
|
209
|
-
),
|
|
210
|
-
),
|
|
211
|
-
...additional,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const RUNTIME_STARTUP_FAILURE_PATTERN =
|
|
215
|
-
/container\b.*\b(?:is not ready|starting|unhealthy)|health(?:check)?\b.*\b(?:failed|starting|unhealthy)|out of memory|\boom\b|no space left|resource temporarily unavailable/iu;
|
|
216
|
-
|
|
217
|
-
export const classifyManagerFailure = ({
|
|
218
|
-
action,
|
|
219
|
-
output,
|
|
220
|
-
inferredCategory,
|
|
221
|
-
}) => {
|
|
222
|
-
if (RUNTIME_STARTUP_FAILURE_PATTERN.test(output)) {
|
|
223
|
-
return "runtime_dependency_failure";
|
|
224
|
-
}
|
|
225
|
-
if (
|
|
226
|
-
action === "migrate" ||
|
|
227
|
-
(action === "run" &&
|
|
228
|
-
/migrateRuntime|candidate migration|migration up/iu.test(output))
|
|
229
|
-
) {
|
|
230
|
-
return "migration_candidate_failure";
|
|
231
|
-
}
|
|
232
|
-
return inferredCategory;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
export const parseDockerCapacity = (output) => {
|
|
236
|
-
const [cpuText, memoryText, runningText, totalText] = output
|
|
237
|
-
.trim()
|
|
238
|
-
.split(/\s+/u);
|
|
239
|
-
const values = [cpuText, memoryText, runningText, totalText].map(Number);
|
|
240
|
-
if (!values.every(Number.isFinite)) return null;
|
|
241
|
-
const [cpus, memoryBytes, runningContainers, containers] = values;
|
|
242
|
-
return { cpus, memoryBytes, runningContainers, containers };
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
export const parseColimaMemory = (output) => {
|
|
246
|
-
const line = output
|
|
247
|
-
.split("\n")
|
|
248
|
-
.map((entry) => entry.trim())
|
|
249
|
-
.find((entry) => entry.startsWith("Mem:"));
|
|
250
|
-
if (!line) return null;
|
|
251
|
-
const columns = line.split(/\s+/u);
|
|
252
|
-
const totalBytes = Number(columns[1]);
|
|
253
|
-
const availableBytes = Number(columns[6]);
|
|
254
|
-
if (![totalBytes, availableBytes].every(Number.isFinite)) return null;
|
|
255
|
-
return { totalBytes, availableBytes };
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const diagnosticCommand = (command, args) =>
|
|
259
|
-
spawnSync(command, args, {
|
|
260
|
-
encoding: "utf8",
|
|
261
|
-
env: projectCommandEnvironment(),
|
|
262
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
263
|
-
timeout: 5_000,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const inspectContainerCapacity = () => {
|
|
267
|
-
const docker = diagnosticCommand("docker", [
|
|
268
|
-
"info",
|
|
269
|
-
"--format",
|
|
270
|
-
"{{.NCPU}} {{.MemTotal}} {{.ContainersRunning}} {{.Containers}}",
|
|
271
|
-
]);
|
|
272
|
-
const capacity =
|
|
273
|
-
docker.status === 0 ? parseDockerCapacity(docker.stdout) : null;
|
|
274
|
-
if (!capacity) return undefined;
|
|
275
|
-
|
|
276
|
-
const context = diagnosticCommand("docker", ["context", "show"]);
|
|
277
|
-
if (context.status !== 0 || context.stdout.trim() !== "colima") {
|
|
278
|
-
return { engine: "docker-compatible", ...capacity };
|
|
279
|
-
}
|
|
280
|
-
const memory = diagnosticCommand("colima", ["ssh", "--", "free", "-b"]);
|
|
281
|
-
return {
|
|
282
|
-
engine: "colima",
|
|
283
|
-
...capacity,
|
|
284
|
-
...(memory.status === 0
|
|
285
|
-
? { runtimeMemory: parseColimaMemory(memory.stdout) ?? undefined }
|
|
286
|
-
: {}),
|
|
287
|
-
};
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
export const topologyCommandEnvironment = (topology) =>
|
|
291
|
-
Object.fromEntries(
|
|
292
|
-
topology.targets.map((target) => [
|
|
293
|
-
target.primary
|
|
294
|
-
? "REHEARSAL_PRIMARY_ENV_FILE"
|
|
295
|
-
: `REHEARSAL_DEPENDENT_${target.name.toUpperCase().replaceAll("-", "_")}_ENV_FILE`,
|
|
296
|
-
target.paths.applicationEnvironment,
|
|
297
|
-
]),
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
export const topologyEnvironmentFiles = (topology) =>
|
|
301
|
-
Object.fromEntries(
|
|
302
|
-
topology.targets.map((target) => [
|
|
303
|
-
target.primary ? "primary" : target.name,
|
|
304
|
-
target.paths.applicationEnvironment,
|
|
305
|
-
]),
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
const runProjectCommand = ({
|
|
309
|
-
source,
|
|
310
|
-
configuredRoot,
|
|
311
|
-
label,
|
|
312
|
-
project,
|
|
313
|
-
environment = {},
|
|
314
|
-
}) => {
|
|
315
|
-
const [command, ...args] = parseCommand(source);
|
|
316
|
-
const result = spawnSync(command, args, {
|
|
317
|
-
cwd: configuredRoot,
|
|
318
|
-
encoding: "utf8",
|
|
319
|
-
env: projectCommandEnvironment(environment),
|
|
320
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
321
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
322
|
-
});
|
|
323
|
-
if (result.error || result.status !== 0) {
|
|
324
|
-
throw new RehearsalError({
|
|
325
|
-
category: "application_proof_failure",
|
|
326
|
-
code: "PROJECT_COMMAND_FAILED",
|
|
327
|
-
message: `The configured ${label} did not pass.`,
|
|
328
|
-
expected: `${source} exits successfully`,
|
|
329
|
-
actual: summarizeProjectCommandFailure(result),
|
|
330
|
-
context: { project, label },
|
|
331
|
-
refused:
|
|
332
|
-
"Rehearsal did not report the local runtime stack as fully verified.",
|
|
333
|
-
suggestions: [
|
|
334
|
-
"Run the command directly in the same project and correct the failure.",
|
|
335
|
-
],
|
|
336
|
-
cause: result.error,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
return {
|
|
340
|
-
command: source,
|
|
341
|
-
output: redactDiagnosticValue(String(result.stdout ?? "").trim()),
|
|
342
|
-
};
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
export const createRuntimeCommands = ({
|
|
346
|
-
packageRoot,
|
|
347
|
-
projectRoot,
|
|
348
|
-
managerPath,
|
|
349
|
-
getActivePackageFingerprint,
|
|
350
|
-
}) => {
|
|
351
|
-
const runManager = ({
|
|
352
|
-
action,
|
|
353
|
-
flags,
|
|
354
|
-
configPath = flags.configPath,
|
|
355
|
-
confirmation = flags.confirmation,
|
|
356
|
-
targetName,
|
|
357
|
-
}) => {
|
|
358
|
-
const fingerprint = getActivePackageFingerprint();
|
|
359
|
-
if (fingerprint) {
|
|
360
|
-
assertInstalledPackageFingerprint({
|
|
361
|
-
packageRoot,
|
|
362
|
-
expected: fingerprint,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
const args = buildRuntimeManagerArguments({
|
|
366
|
-
managerPath,
|
|
367
|
-
action,
|
|
368
|
-
configPath,
|
|
369
|
-
confirmation,
|
|
370
|
-
});
|
|
371
|
-
const runtimeStartedMs = performance.now();
|
|
372
|
-
const actionDescription = {
|
|
373
|
-
run: "Restoring the baseline and applying reviewed migrations",
|
|
374
|
-
start: "Starting the disposable local runtime",
|
|
375
|
-
migrate: "Applying reviewed migrations to the current runtime",
|
|
376
|
-
reset: "Restoring the immutable baseline",
|
|
377
|
-
status: "Inspecting the disposable local runtime",
|
|
378
|
-
stop: "Stopping the disposable local runtime",
|
|
379
|
-
discard: "Removing the disposable local runtime",
|
|
380
|
-
verify: "Verifying the current local runtime",
|
|
381
|
-
}[action];
|
|
382
|
-
const describedAction = targetName
|
|
383
|
-
? `${actionDescription} for ${targetName}`
|
|
384
|
-
: actionDescription;
|
|
385
|
-
const spinner = useStyledPrompts(flags) ? prompts.spinner() : null;
|
|
386
|
-
if (spinner) {
|
|
387
|
-
spinner.start(`${describedAction}. This can take a moment.`);
|
|
388
|
-
} else if (isHumanTerminal(flags)) {
|
|
389
|
-
console.log(
|
|
390
|
-
`\n${terminalStyle(flags, "1;36", action === "run" ? "REHEARSING" : "REHEARSAL")}\n${terminalStyle(flags, "36", "→")} ${describedAction}. This can take a moment.`,
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
const result = spawnSync(process.execPath, args, {
|
|
394
|
-
cwd: projectRoot,
|
|
395
|
-
encoding: "utf8",
|
|
396
|
-
env: projectCommandEnvironment(),
|
|
397
|
-
stdio: ["inherit", "pipe", "pipe"],
|
|
398
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
399
|
-
});
|
|
400
|
-
const currentFingerprint = getActivePackageFingerprint();
|
|
401
|
-
if (currentFingerprint) {
|
|
402
|
-
assertInstalledPackageFingerprint({
|
|
403
|
-
packageRoot,
|
|
404
|
-
expected: currentFingerprint,
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
if (result.error) {
|
|
408
|
-
if (spinner) spinner.stop(`${describedAction} did not complete.`);
|
|
409
|
-
throw result.error;
|
|
410
|
-
}
|
|
411
|
-
if (result.status !== 0) {
|
|
412
|
-
if (spinner) spinner.stop(`${describedAction} did not complete.`);
|
|
413
|
-
const completeOutput = [
|
|
414
|
-
String(result.stdout ?? ""),
|
|
415
|
-
String(result.stderr ?? ""),
|
|
416
|
-
]
|
|
417
|
-
.filter(Boolean)
|
|
418
|
-
.join("\n")
|
|
419
|
-
.trim();
|
|
420
|
-
const inferred = normalizeRehearsalError(
|
|
421
|
-
new Error(completeOutput || `Rehearsal ${action} failed.`),
|
|
422
|
-
);
|
|
423
|
-
const category = classifyManagerFailure({
|
|
424
|
-
action,
|
|
425
|
-
output: completeOutput,
|
|
426
|
-
inferredCategory: inferred.category,
|
|
427
|
-
});
|
|
428
|
-
const startupFailure = category === "runtime_dependency_failure";
|
|
429
|
-
const engineCapacity = startupFailure
|
|
430
|
-
? inspectContainerCapacity()
|
|
431
|
-
: undefined;
|
|
432
|
-
const conciseOutput = completeOutput
|
|
433
|
-
.replaceAll(String.fromCodePoint(27), "")
|
|
434
|
-
.split("\n")
|
|
435
|
-
.map((line) => line.trim())
|
|
436
|
-
.filter(Boolean)
|
|
437
|
-
.slice(-8)
|
|
438
|
-
.join("\n")
|
|
439
|
-
.slice(-2_000);
|
|
440
|
-
throw new RehearsalError({
|
|
441
|
-
category,
|
|
442
|
-
code: `MANAGER_${action.toUpperCase()}_FAILED`,
|
|
443
|
-
message: `The local Rehearsal ${action} operation did not complete.`,
|
|
444
|
-
expected: "the isolated runtime operation to finish and verify",
|
|
445
|
-
actual: conciseOutput || `exit ${result.status}`,
|
|
446
|
-
context: { action, ...(engineCapacity ? { engineCapacity } : {}) },
|
|
447
|
-
refused: "The runtime was not reported as trusted.",
|
|
448
|
-
suggestions: [
|
|
449
|
-
"Review the concise failure above, then rerun rehearsal doctor before retrying.",
|
|
450
|
-
...(startupFailure
|
|
451
|
-
? [
|
|
452
|
-
"Stop unrelated local stacks or increase Docker/Colima capacity, then retry. Rehearsal will not change engine settings or bypass service health checks.",
|
|
453
|
-
"Run docker stats --no-stream to identify active containers using the shared engine.",
|
|
454
|
-
]
|
|
455
|
-
: []),
|
|
456
|
-
"Use --debug only when the safe diagnostic detail is needed.",
|
|
457
|
-
],
|
|
458
|
-
cause: new Error(completeOutput || `exit ${result.status}`),
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
const durationMs = performance.now() - runtimeStartedMs;
|
|
462
|
-
if (spinner) {
|
|
463
|
-
spinner.stop(
|
|
464
|
-
`${describedAction} completed in ${formatDuration(durationMs)}.`,
|
|
465
|
-
);
|
|
466
|
-
} else if (isHumanTerminal(flags)) {
|
|
467
|
-
console.log(
|
|
468
|
-
`${terminalStyle(flags, "32", "✓")} Local runtime step completed in ${formatDuration(durationMs)}.`,
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
return {
|
|
472
|
-
action,
|
|
473
|
-
target: targetName ?? "primary",
|
|
474
|
-
durationMs: Math.round(durationMs * 100) / 100,
|
|
475
|
-
output: redactDiagnosticValue(String(result.stdout ?? "").trim()),
|
|
476
|
-
};
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
const runRuntimeStack = async ({ command, flags, planOptions }) => {
|
|
480
|
-
const topology =
|
|
481
|
-
flags.runtimeTopology ?? (await loadRuntimeTopology(planOptions));
|
|
482
|
-
const reverse = ["stop", "discard"].includes(command);
|
|
483
|
-
const targets = reverse
|
|
484
|
-
? [...topology.targets].reverse()
|
|
485
|
-
: topology.targets;
|
|
486
|
-
const results = targets.map((target) =>
|
|
487
|
-
runManager({
|
|
488
|
-
action: command,
|
|
489
|
-
flags,
|
|
490
|
-
configPath: target.configPath,
|
|
491
|
-
confirmation: flags.targetConfirmations?.get(target.configPath),
|
|
492
|
-
targetName: topology.targets.length > 1 ? target.name : undefined,
|
|
493
|
-
}),
|
|
494
|
-
);
|
|
495
|
-
if (results.length === 1) return { topology, runtime: results[0] };
|
|
496
|
-
return {
|
|
497
|
-
topology,
|
|
498
|
-
runtime: {
|
|
499
|
-
action: command,
|
|
500
|
-
durationMs:
|
|
501
|
-
Math.round(
|
|
502
|
-
results.reduce((total, result) => total + result.durationMs, 0) *
|
|
503
|
-
100,
|
|
504
|
-
) / 100,
|
|
505
|
-
output: results
|
|
506
|
-
.map((result) => `[${result.target}]\n${result.output}`)
|
|
507
|
-
.join("\n\n"),
|
|
508
|
-
targets: results,
|
|
509
|
-
},
|
|
510
|
-
};
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
const runApplicationProof = async (planOptions, options = {}) => {
|
|
514
|
-
const { config, projectRoot: configuredRoot } =
|
|
515
|
-
await loadRehearsalConfig(planOptions);
|
|
516
|
-
return runProjectCommand({
|
|
517
|
-
source: config.application.proofCommand,
|
|
518
|
-
configuredRoot,
|
|
519
|
-
label: options.targetName
|
|
520
|
-
? `application proof for ${options.targetName}`
|
|
521
|
-
: "application proof",
|
|
522
|
-
project: config.project.name,
|
|
523
|
-
environment: options.environment,
|
|
524
|
-
});
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
const prepareDependentTargets = ({ topology, environment }) =>
|
|
528
|
-
topology.dependents.flatMap((target) => {
|
|
529
|
-
if (!target.declaration.prepareCommand) return [];
|
|
530
|
-
return [
|
|
531
|
-
{
|
|
532
|
-
target: target.name,
|
|
533
|
-
...runProjectCommand({
|
|
534
|
-
source: target.declaration.prepareCommand,
|
|
535
|
-
configuredRoot: topology.projectRoot,
|
|
536
|
-
label: `preparation command for ${target.name}`,
|
|
537
|
-
project: topology.primary.config.project.name,
|
|
538
|
-
environment,
|
|
539
|
-
}),
|
|
540
|
-
},
|
|
541
|
-
];
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
const proveRuntimeStack = async ({ topology, planOptions, environment }) => {
|
|
545
|
-
const dependentProofs = [];
|
|
546
|
-
for (const target of topology.dependents) {
|
|
547
|
-
dependentProofs.push({
|
|
548
|
-
target: target.name,
|
|
549
|
-
...(await runApplicationProof(
|
|
550
|
-
{ ...planOptions, configPath: target.configPath },
|
|
551
|
-
{ targetName: target.name, environment },
|
|
552
|
-
)),
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
const applicationProof = await runApplicationProof(planOptions, {
|
|
556
|
-
targetName: topology.targets.length > 1 ? "primary" : undefined,
|
|
557
|
-
environment,
|
|
558
|
-
});
|
|
559
|
-
return { applicationProof, dependentProofs };
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
const openRuntimeApplication = async ({
|
|
563
|
-
flags,
|
|
564
|
-
planOptions,
|
|
565
|
-
onReady = () => undefined,
|
|
566
|
-
}) => {
|
|
567
|
-
const topology = await loadRuntimeTopology(planOptions);
|
|
568
|
-
const application = topology.primary.config.application;
|
|
569
|
-
if (!application.readiness) {
|
|
570
|
-
throw new Error(
|
|
571
|
-
"rehearsal open requires config.application.readiness so it can confirm the local application is usable.",
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
const runtimeReceipts = await Promise.all(
|
|
575
|
-
topology.targets.map((target) =>
|
|
576
|
-
access(join(target.paths.runtimeWorkdir, "baseline.json"))
|
|
577
|
-
.then(() => true)
|
|
578
|
-
.catch((error) => {
|
|
579
|
-
if (error?.code === "ENOENT") return false;
|
|
580
|
-
throw error;
|
|
581
|
-
}),
|
|
582
|
-
),
|
|
583
|
-
);
|
|
584
|
-
if (runtimeReceipts.some((present) => !present)) {
|
|
585
|
-
throw new Error(
|
|
586
|
-
"rehearsal open requires an existing verified runtime. Run rehearsal run first.",
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
flags.runtimeTopology = topology;
|
|
590
|
-
const started = await runRuntimeStack({
|
|
591
|
-
command: "start",
|
|
592
|
-
flags,
|
|
593
|
-
planOptions,
|
|
594
|
-
});
|
|
595
|
-
const verified = await runRuntimeStack({
|
|
596
|
-
command: "verify",
|
|
597
|
-
flags,
|
|
598
|
-
planOptions,
|
|
599
|
-
});
|
|
600
|
-
const preparations = prepareDependentTargets({
|
|
601
|
-
topology,
|
|
602
|
-
environment: topologyCommandEnvironment(topology),
|
|
603
|
-
});
|
|
604
|
-
const session = await startApplicationSession({
|
|
605
|
-
command: application.startCommand,
|
|
606
|
-
cwd: topology.projectRoot,
|
|
607
|
-
files: topologyEnvironmentFiles(topology),
|
|
608
|
-
mappings: application.environmentVariables,
|
|
609
|
-
readiness: application.readiness,
|
|
610
|
-
});
|
|
611
|
-
const closed = await holdApplicationSession({ session, onReady });
|
|
612
|
-
return {
|
|
613
|
-
action: "open",
|
|
614
|
-
started: started.runtime,
|
|
615
|
-
verified: verified.runtime,
|
|
616
|
-
preparations,
|
|
617
|
-
application: {
|
|
618
|
-
command: application.startCommand,
|
|
619
|
-
readiness: session.ready,
|
|
620
|
-
stoppedBy: closed.signal,
|
|
621
|
-
diagnostics: closed.diagnostics,
|
|
622
|
-
},
|
|
623
|
-
};
|
|
624
|
-
};
|
|
625
|
-
|
|
626
|
-
const prepareCandidateConfirmation = async ({
|
|
627
|
-
command,
|
|
628
|
-
flags,
|
|
629
|
-
planOptions,
|
|
630
|
-
}) => {
|
|
631
|
-
if (!["run", "migrate"].includes(command) || flags.dryRun) return true;
|
|
632
|
-
const summary = await topologyCandidateSummary(planOptions);
|
|
633
|
-
flags.runtimeTopology = summary.topology;
|
|
634
|
-
flags.targetConfirmations = new Map(
|
|
635
|
-
summary.targets.map((target) => [
|
|
636
|
-
target.configPath,
|
|
637
|
-
target.candidateSha256,
|
|
638
|
-
]),
|
|
639
|
-
);
|
|
640
|
-
if (summary.candidateCount === 0) return true;
|
|
641
|
-
if (flags.confirmation === summary.candidateSha256) return true;
|
|
642
|
-
const interactive =
|
|
643
|
-
!flags.json && process.stdin.isTTY && process.stdout.isTTY;
|
|
644
|
-
if (!interactive || flags.confirmation) {
|
|
645
|
-
throw new RehearsalError({
|
|
646
|
-
category: "migration_candidate_failure",
|
|
647
|
-
code: "CANDIDATE_CONFIRMATION_REQUIRED",
|
|
648
|
-
message: flags.confirmation
|
|
649
|
-
? "The candidate migration confirmation does not match the current candidate set."
|
|
650
|
-
: "Candidate migration confirmation is required in noninteractive use.",
|
|
651
|
-
expected: summary.candidateSha256,
|
|
652
|
-
actual: flags.confirmation ?? "missing",
|
|
653
|
-
context: {
|
|
654
|
-
command,
|
|
655
|
-
candidateCount: summary.candidateCount,
|
|
656
|
-
candidates: summary.candidates.map(
|
|
657
|
-
(migration) => `${migration.target}: ${migration.filename}`,
|
|
658
|
-
),
|
|
659
|
-
},
|
|
660
|
-
refused:
|
|
661
|
-
"Rehearsal did not reset, start, or modify the disposable runtime.",
|
|
662
|
-
suggestions: [
|
|
663
|
-
"Run rehearsal candidates, review every file, then pass the current digest with --confirm-candidates=.",
|
|
664
|
-
],
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
console.log(
|
|
668
|
-
[
|
|
669
|
-
"",
|
|
670
|
-
terminalStyle(
|
|
671
|
-
flags,
|
|
672
|
-
"1",
|
|
673
|
-
`${summary.candidateCount} candidate migration${summary.candidateCount === 1 ? "" : "s"}`,
|
|
674
|
-
),
|
|
675
|
-
"",
|
|
676
|
-
...summary.candidates.map((migration) =>
|
|
677
|
-
summary.targets.length > 1
|
|
678
|
-
? ` → ${migration.target}: ${migration.filename}`
|
|
679
|
-
: ` → ${migration.filename}`,
|
|
680
|
-
),
|
|
681
|
-
"",
|
|
682
|
-
`Candidate set: ${summary.candidateSha256.slice(0, 12)}`,
|
|
683
|
-
"",
|
|
684
|
-
`Rehearsal will apply only this exact set to ${summary.targets.length === 1 ? "the disposable local runtime" : "the isolated local runtime stack"}.`,
|
|
685
|
-
].join("\n"),
|
|
686
|
-
);
|
|
687
|
-
const accepted = await promptForConfirmation(
|
|
688
|
-
`Apply exactly ${summary.candidateCount === 1 ? "this migration" : "these migrations"}?`,
|
|
689
|
-
flags,
|
|
690
|
-
);
|
|
691
|
-
if (!accepted) {
|
|
692
|
-
console.log("No changes made.");
|
|
693
|
-
return false;
|
|
694
|
-
}
|
|
695
|
-
flags.confirmation = summary.candidateSha256;
|
|
696
|
-
return true;
|
|
697
|
-
};
|
|
698
|
-
|
|
699
|
-
return {
|
|
700
|
-
openRuntimeApplication,
|
|
701
|
-
prepareCandidateConfirmation,
|
|
702
|
-
prepareDependentTargets,
|
|
703
|
-
proveRuntimeStack,
|
|
704
|
-
runManager,
|
|
705
|
-
runRuntimeStack,
|
|
706
|
-
};
|
|
707
|
-
};
|