ardent-cli 0.0.41 → 0.0.43

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 +280 -23
  2. 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,23 @@ 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
+ }
1789
+ if (parsed.database !== void 0) {
1790
+ connectionDetails.database = parsed.database;
1791
+ }
1757
1792
  createPayload = {
1758
1793
  name: connectorName,
1759
1794
  service_name: "postgresql",
1760
1795
  project_id: currentProjectId,
1761
- connection_details: {
1762
- host: parsed.host,
1763
- port: parsed.port,
1764
- username: parsed.username,
1765
- password: parsed.password
1766
- }
1796
+ connection_details: connectionDetails
1767
1797
  };
1768
1798
  }
1769
1799
  if (useByocEnvironment) {
@@ -2122,6 +2152,195 @@ async function deleteAction2(name, options = {}) {
2122
2152
  }
2123
2153
  }
2124
2154
 
2155
+ // src/commands/connector/preflight.ts
2156
+ var CHECK_RENDER_ORDER = [
2157
+ "tcp_reachable",
2158
+ "auth",
2159
+ "postgres_metadata",
2160
+ "is_writer",
2161
+ "wal_level",
2162
+ "replication_slots",
2163
+ "wal_senders",
2164
+ "wal2json",
2165
+ "can_replicate",
2166
+ "can_read_tables",
2167
+ "can_write_tables",
2168
+ "can_modify_schema",
2169
+ "create_event_triggers",
2170
+ "duplicate_source"
2171
+ ];
2172
+ function isCheck(value) {
2173
+ return typeof value === "object" && value !== null && "pass" in value;
2174
+ }
2175
+ function renderCheck(name, check) {
2176
+ const mark = check.unverified ? "?" : check.pass ? "\u2713" : "\u2717";
2177
+ const label = name.replace(/_/g, " ");
2178
+ let line = ` ${mark} ${label}`;
2179
+ if (check.actual !== void 0) line += ` (actual: ${check.actual})`;
2180
+ if (check.max !== void 0 && check.used !== void 0) {
2181
+ line += ` (${check.used}/${check.max} used)`;
2182
+ } else if (check.max !== void 0) {
2183
+ line += ` (max: ${check.max})`;
2184
+ }
2185
+ if (!check.pass && !check.unverified && check.error)
2186
+ line += `
2187
+ ${check.error}`;
2188
+ if (check.unverified && check.error) line += `
2189
+ ${check.error}`;
2190
+ if (!check.pass && check.remediation) {
2191
+ if (check.remediation.message)
2192
+ line += `
2193
+ ${check.remediation.message}`;
2194
+ if (check.remediation.suggestion)
2195
+ line += `
2196
+ \u2192 ${check.remediation.suggestion}`;
2197
+ if (check.remediation.hint)
2198
+ line += `
2199
+ hint: ${check.remediation.hint}`;
2200
+ }
2201
+ if (check.existing_connector_id) {
2202
+ line += `
2203
+ existing connector: ${check.existing_connector_id}`;
2204
+ if (check.existing_connector_name)
2205
+ line += ` (${check.existing_connector_name})`;
2206
+ }
2207
+ return line;
2208
+ }
2209
+ function renderChecks(checks) {
2210
+ const rendered = [];
2211
+ const seen = /* @__PURE__ */ new Set();
2212
+ for (const name of CHECK_RENDER_ORDER) {
2213
+ const entry = checks[name];
2214
+ if (entry === void 0) continue;
2215
+ seen.add(name);
2216
+ if (!isCheck(entry)) continue;
2217
+ rendered.push(renderCheck(name, entry));
2218
+ }
2219
+ const SKIP = /* @__PURE__ */ new Set(["pass", "missing", "engine_config", "warnings"]);
2220
+ for (const [name, entry] of Object.entries(checks)) {
2221
+ if (seen.has(name) || SKIP.has(name)) continue;
2222
+ if (!isCheck(entry)) continue;
2223
+ rendered.push(renderCheck(name, entry));
2224
+ }
2225
+ return rendered;
2226
+ }
2227
+ function allChecksPass(checks) {
2228
+ for (const entry of Object.values(checks)) {
2229
+ if (!isCheck(entry)) continue;
2230
+ if (entry.unverified === true) continue;
2231
+ if (entry.pass !== true) return false;
2232
+ }
2233
+ return true;
2234
+ }
2235
+ async function preflightAction(type, url, options) {
2236
+ if (type !== "postgresql") {
2237
+ console.error(
2238
+ `\u2717 Preflight is currently only supported for postgresql connectors.`
2239
+ );
2240
+ console.error(` Got: ${type}`);
2241
+ process.exit(1);
2242
+ }
2243
+ if (!url) {
2244
+ console.error("\u2717 Connection URL required");
2245
+ console.error(
2246
+ " Example: ardent connector preflight postgresql postgresql://user:pass@host:5432/db"
2247
+ );
2248
+ process.exit(1);
2249
+ }
2250
+ let parsed;
2251
+ try {
2252
+ parsed = parsePostgresUrl(url);
2253
+ } catch (err) {
2254
+ const message = err instanceof Error ? err.message : String(err);
2255
+ console.error(`\u2717 ${message}`);
2256
+ process.exit(1);
2257
+ }
2258
+ if (!parsed.password) {
2259
+ console.error("\u2717 Password required in connection URL");
2260
+ process.exit(1);
2261
+ }
2262
+ if (options.environmentId && !options.byoc) {
2263
+ console.error("\u2717 --environment-id requires --byoc");
2264
+ process.exit(1);
2265
+ }
2266
+ if (options.privateLinkId && !options.byoc) {
2267
+ console.error("\u2717 --private-link-id requires --byoc");
2268
+ process.exit(1);
2269
+ }
2270
+ const connectionDetails = {
2271
+ host: parsed.host,
2272
+ port: parsed.port,
2273
+ username: parsed.username,
2274
+ password: parsed.password
2275
+ };
2276
+ if (parsed.ssl_mode !== void 0) {
2277
+ connectionDetails.ssl_mode = parsed.ssl_mode;
2278
+ }
2279
+ if (parsed.database !== void 0) {
2280
+ connectionDetails.database = parsed.database;
2281
+ }
2282
+ const payload = {
2283
+ service_name: "postgresql",
2284
+ connection_details: connectionDetails
2285
+ };
2286
+ if (parsed.database !== void 0) {
2287
+ payload.database = parsed.database;
2288
+ }
2289
+ if (options.byoc) {
2290
+ payload.use_environment = true;
2291
+ if (options.environmentId) payload.environment_id = options.environmentId;
2292
+ if (options.privateLinkId) payload.private_link_id = options.privateLinkId;
2293
+ }
2294
+ if (options.schemas) {
2295
+ payload.selected_schemas = options.schemas.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
2296
+ }
2297
+ console.log(`Running preflight against ${parsed.host}:${parsed.port}\u2026`);
2298
+ let response;
2299
+ try {
2300
+ response = await api.post(
2301
+ "/v1/connectors/preflight",
2302
+ payload
2303
+ );
2304
+ } catch (err) {
2305
+ const message = err instanceof Error ? err.message : String(err);
2306
+ console.error(`\u2717 Preflight request failed: ${message}`);
2307
+ process.exit(1);
2308
+ }
2309
+ console.log("");
2310
+ console.log("Connection checks:");
2311
+ for (const line of renderChecks(response.checks)) {
2312
+ console.log(line);
2313
+ }
2314
+ if (response.source_provider) {
2315
+ console.log("");
2316
+ console.log(`Detected source provider: ${response.source_provider}`);
2317
+ }
2318
+ if (response.grant_script) {
2319
+ console.log("");
2320
+ console.log("--- Grant script ---");
2321
+ console.log(response.grant_script.sql);
2322
+ }
2323
+ const overallPass = response.preflight_pass ?? (response.branching_prerequisites_pass && allChecksPass(response.checks));
2324
+ console.log("");
2325
+ if (overallPass) {
2326
+ console.log(
2327
+ "\u2713 Preflight checks passed. You're ready to create this connector."
2328
+ );
2329
+ } else {
2330
+ console.log(
2331
+ "\u2717 Preflight checks did NOT pass. Fix the failures above before creating."
2332
+ );
2333
+ }
2334
+ trackEvent("CLI: connector preflight", {
2335
+ source_provider: response.source_provider,
2336
+ branching_prerequisites_pass: response.branching_prerequisites_pass,
2337
+ preflight_pass: overallPass,
2338
+ byoc: Boolean(options.byoc),
2339
+ private_link: Boolean(options.privateLinkId)
2340
+ });
2341
+ process.exit(overallPass ? 0 : 2);
2342
+ }
2343
+
2125
2344
  // src/commands/connector/retry-setup.ts
