ardent-cli 0.0.29 → 0.0.31

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 +450 -8
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -762,6 +762,42 @@ branchCommand.command("diff [name]", { hidden: true }).description("(removed) Sh
762
762
  // src/commands/connector/index.ts
763
763
  import { Command as Command2 } from "commander";
764
764
 
765
+ // src/lib/connector_warnings.ts
766
+ var YELLOW = "\x1B[33m";
767
+ var RESET = "\x1B[0m";
768
+ async function fetchConnectorWarnings(projectId, connectorId) {
769
+ const listed = await api.get(
770
+ `/v1/cli/connectors?project_id=${encodeURIComponent(projectId)}`
771
+ );
772
+ if (!listed.connectors) {
773
+ throw new Error("connector list response missing connectors array");
774
+ }
775
+ for (const connector of listed.connectors) {
776
+ if (connector.id === connectorId) {
777
+ return connector.warnings ?? [];
778
+ }
779
+ }
780
+ throw new Error(
781
+ `connector ${connectorId} absent from /v1/cli/connectors response`
782
+ );
783
+ }
784
+ function printDegradedWarnings(warnings) {
785
+ if (warnings.length > 0) {
786
+ console.log(
787
+ `${YELLOW}\u26A0 This connector is degraded \u2014 some source objects are excluded from branches:${RESET}`
788
+ );
789
+ for (const warning of warnings) {
790
+ console.log(`${YELLOW} \u2022 ${warning}${RESET}`);
791
+ }
792
+ } else {
793
+ console.log(
794
+ `${YELLOW}\u26A0 This connector is degraded \u2014 some source objects are excluded from branches.${RESET}`
795
+ );
796
+ }
797
+ console.log("");
798
+ console.log(" Review this connector with: ardent connector list");
799
+ }
800
+
765
801
  // src/lib/engine_setup_result.ts
766
802
  var SUCCESS_ENGINE_STATUSES = /* @__PURE__ */ new Set(["healthy", "degraded"]);
767
803
  var RETRYABLE_ENGINE_STATUSES = /* @__PURE__ */ new Set(["configuration_verified"]);
@@ -1042,6 +1078,349 @@ async function multiSelect(items, promptForItem, options = {}) {
1042
1078
  return accepted;
1043
1079
  }
1044
1080
 
1081
+ // src/lib/replica_identity_prompt.ts
1082
+ import { createInterface as createInterface2 } from "readline/promises";
1083
+
1084
+ // src/lib/replica_identity_decisions.ts
1085
+ import { readFileSync as readFileSync3 } from "fs";
1086
+ var REPLICA_IDENTITY_DECISIONS = [
1087
+ "exclude",
1088
+ "add_pk",
1089
+ "replica_identity_full"
1090
+ ];
1091
+ function isReplicaIdentityDecision(value) {
1092
+ return typeof value === "string" && REPLICA_IDENTITY_DECISIONS.includes(value);
1093
+ }
1094
+ function loadDecisionsFromFile(path) {
1095
+ let raw;
1096
+ try {
1097
+ raw = readFileSync3(path, "utf-8");
1098
+ } catch (err) {
1099
+ throw new Error(
1100
+ `Could not read replica-identity decisions file ${path}: ${err instanceof Error ? err.message : String(err)}`
1101
+ );
1102
+ }
1103
+ let parsed;
1104
+ try {
1105
+ parsed = JSON.parse(raw);
1106
+ } catch (err) {
1107
+ throw new Error(
1108
+ `Replica-identity decisions file ${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
1109
+ );
1110
+ }
1111
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1112
+ throw new Error(
1113
+ `Replica-identity decisions file ${path} must be a JSON object mapping "database.schema.table" to a decision.`
1114
+ );
1115
+ }
1116
+ const decisions = {};
1117
+ for (const [key, value] of Object.entries(parsed)) {
1118
+ const parts = key.split(".");
1119
+ if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
1120
+ throw new Error(
1121
+ `Replica-identity decisions file ${path}: key ${JSON.stringify(
1122
+ key
1123
+ )} must be of form "database.schema.table" with three non-empty components.`
1124
+ );
1125
+ }
1126
+ if (!isReplicaIdentityDecision(value)) {
1127
+ throw new Error(
1128
+ `Replica-identity decisions file ${path}: value for ${JSON.stringify(
1129
+ key
1130
+ )} must be one of ${REPLICA_IDENTITY_DECISIONS.join(
1131
+ ", "
1132
+ )} (got ${JSON.stringify(value)}).`
1133
+ );
1134
+ }
1135
+ decisions[key] = value;
1136
+ }
1137
+ return decisions;
1138
+ }
1139
+ function validateDecisionsAgainstPreflight(decisions, preflight) {
1140
+ const known = new Set(
1141
+ preflight.no_replication_identity_tables.map((row) => row.fqn)
1142
+ );
1143
+ const unknown = Object.keys(decisions).filter((fqn) => !known.has(fqn)).sort();
1144
+ if (unknown.length > 0) {
1145
+ throw new Error(
1146
+ "Replica-identity decisions reference tables that are not in this connector's current preflight list: " + unknown.join(", ") + ".\n Re-run `ardent connector list` to refresh, then re-submit using FQNs from the connector's preflight (key shape: database.schema.table)."
1147
+ );
1148
+ }
1149
+ const missing = preflight.no_replication_identity_tables.map((row) => row.fqn).filter((fqn) => !Object.prototype.hasOwnProperty.call(decisions, fqn)).sort();
1150
+ if (missing.length > 0) {
1151
+ throw new Error(
1152
+ "Replica-identity decisions file is incomplete. It must include a decision for every table in this connector's current preflight list. Missing: " + missing.join(", ") + ".\n Add those FQNs to the file, or re-run with --accept-replica-identity-defaults to explicitly keep existing choices and exclude undecided tables."
1153
+ );
1154
+ }
1155
+ }
1156
+ function buildDefaultedDecisions(preflight) {
1157
+ const decisions = {};
1158
+ for (const row of preflight.no_replication_identity_tables) {
1159
+ decisions[row.fqn] = row.current_decision;
1160
+ }
1161
+ return decisions;
1162
+ }
1163
+ function formatRowCountEstimate(value) {
1164
+ if (value === null || value < 0) return "unknown";
1165
+ return value.toLocaleString("en-US");
1166
+ }
1167
+
1168
+ // src/lib/replica_identity_prompt.ts
1169
+ var DECISION_DESCRIPTIONS = {
1170
+ exclude: "Exclude the table from replication. Branches will not carry rows for this table.",
1171
+ add_pk: "After adding a PRIMARY KEY on source, keep this table in replication. Setup checks the source before replication starts.",
1172
+ replica_identity_full: "You will run ALTER TABLE ... REPLICA IDENTITY FULL on source. Increases WAL volume on UPDATE/DELETE for this table."
1173
+ };
1174
+ function isInteractive() {
1175
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
1176
+ }
1177
+ function logPreflightTable(rows) {
1178
+ console.log("");
1179
+ const noun = rows.length === 1 ? "table" : "tables";
1180
+ console.log(
1181
+ `Found ${rows.length} ${noun} on your source database with no replication identity:`
1182
+ );
1183
+ for (const row of rows) {
1184
+ const estimate = formatRowCountEstimate(row.row_count_estimate);
1185
+ console.log(
1186
+ ` - ${row.fqn} (~${estimate} rows, currently: ${row.current_decision})`
1187
+ );
1188
+ }
1189
+ console.log("");
1190
+ console.log(
1191
+ "Replication needs a PRIMARY KEY, a UNIQUE NOT NULL column, or REPLICA IDENTITY FULL"
1192
+ );
1193
+ console.log(
1194
+ "to carry UPDATE/DELETE rows. Tables without any of these cannot be replicated."
1195
+ );
1196
+ console.log("Ardent never runs source-side DDL on your behalf.");
1197
+ console.log("");
1198
+ }
1199
+ async function promptForDecision(rl, row, index, total) {
1200
+ const estimate = formatRowCountEstimate(row.row_count_estimate);
1201
+ console.log(
1202
+ `[${index + 1}/${total}] ${row.fqn} (~${estimate} rows, currently: ${row.current_decision})`
1203
+ );
1204
+ for (let optionIndex = 0; optionIndex < REPLICA_IDENTITY_DECISIONS.length; optionIndex += 1) {
1205
+ const option = REPLICA_IDENTITY_DECISIONS[optionIndex];
1206
+ console.log(
1207
+ ` ${optionIndex + 1}) ${option} -- ${DECISION_DESCRIPTIONS[option]}`
1208
+ );
1209
+ }
1210
+ const defaultIndex = REPLICA_IDENTITY_DECISIONS.indexOf(row.current_decision);
1211
+ while (true) {
1212
+ const raw = await rl.question(
1213
+ `Choose [1-${REPLICA_IDENTITY_DECISIONS.length}, default=${defaultIndex + 1}]: `
1214
+ );
1215
+ const answer = raw.trim();
1216
+ if (answer === "") return row.current_decision;
1217
+ const numeric = Number.parseInt(answer, 10);
1218
+ if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= REPLICA_IDENTITY_DECISIONS.length) {
1219
+ return REPLICA_IDENTITY_DECISIONS[numeric - 1];
1220
+ }
1221
+ if (REPLICA_IDENTITY_DECISIONS.includes(answer)) {
1222
+ return answer;
1223
+ }
1224
+ console.log(
1225
+ `Unrecognized response. Enter 1-${REPLICA_IDENTITY_DECISIONS.length} or one of: ${REPLICA_IDENTITY_DECISIONS.join(", ")}.`
1226
+ );
1227
+ }
1228
+ }
1229
+ async function runInteractivePrompt(rows) {
1230
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1231
+ try {
1232
+ const decisions = {};
1233
+ for (let index = 0; index < rows.length; index += 1) {
1234
+ const row = rows[index];
1235
+ decisions[row.fqn] = await promptForDecision(rl, row, index, rows.length);
1236
+ console.log("");
1237
+ }
1238
+ return decisions;
1239
+ } finally {
1240
+ rl.close();
1241
+ }
1242
+ }
1243
+ function logAcceptDefaults(rows, decisions) {
1244
+ logPreflightTable(rows);
1245
+ console.log(
1246
+ "Saving the current/default decision for every table above. Undecided tables will be excluded from replication."
1247
+ );
1248
+ for (const fqn of Object.keys(decisions).sort()) {
1249
+ console.log(` - ${fqn} -> ${decisions[fqn]}`);
1250
+ }
1251
+ console.log("");
1252
+ }
1253
+ function logSubmissionDisallowed() {
1254
+ console.error("");
1255
+ console.error(
1256
+ "\u2717 Replica-identity decisions cannot be changed while setup is already in progress."
1257
+ );
1258
+ console.error(
1259
+ " Wait for the current setup attempt to finish, then retry setup if the connector still shows setup pending."
1260
+ );
1261
+ console.error(" Contact Ardent support if the connector stays validating.");
1262
+ }
1263
+ function logNonInteractiveRequired(decisionsNeeded) {
1264
+ const noun = decisionsNeeded === 1 ? "table" : "tables";
1265
+ const verb = decisionsNeeded === 1 ? "has" : "have";
1266
+ console.error("");
1267
+ console.error(
1268
+ `\u2717 ${decisionsNeeded} ${noun} on your source database ${verb} no replication identity. A decision is required before setup can continue.`
1269
+ );
1270
+ console.error("");
1271
+ console.error(" This shell is not interactive. Re-run with either:");
1272
+ console.error(
1273
+ ' --replica-identity-decisions <path> Complete JSON file: { "database.schema.table": "exclude|add_pk|replica_identity_full" }'
1274
+ );
1275
+ console.error(
1276
+ " --accept-replica-identity-defaults explicitly keep current choices and exclude undecided tables"
1277
+ );
1278
+ }
1279
+ function renderReplicaIdentityFullSql(preflight) {
1280
+ const sql = (preflight?.replica_identity_full_sql ?? "").trim();
1281
+ if (sql.length === 0) return;
1282
+ console.log("");
1283
+ console.log(
1284
+ "Run the following on your source database before the next snapshot:"
1285
+ );
1286
+ console.log("");
1287
+ console.log(sql);
1288
+ console.log("");
1289
+ }
1290
+ async function submitDecisions(connectorId, decisions) {
1291
+ const response = await api.put(
1292
+ `/v1/connectors/${connectorId}/replica-identity-decisions`,
1293
+ { decisions }
1294
+ );
1295
+ return response.replica_identity_preflight ?? null;
1296
+ }
1297
+ function summarizeDecisions(decisions) {
1298
+ const summary = {
1299
+ decisions_total: Object.keys(decisions).length,
1300
+ decisions_exclude: 0,
1301
+ decisions_add_pk: 0,
1302
+ decisions_replica_identity_full: 0,
1303
+ decisions_unknown: 0
1304
+ };
1305
+ for (const value of Object.values(decisions)) {
1306
+ if (value === "exclude") {
1307
+ summary.decisions_exclude += 1;
1308
+ } else if (value === "add_pk") {
1309
+ summary.decisions_add_pk += 1;
1310
+ } else if (value === "replica_identity_full") {
1311
+ summary.decisions_replica_identity_full += 1;
1312
+ } else {
1313
+ summary.decisions_unknown += 1;
1314
+ }
1315
+ }
1316
+ return summary;
1317
+ }
1318
+ async function handleReplicaIdentityPreflight(connectorId, options, behavior = {}) {
1319
+ const allowDecisionSubmission = behavior.allowDecisionSubmission ?? true;
1320
+ if (options.replicaIdentityDecisions && options.acceptReplicaIdentityDefaults) {
1321
+ console.error(
1322
+ "\u2717 --replica-identity-decisions and --accept-replica-identity-defaults are mutually exclusive."
1323
+ );
1324
+ console.error(" Pass one or the other.");
1325
+ process.exit(1);
1326
+ }
1327
+ let connector;
1328
+ try {
1329
+ connector = await api.get(
1330
+ `/v1/connectors/${connectorId}`
1331
+ );
1332
+ } catch (err) {
1333
+ console.error(
1334
+ "\u2717 Could not fetch connector for replica-identity preflight:",
1335
+ err instanceof Error ? err.message : String(err)
1336
+ );
1337
+ process.exit(1);
1338
+ }
1339
+ const preflight = connector.replica_identity_preflight ?? null;
1340
+ if (!preflight) {
1341
+ return { submitted: false, preflight: null };
1342
+ }
1343
+ if (preflight.status === "unavailable") {
1344
+ trackEvent("CLI: replica identity preflight unavailable", {
1345
+ connector_id: connectorId
1346
+ });
1347
+ console.error("");
1348
+ console.error(
1349
+ "\u2717 Replica identity preflight is unavailable for this connector. Contact Ardent support."
1350
+ );
1351
+ process.exit(1);
1352
+ }
1353
+ if (preflight.decisions_needed === 0) {
1354
+ return { submitted: false, preflight };
1355
+ }
1356
+ const allDecisionsRecorded = preflight.decisions_recorded >= preflight.decisions_needed;
1357
+ if (allDecisionsRecorded && !options.replicaIdentityDecisions && !options.acceptReplicaIdentityDefaults) {
1358
+ renderReplicaIdentityFullSql(preflight);
1359
+ return { submitted: false, preflight };
1360
+ }
1361
+ if (!allowDecisionSubmission) {
1362
+ logSubmissionDisallowed();
1363
+ process.exit(1);
1364
+ }
1365
+ let decisions;
1366
+ let submissionMode;
1367
+ if (options.replicaIdentityDecisions) {
1368
+ try {
1369
+ decisions = loadDecisionsFromFile(options.replicaIdentityDecisions);
1370
+ validateDecisionsAgainstPreflight(decisions, preflight);
1371
+ } catch (err) {
1372
+ console.error(
1373
+ "\u2717",
1374
+ err instanceof Error ? err.message : String(err)
1375
+ );
1376
+ process.exit(1);
1377
+ }
1378
+ submissionMode = "file";
1379
+ } else if (options.acceptReplicaIdentityDefaults) {
1380
+ decisions = buildDefaultedDecisions(preflight);
1381
+ logAcceptDefaults(preflight.no_replication_identity_tables, decisions);
1382
+ submissionMode = "defaults";
1383
+ } else if (isInteractive()) {
1384
+ logPreflightTable(preflight.no_replication_identity_tables);
1385
+ decisions = await runInteractivePrompt(
1386
+ preflight.no_replication_identity_tables
1387
+ );
1388
+ submissionMode = "interactive";
1389
+ } else {
1390
+ trackEvent("CLI: replica identity preflight non_interactive_required", {
1391
+ connector_id: connectorId,
1392
+ decisions_needed: preflight.decisions_needed
1393
+ });
1394
+ logNonInteractiveRequired(preflight.decisions_needed);
1395
+ process.exit(1);
1396
+ }
1397
+ let refreshed;
1398
+ try {
1399
+ refreshed = await submitDecisions(connectorId, decisions);
1400
+ } catch (err) {
1401
+ trackEvent("CLI: replica identity preflight submission failed", {
1402
+ connector_id: connectorId,
1403
+ mode: submissionMode
1404
+ });
1405
+ console.error(
1406
+ "\u2717 Failed to save replica-identity decisions:",
1407
+ err instanceof Error ? err.message : String(err)
1408
+ );
1409
+ process.exit(1);
1410
+ }
1411
+ trackEvent("CLI: replica identity preflight submitted", {
1412
+ connector_id: connectorId,
1413
+ mode: submissionMode,
1414
+ ...summarizeDecisions(decisions)
1415
+ });
1416
+ renderReplicaIdentityFullSql(refreshed);
1417
+ const total = Object.keys(decisions).length;
1418
+ console.log(
1419
+ `\u2713 Replica-identity decisions saved (${total} ${total === 1 ? "table" : "tables"}).`
1420
+ );
1421
+ return { submitted: true, preflight: refreshed };
1422
+ }
1423
+
1045
1424
  // src/commands/connector/create.ts
1046
1425
  function parsePostgresUrl(url) {
1047
1426
  const atIndex = url.lastIndexOf("@");
@@ -1220,7 +1599,15 @@ async function createAction2(type, url, options) {
1220
1599
  }
1221
1600
  const created = await api.post("/v1/connectors", createPayload);
1222
1601
  const connectorId = created.id;
1602
+ const replicaIdentityPreflightOptions = {
1603
+ replicaIdentityDecisions: options.replicaIdentityDecisions,
1604
+ acceptReplicaIdentityDefaults: options.acceptReplicaIdentityDefaults
1605
+ };
1223
1606
  if (isByoc) {
1607
+ await handleReplicaIdentityPreflight(
1608
+ connectorId,
1609
+ replicaIdentityPreflightOptions
1610
+ );
1224
1611
  console.log("Setting up branching engine...");
1225
1612
  try {
1226
1613
  await runEngineSetupWithPolling(connectorId, connectorName);
@@ -1282,6 +1669,10 @@ async function createAction2(type, url, options) {
1282
1669
  }
1283
1670
  }
1284
1671
  if (connector.branching_engine_status === "configuration_verified") {
1672
+ await handleReplicaIdentityPreflight(
1673
+ connectorId,
1674
+ replicaIdentityPreflightOptions
1675
+ );
1285
1676
  console.log("Setting up branching engine...");
1286
1677
  try {
1287
1678
  await runEngineSetupWithPolling(connectorId, connectorName);
@@ -1308,13 +1699,30 @@ async function createAction2(type, url, options) {
1308
1699
  setCacheEntry("connectors", cachedConnectors);
1309
1700
  setConfig("currentConnectorId", connectorId);
1310
1701
  setConfig("currentConnectorName", connectorName);
1702
+ const isDegraded = finalConnector.branching_engine_status === "degraded";
1311
1703
  trackEvent("CLI: connector create succeeded", {
1312
1704
  db_type: type,
1313
1705
  byoc: isByoc,
1314
- deployment_model: deploymentModel
1706
+ deployment_model: deploymentModel,
1707
+ degraded: isDegraded
1315
1708
  });
1316
- console.log("\u2713 Connector created and ready");
1317
- console.log(` ID: ${connectorId}`);
1709
+ if (isDegraded) {
1710
+ console.log("\u2713 Connector created");
1711
+ console.log(` ID: ${connectorId}`);
1712
+ console.log("");
1713
+ let warnings = [];
1714
+ try {
1715
+ warnings = await fetchConnectorWarnings(currentProjectId, connectorId);
1716
+ } catch (warningsErr) {
1717
+ trackEvent("CLI: connector create degraded warnings unavailable", {
1718
+ reason: warningsErr instanceof Error ? warningsErr.message : "unknown"
1719
+ });
1720
+ }
1721
+ printDegradedWarnings(warnings);
1722
+ } else {
1723
+ console.log("\u2713 Connector created and ready");
1724
+ console.log(` ID: ${connectorId}`);
1725
+ }
1318
1726
  showNextStep();
1319
1727
  } catch (err) {
1320
1728
  if (isPermissionError(err)) {
@@ -1334,6 +1742,7 @@ async function createAction2(type, url, options) {
1334
1742
 
1335
1743
  // src/lib/connector_render.ts
1336
1744
  var GREEN = "\x1B[32m";
1745
+ var YELLOW2 = "\x1B[33m";
1337
1746
  var CYAN = "\x1B[36m";
1338
1747
  var RED = "\x1B[31m";
1339
1748
  function pendingHint(state, connectorName) {
@@ -1357,6 +1766,9 @@ function renderConnectorIcon(connector) {
1357
1766
  if (connector.status === "healthy") {
1358
1767
  return { kind: "ready", icon: "\u25CF", color: GREEN };
1359
1768
  }
1769
+ if (connector.status === "degraded") {
1770
+ return { kind: "degraded", icon: "\u25CF", color: YELLOW2 };
1771
+ }
1360
1772
  return { kind: "broken", icon: "\u25CB", color: RED };
1361
1773
  }
1362
1774
 
@@ -1540,7 +1952,7 @@ function connectorRetrySetupFailureTelemetry(err) {
1540
1952
  }
1541
1953
  return { reason: "api_error" };
1542
1954
  }
1543
- async function retrySetupAction(name) {
1955
+ async function retrySetupAction(name, options = {}) {
1544
1956
  const currentProjectId = getConfig("currentProjectId");
1545
1957
  if (!currentProjectId) {
1546
1958
  console.error("\u2717 No current project set. Switch to a project first:");
@@ -1574,6 +1986,16 @@ async function retrySetupAction(name) {
1574
1986
  connector_id: connector.id,
1575
1987
  starting_engine_status: connector.branching_engine_status ?? null
1576
1988
  });
1989
+ await handleReplicaIdentityPreflight(
1990
+ connector.id,
1991
+ {
1992
+ replicaIdentityDecisions: options.replicaIdentityDecisions,
1993
+ acceptReplicaIdentityDefaults: options.acceptReplicaIdentityDefaults
1994
+ },
1995
+ {
1996
+ allowDecisionSubmission: connector.branching_engine_status !== "validating"
1997
+ }
1998
+ );
1577
1999
  console.log(`Setting up branching engine for ${name}...`);
1578
2000
  try {
1579
2001
  const { dispatched } = await runEngineSetupWithPolling(connector.id, name);
@@ -1586,12 +2008,16 @@ async function retrySetupAction(name) {
1586
2008
  connector_id: connector.id,
1587
2009
  dispatched
1588
2010
  });
2011
+ let postSetupConnector;
1589
2012
  try {
1590
2013
  const refreshed = await api.get(
1591
2014
  `/v1/cli/connectors?project_id=${currentProjectId}`
1592
2015
  );
1593
2016
  if (refreshed.connectors) {
1594
2017
  setCacheEntry("connectors", refreshed.connectors);
2018
+ postSetupConnector = refreshed.connectors.find(
2019
+ (candidate) => candidate.id === connector.id
2020
+ );
1595
2021
  }
1596
2022
  } catch {
1597
2023
  }
@@ -1600,6 +2026,10 @@ async function retrySetupAction(name) {
1600
2026
  } else {
1601
2027
  console.log("\u2713 Engine setup complete");
1602
2028
  }
2029
+ if (postSetupConnector?.branching_engine_status === "degraded") {
2030
+ console.log("");
2031
+ printDegradedWarnings(postSetupConnector.warnings ?? []);
2032
+ }
1603
2033
  } catch (err) {
1604
2034
  const failureTelemetry = connectorRetrySetupFailureTelemetry(err);
1605
2035
  if (isPermissionError(err)) {
@@ -1768,11 +2198,23 @@ connectorCommand.command("create <type> [url]").description("Create a new connec
1768
2198
  ).option(
1769
2199
  "--aws-cluster-name <name>",
1770
2200
  "EKS cluster name in the customer account where pgstream pods run (required with --deployment-model=customer-cloud)"
2201
+ ).option(
2202
+ "--replica-identity-decisions <path>",
2203
+ 'Path to a complete JSON file mapping every current "database.schema.table" preflight FQN -> "exclude" | "add_pk" | "replica_identity_full".'
2204
+ ).option(
2205
+ "--accept-replica-identity-defaults",
2206
+ "Non-interactively save the current/default decision for every table with no replication identity. Undecided tables are excluded from replication."
1771
2207
  ).action(createAction2);
1772
2208
  connectorCommand.command("list").description("List your connectors").action(listAction2);
1773
2209
  connectorCommand.command("switch <name>").description("Switch to a different connector").action(switchAction2);
1774
2210
  connectorCommand.command("retry-setup <name>").description(
1775
2211
  "Retry branching engine setup for a connector that didn't finish (status: configuration_verified or validating)"
2212
+ ).option(
2213
+ "--replica-identity-decisions <path>",
2214
+ 'Path to a complete JSON file mapping every current "database.schema.table" preflight FQN -> "exclude" | "add_pk" | "replica_identity_full".'
2215
+ ).option(
2216
+ "--accept-replica-identity-defaults",
2217
+ "Non-interactively save the current/default decision for every table with no replication identity. Undecided tables are excluded from replication."
1776
2218
  ).action(retrySetupAction);
1777
2219
  connectorCommand.command("update <name>").description("Update a connector's configuration").option(
1778
2220
  "--drop-extensions <list>",
@@ -2264,7 +2706,7 @@ projectCommand.command("delete <name>").description("Delete a project by name").
2264
2706
  import { Command as Command6 } from "commander";
2265
2707
 
2266
2708
  // src/lib/settings.ts
2267
- import { readFileSync as readFileSync3 } from "fs";
2709
+ import { readFileSync as readFileSync4 } from "fs";
2268
2710
  var SETTING_KEYS = ["default_db", "branch_sql"];
2269
2711
  function requireConnectorAndOrg() {
2270
2712
  const connectorId = getConfig("currentConnectorId");
@@ -2314,7 +2756,7 @@ function resolveValueFromArg(value) {
2314
2756
  if (value.startsWith("@")) {
2315
2757
  const path = value.slice(1);
2316
2758
  try {
2317
- return readFileSync3(path, "utf-8");
2759
+ return readFileSync4(path, "utf-8");
2318
2760
  } catch (err) {
2319
2761
  throw new Error(
2320
2762
  `Failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -2615,7 +3057,7 @@ var logoutCommand = new Command7("logout").description("Logout from Ardent").act
2615
3057
  var statusCommand = new Command7("status").description("Show status").action(statusAction);
2616
3058
 
2617
3059
  // src/lib/update-check.ts
2618
- import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
3060
+ import { existsSync as existsSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
2619
3061
  import { join as join2 } from "path";
2620
3062
  import { homedir as homedir2 } from "os";
2621
3063
  var UPDATE_CHECK_FILE = join2(homedir2(), ".ardent", "update-check.json");
@@ -2624,7 +3066,7 @@ var PACKAGE_NAME = "ardent-cli";
2624
3066
  function loadCache() {
2625
3067
  try {
2626
3068
  if (existsSync2(UPDATE_CHECK_FILE)) {
2627
- return JSON.parse(readFileSync4(UPDATE_CHECK_FILE, "utf-8"));
3069
+ return JSON.parse(readFileSync5(UPDATE_CHECK_FILE, "utf-8"));
2628
3070
  }
2629
3071
  } catch {
2630
3072
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "Git for Data infrastructure",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "build": "tsup",
11
11
  "dev": "tsup --watch",
12
12
  "start": "node dist/index.js",
13
- "test": "tsx --test src/lib/*.test.ts"
13
+ "//test": "Keep --test-concurrency=1: the *.test.ts files mutate process-wide globals (process.env.HOME, globalThis.fetch) and race if run in parallel. Remove only after introducing per-file process isolation.",
14
+ "test": "tsx --test --test-concurrency=1 src/lib/*.test.ts"
14
15
  },
15
16
  "files": [
16
17
  "dist"