clivly 0.10.5 → 0.10.7

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.
@@ -6,6 +6,7 @@ require("./deprecate-subpath-B9ngvizt.cjs").warnDeprecatedSubpath("clivly/core/e
6
6
  exports.TARGET_CONCEPTS = require_internal_entity_heuristics.TARGET_CONCEPTS;
7
7
  exports.hasDefiningColumn = require_internal_entity_heuristics.hasDefiningColumn;
8
8
  exports.hasSensitiveColumns = require_internal_entity_heuristics.hasSensitiveColumns;
9
+ exports.isFrameworkTable = require_internal_entity_heuristics.isFrameworkTable;
9
10
  exports.matchesConcept = require_internal_entity_heuristics.matchesConcept;
10
11
  exports.requiredFieldsFor = require_internal_entity_heuristics.requiredFieldsFor;
11
12
  exports.scoreConcepts = require_internal_entity_heuristics.scoreConcepts;
@@ -1,3 +1,3 @@
1
1
  import { p as DiscoveredColumnMeta } from "./entity-config-w8oMBWf8.cjs";
2
- import { FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.cjs";
3
- export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
2
+ import { FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.cjs";
3
+ export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
@@ -1,3 +1,3 @@
1
1
  import { p as DiscoveredColumnMeta } from "./entity-config-w8oMBWf8.mjs";
2
- import { FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.mjs";
3
- export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
2
+ import { FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.mjs";
3
+ export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
@@ -1,6 +1,6 @@
1
- import { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.mjs";
1
+ import { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.mjs";
2
2
  import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
3
3
  //#region src/core-entity-heuristics.ts
4
4
  warnDeprecatedSubpath("clivly/core/entity-heuristics", "clivly/internal/entity-heuristics");
5
5
  //#endregion
6
- export { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
6
+ export { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
package/index.cjs CHANGED
@@ -480,23 +480,38 @@ function cloudflaredMissingMessage() {
480
480
  * Warning someone away from a tunnel that was fine is worse than staying quiet,
481
481
  * for the same reason the port probe defaults to assuming the port is alive.
482
482
  */
483
- /** How long to wait before giving up and returning `unknown`. */
483
+ /** How long any single probe attempt may take. */
484
484
  const IDENTITY_PROBE_TIMEOUT_MS = 8e3;
485
485
  /**
486
- * Probe the published tunnel URL and report who is on the other end.
486
+ * How long to keep retrying before giving up.
487
487
  *
488
- * `fetchImpl` is injectable so tests do not need a network or a live tunnel.
488
+ * The probe fires the moment `cloudflared` publishes a URL, which on a
489
+ * Vite/Nuxt app is *before* the dev server has finished building its route
490
+ * table. The first attempt therefore reaches a server that is up but does not
491
+ * yet serve the tick route — a cold start, not a wrong port. The condition
492
+ * being detected (a tunnel aimed at the wrong process) does not resolve
493
+ * itself, so waiting costs nothing and a premature verdict costs a lot.
489
494
  */
490
- async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
495
+ const IDENTITY_RETRY_BUDGET_MS = 2e4;
496
+ const IDENTITY_RETRY_DELAY_MS = 2e3;
497
+ const sleep = (ms) => new Promise((resolve) => {
498
+ setTimeout(resolve, ms);
499
+ });
500
+ /** One attempt. Separated so the retry loop stays readable. */
501
+ async function attempt(triggerUrl, fetchImpl) {
491
502
  const controller = new AbortController();
492
503
  const timer = setTimeout(() => controller.abort(), IDENTITY_PROBE_TIMEOUT_MS);
493
504
  try {
494
- return (await fetchImpl(triggerUrl, {
505
+ const response = await fetchImpl(triggerUrl, {
495
506
  method: "POST",
496
507
  headers: { "content-type": "application/json" },
497
508
  body: "{}",
498
509
  signal: controller.signal
499
- })).status === 401 ? "clivly" : "mismatch";
510
+ });
511
+ if (response.status === 401) return "clivly";
512
+ if (response.status === 403) return "host-rejected";
513
+ if (response.status === 404 || response.status >= 500) return "unknown";
514
+ return "mismatch";
500
515
  } catch {
501
516
  return "unknown";
502
517
  } finally {
@@ -504,6 +519,40 @@ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
504
519
  }
505
520
  }
506
521
  /**
522
+ * Probe the published tunnel URL and report who is on the other end.
523
+ *
524
+ * Retries while the answer is inconclusive, because the first attempt lands
525
+ * during the dev server's boot. Only a confident contradiction is reported.
526
+ *
527
+ * `fetchImpl` and `now` are injectable so tests need no network or clock.
528
+ */
529
+ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch, now = Date.now) {
530
+ const deadline = now() + IDENTITY_RETRY_BUDGET_MS;
531
+ let last = "unknown";
532
+ do {
533
+ last = await attempt(triggerUrl, fetchImpl);
534
+ if (last !== "unknown") return last;
535
+ if (now() >= deadline) break;
536
+ await sleep(IDENTITY_RETRY_DELAY_MS);
537
+ } while (now() < deadline);
538
+ return last;
539
+ }
540
+ /**
541
+ * What to tell someone whose dev server refused the tunnel's Host header.
542
+ *
543
+ * `clivly dev` already prints the allowedHosts hint for Vite frameworks before
544
+ * the tunnel opens; this fires when the tunnel proves it was not applied.
545
+ */
546
+ function hostRejectedMessage() {
547
+ return [
548
+ "Warning: your dev server refused the tunnel's Host header (403).",
549
+ " Vite rejects hostnames it does not recognise, so Clivly's request never",
550
+ " reaches your tick route. Add the tunnel domain to your Vite config:",
551
+ " server: { allowedHosts: ['.trycloudflare.com'] }",
552
+ " (in nuxt.config.ts, nest it under `vite:`), then restart the dev server."
553
+ ].join("\n");
554
+ }
555
+ /**
507
556
  * What to tell someone whose tunnel reaches the wrong process.
508
557
  *
509
558
  * Names the likely cause rather than only the symptom: the overwhelmingly
@@ -526,6 +575,12 @@ const UNREACHABLE_RESULT = {
526
575
  ok: false,
527
576
  kind: "unreachable"
528
577
  };
578
+ /** The line to print for a probe verdict, or null when there is nothing to say. */
579
+ function tunnelWarning(identity, port) {
580
+ if (identity === "mismatch") return tunnelMismatchMessage(port);
581
+ if (identity === "host-rejected") return hostRejectedMessage();
582
+ return null;
583
+ }
529
584
  function flagValue$1(argv, flag) {
530
585
  const i = argv.indexOf(flag);
531
586
  return i >= 0 ? argv[i + 1] : void 0;
@@ -570,9 +625,10 @@ async function runDev(options) {
570
625
  if (!await deps.isPortListening(port).catch(() => true)) deps.out(deadPortMessage(port, framework));
571
626
  }
572
627
  const identity = deps.probeTunnel && !flagValue$1(options.argv, "--url") ? await deps.probeTunnel(triggerUrl).catch(() => "unknown") : "unknown";
573
- if (identity === "mismatch") deps.out(tunnelMismatchMessage(port));
628
+ const warning = tunnelWarning(identity, port);
629
+ if (warning) deps.out(warning);
574
630
  const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
575
- if (result.ok) deps.out(identity === "mismatch" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
631
+ if (result.ok) deps.out(identity === "mismatch" || identity === "host-rejected" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
576
632
  else deps.out(overrideFailureMessage(result.kind, result.status));
577
633
  });
578
634
  const stopWatching = deps.watchConfig?.(options.cwd, () => {
@@ -1601,13 +1657,14 @@ function writeCredentials(options, credentials) {
1601
1657
  const entries = credentialEntries(credentials);
1602
1658
  const result = upsertEnvValues(deps.readEnv(options.cwd), entries, { force: options.force });
1603
1659
  deps.writeEnv(options.cwd, result.contents);
1660
+ const envFile = deps.envFileName?.(options.cwd) ?? ".env";
1604
1661
  deps.out("\nLogin successful.\n");
1605
1662
  const valuesByKey = {
1606
1663
  [SECRET_KEY_ENV]: credentials.apiKey,
1607
1664
  CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
1608
1665
  };
1609
1666
  for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
1610
- else deps.out(` ✓ ${entry.key}=${maskSecret(valuesByKey[entry.key] ?? "")} written to .env`);
1667
+ else deps.out(` ✓ ${entry.key}=${maskSecret(valuesByKey[entry.key] ?? "")} written to ${envFile}`);
1611
1668
  }
1612
1669
  async function runLogin(options) {
1613
1670
  const { deps } = options;
@@ -1650,7 +1707,10 @@ function rankCandidates$1(tables, concept) {
1650
1707
  m: require_internal_entity_heuristics.matchesConcept(t.tableName, concept)
1651
1708
  })).filter((c) => Boolean(c.m)).sort((a, b) => {
1652
1709
  const usable = Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(b.t))) - Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(a.t)));
1653
- return usable === 0 ? Number(b.m.exact) - Number(a.m.exact) : usable;
1710
+ if (usable !== 0) return usable;
1711
+ const exact = Number(b.m.exact) - Number(a.m.exact);
1712
+ if (exact !== 0) return exact;
1713
+ return Number(require_internal_entity_heuristics.isFrameworkTable(a.t.tableName)) - Number(require_internal_entity_heuristics.isFrameworkTable(b.t.tableName));
1654
1714
  }).map((c) => c.t);
1655
1715
  }
1656
1716
  function selectEntities(tables) {
@@ -1893,7 +1953,14 @@ export async function register() {
1893
1953
  return;
1894
1954
  }
1895
1955
  try {
1896
- const { default: clivly } = await import("${importPath}");
1956
+ // Built at runtime so the bundler cannot statically trace it. Next
1957
+ // compiles this file for BOTH runtimes, and the guard above only runs on
1958
+ // the edge — it does not stop the edge BUILD from following a literal
1959
+ // import into your config, and from there into your database driver. A
1960
+ // node-postgres app fails that pass on \`require('path')\` and every route
1961
+ // 500s before Clivly is ever reached.
1962
+ const specifier = "${importPath}";
1963
+ const { default: clivly } = await import(/* webpackIgnore: true */ specifier);
1897
1964
  await clivly.start();
1898
1965
  } catch (error) {
1899
1966
  console.warn("[clivly] could not check in:", error);
@@ -2701,7 +2768,14 @@ function reportSchema(report, out) {
2701
2768
  const updated = report.mappings.filter((m) => m.updated);
2702
2769
  if (created.length > 0) out(` ✅ Mapped ${countOf(created.length, "entity", "entities")}: ${created.map((m) => m.entityKey).join(", ")}.\n`);
2703
2770
  if (updated.length > 0) out(` ✅ Updated ${countOf(updated.length, "mapping")}: ${updated.map((m) => m.entityKey).join(", ")}.\n`);
2704
- if (report.mappings.length > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(report.mappings.length, "mapping")} already match your config.\n`);
2771
+ const skipped = report.mappings.filter((m) => m.skipped === "dashboard-owned");
2772
+ if (skipped.length > 0) {
2773
+ out(` ↩︎ Kept the dashboard's version of ${countOf(skipped.length, "entity", "entities")}: ${skipped.map((m) => m.entityKey).join(", ")}.\n`);
2774
+ out(" The dashboard owns these mappings, so clivly.config did not change them.\n");
2775
+ out(" To let your config own one, delete it in Integrations → Mapping, then push again.\n");
2776
+ }
2777
+ const settled = report.mappings.length - skipped.length;
2778
+ if (settled > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(settled, "mapping")} already match your config.\n`);
2705
2779
  }
2706
2780
  async function runPushSchema(options) {
2707
2781
  const out = options.out ?? ((line) => process.stdout.write(line));
@@ -4277,7 +4351,8 @@ function defaultLoginDeps() {
4277
4351
  sleep: (ms) => new Promise((resolve) => {
4278
4352
  setTimeout(resolve, ms);
4279
4353
  }),
4280
- writeEnv: defaultWriteEnv
4354
+ writeEnv: defaultWriteEnv,
4355
+ envFileName: (cwd) => (0, node_path.basename)(resolveEnvPath(cwd))
4281
4356
  };
4282
4357
  }
4283
4358
  function runInitFromArgv(argv, cwd) {
package/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as CANONICAL_FIELDS } from "./entity-config-bC3DX9f1.mjs";
3
3
  import "./mapping-score-C68uAqGB.mjs";
4
- import { hasDefiningColumn, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
4
+ import { hasDefiningColumn, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
5
5
  import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-BTzaM8bF.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { spawn } from "node:child_process";
8
8
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
9
9
  import { homedir, hostname } from "node:os";
10
- import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
11
11
  import { createInterface } from "node:readline/promises";
12
12
  import { createJiti } from "jiti";
13
13
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -479,23 +479,38 @@ function cloudflaredMissingMessage() {
479
479
  * Warning someone away from a tunnel that was fine is worse than staying quiet,
480
480
  * for the same reason the port probe defaults to assuming the port is alive.
481
481
  */
482
- /** How long to wait before giving up and returning `unknown`. */
482
+ /** How long any single probe attempt may take. */
483
483
  const IDENTITY_PROBE_TIMEOUT_MS = 8e3;
484
484
  /**
485
- * Probe the published tunnel URL and report who is on the other end.
485
+ * How long to keep retrying before giving up.
486
486
  *
487
- * `fetchImpl` is injectable so tests do not need a network or a live tunnel.
487
+ * The probe fires the moment `cloudflared` publishes a URL, which on a
488
+ * Vite/Nuxt app is *before* the dev server has finished building its route
489
+ * table. The first attempt therefore reaches a server that is up but does not
490
+ * yet serve the tick route — a cold start, not a wrong port. The condition
491
+ * being detected (a tunnel aimed at the wrong process) does not resolve
492
+ * itself, so waiting costs nothing and a premature verdict costs a lot.
488
493
  */
489
- async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
494
+ const IDENTITY_RETRY_BUDGET_MS = 2e4;
495
+ const IDENTITY_RETRY_DELAY_MS = 2e3;
496
+ const sleep = (ms) => new Promise((resolve) => {
497
+ setTimeout(resolve, ms);
498
+ });
499
+ /** One attempt. Separated so the retry loop stays readable. */
500
+ async function attempt(triggerUrl, fetchImpl) {
490
501
  const controller = new AbortController();
491
502
  const timer = setTimeout(() => controller.abort(), IDENTITY_PROBE_TIMEOUT_MS);
492
503
  try {
493
- return (await fetchImpl(triggerUrl, {
504
+ const response = await fetchImpl(triggerUrl, {
494
505
  method: "POST",
495
506
  headers: { "content-type": "application/json" },
496
507
  body: "{}",
497
508
  signal: controller.signal
498
- })).status === 401 ? "clivly" : "mismatch";
509
+ });
510
+ if (response.status === 401) return "clivly";
511
+ if (response.status === 403) return "host-rejected";
512
+ if (response.status === 404 || response.status >= 500) return "unknown";
513
+ return "mismatch";
499
514
  } catch {
500
515
  return "unknown";
501
516
  } finally {
@@ -503,6 +518,40 @@ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
503
518
  }
504
519
  }
505
520
  /**
521
+ * Probe the published tunnel URL and report who is on the other end.
522
+ *
523
+ * Retries while the answer is inconclusive, because the first attempt lands
524
+ * during the dev server's boot. Only a confident contradiction is reported.
525
+ *
526
+ * `fetchImpl` and `now` are injectable so tests need no network or clock.
527
+ */
528
+ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch, now = Date.now) {
529
+ const deadline = now() + IDENTITY_RETRY_BUDGET_MS;
530
+ let last = "unknown";
531
+ do {
532
+ last = await attempt(triggerUrl, fetchImpl);
533
+ if (last !== "unknown") return last;
534
+ if (now() >= deadline) break;
535
+ await sleep(IDENTITY_RETRY_DELAY_MS);
536
+ } while (now() < deadline);
537
+ return last;
538
+ }
539
+ /**
540
+ * What to tell someone whose dev server refused the tunnel's Host header.
541
+ *
542
+ * `clivly dev` already prints the allowedHosts hint for Vite frameworks before
543
+ * the tunnel opens; this fires when the tunnel proves it was not applied.
544
+ */
545
+ function hostRejectedMessage() {
546
+ return [
547
+ "Warning: your dev server refused the tunnel's Host header (403).",
548
+ " Vite rejects hostnames it does not recognise, so Clivly's request never",
549
+ " reaches your tick route. Add the tunnel domain to your Vite config:",
550
+ " server: { allowedHosts: ['.trycloudflare.com'] }",
551
+ " (in nuxt.config.ts, nest it under `vite:`), then restart the dev server."
552
+ ].join("\n");
553
+ }
554
+ /**
506
555
  * What to tell someone whose tunnel reaches the wrong process.
507
556
  *
508
557
  * Names the likely cause rather than only the symptom: the overwhelmingly
@@ -525,6 +574,12 @@ const UNREACHABLE_RESULT = {
525
574
  ok: false,
526
575
  kind: "unreachable"
527
576
  };
577
+ /** The line to print for a probe verdict, or null when there is nothing to say. */
578
+ function tunnelWarning(identity, port) {
579
+ if (identity === "mismatch") return tunnelMismatchMessage(port);
580
+ if (identity === "host-rejected") return hostRejectedMessage();
581
+ return null;
582
+ }
528
583
  function flagValue$1(argv, flag) {
529
584
  const i = argv.indexOf(flag);
530
585
  return i >= 0 ? argv[i + 1] : void 0;
@@ -569,9 +624,10 @@ async function runDev(options) {
569
624
  if (!await deps.isPortListening(port).catch(() => true)) deps.out(deadPortMessage(port, framework));
570
625
  }
571
626
  const identity = deps.probeTunnel && !flagValue$1(options.argv, "--url") ? await deps.probeTunnel(triggerUrl).catch(() => "unknown") : "unknown";
572
- if (identity === "mismatch") deps.out(tunnelMismatchMessage(port));
627
+ const warning = tunnelWarning(identity, port);
628
+ if (warning) deps.out(warning);
573
629
  const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
574
- if (result.ok) deps.out(identity === "mismatch" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
630
+ if (result.ok) deps.out(identity === "mismatch" || identity === "host-rejected" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
575
631
  else deps.out(overrideFailureMessage(result.kind, result.status));
576
632
  });
577
633
  const stopWatching = deps.watchConfig?.(options.cwd, () => {
@@ -1600,13 +1656,14 @@ function writeCredentials(options, credentials) {
1600
1656
  const entries = credentialEntries(credentials);
1601
1657
  const result = upsertEnvValues(deps.readEnv(options.cwd), entries, { force: options.force });
1602
1658
  deps.writeEnv(options.cwd, result.contents);
1659
+ const envFile = deps.envFileName?.(options.cwd) ?? ".env";
1603
1660
  deps.out("\nLogin successful.\n");
1604
1661
  const valuesByKey = {
1605
1662
  [SECRET_KEY_ENV]: credentials.apiKey,
1606
1663
  CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
1607
1664
  };
1608
1665
  for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
1609
- else deps.out(` ✓ ${entry.key}=${maskSecret(valuesByKey[entry.key] ?? "")} written to .env`);
1666
+ else deps.out(` ✓ ${entry.key}=${maskSecret(valuesByKey[entry.key] ?? "")} written to ${envFile}`);
1610
1667
  }
1611
1668
  async function runLogin(options) {
1612
1669
  const { deps } = options;
@@ -1649,7 +1706,10 @@ function rankCandidates$1(tables, concept) {
1649
1706
  m: matchesConcept(t.tableName, concept)
1650
1707
  })).filter((c) => Boolean(c.m)).sort((a, b) => {
1651
1708
  const usable = Number(hasDefiningColumn(concept, columnNames(b.t))) - Number(hasDefiningColumn(concept, columnNames(a.t)));
1652
- return usable === 0 ? Number(b.m.exact) - Number(a.m.exact) : usable;
1709
+ if (usable !== 0) return usable;
1710
+ const exact = Number(b.m.exact) - Number(a.m.exact);
1711
+ if (exact !== 0) return exact;
1712
+ return Number(isFrameworkTable(a.t.tableName)) - Number(isFrameworkTable(b.t.tableName));
1653
1713
  }).map((c) => c.t);
1654
1714
  }
1655
1715
  function selectEntities(tables) {
@@ -1892,7 +1952,14 @@ export async function register() {
1892
1952
  return;
1893
1953
  }
1894
1954
  try {
1895
- const { default: clivly } = await import("${importPath}");
1955
+ // Built at runtime so the bundler cannot statically trace it. Next
1956
+ // compiles this file for BOTH runtimes, and the guard above only runs on
1957
+ // the edge — it does not stop the edge BUILD from following a literal
1958
+ // import into your config, and from there into your database driver. A
1959
+ // node-postgres app fails that pass on \`require('path')\` and every route
1960
+ // 500s before Clivly is ever reached.
1961
+ const specifier = "${importPath}";
1962
+ const { default: clivly } = await import(/* webpackIgnore: true */ specifier);
1896
1963
  await clivly.start();
1897
1964
  } catch (error) {
1898
1965
  console.warn("[clivly] could not check in:", error);
@@ -2700,7 +2767,14 @@ function reportSchema(report, out) {
2700
2767
  const updated = report.mappings.filter((m) => m.updated);
2701
2768
  if (created.length > 0) out(` ✅ Mapped ${countOf(created.length, "entity", "entities")}: ${created.map((m) => m.entityKey).join(", ")}.\n`);
2702
2769
  if (updated.length > 0) out(` ✅ Updated ${countOf(updated.length, "mapping")}: ${updated.map((m) => m.entityKey).join(", ")}.\n`);
2703
- if (report.mappings.length > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(report.mappings.length, "mapping")} already match your config.\n`);
2770
+ const skipped = report.mappings.filter((m) => m.skipped === "dashboard-owned");
2771
+ if (skipped.length > 0) {
2772
+ out(` ↩︎ Kept the dashboard's version of ${countOf(skipped.length, "entity", "entities")}: ${skipped.map((m) => m.entityKey).join(", ")}.\n`);
2773
+ out(" The dashboard owns these mappings, so clivly.config did not change them.\n");
2774
+ out(" To let your config own one, delete it in Integrations → Mapping, then push again.\n");
2775
+ }
2776
+ const settled = report.mappings.length - skipped.length;
2777
+ if (settled > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(settled, "mapping")} already match your config.\n`);
2704
2778
  }
2705
2779
  async function runPushSchema(options) {
2706
2780
  const out = options.out ?? ((line) => process.stdout.write(line));
@@ -4276,7 +4350,8 @@ function defaultLoginDeps() {
4276
4350
  sleep: (ms) => new Promise((resolve) => {
4277
4351
  setTimeout(resolve, ms);
4278
4352
  }),
4279
- writeEnv: defaultWriteEnv
4353
+ writeEnv: defaultWriteEnv,
4354
+ envFileName: (cwd) => basename(resolveEnvPath(cwd))
4280
4355
  };
4281
4356
  }
4282
4357
  function runInitFromArgv(argv, cwd) {
@@ -283,6 +283,54 @@ const SENSITIVE_COLUMNS = /* @__PURE__ */ new Set([
283
283
  function hasSensitiveColumns(columns) {
284
284
  return columns.some((column) => SENSITIVE_COLUMNS.has(column.toLowerCase().replaceAll("-", "_")));
285
285
  }
286
+ /**
287
+ * Table names the supported auth libraries create.
288
+ *
289
+ * `hasSensitiveColumns` cannot catch these. Better Auth's `organization` table
290
+ * is `{id, name, slug, logo}` — no credential column anywhere, a real `name`,
291
+ * and "organizations" is in the company vocabulary above. It is, by every
292
+ * signal this module has, an excellent company candidate. On `vue-academy`
293
+ * `clivly init` duly chose it over `campuses`, the actual business table that
294
+ * every student row points at, and the first sync exported rows nothing could
295
+ * receive.
296
+ *
297
+ * Matched on name, deliberately, and this is the inverse of the rule above:
298
+ * withholding a table on a wrong guess is a real loss, so that is decided on
299
+ * columns; *ranking* one below another is cheap to get wrong, so a name match
300
+ * is enough. These are fixed names the plugins create, and an app whose own
301
+ * business table is called `member` still ranks — just after its rivals.
302
+ */
303
+ const FRAMEWORK_TABLE_NAMES = /* @__PURE__ */ new Set([
304
+ "account",
305
+ "session",
306
+ "user",
307
+ "verification",
308
+ "invitation",
309
+ "member",
310
+ "organization",
311
+ "team",
312
+ "team_member",
313
+ "apikey",
314
+ "api_key",
315
+ "jwks",
316
+ "passkey",
317
+ "rate_limit",
318
+ "ratelimit",
319
+ "two_factor",
320
+ "twofactor",
321
+ "__drizzle_migrations",
322
+ "drizzle_migrations"
323
+ ]);
324
+ /**
325
+ * Is this table one your auth library created, rather than one you wrote?
326
+ *
327
+ * Used to rank such tables below the developer's own when both could serve a
328
+ * concept. Never to exclude them: mapping `user` as the contact entity is
329
+ * legitimate for plenty of apps, and it stays selectable.
330
+ */
331
+ function isFrameworkTable(name) {
332
+ return FRAMEWORK_TABLE_NAMES.has(name.toLowerCase().replaceAll("-", "_"));
333
+ }
286
334
  function metaFor(table, column) {
287
335
  return table.columnsMeta?.find((c) => c.name === column);
288
336
  }
@@ -497,6 +545,7 @@ function toFieldMap(scored) {
497
545
  exports.TARGET_CONCEPTS = TARGET_CONCEPTS;
498
546
  exports.hasDefiningColumn = hasDefiningColumn;
499
547
  exports.hasSensitiveColumns = hasSensitiveColumns;
548
+ exports.isFrameworkTable = isFrameworkTable;
500
549
  exports.matchesConcept = matchesConcept;
501
550
  exports.requiredFieldsFor = requiredFieldsFor;
502
551
  exports.scoreConcepts = scoreConcepts;
@@ -19,6 +19,14 @@ interface ScoredTable {
19
19
  * They stay manually selectable — an explicit, informed choice is legitimate.
20
20
  */
21
21
  declare function hasSensitiveColumns(columns: string[]): boolean;
22
+ /**
23
+ * Is this table one your auth library created, rather than one you wrote?
24
+ *
25
+ * Used to rank such tables below the developer's own when both could serve a
26
+ * concept. Never to exclude them: mapping `user` as the contact entity is
27
+ * legitimate for plenty of apps, and it stays selectable.
28
+ */
29
+ declare function isFrameworkTable(name: string): boolean;
22
30
  declare function hasDefiningColumn(concept: TargetConcept, columns: string[]): boolean;
23
31
  declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
24
32
  declare function topConcept(table: ScoredTable): {
@@ -61,4 +69,4 @@ interface FieldSuggestion {
61
69
  declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
62
70
  declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
63
71
  //#endregion
64
- export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
72
+ export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
@@ -19,6 +19,14 @@ interface ScoredTable {
19
19
  * They stay manually selectable — an explicit, informed choice is legitimate.
20
20
  */
21
21
  declare function hasSensitiveColumns(columns: string[]): boolean;
22
+ /**
23
+ * Is this table one your auth library created, rather than one you wrote?
24
+ *
25
+ * Used to rank such tables below the developer's own when both could serve a
26
+ * concept. Never to exclude them: mapping `user` as the contact entity is
27
+ * legitimate for plenty of apps, and it stays selectable.
28
+ */
29
+ declare function isFrameworkTable(name: string): boolean;
22
30
  declare function hasDefiningColumn(concept: TargetConcept, columns: string[]): boolean;
23
31
  declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
24
32
  declare function topConcept(table: ScoredTable): {
@@ -61,4 +69,4 @@ interface FieldSuggestion {
61
69
  declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
62
70
  declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
63
71
  //#endregion
64
- export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
72
+ export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
@@ -282,6 +282,54 @@ const SENSITIVE_COLUMNS = /* @__PURE__ */ new Set([
282
282
  function hasSensitiveColumns(columns) {
283
283
  return columns.some((column) => SENSITIVE_COLUMNS.has(column.toLowerCase().replaceAll("-", "_")));
284
284
  }
285
+ /**
286
+ * Table names the supported auth libraries create.
287
+ *
288
+ * `hasSensitiveColumns` cannot catch these. Better Auth's `organization` table
289
+ * is `{id, name, slug, logo}` — no credential column anywhere, a real `name`,
290
+ * and "organizations" is in the company vocabulary above. It is, by every
291
+ * signal this module has, an excellent company candidate. On `vue-academy`
292
+ * `clivly init` duly chose it over `campuses`, the actual business table that
293
+ * every student row points at, and the first sync exported rows nothing could
294
+ * receive.
295
+ *
296
+ * Matched on name, deliberately, and this is the inverse of the rule above:
297
+ * withholding a table on a wrong guess is a real loss, so that is decided on
298
+ * columns; *ranking* one below another is cheap to get wrong, so a name match
299
+ * is enough. These are fixed names the plugins create, and an app whose own
300
+ * business table is called `member` still ranks — just after its rivals.
301
+ */
302
+ const FRAMEWORK_TABLE_NAMES = /* @__PURE__ */ new Set([
303
+ "account",
304
+ "session",
305
+ "user",
306
+ "verification",
307
+ "invitation",
308
+ "member",
309
+ "organization",
310
+ "team",
311
+ "team_member",
312
+ "apikey",
313
+ "api_key",
314
+ "jwks",
315
+ "passkey",
316
+ "rate_limit",
317
+ "ratelimit",
318
+ "two_factor",
319
+ "twofactor",
320
+ "__drizzle_migrations",
321
+ "drizzle_migrations"
322
+ ]);
323
+ /**
324
+ * Is this table one your auth library created, rather than one you wrote?
325
+ *
326
+ * Used to rank such tables below the developer's own when both could serve a
327
+ * concept. Never to exclude them: mapping `user` as the contact entity is
328
+ * legitimate for plenty of apps, and it stays selectable.
329
+ */
330
+ function isFrameworkTable(name) {
331
+ return FRAMEWORK_TABLE_NAMES.has(name.toLowerCase().replaceAll("-", "_"));
332
+ }
285
333
  function metaFor(table, column) {
286
334
  return table.columnsMeta?.find((c) => c.name === column);
287
335
  }
@@ -493,4 +541,4 @@ function toFieldMap(scored) {
493
541
  return map;
494
542
  }
495
543
  //#endregion
496
- export { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
544
+ export { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clivly",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly CLI — scaffold a CRM into your app (clivly init).",