ardent-cli 0.0.26 → 0.0.27

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 +270 -75
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -757,29 +757,125 @@ import { Command as Command2 } from "commander";
757
757
  var SUCCESS_ENGINE_STATUSES = /* @__PURE__ */ new Set(["healthy", "degraded"]);
758
758
  var RETRYABLE_ENGINE_STATUSES = /* @__PURE__ */ new Set(["configuration_verified"]);
759
759
  var FAILED_ENGINE_STATUSES = /* @__PURE__ */ new Set(["configuration_failed", "failed_validation"]);
760
- function assertEngineSetupCompleted(operation) {
760
+ var EngineSetupTerminalStatusError = class extends Error {
761
+ kind;
762
+ engineStatus;
763
+ constructor(kind, engineStatus, message) {
764
+ super(message);
765
+ this.name = "EngineSetupTerminalStatusError";
766
+ this.kind = kind;
767
+ this.engineStatus = engineStatus;
768
+ }
769
+ };
770
+ function assertEngineSetupCompleted(operation, connectorName) {
761
771
  const result = operation.result;
762
772
  if (!result) {
763
- throw new Error("Engine setup completed without a result payload.");
773
+ throw new EngineSetupTerminalStatusError(
774
+ "malformed_result",
775
+ null,
776
+ "Engine setup completed without a result payload."
777
+ );
764
778
  }
765
779
  const engineStatus = result.branching_engine_status;
766
780
  if (typeof engineStatus !== "string" || engineStatus.length === 0) {
767
- throw new Error("Engine setup completed without branching_engine_status in the result.");
781
+ throw new EngineSetupTerminalStatusError(
782
+ "malformed_result",
783
+ null,
784
+ "Engine setup completed without branching_engine_status in the result."
785
+ );
768
786
  }
769
787
  if (SUCCESS_ENGINE_STATUSES.has(engineStatus)) {
770
788
  return;
771
789
  }
772
790
  if (RETRYABLE_ENGINE_STATUSES.has(engineStatus)) {
773
- throw new Error(
774
- `Engine setup needs retry (status: ${engineStatus}). Run \`ardent connector list\` to inspect connector state, then retry engine setup.`
791
+ throw new EngineSetupTerminalStatusError(
792
+ "retryable_engine_status",
793
+ engineStatus,
794
+ `Engine setup needs retry (status: ${engineStatus}). Run \`ardent connector retry-setup ${connectorName}\` to retry, or \`ardent connector list\` to inspect connector state first.`
775
795
  );
776
796
  }
777
797
  if (FAILED_ENGINE_STATUSES.has(engineStatus)) {
778
- throw new Error(
798
+ throw new EngineSetupTerminalStatusError(
799
+ "failed_engine_status",
800
+ engineStatus,
779
801
  `Engine setup failed (status: ${engineStatus}). Run \`ardent connector list\` to inspect connector state.`
780
802
  );
781
803
  }
782
- throw new Error(`Engine setup completed with unexpected status: ${engineStatus}.`);
804
+ throw new EngineSetupTerminalStatusError(
805
+ "unexpected_engine_status",
806
+ engineStatus,
807
+ `Engine setup completed with unexpected status: ${engineStatus}.`
808
+ );
809
+ }
810
+
811
+ // src/lib/engine_setup.ts
812
+ var EngineSetupTimeoutError = class extends Error {
813
+ constructor(message) {
814
+ super(message);
815
+ this.name = "EngineSetupTimeoutError";
816
+ }
817
+ };
818
+ var EngineSetupOperationFailedError = class extends Error {
819
+ operationError;
820
+ constructor(operationError, connectorName) {
821
+ super(
822
+ `Engine setup failed: ${operationError ?? "unknown error"}. Inspect connector state with \`ardent connector list\`, then retry with \`ardent connector retry-setup ${connectorName}\` once the underlying issue is resolved.`
823
+ );
824
+ this.name = "EngineSetupOperationFailedError";
825
+ this.operationError = operationError;
826
+ }
827
+ };
828
+ async function runEngineSetupWithPolling(connectorId, connectorName) {
829
+ let dispatch;
830
+ try {
831
+ dispatch = await api.post(
832
+ `/v1/connectors/${connectorId}/engine-setup`,
833
+ {}
834
+ );
835
+ } catch (err) {
836
+ if (isGatewayTimeoutError(err)) {
837
+ throw new Error(
838
+ "Backend dispatch timed out. The engine-setup endpoint is now async and should respond in under a second; this may indicate a backend incident. Try again, or check `ardent connector list` to see if an operation was created anyway."
839
+ );
840
+ }
841
+ throw err;
842
+ }
843
+ const operationId = dispatch?.operation_id;
844
+ if (!operationId) {
845
+ return { dispatched: false };
846
+ }
847
+ const startedAt = Date.now();
848
+ const maxWaitMs = 20 * 60 * 1e3;
849
+ const pollIntervalMs = 5 * 1e3;
850
+ let lastStage = null;
851
+ while (Date.now() - startedAt < maxWaitMs) {
852
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
853
+ let op;
854
+ try {
855
+ op = await api.get(`/v1/operations/${operationId}`);
856
+ } catch (pollErr) {
857
+ if (isGatewayTimeoutError(pollErr)) continue;
858
+ throw pollErr;
859
+ }
860
+ if (op.stage && op.stage !== lastStage) {
861
+ const progressLabel = op.progress != null ? ` (${op.progress}%)` : "";
862
+ console.log(` ${op.stage}${progressLabel}`);
863
+ lastStage = op.stage;
864
+ }
865
+ if (op.status === "completed") {
866
+ assertEngineSetupCompleted(
867
+ { status: op.status, result: op.result },
868
+ connectorName
869
+ );
870
+ return { dispatched: true };
871
+ }
872
+ if (op.status === "failed") {
873
+ throw new EngineSetupOperationFailedError(op.error, connectorName);
874
+ }
875
+ }
876
+ throw new EngineSetupTimeoutError(
877
+ `Engine setup did not complete within ${maxWaitMs / 6e4} minutes. Setup may still be running server-side \u2014 do NOT delete the connector. Check status with: ardent connector list (operation ${operationId})`
878
+ );
783
879
  }
784
880
 
785
881
  // src/lib/onboarding.ts
@@ -892,63 +988,6 @@ Example:
892
988
  }
893
989
 
894
990
  // src/commands/connector/create.ts
895
- var EngineSetupTimeoutError = class extends Error {
896
- constructor(message) {
897
- super(message);
898
- this.name = "EngineSetupTimeoutError";
899
- }
900
- };
901
- async function runEngineSetupWithPolling(connectorId) {
902
- let dispatch;
903
- try {
904
- dispatch = await api.post(
905
- `/v1/connectors/${connectorId}/engine-setup`,
906
- {}
907
- );
908
- } catch (err) {
909
- if (isGatewayTimeoutError(err)) {
910
- throw new Error(
911
- "Backend dispatch timed out. The engine-setup endpoint is now async and should respond in under a second; this may indicate a backend incident. Try again, or check `ardent connector list` to see if an operation was created anyway."
912
- );
913
- }
914
- throw err;
915
- }
916
- const operationId = dispatch?.operation_id;
917
- if (!operationId) {
918
- return;
919
- }
920
- const startedAt = Date.now();
921
- const maxWaitMs = 20 * 60 * 1e3;
922
- const pollIntervalMs = 5 * 1e3;
923
- let lastStage = null;
924
- while (Date.now() - startedAt < maxWaitMs) {
925
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
926
- let op;
927
- try {
928
- op = await api.get(`/v1/operations/${operationId}`);
929
- } catch (pollErr) {
930
- if (isGatewayTimeoutError(pollErr)) continue;
931
- throw pollErr;
932
- }
933
- if (op.stage && op.stage !== lastStage) {
934
- const progressLabel = op.progress != null ? ` (${op.progress}%)` : "";
935
- console.log(` ${op.stage}${progressLabel}`);
936
- lastStage = op.stage;
937
- }
938
- if (op.status === "completed") {
939
- assertEngineSetupCompleted({ status: op.status, result: op.result });
940
- return;
941
- }
942
- if (op.status === "failed") {
943
- throw new Error(
944
- `Engine setup failed: ${op.error ?? "unknown error"}. Run \`ardent connector list\` to inspect connector state, or re-trigger setup once the underlying issue is resolved.`
945
- );
946
- }
947
- }
948
- throw new EngineSetupTimeoutError(
949
- `Engine setup did not complete within ${maxWaitMs / 6e4} minutes. Setup may still be running server-side \u2014 do NOT delete the connector. Check status with: ardent connector list (operation ${operationId})`
950
- );
951
- }
952
991
  function parsePostgresUrl(url) {
953
992
  const atIndex = url.lastIndexOf("@");
954
993
  const credentialsPart = atIndex > 0 ? url.substring(0, atIndex) : url;
@@ -963,6 +1002,11 @@ function parsePostgresUrl(url) {
963
1002
  if (!username) throw new Error("Username required in connection URL");
964
1003
  return { host, port, username, password };
965
1004
  }
1005
+ function printEngineSetupRecoveryHint(connectorName) {
1006
+ console.error("\u2717 Engine setup did not complete for this connector.");
1007
+ console.error(" Inspect: ardent connector list");
1008
+ console.error(` Retry: ardent connector retry-setup ${connectorName}`);
1009
+ }
966
1010
  async function createAction2(type, url, options) {
967
1011
  const supportedTypes = ["postgresql"];
968
1012
  if (!supportedTypes.includes(type.toLowerCase())) {
@@ -1073,11 +1117,10 @@ async function createAction2(type, url, options) {
1073
1117
  if (isByoc) {
1074
1118
  console.log("Setting up branching engine...");
1075
1119
  try {
1076
- await runEngineSetupWithPolling(connectorId);
1120
+ await runEngineSetupWithPolling(connectorId, connectorName);
1077
1121
  } catch (setupErr) {
1078
1122
  if (!(setupErr instanceof EngineSetupTimeoutError)) {
1079
- console.error("\u2717 Engine setup failed. To retry, delete and recreate:");
1080
- console.error(` ardent connector delete ${connectorName}`);
1123
+ printEngineSetupRecoveryHint(connectorName);
1081
1124
  }
1082
1125
  throw setupErr;
1083
1126
  }
@@ -1104,7 +1147,14 @@ async function createAction2(type, url, options) {
1104
1147
  const connector = await api.get(`/v1/connectors/${connectorId}`);
1105
1148
  if (connector.branching_engine_status === "configuration_verified") {
1106
1149
  console.log("Setting up branching engine...");
1107
- await runEngineSetupWithPolling(connectorId);
1150
+ try {
1151
+ await runEngineSetupWithPolling(connectorId, connectorName);
1152
+ } catch (setupErr) {
1153
+ if (!(setupErr instanceof EngineSetupTimeoutError)) {
1154
+ printEngineSetupRecoveryHint(connectorName);
1155
+ }
1156
+ throw setupErr;
1157
+ }
1108
1158
  }
1109
1159
  }
1110
1160
  const finalConnector = await api.get(`/v1/connectors/${connectorId}`);
@@ -1112,7 +1162,9 @@ async function createAction2(type, url, options) {
1112
1162
  id: connectorId,
1113
1163
  name: connectorName,
1114
1164
  service_name: "postgresql",
1115
- status: finalConnector.branching_engine_status ?? finalConnector.connection_status ?? "pending"
1165
+ status: finalConnector.branching_engine_status ?? finalConnector.connection_status ?? "pending",
1166
+ branching_engine_status: finalConnector.branching_engine_status ?? null,
1167
+ connection_error: finalConnector.connection_error ?? null
1116
1168
  };
1117
1169
  const cached = getCacheEntry("connectors");
1118
1170
  const cachedConnectors = cached?.data || [];
@@ -1144,6 +1196,34 @@ async function createAction2(type, url, options) {
1144
1196
  }
1145
1197
  }
1146
1198
 
1199
+ // src/lib/connector_render.ts
1200
+ var GREEN = "\x1B[32m";
1201
+ var CYAN = "\x1B[36m";
1202
+ var RED = "\x1B[31m";
1203
+ function pendingHint(state, connectorName) {
1204
+ if (state === "validating") {
1205
+ return `engine setup running \u2014 re-run \`ardent connector list\` to refresh, or \`ardent connector retry-setup ${connectorName}\` if it stays this way`;
1206
+ }
1207
+ return `engine setup not finished \u2014 run: ardent connector retry-setup ${connectorName}`;
1208
+ }
1209
+ var ENGINE_PENDING_STATES = /* @__PURE__ */ new Set(["configuration_verified", "validating"]);
1210
+ function renderConnectorIcon(connector) {
1211
+ const engine = connector.branching_engine_status;
1212
+ const statusAllowsEnginePending = connector.status === "healthy" || ENGINE_PENDING_STATES.has(connector.status);
1213
+ if (engine && ENGINE_PENDING_STATES.has(engine) && statusAllowsEnginePending) {
1214
+ return {
1215
+ kind: "engine_pending",
1216
+ icon: "\u25D0",
1217
+ color: CYAN,
1218
+ hint: pendingHint(engine, connector.name)
1219
+ };
1220
+ }
1221
+ if (connector.status === "healthy") {
1222
+ return { kind: "ready", icon: "\u25CF", color: GREEN };
1223
+ }
1224
+ return { kind: "broken", icon: "\u25CB", color: RED };
1225
+ }
1226
+
1147
1227
  // src/commands/connector/list.ts
1148
1228
  async function listAction2() {
1149
1229
  let connectors = [];
@@ -1198,23 +1278,28 @@ async function listAction2() {
1198
1278
  const green2 = "\x1B[32m";
1199
1279
  const dim2 = "\x1B[2m";
1200
1280
  const yellow = "\x1B[33m";
1201
- const reset2 = "\x1B[0m";
1202
1281
  const red = "\x1B[31m";
1282
+ const reset2 = "\x1B[0m";
1203
1283
  console.log("Connectors:\n");
1284
+ let enginePendingCount = 0;
1204
1285
  for (const connector of connectors) {
1205
1286
  const isCurrent = connector.id === currentConnectorId;
1206
- const icon = connector.status === "healthy" ? "\u25CF" : "\u25CB";
1287
+ const render = renderConnectorIcon(connector);
1288
+ if (render.kind === "engine_pending") enginePendingCount += 1;
1207
1289
  const warnings = connector.warnings ?? [];
1208
1290
  const warningSuffix = warnings.length > 0 ? ` ${yellow}\u26A0 ${warnings.length}${reset2}` : "";
1209
- const statusSuffix = connector.status === "healthy" ? "" : ` ${red}[${connector.status}]${reset2}`;
1291
+ const statusSuffix = render.kind === "ready" ? "" : ` ${render.color}[${connector.status}]${reset2}`;
1292
+ const nameLine = isCurrent ? `${green2}* ${render.color}${render.icon}${green2} ${connector.name}${reset2}${warningSuffix}${statusSuffix}` : ` ${render.color}${render.icon}${reset2} ${connector.name}${warningSuffix}${statusSuffix}`;
1293
+ console.log(nameLine);
1210
1294
  if (isCurrent) {
1211
- console.log(`${green2}* ${icon} ${connector.name}${reset2}${warningSuffix}${statusSuffix}`);
1212
1295
  console.log(`${green2} ${connector.service_name}${reset2}`);
1213
1296
  } else {
1214
- console.log(` ${icon} ${connector.name}${warningSuffix}${statusSuffix}`);
1215
1297
  console.log(`${dim2} ${connector.service_name}${reset2}`);
1216
1298
  }
1217
- if (connector.status !== "healthy" && connector.connection_error) {
1299
+ if (render.kind === "engine_pending") {
1300
+ console.log(`${render.color} ${render.hint}${reset2}`);
1301
+ }
1302
+ if (render.kind === "broken" && connector.connection_error) {
1218
1303
  for (const line of connector.connection_error.split("\n")) {
1219
1304
  if (line.trim().length === 0) continue;
1220
1305
  console.log(`${red} ${line}${reset2}`);
@@ -1225,6 +1310,12 @@ async function listAction2() {
1225
1310
  }
1226
1311
  console.log();
1227
1312
  }
1313
+ if (enginePendingCount > 0) {
1314
+ trackEvent("CLI: connector list rendered engine-pending", {
1315
+ engine_pending_count: enginePendingCount,
1316
+ connector_count: connectors.length
1317
+ });
1318
+ }
1228
1319
  }
1229
1320
 
1230
1321
  // src/commands/connector/delete.ts
@@ -1282,6 +1373,106 @@ async function deleteAction2(name) {
1282
1373
  }
1283
1374
  }
1284
1375
 
1376
+ // src/commands/connector/retry-setup.ts
1377
+ function connectorRetrySetupFailureTelemetry(err) {
1378
+ if (isPermissionError(err)) {
1379
+ return { reason: "permission_denied" };
1380
+ }
1381
+ if (err instanceof EngineSetupTimeoutError) {
1382
+ return { reason: "polling_timeout" };
1383
+ }
1384
+ if (err instanceof EngineSetupOperationFailedError) {
1385
+ return {
1386
+ reason: "operation_failed",
1387
+ operation_error: err.operationError ?? "unknown error"
1388
+ };
1389
+ }
1390
+ if (err instanceof EngineSetupTerminalStatusError) {
1391
+ return {
1392
+ reason: err.kind,
1393
+ engine_status: err.engineStatus
1394
+ };
1395
+ }
1396
+ return { reason: "api_error" };
1397
+ }
1398
+ async function retrySetupAction(name) {
1399
+ const currentProjectId = getConfig("currentProjectId");
1400
+ if (!currentProjectId) {
1401
+ console.error("\u2717 No current project set. Switch to a project first:");
1402
+ console.error(" ardent project list");
1403
+ console.error(" ardent project switch <name>");
1404
+ process.exit(1);
1405
+ }
1406
+ let connector;
1407
+ try {
1408
+ const result = await api.get(
1409
+ `/v1/cli/connectors?project_id=${currentProjectId}`
1410
+ );
1411
+ if (!result.connectors) {
1412
+ throw new Error("API returned invalid response: missing connectors array");
1413
+ }
1414
+ setCacheEntry("connectors", result.connectors);
1415
+ connector = result.connectors.find((candidate) => candidate.name === name);
1416
+ } catch (err) {
1417
+ if (isNetworkError(err)) {
1418
+ console.error("\u2717 Cannot run connector retry-setup while offline");
1419
+ process.exit(1);
1420
+ }
1421
+ throw err;
1422
+ }
1423
+ if (!connector) {
1424
+ console.error(`\u2717 Connector "${name}" not found`);
1425
+ console.log(" Run: ardent connector list");
1426
+ process.exit(1);
1427
+ }
1428
+ trackEvent("CLI: connector retry-setup initiated", {
1429
+ connector_id: connector.id,
1430
+ starting_engine_status: connector.branching_engine_status ?? null
1431
+ });
1432
+ console.log(`Setting up branching engine for ${name}...`);
1433
+ try {
1434
+ const { dispatched } = await runEngineSetupWithPolling(connector.id, name);
1435
+ if (dispatched) {
1436
+ trackEvent("CLI: connector retry-setup dispatched", {
1437
+ connector_id: connector.id
1438
+ });
1439
+ }
1440
+ trackEvent("CLI: connector retry-setup succeeded", {
1441
+ connector_id: connector.id,
1442
+ dispatched
1443
+ });
1444
+ try {
1445
+ const refreshed = await api.get(
1446
+ `/v1/cli/connectors?project_id=${currentProjectId}`
1447
+ );
1448
+ if (refreshed.connectors) {
1449
+ setCacheEntry("connectors", refreshed.connectors);
1450
+ }
1451
+ } catch {
1452
+ }
1453
+ if (!dispatched) {
1454
+ console.log("\u2713 Engine already set up \u2014 no work needed");
1455
+ } else {
1456
+ console.log("\u2713 Engine setup complete");
1457
+ }
1458
+ } catch (err) {
1459
+ const failureTelemetry = connectorRetrySetupFailureTelemetry(err);
1460
+ if (isPermissionError(err)) {
1461
+ trackEvent("CLI: connector retry-setup failed", failureTelemetry);
1462
+ console.error("\u2717 You don't have permission to run engine setup on this connector.");
1463
+ process.exit(1);
1464
+ }
1465
+ if (err instanceof EngineSetupTimeoutError) {
1466
+ trackEvent("CLI: connector retry-setup failed", failureTelemetry);
1467
+ console.error(`\u2717 ${err.message}`);
1468
+ process.exit(1);
1469
+ }
1470
+ trackEvent("CLI: connector retry-setup failed", failureTelemetry);
1471
+ console.error("\u2717 Failed:", err instanceof Error ? err.message : err);
1472
+ process.exit(1);
1473
+ }
1474
+ }
1475
+
1285
1476
  // src/commands/connector/switch.ts
1286
1477
  async function switchAction2(name) {
1287
1478
  const cached = getCacheEntry("connectors");
@@ -1355,6 +1546,9 @@ connectorCommand.command("create <type> [url]").description("Create a new connec
1355
1546
  ).action(createAction2);
1356
1547
  connectorCommand.command("list").description("List your connectors").action(listAction2);
1357
1548
  connectorCommand.command("switch <name>").description("Switch to a different connector").action(switchAction2);
1549
+ connectorCommand.command("retry-setup <name>").description(
1550
+ "Retry branching engine setup for a connector that didn't finish (status: configuration_verified or validating)"
1551
+ ).action(retrySetupAction);
1358
1552
  connectorCommand.command("delete <name>").description("Delete a connector by name").action(deleteAction2);
1359
1553
 
1360
1554
  // src/commands/invite/index.ts
@@ -2283,6 +2477,7 @@ CONNECTORS
2283
2477
  connector create Connect a database (postgresql, snowflake, etc.)
2284
2478
  connector list List your connectors (* = current)
2285
2479
  connector switch Switch to a different connector
2480
+ connector retry-setup Retry branching engine setup for a connector
2286
2481
  connector delete Delete a connector
2287
2482
 
2288
2483
  BRANCHES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "description": "Git for Data infrastructure",
5
5
  "type": "module",
6
6
  "bin": {