clivly 0.10.6 → 0.10.8

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/drizzle.cjs CHANGED
@@ -60,6 +60,7 @@ function fromDrizzleProjection(db, projection, opts) {
60
60
  const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
61
61
  return {
62
62
  entity: opts.entity,
63
+ pushSource: projection.baseTable,
63
64
  cursorField: cursorToken,
64
65
  idField: "id",
65
66
  ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
@@ -89,6 +90,7 @@ function fromDrizzleProjectionByIdentity(client, projection, identityExpr, fromC
89
90
  const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
90
91
  return {
91
92
  entity: opts.entity,
93
+ pushSource: projection.baseTable,
92
94
  cursorField: "id",
93
95
  idField: "id",
94
96
  ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
package/drizzle.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { R as ProjectionMapping, z as ProjectionRef } from "./entity-config-w8oMBWf8.cjs";
2
2
  import { n as buildUniqueConstraints, r as discoverFromDrizzle } from "./drizzle-SD8WCvYj.cjs";
3
- import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-lj_0bJrc.cjs";
3
+ import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-C3xMOqWE.cjs";
4
4
  import { Table } from "drizzle-orm";
5
5
  //#region ../sdk/src/drizzle-projection.d.ts
6
6
  /**
package/drizzle.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { R as ProjectionMapping, z as ProjectionRef } from "./entity-config-w8oMBWf8.mjs";
2
2
  import { n as buildUniqueConstraints, r as discoverFromDrizzle } from "./drizzle-axugRJ0u.mjs";
3
- import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-ouHKZas2.mjs";
3
+ import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-BmxtnOfc.mjs";
4
4
  import { Table } from "drizzle-orm";
5
5
  //#region ../sdk/src/drizzle-projection.d.ts
6
6
  /**
package/drizzle.mjs CHANGED
@@ -59,6 +59,7 @@ function fromDrizzleProjection(db, projection, opts) {
59
59
  const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
60
60
  return {
61
61
  entity: opts.entity,
62
+ pushSource: projection.baseTable,
62
63
  cursorField: cursorToken,
63
64
  idField: "id",
64
65
  ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
@@ -88,6 +89,7 @@ function fromDrizzleProjectionByIdentity(client, projection, identityExpr, fromC
88
89
  const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
89
90
  return {
90
91
  entity: opts.entity,
92
+ pushSource: projection.baseTable,
91
93
  cursorField: "id",
92
94
  idField: "id",
93
95
  ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
package/index.cjs CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_entity_config = require("./entity-config-C2MfIDL2.cjs");
4
4
  require("./mapping-score-CaB3F4Uu.cjs");
5
5
  const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
6
- const require_sdk = require("./sdk-DbvqXNSh.cjs");
6
+ const require_sdk = require("./sdk-BAHcnhm7.cjs");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_fs = require("node:fs");
9
9
  let node_module = require("node:module");
@@ -1657,13 +1657,14 @@ function writeCredentials(options, credentials) {
1657
1657
  const entries = credentialEntries(credentials);
1658
1658
  const result = upsertEnvValues(deps.readEnv(options.cwd), entries, { force: options.force });
1659
1659
  deps.writeEnv(options.cwd, result.contents);
1660
+ const envFile = deps.envFileName?.(options.cwd) ?? ".env";
1660
1661
  deps.out("\nLogin successful.\n");
1661
1662
  const valuesByKey = {
1662
1663
  [SECRET_KEY_ENV]: credentials.apiKey,
1663
1664
  CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
1664
1665
  };
1665
1666
  for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
1666
- 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}`);
1667
1668
  }
1668
1669
  async function runLogin(options) {
1669
1670
  const { deps } = options;
@@ -1706,7 +1707,10 @@ function rankCandidates$1(tables, concept) {
1706
1707
  m: require_internal_entity_heuristics.matchesConcept(t.tableName, concept)
1707
1708
  })).filter((c) => Boolean(c.m)).sort((a, b) => {
1708
1709
  const usable = Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(b.t))) - Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(a.t)));
1709
- 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));
1710
1714
  }).map((c) => c.t);
1711
1715
  }
1712
1716
  function selectEntities(tables) {
@@ -1949,6 +1953,18 @@ export async function register() {
1949
1953
  return;
1950
1954
  }
1951
1955
  try {
1956
+ // A plain import, deliberately. Next compiles this file for BOTH runtimes
1957
+ // and follows this into your config and on into your database driver, so a
1958
+ // Node-only driver breaks the EDGE build — the guard above is a runtime
1959
+ // check and cannot prevent that. The fix belongs in next.config, where
1960
+ // \`clivly init\` writes an edge-only \`config.externals\` entry; nothing here
1961
+ // ever executes on the edge, so the driver only has to resolve.
1962
+ //
1963
+ // Do NOT make this untraceable (a variable specifier, webpackIgnore).
1964
+ // That silences the build error and breaks the app in a worse way:
1965
+ // clivly.config is TypeScript, so an unbundled import cannot load it, and
1966
+ // the catch below turns that into a warning — the app boots, serves
1967
+ // traffic, and never checks in.
1952
1968
  const { default: clivly } = await import("${importPath}");
1953
1969
  await clivly.start();
1954
1970
  } catch (error) {
@@ -2002,6 +2018,46 @@ function nextNeedsInstrumentationFlag(versionRange) {
2002
2018
  }
2003
2019
  const NEXT_INSTRUMENTATION_FLAG_STEP = `Your Next.js version reads instrumentation.ts only when it is switched on. Add this to next.config, or Clivly will never be told your app started:
2004
2020
  experimental: { instrumentationHook: true }`;
2021
+ const NODE_ONLY_DRIVERS = /* @__PURE__ */ new Set([
2022
+ "pg",
2023
+ "mysql",
2024
+ "mysql2",
2025
+ "better-sqlite3",
2026
+ "sqlite3",
2027
+ "tedious",
2028
+ "oracledb",
2029
+ "ioredis"
2030
+ ]);
2031
+ /**
2032
+ * The Node-only database drivers a config's dependency list contains.
2033
+ *
2034
+ * Next compiles instrumentation.ts for the edge runtime whether or not the app
2035
+ * uses it, and follows the boot hook's config import into the driver. `pg`
2036
+ * pulls in `pgpass` → `split2` → `stream`, none of which resolve there, so the
2037
+ * build fails and every route 500s — before any Clivly code runs.
2038
+ *
2039
+ * `serverExternalPackages` does NOT fix this. It applies to the Node server
2040
+ * bundle only; the edge pass ignores it entirely, which is why adding package
2041
+ * after package to it never terminates. An edge-only `config.externals` is what
2042
+ * works, and it is safe precisely because nothing in that bundle ever executes
2043
+ * — the boot hook returns early unless NEXT_RUNTIME is "nodejs".
2044
+ */
2045
+ function nodeOnlyDrivers(dependencies) {
2046
+ if (!dependencies) return [];
2047
+ return Object.keys(dependencies).filter((name) => NODE_ONLY_DRIVERS.has(name)).sort();
2048
+ }
2049
+ /** The next.config step an app needs when its config reaches a Node driver. */
2050
+ function nextEdgeExternalsStep(drivers) {
2051
+ const list = drivers.map((d) => `"${d}"`).join(", ");
2052
+ return `Your database driver (${drivers.join(", ")}) cannot be bundled for Next's edge runtime, which compiles instrumentation.ts whether or not you use it. Without this, every route fails to build. Add to next.config:
2053
+ webpack: (config, { nextRuntime }) => {
2054
+ if (nextRuntime === "edge") {
2055
+ config.externals = [...(config.externals ?? []), ${list}];
2056
+ }
2057
+ return config;
2058
+ }
2059
+ Nothing runs on the edge — the boot hook exits early there — so the driver only has to resolve.`;
2060
+ }
2005
2061
  const REMIX_BOOT_STEP = `Start Clivly when your app boots. Add this near the top of app/entry.server.tsx:
2006
2062
  import clivly from "./clivly.config";
2007
2063
  clivly.start().catch((error) => console.warn("[clivly] could not check in:", error));`;
@@ -2552,21 +2608,32 @@ function planEnv(args) {
2552
2608
  action: envAction(addedKeys.length, existing.length)
2553
2609
  };
2554
2610
  }
2555
- function declaredNextVersion(args) {
2611
+ function hostManifest(args) {
2556
2612
  const raw = args.readFile((0, node_path.join)(args.cwd, "package.json"));
2557
- if (!raw) return;
2558
- let pkg = {};
2613
+ if (!raw) return {};
2559
2614
  try {
2560
- pkg = JSON.parse(raw);
2615
+ return JSON.parse(raw);
2561
2616
  } catch {
2562
- pkg = {};
2617
+ return {};
2563
2618
  }
2619
+ }
2620
+ function declaredNextVersion(args) {
2621
+ const pkg = hostManifest(args);
2564
2622
  return pkg.dependencies?.next ?? pkg.devDependencies?.next;
2565
2623
  }
2624
+ function declaredDependencies(args) {
2625
+ return hostManifest(args).dependencies;
2626
+ }
2627
+ /** The edge-externals step, or null when no Node-only driver is declared. */
2628
+ function edgeExternalsStepFor(dependencies) {
2629
+ const drivers = nodeOnlyDrivers(dependencies);
2630
+ return drivers.length > 0 ? nextEdgeExternalsStep(drivers) : null;
2631
+ }
2566
2632
  function planBootPatch(args) {
2567
2633
  if (bootHookPath(args.framework, args.layout) !== null) return {
2568
2634
  file: null,
2569
- manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null
2635
+ manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null,
2636
+ extra: args.framework === "nextjs" ? edgeExternalsStepFor(declaredDependencies(args)) : null
2570
2637
  };
2571
2638
  if (args.framework === "remix") return {
2572
2639
  file: null,
@@ -2612,7 +2679,7 @@ function planMutations(args) {
2612
2679
  return {
2613
2680
  files,
2614
2681
  env: planEnv(args),
2615
- manualSteps: boot.manual ? [boot.manual] : []
2682
+ manualSteps: [boot.manual, boot.extra ?? null].filter((step) => step !== null)
2616
2683
  };
2617
2684
  }
2618
2685
  function planWidgetMutations(args) {
@@ -2757,7 +2824,14 @@ function reportSchema(report, out) {
2757
2824
  const updated = report.mappings.filter((m) => m.updated);
2758
2825
  if (created.length > 0) out(` ✅ Mapped ${countOf(created.length, "entity", "entities")}: ${created.map((m) => m.entityKey).join(", ")}.\n`);
2759
2826
  if (updated.length > 0) out(` ✅ Updated ${countOf(updated.length, "mapping")}: ${updated.map((m) => m.entityKey).join(", ")}.\n`);
2760
- if (report.mappings.length > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(report.mappings.length, "mapping")} already match your config.\n`);
2827
+ const skipped = report.mappings.filter((m) => m.skipped === "dashboard-owned");
2828
+ if (skipped.length > 0) {
2829
+ out(` ↩︎ Kept the dashboard's version of ${countOf(skipped.length, "entity", "entities")}: ${skipped.map((m) => m.entityKey).join(", ")}.\n`);
2830
+ out(" The dashboard owns these mappings, so clivly.config did not change them.\n");
2831
+ out(" To let your config own one, delete it in Integrations → Mapping, then push again.\n");
2832
+ }
2833
+ const settled = report.mappings.length - skipped.length;
2834
+ if (settled > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(settled, "mapping")} already match your config.\n`);
2761
2835
  }
2762
2836
  async function runPushSchema(options) {
2763
2837
  const out = options.out ?? ((line) => process.stdout.write(line));
@@ -4012,6 +4086,10 @@ function previewMutations(deps, mutations, options) {
4012
4086
  out(` ${mutations.env.action.padEnd(9)} .env\n`);
4013
4087
  for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
4014
4088
  }
4089
+ if (mutations.manualSteps.length > 0) {
4090
+ out("\nYou will also need to:\n");
4091
+ for (const step of mutations.manualSteps) out(`\n ! ${step}\n`);
4092
+ }
4015
4093
  out(options.diff ? "\nRe-run without --diff to apply; --force to overwrite the files marked conflict.\n" : "\nRe-run without --dry-run to apply.\n");
4016
4094
  }
4017
4095
  function printNextSteps(out, authComplete, loginRan, envFile, needsViteDotenv = false) {
@@ -4333,7 +4411,8 @@ function defaultLoginDeps() {
4333
4411
  sleep: (ms) => new Promise((resolve) => {
4334
4412
  setTimeout(resolve, ms);
4335
4413
  }),
4336
- writeEnv: defaultWriteEnv
4414
+ writeEnv: defaultWriteEnv,
4415
+ envFileName: (cwd) => (0, node_path.basename)(resolveEnvPath(cwd))
4337
4416
  };
4338
4417
  }
4339
4418
  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";
5
- import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-BTzaM8bF.mjs";
4
+ import { hasDefiningColumn, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
5
+ import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-C0DU5KDS.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";
@@ -1656,13 +1656,14 @@ function writeCredentials(options, credentials) {
1656
1656
  const entries = credentialEntries(credentials);
1657
1657
  const result = upsertEnvValues(deps.readEnv(options.cwd), entries, { force: options.force });
1658
1658
  deps.writeEnv(options.cwd, result.contents);
1659
+ const envFile = deps.envFileName?.(options.cwd) ?? ".env";
1659
1660
  deps.out("\nLogin successful.\n");
1660
1661
  const valuesByKey = {
1661
1662
  [SECRET_KEY_ENV]: credentials.apiKey,
1662
1663
  CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
1663
1664
  };
1664
1665
  for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
1665
- 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}`);
1666
1667
  }
1667
1668
  async function runLogin(options) {
1668
1669
  const { deps } = options;
@@ -1705,7 +1706,10 @@ function rankCandidates$1(tables, concept) {
1705
1706
  m: matchesConcept(t.tableName, concept)
1706
1707
  })).filter((c) => Boolean(c.m)).sort((a, b) => {
1707
1708
  const usable = Number(hasDefiningColumn(concept, columnNames(b.t))) - Number(hasDefiningColumn(concept, columnNames(a.t)));
1708
- 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));
1709
1713
  }).map((c) => c.t);
1710
1714
  }
1711
1715
  function selectEntities(tables) {
@@ -1948,6 +1952,18 @@ export async function register() {
1948
1952
  return;
1949
1953
  }
1950
1954
  try {
1955
+ // A plain import, deliberately. Next compiles this file for BOTH runtimes
1956
+ // and follows this into your config and on into your database driver, so a
1957
+ // Node-only driver breaks the EDGE build — the guard above is a runtime
1958
+ // check and cannot prevent that. The fix belongs in next.config, where
1959
+ // \`clivly init\` writes an edge-only \`config.externals\` entry; nothing here
1960
+ // ever executes on the edge, so the driver only has to resolve.
1961
+ //
1962
+ // Do NOT make this untraceable (a variable specifier, webpackIgnore).
1963
+ // That silences the build error and breaks the app in a worse way:
1964
+ // clivly.config is TypeScript, so an unbundled import cannot load it, and
1965
+ // the catch below turns that into a warning — the app boots, serves
1966
+ // traffic, and never checks in.
1951
1967
  const { default: clivly } = await import("${importPath}");
1952
1968
  await clivly.start();
1953
1969
  } catch (error) {
@@ -2001,6 +2017,46 @@ function nextNeedsInstrumentationFlag(versionRange) {
2001
2017
  }
2002
2018
  const NEXT_INSTRUMENTATION_FLAG_STEP = `Your Next.js version reads instrumentation.ts only when it is switched on. Add this to next.config, or Clivly will never be told your app started:
2003
2019
  experimental: { instrumentationHook: true }`;
2020
+ const NODE_ONLY_DRIVERS = /* @__PURE__ */ new Set([
2021
+ "pg",
2022
+ "mysql",
2023
+ "mysql2",
2024
+ "better-sqlite3",
2025
+ "sqlite3",
2026
+ "tedious",
2027
+ "oracledb",
2028
+ "ioredis"
2029
+ ]);
2030
+ /**
2031
+ * The Node-only database drivers a config's dependency list contains.
2032
+ *
2033
+ * Next compiles instrumentation.ts for the edge runtime whether or not the app
2034
+ * uses it, and follows the boot hook's config import into the driver. `pg`
2035
+ * pulls in `pgpass` → `split2` → `stream`, none of which resolve there, so the
2036
+ * build fails and every route 500s — before any Clivly code runs.
2037
+ *
2038
+ * `serverExternalPackages` does NOT fix this. It applies to the Node server
2039
+ * bundle only; the edge pass ignores it entirely, which is why adding package
2040
+ * after package to it never terminates. An edge-only `config.externals` is what
2041
+ * works, and it is safe precisely because nothing in that bundle ever executes
2042
+ * — the boot hook returns early unless NEXT_RUNTIME is "nodejs".
2043
+ */
2044
+ function nodeOnlyDrivers(dependencies) {
2045
+ if (!dependencies) return [];
2046
+ return Object.keys(dependencies).filter((name) => NODE_ONLY_DRIVERS.has(name)).sort();
2047
+ }
2048
+ /** The next.config step an app needs when its config reaches a Node driver. */
2049
+ function nextEdgeExternalsStep(drivers) {
2050
+ const list = drivers.map((d) => `"${d}"`).join(", ");
2051
+ return `Your database driver (${drivers.join(", ")}) cannot be bundled for Next's edge runtime, which compiles instrumentation.ts whether or not you use it. Without this, every route fails to build. Add to next.config:
2052
+ webpack: (config, { nextRuntime }) => {
2053
+ if (nextRuntime === "edge") {
2054
+ config.externals = [...(config.externals ?? []), ${list}];
2055
+ }
2056
+ return config;
2057
+ }
2058
+ Nothing runs on the edge — the boot hook exits early there — so the driver only has to resolve.`;
2059
+ }
2004
2060
  const REMIX_BOOT_STEP = `Start Clivly when your app boots. Add this near the top of app/entry.server.tsx:
2005
2061
  import clivly from "./clivly.config";
2006
2062
  clivly.start().catch((error) => console.warn("[clivly] could not check in:", error));`;
@@ -2551,21 +2607,32 @@ function planEnv(args) {
2551
2607
  action: envAction(addedKeys.length, existing.length)
2552
2608
  };
2553
2609
  }
2554
- function declaredNextVersion(args) {
2610
+ function hostManifest(args) {
2555
2611
  const raw = args.readFile(join(args.cwd, "package.json"));
2556
- if (!raw) return;
2557
- let pkg = {};
2612
+ if (!raw) return {};
2558
2613
  try {
2559
- pkg = JSON.parse(raw);
2614
+ return JSON.parse(raw);
2560
2615
  } catch {
2561
- pkg = {};
2616
+ return {};
2562
2617
  }
2618
+ }
2619
+ function declaredNextVersion(args) {
2620
+ const pkg = hostManifest(args);
2563
2621
  return pkg.dependencies?.next ?? pkg.devDependencies?.next;
2564
2622
  }
2623
+ function declaredDependencies(args) {
2624
+ return hostManifest(args).dependencies;
2625
+ }
2626
+ /** The edge-externals step, or null when no Node-only driver is declared. */
2627
+ function edgeExternalsStepFor(dependencies) {
2628
+ const drivers = nodeOnlyDrivers(dependencies);
2629
+ return drivers.length > 0 ? nextEdgeExternalsStep(drivers) : null;
2630
+ }
2565
2631
  function planBootPatch(args) {
2566
2632
  if (bootHookPath(args.framework, args.layout) !== null) return {
2567
2633
  file: null,
2568
- manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null
2634
+ manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null,
2635
+ extra: args.framework === "nextjs" ? edgeExternalsStepFor(declaredDependencies(args)) : null
2569
2636
  };
2570
2637
  if (args.framework === "remix") return {
2571
2638
  file: null,
@@ -2611,7 +2678,7 @@ function planMutations(args) {
2611
2678
  return {
2612
2679
  files,
2613
2680
  env: planEnv(args),
2614
- manualSteps: boot.manual ? [boot.manual] : []
2681
+ manualSteps: [boot.manual, boot.extra ?? null].filter((step) => step !== null)
2615
2682
  };
2616
2683
  }
2617
2684
  function planWidgetMutations(args) {
@@ -2756,7 +2823,14 @@ function reportSchema(report, out) {
2756
2823
  const updated = report.mappings.filter((m) => m.updated);
2757
2824
  if (created.length > 0) out(` ✅ Mapped ${countOf(created.length, "entity", "entities")}: ${created.map((m) => m.entityKey).join(", ")}.\n`);
2758
2825
  if (updated.length > 0) out(` ✅ Updated ${countOf(updated.length, "mapping")}: ${updated.map((m) => m.entityKey).join(", ")}.\n`);
2759
- if (report.mappings.length > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(report.mappings.length, "mapping")} already match your config.\n`);
2826
+ const skipped = report.mappings.filter((m) => m.skipped === "dashboard-owned");
2827
+ if (skipped.length > 0) {
2828
+ out(` ↩︎ Kept the dashboard's version of ${countOf(skipped.length, "entity", "entities")}: ${skipped.map((m) => m.entityKey).join(", ")}.\n`);
2829
+ out(" The dashboard owns these mappings, so clivly.config did not change them.\n");
2830
+ out(" To let your config own one, delete it in Integrations → Mapping, then push again.\n");
2831
+ }
2832
+ const settled = report.mappings.length - skipped.length;
2833
+ if (settled > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(settled, "mapping")} already match your config.\n`);
2760
2834
  }
2761
2835
  async function runPushSchema(options) {
2762
2836
  const out = options.out ?? ((line) => process.stdout.write(line));
@@ -4011,6 +4085,10 @@ function previewMutations(deps, mutations, options) {
4011
4085
  out(` ${mutations.env.action.padEnd(9)} .env\n`);
4012
4086
  for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
4013
4087
  }
4088
+ if (mutations.manualSteps.length > 0) {
4089
+ out("\nYou will also need to:\n");
4090
+ for (const step of mutations.manualSteps) out(`\n ! ${step}\n`);
4091
+ }
4014
4092
  out(options.diff ? "\nRe-run without --diff to apply; --force to overwrite the files marked conflict.\n" : "\nRe-run without --dry-run to apply.\n");
4015
4093
  }
4016
4094
  function printNextSteps(out, authComplete, loginRan, envFile, needsViteDotenv = false) {
@@ -4332,7 +4410,8 @@ function defaultLoginDeps() {
4332
4410
  sleep: (ms) => new Promise((resolve) => {
4333
4411
  setTimeout(resolve, ms);
4334
4412
  }),
4335
- writeEnv: defaultWriteEnv
4413
+ writeEnv: defaultWriteEnv,
4414
+ envFileName: (cwd) => basename(resolveEnvPath(cwd))
4336
4415
  };
4337
4416
  }
4338
4417
  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 };
@@ -332,6 +332,22 @@ interface SyncSource {
332
332
  * absent from another, and read by the runtime.
333
333
  */
334
334
  objectType?: string;
335
+ /**
336
+ * The `source` label a push carries, when it differs from `entity`.
337
+ *
338
+ * Only a projection sets this, and only because a projection entity has no
339
+ * choice but to carry two names: its entity key doubles as the custom-object
340
+ * slug and must be kebab-case, while `projection.entityKey` is validated by
341
+ * `/^[a-z][a-z0-9_]*$/` and rejects hyphens. The server resolves that by
342
+ * storing the mapping's `sourceTable` as `projection.baseTable` — so a push
343
+ * labelled with the entity key matched no mapping and every row was dropped,
344
+ * while `push-schema` reported the mapping created and exited 0.
345
+ *
346
+ * Deliberately NOT folded into `entity`: that value is also the cursor-store
347
+ * key, so redefining it would silently reset the sync cursor of every
348
+ * existing projection and re-push its whole history.
349
+ */
350
+ pushSource?: string;
335
351
  /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
336
352
  [DRIZZLE_META]?: DrizzleSourceMeta;
337
353
  }
@@ -332,6 +332,22 @@ interface SyncSource {
332
332
  * absent from another, and read by the runtime.
333
333
  */
334
334
  objectType?: string;
335
+ /**
336
+ * The `source` label a push carries, when it differs from `entity`.
337
+ *
338
+ * Only a projection sets this, and only because a projection entity has no
339
+ * choice but to carry two names: its entity key doubles as the custom-object
340
+ * slug and must be kebab-case, while `projection.entityKey` is validated by
341
+ * `/^[a-z][a-z0-9_]*$/` and rejects hyphens. The server resolves that by
342
+ * storing the mapping's `sourceTable` as `projection.baseTable` — so a push
343
+ * labelled with the entity key matched no mapping and every row was dropped,
344
+ * while `push-schema` reported the mapping created and exited 0.
345
+ *
346
+ * Deliberately NOT folded into `entity`: that value is also the cursor-store
347
+ * key, so redefining it would silently reset the sync cursor of every
348
+ * existing projection and re-push its whole history.
349
+ */
350
+ pushSource?: string;
335
351
  /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
336
352
  [DRIZZLE_META]?: DrizzleSourceMeta;
337
353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clivly",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly CLI — scaffold a CRM into your app (clivly init).",
@@ -77,6 +77,43 @@ function resolveEntitySource(config, entityKey) {
77
77
  function expectedSourceKey(concept) {
78
78
  return CONCEPT_KEYS[concept] ?? "custom";
79
79
  }
80
+ /**
81
+ * The `source` label a push must carry for the server to find its mapping.
82
+ *
83
+ * `entity` is the SDK's own name for a source — the cursor-store key, and what
84
+ * the developer wrote in `config.source`. It is NOT always what the server
85
+ * filed the mapping under: for a projection, `ensureSchema` stores
86
+ * `sourceTable` as `projection.baseTable`, because a projection entity's key
87
+ * doubles as a kebab-case object slug and cannot also be the snake_case
88
+ * `projection.entityKey`.
89
+ *
90
+ * Pushing under `entity` therefore dropped 100% of a projection's rows while
91
+ * every local signal said the setup was correct. One function so the three
92
+ * push sites cannot drift apart again.
93
+ */
94
+ function pushSourceOf(source) {
95
+ return source.pushSource ?? source.entity;
96
+ }
97
+ /**
98
+ * The push label for a custom source, resolved against the config.
99
+ *
100
+ * `fromDrizzleProjection` stamps `pushSource` itself, but a projection source
101
+ * can equally be hand-built — `fromDrizzle` with an overridden `fetchPage` is
102
+ * what the README's own example leads you to, and next-finance did exactly
103
+ * that. Such a source has no `pushSource`, so the label has to come from the
104
+ * one place that is true either way: the entity's declared
105
+ * `projection.baseTable`.
106
+ *
107
+ * Matched on the source's `entity`, which for a custom source is also the
108
+ * config entity key (that is what `resolveEntitySource` above pairs them by).
109
+ * Falls back to `pushSourceOf`, so a non-projection custom source and an
110
+ * already-stamped one both keep their current label.
111
+ */
112
+ function customPushSource(config, source) {
113
+ if (source.pushSource) return source.pushSource;
114
+ const entities = config.entities?.entities;
115
+ return (entities?.[source.entity] ?? Object.values(entities ?? {}).find((e) => e.source === source.entity))?.projection?.baseTable ?? source.entity;
116
+ }
80
117
  //#endregion
81
118
  //#region ../sdk/src/concept-coverage.ts
82
119
  const CHECK_NAME$1 = "Entity → source coverage";
@@ -1203,7 +1240,7 @@ async function pushRemoteProjections(args) {
1203
1240
  }
1204
1241
  try {
1205
1242
  const ok = await args.push(SYNC_CUSTOM_PATH$1, {
1206
- source: source.entity,
1243
+ source: pushSourceOf(source),
1207
1244
  rows: all,
1208
1245
  mode: "full",
1209
1246
  fetched: all.length,
@@ -1278,7 +1315,7 @@ async function syncSource(args) {
1278
1315
  if (rows.length === 0) break;
1279
1316
  const capped = maxRows === void 0 ? rows : rows.slice(0, maxRows);
1280
1317
  if (!await push(path, {
1281
- source: source.entity,
1318
+ source: pushSourceOf(source),
1282
1319
  rows: capped,
1283
1320
  mode: "delta",
1284
1321
  fetched: rows.length,
@@ -1322,7 +1359,7 @@ async function fullSyncSource(args) {
1322
1359
  ok: true
1323
1360
  };
1324
1361
  const ok = await push(path, {
1325
- source: source.entity,
1362
+ source: pushSourceOf(source),
1326
1363
  rows: all,
1327
1364
  mode: "full",
1328
1365
  fetched: all.length,
@@ -1922,7 +1959,10 @@ function createClivlySDK(config) {
1922
1959
  };
1923
1960
  const syncCustomSources = async (customSources, mode, runId) => {
1924
1961
  const failed = [];
1925
- for (const customSource of customSources ?? []) if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
1962
+ for (const customSource of customSources ?? []) {
1963
+ customSource.pushSource = customPushSource(config, customSource);
1964
+ if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
1965
+ }
1926
1966
  return failed;
1927
1967
  };
1928
1968
  const syncRemoteSources = async (sources, runId) => {
@@ -77,6 +77,43 @@ function resolveEntitySource(config, entityKey) {
77
77
  function expectedSourceKey(concept) {
78
78
  return CONCEPT_KEYS[concept] ?? "custom";
79
79
  }
80
+ /**
81
+ * The `source` label a push must carry for the server to find its mapping.
82
+ *
83
+ * `entity` is the SDK's own name for a source — the cursor-store key, and what
84
+ * the developer wrote in `config.source`. It is NOT always what the server
85
+ * filed the mapping under: for a projection, `ensureSchema` stores
86
+ * `sourceTable` as `projection.baseTable`, because a projection entity's key
87
+ * doubles as a kebab-case object slug and cannot also be the snake_case
88
+ * `projection.entityKey`.
89
+ *
90
+ * Pushing under `entity` therefore dropped 100% of a projection's rows while
91
+ * every local signal said the setup was correct. One function so the three
92
+ * push sites cannot drift apart again.
93
+ */
94
+ function pushSourceOf(source) {
95
+ return source.pushSource ?? source.entity;
96
+ }
97
+ /**
98
+ * The push label for a custom source, resolved against the config.
99
+ *
100
+ * `fromDrizzleProjection` stamps `pushSource` itself, but a projection source
101
+ * can equally be hand-built — `fromDrizzle` with an overridden `fetchPage` is
102
+ * what the README's own example leads you to, and next-finance did exactly
103
+ * that. Such a source has no `pushSource`, so the label has to come from the
104
+ * one place that is true either way: the entity's declared
105
+ * `projection.baseTable`.
106
+ *
107
+ * Matched on the source's `entity`, which for a custom source is also the
108
+ * config entity key (that is what `resolveEntitySource` above pairs them by).
109
+ * Falls back to `pushSourceOf`, so a non-projection custom source and an
110
+ * already-stamped one both keep their current label.
111
+ */
112
+ function customPushSource(config, source) {
113
+ if (source.pushSource) return source.pushSource;
114
+ const entities = config.entities?.entities;
115
+ return (entities?.[source.entity] ?? Object.values(entities ?? {}).find((e) => e.source === source.entity))?.projection?.baseTable ?? source.entity;
116
+ }
80
117
  //#endregion
81
118
  //#region ../sdk/src/concept-coverage.ts
82
119
  const CHECK_NAME$1 = "Entity → source coverage";
@@ -1203,7 +1240,7 @@ async function pushRemoteProjections(args) {
1203
1240
  }
1204
1241
  try {
1205
1242
  const ok = await args.push(SYNC_CUSTOM_PATH$1, {
1206
- source: source.entity,
1243
+ source: pushSourceOf(source),
1207
1244
  rows: all,
1208
1245
  mode: "full",
1209
1246
  fetched: all.length,
@@ -1278,7 +1315,7 @@ async function syncSource(args) {
1278
1315
  if (rows.length === 0) break;
1279
1316
  const capped = maxRows === void 0 ? rows : rows.slice(0, maxRows);
1280
1317
  if (!await push(path, {
1281
- source: source.entity,
1318
+ source: pushSourceOf(source),
1282
1319
  rows: capped,
1283
1320
  mode: "delta",
1284
1321
  fetched: rows.length,
@@ -1322,7 +1359,7 @@ async function fullSyncSource(args) {
1322
1359
  ok: true
1323
1360
  };
1324
1361
  const ok = await push(path, {
1325
- source: source.entity,
1362
+ source: pushSourceOf(source),
1326
1363
  rows: all,
1327
1364
  mode: "full",
1328
1365
  fetched: all.length,
@@ -1922,7 +1959,10 @@ function createClivlySDK(config) {
1922
1959
  };
1923
1960
  const syncCustomSources = async (customSources, mode, runId) => {
1924
1961
  const failed = [];
1925
- for (const customSource of customSources ?? []) if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
1962
+ for (const customSource of customSources ?? []) {
1963
+ customSource.pushSource = customPushSource(config, customSource);
1964
+ if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
1965
+ }
1926
1966
  return failed;
1927
1967
  };
1928
1968
  const syncRemoteSources = async (sources, runId) => {
package/sdk.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_entity_config = require("./entity-config-C2MfIDL2.cjs");
3
- const require_sdk = require("./sdk-DbvqXNSh.cjs");
3
+ const require_sdk = require("./sdk-BAHcnhm7.cjs");
4
4
  exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
5
5
  exports.ClivlyError = require_sdk.ClivlyError;
6
6
  exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
package/sdk.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as customObjectDeclarations, M as validateEntitiesConfig, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult, w as defineClivlyConfig } from "./entity-config-w8oMBWf8.cjs";
2
- import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-lj_0bJrc.cjs";
2
+ import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-C3xMOqWE.cjs";
3
3
  //#region ../sdk/src/trigger-url.d.ts
4
4
  declare function isPlaceholderUrl(value: string): boolean;
5
5
  //#endregion
package/sdk.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as customObjectDeclarations, M as validateEntitiesConfig, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult, w as defineClivlyConfig } from "./entity-config-w8oMBWf8.mjs";
2
- import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-ouHKZas2.mjs";
2
+ import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-BmxtnOfc.mjs";
3
3
  //#region ../sdk/src/trigger-url.d.ts
4
4
  declare function isPlaceholderUrl(value: string): boolean;
5
5
  //#endregion
package/sdk.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { c as defineClivlyConfig, g as validateEntitiesConfig, s as customObjectDeclarations } from "./entity-config-bC3DX9f1.mjs";
2
- import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-BTzaM8bF.mjs";
2
+ import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-C0DU5KDS.mjs";
3
3
  export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
package/vite.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as ClivlySDK } from "./namespace-probe-lj_0bJrc.cjs";
1
+ import { o as ClivlySDK } from "./namespace-probe-C3xMOqWE.cjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region ../sdk/src/vite.d.ts
4
4
  interface ClivlyViteOptions {
package/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as ClivlySDK } from "./namespace-probe-ouHKZas2.mjs";
1
+ import { o as ClivlySDK } from "./namespace-probe-BmxtnOfc.mjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region ../sdk/src/vite.d.ts
4
4
  interface ClivlyViteOptions {