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.
- package/dist/index.js +280 -23
- 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 =
|
|
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(
|
|
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({
|