clivly 0.3.0-next.3 → 0.3.0-next.5

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.
@@ -22,7 +22,20 @@ const TABLE_PATTERNS = [
22
22
  "attendees",
23
23
  "people",
24
24
  "members",
25
- "contacts"
25
+ "contacts",
26
+ "students",
27
+ "pupils",
28
+ "learners",
29
+ "patients",
30
+ "subscribers",
31
+ "donors",
32
+ "guests",
33
+ "residents",
34
+ "tenants",
35
+ "applicants",
36
+ "candidates",
37
+ "leads",
38
+ "prospects"
26
39
  ]
27
40
  },
28
41
  {
@@ -21,7 +21,20 @@ const TABLE_PATTERNS = [
21
21
  "attendees",
22
22
  "people",
23
23
  "members",
24
- "contacts"
24
+ "contacts",
25
+ "students",
26
+ "pupils",
27
+ "learners",
28
+ "patients",
29
+ "subscribers",
30
+ "donors",
31
+ "guests",
32
+ "residents",
33
+ "tenants",
34
+ "applicants",
35
+ "candidates",
36
+ "leads",
37
+ "prospects"
25
38
  ]
26
39
  },
27
40
  {
package/index.cjs CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_entity_config = require("./entity-config-DMuZpchz.cjs");
4
4
  require("./mapping-score-i3p8LWDF.cjs");
5
5
  const require_core_entity_heuristics = require("./core-entity-heuristics.cjs");
6
- const require_sdk = require("./sdk-r4khXzoH.cjs");
6
+ const require_sdk = require("./sdk-Cc51uMdl.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");
@@ -533,6 +533,71 @@ function isEntrypoint(argv1, moduleUrl, realpath = node_fs.realpathSync) {
533
533
  return resolve(argv1) === resolve(modulePath);
534
534
  }
535
535
  //#endregion
536
+ //#region src/load-env.ts
537
+ const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
538
+ const NEWLINE = /\r?\n/;
539
+ const ESCAPED_NEWLINE = /\\n/g;
540
+ const ESCAPED_CARRIAGE = /\\r/g;
541
+ const TRAILING_COMMENT = /\s#/;
542
+ /**
543
+ * Parse `.env` file text into a flat key→value map. Understands `#` comments
544
+ * (whole-line and trailing on unquoted values), single/double quotes,
545
+ * `export KEY=`, and `\n` escapes inside double quotes. Later duplicate keys
546
+ * win, matching how a shell would source the file top-to-bottom.
547
+ */
548
+ function parseEnv(content) {
549
+ const out = {};
550
+ for (const rawLine of content.split(NEWLINE)) {
551
+ const line = rawLine.trimStart();
552
+ if (line === "" || line.startsWith("#")) continue;
553
+ const match = LINE.exec(line);
554
+ const key = match?.[1];
555
+ if (!key) continue;
556
+ out[key] = parseValue(match[2] ?? "");
557
+ }
558
+ return out;
559
+ }
560
+ function parseValue(raw) {
561
+ const value = raw.trim();
562
+ if (value === "") return "";
563
+ const quote = value[0];
564
+ if (quote === "\"" || quote === "'") {
565
+ const end = value.indexOf(quote, 1);
566
+ const inner = end === -1 ? value.slice(1) : value.slice(1, end);
567
+ return quote === "\"" ? inner.replace(ESCAPED_NEWLINE, "\n").replace(ESCAPED_CARRIAGE, "\r") : inner;
568
+ }
569
+ const commentAt = value.search(TRAILING_COMMENT);
570
+ return (commentAt === -1 ? value : value.slice(0, commentAt)).trim();
571
+ }
572
+ /**
573
+ * Load `.env` then `.env.local` from `cwd` into `env` (default `process.env`),
574
+ * WITHOUT overriding anything already set. Precedence, strongest first:
575
+ * real environment (shell) > `.env.local` > `.env`. This mirrors the framework
576
+ * convention developers already expect (Next.js, Vite), so pointing
577
+ * `CLIVLY_API_URL` at a staging host from the shell still wins over the file.
578
+ */
579
+ function loadEnv(cwd, env = process.env) {
580
+ const files = [];
581
+ const merged = {};
582
+ for (const name of [".env", ".env.local"]) {
583
+ const path = (0, node_path.join)(cwd, name);
584
+ if (!(0, node_fs.existsSync)(path)) continue;
585
+ files.push(name);
586
+ try {
587
+ Object.assign(merged, parseEnv((0, node_fs.readFileSync)(path, "utf8")));
588
+ } catch {}
589
+ }
590
+ const applied = [];
591
+ for (const [key, value] of Object.entries(merged)) if (env[key] === void 0) {
592
+ env[key] = value;
593
+ applied.push(key);
594
+ }
595
+ return {
596
+ applied,
597
+ files
598
+ };
599
+ }
600
+ //#endregion
536
601
  //#region src/login.ts
537
602
  const POLL_INTERVAL_MS = 2e3;
538
603
  const POLL_BACKOFF_MULTIPLIER = 1.5;
@@ -752,15 +817,25 @@ function placeholderContact(todos) {
752
817
  idField: null
753
818
  };
754
819
  }
