@abloatai/cli 0.46.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.
- package/dist/cli.cjs +481 -122
- package/package.json +3 -3
package/dist/cli.cjs
CHANGED
|
@@ -3755,7 +3755,8 @@ async function confirmFromServer(opts) {
|
|
|
3755
3755
|
}
|
|
3756
3756
|
}
|
|
3757
3757
|
async function nameProject(projectId, organizationId, opts) {
|
|
3758
|
-
const
|
|
3758
|
+
const namingKey = resolveOrgManagementKey() ?? opts.apiKey;
|
|
3759
|
+
const listed = await listProjects(namingKey, opts.url);
|
|
3759
3760
|
const match = listed.ok ? listed.projects.find((p2) => p2.id === projectId) : void 0;
|
|
3760
3761
|
if (match) {
|
|
3761
3762
|
return { id: match.id, slug: match.slug, name: match.name, isDefault: match.default };
|
|
@@ -4098,6 +4099,12 @@ async function loadSchema(schemaPath, exportName) {
|
|
|
4098
4099
|
function maskKey(key) {
|
|
4099
4100
|
return key ? `${key.slice(0, 12)}\u2026` : "(none)";
|
|
4100
4101
|
}
|
|
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;
|
|
4107
|
+
}
|
|
4101
4108
|
function schemaGitState(schemaPath) {
|
|
4102
4109
|
try {
|
|
4103
4110
|
const out = (0, import_child_process.execFileSync)("git", ["status", "--porcelain", "--", schemaPath], {
|
|
@@ -4406,7 +4413,10 @@ async function push(argv) {
|
|
|
4406
4413
|
const serverMsg = body.message ?? body.reason;
|
|
4407
4414
|
console.error(import_picocolors5.default.red(` Forbidden${code ? ` [${code}]` : ""}: ${serverMsg ?? "permission denied"}`));
|
|
4408
4415
|
console.error(import_picocolors5.default.dim(` Push used ${import_picocolors5.default.bold(maskKey(args.apiKey))} from ${describeKeySource(keySource)}.`));
|
|
4409
|
-
|
|
4416
|
+
const storageHint = schemaPushStorageHint(code);
|
|
4417
|
+
if (storageHint) {
|
|
4418
|
+
console.error(import_picocolors5.default.dim(` ${storageHint}`));
|
|
4419
|
+
} else if (code === "database_role_cannot_enforce_rls") {
|
|
4410
4420
|
console.error(
|
|
4411
4421
|
import_picocolors5.default.dim(
|
|
4412
4422
|
` Your database role bypasses row-level security. Run ${import_picocolors5.default.bold("npx ablo migrate")} to create a scoped (NOBYPASSRLS) role and repoint DATABASE_URL, then re-push.`
|
|
@@ -4566,11 +4576,13 @@ var init_remoteValidation = __esm({
|
|
|
4566
4576
|
wal_level: () => `your database isn't set up to share changes as they happen yet`,
|
|
4567
4577
|
publication: () => `none of your tables are shared with Ablo yet`,
|
|
4568
4578
|
replication_role: () => `the login Ablo reads with can't follow your changes yet`,
|
|
4579
|
+
replication_slot_capacity: (f) => withActual(`this database has no remaining change-stream capacity for this binding`, f.actual),
|
|
4569
4580
|
replica_identity: (f) => withActual(
|
|
4570
4581
|
`some shared tables don't record enough for Ablo to track edits and deletes`,
|
|
4571
4582
|
f.actual
|
|
4572
4583
|
),
|
|
4573
4584
|
table_select: (f) => withActual(`the login Ablo reads with can't read some shared tables`, f.actual),
|
|
4585
|
+
snapshot_row_security: (f) => withActual(`row-level security hides historical rows from Ablo's initial load`, f.actual),
|
|
4574
4586
|
write_role: () => `the login Ablo writes with isn't set up yet`,
|
|
4575
4587
|
row_security: () => `the writer login isn't set to honor your row-level security`,
|
|
4576
4588
|
database_privileges: () => `the writer login can still create things in your database`,
|
|
@@ -4670,7 +4682,7 @@ function quoteIdent(id) {
|
|
|
4670
4682
|
function quoteLiteral(value) {
|
|
4671
4683
|
return `'${value.replace(/'/g, "''")}'`;
|
|
4672
4684
|
}
|
|
4673
|
-
function scopedSequenceGrant(tables, writeRole) {
|
|
4685
|
+
function scopedSequenceGrant(tables, writeRole, schema) {
|
|
4674
4686
|
const names = tables.map(quoteLiteral).join(", ");
|
|
4675
4687
|
return `DO $$
|
|
4676
4688
|
DECLARE seq regclass;
|
|
@@ -4681,7 +4693,7 @@ BEGIN
|
|
|
4681
4693
|
JOIN pg_class s ON s.oid = d.objid AND s.relkind = 'S'
|
|
4682
4694
|
JOIN pg_class t ON t.oid = d.refobjid
|
|
4683
4695
|
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
4684
|
-
WHERE d.deptype IN ('a', 'i') AND n.nspname =
|
|
4696
|
+
WHERE d.deptype IN ('a', 'i') AND n.nspname = ${quoteLiteral(schema)} AND t.relname IN (${names})
|
|
4685
4697
|
LOOP
|
|
4686
4698
|
EXECUTE format('GRANT USAGE, SELECT ON SEQUENCE %s TO ${quoteIdent(writeRole)}', seq);
|
|
4687
4699
|
END LOOP;
|
|
@@ -4691,24 +4703,29 @@ function connectSetupSql(input) {
|
|
|
4691
4703
|
const role = input.role && input.role.length > 0 ? input.role : import_footprint.ABLO_REPLICATION_ROLE;
|
|
4692
4704
|
const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : import_footprint.ABLO_WRITE_ROLE;
|
|
4693
4705
|
const tables = input.tables ?? [];
|
|
4694
|
-
const
|
|
4695
|
-
const
|
|
4706
|
+
const schema = input.schema ?? "public";
|
|
4707
|
+
const publication = input.publication;
|
|
4708
|
+
const qualifiedTables = tables.map((table) => `${quoteIdent(schema)}.${quoteIdent(table)}`);
|
|
4709
|
+
const publicationTarget = tables.length > 0 ? `FOR TABLE ${qualifiedTables.join(", ")}` : "FOR ALL TABLES";
|
|
4710
|
+
const tableList = qualifiedTables.join(", ");
|
|
4696
4711
|
const scoped = tables.length > 0;
|
|
4697
|
-
const applicationGrant = scoped ? `GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE ${tableList} TO ${quoteIdent(writeRole)};` : `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA
|
|
4712
|
+
const applicationGrant = scoped ? `GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE ${tableList} TO ${quoteIdent(writeRole)};` : `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ${quoteIdent(schema)} TO ${quoteIdent(writeRole)};`;
|
|
4698
4713
|
const replicationReadGrants = scoped ? [`GRANT SELECT ON TABLE ${tableList} TO ${quoteIdent(role)};`] : [
|
|
4699
|
-
`GRANT SELECT ON ALL TABLES IN SCHEMA
|
|
4700
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA
|
|
4714
|
+
`GRANT SELECT ON ALL TABLES IN SCHEMA ${quoteIdent(schema)} TO ${quoteIdent(role)};`,
|
|
4715
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA ${quoteIdent(schema)} GRANT SELECT ON TABLES TO ${quoteIdent(role)};`
|
|
4701
4716
|
];
|
|
4702
|
-
const writerSequenceGrants = scoped ? [scopedSequenceGrant(tables, writeRole)] : [`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA
|
|
4703
|
-
const ledger = (0, import_source2.idempotencyLedgerMigrations)().map((migration) => migration.up);
|
|
4717
|
+
const writerSequenceGrants = scoped ? [scopedSequenceGrant(tables, writeRole, schema)] : [`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${quoteIdent(schema)} TO ${quoteIdent(writeRole)};`];
|
|
4718
|
+
const ledger = (0, import_source2.idempotencyLedgerMigrations)(schema).map((migration) => migration.up);
|
|
4704
4719
|
return [
|
|
4705
4720
|
// 1. Turn on logical decoding. Requires a restart (it's not reloadable).
|
|
4706
4721
|
`ALTER SYSTEM SET wal_level = 'logical';`,
|
|
4707
4722
|
// 2. Publish the tables Ablo should read.
|
|
4708
|
-
`CREATE PUBLICATION ${quoteIdent(
|
|
4723
|
+
`CREATE PUBLICATION ${quoteIdent(publication)} ${publicationTarget};`,
|
|
4709
4724
|
// 3. A least-privilege role: it can stream replication and SELECT the
|
|
4710
|
-
// published tables,
|
|
4711
|
-
|
|
4725
|
+
// published tables, including the initial snapshot of RLS-protected tables.
|
|
4726
|
+
// Logical decoding already exposes every published row independently of
|
|
4727
|
+
// RLS; BYPASSRLS makes the ordinary SELECT snapshot match that same scope.
|
|
4728
|
+
`CREATE ROLE ${quoteIdent(role)} WITH NOSUPERUSER BYPASSRLS NOCREATEDB NOCREATEROLE REPLICATION NOINHERIT LOGIN PASSWORD '<password>';`,
|
|
4712
4729
|
...replicationReadGrants,
|
|
4713
4730
|
// 4. A distinct DML role: no replication, role administration, ownership,
|
|
4714
4731
|
// schema creation, or DDL. It runs NOBYPASSRLS with row_security on, so on a
|
|
@@ -4729,20 +4746,20 @@ function connectSetupSql(input) {
|
|
|
4729
4746
|
DO $$ BEGIN
|
|
4730
4747
|
EXECUTE format('REVOKE TEMPORARY, CREATE ON DATABASE %I FROM PUBLIC', current_database());
|
|
4731
4748
|
END $$;`,
|
|
4732
|
-
`GRANT USAGE ON SCHEMA
|
|
4749
|
+
`GRANT USAGE ON SCHEMA ${quoteIdent(schema)} TO ${quoteIdent(writeRole)};`,
|
|
4733
4750
|
applicationGrant,
|
|
4734
4751
|
...writerSequenceGrants,
|
|
4735
4752
|
...scoped ? [] : [
|
|
4736
4753
|
// "All tables" mode only: keep future tables/sequences writable so the
|
|
4737
4754
|
// publication doesn't outgrow the grant.
|
|
4738
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA
|
|
4739
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA
|
|
4755
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA ${quoteIdent(schema)} GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${quoteIdent(writeRole)};`,
|
|
4756
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA ${quoteIdent(schema)} GRANT USAGE, SELECT ON SEQUENCES TO ${quoteIdent(writeRole)};`
|
|
4740
4757
|
],
|
|
4741
4758
|
// 5. Direct uses the durable replay ledger but deliberately no outbox.
|
|
4742
4759
|
...ledger,
|
|
4743
|
-
`REVOKE ALL ON TABLE
|
|
4744
|
-
`GRANT SELECT, INSERT, UPDATE ON TABLE
|
|
4745
|
-
`REVOKE DELETE ON TABLE
|
|
4760
|
+
`REVOKE ALL ON TABLE ${quoteIdent(schema)}.${quoteIdent("ablo_idempotency")} FROM PUBLIC;`,
|
|
4761
|
+
`GRANT SELECT, INSERT, UPDATE ON TABLE ${quoteIdent(schema)}.${quoteIdent("ablo_idempotency")} TO ${quoteIdent(writeRole)};`,
|
|
4762
|
+
`REVOKE DELETE ON TABLE ${quoteIdent(schema)}.${quoteIdent("ablo_idempotency")} FROM ${quoteIdent(writeRole)};`,
|
|
4746
4763
|
// The writer emits a transactional marker on your WAL so Ablo can correlate
|
|
4747
4764
|
// the committed row back to the originating write and confirm it — this
|
|
4748
4765
|
// EXECUTE grant is what makes that confirmation possible. Granted by lookup
|
|
@@ -4765,10 +4782,12 @@ BEGIN
|
|
|
4765
4782
|
END $$;`
|
|
4766
4783
|
];
|
|
4767
4784
|
}
|
|
4768
|
-
function reconcilePublicationPlan(current, desiredTables) {
|
|
4769
|
-
const pub = quoteIdent(
|
|
4785
|
+
function reconcilePublicationPlan(current, desiredTables, opts) {
|
|
4786
|
+
const pub = quoteIdent(opts.publication);
|
|
4787
|
+
const schema = opts.schema ?? "public";
|
|
4788
|
+
const qualified = (table) => `${quoteIdent(schema)}.${quoteIdent(table)}`;
|
|
4770
4789
|
const desiredAll = desiredTables.length === 0;
|
|
4771
|
-
const target = desiredAll ? "FOR ALL TABLES" : `FOR TABLE ${desiredTables.map(
|
|
4790
|
+
const target = desiredAll ? "FOR ALL TABLES" : `FOR TABLE ${desiredTables.map(qualified).join(", ")}`;
|
|
4772
4791
|
if (!current.exists) {
|
|
4773
4792
|
return {
|
|
4774
4793
|
sql: [`CREATE PUBLICATION ${pub} ${target};`],
|
|
@@ -4794,28 +4813,31 @@ function reconcilePublicationPlan(current, desiredTables) {
|
|
|
4794
4813
|
return { sql: [], added: [], removed: [], recreated: false };
|
|
4795
4814
|
}
|
|
4796
4815
|
return {
|
|
4797
|
-
sql: [`ALTER PUBLICATION ${pub} SET TABLE ${desiredTables.map(
|
|
4816
|
+
sql: [`ALTER PUBLICATION ${pub} SET TABLE ${desiredTables.map(qualified).join(", ")};`],
|
|
4798
4817
|
added,
|
|
4799
4818
|
removed,
|
|
4800
4819
|
recreated: false
|
|
4801
4820
|
};
|
|
4802
4821
|
}
|
|
4803
|
-
async function readPublicationState(sql) {
|
|
4822
|
+
async function readPublicationState(sql, opts) {
|
|
4823
|
+
const publication = opts.publication;
|
|
4824
|
+
const schema = opts.schema ?? "public";
|
|
4804
4825
|
const pubRows = await sql.unsafe(
|
|
4805
4826
|
`SELECT puballtables FROM pg_publication WHERE pubname = $1`,
|
|
4806
|
-
[
|
|
4827
|
+
[publication]
|
|
4807
4828
|
);
|
|
4808
4829
|
const pubRow = pubRows[0];
|
|
4809
4830
|
if (!pubRow) return { exists: false, allTables: false, tables: [] };
|
|
4810
4831
|
if (pubRow.puballtables) return { exists: true, allTables: true, tables: [] };
|
|
4811
4832
|
const tableRows = await sql.unsafe(
|
|
4812
|
-
`SELECT tablename FROM pg_publication_tables WHERE pubname = $1 AND schemaname =
|
|
4813
|
-
[
|
|
4833
|
+
`SELECT tablename FROM pg_publication_tables WHERE pubname = $1 AND schemaname = $2 ORDER BY tablename`,
|
|
4834
|
+
[publication, schema]
|
|
4814
4835
|
);
|
|
4815
4836
|
return { exists: true, allTables: false, tables: tableRows.map((r2) => r2.tablename) };
|
|
4816
4837
|
}
|
|
4817
|
-
async function probeReadiness(sql, opts
|
|
4818
|
-
const publication = opts.publication
|
|
4838
|
+
async function probeReadiness(sql, opts) {
|
|
4839
|
+
const publication = opts.publication;
|
|
4840
|
+
const schema = opts.schema ?? "public";
|
|
4819
4841
|
const coordinated = opts.coordinatedTables && opts.coordinatedTables.length > 0 ? new Set(opts.coordinatedTables) : null;
|
|
4820
4842
|
const items = [];
|
|
4821
4843
|
const walRows = await sql.unsafe(
|
|
@@ -4847,7 +4869,7 @@ On Neon enable Logical Replication in the project (Console \u2192 Settings \u219
|
|
|
4847
4869
|
}
|
|
4848
4870
|
);
|
|
4849
4871
|
const roleRows = await sql.unsafe(
|
|
4850
|
-
`SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
|
|
4872
|
+
`SELECT rolreplication, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user`
|
|
4851
4873
|
);
|
|
4852
4874
|
const role = roleRows[0];
|
|
4853
4875
|
const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
|
|
@@ -4863,12 +4885,31 @@ On RDS: GRANT rds_replication TO <your_role>;`
|
|
|
4863
4885
|
}
|
|
4864
4886
|
);
|
|
4865
4887
|
if (pubRows.length > 0) {
|
|
4888
|
+
const rlsRows = await sql.unsafe(
|
|
4889
|
+
`SELECT DISTINCT pt.tablename AS table_name
|
|
4890
|
+
FROM pg_publication_tables pt
|
|
4891
|
+
JOIN pg_namespace n ON n.nspname = pt.schemaname
|
|
4892
|
+
JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = pt.tablename
|
|
4893
|
+
WHERE pt.pubname = $1 AND pt.schemaname = $2
|
|
4894
|
+
AND row_security_active(c.oid)
|
|
4895
|
+
ORDER BY table_name`,
|
|
4896
|
+
[publication, schema]
|
|
4897
|
+
);
|
|
4898
|
+
const rlsRelevant = coordinated ? rlsRows.filter((row) => coordinated.has(row.table_name)) : rlsRows;
|
|
4899
|
+
items.push(
|
|
4900
|
+
rlsRelevant.length === 0 ? { ok: true, label: "the initial snapshot can read every published row" } : {
|
|
4901
|
+
ok: false,
|
|
4902
|
+
label: `${rlsRelevant.length} published table${rlsRelevant.length === 1 ? "" : "s"} hide historical rows behind RLS`,
|
|
4903
|
+
fix: `ALTER ROLE current_user WITH BYPASSRLS;
|
|
4904
|
+
Logical replication already exposes every published row; this lets the ordinary initial SELECT read that same scope.`
|
|
4905
|
+
}
|
|
4906
|
+
);
|
|
4866
4907
|
const badRows = await sql.unsafe(
|
|
4867
4908
|
`SELECT c.relname AS table_name, c.relreplident
|
|
4868
4909
|
FROM pg_publication_tables pt
|
|
4869
4910
|
JOIN pg_class c ON c.relname = pt.tablename
|
|
4870
4911
|
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
4871
|
-
WHERE pt.pubname = $1
|
|
4912
|
+
WHERE pt.pubname = $1 AND pt.schemaname = $2
|
|
4872
4913
|
AND (
|
|
4873
4914
|
c.relreplident = 'n'
|
|
4874
4915
|
OR (
|
|
@@ -4879,7 +4920,7 @@ On RDS: GRANT rds_replication TO <your_role>;`
|
|
|
4879
4920
|
)
|
|
4880
4921
|
)
|
|
4881
4922
|
)`,
|
|
4882
|
-
[publication]
|
|
4923
|
+
[publication, schema]
|
|
4883
4924
|
);
|
|
4884
4925
|
const relevant = coordinated ? badRows.filter((row) => coordinated.has(row.table_name)) : badRows;
|
|
4885
4926
|
items.push(
|
|
@@ -4887,7 +4928,7 @@ On RDS: GRANT rds_replication TO <your_role>;`
|
|
|
4887
4928
|
ok: false,
|
|
4888
4929
|
label: `${relevant.length} published table${relevant.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
|
|
4889
4930
|
fix: relevant.map(
|
|
4890
|
-
(r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
|
|
4931
|
+
(r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(schema)}.${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
|
|
4891
4932
|
).join("\n")
|
|
4892
4933
|
}
|
|
4893
4934
|
);
|
|
@@ -4904,7 +4945,10 @@ async function registerDirectDataSource(opts) {
|
|
|
4904
4945
|
connection: "direct",
|
|
4905
4946
|
connectionString: opts.replicationUrl,
|
|
4906
4947
|
writeConnectionString: opts.writeUrl,
|
|
4907
|
-
route: opts.route
|
|
4948
|
+
route: opts.route,
|
|
4949
|
+
...opts.schema ? { schema: opts.schema } : {},
|
|
4950
|
+
...opts.replicationSlot ? { replicationSlot: opts.replicationSlot } : {},
|
|
4951
|
+
...opts.publication ? { publication: opts.publication } : {}
|
|
4908
4952
|
},
|
|
4909
4953
|
responseSchema: import_wire3.datasourceSummarySchema
|
|
4910
4954
|
});
|
|
@@ -5218,26 +5262,27 @@ function ownershipRemediation(blockers2, admin) {
|
|
|
5218
5262
|
const unresolved = blockers2.filter((blocker) => !resolvedOwners.has(blocker.owner));
|
|
5219
5263
|
return { inheritGrants, unresolved };
|
|
5220
5264
|
}
|
|
5221
|
-
async function publishedTableBlockers(sql, tables) {
|
|
5265
|
+
async function publishedTableBlockers(sql, tables, schema = "public") {
|
|
5222
5266
|
const scoped = tables.length > 0;
|
|
5223
5267
|
const raw = await sql.unsafe(
|
|
5224
5268
|
`SELECT format('%I.%I', n.nspname, c.relname) AS relation, ${OWNERSHIP_COLUMNS}
|
|
5225
5269
|
${OWNERSHIP_FROM}
|
|
5226
5270
|
WHERE c.relkind = 'r'
|
|
5227
|
-
AND n.nspname =
|
|
5271
|
+
AND n.nspname = $${scoped ? "2" : "1"}
|
|
5228
5272
|
AND c.relname <> 'ablo_idempotency'
|
|
5229
5273
|
${scoped ? "AND c.relname = ANY($1)" : ""}`,
|
|
5230
|
-
scoped ? [tables] : []
|
|
5274
|
+
scoped ? [tables, schema] : [schema]
|
|
5231
5275
|
);
|
|
5232
5276
|
return ownershipBlockers(import_zod3.z.array(ownedRelationRowSchema).parse(raw));
|
|
5233
5277
|
}
|
|
5234
|
-
async function ledgerBlocker(sql) {
|
|
5278
|
+
async function ledgerBlocker(sql, schema = "public") {
|
|
5235
5279
|
const raw = await sql.unsafe(
|
|
5236
5280
|
`SELECT format('%I.%I', n.nspname, c.relname) AS relation, ${OWNERSHIP_COLUMNS}
|
|
5237
5281
|
${OWNERSHIP_FROM}
|
|
5238
5282
|
WHERE c.relkind = 'r'
|
|
5239
|
-
AND n.nspname =
|
|
5240
|
-
AND c.relname = 'ablo_idempotency'
|
|
5283
|
+
AND n.nspname = $1
|
|
5284
|
+
AND c.relname = 'ablo_idempotency'`,
|
|
5285
|
+
[schema]
|
|
5241
5286
|
);
|
|
5242
5287
|
const rows = import_zod3.z.array(ownedRelationRowSchema).parse(raw);
|
|
5243
5288
|
const row = rows[0];
|
|
@@ -5422,7 +5467,7 @@ function blockers(input) {
|
|
|
5422
5467
|
}
|
|
5423
5468
|
if (input.dataSource.kind === "none") {
|
|
5424
5469
|
found.push({
|
|
5425
|
-
problem: "
|
|
5470
|
+
problem: "this branch is not connected to a database, so writes are held",
|
|
5426
5471
|
fix: "connect one with `ablo connect apply`"
|
|
5427
5472
|
});
|
|
5428
5473
|
}
|
|
@@ -5473,13 +5518,15 @@ function connectApplyPlan(input) {
|
|
|
5473
5518
|
const role = input.role && input.role.length > 0 ? input.role : import_footprint.ABLO_REPLICATION_ROLE;
|
|
5474
5519
|
const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : import_footprint.ABLO_WRITE_ROLE;
|
|
5475
5520
|
const tables = input.tables ?? [];
|
|
5521
|
+
const schema = input.schema ?? "public";
|
|
5522
|
+
const publication = input.publication;
|
|
5476
5523
|
const provider = input.provider ?? "generic";
|
|
5477
|
-
const recipe = connectSetupSql({ tables, role, writeRole });
|
|
5524
|
+
const recipe = connectSetupSql({ tables, role, writeRole, schema, publication });
|
|
5478
5525
|
const isWal = (s) => s.startsWith("ALTER SYSTEM SET wal_level");
|
|
5479
5526
|
const isPublication = (s) => s.startsWith("CREATE PUBLICATION");
|
|
5480
5527
|
const isRoleCreate = (s) => s.startsWith("CREATE ROLE ");
|
|
5481
5528
|
const grants = recipe.filter((s) => !isWal(s) && !isPublication(s) && !isRoleCreate(s));
|
|
5482
|
-
const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
|
|
5529
|
+
const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map((table) => `${quoteIdent(schema)}.${quoteIdent(table)}`).join(", ")}` : "FOR ALL TABLES";
|
|
5483
5530
|
const affectsOthers = effectsOnOthers({ grants, allTables: tables.length === 0 });
|
|
5484
5531
|
const walStep = input.walAlreadyLogical ? [] : [
|
|
5485
5532
|
provider === "generic" ? {
|
|
@@ -5494,10 +5541,10 @@ function connectApplyPlan(input) {
|
|
|
5494
5541
|
sql: []
|
|
5495
5542
|
}
|
|
5496
5543
|
];
|
|
5497
|
-
const reconcile2 = input.existingPublication ? reconcilePublicationPlan(input.existingPublication, tables) : null;
|
|
5544
|
+
const reconcile2 = input.existingPublication ? reconcilePublicationPlan(input.existingPublication, tables, { schema, publication }) : null;
|
|
5498
5545
|
const publicationSql = reconcile2 ? reconcile2.sql : [
|
|
5499
5546
|
`DO $$ BEGIN
|
|
5500
|
-
CREATE PUBLICATION ${quoteIdent(
|
|
5547
|
+
CREATE PUBLICATION ${quoteIdent(publication)} ${publicationTarget};
|
|
5501
5548
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
5502
5549
|
END $$;`
|
|
5503
5550
|
];
|
|
@@ -5522,14 +5569,19 @@ END $$;`
|
|
|
5522
5569
|
{
|
|
5523
5570
|
key: "replication-role",
|
|
5524
5571
|
title: "Create the read-only login Ablo reads with",
|
|
5525
|
-
detail: `${role} \u2014 it can follow your changes and
|
|
5572
|
+
detail: `${role} \u2014 it can follow your changes and snapshot the same published rows, including through RLS`,
|
|
5526
5573
|
sql: [
|
|
5527
5574
|
idempotentRole(
|
|
5528
5575
|
role,
|
|
5529
|
-
"REPLICATION",
|
|
5576
|
+
"NOSUPERUSER BYPASSRLS NOCREATEDB NOCREATEROLE REPLICATION NOINHERIT",
|
|
5530
5577
|
input.credentials.replicationClause,
|
|
5531
5578
|
input.rotate === true
|
|
5532
|
-
)
|
|
5579
|
+
),
|
|
5580
|
+
// Unlike a password, this is a required invariant rather than secret
|
|
5581
|
+
// material owned by one plane. Re-assert it for an existing pre-snapshot
|
|
5582
|
+
// role so `connect apply` repairs the exact upgrade gap that otherwise
|
|
5583
|
+
// certifies an RLS-filtered empty snapshot as complete.
|
|
5584
|
+
`ALTER ROLE ${quoteIdent(role)} WITH BYPASSRLS;`
|
|
5533
5585
|
]
|
|
5534
5586
|
},
|
|
5535
5587
|
{
|
|
@@ -5683,7 +5735,7 @@ function rotateWithoutConnection(input) {
|
|
|
5683
5735
|
}
|
|
5684
5736
|
if (!input.known || input.planeHasConnection) return null;
|
|
5685
5737
|
if (input.existingRoles.length > 0) return null;
|
|
5686
|
-
return "This
|
|
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.";
|
|
5687
5739
|
}
|
|
5688
5740
|
async function locateExistingConnection(input) {
|
|
5689
5741
|
const result = await tryControlPlane({
|
|
@@ -5691,16 +5743,23 @@ async function locateExistingConnection(input) {
|
|
|
5691
5743
|
method: "POST",
|
|
5692
5744
|
baseUrl: input.apiUrl,
|
|
5693
5745
|
apiKey: input.apiKey,
|
|
5694
|
-
body: {
|
|
5746
|
+
body: {
|
|
5747
|
+
connectionString: input.connectionString,
|
|
5748
|
+
...input.schema ? { schema: input.schema } : {}
|
|
5749
|
+
},
|
|
5695
5750
|
responseSchema: import_wire6.datasourceLocationResponseSchema
|
|
5696
5751
|
});
|
|
5697
5752
|
if (!result.ok) return null;
|
|
5698
|
-
return result.value.held;
|
|
5753
|
+
if (result.value.held) return result.value.held;
|
|
5754
|
+
return result.value.available === false ? { private: true } : null;
|
|
5699
5755
|
}
|
|
5700
5756
|
function alreadyConnectedElsewhere(held) {
|
|
5701
5757
|
if (!held) return null;
|
|
5758
|
+
if ("private" in held) {
|
|
5759
|
+
return "This database schema is already connected to another Ablo organization. Ablo does not reveal that organization\u2019s project or branch.";
|
|
5760
|
+
}
|
|
5702
5761
|
const where = held.project ? `project ${held.project}, branch ${held.branch}` : `branch ${held.branch}`;
|
|
5703
|
-
return `This database is already connected to ${where}.
|
|
5762
|
+
return `This database schema is already connected to ${where}. A (database, schema) binding belongs to one plane at a time.`;
|
|
5704
5763
|
}
|
|
5705
5764
|
var import_wire6, import_schema5;
|
|
5706
5765
|
var init_connectPreflight = __esm({
|
|
@@ -5849,12 +5908,18 @@ ${ambient}` : ""}`,
|
|
|
5849
5908
|
)
|
|
5850
5909
|
);
|
|
5851
5910
|
}
|
|
5852
|
-
const tables = args.tables;
|
|
5853
5911
|
const coordinatedTables = await schemaDeclaredTables() ?? [];
|
|
5912
|
+
const tables = args.tables.length > 0 ? args.tables : coordinatedTables;
|
|
5913
|
+
if (tables.length === 0) {
|
|
5914
|
+
throw new import_errors9.AbloValidationError(
|
|
5915
|
+
`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.`,
|
|
5916
|
+
{ code: "cli_invalid_arguments" }
|
|
5917
|
+
);
|
|
5918
|
+
}
|
|
5854
5919
|
if (args.tables.length === 0) {
|
|
5855
5920
|
console.log(
|
|
5856
5921
|
import_picocolors11.default.dim(
|
|
5857
|
-
` publishing
|
|
5922
|
+
` 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)
|
|
5858
5923
|
`
|
|
5859
5924
|
)
|
|
5860
5925
|
);
|
|
@@ -5869,8 +5934,28 @@ ${ambient}` : ""}`,
|
|
|
5869
5934
|
console.log(` ${import_picocolors11.default.yellow("!")} ${mismatch}
|
|
5870
5935
|
`);
|
|
5871
5936
|
}
|
|
5937
|
+
const confirmed = connectTarget?.confirmed;
|
|
5938
|
+
if (!confirmed?.branchId) {
|
|
5939
|
+
throw new import_errors9.AbloConnectionError(
|
|
5940
|
+
"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.",
|
|
5941
|
+
{ code: "cli_database_unreachable" }
|
|
5942
|
+
);
|
|
5943
|
+
}
|
|
5944
|
+
const footprint = (0, import_footprint2.footprintNamesFor)({
|
|
5945
|
+
organizationId: confirmed.organizationId,
|
|
5946
|
+
branchId: confirmed.branchId,
|
|
5947
|
+
...confirmed.projectId ? { projectId: confirmed.projectId } : {}
|
|
5948
|
+
});
|
|
5949
|
+
const role = args.role === import_footprint.ABLO_REPLICATION_ROLE ? footprint.replicationRole : args.role;
|
|
5950
|
+
const writeRole = args.writeRole === import_footprint.ABLO_WRITE_ROLE ? footprint.writeRole : args.writeRole;
|
|
5951
|
+
const publication = footprint.publication;
|
|
5872
5952
|
const heldElsewhere = alreadyConnectedElsewhere(
|
|
5873
|
-
await locateExistingConnection({
|
|
5953
|
+
await locateExistingConnection({
|
|
5954
|
+
apiUrl: apiBaseUrl(),
|
|
5955
|
+
apiKey,
|
|
5956
|
+
connectionString: adminUrl,
|
|
5957
|
+
schema: args.schema
|
|
5958
|
+
})
|
|
5874
5959
|
);
|
|
5875
5960
|
if (heldElsewhere) {
|
|
5876
5961
|
console.error(` ${import_picocolors11.default.yellow("!")} ${heldElsewhere}
|
|
@@ -5930,8 +6015,8 @@ ${ambient}` : ""}`,
|
|
|
5930
6015
|
);
|
|
5931
6016
|
process.exit(1);
|
|
5932
6017
|
}
|
|
5933
|
-
const ledger = await ledgerBlocker(admin).catch(() => null);
|
|
5934
|
-
const foreignTables = await publishedTableBlockers(admin, tables).catch(() => []);
|
|
6018
|
+
const ledger = await ledgerBlocker(admin, args.schema).catch(() => null);
|
|
6019
|
+
const foreignTables = await publishedTableBlockers(admin, tables, args.schema).catch(() => []);
|
|
5935
6020
|
const { inheritGrants, unresolved } = ownershipRemediation(
|
|
5936
6021
|
[...ledger ? [ledger] : [], ...foreignTables],
|
|
5937
6022
|
capability.rolname
|
|
@@ -5955,12 +6040,16 @@ ${ambient}` : ""}`,
|
|
|
5955
6040
|
);
|
|
5956
6041
|
process.exit(1);
|
|
5957
6042
|
}
|
|
5958
|
-
const existingPublication = await readPublicationState(admin
|
|
6043
|
+
const existingPublication = await readPublicationState(admin, {
|
|
6044
|
+
schema: args.schema,
|
|
6045
|
+
publication
|
|
6046
|
+
}).catch(
|
|
5959
6047
|
() => ({ exists: false, allTables: false, tables: [] })
|
|
5960
6048
|
);
|
|
5961
|
-
const pubReconcile = reconcilePublicationPlan(existingPublication, tables
|
|
5962
|
-
|
|
5963
|
-
|
|
6049
|
+
const pubReconcile = reconcilePublicationPlan(existingPublication, tables, {
|
|
6050
|
+
schema: args.schema,
|
|
6051
|
+
publication
|
|
6052
|
+
});
|
|
5964
6053
|
const existingRoles = await presentRoles(admin, [role, writeRole]).catch(() => []);
|
|
5965
6054
|
if (rotatePlane) {
|
|
5966
6055
|
const refusal = rotateWithoutConnection({ rotating, ...rotatePlane, existingRoles });
|
|
@@ -6001,8 +6090,10 @@ ${ambient}` : ""}`,
|
|
|
6001
6090
|
const writePassword = generateRolePassword();
|
|
6002
6091
|
const buildPlan = (mode) => connectApplyPlan({
|
|
6003
6092
|
tables,
|
|
6004
|
-
role
|
|
6005
|
-
writeRole
|
|
6093
|
+
role,
|
|
6094
|
+
writeRole,
|
|
6095
|
+
schema: args.schema,
|
|
6096
|
+
publication,
|
|
6006
6097
|
rotate: rotating,
|
|
6007
6098
|
credentials: {
|
|
6008
6099
|
replicationClause: passwordClause(replicationPassword, mode),
|
|
@@ -6016,7 +6107,7 @@ ${ambient}` : ""}`,
|
|
|
6016
6107
|
const steps = buildPlan("scram-verifier");
|
|
6017
6108
|
if (pubReconcile.removed.length > 0 || pubReconcile.recreated) {
|
|
6018
6109
|
console.log(
|
|
6019
|
-
` ${import_picocolors11.default.yellow("!")} ${import_picocolors11.default.bold(
|
|
6110
|
+
` ${import_picocolors11.default.yellow("!")} ${import_picocolors11.default.bold(publication)} already publishes a different set; reconciling to your mapped tables:`
|
|
6020
6111
|
);
|
|
6021
6112
|
for (const t of pubReconcile.added) console.log(` ${import_picocolors11.default.green("+")} ${t}`);
|
|
6022
6113
|
for (const t of pubReconcile.removed)
|
|
@@ -6093,9 +6184,12 @@ ${ambient}` : ""}`,
|
|
|
6093
6184
|
}
|
|
6094
6185
|
const replicationProbe = await probeAsRole(
|
|
6095
6186
|
replicationUrl,
|
|
6096
|
-
(sql) => probeReadiness(sql, { coordinatedTables })
|
|
6187
|
+
(sql) => probeReadiness(sql, { coordinatedTables, schema: args.schema, publication })
|
|
6188
|
+
);
|
|
6189
|
+
const writeProbe = await probeAsRole(
|
|
6190
|
+
writeUrl,
|
|
6191
|
+
(sql) => probeDirectWriteReadiness(sql, { schema: args.schema, publication })
|
|
6097
6192
|
);
|
|
6098
|
-
const writeProbe = await probeAsRole(writeUrl, probeDirectWriteReadiness);
|
|
6099
6193
|
const refused = [
|
|
6100
6194
|
...replicationProbe.credentialRefused ? [role] : [],
|
|
6101
6195
|
...writeProbe.credentialRefused ? [writeRole] : []
|
|
@@ -6150,7 +6244,10 @@ ${ambient}` : ""}`,
|
|
|
6150
6244
|
apiKey,
|
|
6151
6245
|
replicationUrl,
|
|
6152
6246
|
writeUrl,
|
|
6153
|
-
route: args.route
|
|
6247
|
+
route: args.route,
|
|
6248
|
+
schema: args.schema,
|
|
6249
|
+
replicationSlot: footprint.slot,
|
|
6250
|
+
publication
|
|
6154
6251
|
});
|
|
6155
6252
|
process.off("SIGINT", onRotateInterrupt);
|
|
6156
6253
|
process.off("SIGTERM", onRotateInterrupt);
|
|
@@ -6162,7 +6259,7 @@ ${ambient}` : ""}`,
|
|
|
6162
6259
|
}
|
|
6163
6260
|
process.exit(outcome.exitCode);
|
|
6164
6261
|
}
|
|
6165
|
-
var import_picocolors11, import_errors9, ROTATE_STRANDED_CREDENTIALS_NOTICE;
|
|
6262
|
+
var import_picocolors11, import_errors9, import_footprint2, ROTATE_STRANDED_CREDENTIALS_NOTICE;
|
|
6166
6263
|
var init_connectApply = __esm({
|
|
6167
6264
|
"src/connectApply.ts"() {
|
|
6168
6265
|
"use strict";
|
|
@@ -6171,6 +6268,7 @@ var init_connectApply = __esm({
|
|
|
6171
6268
|
init_src();
|
|
6172
6269
|
init_dist2();
|
|
6173
6270
|
import_errors9 = require("@abloatai/transaction/errors");
|
|
6271
|
+
import_footprint2 = require("@abloatai/transaction/footprint");
|
|
6174
6272
|
init_connectSetup();
|
|
6175
6273
|
init_connectOwnership();
|
|
6176
6274
|
init_connect();
|
|
@@ -6194,6 +6292,7 @@ function parseConnectArgs(argv) {
|
|
|
6194
6292
|
let register = false;
|
|
6195
6293
|
let apply = false;
|
|
6196
6294
|
let rotate = false;
|
|
6295
|
+
let resnapshot = false;
|
|
6197
6296
|
let url;
|
|
6198
6297
|
let envFile;
|
|
6199
6298
|
let yes = false;
|
|
@@ -6202,6 +6301,7 @@ function parseConnectArgs(argv) {
|
|
|
6202
6301
|
let locate = false;
|
|
6203
6302
|
let manual = false;
|
|
6204
6303
|
let tables = [];
|
|
6304
|
+
let schema = "public";
|
|
6205
6305
|
let role = import_footprint.ABLO_REPLICATION_ROLE;
|
|
6206
6306
|
let writeRole = import_footprint.ABLO_WRITE_ROLE;
|
|
6207
6307
|
let route = "public-allowlist";
|
|
@@ -6221,6 +6321,9 @@ function parseConnectArgs(argv) {
|
|
|
6221
6321
|
case "rotate":
|
|
6222
6322
|
rotate = true;
|
|
6223
6323
|
break;
|
|
6324
|
+
case "resnapshot":
|
|
6325
|
+
resnapshot = true;
|
|
6326
|
+
break;
|
|
6224
6327
|
case "scan":
|
|
6225
6328
|
scan = true;
|
|
6226
6329
|
break;
|
|
@@ -6229,7 +6332,7 @@ function parseConnectArgs(argv) {
|
|
|
6229
6332
|
break;
|
|
6230
6333
|
default:
|
|
6231
6334
|
throw new import_errors10.AbloValidationError(
|
|
6232
|
-
`unknown connect subcommand: ${lead} (expected register, deregister, check, apply, rotate, scan, locate)`,
|
|
6335
|
+
`unknown connect subcommand: ${lead} (expected register, deregister, check, apply, rotate, resnapshot, scan, locate)`,
|
|
6233
6336
|
{ code: "cli_invalid_arguments" }
|
|
6234
6337
|
);
|
|
6235
6338
|
}
|
|
@@ -6259,6 +6362,9 @@ function parseConnectArgs(argv) {
|
|
|
6259
6362
|
tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
6260
6363
|
break;
|
|
6261
6364
|
}
|
|
6365
|
+
case "--schema":
|
|
6366
|
+
schema = argv[++i] ?? schema;
|
|
6367
|
+
break;
|
|
6262
6368
|
case "--role":
|
|
6263
6369
|
role = argv[++i] ?? role;
|
|
6264
6370
|
break;
|
|
@@ -6290,6 +6396,7 @@ function parseConnectArgs(argv) {
|
|
|
6290
6396
|
register,
|
|
6291
6397
|
apply,
|
|
6292
6398
|
rotate,
|
|
6399
|
+
resnapshot,
|
|
6293
6400
|
url,
|
|
6294
6401
|
envFile,
|
|
6295
6402
|
yes,
|
|
@@ -6297,17 +6404,22 @@ function parseConnectArgs(argv) {
|
|
|
6297
6404
|
scan,
|
|
6298
6405
|
locate,
|
|
6299
6406
|
tables,
|
|
6407
|
+
schema,
|
|
6300
6408
|
role,
|
|
6301
6409
|
writeRole,
|
|
6302
6410
|
route,
|
|
6303
6411
|
manual
|
|
6304
6412
|
};
|
|
6305
6413
|
}
|
|
6306
|
-
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;
|
|
6307
6417
|
const sql = connectSetupSql({
|
|
6308
6418
|
tables: args.tables,
|
|
6309
|
-
role
|
|
6310
|
-
writeRole
|
|
6419
|
+
role,
|
|
6420
|
+
writeRole,
|
|
6421
|
+
schema: args.schema,
|
|
6422
|
+
publication: footprint.publication
|
|
6311
6423
|
});
|
|
6312
6424
|
console.log(
|
|
6313
6425
|
`
|
|
@@ -6358,7 +6470,7 @@ function printConnectRecipe(args) {
|
|
|
6358
6470
|
console.log(
|
|
6359
6471
|
import_picocolors12.default.dim(
|
|
6360
6472
|
` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
|
|
6361
|
-
${import_picocolors12.default.bold(`GRANT rds_replication TO ${quoteIdent(
|
|
6473
|
+
${import_picocolors12.default.bold(`GRANT rds_replication TO ${quoteIdent(role)};`)}`
|
|
6362
6474
|
)
|
|
6363
6475
|
);
|
|
6364
6476
|
console.log(
|
|
@@ -6382,8 +6494,8 @@ function printConnectRecipe(args) {
|
|
|
6382
6494
|
`
|
|
6383
6495
|
${import_picocolors12.default.bold("5.")} Register the two roles with Ablo. Set them just long enough to register \u2014
|
|
6384
6496
|
Ablo holds them from here, so your app keeps only ${import_picocolors12.default.bold("ABLO_API_KEY")}:
|
|
6385
|
-
${import_picocolors12.default.bold("ABLO_REPLICATION_DATABASE_URL")} ${import_picocolors12.default.dim(`\u2192 ${
|
|
6386
|
-
${import_picocolors12.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors12.default.dim(`\u2192 ${
|
|
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)`)}
|
|
6387
6499
|
${import_picocolors12.default.cyan("npx ablo connect register")}
|
|
6388
6500
|
`
|
|
6389
6501
|
);
|
|
@@ -6411,9 +6523,9 @@ function printCheckItem(item) {
|
|
|
6411
6523
|
}
|
|
6412
6524
|
}
|
|
6413
6525
|
}
|
|
6414
|
-
async function probeDirectWriteReadiness(sql, opts
|
|
6526
|
+
async function probeDirectWriteReadiness(sql, opts) {
|
|
6415
6527
|
const schema = opts.schema ?? "public";
|
|
6416
|
-
const publication = opts.publication
|
|
6528
|
+
const publication = opts.publication;
|
|
6417
6529
|
const items = [];
|
|
6418
6530
|
const roleRows = await sql.unsafe(
|
|
6419
6531
|
`SELECT rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole, rolreplication
|
|
@@ -6526,20 +6638,27 @@ async function probeDirectWriteReadiness(sql, opts = {}) {
|
|
|
6526
6638
|
);
|
|
6527
6639
|
return items;
|
|
6528
6640
|
}
|
|
6529
|
-
async function auditTenantSyncInfra(sql) {
|
|
6641
|
+
async function auditTenantSyncInfra(sql, opts = {}) {
|
|
6530
6642
|
const artifacts = [];
|
|
6531
|
-
|
|
6532
|
-
|
|
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
|
+
]);
|
|
6649
|
+
for (const artifact of import_footprint3.ABLO_FOOTPRINT) {
|
|
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;
|
|
6651
|
+
const key = artifact.kind === "table" || artifact.kind === "type" ? `${artifact.retired ? "public" : opts.schema ?? "public"}.${name}` : name;
|
|
6533
6652
|
const rows = await sql.unsafe(FOOTPRINT_LOOKUP[artifact.kind], [
|
|
6534
6653
|
key
|
|
6535
6654
|
]);
|
|
6536
6655
|
artifacts.push({
|
|
6537
6656
|
kind: artifact.kind,
|
|
6538
|
-
name
|
|
6657
|
+
name,
|
|
6539
6658
|
present: rows[0]?.present === true,
|
|
6540
6659
|
purpose: artifact.purpose,
|
|
6541
6660
|
...artifact.hazard ? { hazard: artifact.hazard } : {},
|
|
6542
|
-
...artifact.retired ? { retired: true } : {}
|
|
6661
|
+
...artifact.retired || !opts.names && branchScopedTemplates.has(artifact.name) ? { retired: true } : {}
|
|
6543
6662
|
});
|
|
6544
6663
|
}
|
|
6545
6664
|
return artifacts;
|
|
@@ -6564,12 +6683,12 @@ function requireScopedUrl(kind, verb) {
|
|
|
6564
6683
|
);
|
|
6565
6684
|
process.exit(1);
|
|
6566
6685
|
}
|
|
6567
|
-
async function probeAndReport(dbUrl, kind
|
|
6686
|
+
async function probeAndReport(dbUrl, kind, opts) {
|
|
6568
6687
|
const sql = src_default(dbUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
|
|
6569
6688
|
} });
|
|
6570
6689
|
let items;
|
|
6571
6690
|
try {
|
|
6572
|
-
items = kind === "replication" ? await probeReadiness(sql) : await probeDirectWriteReadiness(sql);
|
|
6691
|
+
items = kind === "replication" ? await probeReadiness(sql, opts) : await probeDirectWriteReadiness(sql, opts);
|
|
6573
6692
|
} catch (err) {
|
|
6574
6693
|
await sql.end({ timeout: 2 }).catch(() => void 0);
|
|
6575
6694
|
const dial = dialFailureReason(err);
|
|
@@ -6605,7 +6724,7 @@ ${ambient}` : ""}`,
|
|
|
6605
6724
|
if (!result.ok) {
|
|
6606
6725
|
if (result.code === "no_data_source_registered") {
|
|
6607
6726
|
console.error(
|
|
6608
|
-
` ${import_picocolors12.default.yellow("\u2014")}
|
|
6727
|
+
` ${import_picocolors12.default.yellow("\u2014")} This branch is not connected to a database yet, so there's nothing to check.
|
|
6609
6728
|
` + import_picocolors12.default.dim(
|
|
6610
6729
|
` Connect one with ${import_picocolors12.default.bold("ablo connect apply")}, then re-run ${import_picocolors12.default.bold("ablo connect check")}.
|
|
6611
6730
|
`
|
|
@@ -6697,13 +6816,32 @@ ${ambient}` : ""}`,
|
|
|
6697
6816
|
${brand("ablo")} ${import_picocolors12.default.dim("connect register")} ${import_picocolors12.default.dim("register a direct DataSource")}
|
|
6698
6817
|
`
|
|
6699
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;
|
|
6700
6832
|
console.log(` ${import_picocolors12.default.bold("Replication role")}
|
|
6701
6833
|
`);
|
|
6702
|
-
const replication = await probeAndReport(dbUrl, "replication"
|
|
6834
|
+
const replication = await probeAndReport(dbUrl, "replication", {
|
|
6835
|
+
schema: args.schema,
|
|
6836
|
+
publication
|
|
6837
|
+
});
|
|
6703
6838
|
console.log(`
|
|
6704
6839
|
${import_picocolors12.default.bold("Direct-write role")}
|
|
6705
6840
|
`);
|
|
6706
|
-
const write = await probeAndReport(writeDbUrl, "write"
|
|
6841
|
+
const write = await probeAndReport(writeDbUrl, "write", {
|
|
6842
|
+
schema: args.schema,
|
|
6843
|
+
publication
|
|
6844
|
+
});
|
|
6707
6845
|
const noDial = [
|
|
6708
6846
|
replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
|
|
6709
6847
|
write.kind === "no-dial" ? `write: ${write.reason}` : null
|
|
@@ -6731,17 +6869,35 @@ ${ambient}` : ""}`,
|
|
|
6731
6869
|
apiKey,
|
|
6732
6870
|
replicationUrl: dbUrl,
|
|
6733
6871
|
writeUrl: writeDbUrl,
|
|
6734
|
-
route: args.route
|
|
6872
|
+
route: args.route,
|
|
6873
|
+
schema: args.schema
|
|
6735
6874
|
});
|
|
6736
6875
|
process.exit(registered ? 0 : 1);
|
|
6737
6876
|
}
|
|
6738
|
-
async function runScan() {
|
|
6877
|
+
async function runScan(args) {
|
|
6739
6878
|
const dbUrl = requireScopedUrl("replication", "scan");
|
|
6740
6879
|
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
6741
6880
|
} });
|
|
6742
6881
|
let artifacts;
|
|
6743
6882
|
try {
|
|
6744
|
-
|
|
6883
|
+
const apiKey = resolveRuntimeApiKey().key;
|
|
6884
|
+
const target = apiKey ? await resolveTarget({ url: apiBaseUrl(), apiKey, keySource: "env" }).catch(() => null) : null;
|
|
6885
|
+
const confirmed = target?.confirmed;
|
|
6886
|
+
const names = confirmed?.branchId ? (0, import_footprint3.footprintNamesFor)({
|
|
6887
|
+
organizationId: confirmed.organizationId,
|
|
6888
|
+
branchId: confirmed.branchId,
|
|
6889
|
+
...confirmed.projectId ? { projectId: confirmed.projectId } : {}
|
|
6890
|
+
}) : void 0;
|
|
6891
|
+
const isolated = await auditTenantSyncInfra(sql, {
|
|
6892
|
+
schema: args.schema,
|
|
6893
|
+
...names ? { names } : {}
|
|
6894
|
+
});
|
|
6895
|
+
const legacy = names ? await auditTenantSyncInfra(sql, { schema: args.schema }) : [];
|
|
6896
|
+
artifacts = [...isolated, ...legacy].filter(
|
|
6897
|
+
(artifact, index, all) => all.findIndex(
|
|
6898
|
+
(candidate) => candidate.kind === artifact.kind && candidate.name === artifact.name
|
|
6899
|
+
) === index
|
|
6900
|
+
);
|
|
6745
6901
|
} catch (err) {
|
|
6746
6902
|
const pg = err ?? {};
|
|
6747
6903
|
await sql.end({ timeout: 2 });
|
|
@@ -6792,7 +6948,7 @@ async function runScan() {
|
|
|
6792
6948
|
async function runLocate(args) {
|
|
6793
6949
|
console.log(
|
|
6794
6950
|
`
|
|
6795
|
-
${brand("ablo")} ${import_picocolors12.default.dim("connect locate")} ${import_picocolors12.default.dim("which
|
|
6951
|
+
${brand("ablo")} ${import_picocolors12.default.dim("connect locate")} ${import_picocolors12.default.dim("which branch is connected to this database")}
|
|
6796
6952
|
`
|
|
6797
6953
|
);
|
|
6798
6954
|
const url = args.url ?? readProjectAdminDatabaseUrl();
|
|
@@ -6822,12 +6978,20 @@ ${ambient}` : ""}`,
|
|
|
6822
6978
|
path: "/v1/datasources/locate",
|
|
6823
6979
|
method: "POST",
|
|
6824
6980
|
apiKey,
|
|
6825
|
-
body: { connectionString: url },
|
|
6981
|
+
body: { connectionString: url, schema: args.schema },
|
|
6826
6982
|
responseSchema: import_wire7.datasourceLocationResponseSchema
|
|
6827
6983
|
});
|
|
6984
|
+
if (answer.available === false && !answer.held) {
|
|
6985
|
+
console.log(
|
|
6986
|
+
` ${import_picocolors12.default.yellow("!")} ${import_picocolors12.default.bold(label)} schema ${import_picocolors12.default.bold(args.schema)} is connected to another organization.
|
|
6987
|
+
` + import_picocolors12.default.dim(` Its project and branch are private. Use another schema or provider database URL.
|
|
6988
|
+
`)
|
|
6989
|
+
);
|
|
6990
|
+
return;
|
|
6991
|
+
}
|
|
6828
6992
|
if (!answer.held) {
|
|
6829
6993
|
console.log(
|
|
6830
|
-
` ${import_picocolors12.default.green("\u2713")} No
|
|
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.
|
|
6831
6995
|
`
|
|
6832
6996
|
);
|
|
6833
6997
|
return;
|
|
@@ -6838,13 +7002,50 @@ ${ambient}` : ""}`,
|
|
|
6838
7002
|
);
|
|
6839
7003
|
console.log(
|
|
6840
7004
|
import_picocolors12.default.dim(
|
|
6841
|
-
` Ablo
|
|
7005
|
+
` Ablo connects one branch to each database schema. To move this schema, disconnect it there
|
|
6842
7006
|
first \u2014 run `
|
|
6843
|
-
) + import_picocolors12.default.cyan("ablo connect deregister") + import_picocolors12.default.dim(` with a key for that
|
|
7007
|
+
) + import_picocolors12.default.cyan("ablo connect deregister") + import_picocolors12.default.dim(` with a key for that branch, then connect here.
|
|
6844
7008
|
`) + import_picocolors12.default.dim(` Confirm a candidate key first with `) + import_picocolors12.default.bold("ablo whoami --key-env <NAME>") + import_picocolors12.default.dim(`.
|
|
6845
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"
|
|
6846
7010
|
);
|
|
6847
7011
|
}
|
|
7012
|
+
async function runResnapshot() {
|
|
7013
|
+
const apiKey = resolveMutationApiKey();
|
|
7014
|
+
if (!apiKey) {
|
|
7015
|
+
throw new import_errors10.AbloAuthenticationError(
|
|
7016
|
+
"No branch-bound secret key found. Set ABLO_API_KEY to the sk_ key for the branch to resnapshot.",
|
|
7017
|
+
{ code: "cli_api_key_missing" }
|
|
7018
|
+
);
|
|
7019
|
+
}
|
|
7020
|
+
console.log(
|
|
7021
|
+
`
|
|
7022
|
+
${brand("ablo")} ${import_picocolors12.default.dim("connect resnapshot")} ${import_picocolors12.default.dim("reload existing rows")}
|
|
7023
|
+
`
|
|
7024
|
+
);
|
|
7025
|
+
const result = await requestControlPlane({
|
|
7026
|
+
path: "/v1/datasources/resnapshot",
|
|
7027
|
+
method: "POST",
|
|
7028
|
+
apiKey,
|
|
7029
|
+
body: {},
|
|
7030
|
+
responseSchema: import_wire7.datasourceResnapshotResponseSchema
|
|
7031
|
+
});
|
|
7032
|
+
if (result.replication_slot?.released === false) {
|
|
7033
|
+
console.log(` ${import_picocolors12.default.yellow("\u2014")} Snapshot reset recorded, but the old slot is still active.`);
|
|
7034
|
+
if (result.replication_slot.detail) console.log(` ${import_picocolors12.default.dim(result.replication_slot.detail)}`);
|
|
7035
|
+
if (result.replication_slot.remove_with) {
|
|
7036
|
+
console.log(` ${import_picocolors12.default.dim("Remove it with:")} ${import_picocolors12.default.bold(result.replication_slot.remove_with)}`);
|
|
7037
|
+
}
|
|
7038
|
+
console.log(`
|
|
7039
|
+
Re-run ${import_picocolors12.default.bold("ablo connect resnapshot")} after releasing it.
|
|
7040
|
+
`);
|
|
7041
|
+
process.exit(1);
|
|
7042
|
+
}
|
|
7043
|
+
console.log(
|
|
7044
|
+
` ${import_picocolors12.default.green("\u2713")} Fresh snapshot requested. The DataSource and credentials were kept.
|
|
7045
|
+
${import_picocolors12.default.dim(`Run ${import_picocolors12.default.bold("ablo connect check")} until the existing-row load is complete.`)}
|
|
7046
|
+
`
|
|
7047
|
+
);
|
|
7048
|
+
}
|
|
6848
7049
|
async function connect(argv) {
|
|
6849
7050
|
if (argv[0] === "deregister") {
|
|
6850
7051
|
const { disconnect: disconnect2 } = await Promise.resolve().then(() => (init_disconnect(), disconnect_exports));
|
|
@@ -6871,12 +7072,16 @@ async function connect(argv) {
|
|
|
6871
7072
|
await runCheck();
|
|
6872
7073
|
return;
|
|
6873
7074
|
}
|
|
7075
|
+
if (args.resnapshot) {
|
|
7076
|
+
await runResnapshot();
|
|
7077
|
+
return;
|
|
7078
|
+
}
|
|
6874
7079
|
if (args.register) {
|
|
6875
7080
|
await runRegister(args);
|
|
6876
7081
|
return;
|
|
6877
7082
|
}
|
|
6878
7083
|
if (args.scan) {
|
|
6879
|
-
await runScan();
|
|
7084
|
+
await runScan(args);
|
|
6880
7085
|
return;
|
|
6881
7086
|
}
|
|
6882
7087
|
if (args.locate) {
|
|
@@ -6890,9 +7095,31 @@ async function connect(argv) {
|
|
|
6890
7095
|
await runConnectApply2(args);
|
|
6891
7096
|
return;
|
|
6892
7097
|
}
|
|
6893
|
-
|
|
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
|
+
);
|
|
6894
7121
|
}
|
|
6895
|
-
var import_errors10, import_picocolors12,
|
|
7122
|
+
var import_errors10, import_picocolors12, import_footprint3, import_wire7, FOOTPRINT_LOOKUP, CONNECT_USAGE;
|
|
6896
7123
|
var init_connect = __esm({
|
|
6897
7124
|
"src/connect.ts"() {
|
|
6898
7125
|
"use strict";
|
|
@@ -6900,12 +7127,13 @@ var init_connect = __esm({
|
|
|
6900
7127
|
import_errors10 = require("@abloatai/transaction/errors");
|
|
6901
7128
|
import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
6902
7129
|
init_src();
|
|
6903
|
-
|
|
7130
|
+
import_footprint3 = require("@abloatai/transaction/footprint");
|
|
6904
7131
|
init_dbRole();
|
|
6905
7132
|
init_config();
|
|
6906
7133
|
init_controlPlane();
|
|
6907
7134
|
import_wire7 = require("@abloatai/transaction/wire");
|
|
6908
7135
|
init_theme();
|
|
7136
|
+
init_target();
|
|
6909
7137
|
init_remoteValidation();
|
|
6910
7138
|
init_connectSetup();
|
|
6911
7139
|
FOOTPRINT_LOOKUP = {
|
|
@@ -6927,8 +7155,9 @@ var init_connect = __esm({
|
|
|
6927
7155
|
npx ablo connect deregister Disconnect this project's database \u2014 Ablo stops reading and writing it
|
|
6928
7156
|
npx ablo connect check Confirm the connected database is ready, from Ablo's side (needs only ABLO_API_KEY)
|
|
6929
7157
|
npx ablo connect rotate New passwords for both logins, then re-register
|
|
7158
|
+
npx ablo connect resnapshot Recreate only the slot and reload existing rows
|
|
6930
7159
|
npx ablo connect scan List anything Ablo ever set up in your database (read-only, never drops)
|
|
6931
|
-
npx ablo connect locate See which
|
|
7160
|
+
npx ablo connect locate See which branch is connected to this database (read-only; nothing is changed)
|
|
6932
7161
|
|
|
6933
7162
|
Running it: bare \`ablo connect\` sets everything up for you \u2014 creating the two
|
|
6934
7163
|
scoped logins, sharing your tables, and registering \u2014 whenever it finds a
|
|
@@ -6941,6 +7170,7 @@ var init_connect = __esm({
|
|
|
6941
7170
|
--url <admin-conn> Admin connection used once to set up (else DATABASE_URL); never stored
|
|
6942
7171
|
--env-file <path> Explicitly load DATABASE_URL and ABLO_API_KEY from this file
|
|
6943
7172
|
--tables a,b,c Publish only these tables (default: all tables)
|
|
7173
|
+
--schema <name> Bind this project to one database schema (default: public)
|
|
6944
7174
|
--role <name> Name the replication role (default: ablo_replicator)
|
|
6945
7175
|
--write-role <name> Name the DML role (default: ablo_writer)
|
|
6946
7176
|
--route <route> public-allowlist | privatelink | peering | vpn
|
|
@@ -283406,7 +283636,7 @@ async function readStatus(ref, context = {}) {
|
|
|
283406
283636
|
return import_branches.branchStatusResponseSchema.parse(response.body);
|
|
283407
283637
|
}
|
|
283408
283638
|
function printStatus(status2) {
|
|
283409
|
-
const { branch, schema, data_source: source } = status2;
|
|
283639
|
+
const { branch, schema, storage, data_source: source } = status2;
|
|
283410
283640
|
console.log(` ${import_picocolors14.default.bold(branch.slug)} ${import_picocolors14.default.dim(branch.id)}`);
|
|
283411
283641
|
console.log(
|
|
283412
283642
|
` ${import_picocolors14.default.dim("state")} ${branch.state === "ready" ? import_picocolors14.default.green(branch.state) : import_picocolors14.default.yellow(branch.state)}`
|
|
@@ -283422,7 +283652,7 @@ function printStatus(status2) {
|
|
|
283422
283652
|
` ${import_picocolors14.default.dim("parent")} ${import_picocolors14.default.bold(schema.parent_compatibility)}${counts}`
|
|
283423
283653
|
);
|
|
283424
283654
|
}
|
|
283425
|
-
const sourceLabel = source
|
|
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";
|
|
283426
283656
|
console.log(` ${import_picocolors14.default.dim("data")} ${sourceLabel}`);
|
|
283427
283657
|
if (status2.ready) {
|
|
283428
283658
|
console.log(`
|
|
@@ -283569,11 +283799,15 @@ init_controlPlane();
|
|
|
283569
283799
|
init_config();
|
|
283570
283800
|
init_readiness();
|
|
283571
283801
|
init_theme();
|
|
283802
|
+
init_dbRole();
|
|
283803
|
+
var import_source3 = require("@abloatai/transaction/source");
|
|
283572
283804
|
function parseDevArgs(argv) {
|
|
283573
283805
|
let schemaPath = DEFAULT_SCHEMA_PATH;
|
|
283574
283806
|
let exportName = DEFAULT_EXPORT;
|
|
283575
283807
|
let url = process.env.ABLO_API_URL ?? DEFAULT_URL;
|
|
283576
283808
|
let watchEnabled = false;
|
|
283809
|
+
let local = false;
|
|
283810
|
+
let sourcePath = "ablo/data-source.ts";
|
|
283577
283811
|
for (let i = 0; i < argv.length; i++) {
|
|
283578
283812
|
const arg = argv[i];
|
|
283579
283813
|
switch (arg) {
|
|
@@ -283592,6 +283826,12 @@ function parseDevArgs(argv) {
|
|
|
283592
283826
|
case "--no-watch":
|
|
283593
283827
|
watchEnabled = false;
|
|
283594
283828
|
break;
|
|
283829
|
+
case "--local":
|
|
283830
|
+
local = true;
|
|
283831
|
+
break;
|
|
283832
|
+
case "--source":
|
|
283833
|
+
sourcePath = argv[++i] ?? sourcePath;
|
|
283834
|
+
break;
|
|
283595
283835
|
default:
|
|
283596
283836
|
throw new import_errors11.AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
|
|
283597
283837
|
}
|
|
@@ -283603,9 +283843,55 @@ function parseDevArgs(argv) {
|
|
|
283603
283843
|
url,
|
|
283604
283844
|
apiKey: process.env.ABLO_API_KEY,
|
|
283605
283845
|
watch: watchEnabled,
|
|
283846
|
+
local,
|
|
283847
|
+
sourcePath,
|
|
283606
283848
|
planeLabel: "branch"
|
|
283607
283849
|
};
|
|
283608
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
|
+
}
|
|
283609
283895
|
function classifyKey(apiKey) {
|
|
283610
283896
|
if (!apiKey) {
|
|
283611
283897
|
return {
|
|
@@ -283625,13 +283911,21 @@ function classifyKey(apiKey) {
|
|
|
283625
283911
|
reason: `${import_picocolors15.default.bold("ABLO_API_KEY")} is not a secret Ablo key. Expected a branch-bound ${import_picocolors15.default.bold("sk_\u2026")} credential. Run ${import_picocolors15.default.bold("npx ablo dev")} to prepare a development branch.`
|
|
283626
283912
|
};
|
|
283627
283913
|
}
|
|
283628
|
-
function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
283914
|
+
function wireEnvLocal(apiKey, cwd = process.cwd(), projectId, branchId) {
|
|
283629
283915
|
const envPath = (0, import_path5.resolve)(cwd, ".env.local");
|
|
283630
283916
|
const line = `ABLO_API_KEY=${apiKey}`;
|
|
283917
|
+
const projectLine = projectId ? `ABLO_PROJECT_ID=${projectId}` : null;
|
|
283918
|
+
const branchLine = branchId ? `ABLO_BRANCH_ID=${branchId}` : null;
|
|
283631
283919
|
let action;
|
|
283632
283920
|
if (!(0, import_fs7.existsSync)(envPath)) {
|
|
283633
|
-
(0, import_fs7.writeFileSync)(
|
|
283634
|
-
|
|
283921
|
+
(0, import_fs7.writeFileSync)(
|
|
283922
|
+
envPath,
|
|
283923
|
+
`${line}
|
|
283924
|
+
${projectLine ? `${projectLine}
|
|
283925
|
+
` : ""}${branchLine ? `${branchLine}
|
|
283926
|
+
` : ""}`,
|
|
283927
|
+
{ mode: 384 }
|
|
283928
|
+
);
|
|
283635
283929
|
action = `Created ${import_picocolors15.default.bold(".env.local")} with ${import_picocolors15.default.bold("ABLO_API_KEY")}`;
|
|
283636
283930
|
} else {
|
|
283637
283931
|
const content = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
@@ -283647,6 +283941,24 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
|
283647
283941
|
(0, import_fs7.writeFileSync)(envPath, content.replace(/^ABLO_API_KEY=.*$/m, line));
|
|
283648
283942
|
action = `Updated ${import_picocolors15.default.bold("ABLO_API_KEY")} in ${import_picocolors15.default.bold(".env.local")} ${import_picocolors15.default.dim(`(was ${existing.slice(0, 12)}\u2026)`)}`;
|
|
283649
283943
|
}
|
|
283944
|
+
if (projectLine) {
|
|
283945
|
+
const next = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
283946
|
+
if (/^ABLO_PROJECT_ID=.*$/m.test(next)) {
|
|
283947
|
+
(0, import_fs7.writeFileSync)(envPath, next.replace(/^ABLO_PROJECT_ID=.*$/m, projectLine));
|
|
283948
|
+
} else {
|
|
283949
|
+
(0, import_fs7.appendFileSync)(envPath, `${next.endsWith("\n") || next.length === 0 ? "" : "\n"}${projectLine}
|
|
283950
|
+
`);
|
|
283951
|
+
}
|
|
283952
|
+
}
|
|
283953
|
+
if (branchLine) {
|
|
283954
|
+
const next = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
283955
|
+
if (/^ABLO_BRANCH_ID=.*$/m.test(next)) {
|
|
283956
|
+
(0, import_fs7.writeFileSync)(envPath, next.replace(/^ABLO_BRANCH_ID=.*$/m, branchLine));
|
|
283957
|
+
} else {
|
|
283958
|
+
(0, import_fs7.appendFileSync)(envPath, `${next.endsWith("\n") || next.length === 0 ? "" : "\n"}${branchLine}
|
|
283959
|
+
`);
|
|
283960
|
+
}
|
|
283961
|
+
}
|
|
283650
283962
|
}
|
|
283651
283963
|
const gitignorePath = (0, import_path5.resolve)(cwd, ".gitignore");
|
|
283652
283964
|
const gitignore = (0, import_fs7.existsSync)(gitignorePath) ? (0, import_fs7.readFileSync)(gitignorePath, "utf8") : "";
|
|
@@ -283717,7 +284029,7 @@ async function runPush(schema, args) {
|
|
|
283717
284029
|
}
|
|
283718
284030
|
if (status2 === 403) {
|
|
283719
284031
|
const serverSays = body.message ?? body.reason;
|
|
283720
|
-
const hint = 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")}.`);
|
|
283721
284033
|
return {
|
|
283722
284034
|
ok: false,
|
|
283723
284035
|
message: `${serverSays ?? "This key can't author schema (missing schema:push scope)."}
|
|
@@ -283725,6 +284037,11 @@ async function runPush(schema, args) {
|
|
|
283725
284037
|
};
|
|
283726
284038
|
}
|
|
283727
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
|
+
}
|
|
283728
284045
|
if (looksLikeCredentialRefusal(serverMessage)) {
|
|
283729
284046
|
const pooled = await poolerExplanation(apiBaseUrl(), args.apiKey);
|
|
283730
284047
|
if (pooled) {
|
|
@@ -283745,6 +284062,12 @@ async function dev(argv, runtime = {}) {
|
|
|
283745
284062
|
if (runtime.apiKey) args.apiKey = runtime.apiKey;
|
|
283746
284063
|
else if (!args.apiKey) args.apiKey = resolveRuntimeApiKey("sandbox").key;
|
|
283747
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
|
+
}
|
|
283748
284071
|
const key = classifyKey(args.apiKey);
|
|
283749
284072
|
if (!key.ok) {
|
|
283750
284073
|
console.error(import_picocolors15.default.red(` ${key.reason}`));
|
|
@@ -283761,6 +284084,35 @@ async function dev(argv, runtime = {}) {
|
|
|
283761
284084
|
` ${import_picocolors15.default.dim("key")} temporary \xB7 expires ${runtime.branch.expiresAt}`
|
|
283762
284085
|
);
|
|
283763
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
|
+
}
|
|
283764
284116
|
const schema = await loadSchema(args.schemaPath, args.exportName);
|
|
283765
284117
|
const modelCount = Object.keys(schema.models).length;
|
|
283766
284118
|
console.log(
|
|
@@ -283775,10 +284127,15 @@ async function dev(argv, runtime = {}) {
|
|
|
283775
284127
|
s.start("Pushing schema definition (development branch)");
|
|
283776
284128
|
const first = await runPush(schema, args);
|
|
283777
284129
|
s.stop(first.message, first.ok ? 0 : 1);
|
|
283778
|
-
if (!first.ok)
|
|
284130
|
+
if (!first.ok) {
|
|
284131
|
+
localAbort?.abort();
|
|
284132
|
+
process.exit(1);
|
|
284133
|
+
}
|
|
283779
284134
|
if (runtime.branch) {
|
|
283780
|
-
console.log(
|
|
283781
|
-
|
|
284135
|
+
console.log(
|
|
284136
|
+
`
|
|
284137
|
+
${import_picocolors15.default.green("\u2713")} ${wireEnvLocal(args.apiKey, process.cwd(), runtime.branch.projectId, runtime.branch.id)}`
|
|
284138
|
+
);
|
|
283782
284139
|
console.log(
|
|
283783
284140
|
` ${import_picocolors15.default.dim(`Temporary branch credential expires ${runtime.branch.expiresAt}; rerun ablo dev to rotate it.`)}`
|
|
283784
284141
|
);
|
|
@@ -283827,6 +284184,7 @@ async function dev(argv, runtime = {}) {
|
|
|
283827
284184
|
}
|
|
283828
284185
|
const stop = () => {
|
|
283829
284186
|
watcher.close();
|
|
284187
|
+
localAbort?.abort();
|
|
283830
284188
|
console.log(`
|
|
283831
284189
|
${import_picocolors15.default.dim("stopped.")}`);
|
|
283832
284190
|
process.exit(0);
|
|
@@ -283842,6 +284200,7 @@ init_controlPlane();
|
|
|
283842
284200
|
var BRANCH_DEV_USAGE = `Usage:
|
|
283843
284201
|
ablo dev [--branch <slug>] [--branch-ttl-hours <1-168>]
|
|
283844
284202
|
[--schema <path>] [--export <name>] [--url <url>]
|
|
284203
|
+
[--local] [--source <path>]
|
|
283845
284204
|
ablo dev --no-watch [branch options]
|
|
283846
284205
|
|
|
283847
284206
|
By default, dev discovers the Git/CI branch, ensures its isolated Ablo branch,
|
|
@@ -283953,6 +284312,7 @@ async function runBranchDev(argv, dependencies = {}) {
|
|
|
283953
284312
|
apiKey: result.credential.api_key,
|
|
283954
284313
|
branch: {
|
|
283955
284314
|
id: result.branch.id,
|
|
284315
|
+
projectId: result.branch.project_id,
|
|
283956
284316
|
slug: result.branch.slug,
|
|
283957
284317
|
expiresAt: result.credential.expires_at
|
|
283958
284318
|
}
|
|
@@ -284227,8 +284587,9 @@ var COMMANDS = [
|
|
|
284227
284587
|
{ run: "connect", does: "Connect your database \u2014 shows the setup to run, or applies it for you" },
|
|
284228
284588
|
{ run: "connect apply", does: "Run that setup for you, from a one-time admin URL" },
|
|
284229
284589
|
{ run: "connect check", does: "Confirm your database is ready to share changes with Ablo" },
|
|
284590
|
+
{ run: "connect resnapshot", does: "Reload existing rows without deregistering or rotating credentials" },
|
|
284230
284591
|
{ run: "connect scan", does: "List anything Ablo ever set up in your database (read-only)" },
|
|
284231
|
-
{ run: "connect locate", does: "See which
|
|
284592
|
+
{ run: "connect locate", does: "See which branch is connected to a database before connecting it" },
|
|
284232
284593
|
{ run: "connect deregister", does: "Disconnect this project's database \u2014 Ablo stops reading and writing it" }
|
|
284233
284594
|
]
|
|
284234
284595
|
}
|
|
@@ -284972,11 +285333,11 @@ async function status(args = []) {
|
|
|
284972
285333
|
}
|
|
284973
285334
|
} else if (dataSource.kind === "none") {
|
|
284974
285335
|
console.log(
|
|
284975
|
-
` ${import_picocolors19.default.dim("data")} ${import_picocolors19.default.red("\u2717
|
|
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")}`
|
|
284976
285337
|
);
|
|
284977
285338
|
} else if (reachable) {
|
|
284978
285339
|
console.log(
|
|
284979
|
-
` ${import_picocolors19.default.dim("data")} ${import_picocolors19.default.yellow("?")} ${import_picocolors19.default.dim(`could not read
|
|
285340
|
+
` ${import_picocolors19.default.dim("data")} ${import_picocolors19.default.yellow("?")} ${import_picocolors19.default.dim(`could not read this branch's database connection (${dataSource.detail})`)}`
|
|
284980
285341
|
);
|
|
284981
285342
|
}
|
|
284982
285343
|
const pushed = reachable ? await fetchPushedSchema(apiUrl3, introspectKey) : null;
|
|
@@ -285022,7 +285383,7 @@ async function status(args = []) {
|
|
|
285022
285383
|
}
|
|
285023
285384
|
} else if (dataSource.kind === "unknown") {
|
|
285024
285385
|
console.log(
|
|
285025
|
-
` ${import_picocolors19.default.yellow("?")} ${import_picocolors19.default.dim("nothing is blocking a write, but this key could not read the
|
|
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")}`
|
|
285026
285387
|
);
|
|
285027
285388
|
} else {
|
|
285028
285389
|
console.log(
|
|
@@ -285129,7 +285490,7 @@ async function doctor() {
|
|
|
285129
285490
|
checks.push({
|
|
285130
285491
|
label: "data",
|
|
285131
285492
|
state: "fail",
|
|
285132
|
-
detail: "
|
|
285493
|
+
detail: "this branch is not connected to a database \u2014 writes are held",
|
|
285133
285494
|
fix: "connect one with `ablo connect apply`"
|
|
285134
285495
|
});
|
|
285135
285496
|
} else {
|
|
@@ -285721,7 +286082,7 @@ async function reportReadSubject(dbUrl) {
|
|
|
285721
286082
|
}
|
|
285722
286083
|
if (state.kind === "none") {
|
|
285723
286084
|
console.log(
|
|
285724
|
-
` ${import_picocolors23.default.dim("ablo")} ${import_picocolors23.default.yellow("!")}
|
|
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`
|
|
285725
286086
|
);
|
|
285726
286087
|
console.log(
|
|
285727
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.`)}
|
|
@@ -287138,6 +287499,8 @@ import { schema } from './schema';
|
|
|
287138
287499
|
// via the session route and never touches the key.
|
|
287139
287500
|
export const sync = Ablo({
|
|
287140
287501
|
apiKey: process.env.ABLO_API_KEY,${authLine}
|
|
287502
|
+
projectId: process.env.ABLO_PROJECT_ID,
|
|
287503
|
+
branchId: process.env.ABLO_BRANCH_ID,
|
|
287141
287504
|
schema,
|
|
287142
287505
|
});
|
|
287143
287506
|
|
|
@@ -287163,7 +287526,7 @@ function generateEnv(storage, opts = {}) {
|
|
|
287163
287526
|
const { includeApiKey = true } = opts;
|
|
287164
287527
|
const databaseBlock = storage === "replication" ? "# Used by `npx ablo connect` to set up + register logical replication \u2014 the\n# DIRECT (un-pooled) endpoint. Ablo TAILS your WAL from here; it never writes.\n# The client never sees it; the browser never sees it. Your DB stays yours.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : "# Used by ablo/data-source.ts (your DB endpoint) + `ablo migrate` \u2014 NOT the client.\n# Ablo never sees it; the browser never sees it. Your DB stays in your app.\nDATABASE_URL=postgres://user:password@host:5432/db\n";
|
|
287165
287528
|
const webhookBlock = storage === "endpoint" ? "# Signing secret for the webhook receiver (app/api/ablo/webhooks/route.ts).\n# Ablo mints this when you register the endpoint's URL (POST /v1/webhook_endpoints\n# or the dashboard) and returns it once \u2014 paste it here.\nABLO_WEBHOOK_SECRET=whsec_your_endpoint_secret_here\n" : "";
|
|
287166
|
-
const apiKeyBlock = includeApiKey ? "# Ablo: a branch-bound sk_ key (`npx ablo dev` wires
|
|
287529
|
+
const apiKeyBlock = includeApiKey ? "# Ablo: a branch-bound sk_ key (`npx ablo dev` wires both values for you).\n# Project + branch are assertions: the SDK rejects a key for another app or environment.\nABLO_API_KEY=sk_your_key_here\nABLO_PROJECT_ID=proj_your_project_id\nABLO_BRANCH_ID=br_your_branch_id\n" : "";
|
|
287167
287530
|
return `${apiKeyBlock}${webhookBlock}${databaseBlock}`;
|
|
287168
287531
|
}
|
|
287169
287532
|
function generateDataSource(orm) {
|
|
@@ -287324,8 +287687,7 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
287324
287687
|
`;
|
|
287325
287688
|
}
|
|
287326
287689
|
function generateAgent() {
|
|
287327
|
-
return `import
|
|
287328
|
-
import { schema } from './schema';
|
|
287690
|
+
return `import { sync as ablo } from './sync';
|
|
287329
287691
|
|
|
287330
287692
|
/**
|
|
287331
287693
|
* An AI "teammate" that works the same synced tasks a human does.
|
|
@@ -287335,8 +287697,6 @@ import { schema } from './schema';
|
|
|
287335
287697
|
* \`npx ablo logs\`. That's the whole idea: agents and people on one typed,
|
|
287336
287698
|
* synced dataset.
|
|
287337
287699
|
*/
|
|
287338
|
-
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
287339
|
-
|
|
287340
287700
|
async function main() {
|
|
287341
287701
|
await ablo.ready();
|
|
287342
287702
|
|
|
@@ -287354,7 +287714,6 @@ async function main() {
|
|
|
287354
287714
|
data: { priority: 10 },
|
|
287355
287715
|
readAt: snap.stamp,
|
|
287356
287716
|
onStale: 'reject',
|
|
287357
|
-
wait: 'confirmed',
|
|
287358
287717
|
});
|
|
287359
287718
|
console.log('prioritized:', urgent.title);
|
|
287360
287719
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/cli",
|
|
3
|
-
"version": "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.
|
|
33
|
+
"@abloatai/transaction": "^0.48.0",
|
|
34
34
|
"jiti": "^2.7.0",
|
|
35
35
|
"zod": "^4.4.3",
|
|
36
|
-
"@abloatai/humans": "^0.
|
|
36
|
+
"@abloatai/humans": "^0.48.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@clack/prompts": "^0.11.0",
|