clivly 0.4.1 → 0.7.1

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 (164) hide show
  1. package/README.md +89 -10
  2. package/{adapter-CXr12l4K.d.cts → adapter-C2eRbBfo.d.cts} +1 -2
  3. package/{adapter-CvNBD_3y.d.mts → adapter-D5LRa-NA.d.mts} +1 -2
  4. package/{auth-adapter-CdLODn2p.d.cts → auth-adapter-Bjgo8LhN.d.mts} +1 -2
  5. package/{auth-adapter-CRBW4elj.d.mts → auth-adapter-DWzgl5Yq.d.cts} +1 -2
  6. package/auth-better-auth.d.cts +1 -2
  7. package/auth-better-auth.d.mts +1 -2
  8. package/auth-clerk.d.cts +1 -2
  9. package/auth-clerk.d.mts +1 -2
  10. package/auth-workos.d.cts +1 -2
  11. package/auth-workos.d.mts +1 -2
  12. package/core-adapter.cjs +5 -0
  13. package/core-adapter.d.cts +1 -1
  14. package/core-adapter.d.mts +1 -1
  15. package/core-adapter.mjs +5 -0
  16. package/core-auth-adapter.cjs +6 -1
  17. package/core-auth-adapter.d.cts +1 -1
  18. package/core-auth-adapter.d.mts +1 -1
  19. package/core-auth-adapter.mjs +6 -1
  20. package/core-drizzle.cjs +6 -30
  21. package/core-drizzle.d.cts +1 -20
  22. package/core-drizzle.d.mts +1 -20
  23. package/core-drizzle.mjs +5 -29
  24. package/core-entity-config.cjs +4 -1
  25. package/core-entity-config.d.cts +1 -1
  26. package/core-entity-config.d.mts +1 -1
  27. package/core-entity-config.mjs +5 -1
  28. package/core-entity-heuristics.cjs +14 -453
  29. package/core-entity-heuristics.d.cts +1 -67
  30. package/core-entity-heuristics.d.mts +1 -67
  31. package/core-entity-heuristics.mjs +4 -442
  32. package/core-mapping-form.cjs +6 -1
  33. package/core-mapping-form.d.cts +1 -1
  34. package/core-mapping-form.d.mts +1 -1
  35. package/core-mapping-form.mjs +6 -1
  36. package/core-mapping-score.cjs +4 -1
  37. package/core-mapping-score.d.cts +1 -1
  38. package/core-mapping-score.d.mts +1 -1
  39. package/core-mapping-score.mjs +5 -1
  40. package/core-mappings-config.cjs +6 -1
  41. package/core-mappings-config.d.cts +1 -1
  42. package/core-mappings-config.d.mts +1 -1
  43. package/core-mappings-config.mjs +6 -1
  44. package/core-sync-engine.cjs +6 -1
  45. package/core-sync-engine.d.cts +1 -1
  46. package/core-sync-engine.d.mts +1 -1
  47. package/core-sync-engine.mjs +6 -1
  48. package/core-types.cjs +5 -0
  49. package/core-types.d.cts +1 -1
  50. package/core-types.d.mts +1 -1
  51. package/core-types.mjs +5 -0
  52. package/core-view-compiler.cjs +6 -1
  53. package/core-view-compiler.d.cts +1 -1
  54. package/core-view-compiler.d.mts +1 -1
  55. package/core-view-compiler.mjs +6 -1
  56. package/core.cjs +11 -6
  57. package/core.d.cts +9 -9
  58. package/core.d.mts +9 -9
  59. package/core.mjs +11 -6
  60. package/deprecate-subpath-B9ngvizt.cjs +30 -0
  61. package/deprecate-subpath-CmvZtCpE.mjs +25 -0
  62. package/drizzle-B89tLo4T.mjs +31 -0
  63. package/drizzle-CFOr9dU9.d.mts +20 -0
  64. package/drizzle-DiReZf6d.cjs +36 -0
  65. package/drizzle-DmeKSqeF.d.cts +20 -0
  66. package/drizzle.cjs +3 -1
  67. package/drizzle.d.cts +11 -3
  68. package/drizzle.d.mts +11 -3
  69. package/drizzle.mjs +3 -2
  70. package/{entity-config-DMuZpchz.cjs → entity-config-D87rV-tE.cjs} +194 -192
  71. package/{entity-config-D1DFyeUp.d.mts → entity-config-DoUCXtq5.d.cts} +48 -35
  72. package/{entity-config-GczjD3y8.d.cts → entity-config-DoUCXtq5.d.mts} +48 -35
  73. package/{entity-config-D3YpQB0z.mjs → entity-config-va2bkH5o.mjs} +194 -192
  74. package/index.cjs +910 -234
  75. package/index.d.cts +30 -2
  76. package/index.d.mts +30 -2
  77. package/index.mjs +900 -226
  78. package/internal-adapter.cjs +0 -0
  79. package/internal-adapter.d.cts +2 -0
  80. package/internal-adapter.d.mts +2 -0
  81. package/internal-adapter.mjs +1 -0
  82. package/internal-auth-adapter.cjs +3 -0
  83. package/internal-auth-adapter.d.cts +2 -0
  84. package/internal-auth-adapter.d.mts +2 -0
  85. package/internal-auth-adapter.mjs +2 -0
  86. package/internal-drizzle.cjs +3 -0
  87. package/internal-drizzle.d.cts +2 -0
  88. package/internal-drizzle.d.mts +2 -0
  89. package/internal-drizzle.mjs +2 -0
  90. package/internal-entity-config.cjs +13 -0
  91. package/internal-entity-config.d.cts +2 -0
  92. package/internal-entity-config.d.mts +2 -0
  93. package/internal-entity-config.mjs +2 -0
  94. package/internal-entity-heuristics.cjs +455 -0
  95. package/internal-entity-heuristics.d.cts +67 -0
  96. package/internal-entity-heuristics.d.mts +67 -0
  97. package/internal-entity-heuristics.mjs +444 -0
  98. package/internal-mapping-form.cjs +5 -0
  99. package/internal-mapping-form.d.cts +2 -0
  100. package/internal-mapping-form.d.mts +2 -0
  101. package/internal-mapping-form.mjs +2 -0
  102. package/internal-mapping-score.cjs +12 -0
  103. package/internal-mapping-score.d.cts +2 -0
  104. package/internal-mapping-score.d.mts +2 -0
  105. package/internal-mapping-score.mjs +2 -0
  106. package/internal-mappings-config.cjs +3 -0
  107. package/internal-mappings-config.d.cts +2 -0
  108. package/internal-mappings-config.d.mts +2 -0
  109. package/internal-mappings-config.mjs +2 -0
  110. package/internal-sync-engine.cjs +6 -0
  111. package/internal-sync-engine.d.cts +2 -0
  112. package/internal-sync-engine.d.mts +2 -0
  113. package/internal-sync-engine.mjs +2 -0
  114. package/internal-types.cjs +0 -0
  115. package/internal-types.d.cts +2 -0
  116. package/internal-types.d.mts +2 -0
  117. package/internal-types.mjs +1 -0
  118. package/internal-view-compiler.cjs +6 -0
  119. package/internal-view-compiler.d.cts +2 -0
  120. package/internal-view-compiler.d.mts +2 -0
  121. package/internal-view-compiler.mjs +2 -0
  122. package/internal.cjs +28 -0
  123. package/internal.d.cts +9 -0
  124. package/internal.d.mts +9 -0
  125. package/internal.mjs +7 -0
  126. package/{mapping-form-CUlFVfG8.d.cts → mapping-form-D93lhHEc.d.mts} +1 -2
  127. package/{mapping-form-kjSiJaCX.d.mts → mapping-form-lfyEE7EJ.d.cts} +1 -2
  128. package/{mappings-config-DYZ_uOOB.cjs → mappings-config-BsRADNRv.cjs} +1 -1
  129. package/{mappings-config-CNTZRD4g.mjs → mappings-config-Dbgxx3Nv.mjs} +1 -1
  130. package/{mappings-config-DjRLCb2Q.d.cts → mappings-config-DsUa7-1w.d.mts} +1 -2
  131. package/{mappings-config-GxiYsSBQ.d.mts → mappings-config-XmzB4dag.d.cts} +1 -2
  132. package/package.json +152 -4
  133. package/{sdk-DdzBxgMx.mjs → sdk-C6WANW8M.mjs} +173 -34
  134. package/{sdk-D1BHQDf5.cjs → sdk-iqOXeWIp.cjs} +173 -34
  135. package/sdk.cjs +3 -1
  136. package/sdk.d.cts +1 -41
  137. package/sdk.d.mts +1 -41
  138. package/sdk.mjs +3 -2
  139. package/{sync-engine-DIVMG9EV.d.cts → sync-engine-B7QIbXro.d.cts} +6 -5
  140. package/{sync-engine-BtzkzKmY.cjs → sync-engine-CzEbpQGv.cjs} +1 -1
  141. package/{sync-engine-DLx243iw.d.mts → sync-engine-DGk_AeHJ.d.mts} +6 -5
  142. package/{sync-engine-D9vghQab.mjs → sync-engine-Do5ru3WF.mjs} +1 -1
  143. package/types-CsL0rFYh.d.cts +155 -0
  144. package/types-CsL0rFYh.d.mts +155 -0
  145. package/{view-compiler-4HsRUTWH.d.cts → view-compiler-BWz0sGdm.d.cts} +1 -2
  146. package/{view-compiler-BpIDExjd.d.mts → view-compiler-Bn_EJJMO.d.mts} +1 -2
  147. package/vite.d.cts +1 -2
  148. package/vite.d.mts +1 -2
  149. package/namespace-probe-CA4Vj40g.d.mts +0 -331
  150. package/namespace-probe-CkHCj9tQ.d.cts +0 -331
  151. /package/{auth-adapter-CBOKDhlc.cjs → auth-adapter-CiEXcweZ.cjs} +0 -0
  152. /package/{auth-adapter-wqnnEjo3.mjs → auth-adapter-gfVDxf3d.mjs} +0 -0
  153. /package/{drizzle-meta-BVR1qiyu.cjs → drizzle-meta-Bb1JWYqf.cjs} +0 -0
  154. /package/{drizzle-meta-DpMKataU.mjs → drizzle-meta-D-5k3n0i.mjs} +0 -0
  155. /package/{mapping-form-DdmDebC3.cjs → mapping-form-BpfON_ME.cjs} +0 -0
  156. /package/{mapping-form-BHtPexzI.mjs → mapping-form-DigkuvFc.mjs} +0 -0
  157. /package/{mapping-score-CPPvwoZC.mjs → mapping-score-C68uAqGB.mjs} +0 -0
  158. /package/{mapping-score-i3p8LWDF.cjs → mapping-score-CaB3F4Uu.cjs} +0 -0
  159. /package/{mapping-score-A2lRnlpk.d.mts → mapping-score-D7Gzg7Ii.d.cts} +0 -0
  160. /package/{mapping-score-BcjC0ZQs.d.cts → mapping-score-D7Gzg7Ii.d.mts} +0 -0
  161. /package/{types-BpsKVccL.d.cts → types-B1oRXdNP.d.cts} +0 -0
  162. /package/{types-_qbJXMt_.d.mts → types-B1oRXdNP.d.mts} +0 -0
  163. /package/{view-compiler-BcgJINr7.cjs → view-compiler-BIwvm-o2.cjs} +0 -0
  164. /package/{view-compiler-DeZzx_jn.mjs → view-compiler-ChWr_Lml.mjs} +0 -0
package/index.mjs CHANGED
@@ -1,16 +1,113 @@
1
1
  #!/usr/bin/env node
