clivly 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +330 -134
  2. package/core-drizzle.cjs +3 -1
  3. package/core-drizzle.d.cts +3 -2
  4. package/core-drizzle.d.mts +3 -2
  5. package/core-drizzle.mjs +2 -2
  6. package/core-entity-config.cjs +1 -1
  7. package/core-entity-config.d.cts +2 -2
  8. package/core-entity-config.d.mts +2 -2
  9. package/core-entity-config.mjs +1 -1
  10. package/core-entity-heuristics.d.cts +3 -2
  11. package/core-entity-heuristics.d.mts +3 -2
  12. package/core-mapping-form.d.cts +1 -1
  13. package/core-mapping-form.d.mts +1 -1
  14. package/core-mappings-config.cjs +1 -1
  15. package/core-mappings-config.d.cts +1 -1
  16. package/core-mappings-config.d.mts +1 -1
  17. package/core-mappings-config.mjs +1 -1
  18. package/core-sync-engine.cjs +1 -1
  19. package/core-sync-engine.d.cts +1 -1
  20. package/core-sync-engine.d.mts +1 -1
  21. package/core-sync-engine.mjs +1 -1
  22. package/core-view-compiler.cjs +1 -1
  23. package/core-view-compiler.d.cts +1 -1
  24. package/core-view-compiler.d.mts +1 -1
  25. package/core-view-compiler.mjs +1 -1
  26. package/core.cjs +21 -5
  27. package/core.d.cts +7 -6
  28. package/core.d.mts +7 -6
  29. package/core.mjs +7 -6
  30. package/drizzle-3kvhkwO7.mjs +110 -0
  31. package/drizzle-BhN5M9zt.d.cts +41 -0
  32. package/drizzle-bfjVzYR6.cjs +127 -0
  33. package/drizzle-kBULBAOa.d.mts +41 -0
  34. package/drizzle.cjs +12 -40
  35. package/drizzle.d.cts +3 -11
  36. package/drizzle.d.mts +3 -11
  37. package/drizzle.mjs +12 -41
  38. package/{entity-config-DoUCXtq5.d.cts → entity-config-C4Yf_fPj.d.cts} +120 -1
  39. package/{entity-config-DoUCXtq5.d.mts → entity-config-C4Yf_fPj.d.mts} +120 -1
  40. package/entity-config-CsLl-ftC.cjs +727 -0
  41. package/entity-config-D3LmzgLb.mjs +612 -0
  42. package/index.cjs +764 -120
  43. package/index.d.cts +12 -0
  44. package/index.d.mts +12 -0
  45. package/index.mjs +765 -121
  46. package/internal-B8ydhsKd.d.cts +73 -0
  47. package/internal-BARjyN4n.cjs +97 -0
  48. package/internal-DMySVbM0.mjs +68 -0
  49. package/internal-MBkNo3bw.d.mts +73 -0
  50. package/internal-drizzle.cjs +3 -1
  51. package/internal-drizzle.d.cts +2 -2
  52. package/internal-drizzle.d.mts +2 -2
  53. package/internal-drizzle.mjs +2 -2
  54. package/internal-entity-config.cjs +1 -1
  55. package/internal-entity-config.d.cts +2 -2
  56. package/internal-entity-config.d.mts +2 -2
  57. package/internal-entity-config.mjs +1 -1
  58. package/internal-entity-heuristics.cjs +28 -8
  59. package/internal-entity-heuristics.d.cts +2 -12
  60. package/internal-entity-heuristics.d.mts +2 -12
  61. package/internal-entity-heuristics.mjs +28 -8
  62. package/internal-mapping-form.d.cts +1 -1
  63. package/internal-mapping-form.d.mts +1 -1
  64. package/internal-mappings-config.cjs +1 -1
  65. package/internal-mappings-config.d.cts +1 -1
  66. package/internal-mappings-config.d.mts +1 -1
  67. package/internal-mappings-config.mjs +1 -1
  68. package/internal-sync-engine.cjs +1 -1
  69. package/internal-sync-engine.d.cts +1 -1
  70. package/internal-sync-engine.d.mts +1 -1
  71. package/internal-sync-engine.mjs +1 -1
  72. package/internal-view-compiler.cjs +1 -1
  73. package/internal-view-compiler.d.cts +1 -1
  74. package/internal-view-compiler.d.mts +1 -1
  75. package/internal-view-compiler.mjs +1 -1
  76. package/internal.cjs +21 -4
  77. package/internal.d.cts +7 -6
  78. package/internal.d.mts +7 -6
  79. package/internal.mjs +7 -5
  80. package/{mapping-form-D93lhHEc.d.mts → mapping-form-43bwOTat.d.cts} +1 -1
  81. package/{mapping-form-lfyEE7EJ.d.cts → mapping-form-DUvWUYCo.d.mts} +1 -1
  82. package/{mappings-config-BsRADNRv.cjs → mappings-config-BSSwdHiq.cjs} +32 -6
  83. package/mappings-config-BpcR8bBo.d.mts +167 -0
  84. package/{mappings-config-Dbgxx3Nv.mjs → mappings-config-bJQT_gSa.mjs} +32 -6
  85. package/mappings-config-xsLMn3I-.d.cts +167 -0
  86. package/namespace-probe-BSH_p9TK.d.cts +381 -0
  87. package/namespace-probe-Dsp5uNkS.d.mts +381 -0
  88. package/package.json +1 -1
  89. package/{entity-config-va2bkH5o.mjs → projection-C5244vGa.cjs} +130 -309
  90. package/{entity-config-D87rV-tE.cjs → projection-D7xmQP4S.mjs} +60 -381
  91. package/{sdk-iqOXeWIp.cjs → sdk-Bj-ZDf4u.cjs} +126 -28
  92. package/{sdk-C6WANW8M.mjs → sdk-BvRD2jAp.mjs} +126 -28
  93. package/sdk.cjs +2 -2
  94. package/sdk.d.cts +36 -1
  95. package/sdk.d.mts +36 -1
  96. package/sdk.mjs +2 -2
  97. package/{sync-engine-Do5ru3WF.mjs → sync-engine-Bg6-SznL.mjs} +9 -2
  98. package/{sync-engine-CzEbpQGv.cjs → sync-engine-DEJvKw6E.cjs} +9 -2
  99. package/{sync-engine-DGk_AeHJ.d.mts → sync-engine-GNqRB8eS.d.cts} +2 -2
  100. package/{sync-engine-B7QIbXro.d.cts → sync-engine-ZsusW-q_.d.mts} +2 -2
  101. package/{view-compiler-BWz0sGdm.d.cts → view-compiler-Cin0neDO.d.mts} +1 -1
  102. package/{view-compiler-ChWr_Lml.mjs → view-compiler-DlTpOfZi.mjs} +81 -12
  103. package/{view-compiler-BIwvm-o2.cjs → view-compiler-DplczSjF.cjs} +81 -12
  104. package/{view-compiler-Bn_EJJMO.d.mts → view-compiler-Ll9cbIJz.d.cts} +1 -1
  105. package/vite.d.cts +1 -1
  106. package/vite.d.mts +1 -1
  107. package/drizzle-B89tLo4T.mjs +0 -31
  108. package/drizzle-CFOr9dU9.d.mts +0 -20
  109. package/drizzle-DiReZf6d.cjs +0 -36
  110. package/drizzle-DmeKSqeF.d.cts +0 -20
  111. package/mappings-config-DsUa7-1w.d.mts +0 -41
  112. package/mappings-config-XmzB4dag.d.cts +0 -41
  113. package/types-CsL0rFYh.d.cts +0 -155
  114. package/types-CsL0rFYh.d.mts +0 -155
package/index.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_entity_config = require("./entity-config-D87rV-tE.cjs");
3
+ const require_entity_config = require("./entity-config-CsLl-ftC.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-iqOXeWIp.cjs");
6
+ const require_sdk = require("./sdk-Bj-ZDf4u.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");
@@ -12,6 +12,72 @@ let node_path = require("node:path");
12
12
  let node_readline_promises = require("node:readline/promises");
13
13
  let jiti = require("jiti");
14
14
  let node_url = require("node:url");
15
+ //#region ../core/src/env-file.ts
16
+ const ENV_FILES = {
17
+ next: ".env.local",
18
+ "tanstack-start": ".env",
19
+ sveltekit: ".env",
20
+ remix: ".env",
21
+ nuxt: ".env",
22
+ "hono-workers": ".dev.vars"
23
+ };
24
+ /** The conventional env file for a framework. `.env` when we do not know. */
25
+ function envFileFor(framework) {
26
+ return ENV_FILES[normalizeFramework(framework)] ?? ".env";
27
+ }
28
+ const ALIASES = {
29
+ next: "next",
30
+ nextjs: "next",
31
+ "next.js": "next",
32
+ "tanstack-start": "tanstack-start",
33
+ tanstack: "tanstack-start",
34
+ sveltekit: "sveltekit",
35
+ remix: "remix",
36
+ nuxt: "nuxt",
37
+ "hono-workers": "hono-workers",
38
+ hono: "hono-workers",
39
+ workers: "hono-workers"
40
+ };
41
+ /** The canonical id for a framework name, or null when unrecognised. */
42
+ function normalizeFramework(framework) {
43
+ if (!framework) return null;
44
+ return ALIASES[framework.toLowerCase()] ?? null;
45
+ }
46
+ /**
47
+ * Which env file to actually write to.
48
+ *
49
+ * Prefers a file that already holds the key over the conventional one: a
50
+ * project that has been running with credentials in `.env` must not suddenly
51
+ * get a second copy in `.env.local`, leaving two files disagreeing and the
52
+ * stale one still loading. Convention applies to new setups, not to a working
53
+ * one someone already has.
54
+ */
55
+ function resolveEnvFile(input) {
56
+ const conventional = envFileFor(input.framework);
57
+ if (input.existing.includes(conventional)) return conventional;
58
+ return input.existing[0] ?? conventional;
59
+ }
60
+ //#endregion
61
+ //#region src/app-layout.ts
62
+ function resolveAppLayout(cwd, fileExists = node_fs.existsSync) {
63
+ if (fileExists((0, node_path.join)(cwd, "src", "app"))) return "src/app";
64
+ if (fileExists((0, node_path.join)(cwd, "app"))) return "app";
65
+ return fileExists((0, node_path.join)(cwd, "src")) ? "src/app" : "app";
66
+ }
67
+ function shadowedAppDirError(cwd, framework, plannedPaths, fileExists = node_fs.existsSync) {
68
+ if (framework !== "nextjs") return null;
69
+ if (!plannedPaths.some((path) => path.startsWith("app/"))) return null;
70
+ if (!fileExists((0, node_path.join)(cwd, "src", "app"))) return null;
71
+ return `This project has a src/app directory, so writing routes to app/ would shadow it.
72
+
73
+ Next.js reads either app/ or src/app/, never both. A root app/ directory wins,
74
+ and every route under src/app/ disappears from the build.
75
+
76
+ Nothing was written. Please report this at https://github.com/amani-joseph/clivly/issues
77
+ — Clivly should have detected the src/app layout automatically.
78
+ `;
79
+ }
80
+ //#endregion
15
81
  //#region src/load-config.ts
