@rehearsal-db/core 0.1.0-beta.5 → 0.1.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ export type RehearsalConfigVersion = 1;
3
3
  export interface RehearsalConfig {
4
4
  schemaVersion: RehearsalConfigVersion;
5
5
  project: { name: string };
6
- supabase: {
6
+ supabase?: {
7
7
  workdir: string;
8
8
  migrationDirectory: string;
9
9
  rehearsalConfig: string;
@@ -11,10 +11,23 @@ export interface RehearsalConfig {
11
11
  serviceEnvironmentFile?: string;
12
12
  serviceEnvironmentVariables?: string[];
13
13
  };
14
+ postgresql?: {
15
+ migrationDirectory: string;
16
+ runtimeWorkdir?: string;
17
+ image?: string;
18
+ database?: string;
19
+ user?: string;
20
+ };
14
21
  baseline: {
15
22
  artifactDirectory?: string;
16
23
  sanitizationPolicy: string;
17
24
  };
25
+ containerRuntime?: {
26
+ autoStartColima?: boolean;
27
+ };
28
+ cleanup?: {
29
+ retainBaselineGenerations?: number;
30
+ };
18
31
  application: {
19
32
  startCommand: string;
20
33
  proofCommand: string;
@@ -22,11 +35,12 @@ export interface RehearsalConfig {
22
35
  runtimeAdapter?: string;
23
36
  };
24
37
  runtime: {
38
+ target?: "supabase" | "postgresql";
25
39
  applicationUrl?: string;
26
40
  projectId?: string;
27
- apiPort: number;
41
+ apiPort?: number;
28
42
  databasePort: number;
29
- studioPort: number;
43
+ studioPort?: number;
30
44
  };
31
45
  safety?: {
32
46
  allowedHosts?: string[];
@@ -58,6 +72,13 @@ export declare const renderDetectedConfig: (
58
72
  ports?: { api: number; database: number; studio: number };
59
73
  },
60
74
  ) => string;
75
+ export declare const renderDetectedPostgresqlConfig: (
76
+ detected: unknown,
77
+ options?: {
78
+ applicationUrl?: string;
79
+ databasePort?: number;
80
+ },
81
+ ) => string;
61
82
  export declare const SANITIZATION_ACTIONS: Readonly<{
62
83
  KEEP: "KEEP";
63
84
  PSEUDONYMIZE: "PSEUDONYMIZE";
@@ -7,6 +7,7 @@
7
7
  import { access, readFile, stat } from "node:fs/promises";
8
8
  import { basename, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { pathToFileURL } from "node:url";
10
+ import { resolveRuntimeTarget } from "../runtime/runtime_target.mjs";
10
11
 
11
12
  export {
12
13
  EXCLUDED_VALUE,
@@ -22,6 +23,8 @@ export const REHEARSAL_CONFIG_VERSION = 1;
22
23
 
23
24
  export const REHEARSAL_DEFAULTS = Object.freeze({
24
25
  baseline: Object.freeze({ artifactDirectory: ".rehearsal" }),
26
+ containerRuntime: Object.freeze({ autoStartColima: true }),
27
+ cleanup: Object.freeze({ retainBaselineGenerations: 2 }),
25
28
  runtime: Object.freeze({
26
29
  applicationUrl: "http://localhost:5175",
27
30
  projectId: "rehearsal-local",
@@ -29,6 +32,11 @@ export const REHEARSAL_DEFAULTS = Object.freeze({
29
32
  safety: Object.freeze({
30
33
  allowedHosts: Object.freeze(["127.0.0.1", "::1", "localhost"]),
31
34
  blockedEnvironmentVariables: Object.freeze([
35
+ "DATABASE_URL",
36
+ "PGHOST",
37
+ "PGPASSWORD",
38
+ "PGPORT",
39
+ "PGUSER",
32
40
  "SUPABASE_ACCESS_TOKEN",
33
41
  "SUPABASE_DB_PASSWORD",
34
42
  "SUPABASE_PROJECT_ID",
@@ -48,6 +56,8 @@ const CONFIG_FILENAMES = Object.freeze([
48
56
 
49
57
  const LOOPBACK_HOSTS = new Set(REHEARSAL_DEFAULTS.safety.allowedHosts);
50
58
  const IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}$/u;
59
+ const DATABASE_IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]{0,62}$/u;
60
+ const POSTGRES_IMAGE_PATTERN = /^postgres:\d+(?:\.\d+)?-alpine$/u;
51
61
  const SAFE_COMMAND_PATTERN = /^[^\n\r\0]+$/u;
52
62
 
53
63
  const isPlainObject = (value) =>
@@ -103,6 +113,20 @@ const assertPort = (value, path) => {
103
113
  return value;
104
114
  };
105
115
 
116
+ const assertBoolean = (value, path) => {
117
+ if (typeof value !== "boolean") {
118
+ throw new Error(`${path} must be true or false.`);
119
+ }
120
+ return value;
121
+ };
122
+
123
+ const assertPositiveInteger = (value, path) => {
124
+ if (!Number.isSafeInteger(value) || value < 1) {
125
+ throw new Error(`${path} must be a positive integer.`);
126
+ }
127
+ return value;
128
+ };
129
+
106
130
  const assertStringArray = (value, path) => {
107
131
  if (
108
132
  !Array.isArray(value) ||
@@ -141,7 +165,10 @@ const normalizeConfig = (input) => {
141
165
  "schemaVersion",
142
166
  "project",
143
167
  "supabase",
168
+ "postgresql",
144
169
  "baseline",
170
+ "containerRuntime",
171
+ "cleanup",
145
172
  "application",
146
173
  "runtime",
147
174
  "safety",
@@ -164,25 +191,63 @@ const normalizeConfig = (input) => {
164
191
  );
165
192
  }
166
193
 
167
- const supabase = assertPlainObject(root.supabase, "config.supabase");
168
- assertKnownKeys(
169
- supabase,
170
- [
171
- "workdir",
172
- "migrationDirectory",
173
- "rehearsalConfig",
174
- "runtimeWorkdir",
175
- "serviceEnvironmentFile",
176
- "serviceEnvironmentVariables",
177
- ],
178
- "config.supabase",
179
- );
194
+ const runtimeTarget = resolveRuntimeTarget(root.runtime?.target).id;
195
+ const supabase =
196
+ runtimeTarget === "supabase"
197
+ ? assertPlainObject(root.supabase, "config.supabase")
198
+ : null;
199
+ const postgresql =
200
+ runtimeTarget === "postgresql"
201
+ ? assertPlainObject(root.postgresql, "config.postgresql")
202
+ : null;
203
+ if (runtimeTarget === "supabase" && root.postgresql !== undefined) {
204
+ throw new Error(
205
+ "config.postgresql is not allowed when config.runtime.target is supabase.",
206
+ );
207
+ }
208
+ if (runtimeTarget === "postgresql" && root.supabase !== undefined) {
209
+ throw new Error(
210
+ "config.supabase is not allowed when config.runtime.target is postgresql.",
211
+ );
212
+ }
213
+ if (supabase) {
214
+ assertKnownKeys(
215
+ supabase,
216
+ [
217
+ "workdir",
218
+ "migrationDirectory",
219
+ "rehearsalConfig",
220
+ "runtimeWorkdir",
221
+ "serviceEnvironmentFile",
222
+ "serviceEnvironmentVariables",
223
+ ],
224
+ "config.supabase",
225
+ );
226
+ }
227
+ if (postgresql) {
228
+ assertKnownKeys(
229
+ postgresql,
230
+ ["migrationDirectory", "runtimeWorkdir", "image", "database", "user"],
231
+ "config.postgresql",
232
+ );
233
+ }
180
234
  const baseline = assertPlainObject(root.baseline, "config.baseline");
181
235
  assertKnownKeys(
182
236
  baseline,
183
237
  ["artifactDirectory", "sanitizationPolicy"],
184
238
  "config.baseline",
185
239
  );
240
+ const containerRuntime = assertPlainObject(
241
+ root.containerRuntime ?? {},
242
+ "config.containerRuntime",
243
+ );
244
+ assertKnownKeys(
245
+ containerRuntime,
246
+ ["autoStartColima"],
247
+ "config.containerRuntime",
248
+ );
249
+ const cleanup = assertPlainObject(root.cleanup ?? {}, "config.cleanup");
250
+ assertKnownKeys(cleanup, ["retainBaselineGenerations"], "config.cleanup");
186
251
  const application = assertPlainObject(root.application, "config.application");
187
252
  assertKnownKeys(
188
253
  application,
@@ -192,7 +257,14 @@ const normalizeConfig = (input) => {
192
257
  const runtime = assertPlainObject(root.runtime ?? {}, "config.runtime");
193
258
  assertKnownKeys(
194
259
  runtime,
195
- ["applicationUrl", "projectId", "apiPort", "databasePort", "studioPort"],
260
+ [
261
+ "target",
262
+ "applicationUrl",
263
+ "projectId",
264
+ "apiPort",
265
+ "databasePort",
266
+ "studioPort",
267
+ ],
196
268
  "config.runtime",
197
269
  );
198
270
  const safety = assertPlainObject(root.safety ?? {}, "config.safety");
@@ -234,9 +306,13 @@ const normalizeConfig = (input) => {
234
306
  }
235
307
 
236
308
  const ports = {
237
- api: assertPort(runtime.apiPort, "config.runtime.apiPort"),
309
+ ...(runtimeTarget === "supabase"
310
+ ? { api: assertPort(runtime.apiPort, "config.runtime.apiPort") }
311
+ : {}),
238
312
  database: assertPort(runtime.databasePort, "config.runtime.databasePort"),
239
- studio: assertPort(runtime.studioPort, "config.runtime.studioPort"),
313
+ ...(runtimeTarget === "supabase"
314
+ ? { studio: assertPort(runtime.studioPort, "config.runtime.studioPort") }
315
+ : {}),
240
316
  };
241
317
  if (new Set(Object.values(ports)).size !== Object.values(ports).length) {
242
318
  throw new Error("Rehearsal runtime ports must be unique.");
@@ -247,7 +323,7 @@ const normalizeConfig = (input) => {
247
323
  throw new Error("config.verification.commands must be an array.");
248
324
  }
249
325
  const serviceEnvironmentVariables =
250
- supabase.serviceEnvironmentVariables ?? [];
326
+ supabase?.serviceEnvironmentVariables ?? [];
251
327
  if (!Array.isArray(serviceEnvironmentVariables)) {
252
328
  throw new Error(
253
329
  "config.supabase.serviceEnvironmentVariables must be an array.",
@@ -269,41 +345,105 @@ const normalizeConfig = (input) => {
269
345
  );
270
346
  }
271
347
  if (
272
- Boolean(supabase.serviceEnvironmentFile) !==
348
+ Boolean(supabase?.serviceEnvironmentFile) !==
273
349
  Boolean(normalizedServiceEnvironmentVariables.length)
274
350
  ) {
275
351
  throw new Error(
276
352
  "config.supabase.serviceEnvironmentFile and serviceEnvironmentVariables must be configured together.",
277
353
  );
278
354
  }
355
+ const authenticationProviders = safety.authenticationProviders
356
+ ? assertStringArray(
357
+ safety.authenticationProviders,
358
+ "config.safety.authenticationProviders",
359
+ )
360
+ : [];
361
+ if (runtimeTarget === "postgresql" && authenticationProviders.length > 0) {
362
+ throw new Error(
363
+ "config.safety.authenticationProviders is available only for the Supabase target.",
364
+ );
365
+ }
279
366
 
280
367
  return Object.freeze({
281
368
  schemaVersion: REHEARSAL_CONFIG_VERSION,
282
369
  project: Object.freeze({ name: projectName }),
283
- supabase: Object.freeze({
284
- workdir: assertRelativePath(supabase.workdir, "config.supabase.workdir"),
285
- migrationDirectory: assertRelativePath(
286
- supabase.migrationDirectory,
287
- "config.supabase.migrationDirectory",
288
- ),
289
- rehearsalConfig: assertRelativePath(
290
- supabase.rehearsalConfig,
291
- "config.supabase.rehearsalConfig",
292
- ),
293
- runtimeWorkdir: assertRelativePath(
294
- supabase.runtimeWorkdir,
295
- "config.supabase.runtimeWorkdir",
296
- ),
297
- serviceEnvironmentFile: supabase.serviceEnvironmentFile
298
- ? assertRelativePath(
299
- supabase.serviceEnvironmentFile,
300
- "config.supabase.serviceEnvironmentFile",
301
- )
302
- : null,
303
- serviceEnvironmentVariables: Object.freeze(
304
- normalizedServiceEnvironmentVariables,
305
- ),
306
- }),
370
+ supabase: supabase
371
+ ? Object.freeze({
372
+ workdir: assertRelativePath(
373
+ supabase.workdir,
374
+ "config.supabase.workdir",
375
+ ),
376
+ migrationDirectory: assertRelativePath(
377
+ supabase.migrationDirectory,
378
+ "config.supabase.migrationDirectory",
379
+ ),
380
+ rehearsalConfig: assertRelativePath(
381
+ supabase.rehearsalConfig,
382
+ "config.supabase.rehearsalConfig",
383
+ ),
384
+ runtimeWorkdir: assertRelativePath(
385
+ supabase.runtimeWorkdir,
386
+ "config.supabase.runtimeWorkdir",
387
+ ),
388
+ serviceEnvironmentFile: supabase.serviceEnvironmentFile
389
+ ? assertRelativePath(
390
+ supabase.serviceEnvironmentFile,
391
+ "config.supabase.serviceEnvironmentFile",
392
+ )
393
+ : null,
394
+ serviceEnvironmentVariables: Object.freeze(
395
+ normalizedServiceEnvironmentVariables,
396
+ ),
397
+ })
398
+ : null,
399
+ postgresql: postgresql
400
+ ? Object.freeze({
401
+ migrationDirectory: assertRelativePath(
402
+ postgresql.migrationDirectory,
403
+ "config.postgresql.migrationDirectory",
404
+ ),
405
+ runtimeWorkdir: assertRelativePath(
406
+ postgresql.runtimeWorkdir ?? ".rehearsal/runtime",
407
+ "config.postgresql.runtimeWorkdir",
408
+ ),
409
+ image: (() => {
410
+ const image = assertNonEmptyString(
411
+ postgresql.image ?? "postgres:17-alpine",
412
+ "config.postgresql.image",
413
+ );
414
+ if (!POSTGRES_IMAGE_PATTERN.test(image)) {
415
+ throw new Error(
416
+ "config.postgresql.image must name an official versioned Alpine PostgreSQL image, such as postgres:17-alpine.",
417
+ );
418
+ }
419
+ return image;
420
+ })(),
421
+ database: (() => {
422
+ const database = assertNonEmptyString(
423
+ postgresql.database ?? "postgres",
424
+ "config.postgresql.database",
425
+ );
426
+ if (!DATABASE_IDENTIFIER_PATTERN.test(database)) {
427
+ throw new Error(
428
+ "config.postgresql.database must be a safe lowercase PostgreSQL identifier.",
429
+ );
430
+ }
431
+ return database;
432
+ })(),
433
+ user: (() => {
434
+ const user = assertNonEmptyString(
435
+ postgresql.user ?? "postgres",
436
+ "config.postgresql.user",
437
+ );
438
+ if (!DATABASE_IDENTIFIER_PATTERN.test(user)) {
439
+ throw new Error(
440
+ "config.postgresql.user must be a safe lowercase PostgreSQL identifier.",
441
+ );
442
+ }
443
+ return user;
444
+ })(),
445
+ })
446
+ : null,
307
447
  baseline: Object.freeze({
308
448
  artifactDirectory: assertRelativePath(
309
449
  baseline.artifactDirectory ??
@@ -315,6 +455,20 @@ const normalizeConfig = (input) => {
315
455
  "config.baseline.sanitizationPolicy",
316
456
  ),
317
457
  }),
458
+ containerRuntime: Object.freeze({
459
+ autoStartColima: assertBoolean(
460
+ containerRuntime.autoStartColima ??
461
+ REHEARSAL_DEFAULTS.containerRuntime.autoStartColima,
462
+ "config.containerRuntime.autoStartColima",
463
+ ),
464
+ }),
465
+ cleanup: Object.freeze({
466
+ retainBaselineGenerations: assertPositiveInteger(
467
+ cleanup.retainBaselineGenerations ??
468
+ REHEARSAL_DEFAULTS.cleanup.retainBaselineGenerations,
469
+ "config.cleanup.retainBaselineGenerations",
470
+ ),
471
+ }),
318
472
  application: Object.freeze({
319
473
  startCommand: assertCommand(
320
474
  application.startCommand,
@@ -337,6 +491,7 @@ const normalizeConfig = (input) => {
337
491
  ),
338
492
  }),
339
493
  runtime: Object.freeze({
494
+ target: runtimeTarget,
340
495
  applicationUrl: assertLoopbackUrl(
341
496
  runtime.applicationUrl ?? REHEARSAL_DEFAULTS.runtime.applicationUrl,
342
497
  "config.runtime.applicationUrl",
@@ -357,14 +512,7 @@ const normalizeConfig = (input) => {
357
512
  "config.safety.blockedEnvironmentVariables",
358
513
  ),
359
514
  ),
360
- authenticationProviders: Object.freeze(
361
- safety.authenticationProviders
362
- ? assertStringArray(
363
- safety.authenticationProviders,
364
- "config.safety.authenticationProviders",
365
- )
366
- : [],
367
- ),
515
+ authenticationProviders: Object.freeze(authenticationProviders),
368
516
  hostedAccess: "disabled",
369
517
  outboundNetwork: "deny",
370
518
  }),
@@ -428,14 +576,22 @@ export const loadRehearsalConfig = async ({
428
576
  applicationEnvironment: resolveOwnedPath(
429
577
  config.application.environmentFile,
430
578
  ),
431
- migrationDirectory: resolveOwnedPath(config.supabase.migrationDirectory),
432
- rehearsalConfig: resolveOwnedPath(config.supabase.rehearsalConfig),
433
- runtimeWorkdir: resolveOwnedPath(config.supabase.runtimeWorkdir),
434
- serviceEnvironment: config.supabase.serviceEnvironmentFile
579
+ migrationDirectory: resolveOwnedPath(
580
+ (config.supabase ?? config.postgresql).migrationDirectory,
581
+ ),
582
+ rehearsalConfig: config.supabase
583
+ ? resolveOwnedPath(config.supabase.rehearsalConfig)
584
+ : null,
585
+ runtimeWorkdir: resolveOwnedPath(
586
+ (config.supabase ?? config.postgresql).runtimeWorkdir,
587
+ ),
588
+ serviceEnvironment: config.supabase?.serviceEnvironmentFile
435
589
  ? resolveOwnedPath(config.supabase.serviceEnvironmentFile)
436
590
  : null,
437
591
  sanitizationPolicy: resolveOwnedPath(config.baseline.sanitizationPolicy),
438
- supabaseWorkdir: resolveOwnedPath(config.supabase.workdir),
592
+ supabaseWorkdir: config.supabase
593
+ ? resolveOwnedPath(config.supabase.workdir)
594
+ : null,
439
595
  runtimeAdapter: config.application.runtimeAdapter
440
596
  ? resolveOwnedPath(config.application.runtimeAdapter)
441
597
  : null,
@@ -448,7 +604,7 @@ export const loadRehearsalConfig = async ({
448
604
  const requiredRuntimeWorkdir = join(paths.artifactDirectory, "runtime");
449
605
  if (paths.runtimeWorkdir !== requiredRuntimeWorkdir) {
450
606
  throw new Error(
451
- "config.supabase.runtimeWorkdir must resolve to the runtime directory inside config.baseline.artifactDirectory.",
607
+ `config.${config.runtime.target}.runtimeWorkdir must resolve to the runtime directory inside config.baseline.artifactDirectory.`,
452
608
  );
453
609
  }
454
610
  const environmentRelativePath = relative(
@@ -496,6 +652,13 @@ export const inspectDetectedProject = async ({
496
652
  ? "yarn"
497
653
  : "npm";
498
654
  const scripts = packageJson.scripts ?? {};
655
+ const postgresqlMigrationDirectory = (
656
+ await Promise.all(
657
+ ["database/migrations", "db/migrations", "migrations"].map(
658
+ async (path) => ({ path, exists: await hasPath(path) }),
659
+ ),
660
+ )
661
+ ).find(({ exists }) => exists)?.path;
499
662
  const normalizedProjectName = String(packageJson.name ?? basename(root))
500
663
  .toLowerCase()
501
664
  .replace(/[^a-z0-9-]+/gu, "-")
@@ -508,6 +671,9 @@ export const inspectDetectedProject = async ({
508
671
  packageManager,
509
672
  hasSupabaseConfig: await hasPath("supabase/config.toml"),
510
673
  hasMigrations: await hasPath("supabase/migrations"),
674
+ hasPostgresqlMigrations: Boolean(postgresqlMigrationDirectory),
675
+ postgresqlMigrationDirectory:
676
+ postgresqlMigrationDirectory ?? "database/migrations",
511
677
  applicationCommand:
512
678
  ["dev:rehearsal", "dev", "start"]
513
679
  .find((name) => typeof scripts[name] === "string")
@@ -526,34 +692,70 @@ export const renderDetectedConfig = (
526
692
  applicationUrl = "http://localhost:5175",
527
693
  ports = { api: 58321, database: 58322, studio: 58323 },
528
694
  } = {},
529
- ) => `// @ts-check
695
+ ) =>
696
+ `// @ts-check
697
+ /**
698
+ * Rehearsal configuration for ${detected.projectName}.
699
+ * Generated from this project by \`npx rehearsal\` and safe to commit.
700
+ * Review the CHECK comments. Keep passwords, tokens, and production URLs out of this file.
701
+ * Docs: https://github.com/Ddupasquier/rehearsal-db/blob/main/docs/configuration.md
702
+ */
530
703
  import { defineRehearsalConfig } from "@rehearsal-db/core";
531
704
 
532
705
  export default defineRehearsalConfig({
706
+ // Configuration format. Rehearsal will explain if an upgrade is ever needed.
533
707
  schemaVersion: 1,
708
+ // Stable local name used in Rehearsal labels and reports.
534
709
  project: { name: ${JSON.stringify(detected.projectName)} },
710
+
711
+ // Project files Rehearsal reads. Every path stays inside this repository.
535
712
  supabase: {
536
713
  workdir: ".",
537
714
  migrationDirectory: "supabase/migrations",
538
715
  rehearsalConfig: "infrastructure/rehearsal/supabase/config.toml",
539
716
  runtimeWorkdir: ".rehearsal/runtime",
717
+
718
+ // Optional local-only identity-provider credentials. Enable both keys together.
719
+ // serviceEnvironmentFile: ".env.rehearsal-service.local",
720
+ // serviceEnvironmentVariables: ["LOCAL_IDP_CLIENT_ID", "LOCAL_IDP_SECRET"],
540
721
  },
722
+
723
+ // Immutable local baseline files. Rehearsal never reads production on your behalf.
541
724
  baseline: {
542
725
  artifactDirectory: ".rehearsal",
543
726
  sanitizationPolicy: "infrastructure/rehearsal/sanitization-policy.json",
544
727
  },
728
+
729
+ // Reuse any running Docker-compatible engine; start Colima only when needed.
730
+ containerRuntime: {
731
+ autoStartColima: true,
732
+ },
733
+
734
+ // Keep this many baseline generations, always including the active one.
735
+ cleanup: {
736
+ retainBaselineGenerations: 2,
737
+ },
738
+
545
739
  application: {
740
+ // CHECK: commands detected from package.json. Change them if they are not correct.
546
741
  startCommand: ${JSON.stringify(detected.applicationCommand)},
547
742
  proofCommand: ${JSON.stringify(detected.verificationCommand)},
548
743
  environmentFile: ".rehearsal/runtime.env",
744
+ // Optional project-specific restore hook:
745
+ // runtimeAdapter: "infrastructure/rehearsal/runtime-adapter.mjs",
549
746
  },
747
+
748
+ // Disposable local runtime identity, URL, and dedicated ports.
550
749
  runtime: {
750
+ target: "supabase",
551
751
  applicationUrl: ${JSON.stringify(applicationUrl)},
552
752
  projectId: "${detected.projectName}-rehearsal",
553
753
  apiPort: ${ports.api},
554
754
  databasePort: ${ports.database},
555
755
  studioPort: ${ports.studio},
556
756
  },
757
+
758
+ // Only local runtime URLs are accepted; common hosted credentials are quarantined.
557
759
  safety: {
558
760
  allowedHosts: ["127.0.0.1", "::1", "localhost"],
559
761
  blockedEnvironmentVariables: [
@@ -564,6 +766,82 @@ export default defineRehearsalConfig({
564
766
  hostedAccess: "disabled",
565
767
  outboundNetwork: "deny",
566
768
  },
567
- verification: { commands: [${JSON.stringify(detected.verificationCommand)}] },
568
769
  });
569
- `;
770
+ `.replaceAll("\t", " ");
771
+
772
+ export const renderDetectedPostgresqlConfig = (
773
+ detected,
774
+ { applicationUrl = "http://localhost:5175", databasePort = 58322 } = {},
775
+ ) =>
776
+ `// @ts-check
777
+ /**
778
+ * Rehearsal configuration for ${detected.projectName}.
779
+ * Generated from this project by \`npx rehearsal\` and safe to commit.
780
+ * Review the CHECK comments. Keep passwords, tokens, and production URLs out of this file.
781
+ * Docs: https://github.com/Ddupasquier/rehearsal-db/blob/main/docs/configuration.md
782
+ */
783
+ import { defineRehearsalConfig } from "@rehearsal-db/core";
784
+
785
+ export default defineRehearsalConfig({
786
+ // Configuration format. Rehearsal will explain if an upgrade is ever needed.
787
+ schemaVersion: 1,
788
+ // Stable local name used in Rehearsal labels and reports.
789
+ project: { name: ${JSON.stringify(detected.projectName)} },
790
+
791
+ // Disposable PostgreSQL settings. The image must already exist locally.
792
+ postgresql: {
793
+ migrationDirectory: ${JSON.stringify(detected.postgresqlMigrationDirectory)},
794
+ runtimeWorkdir: ".rehearsal/runtime",
795
+ image: "postgres:17-alpine",
796
+ database: "postgres",
797
+ user: "postgres",
798
+ },
799
+
800
+ // Immutable local baseline files. Rehearsal never reads production on your behalf.
801
+ baseline: {
802
+ artifactDirectory: ".rehearsal",
803
+ sanitizationPolicy: "infrastructure/rehearsal/sanitization-policy.json",
804
+ },
805
+
806
+ // Reuse any running Docker-compatible engine; start Colima only when needed.
807
+ containerRuntime: {
808
+ autoStartColima: true,
809
+ },
810
+
811
+ // Keep this many baseline generations, always including the active one.
812
+ cleanup: {
813
+ retainBaselineGenerations: 2,
814
+ },
815
+
816
+ application: {
817
+ // CHECK: commands detected from package.json. Change them if they are not correct.
818
+ startCommand: ${JSON.stringify(detected.applicationCommand)},
819
+ proofCommand: ${JSON.stringify(detected.verificationCommand)},
820
+ environmentFile: ".rehearsal/runtime.env",
821
+ // Optional project-specific restore hook:
822
+ // runtimeAdapter: "infrastructure/rehearsal/runtime-adapter.mjs",
823
+ },
824
+
825
+ // Disposable local runtime identity, URL, and dedicated database port.
826
+ runtime: {
827
+ target: "postgresql",
828
+ applicationUrl: ${JSON.stringify(applicationUrl)},
829
+ projectId: "${detected.projectName}-rehearsal",
830
+ databasePort: ${databasePort},
831
+ },
832
+
833
+ // Only local runtime URLs are accepted; common hosted credentials are quarantined.
834
+ safety: {
835
+ allowedHosts: ["127.0.0.1", "::1", "localhost"],
836
+ blockedEnvironmentVariables: [
837
+ "DATABASE_URL",
838
+ "PGHOST",
839
+ "PGPASSWORD",
840
+ "PGPORT",
841
+ "PGUSER",
842
+ ],
843
+ hostedAccess: "disabled",
844
+ outboundNetwork: "deny",
845
+ },
846
+ });
847
+ `.replaceAll("\t", " ");
@@ -7,7 +7,7 @@
7
7
  import { createHash } from "node:crypto";
8
8
 
9
9
  export const REHEARSAL_RESULT_VERSION = 1;
10
- export const REHEARSAL_VERSION = "0.1.0-beta.5";
10
+ export const REHEARSAL_VERSION = "0.1.0-beta.7";
11
11
 
12
12
  export const REHEARSAL_EXIT_CODES = Object.freeze({
13
13
  success: 0,
@@ -109,7 +109,9 @@ const inferCategory = (error) => {
109
109
  return "unsafe_environment";
110
110
  }
111
111
  if (
112
- /docker|supabase|runtime|command|enoent|node\.js|ports?\b/iu.test(message)
112
+ /docker|postgres(?:ql)?|supabase|runtime|command|enoent|node\.js|ports?\b/iu.test(
113
+ message,
114
+ )
113
115
  ) {
114
116
  return "runtime_dependency_failure";
115
117
  }