@abloatai/ablo 0.16.3 → 0.18.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/cli.cjs CHANGED
@@ -276700,7 +276700,7 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
276700
276700
  };
276701
276701
 
276702
276702
  // src/cli/index.ts
276703
- var import_picocolors18 = __toESM(require_picocolors(), 1);
276703
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
276704
276704
  var import_fs12 = require("fs");
276705
276705
  var import_path7 = require("path");
276706
276706
  var import_child_process2 = require("child_process");
@@ -277009,6 +277009,18 @@ var ERROR_CODES = {
277009
277009
  invalid_schema: wire("validation", 400, false, "The submitted schema could not be parsed."),
277010
277010
  incompatible_change: wire("conflict", 409, false, "The schema change is incompatible with the current schema.")
277011
277011
  };
277012
+ function errorCodeSpec(code) {
277013
+ return ERROR_CODES[code];
277014
+ }
277015
+ function classifyRecovery(code) {
277016
+ const spec = errorCodeSpec(code);
277017
+ if (!spec) return "none";
277018
+ if (spec.recovery) return spec.recovery;
277019
+ if (spec.retryable) return "transient";
277020
+ if (spec.httpStatus === 403) return "permission";
277021
+ if (spec.category === "auth") return "auth_blocked";
277022
+ return "none";
277023
+ }
277012
277024
 
277013
277025
  // src/coordination/schema.ts
277014
277026
  init_cjs_shims();
@@ -277331,6 +277343,20 @@ var AbloError = class extends Error {
277331
277343
  ...this.details ?? {}
277332
277344
  };
277333
277345
  }
277346
+ /**
277347
+ * A single, leak-proof line for logs and `String(err)` / template
277348
+ * interpolation: `AbloValidationError [code]: message (see docs) [request_id: …]`.
277349
+ *
277350
+ * Deliberately does NOT dump `details`, `cause`, or the stack — the thing that
277351
+ * makes `console.error(richError)` an unreadable wall of text. The structured
277352
+ * payload stays available via {@link toJSON}; this is the human one-liner.
277353
+ */
277354
+ toString() {
277355
+ const code = this.code ? ` [${this.code}]` : "";
277356
+ const docs = this.docUrl ? ` (see ${this.docUrl})` : "";
277357
+ const req = this.requestId ? ` [request_id: ${this.requestId}]` : "";
277358
+ return `${this.name}${code}: ${this.message}${docs}${req}`;
277359
+ }
277334
277360
  };
