@rehearsal-db/core 0.1.0-beta.7 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +77 -1
  2. package/COMPATIBILITY.md +15 -3
  3. package/README.md +31 -13
  4. package/docs/README.md +29 -0
  5. package/docs/adapters.md +15 -6
  6. package/docs/architecture.md +58 -0
  7. package/docs/baselines.md +22 -1
  8. package/docs/commands.md +81 -14
  9. package/docs/configuration.md +144 -11
  10. package/docs/getting-started.md +12 -2
  11. package/docs/glossary.md +4 -0
  12. package/docs/production-source.md +176 -33
  13. package/docs/roadmap.md +30 -28
  14. package/docs/runtime-policies.md +191 -0
  15. package/docs/sanitization.md +24 -8
  16. package/docs/security-model.md +29 -8
  17. package/docs/standalone-workflow.md +107 -0
  18. package/docs/troubleshooting.md +8 -0
  19. package/package.json +25 -22
  20. package/scripts/runtime/manage_database.mjs +18 -0
  21. package/src/README.md +17 -0
  22. package/src/application/session.mjs +313 -0
  23. package/{scripts/lib/rehearsal/baseline_artifact.mjs → src/baseline/artifact.mjs} +110 -14
  24. package/{scripts/lib/rehearsal/baseline_builder.mjs → src/baseline/builder.mjs} +8 -4
  25. package/{scripts/lib/rehearsal/baseline_preparation.mjs → src/baseline/preparation.mjs} +11 -4
  26. package/src/baseline/privacy_engine.mjs +413 -0
  27. package/{scripts/lib/rehearsal → src/baseline}/sanitization_policy.mjs +27 -7
  28. package/{scripts/lib/rehearsal → src/baseline}/schema_snapshot.mjs +1 -1
  29. package/src/cli/arguments.mjs +120 -0
  30. package/src/cli/guided.mjs +807 -0
  31. package/src/cli/rehearsal.mjs +933 -0
  32. package/src/cli/renderers.mjs +584 -0
  33. package/src/cli/runtime_commands.mjs +553 -0
  34. package/src/cli/source_commands.mjs +326 -0
  35. package/src/cli/terminal.mjs +275 -0
  36. package/src/identity/claim.mjs +975 -0
  37. package/src/identity/storage.mjs +165 -0
  38. package/{scripts/lib/rehearsal → src/project}/configuration.d.mts +34 -0
  39. package/{scripts/lib/rehearsal → src/project}/configuration.mjs +353 -3
  40. package/{scripts/lib/rehearsal → src/project}/setup.mjs +1 -1
  41. package/{scripts/lib/rehearsal → src/project}/support_report.mjs +5 -2
  42. package/{scripts/lib/rehearsal → src/runtime}/cleanup.mjs +3 -3
  43. package/{scripts/lib/rehearsal → src/runtime}/plan.mjs +5 -5
  44. package/src/runtime/policy.mjs +438 -0
  45. package/{scripts/lib/rehearsal/runtime_restore.mjs → src/runtime/restore.mjs} +40 -25
  46. package/src/runtime/topology.mjs +177 -0
  47. package/{scripts/lib/rehearsal → src/shared}/diagnostics.mjs +1 -1
  48. package/src/shared/operation_guard.mjs +162 -0
  49. package/{scripts/lib/rehearsal → src/shared}/process_environment.mjs +5 -1
  50. package/src/source/access.mjs +578 -0
  51. package/src/source/asset_transfer.mjs +177 -0
  52. package/src/source/baseline.mjs +446 -0
  53. package/src/source/postgresql_access.mjs +480 -0
  54. package/{scripts/lib/runtime/postgresql_runtime.mjs → src/targets/postgresql.mjs} +49 -13
  55. package/{scripts/lib/runtime/supabase_runtime.mjs → src/targets/supabase.mjs} +54 -16
  56. package/{scripts/lib/environment/local_supabase.mjs → src/targets/supabase_environment.mjs} +52 -20
  57. package/{scripts/lib/runtime/runtime_target.mjs → src/targets/target.mjs} +27 -2
  58. package/scripts/operations/database/manage_rehearsal_database.mjs +0 -11
  59. package/scripts/operations/rehearsal/rehearsal_cli.mjs +0 -2166
  60. /package/{scripts/lib/rehearsal → src/baseline}/input_discovery.mjs +0 -0
  61. /package/{scripts/lib/rehearsal → src/baseline}/policy_review.mjs +0 -0
  62. /package/{scripts/lib/rehearsal → src/runtime}/migration_history.mjs +0 -0
  63. /package/{scripts/lib/rehearsal → src/runtime}/service_environment.mjs +0 -0
  64. /package/{scripts/lib/rehearsal → src/shared}/human_output.mjs +0 -0
