@rehearsal-db/core 0.1.0-beta.3 → 0.1.0-beta.5

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.
@@ -11,6 +11,7 @@ import { join, relative } from "node:path";
11
11
  import { performance } from "node:perf_hooks";
12
12
  import { createInterface } from "node:readline/promises";
13
13
  import { fileURLToPath } from "node:url";
14
+ import * as prompts from "@clack/prompts";
14
15
  import {
15
16
  RehearsalError,
16
17
  createRehearsalResult,
@@ -35,9 +36,11 @@ import {
35
36
  import { createSyntheticBaselineFromFiles } from "../../lib/rehearsal/baseline_builder.mjs";
36
37
  import {
37
38
  applyBaselinePreparation,
39
+ inspectBaselineInputFiles,
38
40
  planBaselinePreparation,
39
41
  summarizeBaselinePreparation,
40
42
  } from "../../lib/rehearsal/baseline_preparation.mjs";
43
+ import { discoverBaselineInputFiles } from "../../lib/rehearsal/input_discovery.mjs";
41
44
  import {
42
45
  applyRehearsalSetup,
43
46
  inspectRehearsalNodeRuntime,
@@ -45,6 +48,14 @@ import {
45
48
  summarizeRehearsalSetup,
46
49
  } from "../../lib/rehearsal/setup.mjs";
47
50
  import { formatCount } from "../../lib/rehearsal/human_output.mjs";
51
+ import {
52
+ applyReviewedPolicy,
53
+ completePolicyDraft,
54
+ createSafeTablePreset,
55
+ readReviewablePolicyDraft,
56
+ suggestPolicyExceptionColumns,
57
+ } from "../../lib/rehearsal/policy_review.mjs";
58
+ import { collectRehearsalSupportReport } from "../../lib/rehearsal/support_report.mjs";
48
59
 
49
60
  const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
50
61
  const projectRoot = process.cwd();
@@ -54,6 +65,7 @@ const managerPath = join(
54
65
  packageRoot,
55
66
  "scripts/operations/database/manage_rehearsal_database.mjs",
56
67
  );
68
+ let lastGuidedDetails;
57
69
 
58
70
  const parseArguments = (arguments_) => {
59
71
  const flags = {
@@ -255,12 +267,28 @@ const terminalStyle = (flags, code, value) =>
255
267
  const isHumanTerminal = (flags) =>
256
268
  !flags.json && process.stdin.isTTY && process.stdout.isTTY;
257
269
 
270
+ const useStyledPrompts = (flags) => isHumanTerminal(flags) && useColor(flags);
271
+
258
272
  const formatDuration = (durationMs) =>
259
273
  durationMs < 1_000
260
274
  ? `${Math.round(durationMs)}ms`
261
275
  : `${(durationMs / 1_000).toFixed(1)}s`;
262
276
 
263
- const promptForChoice = async ({ message, options }) => {
277
+ const promptForChoice = async ({ message, options, flags = {} }) => {
278
+ if (useStyledPrompts(flags)) {
279
+ const selected = await prompts.select({
280
+ message,
281
+ maxItems: options.length,
282
+ options: options.map((option) => ({
283
+ value: option,
284
+ label: option.label,
285
+ hint: option.hint,
286
+ })),
287
+ });
288
+ return prompts.isCancel(selected)
289
+ ? options.find((option) => option.command === "exit")
290
+ : selected;
291
+ }
264
292
  const prompt = createInterface({
265
293
  input: process.stdin,
266
294
  output: process.stdout,
@@ -283,7 +311,15 @@ const promptForChoice = async ({ message, options }) => {
283
311
  }
284
312
  };
285
313
 
286
- const promptForConfirmation = async (message) => {
314
+ const promptForConfirmation = async (
315
+ message,
316
+ flags = {},
317
+ { cancelValue = false } = {},
318
+ ) => {
319
+ if (useStyledPrompts(flags)) {
320
+ const accepted = await prompts.confirm({ message, initialValue: false });
321
+ return prompts.isCancel(accepted) ? cancelValue : accepted;
322
+ }
287
323
  const prompt = createInterface({
288
324
  input: process.stdin,
289
325
  output: process.stdout,
@@ -298,7 +334,27 @@ const promptForConfirmation = async (message) => {
298
334
  }
299
335
  };
300
336
 
301
- const promptForPath = async ({ message, defaultValue, optional = false }) => {
337
+ const promptForPath = async ({
338
+ message,
339
+ defaultValue,
340
+ optional = false,
341
+ flags = {},
342
+ validate,
343
+ }) => {
344
+ if (useStyledPrompts(flags)) {
345
+ const answer = await prompts.text({
346
+ message,
347
+ placeholder: defaultValue ?? (optional ? "Leave blank for none" : ""),
348
+ defaultValue,
349
+ validate: (value) => {
350
+ const resolved = String(value || defaultValue || "").trim();
351
+ if (!optional && !resolved) return "Enter a value.";
352
+ return resolved && validate ? validate(resolved) : undefined;
353
+ },
354
+ });
355
+ if (prompts.isCancel(answer)) return undefined;
356
+ return String(answer || defaultValue || "").trim() || undefined;
357
+ }
302
358
  const prompt = createInterface({
303
359
  input: process.stdin,
304
360
  output: process.stdout,
@@ -312,6 +368,11 @@ const promptForPath = async ({ message, defaultValue, optional = false }) => {
312
368
  : " (required)";
313
369
  const answer = (await prompt.question(`${message}${suffix}: `)).trim();
314
370
  const value = answer || defaultValue || "";
371
+ const invalid = value && validate ? validate(value) : undefined;
372
+ if (invalid) {
373
+ console.log(invalid);
374
+ continue;
375
+ }
315
376
  if (value || optional) return value || undefined;
316
377
  console.log("Enter a project-relative path.");
317
378
  }
@@ -320,11 +381,107 @@ const promptForPath = async ({ message, defaultValue, optional = false }) => {
320
381
  }
321
382
  };
322
383
 
323
- const firstExistingPath = async (candidates) => {
324
- for (const candidate of candidates) {
325
- if (await pathExists(join(projectRoot, candidate))) return candidate;
384
+ const promptForDiscoveredPath = async ({
385
+ message,
386
+ candidates,
387
+ optional = false,
388
+ flags,
389
+ }) => {
390
+ if (candidates.length === 0) {
391
+ return promptForPath({ message, optional, flags });
392
+ }
393
+ const noneValue = "\0none";
394
+ const manualValue = "\0manual";
395
+ const selected = await promptForSelection({
396
+ message,
397
+ flags,
398
+ options: [
399
+ ...(optional
400
+ ? [
401
+ {
402
+ label: "No storage assets",
403
+ hint: "Continue without an asset manifest",
404
+ value: noneValue,
405
+ },
406
+ ]
407
+ : []),
408
+ ...candidates.map((path) => ({ label: path, value: path })),
409
+ { label: "Enter another path", value: manualValue },
410
+ ],
411
+ });
412
+ if (!selected || selected === noneValue) return undefined;
413
+ if (selected !== manualValue) return selected;
414
+ return promptForPath({ message, optional, flags });
415
+ };
416
+
417
+ const promptForSelection = async ({ message, options, flags }) => {
418
+ const selected = await promptForChoice({ message, options, flags });
419
+ return selected?.value;
420
+ };
421
+
422
+ const promptForMultipleChoice = async ({
423
+ message,
424
+ options,
425
+ initialValues = [],
426
+ flags,
427
+ }) => {
428
+ if (useStyledPrompts(flags)) {
429
+ const selected = await prompts.multiselect({
430
+ message,
431
+ options: options.map((option) => ({
432
+ value: option.value,
433
+ label: option.label,
434
+ hint: option.hint,
435
+ })),
436
+ initialValues,
437
+ required: false,
438
+ });
439
+ return prompts.isCancel(selected) ? undefined : selected;
440
+ }
441
+ const initial = new Set(initialValues);
442
+ console.log("");
443
+ console.log(message);
444
+ for (const [index, option] of options.entries()) {
445
+ console.log(
446
+ ` ${index + 1}. ${option.label}${initial.has(option.value) ? " (suggested)" : ""}`,
447
+ );
448
+ }
449
+ const defaultNumbers = options
450
+ .map((option, index) => (initial.has(option.value) ? index + 1 : undefined))
451
+ .filter(Boolean)
452
+ .join(",");
453
+ const prompt = createInterface({
454
+ input: process.stdin,
455
+ output: process.stdout,
456
+ });
457
+ try {
458
+ while (true) {
459
+ const suffix = defaultNumbers ? ` [${defaultNumbers}]` : " [none]";
460
+ const answer = (
461
+ await prompt.question(
462
+ `Choose comma-separated numbers, all, or none${suffix}: `,
463
+ )
464
+ )
465
+ .trim()
466
+ .toLowerCase();
467
+ if (!answer) return [...initialValues];
468
+ if (answer === "none") return [];
469
+ if (answer === "all") return options.map((option) => option.value);
470
+ const indexes = answer.split(",").map((value) => Number(value.trim()));
471
+ if (
472
+ indexes.length > 0 &&
473
+ indexes.every(
474
+ (index) =>
475
+ Number.isInteger(index) && index >= 1 && index <= options.length,
476
+ )
477
+ ) {
478
+ return [...new Set(indexes)].map((index) => options[index - 1].value);
479
+ }
480
+ console.log(`Choose numbers from 1 to ${options.length}, all, or none.`);
481
+ }
482
+ } finally {
483
+ prompt.close();
326
484
  }
327
- return undefined;
328
485
  };
329
486
 
330
487
  const inspectGuidedProject = async (planOptions) => {
@@ -391,6 +548,7 @@ const inspectGuidedProject = async (planOptions) => {
391
548
  baseline: Boolean(migrationSummary),
392
549
  policy,
393
550
  policyPath: relative(projectRoot, loaded.paths.sanitizationPolicy),
551
+ policyAbsolutePath: loaded.paths.sanitizationPolicy,
394
552
  migrationSummary,
395
553
  runtime: await pathExists(
396
554
  join(loaded.paths.runtimeWorkdir, "baseline.json"),
@@ -398,6 +556,205 @@ const inspectGuidedProject = async (planOptions) => {
398
556
  };
399
557
  };
400
558
 
559
+ const reviewPolicyInteractively = async ({ state, flags }) => {
560
+ const { source, draft } = await readReviewablePolicyDraft(
561
+ state.policyAbsolutePath,
562
+ );
563
+ const totalColumns = draft.tables.reduce(
564
+ (total, table) => total + (table.columns?.length ?? 0),
565
+ 0,
566
+ );
567
+ const tableSummaries = [];
568
+ let individuallyReviewed = 0;
569
+ if (useStyledPrompts(flags)) {
570
+ prompts.note(
571
+ [
572
+ `${formatCount(totalColumns, "column")} across ${formatCount(draft.tables.length, "table")} require explicit review.`,
573
+ "No row values are displayed or changed by this step.",
574
+ "Safe defaults replace values, mark generated NEVER and identity NO, and set no foreign key.",
575
+ "Likely structural columns start selected for individual review.",
576
+ ].join("\n"),
577
+ "Review the script",
578
+ );
579
+ }
580
+ const reviewColumn = async ({ table, column }) => {
581
+ individuallyReviewed += 1;
582
+ const label = `${table}.${column}`;
583
+ const action = await promptForSelection({
584
+ message: `${label}: how should this value be handled?`,
585
+ flags,
586
+ options: [
587
+ {
588
+ label: "Replace with synthetic data",
589
+ hint: "Safest default",
590
+ value: "REPLACE WITH SYNTHETIC",
591
+ },
592
+ {
593
+ label: "Pseudonymize consistently",
594
+ hint: "Stable links without original values",
595
+ value: "PSEUDONYMIZE",
596
+ },
597
+ {
598
+ label: "Derive a safe value",
599
+ value: "DERIVE",
600
+ },
601
+ { label: "Exclude this field", value: "EXCLUDE" },
602
+ {
603
+ label: "Keep exactly",
604
+ hint: "Only for values explicitly safe to retain",
605
+ value: "KEEP EXACTLY",
606
+ },
607
+ ],
608
+ });
609
+ if (!action) throw new Error("Policy review was cancelled.");
610
+ const generated = await promptForSelection({
611
+ message: `${label}: is this column always database-generated?`,
612
+ flags,
613
+ options: [
614
+ { label: "No", value: "NEVER" },
615
+ { label: "Yes", value: "ALWAYS" },
616
+ ],
617
+ });
618
+ if (!generated) throw new Error("Policy review was cancelled.");
619
+ const identity = await promptForSelection({
620
+ message: `${label}: is this an identity column?`,
621
+ flags,
622
+ options: [
623
+ { label: "No", value: "NO" },
624
+ { label: "Yes", value: "YES" },
625
+ ],
626
+ });
627
+ if (!identity) throw new Error("Policy review was cancelled.");
628
+ const hasForeignKey = await promptForConfirmation(
629
+ `${label}: does this column reference another table?`,
630
+ flags,
631
+ { cancelValue: null },
632
+ );
633
+ if (hasForeignKey === null) {
634
+ throw new Error("Policy review was cancelled.");
635
+ }
636
+ let foreignKey = null;
637
+ if (hasForeignKey) {
638
+ const schema = await promptForPath({
639
+ message: `${label}: referenced schema`,
640
+ defaultValue: "public",
641
+ flags,
642
+ validate: (value) =>
643
+ /^[a-z][a-z0-9_]{0,62}$/u.test(value)
644
+ ? undefined
645
+ : "Use a lowercase PostgreSQL identifier.",
646
+ });
647
+ const referencedTable = await promptForPath({
648
+ message: `${label}: referenced table`,
649
+ flags,
650
+ validate: (value) =>
651
+ /^[a-z][a-z0-9_]{0,62}$/u.test(value)
652
+ ? undefined
653
+ : "Use a lowercase PostgreSQL identifier.",
654
+ });
655
+ const referencedColumn = await promptForPath({
656
+ message: `${label}: referenced column`,
657
+ flags,
658
+ validate: (value) =>
659
+ /^[a-z][a-z0-9_]{0,62}$/u.test(value)
660
+ ? undefined
661
+ : "Use a lowercase PostgreSQL identifier.",
662
+ });
663
+ if (!schema || !referencedTable || !referencedColumn) {
664
+ throw new Error("Policy review was cancelled.");
665
+ }
666
+ foreignKey = {
667
+ schema,
668
+ table: referencedTable,
669
+ column: referencedColumn,
670
+ };
671
+ }
672
+ return { action, generated, identity, foreignKey };
673
+ };
674
+ const policy = await completePolicyDraft({
675
+ draft,
676
+ reviewColumn,
677
+ reviewTable: async ({ table, columns }) => {
678
+ if (columns.length === 1) {
679
+ tableSummaries.push({ table, defaulted: 0, individual: 1 });
680
+ return {};
681
+ }
682
+ const mode = await promptForSelection({
683
+ message: `${table}: how would you like to review ${formatCount(columns.length, "column")}?`,
684
+ flags,
685
+ options: [
686
+ {
687
+ label: "Use safe defaults, then review exceptions",
688
+ hint: "Best for larger tables",
689
+ value: "preset",
690
+ },
691
+ {
692
+ label: "Review every column individually",
693
+ value: "individual",
694
+ },
695
+ ],
696
+ });
697
+ if (!mode) throw new Error("Policy review was cancelled.");
698
+ if (mode === "individual") {
699
+ tableSummaries.push({
700
+ table,
701
+ defaulted: 0,
702
+ individual: columns.length,
703
+ });
704
+ return {};
705
+ }
706
+ const suggested = suggestPolicyExceptionColumns(columns);
707
+ const exceptions = await promptForMultipleChoice({
708
+ message: `${table}: select columns that need individual review`,
709
+ flags,
710
+ options: columns.map((column) => ({ label: column, value: column })),
711
+ initialValues: suggested,
712
+ });
713
+ if (!exceptions) throw new Error("Policy review was cancelled.");
714
+ const tableDecisions = createSafeTablePreset(columns);
715
+ for (const column of exceptions) delete tableDecisions[column];
716
+ tableSummaries.push({
717
+ table,
718
+ defaulted: columns.length - exceptions.length,
719
+ individual: exceptions.length,
720
+ });
721
+ if (useStyledPrompts(flags)) {
722
+ prompts.log.info(
723
+ `${table}: ${formatCount(columns.length - exceptions.length, "safe default")}; ${formatCount(exceptions.length, "exception")}.`,
724
+ );
725
+ }
726
+ return tableDecisions;
727
+ },
728
+ });
729
+ const defaultedColumns = totalColumns - individuallyReviewed;
730
+ const summaryLines = tableSummaries.map(
731
+ (summary) =>
732
+ `${summary.table}: ${formatCount(summary.defaulted, "safe default")}; ${formatCount(summary.individual, "individual review")}`,
733
+ );
734
+ if (useStyledPrompts(flags)) {
735
+ prompts.note(summaryLines.join("\n"), "Review summary");
736
+ } else {
737
+ console.log(["", "Review summary", ...summaryLines, ""].join("\n"));
738
+ }
739
+ const accepted = await promptForConfirmation(
740
+ `Save ${formatCount(totalColumns, "classified column")} and activate this policy?`,
741
+ flags,
742
+ );
743
+ if (!accepted) return false;
744
+ await applyReviewedPolicy({
745
+ path: state.policyAbsolutePath,
746
+ originalSource: source,
747
+ policy,
748
+ });
749
+ const result = `${formatCount(totalColumns, "column")} classified (${formatCount(defaultedColumns, "safe default")}; ${formatCount(individuallyReviewed, "individual review")}).`;
750
+ if (useStyledPrompts(flags)) {
751
+ prompts.log.success(`${result} The policy is ready for baseline creation.`);
752
+ } else {
753
+ console.log(`Reviewed policy saved: ${result}`);
754
+ }
755
+ return true;
756
+ };
757
+
401
758
  const runGuidedHome = async ({ flags, planOptions }) => {
402
759
  const state = await inspectGuidedProject(planOptions);
403
760
  const marker = (complete) =>
@@ -407,89 +764,126 @@ const runGuidedHome = async ({ flags, planOptions }) => {
407
764
  if (!state.node.supported) {
408
765
  options.push({
409
766
  label: "Show Node.js 24 setup instructions",
767
+ hint: "Required before Rehearsal can change this project",
410
768
  command: "node-help",
411
769
  });
412
770
  } else if (state.config === "missing") {
413
771
  options.push({
414
- label: "Set up Rehearsal",
772
+ label: "Set the stage",
773
+ hint: "Create safe local configuration",
415
774
  command: "setup-write",
416
775
  });
417
- } else {
418
- options.push({ label: "Check readiness", command: "doctor" });
419
- }
420
- if (state.node.supported && state.baseline) {
776
+ } else if (state.baseline) {
421
777
  options.push(
422
- { label: "Review the migration plan", command: "explain" },
423
778
  { label: "Run a rehearsal", command: "run" },
779
+ { label: "Review the migration plan", command: "explain" },
424
780
  { label: "Show candidate migrations", command: "candidates" },
781
+ { label: "Check readiness", command: "doctor" },
425
782
  );
426
783
  } else if (state.node.supported && state.config === "valid") {
427
784
  if (state.policy === "missing") {
428
785
  options.push({
429
- label: "Prepare a reviewable baseline policy draft",
786
+ label: "Prepare the script",
787
+ hint: "Create a reviewable policy draft",
430
788
  command: "baseline-prepare",
431
789
  });
432
790
  } else if (state.policy === "needs-review") {
433
791
  options.push({
434
- label: "Review the baseline policy draft",
792
+ label: "Review the script",
793
+ hint: "Apply table presets and review exceptions",
435
794
  command: "policy-review",
436
795
  });
437
796
  } else {
438
797
  options.push({
439
- label: "Create a baseline from safe local files",
798
+ label: "Create the baseline",
799
+ hint: "Activate reviewed local inputs",
440
800
  command: "baseline-guide",
441
801
  });
442
802
  }
803
+ options.push({ label: "Check readiness", command: "doctor" });
804
+ } else {
805
+ options.push({ label: "Check readiness", command: "doctor" });
443
806
  }
444
807
  if (state.node.supported && state.runtime) {
445
- options.push(
446
- { label: "Show local runtime status", command: "status" },
447
- { label: "Verify the local runtime", command: "verify" },
448
- { label: "Reset to the baseline", command: "reset" },
449
- { label: "Stop the local runtime", command: "stop" },
450
- { label: "Discard the local runtime", command: "discard" },
451
- );
808
+ options.push({
809
+ label: "Manage the local runtime",
810
+ hint: "Status, verification, and cleanup",
811
+ command: "manage-runtime",
812
+ });
813
+ }
814
+ if (lastGuidedDetails) {
815
+ options.push({
816
+ label: "Show details from the last action",
817
+ command: "last-details",
818
+ });
452
819
  }
453
820
  options.push(
821
+ {
822
+ label: "Get help",
823
+ hint: "Create a safe, copy-ready support report",
824
+ command: "support",
825
+ },
454
826
  { label: "Show all commands", command: "help" },
455
827
  { label: "Exit", command: "exit" },
456
828
  );
457
829
 
458
- console.log(
459
- [
460
- "",
461
- terminalStyle(flags, "1;36", "REHEARSAL"),
462
- "Safe local migration testing",
463
- "",
464
- terminalStyle(flags, "1", state.detected.projectName),
465
- `${marker(state.node.supported)} Node.js ${state.node.version}${state.node.supported ? "" : " (Node.js 24 required)"}`,
466
- `${marker(state.detected.hasSupabaseConfig)} Supabase project detected`,
467
- `${marker(state.detected.hasMigrations)} Migration history detected`,
468
- `${marker(state.config === "valid")} Rehearsal configuration${state.config === "invalid" ? " needs attention" : ""}`,
469
- ...(state.config === "valid"
470
- ? [
471
- `${marker(state.policy === "reviewed")} ${state.policy === "needs-review" ? "Baseline policy needs review" : state.policy === "reviewed" ? "Baseline policy reviewed" : "Baseline policy needed"}`,
472
- ]
473
- : []),
474
- `${marker(state.baseline)} Verified baseline`,
475
- ...(state.runtime ? [`${marker(true)} Disposable runtime created`] : []),
476
- ...(state.configError
477
- ? ["", terminalStyle(flags, "33", `! ${state.configError}`)]
478
- : []),
479
- "",
480
- "What would you like to do?",
481
- "",
482
- ...options.map(
483
- (option, index) =>
484
- ` ${index === 0 ? terminalStyle(flags, "36", "›") : " "} ${index + 1}. ${option.label}`,
485
- ),
486
- "",
487
- ].join("\n"),
488
- );
830
+ const statusLines = [
831
+ `${marker(state.node.supported)} Node.js ${state.node.version}${state.node.supported ? "" : " (Node.js 24 required)"}`,
832
+ `${marker(state.detected.hasSupabaseConfig)} Supabase project detected`,
833
+ `${marker(state.detected.hasMigrations)} Migration history detected`,
834
+ `${marker(state.config === "valid")} Rehearsal configuration${state.config === "invalid" ? " needs attention" : ""}`,
835
+ ...(state.config === "valid"
836
+ ? [
837
+ `${marker(state.policy === "reviewed")} ${state.policy === "needs-review" ? "Baseline policy needs review" : state.policy === "reviewed" ? "Baseline policy reviewed" : "Baseline policy needed"}`,
838
+ ]
839
+ : []),
840
+ `${marker(state.baseline)} Verified baseline`,
841
+ ...(state.runtime ? [`${marker(true)} Disposable runtime created`] : []),
842
+ ...(state.configError
843
+ ? ["", terminalStyle(flags, "33", `! ${state.configError}`)]
844
+ : []),
845
+ ];
846
+ const stage =
847
+ state.config !== "valid"
848
+ ? 1
849
+ : state.policy !== "reviewed"
850
+ ? 2
851
+ : !state.baseline
852
+ ? 3
853
+ : 4;
854
+ if (useStyledPrompts(flags)) {
855
+ prompts.note(
856
+ statusLines.join("\n"),
857
+ `${state.detected.projectName} · Stage ${stage} of 4`,
858
+ );
859
+ } else {
860
+ console.log(
861
+ [
862
+ "",
863
+ terminalStyle(flags, "1;36", "REHEARSAL"),
864
+ "Safe local migration testing",
865
+ "",
866
+ terminalStyle(flags, "1", state.detected.projectName),
867
+ `Stage ${stage} of 4`,
868
+ ...statusLines,
869
+ "",
870
+ "What would you like to do?",
871
+ "",
872
+ ...options.map(
873
+ (option, index) =>
874
+ ` ${index === 0 ? terminalStyle(flags, "36", "›") : " "} ${index + 1}. ${option.label}`,
875
+ ),
876
+ "",
877
+ ].join("\n"),
878
+ );
879
+ }
489
880
 
490
- const selected = await promptForChoice({ message: "Choose", options });
881
+ const selected = await promptForChoice({
882
+ message: "What would you like to do?",
883
+ options,
884
+ flags,
885
+ });
491
886
  if (selected.command === "exit") {
492
- console.log("No changes made.");
493
887
  return null;
494
888
  }
495
889
  if (selected.command === "node-help") {
@@ -506,20 +900,55 @@ const runGuidedHome = async ({ flags, planOptions }) => {
506
900
  "Then reinstall Rehearsal in your scratch project and run npx rehearsal again.",
507
901
  ].join("\n"),
508
902
  );
509
- return null;
903
+ return "refresh";
510
904
  }
511
905
  if (selected.command === "policy-review") {
512
- console.log(
513
- [
514
- "",
515
- terminalStyle(flags, "1", "REVIEW THE BASELINE POLICY"),
516
- `Open ${state.policyPath} and complete every REVIEW REQUIRED decision.`,
517
- "For each column, classify its sanitization action, generated status, identity behavior, and foreign key.",
518
- 'Remove "draft": true only after every decision has been reviewed.',
519
- "Then run rehearsal again to continue.",
520
- ].join("\n"),
521
- );
522
- return null;
906
+ try {
907
+ await reviewPolicyInteractively({ state, flags });
908
+ } catch (error) {
909
+ if (String(error?.message ?? error) !== "Policy review was cancelled.") {
910
+ throw error;
911
+ }
912
+ if (useStyledPrompts(flags)) {
913
+ prompts.cancel("Policy review cancelled; the draft was not changed.");
914
+ } else {
915
+ console.log("Policy review cancelled; the draft was not changed.");
916
+ }
917
+ }
918
+ return "refresh";
919
+ }
920
+ if (selected.command === "last-details") {
921
+ if (useStyledPrompts(flags)) {
922
+ prompts.note(lastGuidedDetails, "Technical details");
923
+ } else {
924
+ console.log(`\nTECHNICAL DETAILS\n${lastGuidedDetails}`);
925
+ }
926
+ return "refresh";
927
+ }
928
+ if (selected.command === "manage-runtime") {
929
+ const runtimeAction = await promptForChoice({
930
+ message: "Manage the disposable local runtime",
931
+ flags,
932
+ options: [
933
+ { label: "Show status", command: "status" },
934
+ { label: "Verify runtime", command: "verify" },
935
+ { label: "Reset to the immutable baseline", command: "reset" },
936
+ { label: "Stop runtime", command: "stop" },
937
+ { label: "Discard runtime", command: "discard" },
938
+ { label: "Back", command: "exit" },
939
+ ],
940
+ });
941
+ if (!runtimeAction || runtimeAction.command === "exit") return "refresh";
942
+ if (["reset", "discard"].includes(runtimeAction.command)) {
943
+ const accepted = await promptForConfirmation(
944
+ runtimeAction.command === "reset"
945
+ ? "Reset the disposable runtime and remove its current changes?"
946
+ : "Discard the disposable runtime? The immutable baseline will remain.",
947
+ flags,
948
+ );
949
+ if (!accepted) return "refresh";
950
+ }
951
+ return runtimeAction.command;
523
952
  }
524
953
  if (["baseline-guide", "baseline-prepare"].includes(selected.command)) {
525
954
  console.log(
@@ -531,51 +960,95 @@ const runGuidedHome = async ({ flags, planOptions }) => {
531
960
  "",
532
961
  ].join("\n"),
533
962
  );
534
- const recordsDefault = await firstExistingPath([
535
- "rehearsal/sanitized-data.ndjson",
536
- "rehearsal/synthetic-data.ndjson",
537
- ]);
538
- const ledgerDefault = await firstExistingPath([
539
- "rehearsal/migration-ledger.json",
540
- ]);
541
- flags.recordsPath = await promptForPath({
963
+ const discovered = await discoverBaselineInputFiles({ projectRoot });
964
+ const detectedLines = [
965
+ discovered.records.length
966
+ ? `Records: ${discovered.records.join(", ")}`
967
+ : "Records: none detected",
968
+ discovered.ledgers.length
969
+ ? `Migration ledgers: ${discovered.ledgers.join(", ")}`
970
+ : "Migration ledgers: none detected",
971
+ discovered.assetManifests.length
972
+ ? `Storage manifests: ${discovered.assetManifests.join(", ")}`
973
+ : "Storage manifests: none detected",
974
+ "Only paths and file structure were inspected; row values stay hidden.",
975
+ ];
976
+ if (useStyledPrompts(flags)) {
977
+ prompts.note(detectedLines.join("\n"), "Detected safe local inputs");
978
+ } else {
979
+ console.log(
980
+ ["Detected safe local inputs", ...detectedLines, ""].join("\n"),
981
+ );
982
+ }
983
+ flags.recordsPath = await promptForDiscoveredPath({
542
984
  message: "Sanitized NDJSON records",
543
- defaultValue: recordsDefault,
985
+ candidates: discovered.records,
986
+ flags,
544
987
  });
545
- flags.ledgerPath = await promptForPath({
988
+ flags.ledgerPath = await promptForDiscoveredPath({
546
989
  message: "Migration ledger",
547
- defaultValue: ledgerDefault,
990
+ candidates: discovered.ledgers,
991
+ flags,
548
992
  });
549
- if (selected.command === "baseline-prepare") {
550
- const preparationPlan = await planBaselinePreparation({
993
+ try {
994
+ if (selected.command === "baseline-prepare") {
995
+ const preparationPlan = await planBaselinePreparation({
996
+ ...planOptions,
997
+ recordsPath: flags.recordsPath,
998
+ ledgerPath: flags.ledgerPath,
999
+ });
1000
+ const preview = {
1001
+ ...summarizeBaselinePreparation(preparationPlan, {
1002
+ mode: "preview",
1003
+ }),
1004
+ nextAction:
1005
+ "Review this schema-only summary. No file is written unless you approve below.",
1006
+ };
1007
+ console.log(`\n${renderBaselinePreparation(preview, flags)}\n`);
1008
+ const accepted = await promptForConfirmation(
1009
+ `Create the REVIEW REQUIRED draft at ${preview.destination}?`,
1010
+ flags,
1011
+ );
1012
+ if (!accepted) {
1013
+ console.log("No changes made.");
1014
+ return "refresh";
1015
+ }
1016
+ flags.preparationPlan = preparationPlan;
1017
+ flags.write = true;
1018
+ return "baseline prepare";
1019
+ }
1020
+ flags.assetsPath = await promptForDiscoveredPath({
1021
+ message: "Storage assets",
1022
+ candidates: discovered.assetManifests,
1023
+ optional: true,
1024
+ flags,
1025
+ });
1026
+ const inspection = await inspectBaselineInputFiles({
551
1027
  ...planOptions,
552
1028
  recordsPath: flags.recordsPath,
553
1029
  ledgerPath: flags.ledgerPath,
1030
+ assetsPath: flags.assetsPath,
554
1031
  });
555
- const preview = {
556
- ...summarizeBaselinePreparation(preparationPlan, {
557
- mode: "preview",
558
- }),
559
- nextAction:
560
- "Review this schema-only summary. No file is written unless you approve below.",
561
- };
562
- console.log(`\n${renderBaselinePreparation(preview, flags)}\n`);
1032
+ console.log(`\n${renderBaselineInputInspection(inspection, flags)}\n`);
563
1033
  const accepted = await promptForConfirmation(
564
- `Create the REVIEW REQUIRED draft at ${preview.destination}?`,
1034
+ "Create and activate this immutable local baseline?",
1035
+ flags,
565
1036
  );
566
1037
  if (!accepted) {
567
1038
  console.log("No changes made.");
568
- return null;
1039
+ return "refresh";
1040
+ }
1041
+ return "baseline create";
1042
+ } catch (error) {
1043
+ const message = redactDiagnosticValue(String(error?.message ?? error));
1044
+ if (useStyledPrompts(flags)) {
1045
+ prompts.cancel("These baseline inputs need attention.");
1046
+ prompts.note(message, "How to continue");
1047
+ } else {
1048
+ console.log(`\nBASELINE INPUTS NEED ATTENTION\n${message}\n`);
569
1049
  }
570
- flags.preparationPlan = preparationPlan;
571
- flags.write = true;
572
- return "baseline prepare";
1050
+ return "refresh";
573
1051
  }
574
- flags.assetsPath = await promptForPath({
575
- message: "Storage asset manifest (optional)",
576
- optional: true,
577
- });
578
- return "baseline create";
579
1052
  }
580
1053
  if (selected.command !== "setup-write") return selected.command;
581
1054
 
@@ -588,10 +1061,11 @@ const runGuidedHome = async ({ flags, planOptions }) => {
588
1061
  console.log(`\n${renderSetup(preview)}\n`);
589
1062
  const accepted = await promptForConfirmation(
590
1063
  "Create these project-local files?",
1064
+ flags,
591
1065
  );
592
1066
  if (!accepted) {
593
1067
  console.log("No changes made.");
594
- return null;
1068
+ return "refresh";
595
1069
  }
596
1070
  flags.setupPlan = setupPlan;
597
1071
  flags.write = true;
@@ -652,6 +1126,7 @@ const prepareCandidateConfirmation = async ({
652
1126
  );
653
1127
  const accepted = await promptForConfirmation(
654
1128
  `Apply exactly ${summary.candidateCount === 1 ? "this migration" : "these migrations"}?`,
1129
+ flags,
655
1130
  );
656
1131
  if (!accepted) {
657
1132
  console.log("No changes made.");
@@ -709,7 +1184,10 @@ const runManager = ({ action, flags }) => {
709
1184
  discard: "Removing the disposable local runtime",
710
1185
  verify: "Verifying the current local runtime",
711
1186
  }[action];
712
- if (isHumanTerminal(flags)) {
1187
+ const spinner = useStyledPrompts(flags) ? prompts.spinner() : null;
1188
+ if (spinner) {
1189
+ spinner.start(`${actionDescription}. This can take a moment.`);
1190
+ } else if (isHumanTerminal(flags)) {
713
1191
  console.log(
714
1192
  `\n${terminalStyle(flags, "1;36", action === "run" ? "REHEARSING" : "REHEARSAL")}\n${terminalStyle(flags, "36", "→")} ${actionDescription}. This can take a moment.`,
715
1193
  );
@@ -721,8 +1199,12 @@ const runManager = ({ action, flags }) => {
721
1199
  stdio: ["inherit", "pipe", "pipe"],
722
1200
  maxBuffer: 16 * 1024 * 1024,
723
1201
  });
724
- if (result.error) throw result.error;
1202
+ if (result.error) {
1203
+ if (spinner) spinner.stop(`${actionDescription} did not complete.`);
1204
+ throw result.error;
1205
+ }
725
1206
  if (result.status !== 0) {
1207
+ if (spinner) spinner.stop(`${actionDescription} did not complete.`);
726
1208
  const completeOutput = [
727
1209
  String(result.stdout ?? ""),
728
1210
  String(result.stderr ?? ""),
@@ -757,7 +1239,11 @@ const runManager = ({ action, flags }) => {
757
1239
  });
758
1240
  }
759
1241
  const durationMs = performance.now() - runtimeStartedMs;
760
- if (isHumanTerminal(flags)) {
1242
+ if (spinner) {
1243
+ spinner.stop(
1244
+ `${actionDescription} completed in ${formatDuration(durationMs)}.`,
1245
+ );
1246
+ } else if (isHumanTerminal(flags)) {
761
1247
  console.log(
762
1248
  `${terminalStyle(flags, "32", "✓")} Local runtime step completed in ${formatDuration(durationMs)}.`,
763
1249
  );
@@ -913,7 +1399,9 @@ const runSetup = async ({ flags, planOptions }) => {
913
1399
  readiness.state === "READY"
914
1400
  ? "Next: run rehearsal explain to review the migration plan."
915
1401
  : needsBaselinePolicy
916
- ? "Next: run rehearsal again and choose Prepare a reviewable baseline policy draft."
1402
+ ? flags.guided
1403
+ ? "Next: choose Prepare the script to create a reviewable policy draft."
1404
+ : "Next: run rehearsal again and choose Prepare a reviewable baseline policy draft."
917
1405
  : "Next: complete the remaining readiness items shown above.",
918
1406
  };
919
1407
  };
@@ -998,9 +1486,16 @@ const runBaselinePreparation = async ({ flags, planOptions }) => {
998
1486
  ledgerPath: flags.ledgerPath,
999
1487
  }));
1000
1488
  if (flags.write) await applyBaselinePreparation(plan);
1001
- return summarizeBaselinePreparation(plan, {
1489
+ const result = summarizeBaselinePreparation(plan, {
1002
1490
  mode: flags.write ? "written" : "preview",
1003
1491
  });
1492
+ return flags.guided && flags.write
1493
+ ? {
1494
+ ...result,
1495
+ nextAction:
1496
+ "Next: choose Review the script to classify every column interactively.",
1497
+ }
1498
+ : result;
1004
1499
  };
1005
1500
 
1006
1501
  const renderBaselinePreparation = (result, flags = {}) =>
@@ -1022,6 +1517,56 @@ const renderBaselinePreparation = (result, flags = {}) =>
1022
1517
  result.nextAction,
1023
1518
  ].join("\n");
1024
1519
 
1520
+ const renderBaselineInputInspection = (result, flags = {}) =>
1521
+ [
1522
+ "BASELINE INPUTS — READY",
1523
+ `Records: ${formatCount(result.rowCount, "row")} across ${formatCount(result.tables.length, "table")} from ${result.recordsPath}`,
1524
+ `Migrations: ${formatCount(result.migrationCount, "migration")} through ${result.migrationCutoff} from ${result.ledgerPath}`,
1525
+ result.assetsPath
1526
+ ? `Storage: ${formatCount(result.assetCount, "asset")} from ${result.assetsPath}`
1527
+ : "Storage: no asset manifest selected",
1528
+ "",
1529
+ "Detected tables (values are never printed)",
1530
+ ...result.tables.map(
1531
+ (table) =>
1532
+ ` ${terminalStyle(flags, "36", "→")} ${table.name}: ${formatCount(table.rowCount, "row")}`,
1533
+ ),
1534
+ "",
1535
+ "All inputs are project-local regular files and passed structural validation.",
1536
+ ].join("\n");
1537
+
1538
+ const renderSupportReport = (report) => {
1539
+ const toolLine = (label, tool) =>
1540
+ `${label}: ${tool.status === "available" ? tool.version : tool.status.replaceAll("_", " ")}`;
1541
+ return [
1542
+ "REHEARSAL SUPPORT REPORT",
1543
+ "",
1544
+ `Rehearsal: ${report.rehearsalVersion}`,
1545
+ `System: ${report.system.platform} ${report.system.architecture} (${report.system.release})`,
1546
+ `Node.js: ${report.system.node}`,
1547
+ toolLine("npm", report.tools.npm),
1548
+ toolLine("Supabase CLI", report.tools.supabase),
1549
+ toolLine("Docker client", report.tools.dockerClient),
1550
+ toolLine("Docker server", report.tools.dockerServer),
1551
+ "",
1552
+ `Readiness: ${report.readiness.state}`,
1553
+ ...report.readiness.checks.map((check) =>
1554
+ check.status === "pass"
1555
+ ? ` ✓ ${check.label}`
1556
+ : ` ✗ ${check.label}${check.remediation ? ` — ${check.remediation}` : ""}`,
1557
+ ),
1558
+ ...(report.readiness.quarantinedHostedVariableCount
1559
+ ? [
1560
+ ` ! ${formatCount(report.readiness.quarantinedHostedVariableCount, "ambient hosted variable")} detected and quarantined`,
1561
+ ]
1562
+ : []),
1563
+ "",
1564
+ "Privacy: no row values, credentials, project paths, migration SQL, or baseline identifiers are included.",
1565
+ "Review this report before sharing it.",
1566
+ `Report a reproducible issue: ${report.supportUrl}`,
1567
+ ].join("\n");
1568
+ };
1569
+
1025
1570
  const usage = () => `Usage: rehearsal <command> [options]
1026
1571
 
1027
1572
  Commands:
@@ -1031,6 +1576,7 @@ Commands:
1031
1576
  baseline prepare --records= --ledger= [--write] Create a fail-closed policy draft
1032
1577
  baseline create --records= --ledger= [--assets=] Create a baseline from safe local inputs
1033
1578
  doctor Check whether Rehearsal is safe and ready
1579
+ support Print a safe, copy-ready support report
1034
1580
  explain Show the immutable execution plan
1035
1581
  run --dry-run Alias the exact explain plan without mutations
1036
1582
  run --confirm-candidates= Execute reset, migration, and verification locally
@@ -1047,29 +1593,7 @@ Commands:
1047
1593
 
1048
1594
  Options: --json --verbose --debug --plain --config=<path>`;
1049
1595
 
1050
- const main = async () => {
1051
- const { flags, positionals } = parseArguments(process.argv.slice(2));
1052
- let command = positionals.join(" ") || "help";
1053
- const planOptions = {
1054
- projectRoot,
1055
- configPath: flags.configPath,
1056
- };
1057
- const wantsAutomaticGuide =
1058
- positionals.length === 0 &&
1059
- !flags.help &&
1060
- !flags.json &&
1061
- process.stdin.isTTY &&
1062
- process.stdout.isTTY;
1063
- if (command === "guide" || wantsAutomaticGuide) {
1064
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
1065
- throw new Error(
1066
- "The guided home screen requires an interactive terminal. Run rehearsal --help to list scriptable commands.",
1067
- );
1068
- }
1069
- const selected = await runGuidedHome({ flags, planOptions });
1070
- if (!selected) return;
1071
- command = selected;
1072
- }
1596
+ const executeCommand = async ({ command, flags, planOptions, guided }) => {
1073
1597
  if (command === "help" || flags.help) {
1074
1598
  console.log(usage());
1075
1599
  return;
@@ -1095,6 +1619,12 @@ const main = async () => {
1095
1619
  return;
1096
1620
  }
1097
1621
  if (command === "baseline create") {
1622
+ await inspectBaselineInputFiles({
1623
+ ...planOptions,
1624
+ recordsPath: flags.recordsPath,
1625
+ ledgerPath: flags.ledgerPath,
1626
+ assetsPath: flags.assetsPath,
1627
+ });
1098
1628
  const data = await createSyntheticBaselineFromFiles({
1099
1629
  ...planOptions,
1100
1630
  recordsPath: flags.recordsPath,
@@ -1109,7 +1639,9 @@ const main = async () => {
1109
1639
  [
1110
1640
  `Activated synthetic baseline ${baseline.generationId}: ${formatCount(baseline.rowCount, "row")} across ${formatCount(baseline.tableCount, "table")}; ${formatCount(baseline.migrationCount, "migration")} through ${baseline.migrationCutoff}.`,
1111
1641
  "",
1112
- "Next: run rehearsal doctor to check readiness.",
1642
+ flags.guided
1643
+ ? "Next: choose Run a rehearsal when you are ready."
1644
+ : "Next: run rehearsal doctor to check readiness.",
1113
1645
  ].join("\n"),
1114
1646
  });
1115
1647
  return;
@@ -1123,7 +1655,12 @@ const main = async () => {
1123
1655
  render: renderDoctor,
1124
1656
  status: data.state === "READY" ? "success" : "not_ready",
1125
1657
  });
1126
- if (data.state !== "READY") process.exitCode = 1;
1658
+ if (data.state !== "READY" && !guided) process.exitCode = 1;
1659
+ return;
1660
+ }
1661
+ if (command === "support") {
1662
+ const data = await collectRehearsalSupportReport(planOptions);
1663
+ emit({ command, data, flags, render: renderSupportReport });
1127
1664
  return;
1128
1665
  }
1129
1666
  if (command === "explain" || (command === "run" && flags.dryRun)) {
@@ -1171,19 +1708,34 @@ const main = async () => {
1171
1708
  const runtime = runManager({ action: command, flags });
1172
1709
  let applicationProof;
1173
1710
  if (command === "run") {
1174
- if (isHumanTerminal(flags)) {
1711
+ const proofSpinner = useStyledPrompts(flags) ? prompts.spinner() : null;
1712
+ if (proofSpinner) {
1713
+ proofSpinner.start("Running the project-owned application proof.");
1714
+ } else if (isHumanTerminal(flags)) {
1175
1715
  console.log(
1176
1716
  `${terminalStyle(flags, "36", "→")} Running the project-owned application proof.`,
1177
1717
  );
1178
1718
  }
1179
- applicationProof = await runApplicationProof(planOptions);
1180
- if (isHumanTerminal(flags)) {
1719
+ try {
1720
+ applicationProof = await runApplicationProof(planOptions);
1721
+ } catch (error) {
1722
+ if (proofSpinner) proofSpinner.stop("Application proof failed.");
1723
+ throw error;
1724
+ }
1725
+ if (proofSpinner) {
1726
+ proofSpinner.stop("Application proof passed.");
1727
+ } else if (isHumanTerminal(flags)) {
1181
1728
  console.log(
1182
1729
  `${terminalStyle(flags, "32", "✓")} Application proof passed.`,
1183
1730
  );
1184
1731
  }
1185
1732
  }
1186
1733
  const data = command === "run" ? { runtime, applicationProof } : runtime;
1734
+ if (guided) {
1735
+ lastGuidedDetails = [runtime.output, applicationProof?.output]
1736
+ .filter(Boolean)
1737
+ .join("\n");
1738
+ }
1187
1739
  emit({
1188
1740
  command,
1189
1741
  data,
@@ -1203,6 +1755,23 @@ const main = async () => {
1203
1755
  verify:
1204
1756
  "Next: continue testing, reset to the baseline, or stop the runtime.",
1205
1757
  }[runtimeResult.action];
1758
+ if (guided) {
1759
+ const title =
1760
+ runtimeResult.action === "run"
1761
+ ? "REHEARSAL PASSED"
1762
+ : "ACTION COMPLETE";
1763
+ return [
1764
+ title,
1765
+ `✓ ${runtimeResult.action} completed in ${formatDuration(runtimeResult.durationMs)}`,
1766
+ value.applicationProof
1767
+ ? `✓ Application proof passed: ${value.applicationProof.command}`
1768
+ : null,
1769
+ "",
1770
+ "Technical output is available from Show details in the guide.",
1771
+ ]
1772
+ .filter(Boolean)
1773
+ .join("\n");
1774
+ }
1206
1775
  return [
1207
1776
  runtimeResult.output ||
1208
1777
  `Rehearsal ${runtimeResult.action} completed.`,
@@ -1221,6 +1790,63 @@ const main = async () => {
1221
1790
  throw new Error(`Unknown Rehearsal command: ${command}.\n\n${usage()}`);
1222
1791
  };
1223
1792
 
1793
+ const resetGuidedFlags = (flags) => {
1794
+ flags.dryRun = false;
1795
+ flags.write = false;
1796
+ flags.confirmation = undefined;
1797
+ flags.recordsPath = undefined;
1798
+ flags.ledgerPath = undefined;
1799
+ flags.assetsPath = undefined;
1800
+ flags.setupPlan = undefined;
1801
+ flags.preparationPlan = undefined;
1802
+ };
1803
+
1804
+ const main = async () => {
1805
+ const { flags, positionals } = parseArguments(process.argv.slice(2));
1806
+ const command = positionals.join(" ") || "help";
1807
+ const planOptions = {
1808
+ projectRoot,
1809
+ configPath: flags.configPath,
1810
+ };
1811
+ const wantsAutomaticGuide =
1812
+ positionals.length === 0 &&
1813
+ !flags.help &&
1814
+ !flags.json &&
1815
+ process.stdin.isTTY &&
1816
+ process.stdout.isTTY;
1817
+ const guided = command === "guide" || wantsAutomaticGuide;
1818
+ if (!guided) {
1819
+ await executeCommand({ command, flags, planOptions, guided: false });
1820
+ return;
1821
+ }
1822
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1823
+ throw new Error(
1824
+ "The guided home screen requires an interactive terminal. Run rehearsal --help to list scriptable commands.",
1825
+ );
1826
+ }
1827
+ flags.guided = true;
1828
+ if (useStyledPrompts(flags)) {
1829
+ prompts.intro("REHEARSAL · Safe local migration testing");
1830
+ }
1831
+ while (true) {
1832
+ resetGuidedFlags(flags);
1833
+ const selected = await runGuidedHome({ flags, planOptions });
1834
+ if (!selected) break;
1835
+ if (selected === "refresh") continue;
1836
+ await executeCommand({
1837
+ command: selected,
1838
+ flags,
1839
+ planOptions,
1840
+ guided: true,
1841
+ });
1842
+ }
1843
+ if (useStyledPrompts(flags)) {
1844
+ prompts.outro("See you at the next rehearsal.");
1845
+ } else {
1846
+ console.log("See you at the next rehearsal.");
1847
+ }
1848
+ };
1849
+
1224
1850
  try {
1225
1851
  await main();
1226
1852
  } catch (error) {