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