clivly 0.4.0 → 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 +87 -6
  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 +943 -228
  75. package/index.d.cts +32 -2
  76. package/index.d.mts +32 -2
  77. package/index.mjs +932 -221
  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 +156 -3
  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
@@ -1389,49 +1783,16 @@ function parentDir(dir) {
1389
1783
  const parent = (0, node_path.dirname)(dir);
1390
1784
  return parent === dir ? null : parent;
1391
1785
  }
1392
- function findNearestPackageDir(start) {
1786
+ function findNearestPackageDir(start, boundary) {
1393
1787
  let current = (0, node_path.resolve)(start);
1394
1788
  while (true) {
1789
+ if (current === boundary) return null;
1395
1790
  if ((0, node_fs.existsSync)((0, node_path.join)(current, "package.json"))) return current;
1396
1791
  const parent = parentDir(current);
1397
1792
  if (!parent) return null;
1398
1793
  current = parent;
1399
1794
  }
1400
1795
  }
1401
- function readWorkspaceGlobs(dir) {
1402
- const pkg = readPackageJson(dir);
1403
- const workspaces = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
1404
- if (Array.isArray(workspaces) && workspaces.length > 0) return workspaces;
1405
- const pnpmWorkspacePath = (0, node_path.join)(dir, "pnpm-workspace.yaml");
1406
- if (!(0, node_fs.existsSync)(pnpmWorkspacePath)) return [];
1407
- try {
1408
- 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("!"));
1409
- } catch {
1410
- return [];
1411
- }
1412
- }
1413
- function expandWorkspaceGlob(root, pattern) {
1414
- if (pattern.endsWith("/*")) {
1415
- const base = (0, node_path.join)(root, pattern.slice(0, -2));
1416
- if (!(0, node_fs.existsSync)(base)) return [];
1417
- try {
1418
- 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")));
1419
- } catch {
1420
- return [];
1421
- }
1422
- }
1423
- const exact = (0, node_path.join)(root, pattern);
1424
- return (0, node_fs.existsSync)((0, node_path.join)(exact, "package.json")) ? [exact] : [];
1425
- }
1426
- function findWorkspaceRoot(start) {
1427
- let current = (0, node_path.resolve)(start);
1428
- while (true) {
1429
- if (readWorkspaceGlobs(current).length > 0) return current;
1430
- const parent = parentDir(current);
1431
- if (!parent) return null;
1432
- current = parent;
1433
- }
1434
- }
1435
1796
  function uniquePaths(paths) {
1436
1797
  return [...new Set(paths.map((path) => (0, node_path.resolve)(path)))];
1437
1798
  }
@@ -1444,13 +1805,14 @@ function rankCandidates(paths, start) {
1444
1805
  return distance(a) - distance(b) || a.localeCompare(b);
1445
1806
  });
1446
1807
  }
