ardent-cli 0.0.27 → 0.0.29
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/index.js +237 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -343,6 +343,15 @@ var ApiClient = class {
|
|
|
343
343
|
});
|
|
344
344
|
return this.handleResponse(response);
|
|
345
345
|
}
|
|
346
|
+
async put(path, body) {
|
|
347
|
+
const url = `${getApiUrl()}${path}`;
|
|
348
|
+
const response = await fetch(url, {
|
|
349
|
+
method: "PUT",
|
|
350
|
+
headers: this.getHeaders(),
|
|
351
|
+
body: JSON.stringify(body)
|
|
352
|
+
});
|
|
353
|
+
return this.handleResponse(response);
|
|
354
|
+
}
|
|
346
355
|
};
|
|
347
356
|
var api = new ApiClient();
|
|
348
357
|
function isNetworkError(err) {
|
|
@@ -987,6 +996,52 @@ Example:
|
|
|
987
996
|
}
|
|
988
997
|
}
|
|
989
998
|
|
|
999
|
+
// src/lib/prompt.ts
|
|
1000
|
+
import { createInterface } from "readline/promises";
|
|
1001
|
+
function isTtyInteractive() {
|
|
1002
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1003
|
+
}
|
|
1004
|
+
async function readOneLine(question) {
|
|
1005
|
+
const rl = createInterface({
|
|
1006
|
+
input: process.stdin,
|
|
1007
|
+
output: process.stdout
|
|
1008
|
+
});
|
|
1009
|
+
try {
|
|
1010
|
+
const answer = await rl.question(question);
|
|
1011
|
+
return answer.trim();
|
|
1012
|
+
} finally {
|
|
1013
|
+
rl.close();
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
async function confirm(question, options = {}) {
|
|
1017
|
+
if (!isTtyInteractive()) {
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
const defaultYes = options.defaultYes ?? true;
|
|
1021
|
+
const suffix = defaultYes ? "[Y/n] " : "[y/N] ";
|
|
1022
|
+
const answer = (await readOneLine(`${question} ${suffix}`)).toLowerCase();
|
|
1023
|
+
if (answer === "") return defaultYes;
|
|
1024
|
+
if (answer === "y" || answer === "yes") return true;
|
|
1025
|
+
if (answer === "n" || answer === "no") return false;
|
|
1026
|
+
const retry = (await readOneLine(`Please answer yes or no. ${suffix}`)).toLowerCase();
|
|
1027
|
+
if (retry === "") return defaultYes;
|
|
1028
|
+
if (retry === "y" || retry === "yes") return true;
|
|
1029
|
+
if (retry === "n" || retry === "no") return false;
|
|
1030
|
+
console.log('Unrecognised input; treating as "no".');
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
async function multiSelect(items, promptForItem, options = {}) {
|
|
1034
|
+
if (!isTtyInteractive()) {
|
|
1035
|
+
return [];
|
|
1036
|
+
}
|
|
1037
|
+
const accepted = [];
|
|
1038
|
+
for (const item of items) {
|
|
1039
|
+
const yes = await confirm(promptForItem(item), { defaultYes: options.defaultYes });
|
|
1040
|
+
if (yes) accepted.push(item);
|
|
1041
|
+
}
|
|
1042
|
+
return accepted;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
990
1045
|
// src/commands/connector/create.ts
|
|
991
1046
|
function parsePostgresUrl(url) {
|
|
992
1047
|
const atIndex = url.lastIndexOf("@");
|
|
@@ -1007,6 +1062,57 @@ function printEngineSetupRecoveryHint(connectorName) {
|
|
|
1007
1062
|
console.error(" Inspect: ardent connector list");
|
|
1008
1063
|
console.error(` Retry: ardent connector retry-setup ${connectorName}`);
|
|
1009
1064
|
}
|
|
1065
|
+
async function promptForUnsupportedExtensions(connectorId, unsupported, alreadyPersisted) {
|
|
1066
|
+
function mergeAllowlist(newlyAccepted) {
|
|
1067
|
+
return Array.from(/* @__PURE__ */ new Set([...alreadyPersisted, ...newlyAccepted])).sort();
|
|
1068
|
+
}
|
|
1069
|
+
console.log("");
|
|
1070
|
+
if (unsupported.length === 1) {
|
|
1071
|
+
const ext = unsupported[0];
|
|
1072
|
+
console.log(
|
|
1073
|
+
`Found 1 extension on your source database that isn't supported on branches:`
|
|
1074
|
+
);
|
|
1075
|
+
console.log(` \u2022 ${ext}`);
|
|
1076
|
+
console.log("");
|
|
1077
|
+
console.log(
|
|
1078
|
+
"Branches from this connector will fail unless this extension is excluded."
|
|
1079
|
+
);
|
|
1080
|
+
const yes = await confirm(`Branch without ${ext}?`, { defaultYes: true });
|
|
1081
|
+
if (!yes) return "aborted";
|
|
1082
|
+
const merged2 = mergeAllowlist([ext]);
|
|
1083
|
+
await api.put(`/v1/connectors/${connectorId}`, {
|
|
1084
|
+
drop_extensions: merged2
|
|
1085
|
+
});
|
|
1086
|
+
console.log(`\u2713 Saved drop selection: ${merged2.join(", ")}`);
|
|
1087
|
+
return "applied";
|
|
1088
|
+
}
|
|
1089
|
+
console.log(
|
|
1090
|
+
`Found ${unsupported.length} extensions on your source database that aren't supported on branches:`
|
|
1091
|
+
);
|
|
1092
|
+
for (const ext of unsupported) {
|
|
1093
|
+
console.log(` \u2022 ${ext}`);
|
|
1094
|
+
}
|
|
1095
|
+
console.log("");
|
|
1096
|
+
console.log(
|
|
1097
|
+
"Pick which extensions to exclude from branches. Branches from this connector"
|
|
1098
|
+
);
|
|
1099
|
+
console.log(
|
|
1100
|
+
"will fail unless every unsupported extension you keep is configured by support."
|
|
1101
|
+
);
|
|
1102
|
+
console.log("");
|
|
1103
|
+
const accepted = await multiSelect(
|
|
1104
|
+
unsupported,
|
|
1105
|
+
(ext) => `Branch without ${ext}?`,
|
|
1106
|
+
{ defaultYes: true }
|
|
1107
|
+
);
|
|
1108
|
+
if (accepted.length === 0) return "aborted";
|
|
1109
|
+
const merged = mergeAllowlist(accepted);
|
|
1110
|
+
await api.put(`/v1/connectors/${connectorId}`, {
|
|
1111
|
+
drop_extensions: merged
|
|
1112
|
+
});
|
|
1113
|
+
console.log(`\u2713 Saved drop selection: ${merged.join(", ")}`);
|
|
1114
|
+
return "applied";
|
|
1115
|
+
}
|
|
1010
1116
|
async function createAction2(type, url, options) {
|
|
1011
1117
|
const supportedTypes = ["postgresql"];
|
|
1012
1118
|
if (!supportedTypes.includes(type.toLowerCase())) {
|
|
@@ -1145,6 +1251,36 @@ async function createAction2(type, url, options) {
|
|
|
1145
1251
|
selected_paths: ["*"]
|
|
1146
1252
|
});
|
|
1147
1253
|
const connector = await api.get(`/v1/connectors/${connectorId}`);
|
|
1254
|
+
const preview = connector.unsupported_extensions_preview ?? null;
|
|
1255
|
+
const unsupportedExtensions = preview?.unsupported ?? [];
|
|
1256
|
+
const dropExtensionsPersisted = preview?.drop_extensions_persisted ?? [];
|
|
1257
|
+
if (unsupportedExtensions.length > 0) {
|
|
1258
|
+
const promptOutcome = await promptForUnsupportedExtensions(
|
|
1259
|
+
connectorId,
|
|
1260
|
+
unsupportedExtensions,
|
|
1261
|
+
dropExtensionsPersisted
|
|
1262
|
+
);
|
|
1263
|
+
if (promptOutcome === "aborted") {
|
|
1264
|
+
trackEvent("CLI: connector create aborted", {
|
|
1265
|
+
reason: "declined_extension_drops",
|
|
1266
|
+
unsupported_count: unsupportedExtensions.length
|
|
1267
|
+
});
|
|
1268
|
+
console.error(
|
|
1269
|
+
"\u2717 Connector setup aborted because no extension drop selection was made."
|
|
1270
|
+
);
|
|
1271
|
+
console.error(
|
|
1272
|
+
" Branches cannot be created until the connector has a drop selection."
|
|
1273
|
+
);
|
|
1274
|
+
console.error(" Run this when ready:");
|
|
1275
|
+
console.error(
|
|
1276
|
+
` ardent connector update ${connectorName} --drop-extensions <ext,...>`
|
|
1277
|
+
);
|
|
1278
|
+
console.error(
|
|
1279
|
+
` ardent connector retry-setup ${connectorName}`
|
|
1280
|
+
);
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1148
1284
|
if (connector.branching_engine_status === "configuration_verified") {
|
|
1149
1285
|
console.log("Setting up branching engine...");
|
|
1150
1286
|
try {
|
|
@@ -1319,7 +1455,7 @@ async function listAction2() {
|
|
|
1319
1455
|
}
|
|
1320
1456
|
|
|
1321
1457
|
// src/commands/connector/delete.ts
|
|
1322
|
-
async function deleteAction2(name) {
|
|
1458
|
+
async function deleteAction2(name, options = {}) {
|
|
1323
1459
|
const cached = getCacheEntry("connectors");
|
|
1324
1460
|
let connector = cached?.data.find((c) => c.name === name);
|
|
1325
1461
|
if (!connector) {
|
|
@@ -1344,13 +1480,15 @@ async function deleteAction2(name) {
|
|
|
1344
1480
|
}
|
|
1345
1481
|
try {
|
|
1346
1482
|
console.log("Deleting connector...");
|
|
1347
|
-
|
|
1483
|
+
const force = options.force ?? false;
|
|
1484
|
+
const path = force ? `/v1/connectors/${connector.id}?force=true` : `/v1/connectors/${connector.id}`;
|
|
1485
|
+
await api.delete(path);
|
|
1348
1486
|
const currentCache = getCacheEntry("connectors");
|
|
1349
1487
|
if (currentCache?.data) {
|
|
1350
1488
|
const updatedConnectors = currentCache.data.filter((c) => c.id !== connector.id);
|
|
1351
1489
|
setCacheEntry("connectors", updatedConnectors);
|
|
1352
1490
|
}
|
|
1353
|
-
trackEvent("CLI: connector delete succeeded");
|
|
1491
|
+
trackEvent("CLI: connector delete succeeded", { force });
|
|
1354
1492
|
console.log("\u2713 Connector deleted");
|
|
1355
1493
|
} catch (err) {
|
|
1356
1494
|
if (isNetworkError(err)) {
|
|
@@ -1367,8 +1505,15 @@ async function deleteAction2(name) {
|
|
|
1367
1505
|
console.error(" \u2022 Upgrade your role to Admin");
|
|
1368
1506
|
process.exit(1);
|
|
1369
1507
|
}
|
|
1508
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1509
|
+
const isDrainRefusal = message.includes("Branch still has un-replicated changes") || typeof err === "object" && err !== null && "status" in err && err.status === 409;
|
|
1510
|
+
if (isDrainRefusal) {
|
|
1511
|
+
trackEvent("CLI: connector delete failed", { reason: "drain_refused" });
|
|
1512
|
+
console.error(`\u2717 ${message}`);
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
}
|
|
1370
1515
|
trackEvent("CLI: connector delete failed", { reason: "api_error" });
|
|
1371
|
-
console.error("\u2717 Failed:",
|
|
1516
|
+
console.error("\u2717 Failed:", message);
|
|
1372
1517
|
process.exit(1);
|
|
1373
1518
|
}
|
|
1374
1519
|
}
|
|
@@ -1526,6 +1671,86 @@ async function switchAction2(name) {
|
|
|
1526
1671
|
trackEvent("CLI: connector switch");
|
|
1527
1672
|
}
|
|
1528
1673
|
|
|
1674
|
+
// src/lib/drop_extensions.ts
|
|
1675
|
+
function parseDropExtensions(raw) {
|
|
1676
|
+
if (raw === void 0) return null;
|
|
1677
|
+
if (raw.trim() === "") return [];
|
|
1678
|
+
return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/commands/connector/update.ts
|
|
1682
|
+
async function resolveConnectorByName(name) {
|
|
1683
|
+
const cached = getCacheEntry("connectors");
|
|
1684
|
+
let connector = cached?.data.find((c) => c.name === name);
|
|
1685
|
+
if (!connector) {
|
|
1686
|
+
try {
|
|
1687
|
+
const result = await api.get(
|
|
1688
|
+
"/v1/cli/connectors"
|
|
1689
|
+
);
|
|
1690
|
+
if (result.connectors) {
|
|
1691
|
+
setCacheEntry("connectors", result.connectors);
|
|
1692
|
+
connector = result.connectors.find((c) => c.name === name);
|
|
1693
|
+
}
|
|
1694
|
+
} catch (err) {
|
|
1695
|
+
if (isNetworkError(err)) {
|
|
1696
|
+
console.error("\u2717 Connector not found in cache and offline");
|
|
1697
|
+
process.exit(1);
|
|
1698
|
+
}
|
|
1699
|
+
throw err;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (!connector) {
|
|
1703
|
+
console.error(`\u2717 Connector "${name}" not found`);
|
|
1704
|
+
console.log(" Run: ardent connector list");
|
|
1705
|
+
process.exit(1);
|
|
1706
|
+
}
|
|
1707
|
+
return connector;
|
|
1708
|
+
}
|
|
1709
|
+
async function updateAction(name, options = {}) {
|
|
1710
|
+
const dropExtensions = parseDropExtensions(options.dropExtensions);
|
|
1711
|
+
if (dropExtensions === null) {
|
|
1712
|
+
console.error("\u2717 Nothing to update.");
|
|
1713
|
+
console.error(" Try: ardent connector update <name> --drop-extensions <ext,...>");
|
|
1714
|
+
process.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
const connector = await resolveConnectorByName(name);
|
|
1717
|
+
try {
|
|
1718
|
+
const payload = { drop_extensions: dropExtensions };
|
|
1719
|
+
await api.put(`/v1/connectors/${connector.id}`, payload);
|
|
1720
|
+
trackEvent("CLI: connector update succeeded", {
|
|
1721
|
+
drop_extensions_count: dropExtensions.length
|
|
1722
|
+
});
|
|
1723
|
+
if (dropExtensions.length === 0) {
|
|
1724
|
+
console.log(`\u2713 Cleared extension drop allowlist on '${name}'`);
|
|
1725
|
+
} else {
|
|
1726
|
+
console.log(
|
|
1727
|
+
`\u2713 Updated extension drop allowlist on '${name}': ${dropExtensions.join(", ")}`
|
|
1728
|
+
);
|
|
1729
|
+
console.log(
|
|
1730
|
+
" These extensions will be omitted from future branches created from this connector."
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
if (isPermissionError(err)) {
|
|
1735
|
+
trackEvent("CLI: connector update failed", { reason: "permission_denied" });
|
|
1736
|
+
console.error("\u2717 You don't have permission to update this connector.");
|
|
1737
|
+
console.error("");
|
|
1738
|
+
console.error(" Ask your organization admin to either:");
|
|
1739
|
+
console.error(" \u2022 Update the connector for you, or");
|
|
1740
|
+
console.error(" \u2022 Upgrade your role to Admin");
|
|
1741
|
+
process.exit(1);
|
|
1742
|
+
}
|
|
1743
|
+
if (isNetworkError(err)) {
|
|
1744
|
+
trackEvent("CLI: connector update failed", { reason: "offline" });
|
|
1745
|
+
console.error("\u2717 Cannot update connector while offline");
|
|
1746
|
+
process.exit(1);
|
|
1747
|
+
}
|
|
1748
|
+
trackEvent("CLI: connector update failed", { reason: "api_error" });
|
|
1749
|
+
console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
|
|
1750
|
+
process.exit(1);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1529
1754
|
// src/commands/connector/index.ts
|
|
1530
1755
|
var connectorCommand = new Command2("connector").description("Manage database connectors");
|
|
1531
1756
|
connectorCommand.command("create <type> [url]").description("Create a new connector").option("-n, --name <name>", "Connector name").option("--byoc <provider>", "Bring your own Neon project (e.g. neon)").option("--api-key <key>", "Neon API key (required with --byoc)").option("--project-id <id>", "Neon project ID (required with --byoc)").option(
|
|
@@ -1549,7 +1774,14 @@ connectorCommand.command("switch <name>").description("Switch to a different con
|
|
|
1549
1774
|
connectorCommand.command("retry-setup <name>").description(
|
|
1550
1775
|
"Retry branching engine setup for a connector that didn't finish (status: configuration_verified or validating)"
|
|
1551
1776
|
).action(retrySetupAction);
|
|
1552
|
-
connectorCommand.command("
|
|
1777
|
+
connectorCommand.command("update <name>").description("Update a connector's configuration").option(
|
|
1778
|
+
"--drop-extensions <list>",
|
|
1779
|
+
"Comma-separated list of source extensions to drop on branches (e.g. pgmq,supabase_vault). Pass an empty string to clear the allowlist."
|
|
1780
|
+
).action(updateAction);
|
|
1781
|
+
connectorCommand.command("delete <name>").description("Delete a connector by name").option(
|
|
1782
|
+
"--force",
|
|
1783
|
+
"Skip the in-flight WAL/Kafka drain wait. Any un-replicated changes are abandoned and recorded for operator triage."
|
|
1784
|
+
).action(deleteAction2);
|
|
1553
1785
|
|
|
1554
1786
|
// src/commands/invite/index.ts
|
|
1555
1787
|
import { Command as Command3 } from "commander";
|