277335
277361
  function docUrlForCode(code) {
277336
277362
  return `https://docs.abloatai.com/errors#${code}`;
@@ -277378,7 +277404,7 @@ var ErrorBodyShapeSchema = import_zod4.z.object({
277378
277404
  }).passthrough();
277379
277405
 
277380
277406
  // src/cli/migrate.ts
277381
- var import_picocolors4 = __toESM(require_picocolors(), 1);
277407
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
277382
277408
  var import_fs5 = require("fs");
277383
277409
 
277384
277410
  // node_modules/postgres/src/index.js
@@ -278098,7 +278124,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
278098
278124
  async function cancel({ pid, secret }, resolve6, reject) {
278099
278125
  try {
278100
278126
  cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
278101
- await connect();
278127
+ await connect2();
278102
278128
  socket.once("error", reject);
278103
278129
  socket.once("close", resolve6);
278104
278130
  } catch (error2) {
@@ -278231,7 +278257,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
278231
278257
  incomings = null;
278232
278258
  }
278233
278259
  }
278234
- async function connect() {
278260
+ async function connect2() {
278235
278261
  terminated = false;
278236
278262
  backendParameters = {};
278237
278263
  socket || (socket = await createSocket());
@@ -278250,7 +278276,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
278250
278276
  hostIndex = (hostIndex + 1) % port.length;
278251
278277
  }
278252
278278
  function reconnect() {
278253
- setTimeout(connect, closedTime ? Math.max(0, closedTime + delay - import_perf_hooks.performance.now()) : 0);
278279
+ setTimeout(connect2, closedTime ? Math.max(0, closedTime + delay - import_perf_hooks.performance.now()) : 0);
278254
278280
  }
278255
278281
  function connected() {
278256
278282
  try {
@@ -279252,7 +279278,7 @@ function Postgres(a, b4) {
279252
279278
  const c = open.length ? open.shift() : await new Promise((resolve6, reject) => {
279253
279279
  const query = { reserve: resolve6, reject };
279254
279280
  queries.push(query);
279255
- closed.length && connect(closed.shift(), query);
279281
+ closed.length && connect2(closed.shift(), query);
279256
279282
  });
279257
279283
  move(c, reserved);
279258
279284
  c.reserved = () => queue.length ? c.execute(queue.shift()) : move(c, reserved);
@@ -279339,7 +279365,7 @@ function Postgres(a, b4) {
279339
279365
  if (open.length)
279340
279366
  return go(open.shift(), query);
279341
279367
  if (closed.length)
279342
- return connect(closed.shift(), query);
279368
+ return connect2(closed.shift(), query);
279343
279369
  busy.length ? go(busy.shift(), query) : queries.push(query);
279344
279370
  }
279345
279371
  function go(c, query) {
@@ -279372,7 +279398,7 @@ function Postgres(a, b4) {
279372
279398
  queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
279373
279399
  resolve6();
279374
279400
  }
279375
- function connect(c, query) {
279401
+ function connect2(c, query) {
279376
279402
  move(c, connecting);
279377
279403
  c.connect(query);
279378
279404
  return c;
@@ -279397,7 +279423,7 @@ function Postgres(a, b4) {
279397
279423
  c.reserved = null;
279398
279424
  c.onclose && (c.onclose(e2), c.onclose = null);
279399
279425
  options.onclose && options.onclose(c.id);
279400
- queries.length && connect(c, queries.shift());
279426
+ queries.length && connect2(c, queries.shift());
279401
279427
  }
279402
279428
  }
279403
279429
  function parseOptions(a, b4) {
@@ -279517,158 +279543,8 @@ function osUsername() {
279517
279543
 
279518
279544
  // src/cli/dbRole.ts
279519
279545
  init_cjs_shims();
279520
- var import_crypto2 = require("crypto");
279521
- var import_picocolors2 = __toESM(require_picocolors(), 1);
279522
279546
  var import_fs2 = require("fs");
279523
279547
  var import_path = require("path");
279524
- var DEFAULT_SCOPED_ROLE = "ablo_app";
279525
- async function detectRoleSafety(sql) {
279526
- const rows = await sql`SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user`;
279527
- const row = rows[0];
279528
- if (!row) return { role: "unknown", superuser: false, bypassRls: false, unsafe: false };
279529
- return {
279530
- role: row.rolname,
279531
- superuser: row.rolsuper,
279532
- bypassRls: row.rolbypassrls,
279533
- unsafe: row.rolsuper || row.rolbypassrls
279534
- };
279535
- }
279536
- function generateRolePassword() {
279537
- return (0, import_crypto2.randomBytes)(24).toString("base64url");
279538
- }
279539
- function scramSha256Verifier(password, iterations = 4096) {
279540
- const salt = (0, import_crypto2.randomBytes)(16);
279541
- const saltedPassword = (0, import_crypto2.pbkdf2Sync)(password, salt, iterations, 32, "sha256");
279542
- const clientKey = (0, import_crypto2.createHmac)("sha256", saltedPassword).update("Client Key").digest();
279543
- const storedKey = (0, import_crypto2.createHash)("sha256").update(clientKey).digest();
279544
- const serverKey = (0, import_crypto2.createHmac)("sha256", saltedPassword).update("Server Key").digest();
279545
- return `SCRAM-SHA-256$${iterations}:${salt.toString("base64")}$${storedKey.toString("base64")}:${serverKey.toString("base64")}`;
279546
- }
279547
- function scopedRoleStatements(input) {
279548
- const role = input.role ?? DEFAULT_SCOPED_ROLE;
279549
- const q2 = (id) => `"${id.replace(/"/g, '""')}"`;
279550
- const pw = (input.passwordMode ?? "scram-verifier") === "scram-verifier" ? scramSha256Verifier(input.password) : input.password.replace(/'/g, "''");
279551
- return [
279552
- `DO $$ BEGIN
279553
- CREATE ROLE ${q2(role)} LOGIN PASSWORD '${pw}'
279554
- NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE;
279555
- EXCEPTION WHEN duplicate_object THEN
279556
- -- Rerun: rotate ONLY the password. Re-asserting attributes here trips
279557
- -- managed-Postgres permission walls (Neon: "permission denied to alter
279558
- -- role" for attribute changes by non-superusers); the attributes were set
279559
- -- at creation, and the server-side probe still audits the live role.
279560
- ALTER ROLE ${q2(role)} WITH LOGIN PASSWORD '${pw}';
279561
- END $$;`,
279562
- `GRANT CREATE, CONNECT ON DATABASE ${q2(input.database)} TO ${q2(role)};`,
279563
- `GRANT CREATE, USAGE ON SCHEMA public TO ${q2(role)};`
279564
- ];
279565
- }
279566
- function rewriteDatabaseUrl(ownerUrl, role, password) {
279567
- const url = new URL(ownerUrl);
279568
- url.username = role;
279569
- url.password = password;
279570
- return url.toString();
279571
- }
279572
- async function createScopedRole(ownerUrl, options) {
279573
- const role = options?.role ?? DEFAULT_SCOPED_ROLE;
279574
- const password = generateRolePassword();
279575
- const database = new URL(ownerUrl).pathname.replace(/^\//, "") || "postgres";
279576
- const sql = src_default(ownerUrl, { max: 1, prepare: false, onnotice: () => {
279577
- } });
279578
- try {
279579
- try {
279580
- for (const statement of scopedRoleStatements({ database, role, password })) {
279581
- await sql.unsafe(statement);
279582
- }
279583
- } catch (err) {
279584
- const message = err instanceof Error ? err.message : String(err);
279585
- if (!/plaintext password/i.test(message)) throw err;
279586
- for (const statement of scopedRoleStatements({ database, role, password, passwordMode: "plaintext" })) {
279587
- await sql.unsafe(statement);
279588
- }
279589
- }
279590
- } finally {
279591
- await sql.end({ timeout: 5 });
279592
- }
279593
- return { role, databaseUrl: rewriteDatabaseUrl(ownerUrl, role, password) };
279594
- }
279595
- async function ensureScopedRoleInteractive(dbUrl) {
279596
- let safety;
279597
- try {
279598
- const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
279599
- } });
279600
- try {
279601
- safety = await detectRoleSafety(sql);
279602
- } finally {
279603
- await sql.end({ timeout: 5 });
279604
- }
279605
- } catch {
279606
- return dbUrl;
279607
- }
279608
- if (!safety.unsafe) return dbUrl;
279609
- const why = safety.superuser ? "a superuser" : "BYPASSRLS";
279610
- console.log(
279611
- `
279612
- ${import_picocolors2.default.yellow("!")} DATABASE_URL connects as ${import_picocolors2.default.bold(safety.role)} \u2014 ${why}, so row-level security can't be enforced.
279613
- Ablo's server will refuse this connection (${import_picocolors2.default.bold("database_role_cannot_enforce_rls")}).`
279614
- );
279615
- if (!process.stdout.isTTY) {
279616
- console.log(
279617
- import_picocolors2.default.dim(
279618
- ` Create a scoped role and update DATABASE_URL \u2014 run \`npx ablo migrate\` interactively
279619
- to do it automatically, or see https://docs.abloatai.com/quickstart#scoped-role`
279620
- )
279621
- );
279622
- return dbUrl;
279623
- }
279624
- const proceed = await ye({
279625
- message: `Create a scoped role ${DEFAULT_SCOPED_ROLE} (NOSUPERUSER, NOBYPASSRLS) and update DATABASE_URL?`,
279626
- initialValue: true
279627
- });
279628
- if (pD(proceed) || !proceed) {
279629
- console.log(import_picocolors2.default.dim(" Skipped \u2014 see https://docs.abloatai.com/quickstart#scoped-role for the manual recipe."));
279630
- return dbUrl;
279631
- }
279632
- const { role, databaseUrl } = await createScopedRole(dbUrl);
279633
- const where = persistDatabaseUrl(databaseUrl);
279634
- console.log(
279635
- ` ${import_picocolors2.default.green("\u2713")} Created role ${import_picocolors2.default.bold(role)} and updated ${import_picocolors2.default.bold("DATABASE_URL")} in ${import_picocolors2.default.bold(where)}.
279636
- ` + import_picocolors2.default.dim(` The owner credential never left this machine; the new password was written, not printed.`)
279637
- );
279638
- return databaseUrl;
279639
- }
279640
- function persistDatabaseUrl(databaseUrl, cwd = process.cwd()) {
279641
- const line = `DATABASE_URL=${databaseUrl}`;
279642
- for (const name of [".env.local", ".env"]) {
279643
- const path = (0, import_path.resolve)(cwd, name);
279644
- if (!(0, import_fs2.existsSync)(path)) continue;
279645
- const content = (0, import_fs2.readFileSync)(path, "utf8");
279646
- if (/^DATABASE_URL=/m.test(content)) {
279647
- (0, import_fs2.writeFileSync)(path, content.replace(/^DATABASE_URL=.*$/m, line));
279648
- return name;
279649
- }
279650
- }
279651
- const envLocal = (0, import_path.resolve)(cwd, ".env.local");
279652
- if ((0, import_fs2.existsSync)(envLocal)) {
279653
- const content = (0, import_fs2.readFileSync)(envLocal, "utf8");
279654
- (0, import_fs2.appendFileSync)(envLocal, `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${line}
279655
- `);
279656
- } else {
279657
- (0, import_fs2.writeFileSync)(envLocal, `${line}
279658
- `, { mode: 384 });
279659
- }
279660
- const gitignorePath = (0, import_path.resolve)(cwd, ".gitignore");
279661
- const gitignore = (0, import_fs2.existsSync)(gitignorePath) ? (0, import_fs2.readFileSync)(gitignorePath, "utf8") : "";
279662
- if (!/^(\.env\.local|\.env\*|\.env\.\*|\.env.*)$/m.test(gitignore)) {
279663
- (0, import_fs2.writeFileSync)(
279664
- gitignorePath,
279665
- `${gitignore.endsWith("\n") || gitignore.length === 0 ? gitignore : `${gitignore}
279666
- `}.env.local
279667
- `
279668
- );
279669
- }
279670
- return ".env.local";
279671
- }
279672
279548
  function readProjectDatabaseUrl(cwd = process.cwd()) {
279673
279549
  const fromEnv = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
279674
279550
  if (fromEnv) return fromEnv;
@@ -279680,6 +279556,16 @@ function readProjectDatabaseUrl(cwd = process.cwd()) {
279680
279556
  }
279681
279557
  return null;
279682
279558
  }
279559
+ function readProjectApiKey(cwd = process.cwd()) {
279560
+ if (process.env.ABLO_API_KEY) return { key: process.env.ABLO_API_KEY, source: "env" };
279561
+ for (const name of [".env.local", ".env"]) {
279562
+ const path = (0, import_path.resolve)(cwd, name);
279563
+ if (!(0, import_fs2.existsSync)(path)) continue;
279564
+ const match = (0, import_fs2.readFileSync)(path, "utf8").match(/^ABLO_API_KEY=(.+)$/m);
279565
+ if (match?.[1]) return { key: match[1].trim().replace(/^["']|["']$/g, ""), source: name };
279566
+ }
279567
+ return null;
279568
+ }
279683
279569
 
279684
279570
  // src/cli/migrate.ts
279685
279571
  var import_schema3 = require("@abloatai/ablo/schema");
@@ -279687,7 +279573,7 @@ var import_source = require("@abloatai/ablo/source");
279687
279573
 
279688
279574
  // src/cli/push.ts
279689
279575
  init_cjs_shims();
279690
- var import_picocolors3 = __toESM(require_picocolors(), 1);
279576
+ var import_picocolors2 = __toESM(require_picocolors(), 1);
279691
279577
 
279692
279578
  // src/auth/credentialPolicy.ts
279693
279579
  init_cjs_shims();
@@ -279929,7 +279815,16 @@ var DEFAULT_URL = "https://api.abloatai.com";
279929
279815
  function fmtSignal(s) {
279930
279816
  const sig = s;
279931
279817
  const where = sig.field ? `${sig.model}.${sig.field}` : sig.model;
279932
- return ` \u2022 ${import_picocolors3.default.bold(where ?? "?")} \u2014 ${sig.detail ?? ""}`;
279818
+ let line = ` \u2022 ${import_picocolors2.default.bold(where ?? "?")} \u2014 ${sig.detail ?? ""}`;
279819
+ if (sig.shadowed) {
279820
+ const env = sig.shadowed.environment ?? "production";
279821
+ const ver = sig.shadowed.version != null ? `v${sig.shadowed.version}` : "active";
279822
+ const when = sig.shadowed.pushedAt ? new Date(sig.shadowed.pushedAt).toISOString().slice(0, 10) : "unknown date";
279823
+ const by = sig.shadowed.pushedBy ? ` by ${sig.shadowed.pushedBy}` : "";
279824
+ line += `
279825
+ ${import_picocolors2.default.dim(`\u21B3 baseline: ${env} ${ver}, pushed ${when}${by}`)}`;
279826
+ }
279827
+ return line;
279933
279828
  }
279934
279829
  async function pushSchema(schema, args) {
279935
279830
  const schemaJson = JSON.parse((0, import_schema2.serializeSchema)(schema));
@@ -280010,7 +279905,7 @@ async function loadSchema(schemaPath, exportName) {
280010
279905
  const abs = (0, import_path3.resolve)(process.cwd(), schemaPath);
280011
279906
  if (!(0, import_fs4.existsSync)(abs)) {
280012
279907
  throw new AbloValidationError(
280013
- `schema not found at ${import_picocolors3.default.bold(schemaPath)}. Run ${import_picocolors3.default.bold("npx ablo init")} or pass ${import_picocolors3.default.bold("--schema <path>")}.`,
279908
+ `schema not found at ${import_picocolors2.default.bold(schemaPath)}. Run ${import_picocolors2.default.bold("npx ablo init")} or pass ${import_picocolors2.default.bold("--schema <path>")}.`,
280014
279909
  { code: "cli_invalid_arguments" }
280015
279910
  );
280016
279911
  }
@@ -280021,25 +279916,50 @@ async function loadSchema(schemaPath, exportName) {
280021
279916
  const schema = mod[exportName] ?? nested?.[exportName];
280022
279917
  if (!schema || typeof schema !== "object" || !("models" in schema)) {
280023
279918
  throw new AbloValidationError(
280024
- `${import_picocolors3.default.bold(schemaPath)} has no \`${exportName}\` export that looks like a Schema. Did you \`export const ${exportName} = defineSchema({ ... })\`?`,
279919
+ `${import_picocolors2.default.bold(schemaPath)} has no \`${exportName}\` export that looks like a Schema. Did you \`export const ${exportName} = defineSchema({ ... })\`?`,
280025
279920
  { code: "cli_invalid_arguments" }
280026
279921
  );
280027
279922
  }
280028
279923
  return schema;
280029
279924
  }
279925
+ function maskKey(key) {
279926
+ return key ? `${key.slice(0, 12)}\u2026` : "(none)";
279927
+ }
279928
+ function describeKeySource(source) {
279929
+ switch (source) {
279930
+ case "env":
279931
+ return "ABLO_API_KEY (environment)";
279932
+ case ".env.local":
279933
+ return ".env.local";
279934
+ case ".env":
279935
+ return ".env";
279936
+ case "login":
279937
+ return "`ablo login` (stored sandbox config)";
279938
+ }
279939
+ }
280030
279940
  async function push(argv) {
280031
279941
  let args;
280032
279942
  try {
280033
279943
  args = parsePushArgs(argv);
280034
279944
  } catch (err) {
280035
- console.error(import_picocolors3.default.red(` ${err instanceof Error ? err.message : String(err)}`));
279945
+ console.error(import_picocolors2.default.red(` ${err instanceof Error ? err.message : String(err)}`));
280036
279946
  process.exit(1);
280037
279947
  }
280038
- if (!args.apiKey) args.apiKey = resolveApiKey();
279948
+ let keySource = "env";
279949
+ if (!args.apiKey) {
279950
+ const fromProject = readProjectApiKey();
279951
+ if (fromProject) {
279952
+ args.apiKey = fromProject.key;
279953
+ keySource = fromProject.source;
279954
+ } else {
279955
+ args.apiKey = resolveApiKey();
279956
+ keySource = "login";
279957
+ }
279958
+ }
280039
279959
  if (!args.apiKey) {
280040
279960
  console.error(
280041
- import_picocolors3.default.red(` No API key.`) + import_picocolors3.default.dim(
280042
- ` Run ${import_picocolors3.default.bold("npx ablo login")} for the sandbox dev loop \u2014 or set ${import_picocolors3.default.bold("ABLO_API_KEY")} (${import_picocolors3.default.bold("sk_test_")} = sandbox; ${import_picocolors3.default.bold("sk_live_")} = deliberate production deploy). Mode is currently '${getMode()}'.`
279961
+ import_picocolors2.default.red(` No API key.`) + import_picocolors2.default.dim(
279962
+ ` Run ${import_picocolors2.default.bold("npx ablo login")} for the sandbox dev loop \u2014 or set ${import_picocolors2.default.bold("ABLO_API_KEY")} (${import_picocolors2.default.bold("sk_test_")} = sandbox; ${import_picocolors2.default.bold("sk_live_")} = deliberate production deploy). Mode is currently '${getMode()}'.`
280043
279963
  )
280044
279964
  );
280045
279965
  process.exit(1);
@@ -280047,17 +279967,17 @@ async function push(argv) {
280047
279967
  const schema = await loadSchema(args.schemaPath, args.exportName);
280048
279968
  const hash = (0, import_schema2.schemaHash)(schema);
280049
279969
  console.log(
280050
- ` Pushing ${import_picocolors3.default.bold(args.schemaPath)} ${import_picocolors3.default.dim(`(${Object.keys(schema.models).length} models, hash ${hash})`)} \u2192 ${import_picocolors3.default.dim(args.url)}`
279970
+ ` Pushing ${import_picocolors2.default.bold(args.schemaPath)} ${import_picocolors2.default.dim(`(${Object.keys(schema.models).length} models, hash ${hash})`)} \u2192 ${import_picocolors2.default.dim(args.url)}`
280051
279971
  );
280052
279972
  const { ok: resOk, status: status2, body, bodyText } = await pushSchema(schema, args);
280053
279973
  if (resOk) {
280054
279974
  if (body.unchanged) {
280055
- console.log(` ${import_picocolors3.default.dim("\u25CB")} No changes \u2014 schema already active (v${body.version}).`);
279975
+ console.log(` ${import_picocolors2.default.dim("\u25CB")} No changes \u2014 schema already active (v${body.version}).`);
280056
279976
  } else {
280057
- console.log(` ${import_picocolors3.default.green("\u2713")} Activated ${import_picocolors3.default.bold(`v${body.version}`)} ${import_picocolors3.default.dim(`(hash ${body.hash})`)}`);
279977
+ console.log(` ${import_picocolors2.default.green("\u2713")} Activated ${import_picocolors2.default.bold(`v${body.version}`)} ${import_picocolors2.default.dim(`(hash ${body.hash})`)}`);
280058
279978
  if (Array.isArray(body.warnings) && body.warnings.length > 0) {
280059
- console.log(import_picocolors3.default.yellow(` Applied ${body.warnings.length} destructive change(s):`));
280060
- for (const w2 of body.warnings) console.log(import_picocolors3.default.yellow(fmtSignal(w2)));
279979
+ console.log(import_picocolors2.default.yellow(` Applied ${body.warnings.length} destructive change(s):`));
279980
+ for (const w2 of body.warnings) console.log(import_picocolors2.default.yellow(fmtSignal(w2)));
280061
279981
  }
280062
279982
  }
280063
279983
  return;
@@ -280065,27 +279985,63 @@ async function push(argv) {
280065
279985
  if (status2 === 409) {
280066
279986
  const unexecutable = Array.isArray(body.unexecutable) ? body.unexecutable : [];
280067
279987
  const warnings = Array.isArray(body.warnings) ? body.warnings : [];
280068
- console.error(import_picocolors3.default.red(" Incompatible change \u2014 this push is not safe to apply as-is."));
279988
+ console.error(import_picocolors2.default.red(" Incompatible change \u2014 this push is not safe to apply as-is."));
280069
279989
  if (unexecutable.length > 0) {
280070
- console.error(import_picocolors3.default.red(` Unexecutable (would fail on existing rows):`));
280071
- for (const u2 of unexecutable) console.error(import_picocolors3.default.red(fmtSignal(u2)));
279990
+ console.error(import_picocolors2.default.red(` Unexecutable (would fail on existing rows):`));
279991
+ for (const u2 of unexecutable) console.error(import_picocolors2.default.red(fmtSignal(u2)));
280072
279992
  }
280073
279993
  if (warnings.length > 0) {
280074
- console.error(import_picocolors3.default.yellow(` Destructive (data loss):`));
280075
- for (const w2 of warnings) console.error(import_picocolors3.default.yellow(fmtSignal(w2)));
279994
+ console.error(import_picocolors2.default.yellow(` Destructive (data loss):`));
279995
+ for (const w2 of warnings) console.error(import_picocolors2.default.yellow(fmtSignal(w2)));
279996
+ }
279997
+ const hasShadowed = [...unexecutable, ...warnings].some(
279998
+ (s) => s.shadowed != null
279999
+ );
280000
+ if (hasShadowed) {
280001
+ console.error(
280002
+ import_picocolors2.default.dim(
280003
+ " These models exist in the baseline above but not in your push. Sandbox readers fall"
280004
+ )
280005
+ );
280006
+ console.error(
280007
+ import_picocolors2.default.dim(
280008
+ " back to the production schema until you push your own, so applying this drops them."
280009
+ )
280010
+ );
280076
280011
  }
280077
- console.error(import_picocolors3.default.dim(` Re-push with ${import_picocolors3.default.bold("--force")} to override, or use ${import_picocolors3.default.bold("--rename old:new")} if you renamed a model.`));
280012
+ console.error(import_picocolors2.default.dim(` Re-push with ${import_picocolors2.default.bold("--force")} to override, or use ${import_picocolors2.default.bold("--rename old:new")} if you renamed a model.`));
280078
280013
  } else if (status2 === 403) {
280079
- console.error(import_picocolors3.default.red(` Forbidden: ${body.message ?? body.reason ?? "key lacks schema:push scope"}`));
280080
- if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
280014
+ const code = body.code ?? body.reason;
280015
+ const serverMsg = body.message ?? body.reason;
280016
+ console.error(import_picocolors2.default.red(` Forbidden${code ? ` [${code}]` : ""}: ${serverMsg ?? "permission denied"}`));
280017
+ console.error(import_picocolors2.default.dim(` Push used ${import_picocolors2.default.bold(maskKey(args.apiKey))} from ${describeKeySource(keySource)}.`));
280018
+ if (code === "database_role_cannot_enforce_rls") {
280019
+ console.error(
280020
+ import_picocolors2.default.dim(
280021
+ ` Your database role bypasses row-level security. Run ${import_picocolors2.default.bold("npx ablo migrate")} to create a scoped (NOBYPASSRLS) role and repoint DATABASE_URL, then re-push.`
280022
+ )
280023
+ );
280024
+ } else if (code === "database_tables_unforced_rls") {
280025
+ console.error(
280026
+ import_picocolors2.default.dim(
280027
+ ` One or more synced tables don't have FORCE ROW LEVEL SECURITY. Run ${import_picocolors2.default.bold("npx ablo migrate")} to (re)apply the tenant policies, then re-push.`
280028
+ )
280029
+ );
280030
+ } else if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
280031
+ console.error(
280032
+ import_picocolors2.default.dim(
280033
+ ` Schema pushes need a SECRET key: ${import_picocolors2.default.bold("sk_test_")} (sandbox dev loop) or a dashboard ${import_picocolors2.default.bold("sk_live_")} (production deploy: ${import_picocolors2.default.bold("ABLO_API_KEY=sk_live_\u2026 npx ablo push")}).`
280034
+ )
280035
+ );
280036
+ } else {
280081
280037
  console.error(
280082
- import_picocolors3.default.dim(
280083
- ` Schema pushes need a SECRET key: ${import_picocolors3.default.bold("sk_test_")} (sandbox dev loop) or a dashboard ${import_picocolors3.default.bold("sk_live_")} (production deploy: ${import_picocolors3.default.bold("ABLO_API_KEY=sk_live_\u2026 npx ablo push")}).`
280038
+ import_picocolors2.default.dim(
280039
+ ` This key isn't authorized to push schema (needs ${import_picocolors2.default.bold("schema:push")}). ` + (keySource === "login" ? `It's your stored ${import_picocolors2.default.bold("ablo login")} sandbox key \u2014 a key in ${import_picocolors2.default.bold(".env.local")} or ${import_picocolors2.default.bold("ABLO_API_KEY")} takes precedence, so put a schema:push key there (sandbox ${import_picocolors2.default.bold("sk_test_")} or production ${import_picocolors2.default.bold("sk_live_")}) and re-push. ` : `Use a schema:push key \u2014 a sandbox ${import_picocolors2.default.bold("sk_test_")} or production ${import_picocolors2.default.bold("sk_live_")}. `) + `Manage keys at https://abloatai.com`
280084
280040
  )
280085
280041
  );
280086
280042
  }
280087
280043
  } else {
280088
- console.error(import_picocolors3.default.red(` Push failed (${status2}): ${body.message ?? body.reason ?? bodyText}`));
280044
+ console.error(import_picocolors2.default.red(` Push failed (${status2}): ${body.message ?? body.reason ?? bodyText}`));
280089
280045
  }
280090
280046
  process.exit(1);
280091
280047
  }
@@ -280093,6 +280049,10 @@ async function push(argv) {
280093
280049
  // src/cli/migrate.ts
280094
280050
  var MIGRATE_USAGE = ` ablo migrate \u2014 provision your schema's tables in your own Postgres (DATABASE_URL)
280095
280051
 
280052
+ To connect a real database, run \`ablo connect\` \u2014 Ablo reads your WAL via logical
280053
+ replication, the one way. \`migrate\` is the optional escape hatch for provisioning
280054
+ tables when you don't already have them.
280055
+
280096
280056
  Usage:
280097
280057
  npx ablo migrate Create the synced-model tables (with row-level security)
280098
280058
  npx ablo migrate --dry-run Print the SQL without executing it
@@ -280141,8 +280101,8 @@ function planFor(schema, targetSchema = "public") {
280141
280101
  }
280142
280102
  var log = {
280143
280103
  info: (msg, fields) => console.log(`[migrate] ${msg}`, fields),
280144
- warn: (msg, fields) => console.warn(import_picocolors4.default.yellow(`[migrate] ${msg}`), fields),
280145
- error: (msg, fields) => console.error(import_picocolors4.default.red(`[migrate] ${msg}`), fields)
280104
+ warn: (msg, fields) => console.warn(import_picocolors3.default.yellow(`[migrate] ${msg}`), fields),
280105
+ error: (msg, fields) => console.error(import_picocolors3.default.red(`[migrate] ${msg}`), fields)
280146
280106
  };
280147
280107
  var PG_LOCK_NOT_AVAILABLE = "55P03";
280148
280108
  var LOCK_TIMEOUT = process.env.ABLO_SCHEMA_LOCK_TIMEOUT ?? process.env.ABLO_DDL_LOCK_TIMEOUT ?? "5s";
@@ -280217,7 +280177,7 @@ async function migrate(argv) {
280217
280177
  try {
280218
280178
  args = parseMigrateArgs(argv);
280219
280179
  } catch (err) {
280220
- console.error(import_picocolors4.default.red(` ${err instanceof Error ? err.message : String(err)}`));
280180
+ console.error(import_picocolors3.default.red(` ${err instanceof Error ? err.message : String(err)}`));
280221
280181
  process.exit(1);
280222
280182
  }
280223
280183
  const schema = await loadSchema(args.schemaPath, args.exportName);
@@ -280228,31 +280188,290 @@ async function migrate(argv) {
280228
280188
  ].join("\n");
280229
280189
  const totalStatements = plan.statements.length + plan.concurrent.length;
280230
280190
  console.log(
280231
- ` ${import_picocolors4.default.dim("Schema")} ${import_picocolors4.default.bold(args.schemaPath)} \u2192 ${import_picocolors4.default.dim(`${Object.keys(schema.models).length} models, ${totalStatements} statements`)}`
280191
+ ` ${import_picocolors3.default.dim("Schema")} ${import_picocolors3.default.bold(args.schemaPath)} \u2192 ${import_picocolors3.default.dim(`${Object.keys(schema.models).length} models, ${totalStatements} statements`)}`
280232
280192
  );
280233
280193
  if (args.outputFile) {
280234
280194
  (0, import_fs5.writeFileSync)(args.outputFile, sql + "\n");
280235
- console.log(` ${import_picocolors4.default.green("\u2713")} SQL written to ${import_picocolors4.default.bold(args.outputFile)}`);
280195
+ console.log(` ${import_picocolors3.default.green("\u2713")} SQL written to ${import_picocolors3.default.bold(args.outputFile)}`);
280236
280196
  return;
280237
280197
  }
280238
280198
  if (args.dryRun) {
280239
280199
  console.log("\n" + sql + "\n");
280240
280200
  return;
280241
280201
  }
280242
- const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
280202
+ const dbUrl = readProjectDatabaseUrl();
280243
280203
  if (!dbUrl) {
280244
- console.error(import_picocolors4.default.red(" Set DATABASE_URL (or ABLO_DATABASE_URL) to apply, or use --dry-run to preview."));
280204
+ console.error(
280205
+ import_picocolors3.default.red(
280206
+ " No DATABASE_URL found (checked process env, .env.local, .env). Set it to apply, or use --dry-run to preview."
280207
+ )
280208
+ );
280245
280209
  process.exit(1);
280246
280210
  }
280247
- const effectiveUrl = await ensureScopedRoleInteractive(dbUrl);
280248
280211
  try {
280249
- await applyStatements(effectiveUrl, args.targetSchema, plan.statements, plan.concurrent);
280250
- console.log(` ${import_picocolors4.default.green("\u2713")} Migration complete`);
280212
+ await applyStatements(dbUrl, args.targetSchema, plan.statements, plan.concurrent);
280213
+ console.log(` ${import_picocolors3.default.green("\u2713")} Migration complete`);
280251
280214
  } catch {
280252
280215
  process.exit(1);
280253
280216
  }
280254
280217
  }
280255
280218
 
280219
+ // src/cli/connect.ts
280220
+ init_cjs_shims();
280221
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
280222
+
280223
+ // src/cli/theme.ts
280224
+ init_cjs_shims();
280225
+ var RESET = "\x1B[0m";
280226
+ var PAPER_BG = "\x1B[48;2;250;250;250m";
280227
+ var BLACK_FG = "\x1B[38;2;0;0;0m";
280228
+ function colorEnabled() {
280229
+ return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
280230
+ }
280231
+ function brand(label = "ablo") {
280232
+ if (!colorEnabled()) return label;
280233
+ return `${PAPER_BG}${BLACK_FG} ${label} ${RESET}`;
280234
+ }
280235
+
280236
+ // src/cli/connect.ts
280237
+ var ABLO_PUBLICATION = "ablo_publication";
280238
+ var ABLO_REPLICATION_ROLE = "ablo_replicator";
280239
+ function parseConnectArgs(argv) {
280240
+ let check2 = false;
280241
+ let tables = [];
280242
+ let role = ABLO_REPLICATION_ROLE;
280243
+ for (let i = 0; i < argv.length; i++) {
280244
+ const arg = argv[i];
280245
+ switch (arg) {
280246
+ case "--check":
280247
+ check2 = true;
280248
+ break;
280249
+ case "--tables": {
280250
+ const value = argv[++i] ?? "";
280251
+ tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
280252
+ break;
280253
+ }
280254
+ case "--role":
280255
+ role = argv[++i] ?? role;
280256
+ break;
280257
+ default:
280258
+ throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
280259
+ }
280260
+ }
280261
+ return { check: check2, tables, role };
280262
+ }
280263
+ function quoteIdent(id) {
280264
+ return `"${id.replace(/"/g, '""')}"`;
280265
+ }
280266
+ function connectSetupSql(input) {
280267
+ const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
280268
+ const tables = input.tables ?? [];
280269
+ const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
280270
+ return [
280271
+ // 1. Turn on logical decoding. Requires a restart (it's not reloadable).
280272
+ `ALTER SYSTEM SET wal_level = 'logical';`,
280273
+ // 2. Publish the tables Ablo should read.
280274
+ `CREATE PUBLICATION ${quoteIdent(ABLO_PUBLICATION)} ${publicationTarget};`,
280275
+ // 3. A least-privilege role: it can stream replication and SELECT, nothing more.
280276
+ `CREATE ROLE ${quoteIdent(role)} WITH REPLICATION LOGIN PASSWORD '<password>';`,
280277
+ `GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${quoteIdent(role)};`,
280278
+ // Future tables get SELECT automatically, so the publication doesn't outgrow the grant.
280279
+ `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${quoteIdent(role)};`
280280
+ ];
280281
+ }
280282
+ function printConnectRecipe(args) {
280283
+ const sql = connectSetupSql({ tables: args.tables, role: args.role });
280284
+ console.log(`
280285
+ ${brand("ablo")} ${import_picocolors4.default.dim("connect")} ${import_picocolors4.default.dim("logical replication \u2014 the one way to connect a real database")}
280286
+ `);
280287
+ console.log(
280288
+ ` Ablo READS your write-ahead log (WAL) and ${import_picocolors4.default.bold("never")} runs DDL, owns, or migrates your
280289
+ schema. Your app keeps writing through your own backend \u2014 Ablo only tails the changes.
280290
+ Run this once against your Postgres ${import_picocolors4.default.dim("(as a superuser / the DB owner)")}:
280291
+ `
280292
+ );
280293
+ console.log(` ${import_picocolors4.default.bold("1.")} Enable logical decoding ${import_picocolors4.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`);
280294
+ console.log(` ${import_picocolors4.default.cyan(sql[0])}`);
280295
+ console.log(
280296
+ import_picocolors4.default.dim(
280297
+ ` On Amazon RDS / Aurora you can't ALTER SYSTEM: set ${import_picocolors4.default.bold("rds.logical_replication = 1")} in the
280298
+ instance's parameter group instead, then reboot.`
280299
+ )
280300
+ );
280301
+ console.log(`
280302
+ ${import_picocolors4.default.bold("2.")} Publish the tables Ablo should read`);
280303
+ console.log(` ${import_picocolors4.default.cyan(sql[1])}`);
280304
+ if (args.tables.length === 0) {
280305
+ console.log(import_picocolors4.default.dim(` (Scope it with ${import_picocolors4.default.bold("ablo connect --tables a,b,c")} to publish a subset.)`));
280306
+ }
280307
+ console.log(`
280308
+ ${import_picocolors4.default.bold("3.")} Create a least-privilege replication role ${import_picocolors4.default.dim("(pick your own password)")}`);
280309
+ console.log(` ${import_picocolors4.default.cyan(sql[2])}`);
280310
+ console.log(` ${import_picocolors4.default.cyan(sql[3])}`);
280311
+ console.log(` ${import_picocolors4.default.cyan(sql[4])}`);
280312
+ console.log(
280313
+ import_picocolors4.default.dim(
280314
+ ` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
280315
+ ${import_picocolors4.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
280316
+ )
280317
+ );
280318
+ console.log(
280319
+ `
280320
+ ${import_picocolors4.default.bold("4.")} Put the role's connection string in ${import_picocolors4.default.bold("DATABASE_URL")}, then verify:
280321
+ ${import_picocolors4.default.cyan("npx ablo connect --check")}
280322
+ `
280323
+ );
280324
+ console.log(
280325
+ import_picocolors4.default.dim(
280326
+ ` Reminder: Ablo never writes to your database on this path. Provisioning tables with
280327
+ ${import_picocolors4.default.bold("ablo migrate")} is a separate, optional escape hatch \u2014 connecting a real database is this.`
280328
+ )
280329
+ );
280330
+ console.log();
280331
+ }
280332
+ function printCheckItem(item) {
280333
+ if (item.ok) {
280334
+ console.log(` ${import_picocolors4.default.green("\u2713")} ${item.label}`);
280335
+ } else {
280336
+ console.log(` ${import_picocolors4.default.red("\u2717")} ${item.label}`);
280337
+ if (item.fix) {
280338
+ for (const line of item.fix.split("\n")) console.log(` ${import_picocolors4.default.red("\u2022")} ${line}`);
280339
+ }
280340
+ }
280341
+ }
280342
+ async function probeReadiness(sql, opts = {}) {
280343
+ const publication = opts.publication ?? ABLO_PUBLICATION;
280344
+ const items = [];
280345
+ const walRows = await sql.unsafe(`SHOW wal_level`);
280346
+ const walLevel = walRows[0]?.setting ?? "unknown";
280347
+ items.push(
280348
+ walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors4.default.bold("logical")}` } : {
280349
+ ok: false,
280350
+ label: `wal_level is ${import_picocolors4.default.bold(walLevel)} (need ${import_picocolors4.default.bold("logical")})`,
280351
+ fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
280352
+ On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.`
280353
+ }
280354
+ );
280355
+ const pubRows = await sql.unsafe(
280356
+ `SELECT puballtables FROM pg_publication WHERE pubname = $1`,
280357
+ [publication]
280358
+ );
280359
+ items.push(
280360
+ pubRows.length > 0 ? {
280361
+ ok: true,
280362
+ label: `publication ${import_picocolors4.default.bold(publication)} exists ${import_picocolors4.default.dim(pubRows[0].puballtables ? "(all tables)" : "(table subset)")}`
280363
+ } : {
280364
+ ok: false,
280365
+ label: `publication ${import_picocolors4.default.bold(publication)} not found`,
280366
+ fix: `CREATE PUBLICATION ${quoteIdent(publication)} FOR ALL TABLES;`
280367
+ }
280368
+ );
280369
+ const roleRows = await sql.unsafe(
280370
+ `SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
280371
+ );
280372
+ const role = roleRows[0];
280373
+ const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
280374
+ items.push(
280375
+ hasReplication ? { ok: true, label: `DATABASE_URL role can stream replication ${import_picocolors4.default.dim("(REPLICATION)")}` } : {
280376
+ ok: false,
280377
+ label: `DATABASE_URL role lacks the ${import_picocolors4.default.bold("REPLICATION")} attribute`,
280378
+ fix: `ALTER ROLE current_user WITH REPLICATION;
280379
+ On RDS: GRANT rds_replication TO <your_role>;`
280380
+ }
280381
+ );
280382
+ if (pubRows.length > 0) {
280383
+ const badRows = await sql.unsafe(
280384
+ `SELECT c.relname AS table_name, c.relreplident
280385
+ FROM pg_publication_tables pt
280386
+ JOIN pg_class c ON c.relname = pt.tablename
280387
+ JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
280388
+ WHERE pt.pubname = $1
280389
+ AND (
280390
+ c.relreplident = 'n'
280391
+ OR (
280392
+ c.relreplident = 'd'
280393
+ AND NOT EXISTS (
280394
+ SELECT 1 FROM pg_index i
280395
+ WHERE i.indrelid = c.oid AND i.indisprimary
280396
+ )
280397
+ )
280398
+ )`,
280399
+ [publication]
280400
+ );
280401
+ items.push(
280402
+ badRows.length === 0 ? { ok: true, label: `all published tables have a usable REPLICA IDENTITY` } : {
280403
+ ok: false,
280404
+ label: `${badRows.length} published table${badRows.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
280405
+ fix: badRows.map(
280406
+ (r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
280407
+ ).join("\n")
280408
+ }
280409
+ );
280410
+ }
280411
+ return items;
280412
+ }
280413
+ async function runCheck() {
280414
+ const dbUrl = readProjectDatabaseUrl();
280415
+ if (!dbUrl) {
280416
+ console.error(
280417
+ import_picocolors4.default.red(" No DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors4.default.dim(` Set it to the Postgres you want Ablo to read, then re-run ${import_picocolors4.default.bold("ablo connect --check")}.`)
280418
+ );
280419
+ process.exit(1);
280420
+ }
280421
+ console.log(`
280422
+ ${brand("ablo")} ${import_picocolors4.default.dim("connect --check")} ${import_picocolors4.default.dim("logical-replication readiness")}
280423
+ `);
280424
+ const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
280425
+ } });
280426
+ let items;
280427
+ try {
280428
+ items = await probeReadiness(sql);
280429
+ } catch (err) {
280430
+ const pg = err ?? {};
280431
+ console.error(import_picocolors4.default.red(` Couldn't read the database: ${pg.message ?? String(err)}`));
280432
+ await sql.end({ timeout: 2 });
280433
+ process.exit(1);
280434
+ }
280435
+ await sql.end({ timeout: 2 });
280436
+ for (const item of items) printCheckItem(item);
280437
+ const failures = items.filter((i) => !i.ok).length;
280438
+ console.log();
280439
+ if (failures === 0) {
280440
+ console.log(` ${import_picocolors4.default.green("\u2713")} Ready \u2014 Ablo can connect and tail this database's WAL.
280441
+ `);
280442
+ process.exit(0);
280443
+ }
280444
+ console.log(
280445
+ ` ${import_picocolors4.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors4.default.dim(`\u2014 apply the fixes above, then re-run ${import_picocolors4.default.bold("ablo connect --check")}.`)}
280446
+ `
280447
+ );
280448
+ process.exit(1);
280449
+ }
280450
+ async function connect(argv) {
280451
+ let args;
280452
+ try {
280453
+ args = parseConnectArgs(argv);
280454
+ } catch (err) {
280455
+ console.error(import_picocolors4.default.red(` ${err instanceof Error ? err.message : String(err)}`));
280456
+ process.exit(1);
280457
+ }
280458
+ if (args.check) {
280459
+ await runCheck();
280460
+ return;
280461
+ }
280462
+ printConnectRecipe(args);
280463
+ }
280464
+ var CONNECT_USAGE = ` ablo connect \u2014 connect a real database to Ablo via logical replication (the one way)
280465
+
280466
+ Ablo READS your WAL and never runs DDL, owns, or migrates your schema. Your app
280467
+ keeps writing through your own backend.
280468
+
280469
+ Usage:
280470
+ npx ablo connect Print the exact setup SQL for your Postgres
280471
+ npx ablo connect --tables a,b,c Publish only these tables (default: all tables)
280472
+ npx ablo connect --role <name> Name the replication role (default: ablo_replicator)
280473
+ npx ablo connect --check Validate DATABASE_URL is replication-ready`;
280474
+
280256
280475
  // src/cli/generate.ts
280257
280476
  init_cjs_shims();
280258
280477
  var import_fs6 = require("fs");
@@ -280313,21 +280532,6 @@ var import_picocolors6 = __toESM(require_picocolors(), 1);
280313
280532
  var import_fs7 = require("fs");
280314
280533
  var import_path5 = require("path");
280315
280534
  var import_schema5 = require("@abloatai/ablo/schema");
280316
-
280317
- // src/cli/theme.ts
280318
- init_cjs_shims();
280319
- var RESET = "\x1B[0m";
280320
- var PAPER_BG = "\x1B[48;2;250;250;250m";
280321
- var BLACK_FG = "\x1B[38;2;0;0;0m";
280322
- function colorEnabled() {
280323
- return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
280324
- }
280325
- function brand(label = "ablo") {
280326
- if (!colorEnabled()) return label;
280327
- return `${PAPER_BG}${BLACK_FG} ${label} ${RESET}`;
280328
- }
280329
-
280330
- // src/cli/dev.ts
280331
280535
  function parseDevArgs(argv) {
280332
280536
  let schemaPath = DEFAULT_SCHEMA_PATH;
280333
280537
  let exportName = DEFAULT_EXPORT;
@@ -280437,11 +280641,27 @@ async function runPush(schema, args) {
280437
280641
  if (status2 === 409) {
280438
280642
  const unexecutable = Array.isArray(body.unexecutable) ? body.unexecutable : [];
280439
280643
  const warnings = Array.isArray(body.warnings) ? body.warnings : [];
280644
+ const hasShadowed = [...unexecutable, ...warnings].some(
280645
+ (s) => s.shadowed != null
280646
+ );
280440
280647
  const lines = [
280441
- "Incompatible schema change \u2014 not safe to apply as-is.",
280648
+ import_picocolors6.default.bold("Incompatible schema change \u2014 not safe to apply as-is."),
280649
+ "",
280442
280650
  ...unexecutable.map((u2) => import_picocolors6.default.red(fmtSignal(u2))),
280443
280651
  ...warnings.map((w2) => import_picocolors6.default.yellow(fmtSignal(w2))),
280444
- import_picocolors6.default.dim(`Run ${import_picocolors6.default.bold("ablo push --force")} (or ${import_picocolors6.default.bold("--rename old:new")}) to resolve.`)
280652
+ "",
280653
+ ...hasShadowed ? [
280654
+ import_picocolors6.default.dim(
280655
+ " These models exist in the baseline above but not in your push. Sandbox readers"
280656
+ ),
280657
+ import_picocolors6.default.dim(
280658
+ " fall back to the production schema until you push your own, so applying this drops them."
280659
+ ),
280660
+ ""
280661
+ ] : [],
280662
+ import_picocolors6.default.dim(
280663
+ ` Fix: ${import_picocolors6.default.bold("ablo push --force")} to apply anyway, or ${import_picocolors6.default.bold("--rename old:new")} if you renamed a model.`
280664
+ )
280445
280665
  ];
280446
280666
  return { ok: false, message: lines.join("\n") };
280447
280667
  }
@@ -280473,8 +280693,6 @@ async function dev(argv) {
280473
280693
  console.log(`
280474
280694
  ${brand("ablo")} ${import_picocolors6.default.dim("push")} ${import_picocolors6.default.dim("(sandbox)")}
280475
280695
  `);
280476
- const projectDbUrl = readProjectDatabaseUrl();
280477
- if (projectDbUrl) await ensureScopedRoleInteractive(projectDbUrl);
280478
280696
  const schema = await loadSchema(args.schemaPath, args.exportName);
280479
280697
  const modelCount = Object.keys(schema.models).length;
280480
280698
  console.log(
@@ -282350,11 +282568,89 @@ async function drizzlePull(argv) {
282350
282568
  );
282351
282569
  }
282352
282570
 
282571
+ // src/cli/renderError.ts
282572
+ init_cjs_shims();
282573
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
282574
+ var RECOVERY_HINT = {
282575
+ transient: "This looks transient \u2014 retry in a moment.",
282576
+ permission: "Your key isn't allowed to do this \u2014 check its scopes or role.",
282577
+ session_expiry: "Your session expired \u2014 sign in again.",
282578
+ access_credential_expiry: "Your access credential expired \u2014 refresh it and retry.",
282579
+ auth_blocked: "Authentication was blocked."
282580
+ };
282581
+ function titleForType(type) {
282582
+ const core = type.replace(/^Ablo/, "").replace(/Error$/, "");
282583
+ const spaced = core.replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
282584
+ if (!spaced) return "Error";
282585
+ return /error$/i.test(spaced) ? spaced : `${spaced} error`;
282586
+ }
282587
+ function isStringArray(v) {
282588
+ return Array.isArray(v) && v.every((x2) => typeof x2 === "string");
282589
+ }
282590
+ function renderKnownDetails(details, line) {
282591
+ if (!details) return;
282592
+ const { retryAfterSeconds, missingIds, requiredCapability, unexecutable, errors } = details;
282593
+ if (typeof retryAfterSeconds === "number") line(` ${import_picocolors18.default.dim("retry")} after ${retryAfterSeconds}s`);
282594
+ if (isStringArray(missingIds) && missingIds.length > 0) {
282595
+ const shown = missingIds.slice(0, 5).join(", ");
282596
+ const more = missingIds.length > 5 ? ` (+${missingIds.length - 5} more)` : "";
282597
+ line(` ${import_picocolors18.default.dim("missing")} ${shown}${more}`);
282598
+ }
282599
+ if (typeof requiredCapability === "string") line(` ${import_picocolors18.default.dim("needs")} ${requiredCapability}`);
282600
+ if (Array.isArray(unexecutable) && unexecutable.length > 0) {
282601
+ line(` ${import_picocolors18.default.dim("blocked")} ${unexecutable.length} change(s) can't be applied \u2014 see \`unexecutable\` (--verbose)`);
282602
+ }
282603
+ if (Array.isArray(errors)) {
282604
+ for (const e2 of errors.slice(0, 8)) {
282605
+ if (e2 && typeof e2 === "object") {
282606
+ const rec = e2;
282607
+ const where = typeof rec.param === "string" ? `${rec.param}: ` : "";
282608
+ const msg = typeof rec.message === "string" ? rec.message : "";
282609
+ if (msg) line(` ${import_picocolors18.default.dim("\xB7")} ${where}${msg}`);
282610
+ }
282611
+ }
282612
+ }
282613
+ }
282614
+ function renderCliError(err, opts = {}) {
282615
+ const line = opts.write ?? ((l2) => console.error(l2));
282616
+ const verbose = opts.verbose ?? (process.argv.includes("--verbose") || process.env.ABLO_VERBOSE === "1");
282617
+ if (err instanceof AbloError) {
282618
+ const codeTag = err.code ? ` ${import_picocolors18.default.dim(`[${err.code}]`)}` : "";
282619
+ line("");
282620
+ line(` ${brand("ablo")} ${import_picocolors18.default.red("\u2717")} ${import_picocolors18.default.bold(titleForType(err.type))}${codeTag}`);
282621
+ line("");
282622
+ line(` ${err.message}`);
282623
+ if (err.param) line(` ${import_picocolors18.default.dim("field")} ${err.param}`);
282624
+ renderKnownDetails(err.details, line);
282625
+ const hint = err.code ? RECOVERY_HINT[classifyRecovery(err.code)] : void 0;
282626
+ if (hint) line(` ${import_picocolors18.default.dim(hint)}`);
282627
+ if (err.docUrl) line(` ${import_picocolors18.default.dim("docs")} ${err.docUrl}`);
282628
+ if (err.requestId) line(` ${import_picocolors18.default.dim("ref")} ${err.requestId}`);
282629
+ if (verbose) {
282630
+ if (err.details && Object.keys(err.details).length > 0) {
282631
+ line(` ${import_picocolors18.default.dim("details")} ${JSON.stringify(err.details)}`);
282632
+ }
282633
+ if (err.stack) line(import_picocolors18.default.dim(err.stack));
282634
+ }
282635
+ line("");
282636
+ process.exitCode = 1;
282637
+ return;
282638
+ }
282639
+ const message = err instanceof Error ? err.message : String(err);
282640
+ line("");
282641
+ line(` ${brand("ablo")} ${import_picocolors18.default.red("\u2717")} ${message}`);
282642
+ if (verbose && err instanceof Error && err.stack) line(import_picocolors18.default.dim(err.stack));
282643
+ else line(` ${import_picocolors18.default.dim("Run with --verbose for the full error.")}`);
282644
+ line("");
282645
+ process.exitCode = 1;
282646
+ }
282647
+
282353
282648
  // src/cli/index.ts
282354
282649
  var LOGO = `
282355
- ${brand("ablo")} ${import_picocolors18.default.dim("sync engine")}
282650
+ ${brand("ablo")} ${import_picocolors19.default.dim("sync engine")}
282356
282651
  `;
282357
282652
  var SUBCOMMAND_USAGE = {
282653
+ connect: CONNECT_USAGE,
282358
282654
  migrate: MIGRATE_USAGE
282359
282655
  };
282360
282656
  async function main() {
@@ -282386,7 +282682,7 @@ async function main() {
282386
282682
  const devArgs = process.argv.slice(3);
282387
282683
  const oneShot = devArgs.includes("--no-watch");
282388
282684
  console.log(
282389
- import_picocolors18.default.dim(
282685
+ import_picocolors19.default.dim(
282390
282686
  oneShot ? " `ablo dev --no-watch` is `ablo push` (push once, no watcher) \u2014 running that." : " `ablo dev` is now `ablo push --watch` \u2014 running that."
282391
282687
  )
282392
282688
  );
@@ -282402,6 +282698,8 @@ async function main() {
282402
282698
  } else {
282403
282699
  await pull(rest);
282404
282700
  }
282701
+ } else if (command === "connect") {
282702
+ await connect(process.argv.slice(3));
282405
282703
  } else if (command === "migrate") {
282406
282704
  await migrate(process.argv.slice(3));
282407
282705
  } else if (command === "push") {
@@ -282411,14 +282709,14 @@ async function main() {
282411
282709
  const guard = guardActiveProjectKey();
282412
282710
  if (!guard.ok && guard.available.length > 0 && !rest.includes("--url")) {
282413
282711
  console.error(
282414
- ` ${import_picocolors18.default.yellow("\u26A0")} active project ${import_picocolors18.default.bold(guard.activeProfile)} has no stored key ${import_picocolors18.default.dim(
282712
+ ` ${import_picocolors19.default.yellow("\u26A0")} active project ${import_picocolors19.default.bold(guard.activeProfile)} has no stored key ${import_picocolors19.default.dim(
282415
282713
  `(you have keys for: ${guard.available.join(", ")})`
282416
282714
  )}`
282417
282715
  );
282418
282716
  const loginCmd = guard.activeProfile === "default" ? "ablo login" : `ablo login --project ${guard.activeProfile}`;
282419
282717
  console.error(
282420
- import_picocolors18.default.dim(
282421
- ` Mint one with ${import_picocolors18.default.bold(loginCmd)}, or switch with ${import_picocolors18.default.bold("ablo projects use <slug>")}.`
282718
+ import_picocolors19.default.dim(
282719
+ ` Mint one with ${import_picocolors19.default.bold(loginCmd)}, or switch with ${import_picocolors19.default.bold("ablo projects use <slug>")}.`
282422
282720
  )
282423
282721
  );
282424
282722
  process.exitCode = 1;
@@ -282436,13 +282734,13 @@ async function main() {
282436
282734
  await generate(process.argv.slice(3));
282437
282735
  } else if (command === "schema") {
282438
282736
  console.error(
282439
- ` ${import_picocolors18.default.red("\u2717")} \`ablo schema push\` was renamed to \`${brand("ablo push")}\`.`
282737
+ ` ${import_picocolors19.default.red("\u2717")} \`ablo schema push\` was renamed to \`${brand("ablo push")}\`.`
282440
282738
  );
282441
282739
  console.error(` Run \`ablo push${process.argv.slice(4).join(" ") ? " " + process.argv.slice(4).join(" ") : ""}\` instead.`);
282442
282740
  process.exitCode = 1;
282443
282741
  } else {
282444
282742
  console.log(LOGO);
282445
- console.log(` ${import_picocolors18.default.bold("Usage:")}`);
282743
+ console.log(` ${import_picocolors19.default.bold("Usage:")}`);
282446
282744
  console.log(` npx ablo init Scaffold ablo/ directory + starter schema`);
282447
282745
  console.log(` npx ablo init --yes [--framework nextjs] Non-interactive (agents/CI): no prompts, flag-driven`);
282448
282746
  console.log(` [--auth apikey] [--storage direct|endpoint] [--project <slug>] [--no-project]`);
@@ -282465,7 +282763,9 @@ async function main() {
282465
282763
  console.log(` npx ablo pull prisma [path] Generate schema.ts from a Prisma schema (keeps enums + relations)`);
282466
282764
  console.log(` npx ablo pull drizzle <module> Generate schema.ts from a Drizzle schema (keeps enums + relations)`);
282467
282765
  console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
282468
- console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (DATABASE_URL)`);
282766
+ console.log(` npx ablo connect Connect a real database \u2014 prints the logical-replication setup SQL (the one way)`);
282767
+ console.log(` npx ablo connect --check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
282768
+ console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (optional escape hatch \u2014 \`connect\` is the way)`);
282469
282769
  console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
282470
282770
  console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
282471
282771
  console.log(` npx ablo push --force Allow destructive/unexecutable changes`);
@@ -282475,10 +282775,10 @@ async function main() {
282475
282775
  console.log(` npx ablo generate Emit TypeScript types from your schema`);
282476
282776
  console.log(` npx ablo generate --out path.ts Write generated types to a path`);
282477
282777
  console.log();
282478
- console.log(` ${import_picocolors18.default.bold("Schema workflow:")}`);
282778
+ console.log(` ${import_picocolors19.default.bold("Schema workflow:")}`);
282479
282779
  console.log(` The server holds its own copy of your schema \u2014 edit ${brand("ablo/schema.ts")}, then`);
282480
282780
  console.log(` run ${brand("ablo push")} (or keep ${brand("ablo dev")} running) before the server will accept`);
282481
- console.log(` writes to new or changed models. Skip it and writes fail with ${import_picocolors18.default.yellow("server_execute_unknown_model")}.`);
282781
+ console.log(` writes to new or changed models. Skip it and writes fail with ${import_picocolors19.default.yellow("server_execute_unknown_model")}.`);
282482
282782
  console.log();
282483
282783
  }
282484
282784
  }
@@ -282529,7 +282829,7 @@ async function ensureInitProject(opts) {
282529
282829
  const ensured = await ensureProject(slug);
282530
282830
  if (ensured) {
282531
282831
  console.log(
282532
- ` ${import_picocolors18.default.green("\u2713")} ${ensured.created ? "Created" : "Using"} project ${import_picocolors18.default.bold(ensured.slug)} ${import_picocolors18.default.dim(`(${ensured.id})`)} \u2014 keys you mint for it are isolated from the org's other apps.`
282832
+ ` ${import_picocolors19.default.green("\u2713")} ${ensured.created ? "Created" : "Using"} project ${import_picocolors19.default.bold(ensured.slug)} ${import_picocolors19.default.dim(`(${ensured.id})`)} \u2014 keys you mint for it are isolated from the org's other apps.`
282533
282833
  );
282534
282834
  }
282535
282835
  }
@@ -282571,7 +282871,7 @@ async function chooseBool(flagValue, fallback, interactive, prompt) {
282571
282871
  async function init(args = []) {
282572
282872
  const opts = parseInitArgs(args);
282573
282873
  const interactive = Boolean(process.stdin.isTTY) && !opts.yes && !process.env.CI;
282574
- Ie(`${brand("ablo")} ${import_picocolors18.default.dim("sync engine")}`);
282874
+ Ie(`${brand("ablo")} ${import_picocolors19.default.dim("sync engine")}`);
282575
282875
  if (!(0, import_fs12.existsSync)("package.json")) {
282576
282876
  xe("No package.json found. Run this from your project root.");
282577
282877
  process.exit(1);
@@ -282656,7 +282956,7 @@ async function init(args = []) {
282656
282956
  if (pullExisting) {
282657
282957
  const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
282658
282958
  if (!dbUrl) {
282659
- schemaNote = import_picocolors18.default.dim(" (no DATABASE_URL \u2014 wrote starter; run `ablo pull` later)");
282959
+ schemaNote = import_picocolors19.default.dim(" (no DATABASE_URL \u2014 wrote starter; run `ablo pull` later)");
282660
282960
  } else {
282661
282961
  try {
282662
282962
  const pulled = await buildSchemaSourceFromDb({
@@ -282666,12 +282966,12 @@ async function init(args = []) {
282666
282966
  });
282667
282967
  if (pulled.models.length > 0) {
282668
282968
  schemaSource = pulled.source;
282669
- schemaNote = import_picocolors18.default.dim(` (pulled ${pulled.models.length} models)`);
282969
+ schemaNote = import_picocolors19.default.dim(` (pulled ${pulled.models.length} models)`);
282670
282970
  } else {
282671
- schemaNote = import_picocolors18.default.dim(" (no adoptable tables \u2014 wrote starter)");
282971
+ schemaNote = import_picocolors19.default.dim(" (no adoptable tables \u2014 wrote starter)");
282672
282972
  }
282673
282973
  } catch {
282674
- schemaNote = import_picocolors18.default.dim(" (pull failed \u2014 wrote starter)");
282974
+ schemaNote = import_picocolors19.default.dim(" (pull failed \u2014 wrote starter)");
282675
282975
  }
282676
282976
  }
282677
282977
  }
@@ -282697,14 +282997,14 @@ async function init(args = []) {
282697
282997
  const existing = (0, import_fs12.readFileSync)(envFile, "utf-8");
282698
282998
  if (!existing.includes("ABLO_")) {
282699
282999
  (0, import_fs12.writeFileSync)(envFile, existing + "\n" + envBody);
282700
- created.push(`${envFile} ${import_picocolors18.default.dim("(appended)")}`);
283000
+ created.push(`${envFile} ${import_picocolors19.default.dim("(appended)")}`);
282701
283001
  } else {
282702
- created.push(`${envFile} ${import_picocolors18.default.dim("(already configured)")}`);
283002
+ created.push(`${envFile} ${import_picocolors19.default.dim("(already configured)")}`);
282703
283003
  }
282704
283004
  }
282705
283005
  if (wireRealKey && resolvedKey) {
282706
283006
  wireEnvLocal(resolvedKey);
282707
- created.push(`.env.local ${import_picocolors18.default.dim("(ABLO_API_KEY set from your login)")}`);
283007
+ created.push(`.env.local ${import_picocolors19.default.dim("(ABLO_API_KEY set from your login)")}`);
282708
283008
  }
282709
283009
  if (agent) {
282710
283010
  (0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "agent.ts"), generateAgent());
@@ -282719,17 +283019,17 @@ async function init(args = []) {
282719
283019
  }
282720
283020
  const providersPath = (0, import_path7.join)(layout.appBase, "providers.tsx");
282721
283021
  (0, import_fs12.writeFileSync)(providersPath, generateProviders());
282722
- created.push(`${providersPath} ${import_picocolors18.default.dim(`(wrap ${(0, import_path7.join)(layout.appBase, "layout.tsx")} in <Providers>)`)}`);
283022
+ created.push(`${providersPath} ${import_picocolors19.default.dim(`(wrap ${(0, import_path7.join)(layout.appBase, "layout.tsx")} in <Providers>)`)}`);
282723
283023
  const sessionDir = (0, import_path7.join)(layout.appBase, "api", "ablo-session");
282724
283024
  (0, import_fs12.mkdirSync)(sessionDir, { recursive: true });
282725
283025
  (0, import_fs12.writeFileSync)((0, import_path7.join)(sessionDir, "route.ts"), generateSessionRoute());
282726
- created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${import_picocolors18.default.dim("(wire your auth)")}`);
283026
+ created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${import_picocolors19.default.dim("(wire your auth)")}`);
282727
283027
  }
282728
283028
  if (framework !== "vanilla") {
282729
283029
  (0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "TaskList.tsx"), generateComponent());
282730
283030
  created.push(`${abloDir}/TaskList.tsx`);
282731
283031
  }
282732
- Me(created.map((f) => `${import_picocolors18.default.green("\u2713")} ${f}`).join("\n"), "Created");
283032
+ Me(created.map((f) => `${import_picocolors19.default.green("\u2713")} ${f}`).join("\n"), "Created");
282733
283033
  const pm = detectPackageManager();
282734
283034
  if (opts.install) {
282735
283035
  const s = Y2();
@@ -282738,45 +283038,45 @@ async function init(args = []) {
282738
283038
  (0, import_child_process2.execSync)(`${pm} add @abloatai/ablo`, { stdio: "ignore" });
282739
283039
  s.stop("Installed @abloatai/ablo");
282740
283040
  } catch {
282741
- s.stop(`${import_picocolors18.default.yellow("!")} Couldn't auto-install \u2014 run ${import_picocolors18.default.bold(`${pm} install @abloatai/ablo`)}`);
283041
+ s.stop(`${import_picocolors19.default.yellow("!")} Couldn't auto-install \u2014 run ${import_picocolors19.default.bold(`${pm} install @abloatai/ablo`)}`);
282742
283042
  }
282743
283043
  }
282744
283044
  const steps = [
282745
- `Get a ${import_picocolors18.default.bold("sk_test_")} key at ${import_picocolors18.default.cyan("https://abloatai.com")}`,
282746
- `Run ${import_picocolors18.default.bold("npx ablo login")} (or add ${import_picocolors18.default.bold("ABLO_API_KEY")} to ${import_picocolors18.default.bold(envFile)})`,
282747
- `Set ${import_picocolors18.default.bold("DATABASE_URL")} in ${import_picocolors18.default.bold(envFile)} \u2014 your Postgres is the system of record; rows live there, never with Ablo`,
282748
- `Run ${import_picocolors18.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
283045
+ `Get a ${import_picocolors19.default.bold("sk_test_")} key at ${import_picocolors19.default.cyan("https://abloatai.com")}`,
283046
+ `Run ${import_picocolors19.default.bold("npx ablo login")} (or add ${import_picocolors19.default.bold("ABLO_API_KEY")} to ${import_picocolors19.default.bold(envFile)})`,
283047
+ `Set ${import_picocolors19.default.bold("DATABASE_URL")} in ${import_picocolors19.default.bold(envFile)} \u2014 your Postgres is the system of record; rows live there, never with Ablo`,
283048
+ `Run ${import_picocolors19.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
282749
283049
  ...storage === "direct" ? [
282750
- `Provision your DB: ${import_picocolors18.default.bold("npx ablo migrate")} (creates your synced-model tables with row-level security; keep your own migrations for everything else)`
283050
+ `Provision your DB: ${import_picocolors19.default.bold("npx ablo migrate")} (creates your synced-model tables with row-level security; keep your own migrations for everything else)`
282751
283051
  ] : [
282752
- `Provision your DB: ${import_picocolors18.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors18.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors18.default.bold("/api/ablo/source")}`
283052
+ `Provision your DB: ${import_picocolors19.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors19.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors19.default.bold("/api/ablo/source")}`
282753
283053
  ],
282754
283054
  ...framework === "nextjs" ? [
282755
- `Wrap ${import_picocolors18.default.bold((0, import_path7.join)(layout.appBase, "layout.tsx"))} in ${import_picocolors18.default.bold("<Providers>")} (${(0, import_path7.join)(layout.appBase, "providers.tsx")}) and add your auth to ${import_picocolors18.default.bold((0, import_path7.join)(layout.appBase, "api", "ablo-session", "route.ts"))}`
283055
+ `Wrap ${import_picocolors19.default.bold((0, import_path7.join)(layout.appBase, "layout.tsx"))} in ${import_picocolors19.default.bold("<Providers>")} (${(0, import_path7.join)(layout.appBase, "providers.tsx")}) and add your auth to ${import_picocolors19.default.bold((0, import_path7.join)(layout.appBase, "api", "ablo-session", "route.ts"))}`
282756
283056
  ] : [],
282757
- `Run ${import_picocolors18.default.bold(`${pm} run dev`)} and open two browser tabs \u2014 changes sync in real-time`,
283057
+ `Run ${import_picocolors19.default.bold(`${pm} run dev`)} and open two browser tabs \u2014 changes sync in real-time`,
282758
283058
  ...agent ? [
282759
- `Run ${import_picocolors18.default.bold(`npx tsx ${abloDir}/agent.ts`)} \u2014 an AI teammate edits the same tasks`,
282760
- `Run ${import_picocolors18.default.bold("npx ablo logs")} to watch human + agent commits stream by`
283059
+ `Run ${import_picocolors19.default.bold(`npx tsx ${abloDir}/agent.ts`)} \u2014 an AI teammate edits the same tasks`,
283060
+ `Run ${import_picocolors19.default.bold("npx ablo logs")} to watch human + agent commits stream by`
282761
283061
  ] : []
282762
283062
  ];
282763
283063
  Me(steps.map((s, i) => `${i + 1}. ${s}`).join("\n"), "Next steps");
282764
283064
  const existingKey = resolveApiKey("sandbox");
282765
283065
  if (existingKey) {
282766
283066
  await ensureInitProject(opts);
282767
- Se(`Already authorized ${import_picocolors18.default.dim(`(${existingKey.slice(0, 11)}\u2026)`)} \u2014 run ${import_picocolors18.default.bold("npx ablo push")} next. ${import_picocolors18.default.dim("Docs:")} https://abloatai.com/docs`);
283067
+ Se(`Already authorized ${import_picocolors19.default.dim(`(${existingKey.slice(0, 11)}\u2026)`)} \u2014 run ${import_picocolors19.default.bold("npx ablo push")} next. ${import_picocolors19.default.dim("Docs:")} https://abloatai.com/docs`);
282768
283068
  return;
282769
283069
  }
282770
283070
  if (interactive && opts.login) {
282771
283071
  const loginNow = await ye({ message: "Log in now? (opens your browser)", initialValue: true });
282772
283072
  if (!pD(loginNow) && loginNow) {
282773
- Se(`${import_picocolors18.default.dim("Docs:")} https://abloatai.com/docs`);
283073
+ Se(`${import_picocolors19.default.dim("Docs:")} https://abloatai.com/docs`);
282774
283074
  await login();
282775
283075
  await ensureInitProject(opts);
282776
283076
  return;
282777
283077
  }
282778
283078
  }
282779
- Se(`Run ${import_picocolors18.default.bold("npx ablo login")} when ready. ${import_picocolors18.default.dim("Docs:")} https://abloatai.com/docs`);
283079
+ Se(`Run ${import_picocolors19.default.bold("npx ablo login")} when ready. ${import_picocolors19.default.dim("Docs:")} https://abloatai.com/docs`);
282780
283080
  }
282781
283081
  function generateSchema() {
282782
283082
  return `import { defineSchema, model, relation, z } from '@abloatai/ablo/schema';
@@ -283054,7 +283354,9 @@ async function main() {
283054
283354
  }
283055
283355
 
283056
283356
  main().catch((err) => {
283057
- console.error(err);
283357
+ // Ablo errors stringify to one clean line (code + message + docs link),
283358
+ // never a stack/object dump \u2014 see AbloError.toString().
283359
+ console.error(String(err));
283058
283360
  process.exit(1);
283059
283361
  });
283060
283362
  `;
@@ -283141,22 +283443,33 @@ export function Providers({ children }: { children: React.ReactNode }) {
283141
283443
  `;
283142
283444
  }
283143
283445
  function generateSessionRoute() {
283144
- return `import { sync } from '@/ablo';
283446
+ return `import { headers } from 'next/headers';
283447
+ import { sync } from '@/ablo';
283448
+ import { auth } from '@/lib/auth'; // your server-side betterAuth({ ... }) instance
283145
283449
 
283146
283450
  // Mints the browser's session token. The browser never sees your sk_ key \u2014 it
283147
- // only gets this token, which already names your org + the user you assert here.
283148
- // Replace getCurrentUser() with your real auth (NextAuth / Clerk / Better Auth / \u2026).
283451
+ // only gets this short-lived token, already scoped to your org + the user you
283452
+ // assert here. The browser fetches THIS route at a relative URL
283453
+ // ('/api/ablo-session') \u2014 that's correct + best practice (same-origin, cookies
283454
+ // flow automatically); the SDK refreshes it in the browser only.
283149
283455
  export async function POST(): Promise<Response> {
283150
- const user = await getCurrentUser(); // \u2190 your auth: who is making this request?
283456
+ const user = await getCurrentUser();
283151
283457
  if (!user) return new Response('Unauthorized', { status: 401 });
283152
283458
 
283153
283459
  const { token } = await sync.sessions.create({ user: { id: user.id } });
283154
283460
  return Response.json({ token });
283155
283461
  }
283156
283462
 
283157
- // TODO: replace with your auth provider's "current user" lookup.
283463
+ // Validate the signed-in user SERVER-SIDE. This ships wired for Better Auth;
283464
+ // \`getSession\` is the server method \u2014 pass the request headers (async in current
283465
+ // Next.js), it does NOT read cookies implicitly. Using a different provider?
283466
+ // Replace the body:
283467
+ // \u2022 NextAuth: const s = await auth(); return s?.user ? { id: s.user.id } : null;
283468
+ // \u2022 Clerk: const { userId } = await auth(); return userId ? { id: userId } : null;
283469
+ // \u2022 Supabase: const { data } = await supabase.auth.getUser(); return data.user ? { id: data.user.id } : null;
283158
283470
  async function getCurrentUser(): Promise<{ id: string } | null> {
283159
- return null;
283471
+ const session = await auth.api.getSession({ headers: await headers() });
283472
+ return session?.user ? { id: session.user.id } : null;
283160
283473
  }
283161
283474
  `;
283162
283475
  }
@@ -283166,7 +283479,10 @@ function detectPackageManager() {
283166
283479
  if ((0, import_fs12.existsSync)("bun.lockb")) return "bun";
283167
283480
  return "npm";
283168
283481
  }
283169
- main().catch(console.error);
283482
+ main().catch((err) => {
283483
+ renderCliError(err);
283484
+ process.exit(process.exitCode ?? 1);
283485
+ });
283170
283486
  /*! Bundled license information:
283171
283487
 
283172
283488
  @ts-morph/common/dist/typescript.js: