@rehearsal-db/core 0.1.0-beta.15 → 0.1.0-beta.16
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 +34 -1
- package/dist/package.json +10 -9
- package/dist/src/baseline/privacy_contract.d.mts +147 -0
- package/dist/src/baseline/privacy_contract.mjs +2 -0
- package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
- package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
- package/dist/src/baseline/privacy_engine.d.mts +5 -162
- package/dist/src/baseline/privacy_engine.mjs +4 -1162
- package/dist/src/baseline/privacy_key.d.mts +7 -0
- package/dist/src/baseline/privacy_key.mjs +42 -0
- package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
- package/dist/src/baseline/privacy_pipeline.mjs +127 -0
- package/dist/src/baseline/privacy_policy.d.mts +6 -0
- package/dist/src/baseline/privacy_policy.mjs +237 -0
- package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
- package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
- package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
- package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
- package/dist/src/baseline/privacy_transforms.d.mts +18 -0
- package/dist/src/baseline/privacy_transforms.mjs +142 -0
- package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
- package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
- package/dist/src/cli/application_command_handlers.d.mts +8 -0
- package/dist/src/cli/application_command_handlers.mjs +52 -0
- package/dist/src/cli/arguments.d.mts +0 -5
- package/dist/src/cli/arguments.mjs +0 -7
- package/dist/src/cli/basic_command_handlers.d.mts +4 -0
- package/dist/src/cli/basic_command_handlers.mjs +56 -0
- package/dist/src/cli/cli_commands.d.mts +14 -0
- package/dist/src/cli/cli_commands.mjs +32 -0
- package/dist/src/cli/command_contract.d.mts +57 -0
- package/dist/src/cli/command_contract.mjs +39 -0
- package/dist/src/cli/command_output.d.mts +12 -0
- package/dist/src/cli/command_output.mjs +20 -0
- package/dist/src/cli/command_registry.d.mts +7 -0
- package/dist/src/cli/command_registry.mjs +14 -0
- package/dist/src/cli/guided.d.mts +3 -1
- package/dist/src/cli/guided.mjs +13 -7
- package/dist/src/cli/identity_command_handlers.d.mts +8 -0
- package/dist/src/cli/identity_command_handlers.mjs +43 -0
- package/dist/src/cli/init_command.d.mts +1 -1
- package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
- package/dist/src/cli/inspection_command_handlers.mjs +79 -0
- package/dist/src/cli/project_command_handlers.d.mts +11 -0
- package/dist/src/cli/project_command_handlers.mjs +92 -0
- package/dist/src/cli/project_commands.d.mts +6 -4
- package/dist/src/cli/project_commands.mjs +6 -6
- package/dist/src/cli/rehearsal.d.mts +2 -2
- package/dist/src/cli/rehearsal.mjs +51 -567
- package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
- package/dist/src/cli/runtime_action_handlers.mjs +201 -0
- package/dist/src/cli/runtime_commands.d.mts +7 -3
- package/dist/src/cli/runtime_commands.mjs +10 -8
- package/dist/src/cli/source_command_handlers.d.mts +8 -0
- package/dist/src/cli/source_command_handlers.mjs +62 -0
- package/dist/src/cli/source_commands.d.mts +5 -1
- package/dist/src/cli/source_commands.mjs +3 -1
- package/dist/src/identity/claim.d.mts +5 -144
- package/dist/src/identity/claim.mjs +4 -947
- package/dist/src/identity/claim_contract.d.mts +135 -0
- package/dist/src/identity/claim_contract.mjs +6 -0
- package/dist/src/identity/claim_executor.d.mts +10 -0
- package/dist/src/identity/claim_executor.mjs +358 -0
- package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
- package/dist/src/identity/claim_identity_lookup.mjs +125 -0
- package/dist/src/identity/claim_plan.d.mts +8 -0
- package/dist/src/identity/claim_plan.mjs +61 -0
- package/dist/src/identity/claim_policy.d.mts +3 -0
- package/dist/src/identity/claim_policy.mjs +377 -0
- package/dist/src/identity/claim_sql.d.mts +10 -0
- package/dist/src/identity/claim_sql.mjs +46 -0
- package/dist/src/identity/claim_verification.d.mts +13 -0
- package/dist/src/identity/claim_verification.mjs +35 -0
- package/dist/src/index.d.mts +1 -1
- package/dist/src/project/configuration.d.mts +11 -179
- package/dist/src/project/configuration.mjs +7 -956
- package/dist/src/project/configuration_contract.d.mts +125 -0
- package/dist/src/project/configuration_contract.mjs +27 -0
- package/dist/src/project/configuration_discovery.d.mts +7 -0
- package/dist/src/project/configuration_discovery.mjs +37 -0
- package/dist/src/project/configuration_normalization.d.mts +107 -0
- package/dist/src/project/configuration_normalization.mjs +302 -0
- package/dist/src/project/configuration_rendering.d.mts +14 -0
- package/dist/src/project/configuration_rendering.mjs +224 -0
- package/dist/src/project/configuration_resolution.d.mts +125 -0
- package/dist/src/project/configuration_resolution.mjs +81 -0
- package/dist/src/project/configuration_schema.d.mts +22 -0
- package/dist/src/project/configuration_schema.mjs +160 -0
- package/dist/src/project/configuration_validation.d.mts +22 -0
- package/dist/src/project/configuration_validation.mjs +136 -0
- package/dist/src/project/project_detection.d.mts +14 -0
- package/dist/src/project/project_detection.mjs +47 -0
- package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
- package/dist/src/runtime/lifecycle_engine.mjs +114 -0
- package/dist/src/runtime/plan.mjs +2 -1
- package/dist/src/targets/postgresql.mjs +142 -163
- package/dist/src/targets/supabase.mjs +142 -174
- package/docs/architecture.md +118 -3
- package/docs/releasing.md +8 -3
- package/docs/typescript.md +57 -10
- package/package.json +10 -9
- package/types/configuration.d.mts +5 -164
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Redacted primitive validators and closed vocabulary for privacy policy v2. */
|
|
2
|
+
export const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
3
|
+
export const BINDING_NAME = /^[a-z][a-z0-9-]{0,62}$/u;
|
|
4
|
+
export const ENVIRONMENT_KEY = /^[A-Z][A-Z0-9_]*$/u;
|
|
5
|
+
export const HEX_64 = /^[a-f0-9]{64}$/u;
|
|
6
|
+
export const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu;
|
|
7
|
+
export const PORTABLE_CODE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
|
|
8
|
+
export const ACTIONS = new Set([
|
|
9
|
+
"KEEP",
|
|
10
|
+
"PSEUDONYMIZE",
|
|
11
|
+
"REPLACE",
|
|
12
|
+
"EXCLUDE",
|
|
13
|
+
"DERIVE",
|
|
14
|
+
]);
|
|
15
|
+
export const PSEUDONYM_FORMATS = new Set([
|
|
16
|
+
"uuid",
|
|
17
|
+
"email",
|
|
18
|
+
"text",
|
|
19
|
+
"integer",
|
|
20
|
+
"hex",
|
|
21
|
+
"gtin",
|
|
22
|
+
"url",
|
|
23
|
+
]);
|
|
24
|
+
export const HEX_LENGTHS = new Set([32, 64]);
|
|
25
|
+
export const GTIN_LENGTHS = new Set([8, 12, 13, 14]);
|
|
26
|
+
export const DATE_REPRESENTATIONS = new Set([
|
|
27
|
+
"iso-string",
|
|
28
|
+
"epoch-milliseconds",
|
|
29
|
+
]);
|
|
30
|
+
export const DERIVATION_KINDS = new Set([
|
|
31
|
+
"date-shift",
|
|
32
|
+
"digest",
|
|
33
|
+
"enum",
|
|
34
|
+
"validated-string",
|
|
35
|
+
"path-map",
|
|
36
|
+
"json-object",
|
|
37
|
+
"json-array",
|
|
38
|
+
"json-union",
|
|
39
|
+
"json-dictionary",
|
|
40
|
+
"approved-owner",
|
|
41
|
+
"binding-substitute",
|
|
42
|
+
]);
|
|
43
|
+
export const URL_PATH_PREFIX = "/rehearsal/";
|
|
44
|
+
export const URL_TOKEN_LENGTH = 32;
|
|
45
|
+
export const MAXIMUM_URL_LENGTH = 2_048;
|
|
46
|
+
export const MAXIMUM_TEXT_SUBSTITUTION_BYTES = 65_536;
|
|
47
|
+
export const LOOPBACK_HOSTNAMES = new Set([
|
|
48
|
+
"127.0.0.1",
|
|
49
|
+
"::1",
|
|
50
|
+
"[::1]",
|
|
51
|
+
"localhost",
|
|
52
|
+
]);
|
|
53
|
+
export const DEFAULT_PATH_MAXIMUM_BYTES = 2_048;
|
|
54
|
+
export const DEFAULT_PATH_MAXIMUM_SEGMENTS = 32;
|
|
55
|
+
export const MAXIMUM_POLICY_DEPTH = 16;
|
|
56
|
+
export const MAXIMUM_ENUM_VALUES = 128;
|
|
57
|
+
export const MAXIMUM_ENUM_STRING_BYTES = 256;
|
|
58
|
+
export const isPrivacyObject = (value) => value !== null &&
|
|
59
|
+
typeof value === "object" &&
|
|
60
|
+
!Array.isArray(value) &&
|
|
61
|
+
Object.getPrototypeOf(value) === Object.prototype;
|
|
62
|
+
export const assertKnownKeys = (value, allowed, label) => {
|
|
63
|
+
for (const key of Object.keys(value)) {
|
|
64
|
+
if (!allowed.includes(key))
|
|
65
|
+
throw new Error(`${label}.${key} is unknown.`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
export const validateIdentifier = (value, label) => {
|
|
69
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value)) {
|
|
70
|
+
throw new Error(`${label} must be a lowercase PostgreSQL identifier.`);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
};
|
|
74
|
+
export const validateNonEmpty = (value, label) => {
|
|
75
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
76
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
77
|
+
}
|
|
78
|
+
return value.trim();
|
|
79
|
+
};
|
|
80
|
+
export const validatePositiveInteger = (value, label, maximum = Number.MAX_SAFE_INTEGER) => {
|
|
81
|
+
if (typeof value !== "number" ||
|
|
82
|
+
!Number.isSafeInteger(value) ||
|
|
83
|
+
value < 1 ||
|
|
84
|
+
value > maximum) {
|
|
85
|
+
throw new Error(`${label} must be an integer from 1 through ${maximum}.`);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
};
|
|
89
|
+
export const validateReviewedScalar = (value, label) => {
|
|
90
|
+
if (typeof value === "string") {
|
|
91
|
+
if (Buffer.byteLength(value) > MAXIMUM_ENUM_STRING_BYTES) {
|
|
92
|
+
throw new Error(`${label} must contain at most ${MAXIMUM_ENUM_STRING_BYTES} UTF-8 bytes.`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === "boolean")
|
|
97
|
+
return value;
|
|
98
|
+
if (typeof value === "number" &&
|
|
99
|
+
Number.isFinite(value) &&
|
|
100
|
+
!Object.is(value, -0)) {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`${label} must be a JSON string, finite number, or boolean.`);
|
|
104
|
+
};
|
|
105
|
+
export const validateHttpsLoopbackOrigin = (value, label) => {
|
|
106
|
+
if (typeof value !== "string" || value.trim() !== value || !value) {
|
|
107
|
+
throw new Error(`${label} must be a safe HTTPS loopback origin.`);
|
|
108
|
+
}
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = new URL(value);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new Error(`${label} must be a safe HTTPS loopback origin.`);
|
|
115
|
+
}
|
|
116
|
+
if (parsed.protocol !== "https:" ||
|
|
117
|
+
!LOOPBACK_HOSTNAMES.has(parsed.hostname) ||
|
|
118
|
+
![parsed.origin, `${parsed.origin}/`].includes(value) ||
|
|
119
|
+
parsed.username ||
|
|
120
|
+
parsed.password ||
|
|
121
|
+
(parsed.pathname !== "/" && parsed.pathname !== "") ||
|
|
122
|
+
parsed.search ||
|
|
123
|
+
parsed.hash) {
|
|
124
|
+
throw new Error(`${label} must be a safe HTTPS loopback origin.`);
|
|
125
|
+
}
|
|
126
|
+
return parsed.origin;
|
|
127
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Persistent application-session command handler. */
|
|
2
|
+
import type { CommandDefinition } from "./command_contract.mjs";
|
|
3
|
+
import type { createRuntimeCommands } from "./runtime_commands.mjs";
|
|
4
|
+
type RuntimeCommands = ReturnType<typeof createRuntimeCommands>;
|
|
5
|
+
export declare const createApplicationCommandHandlers: ({ openRuntimeApplication, }: {
|
|
6
|
+
openRuntimeApplication: RuntimeCommands["openRuntimeApplication"];
|
|
7
|
+
}) => readonly CommandDefinition[];
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Persistent application-session command handler. */
|
|
2
|
+
import * as prompts from "@clack/prompts";
|
|
3
|
+
import { exactCommands, renderedCommand } from "./command_contract.mjs";
|
|
4
|
+
import { useStyledPrompts } from "./terminal.mjs";
|
|
5
|
+
export const createApplicationCommandHandlers = ({ openRuntimeApplication, }) => [
|
|
6
|
+
exactCommands("open", ["open"], async ({ flags, planOptions, guided, state }) => {
|
|
7
|
+
if (flags.dryRun) {
|
|
8
|
+
throw new Error("--dry-run is supported only by rehearsal run.");
|
|
9
|
+
}
|
|
10
|
+
const data = await openRuntimeApplication({
|
|
11
|
+
flags,
|
|
12
|
+
planOptions,
|
|
13
|
+
session: state,
|
|
14
|
+
onReady: (ready) => {
|
|
15
|
+
const lines = [
|
|
16
|
+
`Application: ${ready.url}`,
|
|
17
|
+
"The verified database runtime stays running and keeps your local changes.",
|
|
18
|
+
guided
|
|
19
|
+
? "Press Ctrl+C to close the app and return here, or Ctrl+Z to exit Rehearsal."
|
|
20
|
+
: "Press Ctrl+C or Ctrl+Z to close the app. Rehearsal will leave the database runtime running.",
|
|
21
|
+
];
|
|
22
|
+
if (useStyledPrompts(flags)) {
|
|
23
|
+
prompts.note(lines.join("\n"), "SANDBOX READY");
|
|
24
|
+
}
|
|
25
|
+
else if (!flags.json) {
|
|
26
|
+
console.log(["", "SANDBOX READY", ...lines, ""].join("\n"));
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (guided && data.application.stoppedBy === "SIGTSTP") {
|
|
31
|
+
flags.exitGuidedSession = true;
|
|
32
|
+
}
|
|
33
|
+
if (guided) {
|
|
34
|
+
state.lastGuidedDetails = [
|
|
35
|
+
data.started.output,
|
|
36
|
+
data.verified.output,
|
|
37
|
+
...data.preparations.map((preparation) => preparation.output),
|
|
38
|
+
]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|
|
42
|
+
return renderedCommand({
|
|
43
|
+
data,
|
|
44
|
+
render: (result) => [
|
|
45
|
+
"SANDBOX APP CLOSED",
|
|
46
|
+
`Application stopped after ${result.application.stoppedBy}.`,
|
|
47
|
+
"The database runtime and its local database and Storage changes were preserved.",
|
|
48
|
+
"Run rehearsal open to return, rehearsal verify to check it, or rehearsal stop to stop the databases.",
|
|
49
|
+
].join("\n"),
|
|
50
|
+
});
|
|
51
|
+
}),
|
|
52
|
+
];
|
|
@@ -22,13 +22,8 @@ export interface RehearsalCliFlags {
|
|
|
22
22
|
refreshConfirmation: string | undefined;
|
|
23
23
|
identityConfirmation: string | undefined;
|
|
24
24
|
identityName: string | undefined;
|
|
25
|
-
runtimeTopology: unknown | undefined;
|
|
26
|
-
targetConfirmations: unknown | undefined;
|
|
27
25
|
exitGuidedSession: boolean;
|
|
28
26
|
guided: boolean;
|
|
29
|
-
setupPlan?: unknown;
|
|
30
|
-
preparationPlan?: unknown;
|
|
31
|
-
cleanupPlan?: unknown;
|
|
32
27
|
}
|
|
33
28
|
export interface ParsedArguments {
|
|
34
29
|
flags: RehearsalCliFlags;
|
|
@@ -22,8 +22,6 @@ export const parseArguments = (arguments_) => {
|
|
|
22
22
|
refreshConfirmation: undefined,
|
|
23
23
|
identityConfirmation: undefined,
|
|
24
24
|
identityName: undefined,
|
|
25
|
-
runtimeTopology: undefined,
|
|
26
|
-
targetConfirmations: undefined,
|
|
27
25
|
exitGuidedSession: false,
|
|
28
26
|
guided: false,
|
|
29
27
|
};
|
|
@@ -97,12 +95,7 @@ export const resetGuidedFlags = (flags) => {
|
|
|
97
95
|
refreshConfirmation: undefined,
|
|
98
96
|
identityConfirmation: undefined,
|
|
99
97
|
identityName: undefined,
|
|
100
|
-
runtimeTopology: undefined,
|
|
101
|
-
targetConfirmations: undefined,
|
|
102
98
|
exitGuidedSession: false,
|
|
103
|
-
setupPlan: undefined,
|
|
104
|
-
preparationPlan: undefined,
|
|
105
|
-
cleanupPlan: undefined,
|
|
106
99
|
});
|
|
107
100
|
};
|
|
108
101
|
export const commandMutatesProject = ({ command, flags, guided, }) => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Stable version and help handlers with no project-side effects. */
|
|
2
|
+
import { REHEARSAL_VERSION } from "../shared/diagnostics.mjs";
|
|
3
|
+
import { exactCommands, textCommand } from "./command_contract.mjs";
|
|
4
|
+
export const usage = () => `Usage: rehearsal <command> [options]
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
version Print the installed Rehearsal version
|
|
8
|
+
guide Open the interactive, state-aware home screen
|
|
9
|
+
setup [--target=] [--write] Preview or create safe first-run scaffolding
|
|
10
|
+
init [--write] Preview or explicitly write safe starter config
|
|
11
|
+
baseline prepare --records= --ledger= [--write] Create a fail-closed policy draft
|
|
12
|
+
baseline create --records= --ledger= [--assets=] Create a baseline from safe local inputs
|
|
13
|
+
privacy key [--write] Preview or create the owner-only pseudonym key
|
|
14
|
+
source plan Preview exact, temporary source access
|
|
15
|
+
source apply --confirm-source-access= Apply the reviewed source-access plan
|
|
16
|
+
source retire [--confirm-source-retirement=] Preview or retire exact source access
|
|
17
|
+
baseline refresh Replace only the baseline from a reviewed source
|
|
18
|
+
refresh [--confirm-refresh=] Replace the database copy, reset locally, and remove old copies
|
|
19
|
+
identity plan --identity= Preview an approved local identity association
|
|
20
|
+
identity claim --identity= --confirm-identity= Apply the exact local association
|
|
21
|
+
doctor Check whether Rehearsal is safe and ready
|
|
22
|
+
support Print a safe, copy-ready support report
|
|
23
|
+
explain Show the immutable execution plan
|
|
24
|
+
run --dry-run Alias the exact explain plan without mutations
|
|
25
|
+
run --confirm-candidates= Execute reset, migration, and verification locally
|
|
26
|
+
open Start the runtime and keep the application open
|
|
27
|
+
candidates Show the exact pending migration digest
|
|
28
|
+
inspect baseline Show verified baseline provenance
|
|
29
|
+
inspect migrations Classify represented, applied, and candidate migrations
|
|
30
|
+
start Start an existing verified local runtime
|
|
31
|
+
migrate --confirm-candidates= Apply the exact candidate suffix without resetting
|
|
32
|
+
reset Restore and verify the immutable local baseline
|
|
33
|
+
status Report the disposable local runtime state
|
|
34
|
+
stop Stop only this project's local runtime
|
|
35
|
+
discard Remove only this project's disposable runtime
|
|
36
|
+
cleanup [--include-runtime] [--include-images] [--write] [--confirm-cleanup=]
|
|
37
|
+
Preview or apply conservative disk cleanup
|
|
38
|
+
verify Verify the current local Rehearsal runtime
|
|
39
|
+
|
|
40
|
+
Options: --help, -h --version, -V --json --verbose --debug --plain
|
|
41
|
+
--config=<path> --target=supabase|postgresql
|
|
42
|
+
--confirm-source-access=<digest> --confirm-source-retirement=<digest>
|
|
43
|
+
--confirm-refresh=<digest>`;
|
|
44
|
+
export const createBasicCommandHandlers = () => [
|
|
45
|
+
{
|
|
46
|
+
id: "version",
|
|
47
|
+
matches: ({ command, flags }) => command === "version" || flags.version,
|
|
48
|
+
handle: async ({ flags }) => textCommand(flags.json
|
|
49
|
+
? JSON.stringify({ name: "@rehearsal-db/core", version: REHEARSAL_VERSION }, null, 2)
|
|
50
|
+
: REHEARSAL_VERSION),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
...exactCommands("help", ["help"], async () => textCommand(usage())),
|
|
54
|
+
matches: ({ command, flags }) => command === "help" || flags.help,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Assemble the CLI command families into one ordered typed registry. */
|
|
2
|
+
import type { createProjectCommands } from "./project_commands.mjs";
|
|
3
|
+
import type { createRuntimeCommands } from "./runtime_commands.mjs";
|
|
4
|
+
import type { createRefreshWorkflow } from "./source_commands.mjs";
|
|
5
|
+
type ProjectCommands = ReturnType<typeof createProjectCommands>;
|
|
6
|
+
type RuntimeCommands = ReturnType<typeof createRuntimeCommands>;
|
|
7
|
+
type RefreshWorkflow = ReturnType<typeof createRefreshWorkflow>;
|
|
8
|
+
export declare const createCliCommandRegistry: ({ projectRoot, projectCommands, runtimeCommands, runRefreshWorkflow, }: {
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
projectCommands: ProjectCommands;
|
|
11
|
+
runtimeCommands: RuntimeCommands;
|
|
12
|
+
runRefreshWorkflow: RefreshWorkflow;
|
|
13
|
+
}) => import("./command_registry.mjs").CommandRegistry;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Assemble the CLI command families into one ordered typed registry. */
|
|
2
|
+
import { createApplicationCommandHandlers } from "./application_command_handlers.mjs";
|
|
3
|
+
import { createBasicCommandHandlers } from "./basic_command_handlers.mjs";
|
|
4
|
+
import { createCommandRegistry } from "./command_registry.mjs";
|
|
5
|
+
import { createIdentityCommandHandlers } from "./identity_command_handlers.mjs";
|
|
6
|
+
import { createInspectionCommandHandlers } from "./inspection_command_handlers.mjs";
|
|
7
|
+
import { createProjectCommandHandlers } from "./project_command_handlers.mjs";
|
|
8
|
+
import { createRuntimeActionHandlers } from "./runtime_action_handlers.mjs";
|
|
9
|
+
import { createSourceCommandHandlers } from "./source_command_handlers.mjs";
|
|
10
|
+
export const createCliCommandRegistry = ({ projectRoot, projectCommands, runtimeCommands, runRefreshWorkflow, }) => createCommandRegistry([
|
|
11
|
+
...createBasicCommandHandlers(),
|
|
12
|
+
...createProjectCommandHandlers({
|
|
13
|
+
projectRoot,
|
|
14
|
+
runSetup: projectCommands.runSetup,
|
|
15
|
+
runBaselinePreparation: projectCommands.runBaselinePreparation,
|
|
16
|
+
runCleanup: projectCommands.runCleanup,
|
|
17
|
+
}),
|
|
18
|
+
...createSourceCommandHandlers({ runRefreshWorkflow }),
|
|
19
|
+
...createIdentityCommandHandlers({
|
|
20
|
+
loadIdentityClaimPlan: projectCommands.loadIdentityClaimPlan,
|
|
21
|
+
}),
|
|
22
|
+
...createInspectionCommandHandlers(),
|
|
23
|
+
...createApplicationCommandHandlers({
|
|
24
|
+
openRuntimeApplication: runtimeCommands.openRuntimeApplication,
|
|
25
|
+
}),
|
|
26
|
+
...createRuntimeActionHandlers({
|
|
27
|
+
prepareCandidateConfirmation: runtimeCommands.prepareCandidateConfirmation,
|
|
28
|
+
prepareDependentTargets: runtimeCommands.prepareDependentTargets,
|
|
29
|
+
proveRuntimeStack: runtimeCommands.proveRuntimeStack,
|
|
30
|
+
runRuntimeStack: runtimeCommands.runRuntimeStack,
|
|
31
|
+
}),
|
|
32
|
+
]);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Typed contracts shared by CLI command handlers and the process entry point. */
|
|
2
|
+
import type { RehearsalConfigPathOptions } from "../project/configuration.mjs";
|
|
3
|
+
import type { RehearsalStatus } from "../shared/diagnostics.mjs";
|
|
4
|
+
import type { RehearsalCliFlags, RehearsalVerbosity } from "./arguments.mjs";
|
|
5
|
+
export interface CliSessionState {
|
|
6
|
+
lastGuidedDetails: string | undefined;
|
|
7
|
+
runtimeTopology: unknown | undefined;
|
|
8
|
+
targetConfirmations: unknown | undefined;
|
|
9
|
+
setupPlan: unknown | undefined;
|
|
10
|
+
preparationPlan: unknown | undefined;
|
|
11
|
+
cleanupPlan: unknown | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare const createCliSessionState: () => CliSessionState;
|
|
14
|
+
export declare const resetCommandSessionState: (state: CliSessionState) => void;
|
|
15
|
+
export interface CommandContext {
|
|
16
|
+
readonly command: string;
|
|
17
|
+
readonly flags: RehearsalCliFlags;
|
|
18
|
+
readonly planOptions: RehearsalConfigPathOptions;
|
|
19
|
+
readonly guided: boolean;
|
|
20
|
+
readonly state: CliSessionState;
|
|
21
|
+
}
|
|
22
|
+
export interface RenderedCommandResult {
|
|
23
|
+
readonly kind: "rendered";
|
|
24
|
+
readonly data: unknown;
|
|
25
|
+
readonly status: RehearsalStatus;
|
|
26
|
+
readonly render: (verbosity: RehearsalVerbosity) => string;
|
|
27
|
+
readonly exitCode?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface TextCommandResult {
|
|
30
|
+
readonly kind: "text";
|
|
31
|
+
readonly output: string;
|
|
32
|
+
readonly exitCode?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface SilentCommandResult {
|
|
35
|
+
readonly kind: "silent";
|
|
36
|
+
readonly exitCode?: number;
|
|
37
|
+
}
|
|
38
|
+
export type CommandResult = RenderedCommandResult | TextCommandResult | SilentCommandResult;
|
|
39
|
+
export type CommandHandler = (context: CommandContext) => Promise<CommandResult>;
|
|
40
|
+
export interface CommandDefinition {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly matches: (context: CommandContext) => boolean;
|
|
43
|
+
readonly handle: CommandHandler;
|
|
44
|
+
}
|
|
45
|
+
export declare const renderedCommand: <Data>({ data, render, status, exitCode, }: {
|
|
46
|
+
data: Data;
|
|
47
|
+
render: (data: Data, verbosity: RehearsalVerbosity) => string;
|
|
48
|
+
status?: RehearsalStatus;
|
|
49
|
+
exitCode?: number;
|
|
50
|
+
}) => RenderedCommandResult;
|
|
51
|
+
export declare const textCommand: (output: string, options?: {
|
|
52
|
+
exitCode?: number;
|
|
53
|
+
}) => TextCommandResult;
|
|
54
|
+
export declare const silentCommand: (options?: {
|
|
55
|
+
exitCode?: number;
|
|
56
|
+
}) => SilentCommandResult;
|
|
57
|
+
export declare const exactCommands: (id: string, commands: readonly string[], handle: CommandHandler) => CommandDefinition;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Typed contracts shared by CLI command handlers and the process entry point. */
|
|
2
|
+
export const createCliSessionState = () => ({
|
|
3
|
+
lastGuidedDetails: undefined,
|
|
4
|
+
runtimeTopology: undefined,
|
|
5
|
+
targetConfirmations: undefined,
|
|
6
|
+
setupPlan: undefined,
|
|
7
|
+
preparationPlan: undefined,
|
|
8
|
+
cleanupPlan: undefined,
|
|
9
|
+
});
|
|
10
|
+
export const resetCommandSessionState = (state) => {
|
|
11
|
+
Object.assign(state, {
|
|
12
|
+
runtimeTopology: undefined,
|
|
13
|
+
targetConfirmations: undefined,
|
|
14
|
+
setupPlan: undefined,
|
|
15
|
+
preparationPlan: undefined,
|
|
16
|
+
cleanupPlan: undefined,
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
export const renderedCommand = ({ data, render, status = "success", exitCode, }) => ({
|
|
20
|
+
kind: "rendered",
|
|
21
|
+
data,
|
|
22
|
+
status,
|
|
23
|
+
render: (verbosity) => render(data, verbosity),
|
|
24
|
+
...(exitCode === undefined ? {} : { exitCode }),
|
|
25
|
+
});
|
|
26
|
+
export const textCommand = (output, options = {}) => ({
|
|
27
|
+
kind: "text",
|
|
28
|
+
output,
|
|
29
|
+
...(options.exitCode === undefined ? {} : { exitCode: options.exitCode }),
|
|
30
|
+
});
|
|
31
|
+
export const silentCommand = (options = {}) => ({
|
|
32
|
+
kind: "silent",
|
|
33
|
+
...(options.exitCode === undefined ? {} : { exitCode: options.exitCode }),
|
|
34
|
+
});
|
|
35
|
+
export const exactCommands = (id, commands, handle) => ({
|
|
36
|
+
id,
|
|
37
|
+
matches: ({ command }) => commands.includes(command),
|
|
38
|
+
handle,
|
|
39
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Emit the stable JSON envelope or human result for a typed command result. */
|
|
2
|
+
import type { RehearsalCliFlags } from "./arguments.mjs";
|
|
3
|
+
import type { CommandResult } from "./command_contract.mjs";
|
|
4
|
+
export declare const createCommandEmitter: ({ startedAt, startedMs, write, }: {
|
|
5
|
+
startedAt: Date;
|
|
6
|
+
startedMs: number;
|
|
7
|
+
write?: (output: string) => void;
|
|
8
|
+
}) => ({ command, flags, result, }: {
|
|
9
|
+
command: string;
|
|
10
|
+
flags: RehearsalCliFlags;
|
|
11
|
+
result: CommandResult;
|
|
12
|
+
}) => number | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Emit the stable JSON envelope or human result for a typed command result. */
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { createRehearsalResult } from "../shared/diagnostics.mjs";
|
|
4
|
+
export const createCommandEmitter = ({ startedAt, startedMs, write = console.log, }) => ({ command, flags, result, }) => {
|
|
5
|
+
if (result.kind === "text")
|
|
6
|
+
write(result.output);
|
|
7
|
+
if (result.kind === "rendered") {
|
|
8
|
+
const envelope = createRehearsalResult({
|
|
9
|
+
command,
|
|
10
|
+
status: result.status,
|
|
11
|
+
data: result.data,
|
|
12
|
+
startedAt,
|
|
13
|
+
durationMs: Math.round((performance.now() - startedMs) * 100) / 100,
|
|
14
|
+
});
|
|
15
|
+
write(flags.json
|
|
16
|
+
? JSON.stringify(envelope, null, 2)
|
|
17
|
+
: result.render(flags.verbosity));
|
|
18
|
+
}
|
|
19
|
+
return result.exitCode;
|
|
20
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Resolve one CLI request to exactly one typed command handler. */
|
|
2
|
+
import type { CommandContext, CommandDefinition, CommandResult } from "./command_contract.mjs";
|
|
3
|
+
export interface CommandRegistry {
|
|
4
|
+
readonly ids: readonly string[];
|
|
5
|
+
execute(context: CommandContext): Promise<CommandResult | null>;
|
|
6
|
+
}
|
|
7
|
+
export declare const createCommandRegistry: (definitions: readonly CommandDefinition[]) => CommandRegistry;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Resolve one CLI request to exactly one typed command handler. */
|
|
2
|
+
export const createCommandRegistry = (definitions) => {
|
|
3
|
+
const ids = definitions.map(({ id }) => id);
|
|
4
|
+
if (new Set(ids).size !== ids.length) {
|
|
5
|
+
throw new Error("CLI command registry IDs must be unique.");
|
|
6
|
+
}
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
ids: Object.freeze(ids),
|
|
9
|
+
async execute(context) {
|
|
10
|
+
const definition = definitions.find((candidate) => candidate.matches(context));
|
|
11
|
+
return definition ? definition.handle(context) : null;
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { RehearsalConfigPathOptions } from "../project/configuration.mjs";
|
|
5
5
|
import type { RehearsalCliFlags } from "./arguments.mjs";
|
|
6
|
+
import type { CliSessionState } from "./command_contract.mjs";
|
|
6
7
|
import type { RuntimeTopologyTarget } from "../runtime/topology.mjs";
|
|
7
8
|
import type { RehearsalCleanupPlan } from "../runtime/cleanup.mjs";
|
|
8
9
|
interface TopologySummary {
|
|
@@ -33,7 +34,7 @@ interface GuidedRefreshPlan {
|
|
|
33
34
|
readonly steps: readonly string[];
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
|
-
export declare const createGuidedHome: ({ projectRoot, topologyCandidateSummary, buildRefreshWorkflowPlan, planRuntimeStackCleanup, getLastGuidedDetails, }: {
|
|
37
|
+
export declare const createGuidedHome: ({ projectRoot, topologyCandidateSummary, buildRefreshWorkflowPlan, planRuntimeStackCleanup, getLastGuidedDetails, session, }: {
|
|
37
38
|
projectRoot: string;
|
|
38
39
|
topologyCandidateSummary: (options: RehearsalConfigPathOptions) => Promise<TopologySummary>;
|
|
39
40
|
buildRefreshWorkflowPlan: (options: RehearsalConfigPathOptions) => Promise<{
|
|
@@ -44,6 +45,7 @@ export declare const createGuidedHome: ({ projectRoot, topologyCandidateSummary,
|
|
|
44
45
|
planOptions: RehearsalConfigPathOptions;
|
|
45
46
|
}) => Promise<GuidedCleanupPlan>;
|
|
46
47
|
getLastGuidedDetails: () => string | undefined;
|
|
48
|
+
session: CliSessionState;
|
|
47
49
|
}) => ({ flags, planOptions, }: {
|
|
48
50
|
flags: RehearsalCliFlags;
|
|
49
51
|
planOptions: RehearsalConfigPathOptions;
|
package/dist/src/cli/guided.mjs
CHANGED
|
@@ -415,7 +415,7 @@ const showGuidedStatus = ({ state, options, flags, }) => {
|
|
|
415
415
|
].join("\n"));
|
|
416
416
|
}
|
|
417
417
|
};
|
|
418
|
-
const guideCleanup = async ({ flags, planOptions, planRuntimeStackCleanup, }) => {
|
|
418
|
+
const guideCleanup = async ({ flags, planOptions, planRuntimeStackCleanup, session, }) => {
|
|
419
419
|
const cleanupScope = await promptForChoice({
|
|
420
420
|
message: "What may Rehearsal include in the cleanup preview?",
|
|
421
421
|
flags,
|
|
@@ -444,7 +444,7 @@ const guideCleanup = async ({ flags, planOptions, planRuntimeStackCleanup, }) =>
|
|
|
444
444
|
flags.includeRuntime = ["runtime", "all"].includes(cleanupScope.command);
|
|
445
445
|
flags.includeImages = ["images", "all"].includes(cleanupScope.command);
|
|
446
446
|
const cleanupPlan = await planRuntimeStackCleanup({ flags, planOptions });
|
|
447
|
-
|
|
447
|
+
session.cleanupPlan = cleanupPlan;
|
|
448
448
|
console.log(`\n${renderCleanup({ mode: "preview", plan: cleanupPlan })}`);
|
|
449
449
|
const selectedCount = "targets" in cleanupPlan
|
|
450
450
|
? cleanupPlan.targets.reduce((total, target) => total +
|
|
@@ -465,7 +465,7 @@ const guideCleanup = async ({ flags, planOptions, planRuntimeStackCleanup, }) =>
|
|
|
465
465
|
flags.cleanupConfirmation = cleanupPlan.digest;
|
|
466
466
|
return "cleanup";
|
|
467
467
|
};
|
|
468
|
-
const guideBaseline = async ({ state, selected, flags, planOptions, projectRoot, }) => {
|
|
468
|
+
const guideBaseline = async ({ state, selected, flags, planOptions, projectRoot, session, }) => {
|
|
469
469
|
console.log([
|
|
470
470
|
"",
|
|
471
471
|
terminalStyle(flags, "1", "CREATE A SAFE BASELINE"),
|
|
@@ -526,7 +526,7 @@ const guideBaseline = async ({ state, selected, flags, planOptions, projectRoot,
|
|
|
526
526
|
console.log("No changes made.");
|
|
527
527
|
return "home";
|
|
528
528
|
}
|
|
529
|
-
|
|
529
|
+
session.preparationPlan = preparationPlan;
|
|
530
530
|
flags.write = true;
|
|
531
531
|
return "baseline prepare";
|
|
532
532
|
}
|
|
@@ -567,7 +567,7 @@ const guideBaseline = async ({ state, selected, flags, planOptions, projectRoot,
|
|
|
567
567
|
return "home";
|
|
568
568
|
}
|
|
569
569
|
};
|
|
570
|
-
export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildRefreshWorkflowPlan, planRuntimeStackCleanup, getLastGuidedDetails, }) => async ({ flags, planOptions, }) => {
|
|
570
|
+
export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildRefreshWorkflowPlan, planRuntimeStackCleanup, getLastGuidedDetails, session, }) => async ({ flags, planOptions, }) => {
|
|
571
571
|
const state = await inspectGuidedProject({
|
|
572
572
|
projectRoot,
|
|
573
573
|
planOptions,
|
|
@@ -659,7 +659,12 @@ export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildR
|
|
|
659
659
|
return runtimeAction.command;
|
|
660
660
|
}
|
|
661
661
|
if (selected.command === "cleanup-guide") {
|
|
662
|
-
return guideCleanup({
|
|
662
|
+
return guideCleanup({
|
|
663
|
+
flags,
|
|
664
|
+
planOptions,
|
|
665
|
+
planRuntimeStackCleanup,
|
|
666
|
+
session,
|
|
667
|
+
});
|
|
663
668
|
}
|
|
664
669
|
if (["baseline-guide", "baseline-prepare"].includes(selected.command)) {
|
|
665
670
|
return guideBaseline({
|
|
@@ -668,6 +673,7 @@ export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildR
|
|
|
668
673
|
flags,
|
|
669
674
|
planOptions,
|
|
670
675
|
projectRoot,
|
|
676
|
+
session,
|
|
671
677
|
});
|
|
672
678
|
}
|
|
673
679
|
if (selected.command !== "setup-write")
|
|
@@ -707,7 +713,7 @@ export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildR
|
|
|
707
713
|
console.log("No changes made.");
|
|
708
714
|
return "home";
|
|
709
715
|
}
|
|
710
|
-
|
|
716
|
+
session.setupPlan = setupPlan;
|
|
711
717
|
flags.write = true;
|
|
712
718
|
return "setup";
|
|
713
719
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Provider-neutral copied-account planning and application handlers. */
|
|
2
|
+
import type { CommandDefinition } from "./command_contract.mjs";
|
|
3
|
+
import type { createProjectCommands } from "./project_commands.mjs";
|
|
4
|
+
type ProjectCommands = ReturnType<typeof createProjectCommands>;
|
|
5
|
+
export declare const createIdentityCommandHandlers: ({ loadIdentityClaimPlan, }: {
|
|
6
|
+
loadIdentityClaimPlan: ProjectCommands["loadIdentityClaimPlan"];
|
|
7
|
+
}) => readonly CommandDefinition[];
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Provider-neutral copied-account planning and application handlers. */
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { parseEnv } from "node:util";
|
|
4
|
+
import { applyIdentityClaim } from "../identity/claim.mjs";
|
|
5
|
+
import { createSupabaseStorageTransfer } from "../identity/storage.mjs";
|
|
6
|
+
import { exactCommands, renderedCommand } from "./command_contract.mjs";
|
|
7
|
+
import { renderIdentityPlan } from "./renderers.mjs";
|
|
8
|
+
export const createIdentityCommandHandlers = ({ loadIdentityClaimPlan, }) => [
|
|
9
|
+
exactCommands("identity", ["identity plan", "identity claim"], async ({ command, flags, planOptions }) => {
|
|
10
|
+
const { loaded, plan } = await loadIdentityClaimPlan({
|
|
11
|
+
flags,
|
|
12
|
+
planOptions,
|
|
13
|
+
});
|
|
14
|
+
if (command === "identity plan" || !flags.identityConfirmation) {
|
|
15
|
+
const data = { mode: "preview", plan };
|
|
16
|
+
return renderedCommand({ data, render: renderIdentityPlan });
|
|
17
|
+
}
|
|
18
|
+
const runtimeEnvironment = parseEnv(await readFile(loaded.paths.applicationEnvironment, "utf8"));
|
|
19
|
+
if (!runtimeEnvironment.DATABASE_URL) {
|
|
20
|
+
throw new Error("The local runtime environment has no DATABASE_URL.");
|
|
21
|
+
}
|
|
22
|
+
const needsStorageTransfer = plan.identity.assets.some((asset) => asset.rewritePath);
|
|
23
|
+
if (needsStorageTransfer &&
|
|
24
|
+
(!runtimeEnvironment.SUPABASE_URL ||
|
|
25
|
+
!runtimeEnvironment.SUPABASE_SERVICE_ROLE_KEY)) {
|
|
26
|
+
throw new Error("Identity asset transfer requires local Supabase URL and service-role credentials.");
|
|
27
|
+
}
|
|
28
|
+
const storageTransfer = needsStorageTransfer
|
|
29
|
+
? createSupabaseStorageTransfer({
|
|
30
|
+
url: runtimeEnvironment.SUPABASE_URL,
|
|
31
|
+
serviceRoleKey: runtimeEnvironment.SUPABASE_SERVICE_ROLE_KEY,
|
|
32
|
+
})
|
|
33
|
+
: undefined;
|
|
34
|
+
const result = await applyIdentityClaim({
|
|
35
|
+
plan,
|
|
36
|
+
confirmation: flags.identityConfirmation,
|
|
37
|
+
connectionString: runtimeEnvironment.DATABASE_URL,
|
|
38
|
+
...(storageTransfer === undefined ? {} : { storageTransfer }),
|
|
39
|
+
});
|
|
40
|
+
const data = { mode: "written", plan, result };
|
|
41
|
+
return renderedCommand({ data, render: renderIdentityPlan });
|
|
42
|
+
}),
|
|
43
|
+
];
|
|
@@ -10,7 +10,7 @@ interface InitOptions {
|
|
|
10
10
|
export declare const runRehearsalInit: ({ projectRoot, flags }: InitOptions) => Promise<{
|
|
11
11
|
mode: string;
|
|
12
12
|
destination: string;
|
|
13
|
-
detected: import("../project/
|
|
13
|
+
detected: import("../project/project_detection.mjs").DetectedProject;
|
|
14
14
|
source: string;
|
|
15
15
|
availableOptions: {
|
|
16
16
|
path: string;
|