@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.
- package/dist/cli.cjs +199 -46
- 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
|
|
4103
|
-
if (code
|
|
4104
|
-
|
|
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
|
|
4415
|
-
if (
|
|
4416
|
-
console.error(import_picocolors5.default.dim(` ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
6416
|
-
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(
|
|
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 ${
|
|
6493
|
-
${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)`)}
|
|
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
|
|
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
|
|
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")}
|
|
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", {
|
|
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", {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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("!")}
|
|
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.
|
|
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",
|