@rehearsal-db/core 0.1.0-beta.14 → 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.
Files changed (236) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +1 -0
  3. package/dist/package.json +101 -0
  4. package/dist/scripts/install/create_root_config.d.mts +3 -0
  5. package/dist/scripts/install/create_root_config.mjs +13 -0
  6. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  7. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  8. package/dist/src/application/session.d.mts +96 -0
  9. package/dist/src/application/session.mjs +380 -0
  10. package/dist/src/baseline/artifact.d.mts +119 -0
  11. package/dist/src/baseline/artifact.mjs +522 -0
  12. package/dist/src/baseline/builder.d.mts +19 -0
  13. package/dist/src/baseline/builder.mjs +142 -0
  14. package/dist/src/baseline/input_discovery.d.mts +13 -0
  15. package/dist/src/baseline/input_discovery.mjs +149 -0
  16. package/dist/src/baseline/policy_review.d.mts +56 -0
  17. package/dist/src/baseline/policy_review.mjs +73 -0
  18. package/dist/src/baseline/preparation.d.mts +63 -0
  19. package/dist/src/baseline/preparation.mjs +295 -0
  20. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  21. package/dist/src/baseline/privacy_contract.mjs +2 -0
  22. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  23. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  24. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  25. package/dist/src/baseline/privacy_engine.mjs +4 -0
  26. package/dist/src/baseline/privacy_key.d.mts +7 -0
  27. package/dist/src/baseline/privacy_key.mjs +42 -0
  28. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  29. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  30. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  31. package/dist/src/baseline/privacy_policy.mjs +237 -0
  32. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  33. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  34. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  35. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  36. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  37. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  38. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  39. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  40. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  41. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  42. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  43. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  44. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  45. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  46. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  47. package/dist/src/cli/application_command_handlers.mjs +52 -0
  48. package/dist/src/cli/arguments.d.mts +38 -0
  49. package/dist/src/cli/arguments.mjs +128 -0
  50. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  51. package/dist/src/cli/basic_command_handlers.mjs +56 -0
  52. package/dist/src/cli/cli_commands.d.mts +14 -0
  53. package/dist/src/cli/cli_commands.mjs +32 -0
  54. package/dist/src/cli/command_contract.d.mts +57 -0
  55. package/dist/src/cli/command_contract.mjs +39 -0
  56. package/dist/src/cli/command_output.d.mts +12 -0
  57. package/dist/src/cli/command_output.mjs +20 -0
  58. package/dist/src/cli/command_registry.d.mts +7 -0
  59. package/dist/src/cli/command_registry.mjs +14 -0
  60. package/dist/src/cli/guided.d.mts +53 -0
  61. package/dist/src/cli/guided.mjs +719 -0
  62. package/dist/src/cli/identity_command_handlers.d.mts +8 -0
  63. package/dist/src/cli/identity_command_handlers.mjs +43 -0
  64. package/dist/src/cli/init_command.d.mts +21 -0
  65. package/dist/src/cli/init_command.mjs +90 -0
  66. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  67. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  68. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  69. package/dist/src/cli/project_command_handlers.mjs +92 -0
  70. package/dist/src/cli/project_commands.d.mts +159 -0
  71. package/dist/src/cli/project_commands.mjs +183 -0
  72. package/dist/src/cli/rehearsal.d.mts +7 -0
  73. package/dist/src/cli/rehearsal.mjs +190 -0
  74. package/dist/src/cli/renderers.d.mts +299 -0
  75. package/dist/src/cli/renderers.mjs +554 -0
  76. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  77. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  78. package/dist/src/cli/runtime_commands.d.mts +269 -0
  79. package/dist/src/cli/runtime_commands.mjs +601 -0
  80. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  81. package/dist/src/cli/source_command_handlers.mjs +62 -0
  82. package/dist/src/cli/source_commands.d.mts +436 -0
  83. package/dist/src/cli/source_commands.mjs +301 -0
  84. package/dist/src/cli/terminal.d.mts +59 -0
  85. package/dist/src/cli/terminal.mjs +230 -0
  86. package/dist/src/identity/claim.d.mts +5 -0
  87. package/dist/src/identity/claim.mjs +4 -0
  88. package/dist/src/identity/claim_contract.d.mts +135 -0
  89. package/dist/src/identity/claim_contract.mjs +6 -0
  90. package/dist/src/identity/claim_executor.d.mts +10 -0
  91. package/dist/src/identity/claim_executor.mjs +358 -0
  92. package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
  93. package/dist/src/identity/claim_identity_lookup.mjs +125 -0
  94. package/dist/src/identity/claim_plan.d.mts +8 -0
  95. package/dist/src/identity/claim_plan.mjs +61 -0
  96. package/dist/src/identity/claim_policy.d.mts +3 -0
  97. package/dist/src/identity/claim_policy.mjs +377 -0
  98. package/dist/src/identity/claim_sql.d.mts +10 -0
  99. package/dist/src/identity/claim_sql.mjs +46 -0
  100. package/dist/src/identity/claim_verification.d.mts +13 -0
  101. package/dist/src/identity/claim_verification.mjs +35 -0
  102. package/dist/src/identity/provider_configuration.d.mts +37 -0
  103. package/dist/src/identity/provider_configuration.mjs +132 -0
  104. package/dist/src/identity/storage.d.mts +21 -0
  105. package/dist/src/identity/storage.mjs +122 -0
  106. package/dist/src/index.d.mts +4 -0
  107. package/dist/src/index.mjs +3 -0
  108. package/dist/src/project/configuration.d.mts +13 -0
  109. package/dist/src/project/configuration.mjs +9 -0
  110. package/dist/src/project/configuration_contract.d.mts +125 -0
  111. package/dist/src/project/configuration_contract.mjs +27 -0
  112. package/dist/src/project/configuration_discovery.d.mts +7 -0
  113. package/dist/src/project/configuration_discovery.mjs +37 -0
  114. package/dist/src/project/configuration_normalization.d.mts +107 -0
  115. package/dist/src/project/configuration_normalization.mjs +302 -0
  116. package/dist/src/project/configuration_rendering.d.mts +14 -0
  117. package/dist/src/project/configuration_rendering.mjs +224 -0
  118. package/dist/src/project/configuration_resolution.d.mts +125 -0
  119. package/dist/src/project/configuration_resolution.mjs +81 -0
  120. package/dist/src/project/configuration_schema.d.mts +22 -0
  121. package/dist/src/project/configuration_schema.mjs +160 -0
  122. package/dist/src/project/configuration_validation.d.mts +22 -0
  123. package/dist/src/project/configuration_validation.mjs +136 -0
  124. package/dist/src/project/install_scaffold.d.mts +42 -0
  125. package/dist/src/project/install_scaffold.mjs +263 -0
  126. package/dist/src/project/project_detection.d.mts +14 -0
  127. package/dist/src/project/project_detection.mjs +47 -0
  128. package/dist/src/project/setup.d.mts +87 -0
  129. package/dist/src/project/setup.mjs +414 -0
  130. package/dist/src/project/support_report.d.mts +88 -0
  131. package/dist/src/project/support_report.mjs +83 -0
  132. package/dist/src/runtime/cleanup.d.mts +85 -0
  133. package/dist/src/runtime/cleanup.mjs +311 -0
  134. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  135. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  136. package/dist/src/runtime/migration_history.d.mts +51 -0
  137. package/dist/src/runtime/migration_history.mjs +170 -0
  138. package/dist/src/runtime/plan.d.mts +134 -0
  139. package/dist/src/runtime/plan.mjs +637 -0
  140. package/dist/src/runtime/policy.d.mts +66 -0
  141. package/dist/src/runtime/policy.mjs +369 -0
  142. package/dist/src/runtime/restore.d.mts +22 -0
  143. package/dist/src/runtime/restore.mjs +276 -0
  144. package/dist/src/runtime/service_environment.d.mts +11 -0
  145. package/dist/src/runtime/service_environment.mjs +58 -0
  146. package/dist/src/runtime/topology.d.mts +51 -0
  147. package/dist/src/runtime/topology.mjs +128 -0
  148. package/dist/src/shared/diagnostics.d.mts +97 -0
  149. package/dist/src/shared/diagnostics.mjs +170 -0
  150. package/dist/src/shared/human_output.d.mts +2 -0
  151. package/{src → dist/src}/shared/human_output.mjs +1 -3
  152. package/dist/src/shared/node_runtime.d.mts +14 -0
  153. package/dist/src/shared/node_runtime.mjs +25 -0
  154. package/dist/src/shared/operation_guard.d.mts +22 -0
  155. package/dist/src/shared/operation_guard.mjs +150 -0
  156. package/dist/src/shared/process_environment.d.mts +15 -0
  157. package/dist/src/shared/process_environment.mjs +50 -0
  158. package/dist/src/source/access.d.mts +165 -0
  159. package/dist/src/source/access.mjs +602 -0
  160. package/dist/src/source/asset_transfer.d.mts +27 -0
  161. package/dist/src/source/asset_transfer.mjs +202 -0
  162. package/dist/src/source/baseline.d.mts +66 -0
  163. package/dist/src/source/baseline.mjs +372 -0
  164. package/dist/src/source/external_reader.d.mts +42 -0
  165. package/dist/src/source/external_reader.mjs +278 -0
  166. package/dist/src/source/postgresql_access.d.mts +86 -0
  167. package/dist/src/source/postgresql_access.mjs +454 -0
  168. package/dist/src/source/privacy_paths.d.mts +10 -0
  169. package/dist/src/source/privacy_paths.mjs +27 -0
  170. package/dist/src/targets/driver.d.mts +2 -0
  171. package/dist/src/targets/driver.mjs +13 -0
  172. package/dist/src/targets/postgresql.d.mts +5 -0
  173. package/dist/src/targets/postgresql.mjs +620 -0
  174. package/dist/src/targets/supabase.d.mts +8 -0
  175. package/dist/src/targets/supabase.mjs +627 -0
  176. package/dist/src/targets/supabase_environment.d.mts +49 -0
  177. package/dist/src/targets/supabase_environment.mjs +162 -0
  178. package/dist/src/targets/target.d.mts +33 -0
  179. package/dist/src/targets/target.mjs +39 -0
  180. package/docs/README.md +1 -0
  181. package/docs/architecture.md +133 -10
  182. package/docs/configuration.md +9 -3
  183. package/docs/index.md +1 -0
  184. package/docs/releasing.md +8 -3
  185. package/docs/typescript.md +134 -0
  186. package/package.json +36 -29
  187. package/types/configuration.d.mts +5 -0
  188. package/scripts/install/create_root_config.mjs +0 -19
  189. package/src/README.md +0 -17
  190. package/src/application/session.mjs +0 -435
  191. package/src/baseline/artifact.mjs +0 -684
  192. package/src/baseline/builder.mjs +0 -164
  193. package/src/baseline/input_discovery.mjs +0 -155
  194. package/src/baseline/policy_review.mjs +0 -92
  195. package/src/baseline/preparation.mjs +0 -323
  196. package/src/baseline/privacy_engine.mjs +0 -1399
  197. package/src/baseline/privacy_structured_json.mjs +0 -554
  198. package/src/baseline/sanitization_policy.mjs +0 -299
  199. package/src/baseline/schema_snapshot.mjs +0 -113
  200. package/src/cli/arguments.mjs +0 -126
  201. package/src/cli/guided.mjs +0 -814
  202. package/src/cli/init_command.mjs +0 -94
  203. package/src/cli/rehearsal.mjs +0 -972
  204. package/src/cli/renderers.mjs +0 -638
  205. package/src/cli/runtime_commands.mjs +0 -707
  206. package/src/cli/source_commands.mjs +0 -355
  207. package/src/cli/terminal.mjs +0 -275
  208. package/src/identity/claim.mjs +0 -1165
  209. package/src/identity/provider_configuration.mjs +0 -177
  210. package/src/identity/storage.mjs +0 -165
  211. package/src/project/configuration.d.mts +0 -164
  212. package/src/project/configuration.mjs +0 -1337
  213. package/src/project/install_scaffold.mjs +0 -325
  214. package/src/project/setup.mjs +0 -474
  215. package/src/project/support_report.mjs +0 -105
  216. package/src/runtime/cleanup.mjs +0 -381
  217. package/src/runtime/migration_history.mjs +0 -220
  218. package/src/runtime/plan.mjs +0 -722
  219. package/src/runtime/policy.mjs +0 -438
  220. package/src/runtime/restore.mjs +0 -337
  221. package/src/runtime/service_environment.mjs +0 -82
  222. package/src/runtime/topology.mjs +0 -177
  223. package/src/shared/diagnostics.mjs +0 -207
  224. package/src/shared/node_runtime.mjs +0 -31
  225. package/src/shared/operation_guard.mjs +0 -162
  226. package/src/shared/process_environment.mjs +0 -68
  227. package/src/source/access.mjs +0 -798
  228. package/src/source/asset_transfer.mjs +0 -259
  229. package/src/source/baseline.mjs +0 -482
  230. package/src/source/external_reader.mjs +0 -368
  231. package/src/source/postgresql_access.mjs +0 -638
  232. package/src/source/privacy_paths.mjs +0 -38
  233. package/src/targets/postgresql.mjs +0 -808
  234. package/src/targets/supabase.mjs +0 -840
  235. package/src/targets/supabase_environment.mjs +0 -229
  236. package/src/targets/target.mjs +0 -66
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Purpose: Plan and apply conservative first-run Rehearsal scaffolding without
3
+ * contacting hosted services or overwriting project-owned files.
4
+ */
5
+ import { randomUUID } from "node:crypto";
6
+ import { createConnection, createServer } from "node:net";
7
+ import { access, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
8
+ import { basename, dirname, join, relative, resolve } from "node:path";
9
+ import { findRehearsalConfigPath, inspectDetectedProject, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./configuration.mjs";
10
+ import { resolveRuntimeTarget } from "../targets/target.mjs";
11
+ export { assertSupportedRehearsalNodeRuntime, inspectRehearsalNodeRuntime, } from "../shared/node_runtime.mjs";
12
+ import { assertSupportedRehearsalNodeRuntime } from "../shared/node_runtime.mjs";
13
+ const CONFIG_PATH = "rehearsal.config.mjs";
14
+ const LOCAL_SUPABASE_CONFIG_PATH = "infrastructure/rehearsal/supabase/config.toml";
15
+ const GITIGNORE_PATH = ".gitignore";
16
+ const PORT_OFFSETS = Object.freeze({
17
+ shadow: 0,
18
+ api: 1,
19
+ database: 2,
20
+ studio: 3,
21
+ smtp: 4,
22
+ pooler: 9,
23
+ });
24
+ const hasErrorCode = (error, code) => typeof error === "object" &&
25
+ error !== null &&
26
+ "code" in error &&
27
+ error.code === code;
28
+ const pathExists = (path) => access(path)
29
+ .then(() => true)
30
+ .catch((error) => {
31
+ if (hasErrorCode(error, "ENOENT"))
32
+ return false;
33
+ throw error;
34
+ });
35
+ const readOptionalFile = async (path) => {
36
+ try {
37
+ return await readFile(path, "utf8");
38
+ }
39
+ catch (error) {
40
+ if (hasErrorCode(error, "ENOENT"))
41
+ return null;
42
+ throw error;
43
+ }
44
+ };
45
+ const canConnectToPort = (port) => new Promise((resolveAvailability) => {
46
+ const socket = createConnection({ host: "127.0.0.1", port });
47
+ const finish = (result) => {
48
+ socket.destroy();
49
+ resolveAvailability(result);
50
+ };
51
+ socket.setTimeout(500);
52
+ socket.once("connect", () => finish(true));
53
+ socket.once("error", () => finish(false));
54
+ socket.once("timeout", () => finish(false));
55
+ });
56
+ const canBindToPort = (port) => new Promise((resolveAvailability) => {
57
+ const server = createServer();
58
+ server.unref();
59
+ server.once("error", () => resolveAvailability(false));
60
+ server.listen({ host: "127.0.0.1", port, exclusive: true }, () => {
61
+ server.close(() => resolveAvailability(true));
62
+ });
63
+ });
64
+ export const isRehearsalPortAvailable = async (port) => {
65
+ if (await canConnectToPort(port))
66
+ return false;
67
+ return canBindToPort(port);
68
+ };
69
+ export const findAvailableRehearsalPorts = async ({ isAvailable = isRehearsalPortAvailable, } = {}) => {
70
+ for (let base = 58_320; base <= 65_520; base += 20) {
71
+ const entries = Object.entries(PORT_OFFSETS).map(([name, offset]) => [
72
+ name,
73
+ base + offset,
74
+ ]);
75
+ const availability = await Promise.all(entries.map(([, port]) => isAvailable(port)));
76
+ if (availability.every(Boolean))
77
+ return Object.fromEntries(entries);
78
+ }
79
+ throw new Error("Rehearsal could not find an available local port block between 58320 and 65529.");
80
+ };
81
+ const databaseMajorVersionFrom = (source) => {
82
+ const match = /^\s*major_version\s*=\s*(\d+)\s*(?:#.*)?$/imu.exec(source ?? "");
83
+ return match ? Number(match[1]) : 17;
84
+ };
85
+ export const renderSafeLocalSupabaseConfig = ({ projectId, applicationUrl, ports, databaseMajorVersion = 17, }) => `# Generated by Rehearsal for a disposable, local-only runtime.
86
+ # Review project-specific Auth or Storage additions before enabling them here.
87
+ project_id = ${JSON.stringify(projectId)}
88
+
89
+ [api]
90
+ enabled = true
91
+ port = ${ports.api}
92
+ schemas = ["public"]
93
+ extra_search_path = ["public", "extensions"]
94
+
95
+ [db]
96
+ port = ${ports.database}
97
+ shadow_port = ${ports.shadow}
98
+ major_version = ${databaseMajorVersion}
99
+
100
+ [db.pooler]
101
+ enabled = false
102
+ port = ${ports.pooler}
103
+
104
+ [db.migrations]
105
+ enabled = true
106
+ schema_paths = []
107
+
108
+ [db.seed]
109
+ enabled = false
110
+ sql_paths = []
111
+
112
+ [studio]
113
+ enabled = true
114
+ port = ${ports.studio}
115
+ api_url = "http://127.0.0.1"
116
+
117
+ [local_smtp]
118
+ enabled = false
119
+ port = ${ports.smtp}
120
+
121
+ [auth]
122
+ enabled = true
123
+ site_url = ${JSON.stringify(applicationUrl)}
124
+ enable_signup = false
125
+
126
+ [storage]
127
+ enabled = true
128
+
129
+ [realtime]
130
+ enabled = false
131
+
132
+ [edge_runtime]
133
+ enabled = false
134
+
135
+ [analytics]
136
+ enabled = false
137
+ `;
138
+ export const appendRehearsalGitignoreEntries = (source) => {
139
+ const original = source ?? "";
140
+ const lines = new Set(original
141
+ .split(/\r?\n/u)
142
+ .map((line) => line.trim())
143
+ .filter(Boolean));
144
+ const entries = [".rehearsal/", ".env.rehearsal-service.local"];
145
+ const missing = entries.filter((entry) => !lines.has(entry) &&
146
+ !lines.has(`/${entry}`) &&
147
+ !lines.has(entry.replace(/\/$/u, "")));
148
+ if (missing.length === 0)
149
+ return original;
150
+ const prefix = original && !original.endsWith("\n") ? `${original}\n` : original;
151
+ const separator = prefix && !prefix.endsWith("\n\n") ? "\n" : "";
152
+ return `${prefix}${separator}# Rehearsal local artifacts\n${missing.join("\n")}\n`;
153
+ };
154
+ const findExistingConfigPath = async ({ projectRoot, configPath, }) => {
155
+ try {
156
+ return await findRehearsalConfigPath({
157
+ projectRoot,
158
+ ...(configPath === undefined ? {} : { configPath }),
159
+ });
160
+ }
161
+ catch (error) {
162
+ if ((error instanceof Error ? error.message : String(error)).startsWith("No Rehearsal configuration")) {
163
+ return null;
164
+ }
165
+ throw error;
166
+ }
167
+ };
168
+ const deriveSupabasePorts = (runtimePorts) => {
169
+ if (runtimePorts.api === undefined || runtimePorts.studio === undefined) {
170
+ throw new Error("The Supabase runtime requires API and Studio ports.");
171
+ }
172
+ return {
173
+ shadow: runtimePorts.database - 2,
174
+ api: runtimePorts.api,
175
+ database: runtimePorts.database,
176
+ studio: runtimePorts.studio,
177
+ smtp: runtimePorts.database + 2,
178
+ pooler: runtimePorts.database + 7,
179
+ };
180
+ };
181
+ const applyGitignoreChange = async (file) => {
182
+ if (file.action === "unchanged")
183
+ return;
184
+ if (file.action === "create") {
185
+ if (file.content === null) {
186
+ throw new Error(`Rehearsal setup content is missing for ${file.path}.`);
187
+ }
188
+ await writeFile(file.absolutePath, file.content, {
189
+ flag: "wx",
190
+ mode: file.mode,
191
+ });
192
+ return;
193
+ }
194
+ const lockPath = `${file.absolutePath}.rehearsal.lock`;
195
+ let lock;
196
+ let temporaryPath;
197
+ try {
198
+ lock = await open(lockPath, "wx", 0o600);
199
+ const current = await readOptionalFile(file.absolutePath);
200
+ if (current !== file.originalContent) {
201
+ throw new Error(".gitignore changed after the setup preview; rerun rehearsal setup before writing.");
202
+ }
203
+ temporaryPath = join(dirname(file.absolutePath), `.${basename(file.absolutePath)}.rehearsal-${process.pid}-${randomUUID()}.tmp`);
204
+ if (file.content === null) {
205
+ throw new Error(`Rehearsal setup content is missing for ${file.path}.`);
206
+ }
207
+ await writeFile(temporaryPath, file.content, {
208
+ flag: "wx",
209
+ mode: file.mode,
210
+ });
211
+ await rename(temporaryPath, file.absolutePath);
212
+ temporaryPath = null;
213
+ }
214
+ catch (error) {
215
+ if (hasErrorCode(error, "EEXIST") && !lock) {
216
+ throw new Error("Another Rehearsal process is updating .gitignore; rerun setup after it finishes.");
217
+ }
218
+ throw error;
219
+ }
220
+ finally {
221
+ if (temporaryPath)
222
+ await rm(temporaryPath, { force: true });
223
+ if (lock) {
224
+ await lock.close();
225
+ await rm(lockPath, { force: true });
226
+ }
227
+ }
228
+ };
229
+ const assertCreateTargetAvailable = async (file) => {
230
+ if (file.action === "create" && (await pathExists(file.absolutePath))) {
231
+ throw new Error(`Rehearsal setup will not overwrite the existing file at ${file.path}.`);
232
+ }
233
+ };
234
+ export const planRehearsalSetup = async ({ projectRoot = process.cwd(), configPath, isPortAvailable, target, } = {}) => {
235
+ const root = resolve(projectRoot);
236
+ const detected = await inspectDetectedProject({ projectRoot: root });
237
+ const existingConfigPath = await findExistingConfigPath({
238
+ projectRoot: root,
239
+ ...(configPath === undefined ? {} : { configPath }),
240
+ });
241
+ const existing = existingConfigPath
242
+ ? await loadRehearsalConfig({
243
+ projectRoot: root,
244
+ ...(configPath === undefined ? {} : { configPath }),
245
+ })
246
+ : null;
247
+ const requestedTarget = resolveRuntimeTarget(target ?? existing?.config.runtime.target ?? "supabase").id;
248
+ const runtimeTarget = existing?.config.runtime.target ?? requestedTarget;
249
+ if (existing && target && runtimeTarget !== requestedTarget) {
250
+ throw new Error(`The existing Rehearsal configuration targets ${runtimeTarget}; setup will not replace it with ${requestedTarget}.`);
251
+ }
252
+ const ports = existing
253
+ ? runtimeTarget === "supabase"
254
+ ? deriveSupabasePorts(existing.config.runtime.ports)
255
+ : { database: existing.config.runtime.ports.database }
256
+ : await findAvailableRehearsalPorts({
257
+ ...(isPortAvailable ? { isAvailable: isPortAvailable } : {}),
258
+ });
259
+ const applicationUrl = existing?.config.runtime.applicationUrl ?? "http://localhost:5175";
260
+ const projectId = existing?.config.runtime.projectId ?? `${detected.projectName}-rehearsal`;
261
+ const sourceSupabaseConfig = runtimeTarget === "supabase"
262
+ ? await readOptionalFile(join(root, "supabase/config.toml"))
263
+ : null;
264
+ const originalGitignore = await readOptionalFile(join(root, GITIGNORE_PATH));
265
+ const nextGitignore = appendRehearsalGitignoreEntries(originalGitignore);
266
+ const rehearsalConfigPath = existing?.paths.rehearsalConfig;
267
+ const localSupabasePath = rehearsalConfigPath
268
+ ? relative(root, rehearsalConfigPath)
269
+ : LOCAL_SUPABASE_CONFIG_PATH;
270
+ const localSupabaseExists = rehearsalConfigPath
271
+ ? await pathExists(rehearsalConfigPath)
272
+ : false;
273
+ const supabasePorts = runtimeTarget === "supabase" ? deriveSupabasePorts(ports) : null;
274
+ const localRuntimeFiles = runtimeTarget === "supabase"
275
+ ? [
276
+ {
277
+ path: localSupabasePath,
278
+ absolutePath: rehearsalConfigPath ?? join(root, LOCAL_SUPABASE_CONFIG_PATH),
279
+ action: localSupabaseExists ? "unchanged" : "create",
280
+ mode: 0o600,
281
+ content: localSupabaseExists
282
+ ? null
283
+ : renderSafeLocalSupabaseConfig({
284
+ projectId,
285
+ applicationUrl,
286
+ ports: supabasePorts,
287
+ databaseMajorVersion: databaseMajorVersionFrom(sourceSupabaseConfig),
288
+ }),
289
+ },
290
+ ]
291
+ : [];
292
+ const files = [
293
+ {
294
+ path: existingConfigPath
295
+ ? relative(root, existingConfigPath)
296
+ : CONFIG_PATH,
297
+ absolutePath: existingConfigPath ?? join(root, CONFIG_PATH),
298
+ action: existingConfigPath ? "unchanged" : "create",
299
+ mode: 0o600,
300
+ content: existingConfigPath
301
+ ? null
302
+ : runtimeTarget === "supabase"
303
+ ? renderDetectedConfig(detected, {
304
+ applicationUrl,
305
+ ports: supabasePorts,
306
+ })
307
+ : renderDetectedPostgresqlConfig(detected, {
308
+ applicationUrl,
309
+ databasePort: ports.database,
310
+ }),
311
+ },
312
+ ...localRuntimeFiles,
313
+ {
314
+ path: GITIGNORE_PATH,
315
+ absolutePath: join(root, GITIGNORE_PATH),
316
+ action: nextGitignore === (originalGitignore ?? "")
317
+ ? "unchanged"
318
+ : originalGitignore === null
319
+ ? "create"
320
+ : "update",
321
+ mode: 0o644,
322
+ originalContent: originalGitignore,
323
+ content: nextGitignore,
324
+ },
325
+ ];
326
+ return {
327
+ projectRoot: root,
328
+ configurationPath: existingConfigPath
329
+ ? relative(root, existingConfigPath)
330
+ : CONFIG_PATH,
331
+ project: detected.projectName,
332
+ projectId,
333
+ target: runtimeTarget,
334
+ applicationUrl,
335
+ detected,
336
+ ports: runtimeTarget === "supabase" ? ports : { database: ports.database },
337
+ requiresAvailablePorts: files.some((file) => file.action === "create" &&
338
+ [CONFIG_PATH, localSupabasePath].includes(file.path)),
339
+ isPortAvailable: isPortAvailable ?? isRehearsalPortAvailable,
340
+ files,
341
+ safety: [
342
+ "hosted access remains disabled",
343
+ runtimeTarget === "supabase"
344
+ ? "the Supabase runtime binds to dedicated local ports"
345
+ : "PostgreSQL binds only to a dedicated loopback port",
346
+ ...(runtimeTarget === "supabase"
347
+ ? [
348
+ "external identity providers, realtime, edge functions, and analytics start disabled",
349
+ ]
350
+ : [
351
+ "the runtime uses an already-installed official PostgreSQL image and never pulls implicitly",
352
+ ]),
353
+ "configuration files are never overwritten and .gitignore updates are concurrency-checked",
354
+ ],
355
+ };
356
+ };
357
+ export const applyRehearsalSetup = async (plan) => {
358
+ assertSupportedRehearsalNodeRuntime();
359
+ const unavailablePorts = plan.requiresAvailablePorts
360
+ ? (await Promise.all(Object.values(plan.ports).map(async (port) => ({
361
+ port,
362
+ available: await plan.isPortAvailable(port),
363
+ })))).filter(({ available }) => !available)
364
+ : [];
365
+ if (unavailablePorts.length) {
366
+ throw new Error(`Rehearsal setup ports became occupied after the preview (${unavailablePorts.map(({ port }) => port).join(", ")}); rerun setup to choose another local port block.`);
367
+ }
368
+ for (const file of plan.files)
369
+ await assertCreateTargetAvailable(file);
370
+ const gitignore = plan.files.find((file) => file.path === GITIGNORE_PATH);
371
+ if (gitignore)
372
+ await applyGitignoreChange(gitignore);
373
+ for (const file of plan.files.filter((entry) => entry.action !== "unchanged" && entry.path !== GITIGNORE_PATH)) {
374
+ await mkdir(dirname(file.absolutePath), { recursive: true, mode: 0o700 });
375
+ if (file.content === null) {
376
+ throw new Error(`Rehearsal setup content is missing for ${file.path}.`);
377
+ }
378
+ await writeFile(file.absolutePath, file.content, {
379
+ flag: file.action === "create" ? "wx" : "w",
380
+ mode: file.mode,
381
+ });
382
+ }
383
+ return {
384
+ projectRoot: plan.projectRoot,
385
+ files: plan.files.map((file) => ({
386
+ path: relative(plan.projectRoot, file.absolutePath),
387
+ action: file.action,
388
+ })),
389
+ };
390
+ };
391
+ export const summarizeRehearsalSetup = (plan, { mode }) => ({
392
+ mode,
393
+ configurationPath: plan.configurationPath,
394
+ configurationAction: plan.files.find((file) => file.path === plan.configurationPath)?.action ??
395
+ "unchanged",
396
+ rootConfigGenerationSkipped: plan.configurationPath !== CONFIG_PATH,
397
+ project: plan.project,
398
+ projectId: plan.projectId,
399
+ target: plan.target,
400
+ applicationUrl: plan.applicationUrl,
401
+ detected: plan.detected,
402
+ ports: plan.target === "supabase"
403
+ ? {
404
+ api: plan.ports.api,
405
+ database: plan.ports.database,
406
+ studio: plan.ports.studio,
407
+ }
408
+ : { database: plan.ports.database },
409
+ files: plan.files.map((file) => ({ path: file.path, action: file.action })),
410
+ safety: plan.safety,
411
+ nextAction: mode === "written"
412
+ ? `Review ${plan.configurationPath}, including its commands and sanitization policy path, then run rehearsal doctor.`
413
+ : "Review this plan, then rerun rehearsal setup --write to create it.",
414
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Build a copy-ready, data-free support report. The report intentionally omits
3
+ * project names, paths, command output, environment-variable names, row data,
4
+ * migration contents, and baseline identifiers.
5
+ */
6
+ import type { RehearsalConfigPathOptions } from "./configuration.mjs";
7
+ interface VersionCommandResult {
8
+ readonly error?: NodeJS.ErrnoException;
9
+ readonly status: number | null;
10
+ readonly stdout: string;
11
+ readonly stderr: string;
12
+ }
13
+ type VersionCommandRunner = (command: string, args: readonly string[]) => VersionCommandResult;
14
+ interface DoctorCheck {
15
+ readonly id: string;
16
+ readonly label: string;
17
+ readonly status: string;
18
+ readonly remediation?: string;
19
+ }
20
+ interface DoctorReport {
21
+ readonly state: string;
22
+ readonly checks: readonly DoctorCheck[];
23
+ readonly ambientHostedVariables?: Readonly<{
24
+ presentButQuarantined: readonly string[];
25
+ }>;
26
+ }
27
+ interface SupportSystem {
28
+ readonly platform: string;
29
+ readonly release: string;
30
+ readonly architecture: string;
31
+ readonly node: string;
32
+ }
33
+ interface SupportDependencies {
34
+ readonly runDoctor?: (options: Record<string, unknown>) => Promise<DoctorReport>;
35
+ readonly runCommand?: VersionCommandRunner;
36
+ readonly system?: SupportSystem;
37
+ }
38
+ export declare const REHEARSAL_SUPPORT_URL = "https://github.com/Ddupasquier/rehearsal-db/issues/new?template=bug.yml";
39
+ export declare const collectRehearsalSupportReport: (options?: RehearsalConfigPathOptions, { runDoctor, runCommand, system, }?: SupportDependencies) => Promise<{
40
+ rehearsalVersion: string;
41
+ system: SupportSystem;
42
+ tools: {
43
+ npm: {
44
+ status: string;
45
+ version?: never;
46
+ } | {
47
+ status: string;
48
+ version: unknown;
49
+ };
50
+ supabase: {
51
+ status: string;
52
+ version?: never;
53
+ } | {
54
+ status: string;
55
+ version: unknown;
56
+ };
57
+ dockerClient: {
58
+ status: string;
59
+ version?: never;
60
+ } | {
61
+ status: string;
62
+ version: unknown;
63
+ };
64
+ dockerServer: {
65
+ status: string;
66
+ version?: never;
67
+ } | {
68
+ status: string;
69
+ version: unknown;
70
+ };
71
+ };
72
+ readiness: {
73
+ state: string;
74
+ checks: {
75
+ id: string;
76
+ label: string;
77
+ status: string;
78
+ remediation?: string;
79
+ }[];
80
+ quarantinedHostedVariableCount: number;
81
+ };
82
+ privacy: {
83
+ omits: string[];
84
+ reviewBeforeSharing: boolean;
85
+ };
86
+ supportUrl: string;
87
+ }>;
88
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Build a copy-ready, data-free support report. The report intentionally omits
3
+ * project names, paths, command output, environment-variable names, row data,
4
+ * migration contents, and baseline identifiers.
5
+ */
6
+ import { spawnSync } from "node:child_process";
7
+ import { arch, platform, release } from "node:os";
8
+ import { REHEARSAL_VERSION, redactDiagnosticValue, } from "../shared/diagnostics.mjs";
9
+ import { runRehearsalDoctor } from "../runtime/plan.mjs";
10
+ export const REHEARSAL_SUPPORT_URL = "https://github.com/Ddupasquier/rehearsal-db/issues/new?template=bug.yml";
11
+ const safeEnvironment = () => Object.fromEntries(["HOME", "LANG", "LC_ALL", "PATH", "SHELL", "TMPDIR"].flatMap((key) => process.env[key] === undefined ? [] : [[key, process.env[key]]]));
12
+ const runVersionCommand = (command, args) => spawnSync(command, args, {
13
+ encoding: "utf8",
14
+ env: safeEnvironment(),
15
+ stdio: ["ignore", "pipe", "pipe"],
16
+ timeout: 5_000,
17
+ });
18
+ const inspectTool = ({ command, args, runCommand, }) => {
19
+ const result = runCommand(command, args);
20
+ if (result.error?.code === "ENOENT")
21
+ return { status: "not_installed" };
22
+ if (result.status !== 0)
23
+ return { status: "unavailable" };
24
+ const version = String(result.stdout || result.stderr)
25
+ .trim()
26
+ .split(/\r?\n/u)[0]
27
+ .slice(0, 200);
28
+ return {
29
+ status: "available",
30
+ version: redactDiagnosticValue(version || "version not reported"),
31
+ };
32
+ };
33
+ export const collectRehearsalSupportReport = async (options = {}, { runDoctor = runRehearsalDoctor, runCommand = runVersionCommand, system = {
34
+ platform: platform(),
35
+ release: release(),
36
+ architecture: arch(),
37
+ node: process.versions.node,
38
+ }, } = {}) => {
39
+ const doctor = await runDoctor(options);
40
+ return {
41
+ rehearsalVersion: REHEARSAL_VERSION,
42
+ system,
43
+ tools: {
44
+ npm: inspectTool({ command: "npm", args: ["--version"], runCommand }),
45
+ supabase: inspectTool({
46
+ command: "supabase",
47
+ args: ["--version"],
48
+ runCommand,
49
+ }),
50
+ dockerClient: inspectTool({
51
+ command: "docker",
52
+ args: ["--version"],
53
+ runCommand,
54
+ }),
55
+ dockerServer: inspectTool({
56
+ command: "docker",
57
+ args: ["info", "--format", "{{.ServerVersion}}"],
58
+ runCommand,
59
+ }),
60
+ },
61
+ readiness: {
62
+ state: doctor.state,
63
+ checks: doctor.checks.map(({ id, label, status, remediation }) => ({
64
+ id,
65
+ label,
66
+ status,
67
+ ...(status === "fail" && remediation ? { remediation } : {}),
68
+ })),
69
+ quarantinedHostedVariableCount: doctor.ambientHostedVariables?.presentButQuarantined.length ?? 0,
70
+ },
71
+ privacy: {
72
+ omits: [
73
+ "row values",
74
+ "credentials",
75
+ "project paths",
76
+ "migration SQL",
77
+ "baseline identifiers",
78
+ ],
79
+ reviewBeforeSharing: true,
80
+ },
81
+ supportUrl: REHEARSAL_SUPPORT_URL,
82
+ };
83
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Purpose: Preview and apply narrowly scoped Rehearsal cleanup without global
3
+ * Docker pruning or deletion of unowned database resources.
4
+ */
5
+ import { loadRehearsalConfig } from "../project/configuration.mjs";
6
+ type LoadedConfiguration = Awaited<ReturnType<typeof loadRehearsalConfig>>;
7
+ interface DiskUsage {
8
+ readonly totalBytes: number;
9
+ readonly usedBytes: number;
10
+ readonly availableBytes: number;
11
+ readonly usedPercent: number;
12
+ }
13
+ interface ContainerDiskUsage extends DiskUsage {
14
+ readonly backend: "colima";
15
+ }
16
+ interface DockerUsage {
17
+ readonly available: boolean;
18
+ readonly resources: unknown[];
19
+ readonly disk: ContainerDiskUsage | null;
20
+ }
21
+ interface DockerImage {
22
+ readonly Id: string;
23
+ readonly Created: string;
24
+ readonly Size: number;
25
+ readonly RepoTags?: readonly string[];
26
+ }
27
+ interface ImageCandidate {
28
+ readonly id: string;
29
+ readonly createdAt: string;
30
+ readonly bytes: number;
31
+ readonly tags: string[];
32
+ }
33
+ interface CleanupInspection {
34
+ readonly inspectDockerUsage: (projectRoot: string) => DockerUsage;
35
+ readonly runtimeDetected: (loaded: LoadedConfiguration) => Promise<boolean>;
36
+ }
37
+ export interface RehearsalCleanupPlan {
38
+ readonly project: string;
39
+ readonly projectId: string;
40
+ readonly target: "supabase" | "postgresql";
41
+ readonly retainBaselineGenerations: number;
42
+ readonly baselines: Readonly<{
43
+ available: boolean;
44
+ retained: string[];
45
+ removed: string[];
46
+ }>;
47
+ readonly runtime: Readonly<{
48
+ included: boolean;
49
+ detected: boolean;
50
+ }>;
51
+ readonly images: Readonly<{
52
+ requested: boolean;
53
+ inspected: boolean;
54
+ candidates: ImageCandidate[];
55
+ }>;
56
+ readonly docker: DockerUsage;
57
+ readonly digest: string;
58
+ }
59
+ export declare const parsePosixDiskUsage: (output: string) => DiskUsage | null;
60
+ export declare const selectOlderUnusedSupabaseImages: ({ images, usedImageIds, }: {
61
+ images: readonly DockerImage[];
62
+ usedImageIds: ReadonlySet<string>;
63
+ }) => ImageCandidate[];
64
+ export declare const planRehearsalCleanup: ({ projectRoot, configPath, includeRuntime, includeImages, inspection, }?: {
65
+ projectRoot?: string;
66
+ configPath?: string;
67
+ includeRuntime?: boolean;
68
+ includeImages?: boolean;
69
+ inspection?: CleanupInspection;
70
+ }) => Promise<RehearsalCleanupPlan>;
71
+ export declare const applyRehearsalCleanup: ({ plan, confirmation, projectRoot, configPath, removeRuntime, inspection, }?: {
72
+ plan?: RehearsalCleanupPlan;
73
+ confirmation?: string;
74
+ projectRoot?: string;
75
+ configPath?: string;
76
+ removeRuntime?: () => Promise<unknown>;
77
+ inspection?: CleanupInspection;
78
+ }) => Promise<{
79
+ project: string;
80
+ projectId: string;
81
+ baselines: import("../baseline/artifact.mjs").BaselinePrunePlan;
82
+ runtimeRemoved: boolean;
83
+ imagesRemoved: string[];
84
+ }>;
85
+ export {};