@rehearsal-db/core 0.1.0-beta.1
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/BENCHMARKS.md +61 -0
- package/CHANGELOG.md +59 -0
- package/COMPATIBILITY.md +22 -0
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/SECURITY.md +19 -0
- package/SUPPORT.md +15 -0
- package/docs/adapters.md +23 -0
- package/docs/baselines.md +33 -0
- package/docs/commands.md +35 -0
- package/docs/configuration.md +98 -0
- package/docs/getting-started.md +247 -0
- package/docs/glossary.md +33 -0
- package/docs/production-source.md +33 -0
- package/docs/releasing.md +79 -0
- package/docs/sanitization.md +69 -0
- package/docs/security-model.md +40 -0
- package/docs/troubleshooting.md +50 -0
- package/docs/tutorial.md +96 -0
- package/package.json +77 -0
- package/scripts/lib/environment/local_supabase.mjs +197 -0
- package/scripts/lib/rehearsal/baseline_artifact.mjs +536 -0
- package/scripts/lib/rehearsal/baseline_builder.mjs +155 -0
- package/scripts/lib/rehearsal/configuration.d.mts +85 -0
- package/scripts/lib/rehearsal/configuration.mjs +559 -0
- package/scripts/lib/rehearsal/diagnostics.mjs +193 -0
- package/scripts/lib/rehearsal/migration_history.mjs +220 -0
- package/scripts/lib/rehearsal/plan.mjs +587 -0
- package/scripts/lib/rehearsal/process_environment.mjs +64 -0
- package/scripts/lib/rehearsal/runtime_restore.mjs +310 -0
- package/scripts/lib/rehearsal/sanitization_policy.mjs +169 -0
- package/scripts/lib/rehearsal/schema_snapshot.mjs +113 -0
- package/scripts/lib/rehearsal/service_environment.mjs +82 -0
- package/scripts/operations/database/manage_rehearsal_database.mjs +784 -0
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +565 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Purpose: Run the versioned, project-configured Rehearsal developer CLI for
|
|
4
|
+
* initialization, readiness, planning, inspection, and verified local execution.
|
|
5
|
+
* Run: `npm run rehearsal -- doctor` or `npm run rehearsal -- explain`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { writeFile } from "node:fs/promises";
|
|
10
|
+
import { join, relative } from "node:path";
|
|
11
|
+
import { performance } from "node:perf_hooks";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import {
|
|
14
|
+
RehearsalError,
|
|
15
|
+
createRehearsalResult,
|
|
16
|
+
normalizeRehearsalError,
|
|
17
|
+
redactDiagnosticValue,
|
|
18
|
+
renderHumanError,
|
|
19
|
+
serializeRehearsalError,
|
|
20
|
+
} from "../../lib/rehearsal/diagnostics.mjs";
|
|
21
|
+
import {
|
|
22
|
+
inspectDetectedProject,
|
|
23
|
+
findRehearsalConfigPath,
|
|
24
|
+
loadRehearsalConfig,
|
|
25
|
+
renderDetectedConfig,
|
|
26
|
+
} from "../../lib/rehearsal/configuration.mjs";
|
|
27
|
+
import {
|
|
28
|
+
buildRehearsalPlan,
|
|
29
|
+
inspectRehearsalBaseline,
|
|
30
|
+
inspectRehearsalMigrations,
|
|
31
|
+
runRehearsalDoctor,
|
|
32
|
+
} from "../../lib/rehearsal/plan.mjs";
|
|
33
|
+
import { createSyntheticBaselineFromFiles } from "../../lib/rehearsal/baseline_builder.mjs";
|
|
34
|
+
|
|
35
|
+
const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
|
|
36
|
+
const projectRoot = process.cwd();
|
|
37
|
+
const commandStartedAt = new Date();
|
|
38
|
+
const commandStartedMs = performance.now();
|
|
39
|
+
const managerPath = join(
|
|
40
|
+
packageRoot,
|
|
41
|
+
"scripts/operations/database/manage_rehearsal_database.mjs",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const parseArguments = (arguments_) => {
|
|
45
|
+
const flags = {
|
|
46
|
+
json: false,
|
|
47
|
+
help: false,
|
|
48
|
+
verbosity: "normal",
|
|
49
|
+
dryRun: false,
|
|
50
|
+
write: false,
|
|
51
|
+
configPath: undefined,
|
|
52
|
+
confirmation: undefined,
|
|
53
|
+
recordsPath: undefined,
|
|
54
|
+
ledgerPath: undefined,
|
|
55
|
+
assetsPath: undefined,
|
|
56
|
+
};
|
|
57
|
+
const positionals = [];
|
|
58
|
+
for (const argument of arguments_) {
|
|
59
|
+
if (argument === "--json") flags.json = true;
|
|
60
|
+
else if (argument === "--help" || argument === "-h") flags.help = true;
|
|
61
|
+
else if (argument === "--verbose") flags.verbosity = "verbose";
|
|
62
|
+
else if (argument === "--debug") flags.verbosity = "debug";
|
|
63
|
+
else if (argument === "--dry-run") flags.dryRun = true;
|
|
64
|
+
else if (argument === "--write") flags.write = true;
|
|
65
|
+
else if (argument.startsWith("--config=")) {
|
|
66
|
+
flags.configPath = argument.slice("--config=".length);
|
|
67
|
+
} else if (argument.startsWith("--confirm-candidates=")) {
|
|
68
|
+
flags.confirmation = argument.slice("--confirm-candidates=".length);
|
|
69
|
+
} else if (argument.startsWith("--records=")) {
|
|
70
|
+
flags.recordsPath = argument.slice("--records=".length);
|
|
71
|
+
} else if (argument.startsWith("--ledger=")) {
|
|
72
|
+
flags.ledgerPath = argument.slice("--ledger=".length);
|
|
73
|
+
} else if (argument.startsWith("--assets=")) {
|
|
74
|
+
flags.assetsPath = argument.slice("--assets=".length);
|
|
75
|
+
} else if (argument.startsWith("--")) {
|
|
76
|
+
throw new Error(`Unknown Rehearsal option: ${argument}.`);
|
|
77
|
+
} else positionals.push(argument);
|
|
78
|
+
}
|
|
79
|
+
return { flags, positionals };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const renderPlan = (plan, verbosity) => {
|
|
83
|
+
const candidates = plan.migrations.candidates.length
|
|
84
|
+
? plan.migrations.candidates
|
|
85
|
+
.map((entry) => ` → ${entry.filename} (${entry.sha256.slice(0, 12)})`)
|
|
86
|
+
.join("\n")
|
|
87
|
+
: " ✓ No candidate migrations";
|
|
88
|
+
const verbose =
|
|
89
|
+
verbosity === "normal"
|
|
90
|
+
? []
|
|
91
|
+
: [
|
|
92
|
+
"",
|
|
93
|
+
"Safety barriers",
|
|
94
|
+
...plan.environment.barriers.map((barrier) => ` ✓ ${barrier}`),
|
|
95
|
+
` ✓ Candidate digest: ${plan.migrations.candidateSha256}`,
|
|
96
|
+
];
|
|
97
|
+
return [
|
|
98
|
+
"REHEARSAL PLAN",
|
|
99
|
+
"",
|
|
100
|
+
"Environment",
|
|
101
|
+
` ✓ ${plan.environment.kind}`,
|
|
102
|
+
` ✓ Hosted access ${plan.environment.hostedAccess}`,
|
|
103
|
+
` ✓ Application/provider-data egress ${plan.environment.outboundNetwork}`,
|
|
104
|
+
...(plan.environment.authenticationProviders.length
|
|
105
|
+
? [
|
|
106
|
+
` ✓ External identity providers ${plan.environment.authenticationProviders.join(", ")}`,
|
|
107
|
+
]
|
|
108
|
+
: []),
|
|
109
|
+
"",
|
|
110
|
+
"Baseline",
|
|
111
|
+
` ✓ ${plan.baseline.generationId}`,
|
|
112
|
+
` ✓ ${plan.baseline.tableCount} tables; ${plan.baseline.rowCount} rows`,
|
|
113
|
+
` ✓ ${plan.baseline.verification}`,
|
|
114
|
+
"",
|
|
115
|
+
"Migrations",
|
|
116
|
+
` ✓ ${plan.migrations.representedCount} represented by baseline`,
|
|
117
|
+
candidates,
|
|
118
|
+
"",
|
|
119
|
+
"Execution plan",
|
|
120
|
+
...plan.execution.map((step, index) => ` ${index + 1}. ${step}`),
|
|
121
|
+
...verbose,
|
|
122
|
+
"",
|
|
123
|
+
plan.guarantee,
|
|
124
|
+
].join("\n");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const renderDoctor = (doctor, verbosity) => {
|
|
128
|
+
const lines = doctor.checks.map((check) => {
|
|
129
|
+
const marker = check.status === "pass" ? "✓" : "✗";
|
|
130
|
+
const detail =
|
|
131
|
+
verbosity === "normal" && check.status === "pass"
|
|
132
|
+
? ""
|
|
133
|
+
: ` — ${check.detail}`;
|
|
134
|
+
return `${marker} ${check.label}${detail}`;
|
|
135
|
+
});
|
|
136
|
+
for (const check of doctor.checks.filter(
|
|
137
|
+
(entry) => entry.status === "fail",
|
|
138
|
+
)) {
|
|
139
|
+
if (check.remediation) lines.push(` Try: ${check.remediation}`);
|
|
140
|
+
}
|
|
141
|
+
if (doctor.ambientHostedVariables.presentButQuarantined.length) {
|
|
142
|
+
lines.push(
|
|
143
|
+
`! ${doctor.ambientHostedVariables.presentButQuarantined.length} ambient hosted credential variables detected and quarantined.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return [...lines, "", doctor.state].join("\n");
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const renderBaseline = (baseline) =>
|
|
150
|
+
[
|
|
151
|
+
"REHEARSAL BASELINE",
|
|
152
|
+
`Rehearsal: ${baseline.rehearsalVersion}`,
|
|
153
|
+
`Generation: ${baseline.generationId}`,
|
|
154
|
+
`Created: ${baseline.createdAt ?? "unknown"}`,
|
|
155
|
+
`Format: ${baseline.formatVersion}`,
|
|
156
|
+
`Tables: ${baseline.tableCount}`,
|
|
157
|
+
`Rows: ${baseline.rowCount}`,
|
|
158
|
+
`Migrations: ${baseline.migrationCount} through ${baseline.migrationCutoff}`,
|
|
159
|
+
`Data SHA-256: ${baseline.dataSha256}`,
|
|
160
|
+
`Sanitization policy SHA-256: ${baseline.sanitizationPolicySha256}`,
|
|
161
|
+
`Verification: ${baseline.verification}`,
|
|
162
|
+
"Source rows and sensitive values are never printed by inspect.",
|
|
163
|
+
].join("\n");
|
|
164
|
+
|
|
165
|
+
const renderMigrations = (inspection, verbosity) => {
|
|
166
|
+
const entries = inspection.migrations.map((migration) => {
|
|
167
|
+
const digest = verbosity === "normal" ? "" : ` (${migration.sha256})`;
|
|
168
|
+
return `${migration.status.padEnd(28)} ${migration.filename}${digest}`;
|
|
169
|
+
});
|
|
170
|
+
return [
|
|
171
|
+
"REHEARSAL MIGRATIONS",
|
|
172
|
+
`Baseline: ${inspection.baselineGenerationId} through ${inspection.baselineCutoff}`,
|
|
173
|
+
`Candidate digest: ${inspection.candidateSha256}`,
|
|
174
|
+
"",
|
|
175
|
+
...entries,
|
|
176
|
+
].join("\n");
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const candidateSummary = (inspection) => {
|
|
180
|
+
const candidates = inspection.migrations.filter(
|
|
181
|
+
(migration) => migration.status === "candidate",
|
|
182
|
+
);
|
|
183
|
+
return {
|
|
184
|
+
baselineGenerationId: inspection.baselineGenerationId,
|
|
185
|
+
candidateSha256: inspection.candidateSha256,
|
|
186
|
+
candidateCount: candidates.length,
|
|
187
|
+
candidates,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const renderCandidates = (summary, verbosity) =>
|
|
192
|
+
[
|
|
193
|
+
"REHEARSAL CANDIDATES",
|
|
194
|
+
`Baseline: ${summary.baselineGenerationId}`,
|
|
195
|
+
`Candidate digest: ${summary.candidateSha256}`,
|
|
196
|
+
`Pending: ${summary.candidateCount}`,
|
|
197
|
+
...(summary.candidates.length
|
|
198
|
+
? summary.candidates.map((migration) =>
|
|
199
|
+
verbosity === "normal"
|
|
200
|
+
? ` → ${migration.filename}`
|
|
201
|
+
: ` → ${migration.filename} (${migration.sha256})`,
|
|
202
|
+
)
|
|
203
|
+
: [" ✓ No candidate migrations"]),
|
|
204
|
+
].join("\n");
|
|
205
|
+
|
|
206
|
+
const emit = ({ command, data, flags, render, status = "success" }) => {
|
|
207
|
+
const result = createRehearsalResult({
|
|
208
|
+
command,
|
|
209
|
+
status,
|
|
210
|
+
data,
|
|
211
|
+
startedAt: commandStartedAt,
|
|
212
|
+
durationMs: Math.round((performance.now() - commandStartedMs) * 100) / 100,
|
|
213
|
+
});
|
|
214
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
215
|
+
else console.log(render(data, flags.verbosity));
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const runManager = ({ action, flags }) => {
|
|
219
|
+
const args = [managerPath, action];
|
|
220
|
+
if (flags.confirmation) {
|
|
221
|
+
args.push(`--confirm-candidates=${flags.confirmation}`);
|
|
222
|
+
}
|
|
223
|
+
const environment = Object.fromEntries(
|
|
224
|
+
[
|
|
225
|
+
"CI",
|
|
226
|
+
"COLORTERM",
|
|
227
|
+
"FORCE_COLOR",
|
|
228
|
+
"HOME",
|
|
229
|
+
"LANG",
|
|
230
|
+
"LC_ALL",
|
|
231
|
+
"NO_COLOR",
|
|
232
|
+
"PATH",
|
|
233
|
+
"SHELL",
|
|
234
|
+
"TERM",
|
|
235
|
+
"TMPDIR",
|
|
236
|
+
"USER",
|
|
237
|
+
].flatMap((key) =>
|
|
238
|
+
process.env[key] === undefined ? [] : [[key, process.env[key]]],
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
const result = spawnSync(process.execPath, args, {
|
|
242
|
+
cwd: projectRoot,
|
|
243
|
+
encoding: "utf8",
|
|
244
|
+
env: environment,
|
|
245
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
246
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
247
|
+
});
|
|
248
|
+
if (result.error) throw result.error;
|
|
249
|
+
if (result.status !== 0) {
|
|
250
|
+
const completeOutput = [
|
|
251
|
+
String(result.stdout ?? ""),
|
|
252
|
+
String(result.stderr ?? ""),
|
|
253
|
+
]
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.join("\n")
|
|
256
|
+
.trim();
|
|
257
|
+
const inferred = normalizeRehearsalError(
|
|
258
|
+
new Error(completeOutput || `Rehearsal ${action} failed.`),
|
|
259
|
+
);
|
|
260
|
+
const conciseOutput = completeOutput
|
|
261
|
+
.replaceAll(String.fromCodePoint(27), "")
|
|
262
|
+
.split("\n")
|
|
263
|
+
.map((line) => line.trim())
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
.slice(-8)
|
|
266
|
+
.join("\n")
|
|
267
|
+
.slice(-2_000);
|
|
268
|
+
throw new RehearsalError({
|
|
269
|
+
category: inferred.category,
|
|
270
|
+
code: `MANAGER_${action.toUpperCase()}_FAILED`,
|
|
271
|
+
message: `The local Rehearsal ${action} operation did not complete.`,
|
|
272
|
+
expected: "the isolated runtime operation to finish and verify",
|
|
273
|
+
actual: conciseOutput || `exit ${result.status}`,
|
|
274
|
+
context: { action },
|
|
275
|
+
refused: "The runtime was not reported as trusted.",
|
|
276
|
+
suggestions: [
|
|
277
|
+
"Review the concise failure above, then rerun rehearsal doctor before retrying.",
|
|
278
|
+
"Use --debug only when the safe diagnostic detail is needed.",
|
|
279
|
+
],
|
|
280
|
+
cause: new Error(completeOutput || `exit ${result.status}`),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
action,
|
|
285
|
+
output: redactDiagnosticValue(String(result.stdout ?? "").trim()),
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const parseCommand = (source) => {
|
|
290
|
+
const values = [];
|
|
291
|
+
let current = "";
|
|
292
|
+
let quote = null;
|
|
293
|
+
for (const character of source) {
|
|
294
|
+
if (quote) {
|
|
295
|
+
if (character === quote) quote = null;
|
|
296
|
+
else current += character;
|
|
297
|
+
} else if (character === '"' || character === "'") quote = character;
|
|
298
|
+
else if (/\s/u.test(character)) {
|
|
299
|
+
if (current) values.push(current);
|
|
300
|
+
current = "";
|
|
301
|
+
} else current += character;
|
|
302
|
+
}
|
|
303
|
+
if (quote)
|
|
304
|
+
throw new Error("Application proof command contains an unmatched quote.");
|
|
305
|
+
if (current) values.push(current);
|
|
306
|
+
if (values.length === 0)
|
|
307
|
+
throw new Error("Application proof command is empty.");
|
|
308
|
+
return values;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const runApplicationProof = async (planOptions) => {
|
|
312
|
+
const { config, projectRoot: configuredRoot } =
|
|
313
|
+
await loadRehearsalConfig(planOptions);
|
|
314
|
+
const [command, ...args] = parseCommand(config.application.proofCommand);
|
|
315
|
+
const environment = Object.fromEntries(
|
|
316
|
+
[
|
|
317
|
+
"CI",
|
|
318
|
+
"COLORTERM",
|
|
319
|
+
"FORCE_COLOR",
|
|
320
|
+
"HOME",
|
|
321
|
+
"LANG",
|
|
322
|
+
"LC_ALL",
|
|
323
|
+
"NO_COLOR",
|
|
324
|
+
"PATH",
|
|
325
|
+
"SHELL",
|
|
326
|
+
"TERM",
|
|
327
|
+
"TMPDIR",
|
|
328
|
+
"USER",
|
|
329
|
+
].flatMap((key) =>
|
|
330
|
+
process.env[key] === undefined ? [] : [[key, process.env[key]]],
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
const result = spawnSync(command, args, {
|
|
334
|
+
cwd: configuredRoot,
|
|
335
|
+
encoding: "utf8",
|
|
336
|
+
env: environment,
|
|
337
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
338
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
339
|
+
});
|
|
340
|
+
if (result.error || result.status !== 0) {
|
|
341
|
+
throw new RehearsalError({
|
|
342
|
+
category: "application_proof_failure",
|
|
343
|
+
code: "APPLICATION_PROOF_FAILED",
|
|
344
|
+
message: "The configured application proof did not pass.",
|
|
345
|
+
expected: `${config.application.proofCommand} exits successfully`,
|
|
346
|
+
actual: result.error?.message ?? result.stderr ?? `exit ${result.status}`,
|
|
347
|
+
context: { project: config.project.name },
|
|
348
|
+
refused:
|
|
349
|
+
"Rehearsal did not report the migrated runtime as fully verified.",
|
|
350
|
+
suggestions: [
|
|
351
|
+
"Run the proof command directly in the same project and correct the failure.",
|
|
352
|
+
],
|
|
353
|
+
cause: result.error,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
command: config.application.proofCommand,
|
|
358
|
+
output: redactDiagnosticValue(String(result.stdout ?? "").trim()),
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const runInit = async ({ flags }) => {
|
|
363
|
+
const detected = await inspectDetectedProject({ projectRoot });
|
|
364
|
+
const source = renderDetectedConfig(detected);
|
|
365
|
+
const destination = join(projectRoot, "rehearsal.config.ts");
|
|
366
|
+
let existingPath = null;
|
|
367
|
+
try {
|
|
368
|
+
existingPath = await findRehearsalConfigPath({ projectRoot });
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (
|
|
371
|
+
!String(error?.message ?? error).startsWith("No Rehearsal configuration")
|
|
372
|
+
) {
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (existingPath) {
|
|
377
|
+
if (flags.write) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
`A Rehearsal configuration already exists at ${relative(projectRoot, existingPath)}; Rehearsal will not overwrite it.`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
mode: "existing",
|
|
384
|
+
destination: relative(projectRoot, existingPath),
|
|
385
|
+
detected,
|
|
386
|
+
source: "",
|
|
387
|
+
nextAction:
|
|
388
|
+
"Review the existing configuration, then run rehearsal doctor.",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (flags.write)
|
|
392
|
+
await writeFile(destination, source, { flag: "wx", mode: 0o600 });
|
|
393
|
+
return {
|
|
394
|
+
mode: flags.write ? "written" : "preview",
|
|
395
|
+
destination: relative(projectRoot, destination),
|
|
396
|
+
detected,
|
|
397
|
+
source,
|
|
398
|
+
nextAction: flags.write
|
|
399
|
+
? "Review the generated safety settings and run rehearsal doctor."
|
|
400
|
+
: "Review this preview, then rerun rehearsal init --write to create it.",
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const renderInit = (result) =>
|
|
405
|
+
[
|
|
406
|
+
`REHEARSAL INIT — ${result.mode.toUpperCase()}`,
|
|
407
|
+
`Destination: ${result.destination}`,
|
|
408
|
+
`Detected package manager: ${result.detected.packageManager}`,
|
|
409
|
+
`Detected Supabase config: ${result.detected.hasSupabaseConfig ? "yes" : "no"}`,
|
|
410
|
+
`Detected migrations: ${result.detected.hasMigrations ? "yes" : "no"}`,
|
|
411
|
+
...(result.source ? ["", result.source] : []),
|
|
412
|
+
result.nextAction,
|
|
413
|
+
].join("\n");
|
|
414
|
+
|
|
415
|
+
const usage = () => `Usage: rehearsal <command> [options]
|
|
416
|
+
|
|
417
|
+
Commands:
|
|
418
|
+
init [--write] Preview or explicitly write safe starter config
|
|
419
|
+
baseline create --records= --ledger= [--assets=] Create a baseline from safe local inputs
|
|
420
|
+
doctor Check whether Rehearsal is safe and ready
|
|
421
|
+
explain Show the immutable execution plan
|
|
422
|
+
run --dry-run Alias the exact explain plan without mutations
|
|
423
|
+
run --confirm-candidates= Execute reset, migration, and verification locally
|
|
424
|
+
candidates Show the exact pending migration digest
|
|
425
|
+
inspect baseline Show verified baseline provenance
|
|
426
|
+
inspect migrations Classify represented, applied, and candidate migrations
|
|
427
|
+
start Start an existing verified local runtime
|
|
428
|
+
migrate --confirm-candidates= Apply the exact candidate suffix without resetting
|
|
429
|
+
reset Restore and verify the immutable local baseline
|
|
430
|
+
status Report the disposable local runtime state
|
|
431
|
+
stop Stop only this project's local runtime
|
|
432
|
+
discard Remove only this project's disposable runtime
|
|
433
|
+
verify Verify the current local Rehearsal runtime
|
|
434
|
+
|
|
435
|
+
Options: --json --verbose --debug --config=<path>`;
|
|
436
|
+
|
|
437
|
+
const main = async () => {
|
|
438
|
+
const { flags, positionals } = parseArguments(process.argv.slice(2));
|
|
439
|
+
const command = positionals.join(" ") || "help";
|
|
440
|
+
const planOptions = {
|
|
441
|
+
projectRoot,
|
|
442
|
+
configPath: flags.configPath,
|
|
443
|
+
};
|
|
444
|
+
if (command === "help" || flags.help) {
|
|
445
|
+
console.log(usage());
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (command === "init") {
|
|
449
|
+
const data = await runInit({ flags });
|
|
450
|
+
emit({ command, data, flags, render: renderInit });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (command === "baseline create") {
|
|
454
|
+
const data = await createSyntheticBaselineFromFiles({
|
|
455
|
+
...planOptions,
|
|
456
|
+
recordsPath: flags.recordsPath,
|
|
457
|
+
ledgerPath: flags.ledgerPath,
|
|
458
|
+
assetsPath: flags.assetsPath,
|
|
459
|
+
});
|
|
460
|
+
emit({
|
|
461
|
+
command,
|
|
462
|
+
data,
|
|
463
|
+
flags,
|
|
464
|
+
render: (baseline) =>
|
|
465
|
+
`Activated synthetic baseline ${baseline.generationId}: ${baseline.rowCount} rows across ${baseline.tableCount} tables; ${baseline.migrationCount} migrations through ${baseline.migrationCutoff}.`,
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (command === "doctor") {
|
|
470
|
+
const data = await runRehearsalDoctor(planOptions);
|
|
471
|
+
emit({
|
|
472
|
+
command,
|
|
473
|
+
data,
|
|
474
|
+
flags,
|
|
475
|
+
render: renderDoctor,
|
|
476
|
+
status: data.state === "READY" ? "success" : "not_ready",
|
|
477
|
+
});
|
|
478
|
+
if (data.state !== "READY") process.exitCode = 1;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (command === "explain" || (command === "run" && flags.dryRun)) {
|
|
482
|
+
const data = await buildRehearsalPlan(planOptions);
|
|
483
|
+
emit({ command, data, flags, render: renderPlan });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (command === "inspect baseline") {
|
|
487
|
+
const data = await inspectRehearsalBaseline(planOptions);
|
|
488
|
+
emit({ command, data, flags, render: renderBaseline });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (command === "inspect migrations") {
|
|
492
|
+
const data = await inspectRehearsalMigrations(planOptions);
|
|
493
|
+
emit({ command, data, flags, render: renderMigrations });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (command === "candidates") {
|
|
497
|
+
const data = candidateSummary(
|
|
498
|
+
await inspectRehearsalMigrations(planOptions),
|
|
499
|
+
);
|
|
500
|
+
emit({ command, data, flags, render: renderCandidates });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (
|
|
504
|
+
[
|
|
505
|
+
"run",
|
|
506
|
+
"start",
|
|
507
|
+
"migrate",
|
|
508
|
+
"reset",
|
|
509
|
+
"status",
|
|
510
|
+
"stop",
|
|
511
|
+
"discard",
|
|
512
|
+
"verify",
|
|
513
|
+
].includes(command)
|
|
514
|
+
) {
|
|
515
|
+
if (flags.dryRun && command !== "run") {
|
|
516
|
+
throw new Error("--dry-run is supported only by rehearsal run.");
|
|
517
|
+
}
|
|
518
|
+
const runtime = runManager({ action: command, flags });
|
|
519
|
+
const data =
|
|
520
|
+
command === "run"
|
|
521
|
+
? {
|
|
522
|
+
runtime,
|
|
523
|
+
applicationProof: await runApplicationProof(planOptions),
|
|
524
|
+
}
|
|
525
|
+
: runtime;
|
|
526
|
+
emit({
|
|
527
|
+
command,
|
|
528
|
+
data,
|
|
529
|
+
flags,
|
|
530
|
+
render: (value) => {
|
|
531
|
+
const runtimeResult = value.runtime ?? value;
|
|
532
|
+
return [
|
|
533
|
+
runtimeResult.output ||
|
|
534
|
+
`Rehearsal ${runtimeResult.action} completed.`,
|
|
535
|
+
value.applicationProof
|
|
536
|
+
? `Application proof passed: ${value.applicationProof.command}`
|
|
537
|
+
: null,
|
|
538
|
+
]
|
|
539
|
+
.filter(Boolean)
|
|
540
|
+
.join("\n");
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
throw new Error(`Unknown Rehearsal command: ${command}.\n\n${usage()}`);
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
await main();
|
|
550
|
+
} catch (error) {
|
|
551
|
+
const failure = normalizeRehearsalError(error, {
|
|
552
|
+
expected: "a safe, versioned, local-only Rehearsal operation",
|
|
553
|
+
actual: String(error?.message ?? error),
|
|
554
|
+
refused: "No further Rehearsal action was performed.",
|
|
555
|
+
suggestions: ["Run rehearsal doctor for actionable readiness checks."],
|
|
556
|
+
});
|
|
557
|
+
const wantsJson = process.argv.includes("--json");
|
|
558
|
+
const debug = process.argv.includes("--debug");
|
|
559
|
+
if (wantsJson)
|
|
560
|
+
console.log(
|
|
561
|
+
JSON.stringify(serializeRehearsalError(failure, { debug }), null, 2),
|
|
562
|
+
);
|
|
563
|
+
else console.error(renderHumanError(failure, { debug }));
|
|
564
|
+
process.exitCode = failure.exitCode;
|
|
565
|
+
}
|