ardent-cli 0.0.41 → 0.0.42
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 +251 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1247,6 +1247,43 @@ async function multiSelect(items, promptForItem, options = {}) {
|
|
|
1247
1247
|
return accepted;
|
|
1248
1248
|
}
|
|
1249
1249
|
|
|
1250
|
+
// src/lib/postgres_url.ts
|
|
1251
|
+
var VALID_SSL_MODES = /* @__PURE__ */ new Set([
|
|
1252
|
+
"disable",
|
|
1253
|
+
"prefer",
|
|
1254
|
+
"require"
|
|
1255
|
+
]);
|
|
1256
|
+
function parsePostgresUrl(url) {
|
|
1257
|
+
const atIndex = url.lastIndexOf("@");
|
|
1258
|
+
const credentialsPart = atIndex > 0 ? url.substring(0, atIndex) : url;
|
|
1259
|
+
const hostPart = atIndex > 0 ? url.substring(atIndex) : "";
|
|
1260
|
+
const cleanedUrl = credentialsPart.replace(/\\!/g, "!") + hostPart;
|
|
1261
|
+
const parsed = new URL(cleanedUrl);
|
|
1262
|
+
const host = parsed.hostname;
|
|
1263
|
+
const port = parsed.port || "5432";
|
|
1264
|
+
const username = decodeURIComponent(parsed.username);
|
|
1265
|
+
const password = parsed.password ? decodeURIComponent(parsed.password) : "";
|
|
1266
|
+
const databasePath = parsed.pathname.replace(/^\/+/, "");
|
|
1267
|
+
const database = databasePath ? decodeURIComponent(databasePath) : void 0;
|
|
1268
|
+
if (!host) throw new Error("Host required in connection URL");
|
|
1269
|
+
if (!username) throw new Error("Username required in connection URL");
|
|
1270
|
+
const result = { host, port, username, password };
|
|
1271
|
+
if (database !== void 0) {
|
|
1272
|
+
result.database = database;
|
|
1273
|
+
}
|
|
1274
|
+
const sslMode = parsed.searchParams.get("sslmode");
|
|
1275
|
+
if (sslMode !== null) {
|
|
1276
|
+
const normalized = sslMode.trim().toLowerCase();
|
|
1277
|
+
if (!VALID_SSL_MODES.has(normalized)) {
|
|
1278
|
+
throw new Error(
|
|
1279
|
+
`Invalid sslmode '${sslMode}' in connection URL. Must be one of: ${Array.from(VALID_SSL_MODES).join(", ")}.`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
result.ssl_mode = normalized;
|
|
1283
|
+
}
|
|
1284
|
+
return result;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1250
1287
|
// src/lib/replica_identity_prompt.ts
|
|
1251
1288
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
1252
1289
|
|
|
@@ -1593,20 +1630,6 @@ async function handleReplicaIdentityPreflight(connectorId, options, behavior = {
|
|
|
1593
1630
|
}
|
|
1594
1631
|
|
|
1595
1632
|
// src/commands/connector/create.ts
|
|
1596
|
-
function parsePostgresUrl(url) {
|
|
1597
|
-
const atIndex = url.lastIndexOf("@");
|
|
1598
|
-
const credentialsPart = atIndex > 0 ? url.substring(0, atIndex) : url;
|
|
1599
|
-
const hostPart = atIndex > 0 ? url.substring(atIndex) : "";
|
|
1600
|
-
const cleanedUrl = credentialsPart.replace(/\\!/g, "!") + hostPart;
|
|
1601
|
-
const parsed = new URL(cleanedUrl);
|
|
1602
|
-
const host = parsed.hostname;
|
|
1603
|
-
const port = parsed.port || "5432";
|
|
1604
|
-
const username = decodeURIComponent(parsed.username);
|
|
1605
|
-
const password = parsed.password ? decodeURIComponent(parsed.password) : "";
|
|
1606
|
-
if (!host) throw new Error("Host required in connection URL");
|
|
1607
|
-
if (!username) throw new Error("Username required in connection URL");
|
|
1608
|
-
return { host, port, username, password };
|
|
1609
|
-
}
|
|
1610
1633
|
function printEngineSetupRecoveryHint(connectorName) {
|
|
1611
1634
|
console.error("\u2717 Engine setup did not complete for this connector.");
|
|
1612
1635
|
console.error(" Inspect: ardent connector list");
|
|
@@ -1754,16 +1777,20 @@ async function createAction2(type, url, options) {
|
|
|
1754
1777
|
console.log(
|
|
1755
1778
|
`Creating connector${useByocEnvironment ? " (BYOC environment)" : ""}...`
|
|
1756
1779
|
);
|
|
1780
|
+
const connectionDetails = {
|
|
1781
|
+
host: parsed.host,
|
|
1782
|
+
port: parsed.port,
|
|
1783
|
+
username: parsed.username,
|
|
1784
|
+
password: parsed.password
|
|
1785
|
+
};
|
|
1786
|
+
if (parsed.ssl_mode !== void 0) {
|
|
1787
|
+
connectionDetails.ssl_mode = parsed.ssl_mode;
|
|
1788
|
+
}
|
|
1757
1789
|
createPayload = {
|
|
1758
1790
|
name: connectorName,
|
|
1759
1791
|
service_name: "postgresql",
|
|
1760
1792
|
project_id: currentProjectId,
|
|
1761
|
-
connection_details:
|
|
1762
|
-
host: parsed.host,
|
|
1763
|
-
port: parsed.port,
|
|
1764
|
-
username: parsed.username,
|
|
1765
|
-
password: parsed.password
|
|
1766
|
-
}
|
|
1793
|
+
connection_details: connectionDetails
|
|
1767
1794
|
};
|
|
1768
1795
|
}
|
|
1769
1796
|
if (useByocEnvironment) {
|
|
@@ -2122,6 +2149,195 @@ async function deleteAction2(name, options = {}) {
|
|
|
2122
2149
|
}
|
|
2123
2150
|
}
|
|
2124
2151
|
|
|
2152
|
+
// src/commands/connector/preflight.ts
|
|
2153
|
+
var CHECK_RENDER_ORDER = [
|
|
2154
|
+
"tcp_reachable",
|
|
2155
|
+
"auth",
|
|
2156
|
+
"postgres_metadata",
|
|
2157
|
+
"is_writer",
|
|
2158
|
+
"wal_level",
|
|
2159
|
+
"replication_slots",
|
|
2160
|
+
"wal_senders",
|
|
2161
|
+
"wal2json",
|
|
2162
|
+
"can_replicate",
|
|
2163
|
+
"can_read_tables",
|
|
2164
|
+
"can_write_tables",
|
|
2165
|
+
"can_modify_schema",
|
|
2166
|
+
"create_event_triggers",
|
|
2167
|
+
"duplicate_source"
|
|
2168
|
+
];
|
|
2169
|
+
function isCheck(value) {
|
|
2170
|
+
return typeof value === "object" && value !== null && "pass" in value;
|
|
2171
|
+
}
|
|
2172
|
+
function renderCheck(name, check) {
|
|
2173
|
+
const mark = check.unverified ? "?" : check.pass ? "\u2713" : "\u2717";
|
|
2174
|
+
const label = name.replace(/_/g, " ");
|
|
2175
|
+
let line = ` ${mark} ${label}`;
|
|
2176
|
+
if (check.actual !== void 0) line += ` (actual: ${check.actual})`;
|
|
2177
|
+
if (check.max !== void 0 && check.used !== void 0) {
|
|
2178
|
+
line += ` (${check.used}/${check.max} used)`;
|
|
2179
|
+
} else if (check.max !== void 0) {
|
|
2180
|
+
line += ` (max: ${check.max})`;
|
|
2181
|
+
}
|
|
2182
|
+
if (!check.pass && !check.unverified && check.error)
|
|
2183
|
+
line += `
|
|
2184
|
+
${check.error}`;
|
|
2185
|
+
if (check.unverified && check.error) line += `
|
|
2186
|
+
${check.error}`;
|
|
2187
|
+
if (!check.pass && check.remediation) {
|
|
2188
|
+
if (check.remediation.message)
|
|
2189
|
+
line += `
|
|
2190
|
+
${check.remediation.message}`;
|
|
2191
|
+
if (check.remediation.suggestion)
|
|
2192
|
+
line += `
|
|
2193
|
+
\u2192 ${check.remediation.suggestion}`;
|
|
2194
|
+
if (check.remediation.hint)
|
|
2195
|
+
line += `
|
|
2196
|
+
hint: ${check.remediation.hint}`;
|
|
2197
|
+
}
|
|
2198
|
+
if (check.existing_connector_id) {
|
|
2199
|
+
line += `
|
|
2200
|
+
existing connector: ${check.existing_connector_id}`;
|
|
2201
|
+
if (check.existing_connector_name)
|
|
2202
|
+
line += ` (${check.existing_connector_name})`;
|
|
2203
|
+
}
|
|
2204
|
+
return line;
|
|
2205
|
+
}
|
|
2206
|
+
function renderChecks(checks) {
|
|
2207
|
+
const rendered = [];
|
|
2208
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2209
|
+
for (const name of CHECK_RENDER_ORDER) {
|
|
2210
|
+
const entry = checks[name];
|
|
2211
|
+
if (entry === void 0) continue;
|
|
2212
|
+
seen.add(name);
|
|
2213
|
+
if (!isCheck(entry)) continue;
|
|
2214
|
+
rendered.push(renderCheck(name, entry));
|
|
2215
|
+
}
|
|
2216
|
+
const SKIP = /* @__PURE__ */ new Set(["pass", "missing", "engine_config", "warnings"]);
|
|
2217
|
+
for (const [name, entry] of Object.entries(checks)) {
|
|
2218
|
+
if (seen.has(name) || SKIP.has(name)) continue;
|
|
2219
|
+
if (!isCheck(entry)) continue;
|
|
2220
|
+
rendered.push(renderCheck(name, entry));
|
|
2221
|
+
}
|
|
2222
|
+
return rendered;
|
|
2223
|
+
}
|
|
2224
|
+
function allChecksPass(checks) {
|
|
2225
|
+
for (const entry of Object.values(checks)) {
|
|
2226
|
+
if (!isCheck(entry)) continue;
|
|
2227
|
+
if (entry.unverified === true) continue;
|
|
2228
|
+
if (entry.pass !== true) return false;
|
|
2229
|
+
}
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
async function preflightAction(type, url, options) {
|
|
2233
|
+
if (type !== "postgresql") {
|
|
2234
|
+
console.error(
|
|
2235
|
+
`\u2717 Preflight is currently only supported for postgresql connectors.`
|
|
2236
|
+
);
|
|
2237
|
+
console.error(` Got: ${type}`);
|
|
2238
|
+
process.exit(1);
|
|
2239
|
+
}
|
|
2240
|
+
if (!url) {
|
|
2241
|
+
console.error("\u2717 Connection URL required");
|
|
2242
|
+
console.error(
|
|
2243
|
+
" Example: ardent connector preflight postgresql postgresql://user:pass@host:5432/db"
|
|
2244
|
+
);
|
|
2245
|
+
process.exit(1);
|
|
2246
|
+
}
|
|
2247
|
+
let parsed;
|
|
2248
|
+
try {
|
|
2249
|
+
parsed = parsePostgresUrl(url);
|
|
2250
|
+
} catch (err) {
|
|
2251
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2252
|
+
console.error(`\u2717 ${message}`);
|
|
2253
|
+
process.exit(1);
|
|
2254
|
+
}
|
|
2255
|
+
if (!parsed.password) {
|
|
2256
|
+
console.error("\u2717 Password required in connection URL");
|
|
2257
|
+
process.exit(1);
|
|
2258
|
+
}
|
|
2259
|
+
if (options.environmentId && !options.byoc) {
|
|
2260
|
+
console.error("\u2717 --environment-id requires --byoc");
|
|
2261
|
+
process.exit(1);
|
|
2262
|
+
}
|
|
2263
|
+
if (options.privateLinkId && !options.byoc) {
|
|
2264
|
+
console.error("\u2717 --private-link-id requires --byoc");
|
|
2265
|
+
process.exit(1);
|
|
2266
|
+
}
|
|
2267
|
+
const connectionDetails = {
|
|
2268
|
+
host: parsed.host,
|
|
2269
|
+
port: parsed.port,
|
|
2270
|
+
username: parsed.username,
|
|
2271
|
+
password: parsed.password
|
|
2272
|
+
};
|
|
2273
|
+
if (parsed.ssl_mode !== void 0) {
|
|
2274
|
+
connectionDetails.ssl_mode = parsed.ssl_mode;
|
|
2275
|
+
}
|
|
2276
|
+
if (parsed.database !== void 0) {
|
|
2277
|
+
connectionDetails.database = parsed.database;
|
|
2278
|
+
}
|
|
2279
|
+
const payload = {
|
|
2280
|
+
service_name: "postgresql",
|
|
2281
|
+
connection_details: connectionDetails
|
|
2282
|
+
};
|
|
2283
|
+
if (parsed.database !== void 0) {
|
|
2284
|
+
payload.database = parsed.database;
|
|
2285
|
+
}
|
|
2286
|
+
if (options.byoc) {
|
|
2287
|
+
payload.use_environment = true;
|
|
2288
|
+
if (options.environmentId) payload.environment_id = options.environmentId;
|
|
2289
|
+
if (options.privateLinkId) payload.private_link_id = options.privateLinkId;
|
|
2290
|
+
}
|
|
2291
|
+
if (options.schemas) {
|
|
2292
|
+
payload.selected_schemas = options.schemas.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2293
|
+
}
|
|
2294
|
+
console.log(`Running preflight against ${parsed.host}:${parsed.port}\u2026`);
|
|
2295
|
+
let response;
|
|
2296
|
+
try {
|
|
2297
|
+
response = await api.post(
|
|
2298
|
+
"/v1/connectors/preflight",
|
|
2299
|
+
payload
|
|
2300
|
+
);
|
|
2301
|
+
} catch (err) {
|
|
2302
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2303
|
+
console.error(`\u2717 Preflight request failed: ${message}`);
|
|
2304
|
+
process.exit(1);
|
|
2305
|
+
}
|
|
2306
|
+
console.log("");
|
|
2307
|
+
console.log("Connection checks:");
|
|
2308
|
+
for (const line of renderChecks(response.checks)) {
|
|
2309
|
+
console.log(line);
|
|
2310
|
+
}
|
|
2311
|
+
if (response.source_provider) {
|
|
2312
|
+
console.log("");
|
|
2313
|
+
console.log(`Detected source provider: ${response.source_provider}`);
|
|
2314
|
+
}
|
|
2315
|
+
if (response.grant_script) {
|
|
2316
|
+
console.log("");
|
|
2317
|
+
console.log("--- Grant script ---");
|
|
2318
|
+
console.log(response.grant_script.sql);
|
|
2319
|
+
}
|
|
2320
|
+
const overallPass = response.preflight_pass ?? (response.branching_prerequisites_pass && allChecksPass(response.checks));
|
|
2321
|
+
console.log("");
|
|
2322
|
+
if (overallPass) {
|
|
2323
|
+
console.log(
|
|
2324
|
+
"\u2713 Preflight checks passed. You're ready to create this connector."
|
|
2325
|
+
);
|
|
2326
|
+
} else {
|
|
2327
|
+
console.log(
|
|
2328
|
+
"\u2717 Preflight checks did NOT pass. Fix the failures above before creating."
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
trackEvent("CLI: connector preflight", {
|
|
2332
|
+
source_provider: response.source_provider,
|
|
2333
|
+
branching_prerequisites_pass: response.branching_prerequisites_pass,
|
|
2334
|
+
preflight_pass: overallPass,
|
|
2335
|
+
byoc: Boolean(options.byoc),
|
|
2336
|
+
private_link: Boolean(options.privateLinkId)
|
|
2337
|
+
});
|
|
2338
|
+
process.exit(overallPass ? 0 : 2);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2125
2341
|
// src/commands/connector/retry-setup.ts
|
|
2126
2342
|
function connectorRetrySetupFailureTelemetry(err) {
|
|
2127
2343
|
if (isPermissionError(err)) {
|
|
@@ -2375,6 +2591,21 @@ async function updateAction(name, options = {}) {
|
|
|
2375
2591
|
|
|
2376
2592
|
// src/commands/connector/index.ts
|
|
2377
2593
|
var connectorCommand = new Command2("connector").description("Manage database connectors");
|
|
2594
|
+
connectorCommand.command("preflight <type> [url]").description(
|
|
2595
|
+
"Validate a source database without persisting a connector row (no Key Vault write)"
|
|
2596
|
+
).option(
|
|
2597
|
+
"--byoc",
|
|
2598
|
+
"Route preflight through the customer's BYOC environment (requires an active environment)"
|
|
2599
|
+
).option(
|
|
2600
|
+
"--environment-id <id>",
|
|
2601
|
+
"BYOC environment to probe through (required with --byoc when you have more than one)"
|
|
2602
|
+
).option(
|
|
2603
|
+
"--private-link-id <id>",
|
|
2604
|
+
"Private connection to use for the source database (requires --byoc)"
|
|
2605
|
+
).option(
|
|
2606
|
+
"--schemas <list>",
|
|
2607
|
+
"Comma-separated schema names to include in the rendered grant script"
|
|
2608
|
+
).action(preflightAction);
|
|
2378
2609
|
connectorCommand.command("create <type> [url]").description("Create a new connector").option("-n, --name <name>", "Connector name").option(
|
|
2379
2610
|
"--byoc [provider]",
|
|
2380
2611
|
"Deploy connector on your BYOC environment, or legacy BYOC Neon provider form: --byoc neon"
|