@rehearsal-db/core 0.1.0-beta.8 → 0.1.0-beta.9
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 +68 -1
- package/COMPATIBILITY.md +15 -3
- package/README.md +31 -13
- package/docs/README.md +29 -0
- package/docs/adapters.md +15 -6
- package/docs/architecture.md +58 -0
- package/docs/baselines.md +22 -1
- package/docs/commands.md +76 -14
- package/docs/configuration.md +144 -11
- package/docs/getting-started.md +12 -2
- package/docs/glossary.md +4 -0
- package/docs/production-source.md +176 -33
- package/docs/roadmap.md +30 -28
- package/docs/runtime-policies.md +191 -0
- package/docs/sanitization.md +24 -8
- package/docs/security-model.md +29 -8
- package/docs/standalone-workflow.md +107 -0
- package/docs/troubleshooting.md +8 -0
- package/package.json +25 -22
- package/scripts/{operations/database/manage_rehearsal_database.mjs → runtime/manage_database.mjs} +2 -2
- package/src/README.md +17 -0
- package/src/application/session.mjs +313 -0
- package/{scripts/lib/rehearsal/baseline_artifact.mjs → src/baseline/artifact.mjs} +110 -14
- package/{scripts/lib/rehearsal/baseline_builder.mjs → src/baseline/builder.mjs} +8 -4
- package/{scripts/lib/rehearsal/baseline_preparation.mjs → src/baseline/preparation.mjs} +11 -4
- package/src/baseline/privacy_engine.mjs +413 -0
- package/{scripts/lib/rehearsal → src/baseline}/sanitization_policy.mjs +27 -7
- package/{scripts/lib/rehearsal → src/baseline}/schema_snapshot.mjs +1 -1
- package/src/cli/arguments.mjs +120 -0
- package/src/cli/guided.mjs +807 -0
- package/src/cli/rehearsal.mjs +933 -0
- package/src/cli/renderers.mjs +584 -0
- package/src/cli/runtime_commands.mjs +553 -0
- package/src/cli/source_commands.mjs +326 -0
- package/src/cli/terminal.mjs +275 -0
- package/src/identity/claim.mjs +975 -0
- package/src/identity/storage.mjs +165 -0
- package/{scripts/lib/rehearsal → src/project}/configuration.d.mts +34 -0
- package/{scripts/lib/rehearsal → src/project}/configuration.mjs +353 -3
- package/{scripts/lib/rehearsal → src/project}/setup.mjs +1 -1
- package/{scripts/lib/rehearsal → src/project}/support_report.mjs +5 -2
- package/{scripts/lib/rehearsal → src/runtime}/cleanup.mjs +3 -3
- package/{scripts/lib/rehearsal → src/runtime}/plan.mjs +5 -5
- package/src/runtime/policy.mjs +438 -0
- package/{scripts/lib/rehearsal/runtime_restore.mjs → src/runtime/restore.mjs} +40 -25
- package/src/runtime/topology.mjs +177 -0
- package/{scripts/lib/rehearsal → src/shared}/diagnostics.mjs +1 -1
- package/src/shared/operation_guard.mjs +162 -0
- package/{scripts/lib/rehearsal → src/shared}/process_environment.mjs +5 -1
- package/src/source/access.mjs +578 -0
- package/src/source/asset_transfer.mjs +177 -0
- package/src/source/baseline.mjs +446 -0
- package/src/source/postgresql_access.mjs +480 -0
- package/{scripts/lib/runtime/postgresql_runtime.mjs → src/targets/postgresql.mjs} +45 -10
- package/{scripts/lib/runtime/supabase_runtime.mjs → src/targets/supabase.mjs} +50 -13
- package/{scripts/lib/environment/local_supabase.mjs → src/targets/supabase_environment.mjs} +52 -20
- package/{scripts/lib/runtime/runtime_target.mjs → src/targets/target.mjs} +2 -2
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +0 -2169
- /package/{scripts/lib/rehearsal → src/baseline}/input_discovery.mjs +0 -0
- /package/{scripts/lib/rehearsal → src/baseline}/policy_review.mjs +0 -0
- /package/{scripts/lib/rehearsal → src/runtime}/migration_history.mjs +0 -0
- /package/{scripts/lib/rehearsal → src/runtime}/service_environment.mjs +0 -0
- /package/{scripts/lib/rehearsal → src/shared}/human_output.mjs +0 -0
|
@@ -0,0 +1,933 @@
|
|
|
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 { createHash } from "node:crypto";
|
|
9
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { join, relative } from "node:path";
|
|
11
|
+
import { performance } from "node:perf_hooks";
|
|
12
|
+
import { parseEnv } from "node:util";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import * as prompts from "@clack/prompts";
|
|
15
|
+
import {
|
|
16
|
+
createRehearsalResult,
|
|
17
|
+
normalizeRehearsalError,
|
|
18
|
+
renderHumanError,
|
|
19
|
+
serializeRehearsalError,
|
|
20
|
+
} from "../shared/diagnostics.mjs";
|
|
21
|
+
import {
|
|
22
|
+
inspectDetectedProject,
|
|
23
|
+
findRehearsalConfigPath,
|
|
24
|
+
loadRehearsalConfig,
|
|
25
|
+
renderDetectedConfig,
|
|
26
|
+
} from "../project/configuration.mjs";
|
|
27
|
+
import {
|
|
28
|
+
inspectRehearsalBaseline,
|
|
29
|
+
inspectRehearsalMigrations,
|
|
30
|
+
runRehearsalDoctor,
|
|
31
|
+
} from "../runtime/plan.mjs";
|
|
32
|
+
import { createSyntheticBaselineFromFiles } from "../baseline/builder.mjs";
|
|
33
|
+
import {
|
|
34
|
+
applyBaselinePreparation,
|
|
35
|
+
inspectBaselineInputFiles,
|
|
36
|
+
planBaselinePreparation,
|
|
37
|
+
summarizeBaselinePreparation,
|
|
38
|
+
} from "../baseline/preparation.mjs";
|
|
39
|
+
import {
|
|
40
|
+
applyRehearsalSetup,
|
|
41
|
+
planRehearsalSetup,
|
|
42
|
+
summarizeRehearsalSetup,
|
|
43
|
+
} from "../project/setup.mjs";
|
|
44
|
+
import { formatCount } from "../shared/human_output.mjs";
|
|
45
|
+
import { collectRehearsalSupportReport } from "../project/support_report.mjs";
|
|
46
|
+
import {
|
|
47
|
+
applyRehearsalCleanup,
|
|
48
|
+
planRehearsalCleanup,
|
|
49
|
+
} from "../runtime/cleanup.mjs";
|
|
50
|
+
import { loadRuntimeTopology } from "../runtime/topology.mjs";
|
|
51
|
+
import { createPrivacyKey } from "../baseline/privacy_engine.mjs";
|
|
52
|
+
import { createSourceAccessPlan } from "../source/access.mjs";
|
|
53
|
+
import {
|
|
54
|
+
buildApplicationEnvironment,
|
|
55
|
+
runHttpProofs,
|
|
56
|
+
startApplicationSession,
|
|
57
|
+
} from "../application/session.mjs";
|
|
58
|
+
import {
|
|
59
|
+
applyIdentityClaim,
|
|
60
|
+
createIdentityClaimPlan,
|
|
61
|
+
validateIdentityPolicy,
|
|
62
|
+
} from "../identity/claim.mjs";
|
|
63
|
+
import { createSupabaseStorageTransfer } from "../identity/storage.mjs";
|
|
64
|
+
import {
|
|
65
|
+
acquireProjectOperation,
|
|
66
|
+
assertInstalledPackageFingerprint,
|
|
67
|
+
createInstalledPackageFingerprint,
|
|
68
|
+
} from "../shared/operation_guard.mjs";
|
|
69
|
+
import {
|
|
70
|
+
commandMutatesProject,
|
|
71
|
+
parseArguments,
|
|
72
|
+
resetGuidedFlags,
|
|
73
|
+
} from "./arguments.mjs";
|
|
74
|
+
import {
|
|
75
|
+
formatDuration,
|
|
76
|
+
installGuidedExitShortcut,
|
|
77
|
+
isHumanTerminal,
|
|
78
|
+
terminalStyle,
|
|
79
|
+
useStyledPrompts,
|
|
80
|
+
} from "./terminal.mjs";
|
|
81
|
+
import { createGuidedHome } from "./guided.mjs";
|
|
82
|
+
import {
|
|
83
|
+
buildRefreshWorkflowPlan,
|
|
84
|
+
createRefreshWorkflow,
|
|
85
|
+
loadPreparationContext,
|
|
86
|
+
runSourceAccess,
|
|
87
|
+
runSourceBaselineRefresh,
|
|
88
|
+
runSourceRetirement,
|
|
89
|
+
} from "./source_commands.mjs";
|
|
90
|
+
import {
|
|
91
|
+
buildRuntimeStackPlan,
|
|
92
|
+
createRuntimeCommands,
|
|
93
|
+
runRuntimeStackDoctor,
|
|
94
|
+
topologyCandidateSummary,
|
|
95
|
+
topologyCommandEnvironment,
|
|
96
|
+
topologyEnvironmentFiles,
|
|
97
|
+
} from "./runtime_commands.mjs";
|
|
98
|
+
import {
|
|
99
|
+
renderBaseline,
|
|
100
|
+
renderBaselineInputInspection,
|
|
101
|
+
renderBaselinePreparation,
|
|
102
|
+
renderCandidates,
|
|
103
|
+
renderCleanup,
|
|
104
|
+
renderDoctor,
|
|
105
|
+
formatBytes,
|
|
106
|
+
renderIdentityPlan,
|
|
107
|
+
renderInit,
|
|
108
|
+
renderMigrations,
|
|
109
|
+
renderPlan,
|
|
110
|
+
renderRefreshWorkflow,
|
|
111
|
+
renderSetup,
|
|
112
|
+
renderSourceAccessPlan,
|
|
113
|
+
renderSourceRetirement,
|
|
114
|
+
renderSupportReport,
|
|
115
|
+
} from "./renderers.mjs";
|
|
116
|
+
|
|
117
|
+
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
118
|
+
const projectRoot = process.cwd();
|
|
119
|
+
let activePackageFingerprint = null;
|
|
120
|
+
const commandStartedAt = new Date();
|
|
121
|
+
const commandStartedMs = performance.now();
|
|
122
|
+
const managerPath = join(packageRoot, "scripts/runtime/manage_database.mjs");
|
|
123
|
+
let lastGuidedDetails;
|
|
124
|
+
|
|
125
|
+
const {
|
|
126
|
+
prepareCandidateConfirmation,
|
|
127
|
+
prepareDependentTargets,
|
|
128
|
+
proveRuntimeStack,
|
|
129
|
+
runManager,
|
|
130
|
+
runRuntimeStack,
|
|
131
|
+
} = createRuntimeCommands({
|
|
132
|
+
packageRoot,
|
|
133
|
+
projectRoot,
|
|
134
|
+
managerPath,
|
|
135
|
+
getActivePackageFingerprint: () => activePackageFingerprint,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const emit = ({ command, data, flags, render, status = "success" }) => {
|
|
139
|
+
const result = createRehearsalResult({
|
|
140
|
+
command,
|
|
141
|
+
status,
|
|
142
|
+
data,
|
|
143
|
+
startedAt: commandStartedAt,
|
|
144
|
+
durationMs: Math.round((performance.now() - commandStartedMs) * 100) / 100,
|
|
145
|
+
});
|
|
146
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
147
|
+
else console.log(render(data, flags.verbosity));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const runInit = async ({ flags }) => {
|
|
151
|
+
const detected = await inspectDetectedProject({ projectRoot });
|
|
152
|
+
const source = renderDetectedConfig(detected);
|
|
153
|
+
const destination = join(projectRoot, "rehearsal.config.mjs");
|
|
154
|
+
let existingPath = null;
|
|
155
|
+
try {
|
|
156
|
+
existingPath = await findRehearsalConfigPath({ projectRoot });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (
|
|
159
|
+
!String(error?.message ?? error).startsWith("No Rehearsal configuration")
|
|
160
|
+
) {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (existingPath) {
|
|
165
|
+
if (flags.write) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`A Rehearsal configuration already exists at ${relative(projectRoot, existingPath)}; Rehearsal will not overwrite it.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
mode: "existing",
|
|
172
|
+
destination: relative(projectRoot, existingPath),
|
|
173
|
+
detected,
|
|
174
|
+
source: "",
|
|
175
|
+
nextAction:
|
|
176
|
+
"Review the existing configuration, then run rehearsal doctor.",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (flags.write)
|
|
180
|
+
await writeFile(destination, source, { flag: "wx", mode: 0o600 });
|
|
181
|
+
return {
|
|
182
|
+
mode: flags.write ? "written" : "preview",
|
|
183
|
+
destination: relative(projectRoot, destination),
|
|
184
|
+
detected,
|
|
185
|
+
source,
|
|
186
|
+
nextAction: flags.write
|
|
187
|
+
? "Review the generated safety settings and run rehearsal doctor."
|
|
188
|
+
: "Review this preview, then rerun rehearsal init --write to create it.",
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const runSetup = async ({ flags, planOptions }) => {
|
|
193
|
+
const plan =
|
|
194
|
+
flags.setupPlan ??
|
|
195
|
+
(await planRehearsalSetup({
|
|
196
|
+
projectRoot,
|
|
197
|
+
target: flags.target ?? "supabase",
|
|
198
|
+
}));
|
|
199
|
+
if (flags.write) await applyRehearsalSetup(plan);
|
|
200
|
+
const result = summarizeRehearsalSetup(plan, {
|
|
201
|
+
mode: flags.write ? "written" : "preview",
|
|
202
|
+
});
|
|
203
|
+
if (!flags.write) return result;
|
|
204
|
+
const readiness = await runRehearsalDoctor(planOptions);
|
|
205
|
+
const needsBaselinePolicy = readiness.checks.some(
|
|
206
|
+
(check) => check.id === "sanitization-policy" && check.status === "fail",
|
|
207
|
+
);
|
|
208
|
+
return {
|
|
209
|
+
...result,
|
|
210
|
+
readiness,
|
|
211
|
+
nextAction:
|
|
212
|
+
readiness.state === "READY"
|
|
213
|
+
? "Next: run rehearsal explain to review the migration plan."
|
|
214
|
+
: needsBaselinePolicy
|
|
215
|
+
? flags.guided
|
|
216
|
+
? "Next: choose Prepare the script to create a reviewable policy draft."
|
|
217
|
+
: "Next: run rehearsal again and choose Prepare a reviewable baseline policy draft."
|
|
218
|
+
: "Next: complete the remaining readiness items shown above.",
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const runBaselinePreparation = async ({ flags, planOptions }) => {
|
|
223
|
+
const plan =
|
|
224
|
+
flags.preparationPlan ??
|
|
225
|
+
(await planBaselinePreparation({
|
|
226
|
+
...planOptions,
|
|
227
|
+
recordsPath: flags.recordsPath,
|
|
228
|
+
ledgerPath: flags.ledgerPath,
|
|
229
|
+
}));
|
|
230
|
+
if (flags.write) await applyBaselinePreparation(plan);
|
|
231
|
+
const result = summarizeBaselinePreparation(plan, {
|
|
232
|
+
mode: flags.write ? "written" : "preview",
|
|
233
|
+
});
|
|
234
|
+
return flags.guided && flags.write
|
|
235
|
+
? {
|
|
236
|
+
...result,
|
|
237
|
+
nextAction:
|
|
238
|
+
"Next: choose Review the script to classify every column interactively.",
|
|
239
|
+
}
|
|
240
|
+
: result;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const planRuntimeStackCleanup = async ({ flags, planOptions }) => {
|
|
244
|
+
const topology = await loadRuntimeTopology(planOptions);
|
|
245
|
+
const targets = [];
|
|
246
|
+
for (const target of topology.targets) {
|
|
247
|
+
targets.push({
|
|
248
|
+
name: target.name,
|
|
249
|
+
configPath: target.configPath,
|
|
250
|
+
plan: await planRehearsalCleanup({
|
|
251
|
+
projectRoot: topology.projectRoot,
|
|
252
|
+
configPath: target.configPath,
|
|
253
|
+
includeRuntime: flags.includeRuntime,
|
|
254
|
+
includeImages: flags.includeImages && target.primary,
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (targets.length === 1) return targets[0].plan;
|
|
259
|
+
return {
|
|
260
|
+
digest: createHash("sha256")
|
|
261
|
+
.update(
|
|
262
|
+
JSON.stringify(
|
|
263
|
+
targets.map((target) => ({
|
|
264
|
+
name: target.name,
|
|
265
|
+
digest: target.plan.digest,
|
|
266
|
+
})),
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
.digest("hex"),
|
|
270
|
+
targets,
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const runCleanup = async ({ flags, planOptions }) => {
|
|
275
|
+
const plan =
|
|
276
|
+
flags.cleanupPlan ??
|
|
277
|
+
(await planRuntimeStackCleanup({ flags, planOptions }));
|
|
278
|
+
if (!flags.write) return { mode: "preview", plan };
|
|
279
|
+
if (plan.targets) {
|
|
280
|
+
if (flags.cleanupConfirmation !== plan.digest) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"Cleanup requires the exact --confirm-cleanup digest from a fresh preview. Nothing was removed.",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
for (const target of plan.targets) {
|
|
286
|
+
const refreshed = await planRehearsalCleanup({
|
|
287
|
+
projectRoot,
|
|
288
|
+
configPath: target.configPath,
|
|
289
|
+
includeRuntime: target.plan.runtime.included,
|
|
290
|
+
includeImages: target.plan.images.requested,
|
|
291
|
+
});
|
|
292
|
+
if (refreshed.digest !== target.plan.digest) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Cleanup targets changed for ${target.name} after the preview. Nothing was removed; preview the cleanup again.`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const appliedTargets = [];
|
|
299
|
+
for (const target of plan.targets) {
|
|
300
|
+
appliedTargets.push({
|
|
301
|
+
name: target.name,
|
|
302
|
+
applied: await applyRehearsalCleanup({
|
|
303
|
+
plan: target.plan,
|
|
304
|
+
confirmation: target.plan.digest,
|
|
305
|
+
projectRoot,
|
|
306
|
+
configPath: target.configPath,
|
|
307
|
+
removeRuntime: async () => {
|
|
308
|
+
runManager({
|
|
309
|
+
action: "discard",
|
|
310
|
+
flags,
|
|
311
|
+
configPath: target.configPath,
|
|
312
|
+
targetName: target.name,
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
mode: "written",
|
|
320
|
+
plan,
|
|
321
|
+
applied: { targets: appliedTargets },
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const applied = await applyRehearsalCleanup({
|
|
325
|
+
plan,
|
|
326
|
+
confirmation: flags.cleanupConfirmation,
|
|
327
|
+
...planOptions,
|
|
328
|
+
removeRuntime: async () => {
|
|
329
|
+
runManager({ action: "discard", flags });
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
return { mode: "written", plan, applied };
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const loadIdentityClaimPlan = async ({ flags, planOptions }) => {
|
|
336
|
+
const loaded = await loadRehearsalConfig(planOptions);
|
|
337
|
+
if (!loaded.paths.identityPolicy) {
|
|
338
|
+
throw new Error("No config.identityPolicy is configured.");
|
|
339
|
+
}
|
|
340
|
+
if (!flags.identityName) {
|
|
341
|
+
throw new Error("Choose a declared identity with --identity=<name>.");
|
|
342
|
+
}
|
|
343
|
+
const policy = validateIdentityPolicy(
|
|
344
|
+
JSON.parse(await readFile(loaded.paths.identityPolicy, "utf8")),
|
|
345
|
+
);
|
|
346
|
+
return {
|
|
347
|
+
loaded,
|
|
348
|
+
plan: createIdentityClaimPlan({
|
|
349
|
+
policy,
|
|
350
|
+
name: flags.identityName,
|
|
351
|
+
}),
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const usage = () => `Usage: rehearsal <command> [options]
|
|
356
|
+
|
|
357
|
+
Commands:
|
|
358
|
+
guide Open the interactive, state-aware home screen
|
|
359
|
+
setup [--target=] [--write] Preview or create safe first-run scaffolding
|
|
360
|
+
init [--write] Preview or explicitly write safe starter config
|
|
361
|
+
baseline prepare --records= --ledger= [--write] Create a fail-closed policy draft
|
|
362
|
+
baseline create --records= --ledger= [--assets=] Create a baseline from safe local inputs
|
|
363
|
+
privacy key [--write] Preview or create the owner-only pseudonym key
|
|
364
|
+
source plan Preview exact, temporary source access
|
|
365
|
+
source apply --confirm-source-access= Apply the reviewed source-access plan
|
|
366
|
+
source retire [--confirm-source-retirement=] Preview or retire exact source access
|
|
367
|
+
baseline refresh Replace only the baseline from a reviewed source
|
|
368
|
+
refresh [--confirm-refresh=] Replace the database copy, reset locally, and remove old copies
|
|
369
|
+
identity plan --identity= Preview an approved local identity association
|
|
370
|
+
identity claim --identity= --confirm-identity= Apply the exact local association
|
|
371
|
+
doctor Check whether Rehearsal is safe and ready
|
|
372
|
+
support Print a safe, copy-ready support report
|
|
373
|
+
explain Show the immutable execution plan
|
|
374
|
+
run --dry-run Alias the exact explain plan without mutations
|
|
375
|
+
run --confirm-candidates= Execute reset, migration, and verification locally
|
|
376
|
+
candidates Show the exact pending migration digest
|
|
377
|
+
inspect baseline Show verified baseline provenance
|
|
378
|
+
inspect migrations Classify represented, applied, and candidate migrations
|
|
379
|
+
start Start an existing verified local runtime
|
|
380
|
+
migrate --confirm-candidates= Apply the exact candidate suffix without resetting
|
|
381
|
+
reset Restore and verify the immutable local baseline
|
|
382
|
+
status Report the disposable local runtime state
|
|
383
|
+
stop Stop only this project's local runtime
|
|
384
|
+
discard Remove only this project's disposable runtime
|
|
385
|
+
cleanup [--include-runtime] [--include-images] [--write] [--confirm-cleanup=]
|
|
386
|
+
Preview or apply conservative disk cleanup
|
|
387
|
+
verify Verify the current local Rehearsal runtime
|
|
388
|
+
|
|
389
|
+
Options: --json --verbose --debug --plain --config=<path> --target=supabase|postgresql
|
|
390
|
+
--confirm-source-access=<digest> --confirm-source-retirement=<digest>
|
|
391
|
+
--confirm-refresh=<digest>`;
|
|
392
|
+
|
|
393
|
+
const runRefreshWorkflow = createRefreshWorkflow({
|
|
394
|
+
runRuntimeStack,
|
|
395
|
+
prepareDependentTargets,
|
|
396
|
+
topologyCommandEnvironment,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const runGuidedHome = createGuidedHome({
|
|
400
|
+
projectRoot,
|
|
401
|
+
topologyCandidateSummary,
|
|
402
|
+
buildRefreshWorkflowPlan,
|
|
403
|
+
planRuntimeStackCleanup,
|
|
404
|
+
getLastGuidedDetails: () => lastGuidedDetails,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const executeCommand = async ({ command, flags, planOptions, guided }) => {
|
|
408
|
+
if (command === "help" || flags.help) {
|
|
409
|
+
console.log(usage());
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (command === "setup") {
|
|
413
|
+
const data = await runSetup({ flags, planOptions });
|
|
414
|
+
emit({ command, data, flags, render: renderSetup });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (command === "init") {
|
|
418
|
+
const data = await runInit({ flags });
|
|
419
|
+
emit({ command, data, flags, render: renderInit });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (command === "baseline prepare") {
|
|
423
|
+
const data = await runBaselinePreparation({ flags, planOptions });
|
|
424
|
+
emit({
|
|
425
|
+
command,
|
|
426
|
+
data,
|
|
427
|
+
flags,
|
|
428
|
+
render: (result) => renderBaselinePreparation(result, flags),
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (command === "baseline create") {
|
|
433
|
+
await inspectBaselineInputFiles({
|
|
434
|
+
...planOptions,
|
|
435
|
+
recordsPath: flags.recordsPath,
|
|
436
|
+
ledgerPath: flags.ledgerPath,
|
|
437
|
+
assetsPath: flags.assetsPath,
|
|
438
|
+
});
|
|
439
|
+
const data = await createSyntheticBaselineFromFiles({
|
|
440
|
+
...planOptions,
|
|
441
|
+
recordsPath: flags.recordsPath,
|
|
442
|
+
ledgerPath: flags.ledgerPath,
|
|
443
|
+
assetsPath: flags.assetsPath,
|
|
444
|
+
});
|
|
445
|
+
emit({
|
|
446
|
+
command,
|
|
447
|
+
data,
|
|
448
|
+
flags,
|
|
449
|
+
render: (baseline) =>
|
|
450
|
+
[
|
|
451
|
+
`Activated synthetic baseline ${baseline.generationId}: ${formatCount(baseline.rowCount, "row")} across ${formatCount(baseline.tableCount, "table")}; ${formatCount(baseline.migrationCount, "migration")} through ${baseline.migrationCutoff}.`,
|
|
452
|
+
"",
|
|
453
|
+
flags.guided
|
|
454
|
+
? "Next: choose Run a rehearsal when you are ready."
|
|
455
|
+
: "Next: run rehearsal doctor to check readiness.",
|
|
456
|
+
].join("\n"),
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (command === "privacy key") {
|
|
461
|
+
const { loaded } = await loadPreparationContext(planOptions);
|
|
462
|
+
const data = flags.write
|
|
463
|
+
? {
|
|
464
|
+
mode: "written",
|
|
465
|
+
path: relative(loaded.projectRoot, loaded.paths.privacyKey),
|
|
466
|
+
...(await createPrivacyKey(loaded.paths.privacyKey)),
|
|
467
|
+
}
|
|
468
|
+
: {
|
|
469
|
+
mode: "preview",
|
|
470
|
+
path: relative(loaded.projectRoot, loaded.paths.privacyKey),
|
|
471
|
+
};
|
|
472
|
+
emit({
|
|
473
|
+
command,
|
|
474
|
+
data,
|
|
475
|
+
flags,
|
|
476
|
+
render: (result) =>
|
|
477
|
+
result.mode === "preview"
|
|
478
|
+
? `PRIVACY KEY — PREVIEW\n\nWould create ${result.path} with owner-only permissions.\nNothing was written. Rerun with --write after reviewing the path.`
|
|
479
|
+
: `Created owner-only privacy key at ${result.path}.\nFingerprint: ${result.fingerprint}\nThe key is excluded from source control and must not be shared.`,
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (command === "source plan") {
|
|
484
|
+
const { sourcePolicy } = await loadPreparationContext(planOptions);
|
|
485
|
+
const data = createSourceAccessPlan({ policy: sourcePolicy });
|
|
486
|
+
emit({ command, data, flags, render: renderSourceAccessPlan });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (command === "source apply") {
|
|
490
|
+
const data = await runSourceAccess({ flags, planOptions });
|
|
491
|
+
emit({
|
|
492
|
+
command,
|
|
493
|
+
data,
|
|
494
|
+
flags,
|
|
495
|
+
render: (result) =>
|
|
496
|
+
result.mode === "preview"
|
|
497
|
+
? renderSourceAccessPlan(result.plan)
|
|
498
|
+
: [
|
|
499
|
+
"SOURCE ACCESS — READY",
|
|
500
|
+
`Temporary reader expires: ${result.receipt.expiresAt}`,
|
|
501
|
+
`Local credential: ${result.credentialFile}`,
|
|
502
|
+
"",
|
|
503
|
+
"Next: run rehearsal baseline refresh, then retire source access.",
|
|
504
|
+
].join("\n"),
|
|
505
|
+
});
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (command === "source retire") {
|
|
509
|
+
const data = await runSourceRetirement({ flags, planOptions });
|
|
510
|
+
emit({ command, data, flags, render: renderSourceRetirement });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (command === "baseline refresh") {
|
|
514
|
+
const data = await runSourceBaselineRefresh({ planOptions });
|
|
515
|
+
emit({
|
|
516
|
+
command,
|
|
517
|
+
data,
|
|
518
|
+
flags,
|
|
519
|
+
render: (result) =>
|
|
520
|
+
[
|
|
521
|
+
`Activated refreshed baseline ${result.generationId}.`,
|
|
522
|
+
`${formatCount(result.manifest.rowCount, "sanitized row")} across ${formatCount(Object.keys(result.manifest.tableCounts).length, "table")}.`,
|
|
523
|
+
`Source estimate: ${formatCount(result.estimate.rows, "row")}, ${formatBytes(result.estimate.bytes)}.`,
|
|
524
|
+
"",
|
|
525
|
+
"Next: run rehearsal source retire, then rehearsal doctor.",
|
|
526
|
+
].join("\n"),
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (command === "refresh") {
|
|
531
|
+
const data = await runRefreshWorkflow({ flags, planOptions });
|
|
532
|
+
emit({ command, data, flags, render: renderRefreshWorkflow });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (command === "identity plan" || command === "identity claim") {
|
|
536
|
+
const { loaded, plan } = await loadIdentityClaimPlan({
|
|
537
|
+
flags,
|
|
538
|
+
planOptions,
|
|
539
|
+
});
|
|
540
|
+
if (command === "identity plan" || !flags.identityConfirmation) {
|
|
541
|
+
emit({
|
|
542
|
+
command,
|
|
543
|
+
data: { mode: "preview", plan },
|
|
544
|
+
flags,
|
|
545
|
+
render: renderIdentityPlan,
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const runtimeEnvironment = parseEnv(
|
|
550
|
+
await readFile(loaded.paths.applicationEnvironment, "utf8"),
|
|
551
|
+
);
|
|
552
|
+
if (!runtimeEnvironment.DATABASE_URL) {
|
|
553
|
+
throw new Error("The local runtime environment has no DATABASE_URL.");
|
|
554
|
+
}
|
|
555
|
+
const result = await applyIdentityClaim({
|
|
556
|
+
plan,
|
|
557
|
+
confirmation: flags.identityConfirmation,
|
|
558
|
+
connectionString: runtimeEnvironment.DATABASE_URL,
|
|
559
|
+
storageTransfer: plan.identity.assets.some((asset) => asset.rewritePath)
|
|
560
|
+
? createSupabaseStorageTransfer({
|
|
561
|
+
url: runtimeEnvironment.SUPABASE_URL,
|
|
562
|
+
serviceRoleKey: runtimeEnvironment.SUPABASE_SERVICE_ROLE_KEY,
|
|
563
|
+
})
|
|
564
|
+
: undefined,
|
|
565
|
+
});
|
|
566
|
+
emit({
|
|
567
|
+
command,
|
|
568
|
+
data: { mode: "written", plan, result },
|
|
569
|
+
flags,
|
|
570
|
+
render: renderIdentityPlan,
|
|
571
|
+
});
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (command === "doctor") {
|
|
575
|
+
const data = await runRuntimeStackDoctor(planOptions);
|
|
576
|
+
emit({
|
|
577
|
+
command,
|
|
578
|
+
data,
|
|
579
|
+
flags,
|
|
580
|
+
render: renderDoctor,
|
|
581
|
+
status: data.state === "READY" ? "success" : "not_ready",
|
|
582
|
+
});
|
|
583
|
+
if (data.state !== "READY" && !guided) process.exitCode = 1;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (command === "support") {
|
|
587
|
+
const data = await collectRehearsalSupportReport(planOptions, {
|
|
588
|
+
runDoctor: async (options) => {
|
|
589
|
+
const report = await runRuntimeStackDoctor(options);
|
|
590
|
+
const targetAliases = new Map();
|
|
591
|
+
const aliasFor = (name) => {
|
|
592
|
+
if (!targetAliases.has(name)) {
|
|
593
|
+
targetAliases.set(name, `runtime-${targetAliases.size + 1}`);
|
|
594
|
+
}
|
|
595
|
+
return targetAliases.get(name);
|
|
596
|
+
};
|
|
597
|
+
return {
|
|
598
|
+
...report,
|
|
599
|
+
checks: report.checks.map((check) => {
|
|
600
|
+
const separator = check.id.indexOf(":");
|
|
601
|
+
if (separator === -1) return check;
|
|
602
|
+
const target = check.id.slice(0, separator);
|
|
603
|
+
const alias = aliasFor(target);
|
|
604
|
+
return {
|
|
605
|
+
...check,
|
|
606
|
+
id: `${alias}:${check.id.slice(separator + 1)}`,
|
|
607
|
+
label: check.label.replace(`${target}:`, `${alias}:`),
|
|
608
|
+
};
|
|
609
|
+
}),
|
|
610
|
+
};
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
emit({ command, data, flags, render: renderSupportReport });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (command === "cleanup") {
|
|
617
|
+
const data = await runCleanup({ flags, planOptions });
|
|
618
|
+
emit({ command, data, flags, render: renderCleanup });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (command === "explain" || (command === "run" && flags.dryRun)) {
|
|
622
|
+
const data = await buildRuntimeStackPlan(planOptions);
|
|
623
|
+
emit({ command, data, flags, render: renderPlan });
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (command === "inspect baseline") {
|
|
627
|
+
const data = await inspectRehearsalBaseline(planOptions);
|
|
628
|
+
emit({ command, data, flags, render: renderBaseline });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (command === "inspect migrations") {
|
|
632
|
+
const data = await inspectRehearsalMigrations(planOptions);
|
|
633
|
+
emit({ command, data, flags, render: renderMigrations });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (command === "candidates") {
|
|
637
|
+
const { topology: _topology, ...data } =
|
|
638
|
+
await topologyCandidateSummary(planOptions);
|
|
639
|
+
emit({ command, data, flags, render: renderCandidates });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (
|
|
643
|
+
[
|
|
644
|
+
"run",
|
|
645
|
+
"start",
|
|
646
|
+
"migrate",
|
|
647
|
+
"reset",
|
|
648
|
+
"status",
|
|
649
|
+
"stop",
|
|
650
|
+
"discard",
|
|
651
|
+
"verify",
|
|
652
|
+
].includes(command)
|
|
653
|
+
) {
|
|
654
|
+
if (flags.dryRun && command !== "run") {
|
|
655
|
+
throw new Error("--dry-run is supported only by rehearsal run.");
|
|
656
|
+
}
|
|
657
|
+
if (
|
|
658
|
+
!(await prepareCandidateConfirmation({ command, flags, planOptions }))
|
|
659
|
+
) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const stack = await runRuntimeStack({ command, flags, planOptions });
|
|
663
|
+
const { runtime, topology } = stack;
|
|
664
|
+
const stackEnvironment = topologyCommandEnvironment(topology);
|
|
665
|
+
const preparations = ["run", "reset", "migrate"].includes(command)
|
|
666
|
+
? prepareDependentTargets({
|
|
667
|
+
topology,
|
|
668
|
+
environment: stackEnvironment,
|
|
669
|
+
})
|
|
670
|
+
: [];
|
|
671
|
+
let applicationProof;
|
|
672
|
+
let dependentProofs = [];
|
|
673
|
+
let httpProofResults = [];
|
|
674
|
+
let applicationReadiness;
|
|
675
|
+
if (command === "run") {
|
|
676
|
+
const applicationConfig = topology.primary.config.application;
|
|
677
|
+
const mappedApplicationEnvironment = Object.keys(
|
|
678
|
+
applicationConfig.environmentVariables,
|
|
679
|
+
).length
|
|
680
|
+
? await buildApplicationEnvironment({
|
|
681
|
+
files: topologyEnvironmentFiles(topology),
|
|
682
|
+
mappings: applicationConfig.environmentVariables,
|
|
683
|
+
})
|
|
684
|
+
: {};
|
|
685
|
+
const proofEnvironment = {
|
|
686
|
+
...stackEnvironment,
|
|
687
|
+
...mappedApplicationEnvironment,
|
|
688
|
+
};
|
|
689
|
+
const proofLabel =
|
|
690
|
+
topology.targets.length > 1
|
|
691
|
+
? "project-owned dependent and application proofs"
|
|
692
|
+
: "project-owned application proof";
|
|
693
|
+
const proofSpinner = useStyledPrompts(flags) ? prompts.spinner() : null;
|
|
694
|
+
if (proofSpinner) {
|
|
695
|
+
proofSpinner.start(`Running the ${proofLabel}.`);
|
|
696
|
+
} else if (isHumanTerminal(flags)) {
|
|
697
|
+
console.log(
|
|
698
|
+
`${terminalStyle(flags, "36", "→")} Running the ${proofLabel}.`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
let applicationSession;
|
|
702
|
+
try {
|
|
703
|
+
if (applicationConfig.readiness) {
|
|
704
|
+
applicationSession = await startApplicationSession({
|
|
705
|
+
command: applicationConfig.startCommand,
|
|
706
|
+
cwd: topology.projectRoot,
|
|
707
|
+
files: topologyEnvironmentFiles(topology),
|
|
708
|
+
mappings: applicationConfig.environmentVariables,
|
|
709
|
+
readiness: applicationConfig.readiness,
|
|
710
|
+
});
|
|
711
|
+
applicationReadiness = applicationSession.ready;
|
|
712
|
+
}
|
|
713
|
+
({ applicationProof, dependentProofs } = await proveRuntimeStack({
|
|
714
|
+
topology,
|
|
715
|
+
planOptions,
|
|
716
|
+
environment: proofEnvironment,
|
|
717
|
+
}));
|
|
718
|
+
if (applicationConfig.httpProofs.length) {
|
|
719
|
+
httpProofResults = await runHttpProofs({
|
|
720
|
+
checks: applicationConfig.httpProofs,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
} catch (error) {
|
|
724
|
+
if (proofSpinner) proofSpinner.stop("A project proof failed.");
|
|
725
|
+
throw error;
|
|
726
|
+
} finally {
|
|
727
|
+
await applicationSession?.stop();
|
|
728
|
+
}
|
|
729
|
+
if (proofSpinner) {
|
|
730
|
+
proofSpinner.stop("Project proofs passed.");
|
|
731
|
+
} else if (isHumanTerminal(flags)) {
|
|
732
|
+
console.log(
|
|
733
|
+
`${terminalStyle(flags, "32", "✓")} Project proofs passed.`,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const data =
|
|
738
|
+
command === "run"
|
|
739
|
+
? {
|
|
740
|
+
runtime,
|
|
741
|
+
preparations,
|
|
742
|
+
dependentProofs,
|
|
743
|
+
applicationProof,
|
|
744
|
+
applicationReadiness,
|
|
745
|
+
httpProofResults,
|
|
746
|
+
}
|
|
747
|
+
: preparations.length
|
|
748
|
+
? { ...runtime, preparations }
|
|
749
|
+
: runtime;
|
|
750
|
+
if (guided) {
|
|
751
|
+
lastGuidedDetails = [
|
|
752
|
+
runtime.output,
|
|
753
|
+
...preparations.map((preparation) => preparation.output),
|
|
754
|
+
...dependentProofs.map((proof) => proof.output),
|
|
755
|
+
applicationProof?.output,
|
|
756
|
+
]
|
|
757
|
+
.filter(Boolean)
|
|
758
|
+
.join("\n");
|
|
759
|
+
}
|
|
760
|
+
emit({
|
|
761
|
+
command,
|
|
762
|
+
data,
|
|
763
|
+
flags,
|
|
764
|
+
render: (value) => {
|
|
765
|
+
const runtimeResult = value.runtime ?? value;
|
|
766
|
+
const nextAction = {
|
|
767
|
+
run: "Next: exercise the local application, then run rehearsal verify.",
|
|
768
|
+
start:
|
|
769
|
+
"Next: exercise the local application or run rehearsal status.",
|
|
770
|
+
migrate: "Next: run rehearsal verify to prove the current runtime.",
|
|
771
|
+
reset: "Next: run rehearsal verify or continue testing locally.",
|
|
772
|
+
status: "Next: run rehearsal verify, reset, stop, or discard.",
|
|
773
|
+
stop: "Next: run rehearsal start when you want to resume.",
|
|
774
|
+
discard:
|
|
775
|
+
"The disposable runtime was removed; the immutable baseline remains.",
|
|
776
|
+
verify:
|
|
777
|
+
"Next: continue testing, reset to the baseline, or stop the runtime.",
|
|
778
|
+
}[runtimeResult.action];
|
|
779
|
+
if (guided) {
|
|
780
|
+
const title =
|
|
781
|
+
runtimeResult.action === "run"
|
|
782
|
+
? "REHEARSAL PASSED"
|
|
783
|
+
: "ACTION COMPLETE";
|
|
784
|
+
return [
|
|
785
|
+
title,
|
|
786
|
+
`✓ ${runtimeResult.action} completed in ${formatDuration(runtimeResult.durationMs)}`,
|
|
787
|
+
...(value.preparations ?? []).map(
|
|
788
|
+
(preparation) =>
|
|
789
|
+
`✓ ${preparation.target} preparation passed: ${preparation.command}`,
|
|
790
|
+
),
|
|
791
|
+
...(value.dependentProofs ?? []).map(
|
|
792
|
+
(proof) => `✓ ${proof.target} proof passed: ${proof.command}`,
|
|
793
|
+
),
|
|
794
|
+
value.applicationProof
|
|
795
|
+
? `✓ Application proof passed: ${value.applicationProof.command}`
|
|
796
|
+
: null,
|
|
797
|
+
value.applicationReadiness
|
|
798
|
+
? `✓ Application became ready at ${value.applicationReadiness.url}`
|
|
799
|
+
: null,
|
|
800
|
+
...(value.httpProofResults ?? []).map(
|
|
801
|
+
(proof) => `✓ ${proof.kind} HTTP proof passed: ${proof.name}`,
|
|
802
|
+
),
|
|
803
|
+
"",
|
|
804
|
+
"Technical output is available from Show details in the guide.",
|
|
805
|
+
]
|
|
806
|
+
.filter(Boolean)
|
|
807
|
+
.join("\n");
|
|
808
|
+
}
|
|
809
|
+
return [
|
|
810
|
+
runtimeResult.output ||
|
|
811
|
+
`Rehearsal ${runtimeResult.action} completed.`,
|
|
812
|
+
...(value.preparations ?? []).map(
|
|
813
|
+
(preparation) =>
|
|
814
|
+
`${preparation.target} preparation passed: ${preparation.command}`,
|
|
815
|
+
),
|
|
816
|
+
...(value.dependentProofs ?? []).map(
|
|
817
|
+
(proof) => `${proof.target} proof passed: ${proof.command}`,
|
|
818
|
+
),
|
|
819
|
+
value.applicationProof
|
|
820
|
+
? `Application proof passed: ${value.applicationProof.command}`
|
|
821
|
+
: null,
|
|
822
|
+
value.applicationReadiness
|
|
823
|
+
? `Application readiness passed: ${value.applicationReadiness.url}`
|
|
824
|
+
: null,
|
|
825
|
+
...(value.httpProofResults ?? []).map(
|
|
826
|
+
(proof) => `${proof.kind} HTTP proof passed: ${proof.name}`,
|
|
827
|
+
),
|
|
828
|
+
nextAction ? "" : null,
|
|
829
|
+
nextAction,
|
|
830
|
+
]
|
|
831
|
+
.filter((entry) => entry !== null)
|
|
832
|
+
.join("\n");
|
|
833
|
+
},
|
|
834
|
+
});
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
throw new Error(`Unknown Rehearsal command: ${command}.\n\n${usage()}`);
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
const main = async () => {
|
|
841
|
+
const { flags, positionals } = parseArguments(process.argv.slice(2));
|
|
842
|
+
const command = positionals.join(" ") || "help";
|
|
843
|
+
const planOptions = {
|
|
844
|
+
projectRoot,
|
|
845
|
+
configPath: flags.configPath,
|
|
846
|
+
};
|
|
847
|
+
const wantsAutomaticGuide =
|
|
848
|
+
positionals.length === 0 &&
|
|
849
|
+
!flags.help &&
|
|
850
|
+
!flags.json &&
|
|
851
|
+
process.stdin.isTTY &&
|
|
852
|
+
process.stdout.isTTY;
|
|
853
|
+
const guided = command === "guide" || wantsAutomaticGuide;
|
|
854
|
+
const mutating = commandMutatesProject({ command, flags, guided });
|
|
855
|
+
const operation = mutating
|
|
856
|
+
? await acquireProjectOperation({ projectRoot, command })
|
|
857
|
+
: null;
|
|
858
|
+
if (operation) {
|
|
859
|
+
activePackageFingerprint = createInstalledPackageFingerprint({
|
|
860
|
+
packageRoot,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
if (!guided) {
|
|
865
|
+
await executeCommand({ command, flags, planOptions, guided: false });
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
869
|
+
throw new Error(
|
|
870
|
+
"The guided home screen requires an interactive terminal. Run rehearsal --help to list scriptable commands.",
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
flags.guided = true;
|
|
874
|
+
const removeGuidedExitShortcut = installGuidedExitShortcut(flags, {
|
|
875
|
+
beforeExit: () => operation?.releaseSync(),
|
|
876
|
+
});
|
|
877
|
+
if (useStyledPrompts(flags)) {
|
|
878
|
+
prompts.intro("REHEARSAL · Safe local migration testing");
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
while (true) {
|
|
882
|
+
resetGuidedFlags(flags);
|
|
883
|
+
const selected = await runGuidedHome({ flags, planOptions });
|
|
884
|
+
if (!selected) break;
|
|
885
|
+
if (selected === "home") continue;
|
|
886
|
+
await executeCommand({
|
|
887
|
+
command: selected,
|
|
888
|
+
flags,
|
|
889
|
+
planOptions,
|
|
890
|
+
guided: true,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
} finally {
|
|
894
|
+
removeGuidedExitShortcut();
|
|
895
|
+
}
|
|
896
|
+
if (useStyledPrompts(flags)) {
|
|
897
|
+
prompts.outro("See you at the next rehearsal.");
|
|
898
|
+
} else {
|
|
899
|
+
console.log("See you at the next rehearsal.");
|
|
900
|
+
}
|
|
901
|
+
} finally {
|
|
902
|
+
if (operation) {
|
|
903
|
+
try {
|
|
904
|
+
assertInstalledPackageFingerprint({
|
|
905
|
+
packageRoot,
|
|
906
|
+
expected: activePackageFingerprint,
|
|
907
|
+
});
|
|
908
|
+
} finally {
|
|
909
|
+
activePackageFingerprint = null;
|
|
910
|
+
await operation.release();
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
await main();
|
|
918
|
+
} catch (error) {
|
|
919
|
+
const failure = normalizeRehearsalError(error, {
|
|
920
|
+
expected: "a safe, versioned, local-only Rehearsal operation",
|
|
921
|
+
actual: String(error?.message ?? error),
|
|
922
|
+
refused: "No further Rehearsal action was performed.",
|
|
923
|
+
suggestions: ["Run rehearsal doctor for actionable readiness checks."],
|
|
924
|
+
});
|
|
925
|
+
const wantsJson = process.argv.includes("--json");
|
|
926
|
+
const debug = process.argv.includes("--debug");
|
|
927
|
+
if (wantsJson)
|
|
928
|
+
console.log(
|
|
929
|
+
JSON.stringify(serializeRehearsalError(failure, { debug }), null, 2),
|
|
930
|
+
);
|
|
931
|
+
else console.error(renderHumanError(failure, { debug }));
|
|
932
|
+
process.exitCode = failure.exitCode;
|
|
933
|
+
}
|