@rehearsal-db/core 0.1.0-beta.15 → 0.1.0-beta.17

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.
Files changed (113) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/dist/package.json +10 -9
  3. package/dist/src/application/interactive_task.d.mts +30 -0
  4. package/dist/src/application/interactive_task.mjs +83 -0
  5. package/dist/src/application/session.d.mts +1 -0
  6. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  7. package/dist/src/baseline/privacy_contract.mjs +2 -0
  8. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  9. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  10. package/dist/src/baseline/privacy_engine.d.mts +5 -162
  11. package/dist/src/baseline/privacy_engine.mjs +4 -1162
  12. package/dist/src/baseline/privacy_key.d.mts +7 -0
  13. package/dist/src/baseline/privacy_key.mjs +42 -0
  14. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  15. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  16. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  17. package/dist/src/baseline/privacy_policy.mjs +237 -0
  18. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  19. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  20. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  21. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  22. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  23. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  24. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  25. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  26. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  27. package/dist/src/cli/application_command_handlers.mjs +52 -0
  28. package/dist/src/cli/arguments.d.mts +0 -5
  29. package/dist/src/cli/arguments.mjs +2 -7
  30. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  31. package/dist/src/cli/basic_command_handlers.mjs +57 -0
  32. package/dist/src/cli/cli_commands.d.mts +14 -0
  33. package/dist/src/cli/cli_commands.mjs +34 -0
  34. package/dist/src/cli/command_contract.d.mts +57 -0
  35. package/dist/src/cli/command_contract.mjs +39 -0
  36. package/dist/src/cli/command_output.d.mts +12 -0
  37. package/dist/src/cli/command_output.mjs +20 -0
  38. package/dist/src/cli/command_registry.d.mts +7 -0
  39. package/dist/src/cli/command_registry.mjs +14 -0
  40. package/dist/src/cli/guided.d.mts +3 -1
  41. package/dist/src/cli/guided.mjs +21 -7
  42. package/dist/src/cli/identity_command_handlers.d.mts +12 -0
  43. package/dist/src/cli/identity_command_handlers.mjs +239 -0
  44. package/dist/src/cli/identity_renderer.d.mts +8 -0
  45. package/dist/src/cli/identity_renderer.mjs +33 -0
  46. package/dist/src/cli/init_command.d.mts +1 -1
  47. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  48. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  49. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  50. package/dist/src/cli/project_command_handlers.mjs +92 -0
  51. package/dist/src/cli/project_commands.d.mts +131 -5
  52. package/dist/src/cli/project_commands.mjs +17 -8
  53. package/dist/src/cli/rehearsal.d.mts +2 -2
  54. package/dist/src/cli/rehearsal.mjs +51 -567
  55. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  56. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  57. package/dist/src/cli/runtime_commands.d.mts +32 -3
  58. package/dist/src/cli/runtime_commands.mjs +33 -13
  59. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  60. package/dist/src/cli/source_command_handlers.mjs +62 -0
  61. package/dist/src/cli/source_commands.d.mts +5 -1
  62. package/dist/src/cli/source_commands.mjs +3 -1
  63. package/dist/src/identity/claim.d.mts +5 -144
  64. package/dist/src/identity/claim.mjs +4 -947
  65. package/dist/src/identity/claim_contract.d.mts +135 -0
  66. package/dist/src/identity/claim_contract.mjs +6 -0
  67. package/dist/src/identity/claim_executor.d.mts +10 -0
  68. package/dist/src/identity/claim_executor.mjs +358 -0
  69. package/dist/src/identity/claim_identity_lookup.d.mts +23 -0
  70. package/dist/src/identity/claim_identity_lookup.mjs +134 -0
  71. package/dist/src/identity/claim_plan.d.mts +8 -0
  72. package/dist/src/identity/claim_plan.mjs +61 -0
  73. package/dist/src/identity/claim_policy.d.mts +3 -0
  74. package/dist/src/identity/claim_policy.mjs +377 -0
  75. package/dist/src/identity/claim_sql.d.mts +10 -0
  76. package/dist/src/identity/claim_sql.mjs +46 -0
  77. package/dist/src/identity/claim_verification.d.mts +13 -0
  78. package/dist/src/identity/claim_verification.mjs +35 -0
  79. package/dist/src/identity/connect.d.mts +30 -0
  80. package/dist/src/identity/connect.mjs +111 -0
  81. package/dist/src/index.d.mts +1 -1
  82. package/dist/src/project/configuration.d.mts +11 -179
  83. package/dist/src/project/configuration.mjs +7 -956
  84. package/dist/src/project/configuration_contract.d.mts +125 -0
  85. package/dist/src/project/configuration_contract.mjs +27 -0
  86. package/dist/src/project/configuration_discovery.d.mts +7 -0
  87. package/dist/src/project/configuration_discovery.mjs +37 -0
  88. package/dist/src/project/configuration_normalization.d.mts +107 -0
  89. package/dist/src/project/configuration_normalization.mjs +302 -0
  90. package/dist/src/project/configuration_rendering.d.mts +14 -0
  91. package/dist/src/project/configuration_rendering.mjs +226 -0
  92. package/dist/src/project/configuration_resolution.d.mts +125 -0
  93. package/dist/src/project/configuration_resolution.mjs +81 -0
  94. package/dist/src/project/configuration_schema.d.mts +22 -0
  95. package/dist/src/project/configuration_schema.mjs +160 -0
  96. package/dist/src/project/configuration_validation.d.mts +22 -0
  97. package/dist/src/project/configuration_validation.mjs +136 -0
  98. package/dist/src/project/project_detection.d.mts +14 -0
  99. package/dist/src/project/project_detection.mjs +47 -0
  100. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  101. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  102. package/dist/src/runtime/plan.mjs +2 -1
  103. package/dist/src/targets/postgresql.mjs +142 -163
  104. package/dist/src/targets/supabase.mjs +142 -174
  105. package/docs/architecture.md +120 -3
  106. package/docs/commands.md +18 -15
  107. package/docs/configuration.md +1 -0
  108. package/docs/releasing.md +8 -3
  109. package/docs/runtime-policies.md +12 -3
  110. package/docs/troubleshooting.md +17 -0
  111. package/docs/typescript.md +57 -10
  112. package/package.json +10 -9
  113. 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, }) => {
@@ -120,6 +113,8 @@ export const commandMutatesProject = ({ command, flags, guided, }) => {
120
113
  return Boolean(flags.refreshConfirmation);
121
114
  if (command === "identity claim")
122
115
  return Boolean(flags.identityConfirmation);
116
+ if (command === "identity connect")
117
+ return true;
123
118
  if (command === "run")
124
119
  return !flags.dryRun;
125
120
  return [
@@ -0,0 +1,4 @@
1
+ /** Stable version and help handlers with no project-side effects. */
2
+ import type { CommandDefinition } from "./command_contract.mjs";
3
+ export declare const usage: () => string;
4
+ export declare const createBasicCommandHandlers: () => readonly CommandDefinition[];
@@ -0,0 +1,57 @@
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
+ identity connect [--identity=] Keep the app open, detect sign-in, and connect once
22
+ doctor Check whether Rehearsal is safe and ready
23
+ support Print a safe, copy-ready support report
24
+ explain Show the immutable execution plan
25
+ run --dry-run Alias the exact explain plan without mutations
26
+ run --confirm-candidates= Execute reset, migration, and verification locally
27
+ open Start the runtime and keep the application open
28
+ candidates Show the exact pending migration digest
29
+ inspect baseline Show verified baseline provenance
30
+ inspect migrations Classify represented, applied, and candidate migrations
31
+ start Start an existing verified local runtime
32
+ migrate --confirm-candidates= Apply the exact candidate suffix without resetting
33
+ reset Restore and verify the immutable local baseline
34
+ status Report the disposable local runtime state
35
+ stop Stop only this project's local runtime
36
+ discard Remove only this project's disposable runtime
37
+ cleanup [--include-runtime] [--include-images] [--write] [--confirm-cleanup=]
38
+ Preview or apply conservative disk cleanup
39
+ verify Verify the current local Rehearsal runtime
40
+
41
+ Options: --help, -h --version, -V --json --verbose --debug --plain
42
+ --config=<path> --target=supabase|postgresql
43
+ --confirm-source-access=<digest> --confirm-source-retirement=<digest>
44
+ --confirm-refresh=<digest>`;
45
+ export const createBasicCommandHandlers = () => [
46
+ {
47
+ id: "version",
48
+ matches: ({ command, flags }) => command === "version" || flags.version,
49
+ handle: async ({ flags }) => textCommand(flags.json
50
+ ? JSON.stringify({ name: "@rehearsal-db/core", version: REHEARSAL_VERSION }, null, 2)
51
+ : REHEARSAL_VERSION),
52
+ },
53
+ {
54
+ ...exactCommands("help", ["help"], async () => textCommand(usage())),
55
+ matches: ({ command, flags }) => command === "help" || flags.help,
56
+ },
57
+ ];
@@ -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,34 @@
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
+ loadIdentityPolicy: projectCommands.loadIdentityPolicy,
22
+ startRuntimeApplication: runtimeCommands.startRuntimeApplication,
23
+ }),
24
+ ...createInspectionCommandHandlers(),
25
+ ...createApplicationCommandHandlers({
26
+ openRuntimeApplication: runtimeCommands.openRuntimeApplication,
27
+ }),
28
+ ...createRuntimeActionHandlers({
29
+ prepareCandidateConfirmation: runtimeCommands.prepareCandidateConfirmation,
30
+ prepareDependentTargets: runtimeCommands.prepareDependentTargets,
31
+ proveRuntimeStack: runtimeCommands.proveRuntimeStack,
32
+ runRuntimeStack: runtimeCommands.runRuntimeStack,
33
+ }),
34
+ ]);
@@ -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;
@@ -95,6 +95,7 @@ const inspectGuidedProject = async ({ projectRoot, planOptions, topologyCandidat
95
95
  policyAbsolutePath: loaded.paths.sanitizationPolicy,
96
96
  ...(migrationSummary === undefined ? {} : { migrationSummary }),
97
97
  refreshConfigured: Boolean(loaded.config.preparation),
98
+ identityConfigured: Boolean(loaded.paths.identityPolicy),
98
99
  dependentCount: topology?.dependents.length ?? 0,
99
100
  runtime: topology
100
101
  ? (await Promise.all(topology.targets.map((target) => pathExists(join(target.paths.runtimeWorkdir, "baseline.json"))))).every(Boolean)
@@ -334,6 +335,13 @@ const guidedOptions = (state, hasLastDetails) => {
334
335
  options.push({ label: "Check readiness", command: "doctor" });
335
336
  }
336
337
  if (state.node.supported && state.runtime) {
338
+ if (state.identityConfigured) {
339
+ options.push({
340
+ label: "Connect my copied account",
341
+ hint: "Sign in normally, review once, then open the copied profile",
342
+ command: "identity connect",
343
+ });
344
+ }
337
345
  options.push({
338
346
  label: "Open the sandbox app",
339
347
  hint: "Keep it running for hands-on testing",
@@ -415,7 +423,7 @@ const showGuidedStatus = ({ state, options, flags, }) => {
415
423
  ].join("\n"));
416
424
  }
417
425
  };
418
- const guideCleanup = async ({ flags, planOptions, planRuntimeStackCleanup, }) => {
426
+ const guideCleanup = async ({ flags, planOptions, planRuntimeStackCleanup, session, }) => {
419
427
  const cleanupScope = await promptForChoice({
420
428
  message: "What may Rehearsal include in the cleanup preview?",
421
429
  flags,
@@ -444,7 +452,7 @@ const guideCleanup = async ({ flags, planOptions, planRuntimeStackCleanup, }) =>
444
452
  flags.includeRuntime = ["runtime", "all"].includes(cleanupScope.command);
445
453
  flags.includeImages = ["images", "all"].includes(cleanupScope.command);
446
454
  const cleanupPlan = await planRuntimeStackCleanup({ flags, planOptions });
447
- flags.cleanupPlan = cleanupPlan;
455
+ session.cleanupPlan = cleanupPlan;
448
456
  console.log(`\n${renderCleanup({ mode: "preview", plan: cleanupPlan })}`);
449
457
  const selectedCount = "targets" in cleanupPlan
450
458
  ? cleanupPlan.targets.reduce((total, target) => total +
@@ -465,7 +473,7 @@ const guideCleanup = async ({ flags, planOptions, planRuntimeStackCleanup, }) =>
465
473
  flags.cleanupConfirmation = cleanupPlan.digest;
466
474
  return "cleanup";
467
475
  };
468
- const guideBaseline = async ({ state, selected, flags, planOptions, projectRoot, }) => {
476
+ const guideBaseline = async ({ state, selected, flags, planOptions, projectRoot, session, }) => {
469
477
  console.log([
470
478
  "",
471
479
  terminalStyle(flags, "1", "CREATE A SAFE BASELINE"),
@@ -526,7 +534,7 @@ const guideBaseline = async ({ state, selected, flags, planOptions, projectRoot,
526
534
  console.log("No changes made.");
527
535
  return "home";
528
536
  }
529
- flags.preparationPlan = preparationPlan;
537
+ session.preparationPlan = preparationPlan;
530
538
  flags.write = true;
531
539
  return "baseline prepare";
532
540
  }
@@ -567,7 +575,7 @@ const guideBaseline = async ({ state, selected, flags, planOptions, projectRoot,
567
575
  return "home";
568
576
  }
569
577
  };
570
- export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildRefreshWorkflowPlan, planRuntimeStackCleanup, getLastGuidedDetails, }) => async ({ flags, planOptions, }) => {
578
+ export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildRefreshWorkflowPlan, planRuntimeStackCleanup, getLastGuidedDetails, session, }) => async ({ flags, planOptions, }) => {
571
579
  const state = await inspectGuidedProject({
572
580
  projectRoot,
573
581
  planOptions,
@@ -659,7 +667,12 @@ export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildR
659
667
  return runtimeAction.command;
660
668
  }
661
669
  if (selected.command === "cleanup-guide") {
662
- return guideCleanup({ flags, planOptions, planRuntimeStackCleanup });
670
+ return guideCleanup({
671
+ flags,
672
+ planOptions,
673
+ planRuntimeStackCleanup,
674
+ session,
675
+ });
663
676
  }
664
677
  if (["baseline-guide", "baseline-prepare"].includes(selected.command)) {
665
678
  return guideBaseline({
@@ -668,6 +681,7 @@ export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildR
668
681
  flags,
669
682
  planOptions,
670
683
  projectRoot,
684
+ session,
671
685
  });
672
686
  }
673
687
  if (selected.command !== "setup-write")
@@ -707,7 +721,7 @@ export const createGuidedHome = ({ projectRoot, topologyCandidateSummary, buildR
707
721
  console.log("No changes made.");
708
722
  return "home";
709
723
  }
710
- flags.setupPlan = setupPlan;
724
+ session.setupPlan = setupPlan;
711
725
  flags.write = true;
712
726
  return "setup";
713
727
  };
@@ -0,0 +1,12 @@
1
+ /** Provider-neutral copied-account planning, application, and guided connection. */
2
+ import type { CommandDefinition } from "./command_contract.mjs";
3
+ import type { createProjectCommands } from "./project_commands.mjs";
4
+ import type { createRuntimeCommands } from "./runtime_commands.mjs";
5
+ type ProjectCommands = ReturnType<typeof createProjectCommands>;
6
+ type RuntimeCommands = ReturnType<typeof createRuntimeCommands>;
7
+ export declare const createIdentityCommandHandlers: ({ loadIdentityClaimPlan, loadIdentityPolicy, startRuntimeApplication, }: {
8
+ loadIdentityClaimPlan: ProjectCommands["loadIdentityClaimPlan"];
9
+ loadIdentityPolicy: ProjectCommands["loadIdentityPolicy"];
10
+ startRuntimeApplication: RuntimeCommands["startRuntimeApplication"];
11
+ }) => readonly CommandDefinition[];
12
+ export {};