clivly 0.4.1 → 0.7.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 (164) hide show
  1. package/README.md +83 -3
  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 +5 -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 +15 -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 +5 -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 +896 -228
  75. package/index.d.cts +30 -2
  76. package/index.d.mts +30 -2
  77. package/index.mjs +886 -220
  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 +150 -2
  133. package/{sdk-DdzBxgMx.mjs → sdk-C_1sGiH8.mjs} +15 -11
  134. package/{sdk-D1BHQDf5.cjs → sdk-CzOf596t.cjs} +15 -11
  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,116 @@
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 "./core-entity-config.mjs";
4
+ import "./mapping-score-C68uAqGB.mjs";
5
+ import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
6
+ import "./core-entity-heuristics.mjs";
7
+ import "./core-mapping-score.mjs";
8
+ import { t as createClivlySDK } from "./sdk-C_1sGiH8.mjs";
6
9
  import { createRequire } from "node:module";
7
10
  import { spawn } from "node:child_process";
8
11
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
9
12
  import { homedir, hostname } from "node:os";
10
- import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
13
+ import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
11
14
  import { createInterface } from "node:readline/promises";
12
- import { fileURLToPath, pathToFileURL } from "node:url";
13
15
  import { createJiti } from "jiti";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
+ //#region src/load-config.ts
18
+ const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
19
+ function resolveConfigPath(cwd, configPath) {
20
+ return resolve(cwd, configPath ?? "clivly.config.ts");
21
+ }
22
+ /**
23
+ * Load the config and return its default export, checking it carries the method
24
+ * the caller needs. The check is by capability rather than by type: the config
25
+ * is the developer's own SDK instance, and a clear "this isn't an SDK instance"
26
+ * beats a TypeError deep inside a command.
27
+ */
28
+ /**
29
+ * The first meaningful line of an error message. jiti reports a failed
30
+ * resolution with a multi-line "Require stack:" dump naming Clivly's own
31
+ * internal files — noise that tells the developer nothing about their app and
32
+ * buries the one line that does.
33
+ */
34
+ function firstLine(message) {
35
+ return message.split("\n")[0]?.trim() ?? "";
36
+ }
37
+ /**
38
+ * Describe why the config failed to load, in one line the developer can act on.
39
+ *
40
+ * A config that fails its own schema validation throws a zod error carrying
41
+ * structured `issues`, but whose `message` is a multi-line JSON dump — its
42
+ * first line is a bare "[", so `firstLine` alone reduced the entire diagnosis
43
+ * to a single bracket. Render the issues as `path: message` instead, which is
44
+ * what actually names the entity to fix. Anything else keeps the require-stack
45
+ * trimming `firstLine` exists for.
46
+ */
47
+ function describeLoadError(error) {
48
+ const issues = error.issues;
49
+ if (Array.isArray(issues) && issues.length > 0) return issues.map((raw) => {
50
+ const issue = raw;
51
+ const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
52
+ const message = typeof issue.message === "string" ? issue.message : "invalid value";
53
+ return path ? `${path}: ${message}` : message;
54
+ }).join("; ");
55
+ return firstLine(error?.message ?? String(error));
56
+ }
57
+ async function loadConfigSdk(path, requiredMethod) {
58
+ const jiti = createJiti(import.meta.url);
59
+ let mod;
60
+ try {
61
+ mod = await jiti.import(path);
62
+ } catch (error) {
63
+ throw new Error(describeLoadError(error));
64
+ }
65
+ const sdk = mod.default ?? mod;
66
+ if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
67
+ return sdk;
68
+ }
69
+ //#endregion
70
+ //#region src/cli-config.ts
71
+ const CLI_CONFIG_NAMES = [
72
+ "clivly.cli.ts",
73
+ "clivly.cli.js",
74
+ "clivly.cli.mjs"
75
+ ];
76
+ /** The app's CLI config, or null when it has none — which is the normal case. */
77
+ function resolveCliConfigPath(cwd) {
78
+ for (const name of CLI_CONFIG_NAMES) {
79
+ const path = join(cwd, name);
80
+ if (existsSync(path)) return path;
81
+ }
82
+ return null;
83
+ }
84
+ function readEnvFiles(config) {
85
+ const envFiles = config.envFiles;
86
+ if (envFiles === void 0) return;
87
+ if (!Array.isArray(envFiles)) throw new Error("clivly.cli.ts: envFiles must be an array of paths, e.g. [\"../server/.env\"]");
88
+ if (envFiles.some((entry) => typeof entry !== "string")) throw new Error("clivly.cli.ts: every envFiles entry must be a string");
89
+ return envFiles;
90
+ }
91
+ /**
92
+ * Load `clivly.cli.ts` if the app has one.
93
+ *
94
+ * A malformed config throws rather than being silently ignored: a developer who
95
+ * wrote `envFiles` and sees nothing happen has no way to tell the difference
96
+ * between "not supported" and "I typed it wrong", and the symptom they get
97
+ * instead is a missing API key several steps later.
98
+ */
99
+ async function loadCliConfig(cwd) {
100
+ const path = resolveCliConfigPath(cwd);
101
+ if (!path) return null;
102
+ let mod;
103
+ try {
104
+ mod = await createJiti(import.meta.url).import(path);
105
+ } catch (error) {
106
+ throw new Error(`clivly.cli.ts: ${describeLoadError(error)}`);
107
+ }
108
+ const config = mod?.default ?? mod;
109
+ if (typeof config !== "object" || config === null || Array.isArray(config)) throw new Error("clivly.cli.ts must export an object as its default export");
110
+ const envFiles = readEnvFiles(config);
111
+ return envFiles ? { envFiles } : {};
112
+ }
113
+ //#endregion
14
114
  //#region src/detect.ts
15
115
  const FRAMEWORK_MARKERS = [
16
116
  {
@@ -152,7 +252,7 @@ function detectDevCommand(scripts, explicit) {
152
252
  return null;
153
253
  }
154
254
  const OVERRIDE_FAILURE_CAUSES = {
155
- unauthorized: "CLIVLY_API_KEY is invalid for this workspace/backend.",
255
+ unauthorized: "CLIVLY_SECRET_KEY is invalid for this workspace/backend.",
156
256
  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
257
  unreachable: "Could not reach Clivly at the selected API base URL."
158
258
  };
@@ -200,7 +300,7 @@ async function runDev(options) {
200
300
  return 0;
201
301
  }
202
302
  if (!options.apiKey) {
203
- deps.out("CLIVLY_API_KEY is not set — cannot register the tunnel URL. Set it and re-run.");
303
+ deps.out("CLIVLY_SECRET_KEY is not set — cannot register the tunnel URL. Set it and re-run.");
204
304
  return 1;
205
305
  }
206
306
  const app = command ? deps.spawnApp(command) : null;
@@ -279,16 +379,275 @@ async function probeModule(opts) {
279
379
  }
280
380
  for (const [moduleKey, value] of Object.entries(mod)) if (opts.match(value)) return {
281
381
  moduleKey,
282
- importPath: toImportPath(candidate, opts.cwd)
382
+ importPath: toImportPath(candidate, opts.cwd, opts.graph)
283
383
  };
284
384
  }
285
385
  return null;
286
386
  }
287
387
  //#endregion
