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.
Files changed (2) hide show
  1. package/dist/index.js +237 -5
  2. 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
- await api.delete(`/v1/connectors/${connector.id}`);
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:", err instanceof Error ? err.message : err);
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("delete <name>").description("Delete a connector by name").action(deleteAction2);
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "Git for Data infrastructure",
5
5
  "type": "module",
6
6
  "bin": {