1447
- function resolveTargetApp(start) {
1448
- const nearestPackageDir = findNearestPackageDir(start);
1808
+ function resolveTargetApp(start, boundary = (0, node_os.homedir)()) {
1809
+ const stop = (0, node_path.resolve)(boundary);
1810
+ const nearestPackageDir = findNearestPackageDir(start, stop);
1449
1811
  if (nearestPackageDir && isDrizzleApp(nearestPackageDir)) return {
1450
1812
  cwd: nearestPackageDir,
1451
1813
  reason: "direct"
1452
1814
  };
1453
- const workspaceRoot = findWorkspaceRoot(start);
1815
+ const workspaceRoot = findWorkspaceRoot(start, stop);
1454
1816
  if (!workspaceRoot) return {
1455
1817
  cwd: nearestPackageDir ?? (0, node_path.resolve)(start),
1456
1818
  reason: "direct"
@@ -1486,7 +1848,7 @@ function resolveTargetApp(start) {
1486
1848
  }
1487
1849
  //#endregion
1488
1850
  //#region src/status-ladder.ts
1489
- const SATISFYING = new Set(["pass", "warn"]);
1851
+ const SATISFYING = /* @__PURE__ */ new Set(["pass", "warn"]);
1490
1852
  function unmetPrerequisites(stage, satisfied) {
1491
1853
  return (stage.needs ?? []).filter((id) => !satisfied.has(id));
1492
1854
  }
@@ -1536,10 +1898,118 @@ function ladderExitCode(results) {
1536
1898
  return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
1537
1899
  }
1538
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
1539
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
+ }
1540
2010
  const REASON_HINT = {
1541
2011
  ok: "",
1542
- 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).",
1543
2013
  forbidden: "The key is valid but lacks access to this org (403).",
1544
2014
  rate_limited: "Rate limited (429). Wait a moment and retry.",
1545
2015
  server_error: "Clivly returned a server error (5xx). Retry shortly.",
@@ -1552,12 +2022,12 @@ const apiKeyStage = {
1552
2022
  run(ctx) {
1553
2023
  if (ctx.apiKey) return Promise.resolve({
1554
2024
  state: "pass",
1555
- detail: "CLIVLY_API_KEY is set."
2025
+ detail: "Secret key is set."
1556
2026
  });
1557
2027
  return Promise.resolve({
1558
2028
  state: "fail",
1559
- detail: "CLIVLY_API_KEY is not set.",
1560
- 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."
1561
2031
  });
1562
2032
  }
1563
2033
  };
@@ -1598,12 +2068,38 @@ const configStage = {
1598
2068
  }
1599
2069
  }
1600
2070
  };
1601
- const LADDER_OWNED_CHECKS = new Set(["API key present", "Heartbeat"]);
2071
+ const LADDER_OWNED_CHECKS = /* @__PURE__ */ new Set(["API key present", "Heartbeat"]);
1602
2072
  /** The full ladder, in the order a developer completes setup. */