388
+ //#region src/workspace-graph.ts
389
+ const WINDOWS_SEP$2 = /\\/g;
390
+ const MODULE_EXT$1 = /\.(ts|tsx|js|jsx|mjs|cjs|cts|mts)$/;
391
+ const BRACE_GROUP = /\{([^{}]*)\}/;
392
+ const MAX_GLOB_DEPTH = 8;
393
+ const IGNORED_DIRS = /* @__PURE__ */ new Set([
394
+ "node_modules",
395
+ "dist",
396
+ "build",
397
+ "coverage"
398
+ ]);
399
+ function isDirectory$1(path) {
400
+ try {
401
+ return statSync(path).isDirectory();
402
+ } catch {
403
+ return false;
404
+ }
405
+ }
406
+ const CONVENTIONAL_ENTRIES = [
407
+ "src/index.ts",
408
+ "src/index.tsx",
409
+ "index.ts",
410
+ "index.tsx",
411
+ "src/index.js",
412
+ "index.js"
413
+ ];
414
+ function readPackageManifest(dir) {
415
+ const path = join(dir, "package.json");
416
+ if (!existsSync(path)) return null;
417
+ try {
418
+ return JSON.parse(readFileSync(path, "utf8"));
419
+ } catch {
420
+ return null;
421
+ }
422
+ }
423
+ function parentDir$1(dir) {
424
+ const parent = dirname(dir);
425
+ return parent === dir ? null : parent;
426
+ }
427
+ /**
428
+ * The workspace globs declared at `dir`, from either `package.json#workspaces`
429
+ * (npm, yarn, bun) or `pnpm-workspace.yaml` (pnpm). Negations are dropped —
430
+ * they only ever narrow the set, and treating one as a literal path would
431
+ * invent a package directory named `!something`.
432
+ */
433
+ function readWorkspaceGlobs(dir) {
434
+ const manifest = readPackageManifest(dir);
435
+ const declared = Array.isArray(manifest?.workspaces) ? manifest.workspaces : manifest?.workspaces?.packages;
436
+ if (Array.isArray(declared) && declared.length > 0) return declared.filter((glob) => !glob.startsWith("!"));
437
+ const pnpmPath = join(dir, "pnpm-workspace.yaml");
438
+ if (!existsSync(pnpmPath)) return [];
439
+ try {
440
+ 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("!"));
441
+ } catch {
442
+ return [];
443
+ }
444
+ }
445
+ /**
446
+ * `{apps,packages}/*` -> [`apps/*`, `packages/*`].
447
+ *
448
+ * Expands one group per pass and recurses, so nested and repeated groups all
449
+ * resolve. A manifest that declares a brace glob is a workspace like any other,
450
+ * and failing to expand it silently yields an empty graph — which downgrades
451
+ * every generated specifier to the relative form this module exists to avoid.
452
+ */
453
+ function expandBraces(pattern) {
454
+ const group = BRACE_GROUP.exec(pattern);
455
+ if (!group) return [pattern];
456
+ return (group[1] ?? "").split(",").flatMap((option) => expandBraces(pattern.replace(group[0], option.trim())));
457
+ }
458
+ function childDirectories(dir) {
459
+ try {
460
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !(IGNORED_DIRS.has(entry.name) || entry.name.startsWith("."))).map((entry) => join(dir, entry.name));
461
+ } catch {
462
+ return [];
463
+ }
464
+ }
465
+ /** `dir` and every directory beneath it, up to `depth` levels down. */
466
+ function withDescendants(dir, depth) {
467
+ if (depth <= 0) return [dir];
468
+ return [dir, ...childDirectories(dir).flatMap((child) => withDescendants(child, depth - 1))];
469
+ }
470
+ /** Resolve the remaining glob segments against the directories matched so far. */
471
+ function matchSegments(dirs, segments) {
472
+ const [segment, ...rest] = segments;
473
+ if (segment === void 0) return dirs;
474
+ let next;
475
+ if (segment === "**") next = dirs.flatMap((dir) => withDescendants(dir, MAX_GLOB_DEPTH));
476
+ else if (segment === "*") next = dirs.flatMap(childDirectories);
477
+ else next = dirs.map((dir) => join(dir, segment)).filter(isDirectory$1);
478
+ return matchSegments(next, rest);
479
+ }
480
+ /**
481
+ * Expand one workspace glob into the package directories it names.
482
+ *
483
+ * Handles exact paths, `*`, `**`, and brace groups — the shapes that actually
484
+ * appear in pnpm/npm/yarn/bun manifests. `node_modules` and dot-directories are
485
+ * never descended into: a dependency's own package.json is not a workspace
486
+ * member, and treating one as such would put a third-party name in the graph.
487
+ */
488
+ function expandWorkspaceGlob(root, pattern) {
489
+ return expandBraces(pattern).flatMap((expanded) => matchSegments([root], expanded.split("/").filter((segment) => segment.length > 0))).filter((dir) => existsSync(join(dir, "package.json")));
490
+ }
491
+ /**
492
+ * Walk up from `start` looking for the directory that declares workspace globs.
493
+ * Stops *before* `boundary` (the user's home directory by default) so a stray
494
+ * manifest above the project is never mistaken for a workspace root.
495
+ */
496
+ function findWorkspaceRoot(start, boundary = homedir()) {
497
+ const stop = resolve(boundary);
498
+ let current = resolve(start);
499
+ while (current !== stop) {
500
+ if (readWorkspaceGlobs(current).length > 0) return current;
501
+ const parent = parentDir$1(current);
502
+ if (!parent) return null;
503
+ current = parent;
504
+ }
505
+ return null;
506
+ }
507
+ /**
508
+ * Build the package graph containing `start`, or null when `start` is not inside
509
+ * a workspace (the single-package case, where relative imports are correct).
510
+ */
511
+ function loadWorkspaceGraph(start, boundary = homedir()) {
512
+ const root = findWorkspaceRoot(start, boundary);
513
+ if (!root) return null;
514
+ const seen = /* @__PURE__ */ new Set();
515
+ const packages = [];
516
+ for (const pattern of readWorkspaceGlobs(root)) for (const dir of expandWorkspaceGlob(root, pattern)) {
517
+ const absolute = resolve(dir);
518
+ if (seen.has(absolute)) continue;
519
+ seen.add(absolute);
520
+ const name = readPackageManifest(absolute)?.name;
521
+ if (typeof name === "string" && name.length > 0) packages.push({
522
+ dir: absolute,
523
+ name
524
+ });
525
+ }
526
+ return {
527
+ root,
528
+ packages
529
+ };
530
+ }
531
+ /**
532
+ * The workspace package that owns `absPath`.
533
+ *
534
+ * Compares against `dir + sep` so a sibling sharing a name prefix
535
+ * (`packages/database-legacy` vs `packages/database`) can't match, and returns
536
+ * the longest match so a nested package beats its parent.
537
+ */
538
+ function findPackageContaining(graph, absPath) {
539
+ if (!graph) return null;
540
+ const target = resolve(absPath);
541
+ let best = null;
542
+ for (const pkg of graph.packages) {
543
+ const withSep = pkg.dir.endsWith(sep) ? pkg.dir : `${pkg.dir}${sep}`;
544
+ if ((target === pkg.dir || target.startsWith(withSep)) && (!best || pkg.dir.length > best.dir.length)) best = pkg;
545
+ }
546
+ return best;
547
+ }
548
+ /** The first string buried in an `exports` value, following conditions in order. */
549
+ function firstExportString(value) {
550
+ if (typeof value === "string") return value;
551
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
552
+ for (const nested of Object.values(value)) {
553
+ const found = firstExportString(nested);
554
+ if (found) return found;
555
+ }
556
+ return null;
557
+ }
558
+ function isFile(path) {
559
+ try {
560
+ return statSync(path).isFile();
561
+ } catch {
562
+ return false;
563
+ }
564
+ }
565
+ /**
566
+ * The file a bare import of this package resolves to, or null if we can't tell.
567
+ *
568
+ * Null rather than a guess on purpose: an entry declared in the manifest but
569
+ * absent on disk (a `dist/` build that hasn't run) would otherwise make `init`
570
+ * generate an import of a file that does not exist.
571
+ */
572
+ function resolvePackageEntry(packageDir) {
573
+ const manifest = readPackageManifest(packageDir);
574
+ const declared = [
575
+ firstExportString((manifest?.exports)?.["."]),
576
+ manifest?.module,
577
+ manifest?.main
578
+ ].filter((entry) => typeof entry === "string");
579
+ for (const entry of [...declared, ...CONVENTIONAL_ENTRIES]) {
580
+ const candidate = resolve(packageDir, entry);
581
+ if (isFile(candidate)) return candidate;
582
+ }
583
+ return null;
584
+ }
585
+ /**
586
+ * Whether `./subpath` is something an importer outside the package may request.
587
+ *
588
+ * A package with no `exports` field uses legacy resolution, where any file is
589
+ * reachable. Once `exports` exists it is an allow-list: Node and TypeScript's
590
+ * `bundler`/`node16` resolution both reject anything it does not name, so
591
+ * emitting such a specifier scaffolds a config that cannot compile.
592
+ */
593
+ function exportsAllowSubpath(packageDir, subpath) {
594
+ const exports = readPackageManifest(packageDir)?.exports;
595
+ if (exports === void 0 || exports === null) return true;
596
+ if (typeof exports !== "object" || Array.isArray(exports)) return false;
597
+ const request = `./${subpath}`;
598
+ return Object.keys(exports).some((key) => {
599
+ if (!key.startsWith(".")) return false;
600
+ const star = key.indexOf("*");
601
+ if (star === -1) return key === request;
602
+ const prefix = key.slice(0, star);
603
+ const suffix = key.slice(star + 1);
604
+ return request.length >= prefix.length + suffix.length && request.startsWith(prefix) && request.endsWith(suffix);
605
+ });
606
+ }
607
+ /**
608
+ * The specifier that imports `absPath` from outside its package: the bare name
609
+ * when the file is the package entry, `name/subpath` when the package's
610
+ * `exports` permits that subpath, and **null** when it does not.
611
+ *
612
+ * Null is the honest answer rather than a specifier that looks right and fails
613
+ * to resolve. The common shape that hits it is a package exporting only `"."`
614
+ * while the Drizzle instance lives in `src/db.ts` — callers should fall back to
615
+ * a relative import, or ask the developer via `--db-import` / `--auth-import`.
616
+ */
617
+ function toPackageSpecifier(pkg, absPath) {
618
+ const target = resolve(absPath);
619
+ if (resolvePackageEntry(pkg.dir) === target) return pkg.name;
620
+ const subpath = relative(pkg.dir, target).replace(WINDOWS_SEP$2, "/").replace(MODULE_EXT$1, "");
621
+ if (subpath.length === 0) return pkg.name;
622
+ return exportsAllowSubpath(pkg.dir, subpath) ? `${pkg.name}/${subpath}` : null;
623
+ }
624
+ //#endregion
288
625
  //#region src/discover-schema.ts
