@rebasepro/server-postgres 0.13.1-canary.gf57a27e → 0.14.0

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 (103) hide show
  1. package/dist/PostgresBootstrapper.d.ts +26 -0
  2. package/dist/auth/services.d.ts +21 -0
  3. package/dist/{auth-users-columns-Dt9g712t.js → auth-users-columns-BfQHf9JE.js} +525 -63
  4. package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
  5. package/dist/{backup-service-Bww-Lg0s.js → backup-service-BH0Dzo_h.js} +2 -3
  6. package/dist/{backup-service-Bww-Lg0s.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
  7. package/dist/cli-output.d.ts +34 -0
  8. package/dist/data-transformer.d.ts +7 -2
  9. package/dist/data_driver-ULAyJEi9.js +193 -0
  10. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  11. package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
  12. package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
  13. package/dist/{ensure-collection-tables-DRxaUG96.js → ensure-collection-tables-CbvaGuVn.js} +89 -10
  14. package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
  15. package/dist/index.es.js +1310 -1060
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/{rls-bootstrap-sql-Bpv3nUZo.js → rls-bootstrap-sql-69hYT8nr.js} +2 -2
  18. package/dist/{rls-bootstrap-sql-Bpv3nUZo.js.map → rls-bootstrap-sql-69hYT8nr.js.map} +1 -1
  19. package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
  20. package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
  21. package/dist/schema/auth-schema.d.ts +102 -0
  22. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  23. package/dist/schema/doctor.d.ts +41 -25
  24. package/dist/schema/ensure-collection-policies.d.ts +33 -9
  25. package/dist/schema/ensure-collection-tables.d.ts +60 -6
  26. package/dist/schema/generate-drizzle-schema-logic.d.ts +9 -1
  27. package/dist/schema/introspect-db-inference.d.ts +8 -1
  28. package/dist/schema/introspect-db-logic.d.ts +49 -0
  29. package/dist/schema/introspect-db-project.d.ts +21 -0
  30. package/dist/schema/search-column.d.ts +49 -0
  31. package/dist/security/policy-drift.d.ts +34 -0
  32. package/dist/security/rls-enforcement.d.ts +8 -3
  33. package/dist/services/FetchService.d.ts +9 -0
  34. package/dist/services/PersistService.d.ts +21 -17
  35. package/dist/services/RelationService.d.ts +9 -57
  36. package/dist/services/RelationWriteService.d.ts +82 -0
  37. package/dist/services/collection-helpers.d.ts +42 -0
  38. package/dist/services/dataService.d.ts +2 -0
  39. package/dist/services/junction-writes.d.ts +82 -0
  40. package/dist/services/realtimeService.d.ts +137 -2
  41. package/dist/services/write-denial.d.ts +36 -0
  42. package/dist/{src-C_wvdMnl.js → src-DCdn3Val.js} +35 -3
  43. package/dist/src-DCdn3Val.js.map +1 -0
  44. package/dist/utils/drizzle-conditions.d.ts +54 -1
  45. package/dist/{websocket-D0TBU3ia.js → websocket-C8ZqVBiV.js} +75 -18
  46. package/dist/websocket-C8ZqVBiV.js.map +1 -0
  47. package/package.json +6 -6
  48. package/src/PostgresBackendDriver.ts +7 -3
  49. package/src/PostgresBootstrapper.ts +95 -9
  50. package/src/auth/ensure-tables.ts +27 -5
  51. package/src/auth/services.ts +82 -5
  52. package/src/backup/backup-cli.ts +59 -57
  53. package/src/cli-errors.ts +6 -6
  54. package/src/cli-helpers.ts +4 -4
  55. package/src/cli-output.ts +43 -0
  56. package/src/cli.ts +155 -147
  57. package/src/collections/buildRegistry.ts +3 -1
  58. package/src/data-transformer.ts +111 -25
  59. package/src/history/ensure-history-table.ts +2 -2
  60. package/src/schema/auth-schema.ts +17 -1
  61. package/src/schema/doctor-cli.ts +14 -65
  62. package/src/schema/doctor-policy-checks.ts +105 -0
  63. package/src/schema/doctor.ts +149 -72
  64. package/src/schema/ensure-collection-policies.ts +99 -6
  65. package/src/schema/ensure-collection-tables.ts +214 -17
  66. package/src/schema/generate-drizzle-schema-logic.ts +121 -65
  67. package/src/schema/generate-drizzle-schema.ts +11 -10
  68. package/src/schema/generate-postgres-ddl-logic.ts +28 -1
  69. package/src/schema/generate-postgres-ddl.ts +14 -13
  70. package/src/schema/generated-schema-staleness.ts +7 -5
  71. package/src/schema/introspect-db-inference.ts +9 -2
  72. package/src/schema/introspect-db-logic.ts +251 -75
  73. package/src/schema/introspect-db-project.ts +78 -0
  74. package/src/schema/introspect-db.ts +42 -25
  75. package/src/schema/introspect-runtime.ts +14 -2
  76. package/src/schema/search-column.ts +85 -0
  77. package/src/security/policy-drift.test.ts +104 -3
  78. package/src/security/policy-drift.ts +129 -7
  79. package/src/security/rls-enforcement.ts +9 -4
  80. package/src/services/FetchService.ts +105 -7
  81. package/src/services/PersistService.ts +68 -42
  82. package/src/services/RelationService.ts +35 -695
  83. package/src/services/RelationWriteService.ts +653 -0
  84. package/src/services/cdc/trigger-cdc.ts +5 -1
  85. package/src/services/channel-history.ts +9 -3
  86. package/src/services/channel-presence.ts +10 -3
  87. package/src/services/collection-helpers.ts +89 -4
  88. package/src/services/dataService.ts +2 -0
  89. package/src/services/junction-writes.ts +295 -0
  90. package/src/services/pg-notify-listener.ts +1 -1
  91. package/src/services/realtimeService.ts +337 -82
  92. package/src/services/write-denial.ts +55 -0
  93. package/src/utils/drizzle-conditions.ts +211 -34
  94. package/src/utils/pg-error-utils.ts +8 -3
  95. package/src/websocket.ts +113 -16
  96. package/dist/auth-users-columns-Dt9g712t.js.map +0 -1
  97. package/dist/ensure-collection-policies-CwYUliAa.js +0 -57
  98. package/dist/ensure-collection-policies-CwYUliAa.js.map +0 -1
  99. package/dist/ensure-collection-tables-DRxaUG96.js.map +0 -1
  100. package/dist/policy-CPkCqVTz.js +0 -105
  101. package/dist/policy-CPkCqVTz.js.map +0 -1
  102. package/dist/src-C_wvdMnl.js.map +0 -1
  103. package/dist/websocket-D0TBU3ia.js.map +0 -1
package/src/cli.ts CHANGED
@@ -5,6 +5,7 @@ import path from "path";
5
5
  import fs from "fs";
6
6
  import { fileURLToPath } from "url";
7
7
  import { logger } from "@rebasepro/server";
8
+ import { out, outWarn, outError } from "./cli-output";
8
9
  import { formatRelativeTime } from "@rebasepro/utils";
9
10
  import {
10
11
  diagnoseMissingBin,
@@ -41,7 +42,7 @@ async function loadEnv(): Promise<void> {
41
42
 
42
43
  for (const p of envPaths) {
43
44
  if (fs.existsSync(p)) {
44
- const parsed = dotenv.config({ path: p });
45
+ const parsed = dotenv.config({ path: p, quiet: true });
45
46
  if (parsed.parsed) {
46
47
  for (const [key, val] of Object.entries(parsed.parsed)) {
47
48
  if (process.env[key] === undefined) {
@@ -58,6 +59,13 @@ async function loadEnv(): Promise<void> {
58
59
  }
59
60
 
60
61
  export async function runPluginCommand(args: string[]) {
62
+ // Also set as an environment variable, not just per `config()` call: this
63
+ // command spawns `tsx` subprocesses (the schema generator, the DDL
64
+ // generator, doctor) that load `.env` themselves, and they inherit this.
65
+ // dotenv reads it before `options.quiet`; an explicit `false` still wins.
66
+ if (process.env.DOTENV_CONFIG_QUIET === undefined) {
67
+ process.env.DOTENV_CONFIG_QUIET = "true";
68
+ }
61
69
  await loadEnv();
62
70
  const domain = args[0]; // "db" or "schema"
63
71
  const subcommand = args[1];
@@ -69,7 +77,7 @@ export async function runPluginCommand(args: string[]) {
69
77
  } else if (domain === "doctor") {
70
78
  await doctorPluginCommand(args);
71
79
  } else {
72
- logger.error(chalk.red(`Unknown domain command: ${domain}`));
80
+ outError(chalk.red(`Unknown domain command: ${domain}`));
73
81
  process.exit(1);
74
82
  }
75
83
  }
@@ -84,7 +92,7 @@ function listMigrationFiles(): string[] {
84
92
  async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
85
93
  const VALID_ACTIONS = ["push", "generate", "migrate", "branch", "backup", "restore", "backups"];
86
94
  if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
87
- logger.error(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
95
+ outError(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
88
96
  process.exit(1);
89
97
  }
90
98
 
@@ -117,15 +125,15 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
117
125
  const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
118
126
 
119
127
  if (subcommand === "generate") {
120
- logger.info("");
121
- logger.info(chalk.bold(" 📦 Rebase DB Generate"));
122
- logger.info(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
123
- logger.info("");
128
+ out("");
129
+ out(chalk.bold(" 📦 Rebase DB Generate"));
130
+ out(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
131
+ out("");
124
132
  await schemaCommand("generate", rawArgs);
125
133
  await generatePostgresDdlCommand(rawArgs);
126
- logger.info("");
127
- logger.info(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
128
- logger.info("");
134
+ out("");
135
+ out(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
136
+ out("");
129
137
  const migrationName = argsList._[0] || "migration";
130
138
  const migrationsBefore = listMigrationFiles();
131
139
  await runAtlas("migrate", ["diff", migrationName, "--dir", "file://drizzle/migrations", "--to", "file://drizzle/schema.sql"], collectionsPath);
@@ -165,12 +173,12 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
165
173
  const searchContent = readSearchDdl();
166
174
  if (searchContent && wroteNewMigration) {
167
175
  migrationContent = `${migrationContent}\n\n${searchContent}`;
168
- logger.info(chalk.gray(" ✓ Appended search DDL to the migration"));
176
+ out(chalk.gray(" ✓ Appended search DDL to the migration"));
169
177
  } else if (searchContent) {
170
178
  // Reachable whenever a `search` block is the *only*
171
179
  // thing that changed: Atlas cannot see one, so it finds
172
180
  // nothing to diff and writes no file.
173
- logger.info(chalk.gray(
181
+ out(chalk.gray(
174
182
  " ℹ Search DDL not written to a migration — this change produced none.\n" +
175
183
  " It is applied by `rebase db push`, and at boot by the schema ensure.\n" +
176
184
  " For a migration-only deployment, add drizzle/search.sql to a migration by hand."
@@ -187,35 +195,35 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
187
195
  if (fs.existsSync(policiesFile)) {
188
196
  const policiesContent = fs.readFileSync(policiesFile, "utf-8");
189
197
  fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
190
- logger.info(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
198
+ out(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
191
199
 
192
200
  // Re-hash the migration directory
193
- logger.info(chalk.gray(" Re-hashing migration files..."));
201
+ out(chalk.gray(" Re-hashing migration files..."));
194
202
  await runAtlas("migrate", ["hash", "--dir", "file://drizzle/migrations"], collectionsPath);
195
- logger.info(chalk.gray(" ✓ Migration directory checksum updated successfully."));
203
+ out(chalk.gray(" ✓ Migration directory checksum updated successfully."));
196
204
  }
197
205
  }
198
206
  }
199
207
  } catch (err) {
200
- logger.warn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
208
+ outWarn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
201
209
  }
202
210
 
203
- logger.info("");
204
- logger.info(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
205
- logger.info("");
211
+ out("");
212
+ out(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
213
+ out("");
206
214
  } else {
207
- logger.info("");
208
- logger.info(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
209
- logger.info("");
215
+ out("");
216
+ out(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
217
+ out("");
210
218
 
211
219
  if (subcommand === "push") {
212
- logger.info(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
213
- logger.info("");
220
+ out(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
221
+ out("");
214
222
  await schemaCommand("generate", rawArgs);
215
223
  await generatePostgresDdlCommand(rawArgs);
216
- logger.info("");
217
- logger.info(chalk.gray(" Step 2/3: Pushing schema to database with Atlas..."));
218
- logger.info("");
224
+ out("");
225
+ out(chalk.gray(" Step 2/3: Pushing schema to database with Atlas..."));
226
+ out("");
219
227
  const databaseUrl = process.env.DATABASE_URL;
220
228
  if (databaseUrl) {
221
229
  await ensureAuthSchemaAndFunctions(databaseUrl);
@@ -240,19 +248,19 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
240
248
  });
241
249
 
242
250
  if (destructive.length > 0) {
243
- logger.warn(chalk.yellow(` ⚠️ This push includes ${destructive.length} destructive change(s) that will DESTROY data:`));
244
- logger.warn("");
251
+ outWarn(chalk.yellow(` ⚠️ This push includes ${destructive.length} destructive change(s) that will DESTROY data:`));
252
+ outWarn("");
245
253
  for (const d of destructive) {
246
- logger.warn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
254
+ outWarn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
247
255
  }
248
- logger.warn("");
249
- logger.warn(chalk.yellow(" Full planned changes:"));
250
- logger.warn(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
251
- logger.warn("");
256
+ outWarn("");
257
+ outWarn(chalk.yellow(" Full planned changes:"));
258
+ outWarn(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
259
+ outWarn("");
252
260
 
253
261
  if (decision === "refuse") {
254
- logger.error(chalk.red(" ✗ Aborting: destructive changes require confirmation."));
255
- logger.error(chalk.gray(" Re-run interactively, or pass --allow-destructive to proceed. Back up first: rebase db backup"));
262
+ outError(chalk.red(" ✗ Aborting: destructive changes require confirmation."));
263
+ outError(chalk.gray(" Re-run interactively, or pass --allow-destructive to proceed. Back up first: rebase db backup"));
256
264
  process.exit(1);
257
265
  }
258
266
  if (decision === "confirm") {
@@ -260,17 +268,17 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
260
268
  chalk.yellow(" Type 'yes' to apply these destructive changes (this cannot be undone): ")
261
269
  );
262
270
  if (!confirmed) {
263
- logger.info(chalk.gray(" Aborted. No changes were made."));
271
+ out(chalk.gray(" Aborted. No changes were made."));
264
272
  process.exit(1);
265
273
  }
266
274
  } else {
267
275
  // decision === "apply" via --allow-destructive
268
- logger.warn(chalk.yellow(" Proceeding because --allow-destructive was passed."));
276
+ outWarn(chalk.yellow(" Proceeding because --allow-destructive was passed."));
269
277
  }
270
278
  }
271
279
 
272
280
  await runAtlas("schema", ["apply", "--to", "file://drizzle/schema.sql", "--auto-approve"], collectionsPath);
273
- logger.info("");
281
+ out("");
274
282
 
275
283
  if (databaseUrl) {
276
284
  await ensureAuthTables(databaseUrl, collectionsPath);
@@ -283,7 +291,7 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
283
291
  await ensureRlsUserRole(databaseUrl);
284
292
  await retireLegacyAuthSchema(databaseUrl);
285
293
  } else {
286
- logger.warn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
294
+ outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
287
295
  }
288
296
  } else if (subcommand === "migrate") {
289
297
  const databaseUrl = process.env.DATABASE_URL;
@@ -298,9 +306,9 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
298
306
  }
299
307
  }
300
308
 
301
- logger.info("");
302
- logger.info(chalk.green(` ✓ rebase db ${subcommand} completed successfully.`));
303
- logger.info("");
309
+ out("");
310
+ out(chalk.green(` ✓ rebase db ${subcommand} completed successfully.`));
311
+ out("");
304
312
  }
305
313
  }
306
314
 
@@ -323,7 +331,7 @@ async function ensureAuthSchemaAndFunctions(databaseUrl: string): Promise<void>
323
331
  await client.end();
324
332
  }
325
333
  } catch (err) {
326
- logger.warn(chalk.yellow(` ⚠️ Failed to bootstrap the RLS helper functions: ${err instanceof Error ? err.message : String(err)}`));
334
+ outWarn(chalk.yellow(` ⚠️ Failed to bootstrap the RLS helper functions: ${err instanceof Error ? err.message : String(err)}`));
327
335
  }
328
336
  }
329
337
 
@@ -360,7 +368,7 @@ async function ensureAuthTables(databaseUrl: string, collectionsPath: string): P
360
368
  await client.end();
361
369
  }
362
370
  } catch (err) {
363
- logger.warn(chalk.yellow(` ⚠️ Failed to ensure framework auth tables: ${err instanceof Error ? err.message : String(err)}`));
371
+ outWarn(chalk.yellow(` ⚠️ Failed to ensure framework auth tables: ${err instanceof Error ? err.message : String(err)}`));
364
372
  }
365
373
  }
366
374
 
@@ -391,7 +399,7 @@ async function ensureRlsUserRole(databaseUrl: string): Promise<void> {
391
399
  } catch (err) {
392
400
  // A module-resolution failure here is a build problem, not a database
393
401
  // one, and it is exactly the case that used to produce the silent exit.
394
- logger.error(chalk.red(
402
+ outError(chalk.red(
395
403
  `\n ✗ Could not load the RLS provisioning module: ${err instanceof Error ? err.message : String(err)}`
396
404
  ));
397
405
  return;
@@ -406,17 +414,17 @@ async function ensureRlsUserRole(databaseUrl: string): Promise<void> {
406
414
  const posture = await rls.detectConnectionPosture(runSql);
407
415
  if (posture.privileged) {
408
416
  await rls.ensureAppRole(runSql, schemas);
409
- logger.info(chalk.gray(` ✓ RLS role "${rls.REBASE_USER_ROLE}" provisioned/refreshed.`));
417
+ out(chalk.gray(` ✓ RLS role "${rls.REBASE_USER_ROLE}" provisioned/refreshed.`));
410
418
  }
411
419
  } finally {
412
420
  await client.end();
413
421
  }
414
422
  } catch (err) {
415
- logger.error(chalk.red(
423
+ outError(chalk.red(
416
424
  `\n ✗ The schema change was applied, but the "${rls.REBASE_USER_ROLE}" role could not be ` +
417
425
  `provisioned: ${err instanceof Error ? err.message : String(err)}`
418
426
  ));
419
- logger.error(chalk.gray(rls.appRoleSetupInstructions("your database user", schemas)));
427
+ outError(chalk.gray(rls.appRoleSetupInstructions("your database user", schemas)));
420
428
  }
421
429
  }
422
430
 
@@ -438,8 +446,8 @@ async function retireLegacyAuthSchema(databaseUrl: string): Promise<void> {
438
446
  await dropLegacyAuthSchema(
439
447
  async (text) => (await client.query(text)).rows as Record<string, unknown>[],
440
448
  {
441
- info: (m) => logger.info(chalk.gray(` ${m}`)),
442
- warn: (m) => logger.warn(chalk.yellow(` ⚠️ ${m}`))
449
+ info: (m) => out(chalk.gray(` ${m}`)),
450
+ warn: (m) => outWarn(chalk.yellow(` ⚠️ ${m}`))
443
451
  }
444
452
  );
445
453
  } finally {
@@ -455,8 +463,8 @@ async function applyPolicies(databaseUrl: string): Promise<void> {
455
463
  const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
456
464
  if (!fs.existsSync(policiesPath)) return;
457
465
 
458
- logger.info(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
459
- logger.info("");
466
+ out(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
467
+ out("");
460
468
 
461
469
  const policiesContent = fs.readFileSync(policiesPath, "utf-8");
462
470
  const { Client } = await import("pg");
@@ -464,16 +472,16 @@ async function applyPolicies(databaseUrl: string): Promise<void> {
464
472
  await client.connect();
465
473
  try {
466
474
  await client.query(policiesContent);
467
- logger.info(chalk.green(" ✓ RLS policies applied successfully."));
475
+ out(chalk.green(" ✓ RLS policies applied successfully."));
468
476
  } finally {
469
477
  await client.end();
470
478
  }
471
479
  } catch (err) {
472
480
  const hint = diagnoseDbError(err, databaseUrl);
473
481
  if (hint) {
474
- logger.error(hint);
482
+ outError(hint);
475
483
  } else {
476
- logger.error(chalk.red(` ✗ Failed to apply RLS policies: ${err instanceof Error ? err.message : String(err)}`));
484
+ outError(chalk.red(` ✗ Failed to apply RLS policies: ${err instanceof Error ? err.message : String(err)}`));
477
485
  }
478
486
  process.exit(1);
479
487
  }
@@ -507,34 +515,34 @@ async function reconcilePolicies(databaseUrl: string, collectionsPath: string):
507
515
  const { dropped, kept } = await dropOrphanedPolicies(client as never, drift, collections);
508
516
 
509
517
  for (const p of dropped) {
510
- logger.info(chalk.gray(` ✓ Dropped superseded policy "${p.name}" on ${p.schema}.${p.table}`));
518
+ out(chalk.gray(` ✓ Dropped superseded policy "${p.name}" on ${p.schema}.${p.table}`));
511
519
  }
512
520
  if (dropped.length > 0) {
513
- logger.info(chalk.green(` ✓ Removed ${dropped.length} superseded RLS ${dropped.length === 1 ? "policy" : "policies"}.`));
521
+ out(chalk.green(` ✓ Removed ${dropped.length} superseded RLS ${dropped.length === 1 ? "policy" : "policies"}.`));
514
522
  }
515
523
 
516
524
  // Custom-named orphans are indistinguishable from policies someone
517
525
  // wrote in SQL deliberately, so they are reported, never dropped.
518
526
  if (kept.length > 0) {
519
- logger.warn(chalk.yellow(" ⚠️ Policies in the database that no collection describes:"));
527
+ outWarn(chalk.yellow(" ⚠️ Policies in the database that no collection describes:"));
520
528
  for (const p of kept) {
521
- logger.warn(chalk.yellow(` • ${p.schema}.${p.table} → "${p.name}" (${p.command} TO ${p.roles.join(", ")})`));
529
+ outWarn(chalk.yellow(` • ${p.schema}.${p.table} → "${p.name}" (${p.command} TO ${p.roles.join(", ")})`));
522
530
  }
523
- logger.warn(chalk.yellow(" These still grant access. Drop them by hand if they are stale."));
531
+ outWarn(chalk.yellow(" These still grant access. Drop them by hand if they are stale."));
524
532
  }
525
533
 
526
534
  // Missing/diverged are not push's to fix, but staying silent about
527
535
  // them is how a database ends up not matching its config.
528
536
  const remaining = { ...drift, orphaned: kept };
529
537
  if (hasDrift(remaining) && (remaining.missing.length > 0 || remaining.diverged.length > 0)) {
530
- logger.warn(chalk.yellow(" ⚠️ RLS policies do not match your collections:"));
531
- logger.warn(formatPolicyDrift({ ...remaining, orphaned: [] }));
538
+ outWarn(chalk.yellow(" ⚠️ RLS policies do not match your collections:"));
539
+ outWarn(formatPolicyDrift({ ...remaining, orphaned: [] }));
532
540
  }
533
541
  } finally {
534
542
  await client.end();
535
543
  }
536
544
  } catch (err) {
537
- logger.warn(chalk.yellow(` ⚠️ Could not reconcile RLS policies: ${err instanceof Error ? err.message : String(err)}`));
545
+ outWarn(chalk.yellow(` ⚠️ Could not reconcile RLS policies: ${err instanceof Error ? err.message : String(err)}`));
538
546
  }
539
547
  }
540
548
 
@@ -551,9 +559,9 @@ async function branchCommand(rawArgs: string[]): Promise<void> {
551
559
  const dotenv = await import("dotenv");
552
560
  const envPath = process.env.DOTENV_CONFIG_PATH;
553
561
  if (envPath) {
554
- dotenv.config({ path: envPath });
562
+ dotenv.config({ path: envPath, quiet: true });
555
563
  } else {
556
- dotenv.config();
564
+ dotenv.config({ quiet: true });
557
565
  }
558
566
  } catch {
559
567
  // dotenv may not be installed
@@ -561,7 +569,7 @@ async function branchCommand(rawArgs: string[]): Promise<void> {
561
569
 
562
570
  const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
563
571
  if (!databaseUrl) {
564
- logger.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
572
+ outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
565
573
  process.exit(1);
566
574
  }
567
575
 
@@ -585,8 +593,8 @@ max: 3 });
585
593
  case "create": {
586
594
  const name = rawArgs[3];
587
595
  if (!name) {
588
- logger.error(chalk.red("✗ Branch name is required."));
589
- logger.info(chalk.gray(" Usage: rebase db branch create <name> [--from <source>]"));
596
+ outError(chalk.red("✗ Branch name is required."));
597
+ out(chalk.gray(" Usage: rebase db branch create <name> [--from <source>]"));
590
598
  process.exit(1);
591
599
  }
592
600
  let source: string | undefined;
@@ -594,81 +602,81 @@ max: 3 });
594
602
  if (fromIdx !== -1 && rawArgs[fromIdx + 1]) {
595
603
  source = rawArgs[fromIdx + 1];
596
604
  }
597
- logger.info("");
598
- logger.info(chalk.bold(" 🌿 Creating database branch..."));
599
- logger.info(chalk.gray(` Name: ${name}`));
600
- if (source) logger.info(chalk.gray(` Source: ${source}`));
601
- logger.info("");
605
+ out("");
606
+ out(chalk.bold(" 🌿 Creating database branch..."));
607
+ out(chalk.gray(` Name: ${name}`));
608
+ if (source) out(chalk.gray(` Source: ${source}`));
609
+ out("");
602
610
  const branch = await branchService.createBranch(name, source ? { source } : undefined);
603
- logger.info(chalk.green(` ✓ Branch "${branch.name}" created successfully.`));
604
- logger.info(chalk.gray(` Database: rb_${branch.name}`));
605
- logger.info(chalk.gray(` Parent: ${branch.parentDatabase}`));
606
- logger.info("");
611
+ out(chalk.green(` ✓ Branch "${branch.name}" created successfully.`));
612
+ out(chalk.gray(` Database: rb_${branch.name}`));
613
+ out(chalk.gray(` Parent: ${branch.parentDatabase}`));
614
+ out("");
607
615
  break;
608
616
  }
609
617
 
610
618
  case "list": {
611
619
  const branches = await branchService.listBranches();
612
- logger.info("");
620
+ out("");
613
621
  if (branches.length === 0) {
614
- logger.info(chalk.gray(" No branches found. Create one with: rebase db branch create <name>"));
622
+ out(chalk.gray(" No branches found. Create one with: rebase db branch create <name>"));
615
623
  } else {
616
- logger.info(chalk.bold(` 🌿 ${branches.length} branch(es):`));
617
- logger.info("");
624
+ out(chalk.bold(` 🌿 ${branches.length} branch(es):`));
625
+ out("");
618
626
  for (const b of branches) {
619
627
  const size = b.sizeBytes != null
620
628
  ? chalk.gray(` (${formatBytes(b.sizeBytes)})`)
621
629
  : "";
622
630
  const age = chalk.gray(` — created ${timeAgo(b.createdAt)}`);
623
- logger.info(` ${chalk.green("●")} ${chalk.bold(b.name)}${size}${age}`);
624
- logger.info(chalk.gray(` from ${b.parentDatabase}`));
631
+ out(` ${chalk.green("●")} ${chalk.bold(b.name)}${size}${age}`);
632
+ out(chalk.gray(` from ${b.parentDatabase}`));
625
633
  }
626
634
  }
627
- logger.info("");
635
+ out("");
628
636
  break;
629
637
  }
630
638
 
631
639
  case "delete": {
632
640
  const name = rawArgs[3];
633
641
  if (!name) {
634
- logger.error(chalk.red("✗ Branch name is required."));
635
- logger.info(chalk.gray(" Usage: rebase db branch delete <name>"));
642
+ outError(chalk.red("✗ Branch name is required."));
643
+ out(chalk.gray(" Usage: rebase db branch delete <name>"));
636
644
  process.exit(1);
637
645
  }
638
- logger.info("");
639
- logger.info(chalk.bold(` 🗑️ Deleting branch "${name}"...`));
646
+ out("");
647
+ out(chalk.bold(` 🗑️ Deleting branch "${name}"...`));
640
648
  await branchService.deleteBranch(name);
641
- logger.info(chalk.green(` ✓ Branch "${name}" deleted.`));
642
- logger.info("");
649
+ out(chalk.green(` ✓ Branch "${name}" deleted.`));
650
+ out("");
643
651
  break;
644
652
  }
645
653
 
646
654
  case "info": {
647
655
  const name = rawArgs[3];
648
656
  if (!name) {
649
- logger.error(chalk.red("✗ Branch name is required."));
650
- logger.info(chalk.gray(" Usage: rebase db branch info <name>"));
657
+ outError(chalk.red("✗ Branch name is required."));
658
+ out(chalk.gray(" Usage: rebase db branch info <name>"));
651
659
  process.exit(1);
652
660
  }
653
661
  const info = await branchService.getBranchInfo(name);
654
- logger.info("");
662
+ out("");
655
663
  if (!info) {
656
- logger.error(chalk.red(` ✗ Branch "${name}" not found.`));
664
+ outError(chalk.red(` ✗ Branch "${name}" not found.`));
657
665
  } else {
658
- logger.info(chalk.bold(` 🌿 Branch: ${info.name}`));
659
- logger.info(chalk.gray(` Database: rb_${info.name}`));
660
- logger.info(chalk.gray(` Parent: ${info.parentDatabase}`));
661
- logger.info(chalk.gray(` Created: ${info.createdAt.toISOString()}`));
666
+ out(chalk.bold(` 🌿 Branch: ${info.name}`));
667
+ out(chalk.gray(` Database: rb_${info.name}`));
668
+ out(chalk.gray(` Parent: ${info.parentDatabase}`));
669
+ out(chalk.gray(` Created: ${info.createdAt.toISOString()}`));
662
670
  if (info.sizeBytes != null) {
663
- logger.info(chalk.gray(` Size: ${formatBytes(info.sizeBytes)}`));
671
+ out(chalk.gray(` Size: ${formatBytes(info.sizeBytes)}`));
664
672
  }
665
673
  }
666
- logger.info("");
674
+ out("");
667
675
  break;
668
676
  }
669
677
 
670
678
  default:
671
- logger.error(chalk.red(`Unknown branch action: "${branchAction}".`));
679
+ outError(chalk.red(`Unknown branch action: "${branchAction}".`));
672
680
  printBranchHelp();
673
681
  process.exit(1);
674
682
  }
@@ -679,7 +687,7 @@ max: 3 });
679
687
  }
680
688
 
681
689
  function printBranchHelp() {
682
- logger.info(`
690
+ out(`
683
691
  ${chalk.bold("rebase db branch")} — Database branching commands
684
692
 
685
693
  ${chalk.green.bold("Usage")}
@@ -734,28 +742,28 @@ async function runAtlas(
734
742
  // other — see `diagnoseMissingBin`. This used to say "Install it with:
735
743
  // pnpm add -D @ariga/atlas" unconditionally, which is the exact command
736
744
  // that produces the far more common of the two states.
737
- logger.error(chalk.red("\n✗ The atlas binary is missing, so the schema cannot be applied.\n"));
745
+ outError(chalk.red("\n✗ The atlas binary is missing, so the schema cannot be applied.\n"));
738
746
 
739
747
  if (diagnoseMissingBin("@ariga/atlas") === "build-script-blocked") {
740
- logger.error(chalk.yellow(" @ariga/atlas IS installed — only its binary is missing.\n"));
741
- logger.error(chalk.gray(
748
+ outError(chalk.yellow(" @ariga/atlas IS installed — only its binary is missing.\n"));
749
+ outError(chalk.gray(
742
750
  " It downloads that binary in a `preinstall` script, and pnpm 10+ does not\n" +
743
751
  " run a dependency's scripts unless you allow it. The install still exits 0,\n" +
744
752
  " so the only sign is `Ignored build scripts: @ariga/atlas` in its output.\n"
745
753
  ));
746
- logger.error(" Fix it with either:\n");
747
- logger.error(chalk.bold(" pnpm approve-builds\n"));
748
- logger.error(" or, to record it in the project (what `rebase init` scaffolds):\n");
749
- logger.error(chalk.bold(
754
+ outError(" Fix it with either:\n");
755
+ outError(chalk.bold(" pnpm approve-builds\n"));
756
+ outError(" or, to record it in the project (what `rebase init` scaffolds):\n");
757
+ outError(chalk.bold(
750
758
  " // package.json\n" +
751
759
  " \"pnpm\": { \"onlyBuiltDependencies\": [\"@ariga/atlas\"] }\n"
752
760
  ));
753
- logger.error(chalk.gray(" Then re-run `pnpm install`.\n"));
761
+ outError(chalk.gray(" Then re-run `pnpm install`.\n"));
754
762
  } else {
755
- logger.error(chalk.gray(" It is not installed in this project.\n"));
756
- logger.error(" Install it with:\n");
757
- logger.error(chalk.bold(" pnpm add -D @ariga/atlas\n"));
758
- logger.error(chalk.gray(
763
+ outError(chalk.gray(" It is not installed in this project.\n"));
764
+ outError(" Install it with:\n");
765
+ outError(chalk.bold(" pnpm add -D @ariga/atlas\n"));
766
+ outError(chalk.gray(
759
767
  " If pnpm then reports `Ignored build scripts`, also run `pnpm approve-builds` —\n" +
760
768
  " the package carries a `preinstall` script that fetches the binary.\n"
761
769
  ));
@@ -775,7 +783,7 @@ async function runAtlas(
775
783
 
776
784
  for (const p of envPaths) {
777
785
  if (fs.existsSync(p)) {
778
- const parsed = dotenv.config({ path: p });
786
+ const parsed = dotenv.config({ path: p, quiet: true });
779
787
  if (parsed.parsed) {
780
788
  for (const [key, val] of Object.entries(parsed.parsed)) {
781
789
  if (env[key] === undefined) {
@@ -792,7 +800,7 @@ async function runAtlas(
792
800
 
793
801
  const databaseUrl = env.DATABASE_URL;
794
802
  if (!databaseUrl) {
795
- logger.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
803
+ outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
796
804
  process.exit(1);
797
805
  }
798
806
 
@@ -852,11 +860,11 @@ async function runAtlas(
852
860
  excludes = await getTableExcludes(databaseUrl, collectionsPath);
853
861
  } catch (err) {
854
862
  if (err instanceof ExcludeIntrospectionError) {
855
- logger.error(chalk.red("\n✗ Aborting push: could not determine which tables to protect."));
856
- logger.error(chalk.gray(` ${err.message}`));
857
- logger.error(chalk.gray(" Refusing to apply — a partial exclude list could drop tables Rebase does not manage."));
863
+ outError(chalk.red("\n✗ Aborting push: could not determine which tables to protect."));
864
+ outError(chalk.gray(` ${err.message}`));
865
+ outError(chalk.gray(" Refusing to apply — a partial exclude list could drop tables Rebase does not manage."));
858
866
  const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
859
- if (hint) logger.error(hint);
867
+ if (hint) outError(hint);
860
868
  process.exit(1);
861
869
  }
862
870
  throw err;
@@ -891,11 +899,11 @@ async function runAtlas(
891
899
  try {
892
900
  await subprocess;
893
901
  } catch {
894
- logger.error(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
902
+ outError(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
895
903
  // Surface actionable recovery guidance for recognized failures.
896
904
  const hint = diagnoseDbError({ message: stderrText }, databaseUrl);
897
905
  if (hint) {
898
- logger.error(hint);
906
+ outError(hint);
899
907
  }
900
908
  process.exit(1);
901
909
  }
@@ -925,7 +933,7 @@ async function generatePostgresDdlCommand(rawArgs: string[]): Promise<void> {
925
933
  const ddlScript = path.join(__cliDirname, "schema", "generate-postgres-ddl.ts");
926
934
  const tsxBin = resolveLocalBin("tsx");
927
935
  if (!tsxBin) {
928
- logger.error(chalk.red("✗ Could not find tsx binary."));
936
+ outError(chalk.red("✗ Could not find tsx binary."));
929
937
  process.exit(1);
930
938
  }
931
939
 
@@ -946,7 +954,7 @@ async function generatePostgresDdlCommand(rawArgs: string[]): Promise<void> {
946
954
  env: { ...process.env as Record<string, string> }
947
955
  });
948
956
  } catch (err: unknown) {
949
- logger.error(chalk.red(`✗ Failed to run Postgres DDL generator: ${err instanceof Error ? err.message : String(err)}`));
957
+ outError(chalk.red(`✗ Failed to run Postgres DDL generator: ${err instanceof Error ? err.message : String(err)}`));
950
958
  process.exit(1);
951
959
  }
952
960
  }
@@ -1005,23 +1013,23 @@ async function schemaStaleCommand(rawArgs: string[]): Promise<void> {
1005
1013
 
1006
1014
  if (stale.length === 0) return;
1007
1015
 
1008
- logger.info("");
1009
- logger.warn(chalk.yellow(
1016
+ out("");
1017
+ outWarn(chalk.yellow(
1010
1018
  ` ⚠️ ${outputPath} names ${stale.length} foreign key(s) the way an earlier release did:`
1011
1019
  ));
1012
- logger.info(chalk.gray(describeLegacyForeignKeyNames(stale)));
1013
- logger.info("");
1020
+ out(chalk.gray(describeLegacyForeignKeyNames(stale)));
1021
+ out("");
1014
1022
 
1015
1023
  if (!argsList["--fix"]) {
1016
- logger.error(chalk.red(
1024
+ outError(chalk.red(
1017
1025
  " The database column has already been renamed at boot, so the generated schema no " +
1018
1026
  "longer matches it and the server will refuse to start."
1019
1027
  ));
1020
- logger.error(chalk.red(" Run `rebase schema generate` to regenerate it."));
1028
+ outError(chalk.red(" Run `rebase schema generate` to regenerate it."));
1021
1029
  process.exit(1);
1022
1030
  }
1023
1031
 
1024
- logger.info(chalk.gray(" Regenerating the Drizzle schema so it matches..."));
1032
+ out(chalk.gray(" Regenerating the Drizzle schema so it matches..."));
1025
1033
  await schemaCommand("generate", ["schema", "generate", `--collections=${collectionsPath}`, `--output=${outputPath}`]);
1026
1034
  }
1027
1035
 
@@ -1051,13 +1059,13 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
1051
1059
  // If installed in node_modules, __cliDirname is node_modules/@rebasepro/server-postgres/dist or src.
1052
1060
  const generatorScript = path.join(__cliDirname, "schema", "generate-drizzle-schema.ts");
1053
1061
  if (!fs.existsSync(generatorScript)) {
1054
- logger.error(chalk.red(`✗ Could not find generate-drizzle-schema.ts at ${generatorScript}`));
1062
+ outError(chalk.red(`✗ Could not find generate-drizzle-schema.ts at ${generatorScript}`));
1055
1063
  process.exit(1);
1056
1064
  }
1057
1065
 
1058
1066
  const tsxBin = resolveLocalBin("tsx");
1059
1067
  if (!tsxBin) {
1060
- logger.error(chalk.red("✗ Could not find tsx binary."));
1068
+ outError(chalk.red("✗ Could not find tsx binary."));
1061
1069
  process.exit(1);
1062
1070
  }
1063
1071
 
@@ -1065,9 +1073,9 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
1065
1073
  const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
1066
1074
  const watch = argsList["--watch"] || false;
1067
1075
 
1068
- logger.info("");
1069
- logger.info(chalk.bold(" 🔧 Rebase Schema Generator"));
1070
- logger.info("");
1076
+ out("");
1077
+ out(chalk.bold(" 🔧 Rebase Schema Generator"));
1078
+ out("");
1071
1079
 
1072
1080
  const cmdParts = [
1073
1081
  tsxBin,
@@ -1086,7 +1094,7 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
1086
1094
  env: { ...process.env as Record<string, string> }
1087
1095
  });
1088
1096
  } catch (err: unknown) {
1089
- logger.error(chalk.red(`✗ Failed to run schema generator: ${err instanceof Error ? err.message : String(err)}`));
1097
+ outError(chalk.red(`✗ Failed to run schema generator: ${err instanceof Error ? err.message : String(err)}`));
1090
1098
  process.exit(1);
1091
1099
  }
1092
1100
  } else if (subcommand === "introspect") {
@@ -1108,21 +1116,21 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
1108
1116
 
1109
1117
  const introspectScript = path.join(__cliDirname, "schema", "introspect-db.ts");
1110
1118
  if (!fs.existsSync(introspectScript)) {
1111
- logger.error(chalk.red(`✗ Could not find introspect-db.ts at ${introspectScript}`));
1119
+ outError(chalk.red(`✗ Could not find introspect-db.ts at ${introspectScript}`));
1112
1120
  process.exit(1);
1113
1121
  }
1114
1122
 
1115
1123
  const tsxBin = resolveLocalBin("tsx");
1116
1124
  if (!tsxBin) {
1117
- logger.error(chalk.red("✗ Could not find tsx binary."));
1125
+ outError(chalk.red("✗ Could not find tsx binary."));
1118
1126
  process.exit(1);
1119
1127
  }
1120
1128
 
1121
1129
  const outputPath = argsList["--output"] || argsList["--collections"] || path.join("..", "config", "collections");
1122
1130
 
1123
- logger.info("");
1124
- logger.info(chalk.bold(" 🔍 Rebase Schema Introspector"));
1125
- logger.info("");
1131
+ out("");
1132
+ out(chalk.bold(" 🔍 Rebase Schema Introspector"));
1133
+ out("");
1126
1134
 
1127
1135
  const cmdParts = [
1128
1136
  tsxBin,
@@ -1139,11 +1147,11 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
1139
1147
  env: { ...process.env as Record<string, string> }
1140
1148
  });
1141
1149
  } catch (err: unknown) {
1142
- logger.error(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
1150
+ outError(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
1143
1151
  process.exit(1);
1144
1152
  }
1145
1153
  } else {
1146
- logger.error(chalk.red("Unknown schema command."));
1154
+ outError(chalk.red("Unknown schema command."));
1147
1155
  process.exit(1);
1148
1156
  }
1149
1157
  }
@@ -1167,13 +1175,13 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
1167
1175
 
1168
1176
  const doctorScript = path.join(__cliDirname, "schema", "doctor-cli.ts");
1169
1177
  if (!fs.existsSync(doctorScript)) {
1170
- logger.error(chalk.red(`✗ Could not find doctor.ts at ${doctorScript}`));
1178
+ outError(chalk.red(`✗ Could not find doctor.ts at ${doctorScript}`));
1171
1179
  process.exit(1);
1172
1180
  }
1173
1181
 
1174
1182
  const tsxBin = resolveLocalBin("tsx");
1175
1183
  if (!tsxBin) {
1176
- logger.error(chalk.red("✗ Could not find tsx binary."));
1184
+ outError(chalk.red("✗ Could not find tsx binary."));
1177
1185
  process.exit(1);
1178
1186
  }
1179
1187