@abloatai/ablo 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +3 -3
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +1 -1
  4. package/dist/cli.cjs +1607 -1212
  5. package/dist/coordination/schema.d.ts +4 -2
  6. package/dist/coordination/schema.js +23 -9
  7. package/dist/errorCodes.d.ts +1 -0
  8. package/dist/errorCodes.js +2 -1
  9. package/dist/schema/index.d.ts +1 -1
  10. package/dist/schema/index.js +1 -1
  11. package/dist/schema/schema.d.ts +57 -0
  12. package/dist/schema/schema.js +53 -0
  13. package/dist/schema/select.js +1 -0
  14. package/dist/schema/serialize.d.ts +3 -1
  15. package/dist/schema/serialize.js +7 -1
  16. package/dist/source/adapters/drizzle.js +4 -3
  17. package/dist/source/adapters/kysely.js +4 -4
  18. package/dist/source/adapters/prisma.js +3 -3
  19. package/dist/source/idempotency.d.ts +19 -4
  20. package/dist/source/idempotency.js +19 -4
  21. package/dist/source/migrations.d.ts +4 -1
  22. package/dist/source/migrations.js +36 -5
  23. package/docs/agent-messaging.md +1 -2
  24. package/docs/api.md +4 -4
  25. package/docs/audit.md +35 -23
  26. package/docs/client-behavior.md +7 -4
  27. package/docs/concurrency-convention.md +3 -3
  28. package/docs/data-sources.md +34 -32
  29. package/docs/examples/agent-human.md +2 -2
  30. package/docs/examples/ai-sdk-tool.md +1 -1
  31. package/docs/examples/nextjs.md +1 -1
  32. package/docs/examples/server-agent.md +2 -2
  33. package/docs/groups.md +160 -0
  34. package/docs/guarantees.md +9 -7
  35. package/docs/how-it-works.md +110 -0
  36. package/docs/integration-guide.md +2 -2
  37. package/docs/interaction-model.md +3 -3
  38. package/docs/quickstart.md +6 -6
  39. package/docs/webhooks.md +23 -22
  40. package/llms.txt +10 -4
  41. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -787,8 +787,8 @@ ${import_picocolors2.default.gray(d2)} ${t}
787
787
  if (i) process.stdout.write(`${I2} ${l2}...`);
788
788
  else if (t === "timer") process.stdout.write(`${I2} ${l2} ${O2(g2)}`);
789
789
  else {
790
- const z7 = ".".repeat(Math.floor(w2)).slice(0, 3);
791
- process.stdout.write(`${I2} ${l2}${z7}`);
790
+ const z8 = ".".repeat(Math.floor(w2)).slice(0, 3);
791
+ process.stdout.write(`${I2} ${l2}${z8}`);
792
792
  }
793
793
  h2 = h2 + 1 < n.length ? h2 + 1 : 0, w2 = w2 < n.length ? w2 + 0.125 : 0;
794
794
  }, r2);