2
- import { t as CANONICAL_FIELDS } from "./entity-config-D3YpQB0z.mjs";
3
- import "./mapping-score-CPPvwoZC.mjs";
4
- import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./core-entity-heuristics.mjs";
5
- import { t as createClivlySDK } from "./sdk-DdzBxgMx.mjs";
2
+ import { t as CANONICAL_FIELDS } from "./entity-config-va2bkH5o.mjs";
3
+ import "./mapping-score-C68uAqGB.mjs";
4
+ import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
5
+ import { t as createClivlySDK } from "./sdk-C6WANW8M.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { spawn } from "node:child_process";
8
8
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
9
9
  import { homedir, hostname } from "node:os";
10
- import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
10
+ import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
11
11
  import { createInterface } from "node:readline/promises";
12
- import { fileURLToPath, pathToFileURL } from "node:url";
13
12
  import { createJiti } from "jiti";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
+ //#region src/load-config.ts
15
+ const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
16
+ function resolveConfigPath(cwd, configPath) {
17
+ return resolve(cwd, configPath ?? "clivly.config.ts");
18
+ }
19
+ /**
20
+ * Load the config and return its default export, checking it carries the method
21
+ * the caller needs. The check is by capability rather than by type: the config
22
+ * is the developer's own SDK instance, and a clear "this isn't an SDK instance"
23
+ * beats a TypeError deep inside a command.
24
+ */
25
+ /**
26
+ * The first meaningful line of an error message. jiti reports a failed
27
+ * resolution with a multi-line "Require stack:" dump naming Clivly's own
28
+ * internal files — noise that tells the developer nothing about their app and
29
+ * buries the one line that does.
30
+ */
31
+ function firstLine(message) {
32
+ return message.split("\n")[0]?.trim() ?? "";
33
+ }
34
+ /**
35
+ * Describe why the config failed to load, in one line the developer can act on.
36
+ *
37
+ * A config that fails its own schema validation throws a zod error carrying
38
+ * structured `issues`, but whose `message` is a multi-line JSON dump — its
39
+ * first line is a bare "[", so `firstLine` alone reduced the entire diagnosis
40
+ * to a single bracket. Render the issues as `path: message` instead, which is
41
+ * what actually names the entity to fix. Anything else keeps the require-stack
42
+ * trimming `firstLine` exists for.
43
+ */
44
+ function describeLoadError(error) {
45
+ const issues = error.issues;
46
+ if (Array.isArray(issues) && issues.length > 0) return issues.map((raw) => {
47
+ const issue = raw;
48
+ const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
49
+ const message = typeof issue.message === "string" ? issue.message : "invalid value";
50
+ return path ? `${path}: ${message}` : message;
51
+ }).join("; ");
52
+ return firstLine(error?.message ?? String(error));
53
+ }
54
+ async function loadConfigSdk(path, requiredMethod) {
55
+ const jiti = createJiti(import.meta.url);
56
+ let mod;
57
+ try {
58
+ mod = await jiti.import(path);
59
+ } catch (error) {
60
+ throw new Error(describeLoadError(error));
61
+ }
62
+ const sdk = mod.default ?? mod;
63
+ if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
64
+ return sdk;
65
+ }
66
+ //#endregion
67
+ //#region src/cli-config.ts
68
+ const CLI_CONFIG_NAMES = [
69
+ "clivly.cli.ts",
70
+ "clivly.cli.js",
71
+ "clivly.cli.mjs"
72
+ ];
73
+ /** The app's CLI config, or null when it has none — which is the normal case. */
74
+ function resolveCliConfigPath(cwd) {
75
+ for (const name of CLI_CONFIG_NAMES) {
76
+ const path = join(cwd, name);
77
+ if (existsSync(path)) return path;
78
+ }
79
+ return null;
80
+ }
81
+ function readEnvFiles(config) {
82
+ const envFiles = config.envFiles;
83
+ if (envFiles === void 0) return;
84
+ if (!Array.isArray(envFiles)) throw new Error("clivly.cli.ts: envFiles must be an array of paths, e.g. [\"../server/.env\"]");
85
+ if (envFiles.some((entry) => typeof entry !== "string")) throw new Error("clivly.cli.ts: every envFiles entry must be a string");
86
+ return envFiles;
87
+ }
88
+ /**
89
+ * Load `clivly.cli.ts` if the app has one.
90
+ *
91
+ * A malformed config throws rather than being silently ignored: a developer who
92
+ * wrote `envFiles` and sees nothing happen has no way to tell the difference
93
+ * between "not supported" and "I typed it wrong", and the symptom they get
94
+ * instead is a missing API key several steps later.
95
+ */
96
+ async function loadCliConfig(cwd) {
97
+ const path = resolveCliConfigPath(cwd);
98
+ if (!path) return null;
99
+ let mod;
100
+ try {
101
+ mod = await createJiti(import.meta.url).import(path);
102
+ } catch (error) {
103
+ throw new Error(`clivly.cli.ts: ${describeLoadError(error)}`);
104
+ }
105
+ const config = mod?.default ?? mod;
106
+ if (typeof config !== "object" || config === null || Array.isArray(config)) throw new Error("clivly.cli.ts must export an object as its default export");
107
+ const envFiles = readEnvFiles(config);
108
+ return envFiles ? { envFiles } : {};
109
+ }
110
+ //#endregion
14
111
  //#region src/detect.ts
15
112
  const FRAMEWORK_MARKERS = [
16
113
  {
@@ -152,7 +249,7 @@ function detectDevCommand(scripts, explicit) {
152
249
  return null;
153
250
  }
154
251
  const OVERRIDE_FAILURE_CAUSES = {
155
- unauthorized: "CLIVLY_API_KEY is invalid for this workspace/backend.",
252
+ unauthorized: "CLIVLY_SECRET_KEY is invalid for this workspace/backend.",
156
253
  not_enabled: "The target backend does not have the feature-flagged trigger-url endpoint enabled (likely the wrong backend, or the flag is off).",
157
254
  unreachable: "Could not reach Clivly at the selected API base URL."
158
255
  };
@@ -200,7 +297,7 @@ async function runDev(options) {
200
297
  return 0;
201
298
  }
202
299
  if (!options.apiKey) {
203
- deps.out("CLIVLY_API_KEY is not set — cannot register the tunnel URL. Set it and re-run.");
300
+ deps.out("CLIVLY_SECRET_KEY is not set — cannot register the tunnel URL. Set it and re-run.");
204
301
  return 1;
205
302
  }
206
303
  const app = command ? deps.spawnApp(command) : null;
@@ -279,16 +376,275 @@ async function probeModule(opts) {
279
376
  }
280
377
  for (const [moduleKey, value] of Object.entries(mod)) if (opts.match(value)) return {
281
378
  moduleKey,
282
- importPath: toImportPath(candidate, opts.cwd)
379
+ importPath: toImportPath(candidate, opts.cwd, opts.graph)
283
380
  };
284
381
  }
285
382
  return null;
286
383
  }
287
384
  //#endregion