820
+ function hasMappedFields(entity) {
821
+ return Object.keys(entity.fields).length > 0;
822
+ }
755
823
  function planInit(schema) {
756
824
  const todos = [];
757
825
  const { contact: contactTable, company: companyTable } = selectEntities(schema.tables);
758
- const contactDetected = contactTable !== null;
759
- const contact = contactTable ? buildEntity(contactTable, "contact", todos) : placeholderContact(todos);
826
+ let contact = contactTable ? buildEntity(contactTable, "contact", todos) : placeholderContact(todos);
827
+ let contactDetected = contactTable !== null;
828
+ if (!hasMappedFields(contact)) {
829
+ contact = placeholderContact(todos);
830
+ contactDetected = false;
831
+ }
760
832
  let company = null;
761
833
  if (companyTable) {
762
- company = buildEntity(companyTable, "company", todos);
763
- if (require_core_entity_heuristics.matchesConcept(companyTable.tableName, "contact")) todos.push(`"${companyTable.tableName}" matches both contact and company naming — assigned to company. Swap it to your contact entity if that's wrong.`);
834
+ const candidate = buildEntity(companyTable, "company", todos);
835
+ if (hasMappedFields(candidate)) {
836
+ company = candidate;
837
+ if (require_core_entity_heuristics.matchesConcept(companyTable.tableName, "contact")) todos.push(`"${companyTable.tableName}" matches both contact and company naming — assigned to company. Swap it to your contact entity if that's wrong.`);
838
+ } else todos.push(`"${companyTable.tableName}" looked like a company but no fields could be mapped — left it out. Add it to \`entities\` once you know which columns map to name/domain.`);
764
839
  }
765
840
  if (contactTable) {
766
841
  const scored = require_core_entity_heuristics.scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
@@ -1165,13 +1240,33 @@ function resolveConfigPath(cwd, configPath) {
1165
1240
  function firstLine(message) {
1166
1241
  return message.split("\n")[0]?.trim() ?? "";
1167
1242
  }
1243
+ /**
1244
+ * Describe why the config failed to load, in one line the developer can act on.
1245
+ *
1246
+ * A config that fails its own schema validation throws a zod error carrying
1247
+ * structured `issues`, but whose `message` is a multi-line JSON dump — its
1248
+ * first line is a bare "[", so `firstLine` alone reduced the entire diagnosis
1249
+ * to a single bracket. Render the issues as `path: message` instead, which is
1250
+ * what actually names the entity to fix. Anything else keeps the require-stack
1251
+ * trimming `firstLine` exists for.
1252
+ */
1253
+ function describeLoadError(error) {
1254
+ const issues = error.issues;
1255
+ if (Array.isArray(issues) && issues.length > 0) return issues.map((raw) => {
1256
+ const issue = raw;
1257
+ const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
1258
+ const message = typeof issue.message === "string" ? issue.message : "invalid value";
1259
+ return path ? `${path}: ${message}` : message;
1260
+ }).join("; ");
1261
+ return firstLine(error?.message ?? String(error));
1262
+ }
1168
1263
  async function loadConfigSdk(path, requiredMethod) {
1169
1264
  const jiti$1 = (0, jiti.createJiti)(require("url").pathToFileURL(__filename).href);
1170
1265
  let mod;
1171
1266
  try {
1172
1267
  mod = await jiti$1.import(path);
1173
1268
  } catch (error) {
1174
- throw new Error(firstLine(error.message));
1269
+ throw new Error(describeLoadError(error));
1175
1270
  }
1176
1271
  const sdk = mod.default ?? mod;
1177
1272
  if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
@@ -1654,7 +1749,7 @@ function previewMutations(deps, mutations) {
1654
1749
  }
1655
1750
  function printNextSteps(out, authComplete, loginRan) {
1656
1751
  out("\nNext steps:\n");
1657
- out(loginRan ? " 1. Credentials written by `clivly login`\n" : " 1. Set the CLIVLY_* values in .env\n");
1752
+ out(loginRan ? " 1. Credentials written by `clivly login`\n" : " 1. Add CLIVLY_API_KEY to .env — create one in the Clivly dashboard\n under Integrations → Create API Key (or Settings → API keys)\n");
1658
1753
  out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
1659
1754
  out(" Local dev? Run `clivly dev` to open a tunnel, or skip this step until you deploy (remote sync is deploy-only locally).\n");
1660
1755
  const step = authComplete ? 3 : 4;
@@ -1678,7 +1773,7 @@ async function scaffold(options, deps, plan) {
1678
1773
  if (options.dryRun) {
1679
1774
  out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
1680
1775
  previewMutations(deps, mutations);
1681
- return plan.contactDetected ? 0 : 1;
1776
+ return 0;
1682
1777
  }
1683
1778
  const result = await deps.install({
1684
1779
  cwd: options.cwd,
@@ -1872,6 +1967,7 @@ function runInitFromArgv(argv, cwd) {
1872
1967
  }
1873
1968
  async function main(argv) {
1874
1969
  const [command, ...rest] = argv;
1970
+ loadEnv(process.cwd());
1875
1971
  switch (command) {
1876
1972
  case "init": return await runInitFromArgv(rest, process.cwd());
1877
1973
  case "push-schema": return await runPushSchema({
package/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { t as CANONICAL_FIELDS } from "./entity-config-D3YpQB0z.mjs";
3
3
  import "./mapping-score-CPPvwoZC.mjs";
4
4
  import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./core-entity-heuristics.mjs";
5
- import { t as createClivlySDK } from "./sdk-DtFmeuwl.mjs";
5
+ import { t as createClivlySDK } from "./sdk-BLgY8K0U.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { spawn } from "node:child_process";
8
8
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
@@ -532,6 +532,71 @@ function isEntrypoint(argv1, moduleUrl, realpath = realpathSync) {
532
532
  return resolve(argv1) === resolve(modulePath);
533
533
  }
534
534
  //#endregion
535
+ //#region src/load-env.ts
536
+ const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
537
+ const NEWLINE = /\r?\n/;
538
+ const ESCAPED_NEWLINE = /\\n/g;
539
+ const ESCAPED_CARRIAGE = /\\r/g;
540
+ const TRAILING_COMMENT = /\s#/;
541
+ /**
542
+ * Parse `.env` file text into a flat key→value map. Understands `#` comments
543
+ * (whole-line and trailing on unquoted values), single/double quotes,
544
+ * `export KEY=`, and `\n` escapes inside double quotes. Later duplicate keys
545
+ * win, matching how a shell would source the file top-to-bottom.
546
+ */
547
+ function parseEnv(content) {
548
+ const out = {};
549
+ for (const rawLine of content.split(NEWLINE)) {
550
+ const line = rawLine.trimStart();
551
+ if (line === "" || line.startsWith("#")) continue;
552
+ const match = LINE.exec(line);
553
+ const key = match?.[1];
554
+ if (!key) continue;
555
+ out[key] = parseValue(match[2] ?? "");
556
+ }
557
+ return out;
558
+ }
559
+ function parseValue(raw) {
560
+ const value = raw.trim();
561
+ if (value === "") return "";
562
+ const quote = value[0];
563
+ if (quote === "\"" || quote === "'") {
564
+ const end = value.indexOf(quote, 1);
565
+ const inner = end === -1 ? value.slice(1) : value.slice(1, end);
566
+ return quote === "\"" ? inner.replace(ESCAPED_NEWLINE, "\n").replace(ESCAPED_CARRIAGE, "\r") : inner;
567
+ }
568
+ const commentAt = value.search(TRAILING_COMMENT);
569
+ return (commentAt === -1 ? value : value.slice(0, commentAt)).trim();
570
+ }
571
+ /**
572
+ * Load `.env` then `.env.local` from `cwd` into `env` (default `process.env`),
573
+ * WITHOUT overriding anything already set. Precedence, strongest first:
574
+ * real environment (shell) > `.env.local` > `.env`. This mirrors the framework
575
+ * convention developers already expect (Next.js, Vite), so pointing
576
+ * `CLIVLY_API_URL` at a staging host from the shell still wins over the file.
577
+ */
578
+ function loadEnv(cwd, env = process.env) {
579
+ const files = [];
580
+ const merged = {};
581
+ for (const name of [".env", ".env.local"]) {
582
+ const path = join(cwd, name);
583
+ if (!existsSync(path)) continue;
584
+ files.push(name);
585
+ try {
586
+ Object.assign(merged, parseEnv(readFileSync(path, "utf8")));
587
+ } catch {}
588
+ }
589
+ const applied = [];
590
+ for (const [key, value] of Object.entries(merged)) if (env[key] === void 0) {
591
+ env[key] = value;
592
+ applied.push(key);
593
+ }
594
+ return {
595
+ applied,
596
+ files
597
+ };
598
+ }
599
+ //#endregion
535
600
  //#region src/login.ts
536
601
  const POLL_INTERVAL_MS = 2e3;
537
602
  const POLL_BACKOFF_MULTIPLIER = 1.5;
@@ -751,15 +816,25 @@ function placeholderContact(todos) {
751
816
  idField: null
752
817
  };
753
818
  }
819
+ function hasMappedFields(entity) {
820
+ return Object.keys(entity.fields).length > 0;
821
+ }
754
822
  function planInit(schema) {
755
823
  const todos = [];
756
824
  const { contact: contactTable, company: companyTable } = selectEntities(schema.tables);
757
- const contactDetected = contactTable !== null;
758
- const contact = contactTable ? buildEntity(contactTable, "contact", todos) : placeholderContact(todos);
825
+ let contact = contactTable ? buildEntity(contactTable, "contact", todos) : placeholderContact(todos);
826
+ let contactDetected = contactTable !== null;
827
+ if (!hasMappedFields(contact)) {
828
+ contact = placeholderContact(todos);
829
+ contactDetected = false;
830
+ }
759
831
  let company = null;
760
832
  if (companyTable) {
761
- company = buildEntity(companyTable, "company", todos);
762
- if (matchesConcept(companyTable.tableName, "contact")) todos.push(`"${companyTable.tableName}" matches both contact and company naming — assigned to company. Swap it to your contact entity if that's wrong.`);
833
+ const candidate = buildEntity(companyTable, "company", todos);
834
+ if (hasMappedFields(candidate)) {
835
+ company = candidate;
836
+ if (matchesConcept(companyTable.tableName, "contact")) todos.push(`"${companyTable.tableName}" matches both contact and company naming — assigned to company. Swap it to your contact entity if that's wrong.`);
837
+ } else todos.push(`"${companyTable.tableName}" looked like a company but no fields could be mapped — left it out. Add it to \`entities\` once you know which columns map to name/domain.`);
763
838
  }
764
839
  if (contactTable) {
765
840
  const scored = scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
@@ -1164,13 +1239,33 @@ function resolveConfigPath(cwd, configPath) {
1164
1239
  function firstLine(message) {
1165
1240
  return message.split("\n")[0]?.trim() ?? "";
1166
1241
  }
1242
+ /**
1243
+ * Describe why the config failed to load, in one line the developer can act on.
1244
+ *
1245
+ * A config that fails its own schema validation throws a zod error carrying
1246
+ * structured `issues`, but whose `message` is a multi-line JSON dump — its
1247
+ * first line is a bare "[", so `firstLine` alone reduced the entire diagnosis
1248
+ * to a single bracket. Render the issues as `path: message` instead, which is
1249
+ * what actually names the entity to fix. Anything else keeps the require-stack
1250
+ * trimming `firstLine` exists for.
1251
+ */
1252
+ function describeLoadError(error) {
1253
+ const issues = error.issues;
1254
+ if (Array.isArray(issues) && issues.length > 0) return issues.map((raw) => {
1255
+ const issue = raw;
1256
+ const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
1257
+ const message = typeof issue.message === "string" ? issue.message : "invalid value";
1258
+ return path ? `${path}: ${message}` : message;
1259
+ }).join("; ");
1260
+ return firstLine(error?.message ?? String(error));
1261
+ }
1167
1262
  async function loadConfigSdk(path, requiredMethod) {
1168
1263
  const jiti = createJiti(import.meta.url);
1169
1264
  let mod;
1170
1265
  try {
1171
1266
  mod = await jiti.import(path);
1172
1267
  } catch (error) {
1173
- throw new Error(firstLine(error.message));
1268
+ throw new Error(describeLoadError(error));
1174
1269
  }
1175
1270
  const sdk = mod.default ?? mod;
1176
1271
  if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
@@ -1653,7 +1748,7 @@ function previewMutations(deps, mutations) {
1653
1748
  }
1654
1749
  function printNextSteps(out, authComplete, loginRan) {
1655
1750
  out("\nNext steps:\n");
1656
- out(loginRan ? " 1. Credentials written by `clivly login`\n" : " 1. Set the CLIVLY_* values in .env\n");
1751
+ out(loginRan ? " 1. Credentials written by `clivly login`\n" : " 1. Add CLIVLY_API_KEY to .env — create one in the Clivly dashboard\n under Integrations → Create API Key (or Settings → API keys)\n");
1657
1752
  out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
1658
1753
  out(" Local dev? Run `clivly dev` to open a tunnel, or skip this step until you deploy (remote sync is deploy-only locally).\n");
1659
1754
  const step = authComplete ? 3 : 4;
@@ -1677,7 +1772,7 @@ async function scaffold(options, deps, plan) {
1677
1772
  if (options.dryRun) {
1678
1773
  out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
1679
1774
  previewMutations(deps, mutations);
1680
- return plan.contactDetected ? 0 : 1;
1775
+ return 0;
1681
1776
  }
1682
1777
  const result = await deps.install({
1683
1778
  cwd: options.cwd,
@@ -1871,6 +1966,7 @@ function runInitFromArgv(argv, cwd) {
1871
1966
  }
1872
1967
  async function main(argv) {
1873
1968
  const [command, ...rest] = argv;
1969
+ loadEnv(process.cwd());
1874
1970
  switch (command) {
1875
1971
  case "init": return await runInitFromArgv(rest, process.cwd());
1876
1972
  case "push-schema": return await runPushSchema({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clivly",
3
- "version": "0.3.0-next.3",
3
+ "version": "0.3.0-next.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly CLI — scaffold a CRM into your app (clivly init).",
@@ -11,7 +11,7 @@ function checkCustomObjectSlugs(config) {
11
11
  name: "Custom object slugs",
12
12
  ok: false,
13
13
  skipped: true,
14
- detail: "No custom objects configured."
14
+ detail: "No custom objects in clivly.config. Objects added in the dashboard need a matching source.custom entry here to sync."
15
15
  };
16
16
  const problems = [];
17
17
  const seen = /* @__PURE__ */ new Map();
@@ -36,6 +36,49 @@ function checkCustomObjectSlugs(config) {
36
36
  };
37
37
  }
38
38
  //#endregion
39
+ //#region ../sdk/src/fetch-error.ts
40
+ /** Deepest `Error` reachable through the `.cause` chain (the original error if none). */
41
+ function rootError(error) {
42
+ let current = error;
43
+ const seen = /* @__PURE__ */ new Set();
44
+ while (current instanceof Error && current.cause !== void 0 && !seen.has(current)) {
45
+ seen.add(current);
46
+ current = current.cause;
47
+ }
48
+ return current;
49
+ }
50
+ function codeOf(error) {
51
+ const code = error?.code;
52
+ return typeof code === "string" ? code : void 0;
53
+ }
54
+ const CODE_HINTS = {
55
+ ECONNREFUSED: "the database refused the connection — is Postgres running and is your database URL (e.g. DATABASE_URL) pointing at it?",
56
+ ENOTFOUND: "the database host couldn't be resolved — check the host in your connection string.",
57
+ ETIMEDOUT: "the database connection timed out — check the host, port, and firewall.",
58
+ "28P01": "password authentication failed — check the credentials in your connection string.",
59
+ "28000": "the database rejected the connection role — check the user in your connection string.",
60
+ "3D000": "that database does not exist — check the database name in your connection string.",
61
+ "08006": "the database connection was lost — check that Postgres is reachable."
62
+ };
63
+ const NEWLINE = /\r?\n/;
64
+ /** One-line, single-spaced version of an error message (drops the SQL/params blob). */
65
+ function firstLine(message) {
66
+ const line = message.split(NEWLINE)[0]?.trim() ?? "";
67
+ return line === "" ? message.trim() : line;
68
+ }
69
+ /**
70
+ * Build the `detail` string for a failed sample fetch of `entity`.
71
+ * Recognised connection errors get a plain-English hint; everything else
72
+ * surfaces the root cause's first line rather than the wrapper's SQL dump.
73
+ */
74
+ function describeFetchError(entity, error) {
75
+ const root = rootError(error);
76
+ const code = codeOf(root) ?? codeOf(error);
77
+ const hint = code ? CODE_HINTS[code] : void 0;
78
+ if (hint) return `Couldn't read "${entity}": ${hint}`;
79
+ return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
80
+ }
81
+ //#endregion
39
82
  //#region ../sdk/src/namespace-probe.ts
40
83
  /**
41
84
  * Verify the tables the integration depends on are genuinely reachable:
@@ -187,7 +230,7 @@ async function runDoctor(args) {
187
230
  checks.push({
188
231
  name: `Sample fetch (${source.entity})`,
189
232
  ok: false,
190
- detail: `"${source.entity}" fetchPage threw: ${error.message}`
233
+ detail: describeFetchError(source.entity, error)
191
234
  });
192
235
  }
193
236
  const result = await connect();
@@ -11,7 +11,7 @@ function checkCustomObjectSlugs(config) {
11
11
  name: "Custom object slugs",
12
12
  ok: false,
13
13
  skipped: true,
14
- detail: "No custom objects configured."
14
+ detail: "No custom objects in clivly.config. Objects added in the dashboard need a matching source.custom entry here to sync."
15
15
  };
16
16
  const problems = [];
17
17
  const seen = /* @__PURE__ */ new Map();
@@ -36,6 +36,49 @@ function checkCustomObjectSlugs(config) {
36
36
  };
37
37
  }
38
38
  //#endregion
39
+ //#region ../sdk/src/fetch-error.ts
40
+ /** Deepest `Error` reachable through the `.cause` chain (the original error if none). */
41
+ function rootError(error) {
42
+ let current = error;
43
+ const seen = /* @__PURE__ */ new Set();
44
+ while (current instanceof Error && current.cause !== void 0 && !seen.has(current)) {
45
+ seen.add(current);
46
+ current = current.cause;
47
+ }
48
+ return current;
49
+ }
50
+ function codeOf(error) {
51
+ const code = error?.code;
52
+ return typeof code === "string" ? code : void 0;
53
+ }
54
+ const CODE_HINTS = {
55
+ ECONNREFUSED: "the database refused the connection — is Postgres running and is your database URL (e.g. DATABASE_URL) pointing at it?",
56
+ ENOTFOUND: "the database host couldn't be resolved — check the host in your connection string.",
57
+ ETIMEDOUT: "the database connection timed out — check the host, port, and firewall.",
58
+ "28P01": "password authentication failed — check the credentials in your connection string.",
59
+ "28000": "the database rejected the connection role — check the user in your connection string.",
60
+ "3D000": "that database does not exist — check the database name in your connection string.",
61
+ "08006": "the database connection was lost — check that Postgres is reachable."
62
+ };
63
+ const NEWLINE = /\r?\n/;
64
+ /** One-line, single-spaced version of an error message (drops the SQL/params blob). */
65
+ function firstLine(message) {
66
+ const line = message.split(NEWLINE)[0]?.trim() ?? "";
67
+ return line === "" ? message.trim() : line;
68
+ }
69
+ /**
70
+ * Build the `detail` string for a failed sample fetch of `entity`.
71
+ * Recognised connection errors get a plain-English hint; everything else
72
+ * surfaces the root cause's first line rather than the wrapper's SQL dump.
73
+ */
74
+ function describeFetchError(entity, error) {
75
+ const root = rootError(error);
76
+ const code = codeOf(root) ?? codeOf(error);
77
+ const hint = code ? CODE_HINTS[code] : void 0;
78
+ if (hint) return `Couldn't read "${entity}": ${hint}`;
79
+ return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
80
+ }
81
+ //#endregion
39
82
  //#region ../sdk/src/namespace-probe.ts
40
83
  /**
41
84
  * Verify the tables the integration depends on are genuinely reachable:
@@ -187,7 +230,7 @@ async function runDoctor(args) {
187
230
  checks.push({
188
231
  name: `Sample fetch (${source.entity})`,
189
232
  ok: false,
190
- detail: `"${source.entity}" fetchPage threw: ${error.message}`
233
+ detail: describeFetchError(source.entity, error)
191
234
  });
192
235
  }
193
236
  const result = await connect();
package/sdk.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sdk = require("./sdk-r4khXzoH.cjs");
2
+ const require_sdk = require("./sdk-Cc51uMdl.cjs");
3
3
  exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
4
4
  exports.ClivlyError = require_sdk.ClivlyError;
5
5
  exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
package/sdk.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, r as ClivlyError, t as createClivlySDK } from "./sdk-DtFmeuwl.mjs";
1
+ import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, r as ClivlyError, t as createClivlySDK } from "./sdk-BLgY8K0U.mjs";
2
2
  export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createClivlySDK };