@@ -959,6 +959,7 @@ var init_errorCodes = __esm({
959
959
  entity_not_found: wire("not_found", 404, false, "No row exists with the requested id. It may have been deleted, or the id may belong to a different environment."),
960
960
  model_not_found: wire("not_found", 404, false, "No row of this model exists with the requested id. It may have been deleted, or the id may belong to a different environment."),
961
961
  mutate_update_entity_not_found: wire("not_found", 404, false, "The row targeted by this update does not exist \u2014 it may have been deleted since you read it. Re-read before retrying."),
962
+ no_data_source_registered: wire("not_found", 404, false, "No database is connected to this plane yet, so there is nothing to check. Connect one with `ablo connect apply`, then run the check again."),
962
963
  task_id_missing: wire("server", 502, true, "The task-create response arrived without a task id, so the result cannot be used. Retry the request."),
963
964
  // ── data integrity / database constraints ──────────────────────────
964
965
  // Emitted when a database integrity constraint rejects a write. None are
@@ -1122,7 +1123,7 @@ var init_errorCodes = __esm({
1122
1123
  query_invalid_boolean: wire("validation", 400, false, "The query compared a boolean column against an invalid boolean literal."),
1123
1124
  protocol_version_unsupported: wire("transport", 426, false, "The client sync-protocol version is outside the range this server supports \u2014 upgrade the SDK (or the server was rolled back mid-fleet)."),
1124
1125
  database_unreachable: wire("validation", 400, false, "Ablo could not reach this database to check that it can stream replication. The connection string may be wrong, the host may not be reachable from Ablo's servers, or the credentials may not be accepted."),
1125
- database_not_replication_ready: wire("validation", 400, false, "This database is not set up for logical replication yet. Every failing item \u2014 wal_level, the publication, the replication grant, a replica identity \u2014 is listed in the error details with its exact fix. `ablo connect` prints the one-time setup; `ablo connect --check` verifies it."),
1126
+ database_not_replication_ready: wire("validation", 400, false, "This database is not set up for logical replication yet. Every failing item \u2014 wal_level, the publication, the replication grant, a replica identity \u2014 is listed in the error details with its exact fix. `ablo connect` prints the one-time setup; `ablo connect check` verifies it."),
1126
1127
  replication_publication_drift: wire("validation", 400, false, "Your schema maps to tables that are not members of the replication publication, so their changes silently never stream and the source looks frozen. The missing tables and the exact `ALTER PUBLICATION \u2026 ADD TABLE \u2026` to add them are in the error details \u2014 Ablo never alters your database for you."),
1127
1128
  query_unknown_relation: wire("validation", 400, false, "The query references a relation the model does not define. Check the relation name against the schema."),
1128
1129
  query_relation_target_unknown: wire("schema", 500, false, "A relation in the query targets a model the schema does not define."),
@@ -1198,7 +1199,7 @@ var init_roles = __esm({
1198
1199
  });
1199
1200
 
1200
1201
  // src/coordination/schema.ts
1201
- var import_zod3, targetRangeSchema, participantKindSchema, wireParticipantKindSchema, targetRefSchema, onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema, claimStatusSchema, wireClaimBaseSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, claimLostSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema, claimHeartbeatFieldsSchema, claimHeartbeatPayloadSchema, claimHeartbeatAckPayloadSchema, claimHeartbeatBatchPayloadSchema, claimHeartbeatBatchAckPayloadSchema, updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema, commitOperationTypeSchema, commitOperationSchema, presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema;
1202
+ var import_zod3, targetRangeSchema, participantKindSchema, wireParticipantKindSchema, targetRefSchema, onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema, claimStatusSchema, grantStampFields, wireClaimBaseSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, claimLostSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema, claimHeartbeatFieldsSchema, claimHeartbeatPayloadSchema, claimHeartbeatAckPayloadSchema, claimHeartbeatBatchPayloadSchema, claimHeartbeatBatchAckPayloadSchema, updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema, commitOperationTypeSchema, commitOperationSchema, presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema;
1202
1203
  var init_schema = __esm({
1203
1204
  "src/coordination/schema.ts"() {
1204
1205
  "use strict";
@@ -1289,6 +1290,21 @@ var init_schema = __esm({
1289
1290
  "expired",
1290
1291
  "canceled"
1291
1292
  ]);
1293
+ grantStampFields = {
1294
+ /**
1295
+ * The monotonic fencing token minted for this grant (Option B). Strictly
1296
+ * increasing per entity across successive grants, so a write that carries it
1297
+ * is rejected at commit if a later holder already advanced the entity's
1298
+ * high-water. A token-less write is simply not fence-checked.
1299
+ */
1300
+ fenceToken: import_zod3.z.number().optional(),
1301
+ /**
1302
+ * Lease origin (epoch ms): when THIS holding was acquired. The cumulative-
1303
+ * hold ceiling measures a holder's fair share from here — and because it
1304
+ * survives a re-announce, a reconnect cannot rewind the clock.
1305
+ */
1306
+ acquiredAt: import_zod3.z.number().optional()
1307
+ };
1292
1308
  wireClaimBaseSchema = targetRefSchema.extend({
1293
1309
  claimId: import_zod3.z.string(),
1294
1310
  /**
@@ -1302,15 +1318,7 @@ var init_schema = __esm({
1302
1318
  /** Server-computed TTL deadline (epoch ms). Readers treat as advisory. */
1303
1319
  expiresAt: import_zod3.z.number(),
1304
1320
  status: claimStatusSchema.optional(),
1305
- /**
1306
- * The monotonic fencing token the server minted for this grant (Option B).
1307
- * Strictly increasing per entity across successive grants, so a write that
1308
- * carries it can be rejected at commit if a later holder already advanced the
1309
- * entity's high-water. Server-stamped on grant and never client-supplied;
1310
- * optional so every existing frame stays valid and a token-less write is
1311
- * simply not fence-checked.
1312
- */
1313
- fenceToken: import_zod3.z.number().optional()
1321
+ ...grantStampFields
1314
1322
  });
1315
1323
  wireClaimSummarySchema = wireClaimBaseSchema.pick({
1316
1324
  claimId: true,
@@ -3996,7 +4004,7 @@ function readProjectApiKey(cwd = process.cwd()) {
3996
4004
  }
3997
4005
  return null;
3998
4006
  }
3999
- var import_crypto2, import_fs2, import_path, REPLICATION_URL_VARS, DATABASE_URL_REMOVAL_VERSION;
4007
+ var import_crypto2, import_fs2, import_path, REPLICATION_URL_VARS;
4000
4008
  var init_dbRole = __esm({
4001
4009
  "src/cli/dbRole.ts"() {
4002
4010
  "use strict";
@@ -4006,8 +4014,7 @@ var init_dbRole = __esm({
4006
4014
  init_dist2();
4007
4015
  import_fs2 = require("fs");
4008
4016
  import_path = require("path");
4009
- REPLICATION_URL_VARS = ["ABLO_REPLICATION_DATABASE_URL", "DATABASE_URL"];
4010
- DATABASE_URL_REMOVAL_VERSION = "0.32.0";
4017
+ REPLICATION_URL_VARS = ["ABLO_REPLICATION_DATABASE_URL"];
4011
4018
  }
4012
4019
  });
4013
4020
 
@@ -4830,6 +4837,9 @@ function coerceBackfill(raw) {
4830
4837
  if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
4831
4838
  return raw;
4832
4839
  }
4840
+ function apiBaseUrl() {
4841
+ return (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
4842
+ }
4833
4843
  function fmtSignal(s) {
4834
4844
  const sig = s;
4835
4845
  const where = sig.field ? `${sig.model}.${sig.field}` : sig.model;
@@ -5234,7 +5244,7 @@ async function push(argv) {
5234
5244
  } else if (code === "schema_provisioning_forbidden") {
5235
5245
  console.error(
5236
5246
  import_picocolors4.default.dim(
5237
- ` This is not a key problem \u2014 the push was authorized, but the target database refused to let the engine create tables (Postgres 42501). On the replication read path Ablo never runs DDL: register your database as a data source with ${import_picocolors4.default.bold("npx ablo connect --register")}, and pushes to that environment record the schema as metadata only \u2014 no tables are created anywhere.`
5247
+ ` This is not a key problem \u2014 the push was authorized, but the target database refused to let the engine create tables (Postgres 42501). On the replication read path Ablo never runs DDL: register your database as a data source with ${import_picocolors4.default.bold("npx ablo connect register")}, and pushes to that environment record the schema as metadata only \u2014 no tables are created anywhere.`
5238
5248
  )
5239
5249
  );
5240
5250
  } else if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
@@ -5278,129 +5288,19 @@ var init_push = __esm({
5278
5288
  }
5279
5289
  });
5280
5290
 
5281
- // src/cli/remoteValidation.ts
5282
- function dialFailureReason(err) {
5283
- if (err === null || typeof err !== "object") return null;
5284
- const coded = err;
5285
- const code = typeof coded.code === "string" ? coded.code : null;
5286
- if (code && DIAL_FAILURE_CODES.has(code)) {
5287
- return typeof coded.message === "string" && coded.message.length > 0 ? coded.message : code;
5288
- }
5289
- if (Array.isArray(coded.errors)) {
5290
- for (const member of coded.errors) {
5291
- const reason = dialFailureReason(member);
5292
- if (reason) return reason;
5293
- }
5294
- }
5295
- return null;
5296
- }
5297
- function validateEndpoint(baseUrl2) {
5298
- return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources/validate`;
5299
- }
5300
- function describeRemoteFailure(failure) {
5301
- switch (failure.item) {
5302
- case "wal_level":
5303
- return {
5304
- label: failure.actual ? `wal_level is ${failure.actual} (need logical)` : `wal_level must be logical`,
5305
- fix: failure.fix
5306
- };
5307
- case "publication":
5308
- return { label: "the Ablo publication does not exist", fix: failure.fix };
5309
- case "replication_role":
5310
- return { label: "the DATABASE_URL role lacks the REPLICATION attribute", fix: failure.fix };
5311
- case "replica_identity":
5312
- return {
5313
- label: `published tables cannot replicate UPDATE/DELETE${failure.actual ? ` (${failure.actual})` : ""}`,
5314
- fix: failure.fix
5315
- };
5316
- case "write_role":
5317
- return { label: "ABLO_WRITE_DATABASE_URL is not a scoped DML role", fix: failure.fix };
5318
- case "row_security":
5319
- return { label: "the direct-write role does not enforce row_security", fix: failure.fix };
5320
- case "schema_privileges":
5321
- return { label: "the direct-write role has unsafe schema privileges", fix: failure.fix };
5322
- case "idempotency_ledger":
5323
- return { label: "ablo_idempotency is missing or has unsafe grants", fix: failure.fix };
5324
- case "table_privileges":
5325
- return {
5326
- label: `the direct-write role lacks application DML${failure.actual ? ` (${failure.actual})` : ""}`,
5327
- fix: failure.fix
5328
- };
5329
- case "logical_marker":
5330
- return { label: "the direct-write role cannot emit the correlation marker", fix: failure.fix };
5331
- default:
5332
- return { label: failure.item, fix: failure.fix };
5333
- }
5334
- }
5335
- function parseWireFailures(value) {
5336
- if (!Array.isArray(value)) return [];
5337
- const failures = [];
5338
- for (const entry of value) {
5339
- if (entry === null || typeof entry !== "object") continue;
5340
- const { item, actual, fix } = entry;
5341
- if (typeof item !== "string" || typeof fix !== "string") continue;
5342
- failures.push({ item, fix, ...typeof actual === "string" ? { actual } : {} });
5343
- }
5344
- return failures;
5345
- }
5346
- async function requestRemoteValidation(input) {
5347
- const doFetch = input.fetchImpl ?? fetch;
5348
- let res;
5349
- try {
5350
- res = await doFetch(validateEndpoint(input.apiUrl), {
5351
- method: "POST",
5352
- headers: { "content-type": "application/json", authorization: `Bearer ${input.apiKey}` },
5353
- body: JSON.stringify({
5354
- connectionString: input.connectionString,
5355
- ...input.writeConnectionString ? { writeConnectionString: input.writeConnectionString } : {}
5356
- })
5357
- });
5358
- } catch (err) {
5359
- return {
5360
- ok: false,
5361
- status: 0,
5362
- message: `couldn't reach ${input.apiUrl}: ${err instanceof Error ? err.message : String(err)}`
5363
- };
5364
- }
5365
- const body = await res.json().catch(() => ({}));
5366
- if (!res.ok) {
5367
- const code = typeof body.code === "string" ? body.code : body.error?.code;
5368
- const message = typeof body.message === "string" ? body.message : typeof body.error?.message === "string" ? body.error.message : `HTTP ${res.status}`;
5369
- return {
5370
- ok: false,
5371
- status: res.status,
5372
- message,
5373
- ...typeof code === "string" ? { code } : {}
5374
- };
5375
- }
5376
- const reachable = body.reachable === true;
5377
- return {
5378
- ok: true,
5379
- reachable,
5380
- ready: body.ready === true,
5381
- ...typeof body.reason === "string" ? { reason: body.reason } : {},
5382
- failures: parseWireFailures(body.failures)
5383
- };
5384
- }
5385
- var DIAL_FAILURE_CODES;
5386
- var init_remoteValidation = __esm({
5387
- "src/cli/remoteValidation.ts"() {
5388
- "use strict";
5389
- init_cjs_shims();
5390
- DIAL_FAILURE_CODES = /* @__PURE__ */ new Set([
5391
- "ENOTFOUND",
5392
- "EAI_AGAIN",
5393
- "ECONNREFUSED",
5394
- "ECONNRESET",
5395
- "ETIMEDOUT",
5396
- "ENETUNREACH",
5397
- "EHOSTUNREACH",
5398
- "CONNECT_TIMEOUT"
5399
- ]);
5400
- }
5401
- });
5402
-
5403
5291
  // src/source/migrations.ts
5292
+ function addColumnIfAbsent(column, definition) {
5293
+ return `DO $$
5294
+ DECLARE ledger regclass := to_regclass('ablo_idempotency');
5295
+ BEGIN
5296
+ IF ledger IS NOT NULL AND NOT EXISTS (
5297
+ SELECT 1 FROM pg_attribute
5298
+ WHERE attrelid = ledger AND attname = '${column}' AND attnum > 0 AND NOT attisdropped
5299
+ ) THEN
5300
+ ALTER TABLE ablo_idempotency ADD COLUMN ${column} ${definition};
5301
+ END IF;
5302
+ END $$;`;
5303
+ }
5404
5304
  function idempotencyLedgerMigrations() {
5405
5305
  return [
5406
5306
  {
@@ -5418,13 +5318,19 @@ function idempotencyLedgerMigrations() {
5418
5318
  // Nullable only for rows created by older adapter versions. New writes
5419
5319
  // always populate it; replaying a legacy NULL row fails closed because
5420
5320
  // the adapter cannot prove that the intent matches.
5421
- up: `ALTER TABLE ablo_idempotency
5422
- ADD COLUMN IF NOT EXISTS request_hash TEXT;`
5321
+ //
5322
+ // The column already exists on any current table (the CREATE above
5323
+ // includes it), so this only matters when upgrading a pre-existing
5324
+ // ledger. `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` checks ownership
5325
+ // BEFORE it evaluates IF NOT EXISTS, so on a ledger that predates the
5326
+ // setup — owned by another role — the no-op still errors "must be owner".
5327
+ // Guard on the catalog (readable regardless of ownership) so the ALTER
5328
+ // runs only when the column is genuinely absent, keeping re-runs safe.
5329
+ up: addColumnIfAbsent("request_hash", "TEXT")
5423
5330
  },
5424
5331
  {
5425
5332
  name: "ablo_idempotency_permanent_retention",
5426
- up: `ALTER TABLE ablo_idempotency
5427
- ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ NOT NULL DEFAULT 'infinity';`
5333
+ up: addColumnIfAbsent("expires_at", `TIMESTAMPTZ NOT NULL DEFAULT 'infinity'`)
5428
5334
  }
5429
5335
  ];
5430
5336
  }
@@ -5435,10 +5341,448 @@ var init_migrations = __esm({
5435
5341
  }
5436
5342
  });
5437
5343
 
5344
+ // src/cli/connectSetup.ts
5345
+ function quoteIdent(id) {
5346
+ return `"${id.replace(/"/g, '""')}"`;
5347
+ }
5348
+ function quoteLiteral(value) {
5349
+ return `'${value.replace(/'/g, "''")}'`;
5350
+ }
5351
+ function scopedSequenceGrant(tables, writeRole) {
5352
+ const names = tables.map(quoteLiteral).join(", ");
5353
+ return `DO $$
5354
+ DECLARE seq regclass;
5355
+ BEGIN
5356
+ FOR seq IN
5357
+ SELECT DISTINCT d.objid::regclass
5358
+ FROM pg_depend d
5359
+ JOIN pg_class s ON s.oid = d.objid AND s.relkind = 'S'
5360
+ JOIN pg_class t ON t.oid = d.refobjid
5361
+ JOIN pg_namespace n ON n.oid = t.relnamespace
5362
+ WHERE d.deptype IN ('a', 'i') AND n.nspname = 'public' AND t.relname IN (${names})
5363
+ LOOP
5364
+ EXECUTE format('GRANT USAGE, SELECT ON SEQUENCE %s TO ${quoteIdent(writeRole)}', seq);
5365
+ END LOOP;
5366
+ END $$;`;
5367
+ }
5368
+ function connectSetupSql(input) {
5369
+ const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
5370
+ const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : ABLO_WRITE_ROLE;
5371
+ const tables = input.tables ?? [];
5372
+ const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
5373
+ const tableList = tables.map(quoteIdent).join(", ");
5374
+ const scoped = tables.length > 0;
5375
+ const applicationGrant = scoped ? `GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE ${tableList} TO ${quoteIdent(writeRole)};` : `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${quoteIdent(writeRole)};`;
5376
+ const replicationReadGrants = scoped ? [`GRANT SELECT ON TABLE ${tableList} TO ${quoteIdent(role)};`] : [
5377
+ `GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${quoteIdent(role)};`,
5378
+ `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${quoteIdent(role)};`
5379
+ ];
5380
+ const writerSequenceGrants = scoped ? [scopedSequenceGrant(tables, writeRole)] : [`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${quoteIdent(writeRole)};`];
5381
+ const ledger = idempotencyLedgerMigrations().map((migration) => migration.up);
5382
+ return [
5383
+ // 1. Turn on logical decoding. Requires a restart (it's not reloadable).
5384
+ `ALTER SYSTEM SET wal_level = 'logical';`,
5385
+ // 2. Publish the tables Ablo should read.
5386
+ `CREATE PUBLICATION ${quoteIdent(ABLO_PUBLICATION)} ${publicationTarget};`,
5387
+ // 3. A least-privilege role: it can stream replication and SELECT the
5388
+ // published tables, nothing more.
5389
+ `CREATE ROLE ${quoteIdent(role)} WITH REPLICATION LOGIN PASSWORD '<password>';`,
5390
+ ...replicationReadGrants,
5391
+ // 4. A distinct DML role: no replication, role administration, ownership,
5392
+ // schema creation, or DDL. It runs NOBYPASSRLS with row_security on, so on a
5393
+ // table that HAS row-level-security policies they govern its writes and it
5394
+ // can't bypass them; on a table without policies there is nothing to apply
5395
+ // and the table grants alone bound it.
5396
+ `CREATE ROLE ${quoteIdent(writeRole)} WITH LOGIN PASSWORD '<write-password>' NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;`,
5397
+ `ALTER ROLE ${quoteIdent(writeRole)} SET row_security = on;`,
5398
+ `GRANT USAGE ON SCHEMA public TO ${quoteIdent(writeRole)};`,
5399
+ applicationGrant,
5400
+ ...writerSequenceGrants,
5401
+ ...scoped ? [] : [
5402
+ // "All tables" mode only: keep future tables/sequences writable so the
5403
+ // publication doesn't outgrow the grant.
5404
+ `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${quoteIdent(writeRole)};`,
5405
+ `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${quoteIdent(writeRole)};`
5406
+ ],
5407
+ // 5. Direct uses the durable replay ledger but deliberately no outbox.
5408
+ ...ledger,
5409
+ `REVOKE ALL ON TABLE public.ablo_idempotency FROM PUBLIC;`,
5410
+ `GRANT SELECT, INSERT, UPDATE ON TABLE public.ablo_idempotency TO ${quoteIdent(writeRole)};`,
5411
+ `REVOKE DELETE ON TABLE public.ablo_idempotency FROM ${quoteIdent(writeRole)};`,
5412
+ // The writer emits a transactional marker on your WAL so Ablo can correlate
5413
+ // the committed row back to the originating write and confirm it — this
5414
+ // EXECUTE grant is what makes that confirmation possible. Granted by lookup
5415
+ // across every pg_logical_emit_message variant instead of one literal
5416
+ // signature: PostgreSQL 17 adds an optional fourth `flush` parameter, so the
5417
+ // historical three-argument form no longer exists there and a signature-
5418
+ // pinned GRANT fails on an otherwise healthy database.
5419
+ `-- required: grants the writer Ablo's WAL write-confirmation marker function
5420
+ DO $$
5421
+ DECLARE fn regprocedure;
5422
+ BEGIN
5423
+ FOR fn IN
5424
+ SELECT p.oid::regprocedure
5425
+ FROM pg_proc p
5426
+ JOIN pg_namespace n ON n.oid = p.pronamespace
5427
+ WHERE n.nspname = 'pg_catalog' AND p.proname = 'pg_logical_emit_message'
5428
+ LOOP
5429
+ EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO ${quoteIdent(writeRole)}', fn);
5430
+ END LOOP;
5431
+ END $$;`
5432
+ ];
5433
+ }
5434
+ async function probeReadiness(sql, opts = {}) {
5435
+ const publication = opts.publication ?? ABLO_PUBLICATION;
5436
+ const items = [];
5437
+ const walRows = await sql.unsafe(
5438
+ `SELECT setting FROM pg_settings WHERE name = 'wal_level'`
5439
+ );
5440
+ const walLevel = walRows[0]?.setting ?? "unknown";
5441
+ items.push(
5442
+ walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors6.default.bold("logical")}` } : {
5443
+ ok: false,
5444
+ label: `wal_level is ${import_picocolors6.default.bold(walLevel)} (need ${import_picocolors6.default.bold("logical")})`,
5445
+ fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
5446
+ On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.
5447
+ On Neon enable Logical Replication in the project (Console \u2192 Settings \u2192 Logical Replication, or the API) \u2014 Neon forbids ALTER SYSTEM; the toggle sets wal_level=logical.`
5448
+ }
5449
+ );
5450
+ const pubRows = await sql.unsafe(
5451
+ `SELECT puballtables FROM pg_publication WHERE pubname = $1`,
5452
+ [publication]
5453
+ );
5454
+ const pubRow = pubRows[0];
5455
+ items.push(
5456
+ pubRow ? {
5457
+ ok: true,
5458
+ label: `publication ${import_picocolors6.default.bold(publication)} exists ${import_picocolors6.default.dim(pubRow.puballtables ? "(all tables)" : "(table subset)")}`
5459
+ } : {
5460
+ ok: false,
5461
+ label: `publication ${import_picocolors6.default.bold(publication)} not found`,
5462
+ fix: `CREATE PUBLICATION ${quoteIdent(publication)} FOR ALL TABLES;`
5463
+ }
5464
+ );
5465
+ const roleRows = await sql.unsafe(
5466
+ `SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
5467
+ );
5468
+ const role = roleRows[0];
5469
+ const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
5470
+ items.push(
5471
+ hasReplication ? {
5472
+ ok: true,
5473
+ label: `the replication role can stream replication ${import_picocolors6.default.dim("(REPLICATION)")}`
5474
+ } : {
5475
+ ok: false,
5476
+ label: `the replication role lacks the ${import_picocolors6.default.bold("REPLICATION")} attribute`,
5477
+ fix: `ALTER ROLE current_user WITH REPLICATION;
5478
+ On RDS: GRANT rds_replication TO <your_role>;`
5479
+ }
5480
+ );
5481
+ if (pubRows.length > 0) {
5482
+ const badRows = await sql.unsafe(
5483
+ `SELECT c.relname AS table_name, c.relreplident
5484
+ FROM pg_publication_tables pt
5485
+ JOIN pg_class c ON c.relname = pt.tablename
5486
+ JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
5487
+ WHERE pt.pubname = $1
5488
+ AND (
5489
+ c.relreplident = 'n'
5490
+ OR (
5491
+ c.relreplident = 'd'
5492
+ AND NOT EXISTS (
5493
+ SELECT 1 FROM pg_index i
5494
+ WHERE i.indrelid = c.oid AND i.indisprimary
5495
+ )
5496
+ )
5497
+ )`,
5498
+ [publication]
5499
+ );
5500
+ items.push(
5501
+ badRows.length === 0 ? { ok: true, label: `all published tables have a usable REPLICA IDENTITY` } : {
5502
+ ok: false,
5503
+ label: `${badRows.length} published table${badRows.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
5504
+ fix: badRows.map(
5505
+ (r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
5506
+ ).join("\n")
5507
+ }
5508
+ );
5509
+ }
5510
+ return items;
5511
+ }
5512
+ function registerEndpoint(baseUrl2) {
5513
+ return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources`;
5514
+ }
5515
+ async function parseJsonBody(res, schema) {
5516
+ const parsed = schema.safeParse(await res.json().catch(() => null));
5517
+ return parsed.success ? parsed.data : {};
5518
+ }
5519
+ async function registerDirectDataSource(opts) {
5520
+ let res;
5521
+ try {
5522
+ res = await fetch(registerEndpoint(opts.apiUrl), {
5523
+ method: "POST",
5524
+ headers: { "content-type": "application/json", authorization: `Bearer ${opts.apiKey}` },
5525
+ body: JSON.stringify({
5526
+ connection: "direct",
5527
+ connectionString: opts.replicationUrl,
5528
+ writeConnectionString: opts.writeUrl,
5529
+ route: opts.route
5530
+ })
5531
+ });
5532
+ } catch (err) {
5533
+ console.error(
5534
+ import_picocolors6.default.red(
5535
+ `
5536
+ Couldn't reach ${opts.apiUrl}: ${err instanceof Error ? err.message : String(err)}
5537
+ `
5538
+ )
5539
+ );
5540
+ return false;
5541
+ }
5542
+ if (res.ok) {
5543
+ const body2 = await parseJsonBody(res, DataSourceRegisterSuccess);
5544
+ const statusNote = body2.status === "active" ? `${opts.route}, active` : opts.route;
5545
+ console.log(
5546
+ `
5547
+ ${import_picocolors6.default.green("\u2713")} Registered${body2.host ? ` ${import_picocolors6.default.dim(body2.host)}` : ""}${body2.id ? ` ${import_picocolors6.default.dim(`(${body2.id})`)}` : ""} as a direct DataSource (${statusNote}).
5548
+ Customer COMMIT is durable acceptance; correlated WAL promotes queued writes to confirmed.
5549
+ `
5550
+ );
5551
+ return true;
5552
+ }
5553
+ const body = await parseJsonBody(res, DataSourceRegisterError);
5554
+ const code = body.code ?? body.error?.code;
5555
+ const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
5556
+ console.error(import_picocolors6.default.red(`
5557
+ Registration failed: ${message}`));
5558
+ if (code === "forbidden") {
5559
+ console.error(
5560
+ import_picocolors6.default.dim(
5561
+ ` Registering a database needs a ${import_picocolors6.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors6.default.bold("ablo login")} for one.`
5562
+ )
5563
+ );
5564
+ } else if (code === "datasource_connection_unsupported") {
5565
+ console.error(
5566
+ import_picocolors6.default.dim(
5567
+ ` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`
5568
+ )
5569
+ );
5570
+ } else if (code === "database_not_replication_ready" || code === "data_source_blocked") {
5571
+ for (const f of body.details?.failures ?? []) {
5572
+ console.error(
5573
+ ` ${import_picocolors6.default.red("\u2717")} ${import_picocolors6.default.bold(f.item ?? "item")}${f.actual ? import_picocolors6.default.dim(` (${f.actual})`) : ""}`
5574
+ );
5575
+ if (f.fix)
5576
+ for (const line of f.fix.split("\n")) console.error(` ${import_picocolors6.default.red("\u2022")} ${line}`);
5577
+ }
5578
+ console.error(
5579
+ import_picocolors6.default.dim(`
5580
+ Apply the fixes, verify with ${import_picocolors6.default.bold("ablo connect check")}, then re-run.`)
5581
+ );
5582
+ } else if (code === "database_unreachable" || code === "source_unreachable") {
5583
+ if (body.details?.reason) console.error(import_picocolors6.default.dim(` ${body.details.reason}`));
5584
+ console.error(
5585
+ import_picocolors6.default.dim(
5586
+ ` Ablo's servers must be able to reach this database \u2014 a localhost or private-network
5587
+ Postgres can't use the direct path. Establish an allowlist, PrivateLink, peering, or VPN.
5588
+ Only when no inbound route is possible, register the signed ${import_picocolors6.default.bold("dataSource()")} endpoint fallback.`
5589
+ )
5590
+ );
5591
+ }
5592
+ console.error();
5593
+ return false;
5594
+ }
5595
+ var import_picocolors6, import_zod6, ABLO_PUBLICATION, ABLO_REPLICATION_ROLE, ABLO_WRITE_ROLE, DIRECT_DATA_SOURCE_ROUTES, DataSourceRegisterSuccess, DataSourceRegisterError;
5596
+ var init_connectSetup = __esm({
5597
+ "src/cli/connectSetup.ts"() {
5598
+ "use strict";
5599
+ init_cjs_shims();
5600
+ import_picocolors6 = __toESM(require_picocolors(), 1);
5601
+ import_zod6 = require("zod");
5602
+ init_migrations();
5603
+ ABLO_PUBLICATION = "ablo_publication";
5604
+ ABLO_REPLICATION_ROLE = "ablo_replicator";
5605
+ ABLO_WRITE_ROLE = "ablo_writer";
5606
+ DIRECT_DATA_SOURCE_ROUTES = [
5607
+ "public-allowlist",
5608
+ "privatelink",
5609
+ "peering",
5610
+ "vpn"
5611
+ ];
5612
+ DataSourceRegisterSuccess = import_zod6.z.object({
5613
+ id: import_zod6.z.string().optional(),
5614
+ host: import_zod6.z.string().optional(),
5615
+ status: import_zod6.z.string().optional()
5616
+ });
5617
+ DataSourceRegisterError = import_zod6.z.object({
5618
+ code: import_zod6.z.string().optional(),
5619
+ message: import_zod6.z.string().optional(),
5620
+ details: import_zod6.z.object({
5621
+ failures: import_zod6.z.array(
5622
+ import_zod6.z.object({
5623
+ item: import_zod6.z.string().optional(),
5624
+ actual: import_zod6.z.string().optional(),
5625
+ fix: import_zod6.z.string().optional()
5626
+ })
5627
+ ).optional(),
5628
+ reason: import_zod6.z.string().optional()
5629
+ }).optional(),
5630
+ error: import_zod6.z.object({ code: import_zod6.z.string().optional(), message: import_zod6.z.string().optional() }).optional()
5631
+ });
5632
+ }
5633
+ });
5634
+
5635
+ // src/cli/disconnect.ts
5636
+ var disconnect_exports = {};
5637
+ __export(disconnect_exports, {
5638
+ DISCONNECT_USAGE: () => DISCONNECT_USAGE,
5639
+ disconnect: () => disconnect
5640
+ });
5641
+ function planeLabel(target) {
5642
+ const confirmed = target.confirmed;
5643
+ const project = confirmed?.project?.name ?? confirmed?.project?.slug ?? confirmed?.projectId ?? "the default project";
5644
+ const env = confirmed?.environment ?? target.keyEnv ?? "unknown environment";
5645
+ return { project, env };
5646
+ }
5647
+ async function disconnect(argv) {
5648
+ let skipConfirm = false;
5649
+ for (const arg of argv) {
5650
+ if (arg === "--yes" || arg === "-y") skipConfirm = true;
5651
+ else if (arg === "--help" || arg === "-h") {
5652
+ console.log(DISCONNECT_USAGE);
5653
+ return;
5654
+ } else {
5655
+ console.error(
5656
+ import_picocolors7.default.red(` Unknown flag ${import_picocolors7.default.bold(arg)}.`) + import_picocolors7.default.dim(` See ${import_picocolors7.default.bold("ablo connect deregister --help")}.`)
5657
+ );
5658
+ process.exit(1);
5659
+ }
5660
+ }
5661
+ console.log(
5662
+ `
5663
+ ${brand("ablo")} ${import_picocolors7.default.dim("connect deregister")} ${import_picocolors7.default.dim("remove this project's data source")}
5664
+ `
5665
+ );
5666
+ const resolved = resolveEffectiveApiKey();
5667
+ const apiKey = resolved.key;
5668
+ const keySource = resolved.source ?? "stored";
5669
+ if (!apiKey) {
5670
+ console.error(
5671
+ import_picocolors7.default.red(` Disconnecting needs an API key, and none was found.`) + import_picocolors7.default.dim(
5672
+ ` Run ${import_picocolors7.default.bold("ablo login")} (or set ${import_picocolors7.default.bold("ABLO_API_KEY")}), then re-run ${import_picocolors7.default.bold("ablo connect deregister")}.`
5673
+ )
5674
+ );
5675
+ process.exit(1);
5676
+ }
5677
+ const apiUrl2 = apiBaseUrl();
5678
+ const target = await resolveTarget({ url: apiUrl2, apiKey, keySource });
5679
+ const { project, env } = planeLabel(target);
5680
+ const envLabel = env === "production" ? import_picocolors7.default.yellow(env) : import_picocolors7.default.dim(env);
5681
+ for (const mismatch of target.mismatches) {
5682
+ console.log(` ${import_picocolors7.default.yellow("\u26A0")} ${describeMismatch(mismatch)}
5683
+ `);
5684
+ }
5685
+ if (!skipConfirm) {
5686
+ if (!process.stdout.isTTY) {
5687
+ console.error(
5688
+ import_picocolors7.default.dim(` Re-run with ${import_picocolors7.default.bold("--yes")} to disconnect in a non-interactive session.
5689
+ `)
5690
+ );
5691
+ process.exit(1);
5692
+ }
5693
+ const proceed = await ye({
5694
+ message: `Disconnect the data source for ${import_picocolors7.default.bold(project)} in ${envLabel}?`,
5695
+ initialValue: true
5696
+ });
5697
+ if (pD(proceed) || !proceed) {
5698
+ console.log(import_picocolors7.default.dim(` Nothing changed. The data source is still connected.
5699
+ `));
5700
+ process.exit(0);
5701
+ }
5702
+ }
5703
+ let res;
5704
+ try {
5705
+ res = await fetch(registerEndpoint(apiUrl2), {
5706
+ method: "DELETE",
5707
+ headers: { authorization: `Bearer ${apiKey}` }
5708
+ });
5709
+ } catch (err) {
5710
+ console.error(
5711
+ import_picocolors7.default.red(`
5712
+ Couldn't reach ${apiUrl2}: ${err instanceof Error ? err.message : String(err)}
5713
+ `)
5714
+ );
5715
+ process.exit(1);
5716
+ }
5717
+ if (res.ok) {
5718
+ const body2 = await res.json().catch(() => ({}));
5719
+ const parts = [];
5720
+ if (body2.cleared?.direct) parts.push("the direct database registration");
5721
+ if (body2.cleared?.endpoints) {
5722
+ parts.push(
5723
+ `${body2.cleared.endpoints} endpoint registration${body2.cleared.endpoints === 1 ? "" : "s"}`
5724
+ );
5725
+ }
5726
+ const what = parts.length > 0 ? parts.join(" and ") : "the data source";
5727
+ console.log(
5728
+ `
5729
+ ${import_picocolors7.default.green("\u2713")} Disconnected ${what} for ${import_picocolors7.default.bold(project)} in ${envLabel}. Reconnect with ${import_picocolors7.default.bold("ablo connect")}.
5730
+ `
5731
+ );
5732
+ return;
5733
+ }
5734
+ const body = await res.json().catch(() => ({}));
5735
+ const code = body.code ?? body.error?.code;
5736
+ const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
5737
+ if (code === "entity_not_found") {
5738
+ console.log(import_picocolors7.default.dim(` No data source registered for ${project} in ${env}.
5739
+ `));
5740
+ return;
5741
+ }
5742
+ console.error(import_picocolors7.default.red(`
5743
+ Disconnect failed: ${message}`));
5744
+ if (code === "forbidden") {
5745
+ console.error(
5746
+ import_picocolors7.default.dim(
5747
+ ` Disconnecting needs a ${import_picocolors7.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors7.default.bold("ablo login")} for one.`
5748
+ )
5749
+ );
5750
+ }
5751
+ console.error();
5752
+ process.exit(1);
5753
+ }
5754
+ var import_picocolors7, DISCONNECT_USAGE;
5755
+ var init_disconnect = __esm({
5756
+ "src/cli/disconnect.ts"() {
5757
+ "use strict";
5758
+ init_cjs_shims();
5759
+ import_picocolors7 = __toESM(require_picocolors(), 1);
5760
+ init_dist2();
5761
+ init_config();
5762
+ init_push();
5763
+ init_theme();
5764
+ init_connectSetup();
5765
+ init_target();
5766
+ DISCONNECT_USAGE = `${brand("ablo")} connect deregister ${import_picocolors7.default.dim("remove this project's data source")}
5767
+
5768
+ Usage
5769
+ npx ablo connect deregister Remove the active project's data source (confirms first)
5770
+ npx ablo connect deregister --yes Skip the confirmation
5771
+
5772
+ Acts on one plane \u2014 the active project (${import_picocolors7.default.bold("ablo projects use")}) in the active
5773
+ environment (${import_picocolors7.default.bold("ablo mode")}), shown before it runs. Removes the registration and
5774
+ Ablo's replication state for that plane, so Ablo stops reading and writing the
5775
+ database. Reconnect with ${import_picocolors7.default.bold("ablo connect")}.`;
5776
+ }
5777
+ });
5778
+
5438
5779
  // src/cli/connectApply.ts
5439
5780
  var connectApply_exports = {};
5440
5781
  __export(connectApply_exports, {
5441
5782
  connectApplyPlan: () => connectApplyPlan,
5783
+ detectProvider: () => detectProvider,
5784
+ ledgerBlockedBy: () => ledgerBlockedBy,
5785
+ logicalReplicationGuidance: () => logicalReplicationGuidance,
5442
5786
  passwordClause: () => passwordClause,
5443
5787
  postRegistrationOutcome: () => postRegistrationOutcome,
5444
5788
  runConnectApply: () => runConnectApply
@@ -5448,7 +5792,7 @@ function postRegistrationOutcome(input) {
5448
5792
  if (!input.rotating) return { exitCode: 1, notice: null };
5449
5793
  return {
5450
5794
  exitCode: 1,
5451
- notice: "The new passwords are set in your database, but Ablo could not be updated with them.\nAblo still holds the previous password, which no longer works \u2014 writes will fail until\nyou re-run `ablo connect --rotate` (each run is idempotent and rotates a fresh password)."
5795
+ notice: "The new passwords are set in your database, but Ablo could not be updated with them.\nAblo still holds the previous password, which no longer works \u2014 writes will fail until\nyou re-run `ablo connect rotate` (each run is idempotent and rotates a fresh password)."
5452
5796
  };
5453
5797
  }
5454
5798
  function passwordClause(password, mode2) {
@@ -5465,19 +5809,28 @@ function connectApplyPlan(input) {
5465
5809
  const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
5466
5810
  const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : ABLO_WRITE_ROLE;
5467
5811
  const tables = input.tables ?? [];
5812
+ const provider = input.provider ?? "generic";
5468
5813
  const recipe = connectSetupSql({ tables, role, writeRole });
5469
5814
  const isWal = (s) => s.startsWith("ALTER SYSTEM SET wal_level");
5470
5815
  const isPublication = (s) => s.startsWith("CREATE PUBLICATION");
5471
- const isRoleCreate = (s) => /^CREATE ROLE /.test(s);
5816
+ const isRoleCreate = (s) => s.startsWith("CREATE ROLE ");
5472
5817
  const grants = recipe.filter((s) => !isWal(s) && !isPublication(s) && !isRoleCreate(s));
5473
5818
  const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
5474
- return [
5475
- {
5819
+ const walStep = input.walAlreadyLogical ? [] : [
5820
+ provider === "generic" ? {
5476
5821
  key: "wal",
5477
5822
  title: "Turn on logical replication",
5478
- detail: "lets Ablo read your changes as they happen",
5823
+ detail: "lets Ablo read your changes as they happen (needs a restart to take effect)",
5479
5824
  sql: [`ALTER SYSTEM SET wal_level = 'logical';`]
5480
- },
5825
+ } : {
5826
+ key: "wal",
5827
+ title: "Turn on logical replication",
5828
+ detail: logicalReplicationGuidance(provider),
5829
+ sql: []
5830
+ }
5831
+ ];
5832
+ return [
5833
+ ...walStep,
5481
5834
  {
5482
5835
  key: "publication",
5483
5836
  title: "Publish your tables to Ablo",
@@ -5498,7 +5851,7 @@ END $$;`
5498
5851
  {
5499
5852
  key: "write-role",
5500
5853
  title: "Create a scoped writer role",
5501
- detail: `${writeRole} \u2014 writes rows through Ablo, and is subject to your row-level security`,
5854
+ detail: `${writeRole} \u2014 writes rows through Ablo; where a table has row-level-security policies, they govern its writes too (it can't bypass them)`,
5502
5855
  sql: [
5503
5856
  idempotentRole(
5504
5857
  writeRole,
@@ -5523,17 +5876,19 @@ function printPlan2(steps, showSql) {
5523
5876
  console.log(` This sets up your database for Ablo:
5524
5877
  `);
5525
5878
  for (const step of steps) {
5526
- console.log(` ${import_picocolors6.default.green("\u2022")} ${step.title}`);
5879
+ console.log(` ${import_picocolors8.default.green("\u2022")} ${step.title}`);
5527
5880
  if (showSql) {
5528
5881
  for (const statement of step.sql) {
5529
- for (const line of statement.split("\n")) console.log(` ${import_picocolors6.default.dim(line)}`);
5882
+ for (const line of statement.split("\n")) console.log(` ${import_picocolors8.default.dim(line)}`);
5530
5883
  }
5531
5884
  }
5532
5885
  }
5533
5886
  console.log(
5534
- import_picocolors6.default.dim(`
5887
+ import_picocolors8.default.dim(
5888
+ `
5535
5889
  Your admin password stays on this machine.${showSql ? "" : " (--show-sql for the exact statements)"}
5536
- `)
5890
+ `
5891
+ )
5537
5892
  );
5538
5893
  }
5539
5894
  async function adminCanCreateRoles(sql) {
@@ -5542,8 +5897,54 @@ async function adminCanCreateRoles(sql) {
5542
5897
  );
5543
5898
  return rows[0] ?? null;
5544
5899
  }
5900
+ function ledgerBlockedBy(row) {
5901
+ if (!row || row.is_owner || row.is_superuser) return null;
5902
+ return row.owner;
5903
+ }
5904
+ async function unmanageableLedgerOwner(sql) {
5905
+ const rows = await sql.unsafe(
5906
+ `SELECT pg_get_userbyid(c.relowner) AS owner,
5907
+ c.relowner = r.oid AS is_owner,
5908
+ r.rolsuper AS is_superuser
5909
+ FROM pg_class c
5910
+ JOIN pg_namespace n ON n.oid = c.relnamespace
5911
+ JOIN pg_roles r ON r.rolname = current_user
5912
+ WHERE c.relkind = 'r'
5913
+ AND c.relname = 'ablo_idempotency'
5914
+ AND n.nspname = 'public'`
5915
+ );
5916
+ return ledgerBlockedBy(rows[0]);
5917
+ }
5918
+ function detectProvider(hostOrTarget) {
5919
+ const host = hostOrTarget.toLowerCase();
5920
+ if (host.includes("neon.tech")) return "neon";
5921
+ if (host.includes("supabase.co") || host.includes("supabase.com") || host.includes("pooler.supabase")) {
5922
+ return "supabase";
5923
+ }
5924
+ if (host.includes("rds.amazonaws.com") || host.includes(".rds.")) return "rds";
5925
+ return "generic";
5926
+ }
5927
+ function logicalReplicationGuidance(provider) {
5928
+ switch (provider) {
5929
+ case "neon":
5930
+ return `enable logical replication in your Neon project settings \u2014 it can't be set over SQL`;
5931
+ case "supabase":
5932
+ return `raise wal_level to logical in your Supabase project's database settings`;
5933
+ case "rds":
5934
+ return `set rds.logical_replication = 1 in the instance's parameter group, then reboot`;
5935
+ case "generic":
5936
+ return `run the ALTER SYSTEM above, then restart Postgres \u2014 wal_level is not reloadable`;
5937
+ }
5938
+ }
5939
+ async function currentWalLevel(sql) {
5940
+ try {
5941
+ const rows = await sql.unsafe(`SHOW wal_level`);
5942
+ return rows[0]?.wal_level ?? "";
5943
+ } catch {
5944
+ return "";
5945
+ }
5946
+ }
5545
5947
  async function executePlan(sql, steps, rebuildPlaintext) {
5546
- let walDeferred = false;
5547
5948
  let plaintextSteps = null;
5548
5949
  for (const step of steps) {
5549
5950
  for (const statement of step.sql) {
@@ -5551,7 +5952,6 @@ async function executePlan(sql, steps, rebuildPlaintext) {
5551
5952
  await sql.unsafe(statement);
5552
5953
  } catch (err) {
5553
5954
  if (step.key === "wal") {
5554
- walDeferred = true;
5555
5955
  continue;
5556
5956
  }
5557
5957
  if ((step.key === "replication-role" || step.key === "write-role") && isPlaintextRefusal(err)) {
@@ -5566,15 +5966,16 @@ async function executePlan(sql, steps, rebuildPlaintext) {
5566
5966
  }
5567
5967
  }
5568
5968
  }
5569
- return { walDeferred };
5570
5969
  }
5571
5970
  async function runConnectApply(args) {
5572
5971
  const rotating = args.rotate;
5573
- const verb = rotating ? "connect --rotate" : "connect --apply";
5972
+ const verb = rotating ? "connect rotate" : "connect apply";
5574
5973
  const adminUrl = args.url ?? readProjectAdminDatabaseUrl();
5575
5974
  if (!adminUrl) {
5576
5975
  console.error(
5577
- import_picocolors6.default.red(" No admin connection string.") + import_picocolors6.default.dim(` Pass ${import_picocolors6.default.bold("--url <admin-conn>")} (or set ${import_picocolors6.default.bold("DATABASE_URL")}) and re-run.`)
5976
+ import_picocolors8.default.red(" No admin connection string.") + import_picocolors8.default.dim(
5977
+ ` Pass ${import_picocolors8.default.bold("--url <admin-conn>")} (or set ${import_picocolors8.default.bold("DATABASE_URL")}) and re-run.`
5978
+ )
5578
5979
  );
5579
5980
  process.exit(1);
5580
5981
  }
@@ -5588,39 +5989,63 @@ async function runConnectApply(args) {
5588
5989
  const apiKey = resolveApiKey();
5589
5990
  if (!apiKey) {
5590
5991
  console.error(
5591
- import_picocolors6.default.red(" Not logged in.") + import_picocolors6.default.dim(
5592
- ` Run ${import_picocolors6.default.bold("ablo login")} (or set ${import_picocolors6.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`
5992
+ import_picocolors8.default.red(" Not logged in.") + import_picocolors8.default.dim(
5993
+ ` Run ${import_picocolors8.default.bold("ablo login")} (or set ${import_picocolors8.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`
5593
5994
  )
5594
5995
  );
5595
5996
  process.exit(1);
5596
5997
  }
5597
5998
  console.log(
5598
5999
  `
5599
- ${brand("ablo")} ${import_picocolors6.default.dim(verb)} ${import_picocolors6.default.dim(rotating ? "re-key the scoped roles" : "set up your database for Ablo")}
6000
+ ${brand("ablo")} ${import_picocolors8.default.dim(verb)} ${import_picocolors8.default.dim(rotating ? "re-key the scoped roles" : "set up your database for Ablo")}
5600
6001
  `
5601
6002
  );
5602
6003
  console.log(
5603
- ` ${import_picocolors6.default.dim("\u2192")} ${import_picocolors6.default.bold(target)}${adminSource === "DATABASE_URL" ? import_picocolors6.default.dim(" (admin via DATABASE_URL)") : ""}
6004
+ ` ${import_picocolors8.default.dim("\u2192")} ${import_picocolors8.default.bold(target)}${adminSource === "DATABASE_URL" ? import_picocolors8.default.dim(" (admin via DATABASE_URL)") : ""}
5604
6005
  `
5605
6006
  );
5606
- const admin = src_default(adminUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
5607
- } });
6007
+ const admin = src_default(adminUrl, {
6008
+ max: 1,
6009
+ prepare: false,
6010
+ connect_timeout: 10,
6011
+ onnotice: () => {
6012
+ }
6013
+ });
5608
6014
  let capability;
5609
6015
  try {
5610
6016
  capability = await adminCanCreateRoles(admin);
5611
6017
  } catch (err) {
5612
6018
  await admin.end({ timeout: 2 }).catch(() => void 0);
5613
6019
  const pg = err ?? {};
5614
- console.error(import_picocolors6.default.red(` Couldn't connect: ${pg.message ?? String(err)}`));
6020
+ console.error(import_picocolors8.default.red(` Couldn't connect: ${pg.message ?? String(err)}`));
5615
6021
  process.exit(1);
5616
6022
  }
5617
6023
  if (!capability || !(capability.rolsuper || capability.rolcreaterole)) {
5618
6024
  await admin.end({ timeout: 2 });
5619
6025
  console.error(
5620
- import_picocolors6.default.red(` ${capability?.rolname ?? "This role"} can't create roles.`) + import_picocolors6.default.dim(` Point ${import_picocolors6.default.bold("--url")} at your owner/admin connection and re-run.`)
6026
+ import_picocolors8.default.red(` ${capability?.rolname ?? "This role"} can't create roles.`) + import_picocolors8.default.dim(` Point ${import_picocolors8.default.bold("--url")} at your owner/admin connection and re-run.`)
6027
+ );
6028
+ process.exit(1);
6029
+ }
6030
+ const blockingOwner = await unmanageableLedgerOwner(admin).catch(() => null);
6031
+ if (blockingOwner) {
6032
+ await admin.end({ timeout: 2 });
6033
+ console.error(
6034
+ import_picocolors8.default.red(
6035
+ `
6036
+ ${import_picocolors8.default.bold("ablo_idempotency")} already exists on ${target}, owned by ${import_picocolors8.default.bold(blockingOwner)}, but you connected as ${import_picocolors8.default.bold(capability.rolname)}.`
6037
+ ) + `
6038
+ Ablo's setup grants the writer role access to this ledger, and Postgres reserves that
6039
+ for the table's owner. Either re-run pointing ${import_picocolors8.default.bold("--url")} at ${import_picocolors8.default.bold(blockingOwner)}'s connection,
6040
+ or drop the existing ledger so Ablo recreates it under this admin \u2014 it holds only
6041
+ idempotency replay records, safe to drop when no commit is in flight:
6042
+ ${import_picocolors8.default.cyan("DROP TABLE ablo_idempotency;")}
6043
+ `
5621
6044
  );
5622
6045
  process.exit(1);
5623
6046
  }
6047
+ const provider = detectProvider(target);
6048
+ const walReady = await currentWalLevel(admin) === "logical";
5624
6049
  const replicationPassword = generateRolePassword();
5625
6050
  const writePassword = generateRolePassword();
5626
6051
  const buildPlan = (mode2) => connectApplyPlan({
@@ -5630,7 +6055,9 @@ async function runConnectApply(args) {
5630
6055
  credentials: {
5631
6056
  replicationClause: passwordClause(replicationPassword, mode2),
5632
6057
  writeClause: passwordClause(writePassword, mode2)
5633
- }
6058
+ },
6059
+ walAlreadyLogical: walReady,
6060
+ provider
5634
6061
  });
5635
6062
  const steps = buildPlan("scram-verifier");
5636
6063
  printPlan2(steps, args.showSql);
@@ -5638,7 +6065,7 @@ async function runConnectApply(args) {
5638
6065
  if (!process.stdout.isTTY) {
5639
6066
  await admin.end({ timeout: 2 });
5640
6067
  console.error(
5641
- import_picocolors6.default.dim(` Re-run with ${import_picocolors6.default.bold("--yes")} to apply this in a non-interactive session.
6068
+ import_picocolors8.default.dim(` Re-run with ${import_picocolors8.default.bold("--yes")} to apply this in a non-interactive session.
5642
6069
  `)
5643
6070
  );
5644
6071
  process.exit(1);
@@ -5649,20 +6076,21 @@ async function runConnectApply(args) {
5649
6076
  });
5650
6077
  if (pD(proceed) || !proceed) {
5651
6078
  await admin.end({ timeout: 2 });
5652
- console.log(import_picocolors6.default.dim(` Nothing applied. Run ${import_picocolors6.default.bold("ablo connect")} to see the manual recipe.
5653
- `));
6079
+ console.log(
6080
+ import_picocolors8.default.dim(` Nothing applied. Run ${import_picocolors8.default.bold("ablo connect")} to see the manual recipe.
6081
+ `)
6082
+ );
5654
6083
  process.exit(0);
5655
6084
  }
5656
6085
  }
5657
- let walDeferred = false;
5658
6086
  try {
5659
- ({ walDeferred } = await executePlan(admin, steps, () => buildPlan("plaintext")));
6087
+ await executePlan(admin, steps, () => buildPlan("plaintext"));
5660
6088
  } catch (err) {
5661
6089
  await admin.end({ timeout: 2 }).catch(() => void 0);
5662
6090
  const pg = err ?? {};
5663
6091
  console.error(
5664
- import_picocolors6.default.red(`
5665
- Setup stopped: ${pg.message ?? String(err)}`) + import_picocolors6.default.dim(` Every step is safe to re-run.
6092
+ import_picocolors8.default.red(`
6093
+ Setup stopped: ${pg.message ?? String(err)}`) + import_picocolors8.default.dim(` Every step is safe to re-run.
5666
6094
  `)
5667
6095
  );
5668
6096
  process.exit(1);
@@ -5673,38 +6101,43 @@ async function runConnectApply(args) {
5673
6101
  const replicationUrl = rewriteDatabaseUrl(adminUrl, role, replicationPassword);
5674
6102
  const writeUrl = rewriteDatabaseUrl(adminUrl, writeRole, writePassword);
5675
6103
  console.log(`
5676
- ${import_picocolors6.default.green("\u2713")} Roles ${rotating ? "re-keyed" : "created"}.
6104
+ ${import_picocolors8.default.green("\u2713")} Roles ${rotating ? "re-keyed" : "created"}.
5677
6105
  `);
5678
- if (walDeferred) {
5679
- console.log(
5680
- ` ${import_picocolors6.default.yellow("!")} One step left \u2014 logical replication needs a restart.
5681
- ` + import_picocolors6.default.dim(` RDS/Aurora: set rds.logical_replication=1, reboot. Neon: enable it in settings.
5682
- `) + `
5683
- Then re-run: ${import_picocolors6.default.cyan(`npx ablo ${verb}`)}
6106
+ if (!walReady) {
6107
+ console.error(
6108
+ ` ${import_picocolors8.default.yellow("!")} One step left \u2014 logical replication isn't on yet.
6109
+ ${logicalReplicationGuidance(provider)}.
6110
+
6111
+ Then re-run: ${import_picocolors8.default.cyan(`npx ablo ${verb}`)}
5684
6112
  `
5685
6113
  );
5686
- process.exit(0);
6114
+ process.exit(1);
5687
6115
  }
5688
- const verifier = src_default(replicationUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
5689
- } });
6116
+ const verifier = src_default(replicationUrl, {
6117
+ max: 1,
6118
+ prepare: false,
6119
+ connect_timeout: 10,
6120
+ onnotice: () => {
6121
+ }
6122
+ });
5690
6123
  let items;
5691
6124
  try {
5692
6125
  items = await probeReadiness(verifier);
5693
6126
  } catch {
5694
6127
  items = null;
5695
- console.log(import_picocolors6.default.dim(` Couldn't verify from here; Ablo will validate from its own network.
6128
+ console.log(import_picocolors8.default.dim(` Couldn't verify from here; Ablo will validate from its own network.
5696
6129
  `));
5697
6130
  }
5698
6131
  await verifier.end({ timeout: 2 }).catch(() => void 0);
5699
6132
  const failed = items?.filter((i) => !i.ok) ?? [];
5700
6133
  if (failed.length > 0) {
5701
- for (const item of failed) console.log(` ${import_picocolors6.default.yellow("!")} ${item.label}`);
6134
+ for (const item of failed) console.log(` ${import_picocolors8.default.yellow("!")} ${item.label}`);
5702
6135
  console.log(`
5703
- ${import_picocolors6.default.dim("Resolve, then re-run")} ${import_picocolors6.default.cyan(`npx ablo ${verb}`)}
6136
+ ${import_picocolors8.default.dim("Resolve, then re-run")} ${import_picocolors8.default.cyan(`npx ablo ${verb}`)}
5704
6137
  `);
5705
6138
  process.exit(1);
5706
6139
  }
5707
- const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
6140
+ const apiUrl2 = apiBaseUrl();
5708
6141
  const registered = await registerDirectDataSource({
5709
6142
  apiUrl: apiUrl2,
5710
6143
  apiKey,
@@ -5715,794 +6148,24 @@ async function runConnectApply(args) {
5715
6148
  const outcome = postRegistrationOutcome({ rotating, registered });
5716
6149
  if (outcome.notice) {
5717
6150
  console.error(`
5718
- ${import_picocolors6.default.red(outcome.notice.split("\n").join("\n "))}
6151
+ ${import_picocolors8.default.red(outcome.notice.split("\n").join("\n "))}
5719
6152
  `);
5720
6153
  }
5721
6154
  process.exit(outcome.exitCode);
5722
6155
  }
5723
- var import_picocolors6;
6156
+ var import_picocolors8;
5724
6157
  var init_connectApply = __esm({
5725
6158
  "src/cli/connectApply.ts"() {
5726
6159
  "use strict";
5727
6160
  init_cjs_shims();
5728
- import_picocolors6 = __toESM(require_picocolors(), 1);
6161
+ import_picocolors8 = __toESM(require_picocolors(), 1);
5729
6162
  init_src();
5730
6163
  init_dist2();
5731
- init_connect();
5732
- init_dbRole();
5733
- init_config();
5734
- init_push();
5735
- init_theme();
5736
- }
5737
- });
5738
-
5739
- // src/cli/connect.ts
5740
- function parseConnectArgs(argv) {
5741
- let check2 = false;
5742
- let register = false;
5743
- let apply2 = false;
5744
- let rotate = false;
5745
- let url;
5746
- let yes = false;
5747
- let showSql = false;
5748
- let auditInfra = false;
5749
- let tables = [];
5750
- let role = ABLO_REPLICATION_ROLE;
5751
- let writeRole = ABLO_WRITE_ROLE;
5752
- let route = "public-allowlist";
5753
- for (let i = 0; i < argv.length; i++) {
5754
- const arg = argv[i];
5755
- switch (arg) {
5756
- case "--check":
5757
- check2 = true;
5758
- break;
5759
- case "--register":
5760
- register = true;
5761
- break;
5762
- case "--apply":
5763
- apply2 = true;
5764
- break;
5765
- case "--rotate":
5766
- rotate = true;
5767
- break;
5768
- case "--url":
5769
- url = argv[++i] ?? url;
5770
- break;
5771
- case "--yes":
5772
- case "-y":
5773
- yes = true;
5774
- break;
5775
- case "--show-sql":
5776
- showSql = true;
5777
- break;
5778
- case "--audit-infra":
5779
- auditInfra = true;
5780
- break;
5781
- case "--tables": {
5782
- const value = argv[++i] ?? "";
5783
- tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
5784
- break;
5785
- }
5786
- case "--role":
5787
- role = argv[++i] ?? role;
5788
- break;
5789
- case "--write-role":
5790
- writeRole = argv[++i] ?? writeRole;
5791
- break;
5792
- case "--route": {
5793
- const value = argv[++i] ?? "";
5794
- if (!DIRECT_DATA_SOURCE_ROUTES.includes(value)) {
5795
- throw new AbloValidationError(
5796
- `invalid direct route: ${value || "(missing)"} (expected ${DIRECT_DATA_SOURCE_ROUTES.join(", ")})`,
5797
- { code: "cli_invalid_arguments" }
5798
- );
5799
- }
5800
- route = value;
5801
- break;
5802
- }
5803
- default:
5804
- throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
5805
- }
5806
- }
5807
- if (role === writeRole) {
5808
- throw new AbloValidationError("replication and write roles must be different", {
5809
- code: "cli_invalid_arguments"
5810
- });
5811
- }
5812
- return { check: check2, register, apply: apply2, rotate, url, yes, showSql, auditInfra, tables, role, writeRole, route };
5813
- }
5814
- function quoteIdent(id) {
5815
- return `"${id.replace(/"/g, '""')}"`;
5816
- }
5817
- function connectSetupSql(input) {
5818
- const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
5819
- const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : ABLO_WRITE_ROLE;
5820
- const tables = input.tables ?? [];
5821
- const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
5822
- const applicationGrant = tables.length > 0 ? `GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE ${tables.map(quoteIdent).join(", ")} TO ${quoteIdent(writeRole)};` : `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${quoteIdent(writeRole)};`;
5823
- const ledger = idempotencyLedgerMigrations().map((migration) => migration.up);
5824
- return [
5825
- // 1. Turn on logical decoding. Requires a restart (it's not reloadable).
5826
- `ALTER SYSTEM SET wal_level = 'logical';`,
5827
- // 2. Publish the tables Ablo should read.
5828
- `CREATE PUBLICATION ${quoteIdent(ABLO_PUBLICATION)} ${publicationTarget};`,
5829
- // 3. A least-privilege role: it can stream replication and SELECT, nothing more.
5830
- `CREATE ROLE ${quoteIdent(role)} WITH REPLICATION LOGIN PASSWORD '<password>';`,
5831
- `GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${quoteIdent(role)};`,
5832
- // Future tables get SELECT automatically, so the publication doesn't outgrow the grant.
5833
- `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${quoteIdent(role)};`,
5834
- // 4. A distinct DML role: no replication, role administration, ownership,
5835
- // schema creation, or DDL. It is subject to RLS on every transaction.
5836
- `CREATE ROLE ${quoteIdent(writeRole)} WITH LOGIN PASSWORD '<write-password>' NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;`,
5837
- `ALTER ROLE ${quoteIdent(writeRole)} SET row_security = on;`,
5838
- // PUBLIC can carry CREATE on older Postgres clusters. A role-level REVOKE
5839
- // cannot override that inherited grant, so harden the schema explicitly.
5840
- `REVOKE CREATE ON SCHEMA public FROM PUBLIC;`,
5841
- `GRANT USAGE ON SCHEMA public TO ${quoteIdent(writeRole)};`,
5842
- applicationGrant,
5843
- `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${quoteIdent(writeRole)};`,
5844
- ...tables.length === 0 ? [
5845
- `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${quoteIdent(writeRole)};`,
5846
- `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${quoteIdent(writeRole)};`
5847
- ] : [],
5848
- // 5. Direct uses the durable replay ledger but deliberately no outbox.
5849
- ...ledger,
5850
- `REVOKE ALL ON TABLE public.ablo_idempotency FROM PUBLIC;`,
5851
- `GRANT SELECT, INSERT, UPDATE ON TABLE public.ablo_idempotency TO ${quoteIdent(writeRole)};`,
5852
- `REVOKE DELETE ON TABLE public.ablo_idempotency FROM ${quoteIdent(writeRole)};`,
5853
- // Grant every pg_logical_emit_message variant by lookup instead of one
5854
- // literal signature: PostgreSQL 17 adds an optional fourth `flush`
5855
- // parameter, so the historical three-argument form no longer exists there
5856
- // and a signature-pinned GRANT fails on an otherwise healthy database.
5857
- `DO $$
5858
- DECLARE fn regprocedure;
5859
- BEGIN
5860
- FOR fn IN
5861
- SELECT p.oid::regprocedure
5862
- FROM pg_proc p
5863
- JOIN pg_namespace n ON n.oid = p.pronamespace
5864
- WHERE n.nspname = 'pg_catalog' AND p.proname = 'pg_logical_emit_message'
5865
- LOOP
5866
- EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO ${quoteIdent(writeRole)}', fn);
5867
- END LOOP;
5868
- END $$;`
5869
- ];
5870
- }
5871
- function printConnectRecipe(args) {
5872
- const sql = connectSetupSql({
5873
- tables: args.tables,
5874
- role: args.role,
5875
- writeRole: args.writeRole
5876
- });
5877
- console.log(`
5878
- ${brand("ablo")} ${import_picocolors7.default.dim("connect")} ${import_picocolors7.default.dim("direct writes + WAL-settled sync")}
5879
- `);
5880
- console.log(
5881
- ` Ablo applies coordinated writes directly to your Postgres with a scoped DML role. WAL
5882
- observes what committed, orders it with external writes, and confirms it in the sync log.
5883
- Your database stays authoritative; Ablo never owns or migrates your application tables.
5884
- Run this once against your Postgres ${import_picocolors7.default.dim("(as a superuser / DB owner)")}:
5885
- `
5886
- );
5887
- console.log(` ${import_picocolors7.default.bold("1.")} Enable logical decoding ${import_picocolors7.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`);
5888
- console.log(` ${import_picocolors7.default.cyan(sql[0])}`);
5889
- console.log(
5890
- import_picocolors7.default.dim(
5891
- ` On Amazon RDS / Aurora you can't ALTER SYSTEM: set ${import_picocolors7.default.bold("rds.logical_replication = 1")} in the
5892
- instance's parameter group instead, then reboot.`
5893
- )
5894
- );
5895
- console.log(`
5896
- ${import_picocolors7.default.bold("2.")} Publish the tables Ablo should read`);
5897
- console.log(` ${import_picocolors7.default.cyan(sql[1])}`);
5898
- if (args.tables.length === 0) {
5899
- console.log(import_picocolors7.default.dim(` (Scope it with ${import_picocolors7.default.bold("ablo connect --tables a,b,c")} to publish a subset.)`));
5900
- }
5901
- console.log(`
5902
- ${import_picocolors7.default.bold("3.")} Create the scoped replication role ${import_picocolors7.default.dim("(pick your own replication password)")}`);
5903
- console.log(` ${import_picocolors7.default.cyan(sql[2])}`);
5904
- console.log(` ${import_picocolors7.default.cyan(sql[3])}`);
5905
- console.log(` ${import_picocolors7.default.cyan(sql[4])}`);
5906
- console.log(
5907
- import_picocolors7.default.dim(
5908
- ` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
5909
- ${import_picocolors7.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
5910
- )
5911
- );
5912
- console.log(
5913
- `
5914
- ${import_picocolors7.default.bold("4.")} Create the separate DML role and permanent idempotency ledger ` + import_picocolors7.default.dim("(pick your own write password)")
5915
- );
5916
- for (const statement of sql.slice(5)) {
5917
- for (const line of statement.split("\n")) console.log(` ${import_picocolors7.default.cyan(line)}`);
5918
- }
5919
- console.log(
5920
- import_picocolors7.default.dim(
5921
- ` The writer gets row DML + ledger access only. It has no REPLICATION, schema CREATE,
5922
- role administration, database creation, ownership, or customer-table DDL. Direct uses
5923
- ${import_picocolors7.default.bold("ablo_idempotency")} but no outbox; WAL carries the committed row changes.`
5924
- )
5925
- );
5926
- console.log(
5927
- `
5928
- ${import_picocolors7.default.bold("5.")} Configure both scoped connections, then verify:
5929
- ${import_picocolors7.default.bold("DATABASE_URL")} ${import_picocolors7.default.dim(`\u2192 ${args.role} (replication only)`)}
5930
- ${import_picocolors7.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors7.default.dim(`\u2192 ${args.writeRole} (DML only)`)}
5931
- ${import_picocolors7.default.cyan("npx ablo connect --check")}
5932
- `
5933
- );
5934
- console.log(
5935
- import_picocolors7.default.dim(
5936
- ` Reachable databases use this direct path. If no inbound route can be established, use the
5937
- signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback; its correlated events confirm writes
5938
- without an Ablo-side customer database socket.`
5939
- )
5940
- );
5941
- console.log();
5942
- }
5943
- function printCheckItem(item) {
5944
- if (item.ok) {
5945
- console.log(` ${import_picocolors7.default.green("\u2713")} ${item.label}`);
5946
- } else {
5947
- console.log(` ${import_picocolors7.default.red("\u2717")} ${item.label}`);
5948
- if (item.fix) {
5949
- for (const line of item.fix.split("\n")) console.log(` ${import_picocolors7.default.red("\u2022")} ${line}`);
5950
- }
5951
- }
5952
- }
5953
- async function probeReadiness(sql, opts = {}) {
5954
- const publication = opts.publication ?? ABLO_PUBLICATION;
5955
- const items = [];
5956
- const walRows = await sql.unsafe(
5957
- `SELECT setting FROM pg_settings WHERE name = 'wal_level'`
5958
- );
5959
- const walLevel = walRows[0]?.setting ?? "unknown";
5960
- items.push(
5961
- walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors7.default.bold("logical")}` } : {
5962
- ok: false,
5963
- label: `wal_level is ${import_picocolors7.default.bold(walLevel)} (need ${import_picocolors7.default.bold("logical")})`,
5964
- fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
5965
- On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.
5966
- On Neon enable Logical Replication in the project (Console \u2192 Settings \u2192 Logical Replication, or the API) \u2014 Neon forbids ALTER SYSTEM; the toggle sets wal_level=logical.`
5967
- }
5968
- );
5969
- const pubRows = await sql.unsafe(
5970
- `SELECT puballtables FROM pg_publication WHERE pubname = $1`,
5971
- [publication]
5972
- );
5973
- const pubRow = pubRows[0];
5974
- items.push(
5975
- pubRow ? {
5976
- ok: true,
5977
- label: `publication ${import_picocolors7.default.bold(publication)} exists ${import_picocolors7.default.dim(pubRow.puballtables ? "(all tables)" : "(table subset)")}`
5978
- } : {
5979
- ok: false,
5980
- label: `publication ${import_picocolors7.default.bold(publication)} not found`,
5981
- fix: `CREATE PUBLICATION ${quoteIdent(publication)} FOR ALL TABLES;`
5982
- }
5983
- );
5984
- const roleRows = await sql.unsafe(
5985
- `SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
5986
- );
5987
- const role = roleRows[0];
5988
- const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
5989
- items.push(
5990
- hasReplication ? { ok: true, label: `DATABASE_URL role can stream replication ${import_picocolors7.default.dim("(REPLICATION)")}` } : {
5991
- ok: false,
5992
- label: `DATABASE_URL role lacks the ${import_picocolors7.default.bold("REPLICATION")} attribute`,
5993
- fix: `ALTER ROLE current_user WITH REPLICATION;
5994
- On RDS: GRANT rds_replication TO <your_role>;`
5995
- }
5996
- );
5997
- if (pubRows.length > 0) {
5998
- const badRows = await sql.unsafe(
5999
- `SELECT c.relname AS table_name, c.relreplident
6000
- FROM pg_publication_tables pt
6001
- JOIN pg_class c ON c.relname = pt.tablename
6002
- JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
6003
- WHERE pt.pubname = $1
6004
- AND (
6005
- c.relreplident = 'n'
6006
- OR (
6007
- c.relreplident = 'd'
6008
- AND NOT EXISTS (
6009
- SELECT 1 FROM pg_index i
6010
- WHERE i.indrelid = c.oid AND i.indisprimary
6011
- )
6012
- )
6013
- )`,
6014
- [publication]
6015
- );
6016
- items.push(
6017
- badRows.length === 0 ? { ok: true, label: `all published tables have a usable REPLICA IDENTITY` } : {
6018
- ok: false,
6019
- label: `${badRows.length} published table${badRows.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
6020
- fix: badRows.map(
6021
- (r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
6022
- ).join("\n")
6023
- }
6024
- );
6025
- }
6026
- return items;
6027
- }
6028
- async function probeDirectWriteReadiness(sql, opts = {}) {
6029
- const schema = opts.schema ?? "public";
6030
- const publication = opts.publication ?? ABLO_PUBLICATION;
6031
- const items = [];
6032
- const roleRows = await sql.unsafe(
6033
- `SELECT rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole, rolreplication
6034
- FROM pg_roles WHERE rolname = current_user`
6035
- );
6036
- const role = roleRows[0];
6037
- const dangerous = Boolean(
6038
- !role || role.rolsuper || role.rolbypassrls || role.rolcreatedb || role.rolcreaterole || role.rolreplication
6039
- );
6040
- items.push(
6041
- dangerous ? {
6042
- ok: false,
6043
- label: `ABLO_WRITE_DATABASE_URL is not a scoped DML role`,
6044
- fix: `Use ${ABLO_WRITE_ROLE} from the setup recipe: NOSUPERUSER, NOBYPASSRLS, NOCREATEDB, NOCREATEROLE, NOREPLICATION.`
6045
- } : { ok: true, label: `write role ${import_picocolors7.default.bold(role?.rolname ?? ABLO_WRITE_ROLE)} is DML-only` }
6046
- );
6047
- const schemaRows = await sql.unsafe(
6048
- `SELECT
6049
- has_schema_privilege(current_user, $1, 'USAGE') AS usage,
6050
- has_schema_privilege(current_user, $1, 'CREATE') AS create,
6051
- current_setting('row_security') AS row_security`,
6052
- [schema]
6053
- );
6054
- const schemaRow = schemaRows[0];
6055
- items.push(
6056
- schemaRow?.usage && !schemaRow.create && schemaRow.row_security === "on" ? { ok: true, label: `writer uses ${schema} with RLS on and no schema CREATE` } : {
6057
- ok: false,
6058
- label: `writer schema/RLS privileges are not least-privilege`,
6059
- fix: `REVOKE CREATE ON SCHEMA ${quoteIdent(schema)} FROM PUBLIC; GRANT USAGE ON SCHEMA ${quoteIdent(schema)} TO ${quoteIdent(role?.rolname ?? ABLO_WRITE_ROLE)}; ALTER ROLE ${quoteIdent(role?.rolname ?? ABLO_WRITE_ROLE)} SET row_security = on;`
6060
- }
6061
- );
6062
- const ledgerName = `${quoteIdent(schema)}.${quoteIdent("ablo_idempotency")}`;
6063
- const ledgerRows = await sql.unsafe(
6064
- `SELECT
6065
- to_regclass($1) IS NOT NULL AS present,
6066
- CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'SELECT,INSERT,UPDATE') END AS writes,
6067
- CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'DELETE') END AS deletes`,
6068
- [ledgerName]
6069
- );
6070
- const ledger = ledgerRows[0];
6071
- items.push(
6072
- ledger?.present && ledger.writes && !ledger.deletes ? { ok: true, label: `${import_picocolors7.default.bold("ablo_idempotency")} is durable and protected from DELETE` } : {
6073
- ok: false,
6074
- label: `${import_picocolors7.default.bold("ablo_idempotency")} is missing or has unsafe grants`,
6075
- fix: `Apply the DML-role and idempotency-ledger statements printed by ${import_picocolors7.default.bold("ablo connect")}.`
6076
- }
6077
- );
6078
- const tableRows = await sql.unsafe(
6079
- `SELECT format('%I.%I', schemaname, tablename) AS relation
6080
- FROM pg_publication_tables
6081
- WHERE pubname = $1 AND schemaname = $2 AND tablename <> 'ablo_idempotency'
6082
- AND NOT has_table_privilege(
6083
- current_user,
6084
- format('%I.%I', schemaname, tablename),
6085
- 'SELECT,INSERT,UPDATE,DELETE'
6086
- )`,
6087
- [publication, schema]
6088
- );
6089
- items.push(
6090
- tableRows.length === 0 ? { ok: true, label: `writer can apply DML to every published application table` } : {
6091
- ok: false,
6092
- label: `writer lacks DML on ${tableRows.length} published table${tableRows.length === 1 ? "" : "s"}`,
6093
- fix: `Grant SELECT, INSERT, UPDATE, DELETE on: ${tableRows.map((row) => row.relation).join(", ")}`
6094
- }
6095
- );
6096
- return items;
6097
- }
6098
- async function auditTenantSyncInfra(sql) {
6099
- const artifacts = [];
6100
- for (const name of SYNC_INFRA_RELATIONS) {
6101
- const rows = await sql.unsafe(
6102
- `SELECT to_regclass($1)::text AS reg`,
6103
- [`public.${name}`]
6104
- );
6105
- artifacts.push({ kind: "relation", name, present: rows[0]?.reg != null });
6106
- }
6107
- for (const name of SYNC_INFRA_TYPES) {
6108
- const rows = await sql.unsafe(
6109
- `SELECT to_regtype($1)::text AS reg`,
6110
- [`public.${name}`]
6111
- );
6112
- artifacts.push({ kind: "type", name, present: rows[0]?.reg != null });
6113
- }
6114
- return artifacts;
6115
- }
6116
- function requireDatabaseUrl(verb, opts) {
6117
- const resolved = readProjectReplicationUrlWithSource();
6118
- if (!resolved) {
6119
- console.error(
6120
- import_picocolors7.default.red(" No replication connection found (checked process env, .env.local, .env).") + import_picocolors7.default.dim(
6121
- ` Set ${import_picocolors7.default.bold("ABLO_REPLICATION_DATABASE_URL")} to the ${ABLO_REPLICATION_ROLE} connection printed by ${import_picocolors7.default.bold("ablo connect")}, then re-run ${import_picocolors7.default.bold(`ablo connect ${verb}`)}.`
6122
- )
6123
- );
6124
- process.exit(1);
6125
- }
6126
- if (resolved.variable === "DATABASE_URL") {
6127
- console.warn(
6128
- import_picocolors7.default.yellow(` ! Falling back to ${import_picocolors7.default.bold("DATABASE_URL")} for the replication role \u2014 deprecated,`) + import_picocolors7.default.dim(
6129
- ` removed in ${DATABASE_URL_REMOVAL_VERSION}.
6130
- If this is your app's own connection string, this is NOT validating the ${import_picocolors7.default.bold(ABLO_REPLICATION_ROLE)} role.
6131
- Set ${import_picocolors7.default.bold("ABLO_REPLICATION_DATABASE_URL")} to the connection ${import_picocolors7.default.bold("ablo connect")} printed.
6132
- `
6133
- )
6134
- );
6135
- if (!(opts?.allowDeprecatedFallback ?? false)) {
6136
- console.error(
6137
- import_picocolors7.default.red(` Refusing to ${verb.replace(/^--/, "")} against a ${import_picocolors7.default.bold("DATABASE_URL")} fallback.`) + import_picocolors7.default.dim(
6138
- ` Set ${import_picocolors7.default.bold("ABLO_REPLICATION_DATABASE_URL")} explicitly and re-run. (${import_picocolors7.default.bold("ablo connect --check")} still accepts the fallback so you can inspect it.)
6139
- `
6140
- )
6141
- );
6142
- process.exit(1);
6143
- }
6144
- }
6145
- return resolved.url;
6146
- }
6147
- function requireWriteDatabaseUrl(verb) {
6148
- const dbUrl = readProjectWriteDatabaseUrl();
6149
- if (!dbUrl) {
6150
- console.error(
6151
- import_picocolors7.default.red(" No ABLO_WRITE_DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors7.default.dim(
6152
- ` Set it to the ${ABLO_WRITE_ROLE} connection printed by ${import_picocolors7.default.bold("ablo connect")}, then re-run ${import_picocolors7.default.bold(`ablo connect ${verb}`)}. The replication credential is never reused for DML.`
6153
- )
6154
- );
6155
- process.exit(1);
6156
- }
6157
- return dbUrl;
6158
- }
6159
- async function probeAndReport(dbUrl, kind = "replication") {
6160
- const sql = src_default(dbUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
6161
- } });
6162
- let items;
6163
- try {
6164
- items = kind === "replication" ? await probeReadiness(sql) : await probeDirectWriteReadiness(sql);
6165
- } catch (err) {
6166
- await sql.end({ timeout: 2 }).catch(() => void 0);
6167
- const dial = dialFailureReason(err);
6168
- if (dial) return { kind: "no-dial", reason: dial };
6169
- const pg = err ?? {};
6170
- console.error(import_picocolors7.default.red(` Couldn't read the database: ${pg.message ?? String(err)}`));
6171
- process.exit(1);
6172
- }
6173
- await sql.end({ timeout: 2 });
6174
- for (const item of items) printCheckItem(item);
6175
- return { kind: "probed", failures: items.filter((i) => !i.ok).length };
6176
- }
6177
- async function runRemoteCheck(dbUrl, writeDbUrl, localReason) {
6178
- console.log(
6179
- ` This machine can't reach one or both scoped connections (${import_picocolors7.default.dim(localReason)}).
6180
- That is not the verdict: direct writes and replication run from Ablo's infrastructure.
6181
- Asking Ablo to check both roles from its side\u2026
6182
- `
6183
- );
6184
- const apiKey = resolveApiKey();
6185
- if (!apiKey) {
6186
- console.error(
6187
- import_picocolors7.default.red(` The engine-side check needs an API key, and none was found.`) + import_picocolors7.default.dim(
6188
- ` Run ${import_picocolors7.default.bold("ablo login")} (or set ${import_picocolors7.default.bold("ABLO_API_KEY")}), then re-run ${import_picocolors7.default.bold("ablo connect --check")}.`
6189
- )
6190
- );
6191
- process.exit(1);
6192
- }
6193
- const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
6194
- const result = await requestRemoteValidation({
6195
- apiUrl: apiUrl2,
6196
- apiKey,
6197
- connectionString: dbUrl,
6198
- writeConnectionString: writeDbUrl
6199
- });
6200
- if (!result.ok) {
6201
- console.error(import_picocolors7.default.red(` The engine-side check failed: ${result.message}`));
6202
- if (result.code === "forbidden") {
6203
- console.error(
6204
- import_picocolors7.default.dim(` Checking a database from Ablo's side needs a ${import_picocolors7.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors7.default.bold("ablo login")} for one.`)
6205
- );
6206
- }
6207
- console.error();
6208
- process.exit(1);
6209
- }
6210
- if (!result.reachable) {
6211
- console.error(
6212
- ` ${import_picocolors7.default.red("\u2717")} Ablo's infrastructure can't reach both direct connections${result.reason ? ` ${import_picocolors7.default.dim(`(${result.reason})`)}` : ""}.`
6213
- );
6214
- console.error(
6215
- import_picocolors7.default.dim(
6216
- ` Direct needs a route Ablo's servers can dial \u2014 public allowlist, PrivateLink, peering,
6217
- or VPN. Only when no inbound route can exist, use the signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback.
6218
- `
6219
- )
6220
- );
6221
- process.exit(1);
6222
- }
6223
- for (const failure of result.failures) {
6224
- const { label, fix } = describeRemoteFailure(failure);
6225
- printCheckItem({ ok: false, label, fix });
6226
- }
6227
- console.log();
6228
- if (result.ready) {
6229
- console.log(
6230
- ` ${import_picocolors7.default.green("\u2713")} Ready \u2014 checked from Ablo's infrastructure. Both direct DML and WAL settlement are available.
6231
- `
6232
- );
6233
- process.exit(0);
6234
- }
6235
- const count = result.failures.length;
6236
- console.log(
6237
- ` ${import_picocolors7.default.red(`${count} item${count === 1 ? "" : "s"} to fix`)} ${import_picocolors7.default.dim(`\u2014 found by Ablo's infrastructure. Apply the fixes above, then re-run ${import_picocolors7.default.bold("ablo connect --check")}.`)}
6238
- `
6239
- );
6240
- process.exit(1);
6241
- }
6242
- async function runCheck() {
6243
- const dbUrl = requireDatabaseUrl("--check", { allowDeprecatedFallback: true });
6244
- const writeDbUrl = requireWriteDatabaseUrl("--check");
6245
- console.log(`
6246
- ${brand("ablo")} ${import_picocolors7.default.dim("connect --check")} ${import_picocolors7.default.dim("direct-write + WAL readiness")}
6247
- `);
6248
- console.log(` ${import_picocolors7.default.bold("Replication role")}
6249
- `);
6250
- const replication = await probeAndReport(dbUrl, "replication");
6251
- console.log(`
6252
- ${import_picocolors7.default.bold("Direct-write role")}
6253
- `);
6254
- const write = await probeAndReport(writeDbUrl, "write");
6255
- if (replication.kind === "no-dial" || write.kind === "no-dial") {
6256
- const reasons = [
6257
- replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
6258
- write.kind === "no-dial" ? `write: ${write.reason}` : null
6259
- ].filter((reason) => reason !== null);
6260
- return runRemoteCheck(dbUrl, writeDbUrl, reasons.join("; "));
6261
- }
6262
- const failures = replication.failures + write.failures;
6263
- console.log();
6264
- if (failures === 0) {
6265
- console.log(` ${import_picocolors7.default.green("\u2713")} Ready \u2014 Ablo can apply scoped DML and settle it from WAL.
6266
- `);
6267
- process.exit(0);
6268
- }
6269
- console.log(
6270
- ` ${import_picocolors7.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors7.default.dim(`\u2014 apply the fixes above, then re-run ${import_picocolors7.default.bold("ablo connect --check")}.`)}
6271
- `
6272
- );
6273
- process.exit(1);
6274
- }
6275
- function registerEndpoint(baseUrl2) {
6276
- return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources`;
6277
- }
6278
- async function runRegister(args) {
6279
- const dbUrl = requireDatabaseUrl("--register");
6280
- const writeDbUrl = requireWriteDatabaseUrl("--register");
6281
- const apiKey = resolveApiKey();
6282
- if (!apiKey) {
6283
- console.error(
6284
- import_picocolors7.default.red(" Not logged in.") + import_picocolors7.default.dim(` Run ${import_picocolors7.default.bold("ablo login")} (or set ${import_picocolors7.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`)
6285
- );
6286
- process.exit(1);
6287
- }
6288
- console.log(`
6289
- ${brand("ablo")} ${import_picocolors7.default.dim("connect --register")} ${import_picocolors7.default.dim("register a direct DataSource")}
6290
- `);
6291
- console.log(` ${import_picocolors7.default.bold("Replication role")}
6292
- `);
6293
- const replication = await probeAndReport(dbUrl, "replication");
6294
- console.log(`
6295
- ${import_picocolors7.default.bold("Direct-write role")}
6296
- `);
6297
- const write = await probeAndReport(writeDbUrl, "write");
6298
- const noDial = [
6299
- replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
6300
- write.kind === "no-dial" ? `write: ${write.reason}` : null
6301
- ].filter((reason) => reason !== null);
6302
- const failures = (replication.kind === "probed" ? replication.failures : 0) + (write.kind === "probed" ? write.failures : 0);
6303
- if (noDial.length > 0) {
6304
- console.log(
6305
- ` This machine can't reach one or both scoped connections (${import_picocolors7.default.dim(noDial.join("; "))}) \u2014 continuing anyway.
6306
- Ablo validates both credentials from the infrastructure that will use them and refuses
6307
- registration unless replication and direct DML are both ready.
6308
- `
6309
- );
6310
- }
6311
- if (failures > 0) {
6312
- console.log(
6313
- `
6314
- ${import_picocolors7.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors7.default.dim("\u2014 direct registration requires both scoped roles. Fix the above, then re-run.")}
6315
- `
6316
- );
6317
- process.exit(1);
6318
- }
6319
- const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
6320
- const registered = await registerDirectDataSource({
6321
- apiUrl: apiUrl2,
6322
- apiKey,
6323
- replicationUrl: dbUrl,
6324
- writeUrl: writeDbUrl,
6325
- route: args.route
6326
- });
6327
- process.exit(registered ? 0 : 1);
6328
- }
6329
- async function registerDirectDataSource(opts) {
6330
- let res;
6331
- try {
6332
- res = await fetch(registerEndpoint(opts.apiUrl), {
6333
- method: "POST",
6334
- headers: { "content-type": "application/json", authorization: `Bearer ${opts.apiKey}` },
6335
- body: JSON.stringify({
6336
- connection: "direct",
6337
- connectionString: opts.replicationUrl,
6338
- writeConnectionString: opts.writeUrl,
6339
- route: opts.route
6340
- })
6341
- });
6342
- } catch (err) {
6343
- console.error(import_picocolors7.default.red(`
6344
- Couldn't reach ${opts.apiUrl}: ${err instanceof Error ? err.message : String(err)}
6345
- `));
6346
- return false;
6347
- }
6348
- if (res.ok) {
6349
- const body2 = await res.json().catch(() => ({}));
6350
- const statusNote = body2.status === "active" ? `${opts.route}, active` : opts.route;
6351
- console.log(
6352
- `
6353
- ${import_picocolors7.default.green("\u2713")} Registered${body2.host ? ` ${import_picocolors7.default.dim(body2.host)}` : ""}${body2.id ? ` ${import_picocolors7.default.dim(`(${body2.id})`)}` : ""} as a direct DataSource (${statusNote}).
6354
- Customer COMMIT is durable acceptance; correlated WAL promotes queued writes to confirmed.
6355
- `
6356
- );
6357
- return true;
6358
- }
6359
- const body = await res.json().catch(() => ({}));
6360
- const code = body.code ?? body.error?.code;
6361
- const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
6362
- console.error(import_picocolors7.default.red(`
6363
- Registration failed: ${message}`));
6364
- if (code === "forbidden") {
6365
- console.error(import_picocolors7.default.dim(` Registering a database needs a ${import_picocolors7.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors7.default.bold("ablo login")} for one.`));
6366
- } else if (code === "datasource_connection_unsupported") {
6367
- console.error(
6368
- import_picocolors7.default.dim(` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`)
6369
- );
6370
- } else if (code === "database_not_replication_ready" || code === "data_source_blocked") {
6371
- for (const f of body.details?.failures ?? []) {
6372
- console.error(` ${import_picocolors7.default.red("\u2717")} ${import_picocolors7.default.bold(f.item ?? "item")}${f.actual ? import_picocolors7.default.dim(` (${f.actual})`) : ""}`);
6373
- if (f.fix) for (const line of f.fix.split("\n")) console.error(` ${import_picocolors7.default.red("\u2022")} ${line}`);
6374
- }
6375
- console.error(import_picocolors7.default.dim(`
6376
- Apply the fixes, verify with ${import_picocolors7.default.bold("ablo connect --check")}, then re-run.`));
6377
- } else if (code === "database_unreachable" || code === "source_unreachable") {
6378
- if (body.details?.reason) console.error(import_picocolors7.default.dim(` ${body.details.reason}`));
6379
- console.error(
6380
- import_picocolors7.default.dim(
6381
- ` Ablo's servers must be able to reach this database \u2014 a localhost or private-network
6382
- Postgres can't use the direct path. Establish an allowlist, PrivateLink, peering, or VPN.
6383
- Only when no inbound route is possible, register the signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback.`
6384
- )
6385
- );
6386
- }
6387
- console.error();
6388
- return false;
6389
- }
6390
- async function runAuditInfra() {
6391
- const dbUrl = requireDatabaseUrl("--audit-infra", { allowDeprecatedFallback: true });
6392
- const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
6393
- } });
6394
- let artifacts;
6395
- try {
6396
- artifacts = await auditTenantSyncInfra(sql);
6397
- } catch (err) {
6398
- const pg = err ?? {};
6399
- console.error(import_picocolors7.default.red(` Couldn't audit the database: ${pg.message ?? String(err)}`));
6400
- await sql.end({ timeout: 2 });
6401
- process.exit(1);
6402
- }
6403
- await sql.end({ timeout: 2 });
6404
- console.log(`
6405
- ${brand("ablo")} ${import_picocolors7.default.dim("connect --audit-infra")} ${import_picocolors7.default.dim("Stage 5 tenant DB sync-infra audit")}
6406
- `);
6407
- const present = artifacts.filter((a) => a.present);
6408
- if (present.length === 0) {
6409
- console.log(` ${import_picocolors7.default.green("\u2713")} No deprecated Ablo sync infrastructure found in public.
6410
- `);
6411
- process.exit(0);
6412
- }
6413
- for (const artifact of present) {
6414
- const label = artifact.kind === "type" ? "type" : "relation";
6415
- console.log(` ${import_picocolors7.default.yellow("!")} ${label} ${import_picocolors7.default.bold(`public.${artifact.name}`)} exists`);
6416
- }
6417
- console.log(
6418
- `
6419
- ${import_picocolors7.default.yellow(`${present.length} artifact${present.length === 1 ? "" : "s"} found`)} ` + import_picocolors7.default.dim("\u2014 do not drop automatically. Confirm the org/environment is log-authoritative, then follow ") + import_picocolors7.default.bold("docs/runbooks/wal-stage5-customer-db-infra-cleanup.md") + import_picocolors7.default.dim(".\n")
6420
- );
6421
- process.exit(1);
6422
- }
6423
- async function connect(argv) {
6424
- let args;
6425
- try {
6426
- args = parseConnectArgs(argv);
6427
- } catch (err) {
6428
- console.error(import_picocolors7.default.red(` ${err instanceof Error ? err.message : String(err)}`));
6429
- process.exit(1);
6430
- }
6431
- if (args.apply || args.rotate) {
6432
- const { runConnectApply: runConnectApply2 } = await Promise.resolve().then(() => (init_connectApply(), connectApply_exports));
6433
- await runConnectApply2(args);
6434
- return;
6435
- }
6436
- if (args.check) {
6437
- await runCheck();
6438
- return;
6439
- }
6440
- if (args.register) {
6441
- await runRegister(args);
6442
- return;
6443
- }
6444
- if (args.auditInfra) {
6445
- await runAuditInfra();
6446
- return;
6447
- }
6448
- printConnectRecipe(args);
6449
- }
6450
- var import_picocolors7, ABLO_PUBLICATION, ABLO_REPLICATION_ROLE, ABLO_WRITE_ROLE, DIRECT_DATA_SOURCE_ROUTES, SYNC_INFRA_RELATIONS, SYNC_INFRA_TYPES, CONNECT_USAGE;
6451
- var init_connect = __esm({
6452
- "src/cli/connect.ts"() {
6453
- "use strict";
6454
- init_cjs_shims();
6455
- init_errors();
6456
- import_picocolors7 = __toESM(require_picocolors(), 1);
6457
- init_src();
6164
+ init_connectSetup();
6458
6165
  init_dbRole();
6459
6166
  init_config();
6460
6167
  init_push();
6461
6168
  init_theme();
6462
- init_remoteValidation();
6463
- init_migrations();
6464
- ABLO_PUBLICATION = "ablo_publication";
6465
- ABLO_REPLICATION_ROLE = "ablo_replicator";
6466
- ABLO_WRITE_ROLE = "ablo_writer";
6467
- DIRECT_DATA_SOURCE_ROUTES = [
6468
- "public-allowlist",
6469
- "privatelink",
6470
- "peering",
6471
- "vpn"
6472
- ];
6473
- SYNC_INFRA_RELATIONS = [
6474
- "sync_deltas",
6475
- "sync_id_seq",
6476
- "sync_metadata",
6477
- "mutation_log"
6478
- ];
6479
- SYNC_INFRA_TYPES = [
6480
- "participant_kind",
6481
- "backfill_provenance",
6482
- "confirmation_state"
6483
- ];
6484
- CONNECT_USAGE = ` ablo connect \u2014 direct writes, settled by logical replication
6485
-
6486
- Ablo applies coordinated DML with a scoped writer role. WAL observes what committed,
6487
- orders it with external changes, and confirms it. Your Postgres remains authoritative.
6488
-
6489
- Usage:
6490
- npx ablo connect --apply Set it up end to end: create the roles, publish, register
6491
- npx ablo connect --url <admin-conn> Admin connection for --apply/--rotate (else DATABASE_URL)
6492
- npx ablo connect --rotate New passwords for both scoped roles, then re-register
6493
- npx ablo connect Print the exact setup SQL instead of running it
6494
- npx ablo connect --tables a,b,c Publish only these tables (default: all tables)
6495
- npx ablo connect --role <name> Name the replication role (default: ablo_replicator)
6496
- npx ablo connect --write-role <name> Name the DML role (default: ablo_writer)
6497
- npx ablo connect --route <route> public-allowlist | privatelink | peering | vpn
6498
- npx ablo connect --check Validate the scoped roles (ABLO_REPLICATION_DATABASE_URL + ABLO_WRITE_DATABASE_URL)
6499
- npx ablo connect --register Register both scoped credentials as one direct DataSource
6500
- npx ablo connect --audit-infra Read-only Stage 5 audit for deprecated Ablo sync tables/types
6501
-
6502
- --apply registers with Ablo directly; the admin credential is used only on this
6503
- machine and never persisted. Your app holds only ABLO_API_KEY.
6504
-
6505
- --apply also takes --yes (skip the confirm) and --show-sql (show the exact statements).`;
6506
6169
  }
6507
6170
  });
6508
6171
 
@@ -218523,11 +218186,11 @@ var require_commonjs2 = __commonJS({
218523
218186
  if (pad) {
218524
218187
  const need = width - c.length;
218525
218188
  if (need > 0) {
218526
- const z7 = new Array(need + 1).join("0");
218189
+ const z8 = new Array(need + 1).join("0");
218527
218190
  if (i < 0) {
218528
- c = "-" + z7 + c.slice(1);
218191
+ c = "-" + z8 + c.slice(1);
218529
218192
  } else {
218530
- c = z7 + c;
218193
+ c = z8 + c;
218531
218194
  }
218532
218195
  }
218533
218196
  }
@@ -282456,7 +282119,7 @@ Node text: ${this.#forgottenText}`;
282456
282119
  // src/cli/index.ts
282457
282120
  init_cjs_shims();
282458
282121
  init_dist2();
282459
- var import_picocolors21 = __toESM(require_picocolors(), 1);
282122
+ var import_picocolors23 = __toESM(require_picocolors(), 1);
282460
282123
  var import_fs12 = require("fs");
282461
282124
  var import_path7 = require("path");
282462
282125
  var import_child_process3 = require("child_process");
@@ -282644,8 +282307,739 @@ async function migrate(argv) {
282644
282307
  }
282645
282308
  }
282646
282309
 
282310
+ // src/cli/connect.ts
282311
+ init_cjs_shims();
282312
+ init_errors();
282313
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
282314
+ init_src();
282315
+ init_dbRole();
282316
+ init_config();
282317
+ init_push();
282318
+ init_theme();
282319
+
282320
+ // src/cli/remoteValidation.ts
282321
+ init_cjs_shims();
282322
+ var DIAL_FAILURE_CODES = /* @__PURE__ */ new Set([
282323
+ "ENOTFOUND",
282324
+ "EAI_AGAIN",
282325
+ "ECONNREFUSED",
282326
+ "ECONNRESET",
282327
+ "ETIMEDOUT",
282328
+ "ENETUNREACH",
282329
+ "EHOSTUNREACH",
282330
+ "CONNECT_TIMEOUT"
282331
+ ]);
282332
+ function dialFailureReason(err) {
282333
+ if (err === null || typeof err !== "object") return null;
282334
+ const coded = err;
282335
+ const code = typeof coded.code === "string" ? coded.code : null;
282336
+ if (code && DIAL_FAILURE_CODES.has(code)) {
282337
+ return typeof coded.message === "string" && coded.message.length > 0 ? coded.message : code;
282338
+ }
282339
+ if (Array.isArray(coded.errors)) {
282340
+ for (const member of coded.errors) {
282341
+ const reason = dialFailureReason(member);
282342
+ if (reason) return reason;
282343
+ }
282344
+ }
282345
+ return null;
282346
+ }
282347
+ function validateEndpoint(baseUrl2) {
282348
+ return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources/validate`;
282349
+ }
282350
+ function describeRemoteFailure(failure) {
282351
+ switch (failure.item) {
282352
+ case "wal_level":
282353
+ return {
282354
+ label: failure.actual ? `wal_level is ${failure.actual} (need logical)` : `wal_level must be logical`,
282355
+ fix: failure.fix
282356
+ };
282357
+ case "publication":
282358
+ return { label: "the Ablo publication does not exist", fix: failure.fix };
282359
+ case "replication_role":
282360
+ return { label: "the DATABASE_URL role lacks the REPLICATION attribute", fix: failure.fix };
282361
+ case "replica_identity":
282362
+ return {
282363
+ label: `published tables cannot replicate UPDATE/DELETE${failure.actual ? ` (${failure.actual})` : ""}`,
282364
+ fix: failure.fix
282365
+ };
282366
+ case "write_role":
282367
+ return { label: "ABLO_WRITE_DATABASE_URL is not a scoped DML role", fix: failure.fix };
282368
+ case "row_security":
282369
+ return { label: "the direct-write role does not enforce row_security", fix: failure.fix };
282370
+ case "schema_privileges":
282371
+ return { label: "the direct-write role has unsafe schema privileges", fix: failure.fix };
282372
+ case "idempotency_ledger":
282373
+ return { label: "ablo_idempotency is missing or has unsafe grants", fix: failure.fix };
282374
+ case "table_privileges":
282375
+ return {
282376
+ label: `the direct-write role lacks application DML${failure.actual ? ` (${failure.actual})` : ""}`,
282377
+ fix: failure.fix
282378
+ };
282379
+ case "logical_marker":
282380
+ return { label: "the direct-write role cannot emit the correlation marker", fix: failure.fix };
282381
+ default:
282382
+ return { label: failure.item, fix: failure.fix };
282383
+ }
282384
+ }
282385
+ function parseWireFailures(value) {
282386
+ if (!Array.isArray(value)) return [];
282387
+ const failures = [];
282388
+ for (const entry of value) {
282389
+ if (entry === null || typeof entry !== "object") continue;
282390
+ const { item, actual, fix } = entry;
282391
+ if (typeof item !== "string" || typeof fix !== "string") continue;
282392
+ failures.push({ item, fix, ...typeof actual === "string" ? { actual } : {} });
282393
+ }
282394
+ return failures;
282395
+ }
282396
+ async function requestRemoteValidation(input) {
282397
+ const doFetch = input.fetchImpl ?? fetch;
282398
+ let res;
282399
+ try {
282400
+ res = await doFetch(validateEndpoint(input.apiUrl), {
282401
+ method: "POST",
282402
+ headers: { "content-type": "application/json", authorization: `Bearer ${input.apiKey}` },
282403
+ body: JSON.stringify({
282404
+ ...input.connectionString ? { connectionString: input.connectionString } : {},
282405
+ ...input.writeConnectionString ? { writeConnectionString: input.writeConnectionString } : {}
282406
+ })
282407
+ });
282408
+ } catch (err) {
282409
+ return {
282410
+ ok: false,
282411
+ status: 0,
282412
+ message: `couldn't reach ${input.apiUrl}: ${err instanceof Error ? err.message : String(err)}`
282413
+ };
282414
+ }
282415
+ const body = await res.json().catch(() => ({}));
282416
+ if (!res.ok) {
282417
+ const code = typeof body.code === "string" ? body.code : body.error?.code;
282418
+ const message = typeof body.message === "string" ? body.message : typeof body.error?.message === "string" ? body.error.message : `HTTP ${res.status}`;
282419
+ return {
282420
+ ok: false,
282421
+ status: res.status,
282422
+ message,
282423
+ ...typeof code === "string" ? { code } : {}
282424
+ };
282425
+ }
282426
+ const reachable = body.reachable === true;
282427
+ return {
282428
+ ok: true,
282429
+ reachable,
282430
+ ready: body.ready === true,
282431
+ ...typeof body.reason === "string" ? { reason: body.reason } : {},
282432
+ failures: parseWireFailures(body.failures)
282433
+ };
282434
+ }
282435
+
282436
+ // src/cli/connect.ts
282437
+ init_connectSetup();
282438
+ function parseConnectArgs(argv) {
282439
+ let check2 = false;
282440
+ let register = false;
282441
+ let apply2 = false;
282442
+ let rotate = false;
282443
+ let url;
282444
+ let yes = false;
282445
+ let showSql = false;
282446
+ let scan = false;
282447
+ let tables = [];
282448
+ let role = ABLO_REPLICATION_ROLE;
282449
+ let writeRole = ABLO_WRITE_ROLE;
282450
+ let route = "public-allowlist";
282451
+ let start = 0;
282452
+ const lead = argv[0];
282453
+ if (lead !== void 0 && !lead.startsWith("-")) {
282454
+ switch (lead) {
282455
+ case "register":
282456
+ register = true;
282457
+ break;
282458
+ case "check":
282459
+ check2 = true;
282460
+ break;
282461
+ case "apply":
282462
+ apply2 = true;
282463
+ break;
282464
+ case "rotate":
282465
+ rotate = true;
282466
+ break;
282467
+ case "scan":
282468
+ scan = true;
282469
+ break;
282470
+ default:
282471
+ throw new AbloValidationError(
282472
+ `unknown connect subcommand: ${lead} (expected register, deregister, check, apply, rotate, scan)`,
282473
+ { code: "cli_invalid_arguments" }
282474
+ );
282475
+ }
282476
+ start = 1;
282477
+ }
282478
+ for (let i = start; i < argv.length; i++) {
282479
+ const arg = argv[i];
282480
+ switch (arg) {
282481
+ case "--url":
282482
+ url = argv[++i] ?? url;
282483
+ break;
282484
+ case "--yes":
282485
+ case "-y":
282486
+ yes = true;
282487
+ break;
282488
+ case "--show-sql":
282489
+ showSql = true;
282490
+ break;
282491
+ case "--tables": {
282492
+ const value = argv[++i] ?? "";
282493
+ tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
282494
+ break;
282495
+ }
282496
+ case "--role":
282497
+ role = argv[++i] ?? role;
282498
+ break;
282499
+ case "--write-role":
282500
+ writeRole = argv[++i] ?? writeRole;
282501
+ break;
282502
+ case "--route": {
282503
+ const value = argv[++i] ?? "";
282504
+ if (!DIRECT_DATA_SOURCE_ROUTES.includes(value)) {
282505
+ throw new AbloValidationError(
282506
+ `invalid direct route: ${value || "(missing)"} (expected ${DIRECT_DATA_SOURCE_ROUTES.join(", ")})`,
282507
+ { code: "cli_invalid_arguments" }
282508
+ );
282509
+ }
282510
+ route = value;
282511
+ break;
282512
+ }
282513
+ default:
282514
+ throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
282515
+ }
282516
+ }
282517
+ if (role === writeRole) {
282518
+ throw new AbloValidationError("replication and write roles must be different", {
282519
+ code: "cli_invalid_arguments"
282520
+ });
282521
+ }
282522
+ return {
282523
+ check: check2,
282524
+ register,
282525
+ apply: apply2,
282526
+ rotate,
282527
+ url,
282528
+ yes,
282529
+ showSql,
282530
+ scan,
282531
+ tables,
282532
+ role,
282533
+ writeRole,
282534
+ route
282535
+ };
282536
+ }
282537
+ function printConnectRecipe(args) {
282538
+ const sql = connectSetupSql({
282539
+ tables: args.tables,
282540
+ role: args.role,
282541
+ writeRole: args.writeRole
282542
+ });
282543
+ console.log(
282544
+ `
282545
+ ${brand("ablo")} ${import_picocolors9.default.dim("connect")} ${import_picocolors9.default.dim("direct writes + WAL-settled sync")}
282546
+ `
282547
+ );
282548
+ console.log(
282549
+ ` Ablo applies coordinated writes directly to your Postgres with a scoped DML role. WAL
282550
+ observes what committed, orders it with external writes, and confirms it in the sync log.
282551
+ Your database stays authoritative; Ablo never owns or migrates your application tables.
282552
+
282553
+ ${import_picocolors9.default.bold("ablo connect apply")} runs every step below for you from a one-time admin
282554
+ connection and leaves your app holding only ${import_picocolors9.default.bold("ABLO_API_KEY")}. To do it by hand,
282555
+ run this once against your Postgres ${import_picocolors9.default.dim("(as a superuser / DB owner)")}:
282556
+ `
282557
+ );
282558
+ console.log(
282559
+ ` ${import_picocolors9.default.bold("1.")} Enable logical decoding ${import_picocolors9.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`
282560
+ );
282561
+ console.log(` ${import_picocolors9.default.cyan(sql[0])}`);
282562
+ console.log(
282563
+ import_picocolors9.default.dim(
282564
+ ` Managed hosts don't take ALTER SYSTEM: on Amazon RDS / Aurora set ${import_picocolors9.default.bold("rds.logical_replication = 1")}
282565
+ in the parameter group and reboot; on Neon or Supabase enable logical replication in the
282566
+ project settings. ${import_picocolors9.default.bold("ablo connect apply")} detects the host and does the right thing.`
282567
+ )
282568
+ );
282569
+ console.log(`
282570
+ ${import_picocolors9.default.bold("2.")} Publish the tables Ablo should read`);
282571
+ console.log(` ${import_picocolors9.default.cyan(sql[1])}`);
282572
+ if (args.tables.length === 0) {
282573
+ console.log(
282574
+ import_picocolors9.default.dim(
282575
+ ` (Scope it with ${import_picocolors9.default.bold("ablo connect --tables a,b,c")} to publish a subset.)`
282576
+ )
282577
+ );
282578
+ }
282579
+ const writerStart = sql.findIndex((s) => s.includes("NOSUPERUSER NOBYPASSRLS"));
282580
+ const replicationStatements = sql.slice(2, writerStart);
282581
+ const writerStatements = sql.slice(writerStart);
282582
+ console.log(
282583
+ `
282584
+ ${import_picocolors9.default.bold("3.")} Create the scoped replication role ${import_picocolors9.default.dim("(pick your own replication password)")}`
282585
+ );
282586
+ for (const statement of replicationStatements) {
282587
+ for (const line of statement.split("\n")) console.log(` ${import_picocolors9.default.cyan(line)}`);
282588
+ }
282589
+ console.log(
282590
+ import_picocolors9.default.dim(
282591
+ ` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
282592
+ ${import_picocolors9.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
282593
+ )
282594
+ );
282595
+ console.log(
282596
+ `
282597
+ ${import_picocolors9.default.bold("4.")} Create the separate DML role and the idempotency ledger ` + import_picocolors9.default.dim("(pick your own write password)")
282598
+ );
282599
+ for (const statement of writerStatements) {
282600
+ for (const line of statement.split("\n")) console.log(` ${import_picocolors9.default.cyan(line)}`);
282601
+ }
282602
+ console.log(
282603
+ import_picocolors9.default.dim(
282604
+ ` The writer gets row DML + ledger access only. It has no REPLICATION, schema CREATE,
282605
+ role administration, database creation, ownership, or customer-table DDL. Direct uses
282606
+ ${import_picocolors9.default.bold("ablo_idempotency")} but no outbox; WAL carries the committed row changes.
282607
+ Each ledger row carries an ${import_picocolors9.default.bold("expires_at")}; the writer can't DELETE (tamper-
282608
+ resistance), so prune it from your own admin/cron when convenient:
282609
+ ${import_picocolors9.default.bold("DELETE FROM ablo_idempotency WHERE expires_at < now();")}`
282610
+ )
282611
+ );
282612
+ console.log(
282613
+ `
282614
+ ${import_picocolors9.default.bold("5.")} Register the two roles with Ablo. Set them just long enough to register \u2014
282615
+ Ablo holds them from here, so your app keeps only ${import_picocolors9.default.bold("ABLO_API_KEY")}:
282616
+ ${import_picocolors9.default.bold("ABLO_REPLICATION_DATABASE_URL")} ${import_picocolors9.default.dim(`\u2192 ${args.role} (replication only)`)}
282617
+ ${import_picocolors9.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors9.default.dim(`\u2192 ${args.writeRole} (DML only)`)}
282618
+ ${import_picocolors9.default.cyan("npx ablo connect register")}
282619
+ `
282620
+ );
282621
+ console.log(
282622
+ ` ${import_picocolors9.default.bold("6.")} Verify readiness ${import_picocolors9.default.dim("(checked from Ablo\u2019s side \u2014 needs only ABLO_API_KEY):")}
282623
+ ${import_picocolors9.default.cyan("npx ablo connect check")}
282624
+ `
282625
+ );
282626
+ console.log(
282627
+ import_picocolors9.default.dim(
282628
+ ` Reachable databases use this direct path. If no inbound route can be established, use the
282629
+ signed ${import_picocolors9.default.bold("dataSource()")} endpoint fallback; its correlated events confirm writes
282630
+ without an Ablo-side customer database socket.`
282631
+ )
282632
+ );
282633
+ console.log();
282634
+ }
282635
+ var SYNC_INFRA_RELATIONS = [
282636
+ "sync_deltas",
282637
+ "sync_id_seq",
282638
+ "sync_metadata",
282639
+ "mutation_log"
282640
+ ];
282641
+ var SYNC_INFRA_TYPES = ["participant_kind", "backfill_provenance", "confirmation_state"];
282642
+ function printCheckItem(item) {
282643
+ if (item.ok) {
282644
+ console.log(` ${import_picocolors9.default.green("\u2713")} ${item.label}`);
282645
+ } else {
282646
+ console.log(` ${import_picocolors9.default.red("\u2717")} ${item.label}`);
282647
+ if (item.fix) {
282648
+ for (const line of item.fix.split("\n")) console.log(` ${import_picocolors9.default.red("\u2022")} ${line}`);
282649
+ }
282650
+ }
282651
+ }
282652
+ async function probeDirectWriteReadiness(sql, opts = {}) {
282653
+ const schema = opts.schema ?? "public";
282654
+ const publication = opts.publication ?? ABLO_PUBLICATION;
282655
+ const items = [];
282656
+ const roleRows = await sql.unsafe(
282657
+ `SELECT rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole, rolreplication
282658
+ FROM pg_roles WHERE rolname = current_user`
282659
+ );
282660
+ const role = roleRows[0];
282661
+ const dangerous = Boolean(
282662
+ !role || role.rolsuper || role.rolbypassrls || role.rolcreatedb || role.rolcreaterole || role.rolreplication
282663
+ );
282664
+ items.push(
282665
+ dangerous ? {
282666
+ ok: false,
282667
+ label: `ABLO_WRITE_DATABASE_URL is not a scoped DML role`,
282668
+ fix: `Use ${ABLO_WRITE_ROLE} from the setup recipe: NOSUPERUSER, NOBYPASSRLS, NOCREATEDB, NOCREATEROLE, NOREPLICATION.`
282669
+ } : { ok: true, label: `write role ${import_picocolors9.default.bold(role?.rolname ?? ABLO_WRITE_ROLE)} is DML-only` }
282670
+ );
282671
+ const schemaRows = await sql.unsafe(
282672
+ `SELECT
282673
+ has_schema_privilege(current_user, $1, 'USAGE') AS usage,
282674
+ has_schema_privilege(current_user, $1, 'CREATE') AS create,
282675
+ current_setting('row_security') AS row_security`,
282676
+ [schema]
282677
+ );
282678
+ const schemaRow = schemaRows[0];
282679
+ items.push(
282680
+ schemaRow?.usage && !schemaRow.create && schemaRow.row_security === "on" ? { ok: true, label: `writer uses ${schema} with row_security on and no schema CREATE` } : {
282681
+ ok: false,
282682
+ label: `writer schema/RLS privileges are not least-privilege`,
282683
+ fix: `REVOKE CREATE ON SCHEMA ${quoteIdent(schema)} FROM PUBLIC; GRANT USAGE ON SCHEMA ${quoteIdent(schema)} TO ${quoteIdent(role?.rolname ?? ABLO_WRITE_ROLE)}; ALTER ROLE ${quoteIdent(role?.rolname ?? ABLO_WRITE_ROLE)} SET row_security = on;`
282684
+ }
282685
+ );
282686
+ const ledgerName = `${quoteIdent(schema)}.${quoteIdent("ablo_idempotency")}`;
282687
+ const ledgerRows = await sql.unsafe(
282688
+ `SELECT
282689
+ to_regclass($1) IS NOT NULL AS present,
282690
+ CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'SELECT,INSERT,UPDATE') END AS writes,
282691
+ CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'DELETE') END AS deletes`,
282692
+ [ledgerName]
282693
+ );
282694
+ const ledger = ledgerRows[0];
282695
+ items.push(
282696
+ ledger?.present && ledger.writes && !ledger.deletes ? { ok: true, label: `${import_picocolors9.default.bold("ablo_idempotency")} is durable and protected from DELETE` } : {
282697
+ ok: false,
282698
+ label: `${import_picocolors9.default.bold("ablo_idempotency")} is missing or has unsafe grants`,
282699
+ fix: `Apply the DML-role and idempotency-ledger statements printed by ${import_picocolors9.default.bold("ablo connect")}.`
282700
+ }
282701
+ );
282702
+ const tableRows = await sql.unsafe(
282703
+ `SELECT format('%I.%I', schemaname, tablename) AS relation
282704
+ FROM pg_publication_tables
282705
+ WHERE pubname = $1 AND schemaname = $2 AND tablename <> 'ablo_idempotency'
282706
+ AND NOT has_table_privilege(
282707
+ current_user,
282708
+ format('%I.%I', schemaname, tablename),
282709
+ 'SELECT,INSERT,UPDATE,DELETE'
282710
+ )`,
282711
+ [publication, schema]
282712
+ );
282713
+ items.push(
282714
+ tableRows.length === 0 ? { ok: true, label: `writer can apply DML to every published application table` } : {
282715
+ ok: false,
282716
+ label: `writer lacks DML on ${tableRows.length} published table${tableRows.length === 1 ? "" : "s"}`,
282717
+ fix: `Grant SELECT, INSERT, UPDATE, DELETE on: ${tableRows.map((row) => row.relation).join(", ")}`
282718
+ }
282719
+ );
282720
+ const rlsCoverageRows = await sql.unsafe(
282721
+ `SELECT
282722
+ count(*)::int AS total,
282723
+ (count(*) FILTER (WHERE c.relrowsecurity))::int AS with_rls
282724
+ FROM pg_publication_tables pt
282725
+ JOIN pg_class c ON c.relname = pt.tablename
282726
+ JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
282727
+ WHERE pt.pubname = $1 AND pt.schemaname = $2 AND pt.tablename <> 'ablo_idempotency'`,
282728
+ [publication, schema]
282729
+ );
282730
+ const coverage = rlsCoverageRows[0];
282731
+ if (coverage && coverage.total > 0) {
282732
+ const { total, with_rls: withRls } = coverage;
282733
+ const plural = total === 1 ? "" : "s";
282734
+ items.push(
282735
+ withRls === total ? {
282736
+ ok: true,
282737
+ label: `row-level security governs the writer on all ${total} published table${plural}`
282738
+ } : {
282739
+ ok: true,
282740
+ label: `row-level security governs the writer on ${withRls} of ${total} published tables \u2014 the other ${total - withRls} have no policies, so the writer's table grants alone bound it`
282741
+ }
282742
+ );
282743
+ }
282744
+ const ownerBypassRows = await sql.unsafe(
282745
+ `SELECT format('%I.%I', n.nspname, c.relname) AS relation
282746
+ FROM pg_publication_tables pt
282747
+ JOIN pg_class c ON c.relname = pt.tablename
282748
+ JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
282749
+ WHERE pt.pubname = $1 AND pt.schemaname = $2 AND pt.tablename <> 'ablo_idempotency'
282750
+ AND c.relowner = (SELECT oid FROM pg_roles WHERE rolname = current_user)
282751
+ AND c.relrowsecurity = true
282752
+ AND c.relforcerowsecurity = false`,
282753
+ [publication, schema]
282754
+ );
282755
+ items.push(
282756
+ ownerBypassRows.length === 0 ? {
282757
+ ok: true,
282758
+ label: `row-level security is enforced on the writer \u2014 it owns no published table that could bypass its own policies`
282759
+ } : {
282760
+ ok: false,
282761
+ label: `writer owns ${ownerBypassRows.length} published table${ownerBypassRows.length === 1 ? "" : "s"} whose RLS is enabled but not forced \u2014 its policies are silently skipped`,
282762
+ fix: `A table owner bypasses row-level security unless the table forces it. Reassign these tables to another owner, or force it: ${ownerBypassRows.map((r2) => `ALTER TABLE ${r2.relation} FORCE ROW LEVEL SECURITY;`).join(" ")}`
282763
+ }
282764
+ );
282765
+ return items;
282766
+ }
282767
+ async function auditTenantSyncInfra(sql) {
282768
+ const artifacts = [];
282769
+ for (const name of SYNC_INFRA_RELATIONS) {
282770
+ const rows = await sql.unsafe(`SELECT to_regclass($1)::text AS reg`, [
282771
+ `public.${name}`
282772
+ ]);
282773
+ artifacts.push({ kind: "relation", name, present: rows[0]?.reg != null });
282774
+ }
282775
+ for (const name of SYNC_INFRA_TYPES) {
282776
+ const rows = await sql.unsafe(`SELECT to_regtype($1)::text AS reg`, [
282777
+ `public.${name}`
282778
+ ]);
282779
+ artifacts.push({ kind: "type", name, present: rows[0]?.reg != null });
282780
+ }
282781
+ return artifacts;
282782
+ }
282783
+ function requireScopedUrl(kind, verb) {
282784
+ if (kind === "replication") {
282785
+ const resolved = readProjectReplicationUrlWithSource();
282786
+ if (resolved) return resolved.url;
282787
+ console.error(
282788
+ import_picocolors9.default.red(" No replication connection found (checked process env, .env.local, .env).") + import_picocolors9.default.dim(
282789
+ ` Set ${import_picocolors9.default.bold("ABLO_REPLICATION_DATABASE_URL")} to the ${ABLO_REPLICATION_ROLE} connection printed by ${import_picocolors9.default.bold("ablo connect")}, then re-run ${import_picocolors9.default.bold(`ablo connect ${verb}`)}.`
282790
+ )
282791
+ );
282792
+ process.exit(1);
282793
+ }
282794
+ const dbUrl = readProjectWriteDatabaseUrl();
282795
+ if (dbUrl) return dbUrl;
282796
+ console.error(
282797
+ import_picocolors9.default.red(" No ABLO_WRITE_DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors9.default.dim(
282798
+ ` Set it to the ${ABLO_WRITE_ROLE} connection printed by ${import_picocolors9.default.bold("ablo connect")}, then re-run ${import_picocolors9.default.bold(`ablo connect ${verb}`)}. The replication credential is never reused for DML.`
282799
+ )
282800
+ );
282801
+ process.exit(1);
282802
+ }
282803
+ async function probeAndReport(dbUrl, kind = "replication") {
282804
+ const sql = src_default(dbUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
282805
+ } });
282806
+ let items;
282807
+ try {
282808
+ items = kind === "replication" ? await probeReadiness(sql) : await probeDirectWriteReadiness(sql);
282809
+ } catch (err) {
282810
+ await sql.end({ timeout: 2 }).catch(() => void 0);
282811
+ const dial = dialFailureReason(err);
282812
+ if (dial) return { kind: "no-dial", reason: dial };
282813
+ const pg = err ?? {};
282814
+ console.error(import_picocolors9.default.red(` Couldn't read the database: ${pg.message ?? String(err)}`));
282815
+ process.exit(1);
282816
+ }
282817
+ await sql.end({ timeout: 2 });
282818
+ for (const item of items) printCheckItem(item);
282819
+ return { kind: "probed", failures: items.filter((i) => !i.ok).length };
282820
+ }
282821
+ async function runCheck() {
282822
+ console.log(
282823
+ `
282824
+ ${brand("ablo")} ${import_picocolors9.default.dim("connect check")} ${import_picocolors9.default.dim("direct-write + WAL readiness")}
282825
+ `
282826
+ );
282827
+ const apiKey = resolveApiKey();
282828
+ if (!apiKey) {
282829
+ console.error(
282830
+ import_picocolors9.default.red(" No API key found.") + import_picocolors9.default.dim(
282831
+ ` Run ${import_picocolors9.default.bold("ablo login")} (or set ${import_picocolors9.default.bold("ABLO_API_KEY")}), then re-run ${import_picocolors9.default.bold("ablo connect check")}.`
282832
+ )
282833
+ );
282834
+ process.exit(1);
282835
+ }
282836
+ const apiUrl2 = apiBaseUrl();
282837
+ const result = await requestRemoteValidation({ apiUrl: apiUrl2, apiKey });
282838
+ if (!result.ok) {
282839
+ if (result.code === "no_data_source_registered") {
282840
+ console.error(
282841
+ ` ${import_picocolors9.default.yellow("\u2014")} No database is connected to this plane yet, so there's nothing to check.
282842
+ ` + import_picocolors9.default.dim(
282843
+ ` Connect one with ${import_picocolors9.default.bold("ablo connect apply")}, then re-run ${import_picocolors9.default.bold("ablo connect check")}.
282844
+ `
282845
+ )
282846
+ );
282847
+ process.exit(1);
282848
+ }
282849
+ console.error(import_picocolors9.default.red(` The check failed: ${result.message}`));
282850
+ if (result.code === "forbidden") {
282851
+ console.error(
282852
+ import_picocolors9.default.dim(
282853
+ ` Checking a connected database needs a ${import_picocolors9.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors9.default.bold("ablo login")} for one.`
282854
+ )
282855
+ );
282856
+ }
282857
+ console.error();
282858
+ process.exit(1);
282859
+ }
282860
+ if (!result.reachable) {
282861
+ console.error(
282862
+ ` ${import_picocolors9.default.red("\u2717")} Ablo's infrastructure can't reach your database${result.reason ? ` ${import_picocolors9.default.dim(`(${result.reason})`)}` : ""}.`
282863
+ );
282864
+ console.error(
282865
+ import_picocolors9.default.dim(
282866
+ ` Direct needs a route Ablo's servers can dial \u2014 public allowlist, PrivateLink, peering,
282867
+ or VPN. Only when no inbound route can exist, use the signed ${import_picocolors9.default.bold("dataSource()")} endpoint fallback.
282868
+ `
282869
+ )
282870
+ );
282871
+ process.exit(1);
282872
+ }
282873
+ for (const failure of result.failures) {
282874
+ const { label, fix } = describeRemoteFailure(failure);
282875
+ printCheckItem({ ok: false, label, fix });
282876
+ }
282877
+ console.log();
282878
+ if (result.ready) {
282879
+ console.log(
282880
+ ` ${import_picocolors9.default.green("\u2713")} Ready \u2014 checked from Ablo's infrastructure. Ablo can apply scoped DML and settle it from WAL.
282881
+ `
282882
+ );
282883
+ process.exit(0);
282884
+ }
282885
+ const count = result.failures.length;
282886
+ console.log(
282887
+ ` ${import_picocolors9.default.red(`${count} item${count === 1 ? "" : "s"} to fix`)} ${import_picocolors9.default.dim(`\u2014 apply the fixes above, then re-run ${import_picocolors9.default.bold("ablo connect check")}.`)}
282888
+ `
282889
+ );
282890
+ process.exit(1);
282891
+ }
282892
+ async function runRegister(args) {
282893
+ const dbUrl = requireScopedUrl("replication", "register");
282894
+ const writeDbUrl = requireScopedUrl("write", "register");
282895
+ const apiKey = resolveApiKey();
282896
+ if (!apiKey) {
282897
+ console.error(
282898
+ import_picocolors9.default.red(" Not logged in.") + import_picocolors9.default.dim(
282899
+ ` Run ${import_picocolors9.default.bold("ablo login")} (or set ${import_picocolors9.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`
282900
+ )
282901
+ );
282902
+ process.exit(1);
282903
+ }
282904
+ console.log(
282905
+ `
282906
+ ${brand("ablo")} ${import_picocolors9.default.dim("connect register")} ${import_picocolors9.default.dim("register a direct DataSource")}
282907
+ `
282908
+ );
282909
+ console.log(` ${import_picocolors9.default.bold("Replication role")}
282910
+ `);
282911
+ const replication = await probeAndReport(dbUrl, "replication");
282912
+ console.log(`
282913
+ ${import_picocolors9.default.bold("Direct-write role")}
282914
+ `);
282915
+ const write = await probeAndReport(writeDbUrl, "write");
282916
+ const noDial = [
282917
+ replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
282918
+ write.kind === "no-dial" ? `write: ${write.reason}` : null
282919
+ ].filter((reason) => reason !== null);
282920
+ const failures = (replication.kind === "probed" ? replication.failures : 0) + (write.kind === "probed" ? write.failures : 0);
282921
+ if (noDial.length > 0) {
282922
+ console.log(
282923
+ ` This machine can't reach one or both scoped connections (${import_picocolors9.default.dim(noDial.join("; "))}) \u2014 continuing anyway.
282924
+ Ablo validates both credentials from the infrastructure that will use them and refuses
282925
+ registration unless replication and direct DML are both ready.
282926
+ `
282927
+ );
282928
+ }
282929
+ if (failures > 0) {
282930
+ console.log(
282931
+ `
282932
+ ${import_picocolors9.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors9.default.dim("\u2014 direct registration requires both scoped roles. Fix the above, then re-run.")}
282933
+ `
282934
+ );
282935
+ process.exit(1);
282936
+ }
282937
+ const apiUrl2 = apiBaseUrl();
282938
+ const registered = await registerDirectDataSource({
282939
+ apiUrl: apiUrl2,
282940
+ apiKey,
282941
+ replicationUrl: dbUrl,
282942
+ writeUrl: writeDbUrl,
282943
+ route: args.route
282944
+ });
282945
+ process.exit(registered ? 0 : 1);
282946
+ }
282947
+ async function runScan() {
282948
+ const dbUrl = requireScopedUrl("replication", "scan");
282949
+ const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
282950
+ } });
282951
+ let artifacts;
282952
+ try {
282953
+ artifacts = await auditTenantSyncInfra(sql);
282954
+ } catch (err) {
282955
+ const pg = err ?? {};
282956
+ console.error(import_picocolors9.default.red(` Couldn't audit the database: ${pg.message ?? String(err)}`));
282957
+ await sql.end({ timeout: 2 });
282958
+ process.exit(1);
282959
+ }
282960
+ await sql.end({ timeout: 2 });
282961
+ console.log(
282962
+ `
282963
+ ${brand("ablo")} ${import_picocolors9.default.dim("connect scan")} ${import_picocolors9.default.dim("audit for leftover Ablo sync infrastructure")}
282964
+ `
282965
+ );
282966
+ const present = artifacts.filter((a) => a.present);
282967
+ if (present.length === 0) {
282968
+ console.log(` ${import_picocolors9.default.green("\u2713")} No deprecated Ablo sync infrastructure found in public.
282969
+ `);
282970
+ process.exit(0);
282971
+ }
282972
+ for (const artifact of present) {
282973
+ const label = artifact.kind === "type" ? "type" : "relation";
282974
+ console.log(` ${import_picocolors9.default.yellow("!")} ${label} ${import_picocolors9.default.bold(`public.${artifact.name}`)} exists`);
282975
+ }
282976
+ console.log(
282977
+ `
282978
+ ${import_picocolors9.default.yellow(`${present.length} artifact${present.length === 1 ? "" : "s"} found`)} ` + import_picocolors9.default.dim(
282979
+ "\u2014 do not drop automatically. Confirm the org/environment is log-authoritative, then follow "
282980
+ ) + import_picocolors9.default.bold("docs/runbooks/wal-stage5-customer-db-infra-cleanup.md") + import_picocolors9.default.dim(".\n")
282981
+ );
282982
+ process.exit(1);
282983
+ }
282984
+ async function connect(argv) {
282985
+ if (argv[0] === "deregister") {
282986
+ const { disconnect: disconnect2 } = await Promise.resolve().then(() => (init_disconnect(), disconnect_exports));
282987
+ await disconnect2(argv.slice(1));
282988
+ return;
282989
+ }
282990
+ let args;
282991
+ try {
282992
+ args = parseConnectArgs(argv);
282993
+ } catch (err) {
282994
+ console.error(import_picocolors9.default.red(` ${err instanceof Error ? err.message : String(err)}`));
282995
+ process.exit(1);
282996
+ }
282997
+ if (args.apply || args.rotate) {
282998
+ const { runConnectApply: runConnectApply2 } = await Promise.resolve().then(() => (init_connectApply(), connectApply_exports));
282999
+ await runConnectApply2(args);
283000
+ return;
283001
+ }
283002
+ if (args.check) {
283003
+ await runCheck();
283004
+ return;
283005
+ }
283006
+ if (args.register) {
283007
+ await runRegister(args);
283008
+ return;
283009
+ }
283010
+ if (args.scan) {
283011
+ await runScan();
283012
+ return;
283013
+ }
283014
+ printConnectRecipe(args);
283015
+ }
283016
+ var CONNECT_USAGE = ` ablo connect \u2014 direct writes, settled by logical replication
283017
+
283018
+ Ablo applies coordinated DML with a scoped writer role. WAL observes what committed,
283019
+ orders it with external changes, and confirms it. Your Postgres remains authoritative.
283020
+
283021
+ Usage:
283022
+ npx ablo connect Print the exact setup SQL instead of running it
283023
+ npx ablo connect apply Set it up end to end: create the roles, publish, register
283024
+ npx ablo connect register Register both scoped credentials as one direct DataSource (hand-run-SQL path)
283025
+ npx ablo connect deregister Remove this project's data source \u2014 Ablo stops reading/writing it
283026
+ npx ablo connect check Verify the connected database from Ablo's side (needs only ABLO_API_KEY)
283027
+ npx ablo connect rotate New passwords for both scoped roles, then re-register
283028
+ npx ablo connect scan Read-only audit for leftover Ablo sync tables/types (never drops)
283029
+
283030
+ Modifiers:
283031
+ --url <admin-conn> Admin connection for apply/rotate (else DATABASE_URL)
283032
+ --tables a,b,c Publish only these tables (default: all tables)
283033
+ --role <name> Name the replication role (default: ablo_replicator)
283034
+ --write-role <name> Name the DML role (default: ablo_writer)
283035
+ --route <route> public-allowlist | privatelink | peering | vpn
283036
+ --yes Skip the apply confirmation (non-interactive)
283037
+ --show-sql Show the exact statements in the apply plan
283038
+
283039
+ apply registers with Ablo directly; the admin credential is used only on this
283040
+ machine and never persisted. Your app holds only ABLO_API_KEY.`;
283041
+
282647
283042
  // src/cli/index.ts
282648
- init_connect();
282649
283043
  init_push();
282650
283044
 
282651
283045
  // src/cli/generate.ts
@@ -282653,7 +283047,7 @@ init_cjs_shims();
282653
283047
  init_errors();
282654
283048
  var import_fs6 = require("fs");
282655
283049
  var import_path4 = require("path");
282656
- var import_picocolors8 = __toESM(require_picocolors(), 1);
283050
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
282657
283051
  var import_schema4 = require("@abloatai/ablo/schema");
282658
283052
  init_push();
282659
283053
  var DEFAULT_SCHEMA_PATH3 = "ablo/schema.ts";
@@ -282686,7 +283080,7 @@ async function generate(argv) {
282686
283080
  try {
282687
283081
  args = parseGenerateArgs(argv);
282688
283082
  } catch (err) {
282689
- console.error(import_picocolors8.default.red(` ${err instanceof Error ? err.message : String(err)}`));
283083
+ console.error(import_picocolors10.default.red(` ${err instanceof Error ? err.message : String(err)}`));
282690
283084
  process.exit(1);
282691
283085
  }
282692
283086
  let source;
@@ -282695,20 +283089,20 @@ async function generate(argv) {
282695
283089
  const schemaJson = JSON.parse((0, import_schema4.serializeSchema)(schema));
282696
283090
  source = (0, import_schema4.generateTypes)(schemaJson);
282697
283091
  } catch (err) {
282698
- console.error(import_picocolors8.default.red(` ${err instanceof Error ? err.message : String(err)}`));
283092
+ console.error(import_picocolors10.default.red(` ${err instanceof Error ? err.message : String(err)}`));
282699
283093
  process.exit(1);
282700
283094
  }
282701
283095
  const abs = (0, import_path4.resolve)(process.cwd(), args.out);
282702
283096
  (0, import_fs6.mkdirSync)((0, import_path4.dirname)(abs), { recursive: true });
282703
283097
  (0, import_fs6.writeFileSync)(abs, source);
282704
- console.log(` ${import_picocolors8.default.green("\u2713")} Generated types \u2192 ${import_picocolors8.default.bold(args.out)}`);
283098
+ console.log(` ${import_picocolors10.default.green("\u2713")} Generated types \u2192 ${import_picocolors10.default.bold(args.out)}`);
282705
283099
  }
282706
283100
 
282707
283101
  // src/cli/dev.ts
282708
283102
  init_cjs_shims();
282709
283103
  init_errors();
282710
283104
  init_credentialPolicy();
282711
- var import_picocolors9 = __toESM(require_picocolors(), 1);
283105
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
282712
283106
  init_dist2();
282713
283107
  var import_fs7 = require("fs");
282714
283108
  var import_path5 = require("path");
@@ -282750,25 +283144,25 @@ function classifyKey(apiKey, activeMode) {
282750
283144
  if (!apiKey) {
282751
283145
  return {
282752
283146
  ok: false,
282753
- reason: `No API key. Run ${import_picocolors9.default.bold("npx ablo login")} for the sandbox dev loop \u2014 or set ${import_picocolors9.default.bold("ABLO_API_KEY")} (${import_picocolors9.default.bold("sk_test_")} = sandbox; ${import_picocolors9.default.bold("sk_live_")} = deliberate production deploy). ` + import_picocolors9.default.dim(`Mode is currently '${activeMode}'.`)
283147
+ reason: `No API key. Run ${import_picocolors11.default.bold("npx ablo login")} for the sandbox dev loop \u2014 or set ${import_picocolors11.default.bold("ABLO_API_KEY")} (${import_picocolors11.default.bold("sk_test_")} = sandbox; ${import_picocolors11.default.bold("sk_live_")} = deliberate production deploy). ` + import_picocolors11.default.dim(`Mode is currently '${activeMode}'.`)
282754
283148
  };
282755
283149
  }
282756
283150
  if (apiKey.startsWith("sk_test_")) return { ok: true };
282757
283151
  if (apiKey.startsWith("sk_live_")) {
282758
283152
  return {
282759
283153
  ok: false,
282760
- reason: `Production schema deploys run one-shot: ${import_picocolors9.default.bold("ABLO_API_KEY=sk_live_\u2026 npx ablo push")} (or ${import_picocolors9.default.bold("ablo mode production")}). ${import_picocolors9.default.bold("--watch")} is sandbox-only.`
283154
+ reason: `Production schema deploys run one-shot: ${import_picocolors11.default.bold("ABLO_API_KEY=sk_live_\u2026 npx ablo push")} (or ${import_picocolors11.default.bold("ablo mode production")}). ${import_picocolors11.default.bold("--watch")} is sandbox-only.`
282761
283155
  };
282762
283156
  }
282763
283157
  if (classifyCredentialKind(apiKey) === "restricted") {
282764
283158
  return {
282765
283159
  ok: false,
282766
- reason: `Restricted (${import_picocolors9.default.bold("rk_")}) keys can't push schema. Use a secret key: ${import_picocolors9.default.bold("sk_test_")} for the sandbox dev loop, or ${import_picocolors9.default.bold("sk_live_")} with ${import_picocolors9.default.bold("npx ablo push")} for a production deploy.`
283160
+ reason: `Restricted (${import_picocolors11.default.bold("rk_")}) keys can't push schema. Use a secret key: ${import_picocolors11.default.bold("sk_test_")} for the sandbox dev loop, or ${import_picocolors11.default.bold("sk_live_")} with ${import_picocolors11.default.bold("npx ablo push")} for a production deploy.`
282767
283161
  };
282768
283162
  }
282769
283163
  return {
282770
283164
  ok: false,
282771
- reason: `${import_picocolors9.default.bold("ABLO_API_KEY")} is not an Ablo key \u2014 expected ${import_picocolors9.default.bold("sk_test_\u2026")} (sandbox) or ${import_picocolors9.default.bold("sk_live_\u2026")} (production deploy via ${import_picocolors9.default.bold("npx ablo push")}).`
283165
+ reason: `${import_picocolors11.default.bold("ABLO_API_KEY")} is not an Ablo key \u2014 expected ${import_picocolors11.default.bold("sk_test_\u2026")} (sandbox) or ${import_picocolors11.default.bold("sk_live_\u2026")} (production deploy via ${import_picocolors11.default.bold("npx ablo push")}).`
282772
283166
  };
282773
283167
  }
282774
283168
  function wireEnvLocal(apiKey, cwd = process.cwd()) {
@@ -282778,7 +283172,7 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
282778
283172
  if (!(0, import_fs7.existsSync)(envPath)) {
282779
283173
  (0, import_fs7.writeFileSync)(envPath, `${line}
282780
283174
  `, { mode: 384 });
282781
- action = `Created ${import_picocolors9.default.bold(".env.local")} with ${import_picocolors9.default.bold("ABLO_API_KEY")}`;
283175
+ action = `Created ${import_picocolors11.default.bold(".env.local")} with ${import_picocolors11.default.bold("ABLO_API_KEY")}`;
282782
283176
  } else {
282783
283177
  const content = (0, import_fs7.readFileSync)(envPath, "utf8");
282784
283178
  const match = /^ABLO_API_KEY=(.*)$/m.exec(content);
@@ -282786,12 +283180,12 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
282786
283180
  if (!match) {
282787
283181
  (0, import_fs7.appendFileSync)(envPath, `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${line}
282788
283182
  `);
282789
- action = `Added ${import_picocolors9.default.bold("ABLO_API_KEY")} to ${import_picocolors9.default.bold(".env.local")}`;
283183
+ action = `Added ${import_picocolors11.default.bold("ABLO_API_KEY")} to ${import_picocolors11.default.bold(".env.local")}`;
282790
283184
  } else if (existing === apiKey) {
282791
- action = `${import_picocolors9.default.bold(".env.local")} already has this key`;
283185
+ action = `${import_picocolors11.default.bold(".env.local")} already has this key`;
282792
283186
  } else {
282793
283187
  (0, import_fs7.writeFileSync)(envPath, content.replace(/^ABLO_API_KEY=.*$/m, line));
282794
- action = `Updated ${import_picocolors9.default.bold("ABLO_API_KEY")} in ${import_picocolors9.default.bold(".env.local")} ${import_picocolors9.default.dim(`(was ${existing.slice(0, 12)}\u2026)`)}`;
283188
+ action = `Updated ${import_picocolors11.default.bold("ABLO_API_KEY")} in ${import_picocolors11.default.bold(".env.local")} ${import_picocolors11.default.dim(`(was ${existing.slice(0, 12)}\u2026)`)}`;
282795
283189
  }
282796
283190
  }
282797
283191
  const gitignorePath = (0, import_path5.resolve)(cwd, ".gitignore");
@@ -282805,7 +283199,7 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
282805
283199
  `}.env.local
282806
283200
  `
282807
283201
  );
282808
- gitignoreNote = ` Added ${import_picocolors9.default.bold(".env.local")} to ${import_picocolors9.default.bold(".gitignore")} so the key can't be committed.`;
283202
+ gitignoreNote = ` Added ${import_picocolors11.default.bold(".env.local")} to ${import_picocolors11.default.bold(".gitignore")} so the key can't be committed.`;
282809
283203
  }
282810
283204
  return `${action}.${gitignoreNote}`;
282811
283205
  }
@@ -282820,7 +283214,7 @@ async function runPush(schema, args) {
282820
283214
  if (ok) {
282821
283215
  return {
282822
283216
  ok: true,
282823
- message: body.unchanged ? `schema unchanged ${import_picocolors9.default.dim(`(v${body.version})`)}` : `schema pushed (sandbox) ${import_picocolors9.default.dim(`(v${body.version}, hash ${body.hash})`)}`
283217
+ message: body.unchanged ? `schema unchanged ${import_picocolors11.default.dim(`(v${body.version})`)}` : `schema pushed (sandbox) ${import_picocolors11.default.dim(`(v${body.version}, hash ${body.hash})`)}`
282824
283218
  };
282825
283219
  }
282826
283220
  if (status2 === 409) {
@@ -282830,33 +283224,33 @@ async function runPush(schema, args) {
282830
283224
  (s) => s.shadowed != null
282831
283225
  );
282832
283226
  const lines = [
282833
- import_picocolors9.default.bold("Incompatible schema change \u2014 not safe to apply as-is."),
283227
+ import_picocolors11.default.bold("Incompatible schema change \u2014 not safe to apply as-is."),
282834
283228
  "",
282835
- ...unexecutable.map((u2) => import_picocolors9.default.red(fmtSignal(u2))),
282836
- ...warnings.map((w2) => import_picocolors9.default.yellow(fmtSignal(w2))),
283229
+ ...unexecutable.map((u2) => import_picocolors11.default.red(fmtSignal(u2))),
283230
+ ...warnings.map((w2) => import_picocolors11.default.yellow(fmtSignal(w2))),
282837
283231
  "",
282838
283232
  ...hasShadowed ? [
282839
- import_picocolors9.default.dim(
283233
+ import_picocolors11.default.dim(
282840
283234
  " These models exist in the baseline above but not in your push. Sandbox readers"
282841
283235
  ),
282842
- import_picocolors9.default.dim(
283236
+ import_picocolors11.default.dim(
282843
283237
  " fall back to the production schema until you push your own, so applying this drops them."
282844
283238
  ),
282845
283239
  ""
282846
283240
  ] : [],
282847
- import_picocolors9.default.dim(
282848
- ` Fix: ${import_picocolors9.default.bold("ablo push --force")} to apply anyway, or ${import_picocolors9.default.bold("--rename old:new")} if you renamed a model.`
283241
+ import_picocolors11.default.dim(
283242
+ ` Fix: ${import_picocolors11.default.bold("ablo push --force")} to apply anyway, or ${import_picocolors11.default.bold("--rename old:new")} if you renamed a model.`
282849
283243
  )
282850
283244
  ];
282851
283245
  return { ok: false, message: lines.join("\n") };
282852
283246
  }
282853
283247
  if (status2 === 403) {
282854
283248
  const serverSays = body.message ?? body.reason;
282855
- const hint = body.code === "database_role_cannot_enforce_rls" ? `Run ${import_picocolors9.default.bold("npx ablo migrate")} \u2014 it creates the scoped role for you (your DB credential never leaves this machine).` : `Schema authoring needs a ${import_picocolors9.default.bold("sandbox")} key with ${import_picocolors9.default.bold("schema:push")} \u2014 manage keys at ${import_picocolors9.default.cyan("https://abloatai.com")}.`;
283249
+ const hint = body.code === "database_role_cannot_enforce_rls" ? `Run ${import_picocolors11.default.bold("npx ablo migrate")} \u2014 it creates the scoped role for you (your DB credential never leaves this machine).` : `Schema authoring needs a ${import_picocolors11.default.bold("sandbox")} key with ${import_picocolors11.default.bold("schema:push")} \u2014 manage keys at ${import_picocolors11.default.cyan("https://abloatai.com")}.`;
282856
283250
  return {
282857
283251
  ok: false,
282858
283252
  message: `${serverSays ?? "This key can't author schema (missing schema:push scope)."}
282859
- ` + import_picocolors9.default.dim(hint)
283253
+ ` + import_picocolors11.default.dim(hint)
282860
283254
  };
282861
283255
  }
282862
283256
  return { ok: false, message: `Push failed (${status2}): ${body.message ?? body.reason ?? bodyText}` };
@@ -282866,25 +283260,25 @@ async function dev(argv) {
282866
283260
  try {
282867
283261
  args = parseDevArgs(argv);
282868
283262
  } catch (err) {
282869
- console.error(import_picocolors9.default.red(` ${err instanceof Error ? err.message : String(err)}`));
283263
+ console.error(import_picocolors11.default.red(` ${err instanceof Error ? err.message : String(err)}`));
282870
283264
  process.exit(1);
282871
283265
  }
282872
283266
  if (!args.apiKey) args.apiKey = resolveEffectiveApiKey("sandbox").key;
282873
283267
  const key = classifyKey(args.apiKey, getMode());
282874
283268
  if (!key.ok) {
282875
- console.error(import_picocolors9.default.red(` ${key.reason}`));
283269
+ console.error(import_picocolors11.default.red(` ${key.reason}`));
282876
283270
  process.exit(1);
282877
283271
  }
282878
283272
  console.log(`
282879
- ${brand("ablo")} ${import_picocolors9.default.dim("push")} ${import_picocolors9.default.dim("(sandbox)")}
283273
+ ${brand("ablo")} ${import_picocolors11.default.dim("push")} ${import_picocolors11.default.dim("(sandbox)")}
282880
283274
  `);
282881
283275
  const schema = await loadSchema(args.schemaPath, args.exportName);
282882
283276
  const modelCount = Object.keys(schema.models).length;
282883
283277
  console.log(
282884
- ` ${import_picocolors9.default.dim("schema")} ${import_picocolors9.default.bold(args.schemaPath)} ${import_picocolors9.default.dim(`(${modelCount} models, hash ${(0, import_schema5.schemaHash)(schema)})`)}`
283278
+ ` ${import_picocolors11.default.dim("schema")} ${import_picocolors11.default.bold(args.schemaPath)} ${import_picocolors11.default.dim(`(${modelCount} models, hash ${(0, import_schema5.schemaHash)(schema)})`)}`
282885
283279
  );
282886
- console.log(` ${import_picocolors9.default.dim("key")} ${args.apiKey.slice(0, 12)}\u2026`);
282887
- console.log(` ${import_picocolors9.default.dim("api")} ${args.url}
283280
+ console.log(` ${import_picocolors11.default.dim("key")} ${args.apiKey.slice(0, 12)}\u2026`);
283281
+ console.log(` ${import_picocolors11.default.dim("api")} ${args.url}
282888
283282
  `);
282889
283283
  const s = Y2();
282890
283284
  s.start("Pushing schema definition (sandbox)");
@@ -282893,16 +283287,16 @@ async function dev(argv) {
282893
283287
  if (!first.ok) process.exit(1);
282894
283288
  if (process.env.ABLO_API_KEY) {
282895
283289
  console.log(`
282896
- ${import_picocolors9.default.green("\u2713")} ${import_picocolors9.default.bold("ABLO_API_KEY")} is set in this shell \u2014 the SDK reads it directly.`);
283290
+ ${import_picocolors11.default.green("\u2713")} ${import_picocolors11.default.bold("ABLO_API_KEY")} is set in this shell \u2014 the SDK reads it directly.`);
282897
283291
  } else {
282898
283292
  console.log(`
282899
- ${import_picocolors9.default.green("\u2713")} ${wireEnvLocal(args.apiKey)}`);
282900
- console.log(` ${import_picocolors9.default.dim("Frameworks load it automatically; plain Node: node --env-file=.env.local app.ts")}`);
283293
+ ${import_picocolors11.default.green("\u2713")} ${wireEnvLocal(args.apiKey)}`);
283294
+ console.log(` ${import_picocolors11.default.dim("Frameworks load it automatically; plain Node: node --env-file=.env.local app.ts")}`);
282901
283295
  }
282902
283296
  console.log(` Your app is wired for the sandbox.`);
282903
283297
  if (!args.watch) return;
282904
283298
  const abs = (0, import_path5.resolve)(process.cwd(), args.schemaPath);
282905
- console.log(` ${import_picocolors9.default.dim(`watching ${args.schemaPath} \u2026 (Ctrl-C to stop)`)}
283299
+ console.log(` ${import_picocolors11.default.dim(`watching ${args.schemaPath} \u2026 (Ctrl-C to stop)`)}
282906
283300
  `);
282907
283301
  let timer2 = null;
282908
283302
  let pushing = false;
@@ -282922,7 +283316,7 @@ async function dev(argv) {
282922
283316
  const r2 = await runPush(next, args);
282923
283317
  s2.stop(r2.message, r2.ok ? 0 : 1);
282924
283318
  } catch (err) {
282925
- s2.stop(import_picocolors9.default.red(`schema reload failed: ${err instanceof Error ? err.message : String(err)}`), 1);
283319
+ s2.stop(import_picocolors11.default.red(`schema reload failed: ${err instanceof Error ? err.message : String(err)}`), 1);
282926
283320
  } finally {
282927
283321
  pushing = false;
282928
283322
  }
@@ -282930,7 +283324,7 @@ async function dev(argv) {
282930
283324
  const stop = () => {
282931
283325
  watcher.close();
282932
283326
  console.log(`
282933
- ${import_picocolors9.default.dim("stopped.")}`);
283327
+ ${import_picocolors11.default.dim("stopped.")}`);
282934
283328
  process.exit(0);
282935
283329
  };
282936
283330
  process.on("SIGINT", stop);
@@ -282942,7 +283336,7 @@ async function dev(argv) {
282942
283336
  // src/cli/login.ts
282943
283337
  init_cjs_shims();
282944
283338
  var import_child_process2 = require("child_process");
282945
- var import_picocolors10 = __toESM(require_picocolors(), 1);
283339
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
282946
283340
  init_dist2();
282947
283341
  init_config();
282948
283342
  init_theme();
@@ -283003,9 +283397,9 @@ async function deviceLogin(argv, deps = {}) {
283003
283397
  const code = await codeRes.json();
283004
283398
  const approvePath = `/cli?user_code=${code.user_code}`;
283005
283399
  const url = account === "signup" ? `${DASHBOARD_URL}/signup?next=${encodeURIComponent(approvePath)}` : `${DASHBOARD_URL}${approvePath}`;
283006
- Me(`${import_picocolors10.default.bold(code.user_code)}
283400
+ Me(`${import_picocolors12.default.bold(code.user_code)}
283007
283401
 
283008
- ${import_picocolors10.default.dim(url)}`, "Approve in your browser");
283402
+ ${import_picocolors12.default.dim(url)}`, "Approve in your browser");
283009
283403
  openUrl(url);
283010
283404
  const s = Y2();
283011
283405
  s.start("Waiting for approval\u2026");
@@ -283072,7 +283466,7 @@ ${import_picocolors10.default.dim(url)}`, "Approve in your browser");
283072
283466
  if (reason) M2.error(reason);
283073
283467
  else if (provRes) M2.error(`Key provisioning returned ${provRes.status} from ${DASHBOARD_URL}/api/cli/provision-key.`);
283074
283468
  M2.error(
283075
- `The browser approval succeeded but the key handoff failed. Try again, or grab a ${import_picocolors10.default.bold("sk_test_")} key from the dashboard and set ${import_picocolors10.default.bold("ABLO_API_KEY")}.`
283469
+ `The browser approval succeeded but the key handoff failed. Try again, or grab a ${import_picocolors12.default.bold("sk_test_")} key from the dashboard and set ${import_picocolors12.default.bold("ABLO_API_KEY")}.`
283076
283470
  );
283077
283471
  process.exit(1);
283078
283472
  }
@@ -283092,9 +283486,9 @@ ${import_picocolors10.default.dim(url)}`, "Approve in your browser");
283092
283486
  { mode: "sandbox", activeProject: prov.project ?? void 0 }
283093
283487
  );
283094
283488
  s.stop(`Saved keys to ${path}`);
283095
- const where = prov.project ? ` ${import_picocolors10.default.dim(`(project ${prov.project.slug})`)}` : "";
283489
+ const where = prov.project ? ` ${import_picocolors12.default.dim(`(project ${prov.project.slug})`)}` : "";
283096
283490
  Se(
283097
- `${import_picocolors10.default.green("\u2713")} Logged in ${import_picocolors10.default.dim("(sandbox)")}${where}. Run ${import_picocolors10.default.bold("npx ablo push")} to push your schema.`
283491
+ `${import_picocolors12.default.green("\u2713")} Logged in ${import_picocolors12.default.dim("(sandbox)")}${where}. Run ${import_picocolors12.default.bold("npx ablo push")} to push your schema.`
283098
283492
  );
283099
283493
  }
283100
283494
  async function login(argv = [], deps = {}) {
@@ -283103,13 +283497,13 @@ async function login(argv = [], deps = {}) {
283103
283497
  function logout() {
283104
283498
  const removed = clearCredential();
283105
283499
  if (removed) {
283106
- console.log(` ${import_picocolors10.default.green("\u2713")} Logged out ${import_picocolors10.default.dim(`(credentials removed from ${configDir()})`)}`);
283500
+ console.log(` ${import_picocolors12.default.green("\u2713")} Logged out ${import_picocolors12.default.dim(`(credentials removed from ${configDir()})`)}`);
283107
283501
  } else {
283108
- console.log(` ${import_picocolors10.default.dim("\u25CB")} Not logged in \u2014 nothing to remove.`);
283502
+ console.log(` ${import_picocolors12.default.dim("\u25CB")} Not logged in \u2014 nothing to remove.`);
283109
283503
  }
283110
283504
  if (process.env.ABLO_API_KEY) {
283111
283505
  console.log(
283112
- import_picocolors10.default.dim(` Note: ${import_picocolors10.default.bold("ABLO_API_KEY")} is still set in this shell and takes precedence.`)
283506
+ import_picocolors12.default.dim(` Note: ${import_picocolors12.default.bold("ABLO_API_KEY")} is still set in this shell and takes precedence.`)
283113
283507
  );
283114
283508
  }
283115
283509
  }
@@ -283119,7 +283513,7 @@ init_config();
283119
283513
 
283120
283514
  // src/cli/mode.ts
283121
283515
  init_cjs_shims();
283122
- var import_picocolors11 = __toESM(require_picocolors(), 1);
283516
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
283123
283517
  init_dist2();
283124
283518
  init_config();
283125
283519
  var PREFIX = { sandbox: "sk_test_", production: "rk_live_" };
@@ -283131,10 +283525,10 @@ function hintFor(m2, current) {
283131
283525
  }
283132
283526
  function apply(m2) {
283133
283527
  setMode(m2);
283134
- console.log(` ${import_picocolors11.default.green("\u2713")} now in ${import_picocolors11.default.bold(m2)}`);
283528
+ console.log(` ${import_picocolors13.default.green("\u2713")} now in ${import_picocolors13.default.bold(m2)}`);
283135
283529
  if (!getKeyEntry(m2)) {
283136
283530
  console.log(
283137
- import_picocolors11.default.dim(` No ${m2} key stored \u2014 run ${import_picocolors11.default.bold("ablo login")} or ${import_picocolors11.default.bold(`ablo login --api-key ${PREFIX[m2]}\u2026`)}.`)
283531
+ import_picocolors13.default.dim(` No ${m2} key stored \u2014 run ${import_picocolors13.default.bold("ablo login")} or ${import_picocolors13.default.bold(`ablo login --api-key ${PREFIX[m2]}\u2026`)}.`)
283138
283532
  );
283139
283533
  }
283140
283534
  }
@@ -283147,14 +283541,14 @@ async function mode(argv) {
283147
283541
  }
283148
283542
  if (arg) {
283149
283543
  console.error(
283150
- import_picocolors11.default.red(` unknown mode: ${arg}`) + import_picocolors11.default.dim(` (expected ${import_picocolors11.default.bold("sandbox")} or ${import_picocolors11.default.bold("production")})`)
283544
+ import_picocolors13.default.red(` unknown mode: ${arg}`) + import_picocolors13.default.dim(` (expected ${import_picocolors13.default.bold("sandbox")} or ${import_picocolors13.default.bold("production")})`)
283151
283545
  );
283152
283546
  process.exit(1);
283153
283547
  }
283154
283548
  const current = getMode();
283155
283549
  if (!process.stdin.isTTY || process.env.CI) {
283156
283550
  console.error(
283157
- import_picocolors11.default.red(" `ablo mode` needs an argument without a TTY: ") + import_picocolors11.default.bold("ablo mode sandbox") + import_picocolors11.default.dim(" | ") + import_picocolors11.default.bold("ablo mode production") + import_picocolors11.default.dim(` (current: ${current})`)
283551
+ import_picocolors13.default.red(" `ablo mode` needs an argument without a TTY: ") + import_picocolors13.default.bold("ablo mode sandbox") + import_picocolors13.default.dim(" | ") + import_picocolors13.default.bold("ablo mode production") + import_picocolors13.default.dim(` (current: ${current})`)
283158
283552
  );
283159
283553
  process.exit(1);
283160
283554
  }
@@ -283178,7 +283572,7 @@ init_projects();
283178
283572
 
283179
283573
  // src/cli/status.ts
283180
283574
  init_cjs_shims();
283181
- var import_picocolors12 = __toESM(require_picocolors(), 1);
283575
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
283182
283576
  init_config();
283183
283577
  init_target();
283184
283578
  init_theme();
@@ -283186,9 +283580,9 @@ init_push();
283186
283580
  function expiryLabel(iso) {
283187
283581
  const ms = Date.parse(iso) - Date.now();
283188
283582
  if (Number.isNaN(ms)) return "";
283189
- if (ms <= 0) return import_picocolors12.default.red("expired");
283583
+ if (ms <= 0) return import_picocolors14.default.red("expired");
283190
283584
  const days = Math.floor(ms / (24 * 60 * 60 * 1e3));
283191
- return import_picocolors12.default.dim(days > 0 ? `expires in ${days}d` : "expires <1d");
283585
+ return import_picocolors14.default.dim(days > 0 ? `expires in ${days}d` : "expires <1d");
283192
283586
  }
283193
283587
  async function ping(apiUrl2) {
283194
283588
  const ctrl = new AbortController();
@@ -283272,31 +283666,31 @@ function formatConflict(conflict) {
283272
283666
  function printTargetLines(target, localProject) {
283273
283667
  const confirmed = target?.confirmed ?? null;
283274
283668
  if (confirmed?.organizationId) {
283275
- console.log(` ${import_picocolors12.default.dim("org")} ${import_picocolors12.default.dim(confirmed.organizationId)}`);
283669
+ console.log(` ${import_picocolors14.default.dim("org")} ${import_picocolors14.default.dim(confirmed.organizationId)}`);
283276
283670
  }
283277
283671
  let projectLine;
283278
283672
  if (confirmed?.project) {
283279
283673
  const p2 = confirmed.project;
283280
- projectLine = p2.isDefault ? `${import_picocolors12.default.bold("default")} ${import_picocolors12.default.dim("(org-default)")}` : `${import_picocolors12.default.bold(p2.slug)} ${import_picocolors12.default.dim(`(${p2.id})`)}`;
283674
+ projectLine = p2.isDefault ? `${import_picocolors14.default.bold("default")} ${import_picocolors14.default.dim("(org-default)")}` : `${import_picocolors14.default.bold(p2.slug)} ${import_picocolors14.default.dim(`(${p2.id})`)}`;
283281
283675
  } else if (confirmed) {
283282
- projectLine = `${import_picocolors12.default.bold("default")} ${import_picocolors12.default.dim("(org-default)")}`;
283676
+ projectLine = `${import_picocolors14.default.bold("default")} ${import_picocolors14.default.dim("(org-default)")}`;
283283
283677
  } else if (localProject) {
283284
- projectLine = `${import_picocolors12.default.bold(localProject.slug)} ${import_picocolors12.default.dim(`(${localProject.id})`)} ${import_picocolors12.default.yellow("(unconfirmed)")}`;
283678
+ projectLine = `${import_picocolors14.default.bold(localProject.slug)} ${import_picocolors14.default.dim(`(${localProject.id})`)} ${import_picocolors14.default.yellow("(unconfirmed)")}`;
283285
283679
  } else {
283286
- projectLine = `${import_picocolors12.default.bold("default")} ${target ? import_picocolors12.default.yellow("(unconfirmed)") : import_picocolors12.default.dim("(org-default)")}`;
283680
+ projectLine = `${import_picocolors14.default.bold("default")} ${target ? import_picocolors14.default.yellow("(unconfirmed)") : import_picocolors14.default.dim("(org-default)")}`;
283287
283681
  }
283288
- console.log(` ${import_picocolors12.default.dim("project")} ${projectLine}`);
283682
+ console.log(` ${import_picocolors14.default.dim("project")} ${projectLine}`);
283289
283683
  const env = confirmed?.environment ?? target?.keyEnv ?? null;
283290
283684
  if (env) {
283291
- const suffix = confirmed ? "" : ` ${import_picocolors12.default.yellow("(unconfirmed)")}`;
283292
- console.log(` ${import_picocolors12.default.dim("env")} ${import_picocolors12.default.bold(env)}${suffix}`);
283685
+ const suffix = confirmed ? "" : ` ${import_picocolors14.default.yellow("(unconfirmed)")}`;
283686
+ console.log(` ${import_picocolors14.default.dim("env")} ${import_picocolors14.default.bold(env)}${suffix}`);
283293
283687
  }
283294
283688
  for (const m2 of target?.mismatches ?? []) {
283295
- console.log(` ${import_picocolors12.default.yellow("\u26A0")} ${import_picocolors12.default.yellow(describeMismatch(m2))}`);
283689
+ console.log(` ${import_picocolors14.default.yellow("\u26A0")} ${import_picocolors14.default.yellow(describeMismatch(m2))}`);
283296
283690
  }
283297
283691
  }
283298
283692
  async function status(args = []) {
283299
- const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
283693
+ const apiUrl2 = apiBaseUrl();
283300
283694
  const cfg = readConfig();
283301
283695
  const mode2 = getMode();
283302
283696
  const effective = resolveEffectiveApiKey();
@@ -283363,80 +283757,80 @@ async function status(args = []) {
283363
283757
  return;
283364
283758
  }
283365
283759
  console.log(`
283366
- ${brand("ablo")} ${import_picocolors12.default.dim("status")}
283760
+ ${brand("ablo")} ${import_picocolors14.default.dim("status")}
283367
283761
  `);
283368
283762
  if (effective.key && effective.source && effective.source !== "stored") {
283369
283763
  const label = effective.source === "env" ? "ABLO_API_KEY env" : effective.source;
283370
283764
  console.log(
283371
- ` ${import_picocolors12.default.dim("key")} ${effective.key.slice(0, 12)}\u2026 ${import_picocolors12.default.dim(`(${label} \u2014 overrides stored)`)}`
283765
+ ` ${import_picocolors14.default.dim("key")} ${effective.key.slice(0, 12)}\u2026 ${import_picocolors14.default.dim(`(${label} \u2014 overrides stored)`)}`
283372
283766
  );
283373
283767
  } else if (!cfg) {
283374
- console.log(` ${import_picocolors12.default.yellow("!")} Not logged in \u2014 run ${import_picocolors12.default.bold("ablo login")}.`);
283768
+ console.log(` ${import_picocolors14.default.yellow("!")} Not logged in \u2014 run ${import_picocolors14.default.bold("ablo login")}.`);
283375
283769
  }
283376
- console.log(` ${import_picocolors12.default.dim("mode")} ${import_picocolors12.default.bold(mode2)}`);
283770
+ console.log(` ${import_picocolors14.default.dim("mode")} ${import_picocolors14.default.bold(mode2)}`);
283377
283771
  const activeEntry = getKeyEntry(mode2);
283378
283772
  const key = describeEffectiveKey(mode2, process.env.ABLO_API_KEY, activeEntry);
283379
283773
  if (key.keyMismatch) {
283380
- console.log(` ${import_picocolors12.default.yellow("!")} ${import_picocolors12.default.yellow(key.keyMismatch.message)}`);
283774
+ console.log(` ${import_picocolors14.default.yellow("!")} ${import_picocolors14.default.yellow(key.keyMismatch.message)}`);
283381
283775
  }
283382
283776
  const activeProject = getActiveProject();
283383
283777
  printTargetLines(target, activeProject);
283384
283778
  for (const m2 of ["sandbox", "production"]) {
283385
283779
  const entry = getKeyEntry(m2);
283386
- const marker = m2 === mode2 ? import_picocolors12.default.green("\u25CF") : import_picocolors12.default.dim("\u25CB");
283780
+ const marker = m2 === mode2 ? import_picocolors14.default.green("\u25CF") : import_picocolors14.default.dim("\u25CB");
283387
283781
  if (entry) {
283388
283782
  const exp = entry.expiresAt ? ` ${expiryLabel(entry.expiresAt)}` : "";
283389
- console.log(` ${marker} ${m2.padEnd(10)} ${import_picocolors12.default.dim(`${entry.apiKey.slice(0, 12)}\u2026`)}${exp}`);
283783
+ console.log(` ${marker} ${m2.padEnd(10)} ${import_picocolors14.default.dim(`${entry.apiKey.slice(0, 12)}\u2026`)}${exp}`);
283390
283784
  } else {
283391
- console.log(` ${marker} ${m2.padEnd(10)} ${import_picocolors12.default.dim("\u2014 no key")}`);
283785
+ console.log(` ${marker} ${m2.padEnd(10)} ${import_picocolors14.default.dim("\u2014 no key")}`);
283392
283786
  }
283393
283787
  }
283394
283788
  const org = target?.confirmed?.organizationId ?? activeEntry?.organizationId;
283395
283789
  const plan = resolvePushPlan();
283396
283790
  console.log(
283397
- ` ${import_picocolors12.default.dim("push")} ${plan.apiKey ? `${import_picocolors12.default.bold(plan.flow)} ${import_picocolors12.default.dim(`with ${plan.apiKey.slice(0, 12)}\u2026 (${plan.source})`)}` : `${import_picocolors12.default.bold(plan.flow)} ${import_picocolors12.default.yellow("\u2014 no credential")} ${import_picocolors12.default.dim(`(run ${import_picocolors12.default.bold("ablo login")} or set ${import_picocolors12.default.bold("ABLO_API_KEY")})`)}`}`
283791
+ ` ${import_picocolors14.default.dim("push")} ${plan.apiKey ? `${import_picocolors14.default.bold(plan.flow)} ${import_picocolors14.default.dim(`with ${plan.apiKey.slice(0, 12)}\u2026 (${plan.source})`)}` : `${import_picocolors14.default.bold(plan.flow)} ${import_picocolors14.default.yellow("\u2014 no credential")} ${import_picocolors14.default.dim(`(run ${import_picocolors14.default.bold("ablo login")} or set ${import_picocolors14.default.bold("ABLO_API_KEY")})`)}`}`
283398
283792
  );
283399
- process.stdout.write(` ${import_picocolors12.default.dim("api")} ${apiUrl2} `);
283793
+ process.stdout.write(` ${import_picocolors14.default.dim("api")} ${apiUrl2} `);
283400
283794
  const reachable = await ping(apiUrl2);
283401
- console.log(reachable ? import_picocolors12.default.green("reachable") : import_picocolors12.default.red("unreachable"));
283795
+ console.log(reachable ? import_picocolors14.default.green("reachable") : import_picocolors14.default.red("unreachable"));
283402
283796
  if (reachable) {
283403
283797
  const introspectKey = effective.key;
283404
283798
  const pushed = await fetchPushedSchema(apiUrl2, introspectKey);
283405
283799
  if (pushed?.active) {
283406
- const when = pushed.pushedAt ? ` ${import_picocolors12.default.dim(`@ ${pushed.pushedAt.slice(0, 10)}`)}` : "";
283407
- const ver = pushed.version != null ? ` ${import_picocolors12.default.dim(`(rev ${pushed.version})`)}` : "";
283408
- const hashLabel = pushed.hash ? ` ${import_picocolors12.default.dim(`hash ${pushed.hash}`)}` : "";
283409
- console.log(` ${import_picocolors12.default.dim("schema")} ${import_picocolors12.default.bold(`${pushed.models.length} models pushed`)}${ver}${hashLabel}${when}`);
283800
+ const when = pushed.pushedAt ? ` ${import_picocolors14.default.dim(`@ ${pushed.pushedAt.slice(0, 10)}`)}` : "";
283801
+ const ver = pushed.version != null ? ` ${import_picocolors14.default.dim(`(rev ${pushed.version})`)}` : "";
283802
+ const hashLabel = pushed.hash ? ` ${import_picocolors14.default.dim(`hash ${pushed.hash}`)}` : "";
283803
+ console.log(` ${import_picocolors14.default.dim("schema")} ${import_picocolors14.default.bold(`${pushed.models.length} models pushed`)}${ver}${hashLabel}${when}`);
283410
283804
  for (const m2 of pushed.models) {
283411
- const tn = m2.typename === m2.key ? import_picocolors12.default.dim(`typename=${m2.typename}`) : import_picocolors12.default.yellow(`typename=${m2.typename}`);
283805
+ const tn = m2.typename === m2.key ? import_picocolors14.default.dim(`typename=${m2.typename}`) : import_picocolors14.default.yellow(`typename=${m2.typename}`);
283412
283806
  const conflict = formatConflict(m2.conflict);
283413
- const conflictStr2 = conflict ? ` ${import_picocolors12.default.dim(`conflict=${conflict}`)}` : "";
283414
- console.log(` ${import_picocolors12.default.dim("\u2022")} ${m2.key.padEnd(14)} ${tn}${conflictStr2}`);
283807
+ const conflictStr2 = conflict ? ` ${import_picocolors14.default.dim(`conflict=${conflict}`)}` : "";
283808
+ console.log(` ${import_picocolors14.default.dim("\u2022")} ${m2.key.padEnd(14)} ${tn}${conflictStr2}`);
283415
283809
  }
283416
283810
  } else if (pushed && !pushed.active) {
283417
- console.log(` ${import_picocolors12.default.dim("schema")} ${import_picocolors12.default.yellow("none pushed")} ${import_picocolors12.default.dim(`(run ${import_picocolors12.default.bold("ablo push")} or ${import_picocolors12.default.bold("ablo dev")})`)}`);
283811
+ console.log(` ${import_picocolors14.default.dim("schema")} ${import_picocolors14.default.yellow("none pushed")} ${import_picocolors14.default.dim(`(run ${import_picocolors14.default.bold("ablo push")} or ${import_picocolors14.default.bold("ablo dev")})`)}`);
283418
283812
  }
283419
283813
  const firstPushedModel = pushed?.active ? pushed.models[0] : void 0;
283420
283814
  if (firstPushedModel !== void 0) {
283421
283815
  const probe = await probeDataPlane(apiUrl2, introspectKey, firstPushedModel.typename);
283422
283816
  if (probe.status === "no_database") {
283423
- console.log(` ${import_picocolors12.default.dim("data")} ${import_picocolors12.default.red("\u2717 no database registered")}${org ? import_picocolors12.default.dim(` for org ${org}`) : ""}`);
283817
+ console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.red("\u2717 no database registered")}${org ? import_picocolors14.default.dim(` for org ${org}`) : ""}`);
283424
283818
  console.log(
283425
- ` ${import_picocolors12.default.dim(
283426
- `reads/writes will fail with ${import_picocolors12.default.bold("tenant_routing_failed")}. Connect one with ${import_picocolors12.default.bold("ablo connect")}, or point ${import_picocolors12.default.bold("ABLO_API_KEY")} at an org that has a database.`
283819
+ ` ${import_picocolors14.default.dim(
283820
+ `reads/writes will fail with ${import_picocolors14.default.bold("tenant_routing_failed")}. Connect one with ${import_picocolors14.default.bold("ablo connect")}, or point ${import_picocolors14.default.bold("ABLO_API_KEY")} at an org that has a database.`
283427
283821
  )}`
283428
283822
  );
283429
283823
  } else if (probe.status === "intermittent") {
283430
- console.log(` ${import_picocolors12.default.dim("data")} ${import_picocolors12.default.red(`\u2717 database routing is intermittent`)} ${import_picocolors12.default.dim(`(${probe.ok} ok / ${probe.failed} failed of ${probe.ok + probe.failed})`)}${org ? import_picocolors12.default.dim(` for org ${org}`) : ""}`);
283824
+ console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.red(`\u2717 database routing is intermittent`)} ${import_picocolors14.default.dim(`(${probe.ok} ok / ${probe.failed} failed of ${probe.ok + probe.failed})`)}${org ? import_picocolors14.default.dim(` for org ${org}`) : ""}`);
283431
283825
  console.log(
283432
- ` ${import_picocolors12.default.dim(
283433
- `some reads/writes fail with ${import_picocolors12.default.bold("tenant_routing_failed")} \u2014 the registration is unstable. Re-establish it with ${import_picocolors12.default.bold("ablo connect")} (or check for a recent server redeploy).`
283826
+ ` ${import_picocolors14.default.dim(
283827
+ `some reads/writes fail with ${import_picocolors14.default.bold("tenant_routing_failed")} \u2014 the registration is unstable. Re-establish it with ${import_picocolors14.default.bold("ablo connect")} (or check for a recent server redeploy).`
283434
283828
  )}`
283435
283829
  );
283436
283830
  } else if (probe.status === "forbidden") {
283437
- console.log(` ${import_picocolors12.default.dim("data")} ${import_picocolors12.default.yellow("? key not authorized to read")}${probe.detail ? import_picocolors12.default.dim(` (${probe.detail})`) : ""}`);
283831
+ console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.yellow("? key not authorized to read")}${probe.detail ? import_picocolors14.default.dim(` (${probe.detail})`) : ""}`);
283438
283832
  } else if (probe.status === "unknown") {
283439
- console.log(` ${import_picocolors12.default.dim("data")} ${import_picocolors12.default.yellow(`? data-plane check inconclusive (${probe.detail})`)}`);
283833
+ console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.yellow(`? data-plane check inconclusive (${probe.detail})`)}`);
283440
283834
  }
283441
283835
  }
283442
283836
  }
@@ -283446,7 +283840,7 @@ async function status(args = []) {
283446
283840
  // src/cli/logs.ts
283447
283841
  init_cjs_shims();
283448
283842
  init_errors();
283449
- var import_picocolors13 = __toESM(require_picocolors(), 1);
283843
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
283450
283844
  init_config();
283451
283845
  init_theme();
283452
283846
  init_push();
@@ -283513,10 +283907,10 @@ function resolveSince(since) {
283513
283907
  var sleep2 = (ms) => new Promise((r2) => setTimeout(r2, ms));
283514
283908
  function colorOp(op) {
283515
283909
  const label = op.padEnd(6);
283516
- if (op === "create") return import_picocolors13.default.green(label);
283517
- if (op === "update") return import_picocolors13.default.yellow(label);
283518
- if (op === "delete") return import_picocolors13.default.red(label);
283519
- return import_picocolors13.default.dim(label);
283910
+ if (op === "create") return import_picocolors15.default.green(label);
283911
+ if (op === "update") return import_picocolors15.default.yellow(label);
283912
+ if (op === "delete") return import_picocolors15.default.red(label);
283913
+ return import_picocolors15.default.dim(label);
283520
283914
  }
283521
283915
  function render(e2, json) {
283522
283916
  if (json) {
@@ -283525,25 +283919,25 @@ function render(e2, json) {
283525
283919
  return;
283526
283920
  }
283527
283921
  const t = new Date(e2.at).toLocaleTimeString();
283528
- const actor = e2.actor ? import_picocolors13.default.dim(` ${e2.actor}`) : "";
283529
- console.log(` ${import_picocolors13.default.dim(t)} ${colorOp(e2.op)} ${import_picocolors13.default.bold(e2.model)} ${import_picocolors13.default.dim(e2.recordId)}${actor}`);
283922
+ const actor = e2.actor ? import_picocolors15.default.dim(` ${e2.actor}`) : "";
283923
+ console.log(` ${import_picocolors15.default.dim(t)} ${colorOp(e2.op)} ${import_picocolors15.default.bold(e2.model)} ${import_picocolors15.default.dim(e2.recordId)}${actor}`);
283530
283924
  }
283531
283925
  async function logs(argv) {
283532
283926
  let args;
283533
283927
  try {
283534
283928
  args = parseLogsArgs(argv);
283535
283929
  } catch (err) {
283536
- console.error(import_picocolors13.default.red(` ${err instanceof Error ? err.message : String(err)}`));
283930
+ console.error(import_picocolors15.default.red(` ${err instanceof Error ? err.message : String(err)}`));
283537
283931
  process.exit(1);
283538
283932
  }
283539
283933
  const apiKey = resolveApiKey(args.mode);
283540
283934
  if (!apiKey) {
283541
283935
  console.error(
283542
- import_picocolors13.default.red(` No API key.`) + import_picocolors13.default.dim(` Run ${import_picocolors13.default.bold("ablo login")} or set ${import_picocolors13.default.bold("ABLO_API_KEY")}.`)
283936
+ import_picocolors15.default.red(` No API key.`) + import_picocolors15.default.dim(` Run ${import_picocolors15.default.bold("ablo login")} or set ${import_picocolors15.default.bold("ABLO_API_KEY")}.`)
283543
283937
  );
283544
283938
  process.exit(1);
283545
283939
  }
283546
- const baseUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
283940
+ const baseUrl2 = apiBaseUrl();
283547
283941
  const since = resolveSince(args.since);
283548
283942
  async function fetchPage(params) {
283549
283943
  const qs = new URLSearchParams(params).toString();
@@ -283553,7 +283947,7 @@ async function logs(argv) {
283553
283947
  if (!res) return null;
283554
283948
  if (!res.ok) {
283555
283949
  const body = await res.json().catch(() => ({}));
283556
- console.error(import_picocolors13.default.red(` logs failed (${res.status}): ${body.reason ?? body.message ?? ""}`));
283950
+ console.error(import_picocolors15.default.red(` logs failed (${res.status}): ${body.reason ?? body.message ?? ""}`));
283557
283951
  process.exit(1);
283558
283952
  }
283559
283953
  const json = await res.json();
@@ -283564,7 +283958,7 @@ async function logs(argv) {
283564
283958
  }
283565
283959
  if (!args.json) {
283566
283960
  console.log(`
283567
- ${brand("ablo")} ${import_picocolors13.default.dim("logs")} ${import_picocolors13.default.dim(`(${args.mode ?? "active"} mode)`)}
283961
+ ${brand("ablo")} ${import_picocolors15.default.dim("logs")} ${import_picocolors15.default.dim(`(${args.mode ?? "active"} mode)`)}
283568
283962
  `);
283569
283963
  }
283570
283964
  const initial = await fetchPage({
@@ -283574,13 +283968,13 @@ async function logs(argv) {
283574
283968
  ...args.op ? { op: args.op } : {}
283575
283969
  });
283576
283970
  if (!initial) {
283577
- console.error(import_picocolors13.default.red(` Couldn't reach ${baseUrl2}.`));
283971
+ console.error(import_picocolors15.default.red(` Couldn't reach ${baseUrl2}.`));
283578
283972
  process.exit(1);
283579
283973
  }
283580
283974
  for (const e2 of initial.events) render(e2, args.json);
283581
283975
  let cursor = initial.cursor;
283582
283976
  if (!args.follow) return;
283583
- if (!args.json) console.log(` ${import_picocolors13.default.dim("watching for new activity \u2026 (Ctrl-C to stop)")}
283977
+ if (!args.json) console.log(` ${import_picocolors15.default.dim("watching for new activity \u2026 (Ctrl-C to stop)")}
283584
283978
  `);
283585
283979
  for (; ; ) {
283586
283980
  await sleep2(1500);
@@ -283598,7 +283992,7 @@ async function logs(argv) {
283598
283992
  // src/cli/webhooks.ts
283599
283993
  init_cjs_shims();
283600
283994
  var import_fs8 = require("fs");
283601
- var import_picocolors14 = __toESM(require_picocolors(), 1);
283995
+ var import_picocolors16 = __toESM(require_picocolors(), 1);
283602
283996
  init_credentialPolicy();
283603
283997
  init_config();
283604
283998
  init_theme();
@@ -283631,17 +284025,17 @@ function requireKey2(mode2) {
283631
284025
  const apiKey = resolveApiKey(mode2);
283632
284026
  if (!apiKey) {
283633
284027
  console.error(
283634
- import_picocolors14.default.red(" No API key.") + import_picocolors14.default.dim(` Run ${import_picocolors14.default.bold("ablo login")} or set ${import_picocolors14.default.bold("ABLO_API_KEY")}.`)
284028
+ import_picocolors16.default.red(" No API key.") + import_picocolors16.default.dim(` Run ${import_picocolors16.default.bold("ablo login")} or set ${import_picocolors16.default.bold("ABLO_API_KEY")}.`)
283635
284029
  );
283636
284030
  process.exit(1);
283637
284031
  }
283638
284032
  if (classifyCredentialKind(apiKey) !== "secret") {
283639
- console.error(import_picocolors14.default.red(" Managing webhooks requires a secret key ") + import_picocolors14.default.dim("(sk_test_ / sk_live_)."));
284033
+ console.error(import_picocolors16.default.red(" Managing webhooks requires a secret key ") + import_picocolors16.default.dim("(sk_test_ / sk_live_)."));
283640
284034
  process.exit(1);
283641
284035
  }
283642
284036
  return apiKey;
283643
284037
  }
283644
- var baseUrl = () => (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
284038
+ var baseUrl = () => apiBaseUrl();
283645
284039
  async function api(apiKey, method, path, body) {
283646
284040
  const res = await fetch(`${baseUrl()}/api/v1/webhook_endpoints${path}`, {
283647
284041
  method,
@@ -283652,12 +284046,12 @@ async function api(apiKey, method, path, body) {
283652
284046
  ...body ? { body: JSON.stringify(body) } : {}
283653
284047
  }).catch(() => null);
283654
284048
  if (!res) {
283655
- console.error(import_picocolors14.default.red(` Couldn't reach ${baseUrl()}.`));
284049
+ console.error(import_picocolors16.default.red(` Couldn't reach ${baseUrl()}.`));
283656
284050
  process.exit(1);
283657
284051
  }
283658
284052
  if (!res.ok) {
283659
284053
  const err = await res.json().catch(() => ({}));
283660
- console.error(import_picocolors14.default.red(` Request failed (${res.status}): ${err.message ?? err.reason ?? ""}`));
284054
+ console.error(import_picocolors16.default.red(` Request failed (${res.status}): ${err.message ?? err.reason ?? ""}`));
283661
284055
  process.exit(1);
283662
284056
  }
283663
284057
  return await res.json();
@@ -283679,11 +284073,11 @@ ${line}
283679
284073
  return file;
283680
284074
  }
283681
284075
  function printEndpoint(e2) {
283682
- const dot = e2.status === "enabled" ? import_picocolors14.default.green("\u25CF") : import_picocolors14.default.red("\u25CF");
283683
- const health = e2.last_error ? import_picocolors14.default.red(` last error: ${e2.last_error}`) : "";
283684
- console.log(` ${dot} ${import_picocolors14.default.bold(e2.id)} ${e2.url}`);
284076
+ const dot = e2.status === "enabled" ? import_picocolors16.default.green("\u25CF") : import_picocolors16.default.red("\u25CF");
284077
+ const health = e2.last_error ? import_picocolors16.default.red(` last error: ${e2.last_error}`) : "";
284078
+ console.log(` ${dot} ${import_picocolors16.default.bold(e2.id)} ${e2.url}`);
283685
284079
  console.log(
283686
- import_picocolors14.default.dim(
284080
+ import_picocolors16.default.dim(
283687
284081
  ` ${e2.status} \xB7 ${e2.environment} \xB7 events ${e2.enabled_events.join(",")} \xB7 cursor ${e2.cursor ?? "\u2014"}${health}`
283688
284082
  )
283689
284083
  );
@@ -283695,7 +284089,7 @@ async function webhooks(argv) {
283695
284089
  if (sub === "create") {
283696
284090
  const url = positional(rest);
283697
284091
  if (!url) {
283698
- console.error(import_picocolors14.default.red(" Usage: ") + brand("ablo webhooks create <url>"));
284092
+ console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks create <url>"));
283699
284093
  process.exit(1);
283700
284094
  }
283701
284095
  const apiKey = requireKey2(mode2);
@@ -283707,8 +284101,8 @@ async function webhooks(argv) {
283707
284101
  });
283708
284102
  const file = writeSecretToEnv(created.secret);
283709
284103
  console.log(`
283710
- ${import_picocolors14.default.green("\u2713")} Registered ${import_picocolors14.default.bold(created.id)} \u2192 ${created.url}`);
283711
- console.log(` ${import_picocolors14.default.green("\u2713")} Wrote ${import_picocolors14.default.bold(ENV_KEY)} to ${import_picocolors14.default.bold(file)} ${import_picocolors14.default.dim("(shown once)")}
284104
+ ${import_picocolors16.default.green("\u2713")} Registered ${import_picocolors16.default.bold(created.id)} \u2192 ${created.url}`);
284105
+ console.log(` ${import_picocolors16.default.green("\u2713")} Wrote ${import_picocolors16.default.bold(ENV_KEY)} to ${import_picocolors16.default.bold(file)} ${import_picocolors16.default.dim("(shown once)")}
283712
284106
  `);
283713
284107
  return;
283714
284108
  }
@@ -283716,7 +284110,7 @@ async function webhooks(argv) {
283716
284110
  const apiKey = requireKey2(mode2);
283717
284111
  const { data } = await api(apiKey, "GET", "");
283718
284112
  if (data.length === 0) {
283719
- console.log(import_picocolors14.default.dim(" No webhook endpoints. ") + brand("ablo webhooks create <url>"));
284113
+ console.log(import_picocolors16.default.dim(" No webhook endpoints. ") + brand("ablo webhooks create <url>"));
283720
284114
  return;
283721
284115
  }
283722
284116
  console.log();
@@ -283727,40 +284121,40 @@ async function webhooks(argv) {
283727
284121
  if (sub === "roll") {
283728
284122
  const id = positional(rest);
283729
284123
  if (!id) {
283730
- console.error(import_picocolors14.default.red(" Usage: ") + brand("ablo webhooks roll <id>"));
284124
+ console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks roll <id>"));
283731
284125
  process.exit(1);
283732
284126
  }
283733
284127
  const apiKey = requireKey2(mode2);
283734
284128
  const rolled = await api(apiKey, "POST", `/${id}/roll_secret`);
283735
284129
  const file = writeSecretToEnv(rolled.secret);
283736
284130
  console.log(`
283737
- ${import_picocolors14.default.green("\u2713")} Rolled secret for ${import_picocolors14.default.bold(id)} \u2192 ${import_picocolors14.default.bold(file)} ${import_picocolors14.default.dim("(old secret now invalid)")}
284131
+ ${import_picocolors16.default.green("\u2713")} Rolled secret for ${import_picocolors16.default.bold(id)} \u2192 ${import_picocolors16.default.bold(file)} ${import_picocolors16.default.dim("(old secret now invalid)")}
283738
284132
  `);
283739
284133
  return;
283740
284134
  }
283741
284135
  if (sub === "enable") {
283742
284136
  const id = positional(rest);
283743
284137
  if (!id) {
283744
- console.error(import_picocolors14.default.red(" Usage: ") + brand("ablo webhooks enable <id>"));
284138
+ console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks enable <id>"));
283745
284139
  process.exit(1);
283746
284140
  }
283747
284141
  const apiKey = requireKey2(mode2);
283748
284142
  const e2 = await api(apiKey, "POST", `/${id}/enable`);
283749
- console.log(` ${import_picocolors14.default.green("\u2713")} Re-enabled ${import_picocolors14.default.bold(e2.id)}`);
284143
+ console.log(` ${import_picocolors16.default.green("\u2713")} Re-enabled ${import_picocolors16.default.bold(e2.id)}`);
283750
284144
  return;
283751
284145
  }
283752
284146
  if (sub === "rm" || sub === "delete") {
283753
284147
  const id = positional(rest);
283754
284148
  if (!id) {
283755
- console.error(import_picocolors14.default.red(" Usage: ") + brand("ablo webhooks rm <id>"));
284149
+ console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks rm <id>"));
283756
284150
  process.exit(1);
283757
284151
  }
283758
284152
  const apiKey = requireKey2(mode2);
283759
284153
  await api(apiKey, "DELETE", `/${id}`);
283760
- console.log(` ${import_picocolors14.default.green("\u2713")} Removed ${import_picocolors14.default.bold(id)}`);
284154
+ console.log(` ${import_picocolors16.default.green("\u2713")} Removed ${import_picocolors16.default.bold(id)}`);
283761
284155
  return;
283762
284156
  }
283763
- console.log(` ${import_picocolors14.default.bold("Usage:")}`);
284157
+ console.log(` ${import_picocolors16.default.bold("Usage:")}`);
283764
284158
  console.log(` ${brand("ablo webhooks create <url>")} Register an endpoint; writes ${ENV_KEY}`);
283765
284159
  console.log(` ${brand("ablo webhooks list")} List endpoints + delivery health`);
283766
284160
  console.log(` ${brand("ablo webhooks roll <id>")} Mint a fresh signing secret`);
@@ -283772,7 +284166,7 @@ async function webhooks(argv) {
283772
284166
  // src/cli/check.ts
283773
284167
  init_cjs_shims();
283774
284168
  init_errors();
283775
- var import_picocolors15 = __toESM(require_picocolors(), 1);
284169
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
283776
284170
  init_src();
283777
284171
  var import_schema6 = require("@abloatai/ablo/schema");
283778
284172
  init_push();
@@ -283810,13 +284204,13 @@ async function check(argv) {
283810
284204
  try {
283811
284205
  args = parseCheckArgs(argv);
283812
284206
  } catch (err) {
283813
- console.error(import_picocolors15.default.red(` ${err instanceof Error ? err.message : String(err)}`));
284207
+ console.error(import_picocolors17.default.red(` ${err instanceof Error ? err.message : String(err)}`));
283814
284208
  process.exit(1);
283815
284209
  }
283816
284210
  const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
283817
284211
  if (!dbUrl) {
283818
284212
  console.error(
283819
- import_picocolors15.default.red(` No database.`) + import_picocolors15.default.dim(` Set ${import_picocolors15.default.bold("DATABASE_URL")} to the Postgres you want Ablo to adopt.`)
284213
+ import_picocolors17.default.red(` No database.`) + import_picocolors17.default.dim(` Set ${import_picocolors17.default.bold("DATABASE_URL")} to the Postgres you want Ablo to adopt.`)
283820
284214
  );
283821
284215
  process.exit(1);
283822
284216
  }
@@ -283831,7 +284225,7 @@ async function check(argv) {
283831
284225
  [args.appSchema]
283832
284226
  );
283833
284227
  } catch (err) {
283834
- console.error(import_picocolors15.default.red(` Couldn't read the database: ${err instanceof Error ? err.message : String(err)}`));
284228
+ console.error(import_picocolors17.default.red(` Couldn't read the database: ${err instanceof Error ? err.message : String(err)}`));
283835
284229
  await sql.end({ timeout: 2 });
283836
284230
  process.exit(1);
283837
284231
  }
@@ -283846,7 +284240,7 @@ async function check(argv) {
283846
284240
  set.add(r2.column_name);
283847
284241
  }
283848
284242
  console.log(`
283849
- ${brand("ablo")} ${import_picocolors15.default.dim("check")} ${import_picocolors15.default.dim(`schema "${args.appSchema}"`)}
284243
+ ${brand("ablo")} ${import_picocolors17.default.dim("check")} ${import_picocolors17.default.dim(`schema "${args.appSchema}"`)}
283850
284244
  `);
283851
284245
  const declaredTables = /* @__PURE__ */ new Set();
283852
284246
  let errors = 0;
@@ -283856,7 +284250,7 @@ async function check(argv) {
283856
284250
  declaredTables.add(table);
283857
284251
  const present = colsByTable.get(table);
283858
284252
  if (!present) {
283859
- console.log(` ${import_picocolors15.default.red("\u2717")} ${import_picocolors15.default.bold(key)} ${import_picocolors15.default.dim("\u2192")} table ${import_picocolors15.default.bold(table)} ${import_picocolors15.default.red("not found")}`);
284253
+ console.log(` ${import_picocolors17.default.red("\u2717")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim("\u2192")} table ${import_picocolors17.default.bold(table)} ${import_picocolors17.default.red("not found")}`);
283860
284254
  errors++;
283861
284255
  continue;
283862
284256
  }
@@ -283878,26 +284272,26 @@ async function check(argv) {
283878
284272
  if (!present.has(col)) problems.push(`missing column "${col}" (field ${fieldName})`);
283879
284273
  }
283880
284274
  if (problems.length > 0) {
283881
- console.log(` ${import_picocolors15.default.red("\u2717")} ${import_picocolors15.default.bold(key)} ${import_picocolors15.default.dim("\u2192")} ${table}`);
283882
- for (const p2 of problems) console.log(` ${import_picocolors15.default.red("\u2022")} ${p2}`);
283883
- for (const w2 of warns) console.log(` ${import_picocolors15.default.yellow("\u2022")} ${w2}`);
284275
+ console.log(` ${import_picocolors17.default.red("\u2717")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim("\u2192")} ${table}`);
284276
+ for (const p2 of problems) console.log(` ${import_picocolors17.default.red("\u2022")} ${p2}`);
284277
+ for (const w2 of warns) console.log(` ${import_picocolors17.default.yellow("\u2022")} ${w2}`);
283884
284278
  errors++;
283885
284279
  } else if (warns.length > 0) {
283886
- console.log(` ${import_picocolors15.default.yellow("!")} ${import_picocolors15.default.bold(key)} ${import_picocolors15.default.dim("\u2192")} ${table}`);
283887
- for (const w2 of warns) console.log(` ${import_picocolors15.default.yellow("\u2022")} ${w2}`);
284280
+ console.log(` ${import_picocolors17.default.yellow("!")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim("\u2192")} ${table}`);
284281
+ for (const w2 of warns) console.log(` ${import_picocolors17.default.yellow("\u2022")} ${w2}`);
283888
284282
  warnings++;
283889
284283
  } else {
283890
- console.log(` ${import_picocolors15.default.green("\u2713")} ${import_picocolors15.default.bold(key)} ${import_picocolors15.default.dim(`\u2192 ${table} (id, ${orgCol ?? "no org"} ok)`)}`);
284284
+ console.log(` ${import_picocolors17.default.green("\u2713")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim(`\u2192 ${table} (id, ${orgCol ?? "no org"} ok)`)}`);
283891
284285
  }
283892
284286
  }
283893
284287
  const modelCount = Object.keys(schemaJson.models).length;
283894
284288
  const ignored = [...colsByTable.keys()].filter((t) => !declaredTables.has(t)).length;
283895
284289
  console.log(
283896
284290
  `
283897
- ${modelCount} model${modelCount === 1 ? "" : "s"} \xB7 ${import_picocolors15.default.green(`${modelCount - errors - warnings} ok`)}` + (warnings ? ` \xB7 ${import_picocolors15.default.yellow(`${warnings} warning${warnings === 1 ? "" : "s"}`)}` : "") + (errors ? ` \xB7 ${import_picocolors15.default.red(`${errors} error${errors === 1 ? "" : "s"}`)}` : "")
284291
+ ${modelCount} model${modelCount === 1 ? "" : "s"} \xB7 ${import_picocolors17.default.green(`${modelCount - errors - warnings} ok`)}` + (warnings ? ` \xB7 ${import_picocolors17.default.yellow(`${warnings} warning${warnings === 1 ? "" : "s"}`)}` : "") + (errors ? ` \xB7 ${import_picocolors17.default.red(`${errors} error${errors === 1 ? "" : "s"}`)}` : "")
283898
284292
  );
283899
284293
  if (ignored > 0) {
283900
- console.log(` ${import_picocolors15.default.dim(`${ignored} other table${ignored === 1 ? "" : "s"} in your database \u2014 ignored by Ablo`)}`);
284294
+ console.log(` ${import_picocolors17.default.dim(`${ignored} other table${ignored === 1 ? "" : "s"} in your database \u2014 ignored by Ablo`)}`);
283901
284295
  }
283902
284296
  console.log();
283903
284297
  process.exit(errors > 0 ? 1 : 0);
@@ -283905,7 +284299,7 @@ async function check(argv) {
283905
284299
 
283906
284300
  // src/cli/upgrade.ts
283907
284301
  init_cjs_shims();
283908
- var import_picocolors16 = __toESM(require_picocolors(), 1);
284302
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
283909
284303
  var import_ts_morph = __toESM(require_ts_morph(), 1);
283910
284304
  var DEFAULT_GLOBS = ["app/**/*.{ts,tsx}", "src/**/*.{ts,tsx}", "ablo/**/*.{ts,tsx}", "lib/**/*.{ts,tsx}"];
283911
284305
  var VERB_ARGS = {
@@ -283983,7 +284377,7 @@ async function upgrade(argv) {
283983
284377
  project.addSourceFilesAtPaths(globs.length > 0 ? globs : DEFAULT_GLOBS);
283984
284378
  const files = project.getSourceFiles();
283985
284379
  if (files.length === 0) {
283986
- console.log(import_picocolors16.default.yellow(' No .ts/.tsx files found. Pass a glob, e.g. `ablo upgrade "src/**/*.tsx"`.'));
284380
+ console.log(import_picocolors18.default.yellow(' No .ts/.tsx files found. Pass a glob, e.g. `ablo upgrade "src/**/*.tsx"`.'));
283987
284381
  return;
283988
284382
  }
283989
284383
  const edits = [];
@@ -284057,39 +284451,39 @@ async function upgrade(argv) {
284057
284451
  const rel = (f) => f.replace(cwd + "/", "");
284058
284452
  console.log();
284059
284453
  if (edits.length === 0 && manual.length === 0) {
284060
- console.log(import_picocolors16.default.green(" \u2713 Nothing to migrate \u2014 your code is already on the current API."));
284454
+ console.log(import_picocolors18.default.green(" \u2713 Nothing to migrate \u2014 your code is already on the current API."));
284061
284455
  return;
284062
284456
  }
284063
284457
  if (edits.length > 0) {
284064
- console.log(import_picocolors16.default.bold(` ${write ? "Applied" : "Would apply"} ${edits.length} change${edits.length === 1 ? "" : "s"}:`));
284458
+ console.log(import_picocolors18.default.bold(` ${write ? "Applied" : "Would apply"} ${edits.length} change${edits.length === 1 ? "" : "s"}:`));
284065
284459
  for (const e2 of edits) {
284066
- console.log(` ${import_picocolors16.default.dim(`${rel(e2.file)}:${e2.line}`)} ${import_picocolors16.default.cyan(e2.rule)}`);
284067
- console.log(` ${import_picocolors16.default.red("-")} ${e2.before}`);
284068
- console.log(` ${import_picocolors16.default.green("+")} ${e2.after}`);
284460
+ console.log(` ${import_picocolors18.default.dim(`${rel(e2.file)}:${e2.line}`)} ${import_picocolors18.default.cyan(e2.rule)}`);
284461
+ console.log(` ${import_picocolors18.default.red("-")} ${e2.before}`);
284462
+ console.log(` ${import_picocolors18.default.green("+")} ${e2.after}`);
284069
284463
  }
284070
284464
  }
284071
284465
  if (manual.length > 0) {
284072
284466
  console.log();
284073
- console.log(import_picocolors16.default.bold(import_picocolors16.default.yellow(` ${manual.length} spot${manual.length === 1 ? "" : "s"} need manual review (structural):`)));
284467
+ console.log(import_picocolors18.default.bold(import_picocolors18.default.yellow(` ${manual.length} spot${manual.length === 1 ? "" : "s"} need manual review (structural):`)));
284074
284468
  for (const m2 of manual) {
284075
- console.log(` ${import_picocolors16.default.dim(`${rel(m2.file)}:${m2.line}`)} ${import_picocolors16.default.yellow(m2.rule)}`);
284076
- console.log(` ${import_picocolors16.default.dim(m2.snippet)}`);
284469
+ console.log(` ${import_picocolors18.default.dim(`${rel(m2.file)}:${m2.line}`)} ${import_picocolors18.default.yellow(m2.rule)}`);
284470
+ console.log(` ${import_picocolors18.default.dim(m2.snippet)}`);
284077
284471
  console.log(` \u2192 ${m2.hint}`);
284078
284472
  }
284079
284473
  }
284080
284474
  console.log();
284081
284475
  if (write) {
284082
284476
  await project.save();
284083
- console.log(import_picocolors16.default.green(` \u2713 Wrote ${edits.length} change${edits.length === 1 ? "" : "s"}. Review the diff, run your typecheck.`));
284477
+ console.log(import_picocolors18.default.green(` \u2713 Wrote ${edits.length} change${edits.length === 1 ? "" : "s"}. Review the diff, run your typecheck.`));
284084
284478
  } else {
284085
- console.log(import_picocolors16.default.dim(" Dry run. Re-run with `--write` to apply the auto-fixes above (manual items are never auto-written)."));
284479
+ console.log(import_picocolors18.default.dim(" Dry run. Re-run with `--write` to apply the auto-fixes above (manual items are never auto-written)."));
284086
284480
  }
284087
284481
  }
284088
284482
 
284089
284483
  // src/cli/pull.ts
284090
284484
  init_cjs_shims();
284091
284485
  init_errors();
284092
- var import_picocolors17 = __toESM(require_picocolors(), 1);
284486
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
284093
284487
  init_src();
284094
284488
  var import_fs9 = require("fs");
284095
284489
  init_theme();
@@ -284203,45 +284597,45 @@ async function pull(argv) {
284203
284597
  try {
284204
284598
  args = parsePullArgs(argv);
284205
284599
  } catch (err) {
284206
- console.error(import_picocolors17.default.red(` ${err instanceof Error ? err.message : String(err)}`));
284600
+ console.error(import_picocolors19.default.red(` ${err instanceof Error ? err.message : String(err)}`));
284207
284601
  process.exit(1);
284208
284602
  }
284209
284603
  const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
284210
284604
  if (!dbUrl) {
284211
- console.error(import_picocolors17.default.red(` No database.`) + import_picocolors17.default.dim(` Set ${import_picocolors17.default.bold("DATABASE_URL")} to the Postgres to pull from.`));
284605
+ console.error(import_picocolors19.default.red(` No database.`) + import_picocolors19.default.dim(` Set ${import_picocolors19.default.bold("DATABASE_URL")} to the Postgres to pull from.`));
284212
284606
  process.exit(1);
284213
284607
  }
284214
284608
  if ((0, import_fs9.existsSync)(args.out) && !args.force) {
284215
284609
  console.error(
284216
- import_picocolors17.default.red(` ${args.out} already exists.`) + import_picocolors17.default.dim(` Re-run with ${import_picocolors17.default.bold("--force")} to overwrite.`)
284610
+ import_picocolors19.default.red(` ${args.out} already exists.`) + import_picocolors19.default.dim(` Re-run with ${import_picocolors19.default.bold("--force")} to overwrite.`)
284217
284611
  );
284218
284612
  process.exit(1);
284219
284613
  }
284220
284614
  console.log(`
284221
- ${brand("ablo")} ${import_picocolors17.default.dim("pull")} ${import_picocolors17.default.dim(`schema "${args.appSchema}"`)}
284615
+ ${brand("ablo")} ${import_picocolors19.default.dim("pull")} ${import_picocolors19.default.dim(`schema "${args.appSchema}"`)}
284222
284616
  `);
284223
284617
  let result;
284224
284618
  try {
284225
284619
  result = await buildSchemaSourceFromDb({ dbUrl, appSchema: args.appSchema, importPath: args.importPath });
284226
284620
  } catch (err) {
284227
- console.error(import_picocolors17.default.red(` Couldn't read the database: ${err instanceof Error ? err.message : String(err)}`));
284621
+ console.error(import_picocolors19.default.red(` Couldn't read the database: ${err instanceof Error ? err.message : String(err)}`));
284228
284622
  process.exit(1);
284229
284623
  }
284230
284624
  if (result.models.length === 0) {
284231
284625
  console.error(
284232
- import_picocolors17.default.yellow(` No adoptable tables found`) + import_picocolors17.default.dim(` (a model needs an ${import_picocolors17.default.bold("id")} + ${import_picocolors17.default.bold("organization_id")} column).`)
284626
+ import_picocolors19.default.yellow(` No adoptable tables found`) + import_picocolors19.default.dim(` (a model needs an ${import_picocolors19.default.bold("id")} + ${import_picocolors19.default.bold("organization_id")} column).`)
284233
284627
  );
284234
284628
  process.exit(1);
284235
284629
  }
284236
284630
  (0, import_fs9.writeFileSync)(args.out, result.source);
284237
- console.log(` ${import_picocolors17.default.green("\u2713")} wrote ${import_picocolors17.default.bold(args.out)} ${import_picocolors17.default.dim(`(${result.models.length} models)`)}`);
284238
- console.log(` ${import_picocolors17.default.dim(`models: ${result.models.join(", ")}`)}`);
284631
+ console.log(` ${import_picocolors19.default.green("\u2713")} wrote ${import_picocolors19.default.bold(args.out)} ${import_picocolors19.default.dim(`(${result.models.length} models)`)}`);
284632
+ console.log(` ${import_picocolors19.default.dim(`models: ${result.models.join(", ")}`)}`);
284239
284633
  if (result.skipped > 0) {
284240
- console.log(` ${import_picocolors17.default.dim(`${result.skipped} table(s) skipped \u2014 no id/organization_id`)}`);
284634
+ console.log(` ${import_picocolors19.default.dim(`${result.skipped} table(s) skipped \u2014 no id/organization_id`)}`);
284241
284635
  }
284242
284636
  console.log(
284243
284637
  `
284244
- ${import_picocolors17.default.dim("Introspection is lossy (enums, JSON shape, relations). Review the file, then")} ${import_picocolors17.default.bold("ablo check")}.
284638
+ ${import_picocolors19.default.dim("Introspection is lossy (enums, JSON shape, relations). Review the file, then")} ${import_picocolors19.default.bold("ablo check")}.
284245
284639
  `
284246
284640
  );
284247
284641
  }
@@ -284249,7 +284643,7 @@ async function pull(argv) {
284249
284643
  // src/cli/prismaPull.ts
284250
284644
  init_cjs_shims();
284251
284645
  init_errors();
284252
- var import_picocolors18 = __toESM(require_picocolors(), 1);
284646
+ var import_picocolors20 = __toESM(require_picocolors(), 1);
284253
284647
  var import_fs10 = require("fs");
284254
284648
  init_theme();
284255
284649
 
@@ -284534,55 +284928,55 @@ async function prismaPull(argv) {
284534
284928
  try {
284535
284929
  args = parsePrismaPullArgs(argv);
284536
284930
  } catch (err) {
284537
- console.error(import_picocolors18.default.red(` ${err instanceof Error ? err.message : String(err)}`));
284931
+ console.error(import_picocolors20.default.red(` ${err instanceof Error ? err.message : String(err)}`));
284538
284932
  process.exit(1);
284539
284933
  }
284540
284934
  if (!(0, import_fs10.existsSync)(args.schema)) {
284541
284935
  console.error(
284542
- import_picocolors18.default.red(` No Prisma schema at ${import_picocolors18.default.bold(args.schema)}.`) + import_picocolors18.default.dim(` Pass a path: ${import_picocolors18.default.bold("ablo pull prisma <path>")}.`)
284936
+ import_picocolors20.default.red(` No Prisma schema at ${import_picocolors20.default.bold(args.schema)}.`) + import_picocolors20.default.dim(` Pass a path: ${import_picocolors20.default.bold("ablo pull prisma <path>")}.`)
284543
284937
  );
284544
284938
  process.exit(1);
284545
284939
  }
284546
284940
  if ((0, import_fs10.existsSync)(args.out) && !args.force) {
284547
284941
  console.error(
284548
- import_picocolors18.default.red(` ${args.out} already exists.`) + import_picocolors18.default.dim(` Re-run with ${import_picocolors18.default.bold("--force")} to overwrite.`)
284942
+ import_picocolors20.default.red(` ${args.out} already exists.`) + import_picocolors20.default.dim(` Re-run with ${import_picocolors20.default.bold("--force")} to overwrite.`)
284549
284943
  );
284550
284944
  process.exit(1);
284551
284945
  }
284552
284946
  console.log(`
284553
- ${brand("ablo")} ${import_picocolors18.default.dim("pull prisma")} ${import_picocolors18.default.dim(args.schema)}
284947
+ ${brand("ablo")} ${import_picocolors20.default.dim("pull prisma")} ${import_picocolors20.default.dim(args.schema)}
284554
284948
  `);
284555
284949
  let result;
284556
284950
  try {
284557
284951
  const src = (0, import_fs10.readFileSync)(args.schema, "utf8");
284558
284952
  result = buildSchemaSourceFromPrisma({ src, importPath: args.importPath });
284559
284953
  } catch (err) {
284560
- console.error(import_picocolors18.default.red(` Couldn't parse the schema: ${err instanceof Error ? err.message : String(err)}`));
284954
+ console.error(import_picocolors20.default.red(` Couldn't parse the schema: ${err instanceof Error ? err.message : String(err)}`));
284561
284955
  process.exit(1);
284562
284956
  }
284563
284957
  if (result.models.length === 0) {
284564
284958
  console.error(
284565
- import_picocolors18.default.yellow(` No adoptable models found`) + import_picocolors18.default.dim(` (a model needs an ${import_picocolors18.default.bold("id")} + ${import_picocolors18.default.bold("organizationId")} / ${import_picocolors18.default.bold("organization_id")}).`)
284959
+ import_picocolors20.default.yellow(` No adoptable models found`) + import_picocolors20.default.dim(` (a model needs an ${import_picocolors20.default.bold("id")} + ${import_picocolors20.default.bold("organizationId")} / ${import_picocolors20.default.bold("organization_id")}).`)
284566
284960
  );
284567
284961
  process.exit(1);
284568
284962
  }
284569
284963
  (0, import_fs10.writeFileSync)(args.out, result.source);
284570
- console.log(` ${import_picocolors18.default.green("\u2713")} wrote ${import_picocolors18.default.bold(args.out)} ${import_picocolors18.default.dim(`(${result.models.length} models)`)}`);
284571
- console.log(` ${import_picocolors18.default.dim(`models: ${result.models.join(", ")}`)}`);
284964
+ console.log(` ${import_picocolors20.default.green("\u2713")} wrote ${import_picocolors20.default.bold(args.out)} ${import_picocolors20.default.dim(`(${result.models.length} models)`)}`);
284965
+ console.log(` ${import_picocolors20.default.dim(`models: ${result.models.join(", ")}`)}`);
284572
284966
  if (result.skipped.length > 0) {
284573
- console.log(` ${import_picocolors18.default.dim(`${result.skipped.length} model(s) skipped:`)}`);
284574
- for (const s of result.skipped) console.log(` ${import_picocolors18.default.dim(`- ${s.name}: ${s.reason}`)}`);
284967
+ console.log(` ${import_picocolors20.default.dim(`${result.skipped.length} model(s) skipped:`)}`);
284968
+ for (const s of result.skipped) console.log(` ${import_picocolors20.default.dim(`- ${s.name}: ${s.reason}`)}`);
284575
284969
  }
284576
284970
  console.log(
284577
284971
  `
284578
- ${import_picocolors18.default.dim("Enums and relations were preserved. Review the file, then")} ${import_picocolors18.default.bold("ablo check")}.
284972
+ ${import_picocolors20.default.dim("Enums and relations were preserved. Review the file, then")} ${import_picocolors20.default.bold("ablo check")}.
284579
284973
  `
284580
284974
  );
284581
284975
  }
284582
284976
 
284583
284977
  // src/cli/drizzlePull.ts
284584
284978
  init_cjs_shims();
284585
- var import_picocolors19 = __toESM(require_picocolors(), 1);
284979
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
284586
284980
  init_errors();
284587
284981
  var import_fs11 = require("fs");
284588
284982
  var import_path6 = require("path");
@@ -284721,27 +285115,27 @@ async function drizzlePull(argv) {
284721
285115
  try {
284722
285116
  args = parseDrizzlePullArgs(argv);
284723
285117
  } catch (err) {
284724
- console.error(import_picocolors19.default.red(` ${err instanceof Error ? err.message : String(err)}`));
285118
+ console.error(import_picocolors21.default.red(` ${err instanceof Error ? err.message : String(err)}`));
284725
285119
  process.exit(1);
284726
285120
  }
284727
285121
  if (!args.schema) {
284728
285122
  console.error(
284729
- import_picocolors19.default.red(` No Drizzle schema given.`) + import_picocolors19.default.dim(` Pass the module: ${import_picocolors19.default.bold("ablo pull drizzle src/db/schema.ts")}.`)
285123
+ import_picocolors21.default.red(` No Drizzle schema given.`) + import_picocolors21.default.dim(` Pass the module: ${import_picocolors21.default.bold("ablo pull drizzle src/db/schema.ts")}.`)
284730
285124
  );
284731
285125
  process.exit(1);
284732
285126
  }
284733
285127
  if (!(0, import_fs11.existsSync)(args.schema)) {
284734
- console.error(import_picocolors19.default.red(` No file at ${import_picocolors19.default.bold(args.schema)}.`));
285128
+ console.error(import_picocolors21.default.red(` No file at ${import_picocolors21.default.bold(args.schema)}.`));
284735
285129
  process.exit(1);
284736
285130
  }
284737
285131
  if ((0, import_fs11.existsSync)(args.out) && !args.force) {
284738
285132
  console.error(
284739
- import_picocolors19.default.red(` ${args.out} already exists.`) + import_picocolors19.default.dim(` Re-run with ${import_picocolors19.default.bold("--force")} to overwrite.`)
285133
+ import_picocolors21.default.red(` ${args.out} already exists.`) + import_picocolors21.default.dim(` Re-run with ${import_picocolors21.default.bold("--force")} to overwrite.`)
284740
285134
  );
284741
285135
  process.exit(1);
284742
285136
  }
284743
285137
  console.log(`
284744
- ${brand("ablo")} ${import_picocolors19.default.dim("pull drizzle")} ${import_picocolors19.default.dim(args.schema)}
285138
+ ${brand("ablo")} ${import_picocolors21.default.dim("pull drizzle")} ${import_picocolors21.default.dim(args.schema)}
284745
285139
  `);
284746
285140
  let result;
284747
285141
  try {
@@ -284749,26 +285143,26 @@ async function drizzlePull(argv) {
284749
285143
  result = await buildSchemaSourceFromDrizzle({ mod, importPath: args.importPath });
284750
285144
  } catch (err) {
284751
285145
  const msg = err instanceof Error ? err.message : String(err);
284752
- const hint = msg.includes("Cannot find package 'drizzle-orm'") ? import_picocolors19.default.dim(` (install ${import_picocolors19.default.bold("drizzle-orm")} in this project)`) : "";
284753
- console.error(import_picocolors19.default.red(` Couldn't load the schema: ${msg}`) + hint);
285146
+ const hint = msg.includes("Cannot find package 'drizzle-orm'") ? import_picocolors21.default.dim(` (install ${import_picocolors21.default.bold("drizzle-orm")} in this project)`) : "";
285147
+ console.error(import_picocolors21.default.red(` Couldn't load the schema: ${msg}`) + hint);
284754
285148
  process.exit(1);
284755
285149
  }
284756
285150
  if (result.models.length === 0) {
284757
285151
  console.error(
284758
- import_picocolors19.default.yellow(` No adoptable tables found`) + import_picocolors19.default.dim(` (a table needs an ${import_picocolors19.default.bold("id")} + ${import_picocolors19.default.bold("organization_id")} column).`)
285152
+ import_picocolors21.default.yellow(` No adoptable tables found`) + import_picocolors21.default.dim(` (a table needs an ${import_picocolors21.default.bold("id")} + ${import_picocolors21.default.bold("organization_id")} column).`)
284759
285153
  );
284760
285154
  process.exit(1);
284761
285155
  }
284762
285156
  (0, import_fs11.writeFileSync)(args.out, result.source);
284763
- console.log(` ${import_picocolors19.default.green("\u2713")} wrote ${import_picocolors19.default.bold(args.out)} ${import_picocolors19.default.dim(`(${result.models.length} models)`)}`);
284764
- console.log(` ${import_picocolors19.default.dim(`models: ${result.models.join(", ")}`)}`);
285157
+ console.log(` ${import_picocolors21.default.green("\u2713")} wrote ${import_picocolors21.default.bold(args.out)} ${import_picocolors21.default.dim(`(${result.models.length} models)`)}`);
285158
+ console.log(` ${import_picocolors21.default.dim(`models: ${result.models.join(", ")}`)}`);
284765
285159
  if (result.skipped.length > 0) {
284766
- console.log(` ${import_picocolors19.default.dim(`${result.skipped.length} table(s) skipped:`)}`);
284767
- for (const s of result.skipped) console.log(` ${import_picocolors19.default.dim(`- ${s.name}: ${s.reason}`)}`);
285160
+ console.log(` ${import_picocolors21.default.dim(`${result.skipped.length} table(s) skipped:`)}`);
285161
+ for (const s of result.skipped) console.log(` ${import_picocolors21.default.dim(`- ${s.name}: ${s.reason}`)}`);
284768
285162
  }
284769
285163
  console.log(
284770
285164
  `
284771
- ${import_picocolors19.default.dim("Enums and relations were preserved. Review the file, then")} ${import_picocolors19.default.bold("ablo check")}.
285165
+ ${import_picocolors21.default.dim("Enums and relations were preserved. Review the file, then")} ${import_picocolors21.default.bold("ablo check")}.
284772
285166
  `
284773
285167
  );
284774
285168
  }
@@ -284778,7 +285172,7 @@ init_theme();
284778
285172
 
284779
285173
  // src/cli/renderError.ts
284780
285174
  init_cjs_shims();
284781
- var import_picocolors20 = __toESM(require_picocolors(), 1);
285175
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
284782
285176
  init_errors();
284783
285177
  init_theme();
284784
285178
  var RECOVERY_HINT = {
@@ -284800,15 +285194,15 @@ function isStringArray(v2) {
284800
285194
  function renderKnownDetails(details, line) {
284801
285195
  if (!details) return;
284802
285196
  const { retryAfterSeconds, missingIds, requiredCapability, unexecutable, errors } = details;
284803
- if (typeof retryAfterSeconds === "number") line(` ${import_picocolors20.default.dim("retry")} after ${retryAfterSeconds}s`);
285197
+ if (typeof retryAfterSeconds === "number") line(` ${import_picocolors22.default.dim("retry")} after ${retryAfterSeconds}s`);
284804
285198
  if (isStringArray(missingIds) && missingIds.length > 0) {
284805
285199
  const shown = missingIds.slice(0, 5).join(", ");
284806
285200
  const more = missingIds.length > 5 ? ` (+${missingIds.length - 5} more)` : "";
284807
- line(` ${import_picocolors20.default.dim("missing")} ${shown}${more}`);
285201
+ line(` ${import_picocolors22.default.dim("missing")} ${shown}${more}`);
284808
285202
  }
284809
- if (typeof requiredCapability === "string") line(` ${import_picocolors20.default.dim("needs")} ${requiredCapability}`);
285203
+ if (typeof requiredCapability === "string") line(` ${import_picocolors22.default.dim("needs")} ${requiredCapability}`);
284810
285204
  if (Array.isArray(unexecutable) && unexecutable.length > 0) {
284811
- line(` ${import_picocolors20.default.dim("blocked")} ${unexecutable.length} change(s) can't be applied \u2014 see \`unexecutable\` (--verbose)`);
285205
+ line(` ${import_picocolors22.default.dim("blocked")} ${unexecutable.length} change(s) can't be applied \u2014 see \`unexecutable\` (--verbose)`);
284812
285206
  }
284813
285207
  if (Array.isArray(errors)) {
284814
285208
  for (const e2 of errors.slice(0, 8)) {
@@ -284816,7 +285210,7 @@ function renderKnownDetails(details, line) {
284816
285210
  const rec = e2;
284817
285211
  const where = typeof rec.param === "string" ? `${rec.param}: ` : "";
284818
285212
  const msg = typeof rec.message === "string" ? rec.message : "";
284819
- if (msg) line(` ${import_picocolors20.default.dim("\xB7")} ${where}${msg}`);
285213
+ if (msg) line(` ${import_picocolors22.default.dim("\xB7")} ${where}${msg}`);
284820
285214
  }
284821
285215
  }
284822
285216
  }
@@ -284827,22 +285221,22 @@ function renderCliError(err, opts = {}) {
284827
285221
  });
284828
285222
  const verbose = opts.verbose ?? (process.argv.includes("--verbose") || process.env.ABLO_VERBOSE === "1");
284829
285223
  if (err instanceof AbloError) {
284830
- const codeTag = err.code ? ` ${import_picocolors20.default.dim(`[${err.code}]`)}` : "";
285224
+ const codeTag = err.code ? ` ${import_picocolors22.default.dim(`[${err.code}]`)}` : "";
284831
285225
  line("");
284832
- line(` ${brand("ablo")} ${import_picocolors20.default.red("\u2717")} ${import_picocolors20.default.bold(titleForType(err.type))}${codeTag}`);
285226
+ line(` ${brand("ablo")} ${import_picocolors22.default.red("\u2717")} ${import_picocolors22.default.bold(titleForType(err.type))}${codeTag}`);
284833
285227
  line("");
284834
285228
  line(` ${err.message}`);
284835
- if (err.param) line(` ${import_picocolors20.default.dim("field")} ${err.param}`);
285229
+ if (err.param) line(` ${import_picocolors22.default.dim("field")} ${err.param}`);
284836
285230
  renderKnownDetails(err.details, line);
284837
285231
  const hint = err.code ? RECOVERY_HINT[classifyRecovery(err.code)] : void 0;
284838
- if (hint) line(` ${import_picocolors20.default.dim(hint)}`);
284839
- if (err.docUrl) line(` ${import_picocolors20.default.dim("docs")} ${err.docUrl}`);
284840
- if (err.requestId) line(` ${import_picocolors20.default.dim("ref")} ${err.requestId}`);
285232
+ if (hint) line(` ${import_picocolors22.default.dim(hint)}`);
285233
+ if (err.docUrl) line(` ${import_picocolors22.default.dim("docs")} ${err.docUrl}`);
285234
+ if (err.requestId) line(` ${import_picocolors22.default.dim("ref")} ${err.requestId}`);
284841
285235
  if (verbose) {
284842
285236
  if (err.details && Object.keys(err.details).length > 0) {
284843
- line(` ${import_picocolors20.default.dim("details")} ${JSON.stringify(err.details)}`);
285237
+ line(` ${import_picocolors22.default.dim("details")} ${JSON.stringify(err.details)}`);
284844
285238
  }
284845
- if (err.stack) line(import_picocolors20.default.dim(err.stack));
285239
+ if (err.stack) line(import_picocolors22.default.dim(err.stack));
284846
285240
  }
284847
285241
  line("");
284848
285242
  process.exitCode = 1;
@@ -284850,16 +285244,16 @@ function renderCliError(err, opts = {}) {
284850
285244
  }
284851
285245
  const message = err instanceof Error ? err.message : String(err);
284852
285246
  line("");
284853
- line(` ${brand("ablo")} ${import_picocolors20.default.red("\u2717")} ${message}`);
284854
- if (verbose && err instanceof Error && err.stack) line(import_picocolors20.default.dim(err.stack));
284855
- else line(` ${import_picocolors20.default.dim("Run with --verbose for the full error.")}`);
285247
+ line(` ${brand("ablo")} ${import_picocolors22.default.red("\u2717")} ${message}`);
285248
+ if (verbose && err instanceof Error && err.stack) line(import_picocolors22.default.dim(err.stack));
285249
+ else line(` ${import_picocolors22.default.dim("Run with --verbose for the full error.")}`);
284856
285250
  line("");
284857
285251
  process.exitCode = 1;
284858
285252
  }
284859
285253
 
284860
285254
  // src/cli/index.ts
284861
285255
  var LOGO = `
284862
- ${brand("ablo")} ${import_picocolors21.default.dim("sync engine")}
285256
+ ${brand("ablo")} ${import_picocolors23.default.dim("sync engine")}
284863
285257
  `;
284864
285258
  var SUBCOMMAND_USAGE = {
284865
285259
  connect: CONNECT_USAGE,
@@ -284894,7 +285288,7 @@ async function main() {
284894
285288
  const devArgs = process.argv.slice(3);
284895
285289
  const oneShot = devArgs.includes("--no-watch");
284896
285290
  console.log(
284897
- import_picocolors21.default.dim(
285291
+ import_picocolors23.default.dim(
284898
285292
  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."
284899
285293
  )
284900
285294
  );
@@ -284923,14 +285317,14 @@ async function main() {
284923
285317
  const guard = guardActiveProjectKey();
284924
285318
  if (!guard.ok && guard.available.length > 0 && !rest.includes("--url")) {
284925
285319
  console.error(
284926
- ` ${import_picocolors21.default.yellow("\u26A0")} active project ${import_picocolors21.default.bold(guard.activeProfile)} has no stored key ${import_picocolors21.default.dim(
285320
+ ` ${import_picocolors23.default.yellow("\u26A0")} active project ${import_picocolors23.default.bold(guard.activeProfile)} has no stored key ${import_picocolors23.default.dim(
284927
285321
  `(you have keys for: ${guard.available.join(", ")})`
284928
285322
  )}`
284929
285323
  );
284930
285324
  const loginCmd = guard.activeProfile === "default" ? "ablo login" : `ablo login --project ${guard.activeProfile}`;
284931
285325
  console.error(
284932
- import_picocolors21.default.dim(
284933
- ` Mint one with ${import_picocolors21.default.bold(loginCmd)}, or switch with ${import_picocolors21.default.bold("ablo projects use <slug>")}.`
285326
+ import_picocolors23.default.dim(
285327
+ ` Mint one with ${import_picocolors23.default.bold(loginCmd)}, or switch with ${import_picocolors23.default.bold("ablo projects use <slug>")}.`
284934
285328
  )
284935
285329
  );
284936
285330
  process.exitCode = 1;
@@ -284948,13 +285342,13 @@ async function main() {
284948
285342
  await generate(process.argv.slice(3));
284949
285343
  } else if (command === "schema") {
284950
285344
  console.error(
284951
- ` ${import_picocolors21.default.red("\u2717")} \`ablo schema push\` was renamed to \`${brand("ablo push")}\`.`
285345
+ ` ${import_picocolors23.default.red("\u2717")} \`ablo schema push\` was renamed to \`${brand("ablo push")}\`.`
284952
285346
  );
284953
285347
  console.error(` Run \`ablo push${process.argv.slice(4).join(" ") ? " " + process.argv.slice(4).join(" ") : ""}\` instead.`);
284954
285348
  process.exitCode = 1;
284955
285349
  } else {
284956
285350
  console.log(LOGO);
284957
- console.log(` ${import_picocolors21.default.bold("Usage:")}`);
285351
+ console.log(` ${import_picocolors23.default.bold("Usage:")}`);
284958
285352
  console.log(` npx ablo init Scaffold ablo/ directory + starter schema`);
284959
285353
  console.log(` npx ablo init --yes [--framework nextjs] Non-interactive (agents/CI): no prompts, flag-driven`);
284960
285354
  console.log(` [--auth apikey] [--storage replication|endpoint] [--project <slug>] [--no-project]`);
@@ -284978,8 +285372,9 @@ async function main() {
284978
285372
  console.log(` npx ablo pull drizzle <module> Generate schema.ts from a Drizzle schema (keeps enums + relations)`);
284979
285373
  console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
284980
285374
  console.log(` npx ablo connect Connect a real database \u2014 prints the logical-replication setup SQL (the one way)`);
284981
- console.log(` npx ablo connect --check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
284982
- console.log(` npx ablo connect --audit-infra Read-only audit for deprecated Ablo sync infra in a customer DB`);
285375
+ console.log(` npx ablo connect check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
285376
+ console.log(` npx ablo connect scan Read-only audit for leftover Ablo sync infra in a customer DB`);
285377
+ console.log(` npx ablo connect deregister Remove this project's data source \u2014 Ablo stops reading/writing it`);
284983
285378
  console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (optional escape hatch \u2014 \`connect\` is the way)`);
284984
285379
  console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
284985
285380
  console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
@@ -284990,10 +285385,10 @@ async function main() {
284990
285385
  console.log(` npx ablo generate Emit TypeScript types from your schema`);
284991
285386
  console.log(` npx ablo generate --out path.ts Write generated types to a path`);
284992
285387
  console.log();
284993
- console.log(` ${import_picocolors21.default.bold("Schema workflow:")}`);
285388
+ console.log(` ${import_picocolors23.default.bold("Schema workflow:")}`);
284994
285389
  console.log(` The server holds its own copy of your schema \u2014 edit ${brand("ablo/schema.ts")}, then`);
284995
285390
  console.log(` run ${brand("ablo push")} (or keep ${brand("ablo dev")} running) before the server will accept`);
284996
- console.log(` writes to new or changed models. Skip it and writes fail with ${import_picocolors21.default.yellow("server_execute_unknown_model")}.`);
285391
+ console.log(` writes to new or changed models. Skip it and writes fail with ${import_picocolors23.default.yellow("server_execute_unknown_model")}.`);
284997
285392
  console.log();
284998
285393
  }
284999
285394
  }
@@ -285044,7 +285439,7 @@ async function ensureInitProject(opts) {
285044
285439
  const ensured = await ensureProject(slug);
285045
285440
  if (ensured) {
285046
285441
  console.log(
285047
- ` ${import_picocolors21.default.green("\u2713")} ${ensured.created ? "Created" : "Using"} project ${import_picocolors21.default.bold(ensured.slug)} ${import_picocolors21.default.dim(`(${ensured.id})`)} \u2014 keys you mint for it are isolated from the org's other apps.`
285442
+ ` ${import_picocolors23.default.green("\u2713")} ${ensured.created ? "Created" : "Using"} project ${import_picocolors23.default.bold(ensured.slug)} ${import_picocolors23.default.dim(`(${ensured.id})`)} \u2014 keys you mint for it are isolated from the org's other apps.`
285048
285443
  );
285049
285444
  }
285050
285445
  }
@@ -285086,7 +285481,7 @@ async function chooseBool(flagValue, fallback, interactive, prompt) {
285086
285481
  async function init(args = []) {
285087
285482
  const opts = parseInitArgs(args);
285088
285483
  const interactive = Boolean(process.stdin.isTTY) && !opts.yes && !process.env.CI;
285089
- Ie(`${brand("ablo")} ${import_picocolors21.default.dim("sync engine")}`);
285484
+ Ie(`${brand("ablo")} ${import_picocolors23.default.dim("sync engine")}`);
285090
285485
  if (!(0, import_fs12.existsSync)("package.json")) {
285091
285486
  xe("No package.json found. Run this from your project root.");
285092
285487
  process.exit(1);
@@ -285164,7 +285559,7 @@ async function init(args = []) {
285164
285559
  if (pullExisting) {
285165
285560
  const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
285166
285561
  if (!dbUrl) {
285167
- schemaNote = import_picocolors21.default.dim(" (no DATABASE_URL \u2014 wrote starter; run `ablo pull` later)");
285562
+ schemaNote = import_picocolors23.default.dim(" (no DATABASE_URL \u2014 wrote starter; run `ablo pull` later)");
285168
285563
  } else {
285169
285564
  try {
285170
285565
  const pulled = await buildSchemaSourceFromDb({
@@ -285174,12 +285569,12 @@ async function init(args = []) {
285174
285569
  });
285175
285570
  if (pulled.models.length > 0) {
285176
285571
  schemaSource = pulled.source;
285177
- schemaNote = import_picocolors21.default.dim(` (pulled ${pulled.models.length} models)`);
285572
+ schemaNote = import_picocolors23.default.dim(` (pulled ${pulled.models.length} models)`);
285178
285573
  } else {
285179
- schemaNote = import_picocolors21.default.dim(" (no adoptable tables \u2014 wrote starter)");
285574
+ schemaNote = import_picocolors23.default.dim(" (no adoptable tables \u2014 wrote starter)");
285180
285575
  }
285181
285576
  } catch {
285182
- schemaNote = import_picocolors21.default.dim(" (pull failed \u2014 wrote starter)");
285577
+ schemaNote = import_picocolors23.default.dim(" (pull failed \u2014 wrote starter)");
285183
285578
  }
285184
285579
  }
285185
285580
  }
@@ -285205,14 +285600,14 @@ async function init(args = []) {
285205
285600
  const existing = (0, import_fs12.readFileSync)(envFile, "utf-8");
285206
285601
  if (!existing.includes("ABLO_")) {
285207
285602
  (0, import_fs12.writeFileSync)(envFile, existing + "\n" + envBody);
285208
- created.push(`${envFile} ${import_picocolors21.default.dim("(appended)")}`);
285603
+ created.push(`${envFile} ${import_picocolors23.default.dim("(appended)")}`);
285209
285604
  } else {
285210
- created.push(`${envFile} ${import_picocolors21.default.dim("(already configured)")}`);
285605
+ created.push(`${envFile} ${import_picocolors23.default.dim("(already configured)")}`);
285211
285606
  }
285212
285607
  }
285213
285608
  if (wireRealKey && resolvedKey) {
285214
285609
  wireEnvLocal(resolvedKey);
285215
- created.push(`.env.local ${import_picocolors21.default.dim("(ABLO_API_KEY set from your login)")}`);
285610
+ created.push(`.env.local ${import_picocolors23.default.dim("(ABLO_API_KEY set from your login)")}`);
285216
285611
  }
285217
285612
  if (agent) {
285218
285613
  (0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "agent.ts"), generateAgent());
@@ -285227,17 +285622,17 @@ async function init(args = []) {
285227
285622
  }
285228
285623
  const providersPath = (0, import_path7.join)(layout.appBase, "providers.tsx");
285229
285624
  (0, import_fs12.writeFileSync)(providersPath, generateProviders());
285230
- created.push(`${providersPath} ${import_picocolors21.default.dim(`(wrap ${(0, import_path7.join)(layout.appBase, "layout.tsx")} in <Providers>)`)}`);
285625
+ created.push(`${providersPath} ${import_picocolors23.default.dim(`(wrap ${(0, import_path7.join)(layout.appBase, "layout.tsx")} in <Providers>)`)}`);
285231
285626
  const sessionDir = (0, import_path7.join)(layout.appBase, "api", "ablo-session");
285232
285627
  (0, import_fs12.mkdirSync)(sessionDir, { recursive: true });
285233
285628
  (0, import_fs12.writeFileSync)((0, import_path7.join)(sessionDir, "route.ts"), generateSessionRoute());
285234
- created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${import_picocolors21.default.dim("(wire your auth)")}`);
285629
+ created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${import_picocolors23.default.dim("(wire your auth)")}`);
285235
285630
  }
285236
285631
  if (framework !== "vanilla") {
285237
285632
  (0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "TaskList.tsx"), generateComponent());
285238
285633
  created.push(`${abloDir}/TaskList.tsx`);
285239
285634
  }
285240
- Me(created.map((f) => `${import_picocolors21.default.green("\u2713")} ${f}`).join("\n"), "Created");
285635
+ Me(created.map((f) => `${import_picocolors23.default.green("\u2713")} ${f}`).join("\n"), "Created");
285241
285636
  const pm = detectPackageManager();
285242
285637
  if (opts.install) {
285243
285638
  const s = Y2();
@@ -285246,47 +285641,47 @@ async function init(args = []) {
285246
285641
  (0, import_child_process3.execSync)(`${pm} add @abloatai/ablo`, { stdio: "ignore" });
285247
285642
  s.stop("Installed @abloatai/ablo");
285248
285643
  } catch {
285249
- s.stop(`${import_picocolors21.default.yellow("!")} Couldn't auto-install \u2014 run ${import_picocolors21.default.bold(`${pm} install @abloatai/ablo`)}`);
285644
+ s.stop(`${import_picocolors23.default.yellow("!")} Couldn't auto-install \u2014 run ${import_picocolors23.default.bold(`${pm} install @abloatai/ablo`)}`);
285250
285645
  }
285251
285646
  }
285252
285647
  const steps = [
285253
- `Get a ${import_picocolors21.default.bold("sk_test_")} key at ${import_picocolors21.default.cyan("https://abloatai.com")}`,
285254
- `Run ${import_picocolors21.default.bold("npx ablo login")} (or add ${import_picocolors21.default.bold("ABLO_API_KEY")} to ${import_picocolors21.default.bold(envFile)})`,
285255
- `Set ${import_picocolors21.default.bold("DATABASE_URL")} in ${import_picocolors21.default.bold(envFile)} \u2014 your Postgres is the system of record; rows live there, never with Ablo`,
285256
- `Run ${import_picocolors21.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
285648
+ `Get a ${import_picocolors23.default.bold("sk_test_")} key at ${import_picocolors23.default.cyan("https://abloatai.com")}`,
285649
+ `Run ${import_picocolors23.default.bold("npx ablo login")} (or add ${import_picocolors23.default.bold("ABLO_API_KEY")} to ${import_picocolors23.default.bold(envFile)})`,
285650
+ `Set ${import_picocolors23.default.bold("DATABASE_URL")} in ${import_picocolors23.default.bold(envFile)} \u2014 your Postgres is the system of record; rows live there, never with Ablo`,
285651
+ `Run ${import_picocolors23.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
285257
285652
  ...storage === "replication" ? [
285258
- `Connect your database \u2014 ${import_picocolors21.default.bold("npx ablo connect")} prints the one-time logical-replication setup SQL to run on your Postgres`,
285259
- `Verify it \u2014 ${import_picocolors21.default.bold("npx ablo connect --check")} walks wal_level, the publication, the role, and replica identity, with the exact fix for anything missing`,
285260
- `Register it \u2014 ${import_picocolors21.default.bold("npx ablo connect --register")} tells Ablo to start replicating; your app keeps writing through your own backend while Ablo tails the WAL`
285653
+ `Connect your database \u2014 ${import_picocolors23.default.bold("npx ablo connect")} prints the one-time logical-replication setup SQL to run on your Postgres`,
285654
+ `Verify it \u2014 ${import_picocolors23.default.bold("npx ablo connect check")} walks wal_level, the publication, the role, and replica identity, with the exact fix for anything missing`,
285655
+ `Register it \u2014 ${import_picocolors23.default.bold("npx ablo connect register")} tells Ablo to start replicating; your app keeps writing through your own backend while Ablo tails the WAL`
285261
285656
  ] : [
285262
- `Provision your DB: ${import_picocolors21.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors21.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors21.default.bold("/api/ablo/source")}`
285657
+ `Provision your DB: ${import_picocolors23.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors23.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors23.default.bold("/api/ablo/source")}`
285263
285658
  ],
285264
285659
  ...framework === "nextjs" ? [
285265
- `Wrap ${import_picocolors21.default.bold((0, import_path7.join)(layout.appBase, "layout.tsx"))} in ${import_picocolors21.default.bold("<Providers>")} (${(0, import_path7.join)(layout.appBase, "providers.tsx")}) and add your auth to ${import_picocolors21.default.bold((0, import_path7.join)(layout.appBase, "api", "ablo-session", "route.ts"))}`
285660
+ `Wrap ${import_picocolors23.default.bold((0, import_path7.join)(layout.appBase, "layout.tsx"))} in ${import_picocolors23.default.bold("<Providers>")} (${(0, import_path7.join)(layout.appBase, "providers.tsx")}) and add your auth to ${import_picocolors23.default.bold((0, import_path7.join)(layout.appBase, "api", "ablo-session", "route.ts"))}`
285266
285661
  ] : [],
285267
- `Run ${import_picocolors21.default.bold(`${pm} run dev`)} and open two browser tabs \u2014 changes sync in real-time`,
285662
+ `Run ${import_picocolors23.default.bold(`${pm} run dev`)} and open two browser tabs \u2014 changes sync in real-time`,
285268
285663
  ...agent ? [
285269
- `Run ${import_picocolors21.default.bold(`npx tsx ${abloDir}/agent.ts`)} \u2014 an AI teammate edits the same tasks`,
285270
- `Run ${import_picocolors21.default.bold("npx ablo logs")} to watch human + agent commits stream by`
285664
+ `Run ${import_picocolors23.default.bold(`npx tsx ${abloDir}/agent.ts`)} \u2014 an AI teammate edits the same tasks`,
285665
+ `Run ${import_picocolors23.default.bold("npx ablo logs")} to watch human + agent commits stream by`
285271
285666
  ] : []
285272
285667
  ];
285273
285668
  Me(steps.map((s, i) => `${i + 1}. ${s}`).join("\n"), "Next steps");
285274
285669
  const existingKey = resolveApiKey("sandbox");
285275
285670
  if (existingKey) {
285276
285671
  await ensureInitProject(opts);
285277
- Se(`Already authorized ${import_picocolors21.default.dim(`(${existingKey.slice(0, 11)}\u2026)`)} \u2014 run ${import_picocolors21.default.bold("npx ablo push")} next. ${import_picocolors21.default.dim("Docs:")} https://abloatai.com/docs`);
285672
+ Se(`Already authorized ${import_picocolors23.default.dim(`(${existingKey.slice(0, 11)}\u2026)`)} \u2014 run ${import_picocolors23.default.bold("npx ablo push")} next. ${import_picocolors23.default.dim("Docs:")} https://abloatai.com/docs`);
285278
285673
  return;
285279
285674
  }
285280
285675
  if (interactive && opts.login) {
285281
285676
  const loginNow = await ye({ message: "Log in now? (opens your browser)", initialValue: true });
285282
285677
  if (!pD(loginNow) && loginNow) {
285283
- Se(`${import_picocolors21.default.dim("Docs:")} https://abloatai.com/docs`);
285678
+ Se(`${import_picocolors23.default.dim("Docs:")} https://abloatai.com/docs`);
285284
285679
  await login();
285285
285680
  await ensureInitProject(opts);
285286
285681
  return;
285287
285682
  }
285288
285683
  }
285289
- Se(`Run ${import_picocolors21.default.bold("npx ablo login")} when ready. ${import_picocolors21.default.dim("Docs:")} https://abloatai.com/docs`);
285684
+ Se(`Run ${import_picocolors23.default.bold("npx ablo login")} when ready. ${import_picocolors23.default.dim("Docs:")} https://abloatai.com/docs`);
285290
285685
  }
285291
285686
  function generateSchema() {
285292
285687
  return `import { defineSchema, model, relation, z } from '@abloatai/ablo/schema';