385
+ //#region src/workspace-graph.ts
386
+ const WINDOWS_SEP$2 = /\\/g;
387
+ const MODULE_EXT$1 = /\.(ts|tsx|js|jsx|mjs|cjs|cts|mts)$/;
388
+ const BRACE_GROUP = /\{([^{}]*)\}/;
389
+ const MAX_GLOB_DEPTH = 8;
390
+ const IGNORED_DIRS = /* @__PURE__ */ new Set([
391
+ "node_modules",
392
+ "dist",
393
+ "build",
394
+ "coverage"
395
+ ]);
396
+ function isDirectory$1(path) {
397
+ try {
398
+ return statSync(path).isDirectory();
399
+ } catch {
400
+ return false;
401
+ }
402
+ }
403
+ const CONVENTIONAL_ENTRIES = [
404
+ "src/index.ts",
405
+ "src/index.tsx",
406
+ "index.ts",
407
+ "index.tsx",
408
+ "src/index.js",
409
+ "index.js"
410
+ ];
411
+ function readPackageManifest(dir) {
412
+ const path = join(dir, "package.json");
413
+ if (!existsSync(path)) return null;
414
+ try {
415
+ return JSON.parse(readFileSync(path, "utf8"));
416
+ } catch {
417
+ return null;
418
+ }
419
+ }
420
+ function parentDir$1(dir) {
421
+ const parent = dirname(dir);
422
+ return parent === dir ? null : parent;
423
+ }
424
+ /**
425
+ * The workspace globs declared at `dir`, from either `package.json#workspaces`
426
+ * (npm, yarn, bun) or `pnpm-workspace.yaml` (pnpm). Negations are dropped —
427
+ * they only ever narrow the set, and treating one as a literal path would
428
+ * invent a package directory named `!something`.
429
+ */
430
+ function readWorkspaceGlobs(dir) {
431
+ const manifest = readPackageManifest(dir);
432
+ const declared = Array.isArray(manifest?.workspaces) ? manifest.workspaces : manifest?.workspaces?.packages;
433
+ if (Array.isArray(declared) && declared.length > 0) return declared.filter((glob) => !glob.startsWith("!"));
434
+ const pnpmPath = join(dir, "pnpm-workspace.yaml");
435
+ if (!existsSync(pnpmPath)) return [];
436
+ try {
437
+ return readFileSync(pnpmPath, "utf8").split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim().replace(/^['"]|['"]$/g, "")).filter((glob) => glob.length > 0 && !glob.startsWith("!"));
438
+ } catch {
439
+ return [];
440
+ }
441
+ }
442
+ /**
443
+ * `{apps,packages}/*` -> [`apps/*`, `packages/*`].
444
+ *
445
+ * Expands one group per pass and recurses, so nested and repeated groups all
446
+ * resolve. A manifest that declares a brace glob is a workspace like any other,
447
+ * and failing to expand it silently yields an empty graph — which downgrades
448
+ * every generated specifier to the relative form this module exists to avoid.
449
+ */
450
+ function expandBraces(pattern) {
451
+ const group = BRACE_GROUP.exec(pattern);
452
+ if (!group) return [pattern];
453
+ return (group[1] ?? "").split(",").flatMap((option) => expandBraces(pattern.replace(group[0], option.trim())));
454
+ }
455
+ function childDirectories(dir) {
456
+ try {
457
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !(IGNORED_DIRS.has(entry.name) || entry.name.startsWith("."))).map((entry) => join(dir, entry.name));
458
+ } catch {
459
+ return [];
460
+ }
461
+ }
462
+ /** `dir` and every directory beneath it, up to `depth` levels down. */
463
+ function withDescendants(dir, depth) {
464
+ if (depth <= 0) return [dir];
465
+ return [dir, ...childDirectories(dir).flatMap((child) => withDescendants(child, depth - 1))];
466
+ }
467
+ /** Resolve the remaining glob segments against the directories matched so far. */
468
+ function matchSegments(dirs, segments) {
469
+ const [segment, ...rest] = segments;
470
+ if (segment === void 0) return dirs;
471
+ let next;
472
+ if (segment === "**") next = dirs.flatMap((dir) => withDescendants(dir, MAX_GLOB_DEPTH));
473
+ else if (segment === "*") next = dirs.flatMap(childDirectories);
474
+ else next = dirs.map((dir) => join(dir, segment)).filter(isDirectory$1);
475
+ return matchSegments(next, rest);
476
+ }
477
+ /**
478
+ * Expand one workspace glob into the package directories it names.
479
+ *
480
+ * Handles exact paths, `*`, `**`, and brace groups — the shapes that actually
481
+ * appear in pnpm/npm/yarn/bun manifests. `node_modules` and dot-directories are
482
+ * never descended into: a dependency's own package.json is not a workspace
483
+ * member, and treating one as such would put a third-party name in the graph.
484
+ */
485
+ function expandWorkspaceGlob(root, pattern) {
486
+ return expandBraces(pattern).flatMap((expanded) => matchSegments([root], expanded.split("/").filter((segment) => segment.length > 0))).filter((dir) => existsSync(join(dir, "package.json")));
487
+ }
488
+ /**
489
+ * Walk up from `start` looking for the directory that declares workspace globs.
490
+ * Stops *before* `boundary` (the user's home directory by default) so a stray
491
+ * manifest above the project is never mistaken for a workspace root.
492
+ */
493
+ function findWorkspaceRoot(start, boundary = homedir()) {
494
+ const stop = resolve(boundary);
495
+ let current = resolve(start);
496
+ while (current !== stop) {
497
+ if (readWorkspaceGlobs(current).length > 0) return current;
498
+ const parent = parentDir$1(current);
499
+ if (!parent) return null;
500
+ current = parent;
501
+ }
502
+ return null;
503
+ }
504
+ /**
505
+ * Build the package graph containing `start`, or null when `start` is not inside
506
+ * a workspace (the single-package case, where relative imports are correct).
507
+ */
508
+ function loadWorkspaceGraph(start, boundary = homedir()) {
509
+ const root = findWorkspaceRoot(start, boundary);
510
+ if (!root) return null;
511
+ const seen = /* @__PURE__ */ new Set();
512
+ const packages = [];
513
+ for (const pattern of readWorkspaceGlobs(root)) for (const dir of expandWorkspaceGlob(root, pattern)) {
514
+ const absolute = resolve(dir);
515
+ if (seen.has(absolute)) continue;
516
+ seen.add(absolute);
517
+ const name = readPackageManifest(absolute)?.name;
518
+ if (typeof name === "string" && name.length > 0) packages.push({
519
+ dir: absolute,
520
+ name
521
+ });
522
+ }
523
+ return {
524
+ root,
525
+ packages
526
+ };
527
+ }
528
+ /**
529
+ * The workspace package that owns `absPath`.
530
+ *
531
+ * Compares against `dir + sep` so a sibling sharing a name prefix
532
+ * (`packages/database-legacy` vs `packages/database`) can't match, and returns
533
+ * the longest match so a nested package beats its parent.
534
+ */
535
+ function findPackageContaining(graph, absPath) {
536
+ if (!graph) return null;
537
+ const target = resolve(absPath);
538
+ let best = null;
539
+ for (const pkg of graph.packages) {
540
+ const withSep = pkg.dir.endsWith(sep) ? pkg.dir : `${pkg.dir}${sep}`;
541
+ if ((target === pkg.dir || target.startsWith(withSep)) && (!best || pkg.dir.length > best.dir.length)) best = pkg;
542
+ }
543
+ return best;
544
+ }
545
+ /** The first string buried in an `exports` value, following conditions in order. */
546
+ function firstExportString(value) {
547
+ if (typeof value === "string") return value;
548
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
549
+ for (const nested of Object.values(value)) {
550
+ const found = firstExportString(nested);
551
+ if (found) return found;
552
+ }
553
+ return null;
554
+ }
555
+ function isFile(path) {
556
+ try {
557
+ return statSync(path).isFile();
558
+ } catch {
559
+ return false;
560
+ }
561
+ }
562
+ /**
563
+ * The file a bare import of this package resolves to, or null if we can't tell.
564
+ *
565
+ * Null rather than a guess on purpose: an entry declared in the manifest but
566
+ * absent on disk (a `dist/` build that hasn't run) would otherwise make `init`
567
+ * generate an import of a file that does not exist.
568
+ */
569
+ function resolvePackageEntry(packageDir) {
570
+ const manifest = readPackageManifest(packageDir);
571
+ const declared = [
572
+ firstExportString((manifest?.exports)?.["."]),
573
+ manifest?.module,
574
+ manifest?.main
575
+ ].filter((entry) => typeof entry === "string");
576
+ for (const entry of [...declared, ...CONVENTIONAL_ENTRIES]) {
577
+ const candidate = resolve(packageDir, entry);
578
+ if (isFile(candidate)) return candidate;
579
+ }
580
+ return null;
581
+ }
582
+ /**
583
+ * Whether `./subpath` is something an importer outside the package may request.
584
+ *
585
+ * A package with no `exports` field uses legacy resolution, where any file is
586
+ * reachable. Once `exports` exists it is an allow-list: Node and TypeScript's
587
+ * `bundler`/`node16` resolution both reject anything it does not name, so
588
+ * emitting such a specifier scaffolds a config that cannot compile.
589
+ */
590
+ function exportsAllowSubpath(packageDir, subpath) {
591
+ const exports = readPackageManifest(packageDir)?.exports;
592
+ if (exports === void 0 || exports === null) return true;
593
+ if (typeof exports !== "object" || Array.isArray(exports)) return false;
594
+ const request = `./${subpath}`;
595
+ return Object.keys(exports).some((key) => {
596
+ if (!key.startsWith(".")) return false;
597
+ const star = key.indexOf("*");
598
+ if (star === -1) return key === request;
599
+ const prefix = key.slice(0, star);
600
+ const suffix = key.slice(star + 1);
601
+ return request.length >= prefix.length + suffix.length && request.startsWith(prefix) && request.endsWith(suffix);
602
+ });
603
+ }
604
+ /**
605
+ * The specifier that imports `absPath` from outside its package: the bare name
606
+ * when the file is the package entry, `name/subpath` when the package's
607
+ * `exports` permits that subpath, and **null** when it does not.
608
+ *
609
+ * Null is the honest answer rather than a specifier that looks right and fails
610
+ * to resolve. The common shape that hits it is a package exporting only `"."`
611
+ * while the Drizzle instance lives in `src/db.ts` — callers should fall back to
612
+ * a relative import, or ask the developer via `--db-import` / `--auth-import`.
613
+ */
614
+ function toPackageSpecifier(pkg, absPath) {
615
+ const target = resolve(absPath);
616
+ if (resolvePackageEntry(pkg.dir) === target) return pkg.name;
617
+ const subpath = relative(pkg.dir, target).replace(WINDOWS_SEP$2, "/").replace(MODULE_EXT$1, "");
618
+ if (subpath.length === 0) return pkg.name;
619
+ return exportsAllowSubpath(pkg.dir, subpath) ? `${pkg.name}/${subpath}` : null;
620
+ }
621
+ //#endregion
288
622
  //#region src/discover-schema.ts
289
623
  const WINDOWS_SEP$1 = /\\/g;
290
624
  const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
291
- function toImportPath(schemaPath, cwd) {
625
+ /**
626
+ * Turn an absolute module path into the specifier the generated config imports
627
+ * it by. Every import `init` emits — schema, db instance, auth instance — is
628
+ * minted here, so this is the one place the monorepo fix has to land.
629
+ *
630
+ * "/app/src/db/schema.ts" (cwd "/app") -> "./src/db/schema"
631
+ *
632
+ * With a workspace `graph`, a file owned by a *different* workspace package
633
+ * becomes a package specifier instead: a schema in `packages/database` imports
634
+ * as `@acme/database`, not `../../packages/database/src/index`. The relative
635
+ * form escapes the app's tsconfig rootDir and sidesteps the package boundary the
636
+ * repo already maintains, which is what made generated configs unusable in
637
+ * shared-package layouts.
638
+ *
639
+ * A file inside the app itself still gets a relative specifier — the app is a
640
+ * workspace package too, and importing it by its own name would be a cycle.
641
+ */
642
+ function toImportPath(schemaPath, cwd, graph) {
643
+ const owner = findPackageContaining(graph ?? null, schemaPath);
644
+ if (owner && resolve(owner.dir) !== resolve(cwd)) {
645
+ const specifier = toPackageSpecifier(owner, schemaPath);
646
+ if (specifier) return specifier;
647
+ }
292
648
  const rel = relative(cwd, schemaPath).replace(WINDOWS_SEP$1, "/").replace(MODULE_EXT, "");
293
649
  return rel.startsWith(".") ? rel : `./${rel}`;
294
650
  }
@@ -360,12 +716,18 @@ function siblingDbCandidates(schemaPath) {
360
716
  const dir = isDirectory(schemaPath) ? schemaPath : dirname(schemaPath);
361
717
  return SIBLING_DB_NAMES.map((name) => join(dir, name));
362
718
  }
719
+ function parentDbCandidates(schemaPath, cwd) {
720
+ const parent = dirname(isDirectory(schemaPath) ? schemaPath : dirname(schemaPath));
721
+ const relativeToRoot = relative(resolve(cwd), resolve(parent));
722
+ if (!(relativeToRoot === "" || !relativeToRoot.startsWith(".."))) return [];
723
+ return SIBLING_DB_NAMES.map((name) => join(parent, name));
724
+ }
363
725
  async function discoverSchema(opts) {
364
726
  const load = opts.load ?? defaultLoader$1(opts.cwd);
365
727
  const mod = await loadSchemaModule(opts.schemaPath, load);
366
728
  const drizzle = await load("drizzle-orm");
367
729
  const pgCore = await loadPgCoreRuntime(load);
368
- const importPath = toImportPath(opts.schemaPath, opts.cwd);
730
+ const importPath = toImportPath(opts.schemaPath, opts.cwd, opts.graph);
369
731
  const tables = [];
370
732
  let db = null;
371
733
  for (const [moduleKey, value] of Object.entries(mod)) {
@@ -387,6 +749,14 @@ async function discoverSchema(opts) {
387
749
  if (db === null) db = await probeModule({
388
750
  candidates: siblingDbCandidates(opts.schemaPath),
389
751
  cwd: opts.cwd,
752
+ graph: opts.graph,
753
+ load,
754
+ match: isDrizzleDb
755
+ });
756
+ if (db === null) db = await probeModule({
757
+ candidates: parentDbCandidates(opts.schemaPath, opts.cwd),
758
+ cwd: opts.cwd,
759
+ graph: opts.graph,
390
760
  load,
391
761
  match: isDrizzleDb
392
762
  });
@@ -539,7 +909,7 @@ function isEntrypoint(argv1, moduleUrl, realpath = realpathSync) {
539
909
  //#endregion
540
910
  //#region src/load-env.ts
541
911
  const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
542
- const NEWLINE = /\r?\n/;
912
+ const NEWLINE$1 = /\r?\n/;
543
913
  const ESCAPED_NEWLINE = /\\n/g;
544
914
  const ESCAPED_CARRIAGE = /\\r/g;
545
915
  const TRAILING_COMMENT = /\s#/;
@@ -551,7 +921,7 @@ const TRAILING_COMMENT = /\s#/;
551
921
  */
552
922
  function parseEnv(content) {
553
923
  const out = {};
554
- for (const rawLine of content.split(NEWLINE)) {
924
+ for (const rawLine of content.split(NEWLINE$1)) {
555
925
  const line = rawLine.trimStart();
556
926
  if (line === "" || line.startsWith("#")) continue;
557
927
  const match = LINE.exec(line);
@@ -576,15 +946,26 @@ function parseValue(raw) {
576
946
  /**
577
947
  * Load `.env` then `.env.local` from `cwd` into `env` (default `process.env`),
578
948
  * WITHOUT overriding anything already set. Precedence, strongest first:
579
- * real environment (shell) > `.env.local` > `.env`. This mirrors the framework
580
- * convention developers already expect (Next.js, Vite), so pointing
581
- * `CLIVLY_API_URL` at a staging host from the shell still wins over the file.
949
+ * real environment (shell) > explicit `options.files` > `.env.local` > `.env`.
950
+ * The conventional part mirrors what developers already expect (Next.js, Vite),
951
+ * so pointing `CLIVLY_API_URL` at a staging host from the shell still wins.
952
+ *
953
+ * Explicit files exist for split workspaces, where the app that holds
954
+ * `clivly.config.ts` is not the package that holds the credentials. Reading
955
+ * them here — in the CLI process — is what keeps that need out of
956
+ * `clivly.config.ts`, which the app imports and which therefore has to survive
957
+ * an edge runtime with no filesystem.
582
958
  */
583
- function loadEnv(cwd, env = process.env) {
959
+ function loadEnv(cwd, env = process.env, options = {}) {
584
960
  const files = [];
585
961
  const merged = {};
586
- for (const name of [".env", ".env.local"]) {
587
- const path = join(cwd, name);
962
+ const candidates = [
963
+ ".env",
964
+ ".env.local",
965
+ ...options.files ?? []
966
+ ];
967
+ for (const name of candidates) {
968
+ const path = resolve(cwd, name);
588
969
  if (!existsSync(path)) continue;
589
970
  files.push(name);
590
971
  try {
@@ -602,6 +983,15 @@ function loadEnv(cwd, env = process.env) {
602
983
  };
603
984
  }
604
985
  //#endregion
986
+ //#region src/secret-key-env.ts
987
+ const SECRET_KEY_ENV = "CLIVLY_SECRET_KEY";
988
+ const LEGACY_SECRET_KEY_ENV = "CLIVLY_API_KEY";
989
+ function readSecretKeyEnv(env) {
990
+ const next = env[SECRET_KEY_ENV];
991
+ if (next && next.length > 0) return next;
992
+ return env[LEGACY_SECRET_KEY_ENV];
993
+ }
994
+ //#endregion
605
995
  //#region src/login.ts
606
996
  const POLL_INTERVAL_MS = 2e3;
607
997
  const POLL_BACKOFF_MULTIPLIER = 1.5;
@@ -613,7 +1003,7 @@ const MASK_VISIBLE_CHARS = 4;
613
1003
  const MASK_PREFIX = "••••";
614
1004
  function maskSecret(value) {
615
1005
  if (value.length <= MASK_VISIBLE_CHARS) return MASK_PREFIX;
616
- return `${MASK_PREFIX}${value.slice(-MASK_VISIBLE_CHARS)}`;
1006
+ return `${MASK_PREFIX}${value.slice(-4)}`;
617
1007
  }
618
1008
  async function createPairingSession(apiUrl, deps, rotate) {
619
1009
  const res = await deps.fetchImpl(`${apiUrl}/clivly/cli/pair`, {
@@ -625,7 +1015,7 @@ async function createPairingSession(apiUrl, deps, rotate) {
625
1015
  })
626
1016
  });
627
1017
  if (res.status === 404) {
628
- deps.out("clivly login isn't available yet on this server — CLI pairing is not enabled. Ask whoever runs your Clivly backend to enable it, or set CLIVLY_API_KEY / CLIVLY_SYNC_TRIGGER_SECRET manually.");
1018
+ deps.out("clivly login isn't available yet on this server — CLI pairing is not enabled. Ask whoever runs your Clivly backend to enable it, or set CLIVLY_SECRET_KEY / CLIVLY_SYNC_TRIGGER_SECRET manually.");
629
1019
  return { exitCode: 1 };
630
1020
  }
631
1021
  if (!res.ok) {
@@ -698,9 +1088,9 @@ async function confirmWorkspace(deps, credentials) {
698
1088
  }
699
1089
  function credentialEntries(credentials) {
700
1090
  return [{
701
- key: "CLIVLY_API_KEY",
1091
+ key: SECRET_KEY_ENV,
702
1092
  value: credentials.apiKey,
703
- comment: "Org API key — Settings → API keys"
1093
+ comment: "Org secret key — Settings → API keys"
704
1094
  }, {
705
1095
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
706
1096
  value: credentials.triggerSecret,
@@ -714,7 +1104,7 @@ function writeCredentials(options, credentials) {
714
1104
  deps.writeEnv(options.cwd, result.contents);
715
1105
  deps.out("\nLogin successful.\n");
716
1106
  const valuesByKey = {
717
- CLIVLY_API_KEY: credentials.apiKey,
1107
+ [SECRET_KEY_ENV]: credentials.apiKey,
718
1108
  CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
719
1109
  };
720
1110
  for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
@@ -807,32 +1197,42 @@ function buildEntity(table, concept, todos) {
807
1197
  idField
808
1198
  };
809
1199
  }
810
- function placeholderContact(todos) {
811
- todos.push("No contact table detected. Filled in a placeholder — set `source`, `fields`, and the fromDrizzle table below, then run `clivly doctor`.");
812
- return {
813
- moduleKey: "contacts",
814
- tableName: "contacts",
815
- concept: "contact",
816
- fields: {
817
- name: "name",
818
- email: "email"
819
- },
820
- cursorField: "updatedAt",
821
- idField: null
822
- };
823
- }
824
1200
  function hasMappedFields(entity) {
825
1201
  return Object.keys(entity.fields).length > 0;
826
1202
  }
827
- function planInit(schema) {
1203
+ /** A table by its SQL name or its schema-module export name — either is what a developer would type. */
1204
+ function findTable(tables, name) {
1205
+ return tables.find((table) => table.tableName === name || table.moduleKey === name) ?? null;
1206
+ }
1207
+ /**
1208
+ * Which table serves each concept: an explicit override when given, else the
1209
+ * heuristic pick.
1210
+ *
1211
+ * Split out of `planInit` because resolving two independent overrides, each with
1212
+ * its own unknown-name path, is most of the branching — and keeps `planInit`
1213
+ * inside the cognitive-complexity budget.
1214
+ */
1215
+ function selectTables(tables, overrides, todos) {
1216
+ const heuristic = selectEntities(tables);
1217
+ const pick = (flag, fallback, label) => {
1218
+ if (!flag) return fallback;
1219
+ const found = findTable(tables, flag);
1220
+ if (!found) todos.push(`--${label}-table "${flag}" is not a table in this schema.`);
1221
+ return found;
1222
+ };
1223
+ const contact = pick(overrides.contactTable, heuristic.contact, "contact");
1224
+ const company = pick(overrides.companyTable, heuristic.company, "company");
1225
+ return {
1226
+ contact,
1227
+ company: company?.tableName === contact?.tableName ? null : company
1228
+ };
1229
+ }
1230
+ function planInit(schema, overrides = {}) {
828
1231
  const todos = [];
829
- const { contact: contactTable, company: companyTable } = selectEntities(schema.tables);
830
- let contact = contactTable ? buildEntity(contactTable, "contact", todos) : placeholderContact(todos);
831
- let contactDetected = contactTable !== null;
832
- if (!hasMappedFields(contact)) {
833
- contact = placeholderContact(todos);
834
- contactDetected = false;
835
- }
1232
+ const { contact: contactTable, company: companyTable } = selectTables(schema.tables, overrides, todos);
1233
+ const built = contactTable ? buildEntity(contactTable, "contact", todos) : null;
1234
+ const contact = built && hasMappedFields(built) ? built : null;
1235
+ if (built && !contact) todos.push(`"${built.tableName}" was chosen as the contact entity but none of its columns could be mapped to name/email.`);
836
1236
  let company = null;
837
1237
  if (companyTable) {
838
1238
  const candidate = buildEntity(companyTable, "company", todos);
@@ -841,7 +1241,7 @@ function planInit(schema) {
841
1241
  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.`);
842
1242
  } 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.`);
843
1243
  }
844
- if (contactTable) {
1244
+ if (contactTable && contact) {
845
1245
  const scored = scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
846
1246
  const top = scored[0];
847
1247
  const runnerUp = scored[1];
@@ -856,16 +1256,16 @@ function planInit(schema) {
856
1256
  db: schema.db,
857
1257
  contact,
858
1258
  company,
859
- todos,
860
- contactDetected
1259
+ tableNames: schema.tables.map((table) => table.tableName),
1260
+ todos
861
1261
  };
862
1262
  }
863
1263
  //#endregion
864
1264
  //#region src/render-config.ts
865
1265
  const CLIVLY_ENV_KEYS = [
866
1266
  {
867
- key: "CLIVLY_API_KEY",
868
- comment: "Org API key — Settings → API keys"
1267
+ key: "CLIVLY_SECRET_KEY",
1268
+ comment: "Org secret key — Settings → API keys"
869
1269
  },
870
1270
  {
871
1271
  key: "CLIVLY_SYNC_TRIGGER_URL",
@@ -874,10 +1274,6 @@ const CLIVLY_ENV_KEYS = [
874
1274
  {
875
1275
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
876
1276
  comment: "Signing secret — Integrations → Remote sync"
877
- },
878
- {
879
- key: "CLIVLY_VERIFY_TOKEN",
880
- comment: "Bearer token Clivly presents to /clivly/auth/verify"
881
1277
  }
882
1278
  ];
883
1279
  function renderFields(fields) {
@@ -899,10 +1295,6 @@ function renderSource(role, entity, dbExpr) {
899
1295
  const opts = entity.idField === null ? `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}" }` : `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}", idField: "${entity.idField}" }`;
900
1296
  return ` ${role}: fromDrizzle(${dbExpr}, schema.${entity.moduleKey}, ${opts})`;
901
1297
  }
902
- function fallbackDbImportPath(schemaImportPath) {
903
- const slash = schemaImportPath.lastIndexOf("/");
904
- return `${slash >= 0 ? schemaImportPath.slice(0, slash) : "."}/db`;
905
- }
906
1298
  function dbIsInSchemaModule(plan) {
907
1299
  return plan.db !== null && plan.db.importPath === plan.importPath;
908
1300
  }
@@ -912,13 +1304,12 @@ function renderDbExpr(plan) {
912
1304
  }
913
1305
  function renderImports(plan) {
914
1306
  const lines = [
915
- "import { createClivlySDK } from \"clivly/sdk\";",
1307
+ "import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
916
1308
  "import { fromDrizzle } from \"clivly/drizzle\";",
917
- "import { defineClivlyConfig } from \"clivly/core\";",
918
- "import { discoverFromDrizzle } from \"clivly/core/drizzle\";",
1309
+ "import { discoverFromDrizzle } from \"clivly/drizzle\";",
919
1310
  `import * as schema from "${plan.importPath}";`
920
1311
  ];
921
- if (!plan.db) lines.push(`import { db } from "${fallbackDbImportPath(plan.importPath)}"; // TODO: point at your Drizzle instance`);
1312
+ 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");
922
1313
  else if (!dbIsInSchemaModule(plan)) lines.push(`import { ${plan.db.moduleKey} } from "${plan.db.importPath}";`);
923
1314
  return lines.join("\n");
924
1315
  }
@@ -941,12 +1332,14 @@ ${entities.join(",\n")},
941
1332
  });
942
1333
 
943
1334
  export default createClivlySDK({
944
- apiKey: process.env.CLIVLY_API_KEY!,
1335
+ apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY!,
945
1336
  entities,
946
1337
  // Every table in your Drizzle schema, so the Clivly dashboard can offer them
947
1338
  // all when you add a mapping — not just the ones wired up as sources below.
948
1339
  schema: discoverFromDrizzle(schema),
949
1340
  source: {
1341
+ // Keyed by CRM concept — contacts/companies/deals/custom.
1342
+ // NOT by your entity key. The entity's own \`source:\` above is a table name.
950
1343
  ${sources.join(",\n")},
951
1344
  },
952
1345
  syncTrigger: {
@@ -954,7 +1347,8 @@ ${sources.join(",\n")},
954
1347
  url: process.env.CLIVLY_SYNC_TRIGGER_URL,
955
1348
  secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
956
1349
  },
957
- verifyToken: process.env.CLIVLY_VERIFY_TOKEN,
1350
+ // verifyToken is optional and only needed once a Clivly Cloud consumer calls
1351
+ // your /clivly/auth/verify endpoint; leave it unset otherwise.
958
1352
  });
959
1353
  `;
960
1354
  }
@@ -1040,23 +1434,28 @@ const TS_EXTENSION = /\.ts$/;
1040
1434
  const ROUTE_PATHS = {
1041
1435
  nextjs: {
1042
1436
  tick: "app/api/clivly/tick/route.ts",
1043
- verify: "app/api/clivly/auth/verify/route.ts"
1437
+ verify: "app/api/clivly/auth/verify/route.ts",
1438
+ session: "app/api/clivly/widget/session/route.ts"
1044
1439
  },
1045
1440
  "tanstack-start": {
1046
1441
  tick: "src/routes/api/clivly/tick.ts",
1047
- verify: "src/routes/api/clivly/auth/verify.ts"
1442
+ verify: "src/routes/api/clivly/auth/verify.ts",
1443
+ session: "src/routes/api/clivly/widget/session.ts"
1048
1444
  },
1049
1445
  sveltekit: {
1050
1446
  tick: "src/routes/api/clivly/tick/+server.ts",
1051
- verify: "src/routes/api/clivly/auth/verify/+server.ts"
1447
+ verify: "src/routes/api/clivly/auth/verify/+server.ts",
1448
+ session: "src/routes/api/clivly/widget/session/+server.ts"
1052
1449
  },
1053
1450
  remix: {
1054
1451
  tick: "app/routes/api.clivly.tick.ts",
1055
- verify: "app/routes/api.clivly.auth.verify.ts"
1452
+ verify: "app/routes/api.clivly.auth.verify.ts",
1453
+ session: "app/routes/api.clivly.widget.session.ts"
1056
1454
  },
1057
1455
  nuxt: {
1058
1456
  tick: "server/api/clivly/tick.ts",
1059
- verify: "server/api/clivly/auth/verify.ts"
1457
+ verify: "server/api/clivly/auth/verify.ts",
1458
+ session: "server/api/clivly/widget/session.ts"
1060
1459
  },
1061
1460
  unknown: null
1062
1461
  };
@@ -1086,14 +1485,27 @@ function renderAuthPreamble(routeFilePath, auth) {
1086
1485
  lines.push("", auth.body);
1087
1486
  return lines.join("\n");
1088
1487
  }
1089
- function renderBody(framework, importPath, verify, auth, routeFilePath) {
1090
- const preamble = verify ? `import clivly from "${importPath}";
1488
+ function renderPreamble(kind, importPath, auth, routeFilePath) {
1489
+ if (kind === "session") return `import { createChatSessionHandler } from "clivly/sdk";
1490
+
1491
+ const handler = createChatSessionHandler({
1492
+ apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY ?? "",
1493
+ allowedOrigins: [
1494
+ // TODO: add the origins your widget loads from, e.g. "https://example.com"
1495
+ ],
1496
+ });
1497
+ `;
1498
+ if (kind === "verify") return `import clivly from "${importPath}";
1091
1499
  ${renderAuthPreamble(routeFilePath, auth)}
1092
1500
  const handler = clivly.createAuthVerifyHandler(resolveUser);
1093
- ` : `import clivly from "${importPath}";
1501
+ `;
1502
+ return `import clivly from "${importPath}";
1094
1503
 
1095
1504
  const handler = clivly.createClivlyHandler();
1096
1505
  `;
1506
+ }
1507
+ function renderBody(framework, importPath, kind, auth, routeFilePath) {
1508
+ const preamble = renderPreamble(kind, importPath, auth, routeFilePath);
1097
1509
  switch (framework) {
1098
1510
  case "nextjs": return `${preamble}
1099
1511
  export const POST = (request: Request) => handler(request);
@@ -1125,7 +1537,7 @@ export default defineEventHandler((event) => handler(toWebRequest(event)));
1125
1537
  }
1126
1538
  function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown")) {
1127
1539
  const path = routePath(framework, kind);
1128
- let body = renderBody(framework, configImport(path), kind === "verify", auth, path);
1540
+ let body = renderBody(framework, configImport(path), kind, auth, path);
1129
1541
  if (framework === "tanstack-start") {
1130
1542
  const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
1131
1543
  body = body.replace("__ROUTE__", url);
@@ -1154,11 +1566,21 @@ function planScaffold(framework) {
1154
1566
  ...COMMON_FILES
1155
1567
  ];
1156
1568
  }
1569
+ function planWidgetScaffold(framework) {
1570
+ const paths = ROUTE_PATHS[framework];
1571
+ if (!paths) return [];
1572
+ return [{
1573
+ path: paths.session,
1574
+ description: "Clivly chat-session route (createChatSessionHandler) — add allowedOrigins",
1575
+ owned: true
1576
+ }];
1577
+ }
1157
1578
  //#endregion
1158
1579
  //#region src/plan-mutations.ts
1159
- function fileAction(present, force) {
1160
- if (!present) return "create";
1161
- return force ? "overwrite" : "skip";
1580
+ function fileAction(existing, next, force) {
1581
+ if (existing === null) return "create";
1582
+ if (existing === next) return "unchanged";
1583
+ return force ? "overwrite" : "conflict";
1162
1584
  }
1163
1585
  function hasKey(content, key) {
1164
1586
  return new RegExp(`^\\s*${key}=`, "m").test(content);
@@ -1170,11 +1592,14 @@ function planFiles(args) {
1170
1592
  const kind = path.includes("verify") ? "verify" : "tick";
1171
1593
  return renderRouteFile(args.framework, kind, args.authWiring);
1172
1594
  };
1173
- for (const file of planScaffold(args.framework)) mutations.push({
1174
- path: file.path,
1175
- contents: render(file.path),
1176
- action: fileAction(args.exists(join(args.cwd, file.path)), args.force)
1177
- });
1595
+ for (const file of planScaffold(args.framework)) {
1596
+ const contents = render(file.path);
1597
+ mutations.push({
1598
+ path: file.path,
1599
+ contents,
1600
+ action: fileAction(args.readFile(join(args.cwd, file.path)), contents, args.force)
1601
+ });
1602
+ }
1178
1603
  return mutations;
1179
1604
  }
1180
1605
  function envAction(addedCount, existingLength) {
@@ -1197,6 +1622,16 @@ function planMutations(args) {
1197
1622
  env: planEnv(args)
1198
1623
  };
1199
1624
  }
1625
+ function planWidgetMutations(args) {
1626
+ return planWidgetScaffold(args.framework).map((file) => {
1627
+ const contents = renderRouteFile(args.framework, "session");
1628
+ return {
1629
+ path: file.path,
1630
+ contents,
1631
+ action: fileAction(args.readFile(join(args.cwd, file.path)), contents, args.force)
1632
+ };
1633
+ });
1634
+ }
1200
1635
  //#endregion
1201
1636
  //#region src/probe-auth.ts
1202
1637
  const AUTH_MODULE_PATHS = [
@@ -1228,59 +1663,6 @@ function probeAuthInstance(cwd, load) {
1228
1663
  });
1229
1664
  }
1230
1665
  //#endregion
1231
- //#region src/load-config.ts
1232
- const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
1233
- function resolveConfigPath(cwd, configPath) {
1234
- return resolve(cwd, configPath ?? "clivly.config.ts");
1235
- }
1236
- /**
1237
- * Load the config and return its default export, checking it carries the method
1238
- * the caller needs. The check is by capability rather than by type: the config
1239
- * is the developer's own SDK instance, and a clear "this isn't an SDK instance"
1240
- * beats a TypeError deep inside a command.
1241
- */
1242
- /**
1243
- * The first meaningful line of an error message. jiti reports a failed
1244
- * resolution with a multi-line "Require stack:" dump naming Clivly's own
1245
- * internal files — noise that tells the developer nothing about their app and
1246
- * buries the one line that does.
1247
- */
1248
- function firstLine(message) {
1249
- return message.split("\n")[0]?.trim() ?? "";
1250
- }
1251
- /**
1252
- * Describe why the config failed to load, in one line the developer can act on.
1253
- *
1254
- * A config that fails its own schema validation throws a zod error carrying
1255
- * structured `issues`, but whose `message` is a multi-line JSON dump — its
1256
- * first line is a bare "[", so `firstLine` alone reduced the entire diagnosis
1257
- * to a single bracket. Render the issues as `path: message` instead, which is
1258
- * what actually names the entity to fix. Anything else keeps the require-stack
1259
- * trimming `firstLine` exists for.
1260
- */
1261
- function describeLoadError(error) {
1262
- const issues = error.issues;
1263
- if (Array.isArray(issues) && issues.length > 0) return issues.map((raw) => {
1264
- const issue = raw;
1265
- const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
1266
- const message = typeof issue.message === "string" ? issue.message : "invalid value";
1267
- return path ? `${path}: ${message}` : message;
1268
- }).join("; ");
1269
- return firstLine(error?.message ?? String(error));
1270
- }
1271
- async function loadConfigSdk(path, requiredMethod) {
1272
- const jiti = createJiti(import.meta.url);
1273
- let mod;
1274
- try {
1275
- mod = await jiti.import(path);
1276
- } catch (error) {
1277
- throw new Error(describeLoadError(error));
1278
- }
1279
- const sdk = mod.default ?? mod;
1280
- if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
1281
- return sdk;
1282
- }
1283
- //#endregion
1284
1666
  //#region src/push-schema.ts
1285
1667
  async function runPushSchema(options) {
1286
1668
  const out = options.out ?? ((line) => process.stdout.write(line));
@@ -1349,11 +1731,40 @@ function defaultReadDrizzleConfigSchema(cwd) {
1349
1731
  }
1350
1732
  return null;
1351
1733
  }
1734
+ /**
1735
+ * Resolve a `--schema` value that names a workspace package rather than a file.
1736
+ *
1737
+ * Matches the longest package name that prefixes the specifier, so both
1738
+ * `@acme/database` and `@acme/database/src/schema.ts` work. Returns `undefined`
1739
+ * when nothing matched (caller falls back to path handling) and `null` when a
1740
+ * package matched but its entry could not be resolved — those are different
1741
+ * outcomes and the caller reports them differently.
1742
+ */
1743
+ function resolveFromWorkspace(specifier, graph, deps) {
1744
+ const pkg = graph.packages.filter((pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)).sort((a, b) => b.name.length - a.name.length)[0];
1745
+ if (!pkg) return;
1746
+ if (specifier === pkg.name) return deps.resolveEntry(pkg.dir);
1747
+ const subpath = specifier.slice(pkg.name.length + 1);
1748
+ const candidate = resolve(pkg.dir, subpath);
1749
+ return deps.exists(candidate) ? candidate : deps.resolveEntry(pkg.dir);
1750
+ }
1352
1751
  function resolveSchemaPath(opts) {
1353
1752
  const exists = opts.deps?.exists ?? existsSync;
1354
1753
  const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
1754
+ const resolveEntry = opts.deps?.resolvePackageEntry ?? resolvePackageEntry;
1355
1755
  const toAbs = (p) => isAbsolute(p) ? p : resolve(opts.cwd, p);
1356
- if (opts.flag) return toAbs(opts.flag);
1756
+ if (opts.flag) {
1757
+ const asPath = toAbs(opts.flag);
1758
+ if (exists(asPath)) return asPath;
1759
+ if (opts.graph) {
1760
+ const fromWorkspace = resolveFromWorkspace(opts.flag, opts.graph, {
1761
+ exists,
1762
+ resolveEntry
1763
+ });
1764
+ if (fromWorkspace !== void 0) return fromWorkspace;
1765
+ }
1766
+ return asPath;
1767
+ }
1357
1768
  const fromConfig = readSchema(opts.cwd);
1358
1769
  if (fromConfig) return toAbs(fromConfig);
1359
1770
  for (const candidate of COMMON_PATHS) {
@@ -1364,17 +1775,8 @@ function resolveSchemaPath(opts) {
1364
1775
  }
1365
1776
  //#endregion
1366
1777
  //#region src/resolve-target-app.ts
1367
- function readPackageJson(dir) {
1368
- const path = join(dir, "package.json");
1369
- if (!existsSync(path)) return null;
1370
- try {
1371
- return JSON.parse(readFileSync(path, "utf8"));
1372
- } catch {
1373
- return null;
1374
- }
1375
- }
1376
1778
  function packageDeps(dir) {
1377
- const pkg = readPackageJson(dir);
1779
+ const pkg = readPackageManifest(dir);
1378
1780
  return {
1379
1781
  ...pkg?.dependencies,
1380
1782
  ...pkg?.devDependencies
@@ -1398,41 +1800,6 @@ function findNearestPackageDir(start, boundary) {
1398
1800
  current = parent;
1399
1801
  }
1400
1802
  }
1401
- function readWorkspaceGlobs(dir) {
1402
- const pkg = readPackageJson(dir);
1403
- const workspaces = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
1404
- if (Array.isArray(workspaces) && workspaces.length > 0) return workspaces;
1405
- const pnpmWorkspacePath = join(dir, "pnpm-workspace.yaml");
1406
- if (!existsSync(pnpmWorkspacePath)) return [];
1407
- try {
1408
- return readFileSync(pnpmWorkspacePath, "utf8").split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim().replace(/^['"]|['"]$/g, "")).filter((glob) => glob.length > 0 && !glob.startsWith("!"));
1409
- } catch {
1410
- return [];
1411
- }
1412
- }
1413
- function expandWorkspaceGlob(root, pattern) {
1414
- if (pattern.endsWith("/*")) {
1415
- const base = join(root, pattern.slice(0, -2));
1416
- if (!existsSync(base)) return [];
1417
- try {
1418
- return readdirSync(base, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(base, entry.name)).filter((dir) => existsSync(join(dir, "package.json")));
1419
- } catch {
1420
- return [];
1421
- }
1422
- }
1423
- const exact = join(root, pattern);
1424
- return existsSync(join(exact, "package.json")) ? [exact] : [];
1425
- }
1426
- function findWorkspaceRoot(start, boundary) {
1427
- let current = resolve(start);
1428
- while (true) {
1429
- if (current === boundary) return null;
1430
- if (readWorkspaceGlobs(current).length > 0) return current;
1431
- const parent = parentDir(current);
1432
- if (!parent) return null;
1433
- current = parent;
1434
- }
1435
- }
1436
1803
  function uniquePaths(paths) {
1437
1804
  return [...new Set(paths.map((path) => resolve(path)))];
1438
1805
  }
@@ -1488,7 +1855,7 @@ function resolveTargetApp(start, boundary = homedir()) {
1488
1855
  }
1489
1856
  //#endregion
1490
1857
  //#region src/status-ladder.ts
1491
- const SATISFYING = new Set(["pass", "warn"]);
1858
+ const SATISFYING = /* @__PURE__ */ new Set(["pass", "warn"]);
1492
1859
  function unmetPrerequisites(stage, satisfied) {
1493
1860
  return (stage.needs ?? []).filter((id) => !satisfied.has(id));
1494
1861
  }
@@ -1538,10 +1905,118 @@ function ladderExitCode(results) {
1538
1905
  return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
1539
1906
  }
1540
1907
  //#endregion
1908
+ //#region src/edge-safety.ts
1909
+ const NODE_BUILTINS = /* @__PURE__ */ new Set([
1910
+ "assert",
1911
+ "buffer",
1912
+ "child_process",
1913
+ "cluster",
1914
+ "crypto",
1915
+ "dns",
1916
+ "events",
1917
+ "fs",
1918
+ "http",
1919
+ "http2",
1920
+ "https",
1921
+ "module",
1922
+ "net",
1923
+ "os",
1924
+ "path",
1925
+ "perf_hooks",
1926
+ "process",
1927
+ "readline",
1928
+ "stream",
1929
+ "timers",
1930
+ "tls",
1931
+ "url",
1932
+ "util",
1933
+ "v8",
1934
+ "vm",
1935
+ "worker_threads",
1936
+ "zlib"
1937
+ ]);
1938
+ const SIDE_EFFECT_MODULES = [/^dotenv\/config$/, /^dotenv\/lib\/env-options$/];
1939
+ const IMPORT_FROM = /(?:^|\s)(?:import|export)\b[^"']*from\s*["']([^"']+)["']/;
1940
+ const BARE_IMPORT = /^\s*import\s*["']([^"']+)["']\s*;?\s*$/;
1941
+ const REQUIRE_CALL = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/;
1942
+ const NEWLINE = /\r?\n/;
1943
+ const MODULE_SCOPE_CALL = /^([A-Za-z_$][\w$.]*)\s*\(/;
1944
+ const ALLOWED_CALLS = /* @__PURE__ */ new Set([
1945
+ "export",
1946
+ "return",
1947
+ "if",
1948
+ "for",
1949
+ "while"
1950
+ ]);
1951
+ const SIDE_EFFECT_CALLS = /^(dotenv\.config|config|configDotenv|fileURLToPath|readFileSync|require|process\.chdir)$/;
1952
+ 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.";
1953
+ function isNodeBuiltin(specifier) {
1954
+ if (specifier.startsWith("node:")) return true;
1955
+ return NODE_BUILTINS.has(specifier.split("/")[0] ?? "");
1956
+ }
1957
+ function stripComment(line) {
1958
+ const trimmed = line.trimStart();
1959
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) return "";
1960
+ return line;
1961
+ }
1962
+ function specifierIssue(specifier, line) {
1963
+ if (SIDE_EFFECT_MODULES.some((pattern) => pattern.test(specifier))) return {
1964
+ code: "module_scope_side_effect",
1965
+ detail: `imports "${specifier}" for its side effect`,
1966
+ hint: ENV_HINT,
1967
+ line
1968
+ };
1969
+ if (isNodeBuiltin(specifier)) return {
1970
+ code: "node_builtin_in_edge_config",
1971
+ detail: `imports "${specifier}", which does not exist on an edge runtime`,
1972
+ hint: ENV_HINT,
1973
+ line
1974
+ };
1975
+ return null;
1976
+ }
1977
+ function callIssue(source, line) {
1978
+ const name = MODULE_SCOPE_CALL.exec(source)?.[1];
1979
+ if (!name || ALLOWED_CALLS.has(name) || !SIDE_EFFECT_CALLS.test(name)) return null;
1980
+ return {
1981
+ code: "module_scope_side_effect",
1982
+ detail: `calls ${name}() at module scope`,
1983
+ hint: ENV_HINT,
1984
+ line
1985
+ };
1986
+ }
1987
+ /**
1988
+ * Every reason `source` would fail to import on an edge runtime, in line order.
1989
+ * An empty array means the config is safe to import from the app.
1990
+ */
1991
+ function analyzeEdgeSafety(source) {
1992
+ const issues = [];
1993
+ source.split(NEWLINE).forEach((rawLine, index) => {
1994
+ const line = stripComment(rawLine);
1995
+ if (line.trim() === "") return;
1996
+ const lineNumber = index + 1;
1997
+ const specifier = BARE_IMPORT.exec(line)?.[1] ?? IMPORT_FROM.exec(line)?.[1] ?? REQUIRE_CALL.exec(line)?.[1];
1998
+ if (specifier) {
1999
+ const issue = specifierIssue(specifier, lineNumber);
2000
+ if (issue) issues.push(issue);
2001
+ return;
2002
+ }
2003
+ const call = callIssue(line, lineNumber);
2004
+ if (call) issues.push(call);
2005
+ });
2006
+ return issues;
2007
+ }
2008
+ //#endregion
1541
2009
  //#region src/status-stages.ts
2010
+ function defaultReadConfigSource(path) {
2011
+ try {
2012
+ return readFileSync(path, "utf8");
2013
+ } catch {
2014
+ return null;
2015
+ }
2016
+ }
1542
2017
  const REASON_HINT = {
1543
2018
  ok: "",
1544
- invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
2019
+ invalid_api_key: "The key was rejected (401). Check CLIVLY_SECRET_KEY (or legacy CLIVLY_API_KEY).",
1545
2020
  forbidden: "The key is valid but lacks access to this org (403).",
1546
2021
  rate_limited: "Rate limited (429). Wait a moment and retry.",
1547
2022
  server_error: "Clivly returned a server error (5xx). Retry shortly.",
@@ -1554,12 +2029,12 @@ const apiKeyStage = {
1554
2029
  run(ctx) {
1555
2030
  if (ctx.apiKey) return Promise.resolve({
1556
2031
  state: "pass",
1557
- detail: "CLIVLY_API_KEY is set."
2032
+ detail: "Secret key is set."
1558
2033
  });
1559
2034
  return Promise.resolve({
1560
2035
  state: "fail",
1561
- detail: "CLIVLY_API_KEY is not set.",
1562
- hint: "Copy your API key from Settings → API keys into .env."
2036
+ detail: "Secret key is not set.",
2037
+ hint: "Copy your secret key from Settings → API keys into CLIVLY_SECRET_KEY in .env."
1563
2038
  });
1564
2039
  }
1565
2040
  };
@@ -1600,12 +2075,38 @@ const configStage = {
1600
2075
  }
1601
2076
  }
1602
2077
  };
1603
- const LADDER_OWNED_CHECKS = new Set(["API key present", "Heartbeat"]);
2078
+ const LADDER_OWNED_CHECKS = /* @__PURE__ */ new Set(["API key present", "Heartbeat"]);
1604
2079
  /** The full ladder, in the order a developer completes setup. */
1605
2080
  const STATUS_STAGES = [
1606
2081
  apiKeyStage,
1607
2082
  heartbeatStage,
1608
2083
  configStage,
2084
+ {
2085
+ id: "runtime",
2086
+ label: "clivly.config is edge-safe",
2087
+ run(ctx) {
2088
+ const source = (ctx.readConfigSource ?? defaultReadConfigSource)(ctx.configPath);
2089
+ if (source === null) return Promise.resolve({
2090
+ state: "skip",
2091
+ detail: "config not readable"
2092
+ });
2093
+ const issues = analyzeEdgeSafety(source);
2094
+ if (issues.length === 0) return Promise.resolve({
2095
+ state: "pass",
2096
+ detail: "no Node-only code at module scope"
2097
+ });
2098
+ return Promise.resolve({
2099
+ state: "fail",
2100
+ detail: issues.map((issue) => issue.detail).join("; "),
2101
+ hint: issues[0]?.hint,
2102
+ sub: issues.map((issue) => ({
2103
+ label: `${issue.code} (line ${issue.line})`,
2104
+ ok: false,
2105
+ detail: issue.detail
2106
+ }))
2107
+ });
2108
+ }
2109
+ },
1609
2110
  {
1610
2111
  id: "setup-checks",
1611
2112
  label: "Setup checks",
@@ -1639,7 +2140,7 @@ const STATUS_STAGES = [
1639
2140
  return Promise.resolve({
1640
2141
  state: "warn",
1641
2142
  detail: "Not set — remote sync cannot call back into this app.",
1642
- hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. Push-only setups can ignore this."
2143
+ 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."
1643
2144
  });
1644
2145
  }
1645
2146
  }
@@ -1653,7 +2154,11 @@ function defaultLoadSdk(path) {
1653
2154
  const ALIAS_STAGE_IDS = {
1654
2155
  status: STATUS_STAGES.map((stage) => stage.id),
1655
2156
  ping: ["api-key", "heartbeat"],
1656
- doctor: ["config", "setup-checks"],
2157
+ doctor: [
2158
+ "config",
2159
+ "runtime",
2160
+ "setup-checks"
2161
+ ],
1657
2162
  connect: [
1658
2163
  "api-key",
1659
2164
  "heartbeat",
@@ -1712,6 +2217,14 @@ function flagValue(argv, flag) {
1712
2217
  const index = argv.indexOf(flag);
1713
2218
  return index >= 0 ? argv[index + 1] : void 0;
1714
2219
  }
2220
+ function flagValues(argv, flag) {
2221
+ const values = [];
2222
+ for (const [index, entry] of argv.entries()) {
2223
+ const value = entry === flag ? argv[index + 1] : void 0;
2224
+ if (value !== void 0 && !value.startsWith("--")) values.push(value);
2225
+ }
2226
+ return values;
2227
+ }
1715
2228
  const KNOWN_COMMANDS = [
1716
2229
  "init",
1717
2230
  "status",
@@ -1723,17 +2236,17 @@ const KNOWN_COMMANDS = [
1723
2236
  function editDistance(a, b) {
1724
2237
  const m = a.length;
1725
2238
  const n = b.length;
1726
- const prev = Array.from({ length: n + 1 }, (_, i) => i);
1727
- const curr = Array.from({ length: n + 1 }, () => 0);
2239
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
2240
+ let curr = Array.from({ length: n + 1 }, () => 0);
1728
2241
  for (let i = 1; i <= m; i++) {
1729
2242
  curr[0] = i;
1730
2243
  for (let j = 1; j <= n; j++) {
1731
2244
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1732
- curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
2245
+ curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
1733
2246
  }
1734
- for (let j = 0; j <= n; j++) prev[j] = curr[j];
2247
+ [prev, curr] = [curr, prev];
1735
2248
  }
1736
- return curr[n];
2249
+ return prev[n] ?? 0;
1737
2250
  }
1738
2251
  function nearestCommand(input) {
1739
2252
  let best;
@@ -1759,6 +2272,59 @@ function readDeps(cwd) {
1759
2272
  return {};
1760
2273
  }
1761
2274
  }
2275
+ const ADD_USAGE = "Usage: clivly add widget [--force] [--dry-run]";
2276
+ const defaultAddDeps = {
2277
+ out: (line) => process.stdout.write(line),
2278
+ readFile: (path) => existsSync(path) ? readFileSync(path, "utf8") : null,
2279
+ writeFile: (path, contents) => {
2280
+ mkdirSync(dirname(path), { recursive: true });
2281
+ writeFileSync(path, contents, "utf8");
2282
+ },
2283
+ readDeps
2284
+ };
2285
+ function runAdd(argv, cwd, deps = defaultAddDeps) {
2286
+ const target = argv[0];
2287
+ if (target !== "widget") {
2288
+ deps.out(`${target ? `Unknown target "${target}". ` : "Missing target. "}Only \`widget\` is supported.\n${ADD_USAGE}\n`);
2289
+ return 1;
2290
+ }
2291
+ const framework = detectFramework(deps.readDeps(cwd));
2292
+ if (framework === "unknown") {
2293
+ deps.out("Couldn't detect your framework from package.json. `clivly add widget` supports Next.js, TanStack Start, SvelteKit, Remix, and Nuxt.\n");
2294
+ return 1;
2295
+ }
2296
+ const force = argv.includes("--force");
2297
+ const dryRun = argv.includes("--dry-run");
2298
+ const files = planWidgetMutations({
2299
+ framework,
2300
+ cwd,
2301
+ force,
2302
+ readFile: (path) => deps.readFile(path)
2303
+ });
2304
+ deps.out(`\nClivly add widget — ${FRAMEWORK_LABELS[framework]}\n\n`);
2305
+ if (dryRun) deps.out("Dry run — nothing was written.\n\n");
2306
+ for (const file of files) {
2307
+ if (dryRun) {
2308
+ deps.out(` ${file.action.padEnd(9)} ${file.path}\n`);
2309
+ continue;
2310
+ }
2311
+ if (file.action === "conflict") {
2312
+ deps.out(` ! ${file.path} differs from what Clivly would write — left alone (--force to overwrite)\n`);
2313
+ continue;
2314
+ }
2315
+ if (file.action === "unchanged") {
2316
+ deps.out(` unchanged ${file.path} (already up to date)\n`);
2317
+ continue;
2318
+ }
2319
+ deps.writeFile(join(cwd, file.path), file.contents);
2320
+ deps.out(` ${file.action.padEnd(9)} ${file.path}\n`);
2321
+ }
2322
+ if (!dryRun) {
2323
+ const sessionPath = files[0]?.path ?? "";
2324
+ 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`);
2325
+ }
2326
+ return 0;
2327
+ }
1762
2328
  function readOwnVersion() {
1763
2329
  const req = createRequire(import.meta.url);
1764
2330
  for (const rel of ["./package.json", "../package.json"]) try {
@@ -1766,6 +2332,35 @@ function readOwnVersion() {
1766
2332
  if (pkg.name === "clivly" && pkg.version) return pkg.version;
1767
2333
  } catch {}
1768
2334
  }
2335
+ /**
2336
+ * Parse a `--db-import` / `--auth-import` value into a discovery-shaped override.
2337
+ *
2338
+ * `@acme/database` -> importPath "@acme/database", moduleKey <fallback>
2339
+ * `@acme/database#database` -> importPath "@acme/database", moduleKey "database"
2340
+ *
2341
+ * The optional `#exportName` suffix exists because a specifier alone doesn't say
2342
+ * which export holds the instance. Guessing wrong emits an import of a name that
2343
+ * isn't there, so the flag lets the developer say it outright while keeping the
2344
+ * conventional name as the default.
2345
+ */
2346
+ function importOverride(flag, fallbackModuleKey) {
2347
+ const trimmed = flag?.trim();
2348
+ if (!trimmed) return null;
2349
+ const separator = trimmed.lastIndexOf("#");
2350
+ const importPath = separator === -1 ? trimmed : trimmed.slice(0, separator);
2351
+ const moduleKey = separator === -1 ? "" : trimmed.slice(separator + 1);
2352
+ if (importPath.length === 0) return null;
2353
+ return {
2354
+ importPath,
2355
+ moduleKey: moduleKey || fallbackModuleKey
2356
+ };
2357
+ }
2358
+ function authInstanceFromFlag(flag) {
2359
+ return importOverride(flag, "auth");
2360
+ }
2361
+ function defaultReadFile(path) {
2362
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
2363
+ }
1769
2364
  function defaultReadEnv(cwd) {
1770
2365
  const path = join(cwd, ".env");
1771
2366
  return existsSync(path) ? readFileSync(path, "utf8") : "";
@@ -1777,18 +2372,27 @@ function defaultIsInteractive() {
1777
2372
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !process.env.CI;
1778
2373
  }
1779
2374
  function resolveInitDeps(options) {
2375
+ const loadGraph = options.loadWorkspaceGraph ?? loadWorkspaceGraph;
2376
+ const graphCache = /* @__PURE__ */ new Map();
2377
+ const graphFor = (cwd) => {
2378
+ if (!graphCache.has(cwd)) graphCache.set(cwd, loadGraph(cwd));
2379
+ return graphCache.get(cwd) ?? null;
2380
+ };
1780
2381
  return {
1781
2382
  out: options.out ?? ((line) => process.stdout.write(line)),
1782
2383
  readDepsFn: options.readDeps ?? readDeps,
2384
+ graphFor,
1783
2385
  resolvePath: options.resolveSchemaPath ?? ((cwd) => resolveSchemaPath({
1784
2386
  cwd,
1785
- flag: options.schemaFlag
2387
+ flag: options.schemaFlag,
2388
+ graph: graphFor(cwd)
1786
2389
  })),
1787
2390
  discover: options.discoverSchema ?? ((schemaPath, cwd) => discoverSchema({
1788
2391
  schemaPath,
1789
- cwd
2392
+ cwd,
2393
+ graph: graphFor(cwd)
1790
2394
  })),
1791
- configExists: options.configExists ?? existsSync,
2395
+ readFile: options.readFile ?? defaultReadFile,
1792
2396
  writeConfig: options.writeConfig ?? ((path, contents) => {
1793
2397
  mkdirSync(dirname(path), { recursive: true });
1794
2398
  writeFileSync(path, contents, "utf8");
@@ -1799,7 +2403,7 @@ function resolveInitDeps(options) {
1799
2403
  readEnv: options.readEnv ?? defaultReadEnv,
1800
2404
  readOwnVersion: options.readOwnVersion ?? readOwnVersion,
1801
2405
  writeEnv: options.writeEnv ?? defaultWriteEnv,
1802
- isInteractive: options.isInteractive ?? defaultIsInteractive,
2406
+ isInteractive: options.yes ? () => false : options.isInteractive ?? defaultIsInteractive,
1803
2407
  login: options.login ?? ((cwd) => runLogin({
1804
2408
  apiUrl: process.env.CLIVLY_API_URL ?? "https://api.clivly.com",
1805
2409
  cwd,
@@ -1823,7 +2427,7 @@ async function acquireSchema(options, deps) {
1823
2427
  const orm = detectOrm(appDeps);
1824
2428
  const auth = detectAuth(appDeps);
1825
2429
  deps.framework = framework;
1826
- deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? await deps.probeAuth(options.cwd) : null);
2430
+ deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
1827
2431
  out("\nClivly init\n");
1828
2432
  out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
1829
2433
  out(` ORM: ${orm}\n`);
@@ -1873,14 +2477,18 @@ function mutationsFor(options, deps, plan) {
1873
2477
  framework: deps.framework,
1874
2478
  authWiring: deps.authWiring,
1875
2479
  plan,
1876
- exists: deps.configExists,
2480
+ readFile: deps.readFile,
1877
2481
  readEnv: deps.readEnv
1878
2482
  });
1879
2483
  }
1880
2484
  function applyMutations(options, deps, mutations) {
1881
2485
  for (const file of mutations.files) {
1882
- if (file.action === "skip") {
1883
- deps.out(` • ${file.path} exists — skipped (pass --force to overwrite)\n`);
2486
+ if (file.action === "unchanged") {
2487
+ deps.out(` · ${file.path} already up to date\n`);
2488
+ continue;
2489
+ }
2490
+ if (file.action === "conflict") {
2491
+ deps.out(` ! ${file.path} differs from what init would write — left alone (--diff to see, --force to overwrite)\n`);
1884
2492
  continue;
1885
2493
  }
1886
2494
  deps.writeConfig(join(options.cwd, file.path), file.contents);
@@ -1893,29 +2501,42 @@ function applyMutations(options, deps, mutations) {
1893
2501
  deps.writeEnv(options.cwd, mutations.env.contents);
1894
2502
  deps.out(" ✓ .env placeholders\n");
1895
2503
  }
1896
- function previewMutations(deps, mutations) {
2504
+ const DIFF_CONTEXT_LIMIT = 40;
2505
+ function renderDiff(existing, next) {
2506
+ const before = existing.split("\n");
2507
+ const after = next.split("\n");
2508
+ const lines = [];
2509
+ for (const line of before.filter((l) => !after.includes(l))) lines.push(` - ${line}`);
2510
+ for (const line of after.filter((l) => !before.includes(l))) lines.push(` + ${line}`);
2511
+ return lines.length > DIFF_CONTEXT_LIMIT ? [...lines.slice(0, DIFF_CONTEXT_LIMIT), ` … ${lines.length - DIFF_CONTEXT_LIMIT} more lines`] : lines;
2512
+ }
2513
+ function previewMutations(deps, mutations, options) {
1897
2514
  const { out } = deps;
1898
2515
  out("\nDry run — nothing was written.\n\n");
1899
2516
  for (const file of mutations.files) {
1900
- const verb = {
1901
- create: "create",
1902
- overwrite: "overwrite",
1903
- skip: "skip"
1904
- }[file.action];
1905
2517
  const lines = file.contents.split("\n").length;
1906
- const suffix = file.action === "skip" ? " (exists — pass --force to overwrite)" : ` (${lines} lines)`;
1907
- out(` ${verb.padEnd(9)} ${file.path}${suffix}\n`);
2518
+ const suffix = {
2519
+ conflict: " (differs — --diff to see, --force to overwrite)",
2520
+ create: ` (${lines} lines)`,
2521
+ overwrite: ` (${lines} lines)`,
2522
+ unchanged: " (already up to date)"
2523
+ }[file.action];
2524
+ out(` ${file.action.padEnd(9)} ${file.path}${suffix}\n`);
2525
+ if (options.diff && file.action === "conflict") {
2526
+ const existing = deps.readFile(join(options.cwd, file.path)) ?? "";
2527
+ for (const line of renderDiff(existing, file.contents)) out(`${line}\n`);
2528
+ }
1908
2529
  }
1909
2530
  if (mutations.env.addedKeys.length === 0) out(" skip .env (every CLIVLY_* key already present)\n");
1910
2531
  else {
1911
2532
  out(` ${mutations.env.action.padEnd(9)} .env\n`);
1912
2533
  for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
1913
2534
  }
1914
- out("\nRe-run without --dry-run to apply.\n");
2535
+ 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");
1915
2536
  }
1916
2537
  function printNextSteps(out, authComplete, loginRan) {
1917
2538
  out("\nNext steps:\n");
1918
- 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");
2539
+ 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");
1919
2540
  out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
1920
2541
  out(" Local dev? Run `clivly dev` to open a tunnel, or skip this step until you deploy (remote sync is deploy-only locally).\n");
1921
2542
  const step = authComplete ? 3 : 4;
@@ -1923,7 +2544,7 @@ function printNextSteps(out, authComplete, loginRan) {
1923
2544
  out(` ${step}. Run \`clivly status\` to see where you are\n`);
1924
2545
  }
1925
2546
  function credentialsMissing(envContents) {
1926
- return !isKeySet(envContents, "CLIVLY_API_KEY");
2547
+ return !(isKeySet(envContents, "CLIVLY_SECRET_KEY") || isKeySet(envContents, "CLIVLY_API_KEY"));
1927
2548
  }
1928
2549
  function pairingSkipReason(options, deps) {
1929
2550
  if (options.noLogin) return "--no-login";
@@ -1941,9 +2562,12 @@ async function scaffold(options, deps, plan) {
1941
2562
  const pm = deps.detectPackageManager(options.cwd);
1942
2563
  const packages = [resolveRuntimeInstallSpec(deps), ...deps.authWiring.packages];
1943
2564
  const mutations = mutationsFor(options, deps, plan);
1944
- if (options.dryRun) {
2565
+ if (options.dryRun || options.diff) {
1945
2566
  out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
1946
- previewMutations(deps, mutations);
2567
+ previewMutations(deps, mutations, {
2568
+ cwd: options.cwd,
2569
+ diff: options.diff
2570
+ });
1947
2571
  return 0;
1948
2572
  }
1949
2573
  const result = await deps.install({
@@ -1959,11 +2583,11 @@ async function scaffold(options, deps, plan) {
1959
2583
  const skipReason = pairingSkipReason(options, deps);
1960
2584
  if (skipReason) {
1961
2585
  out(`\nSkipped browser pairing (${skipReason}).\n`);
1962
- out(" Run `clivly login` from a terminal to write CLIVLY_API_KEY and CLIVLY_SYNC_TRIGGER_SECRET.\n");
2586
+ out(" Run `clivly login` from a terminal to write CLIVLY_SECRET_KEY and CLIVLY_SYNC_TRIGGER_SECRET.\n");
1963
2587
  } else loginRan = await deps.login(options.cwd) === 0;
1964
2588
  }
1965
2589
  printNextSteps(out, deps.authWiring.complete, loginRan);
1966
- return plan.contactDetected ? 0 : 1;
2590
+ return 0;
1967
2591
  }
1968
2592
  function normalizeAppRoot(options) {
1969
2593
  const resolution = (options.resolveTargetApp ?? resolveTargetApp)(options.cwd);
@@ -1993,9 +2617,45 @@ async function runInit(options) {
1993
2617
  const deps = resolveInitDeps(resolvedOptions);
1994
2618
  const acquired = await acquireSchema(resolvedOptions, deps);
1995
2619
  if ("code" in acquired) return acquired.code;
1996
- const plan = planInit(acquired);
1997
- printPlan(deps.out, plan);
1998
- return scaffold(resolvedOptions, deps, plan);
2620
+ const dbOverride = importOverride(resolvedOptions.dbImportFlag, "db");
2621
+ const plan = planInit(dbOverride ? {
2622
+ ...acquired,
2623
+ db: dbOverride
2624
+ } : acquired, {
2625
+ contactTable: resolvedOptions.contactTableFlag,
2626
+ companyTable: resolvedOptions.companyTableFlag
2627
+ });
2628
+ const { contact } = plan;
2629
+ if (!contact) {
2630
+ printNoContactGuidance(deps.out, plan, resolvedOptions);
2631
+ return resolvedOptions.dryRun ? 0 : 1;
2632
+ }
2633
+ const renderable = {
2634
+ ...plan,
2635
+ contact
2636
+ };
2637
+ printPlan(deps.out, renderable);
2638
+ return scaffold(resolvedOptions, deps, renderable);
2639
+ }
2640
+ /**
2641
+ * What to do when no contact entity could be mapped. Lists the tables actually
2642
+ * discovered and the flag that picks one, so the next command succeeds rather
2643
+ * than leaving the developer to guess.
2644
+ */
2645
+ function printNoContactGuidance(out, plan, options) {
2646
+ out("No usable contact entity\n\n");
2647
+ for (const todo of plan.todos) out(` • ${todo}\n`);
2648
+ if (plan.tableNames.length === 0) {
2649
+ out(`\n No tables were found in ${plan.importPath}.\n`);
2650
+ out(" Check that --schema points at a module exporting Drizzle tables.\n\n");
2651
+ return;
2652
+ }
2653
+ out(`\n Tables in ${plan.importPath}:\n`);
2654
+ for (const name of plan.tableNames) out(` ${name}\n`);
2655
+ const schemaArg = options.schemaFlag ? ` --schema ${options.schemaFlag}` : "";
2656
+ out("\n Pick the one that holds your people and re-run:\n");
2657
+ out(` clivly init${schemaArg} --contact-table ${plan.tableNames[0]}\n\n`);
2658
+ out(" Nothing was written — Clivly needs one entity before a config can load.\n\n");
1999
2659
  }
2000
2660
  function hasCloudflaredOnPath() {
2001
2661
  const pathEnv = process.env.PATH ?? "";
@@ -2155,16 +2815,30 @@ function runInitFromArgv(argv, cwd) {
2155
2815
  cwd,
2156
2816
  force: argv.includes("--force"),
2157
2817
  dryRun: argv.includes("--dry-run"),
2818
+ diff: argv.includes("--diff"),
2819
+ yes: argv.includes("--yes"),
2158
2820
  noLogin: argv.includes("--no-login"),
2159
2821
  openBrowser: !argv.includes("--no-browser"),
2160
- schemaFlag: flagValue(argv, "--schema")
2822
+ schemaFlag: flagValue(argv, "--schema"),
2823
+ dbImportFlag: flagValue(argv, "--db-import"),
2824
+ authImportFlag: flagValue(argv, "--auth-import"),
2825
+ contactTableFlag: flagValue(argv, "--contact-table"),
2826
+ companyTableFlag: flagValue(argv, "--company-table")
2161
2827
  });
2162
2828
  }
2163
2829
  async function main(argv) {
2164
2830
  const [command, ...rest] = argv;
2165
- loadEnv(process.cwd());
2831
+ let cliEnvFiles = [];
2832
+ try {
2833
+ cliEnvFiles = (await loadCliConfig(process.cwd()))?.envFiles ?? [];
2834
+ } catch (error) {
2835
+ process.stderr.write(`${error.message}\n`);
2836
+ return 1;
2837
+ }
2838
+ loadEnv(process.cwd(), process.env, { files: [...cliEnvFiles, ...flagValues(rest, "--env-file")] });
2166
2839
  switch (command) {
2167
2840
  case "init": return await runInitFromArgv(rest, process.cwd());
2841
+ case "add": return runAdd(rest, process.cwd());
2168
2842
  case "push-schema": return await runPushSchema({
2169
2843
  cwd: process.cwd(),
2170
2844
  configPath: flagValue(rest, "--config")
@@ -2174,14 +2848,14 @@ async function main(argv) {
2174
2848
  case "doctor":
2175
2849
  case "connect": return await runStatus({
2176
2850
  cwd: process.cwd(),
2177
- apiKey: process.env.CLIVLY_API_KEY,
2851
+ apiKey: readSecretKeyEnv(process.env),
2178
2852
  apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
2179
2853
  configPath: flagValue(rest, "--config"),
2180
2854
  only: ALIAS_STAGE_IDS[command],
2181
2855
  verbose: rest.includes("--verbose")
2182
2856
  });
2183
2857
  case "dev": {
2184
- const apiKey = process.env.CLIVLY_API_KEY;
2858
+ const apiKey = readSecretKeyEnv(process.env);
2185
2859
  const apiUrl = flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL ?? "https://api.clivly.com";
2186
2860
  return await runDev({
2187
2861
  cwd: process.cwd(),
@@ -2209,7 +2883,7 @@ async function main(argv) {
2209
2883
  case void 0:
2210
2884
  case "help":
2211
2885
  case "--help":
2212
- process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--dry-run] [--schema <path>] [--no-login] [--no-browser] Scaffold Clivly (config, routes, .env) for your app\n clivly status [--verbose] [--config <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_API_KEY / CLIVLY_SYNC_TRIGGER_SECRET to .env\n clivly --version Print the installed clivly version\n");
2886
+ 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");
2213
2887
  return 0;
2214
2888
  default: {
2215
2889
  process.stdout.write(`Unknown command: ${command}\n`);
@@ -2224,4 +2898,4 @@ if (isEntrypoint(process.argv[1], import.meta.url)) main(process.argv.slice(2)).
2224
2898
  process.exitCode = code;
2225
2899
  });
2226
2900
  //#endregion
2227
- export { main, nearestCommand, runInit, runInitFromArgv };
2901
+ export { credentialsMissing, main, nearestCommand, runAdd, runInit, runInitFromArgv };