@rehearsal-db/core 0.1.0-beta.5 → 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.
- package/CHANGELOG.md +56 -1
- package/COMPATIBILITY.md +5 -0
- package/README.md +112 -377
- package/SECURITY.md +2 -1
- package/docs/adapters.md +5 -0
- package/docs/baselines.md +81 -22
- package/docs/commands.md +105 -75
- package/docs/configuration.md +109 -8
- package/docs/getting-started.md +88 -227
- package/docs/glossary.md +7 -8
- package/docs/releasing.md +10 -1
- package/docs/roadmap.md +39 -0
- package/docs/security-model.md +5 -1
- package/docs/troubleshooting.md +69 -1
- package/docs/tutorial.md +70 -59
- package/package.json +4 -2
- package/scripts/lib/environment/local_supabase.mjs +8 -8
- package/scripts/lib/rehearsal/baseline_artifact.mjs +32 -3
- package/scripts/lib/rehearsal/cleanup.mjs +381 -0
- package/scripts/lib/rehearsal/configuration.d.mts +24 -3
- package/scripts/lib/rehearsal/configuration.mjs +337 -59
- package/scripts/lib/rehearsal/diagnostics.mjs +4 -2
- package/scripts/lib/rehearsal/plan.mjs +89 -14
- package/scripts/lib/rehearsal/runtime_restore.mjs +6 -2
- package/scripts/lib/rehearsal/setup.mjs +60 -28
- package/scripts/lib/runtime/postgresql_runtime.mjs +772 -0
- package/scripts/lib/runtime/runtime_target.mjs +41 -0
- package/scripts/lib/runtime/supabase_runtime.mjs +792 -0
- package/scripts/operations/database/manage_rehearsal_database.mjs +6 -785
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +330 -31
|
@@ -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();
|
|
@@ -80,6 +84,10 @@ const parseArguments = (arguments_) => {
|
|
|
80
84
|
recordsPath: undefined,
|
|
81
85
|
ledgerPath: undefined,
|
|
82
86
|
assetsPath: undefined,
|
|
87
|
+
target: undefined,
|
|
88
|
+
includeRuntime: false,
|
|
89
|
+
includeImages: false,
|
|
90
|
+
cleanupConfirmation: undefined,
|
|
83
91
|
};
|
|
84
92
|
const positionals = [];
|
|
85
93
|
for (const argument of arguments_) {
|
|
@@ -94,12 +102,20 @@ const parseArguments = (arguments_) => {
|
|
|
94
102
|
flags.configPath = argument.slice("--config=".length);
|
|
95
103
|
} else if (argument.startsWith("--confirm-candidates=")) {
|
|
96
104
|
flags.confirmation = argument.slice("--confirm-candidates=".length);
|
|
105
|
+
} else if (argument.startsWith("--confirm-cleanup=")) {
|
|
106
|
+
flags.cleanupConfirmation = argument.slice("--confirm-cleanup=".length);
|
|
97
107
|
} else if (argument.startsWith("--records=")) {
|
|
98
108
|
flags.recordsPath = argument.slice("--records=".length);
|
|
99
109
|
} else if (argument.startsWith("--ledger=")) {
|
|
100
110
|
flags.ledgerPath = argument.slice("--ledger=".length);
|
|
101
111
|
} else if (argument.startsWith("--assets=")) {
|
|
102
112
|
flags.assetsPath = argument.slice("--assets=".length);
|
|
113
|
+
} else if (argument.startsWith("--target=")) {
|
|
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;
|
|
103
119
|
} else if (argument.startsWith("--")) {
|
|
104
120
|
throw new Error(`Unknown Rehearsal option: ${argument}.`);
|
|
105
121
|
} else positionals.push(argument);
|
|
@@ -128,7 +144,7 @@ const renderPlan = (plan, verbosity) => {
|
|
|
128
144
|
"Environment",
|
|
129
145
|
` ✓ ${plan.environment.kind}`,
|
|
130
146
|
` ✓ Hosted access ${plan.environment.hostedAccess}`,
|
|
131
|
-
` ✓
|
|
147
|
+
` ✓ Hosted target configuration ${plan.environment.outboundNetwork}`,
|
|
132
148
|
...(plan.environment.authenticationProviders.length
|
|
133
149
|
? [
|
|
134
150
|
` ✓ External identity providers ${plan.environment.authenticationProviders.join(", ")}`,
|
|
@@ -274,7 +290,12 @@ const formatDuration = (durationMs) =>
|
|
|
274
290
|
? `${Math.round(durationMs)}ms`
|
|
275
291
|
: `${(durationMs / 1_000).toFixed(1)}s`;
|
|
276
292
|
|
|
277
|
-
const promptForChoice = async ({
|
|
293
|
+
const promptForChoice = async ({
|
|
294
|
+
message,
|
|
295
|
+
options,
|
|
296
|
+
flags = {},
|
|
297
|
+
optionsAlreadyShown = false,
|
|
298
|
+
}) => {
|
|
278
299
|
if (useStyledPrompts(flags)) {
|
|
279
300
|
const selected = await prompts.select({
|
|
280
301
|
message,
|
|
@@ -289,6 +310,18 @@ const promptForChoice = async ({ message, options, flags = {} }) => {
|
|
|
289
310
|
? options.find((option) => option.command === "exit")
|
|
290
311
|
: selected;
|
|
291
312
|
}
|
|
313
|
+
if (!optionsAlreadyShown) {
|
|
314
|
+
console.log(
|
|
315
|
+
[
|
|
316
|
+
"",
|
|
317
|
+
...options.map(
|
|
318
|
+
(option, index) =>
|
|
319
|
+
` ${index === 0 ? "›" : " "} ${index + 1}. ${option.label}${option.hint ? ` — ${option.hint}` : ""}`,
|
|
320
|
+
),
|
|
321
|
+
"",
|
|
322
|
+
].join("\n"),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
292
325
|
const prompt = createInterface({
|
|
293
326
|
input: process.stdin,
|
|
294
327
|
output: process.stdout,
|
|
@@ -502,6 +535,7 @@ const inspectGuidedProject = async (planOptions) => {
|
|
|
502
535
|
detected,
|
|
503
536
|
node,
|
|
504
537
|
config: "missing",
|
|
538
|
+
target: detected.hasSupabaseConfig ? "supabase" : "postgresql",
|
|
505
539
|
baseline: false,
|
|
506
540
|
runtime: false,
|
|
507
541
|
};
|
|
@@ -514,6 +548,7 @@ const inspectGuidedProject = async (planOptions) => {
|
|
|
514
548
|
detected,
|
|
515
549
|
node,
|
|
516
550
|
config: "invalid",
|
|
551
|
+
target: undefined,
|
|
517
552
|
configPath,
|
|
518
553
|
configError: redactDiagnosticValue(String(error?.message ?? error)),
|
|
519
554
|
baseline: false,
|
|
@@ -544,6 +579,7 @@ const inspectGuidedProject = async (planOptions) => {
|
|
|
544
579
|
detected,
|
|
545
580
|
node,
|
|
546
581
|
config: "valid",
|
|
582
|
+
target: loaded.config.runtime.target,
|
|
547
583
|
configPath,
|
|
548
584
|
baseline: Boolean(migrationSummary),
|
|
549
585
|
policy,
|
|
@@ -811,6 +847,13 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
811
847
|
command: "manage-runtime",
|
|
812
848
|
});
|
|
813
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
|
+
}
|
|
814
857
|
if (lastGuidedDetails) {
|
|
815
858
|
options.push({
|
|
816
859
|
label: "Show details from the last action",
|
|
@@ -829,8 +872,16 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
829
872
|
|
|
830
873
|
const statusLines = [
|
|
831
874
|
`${marker(state.node.supported)} Node.js ${state.node.version}${state.node.supported ? "" : " (Node.js 24 required)"}`,
|
|
832
|
-
`${marker(
|
|
833
|
-
|
|
875
|
+
`${marker(
|
|
876
|
+
state.target === "supabase"
|
|
877
|
+
? state.detected.hasSupabaseConfig
|
|
878
|
+
: state.detected.hasPostgresqlMigrations,
|
|
879
|
+
)} ${state.target === "supabase" ? "Supabase" : "PostgreSQL"} project detected`,
|
|
880
|
+
`${marker(
|
|
881
|
+
state.target === "supabase"
|
|
882
|
+
? state.detected.hasMigrations
|
|
883
|
+
: state.detected.hasPostgresqlMigrations,
|
|
884
|
+
)} Migration history detected`,
|
|
834
885
|
`${marker(state.config === "valid")} Rehearsal configuration${state.config === "invalid" ? " needs attention" : ""}`,
|
|
835
886
|
...(state.config === "valid"
|
|
836
887
|
? [
|
|
@@ -882,6 +933,7 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
882
933
|
message: "What would you like to do?",
|
|
883
934
|
options,
|
|
884
935
|
flags,
|
|
936
|
+
optionsAlreadyShown: !useStyledPrompts(flags),
|
|
885
937
|
});
|
|
886
938
|
if (selected.command === "exit") {
|
|
887
939
|
return null;
|
|
@@ -950,13 +1002,67 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
950
1002
|
}
|
|
951
1003
|
return runtimeAction.command;
|
|
952
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
|
+
}
|
|
953
1059
|
if (["baseline-guide", "baseline-prepare"].includes(selected.command)) {
|
|
954
1060
|
console.log(
|
|
955
1061
|
[
|
|
956
1062
|
"",
|
|
957
1063
|
terminalStyle(flags, "1", "CREATE A SAFE BASELINE"),
|
|
958
1064
|
"Rehearsal only reads the local files you name here.",
|
|
959
|
-
"It does not extract data or contact a hosted
|
|
1065
|
+
"It does not extract data or contact a hosted database.",
|
|
960
1066
|
"",
|
|
961
1067
|
].join("\n"),
|
|
962
1068
|
);
|
|
@@ -968,9 +1074,13 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
968
1074
|
discovered.ledgers.length
|
|
969
1075
|
? `Migration ledgers: ${discovered.ledgers.join(", ")}`
|
|
970
1076
|
: "Migration ledgers: none detected",
|
|
971
|
-
|
|
972
|
-
?
|
|
973
|
-
|
|
1077
|
+
...(state.target === "supabase"
|
|
1078
|
+
? [
|
|
1079
|
+
discovered.assetManifests.length
|
|
1080
|
+
? `Storage manifests: ${discovered.assetManifests.join(", ")}`
|
|
1081
|
+
: "Storage manifests: none detected",
|
|
1082
|
+
]
|
|
1083
|
+
: []),
|
|
974
1084
|
"Only paths and file structure were inspected; row values stay hidden.",
|
|
975
1085
|
];
|
|
976
1086
|
if (useStyledPrompts(flags)) {
|
|
@@ -1017,12 +1127,15 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
1017
1127
|
flags.write = true;
|
|
1018
1128
|
return "baseline prepare";
|
|
1019
1129
|
}
|
|
1020
|
-
flags.assetsPath =
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1130
|
+
flags.assetsPath =
|
|
1131
|
+
state.target === "supabase"
|
|
1132
|
+
? await promptForDiscoveredPath({
|
|
1133
|
+
message: "Storage assets",
|
|
1134
|
+
candidates: discovered.assetManifests,
|
|
1135
|
+
optional: true,
|
|
1136
|
+
flags,
|
|
1137
|
+
})
|
|
1138
|
+
: undefined;
|
|
1026
1139
|
const inspection = await inspectBaselineInputFiles({
|
|
1027
1140
|
...planOptions,
|
|
1028
1141
|
recordsPath: flags.recordsPath,
|
|
@@ -1052,7 +1165,26 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
1052
1165
|
}
|
|
1053
1166
|
if (selected.command !== "setup-write") return selected.command;
|
|
1054
1167
|
|
|
1055
|
-
const
|
|
1168
|
+
const targetChoice = await promptForChoice({
|
|
1169
|
+
message: "Which database should Rehearsal use?",
|
|
1170
|
+
flags,
|
|
1171
|
+
options:
|
|
1172
|
+
state.target === "supabase"
|
|
1173
|
+
? [
|
|
1174
|
+
{ label: "Supabase", command: "supabase" },
|
|
1175
|
+
{ label: "PostgreSQL", command: "postgresql" },
|
|
1176
|
+
]
|
|
1177
|
+
: [
|
|
1178
|
+
{ label: "PostgreSQL", command: "postgresql" },
|
|
1179
|
+
{ label: "Supabase", command: "supabase" },
|
|
1180
|
+
],
|
|
1181
|
+
});
|
|
1182
|
+
if (!targetChoice) return "refresh";
|
|
1183
|
+
flags.target = targetChoice.command;
|
|
1184
|
+
const setupPlan = await planRehearsalSetup({
|
|
1185
|
+
projectRoot,
|
|
1186
|
+
target: flags.target,
|
|
1187
|
+
});
|
|
1056
1188
|
const preview = {
|
|
1057
1189
|
...summarizeRehearsalSetup(setupPlan, { mode: "preview" }),
|
|
1058
1190
|
nextAction:
|
|
@@ -1215,6 +1347,14 @@ const runManager = ({ action, flags }) => {
|
|
|
1215
1347
|
const inferred = normalizeRehearsalError(
|
|
1216
1348
|
new Error(completeOutput || `Rehearsal ${action} failed.`),
|
|
1217
1349
|
);
|
|
1350
|
+
const category =
|
|
1351
|
+
action === "migrate" ||
|
|
1352
|
+
(action === "run" &&
|
|
1353
|
+
/migrateRuntime|candidate migration|migration up/iu.test(
|
|
1354
|
+
completeOutput,
|
|
1355
|
+
))
|
|
1356
|
+
? "migration_candidate_failure"
|
|
1357
|
+
: inferred.category;
|
|
1218
1358
|
const conciseOutput = completeOutput
|
|
1219
1359
|
.replaceAll(String.fromCodePoint(27), "")
|
|
1220
1360
|
.split("\n")
|
|
@@ -1224,7 +1364,7 @@ const runManager = ({ action, flags }) => {
|
|
|
1224
1364
|
.join("\n")
|
|
1225
1365
|
.slice(-2_000);
|
|
1226
1366
|
throw new RehearsalError({
|
|
1227
|
-
category
|
|
1367
|
+
category,
|
|
1228
1368
|
code: `MANAGER_${action.toUpperCase()}_FAILED`,
|
|
1229
1369
|
message: `The local Rehearsal ${action} operation did not complete.`,
|
|
1230
1370
|
expected: "the isolated runtime operation to finish and verify",
|
|
@@ -1382,7 +1522,12 @@ const renderInit = (result) =>
|
|
|
1382
1522
|
].join("\n");
|
|
1383
1523
|
|
|
1384
1524
|
const runSetup = async ({ flags, planOptions }) => {
|
|
1385
|
-
const plan =
|
|
1525
|
+
const plan =
|
|
1526
|
+
flags.setupPlan ??
|
|
1527
|
+
(await planRehearsalSetup({
|
|
1528
|
+
projectRoot,
|
|
1529
|
+
target: flags.target ?? "supabase",
|
|
1530
|
+
}));
|
|
1386
1531
|
if (flags.write) await applyRehearsalSetup(plan);
|
|
1387
1532
|
const result = summarizeRehearsalSetup(plan, {
|
|
1388
1533
|
mode: flags.write ? "written" : "preview",
|
|
@@ -1454,9 +1599,12 @@ const renderSetup = (result) => {
|
|
|
1454
1599
|
return [
|
|
1455
1600
|
`REHEARSAL SETUP — ${result.mode.toUpperCase()}`,
|
|
1456
1601
|
`Project: ${result.project}`,
|
|
1602
|
+
`Database: ${result.target === "postgresql" ? "PostgreSQL" : "Supabase"}`,
|
|
1457
1603
|
`Runtime ID: ${result.projectId}`,
|
|
1458
1604
|
`Application: ${result.applicationUrl}`,
|
|
1459
|
-
|
|
1605
|
+
result.target === "postgresql"
|
|
1606
|
+
? `Port: database ${result.ports.database}`
|
|
1607
|
+
: `Ports: API ${result.ports.api}, database ${result.ports.database}, Studio ${result.ports.studio}`,
|
|
1460
1608
|
"",
|
|
1461
1609
|
"Files",
|
|
1462
1610
|
...result.files.map(
|
|
@@ -1567,11 +1715,117 @@ const renderSupportReport = (report) => {
|
|
|
1567
1715
|
].join("\n");
|
|
1568
1716
|
};
|
|
1569
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
|
+
|
|
1570
1824
|
const usage = () => `Usage: rehearsal <command> [options]
|
|
1571
1825
|
|
|
1572
1826
|
Commands:
|
|
1573
1827
|
guide Open the interactive, state-aware home screen
|
|
1574
|
-
setup [--write]
|
|
1828
|
+
setup [--target=] [--write] Preview or create safe first-run scaffolding
|
|
1575
1829
|
init [--write] Preview or explicitly write safe starter config
|
|
1576
1830
|
baseline prepare --records= --ledger= [--write] Create a fail-closed policy draft
|
|
1577
1831
|
baseline create --records= --ledger= [--assets=] Create a baseline from safe local inputs
|
|
@@ -1589,9 +1843,11 @@ Commands:
|
|
|
1589
1843
|
status Report the disposable local runtime state
|
|
1590
1844
|
stop Stop only this project's local runtime
|
|
1591
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
|
|
1592
1848
|
verify Verify the current local Rehearsal runtime
|
|
1593
1849
|
|
|
1594
|
-
Options: --json --verbose --debug --plain --config=<path
|
|
1850
|
+
Options: --json --verbose --debug --plain --config=<path> --target=supabase|postgresql`;
|
|
1595
1851
|
|
|
1596
1852
|
const executeCommand = async ({ command, flags, planOptions, guided }) => {
|
|
1597
1853
|
if (command === "help" || flags.help) {
|
|
@@ -1663,6 +1919,11 @@ const executeCommand = async ({ command, flags, planOptions, guided }) => {
|
|
|
1663
1919
|
emit({ command, data, flags, render: renderSupportReport });
|
|
1664
1920
|
return;
|
|
1665
1921
|
}
|
|
1922
|
+
if (command === "cleanup") {
|
|
1923
|
+
const data = await runCleanup({ flags, planOptions });
|
|
1924
|
+
emit({ command, data, flags, render: renderCleanup });
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1666
1927
|
if (command === "explain" || (command === "run" && flags.dryRun)) {
|
|
1667
1928
|
const data = await buildRehearsalPlan(planOptions);
|
|
1668
1929
|
emit({ command, data, flags, render: renderPlan });
|
|
@@ -1797,8 +2058,41 @@ const resetGuidedFlags = (flags) => {
|
|
|
1797
2058
|
flags.recordsPath = undefined;
|
|
1798
2059
|
flags.ledgerPath = undefined;
|
|
1799
2060
|
flags.assetsPath = undefined;
|
|
2061
|
+
flags.target = undefined;
|
|
2062
|
+
flags.includeRuntime = false;
|
|
2063
|
+
flags.includeImages = false;
|
|
2064
|
+
flags.cleanupConfirmation = undefined;
|
|
1800
2065
|
flags.setupPlan = undefined;
|
|
1801
2066
|
flags.preparationPlan = undefined;
|
|
2067
|
+
flags.cleanupPlan = undefined;
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
const installGuidedExitShortcut = (flags) => {
|
|
2071
|
+
let exiting = false;
|
|
2072
|
+
const exit = () => {
|
|
2073
|
+
if (exiting) return;
|
|
2074
|
+
exiting = true;
|
|
2075
|
+
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
2076
|
+
process.stdin.setRawMode(false);
|
|
2077
|
+
}
|
|
2078
|
+
process.stdout.write("\u001B[?25h");
|
|
2079
|
+
if (useStyledPrompts(flags)) {
|
|
2080
|
+
prompts.outro("Rehearsal exited.");
|
|
2081
|
+
} else {
|
|
2082
|
+
console.log("\nRehearsal exited.");
|
|
2083
|
+
}
|
|
2084
|
+
process.exit(0);
|
|
2085
|
+
};
|
|
2086
|
+
const onInput = (chunk) => {
|
|
2087
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2088
|
+
if (bytes.includes(0x1a)) exit();
|
|
2089
|
+
};
|
|
2090
|
+
process.stdin.prependListener("data", onInput);
|
|
2091
|
+
process.prependListener("SIGTSTP", exit);
|
|
2092
|
+
return () => {
|
|
2093
|
+
process.stdin.removeListener("data", onInput);
|
|
2094
|
+
process.removeListener("SIGTSTP", exit);
|
|
2095
|
+
};
|
|
1802
2096
|
};
|
|
1803
2097
|
|
|
1804
2098
|
const main = async () => {
|
|
@@ -1825,20 +2119,25 @@ const main = async () => {
|
|
|
1825
2119
|
);
|
|
1826
2120
|
}
|
|
1827
2121
|
flags.guided = true;
|
|
2122
|
+
const removeGuidedExitShortcut = installGuidedExitShortcut(flags);
|
|
1828
2123
|
if (useStyledPrompts(flags)) {
|
|
1829
2124
|
prompts.intro("REHEARSAL · Safe local migration testing");
|
|
1830
2125
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2126
|
+
try {
|
|
2127
|
+
while (true) {
|
|
2128
|
+
resetGuidedFlags(flags);
|
|
2129
|
+
const selected = await runGuidedHome({ flags, planOptions });
|
|
2130
|
+
if (!selected) break;
|
|
2131
|
+
if (selected === "refresh") continue;
|
|
2132
|
+
await executeCommand({
|
|
2133
|
+
command: selected,
|
|
2134
|
+
flags,
|
|
2135
|
+
planOptions,
|
|
2136
|
+
guided: true,
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
} finally {
|
|
2140
|
+
removeGuidedExitShortcut();
|
|
1842
2141
|
}
|
|
1843
2142
|
if (useStyledPrompts(flags)) {
|
|
1844
2143
|
prompts.outro("See you at the next rehearsal.");
|