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

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.
@@ -27,10 +27,14 @@ import {
27
27
  summarizeRestoreError,
28
28
  } from "../rehearsal/runtime_restore.mjs";
29
29
  import { readBoundRuntimeSanitizationPolicy } from "../rehearsal/sanitization_policy.mjs";
30
+ import { ensureLocalContainerRuntime } from "../environment/local_supabase.mjs";
31
+ import { parseRuntimeInvocation } from "./runtime_target.mjs";
30
32
 
31
33
  const repositoryRoot = process.cwd();
34
+ const invocation = parseRuntimeInvocation();
32
35
  const loadedConfiguration = await loadRehearsalConfig({
33
36
  projectRoot: repositoryRoot,
37
+ configPath: invocation.configPath,
34
38
  });
35
39
  const { config, paths: configuredPaths } = loadedConfiguration;
36
40
  const artifactRoot = configuredPaths.artifactDirectory;
@@ -50,7 +54,7 @@ const image = config.postgresql.image;
50
54
  const database = config.postgresql.database;
51
55
  const databaseUser = config.postgresql.user;
52
56
  const databasePort = config.runtime.ports.database;
53
- const action = process.argv[2] ?? "status";
57
+ const action = invocation.action;
54
58
  const applicationMigrations = new URL(
55
59
  "./",
56
60
  pathToFileURL(`${configuredPaths.migrationDirectory}/`),
@@ -105,11 +109,10 @@ const assertRuntimePath = () => {
105
109
  };
106
110
 
107
111
  const assertDocker = () => {
108
- if (!commandSucceeds("docker", ["info"])) {
109
- throw new Error(
110
- "A running Docker-compatible runtime is required. Start Docker or Colima, then retry.",
111
- );
112
- }
112
+ ensureLocalContainerRuntime({
113
+ cwd: repositoryRoot,
114
+ autoStartColima: config.containerRuntime.autoStartColima,
115
+ });
113
116
  };
114
117
 
115
118
  const inspectResourceLabel = (kind, name) =>
@@ -650,9 +653,7 @@ const migrateRuntime = async () => {
650
653
  const { baseline } = await readActive();
651
654
  await startRuntime();
652
655
  const receipt = await readCandidateReceipt(baseline);
653
- const confirmation = process.argv
654
- .find((argument) => argument.startsWith("--confirm-candidates="))
655
- ?.slice("--confirm-candidates=".length);
656
+ const confirmation = invocation.confirmation;
656
657
  if (confirmation !== receipt.candidateSha256) {
657
658
  throw new Error(
658
659
  "Candidate migration confirmation is missing or does not match the exact SHA-256.",
@@ -17,6 +17,31 @@ const TARGETS = Object.freeze({
17
17
 
18
18
  export const REHEARSAL_RUNTIME_TARGETS = Object.freeze(Object.keys(TARGETS));
19
19
 
20
+ export const parseRuntimeInvocation = (arguments_ = process.argv.slice(2)) => {
21
+ const action = arguments_.find((argument) => !argument.startsWith("--"));
22
+ const valueAfter = (prefix) =>
23
+ arguments_
24
+ .find((argument) => argument.startsWith(prefix))
25
+ ?.slice(prefix.length);
26
+ return Object.freeze({
27
+ action: action ?? "status",
28
+ configPath: valueAfter("--config="),
29
+ confirmation: valueAfter("--confirm-candidates="),
30
+ });
31
+ };
32
+
33
+ export const buildRuntimeManagerArguments = ({
34
+ managerPath,
35
+ action,
36
+ configPath,
37
+ confirmation,
38
+ }) => [
39
+ managerPath,
40
+ action,
41
+ ...(configPath ? [`--config=${configPath}`] : []),
42
+ ...(confirmation ? [`--confirm-candidates=${confirmation}`] : []),
43
+ ];
44
+
20
45
  export const resolveRuntimeTarget = (value = "supabase") => {
21
46
  if (typeof value !== "string" || !Object.hasOwn(TARGETS, value)) {
22
47
  throw new Error(
@@ -50,10 +50,13 @@ import {
50
50
  compareProductionSchemaDumps,
51
51
  } from "../rehearsal/schema_snapshot.mjs";
52
52
  import { formatCount } from "../rehearsal/human_output.mjs";
53
+ import { parseRuntimeInvocation } from "./runtime_target.mjs";
53
54
 
54
55
  const repositoryRoot = process.cwd();
56
+ const invocation = parseRuntimeInvocation();
55
57
  const loadedConfiguration = await loadRehearsalConfig({
56
58
  projectRoot: repositoryRoot,
59
+ configPath: invocation.configPath,
57
60
  });
58
61
  const { config, paths: configuredPaths } = loadedConfiguration;
59
62
  const artifactRoot = configuredPaths.artifactDirectory;
@@ -76,7 +79,7 @@ const runtimeAdapter = configuredPaths.runtimeAdapter
76
79
  ? await import(pathToFileURL(configuredPaths.runtimeAdapter).href)
77
80
  : null;
78
81
  const environmentKeyPattern = /^[A-Z][A-Z0-9_]*$/u;
79
- const action = process.argv[2] ?? "status";
82
+ const action = invocation.action;
80
83
 
81
84
  const readServiceEnvironment = () =>
82
85
  readRehearsalServiceEnvironment({
@@ -468,6 +471,7 @@ const resetRuntime = async () => {
468
471
  workdir: runtimeWorkdir,
469
472
  exclude: ["edge-runtime", "logflare", "vector", "realtime"],
470
473
  environment: serviceEnvironment,
474
+ autoStartColima: config.containerRuntime.autoStartColima,
471
475
  });
472
476
  const structuralSchemaSha256 = await restoreProductionSchema(active);
473
477
  await restoreBaselineRows(active);
@@ -532,6 +536,7 @@ const startRuntime = async () => {
532
536
  workdir: runtimeWorkdir,
533
537
  exclude: ["edge-runtime", "logflare", "vector", "realtime"],
534
538
  environment: serviceEnvironment,
539
+ autoStartColima: config.containerRuntime.autoStartColima,
535
540
  });
536
541
  const projectRuntime = configureProjectRuntime({ environment, baseline });
537
542
  await writeRuntimeEnvironment({
@@ -660,9 +665,7 @@ const migrateRuntime = async () => {
660
665
  console.log(
661
666
  `${receipt.candidates.length === 1 ? "Candidate migration" : "Candidate migrations"} (${receipt.candidateSha256}): ${receipt.candidates.map((candidate) => candidate.filename).join(", ")}`,
662
667
  );
663
- const confirmation = process.argv
664
- .find((argument) => argument.startsWith("--confirm-candidates="))
665
- ?.slice("--confirm-candidates=".length);
668
+ const confirmation = invocation.confirmation;
666
669
  if (confirmation !== receipt.candidateSha256) {
667
670
  throw new Error(
668
671
  "Candidate migration confirmation is missing or does not match the exact SHA-256.",
@@ -5,7 +5,14 @@
5
5
  */
6
6
 
7
7
  import { loadRehearsalConfig } from "../../lib/rehearsal/configuration.mjs";
8
- import { runRuntimeTarget } from "../../lib/runtime/runtime_target.mjs";
8
+ import {
9
+ parseRuntimeInvocation,
10
+ runRuntimeTarget,
11
+ } from "../../lib/runtime/runtime_target.mjs";
9
12
 
10
- const { config } = await loadRehearsalConfig({ projectRoot: process.cwd() });
13
+ const invocation = parseRuntimeInvocation();
14
+ const { config } = await loadRehearsalConfig({
15
+ projectRoot: process.cwd(),
16
+ configPath: invocation.configPath,
17
+ });
11
18
  await runRuntimeTarget(config.runtime.target);
@@ -56,6 +56,11 @@ import {
56
56
  suggestPolicyExceptionColumns,
57
57
  } from "../../lib/rehearsal/policy_review.mjs";
58
58
  import { collectRehearsalSupportReport } from "../../lib/rehearsal/support_report.mjs";
59
+ import {
60
+ applyRehearsalCleanup,
61
+ planRehearsalCleanup,
62
+ } from "../../lib/rehearsal/cleanup.mjs";
63
+ import { buildRuntimeManagerArguments } from "../../lib/runtime/runtime_target.mjs";
59
64
 
60
65
  const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
61
66
  const projectRoot = process.cwd();
@@ -81,6 +86,9 @@ const parseArguments = (arguments_) => {
81
86
  ledgerPath: undefined,
82
87
  assetsPath: undefined,
83
88
  target: undefined,
89
+ includeRuntime: false,
90
+ includeImages: false,
91
+ cleanupConfirmation: undefined,
84
92
  };
85
93
  const positionals = [];
86
94
  for (const argument of arguments_) {
@@ -95,6 +103,8 @@ const parseArguments = (arguments_) => {
95
103
  flags.configPath = argument.slice("--config=".length);
96
104
  } else if (argument.startsWith("--confirm-candidates=")) {
97
105
  flags.confirmation = argument.slice("--confirm-candidates=".length);
106
+ } else if (argument.startsWith("--confirm-cleanup=")) {
107
+ flags.cleanupConfirmation = argument.slice("--confirm-cleanup=".length);
98
108
  } else if (argument.startsWith("--records=")) {
99
109
  flags.recordsPath = argument.slice("--records=".length);
100
110
  } else if (argument.startsWith("--ledger=")) {
@@ -103,6 +113,10 @@ const parseArguments = (arguments_) => {
103
113
  flags.assetsPath = argument.slice("--assets=".length);
104
114
  } else if (argument.startsWith("--target=")) {
105
115
  flags.target = argument.slice("--target=".length);
116
+ } else if (argument === "--include-runtime") {
117
+ flags.includeRuntime = true;
118
+ } else if (argument === "--include-images") {
119
+ flags.includeImages = true;
106
120
  } else if (argument.startsWith("--")) {
107
121
  throw new Error(`Unknown Rehearsal option: ${argument}.`);
108
122
  } else positionals.push(argument);
@@ -131,7 +145,7 @@ const renderPlan = (plan, verbosity) => {
131
145
  "Environment",
132
146
  ` ✓ ${plan.environment.kind}`,
133
147
  ` ✓ Hosted access ${plan.environment.hostedAccess}`,
134
- ` ✓ Application/provider-data egress ${plan.environment.outboundNetwork}`,
148
+ ` ✓ Hosted target configuration ${plan.environment.outboundNetwork}`,
135
149
  ...(plan.environment.authenticationProviders.length
136
150
  ? [
137
151
  ` ✓ External identity providers ${plan.environment.authenticationProviders.join(", ")}`,
@@ -834,6 +848,13 @@ const runGuidedHome = async ({ flags, planOptions }) => {
834
848
  command: "manage-runtime",
835
849
  });
836
850
  }
851
+ if (state.node.supported && state.config === "valid") {
852
+ options.push({
853
+ label: "Clean up disk space",
854
+ hint: "Preview old baselines, the runtime, and unused images",
855
+ command: "cleanup-guide",
856
+ });
857
+ }
837
858
  if (lastGuidedDetails) {
838
859
  options.push({
839
860
  label: "Show details from the last action",
@@ -982,6 +1003,60 @@ const runGuidedHome = async ({ flags, planOptions }) => {
982
1003
  }
983
1004
  return runtimeAction.command;
984
1005
  }
1006
+ if (selected.command === "cleanup-guide") {
1007
+ const cleanupScope = await promptForChoice({
1008
+ message: "What may Rehearsal include in the cleanup preview?",
1009
+ flags,
1010
+ options: [
1011
+ {
1012
+ label: "Old baselines only",
1013
+ hint: "Keep the configured number of recent generations",
1014
+ command: "baselines",
1015
+ },
1016
+ {
1017
+ label: "Old baselines and this project's runtime",
1018
+ hint: "Also remove the disposable database and its volumes",
1019
+ command: "runtime",
1020
+ },
1021
+ {
1022
+ label: "Old baselines and older unused Supabase images",
1023
+ hint: "Images may be shared and can be downloaded again",
1024
+ command: "images",
1025
+ },
1026
+ {
1027
+ label: "Everything above",
1028
+ command: "all",
1029
+ },
1030
+ { label: "Back", command: "exit" },
1031
+ ],
1032
+ });
1033
+ if (!cleanupScope || cleanupScope.command === "exit") return "refresh";
1034
+ flags.includeRuntime = ["runtime", "all"].includes(cleanupScope.command);
1035
+ flags.includeImages = ["images", "all"].includes(cleanupScope.command);
1036
+ flags.cleanupPlan = await planRehearsalCleanup({
1037
+ ...planOptions,
1038
+ includeRuntime: flags.includeRuntime,
1039
+ includeImages: flags.includeImages,
1040
+ });
1041
+ console.log(
1042
+ `\n${renderCleanup({ mode: "preview", plan: flags.cleanupPlan })}`,
1043
+ );
1044
+ const selectedCount =
1045
+ flags.cleanupPlan.baselines.removed.length +
1046
+ (flags.cleanupPlan.runtime.included && flags.cleanupPlan.runtime.detected
1047
+ ? 1
1048
+ : 0) +
1049
+ flags.cleanupPlan.images.candidates.length;
1050
+ if (selectedCount === 0) return "refresh";
1051
+ const accepted = await promptForConfirmation(
1052
+ "Remove exactly the resources shown above?",
1053
+ flags,
1054
+ );
1055
+ if (!accepted) return "refresh";
1056
+ flags.write = true;
1057
+ flags.cleanupConfirmation = flags.cleanupPlan.digest;
1058
+ return "cleanup";
1059
+ }
985
1060
  if (["baseline-guide", "baseline-prepare"].includes(selected.command)) {
986
1061
  console.log(
987
1062
  [
@@ -1209,10 +1284,12 @@ const emit = ({ command, data, flags, render, status = "success" }) => {
1209
1284
  };
1210
1285
 
1211
1286
  const runManager = ({ action, flags }) => {
1212
- const args = [managerPath, action];
1213
- if (flags.confirmation) {
1214
- args.push(`--confirm-candidates=${flags.confirmation}`);
1215
- }
1287
+ const args = buildRuntimeManagerArguments({
1288
+ managerPath,
1289
+ action,
1290
+ configPath: flags.configPath,
1291
+ confirmation: flags.confirmation,
1292
+ });
1216
1293
  const environment = Object.fromEntries(
1217
1294
  [
1218
1295
  "CI",
@@ -1641,6 +1718,112 @@ const renderSupportReport = (report) => {
1641
1718
  ].join("\n");
1642
1719
  };
1643
1720
 
1721
+ const formatBytes = (bytes) => {
1722
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
1723
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
1724
+ const unit = Math.min(
1725
+ Math.floor(Math.log(bytes) / Math.log(1024)),
1726
+ units.length - 1,
1727
+ );
1728
+ const value = bytes / 1024 ** unit;
1729
+ return `${value >= 10 || unit === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`;
1730
+ };
1731
+
1732
+ const renderCleanup = (result) => {
1733
+ const plan = result.plan;
1734
+ const imageBytes = plan.images.candidates.reduce(
1735
+ (total, image) => total + image.bytes,
1736
+ 0,
1737
+ );
1738
+ const dockerImages = plan.docker.resources.find(
1739
+ (resource) => resource.Type === "Images",
1740
+ );
1741
+ const selectedCount =
1742
+ plan.baselines.removed.length +
1743
+ (plan.runtime.included && plan.runtime.detected ? 1 : 0) +
1744
+ plan.images.candidates.length;
1745
+ return [
1746
+ `REHEARSAL CLEANUP — ${result.mode.toUpperCase()}`,
1747
+ `Project: ${plan.project}`,
1748
+ ...(dockerImages
1749
+ ? [
1750
+ `Docker images: ${dockerImages.Size}; ${dockerImages.Reclaimable} reclaimable across Docker`,
1751
+ ]
1752
+ : ["Docker usage: unavailable"]),
1753
+ ...(plan.docker.disk
1754
+ ? [
1755
+ `Container disk: ${formatBytes(plan.docker.disk.usedBytes)} used of ${formatBytes(plan.docker.disk.totalBytes)}; ${formatBytes(plan.docker.disk.availableBytes)} free (${plan.docker.disk.usedPercent}% used)`,
1756
+ ]
1757
+ : []),
1758
+ "",
1759
+ "Project-owned files",
1760
+ plan.baselines.available
1761
+ ? ` ${plan.baselines.removed.length ? "→" : "✓"} ${formatCount(plan.baselines.removed.length, "old baseline generation")} selected; keeping ${plan.baselines.retained.length}`
1762
+ : " ○ No active baseline found",
1763
+ "",
1764
+ "Disposable runtime",
1765
+ !plan.runtime.detected
1766
+ ? " ✓ No runtime found"
1767
+ : plan.runtime.included
1768
+ ? " → Current project runtime selected"
1769
+ : " ○ Current project runtime preserved (add --include-runtime to select it)",
1770
+ "",
1771
+ "Shared Docker images",
1772
+ !plan.images.requested
1773
+ ? " ○ Not inspected (add --include-images to inspect older unused Supabase images)"
1774
+ : !plan.images.inspected
1775
+ ? " ! Docker is unavailable; images could not be inspected"
1776
+ : plan.images.candidates.length
1777
+ ? ` → ${formatCount(plan.images.candidates.length, "older unused Supabase image")} selected; approximately ${formatBytes(imageBytes)}`
1778
+ : " ✓ No older unused Supabase images found",
1779
+ ...(plan.images.candidates.length
1780
+ ? plan.images.candidates.flatMap((image) =>
1781
+ image.tags.map((tag) => ` ${tag}`),
1782
+ )
1783
+ : []),
1784
+ "",
1785
+ ...(result.mode === "preview"
1786
+ ? selectedCount
1787
+ ? [
1788
+ "Nothing has been removed.",
1789
+ `Cleanup set: ${plan.digest.slice(0, 12)}`,
1790
+ `Exact confirmation: --confirm-cleanup=${plan.digest}`,
1791
+ "Review this list, then rerun the same command with --write and the exact confirmation above.",
1792
+ ]
1793
+ : [
1794
+ "Nothing has been removed, and nothing is currently selected.",
1795
+ "Use --include-runtime or --include-images only when you want those shared resources considered.",
1796
+ ]
1797
+ : [
1798
+ `Removed ${formatCount(result.applied.baselines.removed.length, "old baseline generation")}.`,
1799
+ result.applied.runtimeRemoved
1800
+ ? "Removed this project's disposable runtime and volumes."
1801
+ : "Preserved this project's disposable runtime.",
1802
+ `Removed ${formatCount(result.applied.imagesRemoved.length, "unused image tag")}.`,
1803
+ ]),
1804
+ ].join("\n");
1805
+ };
1806
+
1807
+ const runCleanup = async ({ flags, planOptions }) => {
1808
+ const plan =
1809
+ flags.cleanupPlan ??
1810
+ (await planRehearsalCleanup({
1811
+ ...planOptions,
1812
+ includeRuntime: flags.includeRuntime,
1813
+ includeImages: flags.includeImages,
1814
+ }));
1815
+ if (!flags.write) return { mode: "preview", plan };
1816
+ const applied = await applyRehearsalCleanup({
1817
+ plan,
1818
+ confirmation: flags.cleanupConfirmation,
1819
+ ...planOptions,
1820
+ removeRuntime: async () => {
1821
+ runManager({ action: "discard", flags });
1822
+ },
1823
+ });
1824
+ return { mode: "written", plan, applied };
1825
+ };
1826
+
1644
1827
  const usage = () => `Usage: rehearsal <command> [options]
1645
1828
 
1646
1829
  Commands:
@@ -1663,6 +1846,8 @@ Commands:
1663
1846
  status Report the disposable local runtime state
1664
1847
  stop Stop only this project's local runtime
1665
1848
  discard Remove only this project's disposable runtime
1849
+ cleanup [--include-runtime] [--include-images] [--write] [--confirm-cleanup=]
1850
+ Preview or apply conservative disk cleanup
1666
1851
  verify Verify the current local Rehearsal runtime
1667
1852
 
1668
1853
  Options: --json --verbose --debug --plain --config=<path> --target=supabase|postgresql`;
@@ -1737,6 +1922,11 @@ const executeCommand = async ({ command, flags, planOptions, guided }) => {
1737
1922
  emit({ command, data, flags, render: renderSupportReport });
1738
1923
  return;
1739
1924
  }
1925
+ if (command === "cleanup") {
1926
+ const data = await runCleanup({ flags, planOptions });
1927
+ emit({ command, data, flags, render: renderCleanup });
1928
+ return;
1929
+ }
1740
1930
  if (command === "explain" || (command === "run" && flags.dryRun)) {
1741
1931
  const data = await buildRehearsalPlan(planOptions);
1742
1932
  emit({ command, data, flags, render: renderPlan });
@@ -1872,8 +2062,12 @@ const resetGuidedFlags = (flags) => {
1872
2062
  flags.ledgerPath = undefined;
1873
2063
  flags.assetsPath = undefined;
1874
2064
  flags.target = undefined;
2065
+ flags.includeRuntime = false;
2066
+ flags.includeImages = false;
2067
+ flags.cleanupConfirmation = undefined;
1875
2068
  flags.setupPlan = undefined;
1876
2069
  flags.preparationPlan = undefined;
2070
+ flags.cleanupPlan = undefined;
1877
2071
  };
1878
2072
 
1879
2073
  const installGuidedExitShortcut = (flags) => {