@@ -0,0 +1,807 @@
1
+ /**
2
+ * State-aware interactive home screen and first-run flows.
3
+ */
4
+
5
+ import { access, readFile } from "node:fs/promises";
6
+ import { join, relative } from "node:path";
7
+ import * as prompts from "@clack/prompts";
8
+ import {
9
+ findRehearsalConfigPath,
10
+ inspectDetectedProject,
11
+ loadRehearsalConfig,
12
+ validateRuntimeSanitizationPolicy,
13
+ } from "../project/configuration.mjs";
14
+ import {
15
+ inspectRehearsalNodeRuntime,
16
+ planRehearsalSetup,
17
+ summarizeRehearsalSetup,
18
+ } from "../project/setup.mjs";
19
+ import {
20
+ inspectBaselineInputFiles,
21
+ planBaselinePreparation,
22
+ summarizeBaselinePreparation,
23
+ } from "../baseline/preparation.mjs";
24
+ import { discoverBaselineInputFiles } from "../baseline/input_discovery.mjs";
25
+ import {
26
+ applyReviewedPolicy,
27
+ completePolicyDraft,
28
+ createSafeTablePreset,
29
+ readReviewablePolicyDraft,
30
+ suggestPolicyExceptionColumns,
31
+ } from "../baseline/policy_review.mjs";
32
+ import { formatCount } from "../shared/human_output.mjs";
33
+ import { redactDiagnosticValue } from "../shared/diagnostics.mjs";
34
+ import {
35
+ renderBaselineInputInspection,
36
+ renderBaselinePreparation,
37
+ renderCleanup,
38
+ renderRefreshWorkflow,
39
+ renderSetup,
40
+ } from "./renderers.mjs";
41
+ import {
42
+ promptForChoice,
43
+ promptForConfirmation,
44
+ promptForDiscoveredPath,
45
+ promptForMultipleChoice,
46
+ promptForPath,
47
+ promptForSelection,
48
+ terminalStyle,
49
+ useStyledPrompts,
50
+ } from "./terminal.mjs";
51
+
52
+ const pathExists = (path) =>
53
+ access(path)
54
+ .then(() => true)
55
+ .catch((error) => {
56
+ if (error?.code === "ENOENT") return false;
57
+ throw error;
58
+ });
59
+
60
+ const inspectGuidedProject = async ({
61
+ projectRoot,
62
+ planOptions,
63
+ topologyCandidateSummary,
64
+ }) => {
65
+ const detected = await inspectDetectedProject({ projectRoot });
66
+ const node = inspectRehearsalNodeRuntime();
67
+ let configPath;
68
+ try {
69
+ configPath = await findRehearsalConfigPath(planOptions);
70
+ } catch (error) {
71
+ if (
72
+ !String(error?.message ?? error).startsWith("No Rehearsal configuration")
73
+ ) {
74
+ throw error;
75
+ }
76
+ }
77
+ if (!configPath) {
78
+ return {
79
+ detected,
80
+ node,
81
+ config: "missing",
82
+ target: detected.hasSupabaseConfig ? "supabase" : "postgresql",
83
+ baseline: false,
84
+ runtime: false,
85
+ };
86
+ }
87
+ let loaded;
88
+ try {
89
+ loaded = await loadRehearsalConfig(planOptions);
90
+ } catch (error) {
91
+ return {
92
+ detected,
93
+ node,
94
+ config: "invalid",
95
+ target: undefined,
96
+ configPath,
97
+ configError: redactDiagnosticValue(String(error?.message ?? error)),
98
+ baseline: false,
99
+ runtime: false,
100
+ };
101
+ }
102
+ let migrationSummary;
103
+ let topology;
104
+ try {
105
+ const summary = await topologyCandidateSummary(planOptions);
106
+ migrationSummary = summary;
107
+ topology = summary.topology;
108
+ } catch {
109
+ // The doctor command owns the detailed, safely redacted explanation.
110
+ }
111
+ let policy = "missing";
112
+ if (await pathExists(loaded.paths.sanitizationPolicy)) {
113
+ try {
114
+ validateRuntimeSanitizationPolicy(
115
+ JSON.parse(await readFile(loaded.paths.sanitizationPolicy, "utf8")),
116
+ );
117
+ policy = "reviewed";
118
+ } catch {
119
+ policy = "needs-review";
120
+ }
121
+ }
122
+ return {
123
+ detected,
124
+ node,
125
+ config: "valid",
126
+ target: loaded.config.runtime.target,
127
+ configPath,
128
+ baseline: Boolean(migrationSummary),
129
+ policy,
130
+ policyPath: relative(projectRoot, loaded.paths.sanitizationPolicy),
131
+ policyAbsolutePath: loaded.paths.sanitizationPolicy,
132
+ migrationSummary,
133
+ refreshConfigured: Boolean(loaded.config.preparation),
134
+ dependentCount: topology?.dependents.length ?? 0,
135
+ runtime: topology
136
+ ? (
137
+ await Promise.all(
138
+ topology.targets.map((target) =>
139
+ pathExists(join(target.paths.runtimeWorkdir, "baseline.json")),
140
+ ),
141
+ )
142
+ ).every(Boolean)
143
+ : await pathExists(join(loaded.paths.runtimeWorkdir, "baseline.json")),
144
+ };
145
+ };
146
+
147
+ const reviewColumn = async ({ table, column, flags }) => {
148
+ const label = `${table}.${column}`;
149
+ const action = await promptForSelection({
150
+ message: `${label}: how should this value be handled?`,
151
+ flags,
152
+ options: [
153
+ {
154
+ label: "Replace with synthetic data",
155
+ hint: "Safest default",
156
+ value: "REPLACE WITH SYNTHETIC",
157
+ },
158
+ {
159
+ label: "Pseudonymize consistently",
160
+ hint: "Stable links without original values",
161
+ value: "PSEUDONYMIZE",
162
+ },
163
+ { label: "Derive a safe value", value: "DERIVE" },
164
+ { label: "Exclude this field", value: "EXCLUDE" },
165
+ {
166
+ label: "Keep exactly",
167
+ hint: "Only for values explicitly safe to retain",
168
+ value: "KEEP EXACTLY",
169
+ },
170
+ ],
171
+ });
172
+ if (!action) throw new Error("Policy review was cancelled.");
173
+ const generated = await promptForSelection({
174
+ message: `${label}: is this column always database-generated?`,
175
+ flags,
176
+ options: [
177
+ { label: "No", value: "NEVER" },
178
+ { label: "Yes", value: "ALWAYS" },
179
+ ],
180
+ });
181
+ if (!generated) throw new Error("Policy review was cancelled.");
182
+ const identity = await promptForSelection({
183
+ message: `${label}: is this an identity column?`,
184
+ flags,
185
+ options: [
186
+ { label: "No", value: "NO" },
187
+ { label: "Yes", value: "YES" },
188
+ ],
189
+ });
190
+ if (!identity) throw new Error("Policy review was cancelled.");
191
+ const hasForeignKey = await promptForConfirmation(
192
+ `${label}: does this column reference another table?`,
193
+ flags,
194
+ { cancelValue: null },
195
+ );
196
+ if (hasForeignKey === null) throw new Error("Policy review was cancelled.");
197
+ let foreignKey = null;
198
+ if (hasForeignKey) {
199
+ const identifier = (value) =>
200
+ /^[a-z][a-z0-9_]{0,62}$/u.test(value)
201
+ ? undefined
202
+ : "Use a lowercase PostgreSQL identifier.";
203
+ const schema = await promptForPath({
204
+ message: `${label}: referenced schema`,
205
+ defaultValue: "public",
206
+ flags,
207
+ validate: identifier,
208
+ });
209
+ const referencedTable = await promptForPath({
210
+ message: `${label}: referenced table`,
211
+ flags,
212
+ validate: identifier,
213
+ });
214
+ const referencedColumn = await promptForPath({
215
+ message: `${label}: referenced column`,
216
+ flags,
217
+ validate: identifier,
218
+ });
219
+ if (!schema || !referencedTable || !referencedColumn) {
220
+ throw new Error("Policy review was cancelled.");
221
+ }
222
+ foreignKey = {
223
+ schema,
224
+ table: referencedTable,
225
+ column: referencedColumn,
226
+ };
227
+ }
228
+ return { action, generated, identity, foreignKey };
229
+ };
230
+
231
+ const reviewPolicyInteractively = async ({ state, flags }) => {
232
+ const { source, draft } = await readReviewablePolicyDraft(
233
+ state.policyAbsolutePath,
234
+ );
235
+ const totalColumns = draft.tables.reduce(
236
+ (total, table) => total + (table.columns?.length ?? 0),
237
+ 0,
238
+ );
239
+ const tableSummaries = [];
240
+ let individuallyReviewed = 0;
241
+ if (useStyledPrompts(flags)) {
242
+ prompts.note(
243
+ [
244
+ `${formatCount(totalColumns, "column")} across ${formatCount(draft.tables.length, "table")} require explicit review.`,
245
+ "No row values are displayed or changed by this step.",
246
+ "Safe defaults replace values, mark generated NEVER and identity NO, and set no foreign key.",
247
+ "Likely structural columns start selected for individual review.",
248
+ ].join("\n"),
249
+ "Review the script",
250
+ );
251
+ }
252
+ const policy = await completePolicyDraft({
253
+ draft,
254
+ reviewColumn: async (column) => {
255
+ individuallyReviewed += 1;
256
+ return reviewColumn({ ...column, flags });
257
+ },
258
+ reviewTable: async ({ table, columns }) => {
259
+ if (columns.length === 1) {
260
+ tableSummaries.push({ table, defaulted: 0, individual: 1 });
261
+ return {};
262
+ }
263
+ const mode = await promptForSelection({
264
+ message: `${table}: how would you like to review ${formatCount(columns.length, "column")}?`,
265
+ flags,
266
+ options: [
267
+ {
268
+ label: "Use safe defaults, then review exceptions",
269
+ hint: "Best for larger tables",
270
+ value: "preset",
271
+ },
272
+ { label: "Review every column individually", value: "individual" },
273
+ ],
274
+ });
275
+ if (!mode) throw new Error("Policy review was cancelled.");
276
+ if (mode === "individual") {
277
+ tableSummaries.push({
278
+ table,
279
+ defaulted: 0,
280
+ individual: columns.length,
281
+ });
282
+ return {};
283
+ }
284
+ const exceptions = await promptForMultipleChoice({
285
+ message: `${table}: select columns that need individual review`,
286
+ flags,
287
+ options: columns.map((column) => ({ label: column, value: column })),
288
+ initialValues: suggestPolicyExceptionColumns(columns),
289
+ });
290
+ if (!exceptions) throw new Error("Policy review was cancelled.");
291
+ const tableDecisions = createSafeTablePreset(columns);
292
+ for (const column of exceptions) delete tableDecisions[column];
293
+ tableSummaries.push({
294
+ table,
295
+ defaulted: columns.length - exceptions.length,
296
+ individual: exceptions.length,
297
+ });
298
+ if (useStyledPrompts(flags)) {
299
+ prompts.log.info(
300
+ `${table}: ${formatCount(columns.length - exceptions.length, "safe default")}; ${formatCount(exceptions.length, "exception")}.`,
301
+ );
302
+ }
303
+ return tableDecisions;
304
+ },
305
+ });
306
+ const defaultedColumns = totalColumns - individuallyReviewed;
307
+ const summaryLines = tableSummaries.map(
308
+ (summary) =>
309
+ `${summary.table}: ${formatCount(summary.defaulted, "safe default")}; ${formatCount(summary.individual, "individual review")}`,
310
+ );
311
+ if (useStyledPrompts(flags)) {
312
+ prompts.note(summaryLines.join("\n"), "Review summary");
313
+ } else {
314
+ console.log(["", "Review summary", ...summaryLines, ""].join("\n"));
315
+ }
316
+ const accepted = await promptForConfirmation(
317
+ `Save ${formatCount(totalColumns, "classified column")} and activate this policy?`,
318
+ flags,
319
+ );
320
+ if (!accepted) return false;
321
+ await applyReviewedPolicy({
322
+ path: state.policyAbsolutePath,
323
+ originalSource: source,
324
+ policy,
325
+ });
326
+ const result = `${formatCount(totalColumns, "column")} classified (${formatCount(defaultedColumns, "safe default")}; ${formatCount(individuallyReviewed, "individual review")}).`;
327
+ if (useStyledPrompts(flags)) {
328
+ prompts.log.success(`${result} The policy is ready for baseline creation.`);
329
+ } else {
330
+ console.log(`Reviewed policy saved: ${result}`);
331
+ }
332
+ return true;
333
+ };
334
+
335
+ const guidedOptions = (state, hasLastDetails) => {
336
+ const options = [];
337
+ if (!state.node.supported) {
338
+ options.push({
339
+ label: "Show Node.js 24 setup instructions",
340
+ hint: "Required before Rehearsal can change this project",
341
+ command: "node-help",
342
+ });
343
+ } else if (state.config === "missing") {
344
+ options.push({
345
+ label: "Set the stage",
346
+ hint: "Create safe local configuration",
347
+ command: "setup-write",
348
+ });
349
+ } else if (state.baseline) {
350
+ options.push(
351
+ { label: "Run a rehearsal", command: "run" },
352
+ ...(state.refreshConfigured
353
+ ? [
354
+ {
355
+ label: "Refresh the database copy",
356
+ hint: "Replace safely, reset locally, then remove old copies",
357
+ command: "refresh-guide",
358
+ },
359
+ ]
360
+ : []),
361
+ { label: "Review the migration plan", command: "explain" },
362
+ { label: "Show candidate migrations", command: "candidates" },
363
+ { label: "Check readiness", command: "doctor" },
364
+ );
365
+ } else if (state.node.supported && state.config === "valid") {
366
+ if (state.policy === "missing") {
367
+ options.push({
368
+ label: "Prepare the script",
369
+ hint: "Create a reviewable policy draft",
370
+ command: "baseline-prepare",
371
+ });
372
+ } else if (state.policy === "needs-review") {
373
+ options.push({
374
+ label: "Review the script",
375
+ hint: "Apply table presets and review exceptions",
376
+ command: "policy-review",
377
+ });
378
+ } else {
379
+ options.push({
380
+ label: "Create the baseline",
381
+ hint: "Activate reviewed local inputs",
382
+ command: "baseline-guide",
383
+ });
384
+ }
385
+ options.push({ label: "Check readiness", command: "doctor" });
386
+ } else {
387
+ options.push({ label: "Check readiness", command: "doctor" });
388
+ }
389
+ if (state.node.supported && state.runtime) {
390
+ options.push({
391
+ label: "Manage the local runtime",
392
+ hint: "Status, verification, and cleanup",
393
+ command: "manage-runtime",
394
+ });
395
+ }
396
+ if (state.node.supported && state.config === "valid") {
397
+ options.push({
398
+ label: "Clean up disk space",
399
+ hint: "Preview old baselines, the runtime, and unused images",
400
+ command: "cleanup-guide",
401
+ });
402
+ }
403
+ if (hasLastDetails) {
404
+ options.push({
405
+ label: "Show details from the last action",
406
+ command: "last-details",
407
+ });
408
+ }
409
+ options.push(
410
+ {
411
+ label: "Get help",
412
+ hint: "Create a safe, copy-ready support report",
413
+ command: "support",
414
+ },
415
+ { label: "Show all commands", command: "help" },
416
+ { label: "Exit", command: "exit" },
417
+ );
418
+ return options;
419
+ };
420
+
421
+ const showGuidedStatus = ({ state, options, flags }) => {
422
+ const marker = (complete) =>
423
+ complete ? terminalStyle(flags, "32", "✓") : terminalStyle(flags, "2", "○");
424
+ const statusLines = [
425
+ `${marker(state.node.supported)} Node.js ${state.node.version}${state.node.supported ? "" : " (Node.js 24 required)"}`,
426
+ `${marker(state.target === "supabase" ? state.detected.hasSupabaseConfig : state.detected.hasPostgresqlMigrations)} ${state.target === "supabase" ? "Supabase" : "PostgreSQL"} project detected`,
427
+ `${marker(state.target === "supabase" ? state.detected.hasMigrations : state.detected.hasPostgresqlMigrations)} Migration history detected`,
428
+ `${marker(state.config === "valid")} Rehearsal configuration${state.config === "invalid" ? " needs attention" : ""}`,
429
+ ...(state.dependentCount
430
+ ? [
431
+ `${marker(true)} ${formatCount(state.dependentCount, "dependent database")} included`,
432
+ ]
433
+ : []),
434
+ ...(state.config === "valid"
435
+ ? [
436
+ `${marker(state.policy === "reviewed")} ${state.policy === "needs-review" ? "Baseline policy needs review" : state.policy === "reviewed" ? "Baseline policy reviewed" : "Baseline policy needed"}`,
437
+ ]
438
+ : []),
439
+ `${marker(state.baseline)} Verified baseline`,
440
+ ...(state.runtime ? [`${marker(true)} Disposable runtime created`] : []),
441
+ ...(state.configError
442
+ ? ["", terminalStyle(flags, "33", `! ${state.configError}`)]
443
+ : []),
444
+ ];
445
+ const stage =
446
+ state.config !== "valid"
447
+ ? 1
448
+ : state.policy !== "reviewed"
449
+ ? 2
450
+ : !state.baseline
451
+ ? 3
452
+ : 4;
453
+ if (useStyledPrompts(flags)) {
454
+ prompts.note(
455
+ statusLines.join("\n"),
456
+ `${state.detected.projectName} · Stage ${stage} of 4`,
457
+ );
458
+ } else {
459
+ console.log(
460
+ [
461
+ "",
462
+ terminalStyle(flags, "1;36", "REHEARSAL"),
463
+ "Safe local migration testing",
464
+ "",
465
+ terminalStyle(flags, "1", state.detected.projectName),
466
+ `Stage ${stage} of 4`,
467
+ ...statusLines,
468
+ "",
469
+ "What would you like to do?",
470
+ "",
471
+ ...options.map(
472
+ (option, index) =>
473
+ ` ${index === 0 ? terminalStyle(flags, "36", "›") : " "} ${index + 1}. ${option.label}`,
474
+ ),
475
+ "",
476
+ ].join("\n"),
477
+ );
478
+ }
479
+ };
480
+
481
+ const guideCleanup = async ({
482
+ flags,
483
+ planOptions,
484
+ planRuntimeStackCleanup,
485
+ }) => {
486
+ const cleanupScope = await promptForChoice({
487
+ message: "What may Rehearsal include in the cleanup preview?",
488
+ flags,
489
+ options: [
490
+ {
491
+ label: "Old baselines only",
492
+ hint: "Keep the configured number of recent generations",
493
+ command: "baselines",
494
+ },
495
+ {
496
+ label: "Old baselines and this project's runtime",
497
+ hint: "Also remove the disposable database and its volumes",
498
+ command: "runtime",
499
+ },
500
+ {
501
+ label: "Old baselines and older unused Supabase images",
502
+ hint: "Images may be shared and can be downloaded again",
503
+ command: "images",
504
+ },
505
+ { label: "Everything above", command: "all" },
506
+ { label: "Back", command: "exit" },
507
+ ],
508
+ });
509
+ if (!cleanupScope || cleanupScope.command === "exit") return "home";
510
+ flags.includeRuntime = ["runtime", "all"].includes(cleanupScope.command);
511
+ flags.includeImages = ["images", "all"].includes(cleanupScope.command);
512
+ flags.cleanupPlan = await planRuntimeStackCleanup({ flags, planOptions });
513
+ console.log(
514
+ `\n${renderCleanup({ mode: "preview", plan: flags.cleanupPlan })}`,
515
+ );
516
+ const selectedCount = flags.cleanupPlan.targets
517
+ ? flags.cleanupPlan.targets.reduce(
518
+ (total, target) =>
519
+ total +
520
+ target.plan.baselines.removed.length +
521
+ (target.plan.runtime.included && target.plan.runtime.detected
522
+ ? 1
523
+ : 0) +
524
+ target.plan.images.candidates.length,
525
+ 0,
526
+ )
527
+ : flags.cleanupPlan.baselines.removed.length +
528
+ (flags.cleanupPlan.runtime.included && flags.cleanupPlan.runtime.detected
529
+ ? 1
530
+ : 0) +
531
+ flags.cleanupPlan.images.candidates.length;
532
+ if (selectedCount === 0) return "home";
533
+ const accepted = await promptForConfirmation(
534
+ "Remove exactly the resources shown above?",
535
+ flags,
536
+ );
537
+ if (!accepted) return "home";
538
+ flags.write = true;
539
+ flags.cleanupConfirmation = flags.cleanupPlan.digest;
540
+ return "cleanup";
541
+ };
542
+
543
+ const guideBaseline = async ({
544
+ state,
545
+ selected,
546
+ flags,
547
+ planOptions,
548
+ projectRoot,
549
+ }) => {
550
+ console.log(
551
+ [
552
+ "",
553
+ terminalStyle(flags, "1", "CREATE A SAFE BASELINE"),
554
+ "Rehearsal only reads the local files you name here.",
555
+ "It does not extract data or contact a hosted database.",
556
+ "",
557
+ ].join("\n"),
558
+ );
559
+ const discovered = await discoverBaselineInputFiles({ projectRoot });
560
+ const detectedLines = [
561
+ discovered.records.length
562
+ ? `Records: ${discovered.records.join(", ")}`
563
+ : "Records: none detected",
564
+ discovered.ledgers.length
565
+ ? `Migration ledgers: ${discovered.ledgers.join(", ")}`
566
+ : "Migration ledgers: none detected",
567
+ ...(state.target === "supabase"
568
+ ? [
569
+ discovered.assetManifests.length
570
+ ? `Storage manifests: ${discovered.assetManifests.join(", ")}`
571
+ : "Storage manifests: none detected",
572
+ ]
573
+ : []),
574
+ "Only paths and file structure were inspected; row values stay hidden.",
575
+ ];
576
+ if (useStyledPrompts(flags)) {
577
+ prompts.note(detectedLines.join("\n"), "Detected safe local inputs");
578
+ } else {
579
+ console.log(
580
+ ["Detected safe local inputs", ...detectedLines, ""].join("\n"),
581
+ );
582
+ }
583
+ flags.recordsPath = await promptForDiscoveredPath({
584
+ message: "Sanitized NDJSON records",
585
+ candidates: discovered.records,
586
+ flags,
587
+ });
588
+ flags.ledgerPath = await promptForDiscoveredPath({
589
+ message: "Migration ledger",
590
+ candidates: discovered.ledgers,
591
+ flags,
592
+ });
593
+ try {
594
+ if (selected.command === "baseline-prepare") {
595
+ const preparationPlan = await planBaselinePreparation({
596
+ ...planOptions,
597
+ recordsPath: flags.recordsPath,
598
+ ledgerPath: flags.ledgerPath,
599
+ });
600
+ const preview = {
601
+ ...summarizeBaselinePreparation(preparationPlan, { mode: "preview" }),
602
+ nextAction:
603
+ "Review this schema-only summary. No file is written unless you approve below.",
604
+ };
605
+ console.log(`\n${renderBaselinePreparation(preview, flags)}\n`);
606
+ const accepted = await promptForConfirmation(
607
+ `Create the REVIEW REQUIRED draft at ${preview.destination}?`,
608
+ flags,
609
+ );
610
+ if (!accepted) {
611
+ console.log("No changes made.");
612
+ return "home";
613
+ }
614
+ flags.preparationPlan = preparationPlan;
615
+ flags.write = true;
616
+ return "baseline prepare";
617
+ }
618
+ flags.assetsPath =
619
+ state.target === "supabase"
620
+ ? await promptForDiscoveredPath({
621
+ message: "Storage assets",
622
+ candidates: discovered.assetManifests,
623
+ optional: true,
624
+ flags,
625
+ })
626
+ : undefined;
627
+ const inspection = await inspectBaselineInputFiles({
628
+ ...planOptions,
629
+ recordsPath: flags.recordsPath,
630
+ ledgerPath: flags.ledgerPath,
631
+ assetsPath: flags.assetsPath,
632
+ });
633
+ console.log(`\n${renderBaselineInputInspection(inspection, flags)}\n`);
634
+ const accepted = await promptForConfirmation(
635
+ "Create and activate this immutable local baseline?",
636
+ flags,
637
+ );
638
+ if (!accepted) {
639
+ console.log("No changes made.");
640
+ return "home";
641
+ }
642
+ return "baseline create";
643
+ } catch (error) {
644
+ const message = redactDiagnosticValue(String(error?.message ?? error));
645
+ if (useStyledPrompts(flags)) {
646
+ prompts.cancel("These baseline inputs need attention.");
647
+ prompts.note(message, "How to continue");
648
+ } else {
649
+ console.log(`\nBASELINE INPUTS NEED ATTENTION\n${message}\n`);
650
+ }
651
+ return "home";
652
+ }
653
+ };
654
+
655
+ export const createGuidedHome =
656
+ ({
657
+ projectRoot,
658
+ topologyCandidateSummary,
659
+ buildRefreshWorkflowPlan,
660
+ planRuntimeStackCleanup,
661
+ getLastGuidedDetails,
662
+ }) =>
663
+ async ({ flags, planOptions }) => {
664
+ const state = await inspectGuidedProject({
665
+ projectRoot,
666
+ planOptions,
667
+ topologyCandidateSummary,
668
+ });
669
+ const lastDetails = getLastGuidedDetails();
670
+ const options = guidedOptions(state, Boolean(lastDetails));
671
+ showGuidedStatus({ state, options, flags });
672
+ const selected = await promptForChoice({
673
+ message: "What would you like to do?",
674
+ options,
675
+ flags,
676
+ optionsAlreadyShown: !useStyledPrompts(flags),
677
+ });
678
+ if (!selected || selected.command === "exit") return null;
679
+ if (selected.command === "node-help") {
680
+ console.log(
681
+ [
682
+ "",
683
+ terminalStyle(flags, "1", "NODE.JS 24 REQUIRED"),
684
+ `This shell is using Node.js ${state.node.version}. Rehearsal has not changed your project.`,
685
+ "",
686
+ "With nvm:",
687
+ " nvm install 24",
688
+ " nvm use 24",
689
+ "",
690
+ "Then reinstall Rehearsal in your scratch project and run npx rehearsal again.",
691
+ ].join("\n"),
692
+ );
693
+ return "home";
694
+ }
695
+ if (selected.command === "policy-review") {
696
+ try {
697
+ await reviewPolicyInteractively({ state, flags });
698
+ } catch (error) {
699
+ if (
700
+ String(error?.message ?? error) !== "Policy review was cancelled."
701
+ ) {
702
+ throw error;
703
+ }
704
+ if (useStyledPrompts(flags)) {
705
+ prompts.cancel("Policy review cancelled; the draft was not changed.");
706
+ } else {
707
+ console.log("Policy review cancelled; the draft was not changed.");
708
+ }
709
+ }
710
+ return "home";
711
+ }
712
+ if (selected.command === "last-details") {
713
+ if (useStyledPrompts(flags)) {
714
+ prompts.note(lastDetails, "Technical details");
715
+ } else {
716
+ console.log(`\nTECHNICAL DETAILS\n${lastDetails}`);
717
+ }
718
+ return "home";
719
+ }
720
+ if (selected.command === "refresh-guide") {
721
+ const { plan } = await buildRefreshWorkflowPlan(planOptions);
722
+ console.log(`\n${renderRefreshWorkflow({ mode: "preview", plan })}\n`);
723
+ const accepted = await promptForConfirmation(
724
+ "Create this replacement, discard current runtime edits, and remove exactly the listed old copies after success?",
725
+ flags,
726
+ );
727
+ if (!accepted) return "home";
728
+ flags.refreshConfirmation = plan.digest;
729
+ return "refresh";
730
+ }
731
+ if (selected.command === "manage-runtime") {
732
+ const runtimeAction = await promptForChoice({
733
+ message: "Manage the disposable local runtime",
734
+ flags,
735
+ options: [
736
+ { label: "Show status", command: "status" },
737
+ { label: "Verify runtime", command: "verify" },
738
+ { label: "Reset to the immutable baseline", command: "reset" },
739
+ { label: "Stop runtime", command: "stop" },
740
+ { label: "Discard runtime", command: "discard" },
741
+ { label: "Back", command: "exit" },
742
+ ],
743
+ });
744
+ if (!runtimeAction || runtimeAction.command === "exit") return "home";
745
+ if (["reset", "discard"].includes(runtimeAction.command)) {
746
+ const accepted = await promptForConfirmation(
747
+ runtimeAction.command === "reset"
748
+ ? "Reset the disposable runtime and remove its current changes?"
749
+ : "Discard the disposable runtime? The immutable baseline will remain.",
750
+ flags,
751
+ );
752
+ if (!accepted) return "home";
753
+ }
754
+ return runtimeAction.command;
755
+ }
756
+ if (selected.command === "cleanup-guide") {
757
+ return guideCleanup({ flags, planOptions, planRuntimeStackCleanup });
758
+ }
759
+ if (["baseline-guide", "baseline-prepare"].includes(selected.command)) {
760
+ return guideBaseline({
761
+ state,
762
+ selected,
763
+ flags,
764
+ planOptions,
765
+ projectRoot,
766
+ });
767
+ }
768
+ if (selected.command !== "setup-write") return selected.command;
769
+
770
+ const targetChoice = await promptForChoice({
771
+ message: "Which database should Rehearsal use?",
772
+ flags,
773
+ options:
774
+ state.target === "supabase"
775
+ ? [
776
+ { label: "Supabase", command: "supabase" },
777
+ { label: "PostgreSQL", command: "postgresql" },
778
+ ]
779
+ : [
780
+ { label: "PostgreSQL", command: "postgresql" },
781
+ { label: "Supabase", command: "supabase" },
782
+ ],
783
+ });
784
+ if (!targetChoice) return "home";
785
+ flags.target = targetChoice.command;
786
+ const setupPlan = await planRehearsalSetup({
787
+ projectRoot,
788
+ target: flags.target,
789
+ });
790
+ const preview = {
791
+ ...summarizeRehearsalSetup(setupPlan, { mode: "preview" }),
792
+ nextAction:
793
+ "Review this plan. No files are written unless you approve below.",
794
+ };
795
+ console.log(`\n${renderSetup(preview)}\n`);
796
+ const accepted = await promptForConfirmation(
797
+ "Create these project-local files?",
798
+ flags,
799
+ );
800
+ if (!accepted) {
801
+ console.log("No changes made.");
802
+ return "home";
803
+ }
804
+ flags.setupPlan = setupPlan;
805
+ flags.write = true;
806
+ return "setup";
807
+ };