289
626
  const WINDOWS_SEP$1 = /\\/g;
290
627
  const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
291
- function toImportPath(schemaPath, cwd) {
628
+ /**
629
+ * Turn an absolute module path into the specifier the generated config imports
630
+ * it by. Every import `init` emits — schema, db instance, auth instance — is
631
+ * minted here, so this is the one place the monorepo fix has to land.
632
+ *
633
+ * "/app/src/db/schema.ts" (cwd "/app") -> "./src/db/schema"
634
+ *
635
+ * With a workspace `graph`, a file owned by a *different* workspace package
636
+ * becomes a package specifier instead: a schema in `packages/database` imports
637
+ * as `@acme/database`, not `../../packages/database/src/index`. The relative
638
+ * form escapes the app's tsconfig rootDir and sidesteps the package boundary the
639
+ * repo already maintains, which is what made generated configs unusable in
640
+ * shared-package layouts.
641
+ *
642
+ * A file inside the app itself still gets a relative specifier — the app is a
643
+ * workspace package too, and importing it by its own name would be a cycle.
644
+ */
645
+ function toImportPath(schemaPath, cwd, graph) {
646
+ const owner = findPackageContaining(graph ?? null, schemaPath);
647
+ if (owner && resolve(owner.dir) !== resolve(cwd)) {
648
+ const specifier = toPackageSpecifier(owner, schemaPath);
649
+ if (specifier) return specifier;
650
+ }
292
651
  const rel = relative(cwd, schemaPath).replace(WINDOWS_SEP$1, "/").replace(MODULE_EXT, "");
293
652
  return rel.startsWith(".") ? rel : `./${rel}`;
294
653
  }
@@ -365,7 +724,7 @@ async function discoverSchema(opts) {
365
724
  const mod = await loadSchemaModule(opts.schemaPath, load);
366
725
  const drizzle = await load("drizzle-orm");
367
726
  const pgCore = await loadPgCoreRuntime(load);
368
- const importPath = toImportPath(opts.schemaPath, opts.cwd);
727
+ const importPath = toImportPath(opts.schemaPath, opts.cwd, opts.graph);
369
728
  const tables = [];
370
729
  let db = null;
371
730
  for (const [moduleKey, value] of Object.entries(mod)) {
@@ -387,6 +746,7 @@ async function discoverSchema(opts) {
387
746
  if (db === null) db = await probeModule({
388
747
  candidates: siblingDbCandidates(opts.schemaPath),
389
748
  cwd: opts.cwd,
749
+ graph: opts.graph,
390
750
  load,
391
751
  match: isDrizzleDb
392
752
  });
@@ -539,7 +899,7 @@ function isEntrypoint(argv1, moduleUrl, realpath = realpathSync) {
539
899
  //#endregion
540
900
  //#region src/load-env.ts
541
901
  const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
542
- const NEWLINE = /\r?\n/;
902
+ const NEWLINE$1 = /\r?\n/;
543
903
  const ESCAPED_NEWLINE = /\\n/g;
544
904
  const ESCAPED_CARRIAGE = /\\r/g;
545
905
  const TRAILING_COMMENT = /\s#/;
@@ -551,7 +911,7 @@ const TRAILING_COMMENT = /\s#/;
551
911
  */
552
912
  function parseEnv(content) {
553
913
  const out = {};
554
- for (const rawLine of content.split(NEWLINE)) {
914
+ for (const rawLine of content.split(NEWLINE$1)) {
555
915
  const line = rawLine.trimStart();
556
916
  if (line === "" || line.startsWith("#")) continue;
557
917
  const match = LINE.exec(line);
@@ -576,15 +936,26 @@ function parseValue(raw) {
576
936
  /**
577
937
  * Load `.env` then `.env.local` from `cwd` into `env` (default `process.env`),
578
938
  * 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.
939
+ * real environment (shell) > explicit `options.files` > `.env.local` > `.env`.
940
+ * The conventional part mirrors what developers already expect (Next.js, Vite),
941
+ * so pointing `CLIVLY_API_URL` at a staging host from the shell still wins.
942
+ *
943
+ * Explicit files exist for split workspaces, where the app that holds
944
+ * `clivly.config.ts` is not the package that holds the credentials. Reading
945
+ * them here — in the CLI process — is what keeps that need out of
946
+ * `clivly.config.ts`, which the app imports and which therefore has to survive
947
+ * an edge runtime with no filesystem.
582
948
  */
583
- function loadEnv(cwd, env = process.env) {
949
+ function loadEnv(cwd, env = process.env, options = {}) {
584
950
  const files = [];
585
951
  const merged = {};
586
- for (const name of [".env", ".env.local"]) {
587
- const path = join(cwd, name);
952
+ const candidates = [
953
+ ".env",
954
+ ".env.local",
955
+ ...options.files ?? []
956
+ ];
957
+ for (const name of candidates) {
958
+ const path = resolve(cwd, name);
588
959
  if (!existsSync(path)) continue;
589
960
  files.push(name);
590
961
  try {
@@ -602,6 +973,15 @@ function loadEnv(cwd, env = process.env) {
602
973
  };
603
974
  }
604
975
  //#endregion
976
+ //#region src/secret-key-env.ts
977
+ const SECRET_KEY_ENV = "CLIVLY_SECRET_KEY";
978
+ const LEGACY_SECRET_KEY_ENV = "CLIVLY_API_KEY";
979
+ function readSecretKeyEnv(env) {
980
+ const next = env[SECRET_KEY_ENV];
981
+ if (next && next.length > 0) return next;
982
+ return env[LEGACY_SECRET_KEY_ENV];
983
+ }
984
+ //#endregion
605
985
  //#region src/login.ts
606
986
  const POLL_INTERVAL_MS = 2e3;
607
987
  const POLL_BACKOFF_MULTIPLIER = 1.5;
@@ -613,7 +993,7 @@ const MASK_VISIBLE_CHARS = 4;
613
993
  const MASK_PREFIX = "••••";
614
994
  function maskSecret(value) {
615
995
  if (value.length <= MASK_VISIBLE_CHARS) return MASK_PREFIX;
616
- return `${MASK_PREFIX}${value.slice(-MASK_VISIBLE_CHARS)}`;
996
+ return `${MASK_PREFIX}${value.slice(-4)}`;
617
997
  }
618
998
  async function createPairingSession(apiUrl, deps, rotate) {
619
999
  const res = await deps.fetchImpl(`${apiUrl}/clivly/cli/pair`, {
@@ -625,7 +1005,7 @@ async function createPairingSession(apiUrl, deps, rotate) {
625
1005
  })
626
1006
  });
627
1007
  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.");
1008
+ 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
1009
  return { exitCode: 1 };
630
1010
  }
631
1011
  if (!res.ok) {
@@ -698,9 +1078,9 @@ async function confirmWorkspace(deps, credentials) {
698
1078
  }
699
1079
  function credentialEntries(credentials) {
700
1080
  return [{
701
- key: "CLIVLY_API_KEY",
1081
+ key: SECRET_KEY_ENV,
702
1082
  value: credentials.apiKey,
703
- comment: "Org API key — Settings → API keys"
1083
+ comment: "Org secret key — Settings → API keys"
704
1084
  }, {
705
1085
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
706
1086
  value: credentials.triggerSecret,
@@ -714,7 +1094,7 @@ function writeCredentials(options, credentials) {
714
1094
  deps.writeEnv(options.cwd, result.contents);
715
1095
  deps.out("\nLogin successful.\n");
716
1096
  const valuesByKey = {
717
- CLIVLY_API_KEY: credentials.apiKey,
1097
+ [SECRET_KEY_ENV]: credentials.apiKey,
718
1098
  CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
719
1099
  };
720
1100
  for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
@@ -807,32 +1187,42 @@ function buildEntity(table, concept, todos) {
807
1187
  idField
808
1188
  };
809
1189
  }
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
1190
  function hasMappedFields(entity) {
825
1191
  return Object.keys(entity.fields).length > 0;
826
1192
  }
827
- function planInit(schema) {
1193
+ /** A table by its SQL name or its schema-module export name — either is what a developer would type. */
1194
+ function findTable(tables, name) {
1195
+ return tables.find((table) => table.tableName === name || table.moduleKey === name) ?? null;
1196
+ }
1197
+ /**
1198
+ * Which table serves each concept: an explicit override when given, else the
1199
+ * heuristic pick.
1200
+ *
1201
+ * Split out of `planInit` because resolving two independent overrides, each with
1202
+ * its own unknown-name path, is most of the branching — and keeps `planInit`
1203
+ * inside the cognitive-complexity budget.
1204
+ */
1205
+ function selectTables(tables, overrides, todos) {
1206
+ const heuristic = selectEntities(tables);
1207
+ const pick = (flag, fallback, label) => {
1208
+ if (!flag) return fallback;
1209
+ const found = findTable(tables, flag);
1210
+ if (!found) todos.push(`--${label}-table "${flag}" is not a table in this schema.`);
1211
+ return found;
1212
+ };
1213
+ const contact = pick(overrides.contactTable, heuristic.contact, "contact");
1214
+ const company = pick(overrides.companyTable, heuristic.company, "company");
1215
+ return {
1216
+ contact,
1217
+ company: company?.tableName === contact?.tableName ? null : company
1218
+ };
1219
+ }
1220
+ function planInit(schema, overrides = {}) {
828
1221
  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
- }
1222
+ const { contact: contactTable, company: companyTable } = selectTables(schema.tables, overrides, todos);
1223
+ const built = contactTable ? buildEntity(contactTable, "contact", todos) : null;
1224
+ const contact = built && hasMappedFields(built) ? built : null;
1225
+ 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
1226
  let company = null;
837
1227
  if (companyTable) {
838
1228
  const candidate = buildEntity(companyTable, "company", todos);
@@ -841,7 +1231,7 @@ function planInit(schema) {
841
1231
  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
1232
  } 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
1233
  }
844
- if (contactTable) {
1234
+ if (contactTable && contact) {
845
1235
  const scored = scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
846
1236
  const top = scored[0];
847
1237
  const runnerUp = scored[1];
@@ -856,16 +1246,16 @@ function planInit(schema) {
856
1246
  db: schema.db,
857
1247
  contact,
858
1248
  company,
859
- todos,
860
- contactDetected
1249
+ tableNames: schema.tables.map((table) => table.tableName),
1250
+ todos
861
1251
  };
862
1252
  }
863
1253
  //#endregion
864
1254
  //#region src/render-config.ts
865
1255
  const CLIVLY_ENV_KEYS = [
866
1256
  {
867
- key: "CLIVLY_API_KEY",
868
- comment: "Org API key — Settings → API keys"
1257
+ key: "CLIVLY_SECRET_KEY",
1258
+ comment: "Org secret key — Settings → API keys"
869
1259
  },
870
1260
  {
871
1261
  key: "CLIVLY_SYNC_TRIGGER_URL",
@@ -874,10 +1264,6 @@ const CLIVLY_ENV_KEYS = [
874
1264
  {
875
1265
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
876
1266
  comment: "Signing secret — Integrations → Remote sync"
877
- },
878
- {
879
- key: "CLIVLY_VERIFY_TOKEN",
880
- comment: "Bearer token Clivly presents to /clivly/auth/verify"
881
1267
  }
882
1268
  ];
883
1269
  function renderFields(fields) {
@@ -912,10 +1298,9 @@ function renderDbExpr(plan) {
912
1298
  }
913
1299
  function renderImports(plan) {
914
1300
  const lines = [
915
- "import { createClivlySDK } from \"clivly/sdk\";",
1301
+ "import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
916
1302
  "import { fromDrizzle } from \"clivly/drizzle\";",
917
- "import { defineClivlyConfig } from \"clivly/core\";",
918
- "import { discoverFromDrizzle } from \"clivly/core/drizzle\";",
1303
+ "import { discoverFromDrizzle } from \"clivly/drizzle\";",
919
1304
  `import * as schema from "${plan.importPath}";`
920
1305
  ];
921
1306
  if (!plan.db) lines.push(`import { db } from "${fallbackDbImportPath(plan.importPath)}"; // TODO: point at your Drizzle instance`);
@@ -941,7 +1326,7 @@ ${entities.join(",\n")},
941
1326
  });
942
1327
 
943
1328
  export default createClivlySDK({
944
- apiKey: process.env.CLIVLY_API_KEY!,
1329
+ apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY!,
945
1330
  entities,
946
1331
  // Every table in your Drizzle schema, so the Clivly dashboard can offer them
947
1332
  // all when you add a mapping — not just the ones wired up as sources below.
@@ -954,7 +1339,8 @@ ${sources.join(",\n")},
954
1339
  url: process.env.CLIVLY_SYNC_TRIGGER_URL,
955
1340
  secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
956
1341
  },
957
- verifyToken: process.env.CLIVLY_VERIFY_TOKEN,
1342
+ // verifyToken is optional and only needed once a Clivly Cloud consumer calls
1343
+ // your /clivly/auth/verify endpoint; leave it unset otherwise.
958
1344
  });
959
1345
  `;
960
1346
  }
@@ -1040,23 +1426,28 @@ const TS_EXTENSION = /\.ts$/;
1040
1426
  const ROUTE_PATHS = {
1041
1427
  nextjs: {
1042
1428
  tick: "app/api/clivly/tick/route.ts",
1043
- verify: "app/api/clivly/auth/verify/route.ts"
1429
+ verify: "app/api/clivly/auth/verify/route.ts",
1430
+ session: "app/api/clivly/widget/session/route.ts"
1044
1431
  },
1045
1432
  "tanstack-start": {
1046
1433
  tick: "src/routes/api/clivly/tick.ts",
1047
- verify: "src/routes/api/clivly/auth/verify.ts"
1434
+ verify: "src/routes/api/clivly/auth/verify.ts",
1435
+ session: "src/routes/api/clivly/widget/session.ts"
1048
1436
  },
1049
1437
  sveltekit: {
1050
1438
  tick: "src/routes/api/clivly/tick/+server.ts",
1051
- verify: "src/routes/api/clivly/auth/verify/+server.ts"
1439
+ verify: "src/routes/api/clivly/auth/verify/+server.ts",
1440
+ session: "src/routes/api/clivly/widget/session/+server.ts"
1052
1441
  },
1053
1442
  remix: {
1054
1443
  tick: "app/routes/api.clivly.tick.ts",
1055
- verify: "app/routes/api.clivly.auth.verify.ts"
1444
+ verify: "app/routes/api.clivly.auth.verify.ts",
1445
+ session: "app/routes/api.clivly.widget.session.ts"
1056
1446
  },
1057
1447
  nuxt: {
1058
1448
  tick: "server/api/clivly/tick.ts",
1059
- verify: "server/api/clivly/auth/verify.ts"
1449
+ verify: "server/api/clivly/auth/verify.ts",
1450
+ session: "server/api/clivly/widget/session.ts"
1060
1451
  },
1061
1452
  unknown: null
1062
1453
  };
@@ -1086,14 +1477,27 @@ function renderAuthPreamble(routeFilePath, auth) {
1086
1477
  lines.push("", auth.body);
1087
1478
  return lines.join("\n");
1088
1479
  }
1089
- function renderBody(framework, importPath, verify, auth, routeFilePath) {
1090
- const preamble = verify ? `import clivly from "${importPath}";
1480
+ function renderPreamble(kind, importPath, auth, routeFilePath) {
1481
+ if (kind === "session") return `import { createChatSessionHandler } from "clivly/sdk";
1482
+
1483
+ const handler = createChatSessionHandler({
1484
+ apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY ?? "",
1485
+ allowedOrigins: [
1486
+ // TODO: add the origins your widget loads from, e.g. "https://example.com"
1487
+ ],
1488
+ });
1489
+ `;
1490
+ if (kind === "verify") return `import clivly from "${importPath}";
1091
1491
  ${renderAuthPreamble(routeFilePath, auth)}
1092
1492
  const handler = clivly.createAuthVerifyHandler(resolveUser);
1093
- ` : `import clivly from "${importPath}";
1493
+ `;
1494
+ return `import clivly from "${importPath}";
1094
1495
 
1095
1496
  const handler = clivly.createClivlyHandler();
1096
1497
  `;
1498
+ }
1499
+ function renderBody(framework, importPath, kind, auth, routeFilePath) {
1500
+ const preamble = renderPreamble(kind, importPath, auth, routeFilePath);
1097
1501
  switch (framework) {
1098
1502
  case "nextjs": return `${preamble}
1099
1503
  export const POST = (request: Request) => handler(request);
@@ -1125,7 +1529,7 @@ export default defineEventHandler((event) => handler(toWebRequest(event)));
1125
1529
  }
1126
1530
  function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown")) {
1127
1531
  const path = routePath(framework, kind);
1128
- let body = renderBody(framework, configImport(path), kind === "verify", auth, path);
1532
+ let body = renderBody(framework, configImport(path), kind, auth, path);
1129
1533
  if (framework === "tanstack-start") {
1130
1534
  const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
1131
1535
  body = body.replace("__ROUTE__", url);
@@ -1154,11 +1558,21 @@ function planScaffold(framework) {
1154
1558
  ...COMMON_FILES
1155
1559
  ];
1156
1560
  }
1561
+ function planWidgetScaffold(framework) {
1562
+ const paths = ROUTE_PATHS[framework];
1563
+ if (!paths) return [];
1564
+ return [{
1565
+ path: paths.session,
1566
+ description: "Clivly chat-session route (createChatSessionHandler) — add allowedOrigins",
1567
+ owned: true
1568
+ }];
1569
+ }
1157
1570
  //#endregion
1158
1571
  //#region src/plan-mutations.ts
1159
- function fileAction(present, force) {
1160
- if (!present) return "create";
1161
- return force ? "overwrite" : "skip";
1572
+ function fileAction(existing, next, force) {
1573
+ if (existing === null) return "create";
1574
+ if (existing === next) return "unchanged";
1575
+ return force ? "overwrite" : "conflict";
1162
1576
  }
1163
1577
  function hasKey(content, key) {
1164
1578
  return new RegExp(`^\\s*${key}=`, "m").test(content);
@@ -1170,11 +1584,14 @@ function planFiles(args) {
1170
1584
  const kind = path.includes("verify") ? "verify" : "tick";
1171
1585
  return renderRouteFile(args.framework, kind, args.authWiring);
1172
1586
  };
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
- });
1587
+ for (const file of planScaffold(args.framework)) {
1588
+ const contents = render(file.path);
1589
+ mutations.push({
1590
+ path: file.path,
1591
+ contents,
1592
+ action: fileAction(args.readFile(join(args.cwd, file.path)), contents, args.force)
1593
+ });
1594
+ }
1178
1595
  return mutations;
1179
1596
  }
1180
1597
  function envAction(addedCount, existingLength) {
@@ -1197,6 +1614,16 @@ function planMutations(args) {
1197
1614
  env: planEnv(args)
1198
1615
  };
1199
1616
  }
1617
+ function planWidgetMutations(args) {
1618
+ return planWidgetScaffold(args.framework).map((file) => {
1619
+ const contents = renderRouteFile(args.framework, "session");
1620
+ return {
1621
+ path: file.path,
1622
+ contents,
1623
+ action: fileAction(args.readFile(join(args.cwd, file.path)), contents, args.force)
1624
+ };
1625
+ });
1626
+ }
1200
1627
  //#endregion
1201
1628
  //#region src/probe-auth.ts
1202
1629
  const AUTH_MODULE_PATHS = [
@@ -1228,59 +1655,6 @@ function probeAuthInstance(cwd, load) {
1228
1655
  });
1229
1656
  }
1230
1657
  //#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
1658
  //#region src/push-schema.ts
1285
1659
  async function runPushSchema(options) {
1286
1660
  const out = options.out ?? ((line) => process.stdout.write(line));
@@ -1349,11 +1723,40 @@ function defaultReadDrizzleConfigSchema(cwd) {
1349
1723
  }
1350
1724
  return null;
1351
1725
  }
1726
+ /**
1727
+ * Resolve a `--schema` value that names a workspace package rather than a file.
1728
+ *
1729
+ * Matches the longest package name that prefixes the specifier, so both
1730
+ * `@acme/database` and `@acme/database/src/schema.ts` work. Returns `undefined`
1731
+ * when nothing matched (caller falls back to path handling) and `null` when a
1732
+ * package matched but its entry could not be resolved — those are different
1733
+ * outcomes and the caller reports them differently.
1734
+ */
1735
+ function resolveFromWorkspace(specifier, graph, deps) {
1736
+ const pkg = graph.packages.filter((pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)).sort((a, b) => b.name.length - a.name.length)[0];
1737
+ if (!pkg) return;
1738
+ if (specifier === pkg.name) return deps.resolveEntry(pkg.dir);
1739
+ const subpath = specifier.slice(pkg.name.length + 1);
1740
+ const candidate = resolve(pkg.dir, subpath);
1741
+ return deps.exists(candidate) ? candidate : deps.resolveEntry(pkg.dir);
1742
+ }
1352
1743
  function resolveSchemaPath(opts) {
1353
1744
  const exists = opts.deps?.exists ?? existsSync;
1354
1745
  const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
1746
+ const resolveEntry = opts.deps?.resolvePackageEntry ?? resolvePackageEntry;
1355
1747
  const toAbs = (p) => isAbsolute(p) ? p : resolve(opts.cwd, p);
1356
- if (opts.flag) return toAbs(opts.flag);
1748
+ if (opts.flag) {
1749
+ const asPath = toAbs(opts.flag);
1750
+ if (exists(asPath)) return asPath;
1751
+ if (opts.graph) {
1752
+ const fromWorkspace = resolveFromWorkspace(opts.flag, opts.graph, {
1753
+ exists,
1754
+ resolveEntry
1755
+ });
1756
+ if (fromWorkspace !== void 0) return fromWorkspace;
1757
+ }
1758
+ return asPath;
1759
+ }
1357
1760
  const fromConfig = readSchema(opts.cwd);
1358
1761
  if (fromConfig) return toAbs(fromConfig);
1359
1762
  for (const candidate of COMMON_PATHS) {
@@ -1364,17 +1767,8 @@ function resolveSchemaPath(opts) {
1364
1767
  }
1365
1768
  //#endregion
1366
1769
  //#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
1770
  function packageDeps(dir) {
1377
- const pkg = readPackageJson(dir);
1771
+ const pkg = readPackageManifest(dir);
1378
1772
  return {
1379
1773
  ...pkg?.dependencies,
1380
1774
  ...pkg?.devDependencies
@@ -1398,41 +1792,6 @@ function findNearestPackageDir(start, boundary) {
1398
1792
  current = parent;
1399
1793
  }
1400
1794
  }
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
1795
  function uniquePaths(paths) {
1437
1796
  return [...new Set(paths.map((path) => resolve(path)))];
1438
1797
  }
@@ -1488,7 +1847,7 @@ function resolveTargetApp(start, boundary = homedir()) {
1488
1847
  }
1489
1848
  //#endregion
1490
1849
  //#region src/status-ladder.ts
1491
- const SATISFYING = new Set(["pass", "warn"]);
1850
+ const SATISFYING = /* @__PURE__ */ new Set(["pass", "warn"]);
1492
1851
  function unmetPrerequisites(stage, satisfied) {
1493
1852
  return (stage.needs ?? []).filter((id) => !satisfied.has(id));
1494
1853
  }
@@ -1538,10 +1897,118 @@ function ladderExitCode(results) {
1538
1897
  return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
1539
1898
  }
1540
1899
  //#endregion
1900
+ //#region src/edge-safety.ts
1901
+ const NODE_BUILTINS = /* @__PURE__ */ new Set([
1902
+ "assert",
1903
+ "buffer",
1904
+ "child_process",
1905
+ "cluster",
1906
+ "crypto",
1907
+ "dns",
1908
+ "events",
1909
+ "fs",
1910
+ "http",
1911
+ "http2",
1912
+ "https",
1913
+ "module",
1914
+ "net",
1915
+ "os",
1916
+ "path",
1917
+ "perf_hooks",
1918
+ "process",
1919
+ "readline",
1920
+ "stream",
1921
+ "timers",
1922
+ "tls",
1923
+ "url",
1924
+ "util",
1925
+ "v8",
1926
+ "vm",
1927
+ "worker_threads",
1928
+ "zlib"
1929
+ ]);
1930
+ const SIDE_EFFECT_MODULES = [/^dotenv\/config$/, /^dotenv\/lib\/env-options$/];
1931
+ const IMPORT_FROM = /(?:^|\s)(?:import|export)\b[^"']*from\s*["']([^"']+)["']/;
1932
+ const BARE_IMPORT = /^\s*import\s*["']([^"']+)["']\s*;?\s*$/;
1933
+ const REQUIRE_CALL = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/;
1934
+ const NEWLINE = /\r?\n/;
1935
+ const MODULE_SCOPE_CALL = /^([A-Za-z_$][\w$.]*)\s*\(/;
1936
+ const ALLOWED_CALLS = /* @__PURE__ */ new Set([
1937
+ "export",
1938
+ "return",
1939
+ "if",
1940
+ "for",
1941
+ "while"
1942
+ ]);
1943
+ const SIDE_EFFECT_CALLS = /^(dotenv\.config|config|configDotenv|fileURLToPath|readFileSync|require|process\.chdir)$/;
1944
+ 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.";
1945
+ function isNodeBuiltin(specifier) {
1946
+ if (specifier.startsWith("node:")) return true;
1947
+ return NODE_BUILTINS.has(specifier.split("/")[0] ?? "");
1948
+ }
1949
+ function stripComment(line) {
1950
+ const trimmed = line.trimStart();
1951
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) return "";
1952
+ return line;
1953
+ }
1954
+ function specifierIssue(specifier, line) {
1955
+ if (SIDE_EFFECT_MODULES.some((pattern) => pattern.test(specifier))) return {
1956
+ code: "module_scope_side_effect",
1957
+ detail: `imports "${specifier}" for its side effect`,
1958
+ hint: ENV_HINT,
1959
+ line
1960
+ };
1961
+ if (isNodeBuiltin(specifier)) return {
1962
+ code: "node_builtin_in_edge_config",
1963
+ detail: `imports "${specifier}", which does not exist on an edge runtime`,
1964
+ hint: ENV_HINT,
1965
+ line
1966
+ };
1967
+ return null;
1968
+ }
1969
+ function callIssue(source, line) {
1970
+ const name = MODULE_SCOPE_CALL.exec(source)?.[1];
1971
+ if (!name || ALLOWED_CALLS.has(name) || !SIDE_EFFECT_CALLS.test(name)) return null;
1972
+ return {
1973
+ code: "module_scope_side_effect",
1974
+ detail: `calls ${name}() at module scope`,
1975
+ hint: ENV_HINT,
1976
+ line
1977
+ };
1978
+ }
1979
+ /**
1980
+ * Every reason `source` would fail to import on an edge runtime, in line order.
1981
+ * An empty array means the config is safe to import from the app.
1982
+ */
1983
+ function analyzeEdgeSafety(source) {
1984
+ const issues = [];
1985
+ source.split(NEWLINE).forEach((rawLine, index) => {
1986
+ const line = stripComment(rawLine);
1987
+ if (line.trim() === "") return;
1988
+ const lineNumber = index + 1;
1989
+ const specifier = BARE_IMPORT.exec(line)?.[1] ?? IMPORT_FROM.exec(line)?.[1] ?? REQUIRE_CALL.exec(line)?.[1];
1990
+ if (specifier) {
1991
+ const issue = specifierIssue(specifier, lineNumber);
1992
+ if (issue) issues.push(issue);
1993
+ return;
1994
+ }
1995
+ const call = callIssue(line, lineNumber);
1996
+ if (call) issues.push(call);
1997
+ });
1998
+ return issues;
1999
+ }
2000
+ //#endregion
1541
2001
  //#region src/status-stages.ts
2002
+ function defaultReadConfigSource(path) {
2003
+ try {
2004
+ return readFileSync(path, "utf8");
2005
+ } catch {
2006
+ return null;
2007
+ }
2008
+ }
1542
2009
  const REASON_HINT = {
1543
2010
  ok: "",
1544
- invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
2011
+ invalid_api_key: "The key was rejected (401). Check CLIVLY_SECRET_KEY (or legacy CLIVLY_API_KEY).",
1545
2012
  forbidden: "The key is valid but lacks access to this org (403).",
1546
2013
  rate_limited: "Rate limited (429). Wait a moment and retry.",
1547
2014
  server_error: "Clivly returned a server error (5xx). Retry shortly.",
@@ -1554,12 +2021,12 @@ const apiKeyStage = {
1554
2021
  run(ctx) {
1555
2022
  if (ctx.apiKey) return Promise.resolve({
1556
2023
  state: "pass",
1557
- detail: "CLIVLY_API_KEY is set."
2024
+ detail: "Secret key is set."
1558
2025
  });
1559
2026
  return Promise.resolve({
1560
2027
  state: "fail",
1561
- detail: "CLIVLY_API_KEY is not set.",
1562
- hint: "Copy your API key from Settings → API keys into .env."
2028
+ detail: "Secret key is not set.",
2029
+ hint: "Copy your secret key from Settings → API keys into CLIVLY_SECRET_KEY in .env."
1563
2030
  });
1564
2031
  }
1565
2032
  };
@@ -1600,12 +2067,38 @@ const configStage = {
1600
2067
  }
1601
2068
  }
1602
2069
  };
1603
- const LADDER_OWNED_CHECKS = new Set(["API key present", "Heartbeat"]);
2070
+ const LADDER_OWNED_CHECKS = /* @__PURE__ */ new Set(["API key present", "Heartbeat"]);
1604
2071
  /** The full ladder, in the order a developer completes setup. */
1605
2072
  const STATUS_STAGES = [
1606
2073
  apiKeyStage,
1607
2074
  heartbeatStage,
1608
2075
  configStage,
2076
+ {
2077
+ id: "runtime",
2078
+ label: "clivly.config is edge-safe",
2079
+ run(ctx) {
2080
+ const source = (ctx.readConfigSource ?? defaultReadConfigSource)(ctx.configPath);
2081
+ if (source === null) return Promise.resolve({
2082
+ state: "skip",
2083
+ detail: "config not readable"
2084
+ });
2085
+ const issues = analyzeEdgeSafety(source);
2086
+ if (issues.length === 0) return Promise.resolve({
2087
+ state: "pass",
2088
+ detail: "no Node-only code at module scope"
2089
+ });
2090
+ return Promise.resolve({
2091
+ state: "fail",
2092
+ detail: issues.map((issue) => issue.detail).join("; "),
2093
+ hint: issues[0]?.hint,
2094
+ sub: issues.map((issue) => ({
2095
+ label: `${issue.code} (line ${issue.line})`,
2096
+ ok: false,
2097
+ detail: issue.detail
2098
+ }))
2099
+ });
2100
+ }
2101
+ },
1609
2102
  {
1610
2103
  id: "setup-checks",
1611
2104
  label: "Setup checks",
@@ -1653,7 +2146,11 @@ function defaultLoadSdk(path) {
1653
2146
  const ALIAS_STAGE_IDS = {
1654
2147
  status: STATUS_STAGES.map((stage) => stage.id),
1655
2148
  ping: ["api-key", "heartbeat"],
1656
- doctor: ["config", "setup-checks"],
2149
+ doctor: [
2150
+ "config",
2151
+ "runtime",
2152
+ "setup-checks"
2153
+ ],
1657
2154
  connect: [
1658
2155
  "api-key",
1659
2156
  "heartbeat",
@@ -1712,6 +2209,14 @@ function flagValue(argv, flag) {
1712
2209
  const index = argv.indexOf(flag);
1713
2210
  return index >= 0 ? argv[index + 1] : void 0;
1714
2211
  }
2212
+ function flagValues(argv, flag) {
2213
+ const values = [];
2214
+ for (const [index, entry] of argv.entries()) {
2215
+ const value = entry === flag ? argv[index + 1] : void 0;
2216
+ if (value !== void 0 && !value.startsWith("--")) values.push(value);
2217
+ }
2218
+ return values;
2219
+ }
1715
2220
  const KNOWN_COMMANDS = [
1716
2221
  "init",
1717
2222
  "status",
@@ -1723,17 +2228,17 @@ const KNOWN_COMMANDS = [
1723
2228
  function editDistance(a, b) {
1724
2229
  const m = a.length;
1725
2230
  const n = b.length;
1726
- const prev = Array.from({ length: n + 1 }, (_, i) => i);
1727
- const curr = Array.from({ length: n + 1 }, () => 0);
2231
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
2232
+ let curr = Array.from({ length: n + 1 }, () => 0);
1728
2233
  for (let i = 1; i <= m; i++) {
1729
2234
  curr[0] = i;
1730
2235
  for (let j = 1; j <= n; j++) {
1731
2236
  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);
2237
+ curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
1733
2238
  }
1734
- for (let j = 0; j <= n; j++) prev[j] = curr[j];
2239
+ [prev, curr] = [curr, prev];
1735
2240
  }
1736
- return curr[n];
2241
+ return prev[n] ?? 0;
1737
2242
  }
1738
2243
  function nearestCommand(input) {
1739
2244
  let best;
@@ -1759,6 +2264,59 @@ function readDeps(cwd) {
1759
2264
  return {};
1760
2265
  }
1761
2266
  }
2267
+ const ADD_USAGE = "Usage: clivly add widget [--force] [--dry-run]";
2268
+ const defaultAddDeps = {
2269
+ out: (line) => process.stdout.write(line),
2270
+ readFile: (path) => existsSync(path) ? readFileSync(path, "utf8") : null,
2271
+ writeFile: (path, contents) => {
2272
+ mkdirSync(dirname(path), { recursive: true });
2273
+ writeFileSync(path, contents, "utf8");
2274
+ },
2275
+ readDeps
2276
+ };
2277
+ function runAdd(argv, cwd, deps = defaultAddDeps) {
2278
+ const target = argv[0];
2279
+ if (target !== "widget") {
2280
+ deps.out(`${target ? `Unknown target "${target}". ` : "Missing target. "}Only \`widget\` is supported.\n${ADD_USAGE}\n`);
2281
+ return 1;
2282
+ }
2283
+ const framework = detectFramework(deps.readDeps(cwd));
2284
+ if (framework === "unknown") {
2285
+ deps.out("Couldn't detect your framework from package.json. `clivly add widget` supports Next.js, TanStack Start, SvelteKit, Remix, and Nuxt.\n");
2286
+ return 1;
2287
+ }
2288
+ const force = argv.includes("--force");
2289
+ const dryRun = argv.includes("--dry-run");
2290
+ const files = planWidgetMutations({
2291
+ framework,
2292
+ cwd,
2293
+ force,
2294
+ readFile: (path) => deps.readFile(path)
2295
+ });
2296
+ deps.out(`\nClivly add widget — ${FRAMEWORK_LABELS[framework]}\n\n`);
2297
+ if (dryRun) deps.out("Dry run — nothing was written.\n\n");
2298
+ for (const file of files) {
2299
+ if (dryRun) {
2300
+ deps.out(` ${file.action.padEnd(9)} ${file.path}\n`);
2301
+ continue;
2302
+ }
2303
+ if (file.action === "conflict") {
2304
+ deps.out(` ! ${file.path} differs from what Clivly would write — left alone (--force to overwrite)\n`);
2305
+ continue;
2306
+ }
2307
+ if (file.action === "unchanged") {
2308
+ deps.out(` unchanged ${file.path} (already up to date)\n`);
2309
+ continue;
2310
+ }
2311
+ deps.writeFile(join(cwd, file.path), file.contents);
2312
+ deps.out(` ${file.action.padEnd(9)} ${file.path}\n`);
2313
+ }
2314
+ if (!dryRun) {
2315
+ const sessionPath = files[0]?.path ?? "";
2316
+ 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`);
2317
+ }
2318
+ return 0;
2319
+ }
1762
2320
  function readOwnVersion() {
1763
2321
  const req = createRequire(import.meta.url);
1764
2322
  for (const rel of ["./package.json", "../package.json"]) try {
@@ -1766,6 +2324,35 @@ function readOwnVersion() {
1766
2324
  if (pkg.name === "clivly" && pkg.version) return pkg.version;
1767
2325
  } catch {}
1768
2326
  }
2327
+ /**
2328
+ * Parse a `--db-import` / `--auth-import` value into a discovery-shaped override.
2329
+ *
2330
+ * `@acme/database` -> importPath "@acme/database", moduleKey <fallback>
2331
+ * `@acme/database#database` -> importPath "@acme/database", moduleKey "database"
2332
+ *
2333
+ * The optional `#exportName` suffix exists because a specifier alone doesn't say
2334
+ * which export holds the instance. Guessing wrong emits an import of a name that
2335
+ * isn't there, so the flag lets the developer say it outright while keeping the
2336
+ * conventional name as the default.
2337
+ */
2338
+ function importOverride(flag, fallbackModuleKey) {
2339
+ const trimmed = flag?.trim();
2340
+ if (!trimmed) return null;
2341
+ const separator = trimmed.lastIndexOf("#");
2342
+ const importPath = separator === -1 ? trimmed : trimmed.slice(0, separator);
2343
+ const moduleKey = separator === -1 ? "" : trimmed.slice(separator + 1);
2344
+ if (importPath.length === 0) return null;
2345
+ return {
2346
+ importPath,
2347
+ moduleKey: moduleKey || fallbackModuleKey
2348
+ };
2349
+ }
2350
+ function authInstanceFromFlag(flag) {
2351
+ return importOverride(flag, "auth");
2352
+ }
2353
+ function defaultReadFile(path) {
2354
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
2355
+ }
1769
2356
  function defaultReadEnv(cwd) {
1770
2357
  const path = join(cwd, ".env");
1771
2358
  return existsSync(path) ? readFileSync(path, "utf8") : "";
@@ -1777,18 +2364,27 @@ function defaultIsInteractive() {
1777
2364
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !process.env.CI;
1778
2365
  }
1779
2366
  function resolveInitDeps(options) {
2367
+ const loadGraph = options.loadWorkspaceGraph ?? loadWorkspaceGraph;
2368
+ const graphCache = /* @__PURE__ */ new Map();
2369
+ const graphFor = (cwd) => {
2370
+ if (!graphCache.has(cwd)) graphCache.set(cwd, loadGraph(cwd));
2371
+ return graphCache.get(cwd) ?? null;
2372
+ };
1780
2373
  return {
1781
2374
  out: options.out ?? ((line) => process.stdout.write(line)),
1782
2375
  readDepsFn: options.readDeps ?? readDeps,
2376
+ graphFor,
1783
2377
  resolvePath: options.resolveSchemaPath ?? ((cwd) => resolveSchemaPath({
1784
2378
  cwd,
1785
- flag: options.schemaFlag
2379
+ flag: options.schemaFlag,
2380
+ graph: graphFor(cwd)
1786
2381
  })),
1787
2382
  discover: options.discoverSchema ?? ((schemaPath, cwd) => discoverSchema({
1788
2383
  schemaPath,
1789
- cwd
2384
+ cwd,
2385
+ graph: graphFor(cwd)
1790
2386
  })),
1791
- configExists: options.configExists ?? existsSync,
2387
+ readFile: options.readFile ?? defaultReadFile,
1792
2388
  writeConfig: options.writeConfig ?? ((path, contents) => {
1793
2389
  mkdirSync(dirname(path), { recursive: true });
1794
2390
  writeFileSync(path, contents, "utf8");
@@ -1799,7 +2395,7 @@ function resolveInitDeps(options) {
1799
2395
  readEnv: options.readEnv ?? defaultReadEnv,
1800
2396
  readOwnVersion: options.readOwnVersion ?? readOwnVersion,
1801
2397
  writeEnv: options.writeEnv ?? defaultWriteEnv,
1802
- isInteractive: options.isInteractive ?? defaultIsInteractive,
2398
+ isInteractive: options.yes ? () => false : options.isInteractive ?? defaultIsInteractive,
1803
2399
  login: options.login ?? ((cwd) => runLogin({
1804
2400
  apiUrl: process.env.CLIVLY_API_URL ?? "https://api.clivly.com",
1805
2401
  cwd,
@@ -1823,7 +2419,7 @@ async function acquireSchema(options, deps) {
1823
2419
  const orm = detectOrm(appDeps);
1824
2420
  const auth = detectAuth(appDeps);
1825
2421
  deps.framework = framework;
1826
- deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? await deps.probeAuth(options.cwd) : null);
2422
+ deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
1827
2423
  out("\nClivly init\n");
1828
2424
  out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
1829
2425
  out(` ORM: ${orm}\n`);
@@ -1873,14 +2469,18 @@ function mutationsFor(options, deps, plan) {
1873
2469
  framework: deps.framework,
1874
2470
  authWiring: deps.authWiring,
1875
2471
  plan,
1876
- exists: deps.configExists,
2472
+ readFile: deps.readFile,
1877
2473
  readEnv: deps.readEnv
1878
2474
  });
1879
2475
  }
1880
2476
  function applyMutations(options, deps, mutations) {
1881
2477
  for (const file of mutations.files) {
1882
- if (file.action === "skip") {
1883
- deps.out(` • ${file.path} exists — skipped (pass --force to overwrite)\n`);
2478
+ if (file.action === "unchanged") {
2479
+ deps.out(` · ${file.path} already up to date\n`);
2480
+ continue;
2481
+ }
2482
+ if (file.action === "conflict") {
2483
+ deps.out(` ! ${file.path} differs from what init would write — left alone (--diff to see, --force to overwrite)\n`);
1884
2484
  continue;
1885
2485
  }
1886
2486
  deps.writeConfig(join(options.cwd, file.path), file.contents);
@@ -1893,29 +2493,42 @@ function applyMutations(options, deps, mutations) {
1893
2493
  deps.writeEnv(options.cwd, mutations.env.contents);
1894
2494
  deps.out(" ✓ .env placeholders\n");
1895
2495
  }
1896
- function previewMutations(deps, mutations) {
2496
+ const DIFF_CONTEXT_LIMIT = 40;
2497
+ function renderDiff(existing, next) {
2498
+ const before = existing.split("\n");
2499
+ const after = next.split("\n");
2500
+ const lines = [];
2501
+ for (const line of before.filter((l) => !after.includes(l))) lines.push(` - ${line}`);
2502
+ for (const line of after.filter((l) => !before.includes(l))) lines.push(` + ${line}`);
2503
+ return lines.length > DIFF_CONTEXT_LIMIT ? [...lines.slice(0, DIFF_CONTEXT_LIMIT), ` … ${lines.length - DIFF_CONTEXT_LIMIT} more lines`] : lines;
2504
+ }
2505
+ function previewMutations(deps, mutations, options) {
1897
2506
  const { out } = deps;
1898
2507
  out("\nDry run — nothing was written.\n\n");
1899
2508
  for (const file of mutations.files) {
1900
- const verb = {
1901
- create: "create",
1902
- overwrite: "overwrite",
1903
- skip: "skip"
1904
- }[file.action];
1905
2509
  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`);
2510
+ const suffix = {
2511
+ conflict: " (differs — --diff to see, --force to overwrite)",
2512
+ create: ` (${lines} lines)`,
2513
+ overwrite: ` (${lines} lines)`,
2514
+ unchanged: " (already up to date)"
2515
+ }[file.action];
2516
+ out(` ${file.action.padEnd(9)} ${file.path}${suffix}\n`);
2517
+ if (options.diff && file.action === "conflict") {
2518
+ const existing = deps.readFile(join(options.cwd, file.path)) ?? "";
2519
+ for (const line of renderDiff(existing, file.contents)) out(`${line}\n`);
2520
+ }
1908
2521
  }
1909
2522
  if (mutations.env.addedKeys.length === 0) out(" skip .env (every CLIVLY_* key already present)\n");
1910
2523
  else {
1911
2524
  out(` ${mutations.env.action.padEnd(9)} .env\n`);
1912
2525
  for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
1913
2526
  }
1914
- out("\nRe-run without --dry-run to apply.\n");
2527
+ 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
2528
  }
1916
2529
  function printNextSteps(out, authComplete, loginRan) {
1917
2530
  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");
2531
+ 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
2532
  out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
1920
2533
  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
2534
  const step = authComplete ? 3 : 4;
@@ -1923,7 +2536,7 @@ function printNextSteps(out, authComplete, loginRan) {
1923
2536
  out(` ${step}. Run \`clivly status\` to see where you are\n`);
1924
2537
  }
1925
2538
  function credentialsMissing(envContents) {
1926
- return !isKeySet(envContents, "CLIVLY_API_KEY");
2539
+ return !(isKeySet(envContents, "CLIVLY_SECRET_KEY") || isKeySet(envContents, "CLIVLY_API_KEY"));
1927
2540
  }
1928
2541
  function pairingSkipReason(options, deps) {
1929
2542
  if (options.noLogin) return "--no-login";
@@ -1941,9 +2554,12 @@ async function scaffold(options, deps, plan) {
1941
2554
  const pm = deps.detectPackageManager(options.cwd);
1942
2555
  const packages = [resolveRuntimeInstallSpec(deps), ...deps.authWiring.packages];
1943
2556
  const mutations = mutationsFor(options, deps, plan);
1944
- if (options.dryRun) {
2557
+ if (options.dryRun || options.diff) {
1945
2558
  out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
1946
- previewMutations(deps, mutations);
2559
+ previewMutations(deps, mutations, {
2560
+ cwd: options.cwd,
2561
+ diff: options.diff
2562
+ });
1947
2563
  return 0;
1948
2564
  }
1949
2565
  const result = await deps.install({
@@ -1959,11 +2575,11 @@ async function scaffold(options, deps, plan) {
1959
2575
  const skipReason = pairingSkipReason(options, deps);
1960
2576
  if (skipReason) {
1961
2577
  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");
2578
+ out(" Run `clivly login` from a terminal to write CLIVLY_SECRET_KEY and CLIVLY_SYNC_TRIGGER_SECRET.\n");
1963
2579
  } else loginRan = await deps.login(options.cwd) === 0;
1964
2580
  }
1965
2581
  printNextSteps(out, deps.authWiring.complete, loginRan);
1966
- return plan.contactDetected ? 0 : 1;
2582
+ return 0;
1967
2583
  }
1968
2584
  function normalizeAppRoot(options) {
1969
2585
  const resolution = (options.resolveTargetApp ?? resolveTargetApp)(options.cwd);
@@ -1993,9 +2609,45 @@ async function runInit(options) {
1993
2609
  const deps = resolveInitDeps(resolvedOptions);
1994
2610
  const acquired = await acquireSchema(resolvedOptions, deps);
1995
2611
  if ("code" in acquired) return acquired.code;
1996
- const plan = planInit(acquired);
1997
- printPlan(deps.out, plan);
1998
- return scaffold(resolvedOptions, deps, plan);
2612
+ const dbOverride = importOverride(resolvedOptions.dbImportFlag, "db");
2613
+ const plan = planInit(dbOverride ? {
2614
+ ...acquired,
2615
+ db: dbOverride
2616
+ } : acquired, {
2617
+ contactTable: resolvedOptions.contactTableFlag,
2618
+ companyTable: resolvedOptions.companyTableFlag
2619
+ });
2620
+ const { contact } = plan;
2621
+ if (!contact) {
2622
+ printNoContactGuidance(deps.out, plan, resolvedOptions);
2623
+ return resolvedOptions.dryRun ? 0 : 1;
2624
+ }
2625
+ const renderable = {
2626
+ ...plan,
2627
+ contact
2628
+ };
2629
+ printPlan(deps.out, renderable);
2630
+ return scaffold(resolvedOptions, deps, renderable);
2631
+ }
2632
+ /**
2633
+ * What to do when no contact entity could be mapped. Lists the tables actually
2634
+ * discovered and the flag that picks one, so the next command succeeds rather
2635
+ * than leaving the developer to guess.
2636
+ */
2637
+ function printNoContactGuidance(out, plan, options) {
2638
+ out("No usable contact entity\n\n");
2639
+ for (const todo of plan.todos) out(` • ${todo}\n`);
2640
+ if (plan.tableNames.length === 0) {
2641
+ out(`\n No tables were found in ${plan.importPath}.\n`);
2642
+ out(" Check that --schema points at a module exporting Drizzle tables.\n\n");
2643
+ return;
2644
+ }
2645
+ out(`\n Tables in ${plan.importPath}:\n`);
2646
+ for (const name of plan.tableNames) out(` ${name}\n`);
2647
+ const schemaArg = options.schemaFlag ? ` --schema ${options.schemaFlag}` : "";
2648
+ out("\n Pick the one that holds your people and re-run:\n");
2649
+ out(` clivly init${schemaArg} --contact-table ${plan.tableNames[0]}\n\n`);
2650
+ out(" Nothing was written — Clivly needs one entity before a config can load.\n\n");
1999
2651
  }
2000
2652
  function hasCloudflaredOnPath() {
2001
2653
  const pathEnv = process.env.PATH ?? "";
@@ -2155,16 +2807,30 @@ function runInitFromArgv(argv, cwd) {
2155
2807
  cwd,
2156
2808
  force: argv.includes("--force"),
2157
2809
  dryRun: argv.includes("--dry-run"),
2810
+ diff: argv.includes("--diff"),
2811
+ yes: argv.includes("--yes"),
2158
2812
  noLogin: argv.includes("--no-login"),
2159
2813
  openBrowser: !argv.includes("--no-browser"),
2160
- schemaFlag: flagValue(argv, "--schema")
2814
+ schemaFlag: flagValue(argv, "--schema"),
2815
+ dbImportFlag: flagValue(argv, "--db-import"),
2816
+ authImportFlag: flagValue(argv, "--auth-import"),
2817
+ contactTableFlag: flagValue(argv, "--contact-table"),
2818
+ companyTableFlag: flagValue(argv, "--company-table")
2161
2819
  });
2162
2820
  }
2163
2821
  async function main(argv) {
2164
2822
  const [command, ...rest] = argv;
2165
- loadEnv(process.cwd());
2823
+ let cliEnvFiles = [];
2824
+ try {
2825
+ cliEnvFiles = (await loadCliConfig(process.cwd()))?.envFiles ?? [];
2826
+ } catch (error) {
2827
+ process.stderr.write(`${error.message}\n`);
2828
+ return 1;
2829
+ }
2830
+ loadEnv(process.cwd(), process.env, { files: [...cliEnvFiles, ...flagValues(rest, "--env-file")] });
2166
2831
  switch (command) {
2167
2832
  case "init": return await runInitFromArgv(rest, process.cwd());
2833
+ case "add": return runAdd(rest, process.cwd());
2168
2834
  case "push-schema": return await runPushSchema({
2169
2835
  cwd: process.cwd(),
2170
2836
  configPath: flagValue(rest, "--config")
@@ -2174,14 +2840,14 @@ async function main(argv) {
2174
2840
  case "doctor":
2175
2841
  case "connect": return await runStatus({
2176
2842
  cwd: process.cwd(),
2177
- apiKey: process.env.CLIVLY_API_KEY,
2843
+ apiKey: readSecretKeyEnv(process.env),
2178
2844
  apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
2179
2845
  configPath: flagValue(rest, "--config"),
2180
2846
  only: ALIAS_STAGE_IDS[command],
2181
2847
  verbose: rest.includes("--verbose")
2182
2848
  });
2183
2849
  case "dev": {
2184
- const apiKey = process.env.CLIVLY_API_KEY;
2850
+ const apiKey = readSecretKeyEnv(process.env);
2185
2851
  const apiUrl = flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL ?? "https://api.clivly.com";
2186
2852
  return await runDev({
2187
2853
  cwd: process.cwd(),
@@ -2209,7 +2875,7 @@ async function main(argv) {
2209
2875
  case void 0:
2210
2876
  case "help":
2211
2877
  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");
2878
+ 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
2879
  return 0;
2214
2880
  default: {
2215
2881
  process.stdout.write(`Unknown command: ${command}\n`);
@@ -2224,4 +2890,4 @@ if (isEntrypoint(process.argv[1], import.meta.url)) main(process.argv.slice(2)).
2224
2890
  process.exitCode = code;
2225
2891
  });
2226
2892
  //#endregion
2227
- export { main, nearestCommand, runInit, runInitFromArgv };
2893
+ export { credentialsMissing, main, nearestCommand, runAdd, runInit, runInitFromArgv };