@abloatai/cli 0.47.0 → 0.49.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 (2) hide show
  1. package/dist/cli.cjs +562 -223
  2. package/package.json +5 -3
package/dist/cli.cjs CHANGED
@@ -2318,7 +2318,7 @@ function Subscribe(postgres2, options) {
2318
2318
  return;
2319
2319
  stream = null;
2320
2320
  state.pid = state.secret = void 0;
2321
- connected(await init2(sql, slot, options.publications));
2321
+ connected(await init4(sql, slot, options.publications));
2322
2322
  subscribers.forEach((event) => event.forEach(({ onsubscribe }) => onsubscribe()));
2323
2323
  },
2324
2324
  no_subscribe: true
@@ -2337,7 +2337,7 @@ function Subscribe(postgres2, options) {
2337
2337
  async function subscribe(event, fn, onsubscribe = noop2, onerror = noop2) {
2338
2338
  event = parseEvent(event);
2339
2339
  if (!connection2)
2340
- connection2 = init2(sql, slot, options.publications);
2340
+ connection2 = init4(sql, slot, options.publications);
2341
2341
  const subscriber = { fn, onsubscribe };
2342
2342
  const fns = subscribers.has(event) ? subscribers.get(event).add(subscriber) : subscribers.set(event, /* @__PURE__ */ new Set([subscriber])).get(event);
2343
2343
  const unsubscribe = () => {
@@ -2356,7 +2356,7 @@ function Subscribe(postgres2, options) {
2356
2356
  state.pid = x2.state.pid;
2357
2357
  state.secret = x2.state.secret;
2358
2358
  }
2359
- async function init2(sql2, slot2, publications) {
2359
+ async function init4(sql2, slot2, publications) {
2360
2360
  if (!publications)
2361
2361
  throw new Error("Missing publication names");
2362
2362
  const xs = await sql2.unsafe(
@@ -3455,14 +3455,14 @@ function requireKey() {
3455
3455
  }
3456
3456
  return apiKey;
3457
3457
  }
3458
- async function request(path, apiKey, init2 = {}, baseUrl2) {
3458
+ async function request(path, apiKey, init4 = {}, baseUrl2) {
3459
3459
  const res = await fetch(`${baseUrl2 ?? apiUrl()}${path}`, {
3460
- method: init2.method ?? "GET",
3460
+ method: init4.method ?? "GET",
3461
3461
  headers: {
3462
3462
  "content-type": "application/json",
3463
3463
  authorization: `Bearer ${apiKey}`
3464
3464
  },
3465
- ...init2.body !== void 0 ? { body: JSON.stringify(init2.body) } : {}
3465
+ ...init4.body !== void 0 ? { body: JSON.stringify(init4.body) } : {}
3466
3466
  });
3467
3467
  let body = {};
3468
3468
  try {
@@ -3825,6 +3825,153 @@ var init_terminalWidth = __esm({
3825
3825
  }
3826
3826
  });
3827
3827
 
3828
+ // src/observeCliError.ts
3829
+ function installCliExitObservationBoundary() {
3830
+ if (exitBoundaryInstalled) return;
3831
+ exitBoundaryInstalled = true;
3832
+ process.exit = ((code) => {
3833
+ const numericCode = code == null ? 0 : Number(code);
3834
+ if (numericCode === 0) return nativeProcessExit(code);
3835
+ throw new CliFailureExit(Number.isFinite(numericCode) ? numericCode : 1);
3836
+ });
3837
+ }
3838
+ function restoreCliExitObservationBoundary() {
3839
+ if (!exitBoundaryInstalled) return;
3840
+ process.exit = nativeProcessExit;
3841
+ exitBoundaryInstalled = false;
3842
+ }
3843
+ function init2() {
3844
+ if (!dsn) return false;
3845
+ if (!initialized) {
3846
+ Sentry.init({
3847
+ dsn,
3848
+ environment: process.env.ABLO_STAGE ?? "local",
3849
+ release,
3850
+ sendDefaultPii: false,
3851
+ enableLogs: true,
3852
+ beforeSend(event) {
3853
+ return (0, import_errorObservation.sanitizeObservationValue)(event);
3854
+ },
3855
+ beforeBreadcrumb(breadcrumb) {
3856
+ return (0, import_errorObservation.sanitizeObservationValue)(breadcrumb);
3857
+ }
3858
+ });
3859
+ initialized = true;
3860
+ }
3861
+ return true;
3862
+ }
3863
+ function commandOperation() {
3864
+ const command = process.argv[2];
3865
+ const subcommand = process.argv[3];
3866
+ const safe = (value) => value && /^[a-z][a-z0-9-]*$/i.test(value) ? value : void 0;
3867
+ return ["ablo", safe(command), safe(subcommand)].filter(Boolean).join(" ");
3868
+ }
3869
+ function detailString(details, ...keys) {
3870
+ for (const key of keys) {
3871
+ const value = details?.[key];
3872
+ if (typeof value === "string" && value.length > 0) return value;
3873
+ }
3874
+ return void 0;
3875
+ }
3876
+ function observeCliError(err) {
3877
+ const normalized = (0, import_errors5.toAbloError)(err);
3878
+ const eventId = normalized.eventId ?? (dsn ? (0, import_node_crypto.randomUUID)().replaceAll("-", "") : void 0);
3879
+ if (!eventId || !init2()) return normalized.eventId;
3880
+ try {
3881
+ const spec = normalized.code ? (0, import_errorCodes.errorCodeSpec)(normalized.code) : void 0;
3882
+ const policy = spec?.observability ?? {
3883
+ severity: "error",
3884
+ sentry: "issue",
3885
+ pagingEligible: false,
3886
+ expectedVolume: "low",
3887
+ owner: "product"
3888
+ };
3889
+ const details = normalized.details;
3890
+ const organizationId = detailString(details, "organizationId", "organization_id");
3891
+ const projectId = detailString(details, "projectId", "project_id");
3892
+ const branchId = detailString(details, "branchId", "branch_id");
3893
+ const dataSourceId = detailString(details, "dataSourceId", "data_source_id");
3894
+ const model = detailString(details, "model", "modelName", "model_name");
3895
+ const candidate = (0, import_errorObservation.sanitizeObservationValue)({
3896
+ eventId,
3897
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
3898
+ service: "ablo-cli",
3899
+ stage: process.env.ABLO_STAGE ?? "local",
3900
+ ...release ? { release } : {},
3901
+ severity: normalized.eventId ? "warning" : policy.severity,
3902
+ channel: "cli",
3903
+ scope: "command",
3904
+ operation: commandOperation(),
3905
+ errorCode: normalized.code ?? "internal_error",
3906
+ errorType: normalized.type,
3907
+ category: spec?.category ?? "client",
3908
+ retryable: spec?.retryable ?? false,
3909
+ handled: normalized.eventId !== void 0 || policy.sentry === "log",
3910
+ publicMessage: normalized.message,
3911
+ internalMessage: normalized.message,
3912
+ ...normalized.stack ? { stack: normalized.stack } : {},
3913
+ ...normalized.requestId ? { requestId: normalized.requestId } : {},
3914
+ ...organizationId ? { organizationId } : {},
3915
+ ...projectId ? { projectId } : {},
3916
+ ...branchId ? { branchId } : {},
3917
+ ...dataSourceId ? { dataSourceId } : {},
3918
+ ...model ? { model } : {},
3919
+ ...err instanceof CliFailureExit ? { diagnosticContext: { exitCode: err.exitCode } } : {}
3920
+ });
3921
+ const observation = import_errorObservation.errorObservationSchema.parse(candidate);
3922
+ if (normalized.eventId || policy.sentry === "log") {
3923
+ Sentry.logger.warn(observation.publicMessage, { ...observation });
3924
+ } else {
3925
+ Sentry.captureException(err, {
3926
+ event_id: observation.eventId,
3927
+ captureContext: {
3928
+ level: observation.severity,
3929
+ fingerprint: [observation.service, observation.operation, observation.errorCode],
3930
+ tags: {
3931
+ service: observation.service,
3932
+ channel: observation.channel,
3933
+ scope: observation.scope,
3934
+ error_code: observation.errorCode
3935
+ },
3936
+ contexts: { ablo: { ...observation } }
3937
+ }
3938
+ });
3939
+ }
3940
+ return eventId;
3941
+ } catch {
3942
+ return normalized.eventId;
3943
+ }
3944
+ }
3945
+ async function flushCliErrors(timeoutMs = 2e3) {
3946
+ if (!initialized) return true;
3947
+ return Sentry.flush(timeoutMs);
3948
+ }
3949
+ var import_node_crypto, Sentry, import_errorCodes, import_errorObservation, import_errors5, dsn, release, initialized, nativeProcessExit, exitBoundaryInstalled, CliFailureExit;
3950
+ var init_observeCliError = __esm({
3951
+ "src/observeCliError.ts"() {
3952
+ "use strict";
3953
+ init_cjs_shims();
3954
+ import_node_crypto = require("crypto");
3955
+ Sentry = __toESM(require("@sentry/node"), 1);
3956
+ import_errorCodes = require("@abloatai/transaction/errorCodes");
3957
+ import_errorObservation = require("@abloatai/transaction/errorObservation");
3958
+ import_errors5 = require("@abloatai/transaction/errors");
3959
+ dsn = process.env.ABLO_CLI_SENTRY_DSN ?? "https://1ac154bff10b06836e1ea9de9e0d92f0@o4510928209772544.ingest.de.sentry.io/4511660691423312" ?? "";
3960
+ release = process.env.ABLO_CLI_RELEASE ?? "@abloatai/cli@0.49.0";
3961
+ initialized = false;
3962
+ nativeProcessExit = process.exit.bind(process);
3963
+ exitBoundaryInstalled = false;
3964
+ CliFailureExit = class extends Error {
3965
+ exitCode;
3966
+ constructor(exitCode) {
3967
+ super(`CLI command exited with status ${exitCode}`);
3968
+ this.name = "CliFailureExit";
3969
+ this.exitCode = exitCode;
3970
+ }
3971
+ };
3972
+ }
3973
+ });
3974
+
3828
3975
  // src/renderError.ts
3829
3976
  function titleForType(type) {
3830
3977
  const core = type.replace(/^Ablo/, "").replace(/Error$/, "");
@@ -3853,6 +4000,17 @@ function wrapParagraph(text, indent) {
3853
4000
  function renderKnownDetails(details, line) {
3854
4001
  if (!details) return;
3855
4002
  const { retryAfterSeconds, missingIds, requiredCapability, unexecutable, errors, target } = details;
4003
+ if (details.topology === "localhost") {
4004
+ const commands = isStringArray(details.recommended_commands) ? details.recommended_commands : ["ablo migrate", "ablo dev --local"];
4005
+ line(` ${import_picocolors4.default.dim("setup")} ${commands.join(" \u2192 ")}`);
4006
+ if (typeof details.mode === "string") line(` ${import_picocolors4.default.dim("mode")} ${details.mode}`);
4007
+ if (typeof details.limitation === "string") {
4008
+ line(` ${import_picocolors4.default.dim("note")} ${details.limitation}`);
4009
+ }
4010
+ if (isStringArray(details.alternatives)) {
4011
+ line(` ${import_picocolors4.default.dim("other")} ${details.alternatives.join(" \xB7 ")}`);
4012
+ }
4013
+ }
3856
4014
  if (typeof target === "string") line(` ${import_picocolors4.default.dim("tried")} ${target}`);
3857
4015
  if (typeof retryAfterSeconds === "number") line(` ${import_picocolors4.default.dim("retry")} after ${retryAfterSeconds}s`);
3858
4016
  if (isStringArray(missingIds) && missingIds.length > 0) {
@@ -3876,17 +4034,18 @@ function renderKnownDetails(details, line) {
3876
4034
  }
3877
4035
  }
3878
4036
  function renderCliError(err, opts = {}) {
4037
+ const observedEventId = observeCliError(err);
3879
4038
  const line = opts.write ?? ((l2) => {
3880
4039
  console.error(l2);
3881
4040
  });
3882
4041
  const verbose = opts.verbose ?? (process.argv.includes("--verbose") || process.env.ABLO_VERBOSE === "1");
3883
4042
  const json = opts.json ?? (process.argv.includes("--json") || process.env.ABLO_JSON === "1");
3884
4043
  if (json) {
3885
- line(JSON.stringify((0, import_errors5.toAbloError)(err).toJSON()));
4044
+ line(JSON.stringify((0, import_errors6.toAbloError)(err).toJSON()));
3886
4045
  process.exitCode = 1;
3887
4046
  return;
3888
4047
  }
3889
- if (err instanceof import_errors5.AbloError) {
4048
+ if (err instanceof import_errors6.AbloError) {
3890
4049
  const codeTag = err.code ? ` ${import_picocolors4.default.dim(`[${err.code}]`)}` : "";
3891
4050
  line("");
3892
4051
  line(` ${brand("ablo")} ${import_picocolors4.default.red("\u2717")} ${import_picocolors4.default.bold(titleForType(err.type))}${codeTag}`);
@@ -3899,10 +4058,13 @@ function renderCliError(err, opts = {}) {
3899
4058
  const field = (s) => fields.push(s);
3900
4059
  if (err.param) field(` ${import_picocolors4.default.dim("field")} ${err.param}`);
3901
4060
  renderKnownDetails(err.details, field);
3902
- const hint = err.code ? RECOVERY_HINT[(0, import_errors5.classifyRecovery)(err.code)] : void 0;
4061
+ const hint = err.code ? RECOVERY_HINT[(0, import_errors6.classifyRecovery)(err.code)] : void 0;
3903
4062
  if (hint) field(` ${import_picocolors4.default.dim(hint)}`);
3904
4063
  if (err.docUrl) field(` ${import_picocolors4.default.dim("docs")} ${err.docUrl}`);
3905
4064
  if (err.requestId) field(` ${import_picocolors4.default.dim("ref")} ${err.requestId}`);
4065
+ if (err.eventId ?? observedEventId) {
4066
+ field(` ${import_picocolors4.default.dim("event")} ${err.eventId ?? observedEventId}`);
4067
+ }
3906
4068
  if (fields.length > 0) {
3907
4069
  line("");
3908
4070
  for (const f of fields) line(f);
@@ -3925,15 +4087,16 @@ function renderCliError(err, opts = {}) {
3925
4087
  line("");
3926
4088
  process.exitCode = 1;
3927
4089
  }
3928
- var import_picocolors4, import_errors5, RECOVERY_HINT;
4090
+ var import_picocolors4, import_errors6, RECOVERY_HINT;
3929
4091
  var init_renderError = __esm({
3930
4092
  "src/renderError.ts"() {
3931
4093
  "use strict";
3932
4094
  init_cjs_shims();
3933
4095
  import_picocolors4 = __toESM(require_picocolors(), 1);
3934
- import_errors5 = require("@abloatai/transaction/errors");
4096
+ import_errors6 = require("@abloatai/transaction/errors");
3935
4097
  init_terminalWidth();
3936
4098
  init_theme();
4099
+ init_observeCliError();
3937
4100
  RECOVERY_HINT = {
3938
4101
  transient: "This looks transient \u2014 retry in a moment.",
3939
4102
  permission: "Your key isn't allowed to do this \u2014 check its scopes or role.",
@@ -4030,7 +4193,7 @@ function parsePushArgs(argv) {
4030
4193
  const spec = argv[++i] ?? "";
4031
4194
  const [from, to] = spec.split(":");
4032
4195
  if (!from || !to) {
4033
- throw new import_errors6.AbloValidationError(`--rename expects "old:new", got "${spec}"`, { code: "cli_invalid_arguments" });
4196
+ throw new import_errors7.AbloValidationError(`--rename expects "old:new", got "${spec}"`, { code: "cli_invalid_arguments" });
4034
4197
  }
4035
4198
  renames.push({ from, to });
4036
4199
  break;
@@ -4044,13 +4207,13 @@ function parsePushArgs(argv) {
4044
4207
  const modelName = dot === -1 ? "" : path.slice(0, dot);
4045
4208
  const fieldName = dot === -1 ? "" : path.slice(dot + 1);
4046
4209
  if (!modelName || !fieldName || eq === -1) {
4047
- throw new import_errors6.AbloValidationError(`--backfill expects "model.field=value", got "${spec}"`, { code: "cli_invalid_arguments" });
4210
+ throw new import_errors7.AbloValidationError(`--backfill expects "model.field=value", got "${spec}"`, { code: "cli_invalid_arguments" });
4048
4211
  }
4049
4212
  backfills.push({ model: modelName, field: fieldName, value: coerceBackfill(rawValue) });
4050
4213
  break;
4051
4214
  }
4052
4215
  default:
4053
- throw new import_errors6.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
4216
+ throw new import_errors7.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
4054
4217
  }
4055
4218
  }
4056
4219
  url = url.replace(/\/+$/, "");
@@ -4078,7 +4241,7 @@ function publicationGap(value) {
4078
4241
  async function loadSchema(schemaPath, exportName) {
4079
4242
  const abs = (0, import_path3.resolve)(process.cwd(), schemaPath);
4080
4243
  if (!(0, import_fs4.existsSync)(abs)) {
4081
- throw new import_errors6.AbloValidationError(
4244
+ throw new import_errors7.AbloValidationError(
4082
4245
  `schema not found at ${import_picocolors5.default.bold(schemaPath)}. Run ${import_picocolors5.default.bold("npx ablo init")} or pass ${import_picocolors5.default.bold("--schema <path>")}.`,
4083
4246
  { code: "cli_invalid_arguments" }
4084
4247
  );
@@ -4089,7 +4252,7 @@ async function loadSchema(schemaPath, exportName) {
4089
4252
  const nested = mod.default && typeof mod.default === "object" ? mod.default : void 0;
4090
4253
  const schema = mod[exportName] ?? nested?.[exportName];
4091
4254
  if (!schema || typeof schema !== "object" || !("models" in schema)) {
4092
- throw new import_errors6.AbloValidationError(
4255
+ throw new import_errors7.AbloValidationError(
4093
4256
  `${import_picocolors5.default.bold(schemaPath)} has no \`${exportName}\` export that looks like a Schema. Did you \`export const ${exportName} = defineSchema({ ... })\`?`,
4094
4257
  { code: "cli_invalid_arguments" }
4095
4258
  );
@@ -4099,9 +4262,11 @@ async function loadSchema(schemaPath, exportName) {
4099
4262
  function maskKey(key) {
4100
4263
  return key ? `${key.slice(0, 12)}\u2026` : "(none)";
4101
4264
  }
4102
- function schemaPushPlaneHint(code) {
4103
- if (code !== "test_database_not_registered") return null;
4104
- return `The key authenticated and already passed ${import_picocolors5.default.bold("schema:push")} authorization. This is a branch storage-provisioning error, not a key-scope error. Check the plane with ${import_picocolors5.default.bold("npx ablo branch status <branch>")}; a branch reported as hosted should not need a customer database.`;
4265
+ function schemaPushStorageHint(code) {
4266
+ if (code === "no_data_source_registered") {
4267
+ return `This branch is not connected to your database yet. Run ${import_picocolors5.default.bold("ablo connect")} for this branch, then retry the schema push.`;
4268
+ }
4269
+ return null;
4105
4270
  }
4106
4271
  function schemaGitState(schemaPath) {
4107
4272
  try {
@@ -4277,7 +4442,7 @@ async function push(argv) {
4277
4442
  try {
4278
4443
  process.loadEnvFile(args.envFile);
4279
4444
  } catch (error) {
4280
- throw new import_errors6.AbloValidationError(
4445
+ throw new import_errors7.AbloValidationError(
4281
4446
  `could not load --env-file ${args.envFile}: ${error instanceof Error ? error.message : String(error)}`,
4282
4447
  { code: "cli_invalid_arguments" }
4283
4448
  );
@@ -4411,9 +4576,9 @@ async function push(argv) {
4411
4576
  const serverMsg = body.message ?? body.reason;
4412
4577
  console.error(import_picocolors5.default.red(` Forbidden${code ? ` [${code}]` : ""}: ${serverMsg ?? "permission denied"}`));
4413
4578
  console.error(import_picocolors5.default.dim(` Push used ${import_picocolors5.default.bold(maskKey(args.apiKey))} from ${describeKeySource(keySource)}.`));
4414
- const planeHint = schemaPushPlaneHint(code);
4415
- if (planeHint) {
4416
- console.error(import_picocolors5.default.dim(` ${planeHint}`));
4579
+ const storageHint = schemaPushStorageHint(code);
4580
+ if (storageHint) {
4581
+ console.error(import_picocolors5.default.dim(` ${storageHint}`));
4417
4582
  } else if (code === "database_role_cannot_enforce_rls") {
4418
4583
  console.error(
4419
4584
  import_picocolors5.default.dim(
@@ -4452,17 +4617,17 @@ async function push(argv) {
4452
4617
  );
4453
4618
  }
4454
4619
  } else {
4455
- renderCliError((0, import_errors6.translateHttpError)(status2, Object.keys(body).length > 0 ? body : bodyText));
4620
+ renderCliError((0, import_errors7.translateHttpError)(status2, Object.keys(body).length > 0 ? body : bodyText));
4456
4621
  }
4457
4622
  process.exit(1);
4458
4623
  }
4459
- var import_picocolors5, import_errors6, import_credentialPolicy, import_fs4, import_path3, import_child_process, import_schema, import_schema2, DEFAULT_SCHEMA_PATH, DEFAULT_EXPORT, PUSH_USAGE;
4624
+ var import_picocolors5, import_errors7, import_credentialPolicy, import_fs4, import_path3, import_child_process, import_schema, import_schema2, DEFAULT_SCHEMA_PATH, DEFAULT_EXPORT, PUSH_USAGE;
4460
4625
  var init_push = __esm({
4461
4626
  "src/push.ts"() {
4462
4627
  "use strict";
4463
4628
  init_cjs_shims();
4464
4629
  import_picocolors5 = __toESM(require_picocolors(), 1);
4465
- import_errors6 = require("@abloatai/transaction/errors");
4630
+ import_errors7 = require("@abloatai/transaction/errors");
4466
4631
  import_credentialPolicy = require("@abloatai/transaction/auth/credentialPolicy");
4467
4632
  import_fs4 = require("fs");
4468
4633
  import_path3 = require("path");
@@ -4702,7 +4867,7 @@ function connectSetupSql(input) {
4702
4867
  const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : import_footprint.ABLO_WRITE_ROLE;
4703
4868
  const tables = input.tables ?? [];
4704
4869
  const schema = input.schema ?? "public";
4705
- const publication = input.publication ?? import_footprint.ABLO_PUBLICATION;
4870
+ const publication = input.publication;
4706
4871
  const qualifiedTables = tables.map((table) => `${quoteIdent(schema)}.${quoteIdent(table)}`);
4707
4872
  const publicationTarget = tables.length > 0 ? `FOR TABLE ${qualifiedTables.join(", ")}` : "FOR ALL TABLES";
4708
4873
  const tableList = qualifiedTables.join(", ");
@@ -4780,8 +4945,8 @@ BEGIN
4780
4945
  END $$;`
4781
4946
  ];
4782
4947
  }
4783
- function reconcilePublicationPlan(current, desiredTables, opts = {}) {
4784
- const pub = quoteIdent(opts.publication ?? import_footprint.ABLO_PUBLICATION);
4948
+ function reconcilePublicationPlan(current, desiredTables, opts) {
4949
+ const pub = quoteIdent(opts.publication);
4785
4950
  const schema = opts.schema ?? "public";
4786
4951
  const qualified = (table) => `${quoteIdent(schema)}.${quoteIdent(table)}`;
4787
4952
  const desiredAll = desiredTables.length === 0;
@@ -4817,8 +4982,8 @@ function reconcilePublicationPlan(current, desiredTables, opts = {}) {
4817
4982
  recreated: false
4818
4983
  };
4819
4984
  }
4820
- async function readPublicationState(sql, opts = {}) {
4821
- const publication = opts.publication ?? import_footprint.ABLO_PUBLICATION;
4985
+ async function readPublicationState(sql, opts) {
4986
+ const publication = opts.publication;
4822
4987
  const schema = opts.schema ?? "public";
4823
4988
  const pubRows = await sql.unsafe(
4824
4989
  `SELECT puballtables FROM pg_publication WHERE pubname = $1`,
@@ -4833,8 +4998,8 @@ async function readPublicationState(sql, opts = {}) {
4833
4998
  );
4834
4999
  return { exists: true, allTables: false, tables: tableRows.map((r2) => r2.tablename) };
4835
5000
  }
4836
- async function probeReadiness(sql, opts = {}) {
4837
- const publication = opts.publication ?? import_footprint.ABLO_PUBLICATION;
5001
+ async function probeReadiness(sql, opts) {
5002
+ const publication = opts.publication;
4838
5003
  const schema = opts.schema ?? "public";
4839
5004
  const coordinated = opts.coordinatedTables && opts.coordinatedTables.length > 0 ? new Set(opts.coordinatedTables) : null;
4840
5005
  const items = [];
@@ -4981,6 +5146,18 @@ async function registerDirectDataSource(opts) {
4981
5146
  ` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`
4982
5147
  )
4983
5148
  );
5149
+ } else if (err.code === "database_loopback_requires_connector") {
5150
+ console.error(`
5151
+ ${import_picocolors7.default.cyan("Recommended for this localhost-first project")}
5152
+ 1. ${import_picocolors7.default.bold("npx ablo migrate")} ${import_picocolors7.default.dim("(once: models + idempotency + outbox)")}
5153
+ 2. ${import_picocolors7.default.bold("npx ablo dev --local")} ${import_picocolors7.default.dim("(keep running beside the app)")}
5154
+
5155
+ ${import_picocolors7.default.dim("This keeps Postgres private and supports reads, coordinated writes, claims,")}
5156
+ ${import_picocolors7.default.dim("subscriptions, and confirmations through the signed Data Source connector.")}
5157
+ ${import_picocolors7.default.yellow("Note:")} ${import_picocolors7.default.dim("raw SQL or unrelated ORM writes are not automatically observed without WAL.")}
5158
+
5159
+ ${import_picocolors7.default.dim("If every arbitrary database write must be observed, use a secure database-capable")}
5160
+ ${import_picocolors7.default.dim("tunnel, hosted direct Postgres, PrivateLink, peering, or VPN\u2014not a transaction pooler.")}`);
4984
5161
  } else if (err.code === "database_not_replication_ready" || err.code === "data_source_blocked") {
4985
5162
  for (const f of failures) {
4986
5163
  const { label, fix } = describeRemoteFailure(f);
@@ -5011,9 +5188,9 @@ async function registerDirectDataSource(opts) {
5011
5188
  }
5012
5189
  console.error(
5013
5190
  import_picocolors7.default.dim(
5014
- ` Ablo's servers must be able to reach this database \u2014 a localhost or private-network
5015
- Postgres can't use the direct path. Establish an allowlist, PrivateLink, peering, or VPN.
5016
- Only when no inbound route is possible, register the signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback.`
5191
+ ` Ablo's servers must be able to reach this database for the direct WAL path.
5192
+ For localhost development, run ${import_picocolors7.default.bold("ablo dev --local")}. For private deployments,
5193
+ establish an allowlist, PrivateLink, peering, or VPN.`
5017
5194
  )
5018
5195
  );
5019
5196
  }
@@ -5075,9 +5252,9 @@ async function deregisterDataSource(opts) {
5075
5252
  });
5076
5253
  return { removed: true, response };
5077
5254
  } catch (err) {
5078
- if (err instanceof import_errors8.AbloError && err.code === "entity_not_found") return { removed: false };
5079
- if (err instanceof import_errors8.AbloError && err.code === "forbidden") {
5080
- throw new import_errors8.AbloPermissionError(
5255
+ if (err instanceof import_errors9.AbloError && err.code === "entity_not_found") return { removed: false };
5256
+ if (err instanceof import_errors9.AbloError && err.code === "forbidden") {
5257
+ throw new import_errors9.AbloPermissionError(
5081
5258
  `${err.message}. Disconnecting needs a branch-bound secret key (sk_\u2026).`,
5082
5259
  {
5083
5260
  code: "forbidden",
@@ -5133,7 +5310,7 @@ async function disconnect(argv) {
5133
5310
  console.log(DISCONNECT_USAGE);
5134
5311
  return;
5135
5312
  } else {
5136
- throw new import_errors8.AbloValidationError(
5313
+ throw new import_errors9.AbloValidationError(
5137
5314
  `unknown flag: ${arg} \u2014 see \`ablo connect deregister --help\``,
5138
5315
  { code: "cli_invalid_arguments" }
5139
5316
  );
@@ -5148,7 +5325,7 @@ async function disconnect(argv) {
5148
5325
  try {
5149
5326
  process.loadEnvFile(envFile);
5150
5327
  } catch (error) {
5151
- throw new import_errors8.AbloValidationError(
5328
+ throw new import_errors9.AbloValidationError(
5152
5329
  `could not load --env-file ${envFile}: ${error instanceof Error ? error.message : String(error)}`,
5153
5330
  { code: "cli_invalid_arguments" }
5154
5331
  );
@@ -5156,7 +5333,7 @@ async function disconnect(argv) {
5156
5333
  }
5157
5334
  const selected = keyEnv ? readProjectEnvVariable(keyEnv) : null;
5158
5335
  if (keyEnv && !selected) {
5159
- throw new import_errors8.AbloAuthenticationError(
5336
+ throw new import_errors9.AbloAuthenticationError(
5160
5337
  `No value named ${keyEnv} was found in the process environment, .env.local, or .env.`,
5161
5338
  { code: "cli_api_key_missing" }
5162
5339
  );
@@ -5168,7 +5345,7 @@ async function disconnect(argv) {
5168
5345
  const apiKey = resolved.key;
5169
5346
  const keySource = resolved.source ?? "stored";
5170
5347
  if (!apiKey) {
5171
- throw new import_errors8.AbloAuthenticationError(
5348
+ throw new import_errors9.AbloAuthenticationError(
5172
5349
  "Disconnecting needs a branch-bound sk_ key. Set ABLO_API_KEY, pass `--env-file .env.local`, or select a named recovery key with `--key-env <NAME>`.",
5173
5350
  { code: "cli_api_key_missing" }
5174
5351
  );
@@ -5182,7 +5359,7 @@ async function disconnect(argv) {
5182
5359
  `);
5183
5360
  if (!skipConfirm) {
5184
5361
  if (!process.stdout.isTTY) {
5185
- throw new import_errors8.AbloValidationError(
5362
+ throw new import_errors9.AbloValidationError(
5186
5363
  "This session has no terminal to confirm in. Re-run with --yes to disconnect non-interactively.",
5187
5364
  { code: "cli_invalid_arguments" }
5188
5365
  );
@@ -5205,14 +5382,14 @@ async function disconnect(argv) {
5205
5382
  }
5206
5383
  renderDisconnected(outcome.response, project, branchLabel);
5207
5384
  }
5208
- var import_picocolors8, import_errors8, import_wire4, DISCONNECT_USAGE;
5385
+ var import_picocolors8, import_errors9, import_wire4, DISCONNECT_USAGE;
5209
5386
  var init_disconnect = __esm({
5210
5387
  "src/disconnect.ts"() {
5211
5388
  "use strict";
5212
5389
  init_cjs_shims();
5213
5390
  import_picocolors8 = __toESM(require_picocolors(), 1);
5214
5391
  init_dist2();
5215
- import_errors8 = require("@abloatai/transaction/errors");
5392
+ import_errors9 = require("@abloatai/transaction/errors");
5216
5393
  import_wire4 = require("@abloatai/transaction/wire");
5217
5394
  init_config();
5218
5395
  init_dbRole();
@@ -5465,7 +5642,7 @@ function blockers(input) {
5465
5642
  }
5466
5643
  if (input.dataSource.kind === "none") {
5467
5644
  found.push({
5468
- problem: "no database is connected to this plane, so writes are held rather than routed",
5645
+ problem: "no database is connected to this branch, so writes are held",
5469
5646
  fix: "connect one with `ablo connect apply`"
5470
5647
  });
5471
5648
  }
@@ -5517,7 +5694,7 @@ function connectApplyPlan(input) {
5517
5694
  const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : import_footprint.ABLO_WRITE_ROLE;
5518
5695
  const tables = input.tables ?? [];
5519
5696
  const schema = input.schema ?? "public";
5520
- const publication = input.publication ?? import_footprint.ABLO_PUBLICATION;
5697
+ const publication = input.publication;
5521
5698
  const provider = input.provider ?? "generic";
5522
5699
  const recipe = connectSetupSql({ tables, role, writeRole, schema, publication });
5523
5700
  const isWal = (s) => s.startsWith("ALTER SYSTEM SET wal_level");
@@ -5733,7 +5910,7 @@ function rotateWithoutConnection(input) {
5733
5910
  }
5734
5911
  if (!input.known || input.planeHasConnection) return null;
5735
5912
  if (input.existingRoles.length > 0) return null;
5736
- return "This plane has no connected database and Ablo's roles are not in this database, so there is no credential to re-key. Connecting for the first time is `ablo connect apply`, which creates the roles and registers them in one run.";
5913
+ return "This branch has no connected database and Ablo's roles are not in this database, so there is no credential to re-key. Connecting for the first time is `ablo connect apply`, which creates the roles and registers them in one run.";
5737
5914
  }
5738
5915
  async function locateExistingConnection(input) {
5739
5916
  const result = await tryControlPlane({
@@ -5820,7 +5997,7 @@ async function runConnectApply(args) {
5820
5997
  const verb = rotating ? "connect rotate" : "connect apply";
5821
5998
  let adminUrl = args.url ?? readProjectAdminDatabaseUrl();
5822
5999
  if (!adminUrl) {
5823
- throw new import_errors9.AbloValidationError(
6000
+ throw new import_errors10.AbloValidationError(
5824
6001
  "No admin connection string. Pass --url <admin-conn> (or set DATABASE_URL) and re-run.",
5825
6002
  { code: "cli_database_url_missing" }
5826
6003
  );
@@ -5837,7 +6014,7 @@ async function runConnectApply(args) {
5837
6014
  const loggedIn = resolveManagementKey() !== void 0;
5838
6015
  const ambient = ambientEnvKeyNote();
5839
6016
  const retry = `npx ablo connect ${rotating ? "rotate" : "apply"} --env-file .env.local --yes`;
5840
- throw new import_errors9.AbloAuthenticationError(
6017
+ throw new import_errors10.AbloAuthenticationError(
5841
6018
  loggedIn ? `You are logged in, but connect needs a branch-bound runtime key.
5842
6019
 
5843
6020
  Logging in stores an mk_ management credential; it can manage branches but cannot read, write, or register a database. Set ABLO_API_KEY to the sk_ key for the exact branch this database should join. The server confirms whether that branch is the production root or a development child; the CLI does not infer it from the key spelling.${ambient ? `
@@ -5902,22 +6079,6 @@ ${ambient}` : ""}`,
5902
6079
  ` + import_picocolors11.default.dim(
5903
6080
  ` Replication cannot run over a pooler, so if that is what this is, point ${import_picocolors11.default.bold("--url")}
5904
6081
  at the database itself. Carrying on, since a database can use this port too.
5905
- `
5906
- )
5907
- );
5908
- }
5909
- const coordinatedTables = await schemaDeclaredTables() ?? [];
5910
- const tables = args.tables.length > 0 ? args.tables : coordinatedTables;
5911
- if (tables.length === 0) {
5912
- throw new import_errors9.AbloValidationError(
5913
- `No mapped tables were found for schema ${args.schema}. Push the Ablo schema first, or pass --tables a,b,c. A project binding must enumerate its own schema-qualified tables; it cannot publish every table in a shared database.`,
5914
- { code: "cli_invalid_arguments" }
5915
- );
5916
- }
5917
- if (args.tables.length === 0) {
5918
- console.log(
5919
- import_picocolors11.default.dim(
5920
- ` publishing the ${tables.length} table${tables.length === 1 ? "" : "s"} declared by your Ablo schema in ${import_picocolors11.default.bold(args.schema)} (${import_picocolors11.default.bold("--tables")} to override)
5921
6082
  `
5922
6083
  )
5923
6084
  );
@@ -5934,7 +6095,7 @@ ${ambient}` : ""}`,
5934
6095
  }
5935
6096
  const confirmed = connectTarget?.confirmed;
5936
6097
  if (!confirmed?.branchId) {
5937
- throw new import_errors9.AbloConnectionError(
6098
+ throw new import_errors10.AbloConnectionError(
5938
6099
  "Ablo could not confirm the branch this key targets, so it cannot derive an isolated database footprint safely. Check the API URL/key and re-run.",
5939
6100
  { code: "cli_database_unreachable" }
5940
6101
  );
@@ -5965,6 +6126,22 @@ ${ambient}` : ""}`,
5965
6126
  );
5966
6127
  process.exit(1);
5967
6128
  }
6129
+ const coordinatedTables = await schemaDeclaredTables() ?? [];
6130
+ const tables = args.tables.length > 0 ? args.tables : coordinatedTables;
6131
+ if (tables.length === 0) {
6132
+ throw new import_errors10.AbloValidationError(
6133
+ `No mapped tables were found for schema ${args.schema}. Push the Ablo schema first, or pass --tables a,b,c. A project binding must enumerate its own schema-qualified tables; it cannot publish every table in a shared database.`,
6134
+ { code: "cli_invalid_arguments" }
6135
+ );
6136
+ }
6137
+ if (args.tables.length === 0) {
6138
+ console.log(
6139
+ import_picocolors11.default.dim(
6140
+ ` publishing the ${tables.length} table${tables.length === 1 ? "" : "s"} declared by your Ablo schema in ${import_picocolors11.default.bold(args.schema)} (${import_picocolors11.default.bold("--tables")} to override)
6141
+ `
6142
+ )
6143
+ );
6144
+ }
5968
6145
  let rotatePlane = null;
5969
6146
  if (rotating) {
5970
6147
  const state = await fetchDataSourceState(apiBaseUrl(), apiKey).catch(
@@ -6000,7 +6177,7 @@ ${ambient}` : ""}`,
6000
6177
  } catch (err) {
6001
6178
  await admin.end({ timeout: 2 }).catch(() => void 0);
6002
6179
  const pg = err ?? {};
6003
- throw new import_errors9.AbloConnectionError(`Couldn't connect: ${pg.message ?? String(err)}`, {
6180
+ throw new import_errors10.AbloConnectionError(`Couldn't connect: ${pg.message ?? String(err)}`, {
6004
6181
  code: "cli_database_unreachable",
6005
6182
  details: { target },
6006
6183
  cause: err
@@ -6257,7 +6434,7 @@ ${ambient}` : ""}`,
6257
6434
  }
6258
6435
  process.exit(outcome.exitCode);
6259
6436
  }
6260
- var import_picocolors11, import_errors9, import_footprint2, ROTATE_STRANDED_CREDENTIALS_NOTICE;
6437
+ var import_picocolors11, import_errors10, import_footprint2, ROTATE_STRANDED_CREDENTIALS_NOTICE;
6261
6438
  var init_connectApply = __esm({
6262
6439
  "src/connectApply.ts"() {
6263
6440
  "use strict";
@@ -6265,7 +6442,7 @@ var init_connectApply = __esm({
6265
6442
  import_picocolors11 = __toESM(require_picocolors(), 1);
6266
6443
  init_src();
6267
6444
  init_dist2();
6268
- import_errors9 = require("@abloatai/transaction/errors");
6445
+ import_errors10 = require("@abloatai/transaction/errors");
6269
6446
  import_footprint2 = require("@abloatai/transaction/footprint");
6270
6447
  init_connectSetup();
6271
6448
  init_connectOwnership();
@@ -6329,7 +6506,7 @@ function parseConnectArgs(argv) {
6329
6506
  locate = true;
6330
6507
  break;
6331
6508
  default:
6332
- throw new import_errors10.AbloValidationError(
6509
+ throw new import_errors11.AbloValidationError(
6333
6510
  `unknown connect subcommand: ${lead} (expected register, deregister, check, apply, rotate, resnapshot, scan, locate)`,
6334
6511
  { code: "cli_invalid_arguments" }
6335
6512
  );
@@ -6372,7 +6549,7 @@ function parseConnectArgs(argv) {
6372
6549
  case "--route": {
6373
6550
  const value = argv[++i] ?? "";
6374
6551
  if (!DIRECT_DATA_SOURCE_ROUTES.includes(value)) {
6375
- throw new import_errors10.AbloValidationError(
6552
+ throw new import_errors11.AbloValidationError(
6376
6553
  `invalid direct route: ${value || "(missing)"} (expected ${DIRECT_DATA_SOURCE_ROUTES.join(", ")})`,
6377
6554
  { code: "cli_invalid_arguments" }
6378
6555
  );
@@ -6381,11 +6558,11 @@ function parseConnectArgs(argv) {
6381
6558
  break;
6382
6559
  }
6383
6560
  default:
6384
- throw new import_errors10.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
6561
+ throw new import_errors11.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
6385
6562
  }
6386
6563
  }
6387
6564
  if (role === writeRole) {
6388
- throw new import_errors10.AbloValidationError("replication and write roles must be different", {
6565
+ throw new import_errors11.AbloValidationError("replication and write roles must be different", {
6389
6566
  code: "cli_invalid_arguments"
6390
6567
  });
6391
6568
  }
@@ -6409,12 +6586,15 @@ function parseConnectArgs(argv) {
6409
6586
  manual
6410
6587
  };
6411
6588
  }
6412
- function printConnectRecipe(args) {
6589
+ function printConnectRecipe(args, footprint) {
6590
+ const role = args.role === import_footprint.ABLO_REPLICATION_ROLE ? footprint.replicationRole : args.role;
6591
+ const writeRole = args.writeRole === import_footprint.ABLO_WRITE_ROLE ? footprint.writeRole : args.writeRole;
6413
6592
  const sql = connectSetupSql({
6414
6593
  tables: args.tables,
6415
- role: args.role,
6416
- writeRole: args.writeRole,
6417
- schema: args.schema
6594
+ role,
6595
+ writeRole,
6596
+ schema: args.schema,
6597
+ publication: footprint.publication
6418
6598
  });
6419
6599
  console.log(
6420
6600
  `
@@ -6465,7 +6645,7 @@ function printConnectRecipe(args) {
6465
6645
  console.log(
6466
6646
  import_picocolors12.default.dim(
6467
6647
  ` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
6468
- ${import_picocolors12.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
6648
+ ${import_picocolors12.default.bold(`GRANT rds_replication TO ${quoteIdent(role)};`)}`
6469
6649
  )
6470
6650
  );
6471
6651
  console.log(
@@ -6489,8 +6669,8 @@ function printConnectRecipe(args) {
6489
6669
  `
6490
6670
  ${import_picocolors12.default.bold("5.")} Register the two roles with Ablo. Set them just long enough to register \u2014
6491
6671
  Ablo holds them from here, so your app keeps only ${import_picocolors12.default.bold("ABLO_API_KEY")}:
6492
- ${import_picocolors12.default.bold("ABLO_REPLICATION_DATABASE_URL")} ${import_picocolors12.default.dim(`\u2192 ${args.role} (replication only)`)}
6493
- ${import_picocolors12.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors12.default.dim(`\u2192 ${args.writeRole} (DML only)`)}
6672
+ ${import_picocolors12.default.bold("ABLO_REPLICATION_DATABASE_URL")} ${import_picocolors12.default.dim(`\u2192 ${role} (replication only)`)}
6673
+ ${import_picocolors12.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors12.default.dim(`\u2192 ${writeRole} (DML only)`)}
6494
6674
  ${import_picocolors12.default.cyan("npx ablo connect register")}
6495
6675
  `
6496
6676
  );
@@ -6518,9 +6698,9 @@ function printCheckItem(item) {
6518
6698
  }
6519
6699
  }
6520
6700
  }
6521
- async function probeDirectWriteReadiness(sql, opts = {}) {
6701
+ async function probeDirectWriteReadiness(sql, opts) {
6522
6702
  const schema = opts.schema ?? "public";
6523
- const publication = opts.publication ?? import_footprint.ABLO_PUBLICATION;
6703
+ const publication = opts.publication;
6524
6704
  const items = [];
6525
6705
  const roleRows = await sql.unsafe(
6526
6706
  `SELECT rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole, rolreplication
@@ -6635,6 +6815,12 @@ async function probeDirectWriteReadiness(sql, opts = {}) {
6635
6815
  }
6636
6816
  async function auditTenantSyncInfra(sql, opts = {}) {
6637
6817
  const artifacts = [];
6818
+ const branchScopedTemplates = /* @__PURE__ */ new Set([
6819
+ import_footprint.ABLO_PUBLICATION,
6820
+ import_footprint3.ABLO_REPLICATION_SLOT,
6821
+ import_footprint.ABLO_REPLICATION_ROLE,
6822
+ import_footprint.ABLO_WRITE_ROLE
6823
+ ]);
6638
6824
  for (const artifact of import_footprint3.ABLO_FOOTPRINT) {
6639
6825
  const name = opts.names ? artifact.name === import_footprint.ABLO_PUBLICATION ? opts.names.publication : artifact.name === import_footprint3.ABLO_REPLICATION_SLOT ? opts.names.slot : artifact.name === import_footprint.ABLO_REPLICATION_ROLE ? opts.names.replicationRole : artifact.name === import_footprint.ABLO_WRITE_ROLE ? opts.names.writeRole : artifact.name : artifact.name;
6640
6826
  const key = artifact.kind === "table" || artifact.kind === "type" ? `${artifact.retired ? "public" : opts.schema ?? "public"}.${name}` : name;
@@ -6647,7 +6833,7 @@ async function auditTenantSyncInfra(sql, opts = {}) {
6647
6833
  present: rows[0]?.present === true,
6648
6834
  purpose: artifact.purpose,
6649
6835
  ...artifact.hazard ? { hazard: artifact.hazard } : {},
6650
- ...artifact.retired ? { retired: true } : {}
6836
+ ...artifact.retired || !opts.names && branchScopedTemplates.has(artifact.name) ? { retired: true } : {}
6651
6837
  });
6652
6838
  }
6653
6839
  return artifacts;
@@ -6672,7 +6858,7 @@ function requireScopedUrl(kind, verb) {
6672
6858
  );
6673
6859
  process.exit(1);
6674
6860
  }
6675
- async function probeAndReport(dbUrl, kind = "replication", opts = {}) {
6861
+ async function probeAndReport(dbUrl, kind, opts) {
6676
6862
  const sql = src_default(dbUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
6677
6863
  } });
6678
6864
  let items;
@@ -6683,7 +6869,7 @@ async function probeAndReport(dbUrl, kind = "replication", opts = {}) {
6683
6869
  const dial = dialFailureReason(err);
6684
6870
  if (dial) return { kind: "no-dial", reason: dial };
6685
6871
  const pg = err ?? {};
6686
- throw new import_errors10.AbloConnectionError(`Couldn't read the database: ${pg.message ?? String(err)}`, {
6872
+ throw new import_errors11.AbloConnectionError(`Couldn't read the database: ${pg.message ?? String(err)}`, {
6687
6873
  code: "cli_database_unreachable",
6688
6874
  cause: err
6689
6875
  });
@@ -6701,7 +6887,7 @@ async function runCheck() {
6701
6887
  const apiKey = resolveRuntimeApiKey().key;
6702
6888
  if (!apiKey) {
6703
6889
  const ambient = ambientEnvKeyNote();
6704
- throw new import_errors10.AbloAuthenticationError(
6890
+ throw new import_errors11.AbloAuthenticationError(
6705
6891
  `No branch-bound runtime key found. Set ABLO_API_KEY to the sk_ key for the branch to check, or pass \`--env-file .env.local\` explicitly.${ambient ? `
6706
6892
 
6707
6893
  ${ambient}` : ""}`,
@@ -6713,7 +6899,7 @@ ${ambient}` : ""}`,
6713
6899
  if (!result.ok) {
6714
6900
  if (result.code === "no_data_source_registered") {
6715
6901
  console.error(
6716
- ` ${import_picocolors12.default.yellow("\u2014")} No database is connected to this plane yet, so there's nothing to check.
6902
+ ` ${import_picocolors12.default.yellow("\u2014")} This branch is not connected to a database yet, so there's nothing to check.
6717
6903
  ` + import_picocolors12.default.dim(
6718
6904
  ` Connect one with ${import_picocolors12.default.bold("ablo connect apply")}, then re-run ${import_picocolors12.default.bold("ablo connect check")}.
6719
6905
  `
@@ -6738,8 +6924,9 @@ ${ambient}` : ""}`,
6738
6924
  );
6739
6925
  console.error(
6740
6926
  import_picocolors12.default.dim(
6741
- ` Direct needs a route Ablo's servers can dial \u2014 public allowlist, PrivateLink, peering,
6742
- or VPN. Only when no inbound route can exist, use the signed ${import_picocolors12.default.bold("dataSource()")} endpoint fallback.
6927
+ ` For localhost-first development, run ${import_picocolors12.default.bold("ablo dev --local")} and keep it beside the app.
6928
+ The direct WAL path needs a route Ablo's servers can dial \u2014 public allowlist,
6929
+ PrivateLink, peering, VPN, or a database-capable secure tunnel.
6743
6930
  `
6744
6931
  )
6745
6932
  );
@@ -6793,7 +6980,7 @@ async function runRegister(args) {
6793
6980
  const apiKey = resolveMutationApiKey();
6794
6981
  if (!apiKey) {
6795
6982
  const ambient = ambientEnvKeyNote();
6796
- throw new import_errors10.AbloAuthenticationError(
6983
+ throw new import_errors11.AbloAuthenticationError(
6797
6984
  `No branch-bound runtime key found. Set ABLO_API_KEY to the sk_ key for the branch to register, or pass \`--env-file .env.local\` explicitly.${ambient ? `
6798
6985
 
6799
6986
  ${ambient}` : ""}`,
@@ -6805,13 +6992,32 @@ ${ambient}` : ""}`,
6805
6992
  ${brand("ablo")} ${import_picocolors12.default.dim("connect register")} ${import_picocolors12.default.dim("register a direct DataSource")}
6806
6993
  `
6807
6994
  );
6995
+ const target = await resolveTarget({ url: apiBaseUrl(), apiKey, keySource: "env" });
6996
+ const confirmed = target.confirmed;
6997
+ if (!confirmed?.branchId) {
6998
+ throw new import_errors11.AbloValidationError(
6999
+ "This key is not bound to a branch, so Ablo cannot validate isolated database objects safely.",
7000
+ { code: "cli_database_unreachable" }
7001
+ );
7002
+ }
7003
+ const publication = (0, import_footprint3.footprintNamesFor)({
7004
+ organizationId: confirmed.organizationId,
7005
+ branchId: confirmed.branchId,
7006
+ ...confirmed.projectId ? { projectId: confirmed.projectId } : {}
7007
+ }).publication;
6808
7008
  console.log(` ${import_picocolors12.default.bold("Replication role")}
6809
7009
  `);
6810
- const replication = await probeAndReport(dbUrl, "replication", { schema: args.schema });
7010
+ const replication = await probeAndReport(dbUrl, "replication", {
7011
+ schema: args.schema,
7012
+ publication
7013
+ });
6811
7014
  console.log(`
6812
7015
  ${import_picocolors12.default.bold("Direct-write role")}
6813
7016
  `);
6814
- const write = await probeAndReport(writeDbUrl, "write", { schema: args.schema });
7017
+ const write = await probeAndReport(writeDbUrl, "write", {
7018
+ schema: args.schema,
7019
+ publication
7020
+ });
6815
7021
  const noDial = [
6816
7022
  replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
6817
7023
  write.kind === "no-dial" ? `write: ${write.reason}` : null
@@ -6871,7 +7077,7 @@ async function runScan(args) {
6871
7077
  } catch (err) {
6872
7078
  const pg = err ?? {};
6873
7079
  await sql.end({ timeout: 2 });
6874
- throw new import_errors10.AbloConnectionError(`Couldn't audit the database: ${pg.message ?? String(err)}`, {
7080
+ throw new import_errors11.AbloConnectionError(`Couldn't audit the database: ${pg.message ?? String(err)}`, {
6875
7081
  code: "cli_database_unreachable",
6876
7082
  cause: err
6877
7083
  });
@@ -6918,12 +7124,12 @@ async function runScan(args) {
6918
7124
  async function runLocate(args) {
6919
7125
  console.log(
6920
7126
  `
6921
- ${brand("ablo")} ${import_picocolors12.default.dim("connect locate")} ${import_picocolors12.default.dim("which plane holds this database")}
7127
+ ${brand("ablo")} ${import_picocolors12.default.dim("connect locate")} ${import_picocolors12.default.dim("which branch is connected to this database")}
6922
7128
  `
6923
7129
  );
6924
7130
  const url = args.url ?? readProjectAdminDatabaseUrl();
6925
7131
  if (!url) {
6926
- throw new import_errors10.AbloValidationError(
7132
+ throw new import_errors11.AbloValidationError(
6927
7133
  "Locating needs a connection string to identify the database. Pass --url <conn> (or set DATABASE_URL) and re-run.",
6928
7134
  { code: "cli_database_url_missing" }
6929
7135
  );
@@ -6931,7 +7137,7 @@ async function runLocate(args) {
6931
7137
  const apiKey = resolveRuntimeApiKey().key;
6932
7138
  if (!apiKey) {
6933
7139
  const ambient = ambientEnvKeyNote();
6934
- throw new import_errors10.AbloAuthenticationError(
7140
+ throw new import_errors11.AbloAuthenticationError(
6935
7141
  `No branch-bound runtime key found. Set ABLO_API_KEY to a key that can inspect this project, or pass \`--env-file .env.local\` explicitly.${ambient ? `
6936
7142
 
6937
7143
  ${ambient}` : ""}`,
@@ -6961,7 +7167,7 @@ ${ambient}` : ""}`,
6961
7167
  }
6962
7168
  if (!answer.held) {
6963
7169
  console.log(
6964
- ` ${import_picocolors12.default.green("\u2713")} No plane holds ${import_picocolors12.default.bold(`${label}/${args.schema}`)} \u2014 ${import_picocolors12.default.bold("ablo connect apply")} can register it here.
7170
+ ` ${import_picocolors12.default.green("\u2713")} No branch is connected to ${import_picocolors12.default.bold(`${label}/${args.schema}`)} \u2014 ${import_picocolors12.default.bold("ablo connect apply")} can register it here.
6965
7171
  `
6966
7172
  );
6967
7173
  return;
@@ -6972,9 +7178,9 @@ ${ambient}` : ""}`,
6972
7178
  );
6973
7179
  console.log(
6974
7180
  import_picocolors12.default.dim(
6975
- ` Ablo binds one plane to each database schema. To move this schema, disconnect it there
7181
+ ` Ablo connects one branch to each database schema. To move this schema, disconnect it there
6976
7182
  first \u2014 run `
6977
- ) + import_picocolors12.default.cyan("ablo connect deregister") + import_picocolors12.default.dim(` with a key for that plane, then connect here.
7183
+ ) + import_picocolors12.default.cyan("ablo connect deregister") + import_picocolors12.default.dim(` with a key for that branch, then connect here.
6978
7184
  `) + import_picocolors12.default.dim(` Confirm a candidate key first with `) + import_picocolors12.default.bold("ablo whoami --key-env <NAME>") + import_picocolors12.default.dim(`.
6979
7185
  `) + import_picocolors12.default.dim(` Match the project id to a name with `) + import_picocolors12.default.bold("ablo projects list --json") + import_picocolors12.default.dim(".") + "\n"
6980
7186
  );
@@ -6982,7 +7188,7 @@ ${ambient}` : ""}`,
6982
7188
  async function runResnapshot() {
6983
7189
  const apiKey = resolveMutationApiKey();
6984
7190
  if (!apiKey) {
6985
- throw new import_errors10.AbloAuthenticationError(
7191
+ throw new import_errors11.AbloAuthenticationError(
6986
7192
  "No branch-bound secret key found. Set ABLO_API_KEY to the sk_ key for the branch to resnapshot.",
6987
7193
  { code: "cli_api_key_missing" }
6988
7194
  );
@@ -7027,7 +7233,7 @@ async function connect(argv) {
7027
7233
  try {
7028
7234
  process.loadEnvFile(args.envFile);
7029
7235
  } catch (error) {
7030
- throw new import_errors10.AbloValidationError(
7236
+ throw new import_errors11.AbloValidationError(
7031
7237
  `could not load --env-file ${args.envFile}: ${error instanceof Error ? error.message : String(error)}`,
7032
7238
  { code: "cli_invalid_arguments" }
7033
7239
  );
@@ -7065,14 +7271,36 @@ async function connect(argv) {
7065
7271
  await runConnectApply2(args);
7066
7272
  return;
7067
7273
  }
7068
- printConnectRecipe(args);
7274
+ const apiKey = resolveRuntimeApiKey().key;
7275
+ if (!apiKey) {
7276
+ throw new import_errors11.AbloAuthenticationError(
7277
+ "Ablo needs the branch key to derive isolated database object names. Set ABLO_API_KEY for this branch, then re-run `ablo connect --manual`.",
7278
+ { code: "cli_api_key_missing" }
7279
+ );
7280
+ }
7281
+ const target = await resolveTarget({ url: apiBaseUrl(), apiKey, keySource: "env" });
7282
+ const confirmed = target.confirmed;
7283
+ if (!confirmed?.branchId) {
7284
+ throw new import_errors11.AbloValidationError(
7285
+ "This key is not bound to a branch, so Ablo cannot derive isolated database object names safely.",
7286
+ { code: "cli_database_unreachable" }
7287
+ );
7288
+ }
7289
+ printConnectRecipe(
7290
+ args,
7291
+ (0, import_footprint3.footprintNamesFor)({
7292
+ organizationId: confirmed.organizationId,
7293
+ branchId: confirmed.branchId,
7294
+ ...confirmed.projectId ? { projectId: confirmed.projectId } : {}
7295
+ })
7296
+ );
7069
7297
  }
7070
- var import_errors10, import_picocolors12, import_footprint3, import_wire7, FOOTPRINT_LOOKUP, CONNECT_USAGE;
7298
+ var import_errors11, import_picocolors12, import_footprint3, import_wire7, FOOTPRINT_LOOKUP, CONNECT_USAGE;
7071
7299
  var init_connect = __esm({
7072
7300
  "src/connect.ts"() {
7073
7301
  "use strict";
7074
7302
  init_cjs_shims();
7075
- import_errors10 = require("@abloatai/transaction/errors");
7303
+ import_errors11 = require("@abloatai/transaction/errors");
7076
7304
  import_picocolors12 = __toESM(require_picocolors(), 1);
7077
7305
  init_src();
7078
7306
  import_footprint3 = require("@abloatai/transaction/footprint");
@@ -7105,7 +7333,7 @@ var init_connect = __esm({
7105
7333
  npx ablo connect rotate New passwords for both logins, then re-register
7106
7334
  npx ablo connect resnapshot Recreate only the slot and reload existing rows
7107
7335
  npx ablo connect scan List anything Ablo ever set up in your database (read-only, never drops)
7108
- npx ablo connect locate See which plane holds this database (read-only; nothing is changed)
7336
+ npx ablo connect locate See which branch is connected to this database (read-only; nothing is changed)
7109
7337
 
7110
7338
  Running it: bare \`ablo connect\` sets everything up for you \u2014 creating the two
7111
7339
  scoped logins, sharing your tables, and registering \u2014 whenever it finds a
@@ -12800,11 +13028,11 @@ var require_typescript = __commonJS({
12800
13028
  function compareTextSpans(a, b4) {
12801
13029
  return compareValues(a == null ? void 0 : a.start, b4 == null ? void 0 : b4.start) || compareValues(a == null ? void 0 : a.length, b4 == null ? void 0 : b4.length);
12802
13030
  }
12803
- function maxBy(arr, init2, mapper) {
13031
+ function maxBy(arr, init4, mapper) {
12804
13032
  for (let i = 0; i < arr.length; i++) {
12805
- init2 = Math.max(init2, mapper(arr[i]));
13033
+ init4 = Math.max(init4, mapper(arr[i]));
12806
13034
  }
12807
- return init2;
13035
+ return init4;
12808
13036
  }
12809
13037
  function min(items, compare) {
12810
13038
  return reduceLeft(items, (x2, y3) => compare(x2, y3) === -1 ? x2 : y3);
@@ -17303,8 +17531,8 @@ ${lanes.join("\n")}
17303
17531
  function sysLog(s) {
17304
17532
  return curSysLog(s);
17305
17533
  }
17306
- function setSysLog(logger) {
17307
- curSysLog = logger;
17534
+ function setSysLog(logger2) {
17535
+ curSysLog = logger2;
17308
17536
  }
17309
17537
  function createDirectoryWatcherSupportingRecursive({
17310
17538
  watchDirectory,
@@ -28403,8 +28631,8 @@ ${lanes.join("\n")}
28403
28631
  return node.initializer;
28404
28632
  }
28405
28633
  function getDeclaredExpandoInitializer(node) {
28406
- const init2 = getEffectiveInitializer(node);
28407
- return init2 && getExpandoInitializer(init2, isPrototypeAccess(node.name));
28634
+ const init4 = getEffectiveInitializer(node);
28635
+ return init4 && getExpandoInitializer(init4, isPrototypeAccess(node.name));
28408
28636
  }
28409
28637
  function hasExpandoValueProperty(node, isPrototypeAssignment) {
28410
28638
  return forEach(node.properties, (p2) => isPropertyAssignment(p2) && isIdentifier2(p2.name) && p2.name.escapedText === "value" && p2.initializer && getExpandoInitializer(p2.initializer, isPrototypeAssignment));
@@ -61991,11 +62219,11 @@ ${lanes.join("\n")}
61991
62219
  if (node && isCallExpression(node)) {
61992
62220
  return !!getAssignedExpandoInitializer(node);
61993
62221
  }
61994
- let init2 = !node ? void 0 : isVariableDeclaration(node) ? node.initializer : isBinaryExpression(node) ? node.right : isPropertyAccessExpression(node) && isBinaryExpression(node.parent) ? node.parent.right : void 0;
61995
- init2 = init2 && getRightMostAssignedExpression(init2);
61996
- if (init2) {
62222
+ let init4 = !node ? void 0 : isVariableDeclaration(node) ? node.initializer : isBinaryExpression(node) ? node.right : isPropertyAccessExpression(node) && isBinaryExpression(node.parent) ? node.parent.right : void 0;
62223
+ init4 = init4 && getRightMostAssignedExpression(init4);
62224
+ if (init4) {
61997
62225
  const isPrototypeAssignment = isPrototypeAccess(isVariableDeclaration(node) ? node.name : isBinaryExpression(node) ? node.left : node);
61998
- return !!getExpandoInitializer(isBinaryExpression(init2) && (init2.operatorToken.kind === 57 || init2.operatorToken.kind === 61) ? init2.right : init2, isPrototypeAssignment);
62226
+ return !!getExpandoInitializer(isBinaryExpression(init4) && (init4.operatorToken.kind === 57 || init4.operatorToken.kind === 61) ? init4.right : init4, isPrototypeAssignment);
61999
62227
  }
62000
62228
  return false;
62001
62229
  }
@@ -62314,15 +62542,15 @@ ${lanes.join("\n")}
62314
62542
  } else if (isIdentifier2(node)) {
62315
62543
  const symbol = lookupSymbolForName(sourceFile, node.escapedText);
62316
62544
  if (!!symbol && !!symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && !!symbol.valueDeclaration.initializer) {
62317
- const init2 = symbol.valueDeclaration.initializer;
62318
- q2.enqueue(init2);
62545
+ const init4 = symbol.valueDeclaration.initializer;
62546
+ q2.enqueue(init4);
62319
62547
  if (isAssignmentExpression(
62320
- init2,
62548
+ init4,
62321
62549
  /*excludeCompoundAssignment*/
62322
62550
  true
62323
62551
  )) {
62324
- q2.enqueue(init2.left);
62325
- q2.enqueue(init2.right);
62552
+ q2.enqueue(init4.left);
62553
+ q2.enqueue(init4.right);
62326
62554
  }
62327
62555
  }
62328
62556
  }
@@ -66910,9 +67138,9 @@ ${lanes.join("\n")}
66910
67138
  )) {
66911
67139
  return void 0;
66912
67140
  }
66913
- const init2 = isVariableDeclaration(decl) ? getDeclaredExpandoInitializer(decl) : getAssignedExpandoInitializer(decl);
66914
- if (init2) {
66915
- const initSymbol = getSymbolOfNode(init2);
67141
+ const init4 = isVariableDeclaration(decl) ? getDeclaredExpandoInitializer(decl) : getAssignedExpandoInitializer(decl);
67142
+ if (init4) {
67143
+ const initSymbol = getSymbolOfNode(init4);
66916
67144
  if (initSymbol) {
66917
67145
  return mergeJSSymbols(initSymbol, symbol);
66918
67146
  }
@@ -73692,9 +73920,9 @@ ${lanes.join("\n")}
73692
73920
  }
73693
73921
  return widened;
73694
73922
  }
73695
- function getJSContainerObjectType(decl, symbol, init2) {
73923
+ function getJSContainerObjectType(decl, symbol, init4) {
73696
73924
  var _a, _b;
73697
- if (!isInJSFile(decl) || !init2 || !isObjectLiteralExpression(init2) || init2.properties.length) {
73925
+ if (!isInJSFile(decl) || !init4 || !isObjectLiteralExpression(init4) || init4.properties.length) {
73698
73926
  return void 0;
73699
73927
  }
73700
73928
  const exports22 = createSymbolTable();
@@ -88668,8 +88896,8 @@ ${lanes.join("\n")}
88668
88896
  return unreachableNeverType;
88669
88897
  }
88670
88898
  if (isVariableDeclaration(node) && (isInJSFile(node) || isVarConstLike2(node))) {
88671
- const init2 = getDeclaredExpandoInitializer(node);
88672
- if (init2 && (init2.kind === 218 || init2.kind === 219)) {
88899
+ const init4 = getDeclaredExpandoInitializer(node);
88900
+ if (init4 && (init4.kind === 218 || init4.kind === 219)) {
88673
88901
  return getTypeAtFlowNode(flow.antecedent);
88674
88902
  }
88675
88903
  }
@@ -96290,8 +96518,8 @@ ${lanes.join("\n")}
96290
96518
  true
96291
96519
  );
96292
96520
  const prototype = (_a = assignmentSymbol == null ? void 0 : assignmentSymbol.exports) == null ? void 0 : _a.get("prototype");
96293
- const init2 = (prototype == null ? void 0 : prototype.valueDeclaration) && getAssignedJSPrototype(prototype.valueDeclaration);
96294
- return init2 ? getSymbolOfDeclaration(init2) : void 0;
96521
+ const init4 = (prototype == null ? void 0 : prototype.valueDeclaration) && getAssignedJSPrototype(prototype.valueDeclaration);
96522
+ return init4 ? getSymbolOfDeclaration(init4) : void 0;
96295
96523
  }
96296
96524
  function getSymbolOfExpando(node, allowDeclaration) {
96297
96525
  if (!node.parent) {
@@ -99307,8 +99535,8 @@ ${lanes.join("\n")}
99307
99535
  case 3:
99308
99536
  case 4:
99309
99537
  const symbol = getSymbolOfNode(left);
99310
- const init2 = getAssignedExpandoInitializer(right);
99311
- return !!init2 && isObjectLiteralExpression(init2) && !!((_a = symbol == null ? void 0 : symbol.exports) == null ? void 0 : _a.size);
99538
+ const init4 = getAssignedExpandoInitializer(right);
99539
+ return !!init4 && isObjectLiteralExpression(init4) && !!((_a = symbol == null ? void 0 : symbol.exports) == null ? void 0 : _a.size);
99312
99540
  default:
99313
99541
  return false;
99314
99542
  }
@@ -163307,13 +163535,13 @@ ${lanes.join("\n")}
163307
163535
  }
163308
163536
  } else {
163309
163537
  if (isVariableStatement(node) && node.parent === sourceFile && node.declarationList.flags & 2 && node.declarationList.declarations.length === 1) {
163310
- const init2 = node.declarationList.declarations[0].initializer;
163311
- if (init2 && isRequireCall(
163312
- init2,
163538
+ const init4 = node.declarationList.declarations[0].initializer;
163539
+ if (init4 && isRequireCall(
163540
+ init4,
163313
163541
  /*requireStringLiteralLikeArgument*/
163314
163542
  true
163315
163543
  )) {
163316
- diags.push(createDiagnosticForNode(init2, Diagnostics.require_call_may_be_converted_to_an_import));
163544
+ diags.push(createDiagnosticForNode(init4, Diagnostics.require_call_may_be_converted_to_an_import));
163317
163545
  }
163318
163546
  }
163319
163547
  const jsdocTypedefNodes = ts_codefix_exports.getJSDocTypedefNodes(node);
@@ -176204,7 +176432,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
176204
176432
  factory.createIdentifier(name)
176205
176433
  );
176206
176434
  }
176207
- function makeConst(modifiers, name, init2) {
176435
+ function makeConst(modifiers, name, init4) {
176208
176436
  return factory.createVariableStatement(
176209
176437
  modifiers,
176210
176438
  factory.createVariableDeclarationList(
@@ -176214,7 +176442,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
176214
176442
  void 0,
176215
176443
  /*type*/
176216
176444
  void 0,
176217
- init2
176445
+ init4
176218
176446
  )],
176219
176447
  2
176220
176448
  /* Const */
@@ -195188,9 +195416,9 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
195188
195416
  return isFunctionLike(be.right) ? { commentOwner, parameters: be.right.parameters, hasReturn: hasReturn(be.right, options) } : { commentOwner };
195189
195417
  }
195190
195418
  case 172:
195191
- const init2 = commentOwner.initializer;
195192
- if (init2 && (isFunctionExpression(init2) || isArrowFunction(init2))) {
195193
- return { commentOwner, parameters: init2.parameters, hasReturn: hasReturn(init2, options) };
195419
+ const init4 = commentOwner.initializer;
195420
+ if (init4 && (isFunctionExpression(init4) || isArrowFunction(init4))) {
195421
+ return { commentOwner, parameters: init4.parameters, hasReturn: hasReturn(init4, options) };
195194
195422
  }
195195
195423
  }
195196
195424
  }
@@ -206683,13 +206911,13 @@ ${options.prefix}` : "\n" : options.prefix
206683
206911
  return [];
206684
206912
  }
206685
206913
  var ThrottledOperations = class _ThrottledOperations {
206686
- constructor(host, logger) {
206914
+ constructor(host, logger2) {
206687
206915
  this.host = host;
206688
206916
  this.pendingTimeouts = /* @__PURE__ */ new Map();
206689
- this.logger = logger.hasLevel(
206917
+ this.logger = logger2.hasLevel(
206690
206918
  3
206691
206919
  /* verbose */
206692
- ) ? logger : void 0;
206920
+ ) ? logger2 : void 0;
206693
206921
  }
206694
206922
  /**
206695
206923
  * Wait `number` milliseconds and then invoke `cb`. If, while waiting, schedule
@@ -206722,10 +206950,10 @@ ${options.prefix}` : "\n" : options.prefix
206722
206950
  }
206723
206951
  };
206724
206952
  var GcTimer = class _GcTimer {
206725
- constructor(host, delay, logger) {
206953
+ constructor(host, delay, logger2) {
206726
206954
  this.host = host;
206727
206955
  this.delay = delay;
206728
- this.logger = logger;
206956
+ this.logger = logger2;
206729
206957
  }
206730
206958
  scheduleCollect() {
206731
206959
  if (!this.host.gc || this.timerId !== void 0) {
@@ -214086,14 +214314,14 @@ Dynamic files must always be opened with service's current directory or service
214086
214314
  return edits.every((edit) => textSpanEnd(edit.span) < pos);
214087
214315
  }
214088
214316
  var CommandNames = CommandTypes;
214089
- function formatMessage2(msg, logger, byteLength, newLine) {
214090
- const verboseLogging = logger.hasLevel(
214317
+ function formatMessage2(msg, logger2, byteLength, newLine) {
214318
+ const verboseLogging = logger2.hasLevel(
214091
214319
  3
214092
214320
  /* verbose */
214093
214321
  );
214094
214322
  const json = JSON.stringify(msg);
214095
214323
  if (verboseLogging) {
214096
- logger.info(`${msg.type}:${stringifyIndented(msg)}`);
214324
+ logger2.info(`${msg.type}:${stringifyIndented(msg)}`);
214097
214325
  }
214098
214326
  const len = byteLength(json, "utf8");
214099
214327
  return `Content-Length: ${1 + len}\r
@@ -214248,7 +214476,7 @@ ${json}${newLine}`;
214248
214476
  const info = infos && firstOrUndefined(infos);
214249
214477
  return info && !info.isLocal ? { fileName: info.fileName, pos: info.textSpan.start } : void 0;
214250
214478
  }
214251
- function getReferencesWorker(projects2, defaultProject, initialLocation, useCaseSensitiveFileNames2, logger) {
214479
+ function getReferencesWorker(projects2, defaultProject, initialLocation, useCaseSensitiveFileNames2, logger2) {
214252
214480
  var _a, _b;
214253
214481
  const perProjectResults = getPerProjectReferences(
214254
214482
  projects2,
@@ -214262,7 +214490,7 @@ ${json}${newLine}`;
214262
214490
  ),
214263
214491
  mapDefinitionInProject,
214264
214492
  (project, position) => {
214265
- logger.info(`Finding references to ${position.fileName} position ${position.pos} in project ${project.getProjectName()}`);
214493
+ logger2.info(`Finding references to ${position.fileName} position ${position.pos} in project ${project.getProjectName()}`);
214266
214494
  return project.getLanguageService().findReferences(position.fileName, position.pos);
214267
214495
  },
214268
214496
  (referencedSymbol, cb) => {
@@ -218631,9 +218859,9 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
218631
218859
  }
218632
218860
  };
218633
218861
  var _TypingsInstallerAdapter = class _TypingsInstallerAdapter2 {
218634
- constructor(telemetryEnabled, logger, host, globalTypingsCacheLocation, event, maxActiveRequestCount) {
218862
+ constructor(telemetryEnabled, logger2, host, globalTypingsCacheLocation, event, maxActiveRequestCount) {
218635
218863
  this.telemetryEnabled = telemetryEnabled;
218636
- this.logger = logger;
218864
+ this.logger = logger2;
218637
218865
  this.host = host;
218638
218866
  this.globalTypingsCacheLocation = globalTypingsCacheLocation;
218639
218867
  this.event = event;
@@ -225611,13 +225839,13 @@ var require_reusify = __commonJS({
225611
225839
  current.next = null;
225612
225840
  return current;
225613
225841
  }
225614
- function release(obj) {
225842
+ function release2(obj) {
225615
225843
  tail.next = obj;
225616
225844
  tail = obj;
225617
225845
  }
225618
225846
  return {
225619
225847
  get,
225620
- release
225848
+ release: release2
225621
225849
  };
225622
225850
  }
225623
225851
  module2.exports = reusify;
@@ -225661,7 +225889,7 @@ var require_queue = __commonJS({
225661
225889
  if (self.paused) return;
225662
225890
  for (; queueHead && _running < _concurrency; ) {
225663
225891
  _running++;
225664
- release();
225892
+ release2();
225665
225893
  }
225666
225894
  },
225667
225895
  running,
@@ -225706,12 +225934,12 @@ var require_queue = __commonJS({
225706
225934
  self.paused = false;
225707
225935
  if (queueHead === null) {
225708
225936
  _running++;
225709
- release();
225937
+ release2();
225710
225938
  return;
225711
225939
  }
225712
225940
  for (; queueHead && _running < _concurrency; ) {
225713
225941
  _running++;
225714
- release();
225942
+ release2();
225715
225943
  }
225716
225944
  }
225717
225945
  function idle() {
@@ -225720,7 +225948,7 @@ var require_queue = __commonJS({
225720
225948
  function push2(value, done) {
225721
225949
  var current = cache.get();
225722
225950
  current.context = context;
225723
- current.release = release;
225951
+ current.release = release2;
225724
225952
  current.value = value;
225725
225953
  current.callback = done || noop3;
225726
225954
  current.errorHandler = errorHandler;
@@ -225741,7 +225969,7 @@ var require_queue = __commonJS({
225741
225969
  function unshift(value, done) {
225742
225970
  var current = cache.get();
225743
225971
  current.context = context;
225744
- current.release = release;
225972
+ current.release = release2;
225745
225973
  current.value = value;
225746
225974
  current.callback = done || noop3;
225747
225975
  current.errorHandler = errorHandler;
@@ -225759,7 +225987,7 @@ var require_queue = __commonJS({
225759
225987
  worker.call(context, current.value, current.worked);
225760
225988
  }
225761
225989
  }
225762
- function release(holder) {
225990
+ function release2(holder) {
225763
225991
  if (holder) {
225764
225992
  cache.release(holder);
225765
225993
  }
@@ -283116,7 +283344,7 @@ var import_child_process3 = require("child_process");
283116
283344
 
283117
283345
  // src/migrate.ts
283118
283346
  init_cjs_shims();
283119
- var import_errors7 = require("@abloatai/transaction/errors");
283347
+ var import_errors8 = require("@abloatai/transaction/errors");
283120
283348
  init_dist2();
283121
283349
  var import_picocolors6 = __toESM(require_picocolors(), 1);
283122
283350
  var import_fs5 = require("fs");
@@ -283163,7 +283391,7 @@ function parseMigrateArgs(argv) {
283163
283391
  targetSchema = argv[++i] ?? targetSchema;
283164
283392
  break;
283165
283393
  default:
283166
- throw new import_errors7.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
283394
+ throw new import_errors8.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
283167
283395
  }
283168
283396
  }
283169
283397
  return { schemaPath, exportName, targetSchema, dryRun, outputFile };
@@ -283279,7 +283507,7 @@ async function migrate(argv) {
283279
283507
  }
283280
283508
  const dbUrl = readProjectAdminDatabaseUrl();
283281
283509
  if (!dbUrl) {
283282
- throw new import_errors7.AbloValidationError(
283510
+ throw new import_errors8.AbloValidationError(
283283
283511
  `No ${ADMIN_URL_VAR} found (checked process env, .env.local, .env). Set it to apply, or use --dry-run to preview.`,
283284
283512
  { code: "cli_database_url_missing" }
283285
283513
  );
@@ -283494,14 +283722,14 @@ function requireKey2(explicit) {
283494
283722
  }
283495
283723
  return key;
283496
283724
  }
283497
- async function request2(path, init2 = {}, context = {}) {
283725
+ async function request2(path, init4 = {}, context = {}) {
283498
283726
  const response = await fetch(`${apiUrl2(context.baseUrl)}${path}`, {
283499
- method: init2.method ?? "GET",
283727
+ method: init4.method ?? "GET",
283500
283728
  headers: {
283501
283729
  authorization: `Bearer ${requireKey2(context.apiKey)}`,
283502
283730
  "content-type": "application/json"
283503
283731
  },
283504
- ...init2.body !== void 0 ? { body: JSON.stringify(init2.body) } : {}
283732
+ ...init4.body !== void 0 ? { body: JSON.stringify(init4.body) } : {}
283505
283733
  });
283506
283734
  let body;
283507
283735
  try {
@@ -283584,7 +283812,7 @@ async function readStatus(ref, context = {}) {
283584
283812
  return import_branches.branchStatusResponseSchema.parse(response.body);
283585
283813
  }
283586
283814
  function printStatus(status2) {
283587
- const { branch, schema, data_source: source } = status2;
283815
+ const { branch, schema, storage, data_source: source } = status2;
283588
283816
  console.log(` ${import_picocolors14.default.bold(branch.slug)} ${import_picocolors14.default.dim(branch.id)}`);
283589
283817
  console.log(
283590
283818
  ` ${import_picocolors14.default.dim("state")} ${branch.state === "ready" ? import_picocolors14.default.green(branch.state) : import_picocolors14.default.yellow(branch.state)}`
@@ -283600,7 +283828,7 @@ function printStatus(status2) {
283600
283828
  ` ${import_picocolors14.default.dim("parent")} ${import_picocolors14.default.bold(schema.parent_compatibility)}${counts}`
283601
283829
  );
283602
283830
  }
283603
- const sourceLabel = source.kind === "hosted" ? "hosted branch plane" : `${source.kind} \xB7 ${source.host ?? "unknown host"}${source.database ? `/${source.database}` : ""} \xB7 ${source.status ?? "unknown"}`;
283831
+ const sourceLabel = source ? `${source.connection} \xB7 ${source.host ?? "unknown host"}${source.database ? `/${source.database}` : ""} \xB7 ${source.status}` : storage.kind === "unbound" ? "not connected to a database" : storage.kind === "internal" ? `Ablo internal product storage \xB7 ${storage.implementation}` : storage.kind === "blocked" ? "storage configuration needs attention" : "customer database connection unavailable";
283604
283832
  console.log(` ${import_picocolors14.default.dim("data")} ${sourceLabel}`);
283605
283833
  if (status2.ready) {
283606
283834
  console.log(`
@@ -283727,15 +283955,15 @@ async function branches(argv) {
283727
283955
 
283728
283956
  // src/branchDev.ts
283729
283957
  init_cjs_shims();
283730
- var import_node_crypto = require("crypto");
283958
+ var import_node_crypto2 = require("crypto");
283731
283959
  var import_node_child_process = require("child_process");
283732
283960
  var import_branches2 = require("@abloatai/transaction/branches");
283733
- var import_errors12 = require("@abloatai/transaction/errors");
283961
+ var import_errors13 = require("@abloatai/transaction/errors");
283734
283962
  init_config();
283735
283963
 
283736
283964
  // src/dev.ts
283737
283965
  init_cjs_shims();
283738
- var import_errors11 = require("@abloatai/transaction/errors");
283966
+ var import_errors12 = require("@abloatai/transaction/errors");
283739
283967
  var import_credentialPolicy2 = require("@abloatai/transaction/auth/credentialPolicy");
283740
283968
  var import_picocolors15 = __toESM(require_picocolors(), 1);
283741
283969
  init_dist2();
@@ -283747,11 +283975,15 @@ init_controlPlane();
283747
283975
  init_config();
283748
283976
  init_readiness();
283749
283977
  init_theme();
283978
+ init_dbRole();
283979
+ var import_source3 = require("@abloatai/transaction/source");
283750
283980
  function parseDevArgs(argv) {
283751
283981
  let schemaPath = DEFAULT_SCHEMA_PATH;
283752
283982
  let exportName = DEFAULT_EXPORT;
283753
283983
  let url = process.env.ABLO_API_URL ?? DEFAULT_URL;
283754
283984
  let watchEnabled = false;
283985
+ let local = false;
283986
+ let sourcePath = "ablo/data-source.ts";
283755
283987
  for (let i = 0; i < argv.length; i++) {
283756
283988
  const arg = argv[i];
283757
283989
  switch (arg) {
@@ -283770,8 +284002,14 @@ function parseDevArgs(argv) {
283770
284002
  case "--no-watch":
283771
284003
  watchEnabled = false;
283772
284004
  break;
284005
+ case "--local":
284006
+ local = true;
284007
+ break;
284008
+ case "--source":
284009
+ sourcePath = argv[++i] ?? sourcePath;
284010
+ break;
283773
284011
  default:
283774
- throw new import_errors11.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
284012
+ throw new import_errors12.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
283775
284013
  }
283776
284014
  }
283777
284015
  url = url.replace(/\/+$/, "");
@@ -283781,9 +284019,55 @@ function parseDevArgs(argv) {
283781
284019
  url,
283782
284020
  apiKey: process.env.ABLO_API_KEY,
283783
284021
  watch: watchEnabled,
284022
+ local,
284023
+ sourcePath,
283784
284024
  planeLabel: "branch"
283785
284025
  };
283786
284026
  }
284027
+ async function loadLocalSourceHandler(sourcePath) {
284028
+ const abs = (0, import_path5.resolve)(process.cwd(), sourcePath);
284029
+ if (!(0, import_fs7.existsSync)(abs)) {
284030
+ throw new import_errors12.AbloValidationError(
284031
+ `local Data Source not found at ${import_picocolors15.default.bold(sourcePath)}. Add the signed Data Source handler described by ${import_picocolors15.default.bold("npx ablo docs data-sources")}, or pass ${import_picocolors15.default.bold("--source <path>")}.`,
284032
+ { code: "cli_invalid_arguments" }
284033
+ );
284034
+ }
284035
+ const { createJiti } = await import("jiti");
284036
+ const jiti = createJiti(process.cwd());
284037
+ const mod = await jiti.import(abs);
284038
+ const nested = mod.default && typeof mod.default === "object" ? mod.default : void 0;
284039
+ const handler = mod.POST ?? nested?.POST;
284040
+ if (typeof handler !== "function") {
284041
+ throw new import_errors12.AbloValidationError(
284042
+ `${import_picocolors15.default.bold(sourcePath)} must export a ${import_picocolors15.default.bold("POST(request)")} Data Source handler.`,
284043
+ { code: "cli_invalid_arguments" }
284044
+ );
284045
+ }
284046
+ return handler;
284047
+ }
284048
+ async function registerLocalSource(args) {
284049
+ const response = await fetch(`${args.url}/v1/datasources`, {
284050
+ method: "POST",
284051
+ headers: {
284052
+ authorization: `Bearer ${args.apiKey}`,
284053
+ "content-type": "application/json"
284054
+ },
284055
+ body: JSON.stringify({
284056
+ connection: "endpoint",
284057
+ endpoint: "http://localhost/ablo-dev/reverse-channel",
284058
+ signingKey: args.apiKey,
284059
+ reverseChannel: true,
284060
+ metadata: { managed_by: "ablo dev --local" }
284061
+ })
284062
+ });
284063
+ if (!response.ok) {
284064
+ const body = await response.text();
284065
+ throw new import_errors12.AbloValidationError(
284066
+ `Could not register the local Data Source (${response.status}): ${body}`,
284067
+ { code: "cli_invalid_arguments" }
284068
+ );
284069
+ }
284070
+ }
283787
284071
  function classifyKey(apiKey) {
283788
284072
  if (!apiKey) {
283789
284073
  return {
@@ -283921,7 +284205,7 @@ async function runPush(schema, args) {
283921
284205
  }
283922
284206
  if (status2 === 403) {
283923
284207
  const serverSays = body.message ?? body.reason;
283924
- const hint = schemaPushPlaneHint(body.code) ?? (body.code === "database_role_cannot_enforce_rls" ? `Run ${import_picocolors15.default.bold("npx ablo migrate")} \u2014 it creates the scoped role for you (your DB credential never leaves this machine).` : `Schema authoring needs a branch-bound ${import_picocolors15.default.bold("sk_")} key with ${import_picocolors15.default.bold("schema:push")} \u2014 manage keys at ${import_picocolors15.default.cyan("https://abloatai.com")}.`);
284208
+ const hint = schemaPushStorageHint(body.code) ?? (body.code === "database_role_cannot_enforce_rls" ? `Run ${import_picocolors15.default.bold("npx ablo migrate")} \u2014 it creates the scoped role for you (your DB credential never leaves this machine).` : `Schema authoring needs a branch-bound ${import_picocolors15.default.bold("sk_")} key with ${import_picocolors15.default.bold("schema:push")} \u2014 manage keys at ${import_picocolors15.default.cyan("https://abloatai.com")}.`);
283925
284209
  return {
283926
284210
  ok: false,
283927
284211
  message: `${serverSays ?? "This key can't author schema (missing schema:push scope)."}
@@ -283929,6 +284213,11 @@ async function runPush(schema, args) {
283929
284213
  };
283930
284214
  }
283931
284215
  const serverMessage = String(body.message ?? body.reason ?? bodyText);
284216
+ const storageHint = schemaPushStorageHint(body.code);
284217
+ if (storageHint) {
284218
+ return { ok: false, message: `${serverMessage}
284219
+ ${import_picocolors15.default.dim(storageHint)}` };
284220
+ }
283932
284221
  if (looksLikeCredentialRefusal(serverMessage)) {
283933
284222
  const pooled = await poolerExplanation(apiBaseUrl(), args.apiKey);
283934
284223
  if (pooled) {
@@ -283949,6 +284238,12 @@ async function dev(argv, runtime = {}) {
283949
284238
  if (runtime.apiKey) args.apiKey = runtime.apiKey;
283950
284239
  else if (!args.apiKey) args.apiKey = resolveRuntimeApiKey("sandbox").key;
283951
284240
  if (runtime.branch) args.planeLabel = runtime.branch.slug;
284241
+ if (args.local && !args.watch) {
284242
+ throw new import_errors12.AbloValidationError(
284243
+ `${import_picocolors15.default.bold("--local")} opens a long-lived secure connector and cannot be combined with ${import_picocolors15.default.bold("--no-watch")}.`,
284244
+ { code: "cli_invalid_arguments" }
284245
+ );
284246
+ }
283952
284247
  const key = classifyKey(args.apiKey);
283953
284248
  if (!key.ok) {
283954
284249
  console.error(import_picocolors15.default.red(` ${key.reason}`));
@@ -283965,6 +284260,35 @@ async function dev(argv, runtime = {}) {
283965
284260
  ` ${import_picocolors15.default.dim("key")} temporary \xB7 expires ${runtime.branch.expiresAt}`
283966
284261
  );
283967
284262
  }
284263
+ let localAbort = null;
284264
+ if (args.local) {
284265
+ process.env.ABLO_API_KEY = args.apiKey;
284266
+ if (!process.env.DATABASE_URL) {
284267
+ const databaseUrl = readProjectEnvVariable("DATABASE_URL", process.cwd(), false);
284268
+ if (databaseUrl) process.env.DATABASE_URL = databaseUrl.value;
284269
+ }
284270
+ const handler = await loadLocalSourceHandler(args.sourcePath);
284271
+ await registerLocalSource(args);
284272
+ localAbort = new AbortController();
284273
+ const connector = (0, import_source3.createSourceConnector)({
284274
+ apiKey: args.apiKey,
284275
+ handler,
284276
+ baseURL: args.url,
284277
+ client: "ablo-dev",
284278
+ onStatus(status2) {
284279
+ if (status2 === "ready") {
284280
+ console.log(` ${import_picocolors15.default.green("\u2713")} local Postgres connected through the secure reverse channel`);
284281
+ }
284282
+ },
284283
+ onError(error) {
284284
+ console.error(import_picocolors15.default.yellow(` local connector: ${error instanceof Error ? error.message : String(error)}`));
284285
+ }
284286
+ });
284287
+ void connector.run(localAbort.signal).catch((error) => {
284288
+ console.error(import_picocolors15.default.red(` local connector stopped: ${error instanceof Error ? error.message : String(error)}`));
284289
+ });
284290
+ console.log(` ${import_picocolors15.default.dim("source")} ${args.sourcePath} ${import_picocolors15.default.dim("(outbound connector; no public URL)")}`);
284291
+ }
283968
284292
  const schema = await loadSchema(args.schemaPath, args.exportName);
283969
284293
  const modelCount = Object.keys(schema.models).length;
283970
284294
  console.log(
@@ -283979,7 +284303,10 @@ async function dev(argv, runtime = {}) {
283979
284303
  s.start("Pushing schema definition (development branch)");
283980
284304
  const first = await runPush(schema, args);
283981
284305
  s.stop(first.message, first.ok ? 0 : 1);
283982
- if (!first.ok) process.exit(1);
284306
+ if (!first.ok) {
284307
+ localAbort?.abort();
284308
+ process.exit(1);
284309
+ }
283983
284310
  if (runtime.branch) {
283984
284311
  console.log(
283985
284312
  `
@@ -284033,6 +284360,7 @@ async function dev(argv, runtime = {}) {
284033
284360
  }
284034
284361
  const stop = () => {
284035
284362
  watcher.close();
284363
+ localAbort?.abort();
284036
284364
  console.log(`
284037
284365
  ${import_picocolors15.default.dim("stopped.")}`);
284038
284366
  process.exit(0);
@@ -284048,6 +284376,7 @@ init_controlPlane();
284048
284376
  var BRANCH_DEV_USAGE = `Usage:
284049
284377
  ablo dev [--branch <slug>] [--branch-ttl-hours <1-168>]
284050
284378
  [--schema <path>] [--export <name>] [--url <url>]
284379
+ [--local] [--source <path>]
284051
284380
  ablo dev --no-watch [branch options]
284052
284381
 
284053
284382
  By default, dev discovers the Git/CI branch, ensures its isolated Ablo branch,
@@ -284061,7 +284390,7 @@ function parseBranchDevArgs(argv) {
284061
284390
  const arg = argv[index];
284062
284391
  if (!arg) continue;
284063
284392
  if (arg === "--no-branch") {
284064
- throw new import_errors12.AbloValidationError(
284393
+ throw new import_errors13.AbloValidationError(
284065
284394
  "--no-branch was removed: development is branch-isolated. Use --branch <slug> to select explicitly.",
284066
284395
  { code: "cli_invalid_arguments" }
284067
284396
  );
@@ -284069,7 +284398,7 @@ function parseBranchDevArgs(argv) {
284069
284398
  if (arg === "--branch") {
284070
284399
  branchSlug = argv[++index];
284071
284400
  if (!branchSlug) {
284072
- throw new import_errors12.AbloValidationError("--branch requires a slug", {
284401
+ throw new import_errors13.AbloValidationError("--branch requires a slug", {
284073
284402
  code: "cli_invalid_arguments"
284074
284403
  });
284075
284404
  }
@@ -284078,7 +284407,7 @@ function parseBranchDevArgs(argv) {
284078
284407
  if (arg === "--branch-ttl-hours") {
284079
284408
  const value = Number(argv[++index]);
284080
284409
  if (!Number.isInteger(value) || value < 1 || value > 168) {
284081
- throw new import_errors12.AbloValidationError("--branch-ttl-hours must be between 1 and 168", {
284410
+ throw new import_errors13.AbloValidationError("--branch-ttl-hours must be between 1 and 168", {
284082
284411
  code: "cli_invalid_arguments"
284083
284412
  });
284084
284413
  }
@@ -284096,12 +284425,12 @@ function parseBranchDevArgs(argv) {
284096
284425
  function branchSlugFromRef(ref) {
284097
284426
  const base = ref.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
284098
284427
  if (!base) {
284099
- throw new import_errors12.AbloValidationError(`cannot derive an Ablo branch slug from "${ref}"`, {
284428
+ throw new import_errors13.AbloValidationError(`cannot derive an Ablo branch slug from "${ref}"`, {
284100
284429
  code: "cli_invalid_arguments"
284101
284430
  });
284102
284431
  }
284103
284432
  const nonRoot = base === "production" ? "production-dev" : base;
284104
- const shortened = nonRoot.length <= 40 ? nonRoot : `${nonRoot.slice(0, 31).replace(/-+$/g, "")}-${(0, import_node_crypto.createHash)("sha256").update(nonRoot).digest("hex").slice(0, 8)}`;
284433
+ const shortened = nonRoot.length <= 40 ? nonRoot : `${nonRoot.slice(0, 31).replace(/-+$/g, "")}-${(0, import_node_crypto2.createHash)("sha256").update(nonRoot).digest("hex").slice(0, 8)}`;
284105
284434
  return import_branches2.branchSlugSchema.parse(shortened);
284106
284435
  }
284107
284436
  function gitBranch() {
@@ -284118,7 +284447,7 @@ function gitBranch() {
284118
284447
  function discoverBranchRef(explicit, env = process.env, readGitBranch = gitBranch) {
284119
284448
  const value = explicit ?? env.ABLO_BRANCH ?? env.GITHUB_HEAD_REF ?? env.GITHUB_REF_NAME ?? env.VERCEL_GIT_COMMIT_REF ?? env.CI_COMMIT_REF_NAME ?? readGitBranch();
284120
284449
  if (!value) {
284121
- throw new import_errors12.AbloValidationError(
284450
+ throw new import_errors13.AbloValidationError(
284122
284451
  "Could not determine the Git branch. Pass --branch <slug> or set ABLO_BRANCH.",
284123
284452
  { code: "cli_invalid_arguments" }
284124
284453
  );
@@ -284136,13 +284465,13 @@ async function runBranchDev(argv, dependencies = {}) {
284136
284465
  const slug = branchSlugFromRef(ref);
284137
284466
  const managementKey = dependencies.resolveManagementKey?.() ?? resolveManagementKey();
284138
284467
  if (!managementKey) {
284139
- throw new import_errors12.AbloValidationError(
284468
+ throw new import_errors13.AbloValidationError(
284140
284469
  "Creating a development branch needs a project management credential. Run `npx ablo login` or set ABLO_MANAGEMENT_KEY.",
284141
284470
  { code: "cli_invalid_arguments" }
284142
284471
  );
284143
284472
  }
284144
284473
  if (!managementKey.startsWith("mk_")) {
284145
- throw new import_errors12.AbloValidationError(
284474
+ throw new import_errors13.AbloValidationError(
284146
284475
  "Branch creation needs the active project management credential (mk_\u2026). Run `npx ablo login` to refresh it.",
284147
284476
  { code: "cli_invalid_arguments" }
284148
284477
  );
@@ -284169,7 +284498,7 @@ async function runBranchDev(argv, dependencies = {}) {
284169
284498
  // src/whoami.ts
284170
284499
  init_cjs_shims();
284171
284500
  var import_picocolors16 = __toESM(require_picocolors(), 1);
284172
- var import_errors13 = require("@abloatai/transaction/errors");
284501
+ var import_errors14 = require("@abloatai/transaction/errors");
284173
284502
  init_config();
284174
284503
  init_controlPlane();
284175
284504
 
@@ -284244,7 +284573,7 @@ function parseWhoamiArgs(argv) {
284244
284573
  case "--key": {
284245
284574
  const value = argv[++i];
284246
284575
  if (!value || value.startsWith("--")) {
284247
- throw new import_errors13.AbloValidationError("`--key` needs a credential value.", {
284576
+ throw new import_errors14.AbloValidationError("`--key` needs a credential value.", {
284248
284577
  code: "cli_invalid_arguments"
284249
284578
  });
284250
284579
  }
@@ -284254,12 +284583,12 @@ function parseWhoamiArgs(argv) {
284254
284583
  case "--key-env": {
284255
284584
  const value = argv[++i];
284256
284585
  if (!value || value.startsWith("--")) {
284257
- throw new import_errors13.AbloValidationError("`--key-env` needs an environment variable name.", {
284586
+ throw new import_errors14.AbloValidationError("`--key-env` needs an environment variable name.", {
284258
284587
  code: "cli_invalid_arguments"
284259
284588
  });
284260
284589
  }
284261
284590
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
284262
- throw new import_errors13.AbloValidationError(
284591
+ throw new import_errors14.AbloValidationError(
284263
284592
  `\`${value}\` is not a valid environment variable name.`,
284264
284593
  { code: "cli_invalid_arguments" }
284265
284594
  );
@@ -284268,13 +284597,13 @@ function parseWhoamiArgs(argv) {
284268
284597
  break;
284269
284598
  }
284270
284599
  default:
284271
- throw new import_errors13.AbloValidationError(`unknown whoami flag: ${arg}`, {
284600
+ throw new import_errors14.AbloValidationError(`unknown whoami flag: ${arg}`, {
284272
284601
  code: "cli_invalid_arguments"
284273
284602
  });
284274
284603
  }
284275
284604
  }
284276
284605
  if (key && keyEnv) {
284277
- throw new import_errors13.AbloValidationError("Choose one credential source: `--key` or `--key-env`.", {
284606
+ throw new import_errors14.AbloValidationError("Choose one credential source: `--key` or `--key-env`.", {
284278
284607
  code: "cli_invalid_arguments"
284279
284608
  });
284280
284609
  }
@@ -284285,7 +284614,7 @@ function selectWhoamiCredential(args, cwd = process.cwd()) {
284285
284614
  if (args.keyEnv) {
284286
284615
  const found = readProjectEnvVariable(args.keyEnv, cwd);
284287
284616
  if (!found) {
284288
- throw new import_errors13.AbloAuthenticationError(
284617
+ throw new import_errors14.AbloAuthenticationError(
284289
284618
  `${args.keyEnv} is not set in the process environment, .env.local, or .env.`,
284290
284619
  { code: "cli_api_key_missing" }
284291
284620
  );
@@ -284314,7 +284643,7 @@ function selectWhoamiCredential(args, cwd = process.cwd()) {
284314
284643
  };
284315
284644
  }
284316
284645
  const ambient = ambientEnvKeyNote();
284317
- throw new import_errors13.AbloAuthenticationError(
284646
+ throw new import_errors14.AbloAuthenticationError(
284318
284647
  `No credential found. Run \`ablo login\`, set ABLO_API_KEY, or pass \`--key-env <NAME>\`.${ambient ? `
284319
284648
 
284320
284649
  ${ambient}` : ""}`,
@@ -284332,7 +284661,7 @@ async function whoami(argv) {
284332
284661
  });
284333
284662
  const confirmed = target.confirmed;
284334
284663
  if (!confirmed) {
284335
- throw new import_errors13.AbloAuthenticationError(
284664
+ throw new import_errors14.AbloAuthenticationError(
284336
284665
  "The server did not confirm an identity for this credential.",
284337
284666
  { code: "identity_resolve_failed" }
284338
284667
  );
@@ -284436,7 +284765,7 @@ var COMMANDS = [
284436
284765
  { run: "connect check", does: "Confirm your database is ready to share changes with Ablo" },
284437
284766
  { run: "connect resnapshot", does: "Reload existing rows without deregistering or rotating credentials" },
284438
284767
  { run: "connect scan", does: "List anything Ablo ever set up in your database (read-only)" },
284439
- { run: "connect locate", does: "See which plane holds a database before connecting it" },
284768
+ { run: "connect locate", does: "See which branch is connected to a database before connecting it" },
284440
284769
  { run: "connect deregister", does: "Disconnect this project's database \u2014 Ablo stops reading and writing it" }
284441
284770
  ]
284442
284771
  }
@@ -284667,12 +284996,12 @@ function fullRows(group) {
284667
284996
  }
284668
284997
 
284669
284998
  // src/index.ts
284670
- var import_errors21 = require("@abloatai/transaction/errors");
284999
+ var import_errors22 = require("@abloatai/transaction/errors");
284671
285000
  init_push();
284672
285001
 
284673
285002
  // src/generate.ts
284674
285003
  init_cjs_shims();
284675
- var import_errors14 = require("@abloatai/transaction/errors");
285004
+ var import_errors15 = require("@abloatai/transaction/errors");
284676
285005
  var import_fs8 = require("fs");
284677
285006
  var import_path6 = require("path");
284678
285007
  var import_picocolors17 = __toESM(require_picocolors(), 1);
@@ -284698,7 +285027,7 @@ function parseGenerateArgs(argv) {
284698
285027
  out = argv[++i] ?? out;
284699
285028
  break;
284700
285029
  default:
284701
- throw new import_errors14.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
285030
+ throw new import_errors15.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
284702
285031
  }
284703
285032
  }
284704
285033
  return { schemaPath, exportName, out };
@@ -284731,7 +285060,7 @@ init_cjs_shims();
284731
285060
  var import_child_process2 = require("child_process");
284732
285061
  var import_picocolors18 = __toESM(require_picocolors(), 1);
284733
285062
  init_dist2();
284734
- var import_errors15 = require("@abloatai/transaction/errors");
285063
+ var import_errors16 = require("@abloatai/transaction/errors");
284735
285064
  var import_wire8 = require("@abloatai/transaction/wire");
284736
285065
  init_config();
284737
285066
  init_theme();
@@ -284865,7 +285194,7 @@ ${import_picocolors18.default.dim(url)}`, "Approve in your browser");
284865
285194
  }
284866
285195
  if (!provRes.ok) {
284867
285196
  s.stop("Could not provision a key.");
284868
- const err = (0, import_errors15.translateHttpError)(
285197
+ const err = (0, import_errors16.translateHttpError)(
284869
285198
  provRes.status,
284870
285199
  await provRes.json().catch(() => null),
284871
285200
  provRes.headers.get("x-request-id") ?? void 0
@@ -285180,11 +285509,11 @@ async function status(args = []) {
285180
285509
  }
285181
285510
  } else if (dataSource.kind === "none") {
285182
285511
  console.log(
285183
- ` ${import_picocolors19.default.dim("data")} ${import_picocolors19.default.red("\u2717 no database connected to this plane")} ${import_picocolors19.default.dim("\u2014 writes are held")}`
285512
+ ` ${import_picocolors19.default.dim("data")} ${import_picocolors19.default.red("\u2717 this branch is not connected to a database")} ${import_picocolors19.default.dim("\u2014 writes are held")}`
285184
285513
  );
285185
285514
  } else if (reachable) {
285186
285515
  console.log(
285187
- ` ${import_picocolors19.default.dim("data")} ${import_picocolors19.default.yellow("?")} ${import_picocolors19.default.dim(`could not read the plane's databases (${dataSource.detail})`)}`
285516
+ ` ${import_picocolors19.default.dim("data")} ${import_picocolors19.default.yellow("?")} ${import_picocolors19.default.dim(`could not read this branch's database connection (${dataSource.detail})`)}`
285188
285517
  );
285189
285518
  }
285190
285519
  const pushed = reachable ? await fetchPushedSchema(apiUrl3, introspectKey) : null;
@@ -285230,7 +285559,7 @@ async function status(args = []) {
285230
285559
  }
285231
285560
  } else if (dataSource.kind === "unknown") {
285232
285561
  console.log(
285233
- ` ${import_picocolors19.default.yellow("?")} ${import_picocolors19.default.dim("nothing is blocking a write, but this key could not read the plane's databases \u2014 some checks were skipped")}`
285562
+ ` ${import_picocolors19.default.yellow("?")} ${import_picocolors19.default.dim("nothing is blocking a write, but this key could not read the branch's database connection \u2014 some checks were skipped")}`
285234
285563
  );
285235
285564
  } else {
285236
285565
  console.log(
@@ -285337,7 +285666,7 @@ async function doctor() {
285337
285666
  checks.push({
285338
285667
  label: "data",
285339
285668
  state: "fail",
285340
- detail: "no database connected to this plane \u2014 writes are held",
285669
+ detail: "this branch is not connected to a database \u2014 writes are held",
285341
285670
  fix: "connect one with `ablo connect apply`"
285342
285671
  });
285343
285672
  } else {
@@ -285429,7 +285758,7 @@ async function doctor() {
285429
285758
 
285430
285759
  // src/logs.ts
285431
285760
  init_cjs_shims();
285432
- var import_errors16 = require("@abloatai/transaction/errors");
285761
+ var import_errors17 = require("@abloatai/transaction/errors");
285433
285762
  var import_wire9 = require("@abloatai/transaction/wire");
285434
285763
  var import_picocolors21 = __toESM(require_picocolors(), 1);
285435
285764
  init_config();
@@ -285471,12 +285800,12 @@ function parseLogsArgs(argv) {
285471
285800
  args.json = true;
285472
285801
  break;
285473
285802
  case "--mode":
285474
- throw new import_errors16.AbloValidationError(
285803
+ throw new import_errors17.AbloValidationError(
285475
285804
  "--mode was removed. Logs follow the branch bound to ABLO_API_KEY; select a different branch by supplying its key.",
285476
285805
  { code: "cli_invalid_arguments" }
285477
285806
  );
285478
285807
  default:
285479
- throw new import_errors16.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
285808
+ throw new import_errors17.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
285480
285809
  }
285481
285810
  }
285482
285811
  return args;
@@ -285755,7 +286084,7 @@ async function webhooks(argv) {
285755
286084
 
285756
286085
  // src/check.ts
285757
286086
  init_cjs_shims();
285758
- var import_errors17 = require("@abloatai/transaction/errors");
286087
+ var import_errors18 = require("@abloatai/transaction/errors");
285759
286088
  var import_picocolors23 = __toESM(require_picocolors(), 1);
285760
286089
  init_src();
285761
286090
  var import_schema9 = require("@abloatai/transaction/schema");
@@ -285903,7 +286232,7 @@ function parseCheckArgs(argv) {
285903
286232
  appSchema = argv[++i] ?? appSchema;
285904
286233
  break;
285905
286234
  default:
285906
- throw new import_errors17.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
286235
+ throw new import_errors18.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
285907
286236
  }
285908
286237
  }
285909
286238
  return { schemaPath, exportName, appSchema };
@@ -285929,7 +286258,7 @@ async function reportReadSubject(dbUrl) {
285929
286258
  }
285930
286259
  if (state.kind === "none") {
285931
286260
  console.log(
285932
- ` ${import_picocolors23.default.dim("ablo")} ${import_picocolors23.default.yellow("!")} no database is registered for this plane, so Ablo does not read this one`
286261
+ ` ${import_picocolors23.default.dim("ablo")} ${import_picocolors23.default.yellow("!")} this branch is not connected to a database, so Ablo does not read this one`
285933
286262
  );
285934
286263
  console.log(
285935
286264
  ` ${import_picocolors23.default.dim(`Connect it with ${import_picocolors23.default.bold("ablo connect apply")}. Until then a table here is invisible to the engine.`)}
@@ -286069,9 +286398,9 @@ var ABLO_REACT = /* @__PURE__ */ new Set(["@abloatai/ablo/react", "@abloatai/hum
286069
286398
  function clientRoots(sf) {
286070
286399
  const roots = /* @__PURE__ */ new Set(["ablo", "sync"]);
286071
286400
  for (const decl of sf.getVariableDeclarations()) {
286072
- const init2 = decl.getInitializer();
286073
- if (!init2) continue;
286074
- const text = init2.getText();
286401
+ const init4 = decl.getInitializer();
286402
+ if (!init4) continue;
286403
+ const text = init4.getText();
286075
286404
  if (/^Ablo\s*\(/.test(text) || /^useAblo\s*\(\s*\)/.test(text)) {
286076
286405
  roots.add(decl.getName());
286077
286406
  }
@@ -286238,7 +286567,7 @@ async function upgrade(argv) {
286238
286567
 
286239
286568
  // src/pull.ts
286240
286569
  init_cjs_shims();
286241
- var import_errors18 = require("@abloatai/transaction/errors");
286570
+ var import_errors19 = require("@abloatai/transaction/errors");
286242
286571
  var import_picocolors25 = __toESM(require_picocolors(), 1);
286243
286572
  init_src();
286244
286573
  var import_fs10 = require("fs");
@@ -286267,7 +286596,7 @@ function parsePullArgs(argv) {
286267
286596
  force = true;
286268
286597
  break;
286269
286598
  default:
286270
- throw new import_errors18.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
286599
+ throw new import_errors19.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
286271
286600
  }
286272
286601
  }
286273
286602
  return { out, appSchema, importPath, force };
@@ -286385,7 +286714,7 @@ async function pull(argv) {
286385
286714
 
286386
286715
  // src/prismaPull.ts
286387
286716
  init_cjs_shims();
286388
- var import_errors19 = require("@abloatai/transaction/errors");
286717
+ var import_errors20 = require("@abloatai/transaction/errors");
286389
286718
  var import_picocolors26 = __toESM(require_picocolors(), 1);
286390
286719
  var import_fs11 = require("fs");
286391
286720
  init_theme();
@@ -286583,7 +286912,7 @@ function parsePrismaPullArgs(argv) {
286583
286912
  force = true;
286584
286913
  break;
286585
286914
  default:
286586
- if (arg.startsWith("--")) throw new import_errors19.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
286915
+ if (arg.startsWith("--")) throw new import_errors20.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
286587
286916
  schema = arg;
286588
286917
  }
286589
286918
  }
@@ -286643,7 +286972,7 @@ async function prismaPull(argv) {
286643
286972
  // src/drizzlePull.ts
286644
286973
  init_cjs_shims();
286645
286974
  var import_picocolors27 = __toESM(require_picocolors(), 1);
286646
- var import_errors20 = require("@abloatai/transaction/errors");
286975
+ var import_errors21 = require("@abloatai/transaction/errors");
286647
286976
  var import_fs12 = require("fs");
286648
286977
  var import_path7 = require("path");
286649
286978
  init_theme();
@@ -286750,7 +287079,7 @@ function parseDrizzlePullArgs(argv) {
286750
287079
  force = true;
286751
287080
  break;
286752
287081
  default:
286753
- if (arg.startsWith("--")) throw new import_errors20.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
287082
+ if (arg.startsWith("--")) throw new import_errors21.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
286754
287083
  schema = arg;
286755
287084
  }
286756
287085
  }
@@ -286822,6 +287151,7 @@ async function drizzlePull(argv) {
286822
287151
  // src/index.ts
286823
287152
  init_theme();
286824
287153
  init_renderError();
287154
+ init_observeCliError();
286825
287155
 
286826
287156
  // src/generators/authScaffold.ts
286827
287157
  init_cjs_shims();
@@ -286903,7 +287233,7 @@ var LOGO = `
286903
287233
  ${brand("ablo")} ${import_picocolors28.default.dim("sync engine")}
286904
287234
  `;
286905
287235
  var HANDLERS = {
286906
- init: (argv) => init([...argv]),
287236
+ init: (argv) => init3([...argv]),
286907
287237
  login: (argv) => login([...argv]),
286908
287238
  logout: () => logout(),
286909
287239
  projects: (argv) => projects([...argv]),
@@ -286956,7 +287286,7 @@ async function main() {
286956
287286
  const argv = process.argv.slice(3);
286957
287287
  if (!command && raw !== void 0 && raw !== "help" && !raw.startsWith("-")) {
286958
287288
  const suggestion = suggestCommand(raw);
286959
- throw new import_errors21.AbloValidationError(
287289
+ throw new import_errors22.AbloValidationError(
286960
287290
  `\`${raw}\` isn't an ablo command.` + (suggestion ? ` Did you mean \`ablo ${suggestion}\`?` : " Run `ablo help --all` to see every command."),
286961
287291
  { code: "cli_invalid_arguments" }
286962
287292
  );
@@ -287100,7 +287430,7 @@ async function chooseBool(flagValue, fallback, interactive, prompt) {
287100
287430
  bailIfCancelled(value);
287101
287431
  return value;
287102
287432
  }
287103
- async function init(args = []) {
287433
+ async function init3(args = []) {
287104
287434
  const opts = parseInitArgs(args);
287105
287435
  const interactive = Boolean(process.stdin.isTTY) && !opts.yes && !process.env.CI;
287106
287436
  Ie(`${brand("ablo")} ${import_picocolors28.default.dim("sync engine")}`);
@@ -287633,8 +287963,17 @@ function detectPackageManager() {
287633
287963
  if ((0, import_fs13.existsSync)("bun.lockb")) return "bun";
287634
287964
  return "npm";
287635
287965
  }
287636
- main().catch((err) => {
287966
+ installCliExitObservationBoundary();
287967
+ main().catch(async (err) => {
287968
+ if (err instanceof CliFailureExit) {
287969
+ observeCliError(err);
287970
+ await flushCliErrors();
287971
+ restoreCliExitObservationBoundary();
287972
+ process.exit(err.exitCode);
287973
+ }
287637
287974
  renderCliError(err);
287975
+ await flushCliErrors();
287976
+ restoreCliExitObservationBoundary();
287638
287977
  process.exit(process.exitCode ?? 1);
287639
287978
  });
287640
287979
  /*! Bundled license information: