@abloatai/cli 0.47.0 → 0.48.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 +199 -46
  2. package/package.json +3 -3
package/dist/cli.cjs CHANGED
@@ -4099,9 +4099,11 @@ async function loadSchema(schemaPath, exportName) {
4099
4099
  function maskKey(key) {
4100
4100
  return key ? `${key.slice(0, 12)}\u2026` : "(none)";
4101
4101
  }
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.`;
4102
+ function schemaPushStorageHint(code) {
4103
+ if (code === "no_data_source_registered") {
4104
+ 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.`;
4105
+ }
4106
+ return null;
4105
4107
  }
4106
4108
  function schemaGitState(schemaPath) {
4107
4109
  try {
@@ -4411,9 +4413,9 @@ async function push(argv) {
4411
4413
  const serverMsg = body.message ?? body.reason;
4412
4414
  console.error(import_picocolors5.default.red(` Forbidden${code ? ` [${code}]` : ""}: ${serverMsg ?? "permission denied"}`));
4413
4415
  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}`));
4416
+ const storageHint = schemaPushStorageHint(code);
4417
+ if (storageHint) {
4418
+ console.error(import_picocolors5.default.dim(` ${storageHint}`));
4417
4419
  } else if (code === "database_role_cannot_enforce_rls") {
4418
4420
  console.error(
4419
4421
  import_picocolors5.default.dim(
@@ -4702,7 +4704,7 @@ function connectSetupSql(input) {
4702
4704
  const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : import_footprint.ABLO_WRITE_ROLE;
4703
4705
  const tables = input.tables ?? [];
4704
4706
  const schema = input.schema ?? "public";
4705
- const publication = input.publication ?? import_footprint.ABLO_PUBLICATION;
4707
+ const publication = input.publication;
4706
4708
  const qualifiedTables = tables.map((table) => `${quoteIdent(schema)}.${quoteIdent(table)}`);
4707
4709
  const publicationTarget = tables.length > 0 ? `FOR TABLE ${qualifiedTables.join(", ")}` : "FOR ALL TABLES";
4708
4710
  const tableList = qualifiedTables.join(", ");
@@ -4780,8 +4782,8 @@ BEGIN
4780
4782
  END $$;`
4781
4783
  ];
4782
4784
  }
