clivly 0.4.1 → 0.7.1

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