@rebasepro/cli 0.25.0-canary.g70dad01 → 0.25.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.
- package/dist/{auth-DKi6-KQG.js → auth-Vl2hFKXP.js} +4 -4
- package/dist/{auth-DKi6-KQG.js.map → auth-Vl2hFKXP.js.map} +1 -1
- package/dist/{connection-banner-CCIyvjpW.js → connection-banner-DbLJzWf4.js} +16 -3
- package/dist/connection-banner-DbLJzWf4.js.map +1 -0
- package/dist/{console-BfPGw5m8.js → console-CEGm0XNc.js} +2 -2
- package/dist/{console-BfPGw5m8.js.map → console-CEGm0XNc.js.map} +1 -1
- package/dist/{db-4NXrKl5W.js → db-CRNpA5mH.js} +7 -4
- package/dist/{db-4NXrKl5W.js.map → db-CRNpA5mH.js.map} +1 -1
- package/dist/{dev-BXlcTKIP.js → dev-C0pbMmhS.js} +2 -2
- package/dist/{dev-BXlcTKIP.js.map → dev-C0pbMmhS.js.map} +1 -1
- package/dist/dev-db/connection-banner.d.ts +3 -2
- package/dist/{doctor-wSyCajMh.js → doctor-CATRdWm6.js} +2 -2
- package/dist/{doctor-wSyCajMh.js.map → doctor-CATRdWm6.js.map} +1 -1
- package/dist/index.es.js +10 -10
- package/dist/{pull-CVKJTXWQ.js → pull-C3PXduzl.js} +2 -2
- package/dist/{pull-CVKJTXWQ.js.map → pull-C3PXduzl.js.map} +1 -1
- package/dist/{schema-CK7JN1Dc.js → schema-B0-p6T1O.js} +2 -2
- package/dist/{schema-CK7JN1Dc.js.map → schema-B0-p6T1O.js.map} +1 -1
- package/dist/{status-CQrliKxD.js → status-BJ26yZOo.js} +2 -2
- package/dist/{status-CQrliKxD.js.map → status-BJ26yZOo.js.map} +1 -1
- package/package.json +7 -7
- package/dist/connection-banner-CCIyvjpW.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pull-CVKJTXWQ.js","names":[],"sources":["../src/dev-db/pull.ts"],"sourcesContent":["/**\n * `rebase db pull` — copy a database's contents into local development.\n *\n * The common case is production into local, and the reason it exists is that\n * the alternative is worse: without it people hand-roll a `pg_dump | psql` and\n * get the flags wrong in ways that either fail loudly at 2am or, more often,\n * quietly restore half a schema.\n *\n * Three things this command insists on, because copying a production database\n * onto a laptop is a data-protection event whether or not anyone calls it one:\n *\n * 1. **It says what it is about to do, in full, before doing it** — which\n * database it will read, which one it will overwrite, and where the data will\n * come to rest on disk. The target path matters: people forget that\n * `.rebase/pgdata` is a directory their backup software may be indexing.\n *\n * 2. **It refuses to run unattended without being told to.** The target is\n * destroyed, so a mistyped `--from` with no confirmation would take the\n * developer's working database with it.\n *\n * 3. **It will not write to a remote database.** The target is always the local\n * development database; there is no flag that makes this push. A tool that\n * can copy in both directions eventually copies in the wrong one.\n *\n * Anonymization is opt-in (`--anonymize`), which is a deliberate choice and not\n * an obviously safe one — the flag nobody types is the flag nobody gets. It is\n * a best-effort pass over columns whose *names* look like personal data, and\n * {@link ANONYMIZE_PATTERNS} says exactly which. It cannot find personal data in\n * a column called `notes`, and this file says so rather than implying a\n * guarantee it cannot keep.\n *\n * What it does guarantee is that it never half-happens: {@link anonymizeCopy}\n * runs the pass in one transaction, and when the pass fails it empties the\n * tables it would have rewritten rather than leave their real values behind.\n */\n\nimport { execa } from \"execa\";\nimport fs from \"fs\";\nimport type { Client } from \"pg\";\n\nimport { banner, connectionErrorText, hostOf } from \"./connection-banner\";\nimport { PGLITE_EXTENSIONS } from \"./constraints\";\n\n/**\n * How one kind of personal-looking column is overwritten.\n *\n * Two forms, because a constant is the most readable replacement and is wrong\n * for exactly one kind of column: a unique one. Every `token_hash` becoming\n * `'REDACTED'` is what `UNIQUE(token_hash)` refused on the second row of\n * `rebase.refresh_tokens` — so on any project where someone had signed in twice.\n */\nexport interface AnonymizeRule {\n pattern: RegExp;\n /**\n * Only on tables whose name matches, when set. For a column name that is\n * personal on one table and not on another: a user's `name` is a person's,\n * a tag's is not.\n */\n tables?: RegExp;\n /** The value for a column no unique index covers, or null to always use the unique form. */\n value: string | null;\n /** The unique form is `prefix` + random hex + `suffix`, cut to fit the column. */\n prefix: string;\n suffix: string;\n}\n\n/**\n * Tables whose rows are people, by name: `rebase.users` and the usual spellings\n * of a project's own. Singular or plural.\n */\nconst PERSON_TABLES = /^(users?|user_profiles?|profiles?|people|persons?|members?|customers?|contacts?|employees?)$/i;\n\n/**\n * Column-name patterns the anonymizer overwrites.\n *\n * Names, not contents: inspecting values would be slower, and would still miss\n * the same things. This is a reasonable-effort measure for making a local copy\n * less dangerous, and it is not a compliance control.\n */\nexport const ANONYMIZE_PATTERNS: readonly AnonymizeRule[] = [\n // Emails are unique in practice even where nothing enforces it, so they\n // always get a value of their own.\n { pattern: /^(.*_)?e?mail(_.*)?$/i, value: null, prefix: \"user-\", suffix: \"@example.invalid\" },\n { pattern: /^(.*_)?(phone|mobile|tel|telephone)(_.*)?$/i, value: \"+10000000000\", prefix: \"+1-\", suffix: \"\" },\n // `display_name` is how `rebase.users` holds every user's name.\n {\n pattern: /^(.*_)?(display_name|first_name|last_name|full_name|middle_name|family_name|surname|given_name)(_.*)?$/i,\n value: \"Redacted\",\n prefix: \"Redacted-\",\n suffix: \"\"\n },\n // A bare `name` is a person's only on a table of people.\n { pattern: /^name$/i, tables: PERSON_TABLES, value: \"Redacted\", prefix: \"Redacted-\", suffix: \"\" },\n { pattern: /^(.*_)?(address|street|postcode|zip|zipcode)(_.*)?$/i, value: \"Redacted\", prefix: \"Redacted-\", suffix: \"\" },\n { pattern: /^(.*_)?(ssn|tax_id|national_id|passport)(_.*)?$/i, value: \"REDACTED\", prefix: \"REDACTED-\", suffix: \"\" },\n { pattern: /^(.*_)?(password|password_hash|secret|token|api_key|access_token|refresh_token)(_.*)?$/i, value: \"REDACTED\", prefix: \"REDACTED-\", suffix: \"\" },\n { pattern: /^(.*_)?(ip|ip_address|user_agent)(_.*)?$/i, value: \"REDACTED\", prefix: \"REDACTED-\", suffix: \"\" }\n];\n\n/** The rule that overwrites this column, if any. Without a table, a table-bound rule never applies. */\nfunction ruleFor(columnName: string, table?: string): AnonymizeRule | undefined {\n return ANONYMIZE_PATTERNS.find((rule) =>\n rule.pattern.test(columnName) && (!rule.tables || (table !== undefined && rule.tables.test(table))));\n}\n\nexport function shouldAnonymize(columnName: string, table?: string): boolean {\n return ruleFor(columnName, table) !== undefined;\n}\n\n/** A text-ish column the anonymizer can overwrite without a type error. */\nexport interface ColumnRef {\n schema: string;\n table: string;\n column: string;\n dataType: string;\n /** `character_maximum_length`: null or absent for an unbounded type. */\n maxLength?: number | null;\n /** Covered by a unique index — a primary key, a UNIQUE constraint, or a unique expression index. */\n unique?: boolean;\n}\n\n/** Random hex of this length, per row. `gen_random_uuid()` is core since PostgreSQL 13. */\nfunction randomHex(length: number): string {\n return `left(replace(gen_random_uuid()::text, '-', ''), ${length})`;\n}\n\nfunction sqlLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Fewest random hex digits a unique replacement keeps. Twelve is 16^12 values:\n * a collision needs millions of rows, where eight would need tens of thousands.\n */\nconst MIN_UNIQUE_HEX = 12;\n\n/**\n * The SQL expression that overwrites one column, or null when no rule names it.\n *\n * Unique per row when an index makes the column unique, and always cut to the\n * column's declared length — `zip char(5)` refused `'Redacted'` outright, and a\n * failed statement now empties the tables rather than skip one. To fit a short\n * unique column the prefix goes first, then the suffix, and the random part is\n * kept to at least {@link MIN_UNIQUE_HEX} digits while there is room for them.\n * A NULL stays NULL: an empty optional field is not personal data.\n */\nexport function replacementFor(column: ColumnRef): string | null {\n const rule = ruleFor(column.column, column.table);\n if (!rule) return null;\n const limit = typeof column.maxLength === \"number\" && column.maxLength > 0 ? column.maxLength : null;\n\n let value: string;\n if (rule.value !== null && !column.unique) {\n value = sqlLiteral(limit === null ? rule.value : rule.value.slice(0, limit));\n } else {\n const shapes = [[rule.prefix, rule.suffix], [\"\", rule.suffix], [\"\", \"\"]];\n const fitting = shapes.find(([prefix, suffix]) =>\n limit === null || limit - prefix.length - suffix.length >= MIN_UNIQUE_HEX);\n const [prefix, suffix] = fitting ?? [\"\", \"\"];\n const hex = Math.min(32, limit === null ? 32 : limit - prefix.length - suffix.length);\n value = `concat(${sqlLiteral(prefix)}, ${randomHex(Math.max(1, hex))}, ${sqlLiteral(suffix)})`;\n }\n\n const name = quoteIdentifier(column.column);\n return `CASE WHEN ${name} IS NULL THEN NULL ELSE ${value} END`;\n}\n\n/** A double-quoted identifier, with any `\"` in it doubled — a table called `order` or `a\"b` still works. */\nexport function quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Anonymizable columns: name looks personal, and the type can hold the\n * replacement.\n *\n * The type check is what stops this generating `UPDATE … SET user_id =\n * 'Redacted'` for an integer column called `user_id_email_seq` and failing the\n * whole pass on a technicality.\n */\nexport function anonymizableColumns(columns: readonly ColumnRef[]): ColumnRef[] {\n const textual = new Set([\"text\", \"character varying\", \"varchar\", \"character\", \"char\", \"citext\"]);\n\n return columns.filter((column) =>\n shouldAnonymize(column.column, column.table) && textual.has(column.dataType.toLowerCase()));\n}\n\n/** `\"schema\".\"table\"` for each table the pass rewrites, sorted. */\nexport function anonymizedTables(columns: readonly ColumnRef[]): string[] {\n return [...new Set(anonymizableColumns(columns).map((column) =>\n `${quoteIdentifier(column.schema)}.${quoteIdentifier(column.table)}`))].sort();\n}\n\n/** `UPDATE` statements for one anonymization pass, in a stable order. */\nexport function anonymizeStatements(columns: readonly ColumnRef[]): string[] {\n const byTable = new Map<string, ColumnRef[]>();\n for (const column of anonymizableColumns(columns)) {\n const key = `${quoteIdentifier(column.schema)}.${quoteIdentifier(column.table)}`;\n byTable.set(key, [...(byTable.get(key) ?? []), column]);\n }\n\n return [...byTable.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([table, cols]) => {\n const assignments = [...cols]\n .sort((a, b) => a.column.localeCompare(b.column))\n .map((column) => `${quoteIdentifier(column.column)} = ${replacementFor(column)}`)\n .join(\", \");\n\n return `UPDATE ${table} SET ${assignments};`;\n });\n}\n\n/**\n * Every column the pass could overwrite, with what decides how.\n *\n * Base tables only — a view has an `email` column too, and is not something to\n * UPDATE. Generated columns are skipped for the same reason: `email_domain\n * GENERATED ALWAYS AS (…)` matches by name and refuses any value but DEFAULT,\n * and it follows the column it is generated from anyway. An extension type such\n * as `citext` reports `USER-DEFINED`, so its own name is what is compared.\n *\n * `unique` comes from the indexes, through `pg_depend` as well as `indkey`: a\n * unique index on `lower(email)` names no column in `indkey`, and is exactly\n * what a constant replacement would violate.\n */\nexport const ANONYMIZE_COLUMNS_SQL = `\nWITH unique_columns AS (\n SELECT DISTINCT i.indrelid AS relid, a.attname AS column_name\n FROM pg_index i\n JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum > 0 AND NOT a.attisdropped\n WHERE i.indisunique\n AND (a.attnum = ANY (i.indkey)\n OR EXISTS (SELECT 1 FROM pg_depend d\n WHERE d.classid = 'pg_class'::regclass AND d.objid = i.indexrelid\n AND d.refclassid = 'pg_class'::regclass AND d.refobjid = i.indrelid\n AND d.refobjsubid = a.attnum))\n)\nSELECT c.table_schema AS \"schema\",\n c.table_name AS \"table\",\n c.column_name AS \"column\",\n CASE WHEN c.data_type = 'USER-DEFINED' THEN c.udt_name ELSE c.data_type END AS \"dataType\",\n c.character_maximum_length AS \"maxLength\",\n (u.column_name IS NOT NULL) AS \"unique\"\n FROM information_schema.columns c\n JOIN information_schema.tables t ON t.table_schema = c.table_schema AND t.table_name = c.table_name\n LEFT JOIN unique_columns u\n ON u.relid = format('%I.%I', c.table_schema, c.table_name)::regclass AND u.column_name = c.column_name\n WHERE t.table_type = 'BASE TABLE'\n AND c.is_generated = 'NEVER'\n AND c.table_schema NOT IN ('pg_catalog', 'information_schema')`;\n\n/** How {@link anonymizeCopy} ended. */\nexport type AnonymizeOutcome =\n | { ok: true; tables: string[] }\n | {\n ok: false;\n /** Why the pass failed. Nothing it wrote was kept. */\n error: string;\n /** The tables emptied instead, so none of their real values are left. */\n emptied: string[];\n /** Why they could not be emptied, when they could not: the copy then holds real data. */\n emptyError: string | null;\n };\n\nfunction errorText(error: unknown): string {\n return error instanceof Error && error.message ? error.message : String(error);\n}\n\n/**\n * Overwrite the personal-looking columns of a restored copy, all or nothing.\n *\n * One transaction, so a failure part-way leaves no table half done — the old\n * pass ran its UPDATEs one by one, so a failure on `rebase.refresh_tokens` left\n * `public.authors` anonymized and `rebase.users` holding every real email.\n *\n * A rollback alone would be worse than that, though: it puts back the real\n * value of every column, in a copy the developer asked to be anonymized. So on\n * failure the tables the pass would have rewritten are emptied (`TRUNCATE …\n * CASCADE`, which takes the rows that reference them too), and the outcome\n * says which — or, if even that failed, says the copy holds real data, so the\n * caller can say it louder.\n *\n * `row_security = off` makes a role that RLS would filter fail the pass instead\n * of rewriting only the rows its policies show it, which looked like success.\n */\nexport async function anonymizeCopy(client: Client): Promise<AnonymizeOutcome> {\n let listed = false;\n let tables: string[] = [];\n try {\n const { rows } = await client.query<ColumnRef>(ANONYMIZE_COLUMNS_SQL);\n tables = anonymizedTables(rows);\n listed = true;\n await client.query(\"BEGIN\");\n await client.query(\"SET LOCAL row_security = off\");\n for (const statement of anonymizeStatements(rows)) await client.query(statement);\n await client.query(\"COMMIT\");\n\n return { ok: true, tables };\n } catch (error) {\n const reason = errorText(error);\n await client.query(\"ROLLBACK\").catch(() => undefined);\n if (!listed) {\n return { ok: false, error: reason, emptied: [], emptyError: \"the tables to empty could not be listed\" };\n }\n if (tables.length === 0) return { ok: false, error: reason, emptied: [], emptyError: null };\n try {\n await client.query(`TRUNCATE TABLE ${tables.join(\", \")} CASCADE`);\n\n return { ok: false, error: reason, emptied: tables, emptyError: null };\n } catch (emptyError) {\n return { ok: false, error: reason, emptied: [], emptyError: errorText(emptyError) };\n }\n }\n}\n\n/**\n * Schemas the restored copy needs the app role provisioned on.\n *\n * `pg_dump --no-privileges` strips every GRANT, so a pulled database arrives\n * with its RLS policies and its `FORCE ROW LEVEL SECURITY` intact and no\n * privileges behind them. Measured on a 30-table project: 68 policies restored,\n * 14 tables with RLS on, and **0** grants to `rebase_user` — where the source\n * had 60. Reading one table as the role Rebase serves every request through:\n *\n * source: 6\n * copy: ERROR: permission denied for table leads\n *\n * The dump flags are right and stay: without `--no-owner`/`--no-privileges`\n * every `ALTER … OWNER TO` and `GRANT … TO <prod role>` in the dump fails\n * against roles that do not exist on a laptop, and buries the real output. The\n * repair belongs after the restore, and belongs to `ensureAppRole` — the same\n * routine boot and `rebase db push` call — rather than to a second list of\n * grants written here.\n *\n * Which schemas: every non-system schema the restored database actually has.\n * Boot knows its `managedSchemas` from the collections; a restore knows only\n * what arrived, and a source may carry schemas this project does not declare.\n * `pg_catalog`, `information_schema` and the `pg_*` internals are never ours to\n * grant on and PostgreSQL would refuse anyway.\n */\nexport function provisionableSchemas(rows: readonly { schema: string }[]): string[] {\n const skip = (name: string) => name === \"information_schema\" || name.startsWith(\"pg_\");\n\n return [...new Set(rows.map((row) => row.schema))].filter((name) => !skip(name)).sort();\n}\n\n/** Host and database of a connection string, with no credentials in it. */\nexport function describeTarget(connectionString: string): string {\n try {\n const url = new URL(connectionString);\n const database = url.pathname.replace(/^\\//, \"\") || \"(default)\";\n\n return `${url.hostname}${url.port ? `:${url.port}` : \"\"}/${database}`;\n } catch {\n // Never echo the raw string: it carries a password, and this line is\n // printed to a terminal people paste into issues.\n return \"(unparseable connection string)\";\n }\n}\n\n/**\n * What the local database runs on.\n *\n * `pglite` is the managed development database: one PGlite session behind a\n * socket server. Two things a restore relies on are not there. The server runs\n * each protocol message on its own, so the `CopyData` that follows a `COPY …\n * FROM stdin` never reaches the backend waiting for it: the COPY fails on\n * \"unexpected EOF\", and the session stops answering anyone until the daemon\n * is killed. And every connection shares that one session, so what pg_restore\n * `SET`s for itself stays set for everyone after it disconnects.\n */\nexport type PullTargetEngine = \"postgres\" | \"pglite\";\n\nexport interface PullPlan {\n /** Where the data comes from. */\n source: string;\n /** Where it lands. Always local. */\n target: string;\n anonymize: boolean;\n /** Schemas to copy. Empty means every non-system schema. */\n schemas: string[];\n /**\n * What the target runs on, which decides how the dump carries its rows.\n * Absent means `postgres`. {@link dumpSource} reads it off the target.\n */\n targetEngine?: PullTargetEngine;\n}\n\n/**\n * Rows per `INSERT` in a dump for the managed database.\n *\n * One per statement restored 100k rows in 8s; a hundred took 0.8s. A hundred\n * 512KB rows, a 100MB statement, still went in.\n */\nconst ROWS_PER_INSERT = 100;\n\n/**\n * `pg_dump` arguments for the source.\n *\n * `--no-owner` and `--no-acl` because the roles on a production server do not\n * exist locally, and without them every `ALTER … OWNER TO` in the dump fails and\n * buries the real output in noise. `--format=custom` so `pg_restore` can be told\n * to continue past errors selectively rather than all-or-nothing.\n *\n * For the managed database the rows travel as `INSERT`s, which PGlite takes\n * like any other statement, instead of `COPY` data, which it cannot take at\n * all — see {@link PullTargetEngine}. The copy is the same; only how the rows\n * are written in the dump differs.\n */\nexport function dumpArgs(plan: PullPlan): string[] {\n const args = [\"--format=custom\", \"--no-owner\", \"--no-acl\", \"--no-privileges\"];\n if (plan.targetEngine === \"pglite\") args.push(`--rows-per-insert=${ROWS_PER_INSERT}`);\n for (const schema of plan.schemas) args.push(\"--schema\", schema);\n args.push(\"--dbname\", splitPassword(plan.source).connectionString);\n\n return args;\n}\n\n/** The environment `pg_dump` runs with: the source's password, if it has one. */\nexport function dumpEnv(plan: PullPlan): Record<string, string> {\n return passwordEnv(plan.source);\n}\n\n/** The environment `pg_restore` runs with: the target's password, if it has one. */\nexport function restoreEnv(plan: PullPlan): Record<string, string> {\n return passwordEnv(plan.target);\n}\n\nfunction passwordEnv(connectionString: string): Record<string, string> {\n const { password } = splitPassword(connectionString);\n return password === null ? {} : { PGPASSWORD: password };\n}\n\n/**\n * A libpq connection string without its password, and the password.\n *\n * The password goes in `PGPASSWORD`, never on a command line, where every\n * process listing can read it and every failure prints it back (execa's error\n * repeats the command). The same rule `db backup` follows; this mirrors\n * `splitPassword` in `@rebasepro/server-postgres`'s connection-string utils,\n * which is not exported. Covers the user-info password and a `password=`\n * parameter; anything that does not parse as a postgres URL is left alone.\n */\nfunction splitPassword(connectionString: string): { connectionString: string; password: string | null } {\n let url: URL;\n try {\n url = new URL(connectionString);\n } catch {\n return { connectionString, password: null };\n }\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") return { connectionString, password: null };\n\n const fromUserInfo = url.password ? decodePercent(url.password) : null;\n const fromParameter = url.searchParams.get(\"password\");\n if (fromUserInfo === null && fromParameter === null) return { connectionString, password: null };\n\n url.password = \"\";\n if (fromParameter !== null) {\n url.searchParams.delete(\"password\");\n // By hand: URLSearchParams writes a space as `+`, which libpq reads literally.\n url.search = Array.from(url.searchParams.entries())\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join(\"&\");\n }\n return { connectionString: url.toString(), password: fromUserInfo ?? fromParameter };\n}\n\nfunction decodePercent(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n/**\n * `pg_restore` arguments for the target.\n *\n * It restores into schemas {@link restoreCopy} has already emptied, so\n * `--clean --if-exists` drops nothing that is there; it stays for whatever a\n * dump holds outside them — an extension, say — which would otherwise fail as\n * already existing. `--no-owner` for the same reason as the dump.\n */\nexport function restoreArgs(plan: PullPlan, dumpFile: string): string[] {\n return [\"--clean\", \"--if-exists\", \"--no-owner\", \"--no-privileges\", \"--dbname\", splitPassword(plan.target).connectionString, dumpFile];\n}\n\n/**\n * Is this PostgreSQL client tool on PATH, and what version? Both are checked\n * before anything destructive: discovering `pg_restore` is missing after the\n * dump has been taken — or after `--clean` has emptied the target — is the\n * worst possible ordering.\n */\nexport async function findPgTool(tool: \"pg_dump\" | \"pg_restore\"): Promise<string | null> {\n try {\n const { stdout } = await execa(tool, [\"--version\"]);\n\n return stdout.trim();\n } catch {\n return null;\n }\n}\n\n/** PGlite names itself in `version()`: `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32-…`. */\nfunction engineOf(version: string): PullTargetEngine {\n return /\\bPGlite\\b/i.test(version) ? \"pglite\" : \"postgres\";\n}\n\nconst NOTHING_CHANGED = \"Nothing was changed: the local database is as it was.\";\n\n/** What the target database runs on, or why it could not be asked. */\nasync function readTargetEngine(target: string): Promise<{ engine: PullTargetEngine } | { error: unknown }> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: target, connectionTimeoutMillis: 10_000 });\n client.on(\"error\", () => undefined);\n try {\n await client.connect();\n const { rows } = await client.query<{ version: string }>(\"SELECT version() AS version\");\n\n return { engine: engineOf(rows[0]?.version ?? \"\") };\n } catch (error) {\n return { error };\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n/** How {@link dumpSource} ended. */\nexport type DumpOutcome =\n | {\n ok: true;\n /** The plan the dump was taken for, with its target's engine read: what {@link restoreCopy} takes. */\n plan: PullPlan;\n }\n | {\n ok: false;\n /** What failed and what to do, as plain lines for the caller to print. Nothing was changed. */\n lines: string[];\n };\n\n/**\n * `pg_dump` the source into `dumpFile`, in the form the target can restore.\n *\n * It asks the target what it runs on first — the managed database cannot take\n * `COPY` data, see {@link PullTargetEngine} — which also finds a local\n * database that is not there before anything is read from the source.\n *\n * pg_dump's stderr is kept rather than shown as it arrives, so a failure is\n * reported once, in the reader's terms: an unreachable source, or credentials\n * it refused, get the banner every database command prints for a connection\n * that failed, and anything else is pg_dump's own words with a line saying\n * nothing was changed. A dump that succeeds with warnings still shows them.\n * A dump that fails leaves no file behind.\n */\nexport async function dumpSource(plan: PullPlan, dumpFile: string): Promise<DumpOutcome> {\n try {\n const outcome = await dumpInto(plan, dumpFile);\n if (!outcome.ok) fs.rmSync(dumpFile, { force: true });\n\n return outcome;\n } catch (error) {\n fs.rmSync(dumpFile, { force: true });\n throw error;\n }\n}\n\nasync function dumpInto(plan: PullPlan, dumpFile: string): Promise<DumpOutcome> {\n let targetEngine = plan.targetEngine;\n if (!targetEngine) {\n const read = await readTargetEngine(plan.target);\n if (\"error\" in read) {\n return {\n ok: false,\n lines: banner(`Cannot connect to PostgreSQL at ${hostOf(plan.target)}`,\n `The driver said: ${connectionErrorText(read.error)}`, [\n \"That is the local database the pull would replace, and nothing\",\n \"was read from the source.\",\n \"Start it — `rebase dev` starts the managed one, `rebase dev --docker`\",\n \"Postgres in Docker — or check DATABASE_URL in .env, then pull again.\"\n ])\n };\n }\n targetEngine = read.engine;\n }\n\n const planned: PullPlan = { ...plan, targetEngine };\n const run = await execa(\"pg_dump\", [...dumpArgs(planned), \"--file\", dumpFile], {\n env: dumpEnv(planned),\n stdin: \"ignore\",\n stdout: \"inherit\",\n stderr: \"pipe\",\n reject: false\n });\n if (run.exitCode === 0) {\n if (run.stderr.trim()) process.stderr.write(`${run.stderr.trimEnd()}\\n`);\n\n return { ok: true, plan: planned };\n }\n\n return { ok: false, lines: describeDumpFailure(plan.source, run) };\n}\n\n/**\n * Why `pg_dump` could not read the source, as plain lines for the caller to\n * print. The dump is taken before anything local is touched, so every one of\n * them ends the same way: nothing was changed.\n */\nexport function describeDumpFailure(source: string, run: RestoreRun): string[] {\n if (run.signal) return [`✗ pg_dump was stopped by ${run.signal} before it finished.`, NOTHING_CHANGED];\n if (run.exitCode === undefined) {\n return [\n `✗ pg_dump could not be started (${run.code ?? \"unknown error\"}).`,\n NOTHING_CHANGED,\n \"Check that the PostgreSQL client tools are installed (`pg_dump --version`).\"\n ];\n }\n\n const stderr = (run.stderr ?? \"\").trim();\n const host = hostOf(source);\n const error = /pg_dump: error: (.+)/.exec(stderr)?.[1]?.trim() ?? null;\n const said = error ? `pg_dump said: ${error}` : null;\n\n if (/could not translate host name|could not connect to server|connection to server .* failed: (?:Connection refused|timeout expired|No route to host|Network is unreachable|Operation timed out|Host is down)/i.test(stderr)) {\n return banner(`Cannot connect to PostgreSQL at ${host}`, said, [\n \"That is the database --from names, which the pull reads.\",\n NOTHING_CHANGED,\n \"Check the host and port in --from, and that this machine can reach\",\n \"that server: a VPN, an allow-list, or an SSH tunnel.\"\n ]);\n }\n if (/password authentication failed|no pg_hba\\.conf entry|role \"[^\"]+\" does not exist/i.test(stderr)) {\n let user = \"unknown\";\n try {\n user = decodePercent(new URL(source).username) || \"unknown\";\n } catch { /* named as unknown */ }\n return banner(`Authentication failed for user \"${user}\" at ${host}`, said, [\n \"PostgreSQL rejected the credentials --from carries.\",\n NOTHING_CHANGED,\n \"Check the user and password in --from.\"\n ]);\n }\n // The server answered and has no database of that name: a typo in\n // --from, usually, or a database not created yet.\n const missing = /database \"([^\"]+)\" does not exist/i.exec(stderr)?.[1];\n if (missing) {\n return banner(`Database \"${missing}\" does not exist at ${host}`, said, [\n \"That is the database --from names, which the pull reads.\",\n NOTHING_CHANGED,\n \"Check the database name in --from.\"\n ]);\n }\n\n const versions = /server version mismatch/i.test(stderr)\n ? [\"pg_dump is older than the server: install client tools at least as new as it (`brew install libpq`).\"]\n : [];\n return [\n ...(stderr ? stderr.split(\"\\n\") : []),\n `✗ pg_dump could not dump ${describeTarget(source)} (exit code ${run.exitCode}).`,\n NOTHING_CHANGED,\n ...versions\n ];\n}\n\n/** The fields of a finished `pg_restore` (or `pg_dump`) run that decide {@link restoreOutcome}. */\nexport interface RestoreRun {\n exitCode?: number;\n signal?: string;\n /** A spawn failure's code — `ENOENT` when the binary vanished. */\n code?: string;\n stderr?: string;\n}\n\n/** What a `pg_restore` run says about the copy. */\nexport interface RestoreRunOutcome {\n /** Whether anything can have arrived — what decides whether an anonymization pass has data to redact. */\n started: boolean;\n /** Null when the copy is complete. */\n failure: string | null;\n /**\n * What to do about the failure. \"Pull again\" only where a second pull is\n * what finishes the job: everything else repeats until its cause is fixed.\n */\n advice: string | null;\n}\n\n/**\n * What a finished `pg_restore` says about the copy.\n *\n * The target was emptied before anything was written, so every way this can\n * end short of exit 0 leaves a partial or empty local database, and none of\n * them may be reported as a copy. A non-zero exit is pg_restore saying it\n * skipped statements — \"errors ignored on restore: N\" — and whatever those\n * statements created (a table, its rows, an RLS policy) is missing locally.\n *\n * Restoring into an emptied database, those errors come from what the source\n * holds that this Postgres cannot create — an extension it does not have, a\n * feature of a newer server — and a second pull meets them again. So the\n * advice names that, rather than \"pull again\", which sent people in a loop.\n * On the managed database an extension cannot be installed at all, so the\n * advice there is Postgres in Docker.\n */\nexport function restoreOutcome(run: RestoreRun, engine: PullTargetEngine = \"postgres\"): RestoreRunOutcome {\n if (run.signal) {\n return {\n started: true,\n failure: `pg_restore was stopped by ${run.signal} before it finished.`,\n advice: \"It was interrupted, not refused: pull again to finish the copy.\"\n };\n }\n if (run.exitCode === undefined) {\n return {\n started: false,\n failure: `pg_restore could not be started (${run.code ?? \"unknown error\"}).`,\n advice: \"Check that the PostgreSQL client tools are installed (`pg_restore --version`), then pull again.\"\n };\n }\n if (run.exitCode === 0) return { started: true, failure: null, advice: null };\n\n const ignored = /errors ignored on restore: (\\d+)/.exec(run.stderr ?? \"\");\n const repeats = engine === \"pglite\"\n ? \"Pulling again repeats these errors until what they name is fixed — most often an extension the \"\n + `source uses that the managed database does not have (it has ${PGLITE_EXTENSIONS.map(extension => extension.name).join(\", \")}). `\n + \"Pull into Postgres in Docker instead (`rebase db pull --docker`), or pull only the schemas that do \"\n + \"not need it (--schema).\"\n : \"Pulling again repeats these errors until what they name is fixed — most often an extension \"\n + \"the source uses that this Postgres does not have installed. Install it here, or pull only the schemas \"\n + \"that do not need it (--schema).\";\n\n return {\n started: true,\n failure: ignored\n ? `pg_restore could not restore ${ignored[1]} object(s) — the errors are above.`\n : `pg_restore exited with code ${run.exitCode} — the errors are above.`,\n advice: repeats\n };\n}\n\n/** How {@link restoreCopy} ended. */\nexport interface RestoreOutcome extends RestoreRunOutcome {\n /** Whether the local database was emptied first. False means it is exactly as it was. */\n cleared: boolean;\n}\n\n/**\n * The local schemas a pull empties before it restores: every one of the local\n * database's own for a whole-database pull, and those it copies for a\n * `--schema` pull. `pg_catalog`, `information_schema` and the `pg_*` internals\n * never — see {@link provisionableSchemas}.\n */\nexport function schemasToClear(present: readonly { schema: string }[], planned: readonly string[]): string[] {\n const local = provisionableSchemas(present);\n\n return planned.length === 0 ? local : local.filter((schema) => planned.includes(schema));\n}\n\n/**\n * The statements that empty those schemas, in one transaction.\n *\n * `CASCADE`, which `pg_restore --clean` never uses: a table the source lacks —\n * `rebase.atlas_schema_revisions` from `rebase db migrate`, a collection added\n * locally that references a pulled table — is exactly what blocked its plain\n * `DROP SCHEMA` and `DROP TABLE`. `public` is put back empty, with the access\n * every role has to it by default, because a dump does not create a default\n * `public` and every table in it would otherwise have nowhere to go.\n */\nexport function clearStatements(schemas: readonly string[]): string[] {\n const statements = schemas.map((schema) => `DROP SCHEMA IF EXISTS ${quoteIdentifier(schema)} CASCADE`);\n if (schemas.includes(\"public\")) statements.push(\"CREATE SCHEMA public\", \"GRANT USAGE ON SCHEMA public TO PUBLIC\");\n\n return statements;\n}\n\n/**\n * How long emptying the local database waits for a lock before giving up.\n *\n * A `rebase dev` serving from the database holds connections, and one inside\n * a transaction would otherwise make the pull hang without a word.\n */\nconst CLEAR_LOCK_TIMEOUT = \"15s\";\n\n/** Why emptying the local database failed, in the reader's terms, and what fixes it. */\nfunction clearFailure(error: unknown): RestoreOutcome {\n const code = typeof error === \"object\" && error !== null && \"code\" in error ? String(error.code) : \"\";\n const reason = errorText(error);\n const advice = code === \"55P03\"\n ? \"Something holds a lock on it — usually `rebase dev` serving from it. Stop that, then pull again.\"\n : code === \"42501\"\n ? \"This connection may not drop those schemas. Connect as the role that owns them — the one \"\n + \"that created them — and pull with that.\"\n : \"Fix what the error names; pulling again before that repeats it.\";\n\n return {\n cleared: false,\n started: false,\n failure: `Could not empty the local database before restoring: ${reason}`,\n advice\n };\n}\n\n/**\n * Replace the local database's contents with a dump: empty the schemas it\n * holds, then `pg_restore` into them.\n *\n * Only ever `plan.target`, the local development database the command\n * resolved — the pull's one destination, which no flag can change. Emptying it\n * is one transaction: if any schema cannot be dropped, none is, nothing is\n * restored, and the database is as it was.\n *\n * On the managed database the dump must be one {@link dumpSource} took for it,\n * with its rows as INSERTs: a COPY would leave the daemon answering nobody, so\n * one is refused before anything is emptied. And afterwards its one session —\n * every connection's — is given back its own settings: pg_restore leaves\n * `search_path` empty and `row_security` off behind it, which a running\n * `rebase dev` would otherwise query under.\n */\nexport async function restoreCopy(plan: PullPlan, dumpFile: string): Promise<RestoreOutcome> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: plan.target });\n client.on(\"error\", () => undefined);\n let engine: PullTargetEngine;\n try {\n await client.connect();\n engine = engineOf((await client.query<{ version: string }>(\"SELECT version() AS version\")).rows[0]?.version ?? \"\");\n if (engine === \"pglite\" && plan.targetEngine !== \"pglite\") {\n return {\n cleared: false,\n started: false,\n failure: \"The managed development database cannot take this dump: its rows are COPY data, which PGlite's socket server cannot receive.\",\n advice: \"Pull into Postgres in Docker instead: `rebase db pull --docker`.\"\n };\n }\n const { rows } = await client.query<{ schema: string }>(\"SELECT nspname AS \\\"schema\\\" FROM pg_namespace\");\n await client.query(\"BEGIN\");\n await client.query(`SET LOCAL lock_timeout = '${CLEAR_LOCK_TIMEOUT}'`);\n for (const statement of clearStatements(schemasToClear(rows, plan.schemas))) await client.query(statement);\n await client.query(\"COMMIT\");\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n return clearFailure(error);\n } finally {\n await client.end().catch(() => undefined);\n }\n\n // stderr is shown as it arrives and kept, for the count in its summary\n // line. Not rejected: every way this ends is judged by `restoreOutcome`.\n const run = await execa(\"pg_restore\", restoreArgs(plan, dumpFile), {\n env: restoreEnv(plan),\n stdin: \"ignore\",\n stdout: \"inherit\",\n stderr: [\"pipe\", \"inherit\"],\n reject: false\n });\n\n if (engine === \"pglite\") await resetSharedSession(plan.target);\n\n return { cleared: true, ...restoreOutcome(run, engine) };\n}\n\n/**\n * Put the managed database's one session back to its own settings.\n *\n * `RESET ALL` and `DEALLOCATE ALL`, not `DISCARD ALL`: the session is every\n * connection's, and `DISCARD` would also drop the `LISTEN` a running\n * `rebase dev`'s realtime holds on it. The same pair the backup service runs\n * after its tools (`resetSharedSession` in server-postgres' backup-service). Best effort: a daemon that cannot take this cannot take the\n * re-grant that follows either, and that step reports it.\n */\nasync function resetSharedSession(target: string): Promise<void> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: target, connectionTimeoutMillis: 10_000, query_timeout: 15_000 });\n client.on(\"error\", () => undefined);\n try {\n await client.connect();\n await client.query(\"RESET ALL\");\n await client.query(\"DEALLOCATE ALL\");\n } catch {\n // Reported by the next step that connects; see above.\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n/**\n * The lines that report a failed restore: what failed, what state the local\n * database is in now, and what to do. Plain text, for the caller to colour.\n */\nexport function describeRestoreFailure(outcome: RestoreOutcome, source: string): string[] {\n const state = outcome.started\n ? `The local database holds an incomplete copy of ${source}.`\n : outcome.cleared\n ? \"The local database was emptied, and nothing was restored into it.\"\n : \"Nothing was restored, and the local database is as it was.\";\n\n return [outcome.failure ?? \"The restore did not complete.\", state, ...(outcome.advice ? [outcome.advice] : [])];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pull-C3PXduzl.js","names":[],"sources":["../src/dev-db/pull.ts"],"sourcesContent":["/**\n * `rebase db pull` — copy a database's contents into local development.\n *\n * The common case is production into local, and the reason it exists is that\n * the alternative is worse: without it people hand-roll a `pg_dump | psql` and\n * get the flags wrong in ways that either fail loudly at 2am or, more often,\n * quietly restore half a schema.\n *\n * Three things this command insists on, because copying a production database\n * onto a laptop is a data-protection event whether or not anyone calls it one:\n *\n * 1. **It says what it is about to do, in full, before doing it** — which\n * database it will read, which one it will overwrite, and where the data will\n * come to rest on disk. The target path matters: people forget that\n * `.rebase/pgdata` is a directory their backup software may be indexing.\n *\n * 2. **It refuses to run unattended without being told to.** The target is\n * destroyed, so a mistyped `--from` with no confirmation would take the\n * developer's working database with it.\n *\n * 3. **It will not write to a remote database.** The target is always the local\n * development database; there is no flag that makes this push. A tool that\n * can copy in both directions eventually copies in the wrong one.\n *\n * Anonymization is opt-in (`--anonymize`), which is a deliberate choice and not\n * an obviously safe one — the flag nobody types is the flag nobody gets. It is\n * a best-effort pass over columns whose *names* look like personal data, and\n * {@link ANONYMIZE_PATTERNS} says exactly which. It cannot find personal data in\n * a column called `notes`, and this file says so rather than implying a\n * guarantee it cannot keep.\n *\n * What it does guarantee is that it never half-happens: {@link anonymizeCopy}\n * runs the pass in one transaction, and when the pass fails it empties the\n * tables it would have rewritten rather than leave their real values behind.\n */\n\nimport { execa } from \"execa\";\nimport fs from \"fs\";\nimport type { Client } from \"pg\";\n\nimport { banner, connectionErrorText, hostOf } from \"./connection-banner\";\nimport { PGLITE_EXTENSIONS } from \"./constraints\";\n\n/**\n * How one kind of personal-looking column is overwritten.\n *\n * Two forms, because a constant is the most readable replacement and is wrong\n * for exactly one kind of column: a unique one. Every `token_hash` becoming\n * `'REDACTED'` is what `UNIQUE(token_hash)` refused on the second row of\n * `rebase.refresh_tokens` — so on any project where someone had signed in twice.\n */\nexport interface AnonymizeRule {\n pattern: RegExp;\n /**\n * Only on tables whose name matches, when set. For a column name that is\n * personal on one table and not on another: a user's `name` is a person's,\n * a tag's is not.\n */\n tables?: RegExp;\n /** The value for a column no unique index covers, or null to always use the unique form. */\n value: string | null;\n /** The unique form is `prefix` + random hex + `suffix`, cut to fit the column. */\n prefix: string;\n suffix: string;\n}\n\n/**\n * Tables whose rows are people, by name: `rebase.users` and the usual spellings\n * of a project's own. Singular or plural.\n */\nconst PERSON_TABLES = /^(users?|user_profiles?|profiles?|people|persons?|members?|customers?|contacts?|employees?)$/i;\n\n/**\n * Column-name patterns the anonymizer overwrites.\n *\n * Names, not contents: inspecting values would be slower, and would still miss\n * the same things. This is a reasonable-effort measure for making a local copy\n * less dangerous, and it is not a compliance control.\n */\nexport const ANONYMIZE_PATTERNS: readonly AnonymizeRule[] = [\n // Emails are unique in practice even where nothing enforces it, so they\n // always get a value of their own.\n { pattern: /^(.*_)?e?mail(_.*)?$/i, value: null, prefix: \"user-\", suffix: \"@example.invalid\" },\n { pattern: /^(.*_)?(phone|mobile|tel|telephone)(_.*)?$/i, value: \"+10000000000\", prefix: \"+1-\", suffix: \"\" },\n // `display_name` is how `rebase.users` holds every user's name.\n {\n pattern: /^(.*_)?(display_name|first_name|last_name|full_name|middle_name|family_name|surname|given_name)(_.*)?$/i,\n value: \"Redacted\",\n prefix: \"Redacted-\",\n suffix: \"\"\n },\n // A bare `name` is a person's only on a table of people.\n { pattern: /^name$/i, tables: PERSON_TABLES, value: \"Redacted\", prefix: \"Redacted-\", suffix: \"\" },\n { pattern: /^(.*_)?(address|street|postcode|zip|zipcode)(_.*)?$/i, value: \"Redacted\", prefix: \"Redacted-\", suffix: \"\" },\n { pattern: /^(.*_)?(ssn|tax_id|national_id|passport)(_.*)?$/i, value: \"REDACTED\", prefix: \"REDACTED-\", suffix: \"\" },\n { pattern: /^(.*_)?(password|password_hash|secret|token|api_key|access_token|refresh_token)(_.*)?$/i, value: \"REDACTED\", prefix: \"REDACTED-\", suffix: \"\" },\n { pattern: /^(.*_)?(ip|ip_address|user_agent)(_.*)?$/i, value: \"REDACTED\", prefix: \"REDACTED-\", suffix: \"\" }\n];\n\n/** The rule that overwrites this column, if any. Without a table, a table-bound rule never applies. */\nfunction ruleFor(columnName: string, table?: string): AnonymizeRule | undefined {\n return ANONYMIZE_PATTERNS.find((rule) =>\n rule.pattern.test(columnName) && (!rule.tables || (table !== undefined && rule.tables.test(table))));\n}\n\nexport function shouldAnonymize(columnName: string, table?: string): boolean {\n return ruleFor(columnName, table) !== undefined;\n}\n\n/** A text-ish column the anonymizer can overwrite without a type error. */\nexport interface ColumnRef {\n schema: string;\n table: string;\n column: string;\n dataType: string;\n /** `character_maximum_length`: null or absent for an unbounded type. */\n maxLength?: number | null;\n /** Covered by a unique index — a primary key, a UNIQUE constraint, or a unique expression index. */\n unique?: boolean;\n}\n\n/** Random hex of this length, per row. `gen_random_uuid()` is core since PostgreSQL 13. */\nfunction randomHex(length: number): string {\n return `left(replace(gen_random_uuid()::text, '-', ''), ${length})`;\n}\n\nfunction sqlLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Fewest random hex digits a unique replacement keeps. Twelve is 16^12 values:\n * a collision needs millions of rows, where eight would need tens of thousands.\n */\nconst MIN_UNIQUE_HEX = 12;\n\n/**\n * The SQL expression that overwrites one column, or null when no rule names it.\n *\n * Unique per row when an index makes the column unique, and always cut to the\n * column's declared length — `zip char(5)` refused `'Redacted'` outright, and a\n * failed statement now empties the tables rather than skip one. To fit a short\n * unique column the prefix goes first, then the suffix, and the random part is\n * kept to at least {@link MIN_UNIQUE_HEX} digits while there is room for them.\n * A NULL stays NULL: an empty optional field is not personal data.\n */\nexport function replacementFor(column: ColumnRef): string | null {\n const rule = ruleFor(column.column, column.table);\n if (!rule) return null;\n const limit = typeof column.maxLength === \"number\" && column.maxLength > 0 ? column.maxLength : null;\n\n let value: string;\n if (rule.value !== null && !column.unique) {\n value = sqlLiteral(limit === null ? rule.value : rule.value.slice(0, limit));\n } else {\n const shapes = [[rule.prefix, rule.suffix], [\"\", rule.suffix], [\"\", \"\"]];\n const fitting = shapes.find(([prefix, suffix]) =>\n limit === null || limit - prefix.length - suffix.length >= MIN_UNIQUE_HEX);\n const [prefix, suffix] = fitting ?? [\"\", \"\"];\n const hex = Math.min(32, limit === null ? 32 : limit - prefix.length - suffix.length);\n value = `concat(${sqlLiteral(prefix)}, ${randomHex(Math.max(1, hex))}, ${sqlLiteral(suffix)})`;\n }\n\n const name = quoteIdentifier(column.column);\n return `CASE WHEN ${name} IS NULL THEN NULL ELSE ${value} END`;\n}\n\n/** A double-quoted identifier, with any `\"` in it doubled — a table called `order` or `a\"b` still works. */\nexport function quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Anonymizable columns: name looks personal, and the type can hold the\n * replacement.\n *\n * The type check is what stops this generating `UPDATE … SET user_id =\n * 'Redacted'` for an integer column called `user_id_email_seq` and failing the\n * whole pass on a technicality.\n */\nexport function anonymizableColumns(columns: readonly ColumnRef[]): ColumnRef[] {\n const textual = new Set([\"text\", \"character varying\", \"varchar\", \"character\", \"char\", \"citext\"]);\n\n return columns.filter((column) =>\n shouldAnonymize(column.column, column.table) && textual.has(column.dataType.toLowerCase()));\n}\n\n/** `\"schema\".\"table\"` for each table the pass rewrites, sorted. */\nexport function anonymizedTables(columns: readonly ColumnRef[]): string[] {\n return [...new Set(anonymizableColumns(columns).map((column) =>\n `${quoteIdentifier(column.schema)}.${quoteIdentifier(column.table)}`))].sort();\n}\n\n/** `UPDATE` statements for one anonymization pass, in a stable order. */\nexport function anonymizeStatements(columns: readonly ColumnRef[]): string[] {\n const byTable = new Map<string, ColumnRef[]>();\n for (const column of anonymizableColumns(columns)) {\n const key = `${quoteIdentifier(column.schema)}.${quoteIdentifier(column.table)}`;\n byTable.set(key, [...(byTable.get(key) ?? []), column]);\n }\n\n return [...byTable.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([table, cols]) => {\n const assignments = [...cols]\n .sort((a, b) => a.column.localeCompare(b.column))\n .map((column) => `${quoteIdentifier(column.column)} = ${replacementFor(column)}`)\n .join(\", \");\n\n return `UPDATE ${table} SET ${assignments};`;\n });\n}\n\n/**\n * Every column the pass could overwrite, with what decides how.\n *\n * Base tables only — a view has an `email` column too, and is not something to\n * UPDATE. Generated columns are skipped for the same reason: `email_domain\n * GENERATED ALWAYS AS (…)` matches by name and refuses any value but DEFAULT,\n * and it follows the column it is generated from anyway. An extension type such\n * as `citext` reports `USER-DEFINED`, so its own name is what is compared.\n *\n * `unique` comes from the indexes, through `pg_depend` as well as `indkey`: a\n * unique index on `lower(email)` names no column in `indkey`, and is exactly\n * what a constant replacement would violate.\n */\nexport const ANONYMIZE_COLUMNS_SQL = `\nWITH unique_columns AS (\n SELECT DISTINCT i.indrelid AS relid, a.attname AS column_name\n FROM pg_index i\n JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum > 0 AND NOT a.attisdropped\n WHERE i.indisunique\n AND (a.attnum = ANY (i.indkey)\n OR EXISTS (SELECT 1 FROM pg_depend d\n WHERE d.classid = 'pg_class'::regclass AND d.objid = i.indexrelid\n AND d.refclassid = 'pg_class'::regclass AND d.refobjid = i.indrelid\n AND d.refobjsubid = a.attnum))\n)\nSELECT c.table_schema AS \"schema\",\n c.table_name AS \"table\",\n c.column_name AS \"column\",\n CASE WHEN c.data_type = 'USER-DEFINED' THEN c.udt_name ELSE c.data_type END AS \"dataType\",\n c.character_maximum_length AS \"maxLength\",\n (u.column_name IS NOT NULL) AS \"unique\"\n FROM information_schema.columns c\n JOIN information_schema.tables t ON t.table_schema = c.table_schema AND t.table_name = c.table_name\n LEFT JOIN unique_columns u\n ON u.relid = format('%I.%I', c.table_schema, c.table_name)::regclass AND u.column_name = c.column_name\n WHERE t.table_type = 'BASE TABLE'\n AND c.is_generated = 'NEVER'\n AND c.table_schema NOT IN ('pg_catalog', 'information_schema')`;\n\n/** How {@link anonymizeCopy} ended. */\nexport type AnonymizeOutcome =\n | { ok: true; tables: string[] }\n | {\n ok: false;\n /** Why the pass failed. Nothing it wrote was kept. */\n error: string;\n /** The tables emptied instead, so none of their real values are left. */\n emptied: string[];\n /** Why they could not be emptied, when they could not: the copy then holds real data. */\n emptyError: string | null;\n };\n\nfunction errorText(error: unknown): string {\n return error instanceof Error && error.message ? error.message : String(error);\n}\n\n/**\n * Overwrite the personal-looking columns of a restored copy, all or nothing.\n *\n * One transaction, so a failure part-way leaves no table half done — the old\n * pass ran its UPDATEs one by one, so a failure on `rebase.refresh_tokens` left\n * `public.authors` anonymized and `rebase.users` holding every real email.\n *\n * A rollback alone would be worse than that, though: it puts back the real\n * value of every column, in a copy the developer asked to be anonymized. So on\n * failure the tables the pass would have rewritten are emptied (`TRUNCATE …\n * CASCADE`, which takes the rows that reference them too), and the outcome\n * says which — or, if even that failed, says the copy holds real data, so the\n * caller can say it louder.\n *\n * `row_security = off` makes a role that RLS would filter fail the pass instead\n * of rewriting only the rows its policies show it, which looked like success.\n */\nexport async function anonymizeCopy(client: Client): Promise<AnonymizeOutcome> {\n let listed = false;\n let tables: string[] = [];\n try {\n const { rows } = await client.query<ColumnRef>(ANONYMIZE_COLUMNS_SQL);\n tables = anonymizedTables(rows);\n listed = true;\n await client.query(\"BEGIN\");\n await client.query(\"SET LOCAL row_security = off\");\n for (const statement of anonymizeStatements(rows)) await client.query(statement);\n await client.query(\"COMMIT\");\n\n return { ok: true, tables };\n } catch (error) {\n const reason = errorText(error);\n await client.query(\"ROLLBACK\").catch(() => undefined);\n if (!listed) {\n return { ok: false, error: reason, emptied: [], emptyError: \"the tables to empty could not be listed\" };\n }\n if (tables.length === 0) return { ok: false, error: reason, emptied: [], emptyError: null };\n try {\n await client.query(`TRUNCATE TABLE ${tables.join(\", \")} CASCADE`);\n\n return { ok: false, error: reason, emptied: tables, emptyError: null };\n } catch (emptyError) {\n return { ok: false, error: reason, emptied: [], emptyError: errorText(emptyError) };\n }\n }\n}\n\n/**\n * Schemas the restored copy needs the app role provisioned on.\n *\n * `pg_dump --no-privileges` strips every GRANT, so a pulled database arrives\n * with its RLS policies and its `FORCE ROW LEVEL SECURITY` intact and no\n * privileges behind them. Measured on a 30-table project: 68 policies restored,\n * 14 tables with RLS on, and **0** grants to `rebase_user` — where the source\n * had 60. Reading one table as the role Rebase serves every request through:\n *\n * source: 6\n * copy: ERROR: permission denied for table leads\n *\n * The dump flags are right and stay: without `--no-owner`/`--no-privileges`\n * every `ALTER … OWNER TO` and `GRANT … TO <prod role>` in the dump fails\n * against roles that do not exist on a laptop, and buries the real output. The\n * repair belongs after the restore, and belongs to `ensureAppRole` — the same\n * routine boot and `rebase db push` call — rather than to a second list of\n * grants written here.\n *\n * Which schemas: every non-system schema the restored database actually has.\n * Boot knows its `managedSchemas` from the collections; a restore knows only\n * what arrived, and a source may carry schemas this project does not declare.\n * `pg_catalog`, `information_schema` and the `pg_*` internals are never ours to\n * grant on and PostgreSQL would refuse anyway.\n */\nexport function provisionableSchemas(rows: readonly { schema: string }[]): string[] {\n const skip = (name: string) => name === \"information_schema\" || name.startsWith(\"pg_\");\n\n return [...new Set(rows.map((row) => row.schema))].filter((name) => !skip(name)).sort();\n}\n\n/** Host and database of a connection string, with no credentials in it. */\nexport function describeTarget(connectionString: string): string {\n try {\n const url = new URL(connectionString);\n const database = url.pathname.replace(/^\\//, \"\") || \"(default)\";\n\n return `${url.hostname}${url.port ? `:${url.port}` : \"\"}/${database}`;\n } catch {\n // Never echo the raw string: it carries a password, and this line is\n // printed to a terminal people paste into issues.\n return \"(unparseable connection string)\";\n }\n}\n\n/**\n * What the local database runs on.\n *\n * `pglite` is the managed development database: one PGlite session behind a\n * socket server. Two things a restore relies on are not there. The server runs\n * each protocol message on its own, so the `CopyData` that follows a `COPY …\n * FROM stdin` never reaches the backend waiting for it: the COPY fails on\n * \"unexpected EOF\", and the session stops answering anyone until the daemon\n * is killed. And every connection shares that one session, so what pg_restore\n * `SET`s for itself stays set for everyone after it disconnects.\n */\nexport type PullTargetEngine = \"postgres\" | \"pglite\";\n\nexport interface PullPlan {\n /** Where the data comes from. */\n source: string;\n /** Where it lands. Always local. */\n target: string;\n anonymize: boolean;\n /** Schemas to copy. Empty means every non-system schema. */\n schemas: string[];\n /**\n * What the target runs on, which decides how the dump carries its rows.\n * Absent means `postgres`. {@link dumpSource} reads it off the target.\n */\n targetEngine?: PullTargetEngine;\n}\n\n/**\n * Rows per `INSERT` in a dump for the managed database.\n *\n * One per statement restored 100k rows in 8s; a hundred took 0.8s. A hundred\n * 512KB rows, a 100MB statement, still went in.\n */\nconst ROWS_PER_INSERT = 100;\n\n/**\n * `pg_dump` arguments for the source.\n *\n * `--no-owner` and `--no-acl` because the roles on a production server do not\n * exist locally, and without them every `ALTER … OWNER TO` in the dump fails and\n * buries the real output in noise. `--format=custom` so `pg_restore` can be told\n * to continue past errors selectively rather than all-or-nothing.\n *\n * For the managed database the rows travel as `INSERT`s, which PGlite takes\n * like any other statement, instead of `COPY` data, which it cannot take at\n * all — see {@link PullTargetEngine}. The copy is the same; only how the rows\n * are written in the dump differs.\n */\nexport function dumpArgs(plan: PullPlan): string[] {\n const args = [\"--format=custom\", \"--no-owner\", \"--no-acl\", \"--no-privileges\"];\n if (plan.targetEngine === \"pglite\") args.push(`--rows-per-insert=${ROWS_PER_INSERT}`);\n for (const schema of plan.schemas) args.push(\"--schema\", schema);\n args.push(\"--dbname\", splitPassword(plan.source).connectionString);\n\n return args;\n}\n\n/** The environment `pg_dump` runs with: the source's password, if it has one. */\nexport function dumpEnv(plan: PullPlan): Record<string, string> {\n return passwordEnv(plan.source);\n}\n\n/** The environment `pg_restore` runs with: the target's password, if it has one. */\nexport function restoreEnv(plan: PullPlan): Record<string, string> {\n return passwordEnv(plan.target);\n}\n\nfunction passwordEnv(connectionString: string): Record<string, string> {\n const { password } = splitPassword(connectionString);\n return password === null ? {} : { PGPASSWORD: password };\n}\n\n/**\n * A libpq connection string without its password, and the password.\n *\n * The password goes in `PGPASSWORD`, never on a command line, where every\n * process listing can read it and every failure prints it back (execa's error\n * repeats the command). The same rule `db backup` follows; this mirrors\n * `splitPassword` in `@rebasepro/server-postgres`'s connection-string utils,\n * which is not exported. Covers the user-info password and a `password=`\n * parameter; anything that does not parse as a postgres URL is left alone.\n */\nfunction splitPassword(connectionString: string): { connectionString: string; password: string | null } {\n let url: URL;\n try {\n url = new URL(connectionString);\n } catch {\n return { connectionString, password: null };\n }\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") return { connectionString, password: null };\n\n const fromUserInfo = url.password ? decodePercent(url.password) : null;\n const fromParameter = url.searchParams.get(\"password\");\n if (fromUserInfo === null && fromParameter === null) return { connectionString, password: null };\n\n url.password = \"\";\n if (fromParameter !== null) {\n url.searchParams.delete(\"password\");\n // By hand: URLSearchParams writes a space as `+`, which libpq reads literally.\n url.search = Array.from(url.searchParams.entries())\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join(\"&\");\n }\n return { connectionString: url.toString(), password: fromUserInfo ?? fromParameter };\n}\n\nfunction decodePercent(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n/**\n * `pg_restore` arguments for the target.\n *\n * It restores into schemas {@link restoreCopy} has already emptied, so\n * `--clean --if-exists` drops nothing that is there; it stays for whatever a\n * dump holds outside them — an extension, say — which would otherwise fail as\n * already existing. `--no-owner` for the same reason as the dump.\n */\nexport function restoreArgs(plan: PullPlan, dumpFile: string): string[] {\n return [\"--clean\", \"--if-exists\", \"--no-owner\", \"--no-privileges\", \"--dbname\", splitPassword(plan.target).connectionString, dumpFile];\n}\n\n/**\n * Is this PostgreSQL client tool on PATH, and what version? Both are checked\n * before anything destructive: discovering `pg_restore` is missing after the\n * dump has been taken — or after `--clean` has emptied the target — is the\n * worst possible ordering.\n */\nexport async function findPgTool(tool: \"pg_dump\" | \"pg_restore\"): Promise<string | null> {\n try {\n const { stdout } = await execa(tool, [\"--version\"]);\n\n return stdout.trim();\n } catch {\n return null;\n }\n}\n\n/** PGlite names itself in `version()`: `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32-…`. */\nfunction engineOf(version: string): PullTargetEngine {\n return /\\bPGlite\\b/i.test(version) ? \"pglite\" : \"postgres\";\n}\n\nconst NOTHING_CHANGED = \"Nothing was changed: the local database is as it was.\";\n\n/** What the target database runs on, or why it could not be asked. */\nasync function readTargetEngine(target: string): Promise<{ engine: PullTargetEngine } | { error: unknown }> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: target, connectionTimeoutMillis: 10_000 });\n client.on(\"error\", () => undefined);\n try {\n await client.connect();\n const { rows } = await client.query<{ version: string }>(\"SELECT version() AS version\");\n\n return { engine: engineOf(rows[0]?.version ?? \"\") };\n } catch (error) {\n return { error };\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n/** How {@link dumpSource} ended. */\nexport type DumpOutcome =\n | {\n ok: true;\n /** The plan the dump was taken for, with its target's engine read: what {@link restoreCopy} takes. */\n plan: PullPlan;\n }\n | {\n ok: false;\n /** What failed and what to do, as plain lines for the caller to print. Nothing was changed. */\n lines: string[];\n };\n\n/**\n * `pg_dump` the source into `dumpFile`, in the form the target can restore.\n *\n * It asks the target what it runs on first — the managed database cannot take\n * `COPY` data, see {@link PullTargetEngine} — which also finds a local\n * database that is not there before anything is read from the source.\n *\n * pg_dump's stderr is kept rather than shown as it arrives, so a failure is\n * reported once, in the reader's terms: an unreachable source, or credentials\n * it refused, get the banner every database command prints for a connection\n * that failed, and anything else is pg_dump's own words with a line saying\n * nothing was changed. A dump that succeeds with warnings still shows them.\n * A dump that fails leaves no file behind.\n */\nexport async function dumpSource(plan: PullPlan, dumpFile: string): Promise<DumpOutcome> {\n try {\n const outcome = await dumpInto(plan, dumpFile);\n if (!outcome.ok) fs.rmSync(dumpFile, { force: true });\n\n return outcome;\n } catch (error) {\n fs.rmSync(dumpFile, { force: true });\n throw error;\n }\n}\n\nasync function dumpInto(plan: PullPlan, dumpFile: string): Promise<DumpOutcome> {\n let targetEngine = plan.targetEngine;\n if (!targetEngine) {\n const read = await readTargetEngine(plan.target);\n if (\"error\" in read) {\n return {\n ok: false,\n lines: banner(`Cannot connect to PostgreSQL at ${hostOf(plan.target)}`,\n `The driver said: ${connectionErrorText(read.error)}`, [\n \"That is the local database the pull would replace, and nothing\",\n \"was read from the source.\",\n \"Start it — `rebase dev` starts the managed one, `rebase dev --docker`\",\n \"Postgres in Docker — or check DATABASE_URL in .env, then pull again.\"\n ])\n };\n }\n targetEngine = read.engine;\n }\n\n const planned: PullPlan = { ...plan, targetEngine };\n const run = await execa(\"pg_dump\", [...dumpArgs(planned), \"--file\", dumpFile], {\n env: dumpEnv(planned),\n stdin: \"ignore\",\n stdout: \"inherit\",\n stderr: \"pipe\",\n reject: false\n });\n if (run.exitCode === 0) {\n if (run.stderr.trim()) process.stderr.write(`${run.stderr.trimEnd()}\\n`);\n\n return { ok: true, plan: planned };\n }\n\n return { ok: false, lines: describeDumpFailure(plan.source, run) };\n}\n\n/**\n * Why `pg_dump` could not read the source, as plain lines for the caller to\n * print. The dump is taken before anything local is touched, so every one of\n * them ends the same way: nothing was changed.\n */\nexport function describeDumpFailure(source: string, run: RestoreRun): string[] {\n if (run.signal) return [`✗ pg_dump was stopped by ${run.signal} before it finished.`, NOTHING_CHANGED];\n if (run.exitCode === undefined) {\n return [\n `✗ pg_dump could not be started (${run.code ?? \"unknown error\"}).`,\n NOTHING_CHANGED,\n \"Check that the PostgreSQL client tools are installed (`pg_dump --version`).\"\n ];\n }\n\n const stderr = (run.stderr ?? \"\").trim();\n const host = hostOf(source);\n const error = /pg_dump: error: (.+)/.exec(stderr)?.[1]?.trim() ?? null;\n const said = error ? `pg_dump said: ${error}` : null;\n\n if (/could not translate host name|could not connect to server|connection to server .* failed: (?:Connection refused|timeout expired|No route to host|Network is unreachable|Operation timed out|Host is down)/i.test(stderr)) {\n return banner(`Cannot connect to PostgreSQL at ${host}`, said, [\n \"That is the database --from names, which the pull reads.\",\n NOTHING_CHANGED,\n \"Check the host and port in --from, and that this machine can reach\",\n \"that server: a VPN, an allow-list, or an SSH tunnel.\"\n ]);\n }\n if (/password authentication failed|no pg_hba\\.conf entry|role \"[^\"]+\" does not exist/i.test(stderr)) {\n let user = \"unknown\";\n try {\n user = decodePercent(new URL(source).username) || \"unknown\";\n } catch { /* named as unknown */ }\n return banner(`Authentication failed for user \"${user}\" at ${host}`, said, [\n \"PostgreSQL rejected the credentials --from carries.\",\n NOTHING_CHANGED,\n \"Check the user and password in --from.\"\n ]);\n }\n // The server answered and has no database of that name: a typo in\n // --from, usually, or a database not created yet.\n const missing = /database \"([^\"]+)\" does not exist/i.exec(stderr)?.[1];\n if (missing) {\n return banner(`Database \"${missing}\" does not exist at ${host}`, said, [\n \"That is the database --from names, which the pull reads.\",\n NOTHING_CHANGED,\n \"Check the database name in --from.\"\n ]);\n }\n\n const versions = /server version mismatch/i.test(stderr)\n ? [\"pg_dump is older than the server: install client tools at least as new as it (`brew install libpq`).\"]\n : [];\n return [\n ...(stderr ? stderr.split(\"\\n\") : []),\n `✗ pg_dump could not dump ${describeTarget(source)} (exit code ${run.exitCode}).`,\n NOTHING_CHANGED,\n ...versions\n ];\n}\n\n/** The fields of a finished `pg_restore` (or `pg_dump`) run that decide {@link restoreOutcome}. */\nexport interface RestoreRun {\n exitCode?: number;\n signal?: string;\n /** A spawn failure's code — `ENOENT` when the binary vanished. */\n code?: string;\n stderr?: string;\n}\n\n/** What a `pg_restore` run says about the copy. */\nexport interface RestoreRunOutcome {\n /** Whether anything can have arrived — what decides whether an anonymization pass has data to redact. */\n started: boolean;\n /** Null when the copy is complete. */\n failure: string | null;\n /**\n * What to do about the failure. \"Pull again\" only where a second pull is\n * what finishes the job: everything else repeats until its cause is fixed.\n */\n advice: string | null;\n}\n\n/**\n * What a finished `pg_restore` says about the copy.\n *\n * The target was emptied before anything was written, so every way this can\n * end short of exit 0 leaves a partial or empty local database, and none of\n * them may be reported as a copy. A non-zero exit is pg_restore saying it\n * skipped statements — \"errors ignored on restore: N\" — and whatever those\n * statements created (a table, its rows, an RLS policy) is missing locally.\n *\n * Restoring into an emptied database, those errors come from what the source\n * holds that this Postgres cannot create — an extension it does not have, a\n * feature of a newer server — and a second pull meets them again. So the\n * advice names that, rather than \"pull again\", which sent people in a loop.\n * On the managed database an extension cannot be installed at all, so the\n * advice there is Postgres in Docker.\n */\nexport function restoreOutcome(run: RestoreRun, engine: PullTargetEngine = \"postgres\"): RestoreRunOutcome {\n if (run.signal) {\n return {\n started: true,\n failure: `pg_restore was stopped by ${run.signal} before it finished.`,\n advice: \"It was interrupted, not refused: pull again to finish the copy.\"\n };\n }\n if (run.exitCode === undefined) {\n return {\n started: false,\n failure: `pg_restore could not be started (${run.code ?? \"unknown error\"}).`,\n advice: \"Check that the PostgreSQL client tools are installed (`pg_restore --version`), then pull again.\"\n };\n }\n if (run.exitCode === 0) return { started: true, failure: null, advice: null };\n\n const ignored = /errors ignored on restore: (\\d+)/.exec(run.stderr ?? \"\");\n const repeats = engine === \"pglite\"\n ? \"Pulling again repeats these errors until what they name is fixed — most often an extension the \"\n + `source uses that the managed database does not have (it has ${PGLITE_EXTENSIONS.map(extension => extension.name).join(\", \")}). `\n + \"Pull into Postgres in Docker instead (`rebase db pull --docker`), or pull only the schemas that do \"\n + \"not need it (--schema).\"\n : \"Pulling again repeats these errors until what they name is fixed — most often an extension \"\n + \"the source uses that this Postgres does not have installed. Install it here, or pull only the schemas \"\n + \"that do not need it (--schema).\";\n\n return {\n started: true,\n failure: ignored\n ? `pg_restore could not restore ${ignored[1]} object(s) — the errors are above.`\n : `pg_restore exited with code ${run.exitCode} — the errors are above.`,\n advice: repeats\n };\n}\n\n/** How {@link restoreCopy} ended. */\nexport interface RestoreOutcome extends RestoreRunOutcome {\n /** Whether the local database was emptied first. False means it is exactly as it was. */\n cleared: boolean;\n}\n\n/**\n * The local schemas a pull empties before it restores: every one of the local\n * database's own for a whole-database pull, and those it copies for a\n * `--schema` pull. `pg_catalog`, `information_schema` and the `pg_*` internals\n * never — see {@link provisionableSchemas}.\n */\nexport function schemasToClear(present: readonly { schema: string }[], planned: readonly string[]): string[] {\n const local = provisionableSchemas(present);\n\n return planned.length === 0 ? local : local.filter((schema) => planned.includes(schema));\n}\n\n/**\n * The statements that empty those schemas, in one transaction.\n *\n * `CASCADE`, which `pg_restore --clean` never uses: a table the source lacks —\n * `rebase.atlas_schema_revisions` from `rebase db migrate`, a collection added\n * locally that references a pulled table — is exactly what blocked its plain\n * `DROP SCHEMA` and `DROP TABLE`. `public` is put back empty, with the access\n * every role has to it by default, because a dump does not create a default\n * `public` and every table in it would otherwise have nowhere to go.\n */\nexport function clearStatements(schemas: readonly string[]): string[] {\n const statements = schemas.map((schema) => `DROP SCHEMA IF EXISTS ${quoteIdentifier(schema)} CASCADE`);\n if (schemas.includes(\"public\")) statements.push(\"CREATE SCHEMA public\", \"GRANT USAGE ON SCHEMA public TO PUBLIC\");\n\n return statements;\n}\n\n/**\n * How long emptying the local database waits for a lock before giving up.\n *\n * A `rebase dev` serving from the database holds connections, and one inside\n * a transaction would otherwise make the pull hang without a word.\n */\nconst CLEAR_LOCK_TIMEOUT = \"15s\";\n\n/** Why emptying the local database failed, in the reader's terms, and what fixes it. */\nfunction clearFailure(error: unknown): RestoreOutcome {\n const code = typeof error === \"object\" && error !== null && \"code\" in error ? String(error.code) : \"\";\n const reason = errorText(error);\n const advice = code === \"55P03\"\n ? \"Something holds a lock on it — usually `rebase dev` serving from it. Stop that, then pull again.\"\n : code === \"42501\"\n ? \"This connection may not drop those schemas. Connect as the role that owns them — the one \"\n + \"that created them — and pull with that.\"\n : \"Fix what the error names; pulling again before that repeats it.\";\n\n return {\n cleared: false,\n started: false,\n failure: `Could not empty the local database before restoring: ${reason}`,\n advice\n };\n}\n\n/**\n * Replace the local database's contents with a dump: empty the schemas it\n * holds, then `pg_restore` into them.\n *\n * Only ever `plan.target`, the local development database the command\n * resolved — the pull's one destination, which no flag can change. Emptying it\n * is one transaction: if any schema cannot be dropped, none is, nothing is\n * restored, and the database is as it was.\n *\n * On the managed database the dump must be one {@link dumpSource} took for it,\n * with its rows as INSERTs: a COPY would leave the daemon answering nobody, so\n * one is refused before anything is emptied. And afterwards its one session —\n * every connection's — is given back its own settings: pg_restore leaves\n * `search_path` empty and `row_security` off behind it, which a running\n * `rebase dev` would otherwise query under.\n */\nexport async function restoreCopy(plan: PullPlan, dumpFile: string): Promise<RestoreOutcome> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: plan.target });\n client.on(\"error\", () => undefined);\n let engine: PullTargetEngine;\n try {\n await client.connect();\n engine = engineOf((await client.query<{ version: string }>(\"SELECT version() AS version\")).rows[0]?.version ?? \"\");\n if (engine === \"pglite\" && plan.targetEngine !== \"pglite\") {\n return {\n cleared: false,\n started: false,\n failure: \"The managed development database cannot take this dump: its rows are COPY data, which PGlite's socket server cannot receive.\",\n advice: \"Pull into Postgres in Docker instead: `rebase db pull --docker`.\"\n };\n }\n const { rows } = await client.query<{ schema: string }>(\"SELECT nspname AS \\\"schema\\\" FROM pg_namespace\");\n await client.query(\"BEGIN\");\n await client.query(`SET LOCAL lock_timeout = '${CLEAR_LOCK_TIMEOUT}'`);\n for (const statement of clearStatements(schemasToClear(rows, plan.schemas))) await client.query(statement);\n await client.query(\"COMMIT\");\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n return clearFailure(error);\n } finally {\n await client.end().catch(() => undefined);\n }\n\n // stderr is shown as it arrives and kept, for the count in its summary\n // line. Not rejected: every way this ends is judged by `restoreOutcome`.\n const run = await execa(\"pg_restore\", restoreArgs(plan, dumpFile), {\n env: restoreEnv(plan),\n stdin: \"ignore\",\n stdout: \"inherit\",\n stderr: [\"pipe\", \"inherit\"],\n reject: false\n });\n\n if (engine === \"pglite\") await resetSharedSession(plan.target);\n\n return { cleared: true, ...restoreOutcome(run, engine) };\n}\n\n/**\n * Put the managed database's one session back to its own settings.\n *\n * `RESET ALL` and `DEALLOCATE ALL`, not `DISCARD ALL`: the session is every\n * connection's, and `DISCARD` would also drop the `LISTEN` a running\n * `rebase dev`'s realtime holds on it. The same pair the backup service runs\n * after its tools (`resetSharedSession` in server-postgres' backup-service). Best effort: a daemon that cannot take this cannot take the\n * re-grant that follows either, and that step reports it.\n */\nasync function resetSharedSession(target: string): Promise<void> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: target, connectionTimeoutMillis: 10_000, query_timeout: 15_000 });\n client.on(\"error\", () => undefined);\n try {\n await client.connect();\n await client.query(\"RESET ALL\");\n await client.query(\"DEALLOCATE ALL\");\n } catch {\n // Reported by the next step that connects; see above.\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n/**\n * The lines that report a failed restore: what failed, what state the local\n * database is in now, and what to do. Plain text, for the caller to colour.\n */\nexport function describeRestoreFailure(outcome: RestoreOutcome, source: string): string[] {\n const state = outcome.started\n ? `The local database holds an incomplete copy of ${source}.`\n : outcome.cleared\n ? \"The local database was emptied, and nothing was restored into it.\"\n : \"Nothing was restored, and the local database is as it was.\";\n\n return [outcome.failure ?? \"The restore did not complete.\", state, ...(outcome.advice ? [outcome.advice] : [])];\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { f as requireBackendDir, h as resolvePluginCliScript, l as getActiveBack
|
|
|
3
3
|
import { a as unknownCommand, r as wantsHelp } from "./args-BX0YD14a.js";
|
|
4
4
|
import { r as refuseMissingCollectionsPath, t as COLLECTIONS_FLAG_HELP } from "./path-flags-BFgG9V6y.js";
|
|
5
5
|
import { DEV_DATABASE_KIND_ENV, devDatabaseKind, managedNotices, prepareDatabaseEnv } from "./prepare-DdnU70z8.js";
|
|
6
|
-
import { absolutizeLocalPathArgs, ensureDockerDatabase, n as argsFromCommand, spawnDriverCli, t as reportSpawnFailure } from "./db-
|
|
6
|
+
import { absolutizeLocalPathArgs, ensureDockerDatabase, n as argsFromCommand, spawnDriverCli, t as reportSpawnFailure } from "./db-CRNpA5mH.js";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import arg from "arg";
|
|
9
9
|
//#region src/commands/schema.ts
|
|
@@ -148,4 +148,4 @@ ${chalk.green.bold("stale Options")}
|
|
|
148
148
|
//#endregion
|
|
149
149
|
export { SCHEMA_SUBCOMMANDS, schemaCommand };
|
|
150
150
|
|
|
151
|
-
//# sourceMappingURL=schema-
|
|
151
|
+
//# sourceMappingURL=schema-B0-p6T1O.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-CK7JN1Dc.js","names":[],"sources":["../src/commands/schema.ts"],"sourcesContent":["/**\n * CLI command: rebase schema <action>\n */\nimport arg from \"arg\";\nimport chalk from \"chalk\";\nimport { ensureDockerDatabase, spawnDriverCli } from \"./db\";\nimport {\n requireProjectRoot,\n requireBackendDir,\n getActiveBackendPlugin,\n resolvePluginCliScript,\n resolveTsx,\n findEnvFile,\n exitDependenciesNotInstalled\n} from \"../utils/project\";\nimport { recordEvent } from \"../telemetry\";\nimport { wantsHelp } from \"../utils/args\";\nimport { reportSpawnFailure } from \"../utils/spawn-error\";\nimport { argsFromCommand } from \"../utils/command-words\";\nimport { unknownCommand } from \"../utils/unknown-command\";\nimport { DEV_DATABASE_KIND_ENV, devDatabaseKind, managedNotices, prepareDatabaseEnv } from \"../dev-db/prepare\";\nimport { absolutizeLocalPathArgs } from \"./db\";\nimport { COLLECTIONS_FLAG_HELP, refuseMissingCollectionsPath } from \"../utils/path-flags\";\n\n/**\n * The line handed to the driver, its paths resolved from where the command was\n * run (the driver runs in `backend/`). `--schema` is left alone: on\n * `introspect` it names a Postgres schema, not a file.\n */\nexport function schemaDriverArgs(rawArgs: string[], cwd: string): string[] {\n return absolutizeLocalPathArgs(argsFromCommand(rawArgs, \"schema\"), cwd, [\"--collections\", \"-c\", \"--output\", \"-o\", \"--out\"]);\n}\n\nexport async function schemaCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void> {\n // `--help` is answered here, before `requireProjectRoot` and before the\n // driver is spawned — the same shape `db.ts` already uses, and for the same\n // reason.\n //\n // `cli.ts` rewrites the subcommand to `\"--help\"` only when the user named\n // none, so `rebase schema --help` was covered and `rebase schema generate\n // --help` was not: the flag travelled into `rawArgs.slice(2)` and reached\n // the driver, whose own `schemaCommand` has no `--help` case. It *ran the\n // generator* — overwriting `src/schema.generated.ts` — for a flag whose\n // entire job is to print text. Worse, the first thing it hit outside a\n // project was `requireProjectRoot`, so `rebase schema introspect --help` in\n // an empty directory exited 1 with \"Could not find a Rebase project root\":\n // help you cannot read until you already have a project is help for nobody.\n if (!subcommand || subcommand === \"--help\" || wantsHelp(rawArgs)) {\n printSchemaHelp(subcommand === \"--help\" ? undefined : subcommand);\n return;\n }\n\n // A typo is answered here, in the same words every other family uses.\n //\n // It used to travel to the driver, which said the bare line `Unknown schema\n // command.` — no list, no pointer, no did-you-mean, for a family with three\n // subcommands. The set comes from the help table, so what a typo is measured\n // against and what `--help` prints cannot drift apart.\n if (!SCHEMA_ACTION_HELP[subcommand]) {\n unknownCommand(subcommand, Object.keys(SCHEMA_ACTION_HELP), \"schema\");\n }\n\n const projectRoot = requireProjectRoot();\n\n // Fire-and-forget, and a no-op unless the developer opted in. Never awaited:\n // the command is what the user is waiting for, and a slow collector must not\n // sit in front of it.\n void recordEvent(\"cli.schema\", { subcommand: subcommand ?? \"none\" }, { projectRoot });\n const backendDir = requireBackendDir(projectRoot);\n\n const activePlugin = getActiveBackendPlugin(backendDir);\n if (!activePlugin) {\n console.error(chalk.red(\"✗ Could not detect an active database plugin.\"));\n console.error(chalk.gray(\" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json.\"));\n process.exit(1);\n }\n\n const pluginCli = resolvePluginCliScript(backendDir, activePlugin);\n if (!pluginCli) {\n exitDependenciesNotInstalled(projectRoot);\n }\n\n // Set up environment with DOTENV_CONFIG_PATH\n const envFile = findEnvFile(projectRoot);\n const env: Record<string, string> = { ...process.env as Record<string, string> };\n if (envFile) {\n env.DOTENV_CONFIG_PATH = envFile;\n }\n // Which database this project is on, so the driver's closing line does not\n // recommend a command that database refuses. A pure decision — nothing is\n // started, which matters here above all: `schema generate` reads collection\n // files and writes one file, and booting a database to run it would be a\n // side effect nobody asked for.\n env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? \"\";\n\n refuseMissingCollectionsPath(rawArgs, process.cwd(), backendDir);\n\n // `introspect` is the one schema command that reads a database, and it\n // reads the project's — resolved the way every `db` command resolves it,\n // started if that is what it takes. It used to pass `.env` and nothing\n // else, so on the managed database or after `rebase dev --docker` (with\n // the scaffold's DATABASE_URL commented out, on purpose) it said\n // \"DATABASE_URL is not set\", and `--docker` and `--database-url` passed the\n // flag check only to be ignored.\n if (subcommand === \"introspect\") {\n const flags = arg(\n { \"--database-url\": String, \"--docker\": Boolean },\n { argv: rawArgs.slice(2), permissive: true }\n );\n await ensureDockerDatabase(projectRoot, rawArgs);\n const prepared = await prepareDatabaseEnv(projectRoot, {\n flagUrl: flags[\"--database-url\"] ?? null,\n flagDocker: flags[\"--docker\"] === true,\n onProgress: (message) => console.log(chalk.gray(` ${message}`))\n });\n Object.assign(env, prepared.env);\n env[DEV_DATABASE_KIND_ENV] = prepared.database.kind;\n for (const line of managedNotices(prepared)) console.log(chalk.gray(` ${line}`));\n }\n\n try {\n // The one spawn, shared with `db`. This file used to carry its own copy\n // of the tsx-or-node decision; three copies of that rule existed and two\n // were fixed while the third went on spawning plain node.\n await spawnDriverCli(\n { projectRoot, backendDir, pluginCli, env },\n schemaDriverArgs(rawArgs, process.cwd())\n );\n } catch (error) {\n // A child that ran and exited non-zero already printed its diagnostics\n // through inherited stdio; a child that never started (ENOENT on tsx,\n // EACCES on the script) printed nothing at all, and this used to exit 1\n // in silence.\n reportSpawnFailure(error);\n process.exit(1);\n }\n}\n\n/** One page per subcommand, so `rebase schema <action> --help` says something. */\nconst SCHEMA_ACTION_HELP: Record<string, { usage: string; summary: string; notes?: string[] }> = {\n generate: {\n usage: \"rebase schema generate [--collections <dir>] [--output <file>] [--watch]\",\n summary: \"Generate the Drizzle schema from the collection definitions.\",\n notes: [\"--watch regenerates on every change to a collection file.\"]\n },\n introspect: {\n usage: \"rebase schema introspect [--collections <dir>] [--output <dir>] [--schema <name>] [--force] [--data-inference | --no-data-inference]\",\n summary: \"Read an existing database and write Rebase collection definitions from it.\",\n notes: [\n \"--force overwrites collection files that are already there.\",\n \"--data-inference samples up to 100 rows per table to detect formats, constraints and UI settings; \"\n + \"--no-data-inference reads the structure only. Without either it asks, and without a terminal \"\n + \"it reads the structure only.\",\n \"Reads this project's database, the one `rebase db url` prints: --database-url <url> names another; \"\n + \"--docker uses the compose one, starting it if needed.\"\n ]\n },\n stale: {\n usage: \"rebase schema stale [--collections <dir>] [--output <file>] [--fix]\",\n summary: \"Report generated schema files that no longer match the collections.\",\n notes: [\"--fix regenerates them instead of only reporting.\"]\n }\n};\n\n/** Every subcommand the family dispatches — the help table's keys. */\nexport const SCHEMA_SUBCOMMANDS = Object.keys(SCHEMA_ACTION_HELP);\n\nfunction printSchemaHelp(action?: string) {\n const entry = action ? SCHEMA_ACTION_HELP[action] : undefined;\n if (entry) {\n console.log(`\n${chalk.bold(`rebase schema ${action}`)}\n\n ${entry.summary}\n\n${chalk.green.bold(\"Usage\")}\n ${chalk.blue(entry.usage)}\n${entry.notes?.length ? `\\n${chalk.green.bold(\"Notes\")}\\n${entry.notes.map(n => ` ${chalk.gray(`• ${n}`)}`).join(\"\\n\")}\\n` : \"\"}\n${chalk.gray(\"Run `rebase schema --help` for every subcommand.\")}\n`);\n return;\n }\n\n console.log(`\n${chalk.bold(\"rebase schema\")} — Schema management commands\n\n${chalk.green.bold(\"Usage\")}\n rebase schema ${chalk.blue(\"<command>\")} [options]\n\n${chalk.green.bold(\"Commands\")}\n ${chalk.gray(\"(Commands are provided by your active database driver plugin)\")}\n ${chalk.blue.bold(\"generate\")} Generate Schema from collection definitions\n ${chalk.blue.bold(\"introspect\")} Introspect an existing database to generate collection definitions\n ${chalk.blue.bold(\"stale\")} Report generated schema files that no longer match the collections\n\n${chalk.green.bold(\"generate Options\")}\n ${chalk.blue(\"--collections, -c\")} ${COLLECTIONS_FLAG_HELP}\n ${chalk.blue(\"--output, -o\")} Output path for generated schema\n ${chalk.blue(\"--watch, -w\")} Watch for changes and regenerate automatically\n\n${chalk.green.bold(\"introspect Options\")}\n ${chalk.blue(\"--output, -o\")} Output directory for generated collection files\n ${chalk.blue(\"--schema\")} Postgres schema to read (default: public)\n ${chalk.blue(\"--force, -f\")} Overwrite collection files that already exist\n ${chalk.blue(\"--database-url\")} Read this database instead of the project's own\n ${chalk.blue(\"--docker\")} Read the compose database, starting it if needed\n ${chalk.blue(\"--data-inference\")} Sample rows to detect formats and constraints ${chalk.gray(\"(asked when omitted)\")}\n ${chalk.blue(\"--no-data-inference\")} Read the structure only, without asking\n\n${chalk.green.bold(\"stale Options\")}\n ${chalk.blue(\"--fix\")} Regenerate the stale files instead of only reporting them\n`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-B0-p6T1O.js","names":[],"sources":["../src/commands/schema.ts"],"sourcesContent":["/**\n * CLI command: rebase schema <action>\n */\nimport arg from \"arg\";\nimport chalk from \"chalk\";\nimport { ensureDockerDatabase, spawnDriverCli } from \"./db\";\nimport {\n requireProjectRoot,\n requireBackendDir,\n getActiveBackendPlugin,\n resolvePluginCliScript,\n resolveTsx,\n findEnvFile,\n exitDependenciesNotInstalled\n} from \"../utils/project\";\nimport { recordEvent } from \"../telemetry\";\nimport { wantsHelp } from \"../utils/args\";\nimport { reportSpawnFailure } from \"../utils/spawn-error\";\nimport { argsFromCommand } from \"../utils/command-words\";\nimport { unknownCommand } from \"../utils/unknown-command\";\nimport { DEV_DATABASE_KIND_ENV, devDatabaseKind, managedNotices, prepareDatabaseEnv } from \"../dev-db/prepare\";\nimport { absolutizeLocalPathArgs } from \"./db\";\nimport { COLLECTIONS_FLAG_HELP, refuseMissingCollectionsPath } from \"../utils/path-flags\";\n\n/**\n * The line handed to the driver, its paths resolved from where the command was\n * run (the driver runs in `backend/`). `--schema` is left alone: on\n * `introspect` it names a Postgres schema, not a file.\n */\nexport function schemaDriverArgs(rawArgs: string[], cwd: string): string[] {\n return absolutizeLocalPathArgs(argsFromCommand(rawArgs, \"schema\"), cwd, [\"--collections\", \"-c\", \"--output\", \"-o\", \"--out\"]);\n}\n\nexport async function schemaCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void> {\n // `--help` is answered here, before `requireProjectRoot` and before the\n // driver is spawned — the same shape `db.ts` already uses, and for the same\n // reason.\n //\n // `cli.ts` rewrites the subcommand to `\"--help\"` only when the user named\n // none, so `rebase schema --help` was covered and `rebase schema generate\n // --help` was not: the flag travelled into `rawArgs.slice(2)` and reached\n // the driver, whose own `schemaCommand` has no `--help` case. It *ran the\n // generator* — overwriting `src/schema.generated.ts` — for a flag whose\n // entire job is to print text. Worse, the first thing it hit outside a\n // project was `requireProjectRoot`, so `rebase schema introspect --help` in\n // an empty directory exited 1 with \"Could not find a Rebase project root\":\n // help you cannot read until you already have a project is help for nobody.\n if (!subcommand || subcommand === \"--help\" || wantsHelp(rawArgs)) {\n printSchemaHelp(subcommand === \"--help\" ? undefined : subcommand);\n return;\n }\n\n // A typo is answered here, in the same words every other family uses.\n //\n // It used to travel to the driver, which said the bare line `Unknown schema\n // command.` — no list, no pointer, no did-you-mean, for a family with three\n // subcommands. The set comes from the help table, so what a typo is measured\n // against and what `--help` prints cannot drift apart.\n if (!SCHEMA_ACTION_HELP[subcommand]) {\n unknownCommand(subcommand, Object.keys(SCHEMA_ACTION_HELP), \"schema\");\n }\n\n const projectRoot = requireProjectRoot();\n\n // Fire-and-forget, and a no-op unless the developer opted in. Never awaited:\n // the command is what the user is waiting for, and a slow collector must not\n // sit in front of it.\n void recordEvent(\"cli.schema\", { subcommand: subcommand ?? \"none\" }, { projectRoot });\n const backendDir = requireBackendDir(projectRoot);\n\n const activePlugin = getActiveBackendPlugin(backendDir);\n if (!activePlugin) {\n console.error(chalk.red(\"✗ Could not detect an active database plugin.\"));\n console.error(chalk.gray(\" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json.\"));\n process.exit(1);\n }\n\n const pluginCli = resolvePluginCliScript(backendDir, activePlugin);\n if (!pluginCli) {\n exitDependenciesNotInstalled(projectRoot);\n }\n\n // Set up environment with DOTENV_CONFIG_PATH\n const envFile = findEnvFile(projectRoot);\n const env: Record<string, string> = { ...process.env as Record<string, string> };\n if (envFile) {\n env.DOTENV_CONFIG_PATH = envFile;\n }\n // Which database this project is on, so the driver's closing line does not\n // recommend a command that database refuses. A pure decision — nothing is\n // started, which matters here above all: `schema generate` reads collection\n // files and writes one file, and booting a database to run it would be a\n // side effect nobody asked for.\n env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? \"\";\n\n refuseMissingCollectionsPath(rawArgs, process.cwd(), backendDir);\n\n // `introspect` is the one schema command that reads a database, and it\n // reads the project's — resolved the way every `db` command resolves it,\n // started if that is what it takes. It used to pass `.env` and nothing\n // else, so on the managed database or after `rebase dev --docker` (with\n // the scaffold's DATABASE_URL commented out, on purpose) it said\n // \"DATABASE_URL is not set\", and `--docker` and `--database-url` passed the\n // flag check only to be ignored.\n if (subcommand === \"introspect\") {\n const flags = arg(\n { \"--database-url\": String, \"--docker\": Boolean },\n { argv: rawArgs.slice(2), permissive: true }\n );\n await ensureDockerDatabase(projectRoot, rawArgs);\n const prepared = await prepareDatabaseEnv(projectRoot, {\n flagUrl: flags[\"--database-url\"] ?? null,\n flagDocker: flags[\"--docker\"] === true,\n onProgress: (message) => console.log(chalk.gray(` ${message}`))\n });\n Object.assign(env, prepared.env);\n env[DEV_DATABASE_KIND_ENV] = prepared.database.kind;\n for (const line of managedNotices(prepared)) console.log(chalk.gray(` ${line}`));\n }\n\n try {\n // The one spawn, shared with `db`. This file used to carry its own copy\n // of the tsx-or-node decision; three copies of that rule existed and two\n // were fixed while the third went on spawning plain node.\n await spawnDriverCli(\n { projectRoot, backendDir, pluginCli, env },\n schemaDriverArgs(rawArgs, process.cwd())\n );\n } catch (error) {\n // A child that ran and exited non-zero already printed its diagnostics\n // through inherited stdio; a child that never started (ENOENT on tsx,\n // EACCES on the script) printed nothing at all, and this used to exit 1\n // in silence.\n reportSpawnFailure(error);\n process.exit(1);\n }\n}\n\n/** One page per subcommand, so `rebase schema <action> --help` says something. */\nconst SCHEMA_ACTION_HELP: Record<string, { usage: string; summary: string; notes?: string[] }> = {\n generate: {\n usage: \"rebase schema generate [--collections <dir>] [--output <file>] [--watch]\",\n summary: \"Generate the Drizzle schema from the collection definitions.\",\n notes: [\"--watch regenerates on every change to a collection file.\"]\n },\n introspect: {\n usage: \"rebase schema introspect [--collections <dir>] [--output <dir>] [--schema <name>] [--force] [--data-inference | --no-data-inference]\",\n summary: \"Read an existing database and write Rebase collection definitions from it.\",\n notes: [\n \"--force overwrites collection files that are already there.\",\n \"--data-inference samples up to 100 rows per table to detect formats, constraints and UI settings; \"\n + \"--no-data-inference reads the structure only. Without either it asks, and without a terminal \"\n + \"it reads the structure only.\",\n \"Reads this project's database, the one `rebase db url` prints: --database-url <url> names another; \"\n + \"--docker uses the compose one, starting it if needed.\"\n ]\n },\n stale: {\n usage: \"rebase schema stale [--collections <dir>] [--output <file>] [--fix]\",\n summary: \"Report generated schema files that no longer match the collections.\",\n notes: [\"--fix regenerates them instead of only reporting.\"]\n }\n};\n\n/** Every subcommand the family dispatches — the help table's keys. */\nexport const SCHEMA_SUBCOMMANDS = Object.keys(SCHEMA_ACTION_HELP);\n\nfunction printSchemaHelp(action?: string) {\n const entry = action ? SCHEMA_ACTION_HELP[action] : undefined;\n if (entry) {\n console.log(`\n${chalk.bold(`rebase schema ${action}`)}\n\n ${entry.summary}\n\n${chalk.green.bold(\"Usage\")}\n ${chalk.blue(entry.usage)}\n${entry.notes?.length ? `\\n${chalk.green.bold(\"Notes\")}\\n${entry.notes.map(n => ` ${chalk.gray(`• ${n}`)}`).join(\"\\n\")}\\n` : \"\"}\n${chalk.gray(\"Run `rebase schema --help` for every subcommand.\")}\n`);\n return;\n }\n\n console.log(`\n${chalk.bold(\"rebase schema\")} — Schema management commands\n\n${chalk.green.bold(\"Usage\")}\n rebase schema ${chalk.blue(\"<command>\")} [options]\n\n${chalk.green.bold(\"Commands\")}\n ${chalk.gray(\"(Commands are provided by your active database driver plugin)\")}\n ${chalk.blue.bold(\"generate\")} Generate Schema from collection definitions\n ${chalk.blue.bold(\"introspect\")} Introspect an existing database to generate collection definitions\n ${chalk.blue.bold(\"stale\")} Report generated schema files that no longer match the collections\n\n${chalk.green.bold(\"generate Options\")}\n ${chalk.blue(\"--collections, -c\")} ${COLLECTIONS_FLAG_HELP}\n ${chalk.blue(\"--output, -o\")} Output path for generated schema\n ${chalk.blue(\"--watch, -w\")} Watch for changes and regenerate automatically\n\n${chalk.green.bold(\"introspect Options\")}\n ${chalk.blue(\"--output, -o\")} Output directory for generated collection files\n ${chalk.blue(\"--schema\")} Postgres schema to read (default: public)\n ${chalk.blue(\"--force, -f\")} Overwrite collection files that already exist\n ${chalk.blue(\"--database-url\")} Read this database instead of the project's own\n ${chalk.blue(\"--docker\")} Read the compose database, starting it if needed\n ${chalk.blue(\"--data-inference\")} Sample rows to detect formats and constraints ${chalk.gray(\"(asked when omitted)\")}\n ${chalk.blue(\"--no-data-inference\")} Read the structure only, without asking\n\n${chalk.green.bold(\"stale Options\")}\n ${chalk.blue(\"--fix\")} Regenerate the stale files instead of only reporting them\n`);\n}\n"],"mappings":"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"}
|
|
@@ -5,7 +5,7 @@ import { c as withImplicitDefaults, n as deriveOptionsFor, o as readResourceGrap
|
|
|
5
5
|
import { a as parseLoopbackDsn, o as probeTcp } from "./dev-preflight-unHpiYO1.js";
|
|
6
6
|
import { i as managedUrl, r as findRunningDaemon } from "./daemon-CknkDjd6.js";
|
|
7
7
|
import { resolveProjectDatabaseAndBranch } from "./prepare-DdnU70z8.js";
|
|
8
|
-
import { describeTarget } from "./pull-
|
|
8
|
+
import { describeTarget } from "./pull-C3PXduzl.js";
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import fs from "fs";
|
|
11
11
|
import path from "path";
|
|
@@ -428,4 +428,4 @@ async function statusCommand(rawArgs) {
|
|
|
428
428
|
//#endregion
|
|
429
429
|
export { statusCommand };
|
|
430
430
|
|
|
431
|
-
//# sourceMappingURL=status-
|
|
431
|
+
//# sourceMappingURL=status-BJ26yZOo.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-CQrliKxD.js","names":[],"sources":["../src/resources/status.ts","../src/commands/status.ts"],"sourcesContent":["/**\n * What a project declared, what the environment binds it to, and whether the\n * two meet.\n *\n * ## Why this exists\n *\n * Every piece of this was already knowable and none of it was visible in one\n * place. `rebase resources` lists what you declared. The environment holds the\n * variables. The resolvers decide whether a source is usable. So the question a\n * developer actually has — *\"why does uploading to `media` answer 501\"* — could\n * only be answered by knowing the suffix rule, deriving the variable name by\n * hand, and checking it against a `.env` they had to find first.\n *\n * The answer is mechanical, so it should be printed rather than taught.\n *\n * ## Why it calls the resolvers instead of re-deciding\n *\n * The verdict comes from `resolveDataSources` and `resolveStorageBackend` — the\n * same functions that run at boot. A status view that decided for itself what\n * \"configured\" means would be a second implementation of the rule, and the\n * first time it disagreed it would be reassuring a developer about a deployment\n * that is about to refuse to start.\n *\n * The variable *names* come from each kind's `envBases`, and the verdicts from\n * the resolver `@rebasepro/server` registers per kind — one object per kind,\n * held to `envBases` by `resource-env-bases.test.ts`. A kind this view has\n * never heard of is judged by whatever resolver its package registered.\n */\nimport { withImplicitDefaults } from \"./derive\";\nimport {\n DEFAULT_RESOURCE_KEY,\n envBasesForResource,\n resourceEnvSuffix,\n resourceToDataSource,\n type ResourceDeclaration,\n type ResourceGraph\n} from \"@rebasepro/types\";\n\n/** One environment variable a resource reads, and whether it is there. */\nexport interface ResourceBinding {\n /** The variable this resource reads, suffix included. */\n name: string;\n set: boolean;\n /**\n * The account-scoped name consulted when `name` is unset.\n *\n * Only provider-level bindings have one, and only when the resource named\n * an `account`. Shown because a developer looking at an unset\n * `S3_ACCESS_KEY_ID__MEDIA` needs to know it is not the whole story.\n */\n fallback?: { name: string; set: boolean };\n}\n\n/** Whether a declared resource can actually be reached. */\nexport type ResourceState =\n /** Bound and usable. */\n | \"ready\"\n /** Nothing set for it. Legal for a bucket, fatal for a database. */\n | \"unconfigured\"\n /** Set, and set wrongly. The deployment refuses to start. */\n | \"broken\";\n\nexport interface ResourceStatus {\n kind: string;\n key: string;\n engine: string;\n transport: string;\n /** Present when this resource shares another's credentials. */\n account?: string;\n /** True for a resource nobody declared, which exists anyway. */\n implicit: boolean;\n bindings: ResourceBinding[];\n state: ResourceState;\n /** What this means, in the terms the developer will meet it in. */\n detail: string;\n /** Set when a local directory stands in for this engine, in development. */\n standsIn?: string;\n /** What in the project reaches this resource, from the graph. */\n usedBy?: readonly string[];\n}\n\nexport type EnvBag = Record<string, string | undefined>;\n\n/** Set, and not set to the empty string — which the resolvers treat as absent. */\nfunction isSet(env: EnvBag, name: string): boolean {\n const value = env[name];\n return typeof value === \"string\" && value !== \"\";\n}\n\n/**\n * The bindings one declaration reads, in the order a reader should scan them.\n *\n * A `direct`-transport resource gets none: the browser reaches it with a\n * provider SDK and the backend binds nothing for it, so listing variables would\n * invite someone to set variables that are never read.\n */\nexport function bindingsFor(\n declaration: ResourceDeclaration,\n env: EnvBag,\n accountScopedBases: readonly string[]\n): ResourceBinding[] {\n if (declaration.transport !== \"server\") return [];\n\n const suffix = resourceEnvSuffix(declaration.key);\n const account = typeof declaration.options.account === \"string\"\n ? resourceEnvSuffix(declaration.options.account)\n : undefined;\n\n return envBasesForResource(declaration).map((base) => {\n const name = `${base}${suffix}`;\n const binding: ResourceBinding = { name, set: isSet(env, name) };\n\n // Only the provider-level bindings fall back, and only for a resource\n // that named an account. The bucket name never does — it is what\n // distinguishes one source from another.\n if (account !== undefined && !binding.set && accountScopedBases.includes(base)) {\n binding.fallback = { name: `${base}${account}`, set: isSet(env, `${base}${account}`) };\n }\n return binding;\n });\n}\n\n/**\n * What the managed development database covers, when a project is on it.\n *\n * `rebase init` leaves `DATABASE_URL` commented out on purpose and the managed\n * PGlite fills the vacuum — the documented first-run state. Judging that state\n * by the environment alone produced `○ (default) postgres · DATABASE_URL not\n * set` and the remedy \"set DATABASE_URL\", at the same moment `rebase db url`\n * was printing a working connection string for the same project and\n * `rebase dev` was serving from it. The status view was reading the one place\n * the answer deliberately is not.\n *\n * Resolved by the caller, because deciding it needs the project's `.env`, its\n * branch pointer and its compose file, and this module stays free of all three.\n */\nexport interface ManagedDatabaseStatus {\n /**\n * The daemon's connection string, or null when it is not running.\n *\n * Null is not an error: the managed database exists per project and starts\n * on demand. It only means there is nothing to run the whole-set check\n * against, and the row says so rather than implying a misconfiguration.\n */\n url: string | null;\n}\n\n/**\n * The compose `db` service, when a project is on Postgres in Docker.\n *\n * `rebase dev --docker` is remembered, and every command follows it with the\n * scaffold's `DATABASE_URL` still commented out — so, judged by `.env` alone,\n * this view said the default database had no connection string while `rebase\n * dev --docker` served the project from it. Unlike the managed database it\n * stands in for the default only: `dev --docker` hands the backend\n * `DATABASE_URL` and nothing else, so a second declared database still needs\n * its own variable.\n */\nexport interface DockerDatabaseStatus {\n /** The compose service's connection string, which the runtime is given as `DATABASE_URL`. */\n url: string;\n /** What the default database's row says it is. */\n detail: string;\n}\n\n/**\n * The resolver-side of status, as `@rebasepro/server` registers it per kind.\n *\n * Passed in rather than imported so this module stays testable without a\n * server, and so a kind registered by a plugin is judged by the resolver the\n * plugin brought with it rather than by a switch here.\n */\nexport interface StatusResolvers {\n /** `resourceResolver(kind)` from `@rebasepro/server`. */\n resolverFor: (kind: string) => {\n accountScoped?: readonly string[];\n resolve: (\n declaration: ResourceDeclaration,\n env: EnvBag,\n context: { production: boolean; defaultBasePath: string }\n ) => { state: \"ready\" | \"unbound\" | \"blocked\" | \"code\"; detail?: string; standsIn?: string };\n } | undefined;\n /** `resolveDataSources` from `@rebasepro/server`, for the whole-set check. */\n resolveDataSources: (env: EnvBag, definitions: unknown) => unknown;\n /** Whether the process being judged is production. Status judges for development by default. */\n production?: boolean;\n /**\n * Set when this project runs on the managed development database.\n *\n * Absent for a project that named its own connection string, and absent in\n * production — where an unset `DATABASE_URL` is the failure it has always\n * been.\n */\n managedDatabase?: ManagedDatabaseStatus;\n /** Set when this project runs on Postgres in Docker. Absent in production, like the managed one. */\n dockerDatabase?: DockerDatabaseStatus;\n}\n\n/**\n * The environment plus the managed database's URL, for the whole-set check.\n *\n * A copy: the bindings a reader sees are computed against the real `.env`, so\n * nothing here can make the view claim a variable is set that is not. This is\n * only what `resolveDataSources` is handed, and it is handed it so the check\n * judges the environment the runtime would actually get rather than throwing\n * \"Data source \"(default)\" has no connection string\" about a database that is\n * running and reachable.\n */\nfunction withManagedUrls(env: EnvBag, graph: ResourceGraph, url: string): EnvBag {\n const filled: EnvBag = { ...env };\n const keys = [\n DEFAULT_RESOURCE_KEY,\n ...graph.resources.filter(r => r.kind === \"database\").map(r => r.key)\n ];\n for (const key of keys) {\n const name = `DATABASE_URL${resourceEnvSuffix(key)}`;\n if (!isSet(filled, name)) filled[name] = url;\n }\n\n return filled;\n}\n\n/**\n * The environment with the default database's URL filled in, when `.env` leaves it out.\n *\n * What a database's resolver is judged against. The resolver checks a source\n * alongside the default one, so a second database bound by its own\n * `DATABASE_URL__ANALYTICS` was marked ✗ with `Data source \"(default)\" has no\n * connection string` — on a project whose default the managed database or the\n * compose service provides, and whose boot worked. Judged against the\n * environment the runtime actually gets, it is judged on its own string.\n */\nfunction withDefaultUrl(env: EnvBag, url: string | null | undefined): EnvBag {\n if (!url || isSet(env, \"DATABASE_URL\")) return env;\n\n return { ...env, DATABASE_URL: url };\n}\n\n/**\n * Resolve every declared resource against an environment.\n *\n * Each kind's verdict comes from the resolver `@rebasepro/server` registered\n * for it — the same one boot calls — so this view cannot say \"ready\" about a\n * binding boot would refuse. A kind with no resolver is reported as such\n * rather than skipped: a resource nobody shows is one nobody remembers to\n * configure, and a kind this runtime cannot bind is exactly what boot will\n * refuse by name.\n *\n * `resolveDataSources` is then run once over the whole set, so anything it\n * would refuse across sources (a driver package it cannot resolve for a custom\n * engine) is reported rather than missed by a view that only looked per row.\n */\nexport function computeStatus(\n graph: ResourceGraph,\n env: EnvBag,\n resolvers: StatusResolvers\n): { resources: ResourceStatus[]; blocked?: string } {\n const resources: ResourceStatus[] = [];\n const production = resolvers.production ?? false;\n const managed = resolvers.managedDatabase;\n const docker = resolvers.dockerDatabase;\n /** The default database's URL as the runtime gets it, when `.env` does not name one. */\n const devDefaultUrl = managed?.url ?? docker?.url ?? null;\n /** Database rows the managed database answered for, so `blocked` can skip them. */\n let managedCovered = 0;\n\n for (const { declaration, implicit } of withImplicitDefaults(graph)) {\n const resolver = resolvers.resolverFor(declaration.kind);\n const bindings = bindingsFor(declaration, env, resolver?.accountScoped ?? []);\n const base: Omit<ResourceStatus, \"state\" | \"detail\"> = {\n kind: declaration.kind,\n key: declaration.key,\n engine: declaration.engine,\n transport: declaration.transport,\n ...(typeof declaration.options.account === \"string\"\n ? { account: declaration.options.account }\n : {}),\n implicit,\n bindings,\n ...(declaration.usedBy && declaration.usedBy.length > 0 ? { usedBy: declaration.usedBy } : {})\n };\n\n if (!resolver) {\n resources.push({\n ...base,\n state: \"broken\",\n detail: `this runtime has no resolver for a \"${declaration.kind}\" — boot refuses it by name. ` +\n \"Upgrade @rebasepro/server, or remove the declaration.\"\n });\n continue;\n }\n\n // Answered before the resolver, because the resolver reads the\n // environment and the whole point of the managed database is that the\n // environment is deliberately empty. Only for a database nobody bound:\n // a developer who set `DATABASE_URL__ANALYTICS` to a warehouse of their\n // own has said which database that is, and the managed one fills a\n // vacuum, never a choice.\n if (managed && declaration.kind === \"database\" && bindings.every(b => !b.set && !b.fallback?.set)) {\n managedCovered += 1;\n resources.push({\n ...base,\n state: \"ready\",\n detail: managed.url\n ? \"the managed development database (PGlite), this project only\"\n : \"the managed development database (PGlite), this project only — \"\n + \"not running; start it with `rebase dev`\"\n });\n continue;\n }\n\n // The default database on Docker: the compose service, unless `.env`\n // names one itself.\n if (docker && declaration.kind === \"database\" && declaration.key === DEFAULT_RESOURCE_KEY\n && bindings.every(b => !b.set && !b.fallback?.set)) {\n resources.push({ ...base, state: \"ready\", detail: docker.detail });\n continue;\n }\n\n const judged = declaration.kind === \"database\" ? withDefaultUrl(env, devDefaultUrl) : env;\n const verdict = resolver.resolve(declaration, judged, { production, defaultBasePath: \"uploads\" });\n switch (verdict.state) {\n case \"ready\":\n resources.push({\n ...base,\n state: \"ready\",\n ...(verdict.standsIn ? { standsIn: verdict.standsIn } : {}),\n detail: verdict.detail ?? \"\"\n });\n break;\n case \"code\":\n resources.push({ ...base, state: \"ready\", detail: verdict.detail ?? \"needs no environment configuration\" });\n break;\n case \"unbound\":\n resources.push({ ...base, state: \"unconfigured\", detail: verdict.detail ?? \"declared, not configured\" });\n break;\n case \"blocked\":\n resources.push({\n ...base,\n // A database the environment names nothing for is the\n // ordinary first-run state, and reads as unconfigured; a\n // binding that is set and set wrongly is broken.\n state: implicit || bindings.every(b => !b.set && !b.fallback?.set) ? \"unconfigured\" : \"broken\",\n detail: verdict.detail ?? \"boot refuses this binding\"\n });\n break;\n }\n }\n\n // Only asked when every row already looks fine. Its failures overlap with\n // the per-resource ones, and a banner repeating a line printed two rows\n // above teaches nothing — what it is here for is the failure no row models,\n // such as a custom engine whose driver package cannot be resolved.\n let blocked: string | undefined;\n // Not run when the managed database answered for a row and is not running:\n // there is no connection string to check the set against, and inventing one\n // to keep the check formally \"run\" is how a green tick gets printed for\n // work nobody did. The row says the daemon is down; that is the finding.\n const managedSetUnknown = managedCovered > 0 && !managed?.url;\n if (resources.every(r => r.state === \"ready\") && !managedSetUnknown) {\n try {\n resolvers.resolveDataSources(\n // The managed URL for the rows it answered for, so the whole-set\n // check judges the same environment the runtime would get.\n managed?.url ? withManagedUrls(env, graph, managed.url) : withDefaultUrl(env, docker?.url),\n graph.resources.filter(r => r.kind === \"database\").map(resourceToDataSource)\n );\n } catch (err) {\n blocked = err instanceof Error ? err.message : String(err);\n }\n }\n\n return { resources, ...(blocked !== undefined ? { blocked } : {}) };\n}\n","/**\n * `rebase status` — what this project is, and whether it can reach its things.\n *\n * The model a developer has to hold is small and was nowhere visible:\n *\n * rebase.json where the code is, and who runs the server\n * config/resources.ts what the project needs\n * the environment how to reach each of them\n *\n * Everything else — `rebase.resources.json`, the bundle manifest — is a\n * generated copy of the middle one, for readers that cannot run the code. A\n * developer never writes them and should not have to think about them.\n *\n * This command prints the three together, resolved. It exists because the\n * question people actually arrive with — \"why does uploading to `media` answer\n * 501\" — was answerable only by knowing the `<BASE>__<KEY>` suffix rule,\n * deriving the variable name by hand, and checking it against a `.env` they had\n * to find first. Every step of that is mechanical, so it is printed instead.\n */\nimport chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { failAsJson, readEnvFile, requireProjectRoot } from \"../utils/project\";\nimport { findBackendApp, loadManifest, ManifestError, resolveBackendPaths } from \"../manifest\";\nimport { deriveOptionsFor, deriveResourceGraph, RESOURCE_GRAPH_FILENAME, readResourceGraphFile, serializeResourceGraph } from \"../resources/derive\";\nimport { computeStatus, type DockerDatabaseStatus, type ManagedDatabaseStatus, type ResourceStatus } from \"../resources/status\";\nimport { findRunningDaemon, managedUrl } from \"../dev-db/daemon\";\nimport { type ResolvedBranch, resolveProjectDatabaseAndBranch } from \"../dev-db/prepare\";\nimport { describeTarget } from \"../dev-db/pull\";\nimport { parseLoopbackDsn, probeTcp } from \"../utils/dev-preflight\";\nimport type { RebaseBackendAppConfig } from \"@rebasepro/types\";\n\nfunction usage(): void {\n console.log(`\n${chalk.bold(\"rebase status\")} — what this project declares, and whether it is configured\n\n${chalk.bold(\"Usage\")}\n rebase status Show every resource and the variables it reads\n rebase status --json The same, as JSON\n\n${chalk.bold(\"Reads\")}\n rebase.json which apps exist, and who owns the server process\n <config>/resources.ts the databases, buckets, topics and queues declared\n backend/crons, backend/functions the crons and functions, by filename\n .env whether each one's variables are set\n`);\n}\n\nconst MARK = {\n ready: chalk.green(\"✓\"),\n unconfigured: chalk.yellow(\"○\"),\n broken: chalk.red(\"✗\")\n} as const;\n\nfunction printResource(status: ResourceStatus): void {\n const name = status.key === \"(default)\" ? chalk.bold(\"(default)\") : chalk.bold(status.key);\n const tags = [chalk.gray(status.engine)];\n if (status.transport !== \"server\") tags.push(chalk.yellow(status.transport));\n if (status.account) tags.push(chalk.gray(`account:${status.account}`));\n if (status.implicit) tags.push(chalk.gray(\"implicit\"));\n // A stand-in is not a binding. It is shown in yellow beside a green tick:\n // uploads work here, and the same declaration answers 501 in production\n // until the variable is set.\n if (status.standsIn) tags.push(chalk.yellow(`local stand-in for ${status.standsIn}`));\n\n console.log(` ${MARK[status.state]} ${name} ${tags.join(\" · \")}`);\n if (status.usedBy && status.usedBy.length > 0) {\n console.log(` ${chalk.gray(\"used by\")} ${chalk.gray(status.usedBy.join(\", \"))}`);\n }\n\n let hiddenOptional = 0;\n for (const binding of status.bindings) {\n if (binding.set) {\n console.log(` ${chalk.green(\"✓\")} ${chalk.gray(binding.name)}`);\n } else if (binding.fallback?.set) {\n // Not a gap: the account form is the variable this bucket is\n // actually reading, and showing only the unset per-key name would\n // send someone to set the one they were deliberately avoiding.\n console.log(` ${chalk.green(\"✓\")} ${chalk.gray(binding.fallback.name)} ${chalk.gray(`(shared, for ${binding.name})`)}`);\n } else if (status.state === \"ready\") {\n // It resolved without this one, so this one is optional — which is\n // the resolver's answer, not a second table of required names kept\n // here. Listing seven unset optionals under a working database is\n // how a status view teaches people to skim past the line that\n // matters.\n hiddenOptional += 1;\n } else {\n const also = binding.fallback ? chalk.gray(` or ${binding.fallback.name}`) : \"\";\n console.log(` ${chalk.gray(\"·\")} ${chalk.gray(binding.name)}${also} ${chalk.gray(\"not set\")}`);\n }\n }\n if (hiddenOptional > 0) {\n console.log(chalk.gray(` ${hiddenOptional} optional variable${hiddenOptional === 1 ? \"\" : \"s\"} not set`));\n }\n\n if (status.detail) {\n const colour = status.state === \"broken\" ? chalk.red : status.state === \"unconfigured\" ? chalk.yellow : chalk.gray;\n console.log(` ${colour(`└ ${status.detail}`)}`);\n }\n}\n\n/** Which database `rebase status` reports the project on, and what it hands the status view about it. */\nexport interface StatusDatabase {\n managedDatabase?: ManagedDatabaseStatus;\n dockerDatabase?: DockerDatabaseStatus;\n /** The branch this checkout is on, when the database is one. */\n branch: ResolvedBranch | null;\n /** A switched-to branch the managed database cannot serve. */\n ignoredBranch: string | null;\n}\n\n/**\n * Which database this project is on, decided by the same ordered rule\n * `rebase dev` and `rebase db url` use — and read from the project's `.env`,\n * not from this shell: `rebase status` asks what *this project* is on.\n *\n * Nothing is started. `status` is a question, and a question that boots a\n * 180 MB Postgres to answer itself is a different command. So a managed\n * database that is not running is reported as exactly that, rather than as a\n * project with no database; and a compose database likewise, after a probe of\n * its port.\n *\n * `envFile` is the parsed `.env`; production stands nothing in, because there\n * an unset `DATABASE_URL` is the failure it has always been.\n *\n * Exported for its tests.\n */\nexport async function statusDatabase(projectRoot: string, envFile: Record<string, string>): Promise<StatusDatabase> {\n const { database, branch, ignoredBranch } = resolveProjectDatabaseAndBranch(projectRoot, { env: {} });\n if (envFile.NODE_ENV === \"production\") return { branch, ignoredBranch };\n\n if (database.kind === \"managed\") {\n const daemon = await findRunningDaemon(projectRoot);\n return { managedDatabase: { url: daemon ? managedUrl(daemon.port) : null }, branch, ignoredBranch };\n }\n\n if (database.kind === \"docker\" && database.url) {\n const target = parseLoopbackDsn(database.url);\n const listening = target ? await probeTcp(target.host, target.port) : true;\n const which = branch ? `branch \"${branch.name}\" (${branch.database}) of Postgres in Docker` : \"Postgres in Docker\";\n return {\n dockerDatabase: {\n url: database.url,\n detail: which + \", as the last `rebase dev --docker` did · \" + describeTarget(database.url)\n + (listening ? \"\" : \" — not running; `rebase dev --docker` starts it\")\n },\n branch,\n ignoredBranch\n };\n }\n\n return { branch, ignoredBranch };\n}\n\nexport async function statusCommand(rawArgs: string[]): Promise<void> {\n if (wantsHelp(rawArgs)) return usage();\n\n const { flags } = parseCommandArgs({\n spec: { \"--json\": Boolean },\n rawArgs,\n commandWords: 1,\n command: \"status\",\n maxPositionals: 0\n });\n\n const projectRoot = requireProjectRoot();\n\n // Every exit below has a `--json` arm, because `--json` means stdout holds\n // one JSON object on *every* exit of the command. It used to mean that for\n // the one failure `requireProjectRoot` owns and nothing else, so a caller\n // parsing stdout got an envelope outside a project and an empty stream\n // inside a broken one — the case it actually has to handle.\n const asJson = Boolean(flags[\"--json\"]);\n\n let backend: { name: string; app: RebaseBackendAppConfig };\n try {\n const loaded = loadManifest(projectRoot);\n const found = findBackendApp(loaded.manifest);\n if (!found) {\n if (asJson) {\n failAsJson(\n \"This project declares no backend app, so it declares no resources.\",\n \"no_backend_app\",\n \"Add a backend app to rebase.json, or run `rebase apps init`.\"\n );\n }\n console.error(chalk.red(\"This project declares no backend app, so it declares no resources.\"));\n process.exitCode = 1;\n return;\n }\n backend = found;\n } catch (err) {\n if (err instanceof ManifestError) {\n if (asJson) {\n failAsJson(err.message, \"manifest_invalid\", \"Fix rebase.json, then run this again.\",\n err.issues.map(issue => ({ path: issue.path, message: issue.message })));\n }\n console.error(chalk.red(err.message));\n for (const issue of err.issues) console.error(` ${chalk.gray(issue.path)} ${issue.message}`);\n process.exitCode = 1;\n return;\n }\n throw err;\n }\n\n const { name: backendName, app: backendApp } = backend;\n const paths = resolveBackendPaths(backendApp, projectRoot);\n\n const { graph, issues } = await deriveResourceGraph(deriveOptionsFor(projectRoot, backendApp));\n if (issues.length > 0) {\n if (asJson) {\n failAsJson(\n `${issues.length} problem(s) in the declared resources.`,\n \"resource_declaration_invalid\",\n \"Fix the declarations the issues name, then run this again.\",\n issues.map(issue => ({ path: issue.path, message: issue.message }))\n );\n }\n console.error(chalk.red(`\\n✗ ${issues.length} problem(s) in the declared resources:\\n`));\n for (const issue of issues) console.error(` ${chalk.bold(issue.path)} ${issue.message}`);\n console.error(\"\");\n process.exitCode = 1;\n return;\n }\n\n // The project's own `.env`, not this process's environment: `rebase status`\n // is asking what a deployment of THIS repository would do, and the shell a\n // developer happens to be standing in is not that.\n const env = readEnvFile(projectRoot);\n\n const production = env.NODE_ENV === \"production\";\n const { managedDatabase, dockerDatabase, branch, ignoredBranch } = await statusDatabase(projectRoot, env);\n\n const server = await import(\"@rebasepro/server\");\n const { resources, blocked } = computeStatus(graph, env, {\n resolverFor: server.resourceResolver as never,\n resolveDataSources: server.resolveDataSources as never,\n // Judged as production when the .env says so; a stand-in local\n // directory for an unbound bucket is only ever offered to development.\n production,\n ...(managedDatabase ? { managedDatabase } : {}),\n ...(dockerDatabase ? { dockerDatabase } : {})\n });\n\n if (asJson) {\n console.log(JSON.stringify({\n backend: backendName,\n runtime: backendApp.runtime,\n ...(branch ? { branch: { name: branch.name, database: branch.database } } : {}),\n resources,\n blocked\n }, null, 2));\n return;\n }\n\n const ownership = backendApp.runtime === \"managed\"\n ? \"Rebase's runtime boots your bundle\"\n : \"this project builds its own image and entrypoint\";\n\n console.log(\"\");\n console.log(` ${chalk.bold(backendName)} ${chalk.gray(\"·\")} ${chalk.cyan(backendApp.runtime)} ${chalk.gray(ownership)}`);\n // Named whether or not it exists, and said plainly when it does not. A\n // project with no `resources.ts` is legal — it has one default database and\n // one default bucket — but \"declared in <a file you do not have>\" reads as\n // a broken install rather than as the default it is.\n const resourcesFile = path.join(paths.config, \"resources.ts\");\n const declaresAnything = fs.existsSync(path.join(projectRoot, resourcesFile));\n console.log(` ${chalk.gray(\"declared in\")} ${chalk.gray(resourcesFile)}${\n declaresAnything ? \"\" : chalk.gray(\" (none — the defaults below are implicit)\")}`);\n console.log(` ${chalk.gray(\"configured by\")} ${chalk.gray(\".env\")}`);\n // Every database command in this checkout uses the branch, so the view\n // says so: a switch made yesterday is easy to forget.\n if (branch) {\n console.log(` ${chalk.gray(\"on branch\")} ${chalk.bold(branch.name)} ${chalk.gray(`(${branch.database})`)}`\n + chalk.gray(` — ${chalk.cyan(\"rebase db branch switch --off\")} returns to ${branch.parent ?? \"the main database\"}`));\n } else if (ignoredBranch) {\n console.log(` ${chalk.yellow(\"○\")} ${chalk.gray(`switched to branch \"${ignoredBranch}\", which the managed database cannot serve — it uses the main one`)}`);\n }\n\n const kinds = [...new Set(resources.map(r => r.kind))];\n for (const kind of kinds) {\n console.log(\"\");\n console.log(` ${chalk.bold.underline(`${kind}s`)}`);\n for (const status of resources.filter(r => r.kind === kind)) printResource(status);\n }\n\n if (blocked) {\n console.log(\"\");\n console.log(` ${chalk.red(\"✗\")} ${chalk.red(blocked)}`);\n }\n\n // Only worth a line when it is wrong. A generated file that matches its\n // declarations is not news, and a status view that reports its own\n // bookkeeping as an item trains people to skim past the items that matter.\n //\n // Nor is one that has not been written yet: `rebase build` writes it on\n // every build and says \"commit it\" when it changes, so a project that has\n // never been built — every fresh scaffold — has nothing wrong with it.\n // `rebase resources --check` is the gate that refuses a missing one.\n const committed = readResourceGraphFile(projectRoot);\n if (committed !== null && committed !== serializeResourceGraph(graph)) {\n console.log(\"\");\n console.log(` ${chalk.yellow(\"○\")} ${RESOURCE_GRAPH_FILENAME} is stale — ${chalk.blue(\"rebase resources --write\")}`);\n console.log(chalk.gray(\" a host reads it to decide what to provision before it runs anything\"));\n }\n\n console.log(\"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,SAAS,MAAM,KAAa,MAAuB;CAC/C,MAAM,QAAQ,IAAI;CAClB,OAAO,OAAO,UAAU,YAAY,UAAU;AAClD;;;;;;;;AASA,SAAgB,YACZ,aACA,KACA,oBACiB;CACjB,IAAI,YAAY,cAAc,UAAU,OAAO,CAAC;CAEhD,MAAM,SAAS,kBAAkB,YAAY,GAAG;CAChD,MAAM,UAAU,OAAO,YAAY,QAAQ,YAAY,WACjD,kBAAkB,YAAY,QAAQ,OAAO,IAC7C,KAAA;CAEN,OAAO,oBAAoB,WAAW,CAAC,CAAC,KAAK,SAAS;EAClD,MAAM,OAAO,GAAG,OAAO;EACvB,MAAM,UAA2B;GAAE;GAAM,KAAK,MAAM,KAAK,IAAI;EAAE;EAK/D,IAAI,YAAY,KAAA,KAAa,CAAC,QAAQ,OAAO,mBAAmB,SAAS,IAAI,GACzE,QAAQ,WAAW;GAAE,MAAM,GAAG,OAAO;GAAW,KAAK,MAAM,KAAK,GAAG,OAAO,SAAS;EAAE;EAEzF,OAAO;CACX,CAAC;AACL;;;;;;;;;;;AAwFA,SAAS,gBAAgB,KAAa,OAAsB,KAAqB;CAC7E,MAAM,SAAiB,EAAE,GAAG,IAAI;CAChC,MAAM,OAAO,CACT,sBACA,GAAG,MAAM,UAAU,QAAO,MAAK,EAAE,SAAS,UAAU,CAAC,CAAC,KAAI,MAAK,EAAE,GAAG,CACxE;CACA,KAAK,MAAM,OAAO,MAAM;EACpB,MAAM,OAAO,eAAe,kBAAkB,GAAG;EACjD,IAAI,CAAC,MAAM,QAAQ,IAAI,GAAG,OAAO,QAAQ;CAC7C;CAEA,OAAO;AACX;;;;;;;;;;;AAYA,SAAS,eAAe,KAAa,KAAwC;CACzE,IAAI,CAAC,OAAO,MAAM,KAAK,cAAc,GAAG,OAAO;CAE/C,OAAO;EAAE,GAAG;EAAK,cAAc;CAAI;AACvC;;;;;;;;;;;;;;;AAgBA,SAAgB,cACZ,OACA,KACA,WACiD;CACjD,MAAM,YAA8B,CAAC;CACrC,MAAM,aAAa,UAAU,cAAc;CAC3C,MAAM,UAAU,UAAU;CAC1B,MAAM,SAAS,UAAU;;CAEzB,MAAM,gBAAgB,SAAS,OAAO,QAAQ,OAAO;;CAErD,IAAI,iBAAiB;CAErB,KAAK,MAAM,EAAE,aAAa,cAAc,qBAAqB,KAAK,GAAG;EACjE,MAAM,WAAW,UAAU,YAAY,YAAY,IAAI;EACvD,MAAM,WAAW,YAAY,aAAa,KAAK,UAAU,iBAAiB,CAAC,CAAC;EAC5E,MAAM,OAAiD;GACnD,MAAM,YAAY;GAClB,KAAK,YAAY;GACjB,QAAQ,YAAY;GACpB,WAAW,YAAY;GACvB,GAAI,OAAO,YAAY,QAAQ,YAAY,WACrC,EAAE,SAAS,YAAY,QAAQ,QAAQ,IACvC,CAAC;GACP;GACA;GACA,GAAI,YAAY,UAAU,YAAY,OAAO,SAAS,IAAI,EAAE,QAAQ,YAAY,OAAO,IAAI,CAAC;EAChG;EAEA,IAAI,CAAC,UAAU;GACX,UAAU,KAAK;IACX,GAAG;IACH,OAAO;IACP,QAAQ,uCAAuC,YAAY,KAAK;GAEpE,CAAC;GACD;EACJ;EAQA,IAAI,WAAW,YAAY,SAAS,cAAc,SAAS,OAAM,MAAK,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,GAAG;GAC/F,kBAAkB;GAClB,UAAU,KAAK;IACX,GAAG;IACH,OAAO;IACP,QAAQ,QAAQ,MACV,iEACA;GAEV,CAAC;GACD;EACJ;EAIA,IAAI,UAAU,YAAY,SAAS,cAAc,YAAY,QAAQ,wBAC9D,SAAS,OAAM,MAAK,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,GAAG;GACpD,UAAU,KAAK;IAAE,GAAG;IAAM,OAAO;IAAS,QAAQ,OAAO;GAAO,CAAC;GACjE;EACJ;EAEA,MAAM,SAAS,YAAY,SAAS,aAAa,eAAe,KAAK,aAAa,IAAI;EACtF,MAAM,UAAU,SAAS,QAAQ,aAAa,QAAQ;GAAE;GAAY,iBAAiB;EAAU,CAAC;EAChG,QAAQ,QAAQ,OAAhB;GACI,KAAK;IACD,UAAU,KAAK;KACX,GAAG;KACH,OAAO;KACP,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;KACzD,QAAQ,QAAQ,UAAU;IAC9B,CAAC;IACD;GACJ,KAAK;IACD,UAAU,KAAK;KAAE,GAAG;KAAM,OAAO;KAAS,QAAQ,QAAQ,UAAU;IAAqC,CAAC;IAC1G;GACJ,KAAK;IACD,UAAU,KAAK;KAAE,GAAG;KAAM,OAAO;KAAgB,QAAQ,QAAQ,UAAU;IAA2B,CAAC;IACvG;GACJ,KAAK;IACD,UAAU,KAAK;KACX,GAAG;KAIH,OAAO,YAAY,SAAS,OAAM,MAAK,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,iBAAiB;KACtF,QAAQ,QAAQ,UAAU;IAC9B,CAAC;IACD;EACR;CACJ;CAMA,IAAI;CAKJ,MAAM,oBAAoB,iBAAiB,KAAK,CAAC,SAAS;CAC1D,IAAI,UAAU,OAAM,MAAK,EAAE,UAAU,OAAO,KAAK,CAAC,mBAC9C,IAAI;EACA,UAAU,mBAGN,SAAS,MAAM,gBAAgB,KAAK,OAAO,QAAQ,GAAG,IAAI,eAAe,KAAK,QAAQ,GAAG,GACzF,MAAM,UAAU,QAAO,MAAK,EAAE,SAAS,UAAU,CAAC,CAAC,IAAI,oBAAoB,CAC/E;CACJ,SAAS,KAAK;EACV,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC7D;CAGJ,OAAO;EAAE;EAAW,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;CAAG;AACtE;;;;;;;;;;;;;;;;;;;;;;ACpVA,SAAS,QAAc;CACnB,QAAQ,IAAI;EACd,MAAM,KAAK,eAAe,EAAE;;EAE5B,MAAM,KAAK,OAAO,EAAE;;;;EAIpB,MAAM,KAAK,OAAO,EAAE;;;;;CAKrB;AACD;AAEA,IAAM,OAAO;CACT,OAAO,MAAM,MAAM,GAAG;CACtB,cAAc,MAAM,OAAO,GAAG;CAC9B,QAAQ,MAAM,IAAI,GAAG;AACzB;AAEA,SAAS,cAAc,QAA8B;CACjD,MAAM,OAAO,OAAO,QAAQ,cAAc,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,OAAO,GAAG;CACzF,MAAM,OAAO,CAAC,MAAM,KAAK,OAAO,MAAM,CAAC;CACvC,IAAI,OAAO,cAAc,UAAU,KAAK,KAAK,MAAM,OAAO,OAAO,SAAS,CAAC;CAC3E,IAAI,OAAO,SAAS,KAAK,KAAK,MAAM,KAAK,WAAW,OAAO,SAAS,CAAC;CACrE,IAAI,OAAO,UAAU,KAAK,KAAK,MAAM,KAAK,UAAU,CAAC;CAIrD,IAAI,OAAO,UAAU,KAAK,KAAK,MAAM,OAAO,sBAAsB,OAAO,UAAU,CAAC;CAEpF,QAAQ,IAAI,KAAK,KAAK,OAAO,OAAO,GAAG,KAAK,IAAI,KAAK,KAAK,KAAK,GAAG;CAClE,IAAI,OAAO,UAAU,OAAO,OAAO,SAAS,GACxC,QAAQ,IAAI,SAAS,MAAM,KAAK,SAAS,EAAE,GAAG,MAAM,KAAK,OAAO,OAAO,KAAK,IAAI,CAAC,GAAG;CAGxF,IAAI,iBAAiB;CACrB,KAAK,MAAM,WAAW,OAAO,UACzB,IAAI,QAAQ,KACR,QAAQ,IAAI,SAAS,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,KAAK,QAAQ,IAAI,GAAG;MAChE,IAAI,QAAQ,UAAU,KAIzB,QAAQ,IAAI,SAAS,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,KAAK,QAAQ,SAAS,IAAI,EAAE,GAAG,MAAM,KAAK,gBAAgB,QAAQ,KAAK,EAAE,GAAG;MACxH,IAAI,OAAO,UAAU,SAMxB,kBAAkB;MACf;EACH,MAAM,OAAO,QAAQ,WAAW,MAAM,KAAK,OAAO,QAAQ,SAAS,MAAM,IAAI;EAC7E,QAAQ,IAAI,SAAS,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,KAAK,QAAQ,IAAI,IAAI,KAAK,GAAG,MAAM,KAAK,SAAS,GAAG;CACtG;CAEJ,IAAI,iBAAiB,GACjB,QAAQ,IAAI,MAAM,KAAK,SAAS,eAAe,oBAAoB,mBAAmB,IAAI,KAAK,IAAI,SAAS,CAAC;CAGjH,IAAI,OAAO,QAAQ;EACf,MAAM,SAAS,OAAO,UAAU,WAAW,MAAM,MAAM,OAAO,UAAU,iBAAiB,MAAM,SAAS,MAAM;EAC9G,QAAQ,IAAI,SAAS,OAAO,KAAK,OAAO,QAAQ,GAAG;CACvD;AACJ;;;;;;;;;;;;;;;;;AA4BA,eAAsB,eAAe,aAAqB,SAA0D;CAChH,MAAM,EAAE,UAAU,QAAQ,kBAAkB,gCAAgC,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC;CACpG,IAAI,QAAQ,aAAa,cAAc,OAAO;EAAE;EAAQ;CAAc;CAEtE,IAAI,SAAS,SAAS,WAAW;EAC7B,MAAM,SAAS,MAAM,kBAAkB,WAAW;EAClD,OAAO;GAAE,iBAAiB,EAAE,KAAK,SAAS,WAAW,OAAO,IAAI,IAAI,KAAK;GAAG;GAAQ;EAAc;CACtG;CAEA,IAAI,SAAS,SAAS,YAAY,SAAS,KAAK;EAC5C,MAAM,SAAS,iBAAiB,SAAS,GAAG;EAC5C,MAAM,YAAY,SAAS,MAAM,SAAS,OAAO,MAAM,OAAO,IAAI,IAAI;EACtE,MAAM,QAAQ,SAAS,WAAW,OAAO,KAAK,KAAK,OAAO,SAAS,2BAA2B;EAC9F,OAAO;GACH,gBAAgB;IACZ,KAAK,SAAS;IACd,QAAQ,QAAQ,+CAA+C,eAAe,SAAS,GAAG,KACnF,YAAY,KAAK;GAC5B;GACA;GACA;EACJ;CACJ;CAEA,OAAO;EAAE;EAAQ;CAAc;AACnC;AAEA,eAAsB,cAAc,SAAkC;CAClE,IAAI,UAAU,OAAO,GAAG,OAAO,MAAM;CAErC,MAAM,EAAE,UAAU,iBAAiB;EAC/B,MAAM,EAAE,UAAU,QAAQ;EAC1B;EACA,cAAc;EACd,SAAS;EACT,gBAAgB;CACpB,CAAC;CAED,MAAM,cAAc,mBAAmB;CAOvC,MAAM,SAAS,QAAQ,MAAM,SAAS;CAEtC,IAAI;CACJ,IAAI;EAEA,MAAM,QAAQ,eADC,aAAa,WACC,CAAA,CAAO,QAAQ;EAC5C,IAAI,CAAC,OAAO;GACR,IAAI,QACA,WACI,sEACA,kBACA,8DACJ;GAEJ,QAAQ,MAAM,MAAM,IAAI,oEAAoE,CAAC;GAC7F,QAAQ,WAAW;GACnB;EACJ;EACA,UAAU;CACd,SAAS,KAAK;EACV,IAAI,eAAe,eAAe;GAC9B,IAAI,QACA,WAAW,IAAI,SAAS,oBAAoB,yCACxC,IAAI,OAAO,KAAI,WAAU;IAAE,MAAM,MAAM;IAAM,SAAS,MAAM;GAAQ,EAAE,CAAC;GAE/E,QAAQ,MAAM,MAAM,IAAI,IAAI,OAAO,CAAC;GACpC,KAAK,MAAM,SAAS,IAAI,QAAQ,QAAQ,MAAM,KAAK,MAAM,KAAK,MAAM,IAAI,EAAE,GAAG,MAAM,SAAS;GAC5F,QAAQ,WAAW;GACnB;EACJ;EACA,MAAM;CACV;CAEA,MAAM,EAAE,MAAM,aAAa,KAAK,eAAe;CAC/C,MAAM,QAAQ,oBAAoB,YAAY,WAAW;CAEzD,MAAM,EAAE,OAAO,WAAW,MAAM,oBAAoB,iBAAiB,aAAa,UAAU,CAAC;CAC7F,IAAI,OAAO,SAAS,GAAG;EACnB,IAAI,QACA,WACI,GAAG,OAAO,OAAO,yCACjB,gCACA,8DACA,OAAO,KAAI,WAAU;GAAE,MAAM,MAAM;GAAM,SAAS,MAAM;EAAQ,EAAE,CACtE;EAEJ,QAAQ,MAAM,MAAM,IAAI,OAAO,OAAO,OAAO,yCAAyC,CAAC;EACvF,KAAK,MAAM,SAAS,QAAQ,QAAQ,MAAM,KAAK,MAAM,KAAK,MAAM,IAAI,EAAE,IAAI,MAAM,SAAS;EACzF,QAAQ,MAAM,EAAE;EAChB,QAAQ,WAAW;EACnB;CACJ;CAKA,MAAM,MAAM,YAAY,WAAW;CAEnC,MAAM,aAAa,IAAI,aAAa;CACpC,MAAM,EAAE,iBAAiB,gBAAgB,QAAQ,kBAAkB,MAAM,eAAe,aAAa,GAAG;CAExG,MAAM,SAAS,MAAM,OAAO;CAC5B,MAAM,EAAE,WAAW,YAAY,cAAc,OAAO,KAAK;EACrD,aAAa,OAAO;EACpB,oBAAoB,OAAO;EAG3B;EACA,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;EAC7C,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;CAC/C,CAAC;CAED,IAAI,QAAQ;EACR,QAAQ,IAAI,KAAK,UAAU;GACvB,SAAS;GACT,SAAS,WAAW;GACpB,GAAI,SAAS,EAAE,QAAQ;IAAE,MAAM,OAAO;IAAM,UAAU,OAAO;GAAS,EAAE,IAAI,CAAC;GAC7E;GACA;EACJ,GAAG,MAAM,CAAC,CAAC;EACX;CACJ;CAEA,MAAM,YAAY,WAAW,YAAY,YACnC,uCACA;CAEN,QAAQ,IAAI,EAAE;CACd,QAAQ,IAAI,KAAK,MAAM,KAAK,WAAW,EAAE,IAAI,MAAM,KAAK,GAAG,EAAE,IAAI,MAAM,KAAK,WAAW,OAAO,EAAE,IAAI,MAAM,KAAK,SAAS,GAAG;CAK3H,MAAM,gBAAgB,KAAK,KAAK,MAAM,QAAQ,cAAc;CAC5D,MAAM,mBAAmB,GAAG,WAAW,KAAK,KAAK,aAAa,aAAa,CAAC;CAC5E,QAAQ,IAAI,KAAK,MAAM,KAAK,aAAa,EAAE,IAAI,MAAM,KAAK,aAAa,IACnE,mBAAmB,KAAK,MAAM,KAAK,4CAA4C,GAAG;CACtF,QAAQ,IAAI,KAAK,MAAM,KAAK,eAAe,EAAE,IAAI,MAAM,KAAK,MAAM,GAAG;CAGrE,IAAI,QACA,QAAQ,IAAI,KAAK,MAAM,KAAK,WAAW,EAAE,IAAI,MAAM,KAAK,OAAO,IAAI,EAAE,GAAG,MAAM,KAAK,IAAI,OAAO,SAAS,EAAE,MACnG,MAAM,KAAK,MAAM,MAAM,KAAK,+BAA+B,EAAE,cAAc,OAAO,UAAU,qBAAqB,CAAC;MACrH,IAAI,eACP,QAAQ,IAAI,KAAK,MAAM,OAAO,GAAG,EAAE,GAAG,MAAM,KAAK,uBAAuB,cAAc,kEAAkE,GAAG;CAG/J,MAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,UAAU,KAAI,MAAK,EAAE,IAAI,CAAC,CAAC;CACrD,KAAK,MAAM,QAAQ,OAAO;EACtB,QAAQ,IAAI,EAAE;EACd,QAAQ,IAAI,KAAK,MAAM,KAAK,UAAU,GAAG,KAAK,EAAE,GAAG;EACnD,KAAK,MAAM,UAAU,UAAU,QAAO,MAAK,EAAE,SAAS,IAAI,GAAG,cAAc,MAAM;CACrF;CAEA,IAAI,SAAS;EACT,QAAQ,IAAI,EAAE;EACd,QAAQ,IAAI,KAAK,MAAM,IAAI,GAAG,EAAE,GAAG,MAAM,IAAI,OAAO,GAAG;CAC3D;CAUA,MAAM,YAAY,sBAAsB,WAAW;CACnD,IAAI,cAAc,QAAQ,cAAc,uBAAuB,KAAK,GAAG;EACnE,QAAQ,IAAI,EAAE;EACd,QAAQ,IAAI,KAAK,MAAM,OAAO,GAAG,EAAE,GAAG,wBAAwB,cAAc,MAAM,KAAK,0BAA0B,GAAG;EACpH,QAAQ,IAAI,MAAM,KAAK,2EAA2E,CAAC;CACvG;CAEA,QAAQ,IAAI,EAAE;AAClB"}
|
|
1
|
+
{"version":3,"file":"status-BJ26yZOo.js","names":[],"sources":["../src/resources/status.ts","../src/commands/status.ts"],"sourcesContent":["/**\n * What a project declared, what the environment binds it to, and whether the\n * two meet.\n *\n * ## Why this exists\n *\n * Every piece of this was already knowable and none of it was visible in one\n * place. `rebase resources` lists what you declared. The environment holds the\n * variables. The resolvers decide whether a source is usable. So the question a\n * developer actually has — *\"why does uploading to `media` answer 501\"* — could\n * only be answered by knowing the suffix rule, deriving the variable name by\n * hand, and checking it against a `.env` they had to find first.\n *\n * The answer is mechanical, so it should be printed rather than taught.\n *\n * ## Why it calls the resolvers instead of re-deciding\n *\n * The verdict comes from `resolveDataSources` and `resolveStorageBackend` — the\n * same functions that run at boot. A status view that decided for itself what\n * \"configured\" means would be a second implementation of the rule, and the\n * first time it disagreed it would be reassuring a developer about a deployment\n * that is about to refuse to start.\n *\n * The variable *names* come from each kind's `envBases`, and the verdicts from\n * the resolver `@rebasepro/server` registers per kind — one object per kind,\n * held to `envBases` by `resource-env-bases.test.ts`. A kind this view has\n * never heard of is judged by whatever resolver its package registered.\n */\nimport { withImplicitDefaults } from \"./derive\";\nimport {\n DEFAULT_RESOURCE_KEY,\n envBasesForResource,\n resourceEnvSuffix,\n resourceToDataSource,\n type ResourceDeclaration,\n type ResourceGraph\n} from \"@rebasepro/types\";\n\n/** One environment variable a resource reads, and whether it is there. */\nexport interface ResourceBinding {\n /** The variable this resource reads, suffix included. */\n name: string;\n set: boolean;\n /**\n * The account-scoped name consulted when `name` is unset.\n *\n * Only provider-level bindings have one, and only when the resource named\n * an `account`. Shown because a developer looking at an unset\n * `S3_ACCESS_KEY_ID__MEDIA` needs to know it is not the whole story.\n */\n fallback?: { name: string; set: boolean };\n}\n\n/** Whether a declared resource can actually be reached. */\nexport type ResourceState =\n /** Bound and usable. */\n | \"ready\"\n /** Nothing set for it. Legal for a bucket, fatal for a database. */\n | \"unconfigured\"\n /** Set, and set wrongly. The deployment refuses to start. */\n | \"broken\";\n\nexport interface ResourceStatus {\n kind: string;\n key: string;\n engine: string;\n transport: string;\n /** Present when this resource shares another's credentials. */\n account?: string;\n /** True for a resource nobody declared, which exists anyway. */\n implicit: boolean;\n bindings: ResourceBinding[];\n state: ResourceState;\n /** What this means, in the terms the developer will meet it in. */\n detail: string;\n /** Set when a local directory stands in for this engine, in development. */\n standsIn?: string;\n /** What in the project reaches this resource, from the graph. */\n usedBy?: readonly string[];\n}\n\nexport type EnvBag = Record<string, string | undefined>;\n\n/** Set, and not set to the empty string — which the resolvers treat as absent. */\nfunction isSet(env: EnvBag, name: string): boolean {\n const value = env[name];\n return typeof value === \"string\" && value !== \"\";\n}\n\n/**\n * The bindings one declaration reads, in the order a reader should scan them.\n *\n * A `direct`-transport resource gets none: the browser reaches it with a\n * provider SDK and the backend binds nothing for it, so listing variables would\n * invite someone to set variables that are never read.\n */\nexport function bindingsFor(\n declaration: ResourceDeclaration,\n env: EnvBag,\n accountScopedBases: readonly string[]\n): ResourceBinding[] {\n if (declaration.transport !== \"server\") return [];\n\n const suffix = resourceEnvSuffix(declaration.key);\n const account = typeof declaration.options.account === \"string\"\n ? resourceEnvSuffix(declaration.options.account)\n : undefined;\n\n return envBasesForResource(declaration).map((base) => {\n const name = `${base}${suffix}`;\n const binding: ResourceBinding = { name, set: isSet(env, name) };\n\n // Only the provider-level bindings fall back, and only for a resource\n // that named an account. The bucket name never does — it is what\n // distinguishes one source from another.\n if (account !== undefined && !binding.set && accountScopedBases.includes(base)) {\n binding.fallback = { name: `${base}${account}`, set: isSet(env, `${base}${account}`) };\n }\n return binding;\n });\n}\n\n/**\n * What the managed development database covers, when a project is on it.\n *\n * `rebase init` leaves `DATABASE_URL` commented out on purpose and the managed\n * PGlite fills the vacuum — the documented first-run state. Judging that state\n * by the environment alone produced `○ (default) postgres · DATABASE_URL not\n * set` and the remedy \"set DATABASE_URL\", at the same moment `rebase db url`\n * was printing a working connection string for the same project and\n * `rebase dev` was serving from it. The status view was reading the one place\n * the answer deliberately is not.\n *\n * Resolved by the caller, because deciding it needs the project's `.env`, its\n * branch pointer and its compose file, and this module stays free of all three.\n */\nexport interface ManagedDatabaseStatus {\n /**\n * The daemon's connection string, or null when it is not running.\n *\n * Null is not an error: the managed database exists per project and starts\n * on demand. It only means there is nothing to run the whole-set check\n * against, and the row says so rather than implying a misconfiguration.\n */\n url: string | null;\n}\n\n/**\n * The compose `db` service, when a project is on Postgres in Docker.\n *\n * `rebase dev --docker` is remembered, and every command follows it with the\n * scaffold's `DATABASE_URL` still commented out — so, judged by `.env` alone,\n * this view said the default database had no connection string while `rebase\n * dev --docker` served the project from it. Unlike the managed database it\n * stands in for the default only: `dev --docker` hands the backend\n * `DATABASE_URL` and nothing else, so a second declared database still needs\n * its own variable.\n */\nexport interface DockerDatabaseStatus {\n /** The compose service's connection string, which the runtime is given as `DATABASE_URL`. */\n url: string;\n /** What the default database's row says it is. */\n detail: string;\n}\n\n/**\n * The resolver-side of status, as `@rebasepro/server` registers it per kind.\n *\n * Passed in rather than imported so this module stays testable without a\n * server, and so a kind registered by a plugin is judged by the resolver the\n * plugin brought with it rather than by a switch here.\n */\nexport interface StatusResolvers {\n /** `resourceResolver(kind)` from `@rebasepro/server`. */\n resolverFor: (kind: string) => {\n accountScoped?: readonly string[];\n resolve: (\n declaration: ResourceDeclaration,\n env: EnvBag,\n context: { production: boolean; defaultBasePath: string }\n ) => { state: \"ready\" | \"unbound\" | \"blocked\" | \"code\"; detail?: string; standsIn?: string };\n } | undefined;\n /** `resolveDataSources` from `@rebasepro/server`, for the whole-set check. */\n resolveDataSources: (env: EnvBag, definitions: unknown) => unknown;\n /** Whether the process being judged is production. Status judges for development by default. */\n production?: boolean;\n /**\n * Set when this project runs on the managed development database.\n *\n * Absent for a project that named its own connection string, and absent in\n * production — where an unset `DATABASE_URL` is the failure it has always\n * been.\n */\n managedDatabase?: ManagedDatabaseStatus;\n /** Set when this project runs on Postgres in Docker. Absent in production, like the managed one. */\n dockerDatabase?: DockerDatabaseStatus;\n}\n\n/**\n * The environment plus the managed database's URL, for the whole-set check.\n *\n * A copy: the bindings a reader sees are computed against the real `.env`, so\n * nothing here can make the view claim a variable is set that is not. This is\n * only what `resolveDataSources` is handed, and it is handed it so the check\n * judges the environment the runtime would actually get rather than throwing\n * \"Data source \"(default)\" has no connection string\" about a database that is\n * running and reachable.\n */\nfunction withManagedUrls(env: EnvBag, graph: ResourceGraph, url: string): EnvBag {\n const filled: EnvBag = { ...env };\n const keys = [\n DEFAULT_RESOURCE_KEY,\n ...graph.resources.filter(r => r.kind === \"database\").map(r => r.key)\n ];\n for (const key of keys) {\n const name = `DATABASE_URL${resourceEnvSuffix(key)}`;\n if (!isSet(filled, name)) filled[name] = url;\n }\n\n return filled;\n}\n\n/**\n * The environment with the default database's URL filled in, when `.env` leaves it out.\n *\n * What a database's resolver is judged against. The resolver checks a source\n * alongside the default one, so a second database bound by its own\n * `DATABASE_URL__ANALYTICS` was marked ✗ with `Data source \"(default)\" has no\n * connection string` — on a project whose default the managed database or the\n * compose service provides, and whose boot worked. Judged against the\n * environment the runtime actually gets, it is judged on its own string.\n */\nfunction withDefaultUrl(env: EnvBag, url: string | null | undefined): EnvBag {\n if (!url || isSet(env, \"DATABASE_URL\")) return env;\n\n return { ...env, DATABASE_URL: url };\n}\n\n/**\n * Resolve every declared resource against an environment.\n *\n * Each kind's verdict comes from the resolver `@rebasepro/server` registered\n * for it — the same one boot calls — so this view cannot say \"ready\" about a\n * binding boot would refuse. A kind with no resolver is reported as such\n * rather than skipped: a resource nobody shows is one nobody remembers to\n * configure, and a kind this runtime cannot bind is exactly what boot will\n * refuse by name.\n *\n * `resolveDataSources` is then run once over the whole set, so anything it\n * would refuse across sources (a driver package it cannot resolve for a custom\n * engine) is reported rather than missed by a view that only looked per row.\n */\nexport function computeStatus(\n graph: ResourceGraph,\n env: EnvBag,\n resolvers: StatusResolvers\n): { resources: ResourceStatus[]; blocked?: string } {\n const resources: ResourceStatus[] = [];\n const production = resolvers.production ?? false;\n const managed = resolvers.managedDatabase;\n const docker = resolvers.dockerDatabase;\n /** The default database's URL as the runtime gets it, when `.env` does not name one. */\n const devDefaultUrl = managed?.url ?? docker?.url ?? null;\n /** Database rows the managed database answered for, so `blocked` can skip them. */\n let managedCovered = 0;\n\n for (const { declaration, implicit } of withImplicitDefaults(graph)) {\n const resolver = resolvers.resolverFor(declaration.kind);\n const bindings = bindingsFor(declaration, env, resolver?.accountScoped ?? []);\n const base: Omit<ResourceStatus, \"state\" | \"detail\"> = {\n kind: declaration.kind,\n key: declaration.key,\n engine: declaration.engine,\n transport: declaration.transport,\n ...(typeof declaration.options.account === \"string\"\n ? { account: declaration.options.account }\n : {}),\n implicit,\n bindings,\n ...(declaration.usedBy && declaration.usedBy.length > 0 ? { usedBy: declaration.usedBy } : {})\n };\n\n if (!resolver) {\n resources.push({\n ...base,\n state: \"broken\",\n detail: `this runtime has no resolver for a \"${declaration.kind}\" — boot refuses it by name. ` +\n \"Upgrade @rebasepro/server, or remove the declaration.\"\n });\n continue;\n }\n\n // Answered before the resolver, because the resolver reads the\n // environment and the whole point of the managed database is that the\n // environment is deliberately empty. Only for a database nobody bound:\n // a developer who set `DATABASE_URL__ANALYTICS` to a warehouse of their\n // own has said which database that is, and the managed one fills a\n // vacuum, never a choice.\n if (managed && declaration.kind === \"database\" && bindings.every(b => !b.set && !b.fallback?.set)) {\n managedCovered += 1;\n resources.push({\n ...base,\n state: \"ready\",\n detail: managed.url\n ? \"the managed development database (PGlite), this project only\"\n : \"the managed development database (PGlite), this project only — \"\n + \"not running; start it with `rebase dev`\"\n });\n continue;\n }\n\n // The default database on Docker: the compose service, unless `.env`\n // names one itself.\n if (docker && declaration.kind === \"database\" && declaration.key === DEFAULT_RESOURCE_KEY\n && bindings.every(b => !b.set && !b.fallback?.set)) {\n resources.push({ ...base, state: \"ready\", detail: docker.detail });\n continue;\n }\n\n const judged = declaration.kind === \"database\" ? withDefaultUrl(env, devDefaultUrl) : env;\n const verdict = resolver.resolve(declaration, judged, { production, defaultBasePath: \"uploads\" });\n switch (verdict.state) {\n case \"ready\":\n resources.push({\n ...base,\n state: \"ready\",\n ...(verdict.standsIn ? { standsIn: verdict.standsIn } : {}),\n detail: verdict.detail ?? \"\"\n });\n break;\n case \"code\":\n resources.push({ ...base, state: \"ready\", detail: verdict.detail ?? \"needs no environment configuration\" });\n break;\n case \"unbound\":\n resources.push({ ...base, state: \"unconfigured\", detail: verdict.detail ?? \"declared, not configured\" });\n break;\n case \"blocked\":\n resources.push({\n ...base,\n // A database the environment names nothing for is the\n // ordinary first-run state, and reads as unconfigured; a\n // binding that is set and set wrongly is broken.\n state: implicit || bindings.every(b => !b.set && !b.fallback?.set) ? \"unconfigured\" : \"broken\",\n detail: verdict.detail ?? \"boot refuses this binding\"\n });\n break;\n }\n }\n\n // Only asked when every row already looks fine. Its failures overlap with\n // the per-resource ones, and a banner repeating a line printed two rows\n // above teaches nothing — what it is here for is the failure no row models,\n // such as a custom engine whose driver package cannot be resolved.\n let blocked: string | undefined;\n // Not run when the managed database answered for a row and is not running:\n // there is no connection string to check the set against, and inventing one\n // to keep the check formally \"run\" is how a green tick gets printed for\n // work nobody did. The row says the daemon is down; that is the finding.\n const managedSetUnknown = managedCovered > 0 && !managed?.url;\n if (resources.every(r => r.state === \"ready\") && !managedSetUnknown) {\n try {\n resolvers.resolveDataSources(\n // The managed URL for the rows it answered for, so the whole-set\n // check judges the same environment the runtime would get.\n managed?.url ? withManagedUrls(env, graph, managed.url) : withDefaultUrl(env, docker?.url),\n graph.resources.filter(r => r.kind === \"database\").map(resourceToDataSource)\n );\n } catch (err) {\n blocked = err instanceof Error ? err.message : String(err);\n }\n }\n\n return { resources, ...(blocked !== undefined ? { blocked } : {}) };\n}\n","/**\n * `rebase status` — what this project is, and whether it can reach its things.\n *\n * The model a developer has to hold is small and was nowhere visible:\n *\n * rebase.json where the code is, and who runs the server\n * config/resources.ts what the project needs\n * the environment how to reach each of them\n *\n * Everything else — `rebase.resources.json`, the bundle manifest — is a\n * generated copy of the middle one, for readers that cannot run the code. A\n * developer never writes them and should not have to think about them.\n *\n * This command prints the three together, resolved. It exists because the\n * question people actually arrive with — \"why does uploading to `media` answer\n * 501\" — was answerable only by knowing the `<BASE>__<KEY>` suffix rule,\n * deriving the variable name by hand, and checking it against a `.env` they had\n * to find first. Every step of that is mechanical, so it is printed instead.\n */\nimport chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { failAsJson, readEnvFile, requireProjectRoot } from \"../utils/project\";\nimport { findBackendApp, loadManifest, ManifestError, resolveBackendPaths } from \"../manifest\";\nimport { deriveOptionsFor, deriveResourceGraph, RESOURCE_GRAPH_FILENAME, readResourceGraphFile, serializeResourceGraph } from \"../resources/derive\";\nimport { computeStatus, type DockerDatabaseStatus, type ManagedDatabaseStatus, type ResourceStatus } from \"../resources/status\";\nimport { findRunningDaemon, managedUrl } from \"../dev-db/daemon\";\nimport { type ResolvedBranch, resolveProjectDatabaseAndBranch } from \"../dev-db/prepare\";\nimport { describeTarget } from \"../dev-db/pull\";\nimport { parseLoopbackDsn, probeTcp } from \"../utils/dev-preflight\";\nimport type { RebaseBackendAppConfig } from \"@rebasepro/types\";\n\nfunction usage(): void {\n console.log(`\n${chalk.bold(\"rebase status\")} — what this project declares, and whether it is configured\n\n${chalk.bold(\"Usage\")}\n rebase status Show every resource and the variables it reads\n rebase status --json The same, as JSON\n\n${chalk.bold(\"Reads\")}\n rebase.json which apps exist, and who owns the server process\n <config>/resources.ts the databases, buckets, topics and queues declared\n backend/crons, backend/functions the crons and functions, by filename\n .env whether each one's variables are set\n`);\n}\n\nconst MARK = {\n ready: chalk.green(\"✓\"),\n unconfigured: chalk.yellow(\"○\"),\n broken: chalk.red(\"✗\")\n} as const;\n\nfunction printResource(status: ResourceStatus): void {\n const name = status.key === \"(default)\" ? chalk.bold(\"(default)\") : chalk.bold(status.key);\n const tags = [chalk.gray(status.engine)];\n if (status.transport !== \"server\") tags.push(chalk.yellow(status.transport));\n if (status.account) tags.push(chalk.gray(`account:${status.account}`));\n if (status.implicit) tags.push(chalk.gray(\"implicit\"));\n // A stand-in is not a binding. It is shown in yellow beside a green tick:\n // uploads work here, and the same declaration answers 501 in production\n // until the variable is set.\n if (status.standsIn) tags.push(chalk.yellow(`local stand-in for ${status.standsIn}`));\n\n console.log(` ${MARK[status.state]} ${name} ${tags.join(\" · \")}`);\n if (status.usedBy && status.usedBy.length > 0) {\n console.log(` ${chalk.gray(\"used by\")} ${chalk.gray(status.usedBy.join(\", \"))}`);\n }\n\n let hiddenOptional = 0;\n for (const binding of status.bindings) {\n if (binding.set) {\n console.log(` ${chalk.green(\"✓\")} ${chalk.gray(binding.name)}`);\n } else if (binding.fallback?.set) {\n // Not a gap: the account form is the variable this bucket is\n // actually reading, and showing only the unset per-key name would\n // send someone to set the one they were deliberately avoiding.\n console.log(` ${chalk.green(\"✓\")} ${chalk.gray(binding.fallback.name)} ${chalk.gray(`(shared, for ${binding.name})`)}`);\n } else if (status.state === \"ready\") {\n // It resolved without this one, so this one is optional — which is\n // the resolver's answer, not a second table of required names kept\n // here. Listing seven unset optionals under a working database is\n // how a status view teaches people to skim past the line that\n // matters.\n hiddenOptional += 1;\n } else {\n const also = binding.fallback ? chalk.gray(` or ${binding.fallback.name}`) : \"\";\n console.log(` ${chalk.gray(\"·\")} ${chalk.gray(binding.name)}${also} ${chalk.gray(\"not set\")}`);\n }\n }\n if (hiddenOptional > 0) {\n console.log(chalk.gray(` ${hiddenOptional} optional variable${hiddenOptional === 1 ? \"\" : \"s\"} not set`));\n }\n\n if (status.detail) {\n const colour = status.state === \"broken\" ? chalk.red : status.state === \"unconfigured\" ? chalk.yellow : chalk.gray;\n console.log(` ${colour(`└ ${status.detail}`)}`);\n }\n}\n\n/** Which database `rebase status` reports the project on, and what it hands the status view about it. */\nexport interface StatusDatabase {\n managedDatabase?: ManagedDatabaseStatus;\n dockerDatabase?: DockerDatabaseStatus;\n /** The branch this checkout is on, when the database is one. */\n branch: ResolvedBranch | null;\n /** A switched-to branch the managed database cannot serve. */\n ignoredBranch: string | null;\n}\n\n/**\n * Which database this project is on, decided by the same ordered rule\n * `rebase dev` and `rebase db url` use — and read from the project's `.env`,\n * not from this shell: `rebase status` asks what *this project* is on.\n *\n * Nothing is started. `status` is a question, and a question that boots a\n * 180 MB Postgres to answer itself is a different command. So a managed\n * database that is not running is reported as exactly that, rather than as a\n * project with no database; and a compose database likewise, after a probe of\n * its port.\n *\n * `envFile` is the parsed `.env`; production stands nothing in, because there\n * an unset `DATABASE_URL` is the failure it has always been.\n *\n * Exported for its tests.\n */\nexport async function statusDatabase(projectRoot: string, envFile: Record<string, string>): Promise<StatusDatabase> {\n const { database, branch, ignoredBranch } = resolveProjectDatabaseAndBranch(projectRoot, { env: {} });\n if (envFile.NODE_ENV === \"production\") return { branch, ignoredBranch };\n\n if (database.kind === \"managed\") {\n const daemon = await findRunningDaemon(projectRoot);\n return { managedDatabase: { url: daemon ? managedUrl(daemon.port) : null }, branch, ignoredBranch };\n }\n\n if (database.kind === \"docker\" && database.url) {\n const target = parseLoopbackDsn(database.url);\n const listening = target ? await probeTcp(target.host, target.port) : true;\n const which = branch ? `branch \"${branch.name}\" (${branch.database}) of Postgres in Docker` : \"Postgres in Docker\";\n return {\n dockerDatabase: {\n url: database.url,\n detail: which + \", as the last `rebase dev --docker` did · \" + describeTarget(database.url)\n + (listening ? \"\" : \" — not running; `rebase dev --docker` starts it\")\n },\n branch,\n ignoredBranch\n };\n }\n\n return { branch, ignoredBranch };\n}\n\nexport async function statusCommand(rawArgs: string[]): Promise<void> {\n if (wantsHelp(rawArgs)) return usage();\n\n const { flags } = parseCommandArgs({\n spec: { \"--json\": Boolean },\n rawArgs,\n commandWords: 1,\n command: \"status\",\n maxPositionals: 0\n });\n\n const projectRoot = requireProjectRoot();\n\n // Every exit below has a `--json` arm, because `--json` means stdout holds\n // one JSON object on *every* exit of the command. It used to mean that for\n // the one failure `requireProjectRoot` owns and nothing else, so a caller\n // parsing stdout got an envelope outside a project and an empty stream\n // inside a broken one — the case it actually has to handle.\n const asJson = Boolean(flags[\"--json\"]);\n\n let backend: { name: string; app: RebaseBackendAppConfig };\n try {\n const loaded = loadManifest(projectRoot);\n const found = findBackendApp(loaded.manifest);\n if (!found) {\n if (asJson) {\n failAsJson(\n \"This project declares no backend app, so it declares no resources.\",\n \"no_backend_app\",\n \"Add a backend app to rebase.json, or run `rebase apps init`.\"\n );\n }\n console.error(chalk.red(\"This project declares no backend app, so it declares no resources.\"));\n process.exitCode = 1;\n return;\n }\n backend = found;\n } catch (err) {\n if (err instanceof ManifestError) {\n if (asJson) {\n failAsJson(err.message, \"manifest_invalid\", \"Fix rebase.json, then run this again.\",\n err.issues.map(issue => ({ path: issue.path, message: issue.message })));\n }\n console.error(chalk.red(err.message));\n for (const issue of err.issues) console.error(` ${chalk.gray(issue.path)} ${issue.message}`);\n process.exitCode = 1;\n return;\n }\n throw err;\n }\n\n const { name: backendName, app: backendApp } = backend;\n const paths = resolveBackendPaths(backendApp, projectRoot);\n\n const { graph, issues } = await deriveResourceGraph(deriveOptionsFor(projectRoot, backendApp));\n if (issues.length > 0) {\n if (asJson) {\n failAsJson(\n `${issues.length} problem(s) in the declared resources.`,\n \"resource_declaration_invalid\",\n \"Fix the declarations the issues name, then run this again.\",\n issues.map(issue => ({ path: issue.path, message: issue.message }))\n );\n }\n console.error(chalk.red(`\\n✗ ${issues.length} problem(s) in the declared resources:\\n`));\n for (const issue of issues) console.error(` ${chalk.bold(issue.path)} ${issue.message}`);\n console.error(\"\");\n process.exitCode = 1;\n return;\n }\n\n // The project's own `.env`, not this process's environment: `rebase status`\n // is asking what a deployment of THIS repository would do, and the shell a\n // developer happens to be standing in is not that.\n const env = readEnvFile(projectRoot);\n\n const production = env.NODE_ENV === \"production\";\n const { managedDatabase, dockerDatabase, branch, ignoredBranch } = await statusDatabase(projectRoot, env);\n\n const server = await import(\"@rebasepro/server\");\n const { resources, blocked } = computeStatus(graph, env, {\n resolverFor: server.resourceResolver as never,\n resolveDataSources: server.resolveDataSources as never,\n // Judged as production when the .env says so; a stand-in local\n // directory for an unbound bucket is only ever offered to development.\n production,\n ...(managedDatabase ? { managedDatabase } : {}),\n ...(dockerDatabase ? { dockerDatabase } : {})\n });\n\n if (asJson) {\n console.log(JSON.stringify({\n backend: backendName,\n runtime: backendApp.runtime,\n ...(branch ? { branch: { name: branch.name, database: branch.database } } : {}),\n resources,\n blocked\n }, null, 2));\n return;\n }\n\n const ownership = backendApp.runtime === \"managed\"\n ? \"Rebase's runtime boots your bundle\"\n : \"this project builds its own image and entrypoint\";\n\n console.log(\"\");\n console.log(` ${chalk.bold(backendName)} ${chalk.gray(\"·\")} ${chalk.cyan(backendApp.runtime)} ${chalk.gray(ownership)}`);\n // Named whether or not it exists, and said plainly when it does not. A\n // project with no `resources.ts` is legal — it has one default database and\n // one default bucket — but \"declared in <a file you do not have>\" reads as\n // a broken install rather than as the default it is.\n const resourcesFile = path.join(paths.config, \"resources.ts\");\n const declaresAnything = fs.existsSync(path.join(projectRoot, resourcesFile));\n console.log(` ${chalk.gray(\"declared in\")} ${chalk.gray(resourcesFile)}${\n declaresAnything ? \"\" : chalk.gray(\" (none — the defaults below are implicit)\")}`);\n console.log(` ${chalk.gray(\"configured by\")} ${chalk.gray(\".env\")}`);\n // Every database command in this checkout uses the branch, so the view\n // says so: a switch made yesterday is easy to forget.\n if (branch) {\n console.log(` ${chalk.gray(\"on branch\")} ${chalk.bold(branch.name)} ${chalk.gray(`(${branch.database})`)}`\n + chalk.gray(` — ${chalk.cyan(\"rebase db branch switch --off\")} returns to ${branch.parent ?? \"the main database\"}`));\n } else if (ignoredBranch) {\n console.log(` ${chalk.yellow(\"○\")} ${chalk.gray(`switched to branch \"${ignoredBranch}\", which the managed database cannot serve — it uses the main one`)}`);\n }\n\n const kinds = [...new Set(resources.map(r => r.kind))];\n for (const kind of kinds) {\n console.log(\"\");\n console.log(` ${chalk.bold.underline(`${kind}s`)}`);\n for (const status of resources.filter(r => r.kind === kind)) printResource(status);\n }\n\n if (blocked) {\n console.log(\"\");\n console.log(` ${chalk.red(\"✗\")} ${chalk.red(blocked)}`);\n }\n\n // Only worth a line when it is wrong. A generated file that matches its\n // declarations is not news, and a status view that reports its own\n // bookkeeping as an item trains people to skim past the items that matter.\n //\n // Nor is one that has not been written yet: `rebase build` writes it on\n // every build and says \"commit it\" when it changes, so a project that has\n // never been built — every fresh scaffold — has nothing wrong with it.\n // `rebase resources --check` is the gate that refuses a missing one.\n const committed = readResourceGraphFile(projectRoot);\n if (committed !== null && committed !== serializeResourceGraph(graph)) {\n console.log(\"\");\n console.log(` ${chalk.yellow(\"○\")} ${RESOURCE_GRAPH_FILENAME} is stale — ${chalk.blue(\"rebase resources --write\")}`);\n console.log(chalk.gray(\" a host reads it to decide what to provision before it runs anything\"));\n }\n\n console.log(\"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,SAAS,MAAM,KAAa,MAAuB;CAC/C,MAAM,QAAQ,IAAI;CAClB,OAAO,OAAO,UAAU,YAAY,UAAU;AAClD;;;;;;;;AASA,SAAgB,YACZ,aACA,KACA,oBACiB;CACjB,IAAI,YAAY,cAAc,UAAU,OAAO,CAAC;CAEhD,MAAM,SAAS,kBAAkB,YAAY,GAAG;CAChD,MAAM,UAAU,OAAO,YAAY,QAAQ,YAAY,WACjD,kBAAkB,YAAY,QAAQ,OAAO,IAC7C,KAAA;CAEN,OAAO,oBAAoB,WAAW,CAAC,CAAC,KAAK,SAAS;EAClD,MAAM,OAAO,GAAG,OAAO;EACvB,MAAM,UAA2B;GAAE;GAAM,KAAK,MAAM,KAAK,IAAI;EAAE;EAK/D,IAAI,YAAY,KAAA,KAAa,CAAC,QAAQ,OAAO,mBAAmB,SAAS,IAAI,GACzE,QAAQ,WAAW;GAAE,MAAM,GAAG,OAAO;GAAW,KAAK,MAAM,KAAK,GAAG,OAAO,SAAS;EAAE;EAEzF,OAAO;CACX,CAAC;AACL;;;;;;;;;;;AAwFA,SAAS,gBAAgB,KAAa,OAAsB,KAAqB;CAC7E,MAAM,SAAiB,EAAE,GAAG,IAAI;CAChC,MAAM,OAAO,CACT,sBACA,GAAG,MAAM,UAAU,QAAO,MAAK,EAAE,SAAS,UAAU,CAAC,CAAC,KAAI,MAAK,EAAE,GAAG,CACxE;CACA,KAAK,MAAM,OAAO,MAAM;EACpB,MAAM,OAAO,eAAe,kBAAkB,GAAG;EACjD,IAAI,CAAC,MAAM,QAAQ,IAAI,GAAG,OAAO,QAAQ;CAC7C;CAEA,OAAO;AACX;;;;;;;;;;;AAYA,SAAS,eAAe,KAAa,KAAwC;CACzE,IAAI,CAAC,OAAO,MAAM,KAAK,cAAc,GAAG,OAAO;CAE/C,OAAO;EAAE,GAAG;EAAK,cAAc;CAAI;AACvC;;;;;;;;;;;;;;;AAgBA,SAAgB,cACZ,OACA,KACA,WACiD;CACjD,MAAM,YAA8B,CAAC;CACrC,MAAM,aAAa,UAAU,cAAc;CAC3C,MAAM,UAAU,UAAU;CAC1B,MAAM,SAAS,UAAU;;CAEzB,MAAM,gBAAgB,SAAS,OAAO,QAAQ,OAAO;;CAErD,IAAI,iBAAiB;CAErB,KAAK,MAAM,EAAE,aAAa,cAAc,qBAAqB,KAAK,GAAG;EACjE,MAAM,WAAW,UAAU,YAAY,YAAY,IAAI;EACvD,MAAM,WAAW,YAAY,aAAa,KAAK,UAAU,iBAAiB,CAAC,CAAC;EAC5E,MAAM,OAAiD;GACnD,MAAM,YAAY;GAClB,KAAK,YAAY;GACjB,QAAQ,YAAY;GACpB,WAAW,YAAY;GACvB,GAAI,OAAO,YAAY,QAAQ,YAAY,WACrC,EAAE,SAAS,YAAY,QAAQ,QAAQ,IACvC,CAAC;GACP;GACA;GACA,GAAI,YAAY,UAAU,YAAY,OAAO,SAAS,IAAI,EAAE,QAAQ,YAAY,OAAO,IAAI,CAAC;EAChG;EAEA,IAAI,CAAC,UAAU;GACX,UAAU,KAAK;IACX,GAAG;IACH,OAAO;IACP,QAAQ,uCAAuC,YAAY,KAAK;GAEpE,CAAC;GACD;EACJ;EAQA,IAAI,WAAW,YAAY,SAAS,cAAc,SAAS,OAAM,MAAK,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,GAAG;GAC/F,kBAAkB;GAClB,UAAU,KAAK;IACX,GAAG;IACH,OAAO;IACP,QAAQ,QAAQ,MACV,iEACA;GAEV,CAAC;GACD;EACJ;EAIA,IAAI,UAAU,YAAY,SAAS,cAAc,YAAY,QAAQ,wBAC9D,SAAS,OAAM,MAAK,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,GAAG;GACpD,UAAU,KAAK;IAAE,GAAG;IAAM,OAAO;IAAS,QAAQ,OAAO;GAAO,CAAC;GACjE;EACJ;EAEA,MAAM,SAAS,YAAY,SAAS,aAAa,eAAe,KAAK,aAAa,IAAI;EACtF,MAAM,UAAU,SAAS,QAAQ,aAAa,QAAQ;GAAE;GAAY,iBAAiB;EAAU,CAAC;EAChG,QAAQ,QAAQ,OAAhB;GACI,KAAK;IACD,UAAU,KAAK;KACX,GAAG;KACH,OAAO;KACP,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;KACzD,QAAQ,QAAQ,UAAU;IAC9B,CAAC;IACD;GACJ,KAAK;IACD,UAAU,KAAK;KAAE,GAAG;KAAM,OAAO;KAAS,QAAQ,QAAQ,UAAU;IAAqC,CAAC;IAC1G;GACJ,KAAK;IACD,UAAU,KAAK;KAAE,GAAG;KAAM,OAAO;KAAgB,QAAQ,QAAQ,UAAU;IAA2B,CAAC;IACvG;GACJ,KAAK;IACD,UAAU,KAAK;KACX,GAAG;KAIH,OAAO,YAAY,SAAS,OAAM,MAAK,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,iBAAiB;KACtF,QAAQ,QAAQ,UAAU;IAC9B,CAAC;IACD;EACR;CACJ;CAMA,IAAI;CAKJ,MAAM,oBAAoB,iBAAiB,KAAK,CAAC,SAAS;CAC1D,IAAI,UAAU,OAAM,MAAK,EAAE,UAAU,OAAO,KAAK,CAAC,mBAC9C,IAAI;EACA,UAAU,mBAGN,SAAS,MAAM,gBAAgB,KAAK,OAAO,QAAQ,GAAG,IAAI,eAAe,KAAK,QAAQ,GAAG,GACzF,MAAM,UAAU,QAAO,MAAK,EAAE,SAAS,UAAU,CAAC,CAAC,IAAI,oBAAoB,CAC/E;CACJ,SAAS,KAAK;EACV,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC7D;CAGJ,OAAO;EAAE;EAAW,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;CAAG;AACtE;;;;;;;;;;;;;;;;;;;;;;ACpVA,SAAS,QAAc;CACnB,QAAQ,IAAI;EACd,MAAM,KAAK,eAAe,EAAE;;EAE5B,MAAM,KAAK,OAAO,EAAE;;;;EAIpB,MAAM,KAAK,OAAO,EAAE;;;;;CAKrB;AACD;AAEA,IAAM,OAAO;CACT,OAAO,MAAM,MAAM,GAAG;CACtB,cAAc,MAAM,OAAO,GAAG;CAC9B,QAAQ,MAAM,IAAI,GAAG;AACzB;AAEA,SAAS,cAAc,QAA8B;CACjD,MAAM,OAAO,OAAO,QAAQ,cAAc,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,OAAO,GAAG;CACzF,MAAM,OAAO,CAAC,MAAM,KAAK,OAAO,MAAM,CAAC;CACvC,IAAI,OAAO,cAAc,UAAU,KAAK,KAAK,MAAM,OAAO,OAAO,SAAS,CAAC;CAC3E,IAAI,OAAO,SAAS,KAAK,KAAK,MAAM,KAAK,WAAW,OAAO,SAAS,CAAC;CACrE,IAAI,OAAO,UAAU,KAAK,KAAK,MAAM,KAAK,UAAU,CAAC;CAIrD,IAAI,OAAO,UAAU,KAAK,KAAK,MAAM,OAAO,sBAAsB,OAAO,UAAU,CAAC;CAEpF,QAAQ,IAAI,KAAK,KAAK,OAAO,OAAO,GAAG,KAAK,IAAI,KAAK,KAAK,KAAK,GAAG;CAClE,IAAI,OAAO,UAAU,OAAO,OAAO,SAAS,GACxC,QAAQ,IAAI,SAAS,MAAM,KAAK,SAAS,EAAE,GAAG,MAAM,KAAK,OAAO,OAAO,KAAK,IAAI,CAAC,GAAG;CAGxF,IAAI,iBAAiB;CACrB,KAAK,MAAM,WAAW,OAAO,UACzB,IAAI,QAAQ,KACR,QAAQ,IAAI,SAAS,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,KAAK,QAAQ,IAAI,GAAG;MAChE,IAAI,QAAQ,UAAU,KAIzB,QAAQ,IAAI,SAAS,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,KAAK,QAAQ,SAAS,IAAI,EAAE,GAAG,MAAM,KAAK,gBAAgB,QAAQ,KAAK,EAAE,GAAG;MACxH,IAAI,OAAO,UAAU,SAMxB,kBAAkB;MACf;EACH,MAAM,OAAO,QAAQ,WAAW,MAAM,KAAK,OAAO,QAAQ,SAAS,MAAM,IAAI;EAC7E,QAAQ,IAAI,SAAS,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,KAAK,QAAQ,IAAI,IAAI,KAAK,GAAG,MAAM,KAAK,SAAS,GAAG;CACtG;CAEJ,IAAI,iBAAiB,GACjB,QAAQ,IAAI,MAAM,KAAK,SAAS,eAAe,oBAAoB,mBAAmB,IAAI,KAAK,IAAI,SAAS,CAAC;CAGjH,IAAI,OAAO,QAAQ;EACf,MAAM,SAAS,OAAO,UAAU,WAAW,MAAM,MAAM,OAAO,UAAU,iBAAiB,MAAM,SAAS,MAAM;EAC9G,QAAQ,IAAI,SAAS,OAAO,KAAK,OAAO,QAAQ,GAAG;CACvD;AACJ;;;;;;;;;;;;;;;;;AA4BA,eAAsB,eAAe,aAAqB,SAA0D;CAChH,MAAM,EAAE,UAAU,QAAQ,kBAAkB,gCAAgC,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC;CACpG,IAAI,QAAQ,aAAa,cAAc,OAAO;EAAE;EAAQ;CAAc;CAEtE,IAAI,SAAS,SAAS,WAAW;EAC7B,MAAM,SAAS,MAAM,kBAAkB,WAAW;EAClD,OAAO;GAAE,iBAAiB,EAAE,KAAK,SAAS,WAAW,OAAO,IAAI,IAAI,KAAK;GAAG;GAAQ;EAAc;CACtG;CAEA,IAAI,SAAS,SAAS,YAAY,SAAS,KAAK;EAC5C,MAAM,SAAS,iBAAiB,SAAS,GAAG;EAC5C,MAAM,YAAY,SAAS,MAAM,SAAS,OAAO,MAAM,OAAO,IAAI,IAAI;EACtE,MAAM,QAAQ,SAAS,WAAW,OAAO,KAAK,KAAK,OAAO,SAAS,2BAA2B;EAC9F,OAAO;GACH,gBAAgB;IACZ,KAAK,SAAS;IACd,QAAQ,QAAQ,+CAA+C,eAAe,SAAS,GAAG,KACnF,YAAY,KAAK;GAC5B;GACA;GACA;EACJ;CACJ;CAEA,OAAO;EAAE;EAAQ;CAAc;AACnC;AAEA,eAAsB,cAAc,SAAkC;CAClE,IAAI,UAAU,OAAO,GAAG,OAAO,MAAM;CAErC,MAAM,EAAE,UAAU,iBAAiB;EAC/B,MAAM,EAAE,UAAU,QAAQ;EAC1B;EACA,cAAc;EACd,SAAS;EACT,gBAAgB;CACpB,CAAC;CAED,MAAM,cAAc,mBAAmB;CAOvC,MAAM,SAAS,QAAQ,MAAM,SAAS;CAEtC,IAAI;CACJ,IAAI;EAEA,MAAM,QAAQ,eADC,aAAa,WACC,CAAA,CAAO,QAAQ;EAC5C,IAAI,CAAC,OAAO;GACR,IAAI,QACA,WACI,sEACA,kBACA,8DACJ;GAEJ,QAAQ,MAAM,MAAM,IAAI,oEAAoE,CAAC;GAC7F,QAAQ,WAAW;GACnB;EACJ;EACA,UAAU;CACd,SAAS,KAAK;EACV,IAAI,eAAe,eAAe;GAC9B,IAAI,QACA,WAAW,IAAI,SAAS,oBAAoB,yCACxC,IAAI,OAAO,KAAI,WAAU;IAAE,MAAM,MAAM;IAAM,SAAS,MAAM;GAAQ,EAAE,CAAC;GAE/E,QAAQ,MAAM,MAAM,IAAI,IAAI,OAAO,CAAC;GACpC,KAAK,MAAM,SAAS,IAAI,QAAQ,QAAQ,MAAM,KAAK,MAAM,KAAK,MAAM,IAAI,EAAE,GAAG,MAAM,SAAS;GAC5F,QAAQ,WAAW;GACnB;EACJ;EACA,MAAM;CACV;CAEA,MAAM,EAAE,MAAM,aAAa,KAAK,eAAe;CAC/C,MAAM,QAAQ,oBAAoB,YAAY,WAAW;CAEzD,MAAM,EAAE,OAAO,WAAW,MAAM,oBAAoB,iBAAiB,aAAa,UAAU,CAAC;CAC7F,IAAI,OAAO,SAAS,GAAG;EACnB,IAAI,QACA,WACI,GAAG,OAAO,OAAO,yCACjB,gCACA,8DACA,OAAO,KAAI,WAAU;GAAE,MAAM,MAAM;GAAM,SAAS,MAAM;EAAQ,EAAE,CACtE;EAEJ,QAAQ,MAAM,MAAM,IAAI,OAAO,OAAO,OAAO,yCAAyC,CAAC;EACvF,KAAK,MAAM,SAAS,QAAQ,QAAQ,MAAM,KAAK,MAAM,KAAK,MAAM,IAAI,EAAE,IAAI,MAAM,SAAS;EACzF,QAAQ,MAAM,EAAE;EAChB,QAAQ,WAAW;EACnB;CACJ;CAKA,MAAM,MAAM,YAAY,WAAW;CAEnC,MAAM,aAAa,IAAI,aAAa;CACpC,MAAM,EAAE,iBAAiB,gBAAgB,QAAQ,kBAAkB,MAAM,eAAe,aAAa,GAAG;CAExG,MAAM,SAAS,MAAM,OAAO;CAC5B,MAAM,EAAE,WAAW,YAAY,cAAc,OAAO,KAAK;EACrD,aAAa,OAAO;EACpB,oBAAoB,OAAO;EAG3B;EACA,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;EAC7C,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;CAC/C,CAAC;CAED,IAAI,QAAQ;EACR,QAAQ,IAAI,KAAK,UAAU;GACvB,SAAS;GACT,SAAS,WAAW;GACpB,GAAI,SAAS,EAAE,QAAQ;IAAE,MAAM,OAAO;IAAM,UAAU,OAAO;GAAS,EAAE,IAAI,CAAC;GAC7E;GACA;EACJ,GAAG,MAAM,CAAC,CAAC;EACX;CACJ;CAEA,MAAM,YAAY,WAAW,YAAY,YACnC,uCACA;CAEN,QAAQ,IAAI,EAAE;CACd,QAAQ,IAAI,KAAK,MAAM,KAAK,WAAW,EAAE,IAAI,MAAM,KAAK,GAAG,EAAE,IAAI,MAAM,KAAK,WAAW,OAAO,EAAE,IAAI,MAAM,KAAK,SAAS,GAAG;CAK3H,MAAM,gBAAgB,KAAK,KAAK,MAAM,QAAQ,cAAc;CAC5D,MAAM,mBAAmB,GAAG,WAAW,KAAK,KAAK,aAAa,aAAa,CAAC;CAC5E,QAAQ,IAAI,KAAK,MAAM,KAAK,aAAa,EAAE,IAAI,MAAM,KAAK,aAAa,IACnE,mBAAmB,KAAK,MAAM,KAAK,4CAA4C,GAAG;CACtF,QAAQ,IAAI,KAAK,MAAM,KAAK,eAAe,EAAE,IAAI,MAAM,KAAK,MAAM,GAAG;CAGrE,IAAI,QACA,QAAQ,IAAI,KAAK,MAAM,KAAK,WAAW,EAAE,IAAI,MAAM,KAAK,OAAO,IAAI,EAAE,GAAG,MAAM,KAAK,IAAI,OAAO,SAAS,EAAE,MACnG,MAAM,KAAK,MAAM,MAAM,KAAK,+BAA+B,EAAE,cAAc,OAAO,UAAU,qBAAqB,CAAC;MACrH,IAAI,eACP,QAAQ,IAAI,KAAK,MAAM,OAAO,GAAG,EAAE,GAAG,MAAM,KAAK,uBAAuB,cAAc,kEAAkE,GAAG;CAG/J,MAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,UAAU,KAAI,MAAK,EAAE,IAAI,CAAC,CAAC;CACrD,KAAK,MAAM,QAAQ,OAAO;EACtB,QAAQ,IAAI,EAAE;EACd,QAAQ,IAAI,KAAK,MAAM,KAAK,UAAU,GAAG,KAAK,EAAE,GAAG;EACnD,KAAK,MAAM,UAAU,UAAU,QAAO,MAAK,EAAE,SAAS,IAAI,GAAG,cAAc,MAAM;CACrF;CAEA,IAAI,SAAS;EACT,QAAQ,IAAI,EAAE;EACd,QAAQ,IAAI,KAAK,MAAM,IAAI,GAAG,EAAE,GAAG,MAAM,IAAI,OAAO,GAAG;CAC3D;CAUA,MAAM,YAAY,sBAAsB,WAAW;CACnD,IAAI,cAAc,QAAQ,cAAc,uBAAuB,KAAK,GAAG;EACnE,QAAQ,IAAI,EAAE;EACd,QAAQ,IAAI,KAAK,MAAM,OAAO,GAAG,EAAE,GAAG,wBAAwB,cAAc,MAAM,KAAK,0BAA0B,GAAG;EACpH,QAAQ,IAAI,MAAM,KAAK,2EAA2E,CAAC;CACvG;CAEA,QAAQ,IAAI,EAAE;AAClB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/cli",
|
|
3
|
-
"version": "0.25.0
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Developer tools for Rebase projects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"inquirer": "14.0.2",
|
|
44
44
|
"jiti": "^2.7.0",
|
|
45
45
|
"pg": "^8.22.0",
|
|
46
|
-
"@rebasepro/
|
|
47
|
-
"@rebasepro/client": "0.25.0
|
|
48
|
-
"@rebasepro/
|
|
49
|
-
"@rebasepro/server": "0.25.0
|
|
50
|
-
"@rebasepro/types": "0.25.0
|
|
51
|
-
"@rebasepro/server-postgres": "0.25.0
|
|
46
|
+
"@rebasepro/codegen": "0.25.0",
|
|
47
|
+
"@rebasepro/client": "0.25.0",
|
|
48
|
+
"@rebasepro/agent-skills": "0.25.0",
|
|
49
|
+
"@rebasepro/server": "0.25.0",
|
|
50
|
+
"@rebasepro/types": "0.25.0",
|
|
51
|
+
"@rebasepro/server-postgres": "0.25.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^26.1.2",
|