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.
- package/dist/index.js +450 -8
- 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
|
-
|
|
1317
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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": "
|
|
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"
|