2126
2345
  function connectorRetrySetupFailureTelemetry(err) {
2127
2346
  if (isPermissionError(err)) {
@@ -2375,6 +2594,21 @@ async function updateAction(name, options = {}) {
2375
2594
 
2376
2595
  // src/commands/connector/index.ts
2377
2596
  var connectorCommand = new Command2("connector").description("Manage database connectors");
2597
+ connectorCommand.command("preflight <type> [url]").description(
2598
+ "Validate a source database without persisting a connector row (no Key Vault write)"
2599
+ ).option(
2600
+ "--byoc",
2601
+ "Route preflight through the customer's BYOC environment (requires an active environment)"
2602
+ ).option(
2603
+ "--environment-id <id>",
2604
+ "BYOC environment to probe through (required with --byoc when you have more than one)"
2605
+ ).option(
2606
+ "--private-link-id <id>",
2607
+ "Private connection to use for the source database (requires --byoc)"
2608
+ ).option(
2609
+ "--schemas <list>",
2610
+ "Comma-separated schema names to include in the rendered grant script"
2611
+ ).action(preflightAction);
2378
2612
  connectorCommand.command("create <type> [url]").description("Create a new connector").option("-n, --name <name>", "Connector name").option(
2379
2613
  "--byoc [provider]",
2380
2614
  "Deploy connector on your BYOC environment, or legacy BYOC Neon provider form: --byoc neon"
@@ -4401,14 +4635,14 @@ function logoutAction() {
4401
4635
 
4402
4636
  // src/commands/auth/status.ts
4403
4637
  function statusAction() {
4404
- const token = getConfig("token");
4638
+ const token = getToken();
4405
4639
  if (!token) {
4406
4640
  trackEvent("CLI: auth status", { authenticated: false });
4407
4641
  console.log("\u2717 Not authenticated");
4408
4642
  console.log(" Run: ardent login");
4409
4643
  return;
4410
4644
  }
4411
- const user = getConfig("user");
4645
+ const user = getConfig("token") ? getConfig("user") : void 0;
4412
4646
  console.log("\u2713 Authenticated");
4413
4647
  if (user?.full_name) {
4414
4648
  console.log(` Account: ${user.full_name}`);
@@ -4427,7 +4661,12 @@ function statusAction() {
4427
4661
  }
4428
4662
 
4429
4663
  // src/commands/auth/index.ts
4430
- var loginCommand = new Command7("login").description("Login to Ardent").option("-t, --token <token>", "API token (skip browser login)").action(loginAction);
4664
+ var loginCommand = new Command7("login").description(
4665
+ "Login to Ardent. Defaults to GitHub OAuth in the browser; pass --token for headless auth, or set the ARDENT_TOKEN env var to skip 'ardent login' entirely."
4666
+ ).option(
4667
+ "-t, --token <token>",
4668
+ "API token for headless auth (skip browser login). For repeat headless use, prefer the ARDENT_TOKEN env var."
4669
+ ).action(loginAction);
4431
4670
  var logoutCommand = new Command7("logout").description("Logout from Ardent").action(logoutAction);
4432
4671
  var statusCommand = new Command7("status").description("Show status").action(statusAction);
4433
4672
 
@@ -4513,6 +4752,7 @@ USAGE
4513
4752
 
4514
4753
  AUTHENTICATION
4515
4754
  login Login via browser (GitHub OAuth)
4755
+ login --token <token> Login with an API token (headless, no browser)
4516
4756
  logout Clear stored credentials
4517
4757
  status Check authentication status
4518
4758
 
@@ -4554,12 +4794,29 @@ OPTIONS
4554
4794
  --help Show help for any command
4555
4795
  --version Show CLI version
4556
4796
 
4797
+ ENVIRONMENT
4798
+ ARDENT_TOKEN API token used for every request. When set, no
4799
+ 'ardent login' is needed \u2014 ideal for CI and other
4800
+ headless contexts. Generate one in the web app
4801
+ under Settings > API Keys.
4802
+ ARDENT_API_URL Override the API endpoint
4803
+ (default: https://api.tryardent.com).
4804
+
4557
4805
  EXAMPLES
4558
4806
  ardent login
4559
4807
  ardent connector create postgresql postgresql://user:password@your-db-host.com:5432/database
4560
4808
  ardent branch create my-feature
4561
4809
  ardent branch switch my-feature
4562
4810
  ardent org members
4811
+
4812
+ # Headless / CI (GitHub Actions)
4813
+ # env:
4814
+ # ARDENT_TOKEN: \${{ secrets.ARDENT_TOKEN }}
4815
+ # run: |
4816
+ # BRANCH="pr-\${{ github.event.number }}-\${{ github.run_id }}"
4817
+ # ardent branch create "$BRANCH"
4818
+ # # ... run your migration / tests against the branch ...
4819
+ # ardent branch delete "$BRANCH"
4563
4820
  `;
4564
4821
  var program = new Command8();
4565
4822
  program.name("ardent").description("CLI for Ardent database branching").version(CLI_VERSION).configureHelp({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "description": "Git for Data infrastructure",
5
5
  "type": "module",
6
6
  "bin": {