1603
2073
  const STATUS_STAGES = [
1604
2074
  apiKeyStage,
1605
2075
  heartbeatStage,
1606
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
+ },
1607
2103
  {
1608
2104
  id: "setup-checks",
1609
2105
  label: "Setup checks",
@@ -1651,7 +2147,11 @@ function defaultLoadSdk(path) {
1651
2147
  const ALIAS_STAGE_IDS = {
1652
2148
  status: STATUS_STAGES.map((stage) => stage.id),
1653
2149
  ping: ["api-key", "heartbeat"],
1654
- doctor: ["config", "setup-checks"],
2150
+ doctor: [
2151
+ "config",
2152
+ "runtime",
2153
+ "setup-checks"
2154
+ ],
1655
2155
  connect: [
1656
2156
  "api-key",
1657
2157
  "heartbeat",
@@ -1710,6 +2210,48 @@ function flagValue(argv, flag) {
1710
2210
  const index = argv.indexOf(flag);
1711
2211
  return index >= 0 ? argv[index + 1] : void 0;
1712
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
+ }
2221
+ const KNOWN_COMMANDS = [
2222
+ "init",
2223
+ "status",
2224
+ "push-schema",
2225
+ "dev",
2226
+ "login",
2227
+ "help"
2228
+ ];
2229
+ function editDistance(a, b) {
2230
+ const m = a.length;
2231
+ const n = b.length;
2232
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
2233
+ let curr = Array.from({ length: n + 1 }, () => 0);
2234
+ for (let i = 1; i <= m; i++) {
2235
+ curr[0] = i;
2236
+ for (let j = 1; j <= n; j++) {
2237
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2238
+ curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
2239
+ }
2240
+ [prev, curr] = [curr, prev];
2241
+ }
2242
+ return prev[n] ?? 0;
2243
+ }
2244
+ function nearestCommand(input) {
2245
+ let best;
2246
+ for (const command of KNOWN_COMMANDS) {
2247
+ const distance = editDistance(input, command);
2248
+ if (distance <= 2 && (!best || distance < best.distance)) best = {
2249
+ command,
2250
+ distance
2251
+ };
2252
+ }
2253
+ return best?.command;
2254
+ }
1713
2255
  function readDeps(cwd) {
1714
2256
  const pkgPath = (0, node_path.join)(cwd, "package.json");
1715
2257
  if (!(0, node_fs.existsSync)(pkgPath)) return {};
@@ -1723,6 +2265,59 @@ function readDeps(cwd) {
1723
2265
  return {};
1724
2266
  }
1725
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
+ }
1726
2321
  function readOwnVersion() {
1727
2322
  const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
1728
2323
  for (const rel of ["./package.json", "../package.json"]) try {
@@ -1730,6 +2325,35 @@ function readOwnVersion() {
1730
2325
  if (pkg.name === "clivly" && pkg.version) return pkg.version;
1731
2326
  } catch {}
1732
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
+ }
1733
2357
  function defaultReadEnv(cwd) {
1734
2358
  const path = (0, node_path.join)(cwd, ".env");
1735
2359
  return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : "";
@@ -1741,18 +2365,27 @@ function defaultIsInteractive() {
1741
2365
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !process.env.CI;
1742
2366
  }
1743
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
+ };
1744
2374
  return {
1745
2375
  out: options.out ?? ((line) => process.stdout.write(line)),
1746
2376
  readDepsFn: options.readDeps ?? readDeps,
2377
+ graphFor,
1747
2378
  resolvePath: options.resolveSchemaPath ?? ((cwd) => resolveSchemaPath({
1748
2379
  cwd,
1749
- flag: options.schemaFlag
2380
+ flag: options.schemaFlag,
2381
+ graph: graphFor(cwd)
1750
2382
  })),
1751
2383
  discover: options.discoverSchema ?? ((schemaPath, cwd) => discoverSchema({
1752
2384
  schemaPath,
1753
- cwd
2385
+ cwd,
2386
+ graph: graphFor(cwd)
1754
2387
  })),
1755
- configExists: options.configExists ?? node_fs.existsSync,
2388
+ readFile: options.readFile ?? defaultReadFile,
1756
2389
  writeConfig: options.writeConfig ?? ((path, contents) => {
1757
2390
  (0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
1758
2391
  (0, node_fs.writeFileSync)(path, contents, "utf8");
@@ -1763,7 +2396,7 @@ function resolveInitDeps(options) {
1763
2396
  readEnv: options.readEnv ?? defaultReadEnv,
1764
2397
  readOwnVersion: options.readOwnVersion ?? readOwnVersion,
1765
2398
  writeEnv: options.writeEnv ?? defaultWriteEnv,
1766
- isInteractive: options.isInteractive ?? defaultIsInteractive,
2399
+ isInteractive: options.yes ? () => false : options.isInteractive ?? defaultIsInteractive,
1767
2400
  login: options.login ?? ((cwd) => runLogin({
1768
2401
  apiUrl: process.env.CLIVLY_API_URL ?? "https://api.clivly.com",
1769
2402
  cwd,
@@ -1780,14 +2413,14 @@ async function acquireSchema(options, deps) {
1780
2413
  const { out } = deps;
1781
2414
  const appDeps = deps.readDepsFn(options.cwd);
1782
2415
  if (Object.keys(appDeps).length === 0) {
1783
- out("No package.json found here. Run `clivly init` from your app root.\n");
2416
+ out(`No package.json found in ${options.cwd}. Run \`clivly init\` from inside an existing app — cd into your project first.\n`);
1784
2417
  return { code: 1 };
1785
2418
  }
1786
2419
  const framework = detectFramework(appDeps);
1787
2420
  const orm = detectOrm(appDeps);
1788
2421
  const auth = detectAuth(appDeps);
1789
2422
  deps.framework = framework;
1790
- 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);
1791
2424
  out("\nClivly init\n");
1792
2425
  out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
1793
2426
  out(` ORM: ${orm}\n`);
@@ -1837,14 +2470,18 @@ function mutationsFor(options, deps, plan) {
1837
2470
  framework: deps.framework,
1838
2471
  authWiring: deps.authWiring,
1839
2472
  plan,
1840
- exists: deps.configExists,
2473
+ readFile: deps.readFile,
1841
2474
  readEnv: deps.readEnv
1842
2475
  });
1843
2476
  }
1844
2477
  function applyMutations(options, deps, mutations) {
1845
2478
  for (const file of mutations.files) {
1846
- if (file.action === "skip") {
1847
- 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`);
1848
2485
  continue;
1849
2486
  }
1850
2487
  deps.writeConfig((0, node_path.join)(options.cwd, file.path), file.contents);
@@ -1857,29 +2494,42 @@ function applyMutations(options, deps, mutations) {
1857
2494
  deps.writeEnv(options.cwd, mutations.env.contents);
1858
2495
  deps.out(" ✓ .env placeholders\n");
1859
2496
  }
1860
- 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) {
1861
2507
  const { out } = deps;
1862
2508
  out("\nDry run — nothing was written.\n\n");
1863
2509
  for (const file of mutations.files) {
1864
- const verb = {
1865
- create: "create",
1866
- overwrite: "overwrite",
1867
- skip: "skip"
1868
- }[file.action];
1869
2510
  const lines = file.contents.split("\n").length;
1870
- const suffix = file.action === "skip" ? " (exists — pass --force to overwrite)" : ` (${lines} lines)`;
1871
- 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
+ }
1872
2522
  }
1873
2523
  if (mutations.env.addedKeys.length === 0) out(" skip .env (every CLIVLY_* key already present)\n");
1874
2524
  else {
1875
2525
  out(` ${mutations.env.action.padEnd(9)} .env\n`);
1876
2526
  for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
1877
2527
  }
1878
- 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");
1879
2529
  }
1880
2530
  function printNextSteps(out, authComplete, loginRan) {
1881
2531
  out("\nNext steps:\n");
1882
- 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");
1883
2533
  out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
1884
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");
1885
2535
  const step = authComplete ? 3 : 4;
@@ -1887,7 +2537,7 @@ function printNextSteps(out, authComplete, loginRan) {
1887
2537
  out(` ${step}. Run \`clivly status\` to see where you are\n`);
1888
2538
  }
1889
2539
  function credentialsMissing(envContents) {
1890
- return !isKeySet(envContents, "CLIVLY_API_KEY");
2540
+ return !(isKeySet(envContents, "CLIVLY_SECRET_KEY") || isKeySet(envContents, "CLIVLY_API_KEY"));
1891
2541
  }
1892
2542
  function pairingSkipReason(options, deps) {
1893
2543
  if (options.noLogin) return "--no-login";
@@ -1905,9 +2555,12 @@ async function scaffold(options, deps, plan) {
1905
2555
  const pm = deps.detectPackageManager(options.cwd);
1906
2556
  const packages = [resolveRuntimeInstallSpec(deps), ...deps.authWiring.packages];
1907
2557
  const mutations = mutationsFor(options, deps, plan);
1908
- if (options.dryRun) {
2558
+ if (options.dryRun || options.diff) {
1909
2559
  out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
1910
- previewMutations(deps, mutations);
2560
+ previewMutations(deps, mutations, {
2561
+ cwd: options.cwd,
2562
+ diff: options.diff
2563
+ });
1911
2564
  return 0;
1912
2565
  }
1913
2566
  const result = await deps.install({
@@ -1923,11 +2576,11 @@ async function scaffold(options, deps, plan) {
1923
2576
  const skipReason = pairingSkipReason(options, deps);
1924
2577
  if (skipReason) {
1925
2578
  out(`\nSkipped browser pairing (${skipReason}).\n`);
1926
- 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");
1927
2580
  } else loginRan = await deps.login(options.cwd) === 0;
1928
2581
  }
1929
2582
  printNextSteps(out, deps.authWiring.complete, loginRan);
1930
- return plan.contactDetected ? 0 : 1;
2583
+ return 0;
1931
2584
  }
1932
2585
  function normalizeAppRoot(options) {
1933
2586
  const resolution = (options.resolveTargetApp ?? resolveTargetApp)(options.cwd);
@@ -1957,9 +2610,45 @@ async function runInit(options) {
1957
2610
  const deps = resolveInitDeps(resolvedOptions);
1958
2611
  const acquired = await acquireSchema(resolvedOptions, deps);
1959
2612
  if ("code" in acquired) return acquired.code;
1960
- const plan = planInit(acquired);
1961
- printPlan(deps.out, plan);
1962
- 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");
1963
2652
  }
1964
2653
  function hasCloudflaredOnPath() {
1965
2654
  const pathEnv = process.env.PATH ?? "";
@@ -2119,16 +2808,30 @@ function runInitFromArgv(argv, cwd) {
2119
2808
  cwd,
2120
2809
  force: argv.includes("--force"),
2121
2810
  dryRun: argv.includes("--dry-run"),
2811
+ diff: argv.includes("--diff"),
2812
+ yes: argv.includes("--yes"),
2122
2813
  noLogin: argv.includes("--no-login"),
2123
2814
  openBrowser: !argv.includes("--no-browser"),
2124
- 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")
2125
2820
  });
2126
2821
  }
2127
2822
  async function main(argv) {
2128
2823
  const [command, ...rest] = argv;
2129
- 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")] });
2130
2832
  switch (command) {
2131
2833
  case "init": return await runInitFromArgv(rest, process.cwd());
2834
+ case "add": return runAdd(rest, process.cwd());
2132
2835
  case "push-schema": return await runPushSchema({
2133
2836
  cwd: process.cwd(),
2134
2837
  configPath: flagValue(rest, "--config")
@@ -2138,14 +2841,14 @@ async function main(argv) {
2138
2841
  case "doctor":
2139
2842
  case "connect": return await runStatus({
2140
2843
  cwd: process.cwd(),
2141
- apiKey: process.env.CLIVLY_API_KEY,
2844
+ apiKey: readSecretKeyEnv(process.env),
2142
2845
  apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
2143
2846
  configPath: flagValue(rest, "--config"),
2144
2847
  only: ALIAS_STAGE_IDS[command],
2145
2848
  verbose: rest.includes("--verbose")
2146
2849
  });
2147
2850
  case "dev": {
2148
- const apiKey = process.env.CLIVLY_API_KEY;
2851
+ const apiKey = readSecretKeyEnv(process.env);
2149
2852
  const apiUrl = flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL ?? "https://api.clivly.com";
2150
2853
  return await runDev({
2151
2854
  cwd: process.cwd(),
@@ -2166,19 +2869,31 @@ async function main(argv) {
2166
2869
  openBrowser: !rest.includes("--no-browser"),
2167
2870
  deps: defaultLoginDeps()
2168
2871
  });
2872
+ case "--version":
2873
+ case "-v":
2874
+ process.stdout.write(`${readOwnVersion() ?? "unknown"}\n`);
2875
+ return 0;
2169
2876
  case void 0:
2170
2877
  case "help":
2171
2878
  case "--help":
2172
- 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");
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");
2173
2880
  return 0;
2174
- default:
2881
+ default: {
2175
2882
  process.stdout.write(`Unknown command: ${command}\n`);
2883
+ const suggestion = nearestCommand(command);
2884
+ if (suggestion) process.stdout.write(`Did you mean \`clivly ${suggestion}\`?\n`);
2885
+ process.stdout.write("Run `clivly --help` to see available commands.\n");
2176
2886
  return 1;
2887
+ }
2177
2888
  }
2178
2889
  }
2179
2890
  if (isEntrypoint(process.argv[1], require("url").pathToFileURL(__filename).href)) main(process.argv.slice(2)).then((code) => {
2180
2891
  process.exitCode = code;
2181
2892
  });
2182
2893
  //#endregion
2894
+ exports.credentialsMissing = credentialsMissing;
2895
+ exports.main = main;
2896
+ exports.nearestCommand = nearestCommand;
2897
+ exports.runAdd = runAdd;
2183
2898
  exports.runInit = runInit;
2184
2899
  exports.runInitFromArgv = runInitFromArgv;