16
82
  const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
17
83
  function resolveConfigPath(cwd, configPath) {
@@ -109,6 +175,14 @@ async function loadCliConfig(cwd) {
109
175
  return envFiles ? { envFiles } : {};
110
176
  }
111
177
  //#endregion
178
+ //#region src/dashboard-locations.ts
179
+ /** Secret + publishable keys. The Connect dialog mints them and shows the secret once. */
180
+ const CREDENTIALS_LOCATION = "Clivly dashboard → Connect app (or Settings → API keys)";
181
+ /** The signing secret Clivly Cloud uses on sync-trigger callbacks. */
182
+ const SYNC_SECRET_LOCATION = "Clivly dashboard → Integrations → Remote sync";
183
+ /** Chat widget records and their allowed origins. */
184
+ const WIDGET_LOCATION = "Clivly dashboard → Settings → Chat widget";
185
+ //#endregion
112
186
  //#region src/detect.ts
113
187
  const FRAMEWORK_MARKERS = [
114
188
  {
@@ -254,8 +328,19 @@ const OVERRIDE_FAILURE_CAUSES = {
254
328
  not_enabled: "The target backend does not have the feature-flagged trigger-url endpoint enabled (likely the wrong backend, or the flag is off).",
255
329
  unreachable: "Could not reach Clivly at the selected API base URL."
256
330
  };
257
- function overrideFailureMessage(kind) {
258
- return `Warning: could not register the tunnel URL — ${OVERRIDE_FAILURE_CAUSES[kind]} Dashboard-triggered sync is unavailable this session. Your app is still running.`;
331
+ /**
332
+ * `status` is required in practice for `unexpected_response` and meaningless
333
+ * for the others.
334
+ *
335
+ * Every non-401/404 status used to collapse into `unreachable`, so the CLI told
336
+ * developers it "could not reach Clivly" while Clivly was answering. That is
337
+ * not a wording nit: when neon-http broke these endpoints in production the CLI
338
+ * discarded the one honest signal it had (HTTP 500) and sent people to check
339
+ * their network and API URL, where nothing was wrong. A response is proof of
340
+ * reachability — so say what came back.
341
+ */
342
+ function overrideFailureMessage(kind, status) {
343
+ return `Warning: could not register the tunnel URL — ${kind === "unexpected_response" ? `Clivly answered HTTP ${status ?? "an error"} — the request reached Clivly but was rejected, so this is a Clivly-side or request problem rather than a network one.` : OVERRIDE_FAILURE_CAUSES[kind]} Dashboard-triggered sync is unavailable this session. Your app is still running.`;
259
344
  }
260
345
  function cloudflaredMissingMessage() {
261
346
  return [
@@ -310,12 +395,16 @@ async function runDev(options) {
310
395
  deps.out(`Tunnel: ${triggerUrl}`);
311
396
  const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
312
397
  if (result.ok) deps.out("Dashboard-triggered sync now points at this local session.");
313
- else deps.out(overrideFailureMessage(result.kind));
398
+ else deps.out(overrideFailureMessage(result.kind, result.status));
314
399
  });
400
+ const stopWatching = deps.watchConfig?.(options.cwd, () => {
401
+ deps.out("clivly.config.ts changed — restart your app for it to take effect.");
402
+ }) ?? null;
315
403
  return await new Promise((resolve) => {
316
404
  const shutdown = async (code) => {
317
405
  if (shuttingDown) return;
318
406
  shuttingDown = true;
407
+ stopWatching?.();
319
408
  tunnel.kill();
320
409
  app?.kill();
321
410
  if (!(await deps.clear().catch(() => UNREACHABLE_RESULT)).ok) deps.out("Warning: could not clear the tunnel override on exit.");
@@ -337,7 +426,18 @@ const HTTP_NOT_FOUND = 404;
337
426
  function kindForStatus(status) {
338
427
  if (status === HTTP_UNAUTHORIZED) return "unauthorized";
339
428
  if (status === HTTP_NOT_FOUND) return "not_enabled";
340
- return "unreachable";
429
+ return "unexpected_response";
430
+ }
431
+ function failureFor(status) {
432
+ const kind = kindForStatus(status);
433
+ return kind === "unexpected_response" ? {
434
+ ok: false,
435
+ kind,
436
+ status
437
+ } : {
438
+ ok: false,
439
+ kind
440
+ };
341
441
  }
342
442
  async function registerTunnelOverride(deps) {
343
443
  const res = await (deps.fetchImpl ?? globalThis.fetch)(`${deps.apiUrl}${TRIGGER_URL_PATH}`, {
@@ -349,10 +449,7 @@ async function registerTunnelOverride(deps) {
349
449
  body: JSON.stringify({ url: deps.triggerUrl })
350
450
  });
351
451
  if (res.ok) return { ok: true };
352
- return {
353
- ok: false,
354
- kind: kindForStatus(res.status)
355
- };
452
+ return failureFor(res.status);
356
453
  }
357
454
  async function clearTunnelOverride(deps) {
358
455
  const res = await (deps.fetchImpl ?? globalThis.fetch)(`${deps.apiUrl}${TRIGGER_URL_PATH}`, {
@@ -360,10 +457,7 @@ async function clearTunnelOverride(deps) {
360
457
  headers: { authorization: `Bearer ${deps.apiKey}` }
361
458
  });
362
459
  if (res.ok) return { ok: true };
363
- return {
364
- ok: false,
365
- kind: kindForStatus(res.status)
366
- };
460
+ return failureFor(res.status);
367
461
  }
368
462
  //#endregion
369
463
  //#region src/probe-module.ts
@@ -688,6 +782,30 @@ function buildFkByColumn(pgCore, drizzle, table) {
688
782
  } catch {}
689
783
  return fkByColumn;
690
784
  }
785
+ function buildUniqueConstraints(pgCore, table) {
786
+ if (!pgCore) return;
787
+ const keys = [];
788
+ try {
789
+ const config = pgCore.getTableConfig(table);
790
+ for (const column of config.columns ?? []) if (column.isUnique || column.primary) keys.push([column.name]);
791
+ for (const pk of config.primaryKeys ?? []) keys.push(pk.columns.map((c) => c.name));
792
+ for (const constraint of config.uniqueConstraints ?? []) keys.push(constraint.columns.map((c) => c.name));
793
+ for (const index of config.indexes ?? []) {
794
+ if (!index.config?.unique) continue;
795
+ const columns = (index.config.columns ?? []).map((c) => c?.name).filter((name) => Boolean(name));
796
+ if (columns.length > 0) keys.push(columns);
797
+ }
798
+ } catch {
799
+ return;
800
+ }
801
+ const seen = /* @__PURE__ */ new Set();
802
+ return keys.filter((key) => {
803
+ const id = [...key].sort().join("\0");
804
+ if (seen.has(id) || key.length === 0) return false;
805
+ seen.add(id);
806
+ return true;
807
+ });
808
+ }
691
809
  function toDiscoveredColumn(key, col, fkByColumn) {
692
810
  let type;
693
811
  try {
@@ -735,10 +853,12 @@ async function discoverSchema(opts) {
735
853
  if (drizzle.is(value, drizzle.Table)) {
736
854
  const fkByColumn = buildFkByColumn(pgCore, drizzle, value);
737
855
  const columns = Object.entries(drizzle.getTableColumns(value)).map(([key, col]) => toDiscoveredColumn(key, col, fkByColumn));
856
+ const uniqueConstraints = buildUniqueConstraints(pgCore, value);
738
857
  tables.push({
739
858
  moduleKey,
740
859
  tableName: drizzle.getTableName(value),
741
- columns
860
+ columns,
861
+ ...uniqueConstraints === void 0 ? {} : { uniqueConstraints }
742
862
  });
743
863
  continue;
744
864
  }
@@ -768,6 +888,29 @@ async function discoverSchema(opts) {
768
888
  };
769
889
  }
770
890
  //#endregion
891
+ //#region src/engine-check.ts
892
+ const MAJOR = /^v?(\d+)/;
893
+ const FIRST_NUMBER = /(\d+)/;
894
+ function currentMajor(version) {
895
+ const match = MAJOR.exec(version);
896
+ return match ? Number(match[1]) : null;
897
+ }
898
+ function requiredMajor(range) {
899
+ if (!range) return null;
900
+ const match = FIRST_NUMBER.exec(range);
901
+ return match ? Number(match[1]) : null;
902
+ }
903
+ function engineError(currentVersion, enginesRange) {
904
+ const have = currentMajor(currentVersion);
905
+ const need = requiredMajor(enginesRange);
906
+ if (have === null || need === null || have >= need) return null;
907
+ return `Clivly requires Node.js ${need} or newer. You are running ${currentVersion}.
908
+
909
+ Upgrade with \`nvm install ${need}\` (or your version manager of choice), then re-run.
910
+ Set CLIVLY_SKIP_ENGINE_CHECK=1 to run anyway — expect unrelated-looking failures.
911
+ `;
912
+ }
913
+ //#endregion
771
914
  //#region src/env-file.ts
772
915
  const REGEX_METACHARACTERS = /[.*+?^${}()|[\]\\]/g;
773
916
  function escapeForRegExp(key) {
@@ -852,6 +995,123 @@ function upsertEnvValues(existing, entries, options = {}) {
852
995
  };
853
996
  }
854
997
  //#endregion
998
+ //#region src/help.ts
999
+ const ROOT_HELP = `clivly — embed a CRM in your app
1000
+
1001
+ Usage:
1002
+ clivly init [options] Scaffold Clivly (config, routes, env file) for your app
1003
+ clivly add widget [options] Scaffold the chat-session route
1004
+ clivly status [options] Check your setup, step by step
1005
+ clivly push-schema [options] Report your schema to Clivly without deploying
1006
+ clivly dev [options] Tunnel localhost so remote sync can reach you
1007
+ clivly login [options] Pair this machine and write credentials to your env file
1008
+ clivly --version Print the installed clivly version
1009
+
1010
+ Deprecated aliases — each runs part of \`clivly status\` and still works:
1011
+ clivly ping credential + reachability
1012
+ clivly connect the above, plus config and trigger URL
1013
+ clivly doctor config, setup checks and edge safety
1014
+
1015
+ Prefer \`clivly status\`: it runs the whole ladder and tells you which step you
1016
+ are on. The aliases are kept because output and docs have recommended them.
1017
+
1018
+ Run \`clivly <command> --help\` for details on a command.
1019
+ `;
1020
+ const COMMAND_HELP = {
1021
+ init: `clivly init — scaffold Clivly into this app
1022
+
1023
+ Usage:
1024
+ clivly init [--force] [--dry-run] [--diff] [--yes] [--no-login] [--no-browser]
1025
+ [--schema <path|package>] [--db-import <specifier>]
1026
+ [--auth-import <specifier>] [--contact-table <table>]
1027
+ [--company-table <table>]
1028
+
1029
+ Scaffolds clivly.config.ts, the Clivly route files, and .env placeholders.
1030
+ Detects your framework, ORM, auth adapter and app layout from the project.
1031
+
1032
+ Options:
1033
+ --dry-run Show what would be written; write nothing
1034
+ --diff Show a diff for files that already exist
1035
+ --force Overwrite files Clivly owns
1036
+ --yes Skip prompts
1037
+ --no-login Skip browser pairing
1038
+ --schema <p> Path or package holding your ORM schema
1039
+ --db-import <s> Module specifier exporting your database instance
1040
+ --auth-import <s> Module specifier exporting your auth instance
1041
+ --contact-table <t> Table to map as contacts
1042
+ --company-table <t> Table to map as companies
1043
+ `,
1044
+ add: `clivly add widget — scaffold the chat-session route
1045
+
1046
+ Usage:
1047
+ clivly add widget [--force] [--dry-run]
1048
+
1049
+ Writes the route that mints chat-widget sessions. Creates no widget record —
1050
+ that lives in the Clivly dashboard.
1051
+
1052
+ Options:
1053
+ --dry-run Show what would be written; write nothing
1054
+ --force Overwrite the route if it already exists
1055
+ `,
1056
+ status: `clivly status — check your setup, step by step
1057
+
1058
+ Usage:
1059
+ clivly status [--verbose] [--config <path>] [--env-file <path>]
1060
+
1061
+ Runs the setup ladder: credential, reachability, config load, edge-safety,
1062
+ setup checks, sync trigger. Read-only — writes nothing.
1063
+
1064
+ Options:
1065
+ --verbose Show every check, including those that passed
1066
+ --config <path> Path to clivly.config.ts
1067
+ --env-file <path> Extra env file to load
1068
+ `,
1069
+ "push-schema": `clivly push-schema — report your schema without deploying
1070
+
1071
+ Usage:
1072
+ clivly push-schema [--config <path>]
1073
+
1074
+ Sends your discovered tables to Clivly so the dashboard can offer them for
1075
+ mapping. Reads your config; writes nothing locally.
1076
+ `,
1077
+ dev: `clivly dev — tunnel localhost so remote sync can reach you
1078
+
1079
+ Usage:
1080
+ clivly dev [--port <n>] [--api-url <url>] [--route-path <path>] [-- <cmd>]
1081
+
1082
+ Opens a tunnel to your local app and points Clivly's sync trigger at it.
1083
+ Everything after \`--\` runs as your app's dev command.
1084
+
1085
+ Options:
1086
+ --port <n> Local port your app listens on
1087
+ --route-path <path> Path to your mounted tick route
1088
+ `,
1089
+ login: `clivly login — pair this machine with Clivly
1090
+
1091
+ Usage:
1092
+ clivly login [--force] [--rotate] [--api-url <url>] [--no-browser]
1093
+
1094
+ Opens a browser to approve this machine, then writes CLIVLY_SECRET_KEY and
1095
+ CLIVLY_SYNC_TRIGGER_SECRET to .env.
1096
+
1097
+ Options:
1098
+ --force Re-pair even if credentials already exist
1099
+ --rotate Replace the existing credentials
1100
+ --no-browser Print the URL instead of opening a browser
1101
+ `
1102
+ };
1103
+ function isHelpRequest(argv) {
1104
+ for (const arg of argv) {
1105
+ if (arg === "--") return false;
1106
+ if (arg === "--help" || arg === "-h") return true;
1107
+ }
1108
+ return false;
1109
+ }
1110
+ function helpFor(command) {
1111
+ if (!command) return ROOT_HELP;
1112
+ return COMMAND_HELP[command] ?? ROOT_HELP;
1113
+ }
1114
+ //#endregion
855
1115
  //#region src/install.ts
856
1116
  const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject) => {
857
1117
  const child = (0, node_child_process.spawn)(command, args, {
@@ -1091,11 +1351,11 @@ function credentialEntries(credentials) {
1091
1351
  return [{
1092
1352
  key: SECRET_KEY_ENV,
1093
1353
  value: credentials.apiKey,
1094
- comment: "Org secret key — Settings → API keys"
1354
+ comment: `Org secret key — ${CREDENTIALS_LOCATION}`
1095
1355
  }, {
1096
1356
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
1097
1357
  value: credentials.triggerSecret,
1098
- comment: "Signing secret — Integrations → Remote sync"
1358
+ comment: `Signing secret — ${SYNC_SECRET_LOCATION}`
1099
1359
  }];
1100
1360
  }
1101
1361
  function writeCredentials(options, credentials) {
@@ -1182,12 +1442,19 @@ function chooseSyncFields(table, todos) {
1182
1442
  idField
1183
1443
  };
1184
1444
  }
1445
+ const SPLIT_NAME_COLUMNS = /* @__PURE__ */ new Set([
1446
+ "first_name",
1447
+ "firstname",
1448
+ "given_name"
1449
+ ]);
1185
1450
  function buildEntity(table, concept, todos) {
1186
1451
  const canonical = require_entity_config.CANONICAL_FIELDS[concept];
1187
1452
  const suggested = require_internal_entity_heuristics.toFieldMap(require_internal_entity_heuristics.scoreFieldMap(concept, toScoredTable(table)));
1188
1453
  const fields = {};
1189
1454
  for (const [key, column] of Object.entries(suggested)) if (canonical.includes(key)) fields[key] = column;
1190
1455
  for (const required of require_internal_entity_heuristics.requiredFieldsFor(concept)) if (!fields[required]) todos.push(`Could not map required ${concept} field "${required}" on "${table.tableName}" — add it manually.`);
1456
+ const nameColumn = fields.name?.toLowerCase();
1457
+ if (nameColumn && SPLIT_NAME_COLUMNS.has(nameColumn)) todos.push(`Mapped ${concept} "name" to "${fields.name}" on "${table.tableName}" — that is the given name only. Clivly mirrors one column per field, so add a full-name column if you want surnames.`);
1191
1458
  const { cursorField, idField } = chooseSyncFields(table, todos);
1192
1459
  return {
1193
1460
  moduleKey: table.moduleKey,
@@ -1258,15 +1525,99 @@ function planInit(schema, overrides = {}) {
1258
1525
  contact,
1259
1526
  company,
1260
1527
  tableNames: schema.tables.map((table) => table.tableName),
1528
+ contactCandidates: rankContactCandidates(schema.tables),
1261
1529
  todos
1262
1530
  };
1263
1531
  }
1532
+ const CONTACT_COLUMN_EVIDENCE = [
1533
+ {
1534
+ label: "email",
1535
+ names: ["email", "email_address"],
1536
+ weight: 3
1537
+ },
1538
+ {
1539
+ label: "phone",
1540
+ names: [
1541
+ "phone",
1542
+ "phone_number",
1543
+ "mobile"
1544
+ ],
1545
+ weight: 2
1546
+ },
1547
+ {
1548
+ label: "first name",
1549
+ names: [
1550
+ "first_name",
1551
+ "firstname",
1552
+ "given_name"
1553
+ ],
1554
+ weight: 2
1555
+ },
1556
+ {
1557
+ label: "last name",
1558
+ names: [
1559
+ "last_name",
1560
+ "lastname",
1561
+ "surname",
1562
+ "family_name"
1563
+ ],
1564
+ weight: 2
1565
+ },
1566
+ {
1567
+ label: "name",
1568
+ names: [
1569
+ "name",
1570
+ "full_name",
1571
+ "display_name"
1572
+ ],
1573
+ weight: 1
1574
+ }
1575
+ ];
1576
+ const NAME_MATCH_WEIGHT = 4;
1577
+ /**
1578
+ * Rank the schema's tables by how much they look like people, best first.
1579
+ *
1580
+ * Used only on the failure path, where detection found nothing mappable and the
1581
+ * developer has to choose. `init` previously suggested `tableNames[0]` — the
1582
+ * first table in schema order — so a rental app was told to map its `lease`
1583
+ * table as contacts. Wrong guidance is worse than none: it is confident, and
1584
+ * the developer has no way to see it is wrong.
1585
+ *
1586
+ * Deliberately evidence-based rather than name-based alone, so a table whose
1587
+ * name Clivly has never heard of still surfaces on the strength of its columns.
1588
+ */
1589
+ function rankContactCandidates(tables) {
1590
+ return tables.map((table) => {
1591
+ const columns = new Set(table.columns.flatMap((c) => [c.dbName.toLowerCase(), c.key.toLowerCase()]));
1592
+ const reasons = [];
1593
+ let score = 0;
1594
+ if (require_internal_entity_heuristics.matchesConcept(table.tableName, "contact")) {
1595
+ score += NAME_MATCH_WEIGHT;
1596
+ reasons.push("the name looks like a people table");
1597
+ }
1598
+ for (const evidence of CONTACT_COLUMN_EVIDENCE) {
1599
+ const hit = evidence.names.find((name) => columns.has(name));
1600
+ if (hit) {
1601
+ score += evidence.weight;
1602
+ reasons.push(hit);
1603
+ }
1604
+ }
1605
+ return {
1606
+ reasons,
1607
+ score,
1608
+ table: table.tableName
1609
+ };
1610
+ }).filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score || a.table.localeCompare(b.table)).map(({ reasons, table }) => ({
1611
+ reasons,
1612
+ table
1613
+ }));
1614
+ }
1264
1615
  //#endregion
1265
1616
  //#region src/render-config.ts
1266
1617
  const CLIVLY_ENV_KEYS = [
1267
1618
  {
1268
1619
  key: "CLIVLY_SECRET_KEY",
1269
- comment: "Org secret key — Settings → API keys"
1620
+ comment: `Org secret key — ${CREDENTIALS_LOCATION}`
1270
1621
  },
1271
1622
  {
1272
1623
  key: "CLIVLY_SYNC_TRIGGER_URL",
@@ -1274,7 +1625,7 @@ const CLIVLY_ENV_KEYS = [
1274
1625
  },
1275
1626
  {
1276
1627
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
1277
- comment: "Signing secret — Integrations → Remote sync"
1628
+ comment: `Signing secret — ${SYNC_SECRET_LOCATION}`
1278
1629
  }
1279
1630
  ];
1280
1631
  function renderFields(fields) {
@@ -1303,6 +1654,28 @@ function renderDbExpr(plan) {
1303
1654
  if (!plan.db) return "db";
1304
1655
  return dbIsInSchemaModule(plan) ? `schema.${plan.db.moduleKey}` : plan.db.moduleKey;
1305
1656
  }
1657
+ const TRAILING_SCHEMA_SEGMENT = /\/schema(\/index)?$/;
1658
+ /**
1659
+ * Where the `db` instance most likely lives, given where the schema was found.
1660
+ *
1661
+ * Only reached when discovery could not confirm one. Discovery *evaluates* a
1662
+ * candidate module and duck-types its exports for `.select`, which is the right
1663
+ * check but fails for the most common real-world db module: `drizzle(process.
1664
+ * env.DATABASE_URL!)` throws at import time when the env var is absent, and it
1665
+ * is absent precisely when a developer is running `clivly init` for the first
1666
+ * time. So the probe misses, through no fault of the project.
1667
+ *
1668
+ * The old fallback emitted `<YOUR_DB_MODULE>` — not a module specifier, not
1669
+ * valid TypeScript, and invisible until the config failed to load at runtime.
1670
+ * A best-guess relative path is strictly better: right for the conventional
1671
+ * layout (`lib/db/schema.ts` beside `lib/db/index.ts`), and when it is wrong it
1672
+ * is a module-not-found error at build time, naming a real path the developer
1673
+ * can correct.
1674
+ */
1675
+ function guessDbImportPath(schemaImportPath) {
1676
+ const trimmed = schemaImportPath.replace(TRAILING_SCHEMA_SEGMENT, "");
1677
+ return trimmed === schemaImportPath || trimmed === "." || trimmed === "" ? "./db" : trimmed;
1678
+ }
1306
1679
  function renderImports(plan) {
1307
1680
  const lines = [
1308
1681
  "import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
@@ -1310,8 +1683,10 @@ function renderImports(plan) {
1310
1683
  "import { discoverFromDrizzle } from \"clivly/drizzle\";",
1311
1684
  `import * as schema from "${plan.importPath}";`
1312
1685
  ];
1313
- if (!plan.db) lines.push("import { db } from \"<YOUR_DB_MODULE>\"; // TODO(clivly): replace with the path to your Drizzle `db` instance — NOT your schema file");
1314
- else if (!dbIsInSchemaModule(plan)) lines.push(`import { ${plan.db.moduleKey} } from "${plan.db.importPath}";`);
1686
+ if (plan.db === null) {
1687
+ const guess = guessDbImportPath(plan.importPath);
1688
+ lines.push(`import { db } from "${guess}"; // TODO(clivly): verify this path points at your Drizzle \`db\` instance — NOT your schema file`);
1689
+ } else if (!dbIsInSchemaModule(plan)) lines.push(`import { ${plan.db.moduleKey} } from "${plan.db.importPath}";`);
1315
1690
  return lines.join("\n");
1316
1691
  }
1317
1692
  function renderTodoHeader(todos) {
@@ -1429,6 +1804,83 @@ function renderAuthWiring(auth, authInstance = null) {
1429
1804
  }
1430
1805
  }
1431
1806
  //#endregion
1807
+ //#region src/tsconfig-alias.ts
1808
+ const LEADING_DOT_SLASH$1 = /^\.\//;
1809
+ const TRAILING_WILDCARD = /\/?\*$/;
1810
+ const TRAILING_SLASH = /\/$/;
1811
+ const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
1812
+ const LINE_COMMENT = /(^|[^:])\/\/.*$/gm;
1813
+ const TRAILING_COMMA = /,(\s*[}\]])/g;
1814
+ /**
1815
+ * `compilerOptions` from a tsconfig's text, or null when it cannot be read.
1816
+ *
1817
+ * tsconfig is JSONC — comments and trailing commas are legal and common, and
1818
+ * `JSON.parse` rejects both. Stripping them is enough here: this only ever reads
1819
+ * two string-valued fields, so the cost of being wrong is falling back to a
1820
+ * relative import, which already works.
1821
+ */
1822
+ function parseTsconfig(source) {
1823
+ try {
1824
+ const stripped = source.replace(BLOCK_COMMENT, "").replace(LINE_COMMENT, "$1").replace(TRAILING_COMMA, "$1");
1825
+ return JSON.parse(stripped).compilerOptions ?? null;
1826
+ } catch {
1827
+ return null;
1828
+ }
1829
+ }
1830
+ /**
1831
+ * Strip a leading "./" and any trailing wildcard so two paths can be compared.
1832
+ *
1833
+ * `"./*"` — an alias rooted at the app itself — must reduce to the empty string,
1834
+ * not to a stray `"*"`; that is the mapping which makes a root-level
1835
+ * clivly.config alias-reachable.
1836
+ */
1837
+ function normalize(value) {
1838
+ return value.replace(LEADING_DOT_SLASH$1, "").replace(TRAILING_WILDCARD, "").replace(TRAILING_SLASH, "");
1839
+ }
1840
+ /**
1841
+ * The alias specifier that reaches `target`, or null when none does.
1842
+ *
1843
+ * `target` is app-root-relative and extensionless, e.g. `lib/auth` or
1844
+ * `clivly.config`. Only wildcard patterns are considered — an exact mapping
1845
+ * names one module and is not a prefix anything else can be rebased onto.
1846
+ *
1847
+ * Longest prefix wins, so `"@components/*": ["./src/components/*"]` beats
1848
+ * `"@/*": ["./src/*"]` for a module under `src/components`, matching how
1849
+ * TypeScript itself resolves.
1850
+ */
1851
+ function aliasFor(input) {
1852
+ if (!input.paths) return null;
1853
+ const target = normalize(input.target);
1854
+ let best = null;
1855
+ for (const [pattern, targets] of Object.entries(input.paths)) {
1856
+ if (!pattern.endsWith("/*")) continue;
1857
+ const prefix = pattern.slice(0, -2);
1858
+ for (const mapped of targets) {
1859
+ const base = baseReaching(mapped, target);
1860
+ if (base === null) continue;
1861
+ const rest = base === "" ? target : target.slice(base.length + 1);
1862
+ if (!best || base.length > best.length) best = {
1863
+ alias: `${prefix}/${rest}`,
1864
+ length: base.length
1865
+ };
1866
+ }
1867
+ }
1868
+ return best?.alias ?? null;
1869
+ }
1870
+ /**
1871
+ * The normalized base of `mapped` when it contains `target`, else null.
1872
+ *
1873
+ * A prefix must end at a segment boundary — `src` must not match `srcinternal`,
1874
+ * a different directory — and the target must be strictly inside the base, since
1875
+ * a wildcard needs a remainder to substitute.
1876
+ */
1877
+ function baseReaching(mapped, target) {
1878
+ if (!mapped.endsWith("/*")) return null;
1879
+ const base = normalize(mapped);
1880
+ if (base === "") return target === "" ? null : "";
1881
+ return target.startsWith(`${base}/`) ? base : null;
1882
+ }
1883
+ //#endregion
1432
1884
  //#region src/scaffold.ts
1433
1885
  const TANSTACK_ROUTE_PREFIX = /^src\/routes\//;
1434
1886
  const TS_EXTENSION = /\.ts$/;
@@ -1460,10 +1912,14 @@ const ROUTE_PATHS = {
1460
1912
  },
1461
1913
  unknown: null
1462
1914
  };
1463
- function routePath(framework, kind) {
1915
+ function applyLayout(framework, path, layout) {
1916
+ if (framework !== "nextjs" || layout === "app") return path;
1917
+ return `src/${path}`;
1918
+ }
1919
+ function routePath(framework, kind, layout = "app") {
1464
1920
  const paths = ROUTE_PATHS[framework];
1465
1921
  if (!paths) throw new Error(`No route path for framework: ${framework}`);
1466
- return paths[kind];
1922
+ return applyLayout(framework, paths[kind], layout);
1467
1923
  }
1468
1924
  const WINDOWS_SEP = /\\/g;
1469
1925
  const LEADING_DOT_SLASH = /^\.\//;
@@ -1471,22 +1927,27 @@ function configImport(routeFilePath) {
1471
1927
  const rel = (0, node_path.relative)((0, node_path.dirname)(routeFilePath), "clivly.config").replace(WINDOWS_SEP, "/");
1472
1928
  return rel.startsWith(".") ? rel : `./${rel}`;
1473
1929
  }
1474
- function hostImportPath(routeFilePath, appRelative) {
1930
+ function hostImportPath(routeFilePath, appRelative, paths) {
1475
1931
  const fromRoot = appRelative.replace(LEADING_DOT_SLASH, "");
1932
+ const alias = aliasFor({
1933
+ paths,
1934
+ target: fromRoot
1935
+ });
1936
+ if (alias) return alias;
1476
1937
  const rel = (0, node_path.relative)((0, node_path.dirname)(routeFilePath), fromRoot).replace(WINDOWS_SEP, "/");
1477
1938
  return rel.startsWith(".") ? rel : `./${rel}`;
1478
1939
  }
1479
- function renderAuthPreamble(routeFilePath, auth) {
1940
+ function renderAuthPreamble(routeFilePath, auth, paths) {
1480
1941
  const lines = [...auth.imports];
1481
1942
  if (auth.hostImport) {
1482
1943
  if (auth.hostImport.todo) lines.push(`// ${auth.hostImport.todo}`);
1483
- const path = hostImportPath(routeFilePath, auth.hostImport.path);
1944
+ const path = hostImportPath(routeFilePath, auth.hostImport.path, paths);
1484
1945
  lines.push(`import { ${auth.hostImport.binding} } from "${path}";`);
1485
1946
  }
1486
1947
  lines.push("", auth.body);
1487
1948
  return lines.join("\n");
1488
1949
  }
1489
- function renderPreamble(kind, importPath, auth, routeFilePath) {
1950
+ function renderPreamble(kind, importPath, auth, paths, routeFilePath) {
1490
1951
  if (kind === "session") return `import { createChatSessionHandler } from "clivly/sdk";
1491
1952
 
1492
1953
  const handler = createChatSessionHandler({
@@ -1497,7 +1958,7 @@ const handler = createChatSessionHandler({
1497
1958
  });
1498
1959
  `;
1499
1960
  if (kind === "verify") return `import clivly from "${importPath}";
1500
- ${renderAuthPreamble(routeFilePath, auth)}
1961
+ ${renderAuthPreamble(routeFilePath, auth, paths)}
1501
1962
  const handler = clivly.createAuthVerifyHandler(resolveUser);
1502
1963
  `;
1503
1964
  return `import clivly from "${importPath}";
@@ -1505,8 +1966,8 @@ const handler = clivly.createAuthVerifyHandler(resolveUser);
1505
1966
  const handler = clivly.createClivlyHandler();
1506
1967
  `;
1507
1968
  }
1508
- function renderBody(framework, importPath, kind, auth, routeFilePath) {
1509
- const preamble = renderPreamble(kind, importPath, auth, routeFilePath);
1969
+ function renderBody(framework, importPath, kind, auth, routeFilePath, paths) {
1970
+ const preamble = renderPreamble(kind, importPath, auth, paths, routeFilePath);
1510
1971
  switch (framework) {
1511
1972
  case "nextjs": return `${preamble}
1512
1973
  export const POST = (request: Request) => handler(request);
@@ -1536,9 +1997,9 @@ export default defineEventHandler((event) => handler(toWebRequest(event)));
1536
1997
  default: throw new Error(`No route template for framework: ${framework}`);
1537
1998
  }
1538
1999
  }
1539
- function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown")) {
1540
- const path = routePath(framework, kind);
1541
- let body = renderBody(framework, configImport(path), kind, auth, path);
2000
+ function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown"), layout = "app", paths) {
2001
+ const path = routePath(framework, kind, layout);
2002
+ let body = renderBody(framework, configImport(path), kind, auth, path, paths);
1542
2003
  if (framework === "tanstack-start") {
1543
2004
  const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
1544
2005
  body = body.replace("__ROUTE__", url);
@@ -1550,28 +2011,28 @@ const COMMON_FILES = [{
1550
2011
  description: "Your Clivly configuration",
1551
2012
  owned: false
1552
2013
  }];
1553
- function planScaffold(framework) {
2014
+ function planScaffold(framework, layout = "app") {
1554
2015
  const paths = ROUTE_PATHS[framework];
1555
2016
  if (!paths) return [];
1556
2017
  return [
1557
2018
  {
1558
- path: paths.tick,
2019
+ path: applyLayout(framework, paths.tick, layout),
1559
2020
  description: "Clivly sync-trigger route (createClivlyHandler)",
1560
2021
  owned: true
1561
2022
  },
1562
2023
  {
1563
- path: paths.verify,
2024
+ path: applyLayout(framework, paths.verify, layout),
1564
2025
  description: "Clivly auth-verify route (createAuthVerifyHandler) — finish resolveUser",
1565
2026
  owned: true
1566
2027
  },
1567
2028
  ...COMMON_FILES
1568
2029
  ];
1569
2030
  }
1570
- function planWidgetScaffold(framework) {
2031
+ function planWidgetScaffold(framework, layout = "app") {
1571
2032
  const paths = ROUTE_PATHS[framework];
1572
2033
  if (!paths) return [];
1573
2034
  return [{
1574
- path: paths.session,
2035
+ path: applyLayout(framework, paths.session, layout),
1575
2036
  description: "Clivly chat-session route (createChatSessionHandler) — add allowedOrigins",
1576
2037
  owned: true
1577
2038
  }];
@@ -1591,9 +2052,9 @@ function planFiles(args) {
1591
2052
  const render = (path) => {
1592
2053
  if (path === "clivly.config.ts") return renderInitConfig(args.plan);
1593
2054
  const kind = path.includes("verify") ? "verify" : "tick";
1594
- return renderRouteFile(args.framework, kind, args.authWiring);
2055
+ return renderRouteFile(args.framework, kind, args.authWiring, args.layout, parseTsconfig(args.readFile((0, node_path.join)(args.cwd, "tsconfig.json")) ?? "")?.paths);
1595
2056
  };
1596
- for (const file of planScaffold(args.framework)) {
2057
+ for (const file of planScaffold(args.framework, args.layout)) {
1597
2058
  const contents = render(file.path);
1598
2059
  mutations.push({
1599
2060
  path: file.path,
@@ -1624,8 +2085,8 @@ function planMutations(args) {
1624
2085
  };
1625
2086
  }
1626
2087
  function planWidgetMutations(args) {
1627
- return planWidgetScaffold(args.framework).map((file) => {
1628
- const contents = renderRouteFile(args.framework, "session");
2088
+ return planWidgetScaffold(args.framework, args.layout).map((file) => {
2089
+ const contents = renderRouteFile(args.framework, "session", void 0, args.layout);
1629
2090
  return {
1630
2091
  path: file.path,
1631
2092
  contents,
@@ -1950,7 +2411,17 @@ const ALLOWED_CALLS = /* @__PURE__ */ new Set([
1950
2411
  "while"
1951
2412
  ]);
1952
2413
  const SIDE_EFFECT_CALLS = /^(dotenv\.config|config|configDotenv|fileURLToPath|readFileSync|require|process\.chdir)$/;
2414
+ const NATIVE_PACKAGES = /* @__PURE__ */ new Set([
2415
+ "bcrypt",
2416
+ "better-sqlite3",
2417
+ "canvas",
2418
+ "node-gyp",
2419
+ "pg-native",
2420
+ "sharp",
2421
+ "sqlite3"
2422
+ ]);
1953
2423
  const ENV_HINT = "Move it to clivly.cli.ts (`envFiles`) or pass --env-file — the CLI reads those in Node, so your config stays edge-safe.";
2424
+ const NATIVE_HINT = "A compiled binary cannot load on an edge runtime — this is the \"Could not locate the bindings file\" / ERR_DLOPEN_FAILED failure. Import your db lazily inside a function, or use a driver that speaks HTTP (e.g. @neondatabase/serverless) in the config.";
1954
2425
  function isNodeBuiltin(specifier) {
1955
2426
  if (specifier.startsWith("node:")) return true;
1956
2427
  return NODE_BUILTINS.has(specifier.split("/")[0] ?? "");
@@ -1967,6 +2438,12 @@ function specifierIssue(specifier, line) {
1967
2438
  hint: ENV_HINT,
1968
2439
  line
1969
2440
  };
2441
+ if (NATIVE_PACKAGES.has(specifier.split("/")[0] ?? "")) return {
2442
+ code: "native_module_in_edge_config",
2443
+ detail: `imports "${specifier}", which ships a compiled binary`,
2444
+ hint: NATIVE_HINT,
2445
+ line
2446
+ };
1970
2447
  if (isNodeBuiltin(specifier)) return {
1971
2448
  code: "node_builtin_in_edge_config",
1972
2449
  detail: `imports "${specifier}", which does not exist on an edge runtime`,
@@ -2006,8 +2483,56 @@ function analyzeEdgeSafety(source) {
2006
2483
  });
2007
2484
  return issues;
2008
2485
  }
2486
+ /** Every relative specifier `source` imports, in first-seen order, deduped. */
2487
+ function relativeImports(source) {
2488
+ const seen = /* @__PURE__ */ new Set();
2489
+ for (const rawLine of source.split(NEWLINE)) {
2490
+ const line = stripComment(rawLine);
2491
+ const specifier = BARE_IMPORT.exec(line)?.[1] ?? IMPORT_FROM.exec(line)?.[1] ?? REQUIRE_CALL.exec(line)?.[1];
2492
+ if (specifier?.startsWith(".")) seen.add(specifier);
2493
+ }
2494
+ return [...seen];
2495
+ }
2496
+ /**
2497
+ * Edge-safety for the config *and* the modules it imports directly.
2498
+ *
2499
+ * The dominant real-world shape is a clean-looking config with
2500
+ * `import { db } from "./lib/db"`, where the problem is one hop away and
2501
+ * invisible to a scan that reads only the config file.
2502
+ *
2503
+ * One hop, not more: each additional level multiplies the files read for a
2504
+ * shrinking return, and the config's own imports are where a host app puts its
2505
+ * database. Bare specifiers are never followed — resolving into node_modules is
2506
+ * a different job with different failure modes, and the denylist already covers
2507
+ * the packages that matter.
2508
+ *
2509
+ * `readModule` returns source text, or null when the module cannot be read;
2510
+ * unreadable is not a finding, since a scan is not a build.
2511
+ */
2512
+ function analyzeConfigTree(input) {
2513
+ const issues = analyzeEdgeSafety(input.source);
2514
+ for (const specifier of relativeImports(input.source)) {
2515
+ const source = input.readModule(specifier);
2516
+ if (source === null) continue;
2517
+ for (const issue of analyzeEdgeSafety(source)) issues.push({
2518
+ ...issue,
2519
+ module: specifier
2520
+ });
2521
+ }
2522
+ return issues;
2523
+ }
2009
2524
  //#endregion
2010
2525
  //#region src/status-stages.ts
2526
+ const MODULE_CANDIDATES = [
2527
+ ".ts",
2528
+ ".tsx",
2529
+ ".js",
2530
+ ".mjs",
2531
+ ".cjs",
2532
+ "/index.ts",
2533
+ "/index.js",
2534
+ ""
2535
+ ];
2011
2536
  function defaultReadConfigSource(path) {
2012
2537
  try {
2013
2538
  return (0, node_fs.readFileSync)(path, "utf8");
@@ -2022,10 +2547,11 @@ const REASON_HINT = {
2022
2547
  rate_limited: "Rate limited (429). Wait a moment and retry.",
2023
2548
  server_error: "Clivly returned a server error (5xx). Retry shortly.",
2024
2549
  client_error: "The request was rejected (4xx). Check your configuration.",
2550
+ server_too_old: "This Clivly server is older than your CLI and has no /clivly/sdk/ping. Upgrade the server, or pin the CLI to match it.",
2025
2551
  network_error: "Could not reach Clivly. Check your network or --api-url."
2026
2552
  };
2027
2553
  const apiKeyStage = {
2028
- id: "api-key",
2554
+ id: "credential",
2029
2555
  label: "API key present",
2030
2556
  run(ctx) {
2031
2557
  if (ctx.apiKey) return Promise.resolve({
@@ -2035,20 +2561,29 @@ const apiKeyStage = {
2035
2561
  return Promise.resolve({
2036
2562
  state: "fail",
2037
2563
  detail: "Secret key is not set.",
2038
- hint: "Copy your secret key from Settings → API keys into CLIVLY_SECRET_KEY in .env."
2564
+ hint: `Copy your secret key from ${CREDENTIALS_LOCATION} into CLIVLY_SECRET_KEY in .env.`
2039
2565
  });
2040
2566
  }
2041
2567
  };
2042
- const heartbeatStage = {
2043
- id: "heartbeat",
2568
+ const reachabilityStage = {
2569
+ id: "application",
2044
2570
  label: "Clivly reachable",
2045
2571
  needs: [apiKeyStage.id],
2046
2572
  async run(ctx) {
2047
2573
  const result = await ctx.connect(ctx.apiKey, ctx.apiUrl);
2048
- if (result.ok) return {
2049
- state: "pass",
2050
- detail: `Reached org: ${result.org?.name ?? "(unnamed org)"}${result.org?.id ? ` (${result.org.id})` : ""}`
2051
- };
2574
+ if (result.ok) {
2575
+ const name = result.org?.name ?? "(unnamed org)";
2576
+ const id = result.org?.id ? ` (${result.org.id})` : "";
2577
+ if (result.appLastSeenAt === null) return {
2578
+ state: "warn",
2579
+ detail: `Reached org: ${name}${id} — your app has not connected yet`,
2580
+ hint: "Start your app with the Clivly SDK mounted; it will appear in the dashboard within a minute."
2581
+ };
2582
+ return {
2583
+ state: "pass",
2584
+ detail: `Reached org: ${name}${id}`
2585
+ };
2586
+ }
2052
2587
  const status = result.status === null ? "no response" : `HTTP ${result.status}`;
2053
2588
  return {
2054
2589
  state: "fail",
@@ -2076,22 +2611,76 @@ const configStage = {
2076
2611
  }
2077
2612
  }
2078
2613
  };
2079
- const LADDER_OWNED_CHECKS = /* @__PURE__ */ new Set(["API key present", "Heartbeat"]);
2614
+ const LADDER_OWNED_CHECKS = /* @__PURE__ */ new Set(["API key present", "Clivly reachable"]);
2615
+ const sdkChecksStage = {
2616
+ id: "setup-checks",
2617
+ label: "Setup checks",
2618
+ needs: [configStage.id],
2619
+ async run(ctx) {
2620
+ const owned = (await ctx.sdk.doctor()).checks.filter((check) => !LADDER_OWNED_CHECKS.has(check.name));
2621
+ const counted = owned.filter((check) => !check.skipped);
2622
+ const passed = counted.filter((check) => check.ok).length;
2623
+ const ok = counted.every((check) => check.ok);
2624
+ const warned = counted.filter((check) => check.warn).length;
2625
+ const summary = warned > 0 ? `${passed} of ${counted.length} passed, ${warned} with warnings` : `${passed} of ${counted.length} passed`;
2626
+ return {
2627
+ state: ok ? "pass" : "fail",
2628
+ detail: summary,
2629
+ sub: owned.map((check) => ({
2630
+ label: check.name,
2631
+ ok: check.ok,
2632
+ skipped: check.skipped,
2633
+ warn: check.warn,
2634
+ detail: check.detail
2635
+ }))
2636
+ };
2637
+ }
2638
+ };
2639
+ const triggerUrlStage = {
2640
+ id: "trigger-url",
2641
+ label: "Sync trigger URL",
2642
+ needs: [configStage.id],
2643
+ run(ctx) {
2644
+ const url = ctx.sdk?.syncTrigger?.url;
2645
+ if (url) return Promise.resolve({
2646
+ state: "pass",
2647
+ detail: url
2648
+ });
2649
+ return Promise.resolve({
2650
+ state: "warn",
2651
+ detail: "Not set — remote sync cannot call back into this app.",
2652
+ hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. For local development run `clivly dev` — it opens a tunnel and registers the URL for you. Push-only setups can ignore this."
2653
+ });
2654
+ }
2655
+ };
2080
2656
  /** The full ladder, in the order a developer completes setup. */
2081
2657
  const STATUS_STAGES = [
2082
2658
  apiKeyStage,
2083
- heartbeatStage,
2659
+ reachabilityStage,
2084
2660
  configStage,
2085
2661
  {
2086
2662
  id: "runtime",
2087
2663
  label: "clivly.config is edge-safe",
2664
+ needs: [configStage.id],
2088
2665
  run(ctx) {
2089
2666
  const source = (ctx.readConfigSource ?? defaultReadConfigSource)(ctx.configPath);
2090
2667
  if (source === null) return Promise.resolve({
2091
2668
  state: "skip",
2092
2669
  detail: "config not readable"
2093
2670
  });
2094
- const issues = analyzeEdgeSafety(source);
2671
+ const read = ctx.readConfigSource ?? defaultReadConfigSource;
2672
+ const base = (0, node_path.dirname)(ctx.configPath);
2673
+ const issues = analyzeConfigTree({
2674
+ source,
2675
+ readModule: (specifier) => {
2676
+ const target = (0, node_path.resolve)(base, specifier);
2677
+ for (const candidate of MODULE_CANDIDATES) {
2678
+ const found = read(`${target}${candidate}`);
2679
+ if (found !== null) return found;
2680
+ }
2681
+ return null;
2682
+ }
2683
+ });
2095
2684
  if (issues.length === 0) return Promise.resolve({
2096
2685
  state: "pass",
2097
2686
  detail: "no Node-only code at module scope"
@@ -2108,43 +2697,8 @@ const STATUS_STAGES = [
2108
2697
  });
2109
2698
  }
2110
2699
  },
2111
- {
2112
- id: "setup-checks",
2113
- label: "Setup checks",
2114
- needs: [configStage.id],
2115
- async run(ctx) {
2116
- const owned = (await ctx.sdk.doctor()).checks.filter((check) => !LADDER_OWNED_CHECKS.has(check.name));
2117
- const counted = owned.filter((check) => !check.skipped);
2118
- const passed = counted.filter((check) => check.ok).length;
2119
- return {
2120
- state: counted.every((check) => check.ok) ? "pass" : "fail",
2121
- detail: `${passed} of ${counted.length} passed`,
2122
- sub: owned.map((check) => ({
2123
- label: check.name,
2124
- ok: check.ok,
2125
- skipped: check.skipped,
2126
- detail: check.detail
2127
- }))
2128
- };
2129
- }
2130
- },
2131
- {
2132
- id: "trigger-url",
2133
- label: "Sync trigger URL",
2134
- needs: [configStage.id],
2135
- run(ctx) {
2136
- const url = ctx.sdk?.syncTrigger?.url;
2137
- if (url) return Promise.resolve({
2138
- state: "pass",
2139
- detail: url
2140
- });
2141
- return Promise.resolve({
2142
- state: "warn",
2143
- detail: "Not set — remote sync cannot call back into this app.",
2144
- hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. For local development run `clivly dev` — it opens a tunnel and registers the URL for you. Push-only setups can ignore this."
2145
- });
2146
- }
2147
- }
2700
+ sdkChecksStage,
2701
+ triggerUrlStage
2148
2702
  ];
2149
2703
  function defaultLoadSdk(path) {
2150
2704
  return loadConfigSdk(path, "doctor");
@@ -2154,15 +2708,15 @@ function defaultLoadSdk(path) {
2154
2708
  /** Stage ids each legacy command maps onto. */
2155
2709
  const ALIAS_STAGE_IDS = {
2156
2710
  status: STATUS_STAGES.map((stage) => stage.id),
2157
- ping: ["api-key", "heartbeat"],
2711
+ ping: ["credential", "application"],
2158
2712
  doctor: [
2159
2713
  "config",
2160
2714
  "runtime",
2161
2715
  "setup-checks"
2162
2716
  ],
2163
2717
  connect: [
2164
- "api-key",
2165
- "heartbeat",
2718
+ "credential",
2719
+ "application",
2166
2720
  "config",
2167
2721
  "trigger-url"
2168
2722
  ]
@@ -2178,9 +2732,9 @@ const defaultConnect = (apiKey, apiUrl) => require_sdk.createClivlySDK({
2178
2732
  apiKey,
2179
2733
  apiUrl,
2180
2734
  retry: { maxAttempts: 1 }
2181
- }).connect();
2735
+ }).ping();
2182
2736
  function subMark(sub) {
2183
- if (sub.skipped) return "⚠️ ";
2737
+ if (sub.skipped || sub.warn) return "⚠️ ";
2184
2738
  return sub.ok ? "✅" : "❌";
2185
2739
  }
2186
2740
  function renderStage(result, verbose, out) {
@@ -2188,7 +2742,7 @@ function renderStage(result, verbose, out) {
2188
2742
  const detail = result.detail ? ` — ${result.detail}` : "";
2189
2743
  out(` ${MARK[result.state]} ${step} · ${result.label}${detail}\n`);
2190
2744
  for (const sub of result.sub ?? []) {
2191
- if (!(verbose || sub.skipped || !sub.ok)) continue;
2745
+ if (!(verbose || sub.skipped || sub.warn || !sub.ok)) continue;
2192
2746
  const subDetail = sub.detail ? ` — ${sub.detail}` : "";
2193
2747
  out(` ${subMark(sub)} ${sub.label}${subDetail}\n`);
2194
2748
  }
@@ -2214,6 +2768,12 @@ async function runStatus(options) {
2214
2768
  }
2215
2769
  //#endregion
2216
2770
  //#region src/index.ts
2771
+ const ENV_FILE_CANDIDATES = [
2772
+ ".env",
2773
+ ".env.local",
2774
+ ".dev.vars"
2775
+ ];
2776
+ const CONFIG_WATCH_DEBOUNCE_MS = 150;
2217
2777
  function flagValue(argv, flag) {
2218
2778
  const index = argv.indexOf(flag);
2219
2779
  return index >= 0 ? argv[index + 1] : void 0;
@@ -2232,7 +2792,10 @@ const KNOWN_COMMANDS = [
2232
2792
  "push-schema",
2233
2793
  "dev",
2234
2794
  "login",
2235
- "help"
2795
+ "help",
2796
+ "ping",
2797
+ "doctor",
2798
+ "connect"
2236
2799
  ];
2237
2800
  function editDistance(a, b) {
2238
2801
  const m = a.length;
@@ -2275,6 +2838,7 @@ function readDeps(cwd) {
2275
2838
  }
2276
2839
  const ADD_USAGE = "Usage: clivly add widget [--force] [--dry-run]";
2277
2840
  const defaultAddDeps = {
2841
+ fileExists: node_fs.existsSync,
2278
2842
  out: (line) => process.stdout.write(line),
2279
2843
  readFile: (path) => (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : null,
2280
2844
  writeFile: (path, contents) => {
@@ -2294,14 +2858,21 @@ function runAdd(argv, cwd, deps = defaultAddDeps) {
2294
2858
  deps.out("Couldn't detect your framework from package.json. `clivly add widget` supports Next.js, TanStack Start, SvelteKit, Remix, and Nuxt.\n");
2295
2859
  return 1;
2296
2860
  }
2861
+ const layout = resolveAppLayout(cwd, deps.fileExists);
2297
2862
  const force = argv.includes("--force");
2298
2863
  const dryRun = argv.includes("--dry-run");
2299
2864
  const files = planWidgetMutations({
2300
2865
  framework,
2301
2866
  cwd,
2302
2867
  force,
2868
+ layout,
2303
2869
  readFile: (path) => deps.readFile(path)
2304
2870
  });
2871
+ const shadowing = shadowedAppDirError(cwd, framework, files.map((file) => file.path), deps.fileExists);
2872
+ if (shadowing) {
2873
+ deps.out(shadowing);
2874
+ return 1;
2875
+ }
2305
2876
  deps.out(`\nClivly add widget — ${FRAMEWORK_LABELS[framework]}\n\n`);
2306
2877
  if (dryRun) deps.out("Dry run — nothing was written.\n\n");
2307
2878
  for (const file of files) {
@@ -2322,7 +2893,7 @@ function runAdd(argv, cwd, deps = defaultAddDeps) {
2322
2893
  }
2323
2894
  if (!dryRun) {
2324
2895
  const sessionPath = files[0]?.path ?? "";
2325
- deps.out(`\nNext steps:\n 1. Add your site origins to allowedOrigins in ${sessionPath}\n 2. Create the widget + set allowed origins in the Clivly dashboard (Settings → Chat widget)\n 3. Embed it: <ClivlyChatWidget widgetId="…" />\n`);
2896
+ deps.out(`\nNext steps:\n 1. Add your site origins to allowedOrigins in ${sessionPath}\n 2. Create the widget + set allowed origins at ${WIDGET_LOCATION}\n 3. Embed it: <ClivlyChatWidget widgetId="…" />\n`);
2326
2897
  }
2327
2898
  return 0;
2328
2899
  }
@@ -2333,6 +2904,13 @@ function readOwnVersion() {
2333
2904
  if (pkg.name === "clivly" && pkg.version) return pkg.version;
2334
2905
  } catch {}
2335
2906
  }
2907
+ function readOwnEngines() {
2908
+ const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
2909
+ for (const rel of ["./package.json", "../package.json"]) try {
2910
+ const pkg = req(rel);
2911
+ if (pkg.name === "clivly") return pkg.engines?.node;
2912
+ } catch {}
2913
+ }
2336
2914
  /**
2337
2915
  * Parse a `--db-import` / `--auth-import` value into a discovery-shaped override.
2338
2916
  *
@@ -2362,12 +2940,24 @@ function authInstanceFromFlag(flag) {
2362
2940
  function defaultReadFile(path) {
2363
2941
  return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : null;
2364
2942
  }
2943
+ function resolveEnvPath(cwd) {
2944
+ const framework = detectFramework(readDeps(cwd));
2945
+ return (0, node_path.join)(cwd, resolveEnvFile({
2946
+ existing: ENV_FILE_CANDIDATES.filter((name) => {
2947
+ const path = (0, node_path.join)(cwd, name);
2948
+ if (!(0, node_fs.existsSync)(path)) return false;
2949
+ const contents = (0, node_fs.readFileSync)(path, "utf8");
2950
+ return isKeySet(contents, "CLIVLY_SECRET_KEY") || isKeySet(contents, "CLIVLY_API_KEY");
2951
+ }),
2952
+ framework
2953
+ }));
2954
+ }
2365
2955
  function defaultReadEnv(cwd) {
2366
- const path = (0, node_path.join)(cwd, ".env");
2956
+ const path = resolveEnvPath(cwd);
2367
2957
  return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : "";
2368
2958
  }
2369
2959
  function defaultWriteEnv(cwd, contents) {
2370
- (0, node_fs.writeFileSync)((0, node_path.join)(cwd, ".env"), contents, "utf8");
2960
+ (0, node_fs.writeFileSync)(resolveEnvPath(cwd), contents, "utf8");
2371
2961
  }
2372
2962
  function defaultIsInteractive() {
2373
2963
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !process.env.CI;
@@ -2394,6 +2984,8 @@ function resolveInitDeps(options) {
2394
2984
  graph: graphFor(cwd)
2395
2985
  })),
2396
2986
  readFile: options.readFile ?? defaultReadFile,
2987
+ fileExists: options.fileExists ?? node_fs.existsSync,
2988
+ resolveAppLayout: options.resolveAppLayout ?? resolveAppLayout,
2397
2989
  writeConfig: options.writeConfig ?? ((path, contents) => {
2398
2990
  (0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
2399
2991
  (0, node_fs.writeFileSync)(path, contents, "utf8");
@@ -2414,6 +3006,7 @@ function resolveInitDeps(options) {
2414
3006
  rotate: false
2415
3007
  })),
2416
3008
  framework: "unknown",
3009
+ layout: "app",
2417
3010
  authWiring: renderAuthWiring("unknown")
2418
3011
  };
2419
3012
  }
@@ -2428,11 +3021,15 @@ async function acquireSchema(options, deps) {
2428
3021
  const orm = detectOrm(appDeps);
2429
3022
  const auth = detectAuth(appDeps);
2430
3023
  deps.framework = framework;
3024
+ const layout = deps.resolveAppLayout(options.cwd, deps.fileExists);
3025
+ deps.layout = layout;
2431
3026
  deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
2432
3027
  out("\nClivly init\n");
2433
3028
  out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
2434
3029
  out(` ORM: ${orm}\n`);
2435
- out(` Auth: ${auth}${deps.authWiring.complete ? " (wired)" : ""}\n\n`);
3030
+ out(` Auth: ${auth}${deps.authWiring.complete ? " (wired)" : ""}\n`);
3031
+ if (framework === "nextjs") out(` Layout: ${layout}\n`);
3032
+ out("\n");
2436
3033
  if (orm !== "drizzle") {
2437
3034
  out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
2438
3035
  return { code: 1 };
@@ -2476,6 +3073,7 @@ function mutationsFor(options, deps, plan) {
2476
3073
  cwd: options.cwd,
2477
3074
  force: options.force,
2478
3075
  framework: deps.framework,
3076
+ layout: deps.layout,
2479
3077
  authWiring: deps.authWiring,
2480
3078
  plan,
2481
3079
  readFile: deps.readFile,
@@ -2535,14 +3133,15 @@ function previewMutations(deps, mutations, options) {
2535
3133
  }
2536
3134
  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");
2537
3135
  }
2538
- function printNextSteps(out, authComplete, loginRan) {
3136
+ function printNextSteps(out, authComplete, loginRan, envFile) {
2539
3137
  out("\nNext steps:\n");
2540
- out(loginRan ? " 1. Credentials written by `clivly login`\n" : " 1. Add CLIVLY_SECRET_KEY to .env — create one in the Clivly dashboard\n under Integrations → Create API Key (or Settings → API keys)\n (the legacy CLIVLY_API_KEY is still accepted)\n");
3138
+ out(loginRan ? " 1. Credentials written by `clivly login`\n" : ` 1. Add CLIVLY_SECRET_KEY to ${envFile} — create one at\n ${CREDENTIALS_LOCATION}\n (the legacy CLIVLY_API_KEY is still accepted)\n`);
2541
3139
  out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
2542
3140
  out(" Local dev? Run `clivly dev` to open a tunnel, or skip this step until you deploy (remote sync is deploy-only locally).\n");
2543
3141
  const step = authComplete ? 3 : 4;
2544
3142
  if (!authComplete) out(" 3. Finish resolveUser in the auth/verify route\n");
2545
3143
  out(` ${step}. Run \`clivly status\` to see where you are\n`);
3144
+ out("\nAfter editing clivly.config.ts, restart your app — it loads the config\nat boot, so a running server keeps using the old one.\n");
2546
3145
  }
2547
3146
  function credentialsMissing(envContents) {
2548
3147
  return !(isKeySet(envContents, "CLIVLY_SECRET_KEY") || isKeySet(envContents, "CLIVLY_API_KEY"));
@@ -2563,6 +3162,11 @@ async function scaffold(options, deps, plan) {
2563
3162
  const pm = deps.detectPackageManager(options.cwd);
2564
3163
  const packages = [resolveRuntimeInstallSpec(deps), ...deps.authWiring.packages];
2565
3164
  const mutations = mutationsFor(options, deps, plan);
3165
+ const shadowing = shadowedAppDirError(options.cwd, deps.framework, mutations.files.map((file) => file.path), deps.fileExists);
3166
+ if (shadowing) {
3167
+ out(shadowing);
3168
+ return 1;
3169
+ }
2566
3170
  if (options.dryRun || options.diff) {
2567
3171
  out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
2568
3172
  previewMutations(deps, mutations, {
@@ -2587,7 +3191,7 @@ async function scaffold(options, deps, plan) {
2587
3191
  out(" Run `clivly login` from a terminal to write CLIVLY_SECRET_KEY and CLIVLY_SYNC_TRIGGER_SECRET.\n");
2588
3192
  } else loginRan = await deps.login(options.cwd) === 0;
2589
3193
  }
2590
- printNextSteps(out, deps.authWiring.complete, loginRan);
3194
+ printNextSteps(out, deps.authWiring.complete, loginRan, envFileFor(detectFramework(deps.readDepsFn(options.cwd))));
2591
3195
  return 0;
2592
3196
  }
2593
3197
  function normalizeAppRoot(options) {
@@ -2654,8 +3258,18 @@ function printNoContactGuidance(out, plan, options) {
2654
3258
  out(`\n Tables in ${plan.importPath}:\n`);
2655
3259
  for (const name of plan.tableNames) out(` ${name}\n`);
2656
3260
  const schemaArg = options.schemaFlag ? ` --schema ${options.schemaFlag}` : "";
2657
- out("\n Pick the one that holds your people and re-run:\n");
2658
- out(` clivly init${schemaArg} --contact-table ${plan.tableNames[0]}\n\n`);
3261
+ const best = plan.contactCandidates[0];
3262
+ if (best) {
3263
+ out(`\n Best guess: ${best.table} — ${best.reasons.join(", ")}\n`);
3264
+ const runnerUp = plan.contactCandidates.slice(1, 3).map((candidate) => candidate.table);
3265
+ if (runnerUp.length > 0) out(` Also possible: ${runnerUp.join(", ")}\n`);
3266
+ out("\n Confirm it and re-run:\n");
3267
+ out(` clivly init${schemaArg} --contact-table ${best.table}\n\n`);
3268
+ return;
3269
+ }
3270
+ out("\n Nothing here looks like a people table — no email, phone or name\n");
3271
+ out(" columns turned up. Pick the one that holds your people and re-run:\n");
3272
+ out(` clivly init${schemaArg} --contact-table ${plan.tableNames[0] ?? "<table>"}\n\n`);
2659
3273
  out(" Nothing was written — Clivly needs one entity before a config can load.\n\n");
2660
3274
  }
2661
3275
  function hasCloudflaredOnPath() {
@@ -2751,6 +3365,22 @@ function defaultDevDeps(options) {
2751
3365
  },
2752
3366
  out: (line) => {
2753
3367
  process.stdout.write(`${line}\n`);
3368
+ },
3369
+ watchConfig: (cwd, onChange) => {
3370
+ const target = resolveConfigPath(cwd);
3371
+ try {
3372
+ let timer = null;
3373
+ const watcher = (0, node_fs.watch)(target, () => {
3374
+ if (timer) clearTimeout(timer);
3375
+ timer = setTimeout(onChange, CONFIG_WATCH_DEBOUNCE_MS);
3376
+ });
3377
+ return () => {
3378
+ if (timer) clearTimeout(timer);
3379
+ watcher.close();
3380
+ };
3381
+ } catch {
3382
+ return null;
3383
+ }
2754
3384
  }
2755
3385
  };
2756
3386
  }
@@ -2829,6 +3459,21 @@ function runInitFromArgv(argv, cwd) {
2829
3459
  }
2830
3460
  async function main(argv) {
2831
3461
  const [command, ...rest] = argv;
3462
+ if (command === "-h" || command === "--help" || isHelpRequest(rest)) {
3463
+ process.stdout.write(helpFor(command));
3464
+ return 0;
3465
+ }
3466
+ if (command === "--version" || command === "-v") {
3467
+ process.stdout.write(`${readOwnVersion() ?? "unknown"}\n`);
3468
+ return 0;
3469
+ }
3470
+ if (!process.env.CLIVLY_SKIP_ENGINE_CHECK) {
3471
+ const engineProblem = engineError(process.version, readOwnEngines());
3472
+ if (engineProblem) {
3473
+ process.stderr.write(engineProblem);
3474
+ return 1;
3475
+ }
3476
+ }
2832
3477
  let cliEnvFiles = [];
2833
3478
  try {
2834
3479
  cliEnvFiles = (await loadCliConfig(process.cwd()))?.envFiles ?? [];
@@ -2847,14 +3492,16 @@ async function main(argv) {
2847
3492
  case "status":
2848
3493
  case "ping":
2849
3494
  case "doctor":
2850
- case "connect": return await runStatus({
2851
- cwd: process.cwd(),
2852
- apiKey: readSecretKeyEnv(process.env),
2853
- apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
2854
- configPath: flagValue(rest, "--config"),
2855
- only: ALIAS_STAGE_IDS[command],
2856
- verbose: rest.includes("--verbose")
2857
- });
3495
+ case "connect":
3496
+ if (command !== "status") process.stderr.write(`\`clivly ${command}\` is a deprecated alias that runs part of \`clivly status\`. Prefer \`clivly status\`.\n`);
3497
+ return await runStatus({
3498
+ cwd: process.cwd(),
3499
+ apiKey: readSecretKeyEnv(process.env),
3500
+ apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
3501
+ configPath: flagValue(rest, "--config"),
3502
+ only: ALIAS_STAGE_IDS[command],
3503
+ verbose: rest.includes("--verbose")
3504
+ });
2858
3505
  case "dev": {
2859
3506
  const apiKey = readSecretKeyEnv(process.env);
2860
3507
  const apiUrl = flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL ?? "https://api.clivly.com";
@@ -2877,14 +3524,11 @@ async function main(argv) {
2877
3524
  openBrowser: !rest.includes("--no-browser"),
2878
3525
  deps: defaultLoginDeps()
2879
3526
  });
2880
- case "--version":
2881
- case "-v":
2882
- process.stdout.write(`${readOwnVersion() ?? "unknown"}\n`);
2883
- return 0;
2884
3527
  case void 0:
3528
+ process.stdout.write(ROOT_HELP);
3529
+ return 0;
2885
3530
  case "help":
2886
- case "--help":
2887
- process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--dry-run] [--diff] [--yes] [--no-login] [--no-browser]\n [--schema <path|package>] [--db-import <specifier>] [--auth-import <specifier>]\n Scaffold Clivly (config, routes, .env) for your app\n clivly add widget [--force] [--dry-run] Scaffold the chat-session route\n clivly status [--verbose] [--config <path>] [--env-file <path>] Check your setup, step by step\n clivly push-schema [--config <path>] Report your schema to Clivly without deploying\n clivly dev [--port <n>] [--api-url <url>] [--route-path <path>] [-- <cmd>] Tunnel localhost for remote sync\n clivly login [--force] [--rotate] [--api-url <url>] [--no-browser] Pair this machine and write CLIVLY_SECRET_KEY / CLIVLY_SYNC_TRIGGER_SECRET to .env\n clivly --version Print the installed clivly version\n");
3531
+ process.stdout.write(helpFor(rest[0]));
2888
3532
  return 0;
2889
3533
  default: {
2890
3534
  process.stdout.write(`Unknown command: ${command}\n`);