@rebasepro/server-postgres 0.0.1-canary.4829d6e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +86 -0
- package/dist/PostgresAdapter.d.ts +6 -0
- package/dist/PostgresBackendDriver.d.ts +150 -0
- package/dist/PostgresBootstrapper.d.ts +51 -0
- package/dist/auth/ensure-tables.d.ts +10 -0
- package/dist/auth/services.d.ts +250 -0
- package/dist/backup/backup-cli.d.ts +3 -0
- package/dist/backup/backup-cron.d.ts +53 -0
- package/dist/backup/backup-service.d.ts +85 -0
- package/dist/backup/index.d.ts +12 -0
- package/dist/backup/pg-tools.d.ts +110 -0
- package/dist/backup/retention.d.ts +35 -0
- package/dist/chunk-DSJWtz9O.js +40 -0
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +7 -0
- package/dist/cli.d.ts +1 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +47 -0
- package/dist/connection.d.ts +65 -0
- package/dist/data-transformer.d.ts +55 -0
- package/dist/databasePoolManager.d.ts +20 -0
- package/dist/history/HistoryService.d.ts +71 -0
- package/dist/history/ensure-history-table.d.ts +7 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.es.js +23535 -0
- package/dist/index.es.js.map +1 -0
- package/dist/interfaces.d.ts +18 -0
- package/dist/schema/auth-bootstrap-sql.d.ts +24 -0
- package/dist/schema/auth-default-policies.d.ts +10 -0
- package/dist/schema/auth-schema.d.ts +2376 -0
- package/dist/schema/doctor-cli.d.ts +2 -0
- package/dist/schema/doctor.d.ts +58 -0
- package/dist/schema/dynamic-tables.d.ts +31 -0
- package/dist/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/introspect-db-inference.d.ts +5 -0
- package/dist/schema/introspect-db-logic.d.ts +118 -0
- package/dist/schema/introspect-db.d.ts +1 -0
- package/dist/schema/introspect-runtime.d.ts +57 -0
- package/dist/schema/test-schema.d.ts +24 -0
- package/dist/security/policy-drift.d.ts +57 -0
- package/dist/security/rls-enforcement.d.ts +122 -0
- package/dist/services/BranchService.d.ts +47 -0
- package/dist/services/FetchService.d.ts +214 -0
- package/dist/services/PersistService.d.ts +39 -0
- package/dist/services/RelationService.d.ts +109 -0
- package/dist/services/cdc/CdcListener.d.ts +54 -0
- package/dist/services/cdc/trigger-cdc.d.ts +64 -0
- package/dist/services/collection-helpers.d.ts +38 -0
- package/dist/services/dataService.d.ts +110 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/realtimeService.d.ts +298 -0
- package/dist/src-Eh-CZosp.js +595 -0
- package/dist/src-Eh-CZosp.js.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/utils/drizzle-conditions.d.ts +138 -0
- package/dist/utils/pg-array-null-patch.d.ts +16 -0
- package/dist/utils/pg-error-utils.d.ts +65 -0
- package/dist/utils/table-classification.d.ts +8 -0
- package/dist/websocket.d.ts +18 -0
- package/package.json +113 -0
- package/src/PostgresAdapter.ts +58 -0
- package/src/PostgresBackendDriver.ts +1387 -0
- package/src/PostgresBootstrapper.ts +581 -0
- package/src/auth/ensure-tables.ts +367 -0
- package/src/auth/services.ts +1321 -0
- package/src/backup/backup-cli.ts +383 -0
- package/src/backup/backup-cron.ts +189 -0
- package/src/backup/backup-service.ts +299 -0
- package/src/backup/index.ts +12 -0
- package/src/backup/pg-tools.ts +231 -0
- package/src/backup/retention.ts +75 -0
- package/src/cli-errors.ts +265 -0
- package/src/cli-helpers.ts +196 -0
- package/src/cli.ts +786 -0
- package/src/collections/PostgresCollectionRegistry.ts +103 -0
- package/src/connection.ts +166 -0
- package/src/data-transformer.ts +733 -0
- package/src/databasePoolManager.ts +87 -0
- package/src/history/HistoryService.ts +272 -0
- package/src/history/ensure-history-table.ts +46 -0
- package/src/index.ts +15 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-bootstrap-sql.ts +41 -0
- package/src/schema/auth-default-policies.ts +125 -0
- package/src/schema/auth-schema.ts +233 -0
- package/src/schema/doctor-cli.ts +113 -0
- package/src/schema/doctor.ts +733 -0
- package/src/schema/dynamic-tables.test.ts +302 -0
- package/src/schema/dynamic-tables.ts +293 -0
- package/src/schema/generate-drizzle-schema-logic.ts +850 -0
- package/src/schema/generate-drizzle-schema.ts +131 -0
- package/src/schema/generate-postgres-ddl-logic.ts +490 -0
- package/src/schema/generate-postgres-ddl.ts +92 -0
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +910 -0
- package/src/schema/introspect-db.ts +266 -0
- package/src/schema/introspect-runtime.test.ts +212 -0
- package/src/schema/introspect-runtime.ts +293 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/security/policy-drift.test.ts +122 -0
- package/src/security/policy-drift.ts +159 -0
- package/src/security/rls-enforcement.ts +295 -0
- package/src/services/BranchService.ts +251 -0
- package/src/services/FetchService.ts +1661 -0
- package/src/services/PersistService.ts +329 -0
- package/src/services/RelationService.ts +1306 -0
- package/src/services/cdc/CdcListener.ts +167 -0
- package/src/services/cdc/trigger-cdc.ts +169 -0
- package/src/services/collection-helpers.ts +151 -0
- package/src/services/dataService.ts +246 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1502 -0
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +1162 -0
- package/src/utils/pg-array-null-patch.ts +42 -0
- package/src/utils/pg-error-utils.ts +227 -0
- package/src/utils/table-classification.ts +16 -0
- package/src/websocket.ts +640 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import arg from "arg";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { logger } from "@rebasepro/server";
|
|
8
|
+
import {
|
|
9
|
+
resolveLocalBin,
|
|
10
|
+
getTableIncludes,
|
|
11
|
+
getDevDatabaseUrl,
|
|
12
|
+
ensureDevDatabaseExists,
|
|
13
|
+
getTableExcludes
|
|
14
|
+
} from "./cli-helpers";
|
|
15
|
+
import { checkDatabaseConnectivity, diagnoseDbError } from "./cli-errors";
|
|
16
|
+
import { AUTH_BOOTSTRAP_SQL } from "./schema/auth-bootstrap-sql";
|
|
17
|
+
|
|
18
|
+
const __cliDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async function loadEnv(): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
const dotenv = await import("dotenv");
|
|
25
|
+
const envPaths = [
|
|
26
|
+
process.env.DOTENV_CONFIG_PATH,
|
|
27
|
+
path.resolve(process.cwd(), ".env"),
|
|
28
|
+
path.resolve(process.cwd(), "../.env"),
|
|
29
|
+
path.resolve(process.cwd(), "../../.env")
|
|
30
|
+
].filter(Boolean) as string[];
|
|
31
|
+
|
|
32
|
+
for (const p of envPaths) {
|
|
33
|
+
if (fs.existsSync(p)) {
|
|
34
|
+
const parsed = dotenv.config({ path: p });
|
|
35
|
+
if (parsed.parsed) {
|
|
36
|
+
for (const [key, val] of Object.entries(parsed.parsed)) {
|
|
37
|
+
if (process.env[key] === undefined) {
|
|
38
|
+
process.env[key] = val;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runPluginCommand(args: string[]) {
|
|
51
|
+
await loadEnv();
|
|
52
|
+
const domain = args[0]; // "db" or "schema"
|
|
53
|
+
const subcommand = args[1];
|
|
54
|
+
|
|
55
|
+
if (domain === "db") {
|
|
56
|
+
await dbCommand(subcommand, args);
|
|
57
|
+
} else if (domain === "schema") {
|
|
58
|
+
await schemaCommand(subcommand, args);
|
|
59
|
+
} else if (domain === "doctor") {
|
|
60
|
+
await doctorPluginCommand(args);
|
|
61
|
+
} else {
|
|
62
|
+
logger.error(chalk.red(`Unknown domain command: ${domain}`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
68
|
+
const VALID_ACTIONS = ["push", "generate", "migrate", "branch", "backup", "restore", "backups"];
|
|
69
|
+
if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
|
|
70
|
+
logger.error(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (subcommand === "branch") {
|
|
75
|
+
await branchCommand(rawArgs);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (subcommand === "backup" || subcommand === "restore" || subcommand === "backups") {
|
|
80
|
+
const { backupCommand, restoreCommand, backupsCommand } = await import("./backup/backup-cli");
|
|
81
|
+
if (subcommand === "backup") await backupCommand(rawArgs);
|
|
82
|
+
else if (subcommand === "restore") await restoreCommand(rawArgs);
|
|
83
|
+
else await backupsCommand(rawArgs);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const argsList = arg(
|
|
88
|
+
{
|
|
89
|
+
"--collections": String,
|
|
90
|
+
"-c": "--collections"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
argv: rawArgs.slice(2),
|
|
94
|
+
permissive: true
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
98
|
+
|
|
99
|
+
if (subcommand === "generate") {
|
|
100
|
+
logger.info("");
|
|
101
|
+
logger.info(chalk.bold(" 📦 Rebase DB Generate"));
|
|
102
|
+
logger.info(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
103
|
+
logger.info("");
|
|
104
|
+
await schemaCommand("generate", rawArgs);
|
|
105
|
+
await generatePostgresDdlCommand(rawArgs);
|
|
106
|
+
logger.info("");
|
|
107
|
+
logger.info(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
|
|
108
|
+
logger.info("");
|
|
109
|
+
const migrationName = argsList._[0] || "migration";
|
|
110
|
+
await runAtlas("migrate", ["diff", migrationName, "--dir", "file://drizzle/migrations", "--to", "file://drizzle/schema.sql"], collectionsPath);
|
|
111
|
+
|
|
112
|
+
// Post-process the newest migration file
|
|
113
|
+
try {
|
|
114
|
+
const migrationsDir = path.resolve(process.cwd(), "drizzle", "migrations");
|
|
115
|
+
if (fs.existsSync(migrationsDir)) {
|
|
116
|
+
const files = fs.readdirSync(migrationsDir);
|
|
117
|
+
const sqlFiles = files
|
|
118
|
+
.filter(f => f.endsWith(".sql"))
|
|
119
|
+
.sort();
|
|
120
|
+
if (sqlFiles.length > 0) {
|
|
121
|
+
const newestMigrationFile = path.join(migrationsDir, sqlFiles[sqlFiles.length - 1]);
|
|
122
|
+
|
|
123
|
+
// Make CREATE SCHEMA idempotent so it doesn't conflict with
|
|
124
|
+
// --revisions-schema (Atlas pre-creates the rebase schema
|
|
125
|
+
// for its revision table before running migrations).
|
|
126
|
+
let migrationContent = fs.readFileSync(newestMigrationFile, "utf-8");
|
|
127
|
+
migrationContent = migrationContent.replace(
|
|
128
|
+
/CREATE SCHEMA (?!IF NOT EXISTS)("[^"]+");/g,
|
|
129
|
+
"CREATE SCHEMA IF NOT EXISTS $1;"
|
|
130
|
+
);
|
|
131
|
+
fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
|
|
132
|
+
|
|
133
|
+
// Append RLS policies, preceded by the auth bootstrap so the
|
|
134
|
+
// migration is self-contained: Atlas replays migrations
|
|
135
|
+
// against a clean dev database where `auth.uid()` would not
|
|
136
|
+
// otherwise exist.
|
|
137
|
+
const policiesFile = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
138
|
+
if (fs.existsSync(policiesFile)) {
|
|
139
|
+
const policiesContent = fs.readFileSync(policiesFile, "utf-8");
|
|
140
|
+
fs.appendFileSync(newestMigrationFile, "\n\n" + AUTH_BOOTSTRAP_SQL + "\n" + policiesContent);
|
|
141
|
+
logger.info(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
|
|
142
|
+
|
|
143
|
+
// Re-hash the migration directory
|
|
144
|
+
logger.info(chalk.gray(" Re-hashing migration files..."));
|
|
145
|
+
await runAtlas("migrate", ["hash", "--dir", "file://drizzle/migrations"], collectionsPath);
|
|
146
|
+
logger.info(chalk.gray(" ✓ Migration directory checksum updated successfully."));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
logger.warn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
logger.info("");
|
|
155
|
+
logger.info(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
|
|
156
|
+
logger.info("");
|
|
157
|
+
} else {
|
|
158
|
+
logger.info("");
|
|
159
|
+
logger.info(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
|
|
160
|
+
logger.info("");
|
|
161
|
+
|
|
162
|
+
if (subcommand === "push") {
|
|
163
|
+
logger.info(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
164
|
+
logger.info("");
|
|
165
|
+
await schemaCommand("generate", rawArgs);
|
|
166
|
+
await generatePostgresDdlCommand(rawArgs);
|
|
167
|
+
logger.info("");
|
|
168
|
+
logger.info(chalk.gray(" Step 2/3: Pushing schema to database with Atlas..."));
|
|
169
|
+
logger.info("");
|
|
170
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
171
|
+
if (databaseUrl) {
|
|
172
|
+
await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
173
|
+
}
|
|
174
|
+
await runAtlas("schema", ["apply", "--to", "file://drizzle/schema.sql", "--auto-approve"], collectionsPath);
|
|
175
|
+
logger.info("");
|
|
176
|
+
|
|
177
|
+
if (databaseUrl) {
|
|
178
|
+
await applyPolicies(databaseUrl);
|
|
179
|
+
await ensureRlsUserRole(databaseUrl);
|
|
180
|
+
} else {
|
|
181
|
+
logger.warn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
|
|
182
|
+
}
|
|
183
|
+
} else if (subcommand === "migrate") {
|
|
184
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
185
|
+
if (databaseUrl) {
|
|
186
|
+
await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
187
|
+
}
|
|
188
|
+
const extraArgs = argsList._.filter(arg => arg !== "migrate");
|
|
189
|
+
await runAtlas("migrate", ["apply", "--dir", "file://drizzle/migrations", ...extraArgs], collectionsPath);
|
|
190
|
+
if (databaseUrl) {
|
|
191
|
+
await ensureRlsUserRole(databaseUrl);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
logger.info("");
|
|
196
|
+
logger.info(chalk.green(` ✓ rebase db ${subcommand} completed successfully.`));
|
|
197
|
+
logger.info("");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function ensureAuthSchemaAndFunctions(databaseUrl: string): Promise<void> {
|
|
202
|
+
try {
|
|
203
|
+
const { Client } = await import("pg");
|
|
204
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
205
|
+
await client.connect();
|
|
206
|
+
try {
|
|
207
|
+
await client.query(AUTH_BOOTSTRAP_SQL);
|
|
208
|
+
// Runtime-only: pre-create the schema Atlas uses for its revision
|
|
209
|
+
// table (`--revisions-schema rebase`). Kept out of
|
|
210
|
+
// AUTH_BOOTSTRAP_SQL so it never enters the migration stream.
|
|
211
|
+
await client.query("CREATE SCHEMA IF NOT EXISTS rebase");
|
|
212
|
+
} finally {
|
|
213
|
+
await client.end();
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
logger.warn(chalk.yellow(` ⚠️ Failed to bootstrap auth schema and helper functions: ${err instanceof Error ? err.message : String(err)}`));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Provision the restricted `rebase_user` role right after schema changes land,
|
|
222
|
+
* so grants cover freshly created tables (default privileges cover future
|
|
223
|
+
* ones). Only needed — and only possible without extra setup — when the
|
|
224
|
+
* connection would bypass RLS (superuser / BYPASSRLS / table owner).
|
|
225
|
+
*/
|
|
226
|
+
async function ensureRlsUserRole(databaseUrl: string): Promise<void> {
|
|
227
|
+
const { detectConnectionPosture, ensureAppRole, REBASE_USER_ROLE } = await import("./security/rls-enforcement");
|
|
228
|
+
const { Client } = await import("pg");
|
|
229
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
230
|
+
await client.connect();
|
|
231
|
+
try {
|
|
232
|
+
const runSql = async (text: string) => (await client.query(text)).rows as Record<string, unknown>[];
|
|
233
|
+
const posture = await detectConnectionPosture(runSql);
|
|
234
|
+
if (posture.privileged) {
|
|
235
|
+
await ensureAppRole(runSql, ["public", "rebase", "auth"]);
|
|
236
|
+
logger.info(chalk.gray(` ✓ RLS role "${REBASE_USER_ROLE}" provisioned/refreshed.`));
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
await client.end();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function applyPolicies(databaseUrl: string): Promise<void> {
|
|
244
|
+
try {
|
|
245
|
+
const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
246
|
+
if (!fs.existsSync(policiesPath)) return;
|
|
247
|
+
|
|
248
|
+
logger.info(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
|
|
249
|
+
logger.info("");
|
|
250
|
+
|
|
251
|
+
const policiesContent = fs.readFileSync(policiesPath, "utf-8");
|
|
252
|
+
const { Client } = await import("pg");
|
|
253
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
254
|
+
await client.connect();
|
|
255
|
+
try {
|
|
256
|
+
await client.query(policiesContent);
|
|
257
|
+
logger.info(chalk.green(" ✓ RLS policies applied successfully."));
|
|
258
|
+
} finally {
|
|
259
|
+
await client.end();
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
const hint = diagnoseDbError(err, databaseUrl);
|
|
263
|
+
if (hint) {
|
|
264
|
+
logger.error(hint);
|
|
265
|
+
} else {
|
|
266
|
+
logger.error(chalk.red(` ✗ Failed to apply RLS policies: ${err instanceof Error ? err.message : String(err)}`));
|
|
267
|
+
}
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function branchCommand(rawArgs: string[]): Promise<void> {
|
|
273
|
+
const branchAction = rawArgs[2]; // create, list, delete, info
|
|
274
|
+
|
|
275
|
+
if (!branchAction || branchAction === "--help") {
|
|
276
|
+
printBranchHelp();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Load .env for DATABASE_URL
|
|
281
|
+
try {
|
|
282
|
+
const dotenv = await import("dotenv");
|
|
283
|
+
const envPath = process.env.DOTENV_CONFIG_PATH;
|
|
284
|
+
if (envPath) {
|
|
285
|
+
dotenv.config({ path: envPath });
|
|
286
|
+
} else {
|
|
287
|
+
dotenv.config();
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
// dotenv may not be installed
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
|
|
294
|
+
if (!databaseUrl) {
|
|
295
|
+
logger.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Dynamic imports to avoid loading heavy deps when not needed
|
|
300
|
+
const { DatabasePoolManager } = await import("./databasePoolManager");
|
|
301
|
+
const { BranchService } = await import("./services/BranchService");
|
|
302
|
+
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
303
|
+
const { Pool } = await import("pg");
|
|
304
|
+
|
|
305
|
+
const pool = new Pool({ connectionString: databaseUrl,
|
|
306
|
+
max: 3 });
|
|
307
|
+
const db = drizzle(pool);
|
|
308
|
+
const poolManager = new DatabasePoolManager(databaseUrl);
|
|
309
|
+
const branchService = new BranchService(db, poolManager);
|
|
310
|
+
|
|
311
|
+
// Ensure metadata table exists
|
|
312
|
+
await branchService.ensureBranchMetadataTable();
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
switch (branchAction) {
|
|
316
|
+
case "create": {
|
|
317
|
+
const name = rawArgs[3];
|
|
318
|
+
if (!name) {
|
|
319
|
+
logger.error(chalk.red("✗ Branch name is required."));
|
|
320
|
+
logger.info(chalk.gray(" Usage: rebase db branch create <name> [--from <source>]"));
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
let source: string | undefined;
|
|
324
|
+
const fromIdx = rawArgs.indexOf("--from");
|
|
325
|
+
if (fromIdx !== -1 && rawArgs[fromIdx + 1]) {
|
|
326
|
+
source = rawArgs[fromIdx + 1];
|
|
327
|
+
}
|
|
328
|
+
logger.info("");
|
|
329
|
+
logger.info(chalk.bold(" 🌿 Creating database branch..."));
|
|
330
|
+
logger.info(chalk.gray(` Name: ${name}`));
|
|
331
|
+
if (source) logger.info(chalk.gray(` Source: ${source}`));
|
|
332
|
+
logger.info("");
|
|
333
|
+
const branch = await branchService.createBranch(name, source ? { source } : undefined);
|
|
334
|
+
logger.info(chalk.green(` ✓ Branch "${branch.name}" created successfully.`));
|
|
335
|
+
logger.info(chalk.gray(` Database: rb_${branch.name}`));
|
|
336
|
+
logger.info(chalk.gray(` Parent: ${branch.parentDatabase}`));
|
|
337
|
+
logger.info("");
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
case "list": {
|
|
342
|
+
const branches = await branchService.listBranches();
|
|
343
|
+
logger.info("");
|
|
344
|
+
if (branches.length === 0) {
|
|
345
|
+
logger.info(chalk.gray(" No branches found. Create one with: rebase db branch create <name>"));
|
|
346
|
+
} else {
|
|
347
|
+
logger.info(chalk.bold(` 🌿 ${branches.length} branch(es):`));
|
|
348
|
+
logger.info("");
|
|
349
|
+
for (const b of branches) {
|
|
350
|
+
const size = b.sizeBytes != null
|
|
351
|
+
? chalk.gray(` (${formatBytes(b.sizeBytes)})`)
|
|
352
|
+
: "";
|
|
353
|
+
const age = chalk.gray(` — created ${timeAgo(b.createdAt)}`);
|
|
354
|
+
logger.info(` ${chalk.green("●")} ${chalk.bold(b.name)}${size}${age}`);
|
|
355
|
+
logger.info(chalk.gray(` from ${b.parentDatabase}`));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
logger.info("");
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
case "delete": {
|
|
363
|
+
const name = rawArgs[3];
|
|
364
|
+
if (!name) {
|
|
365
|
+
logger.error(chalk.red("✗ Branch name is required."));
|
|
366
|
+
logger.info(chalk.gray(" Usage: rebase db branch delete <name>"));
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
logger.info("");
|
|
370
|
+
logger.info(chalk.bold(` 🗑️ Deleting branch "${name}"...`));
|
|
371
|
+
await branchService.deleteBranch(name);
|
|
372
|
+
logger.info(chalk.green(` ✓ Branch "${name}" deleted.`));
|
|
373
|
+
logger.info("");
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
case "info": {
|
|
378
|
+
const name = rawArgs[3];
|
|
379
|
+
if (!name) {
|
|
380
|
+
logger.error(chalk.red("✗ Branch name is required."));
|
|
381
|
+
logger.info(chalk.gray(" Usage: rebase db branch info <name>"));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
const info = await branchService.getBranchInfo(name);
|
|
385
|
+
logger.info("");
|
|
386
|
+
if (!info) {
|
|
387
|
+
logger.error(chalk.red(` ✗ Branch "${name}" not found.`));
|
|
388
|
+
} else {
|
|
389
|
+
logger.info(chalk.bold(` 🌿 Branch: ${info.name}`));
|
|
390
|
+
logger.info(chalk.gray(` Database: rb_${info.name}`));
|
|
391
|
+
logger.info(chalk.gray(` Parent: ${info.parentDatabase}`));
|
|
392
|
+
logger.info(chalk.gray(` Created: ${info.createdAt.toISOString()}`));
|
|
393
|
+
if (info.sizeBytes != null) {
|
|
394
|
+
logger.info(chalk.gray(` Size: ${formatBytes(info.sizeBytes)}`));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
logger.info("");
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
default:
|
|
402
|
+
logger.error(chalk.red(`Unknown branch action: "${branchAction}".`));
|
|
403
|
+
printBranchHelp();
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
} finally {
|
|
407
|
+
await poolManager.shutdown();
|
|
408
|
+
await pool.end();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function printBranchHelp() {
|
|
413
|
+
logger.info(`
|
|
414
|
+
${chalk.bold("rebase db branch")} — Database branching commands
|
|
415
|
+
|
|
416
|
+
${chalk.green.bold("Usage")}
|
|
417
|
+
rebase db branch ${chalk.blue("<command>")} [options]
|
|
418
|
+
|
|
419
|
+
${chalk.green.bold("Commands")}
|
|
420
|
+
${chalk.blue.bold("create")} <name> [--from <source>] Create a new branch
|
|
421
|
+
${chalk.blue.bold("list")} List all branches
|
|
422
|
+
${chalk.blue.bold("delete")} <name> Delete a branch
|
|
423
|
+
${chalk.blue.bold("info")} <name> Show branch details
|
|
424
|
+
|
|
425
|
+
${chalk.green.bold("Examples")}
|
|
426
|
+
${chalk.gray("# Create a branch from the current database")}
|
|
427
|
+
rebase db branch create feature_auth
|
|
428
|
+
|
|
429
|
+
${chalk.gray("# Create a branch from a specific source")}
|
|
430
|
+
rebase db branch create staging --from production
|
|
431
|
+
|
|
432
|
+
${chalk.gray("# List all branches")}
|
|
433
|
+
rebase db branch list
|
|
434
|
+
|
|
435
|
+
${chalk.gray("# Delete a branch")}
|
|
436
|
+
rebase db branch delete feature_auth
|
|
437
|
+
`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function formatBytes(bytes: number): string {
|
|
441
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
442
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
443
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
444
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function timeAgo(date: Date): string {
|
|
448
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
449
|
+
if (seconds < 60) return "just now";
|
|
450
|
+
const minutes = Math.floor(seconds / 60);
|
|
451
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
452
|
+
const hours = Math.floor(minutes / 60);
|
|
453
|
+
if (hours < 24) return `${hours}h ago`;
|
|
454
|
+
const days = Math.floor(hours / 24);
|
|
455
|
+
return `${days}d ago`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
async function runAtlas(domain: "schema" | "migrate", args: string[], collectionsPath?: string): Promise<void> {
|
|
461
|
+
const atlasBin = resolveLocalBin("atlas");
|
|
462
|
+
if (!atlasBin) {
|
|
463
|
+
logger.error(chalk.red("✗ Could not find atlas binary."));
|
|
464
|
+
const installCmd = "pnpm add -D @ariga/atlas";
|
|
465
|
+
logger.error(chalk.gray(` Install it with: ${installCmd}`));
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const env = { ...process.env as Record<string, string> };
|
|
470
|
+
try {
|
|
471
|
+
const dotenv = await import("dotenv");
|
|
472
|
+
const envPaths = [
|
|
473
|
+
process.env.DOTENV_CONFIG_PATH,
|
|
474
|
+
path.resolve(process.cwd(), ".env"),
|
|
475
|
+
path.resolve(process.cwd(), "../.env"),
|
|
476
|
+
path.resolve(process.cwd(), "../../.env")
|
|
477
|
+
].filter(Boolean) as string[];
|
|
478
|
+
|
|
479
|
+
for (const p of envPaths) {
|
|
480
|
+
if (fs.existsSync(p)) {
|
|
481
|
+
const parsed = dotenv.config({ path: p });
|
|
482
|
+
if (parsed.parsed) {
|
|
483
|
+
for (const [key, val] of Object.entries(parsed.parsed)) {
|
|
484
|
+
if (env[key] === undefined) {
|
|
485
|
+
env[key] = val;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
// ignore
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const databaseUrl = env.DATABASE_URL;
|
|
497
|
+
if (!databaseUrl) {
|
|
498
|
+
logger.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Pre-flight: verify the database is reachable before running Atlas.
|
|
503
|
+
// This catches ECONNREFUSED / auth failures with a friendly banner
|
|
504
|
+
// instead of letting Atlas surface a raw error.
|
|
505
|
+
await checkDatabaseConnectivity(databaseUrl);
|
|
506
|
+
|
|
507
|
+
const devDatabaseUrl = getDevDatabaseUrl(databaseUrl);
|
|
508
|
+
await ensureDevDatabaseExists(databaseUrl, devDatabaseUrl);
|
|
509
|
+
|
|
510
|
+
const atlasArgs = [domain, ...args];
|
|
511
|
+
|
|
512
|
+
if (domain === "schema") {
|
|
513
|
+
if (args.includes("apply")) {
|
|
514
|
+
atlasArgs.push("--url", databaseUrl, "--dev-url", devDatabaseUrl);
|
|
515
|
+
} else if (args.includes("clean") || args.includes("inspect")) {
|
|
516
|
+
atlasArgs.push("--url", databaseUrl);
|
|
517
|
+
}
|
|
518
|
+
} else if (domain === "migrate") {
|
|
519
|
+
if (args.includes("diff")) {
|
|
520
|
+
atlasArgs.push("--dev-url", devDatabaseUrl);
|
|
521
|
+
} else if (args.includes("apply") || args.includes("status")) {
|
|
522
|
+
atlasArgs.push("--url", databaseUrl, "--revisions-schema", "rebase");
|
|
523
|
+
if (args.includes("apply")) {
|
|
524
|
+
atlasArgs.push("--allow-dirty");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (domain === "schema" && args.includes("apply") && collectionsPath) {
|
|
530
|
+
const excludes = await getTableExcludes(databaseUrl, collectionsPath);
|
|
531
|
+
for (const exc of excludes) {
|
|
532
|
+
atlasArgs.push("--exclude", exc);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Stream stdout live but tee stderr so we can inspect Atlas's error text
|
|
537
|
+
// for known, actionable failure modes (e.g. a dependency-drop that leaves
|
|
538
|
+
// the schema half-applied) after the process exits.
|
|
539
|
+
const subprocess = execa(atlasBin, atlasArgs, {
|
|
540
|
+
cwd: process.cwd(),
|
|
541
|
+
stdout: "inherit",
|
|
542
|
+
stderr: "pipe",
|
|
543
|
+
env
|
|
544
|
+
});
|
|
545
|
+
let stderrText = "";
|
|
546
|
+
subprocess.stderr?.on("data", (chunk: Buffer) => {
|
|
547
|
+
const text = chunk.toString();
|
|
548
|
+
stderrText += text;
|
|
549
|
+
process.stderr.write(text);
|
|
550
|
+
});
|
|
551
|
+
try {
|
|
552
|
+
await subprocess;
|
|
553
|
+
} catch {
|
|
554
|
+
logger.error(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
|
|
555
|
+
// Surface actionable recovery guidance for recognized failures.
|
|
556
|
+
const hint = diagnoseDbError({ message: stderrText }, databaseUrl);
|
|
557
|
+
if (hint) {
|
|
558
|
+
logger.error(hint);
|
|
559
|
+
}
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function generatePostgresDdlCommand(rawArgs: string[]): Promise<void> {
|
|
565
|
+
const argsList = arg(
|
|
566
|
+
{
|
|
567
|
+
"--collections": String,
|
|
568
|
+
"--output": String,
|
|
569
|
+
"-c": "--collections",
|
|
570
|
+
"-o": "--output"
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
argv: rawArgs.slice(2),
|
|
574
|
+
permissive: true
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const ddlScript = path.join(__cliDirname, "schema", "generate-postgres-ddl.ts");
|
|
579
|
+
const tsxBin = resolveLocalBin("tsx");
|
|
580
|
+
if (!tsxBin) {
|
|
581
|
+
logger.error(chalk.red("✗ Could not find tsx binary."));
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
586
|
+
const outputPath = argsList["--output"] || path.join("drizzle", "schema.sql");
|
|
587
|
+
|
|
588
|
+
const cmdParts = [
|
|
589
|
+
tsxBin,
|
|
590
|
+
ddlScript,
|
|
591
|
+
`--collections=${collectionsPath}`,
|
|
592
|
+
`--output=${outputPath}`
|
|
593
|
+
];
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
597
|
+
cwd: process.cwd(),
|
|
598
|
+
stdio: "inherit",
|
|
599
|
+
env: { ...process.env as Record<string, string> }
|
|
600
|
+
});
|
|
601
|
+
} catch (err: unknown) {
|
|
602
|
+
logger.error(chalk.red(`✗ Failed to run Postgres DDL generator: ${err instanceof Error ? err.message : String(err)}`));
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
608
|
+
if (subcommand === "generate") {
|
|
609
|
+
const argsList = arg(
|
|
610
|
+
{
|
|
611
|
+
"--collections": String,
|
|
612
|
+
"--output": String,
|
|
613
|
+
"--watch": Boolean,
|
|
614
|
+
"-c": "--collections",
|
|
615
|
+
"-o": "--output",
|
|
616
|
+
"-w": "--watch"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
argv: rawArgs.slice(2), // db generate ... or schema generate ...
|
|
620
|
+
permissive: true
|
|
621
|
+
}
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
// Here we just invoke the local generate-drizzle-schema.ts since we are inside the postgresql-backend
|
|
625
|
+
// If installed in node_modules, __cliDirname is node_modules/@rebasepro/server-postgres/dist or src.
|
|
626
|
+
const generatorScript = path.join(__cliDirname, "schema", "generate-drizzle-schema.ts");
|
|
627
|
+
if (!fs.existsSync(generatorScript)) {
|
|
628
|
+
logger.error(chalk.red(`✗ Could not find generate-drizzle-schema.ts at ${generatorScript}`));
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const tsxBin = resolveLocalBin("tsx");
|
|
633
|
+
if (!tsxBin) {
|
|
634
|
+
logger.error(chalk.red("✗ Could not find tsx binary."));
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
639
|
+
const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
|
|
640
|
+
const watch = argsList["--watch"] || false;
|
|
641
|
+
|
|
642
|
+
logger.info("");
|
|
643
|
+
logger.info(chalk.bold(" 🔧 Rebase Schema Generator"));
|
|
644
|
+
logger.info("");
|
|
645
|
+
|
|
646
|
+
const cmdParts = [
|
|
647
|
+
tsxBin,
|
|
648
|
+
generatorScript,
|
|
649
|
+
`--collections=${collectionsPath}`,
|
|
650
|
+
`--output=${outputPath}`
|
|
651
|
+
];
|
|
652
|
+
if (watch) {
|
|
653
|
+
cmdParts.push("--watch");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
try {
|
|
657
|
+
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
658
|
+
cwd: process.cwd(),
|
|
659
|
+
stdio: "inherit",
|
|
660
|
+
env: { ...process.env as Record<string, string> }
|
|
661
|
+
});
|
|
662
|
+
} catch (err: unknown) {
|
|
663
|
+
logger.error(chalk.red(`✗ Failed to run schema generator: ${err instanceof Error ? err.message : String(err)}`));
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
} else if (subcommand === "introspect") {
|
|
667
|
+
const argsList = arg(
|
|
668
|
+
{
|
|
669
|
+
"--output": String,
|
|
670
|
+
"--collections": String,
|
|
671
|
+
"--force": Boolean,
|
|
672
|
+
"--schema": String,
|
|
673
|
+
"-o": "--output",
|
|
674
|
+
"-c": "--collections",
|
|
675
|
+
"-f": "--force"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
argv: rawArgs.slice(2),
|
|
679
|
+
permissive: true
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const introspectScript = path.join(__cliDirname, "schema", "introspect-db.ts");
|
|
684
|
+
if (!fs.existsSync(introspectScript)) {
|
|
685
|
+
logger.error(chalk.red(`✗ Could not find introspect-db.ts at ${introspectScript}`));
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const tsxBin = resolveLocalBin("tsx");
|
|
690
|
+
if (!tsxBin) {
|
|
691
|
+
logger.error(chalk.red("✗ Could not find tsx binary."));
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const outputPath = argsList["--output"] || argsList["--collections"] || path.join("..", "config", "collections");
|
|
696
|
+
|
|
697
|
+
logger.info("");
|
|
698
|
+
logger.info(chalk.bold(" 🔍 Rebase Schema Introspector"));
|
|
699
|
+
logger.info("");
|
|
700
|
+
|
|
701
|
+
const cmdParts = [
|
|
702
|
+
tsxBin,
|
|
703
|
+
introspectScript,
|
|
704
|
+
`--output=${outputPath}`,
|
|
705
|
+
...(argsList["--force"] ? ["--force"] : []),
|
|
706
|
+
...(argsList["--schema"] ? [`--schema=${argsList["--schema"]}`] : [])
|
|
707
|
+
];
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
711
|
+
cwd: process.cwd(),
|
|
712
|
+
stdio: "inherit",
|
|
713
|
+
env: { ...process.env as Record<string, string> }
|
|
714
|
+
});
|
|
715
|
+
} catch (err: unknown) {
|
|
716
|
+
logger.error(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
logger.error(chalk.red("Unknown schema command."));
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
|
|
726
|
+
const parsedArgs = arg(
|
|
727
|
+
{
|
|
728
|
+
"--collections": String,
|
|
729
|
+
"--schema": String,
|
|
730
|
+
"--sdk": String,
|
|
731
|
+
"--policies": Boolean,
|
|
732
|
+
"-c": "--collections",
|
|
733
|
+
"-s": "--schema",
|
|
734
|
+
"-k": "--sdk"
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
argv: rawArgs.slice(1), // skip "doctor"
|
|
738
|
+
permissive: true
|
|
739
|
+
}
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
const doctorScript = path.join(__cliDirname, "schema", "doctor-cli.ts");
|
|
743
|
+
if (!fs.existsSync(doctorScript)) {
|
|
744
|
+
logger.error(chalk.red(`✗ Could not find doctor.ts at ${doctorScript}`));
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const tsxBin = resolveLocalBin("tsx");
|
|
749
|
+
if (!tsxBin) {
|
|
750
|
+
logger.error(chalk.red("✗ Could not find tsx binary."));
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const collectionsPath = parsedArgs["--collections"] || path.join("..", "config", "collections");
|
|
755
|
+
const schemaPath = parsedArgs["--schema"] || path.join("src", "schema.generated.ts");
|
|
756
|
+
const sdkPath = parsedArgs["--sdk"] || path.join("..", "generated", "sdk", "database.types.ts");
|
|
757
|
+
|
|
758
|
+
const cmdParts = [
|
|
759
|
+
tsxBin,
|
|
760
|
+
doctorScript,
|
|
761
|
+
`--collections=${collectionsPath}`,
|
|
762
|
+
`--schema=${schemaPath}`,
|
|
763
|
+
`--sdk=${sdkPath}`,
|
|
764
|
+
// Only the RLS checks: skips the schema/SDK diff, so it can run against
|
|
765
|
+
// a deployed database as a CI gate.
|
|
766
|
+
...(parsedArgs["--policies"] ? ["--policies"] : [])
|
|
767
|
+
];
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
771
|
+
cwd: process.cwd(),
|
|
772
|
+
stdio: "inherit",
|
|
773
|
+
env: { ...process.env as Record<string, string> }
|
|
774
|
+
});
|
|
775
|
+
} catch {
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
// Entry point when called directly
|
|
782
|
+
const argv1Real = process.argv[1] ? fs.realpathSync(process.argv[1]) : "";
|
|
783
|
+
if (import.meta.url === `file://${argv1Real}`) {
|
|
784
|
+
// Drop node and script path
|
|
785
|
+
runPluginCommand(process.argv.slice(2)).catch(() => process.exit(1));
|
|
786
|
+
}
|