ardent-cli 0.0.44 → 0.0.46

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 +196 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -478,6 +478,37 @@ async function resolveCurrentConnectorId() {
478
478
  );
479
479
  }
480
480
 
481
+ // src/lib/resource_name_validation.ts
482
+ var RESERVED_SUFFIXES = ["pooler", "readonly", "direct"];
483
+ var MAX_RESOURCE_NAME_LENGTH = 100;
484
+ var ALLOWED_CHARS = /^[a-z0-9-]+$/;
485
+ function validateResourceName(name) {
486
+ const length = name ? Array.from(name).length : 0;
487
+ if (!name || length > MAX_RESOURCE_NAME_LENGTH) {
488
+ throw new Error(
489
+ `Resource name must be 1-${MAX_RESOURCE_NAME_LENGTH} characters, got ${length}`
490
+ );
491
+ }
492
+ if (!ALLOWED_CHARS.test(name)) {
493
+ throw new Error(
494
+ `Resource name must contain only lowercase letters, digits, and hyphens: '${name}'`
495
+ );
496
+ }
497
+ if (name.startsWith("-") || name.endsWith("-")) {
498
+ throw new Error(`Resource name must not start or end with a hyphen: '${name}'`);
499
+ }
500
+ if (name.includes("--")) {
501
+ throw new Error(`Resource name must not contain consecutive hyphens: '${name}'`);
502
+ }
503
+ for (const suffix of RESERVED_SUFFIXES) {
504
+ if (name.endsWith(`-${suffix}`)) {
505
+ throw new Error(
506
+ `Resource name must not end with reserved suffix '-${suffix}': '${name}'`
507
+ );
508
+ }
509
+ }
510
+ }
511
+
481
512
  // src/lib/telemetry.ts
482
513
  import { randomUUID } from "crypto";