4783
- function reconcilePublicationPlan(current, desiredTables, opts = {}) {
4784
- const pub = quoteIdent(opts.publication ?? import_footprint.ABLO_PUBLICATION);
4785
+ function reconcilePublicationPlan(current, desiredTables, opts) {
4786
+ const pub = quoteIdent(opts.publication);
4785
4787
  const schema = opts.schema ?? "public";
4786
4788
  const qualified = (table) => `${quoteIdent(schema)}.${quoteIdent(table)}`;
4787
4789
  const desiredAll = desiredTables.length === 0;
@@ -4817,8 +4819,8 @@ function reconcilePublicationPlan(current, desiredTables, opts = {}) {
4817
4819
  recreated: false
4818
4820
  };
4819
4821
  }
4820
- async function readPublicationState(sql, opts = {}) {
4821
- const publication = opts.publication ?? import_footprint.ABLO_PUBLICATION;
4822
+ async function readPublicationState(sql, opts) {
4823
+ const publication = opts.publication;
4822
4824
  const schema = opts.schema ?? "public";
4823
4825
  const pubRows = await sql.unsafe(
4824
4826
  `SELECT puballtables FROM pg_publication WHERE pubname = $1`,
@@ -4833,8 +4835,8 @@ async function readPublicationState(sql, opts = {}) {
4833
4835
  );
4834
4836
  return { exists: true, allTables: false, tables: tableRows.map((r2) => r2.tablename) };
4835
4837
  }
4836
- async function probeReadiness(sql, opts = {}) {
4837
- const publication = opts.publication ?? import_footprint.ABLO_PUBLICATION;
4838
+ async function probeReadiness(sql, opts) {
4839
+ const publication = opts.publication;
4838
4840
  const schema = opts.schema ?? "public";
4839
4841
  const coordinated = opts.coordinatedTables && opts.coordinatedTables.length > 0 ? new Set(opts.coordinatedTables) : null;
4840
4842
  const items = [];
@@ -5465,7 +5467,7 @@ function blockers(input) {
5465
5467
  }
5466
5468
  if (input.dataSource.kind === "none") {
5467
5469
  found.push({
5468
- problem: "no database is connected to this plane, so writes are held rather than routed",
5470
+ problem: "this branch is not connected to a database, so writes are held",
5469
5471
  fix: "connect one with `ablo connect apply`"
5470
5472
  });
5471
5473
  }
@@ -5517,7 +5519,7 @@ function connectApplyPlan(input) {
5517
5519
  const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : import_footprint.ABLO_WRITE_ROLE;
5518
5520
  const tables = input.tables ?? [];
5519
5521
  const schema = input.schema ?? "public";
5520
- const publication = input.publication ?? import_footprint.ABLO_PUBLICATION;
5522
+ const publication = input.publication;
5521
5523
  const provider = input.provider ?? "generic";
5522
5524
  const recipe = connectSetupSql({ tables, role, writeRole, schema, publication });
5523
5525
  const isWal = (s) => s.startsWith("ALTER SYSTEM SET wal_level");
@@ -5733,7 +5735,7 @@ function rotateWithoutConnection(input) {
5733
5735
  }
5734
5736
  if (!input.known || input.planeHasConnection) return null;
5735
5737
  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.";
5738
+ 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
5739
  }
5738
5740
  async function locateExistingConnection(input) {
5739
5741
  const result = await tryControlPlane({
@@ -6409,12 +6411,15 @@ function parseConnectArgs(argv) {
6409
6411
  manual
6410
6412
  };
6411
6413
  }
6412
- function printConnectRecipe(args) {
6414
+ function printConnectRecipe(args, footprint) {
6415
+ const role = args.role === import_footprint.ABLO_REPLICATION_ROLE ? footprint.replicationRole : args.role;
6416
+ const writeRole = args.writeRole === import_footprint.ABLO_WRITE_ROLE ? footprint.writeRole : args.writeRole;
6413
6417
  const sql = connectSetupSql({
6414
6418
  tables: args.tables,
6415
- role: args.role,
6416
- writeRole: args.writeRole,
6417
- schema: args.schema
6419
+ role,
6420
+ writeRole,
6421
+ schema: args.schema,
6422
+ publication: footprint.publication
6418
6423
  });
6419
6424
  console.log(
6420
6425
  `
@@ -6465,7 +6470,7 @@ function printConnectRecipe(args) {
6465
6470
  console.log(
6466
6471
  import_picocolors12.default.dim(
6467
6472
  ` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
6468
- ${import_picocolors12.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
6473
+ ${import_picocolors12.default.bold(`GRANT rds_replication TO ${quoteIdent(role)};`)}`
6469
6474
  )
6470
6475
  );
6471
6476
  console.log(
@@ -6489,8 +6494,8 @@ function printConnectRecipe(args) {
6489
6494
  `
6490
6495
  ${import_picocolors12.default.bold("5.")} Register the two roles with Ablo. Set them just long enough to register \u2014
6491
6496
  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)`)}
6497
+ ${import_picocolors12.default.bold("ABLO_REPLICATION_DATABASE_URL")} ${import_picocolors12.default.dim(`\u2192 ${role} (replication only)`)}
6498
+ ${import_picocolors12.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors12.default.dim(`\u2192 ${writeRole} (DML only)`)}
6494
6499
  ${import_picocolors12.default.cyan("npx ablo connect register")}
6495
6500
  `
6496
6501
  );
@@ -6518,9 +6523,9 @@ function printCheckItem(item) {
6518
6523
  }
6519
6524
  }
6520
6525
  }
6521
- async function probeDirectWriteReadiness(sql, opts = {}) {
6526
+ async function probeDirectWriteReadiness(sql, opts) {
6522
6527
  const schema = opts.schema ?? "public";
6523
- const publication = opts.publication ?? import_footprint.ABLO_PUBLICATION;
6528
+ const publication = opts.publication;
6524
6529
  const items = [];
6525
6530
  const roleRows = await sql.unsafe(
6526
6531
  `SELECT rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole, rolreplication
@@ -6635,6 +6640,12 @@ async function probeDirectWriteReadiness(sql, opts = {}) {
6635
6640
  }
6636
6641
  async function auditTenantSyncInfra(sql, opts = {}) {
6637
6642
  const artifacts = [];
6643
+ const branchScopedTemplates = /* @__PURE__ */ new Set([
6644
+ import_footprint.ABLO_PUBLICATION,
6645
+ import_footprint3.ABLO_REPLICATION_SLOT,
6646
+ import_footprint.ABLO_REPLICATION_ROLE,
6647
+ import_footprint.ABLO_WRITE_ROLE
6648
+ ]);
6638
6649
  for (const artifact of import_footprint3.ABLO_FOOTPRINT) {
6639
6650
  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
6651
  const key = artifact.kind === "table" || artifact.kind === "type" ? `${artifact.retired ? "public" : opts.schema ?? "public"}.${name}` : name;
@@ -6647,7 +6658,7 @@ async function auditTenantSyncInfra(sql, opts = {}) {
6647
6658
  present: rows[0]?.present === true,
6648
6659
  purpose: artifact.purpose,
6649
6660
  ...artifact.hazard ? { hazard: artifact.hazard } : {},
6650
- ...artifact.retired ? { retired: true } : {}
6661
+ ...artifact.retired || !opts.names && branchScopedTemplates.has(artifact.name) ? { retired: true } : {}
6651
6662
  });
6652
6663
  }
6653
6664
  return artifacts;
@@ -6672,7 +6683,7 @@ function requireScopedUrl(kind, verb) {
6672
6683
  );
6673
6684
  process.exit(1);
6674
6685
  }
6675
- async function probeAndReport(dbUrl, kind = "replication", opts = {}) {
6686
+ async function probeAndReport(dbUrl, kind, opts) {
6676
6687
  const sql = src_default(dbUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
6677
6688
  } });
6678
6689
  let items;
@@ -6713,7 +6724,7 @@ ${ambient}` : ""}`,
6713
6724
  if (!result.ok) {
6714
6725
  if (result.code === "no_data_source_registered") {
6715
6726
  console.error(
6716
- ` ${import_picocolors12.default.yellow("\u2014")} No database is connected to this plane yet, so there's nothing to check.
6727
+ ` ${import_picocolors12.default.yellow("\u2014")} This branch is not connected to a database yet, so there's nothing to check.
6717
6728
  ` + import_picocolors12.default.dim(
6718
6729
  ` Connect one with ${import_picocolors12.default.bold("ablo connect apply")}, then re-run ${import_picocolors12.default.bold("ablo connect check")}.
6719
6730
  `
@@ -6805,13 +6816,32 @@ ${ambient}` : ""}`,
6805
6816
  ${brand("ablo")} ${import_picocolors12.default.dim("connect register")} ${import_picocolors12.default.dim("register a direct DataSource")}
6806
6817
  `
6807
6818
  );
6819
+ const target = await resolveTarget({ url: apiBaseUrl(), apiKey, keySource: "env" });
6820
+ const confirmed = target.confirmed;
6821
+ if (!confirmed?.branchId) {
6822
+ throw new import_errors10.AbloValidationError(
6823
+ "This key is not bound to a branch, so Ablo cannot validate isolated database objects safely.",
6824
+ { code: "cli_database_unreachable" }
6825
+ );
6826
+ }
6827
+ const publication = (0, import_footprint3.footprintNamesFor)({
6828
+ organizationId: confirmed.organizationId,
6829
+ branchId: confirmed.branchId,
6830
+ ...confirmed.projectId ? { projectId: confirmed.projectId } : {}
6831
+ }).publication;
6808
6832
  console.log(` ${import_picocolors12.default.bold("Replication role")}
6809
6833
  `);
6810
- const replication = await probeAndReport(dbUrl, "replication", { schema: args.schema });
6834
+ const replication = await probeAndReport(dbUrl, "replication", {
6835
+ schema: args.schema,
6836
+ publication
6837
+ });
6811
6838
  console.log(`
6812
6839
  ${import_picocolors12.default.bold("Direct-write role")}
6813
6840
  `);
6814
- const write = await probeAndReport(writeDbUrl, "write", { schema: args.schema });
6841
+ const write = await probeAndReport(writeDbUrl, "write", {
6842
+ schema: args.schema,
6843
+ publication
6844
+ });
6815
6845
  const noDial = [
6816
6846
  replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
6817
6847
  write.kind === "no-dial" ? `write: ${write.reason}` : null
@@ -6918,7 +6948,7 @@ async function runScan(args) {
6918
6948
  async function runLocate(args) {
6919
6949
  console.log(
6920
6950
  `
6921
- ${brand("ablo")} ${import_picocolors12.default.dim("connect locate")} ${import_picocolors12.default.dim("which plane holds this database")}
6951
+ ${brand("ablo")} ${import_picocolors12.default.dim("connect locate")} ${import_picocolors12.default.dim("which branch is connected to this database")}
6922
6952
  `
6923
6953
  );
6924
6954
  const url = args.url ?? readProjectAdminDatabaseUrl();
@@ -6961,7 +6991,7 @@ ${ambient}` : ""}`,
6961
6991
  }
6962
6992
  if (!answer.held) {
6963
6993
  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.
6994
+ ` ${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
6995
  `
6966
6996
  );
6967
6997
  return;
@@ -6972,9 +7002,9 @@ ${ambient}` : ""}`,
6972
7002
  );
6973
7003
  console.log(
6974
7004
  import_picocolors12.default.dim(
6975
- ` Ablo binds one plane to each database schema. To move this schema, disconnect it there
7005
+ ` Ablo connects one branch to each database schema. To move this schema, disconnect it there
6976
7006
  first \u2014 run `
6977
- ) + import_picocolors12.default.cyan("ablo connect deregister") + import_picocolors12.default.dim(` with a key for that plane, then connect here.
7007
+ ) + import_picocolors12.default.cyan("ablo connect deregister") + import_picocolors12.default.dim(` with a key for that branch, then connect here.
6978
7008
  `) + import_picocolors12.default.dim(` Confirm a candidate key first with `) + import_picocolors12.default.bold("ablo whoami --key-env <NAME>") + import_picocolors12.default.dim(`.
6979
7009
  `) + 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
7010
  );
@@ -7065,7 +7095,29 @@ async function connect(argv) {
7065
7095
  await runConnectApply2(args);
7066
7096
  return;
7067
7097
  }
7068
- printConnectRecipe(args);
7098
+ const apiKey = resolveRuntimeApiKey().key;
7099
+ if (!apiKey) {
7100
+ throw new import_errors10.AbloAuthenticationError(
7101
+ "Ablo needs the branch key to derive isolated database object names. Set ABLO_API_KEY for this branch, then re-run `ablo connect --manual`.",
7102
+ { code: "cli_api_key_missing" }
7103
+ );
7104
+ }
7105
+ const target = await resolveTarget({ url: apiBaseUrl(), apiKey, keySource: "env" });
7106
+ const confirmed = target.confirmed;
7107
+ if (!confirmed?.branchId) {
7108
+ throw new import_errors10.AbloValidationError(
7109
+ "This key is not bound to a branch, so Ablo cannot derive isolated database object names safely.",
7110
+ { code: "cli_database_unreachable" }
7111
+ );
7112
+ }
7113
+ printConnectRecipe(
7114
+ args,
7115
+ (0, import_footprint3.footprintNamesFor)({
7116
+ organizationId: confirmed.organizationId,
7117
+ branchId: confirmed.branchId,
7118
+ ...confirmed.projectId ? { projectId: confirmed.projectId } : {}
7119
+ })
7120
+ );
7069
7121
  }
7070
7122
  var import_errors10, import_picocolors12, import_footprint3, import_wire7, FOOTPRINT_LOOKUP, CONNECT_USAGE;
7071
7123
  var init_connect = __esm({
@@ -7105,7 +7157,7 @@ var init_connect = __esm({
7105
7157
  npx ablo connect rotate New passwords for both logins, then re-register
7106
7158
  npx ablo connect resnapshot Recreate only the slot and reload existing rows
7107
7159
  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)
7160
+ npx ablo connect locate See which branch is connected to this database (read-only; nothing is changed)
7109
7161
 
7110
7162
  Running it: bare \`ablo connect\` sets everything up for you \u2014 creating the two
7111
7163
  scoped logins, sharing your tables, and registering \u2014 whenever it finds a
@@ -283584,7 +283636,7 @@ async function readStatus(ref, context = {}) {
283584
283636
  return import_branches.branchStatusResponseSchema.parse(response.body);
283585
283637
  }
283586
283638
  function printStatus(status2) {
283587
- const { branch, schema, data_source: source } = status2;
283639
+ const { branch, schema, storage, data_source: source } = status2;
283588
283640
  console.log(` ${import_picocolors14.default.bold(branch.slug)} ${import_picocolors14.default.dim(branch.id)}`);
283589
283641
  console.log(
283590
283642
  ` ${import_picocolors14.default.dim("state")} ${branch.state === "ready" ? import_picocolors14.default.green(branch.state) : import_picocolors14.default.yellow(branch.state)}`
@@ -283600,7 +283652,7 @@ function printStatus(status2) {
283600
283652
  ` ${import_picocolors14.default.dim("parent")} ${import_picocolors14.default.bold(schema.parent_compatibility)}${counts}`
283601
283653
  );
283602
283654
  }
283603
- const sourceLabel = source.kind === "hosted" ? "hosted branch plane" : `${source.kind} \xB7 ${source.host ?? "unknown host"}${source.database ? `/${source.database}` : ""} \xB7 ${source.status ?? "unknown"}`;
283655
+ 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
283656
  console.log(` ${import_picocolors14.default.dim("data")} ${sourceLabel}`);
283605
283657
  if (status2.ready) {
283606
283658
  console.log(`
@@ -283747,11 +283799,15 @@ init_controlPlane();
283747
283799
  init_config();
283748
283800
  init_readiness();
283749
283801
  init_theme();
283802
+ init_dbRole();
283803
+ var import_source3 = require("@abloatai/transaction/source");
283750
283804
  function parseDevArgs(argv) {
283751
283805
  let schemaPath = DEFAULT_SCHEMA_PATH;
283752
283806
  let exportName = DEFAULT_EXPORT;
283753
283807
  let url = process.env.ABLO_API_URL ?? DEFAULT_URL;
283754
283808
  let watchEnabled = false;
283809
+ let local = false;
283810
+ let sourcePath = "ablo/data-source.ts";
283755
283811
  for (let i = 0; i < argv.length; i++) {
283756
283812
  const arg = argv[i];
283757
283813
  switch (arg) {
@@ -283770,6 +283826,12 @@ function parseDevArgs(argv) {
283770
283826
  case "--no-watch":
283771
283827
  watchEnabled = false;
283772
283828
  break;
283829
+ case "--local":
283830
+ local = true;
283831
+ break;
283832
+ case "--source":
283833
+ sourcePath = argv[++i] ?? sourcePath;
283834
+ break;
283773
283835
  default:
283774
283836
  throw new import_errors11.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
283775
283837
  }
@@ -283781,9 +283843,55 @@ function parseDevArgs(argv) {
283781
283843
  url,
283782
283844
  apiKey: process.env.ABLO_API_KEY,
283783
283845
  watch: watchEnabled,
283846
+ local,
283847
+ sourcePath,
283784
283848
  planeLabel: "branch"
283785
283849
  };
283786
283850
  }
283851
+ async function loadLocalSourceHandler(sourcePath) {
283852
+ const abs = (0, import_path5.resolve)(process.cwd(), sourcePath);
283853
+ if (!(0, import_fs7.existsSync)(abs)) {
283854
+ throw new import_errors11.AbloValidationError(
283855
+ `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>")}.`,
283856
+ { code: "cli_invalid_arguments" }
283857
+ );
283858
+ }
283859
+ const { createJiti } = await import("jiti");
283860
+ const jiti = createJiti(process.cwd());
283861
+ const mod = await jiti.import(abs);
283862
+ const nested = mod.default && typeof mod.default === "object" ? mod.default : void 0;
283863
+ const handler = mod.POST ?? nested?.POST;
283864
+ if (typeof handler !== "function") {
283865
+ throw new import_errors11.AbloValidationError(
283866
+ `${import_picocolors15.default.bold(sourcePath)} must export a ${import_picocolors15.default.bold("POST(request)")} Data Source handler.`,
283867
+ { code: "cli_invalid_arguments" }
283868
+ );
283869
+ }
283870
+ return handler;
283871
+ }
283872
+ async function registerLocalSource(args) {
283873
+ const response = await fetch(`${args.url}/v1/datasources`, {
283874
+ method: "POST",
283875
+ headers: {
283876
+ authorization: `Bearer ${args.apiKey}`,
283877
+ "content-type": "application/json"
283878
+ },
283879
+ body: JSON.stringify({
283880
+ connection: "endpoint",
283881
+ endpoint: "http://localhost/ablo-dev/reverse-channel",
283882
+ signingKey: args.apiKey,
283883
+ reverseChannel: true,
283884
+ metadata: { managed_by: "ablo dev --local" }
283885
+ })
283886
+ });
283887
+ if (!response.ok) {
283888
+ const body = await response.text();
283889
+ throw new import_errors11.AbloValidationError(
283890
+ `Could not register the local Data Source (${response.status}): ${body}`,
283891
+ { code: "cli_invalid_arguments" }
283892
+ );
283893
+ }
283894
+ }
283787
283895
  function classifyKey(apiKey) {
283788
283896
  if (!apiKey) {
283789
283897
  return {
@@ -283921,7 +284029,7 @@ async function runPush(schema, args) {
283921
284029
  }
283922
284030
  if (status2 === 403) {
283923
284031
  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")}.`);
284032
+ 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
284033
  return {
283926
284034
  ok: false,
283927
284035
  message: `${serverSays ?? "This key can't author schema (missing schema:push scope)."}
@@ -283929,6 +284037,11 @@ async function runPush(schema, args) {
283929
284037
  };
283930
284038
  }
283931
284039
  const serverMessage = String(body.message ?? body.reason ?? bodyText);
284040
+ const storageHint = schemaPushStorageHint(body.code);
284041
+ if (storageHint) {
284042
+ return { ok: false, message: `${serverMessage}
284043
+ ${import_picocolors15.default.dim(storageHint)}` };
284044
+ }
283932
284045
  if (looksLikeCredentialRefusal(serverMessage)) {
283933
284046
  const pooled = await poolerExplanation(apiBaseUrl(), args.apiKey);
283934
284047
  if (pooled) {
@@ -283949,6 +284062,12 @@ async function dev(argv, runtime = {}) {
283949
284062
  if (runtime.apiKey) args.apiKey = runtime.apiKey;
283950
284063
  else if (!args.apiKey) args.apiKey = resolveRuntimeApiKey("sandbox").key;
283951
284064
  if (runtime.branch) args.planeLabel = runtime.branch.slug;
284065
+ if (args.local && !args.watch) {
284066
+ throw new import_errors11.AbloValidationError(
284067
+ `${import_picocolors15.default.bold("--local")} opens a long-lived secure connector and cannot be combined with ${import_picocolors15.default.bold("--no-watch")}.`,
284068
+ { code: "cli_invalid_arguments" }
284069
+ );
284070
+ }
283952
284071
  const key = classifyKey(args.apiKey);
283953
284072
  if (!key.ok) {
283954
284073
  console.error(import_picocolors15.default.red(` ${key.reason}`));
@@ -283965,6 +284084,35 @@ async function dev(argv, runtime = {}) {
283965
284084
  ` ${import_picocolors15.default.dim("key")} temporary \xB7 expires ${runtime.branch.expiresAt}`
283966
284085
  );
283967
284086
  }
284087
+ let localAbort = null;
284088
+ if (args.local) {
284089
+ process.env.ABLO_API_KEY = args.apiKey;
284090
+ if (!process.env.DATABASE_URL) {
284091
+ const databaseUrl = readProjectEnvVariable("DATABASE_URL", process.cwd(), false);
284092
+ if (databaseUrl) process.env.DATABASE_URL = databaseUrl.value;
284093
+ }
284094
+ const handler = await loadLocalSourceHandler(args.sourcePath);
284095
+ await registerLocalSource(args);
284096
+ localAbort = new AbortController();
284097
+ const connector = (0, import_source3.createSourceConnector)({
284098
+ apiKey: args.apiKey,
284099
+ handler,
284100
+ baseURL: args.url,
284101
+ client: "ablo-dev",
284102
+ onStatus(status2) {
284103
+ if (status2 === "ready") {
284104
+ console.log(` ${import_picocolors15.default.green("\u2713")} local Postgres connected through the secure reverse channel`);
284105
+ }
284106
+ },
284107
+ onError(error) {
284108
+ console.error(import_picocolors15.default.yellow(` local connector: ${error instanceof Error ? error.message : String(error)}`));
284109
+ }
284110
+ });
284111
+ void connector.run(localAbort.signal).catch((error) => {
284112
+ console.error(import_picocolors15.default.red(` local connector stopped: ${error instanceof Error ? error.message : String(error)}`));
284113
+ });
284114
+ console.log(` ${import_picocolors15.default.dim("source")} ${args.sourcePath} ${import_picocolors15.default.dim("(outbound connector; no public URL)")}`);
284115
+ }
283968
284116
  const schema = await loadSchema(args.schemaPath, args.exportName);
283969
284117
  const modelCount = Object.keys(schema.models).length;
283970
284118
  console.log(
@@ -283979,7 +284127,10 @@ async function dev(argv, runtime = {}) {
283979
284127
  s.start("Pushing schema definition (development branch)");
283980
284128
  const first = await runPush(schema, args);
283981
284129
  s.stop(first.message, first.ok ? 0 : 1);
283982
- if (!first.ok) process.exit(1);
284130
+ if (!first.ok) {
284131
+ localAbort?.abort();
284132
+ process.exit(1);
284133
+ }
283983
284134
  if (runtime.branch) {
283984
284135
  console.log(
283985
284136
  `
@@ -284033,6 +284184,7 @@ async function dev(argv, runtime = {}) {
284033
284184
  }
284034
284185
  const stop = () => {
284035
284186
  watcher.close();
284187
+ localAbort?.abort();
284036
284188
  console.log(`
284037
284189
  ${import_picocolors15.default.dim("stopped.")}`);
284038
284190
  process.exit(0);
@@ -284048,6 +284200,7 @@ init_controlPlane();
284048
284200
  var BRANCH_DEV_USAGE = `Usage:
284049
284201
  ablo dev [--branch <slug>] [--branch-ttl-hours <1-168>]
284050
284202
  [--schema <path>] [--export <name>] [--url <url>]
284203
+ [--local] [--source <path>]
284051
284204
  ablo dev --no-watch [branch options]
284052
284205
 
284053
284206
  By default, dev discovers the Git/CI branch, ensures its isolated Ablo branch,
@@ -284436,7 +284589,7 @@ var COMMANDS = [
284436
284589
  { run: "connect check", does: "Confirm your database is ready to share changes with Ablo" },
284437
284590
  { run: "connect resnapshot", does: "Reload existing rows without deregistering or rotating credentials" },
284438
284591
  { 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" },
284592
+ { run: "connect locate", does: "See which branch is connected to a database before connecting it" },
284440
284593
  { run: "connect deregister", does: "Disconnect this project's database \u2014 Ablo stops reading and writing it" }
284441
284594
  ]
284442
284595
  }
@@ -285180,11 +285333,11 @@ async function status(args = []) {
285180
285333
  }
285181
285334
  } else if (dataSource.kind === "none") {
285182
285335
  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")}`
285336
+ ` ${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
285337
  );
285185
285338
  } else if (reachable) {
285186
285339
  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})`)}`
285340
+ ` ${import_picocolors19.default.dim("data")} ${import_picocolors19.default.yellow("?")} ${import_picocolors19.default.dim(`could not read this branch's database connection (${dataSource.detail})`)}`
285188
285341
  );
285189
285342
  }
285190
285343
  const pushed = reachable ? await fetchPushedSchema(apiUrl3, introspectKey) : null;
@@ -285230,7 +285383,7 @@ async function status(args = []) {
285230
285383
  }
285231
285384
  } else if (dataSource.kind === "unknown") {
285232
285385
  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")}`
285386
+ ` ${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
285387
  );
285235
285388
  } else {
285236
285389
  console.log(
@@ -285337,7 +285490,7 @@ async function doctor() {
285337
285490
  checks.push({
285338
285491
  label: "data",
285339
285492
  state: "fail",
285340
- detail: "no database connected to this plane \u2014 writes are held",
285493
+ detail: "this branch is not connected to a database \u2014 writes are held",
285341
285494
  fix: "connect one with `ablo connect apply`"
285342
285495
  });
285343
285496
  } else {
@@ -285929,7 +286082,7 @@ async function reportReadSubject(dbUrl) {
285929
286082
  }
285930
286083
  if (state.kind === "none") {
285931
286084
  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`
286085
+ ` ${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
286086
  );
285934
286087
  console.log(
285935
286088
  ` ${import_picocolors23.default.dim(`Connect it with ${import_picocolors23.default.bold("ablo connect apply")}. Until then a table here is invisible to the engine.`)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/cli",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "description": "The ablo command line: set up Ablo, connect your database, and push your schema from the terminal.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -30,10 +30,10 @@
30
30
  "directory": "packages/cli"
31
31
  },
32
32
  "dependencies": {
33
- "@abloatai/transaction": "^0.47.0",
33
+ "@abloatai/transaction": "^0.48.0",
34
34
  "jiti": "^2.7.0",
35
35
  "zod": "^4.4.3",
36
- "@abloatai/humans": "^0.47.0"
36
+ "@abloatai/humans": "^0.48.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@clack/prompts": "^0.11.0",