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

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.
@@ -80,6 +80,7 @@ const parseArguments = (arguments_) => {
80
80
  recordsPath: undefined,
81
81
  ledgerPath: undefined,
82
82
  assetsPath: undefined,
83
+ target: undefined,
83
84
  };
84
85
  const positionals = [];
85
86
  for (const argument of arguments_) {
@@ -100,6 +101,8 @@ const parseArguments = (arguments_) => {
100
101
  flags.ledgerPath = argument.slice("--ledger=".length);
101
102
  } else if (argument.startsWith("--assets=")) {
102
103
  flags.assetsPath = argument.slice("--assets=".length);
104
+ } else if (argument.startsWith("--target=")) {
105
+ flags.target = argument.slice("--target=".length);
103
106
  } else if (argument.startsWith("--")) {
104
107
  throw new Error(`Unknown Rehearsal option: ${argument}.`);
105
108
  } else positionals.push(argument);
@@ -274,7 +277,12 @@ const formatDuration = (durationMs) =>
274
277
  ? `${Math.round(durationMs)}ms`
275
278
  : `${(durationMs / 1_000).toFixed(1)}s`;
276
279
 
277
- const promptForChoice = async ({ message, options, flags = {} }) => {
280
+ const promptForChoice = async ({
281
+ message,
282
+ options,
283
+ flags = {},
284
+ optionsAlreadyShown = false,
285
+ }) => {
278
286
  if (useStyledPrompts(flags)) {
279
287
  const selected = await prompts.select({
280
288
  message,
@@ -289,6 +297,18 @@ const promptForChoice = async ({ message, options, flags = {} }) => {
289
297
  ? options.find((option) => option.command === "exit")
290
298
  : selected;
291
299
  }
300
+ if (!optionsAlreadyShown) {
301
+ console.log(
302
+ [
303
+ "",
304
+ ...options.map(
305
+ (option, index) =>
306
+ ` ${index === 0 ? "›" : " "} ${index + 1}. ${option.label}${option.hint ? ` — ${option.hint}` : ""}`,
307
+ ),
308
+ "",
309
+ ].join("\n"),
310
+ );
311
+ }
292
312
  const prompt = createInterface({
293
313
  input: process.stdin,
294
314
  output: process.stdout,
@@ -502,6 +522,7 @@ const inspectGuidedProject = async (planOptions) => {
502
522
  detected,
503
523
  node,
504
524
  config: "missing",
525
+ target: detected.hasSupabaseConfig ? "supabase" : "postgresql",
505
526
  baseline: false,
506
527
  runtime: false,
507
528
  };
@@ -514,6 +535,7 @@ const inspectGuidedProject = async (planOptions) => {
514
535
  detected,
515
536
  node,
516
537
  config: "invalid",
538
+ target: undefined,
517
539
  configPath,
518
540
  configError: redactDiagnosticValue(String(error?.message ?? error)),
519
541
  baseline: false,
@@ -544,6 +566,7 @@ const inspectGuidedProject = async (planOptions) => {
544
566
  detected,
545
567
  node,
546
568
  config: "valid",
569
+ target: loaded.config.runtime.target,
547
570
  configPath,
548
571
  baseline: Boolean(migrationSummary),
549
572
  policy,
@@ -829,8 +852,16 @@ const runGuidedHome = async ({ flags, planOptions }) => {
829
852
 
830
853
  const statusLines = [
831
854
  `${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`,
855
+ `${marker(
856
+ state.target === "supabase"
857
+ ? state.detected.hasSupabaseConfig
858
+ : state.detected.hasPostgresqlMigrations,
859
+ )} ${state.target === "supabase" ? "Supabase" : "PostgreSQL"} project detected`,
860
+ `${marker(
861
+ state.target === "supabase"
862
+ ? state.detected.hasMigrations
863
+ : state.detected.hasPostgresqlMigrations,
864
+ )} Migration history detected`,
834
865
  `${marker(state.config === "valid")} Rehearsal configuration${state.config === "invalid" ? " needs attention" : ""}`,
835
866
  ...(state.config === "valid"
836
867
  ? [
@@ -882,6 +913,7 @@ const runGuidedHome = async ({ flags, planOptions }) => {
882
913
  message: "What would you like to do?",
883
914
  options,
884
915
  flags,
916
+ optionsAlreadyShown: !useStyledPrompts(flags),
885
917
  });
886
918
  if (selected.command === "exit") {
887
919
  return null;
@@ -956,7 +988,7 @@ const runGuidedHome = async ({ flags, planOptions }) => {
956
988
  "",
957
989
  terminalStyle(flags, "1", "CREATE A SAFE BASELINE"),
958
990
  "Rehearsal only reads the local files you name here.",
959
- "It does not extract data or contact a hosted Supabase project.",
991
+ "It does not extract data or contact a hosted database.",
960
992
  "",
961
993
  ].join("\n"),
962
994
  );
@@ -968,9 +1000,13 @@ const runGuidedHome = async ({ flags, planOptions }) => {
968
1000
  discovered.ledgers.length
969
1001
  ? `Migration ledgers: ${discovered.ledgers.join(", ")}`
970
1002
  : "Migration ledgers: none detected",
971
- discovered.assetManifests.length
972
- ? `Storage manifests: ${discovered.assetManifests.join(", ")}`
973
- : "Storage manifests: none detected",
1003
+ ...(state.target === "supabase"
1004
+ ? [
1005
+ discovered.assetManifests.length
1006
+ ? `Storage manifests: ${discovered.assetManifests.join(", ")}`
1007
+ : "Storage manifests: none detected",
1008
+ ]
1009
+ : []),
974
1010
  "Only paths and file structure were inspected; row values stay hidden.",
975
1011
  ];
976
1012
  if (useStyledPrompts(flags)) {
@@ -1017,12 +1053,15 @@ const runGuidedHome = async ({ flags, planOptions }) => {
1017
1053
  flags.write = true;
1018
1054
  return "baseline prepare";
1019
1055
  }
1020
- flags.assetsPath = await promptForDiscoveredPath({
1021
- message: "Storage assets",
1022
- candidates: discovered.assetManifests,
1023
- optional: true,
1024
- flags,
1025
- });
1056
+ flags.assetsPath =
1057
+ state.target === "supabase"
1058
+ ? await promptForDiscoveredPath({
1059
+ message: "Storage assets",
1060
+ candidates: discovered.assetManifests,
1061
+ optional: true,
1062
+ flags,
1063
+ })
1064
+ : undefined;
1026
1065
  const inspection = await inspectBaselineInputFiles({
1027
1066
  ...planOptions,
1028
1067
  recordsPath: flags.recordsPath,
@@ -1052,7 +1091,26 @@ const runGuidedHome = async ({ flags, planOptions }) => {
1052
1091
  }
1053
1092
  if (selected.command !== "setup-write") return selected.command;
1054
1093
 
1055
- const setupPlan = await planRehearsalSetup({ projectRoot });
1094
+ const targetChoice = await promptForChoice({
1095
+ message: "Which database should Rehearsal use?",
1096
+ flags,
1097
+ options:
1098
+ state.target === "supabase"
1099
+ ? [
1100
+ { label: "Supabase", command: "supabase" },
1101
+ { label: "PostgreSQL", command: "postgresql" },
1102
+ ]
1103
+ : [
1104
+ { label: "PostgreSQL", command: "postgresql" },
1105
+ { label: "Supabase", command: "supabase" },
1106
+ ],
1107
+ });
1108
+ if (!targetChoice) return "refresh";
1109
+ flags.target = targetChoice.command;
1110
+ const setupPlan = await planRehearsalSetup({
1111
+ projectRoot,
1112
+ target: flags.target,
1113
+ });
1056
1114
  const preview = {
1057
1115
  ...summarizeRehearsalSetup(setupPlan, { mode: "preview" }),
1058
1116
  nextAction:
@@ -1215,6 +1273,14 @@ const runManager = ({ action, flags }) => {
1215
1273
  const inferred = normalizeRehearsalError(
1216
1274
  new Error(completeOutput || `Rehearsal ${action} failed.`),
1217
1275
  );
1276
+ const category =
1277
+ action === "migrate" ||
1278
+ (action === "run" &&
1279
+ /migrateRuntime|candidate migration|migration up/iu.test(
1280
+ completeOutput,
1281
+ ))
1282
+ ? "migration_candidate_failure"
1283
+ : inferred.category;
1218
1284
  const conciseOutput = completeOutput
1219
1285
  .replaceAll(String.fromCodePoint(27), "")
1220
1286
  .split("\n")
@@ -1224,7 +1290,7 @@ const runManager = ({ action, flags }) => {
1224
1290
  .join("\n")
1225
1291
  .slice(-2_000);
1226
1292
  throw new RehearsalError({
1227
- category: inferred.category,
1293
+ category,
1228
1294
  code: `MANAGER_${action.toUpperCase()}_FAILED`,
1229
1295
  message: `The local Rehearsal ${action} operation did not complete.`,
1230
1296
  expected: "the isolated runtime operation to finish and verify",
@@ -1382,7 +1448,12 @@ const renderInit = (result) =>
1382
1448
  ].join("\n");
1383
1449
 
1384
1450
  const runSetup = async ({ flags, planOptions }) => {
1385
- const plan = flags.setupPlan ?? (await planRehearsalSetup({ projectRoot }));
1451
+ const plan =
1452
+ flags.setupPlan ??
1453
+ (await planRehearsalSetup({
1454
+ projectRoot,
1455
+ target: flags.target ?? "supabase",
1456
+ }));
1386
1457
  if (flags.write) await applyRehearsalSetup(plan);
1387
1458
  const result = summarizeRehearsalSetup(plan, {
1388
1459
  mode: flags.write ? "written" : "preview",
@@ -1454,9 +1525,12 @@ const renderSetup = (result) => {
1454
1525
  return [
1455
1526
  `REHEARSAL SETUP — ${result.mode.toUpperCase()}`,
1456
1527
  `Project: ${result.project}`,
1528
+ `Database: ${result.target === "postgresql" ? "PostgreSQL" : "Supabase"}`,
1457
1529
  `Runtime ID: ${result.projectId}`,
1458
1530
  `Application: ${result.applicationUrl}`,
1459
- `Ports: API ${result.ports.api}, database ${result.ports.database}, Studio ${result.ports.studio}`,
1531
+ result.target === "postgresql"
1532
+ ? `Port: database ${result.ports.database}`
1533
+ : `Ports: API ${result.ports.api}, database ${result.ports.database}, Studio ${result.ports.studio}`,
1460
1534
  "",
1461
1535
  "Files",
1462
1536
  ...result.files.map(
@@ -1571,7 +1645,7 @@ const usage = () => `Usage: rehearsal <command> [options]
1571
1645
 
1572
1646
  Commands:
1573
1647
  guide Open the interactive, state-aware home screen
1574
- setup [--write] Preview or create safe first-run scaffolding
1648
+ setup [--target=] [--write] Preview or create safe first-run scaffolding
1575
1649
  init [--write] Preview or explicitly write safe starter config
1576
1650
  baseline prepare --records= --ledger= [--write] Create a fail-closed policy draft
1577
1651
  baseline create --records= --ledger= [--assets=] Create a baseline from safe local inputs
@@ -1591,7 +1665,7 @@ Commands:
1591
1665
  discard Remove only this project's disposable runtime
1592
1666
  verify Verify the current local Rehearsal runtime
1593
1667
 
1594
- Options: --json --verbose --debug --plain --config=<path>`;
1668
+ Options: --json --verbose --debug --plain --config=<path> --target=supabase|postgresql`;
1595
1669
 
1596
1670
  const executeCommand = async ({ command, flags, planOptions, guided }) => {
1597
1671
  if (command === "help" || flags.help) {
@@ -1797,10 +1871,39 @@ const resetGuidedFlags = (flags) => {
1797
1871
  flags.recordsPath = undefined;
1798
1872
  flags.ledgerPath = undefined;
1799
1873
  flags.assetsPath = undefined;
1874
+ flags.target = undefined;
1800
1875
  flags.setupPlan = undefined;
1801
1876
  flags.preparationPlan = undefined;
1802
1877
  };
1803
1878
 
1879
+ const installGuidedExitShortcut = (flags) => {
1880
+ let exiting = false;
1881
+ const exit = () => {
1882
+ if (exiting) return;
1883
+ exiting = true;
1884
+ if (process.stdin.isTTY && process.stdin.isRaw) {
1885
+ process.stdin.setRawMode(false);
1886
+ }
1887
+ process.stdout.write("\u001B[?25h");
1888
+ if (useStyledPrompts(flags)) {
1889
+ prompts.outro("Rehearsal exited.");
1890
+ } else {
1891
+ console.log("\nRehearsal exited.");
1892
+ }
1893
+ process.exit(0);
1894
+ };
1895
+ const onInput = (chunk) => {
1896
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1897
+ if (bytes.includes(0x1a)) exit();
1898
+ };
1899
+ process.stdin.prependListener("data", onInput);
1900
+ process.prependListener("SIGTSTP", exit);
1901
+ return () => {
1902
+ process.stdin.removeListener("data", onInput);
1903
+ process.removeListener("SIGTSTP", exit);
1904
+ };
1905
+ };
1906
+
1804
1907
  const main = async () => {
1805
1908
  const { flags, positionals } = parseArguments(process.argv.slice(2));
1806
1909
  const command = positionals.join(" ") || "help";
@@ -1825,20 +1928,25 @@ const main = async () => {
1825
1928
  );
1826
1929
  }
1827
1930
  flags.guided = true;
1931
+ const removeGuidedExitShortcut = installGuidedExitShortcut(flags);
1828
1932
  if (useStyledPrompts(flags)) {
1829
1933
  prompts.intro("REHEARSAL · Safe local migration testing");
1830
1934
  }
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
- });
1935
+ try {
1936
+ while (true) {
1937
+ resetGuidedFlags(flags);
1938
+ const selected = await runGuidedHome({ flags, planOptions });
1939
+ if (!selected) break;
1940
+ if (selected === "refresh") continue;
1941
+ await executeCommand({
1942
+ command: selected,
1943
+ flags,
1944
+ planOptions,
1945
+ guided: true,
1946
+ });
1947
+ }
1948
+ } finally {
1949
+ removeGuidedExitShortcut();
1842
1950
  }
1843
1951
  if (useStyledPrompts(flags)) {
1844
1952
  prompts.outro("See you at the next rehearsal.");