483
514
  function getAnonymousId() {
@@ -626,6 +657,21 @@ async function createAction(name, options) {
626
657
  process.exit(2);
627
658
  }
628
659
  const mode = modeResolution.mode;
660
+ try {
661
+ validateResourceName(name);
662
+ } catch (validationError) {
663
+ const message = validationError instanceof Error ? validationError.message : String(validationError);
664
+ trackEvent("CLI: branch create failed", {
665
+ reason: "invalid_name",
666
+ output_mode: mode
667
+ });
668
+ if (mode === "json") {
669
+ process.stdout.write(renderBranchJsonError("invalid_name", message));
670
+ process.exit(1);
671
+ }
672
+ console.error(`\u2717 ${message}`);
673
+ process.exit(1);
674
+ }
629
675
  try {
630
676
  const startTime = performance.now();
631
677
  const connectorId = await resolveCurrentConnectorId();
@@ -1865,12 +1911,85 @@ async function handleReplicaIdentityPreflight(connectorId, options, behavior = {
1865
1911
  return { submitted: true, preflight: refreshed };
1866
1912
  }
1867
1913
 
1914
+ // src/lib/connector_name.ts
1915
+ var SUFFIX_RESERVE = 5;
1916
+ var MAX_BASE_LENGTH = MAX_RESOURCE_NAME_LENGTH - SUFFIX_RESERVE;
1917
+ var MAX_COLLISION_SUFFIX = 1e3;
1918
+ function slugifyToResourceName(input) {
1919
+ const slug = input.normalize("NFKD").replace(new RegExp("\\p{Mn}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_BASE_LENGTH).replace(/-+$/g, "");
1920
+ if (slug.length === 0) {
1921
+ return null;
1922
+ }
1923
+ try {
1924
+ validateResourceName(slug);
1925
+ } catch {
1926
+ return null;
1927
+ }
1928
+ return slug;
1929
+ }
1930
+ function deriveConnectorName(options) {
1931
+ let base = null;
1932
+ for (const source of options.sources) {
1933
+ if (!source) {
1934
+ continue;
1935
+ }
1936
+ const candidate = slugifyToResourceName(source);
1937
+ if (candidate) {
1938
+ base = candidate;
1939
+ break;
1940
+ }
1941
+ }
1942
+ if (base === null) {
1943
+ base = slugifyToResourceName(options.fallbackBase);
1944
+ }
1945
+ if (base === null) {
1946
+ throw new Error(
1947
+ `Could not derive a connector name: fallback base '${options.fallbackBase}' is not a valid resource name`
1948
+ );
1949
+ }
1950
+ const taken = new Set(options.existingNames);
1951
+ if (!taken.has(base)) {
1952
+ return base;
1953
+ }
1954
+ for (let suffix = 2; suffix <= MAX_COLLISION_SUFFIX; suffix++) {
1955
+ const candidate = `${base}-${suffix}`;
1956
+ if (!taken.has(candidate)) {
1957
+ return candidate;
1958
+ }
1959
+ }
1960
+ throw new Error(
1961
+ `Could not derive a unique connector name from base '${base}' after ${MAX_COLLISION_SUFFIX} attempts; pass --name explicitly`
1962
+ );
1963
+ }
1964
+
1868
1965
  // src/commands/connector/create.ts
1869
1966
  function printEngineSetupRecoveryHint(connectorName) {
1870
1967
  console.error("\u2717 Engine setup did not complete for this connector.");
1871
1968
  console.error(" Inspect: ardent connector list");
1872
1969
  console.error(` Retry: ardent connector retry-setup ${connectorName}`);
1873
1970
  }
1971
+ async function deriveDefaultConnectorName(input) {
1972
+ let existingNames;
1973
+ try {
1974
+ const existing = await api.get(
1975
+ `/v1/cli/connectors?project_id=${input.projectId}`
1976
+ );
1977
+ existingNames = existing.connectors.map((connector) => connector.name);
1978
+ } catch (listErr) {
1979
+ if (isPermissionError(listErr)) {
1980
+ throw listErr;
1981
+ }
1982
+ const detail = listErr instanceof Error ? listErr.message : String(listErr);
1983
+ throw new Error(
1984
+ `Could not look up existing connectors to choose a default name (${detail}). Re-run with --name <name>.`
1985
+ );
1986
+ }
1987
+ return deriveConnectorName({
1988
+ sources: [input.projectName, input.sourceDatabase, input.sourceHost],
1989
+ existingNames,
1990
+ fallbackBase: input.serviceType
1991
+ });
1992
+ }
1874
1993
  async function promptForUnsupportedExtensions(connectorId, unsupported, alreadyPersisted) {
1875
1994
  function mergeAllowlist(newlyAccepted) {
1876
1995
  return Array.from(/* @__PURE__ */ new Set([...alreadyPersisted, ...newlyAccepted])).sort();
@@ -1981,7 +2100,16 @@ async function createAction2(type, url, options) {
1981
2100
  process.exit(1);
1982
2101
  }
1983
2102
  try {
1984
- const connectorName = options.name || (isByocNeon ? "my-neon-connection" : "my-postgresql-connection");
2103
+ if (options.name) {
2104
+ try {
2105
+ validateResourceName(options.name);
2106
+ } catch (validationError) {
2107
+ const message = validationError instanceof Error ? validationError.message : String(validationError);
2108
+ trackEvent("CLI: connector create failed", { reason: "invalid_name" });
2109
+ console.error(`\u2717 ${message}`);
2110
+ process.exit(1);
2111
+ }
2112
+ }
1985
2113
  const currentProjectId = getConfig("currentProjectId");
1986
2114
  if (!currentProjectId) {
1987
2115
  console.error("\u2717 No current project set. Switch to a project first:");
@@ -1989,6 +2117,22 @@ async function createAction2(type, url, options) {
1989
2117
  console.error(" ardent project switch <name>");
1990
2118
  process.exit(1);
1991
2119
  }
2120
+ let parsedUrl;
2121
+ if (!isByocNeon) {
2122
+ parsedUrl = parsePostgresUrl(url);
2123
+ if (!parsedUrl.password) {
2124
+ console.error("\u2717 Password required in connection URL");
2125
+ console.error(" Example: postgresql://user:password@host:5432/db");
2126
+ process.exit(1);
2127
+ }
2128
+ }
2129
+ const connectorName = options.name ? options.name : await deriveDefaultConnectorName({
2130
+ projectId: currentProjectId,
2131
+ projectName: getConfig("currentProjectName"),
2132
+ sourceDatabase: parsedUrl?.database,
2133
+ sourceHost: parsedUrl?.host,
2134
+ serviceType: type.toLowerCase()
2135
+ });
1992
2136
  let createPayload;
1993
2137
  if (isByocNeon) {
1994
2138
  console.log(
@@ -2004,12 +2148,7 @@ async function createAction2(type, url, options) {
2004
2148
  connection_details: {}
2005
2149
  };
2006
2150
  } else {
2007
- const parsed = parsePostgresUrl(url);
2008
- if (!parsed.password) {
2009
- console.error("\u2717 Password required in connection URL");
2010
- console.error(" Example: postgresql://user:password@host:5432/db");
2011
- process.exit(1);
2012
- }
2151
+ const parsed = parsedUrl;
2013
2152
  console.log(
2014
2153
  `Creating connector${useByocEnvironment ? " (BYOC environment)" : ""}...`
2015
2154
  );
@@ -2166,6 +2305,7 @@ async function createAction2(type, url, options) {
2166
2305
  });
2167
2306
  if (isDegraded) {
2168
2307
  console.log("\u2713 Connector created");
2308
+ console.log(` Name: ${connectorName}`);
2169
2309
  console.log(` ID: ${connectorId}`);
2170
2310
  console.log("");
2171
2311
  let warnings = [];
@@ -2179,6 +2319,7 @@ async function createAction2(type, url, options) {
2179
2319
  printDegradedWarnings(warnings);
2180
2320
  } else {
2181
2321
  console.log("\u2713 Connector created and ready");
2322
+ console.log(` Name: ${connectorName}`);
2182
2323
  console.log(` ID: ${connectorId}`);
2183
2324
  }
2184
2325
  showNextStep();
@@ -2231,18 +2372,27 @@ function renderConnectorIcon(connector) {
2231
2372
  }
2232
2373
 
2233
2374
  // src/commands/connector/list.ts
2375
+ function printOtherProjectsHint(summary, currentProjectHasConnectors, dim2, reset2) {
2376
+ if (!summary) return;
2377
+ const connectorWord = summary.connectorCount === 1 ? "connector" : "connectors";
2378
+ const projectWord = summary.projectCount === 1 ? "project" : "projects";
2379
+ const countPhrase = currentProjectHasConnectors ? `${summary.connectorCount} more ${connectorWord}` : `${summary.connectorCount} ${connectorWord}`;
2380
+ console.log(
2381
+ `${dim2}more: ${countPhrase} across ${summary.projectCount} other ${projectWord} \u2014 switch with: ardent project switch <name>${reset2}`
2382
+ );
2383
+ }
2234
2384
  async function listAction2() {
2385
+ const currentProjectId = getConfig("currentProjectId");
2386
+ if (!currentProjectId) {
2387
+ console.error("\u2717 No current project set. Switch to a project first:");
2388
+ console.error(" ardent project list");
2389
+ console.error(" ardent project switch <name>");
2390
+ process.exit(1);
2391
+ }
2235
2392
  let connectors = [];
2236
2393
  let fromCache = false;
2237
2394
  let cacheTime = "";
2238
2395
  try {
2239
- const currentProjectId = getConfig("currentProjectId");
2240
- if (!currentProjectId) {
2241
- console.error("\u2717 No current project set. Switch to a project first:");
2242
- console.error(" ardent project list");
2243
- console.error(" ardent project switch <name>");
2244
- process.exit(1);
2245
- }
2246
2396
  const result = await api.get(`/v1/cli/connectors?project_id=${currentProjectId}`);
2247
2397
  if (!result.connectors) {
2248
2398
  throw new Error("API returned invalid response: missing connectors array");
@@ -2267,26 +2417,47 @@ async function listAction2() {
2267
2417
  process.exit(1);
2268
2418
  }
2269
2419
  }
2420
+ let otherProjectsSummary = null;
2421
+ if (!fromCache) {
2422
+ try {
2423
+ const all = await api.get("/v1/cli/connectors");
2424
+ const others = (all.connectors ?? []).filter(
2425
+ (connector) => connector.project_id && connector.project_id !== currentProjectId
2426
+ );
2427
+ if (others.length > 0) {
2428
+ otherProjectsSummary = {
2429
+ connectorCount: others.length,
2430
+ projectCount: new Set(others.map((connector) => connector.project_id)).size
2431
+ };
2432
+ }
2433
+ } catch {
2434
+ otherProjectsSummary = null;
2435
+ }
2436
+ }
2270
2437
  trackEvent("CLI: connector list succeeded", { connector_count: connectors.length, from_cache: fromCache });
2271
2438
  if (fromCache) {
2272
2439
  console.log(`\u26A0 Offline - showing cached data from ${cacheTime}
2273
2440
  `);
2274
2441
  }
2275
- if (connectors.length === 0) {
2276
- const green3 = "\x1B[32m";
2277
- const reset3 = "\x1B[0m";
2278
- console.log("No connectors found");
2279
- console.log(`${green3} Create one with: ardent connector create postgresql <url>${reset3}`);
2280
- return;
2281
- }
2282
- const selectedConnector = fromCache ? void 0 : reconcileSelectedConnector(connectors);
2283
- const currentConnectorId = selectedConnector?.id ?? getConfig("currentConnectorId");
2284
2442
  const green2 = "\x1B[32m";
2285
2443
  const dim2 = "\x1B[2m";
2286
2444
  const yellow = "\x1B[33m";
2287
2445
  const red = "\x1B[31m";
2288
2446
  const reset2 = "\x1B[0m";
2289
- console.log("Connectors:\n");
2447
+ const projectLabel = getConfig("currentProjectName") ?? currentProjectId;
2448
+ console.log(`Project: ${projectLabel}
2449
+ `);
2450
+ if (connectors.length === 0) {
2451
+ console.log(" No connectors in this project.");
2452
+ console.log(`${green2} Create one with: ardent connector create postgresql <url>${reset2}`);
2453
+ if (otherProjectsSummary) {
2454
+ console.log("");
2455
+ printOtherProjectsHint(otherProjectsSummary, false, dim2, reset2);
2456
+ }
2457
+ return;
2458
+ }
2459
+ const selectedConnector = fromCache ? void 0 : reconcileSelectedConnector(connectors);
2460
+ const currentConnectorId = selectedConnector?.id ?? getConfig("currentConnectorId");
2290
2461
  let enginePendingCount = 0;
2291
2462
  for (const connector of connectors) {
2292
2463
  const isCurrent = connector.id === currentConnectorId;
@@ -2322,6 +2493,7 @@ async function listAction2() {
2322
2493
  connector_count: connectors.length
2323
2494
  });
2324
2495
  }
2496
+ printOtherProjectsHint(otherProjectsSummary, true, dim2, reset2);
2325
2497
  }
2326
2498
 
2327
2499
  // src/commands/connector/delete.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
4
4
  "description": "Git for Data infrastructure",
5
5
  "type": "module",
6
6
  "bin": {