ardent-cli 0.0.40 → 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.
Files changed (2) hide show
  1. package/dist/index.js +269 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -510,11 +510,15 @@ async function createAction(name, options) {
510
510
  try {
511
511
  const startTime = performance.now();
512
512
  const connectorId = await requireCurrentConnectorId();
513
- await api.post("/v1/branch/create", {
514
- connector_id: connectorId,
515
- service_type: options.service,
516
- name
517
- });
513
+ const createResponse = await api.post(
514
+ "/v1/branch/create",
515
+ {
516
+ connector_id: connectorId,
517
+ service_type: options.service,
518
+ name
519
+ }
520
+ );
521
+ const warning = createResponse?.warning;
518
522
  const response = await api.get(`/v1/cli/branches?connector_id=${connectorId}`);
519
523
  const apiBranches = response.branches || [];
520
524
  let apiBranch;
@@ -544,12 +548,20 @@ async function createAction(name, options) {
544
548
  setCacheEntry("branches", cachedBranches);
545
549
  setCurrentBranch(name);
546
550
  const elapsed = ((performance.now() - startTime) / 1e3).toFixed(1);
547
- trackEvent("CLI: branch create succeeded", { service_type: options.service, duration_seconds: parseFloat(elapsed) });
551
+ trackEvent("CLI: branch create succeeded", {
552
+ service_type: options.service,
553
+ duration_seconds: parseFloat(elapsed),
554
+ warning_type: warning?.type
555
+ });
548
556
  console.log(`\u2713 Branch '${name}' created and checked out in ${elapsed}s`);
549
557
  if (branch.branch_url) {
550
558
  console.log(`
551
559
  ${branch.branch_url}`);
552
560
  }
561
+ if (warning) {
562
+ console.log(`
563
+ ! ${warning.message}`);
564
+ }
553
565
  } catch (err) {
554
566
  if (isNetworkError(err)) {
555
567
  trackEvent("CLI: branch create failed", { reason: "offline" });
@@ -1235,6 +1247,43 @@ async function multiSelect(items, promptForItem, options = {}) {
1235
1247
  return accepted;
1236
1248
  }
1237
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
+
1238
1287
  // src/lib/replica_identity_prompt.ts
1239
1288
  import { createInterface as createInterface2 } from "readline/promises";
1240
1289
 
@@ -1581,20 +1630,6 @@ async function handleReplicaIdentityPreflight(connectorId, options, behavior = {
1581
1630
  }
1582
1631
 
1583
1632
  // src/commands/connector/create.ts
1584
- function parsePostgresUrl(url) {
1585
- const atIndex = url.lastIndexOf("@");
1586
- const credentialsPart = atIndex > 0 ? url.substring(0, atIndex) : url;
1587
- const hostPart = atIndex > 0 ? url.substring(atIndex) : "";
1588
- const cleanedUrl = credentialsPart.replace(/\\!/g, "!") + hostPart;
1589
- const parsed = new URL(cleanedUrl);
1590
- const host = parsed.hostname;
1591
- const port = parsed.port || "5432";
1592
- const username = decodeURIComponent(parsed.username);
1593
- const password = parsed.password ? decodeURIComponent(parsed.password) : "";
1594
- if (!host) throw new Error("Host required in connection URL");
1595
- if (!username) throw new Error("Username required in connection URL");
1596
- return { host, port, username, password };
1597
- }
1598
1633
  function printEngineSetupRecoveryHint(connectorName) {
1599
1634
  console.error("\u2717 Engine setup did not complete for this connector.");
1600
1635
  console.error(" Inspect: ardent connector list");
@@ -1742,16 +1777,20 @@ async function createAction2(type, url, options) {
1742
1777
  console.log(
1743
1778
  `Creating connector${useByocEnvironment ? " (BYOC environment)" : ""}...`
1744
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
+ }
1745
1789
  createPayload = {
1746
1790
  name: connectorName,
1747
1791
  service_name: "postgresql",
1748
1792
  project_id: currentProjectId,
1749
- connection_details: {
1750
- host: parsed.host,
1751
- port: parsed.port,
1752
- username: parsed.username,
1753
- password: parsed.password
1754
- }
1793
+ connection_details: connectionDetails
1755
1794
  };
1756
1795
  }
1757
1796
  if (useByocEnvironment) {
@@ -2110,6 +2149,195 @@ async function deleteAction2(name, options = {}) {
2110
2149
  }
2111
2150
  }
2112
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
+
2113
2341
  // src/commands/connector/retry-setup.ts
2114
2342
  function connectorRetrySetupFailureTelemetry(err) {
2115
2343
  if (isPermissionError(err)) {
@@ -2363,6 +2591,21 @@ async function updateAction(name, options = {}) {
2363
2591
 
2364
2592
  // src/commands/connector/index.ts
2365
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);
2366
2609
  connectorCommand.command("create <type> [url]").description("Create a new connector").option("-n, --name <name>", "Connector name").option(
2367
2610
  "--byoc [provider]",
2368
2611
  "Deploy connector on your BYOC environment, or legacy BYOC Neon provider form: --byoc neon"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "Git for Data infrastructure",
5
5
  "type": "module",
6
6
  "bin": {