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

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.
@@ -468,6 +468,7 @@ const resetRuntime = async () => {
468
468
  workdir: runtimeWorkdir,
469
469
  exclude: ["edge-runtime", "logflare", "vector", "realtime"],
470
470
  environment: serviceEnvironment,
471
+ autoStartColima: config.containerRuntime.autoStartColima,
471
472
  });
472
473
  const structuralSchemaSha256 = await restoreProductionSchema(active);
473
474
  await restoreBaselineRows(active);
@@ -532,6 +533,7 @@ const startRuntime = async () => {
532
533
  workdir: runtimeWorkdir,
533
534
  exclude: ["edge-runtime", "logflare", "vector", "realtime"],
534
535
  environment: serviceEnvironment,
536
+ autoStartColima: config.containerRuntime.autoStartColima,
535
537
  });
536
538
  const projectRuntime = configureProjectRuntime({ environment, baseline });
537
539
  await writeRuntimeEnvironment({
@@ -56,6 +56,10 @@ 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";
59
63
 
60
64
  const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
61
65
  const projectRoot = process.cwd();
@@ -81,6 +85,9 @@ const parseArguments = (arguments_) => {
81
85
  ledgerPath: undefined,
82
86
  assetsPath: undefined,
83
87
  target: undefined,
88
+ includeRuntime: false,
89
+ includeImages: false,
90
+ cleanupConfirmation: undefined,
84
91
  };
85
92
  const positionals = [];
86
93
  for (const argument of arguments_) {
@@ -95,6 +102,8 @@ const parseArguments = (arguments_) => {
95
102
  flags.configPath = argument.slice("--config=".length);
96
103
  } else if (argument.startsWith("--confirm-candidates=")) {
97
104
  flags.confirmation = argument.slice("--confirm-candidates=".length);
105
+ } else if (argument.startsWith("--confirm-cleanup=")) {
106
+ flags.cleanupConfirmation = argument.slice("--confirm-cleanup=".length);
98
107
  } else if (argument.startsWith("--records=")) {
99
108
  flags.recordsPath = argument.slice("--records=".length);
100
109
  } else if (argument.startsWith("--ledger=")) {
@@ -103,6 +112,10 @@ const parseArguments = (arguments_) => {
103
112
  flags.assetsPath = argument.slice("--assets=".length);
104
113
  } else if (argument.startsWith("--target=")) {
105
114
  flags.target = argument.slice("--target=".length);
115
+ } else if (argument === "--include-runtime") {
116
+ flags.includeRuntime = true;
117
+ } else if (argument === "--include-images") {
118
+ flags.includeImages = true;
106
119
  } else if (argument.startsWith("--")) {
107
120
  throw new Error(`Unknown Rehearsal option: ${argument}.`);
108
121
  } else positionals.push(argument);
@@ -131,7 +144,7 @@ const renderPlan = (plan, verbosity) => {
131
144
  "Environment",
132
145
  ` ✓ ${plan.environment.kind}`,
133
146
  ` ✓ Hosted access ${plan.environment.hostedAccess}`,
134
- ` ✓ Application/provider-data egress ${plan.environment.outboundNetwork}`,
147
+ ` ✓ Hosted target configuration ${plan.environment.outboundNetwork}`,
135
148
  ...(plan.environment.authenticationProviders.length
136
149
  ? [
137
150
  ` ✓ External identity providers ${plan.environment.authenticationProviders.join(", ")}`,
@@ -834,6 +847,13 @@ const runGuidedHome = async ({ flags, planOptions }) => {
834
847
  command: "manage-runtime",
835
848
  });
836
849
  }
850
+ if (state.node.supported && state.config === "valid") {
851
+ options.push({
852
+ label: "Clean up disk space",
853
+ hint: "Preview old baselines, the runtime, and unused images",
854
+ command: "cleanup-guide",
855
+ });
856
+ }
837
857
  if (lastGuidedDetails) {
838
858
  options.push({
839
859
  label: "Show details from the last action",
@@ -982,6 +1002,60 @@ const runGuidedHome = async ({ flags, planOptions }) => {
982
1002
  }
983
1003
  return runtimeAction.command;
984
1004
  }
1005
+ if (selected.command === "cleanup-guide") {
1006
+ const cleanupScope = await promptForChoice({
1007
+ message: "What may Rehearsal include in the cleanup preview?",
1008
+ flags,
1009
+ options: [
1010
+ {
1011
+ label: "Old baselines only",
1012
+ hint: "Keep the configured number of recent generations",
1013
+ command: "baselines",
1014
+ },
1015
+ {
1016
+ label: "Old baselines and this project's runtime",
1017
+ hint: "Also remove the disposable database and its volumes",
1018
+ command: "runtime",
1019
+ },
1020
+ {
1021
+ label: "Old baselines and older unused Supabase images",
1022
+ hint: "Images may be shared and can be downloaded again",
1023
+ command: "images",
1024
+ },
1025
+ {
1026
+ label: "Everything above",
1027
+ command: "all",
1028
+ },
1029
+ { label: "Back", command: "exit" },
1030
+ ],
1031
+ });
1032
+ if (!cleanupScope || cleanupScope.command === "exit") return "refresh";
1033
+ flags.includeRuntime = ["runtime", "all"].includes(cleanupScope.command);
1034
+ flags.includeImages = ["images", "all"].includes(cleanupScope.command);
1035
+ flags.cleanupPlan = await planRehearsalCleanup({
1036
+ ...planOptions,
1037
+ includeRuntime: flags.includeRuntime,
1038
+ includeImages: flags.includeImages,
1039
+ });
1040
+ console.log(
1041
+ `\n${renderCleanup({ mode: "preview", plan: flags.cleanupPlan })}`,
1042
+ );
1043
+ const selectedCount =
1044
+ flags.cleanupPlan.baselines.removed.length +
1045
+ (flags.cleanupPlan.runtime.included && flags.cleanupPlan.runtime.detected
1046
+ ? 1
1047
+ : 0) +
1048
+ flags.cleanupPlan.images.candidates.length;
1049
+ if (selectedCount === 0) return "refresh";
1050
+ const accepted = await promptForConfirmation(
1051
+ "Remove exactly the resources shown above?",
1052
+ flags,
1053
+ );
1054
+ if (!accepted) return "refresh";
1055
+ flags.write = true;
1056
+ flags.cleanupConfirmation = flags.cleanupPlan.digest;
1057
+ return "cleanup";
1058
+ }
985
1059
  if (["baseline-guide", "baseline-prepare"].includes(selected.command)) {
986
1060
  console.log(
987
1061
  [
@@ -1641,6 +1715,112 @@ const renderSupportReport = (report) => {
1641
1715
  ].join("\n");
1642
1716
  };
1643
1717
 
1718
+ const formatBytes = (bytes) => {
1719
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
1720
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
1721
+ const unit = Math.min(
1722
+ Math.floor(Math.log(bytes) / Math.log(1024)),
1723
+ units.length - 1,
1724
+ );
1725
+ const value = bytes / 1024 ** unit;
1726
+ return `${value >= 10 || unit === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`;
1727
+ };
1728
+
1729
+ const renderCleanup = (result) => {
1730
+ const plan = result.plan;
1731
+ const imageBytes = plan.images.candidates.reduce(
1732
+ (total, image) => total + image.bytes,
1733
+ 0,
1734
+ );
1735
+ const dockerImages = plan.docker.resources.find(
1736
+ (resource) => resource.Type === "Images",
1737
+ );
1738
+ const selectedCount =
1739
+ plan.baselines.removed.length +
1740
+ (plan.runtime.included && plan.runtime.detected ? 1 : 0) +
1741
+ plan.images.candidates.length;
1742
+ return [
1743
+ `REHEARSAL CLEANUP — ${result.mode.toUpperCase()}`,
1744
+ `Project: ${plan.project}`,
1745
+ ...(dockerImages
1746
+ ? [
1747
+ `Docker images: ${dockerImages.Size}; ${dockerImages.Reclaimable} reclaimable across Docker`,
1748
+ ]
1749
+ : ["Docker usage: unavailable"]),
1750
+ ...(plan.docker.disk
1751
+ ? [
1752
+ `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)`,
1753
+ ]
1754
+ : []),
1755
+ "",
1756
+ "Project-owned files",
1757
+ plan.baselines.available
1758
+ ? ` ${plan.baselines.removed.length ? "→" : "✓"} ${formatCount(plan.baselines.removed.length, "old baseline generation")} selected; keeping ${plan.baselines.retained.length}`
1759
+ : " ○ No active baseline found",
1760
+ "",
1761
+ "Disposable runtime",
1762
+ !plan.runtime.detected
1763
+ ? " ✓ No runtime found"
1764
+ : plan.runtime.included
1765
+ ? " → Current project runtime selected"
1766
+ : " ○ Current project runtime preserved (add --include-runtime to select it)",
1767
+ "",
1768
+ "Shared Docker images",
1769
+ !plan.images.requested
1770
+ ? " ○ Not inspected (add --include-images to inspect older unused Supabase images)"
1771
+ : !plan.images.inspected
1772
+ ? " ! Docker is unavailable; images could not be inspected"
1773
+ : plan.images.candidates.length
1774
+ ? ` → ${formatCount(plan.images.candidates.length, "older unused Supabase image")} selected; approximately ${formatBytes(imageBytes)}`
1775
+ : " ✓ No older unused Supabase images found",
1776
+ ...(plan.images.candidates.length
1777
+ ? plan.images.candidates.flatMap((image) =>
1778
+ image.tags.map((tag) => ` ${tag}`),
1779
+ )
1780
+ : []),
1781
+ "",
1782
+ ...(result.mode === "preview"
1783
+ ? selectedCount
1784
+ ? [
1785
+ "Nothing has been removed.",
1786
+ `Cleanup set: ${plan.digest.slice(0, 12)}`,
1787
+ `Exact confirmation: --confirm-cleanup=${plan.digest}`,
1788
+ "Review this list, then rerun the same command with --write and the exact confirmation above.",
1789
+ ]
1790
+ : [
1791
+ "Nothing has been removed, and nothing is currently selected.",
1792
+ "Use --include-runtime or --include-images only when you want those shared resources considered.",
1793
+ ]
1794
+ : [
1795
+ `Removed ${formatCount(result.applied.baselines.removed.length, "old baseline generation")}.`,
1796
+ result.applied.runtimeRemoved
1797
+ ? "Removed this project's disposable runtime and volumes."
1798
+ : "Preserved this project's disposable runtime.",
1799
+ `Removed ${formatCount(result.applied.imagesRemoved.length, "unused image tag")}.`,
1800
+ ]),
1801
+ ].join("\n");
1802
+ };
1803
+
1804
+ const runCleanup = async ({ flags, planOptions }) => {
1805
+ const plan =
1806
+ flags.cleanupPlan ??
1807
+ (await planRehearsalCleanup({
1808
+ ...planOptions,
1809
+ includeRuntime: flags.includeRuntime,
1810
+ includeImages: flags.includeImages,
1811
+ }));
1812
+ if (!flags.write) return { mode: "preview", plan };
1813
+ const applied = await applyRehearsalCleanup({
1814
+ plan,
1815
+ confirmation: flags.cleanupConfirmation,
1816
+ ...planOptions,
1817
+ removeRuntime: async () => {
1818
+ runManager({ action: "discard", flags });
1819
+ },
1820
+ });
1821
+ return { mode: "written", plan, applied };
1822
+ };
1823
+
1644
1824
  const usage = () => `Usage: rehearsal <command> [options]
1645
1825
 
1646
1826
  Commands:
@@ -1663,6 +1843,8 @@ Commands:
1663
1843
  status Report the disposable local runtime state
1664
1844
  stop Stop only this project's local runtime
1665
1845
  discard Remove only this project's disposable runtime
1846
+ cleanup [--include-runtime] [--include-images] [--write] [--confirm-cleanup=]
1847
+ Preview or apply conservative disk cleanup
1666
1848
  verify Verify the current local Rehearsal runtime
1667
1849
 
1668
1850
  Options: --json --verbose --debug --plain --config=<path> --target=supabase|postgresql`;
@@ -1737,6 +1919,11 @@ const executeCommand = async ({ command, flags, planOptions, guided }) => {
1737
1919
  emit({ command, data, flags, render: renderSupportReport });
1738
1920
  return;
1739
1921
  }
1922
+ if (command === "cleanup") {
1923
+ const data = await runCleanup({ flags, planOptions });
1924
+ emit({ command, data, flags, render: renderCleanup });
1925
+ return;
1926
+ }
1740
1927
  if (command === "explain" || (command === "run" && flags.dryRun)) {
1741
1928
  const data = await buildRehearsalPlan(planOptions);
1742
1929
  emit({ command, data, flags, render: renderPlan });
@@ -1872,8 +2059,12 @@ const resetGuidedFlags = (flags) => {
1872
2059
  flags.ledgerPath = undefined;
1873
2060
  flags.assetsPath = undefined;
1874
2061
  flags.target = undefined;
2062
+ flags.includeRuntime = false;
2063
+ flags.includeImages = false;
2064
+ flags.cleanupConfirmation = undefined;
1875
2065
  flags.setupPlan = undefined;
1876
2066
  flags.preparationPlan = undefined;
2067
+ flags.cleanupPlan = undefined;
1877
2068
  };
1878
2069
 
1879
2070
  const installGuidedExitShortcut = (flags) => {