@rehearsal-db/core 0.1.0-beta.3 → 0.1.0-beta.4
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 +14 -1
- package/README.md +4 -2
- package/docs/commands.md +8 -0
- package/docs/getting-started.md +8 -2
- package/docs/releasing.md +1 -1
- package/docs/sanitization.md +6 -0
- package/package.json +5 -2
- package/scripts/lib/rehearsal/diagnostics.mjs +1 -1
- package/scripts/lib/rehearsal/policy_review.mjs +52 -0
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +460 -99
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## [0.1.0-beta.4] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- A persistent guided session with polished terminal prompts, interactive policy review,
|
|
13
|
+
concise runtime receipts, expandable technical details, and real-PTY regression tests.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Guided menus now place the next recommended action first and automatically re-inspect
|
|
18
|
+
project state after every completed step while preserving plain and non-TTY modes.
|
|
19
|
+
|
|
8
20
|
## [0.1.0-beta.3] - 2026-10-01
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -106,7 +118,8 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
106
118
|
publication uses short-lived trusted OIDC, and every release tag must already exist on
|
|
107
119
|
protected `main`.
|
|
108
120
|
|
|
109
|
-
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.
|
|
121
|
+
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.4...HEAD
|
|
122
|
+
[0.1.0-beta.4]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.3...v0.1.0-beta.4
|
|
110
123
|
[0.1.0-beta.3]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.2...v0.1.0-beta.3
|
|
111
124
|
[0.1.0-beta.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.1...v0.1.0-beta.2
|
|
112
125
|
[0.1.0-beta.1]: https://github.com/Ddupasquier/rehearsal-db/releases/tag/v0.1.0-beta.1
|
package/README.md
CHANGED
|
@@ -108,8 +108,10 @@ npx rehearsal discard
|
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
Running `npx rehearsal` in a terminal opens a state-aware guide that shows completed
|
|
111
|
-
setup steps and recommends available actions. The
|
|
112
|
-
|
|
111
|
+
setup steps and recommends available actions. The guide stays open after each action,
|
|
112
|
+
re-inspects the project, and advances to the next useful step. It includes an interactive
|
|
113
|
+
column-by-column policy reviewer, concise completion receipts, and optional technical
|
|
114
|
+
details. The explicit commands remain the stable interface for automation and CI.
|
|
113
115
|
|
|
114
116
|
`setup` previews a conservative first-run scaffold: the Rehearsal configuration, a
|
|
115
117
|
dedicated local-only Supabase configuration on an available port block, and protective
|
package/docs/commands.md
CHANGED
|
@@ -40,6 +40,12 @@ bounded local Storage bytes; every manifest `file` must also remain inside the p
|
|
|
40
40
|
The guided home screen can ask for these paths so they do not need to be supplied as
|
|
41
41
|
flags.
|
|
42
42
|
|
|
43
|
+
The guide is a persistent session: after setup, policy review, baseline creation, or a
|
|
44
|
+
runtime action it re-reads project state and offers the next relevant action. Generated
|
|
45
|
+
policy drafts can be completed interactively without editing JSON. Each column still
|
|
46
|
+
requires explicit action, generated, identity, and foreign-key decisions; the guide does
|
|
47
|
+
not silently infer them.
|
|
48
|
+
|
|
43
49
|
`baseline prepare` reads only table and column names from the NDJSON records; row values
|
|
44
50
|
are never included in its result. Its generated policy deliberately marks every column
|
|
45
51
|
decision `REVIEW REQUIRED` and cannot be activated until a human completes the metadata
|
|
@@ -50,6 +56,8 @@ Automation should use `--json` and inspect both exit status and the versioned en
|
|
|
50
56
|
Exit-code meanings are documented in the root README. Scripts must not parse human text.
|
|
51
57
|
When standard input or output is not an interactive terminal, bare `rehearsal` prints
|
|
52
58
|
help instead of prompting. Use `--plain` to disable decorative terminal styling.
|
|
59
|
+
`NO_COLOR` also selects the plain numbered interface. Both interactive modes preserve
|
|
60
|
+
the same safety decisions and cancellation behavior.
|
|
53
61
|
|
|
54
62
|
`setup` chooses an available local port block and generates a conservative Supabase
|
|
55
63
|
configuration with hosted access and optional networked services disabled. It does not
|
package/docs/getting-started.md
CHANGED
|
@@ -47,7 +47,8 @@ npx rehearsal
|
|
|
47
47
|
|
|
48
48
|
Choose **Set up Rehearsal** in the guide. It previews a versioned configuration, a
|
|
49
49
|
dedicated local-only Supabase configuration using available ports, and protective
|
|
50
|
-
`.gitignore` entries before asking permission to write.
|
|
50
|
+
`.gitignore` entries before asking permission to write. The guide remains open afterward
|
|
51
|
+
and recommends the next incomplete stage.
|
|
51
52
|
|
|
52
53
|
The same flow is available noninteractively as an explicit preview and write:
|
|
53
54
|
|
|
@@ -104,7 +105,12 @@ npx rehearsal baseline prepare \
|
|
|
104
105
|
--write
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
After creating the draft, choose **Review the script** in the guide. Rehearsal walks each
|
|
109
|
+
column through sanitization action, generated status, identity status, and optional
|
|
110
|
+
foreign-key metadata. Every answer is explicit, the complete policy is validated before
|
|
111
|
+
writing, and the original draft is replaced only if it did not change during review.
|
|
112
|
+
|
|
113
|
+
For noninteractive workflows, review every `REVIEW REQUIRED` field directly and remove
|
|
108
114
|
`"draft": true` only after that review. Rehearsal refuses to activate a draft.
|
|
109
115
|
|
|
110
116
|
Historical migration `supabase/migrations/20260101000000_create_widgets.sql`:
|
package/docs/releasing.md
CHANGED
|
@@ -37,7 +37,7 @@ publication must change and prove the workflow before narrowing that permission.
|
|
|
37
37
|
onboarding. Correct and retest the first confusing, missing, or wrong instruction.
|
|
38
38
|
5. Change `private` to `false` only in the reviewed release change.
|
|
39
39
|
6. Record the exact tarball filename, SHA-1, SHA-256, allowlisted files, unpacked size,
|
|
40
|
-
executable,
|
|
40
|
+
executable, declared runtime dependency inventory, and dependency audit result.
|
|
41
41
|
7. Obtain explicit publication authorization for that exact version and artifact.
|
|
42
42
|
8. Merge the approved release commit through protected `main` and create the exact
|
|
43
43
|
`v<package-version>` tag and GitHub release.
|
package/docs/sanitization.md
CHANGED
|
@@ -19,6 +19,12 @@ whether it is an identity column, and its foreign-key target or explicit absence
|
|
|
19
19
|
draft uses `REVIEW REQUIRED` placeholders and cannot be activated until they are
|
|
20
20
|
replaced and the `draft` marker is removed.
|
|
21
21
|
|
|
22
|
+
In an interactive terminal, the guided **Review the script** action completes these
|
|
23
|
+
decisions one column at a time. Replacement-oriented actions are shown first, retaining
|
|
24
|
+
a value is explicitly labeled as sensitive, and no choice is silently inferred. The
|
|
25
|
+
reviewer validates the completed policy and refuses to overwrite a draft changed during
|
|
26
|
+
the session.
|
|
27
|
+
|
|
22
28
|
The package exports `validateSanitizationCoverage` and
|
|
23
29
|
`applySanitizationAction` as generic primitives:
|
|
24
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehearsal-db/core",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Safely rehearse Supabase migrations against sanitized, production-shaped PostgreSQL data.",
|
|
6
6
|
"repository": {
|
|
@@ -69,8 +69,11 @@
|
|
|
69
69
|
"database"
|
|
70
70
|
],
|
|
71
71
|
"license": "MIT",
|
|
72
|
-
"dependencies": {
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@clack/prompts": "^1.8.1"
|
|
74
|
+
},
|
|
73
75
|
"devDependencies": {
|
|
76
|
+
"@lydell/node-pty": "^1.2.0-beta.15",
|
|
74
77
|
"prettier": "^3.9.6",
|
|
75
78
|
"vitest": "^4.1.10"
|
|
76
79
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
|
|
9
9
|
export const REHEARSAL_RESULT_VERSION = 1;
|
|
10
|
-
export const REHEARSAL_VERSION = "0.1.0-beta.
|
|
10
|
+
export const REHEARSAL_VERSION = "0.1.0-beta.4";
|
|
11
11
|
|
|
12
12
|
export const REHEARSAL_EXIT_CODES = Object.freeze({
|
|
13
13
|
success: 0,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Complete and safely apply a generated REVIEW REQUIRED policy draft.
|
|
3
|
+
* The caller owns the interactive presentation; this module owns validation and
|
|
4
|
+
* concurrent-change refusal.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { validateRuntimeSanitizationPolicy } from "./sanitization_policy.mjs";
|
|
9
|
+
|
|
10
|
+
export const readReviewablePolicyDraft = async (path) => {
|
|
11
|
+
const source = await readFile(path, "utf8");
|
|
12
|
+
const draft = JSON.parse(source);
|
|
13
|
+
if (draft?.draft !== true || !Array.isArray(draft.tables)) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Interactive review requires a generated sanitization policy with draft: true.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return { source, draft };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const completePolicyDraft = async ({ draft, reviewColumn }) => {
|
|
22
|
+
if (draft?.draft !== true || !Array.isArray(draft.tables)) {
|
|
23
|
+
throw new Error("A generated REVIEW REQUIRED policy draft is required.");
|
|
24
|
+
}
|
|
25
|
+
const tables = [];
|
|
26
|
+
for (const table of draft.tables) {
|
|
27
|
+
const columns = [];
|
|
28
|
+
for (const column of table.columns ?? []) {
|
|
29
|
+
const reviewed = await reviewColumn({
|
|
30
|
+
table: table.name,
|
|
31
|
+
column: column.name,
|
|
32
|
+
});
|
|
33
|
+
columns.push({ name: column.name, ...reviewed });
|
|
34
|
+
}
|
|
35
|
+
tables.push({ ...table, columns });
|
|
36
|
+
}
|
|
37
|
+
const reviewed = { ...draft, tables };
|
|
38
|
+
delete reviewed.draft;
|
|
39
|
+
return validateRuntimeSanitizationPolicy(reviewed);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const applyReviewedPolicy = async ({ path, originalSource, policy }) => {
|
|
43
|
+
validateRuntimeSanitizationPolicy(policy);
|
|
44
|
+
if ((await readFile(path, "utf8")) !== originalSource) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"The sanitization policy changed during review; no reviewed policy was written.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
await writeFile(path, `${JSON.stringify(policy, null, 2)}\n`, {
|
|
50
|
+
mode: 0o600,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
@@ -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,
|
|
@@ -45,6 +46,11 @@ import {
|
|
|
45
46
|
summarizeRehearsalSetup,
|
|
46
47
|
} from "../../lib/rehearsal/setup.mjs";
|
|
47
48
|
import { formatCount } from "../../lib/rehearsal/human_output.mjs";
|
|
49
|
+
import {
|
|
50
|
+
applyReviewedPolicy,
|
|
51
|
+
completePolicyDraft,
|
|
52
|
+
readReviewablePolicyDraft,
|
|
53
|
+
} from "../../lib/rehearsal/policy_review.mjs";
|
|
48
54
|
|
|
49
55
|
const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
|
|
50
56
|
const projectRoot = process.cwd();
|
|
@@ -54,6 +60,7 @@ const managerPath = join(
|
|
|
54
60
|
packageRoot,
|
|
55
61
|
"scripts/operations/database/manage_rehearsal_database.mjs",
|
|
56
62
|
);
|
|
63
|
+
let lastGuidedDetails;
|
|
57
64
|
|
|
58
65
|
const parseArguments = (arguments_) => {
|
|
59
66
|
const flags = {
|
|
@@ -255,12 +262,27 @@ const terminalStyle = (flags, code, value) =>
|
|
|
255
262
|
const isHumanTerminal = (flags) =>
|
|
256
263
|
!flags.json && process.stdin.isTTY && process.stdout.isTTY;
|
|
257
264
|
|
|
265
|
+
const useStyledPrompts = (flags) => isHumanTerminal(flags) && useColor(flags);
|
|
266
|
+
|
|
258
267
|
const formatDuration = (durationMs) =>
|
|
259
268
|
durationMs < 1_000
|
|
260
269
|
? `${Math.round(durationMs)}ms`
|
|
261
270
|
: `${(durationMs / 1_000).toFixed(1)}s`;
|
|
262
271
|
|
|
263
|
-
const promptForChoice = async ({ message, options }) => {
|
|
272
|
+
const promptForChoice = async ({ message, options, flags = {} }) => {
|
|
273
|
+
if (useStyledPrompts(flags)) {
|
|
274
|
+
const selected = await prompts.select({
|
|
275
|
+
message,
|
|
276
|
+
options: options.map((option) => ({
|
|
277
|
+
value: option,
|
|
278
|
+
label: option.label,
|
|
279
|
+
hint: option.hint,
|
|
280
|
+
})),
|
|
281
|
+
});
|
|
282
|
+
return prompts.isCancel(selected)
|
|
283
|
+
? options.find((option) => option.command === "exit")
|
|
284
|
+
: selected;
|
|
285
|
+
}
|
|
264
286
|
const prompt = createInterface({
|
|
265
287
|
input: process.stdin,
|
|
266
288
|
output: process.stdout,
|
|
@@ -283,7 +305,15 @@ const promptForChoice = async ({ message, options }) => {
|
|
|
283
305
|
}
|
|
284
306
|
};
|
|
285
307
|
|
|
286
|
-
const promptForConfirmation = async (
|
|
308
|
+
const promptForConfirmation = async (
|
|
309
|
+
message,
|
|
310
|
+
flags = {},
|
|
311
|
+
{ cancelValue = false } = {},
|
|
312
|
+
) => {
|
|
313
|
+
if (useStyledPrompts(flags)) {
|
|
314
|
+
const accepted = await prompts.confirm({ message, initialValue: false });
|
|
315
|
+
return prompts.isCancel(accepted) ? cancelValue : accepted;
|
|
316
|
+
}
|
|
287
317
|
const prompt = createInterface({
|
|
288
318
|
input: process.stdin,
|
|
289
319
|
output: process.stdout,
|
|
@@ -298,7 +328,27 @@ const promptForConfirmation = async (message) => {
|
|
|
298
328
|
}
|
|
299
329
|
};
|
|
300
330
|
|
|
301
|
-
const promptForPath = async ({
|
|
331
|
+
const promptForPath = async ({
|
|
332
|
+
message,
|
|
333
|
+
defaultValue,
|
|
334
|
+
optional = false,
|
|
335
|
+
flags = {},
|
|
336
|
+
validate,
|
|
337
|
+
}) => {
|
|
338
|
+
if (useStyledPrompts(flags)) {
|
|
339
|
+
const answer = await prompts.text({
|
|
340
|
+
message,
|
|
341
|
+
placeholder: defaultValue ?? (optional ? "Leave blank for none" : ""),
|
|
342
|
+
defaultValue,
|
|
343
|
+
validate: (value) => {
|
|
344
|
+
const resolved = String(value ?? defaultValue ?? "").trim();
|
|
345
|
+
if (!optional && !resolved) return "Enter a value.";
|
|
346
|
+
return resolved && validate ? validate(resolved) : undefined;
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
if (prompts.isCancel(answer)) return undefined;
|
|
350
|
+
return String(answer || defaultValue || "").trim() || undefined;
|
|
351
|
+
}
|
|
302
352
|
const prompt = createInterface({
|
|
303
353
|
input: process.stdin,
|
|
304
354
|
output: process.stdout,
|
|
@@ -312,6 +362,11 @@ const promptForPath = async ({ message, defaultValue, optional = false }) => {
|
|
|
312
362
|
: " (required)";
|
|
313
363
|
const answer = (await prompt.question(`${message}${suffix}: `)).trim();
|
|
314
364
|
const value = answer || defaultValue || "";
|
|
365
|
+
const invalid = value && validate ? validate(value) : undefined;
|
|
366
|
+
if (invalid) {
|
|
367
|
+
console.log(invalid);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
315
370
|
if (value || optional) return value || undefined;
|
|
316
371
|
console.log("Enter a project-relative path.");
|
|
317
372
|
}
|
|
@@ -320,6 +375,11 @@ const promptForPath = async ({ message, defaultValue, optional = false }) => {
|
|
|
320
375
|
}
|
|
321
376
|
};
|
|
322
377
|
|
|
378
|
+
const promptForSelection = async ({ message, options, flags }) => {
|
|
379
|
+
const selected = await promptForChoice({ message, options, flags });
|
|
380
|
+
return selected?.value;
|
|
381
|
+
};
|
|
382
|
+
|
|
323
383
|
const firstExistingPath = async (candidates) => {
|
|
324
384
|
for (const candidate of candidates) {
|
|
325
385
|
if (await pathExists(join(projectRoot, candidate))) return candidate;
|
|
@@ -391,6 +451,7 @@ const inspectGuidedProject = async (planOptions) => {
|
|
|
391
451
|
baseline: Boolean(migrationSummary),
|
|
392
452
|
policy,
|
|
393
453
|
policyPath: relative(projectRoot, loaded.paths.sanitizationPolicy),
|
|
454
|
+
policyAbsolutePath: loaded.paths.sanitizationPolicy,
|
|
394
455
|
migrationSummary,
|
|
395
456
|
runtime: await pathExists(
|
|
396
457
|
join(loaded.paths.runtimeWorkdir, "baseline.json"),
|
|
@@ -398,6 +459,144 @@ const inspectGuidedProject = async (planOptions) => {
|
|
|
398
459
|
};
|
|
399
460
|
};
|
|
400
461
|
|
|
462
|
+
const reviewPolicyInteractively = async ({ state, flags }) => {
|
|
463
|
+
const { source, draft } = await readReviewablePolicyDraft(
|
|
464
|
+
state.policyAbsolutePath,
|
|
465
|
+
);
|
|
466
|
+
const totalColumns = draft.tables.reduce(
|
|
467
|
+
(total, table) => total + (table.columns?.length ?? 0),
|
|
468
|
+
0,
|
|
469
|
+
);
|
|
470
|
+
let reviewedColumns = 0;
|
|
471
|
+
if (useStyledPrompts(flags)) {
|
|
472
|
+
prompts.note(
|
|
473
|
+
[
|
|
474
|
+
`${formatCount(totalColumns, "column")} require explicit review.`,
|
|
475
|
+
"No row values are displayed or changed by this step.",
|
|
476
|
+
"The safest replacement-oriented choices appear first.",
|
|
477
|
+
].join("\n"),
|
|
478
|
+
"Review the script",
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const policy = await completePolicyDraft({
|
|
482
|
+
draft,
|
|
483
|
+
reviewColumn: async ({ table, column }) => {
|
|
484
|
+
reviewedColumns += 1;
|
|
485
|
+
const label = `${table}.${column} (${reviewedColumns}/${totalColumns})`;
|
|
486
|
+
const action = await promptForSelection({
|
|
487
|
+
message: `${label}: how should this value be handled?`,
|
|
488
|
+
flags,
|
|
489
|
+
options: [
|
|
490
|
+
{
|
|
491
|
+
label: "Replace with synthetic data",
|
|
492
|
+
hint: "Safest default",
|
|
493
|
+
value: "REPLACE WITH SYNTHETIC",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
label: "Pseudonymize consistently",
|
|
497
|
+
hint: "Stable links without original values",
|
|
498
|
+
value: "PSEUDONYMIZE",
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
label: "Derive a safe value",
|
|
502
|
+
value: "DERIVE",
|
|
503
|
+
},
|
|
504
|
+
{ label: "Exclude this field", value: "EXCLUDE" },
|
|
505
|
+
{
|
|
506
|
+
label: "Keep exactly",
|
|
507
|
+
hint: "Only for values explicitly safe to retain",
|
|
508
|
+
value: "KEEP EXACTLY",
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
});
|
|
512
|
+
if (!action) throw new Error("Policy review was cancelled.");
|
|
513
|
+
const generated = await promptForSelection({
|
|
514
|
+
message: `${label}: is this column always database-generated?`,
|
|
515
|
+
flags,
|
|
516
|
+
options: [
|
|
517
|
+
{ label: "No", value: "NEVER" },
|
|
518
|
+
{ label: "Yes", value: "ALWAYS" },
|
|
519
|
+
],
|
|
520
|
+
});
|
|
521
|
+
if (!generated) throw new Error("Policy review was cancelled.");
|
|
522
|
+
const identity = await promptForSelection({
|
|
523
|
+
message: `${label}: is this an identity column?`,
|
|
524
|
+
flags,
|
|
525
|
+
options: [
|
|
526
|
+
{ label: "No", value: "NO" },
|
|
527
|
+
{ label: "Yes", value: "YES" },
|
|
528
|
+
],
|
|
529
|
+
});
|
|
530
|
+
if (!identity) throw new Error("Policy review was cancelled.");
|
|
531
|
+
const hasForeignKey = await promptForConfirmation(
|
|
532
|
+
`${label}: does this column reference another table?`,
|
|
533
|
+
flags,
|
|
534
|
+
{ cancelValue: null },
|
|
535
|
+
);
|
|
536
|
+
if (hasForeignKey === null) {
|
|
537
|
+
throw new Error("Policy review was cancelled.");
|
|
538
|
+
}
|
|
539
|
+
let foreignKey = null;
|
|
540
|
+
if (hasForeignKey) {
|
|
541
|
+
const schema = await promptForPath({
|
|
542
|
+
message: `${label}: referenced schema`,
|
|
543
|
+
defaultValue: "public",
|
|
544
|
+
flags,
|
|
545
|
+
validate: (value) =>
|
|
546
|
+
/^[a-z][a-z0-9_]{0,62}$/u.test(value)
|
|
547
|
+
? undefined
|
|
548
|
+
: "Use a lowercase PostgreSQL identifier.",
|
|
549
|
+
});
|
|
550
|
+
const referencedTable = await promptForPath({
|
|
551
|
+
message: `${label}: referenced table`,
|
|
552
|
+
flags,
|
|
553
|
+
validate: (value) =>
|
|
554
|
+
/^[a-z][a-z0-9_]{0,62}$/u.test(value)
|
|
555
|
+
? undefined
|
|
556
|
+
: "Use a lowercase PostgreSQL identifier.",
|
|
557
|
+
});
|
|
558
|
+
const referencedColumn = await promptForPath({
|
|
559
|
+
message: `${label}: referenced column`,
|
|
560
|
+
flags,
|
|
561
|
+
validate: (value) =>
|
|
562
|
+
/^[a-z][a-z0-9_]{0,62}$/u.test(value)
|
|
563
|
+
? undefined
|
|
564
|
+
: "Use a lowercase PostgreSQL identifier.",
|
|
565
|
+
});
|
|
566
|
+
if (!schema || !referencedTable || !referencedColumn) {
|
|
567
|
+
throw new Error("Policy review was cancelled.");
|
|
568
|
+
}
|
|
569
|
+
foreignKey = {
|
|
570
|
+
schema,
|
|
571
|
+
table: referencedTable,
|
|
572
|
+
column: referencedColumn,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return { action, generated, identity, foreignKey };
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
const accepted = await promptForConfirmation(
|
|
579
|
+
`Save ${formatCount(totalColumns, "reviewed column")} and activate this policy?`,
|
|
580
|
+
flags,
|
|
581
|
+
);
|
|
582
|
+
if (!accepted) return false;
|
|
583
|
+
await applyReviewedPolicy({
|
|
584
|
+
path: state.policyAbsolutePath,
|
|
585
|
+
originalSource: source,
|
|
586
|
+
policy,
|
|
587
|
+
});
|
|
588
|
+
if (useStyledPrompts(flags)) {
|
|
589
|
+
prompts.log.success(
|
|
590
|
+
`${formatCount(totalColumns, "column")} reviewed; the policy is ready for baseline creation.`,
|
|
591
|
+
);
|
|
592
|
+
} else {
|
|
593
|
+
console.log(
|
|
594
|
+
`Reviewed policy saved: ${formatCount(totalColumns, "column")} classified.`,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return true;
|
|
598
|
+
};
|
|
599
|
+
|
|
401
600
|
const runGuidedHome = async ({ flags, planOptions }) => {
|
|
402
601
|
const state = await inspectGuidedProject(planOptions);
|
|
403
602
|
const marker = (complete) =>
|
|
@@ -407,89 +606,121 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
407
606
|
if (!state.node.supported) {
|
|
408
607
|
options.push({
|
|
409
608
|
label: "Show Node.js 24 setup instructions",
|
|
609
|
+
hint: "Required before Rehearsal can change this project",
|
|
410
610
|
command: "node-help",
|
|
411
611
|
});
|
|
412
612
|
} else if (state.config === "missing") {
|
|
413
613
|
options.push({
|
|
414
|
-
label: "Set
|
|
614
|
+
label: "Set the stage",
|
|
615
|
+
hint: "Create safe local configuration",
|
|
415
616
|
command: "setup-write",
|
|
416
617
|
});
|
|
417
|
-
} else {
|
|
418
|
-
options.push({ label: "Check readiness", command: "doctor" });
|
|
419
|
-
}
|
|
420
|
-
if (state.node.supported && state.baseline) {
|
|
618
|
+
} else if (state.baseline) {
|
|
421
619
|
options.push(
|
|
422
|
-
{ label: "Review the migration plan", command: "explain" },
|
|
423
620
|
{ label: "Run a rehearsal", command: "run" },
|
|
621
|
+
{ label: "Review the migration plan", command: "explain" },
|
|
424
622
|
{ label: "Show candidate migrations", command: "candidates" },
|
|
623
|
+
{ label: "Check readiness", command: "doctor" },
|
|
425
624
|
);
|
|
426
625
|
} else if (state.node.supported && state.config === "valid") {
|
|
427
626
|
if (state.policy === "missing") {
|
|
428
627
|
options.push({
|
|
429
|
-
label: "Prepare
|
|
628
|
+
label: "Prepare the script",
|
|
629
|
+
hint: "Create a reviewable policy draft",
|
|
430
630
|
command: "baseline-prepare",
|
|
431
631
|
});
|
|
432
632
|
} else if (state.policy === "needs-review") {
|
|
433
633
|
options.push({
|
|
434
|
-
label: "Review the
|
|
634
|
+
label: "Review the script",
|
|
635
|
+
hint: "Classify every column interactively",
|
|
435
636
|
command: "policy-review",
|
|
436
637
|
});
|
|
437
638
|
} else {
|
|
438
639
|
options.push({
|
|
439
|
-
label: "Create
|
|
640
|
+
label: "Create the baseline",
|
|
641
|
+
hint: "Activate reviewed local inputs",
|
|
440
642
|
command: "baseline-guide",
|
|
441
643
|
});
|
|
442
644
|
}
|
|
645
|
+
options.push({ label: "Check readiness", command: "doctor" });
|
|
646
|
+
} else {
|
|
647
|
+
options.push({ label: "Check readiness", command: "doctor" });
|
|
443
648
|
}
|
|
444
649
|
if (state.node.supported && state.runtime) {
|
|
445
|
-
options.push(
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
650
|
+
options.push({
|
|
651
|
+
label: "Manage the local runtime",
|
|
652
|
+
hint: "Status, verification, and cleanup",
|
|
653
|
+
command: "manage-runtime",
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (lastGuidedDetails) {
|
|
657
|
+
options.push({
|
|
658
|
+
label: "Show details from the last action",
|
|
659
|
+
command: "last-details",
|
|
660
|
+
});
|
|
452
661
|
}
|
|
453
662
|
options.push(
|
|
454
663
|
{ label: "Show all commands", command: "help" },
|
|
455
664
|
{ label: "Exit", command: "exit" },
|
|
456
665
|
);
|
|
457
666
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
667
|
+
const statusLines = [
|
|
668
|
+
`${marker(state.node.supported)} Node.js ${state.node.version}${state.node.supported ? "" : " (Node.js 24 required)"}`,
|
|
669
|
+
`${marker(state.detected.hasSupabaseConfig)} Supabase project detected`,
|
|
670
|
+
`${marker(state.detected.hasMigrations)} Migration history detected`,
|
|
671
|
+
`${marker(state.config === "valid")} Rehearsal configuration${state.config === "invalid" ? " needs attention" : ""}`,
|
|
672
|
+
...(state.config === "valid"
|
|
673
|
+
? [
|
|
674
|
+
`${marker(state.policy === "reviewed")} ${state.policy === "needs-review" ? "Baseline policy needs review" : state.policy === "reviewed" ? "Baseline policy reviewed" : "Baseline policy needed"}`,
|
|
675
|
+
]
|
|
676
|
+
: []),
|
|
677
|
+
`${marker(state.baseline)} Verified baseline`,
|
|
678
|
+
...(state.runtime ? [`${marker(true)} Disposable runtime created`] : []),
|
|
679
|
+
...(state.configError
|
|
680
|
+
? ["", terminalStyle(flags, "33", `! ${state.configError}`)]
|
|
681
|
+
: []),
|
|
682
|
+
];
|
|
683
|
+
const stage =
|
|
684
|
+
state.config !== "valid"
|
|
685
|
+
? 1
|
|
686
|
+
: state.policy !== "reviewed"
|
|
687
|
+
? 2
|
|
688
|
+
: !state.baseline
|
|
689
|
+
? 3
|
|
690
|
+
: 4;
|
|
691
|
+
if (useStyledPrompts(flags)) {
|
|
692
|
+
prompts.note(
|
|
693
|
+
statusLines.join("\n"),
|
|
694
|
+
`${state.detected.projectName} · Stage ${stage} of 4`,
|
|
695
|
+
);
|
|
696
|
+
} else {
|
|
697
|
+
console.log(
|
|
698
|
+
[
|
|
699
|
+
"",
|
|
700
|
+
terminalStyle(flags, "1;36", "REHEARSAL"),
|
|
701
|
+
"Safe local migration testing",
|
|
702
|
+
"",
|
|
703
|
+
terminalStyle(flags, "1", state.detected.projectName),
|
|
704
|
+
`Stage ${stage} of 4`,
|
|
705
|
+
...statusLines,
|
|
706
|
+
"",
|
|
707
|
+
"What would you like to do?",
|
|
708
|
+
"",
|
|
709
|
+
...options.map(
|
|
710
|
+
(option, index) =>
|
|
711
|
+
` ${index === 0 ? terminalStyle(flags, "36", "›") : " "} ${index + 1}. ${option.label}`,
|
|
712
|
+
),
|
|
713
|
+
"",
|
|
714
|
+
].join("\n"),
|
|
715
|
+
);
|
|
716
|
+
}
|
|
489
717
|
|
|
490
|
-
const selected = await promptForChoice({
|
|
718
|
+
const selected = await promptForChoice({
|
|
719
|
+
message: "What would you like to do?",
|
|
720
|
+
options,
|
|
721
|
+
flags,
|
|
722
|
+
});
|
|
491
723
|
if (selected.command === "exit") {
|
|
492
|
-
console.log("No changes made.");
|
|
493
724
|
return null;
|
|
494
725
|
}
|
|
495
726
|
if (selected.command === "node-help") {
|
|
@@ -506,20 +737,55 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
506
737
|
"Then reinstall Rehearsal in your scratch project and run npx rehearsal again.",
|
|
507
738
|
].join("\n"),
|
|
508
739
|
);
|
|
509
|
-
return
|
|
740
|
+
return "refresh";
|
|
510
741
|
}
|
|
511
742
|
if (selected.command === "policy-review") {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
"
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
743
|
+
try {
|
|
744
|
+
await reviewPolicyInteractively({ state, flags });
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (String(error?.message ?? error) !== "Policy review was cancelled.") {
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
if (useStyledPrompts(flags)) {
|
|
750
|
+
prompts.cancel("Policy review cancelled; the draft was not changed.");
|
|
751
|
+
} else {
|
|
752
|
+
console.log("Policy review cancelled; the draft was not changed.");
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return "refresh";
|
|
756
|
+
}
|
|
757
|
+
if (selected.command === "last-details") {
|
|
758
|
+
if (useStyledPrompts(flags)) {
|
|
759
|
+
prompts.note(lastGuidedDetails, "Technical details");
|
|
760
|
+
} else {
|
|
761
|
+
console.log(`\nTECHNICAL DETAILS\n${lastGuidedDetails}`);
|
|
762
|
+
}
|
|
763
|
+
return "refresh";
|
|
764
|
+
}
|
|
765
|
+
if (selected.command === "manage-runtime") {
|
|
766
|
+
const runtimeAction = await promptForChoice({
|
|
767
|
+
message: "Manage the disposable local runtime",
|
|
768
|
+
flags,
|
|
769
|
+
options: [
|
|
770
|
+
{ label: "Show status", command: "status" },
|
|
771
|
+
{ label: "Verify runtime", command: "verify" },
|
|
772
|
+
{ label: "Reset to the immutable baseline", command: "reset" },
|
|
773
|
+
{ label: "Stop runtime", command: "stop" },
|
|
774
|
+
{ label: "Discard runtime", command: "discard" },
|
|
775
|
+
{ label: "Back", command: "exit" },
|
|
776
|
+
],
|
|
777
|
+
});
|
|
778
|
+
if (!runtimeAction || runtimeAction.command === "exit") return "refresh";
|
|
779
|
+
if (["reset", "discard"].includes(runtimeAction.command)) {
|
|
780
|
+
const accepted = await promptForConfirmation(
|
|
781
|
+
runtimeAction.command === "reset"
|
|
782
|
+
? "Reset the disposable runtime and remove its current changes?"
|
|
783
|
+
: "Discard the disposable runtime? The immutable baseline will remain.",
|
|
784
|
+
flags,
|
|
785
|
+
);
|
|
786
|
+
if (!accepted) return "refresh";
|
|
787
|
+
}
|
|
788
|
+
return runtimeAction.command;
|
|
523
789
|
}
|
|
524
790
|
if (["baseline-guide", "baseline-prepare"].includes(selected.command)) {
|
|
525
791
|
console.log(
|
|
@@ -541,10 +807,12 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
541
807
|
flags.recordsPath = await promptForPath({
|
|
542
808
|
message: "Sanitized NDJSON records",
|
|
543
809
|
defaultValue: recordsDefault,
|
|
810
|
+
flags,
|
|
544
811
|
});
|
|
545
812
|
flags.ledgerPath = await promptForPath({
|
|
546
813
|
message: "Migration ledger",
|
|
547
814
|
defaultValue: ledgerDefault,
|
|
815
|
+
flags,
|
|
548
816
|
});
|
|
549
817
|
if (selected.command === "baseline-prepare") {
|
|
550
818
|
const preparationPlan = await planBaselinePreparation({
|
|
@@ -562,10 +830,11 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
562
830
|
console.log(`\n${renderBaselinePreparation(preview, flags)}\n`);
|
|
563
831
|
const accepted = await promptForConfirmation(
|
|
564
832
|
`Create the REVIEW REQUIRED draft at ${preview.destination}?`,
|
|
833
|
+
flags,
|
|
565
834
|
);
|
|
566
835
|
if (!accepted) {
|
|
567
836
|
console.log("No changes made.");
|
|
568
|
-
return
|
|
837
|
+
return "refresh";
|
|
569
838
|
}
|
|
570
839
|
flags.preparationPlan = preparationPlan;
|
|
571
840
|
flags.write = true;
|
|
@@ -574,6 +843,7 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
574
843
|
flags.assetsPath = await promptForPath({
|
|
575
844
|
message: "Storage asset manifest (optional)",
|
|
576
845
|
optional: true,
|
|
846
|
+
flags,
|
|
577
847
|
});
|
|
578
848
|
return "baseline create";
|
|
579
849
|
}
|
|
@@ -588,10 +858,11 @@ const runGuidedHome = async ({ flags, planOptions }) => {
|
|
|
588
858
|
console.log(`\n${renderSetup(preview)}\n`);
|
|
589
859
|
const accepted = await promptForConfirmation(
|
|
590
860
|
"Create these project-local files?",
|
|
861
|
+
flags,
|
|
591
862
|
);
|
|
592
863
|
if (!accepted) {
|
|
593
864
|
console.log("No changes made.");
|
|
594
|
-
return
|
|
865
|
+
return "refresh";
|
|
595
866
|
}
|
|
596
867
|
flags.setupPlan = setupPlan;
|
|
597
868
|
flags.write = true;
|
|
@@ -652,6 +923,7 @@ const prepareCandidateConfirmation = async ({
|
|
|
652
923
|
);
|
|
653
924
|
const accepted = await promptForConfirmation(
|
|
654
925
|
`Apply exactly ${summary.candidateCount === 1 ? "this migration" : "these migrations"}?`,
|
|
926
|
+
flags,
|
|
655
927
|
);
|
|
656
928
|
if (!accepted) {
|
|
657
929
|
console.log("No changes made.");
|
|
@@ -709,7 +981,10 @@ const runManager = ({ action, flags }) => {
|
|
|
709
981
|
discard: "Removing the disposable local runtime",
|
|
710
982
|
verify: "Verifying the current local runtime",
|
|
711
983
|
}[action];
|
|
712
|
-
|
|
984
|
+
const spinner = useStyledPrompts(flags) ? prompts.spinner() : null;
|
|
985
|
+
if (spinner) {
|
|
986
|
+
spinner.start(`${actionDescription}. This can take a moment.`);
|
|
987
|
+
} else if (isHumanTerminal(flags)) {
|
|
713
988
|
console.log(
|
|
714
989
|
`\n${terminalStyle(flags, "1;36", action === "run" ? "REHEARSING" : "REHEARSAL")}\n${terminalStyle(flags, "36", "→")} ${actionDescription}. This can take a moment.`,
|
|
715
990
|
);
|
|
@@ -721,8 +996,12 @@ const runManager = ({ action, flags }) => {
|
|
|
721
996
|
stdio: ["inherit", "pipe", "pipe"],
|
|
722
997
|
maxBuffer: 16 * 1024 * 1024,
|
|
723
998
|
});
|
|
724
|
-
if (result.error)
|
|
999
|
+
if (result.error) {
|
|
1000
|
+
if (spinner) spinner.stop(`${actionDescription} did not complete.`);
|
|
1001
|
+
throw result.error;
|
|
1002
|
+
}
|
|
725
1003
|
if (result.status !== 0) {
|
|
1004
|
+
if (spinner) spinner.stop(`${actionDescription} did not complete.`);
|
|
726
1005
|
const completeOutput = [
|
|
727
1006
|
String(result.stdout ?? ""),
|
|
728
1007
|
String(result.stderr ?? ""),
|
|
@@ -757,7 +1036,11 @@ const runManager = ({ action, flags }) => {
|
|
|
757
1036
|
});
|
|
758
1037
|
}
|
|
759
1038
|
const durationMs = performance.now() - runtimeStartedMs;
|
|
760
|
-
if (
|
|
1039
|
+
if (spinner) {
|
|
1040
|
+
spinner.stop(
|
|
1041
|
+
`${actionDescription} completed in ${formatDuration(durationMs)}.`,
|
|
1042
|
+
);
|
|
1043
|
+
} else if (isHumanTerminal(flags)) {
|
|
761
1044
|
console.log(
|
|
762
1045
|
`${terminalStyle(flags, "32", "✓")} Local runtime step completed in ${formatDuration(durationMs)}.`,
|
|
763
1046
|
);
|
|
@@ -913,7 +1196,9 @@ const runSetup = async ({ flags, planOptions }) => {
|
|
|
913
1196
|
readiness.state === "READY"
|
|
914
1197
|
? "Next: run rehearsal explain to review the migration plan."
|
|
915
1198
|
: needsBaselinePolicy
|
|
916
|
-
?
|
|
1199
|
+
? flags.guided
|
|
1200
|
+
? "Next: choose Prepare the script to create a reviewable policy draft."
|
|
1201
|
+
: "Next: run rehearsal again and choose Prepare a reviewable baseline policy draft."
|
|
917
1202
|
: "Next: complete the remaining readiness items shown above.",
|
|
918
1203
|
};
|
|
919
1204
|
};
|
|
@@ -998,9 +1283,16 @@ const runBaselinePreparation = async ({ flags, planOptions }) => {
|
|
|
998
1283
|
ledgerPath: flags.ledgerPath,
|
|
999
1284
|
}));
|
|
1000
1285
|
if (flags.write) await applyBaselinePreparation(plan);
|
|
1001
|
-
|
|
1286
|
+
const result = summarizeBaselinePreparation(plan, {
|
|
1002
1287
|
mode: flags.write ? "written" : "preview",
|
|
1003
1288
|
});
|
|
1289
|
+
return flags.guided && flags.write
|
|
1290
|
+
? {
|
|
1291
|
+
...result,
|
|
1292
|
+
nextAction:
|
|
1293
|
+
"Next: choose Review the script to classify every column interactively.",
|
|
1294
|
+
}
|
|
1295
|
+
: result;
|
|
1004
1296
|
};
|
|
1005
1297
|
|
|
1006
1298
|
const renderBaselinePreparation = (result, flags = {}) =>
|
|
@@ -1047,29 +1339,7 @@ Commands:
|
|
|
1047
1339
|
|
|
1048
1340
|
Options: --json --verbose --debug --plain --config=<path>`;
|
|
1049
1341
|
|
|
1050
|
-
const
|
|
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
|
-
}
|
|
1342
|
+
const executeCommand = async ({ command, flags, planOptions, guided }) => {
|
|
1073
1343
|
if (command === "help" || flags.help) {
|
|
1074
1344
|
console.log(usage());
|
|
1075
1345
|
return;
|
|
@@ -1109,7 +1379,9 @@ const main = async () => {
|
|
|
1109
1379
|
[
|
|
1110
1380
|
`Activated synthetic baseline ${baseline.generationId}: ${formatCount(baseline.rowCount, "row")} across ${formatCount(baseline.tableCount, "table")}; ${formatCount(baseline.migrationCount, "migration")} through ${baseline.migrationCutoff}.`,
|
|
1111
1381
|
"",
|
|
1112
|
-
|
|
1382
|
+
flags.guided
|
|
1383
|
+
? "Next: choose Run a rehearsal when you are ready."
|
|
1384
|
+
: "Next: run rehearsal doctor to check readiness.",
|
|
1113
1385
|
].join("\n"),
|
|
1114
1386
|
});
|
|
1115
1387
|
return;
|
|
@@ -1123,7 +1395,7 @@ const main = async () => {
|
|
|
1123
1395
|
render: renderDoctor,
|
|
1124
1396
|
status: data.state === "READY" ? "success" : "not_ready",
|
|
1125
1397
|
});
|
|
1126
|
-
if (data.state !== "READY") process.exitCode = 1;
|
|
1398
|
+
if (data.state !== "READY" && !guided) process.exitCode = 1;
|
|
1127
1399
|
return;
|
|
1128
1400
|
}
|
|
1129
1401
|
if (command === "explain" || (command === "run" && flags.dryRun)) {
|
|
@@ -1171,19 +1443,34 @@ const main = async () => {
|
|
|
1171
1443
|
const runtime = runManager({ action: command, flags });
|
|
1172
1444
|
let applicationProof;
|
|
1173
1445
|
if (command === "run") {
|
|
1174
|
-
|
|
1446
|
+
const proofSpinner = useStyledPrompts(flags) ? prompts.spinner() : null;
|
|
1447
|
+
if (proofSpinner) {
|
|
1448
|
+
proofSpinner.start("Running the project-owned application proof.");
|
|
1449
|
+
} else if (isHumanTerminal(flags)) {
|
|
1175
1450
|
console.log(
|
|
1176
1451
|
`${terminalStyle(flags, "36", "→")} Running the project-owned application proof.`,
|
|
1177
1452
|
);
|
|
1178
1453
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1454
|
+
try {
|
|
1455
|
+
applicationProof = await runApplicationProof(planOptions);
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
if (proofSpinner) proofSpinner.stop("Application proof failed.");
|
|
1458
|
+
throw error;
|
|
1459
|
+
}
|
|
1460
|
+
if (proofSpinner) {
|
|
1461
|
+
proofSpinner.stop("Application proof passed.");
|
|
1462
|
+
} else if (isHumanTerminal(flags)) {
|
|
1181
1463
|
console.log(
|
|
1182
1464
|
`${terminalStyle(flags, "32", "✓")} Application proof passed.`,
|
|
1183
1465
|
);
|
|
1184
1466
|
}
|
|
1185
1467
|
}
|
|
1186
1468
|
const data = command === "run" ? { runtime, applicationProof } : runtime;
|
|
1469
|
+
if (guided) {
|
|
1470
|
+
lastGuidedDetails = [runtime.output, applicationProof?.output]
|
|
1471
|
+
.filter(Boolean)
|
|
1472
|
+
.join("\n");
|
|
1473
|
+
}
|
|
1187
1474
|
emit({
|
|
1188
1475
|
command,
|
|
1189
1476
|
data,
|
|
@@ -1203,6 +1490,23 @@ const main = async () => {
|
|
|
1203
1490
|
verify:
|
|
1204
1491
|
"Next: continue testing, reset to the baseline, or stop the runtime.",
|
|
1205
1492
|
}[runtimeResult.action];
|
|
1493
|
+
if (guided) {
|
|
1494
|
+
const title =
|
|
1495
|
+
runtimeResult.action === "run"
|
|
1496
|
+
? "REHEARSAL PASSED"
|
|
1497
|
+
: "ACTION COMPLETE";
|
|
1498
|
+
return [
|
|
1499
|
+
title,
|
|
1500
|
+
`✓ ${runtimeResult.action} completed in ${formatDuration(runtimeResult.durationMs)}`,
|
|
1501
|
+
value.applicationProof
|
|
1502
|
+
? `✓ Application proof passed: ${value.applicationProof.command}`
|
|
1503
|
+
: null,
|
|
1504
|
+
"",
|
|
1505
|
+
"Technical output is available from Show details in the guide.",
|
|
1506
|
+
]
|
|
1507
|
+
.filter(Boolean)
|
|
1508
|
+
.join("\n");
|
|
1509
|
+
}
|
|
1206
1510
|
return [
|
|
1207
1511
|
runtimeResult.output ||
|
|
1208
1512
|
`Rehearsal ${runtimeResult.action} completed.`,
|
|
@@ -1221,6 +1525,63 @@ const main = async () => {
|
|
|
1221
1525
|
throw new Error(`Unknown Rehearsal command: ${command}.\n\n${usage()}`);
|
|
1222
1526
|
};
|
|
1223
1527
|
|
|
1528
|
+
const resetGuidedFlags = (flags) => {
|
|
1529
|
+
flags.dryRun = false;
|
|
1530
|
+
flags.write = false;
|
|
1531
|
+
flags.confirmation = undefined;
|
|
1532
|
+
flags.recordsPath = undefined;
|
|
1533
|
+
flags.ledgerPath = undefined;
|
|
1534
|
+
flags.assetsPath = undefined;
|
|
1535
|
+
flags.setupPlan = undefined;
|
|
1536
|
+
flags.preparationPlan = undefined;
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
const main = async () => {
|
|
1540
|
+
const { flags, positionals } = parseArguments(process.argv.slice(2));
|
|
1541
|
+
const command = positionals.join(" ") || "help";
|
|
1542
|
+
const planOptions = {
|
|
1543
|
+
projectRoot,
|
|
1544
|
+
configPath: flags.configPath,
|
|
1545
|
+
};
|
|
1546
|
+
const wantsAutomaticGuide =
|
|
1547
|
+
positionals.length === 0 &&
|
|
1548
|
+
!flags.help &&
|
|
1549
|
+
!flags.json &&
|
|
1550
|
+
process.stdin.isTTY &&
|
|
1551
|
+
process.stdout.isTTY;
|
|
1552
|
+
const guided = command === "guide" || wantsAutomaticGuide;
|
|
1553
|
+
if (!guided) {
|
|
1554
|
+
await executeCommand({ command, flags, planOptions, guided: false });
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1558
|
+
throw new Error(
|
|
1559
|
+
"The guided home screen requires an interactive terminal. Run rehearsal --help to list scriptable commands.",
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
flags.guided = true;
|
|
1563
|
+
if (useStyledPrompts(flags)) {
|
|
1564
|
+
prompts.intro("REHEARSAL · Safe local migration testing");
|
|
1565
|
+
}
|
|
1566
|
+
while (true) {
|
|
1567
|
+
resetGuidedFlags(flags);
|
|
1568
|
+
const selected = await runGuidedHome({ flags, planOptions });
|
|
1569
|
+
if (!selected) break;
|
|
1570
|
+
if (selected === "refresh") continue;
|
|
1571
|
+
await executeCommand({
|
|
1572
|
+
command: selected,
|
|
1573
|
+
flags,
|
|
1574
|
+
planOptions,
|
|
1575
|
+
guided: true,
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
if (useStyledPrompts(flags)) {
|
|
1579
|
+
prompts.outro("See you at the next rehearsal.");
|
|
1580
|
+
} else {
|
|
1581
|
+
console.log("See you at the next rehearsal.");
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1224
1585
|
try {
|
|
1225
1586
|
await main();
|
|
1226
1587
|
} catch (error) {
|