clivly 0.2.0 → 0.2.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 (116) hide show
  1. package/README.md +52 -3
  2. package/adapter-nLfZpgIG.d.cts +40 -0
  3. package/adapter-xs04z0KX.d.mts +40 -0
  4. package/auth-adapter-CBOKDhlc.cjs +11 -0
  5. package/auth-adapter-FEOR1nx-.d.mts +30 -0
  6. package/auth-adapter-wqnnEjo3.mjs +6 -0
  7. package/auth-adapter-wvfGKsOJ.d.cts +30 -0
  8. package/auth-better-auth.cjs +28 -0
  9. package/auth-better-auth.d.cts +33 -0
  10. package/auth-better-auth.d.mts +33 -0
  11. package/auth-better-auth.mjs +26 -0
  12. package/auth-clerk.cjs +34 -0
  13. package/auth-clerk.d.cts +32 -0
  14. package/auth-clerk.d.mts +32 -0
  15. package/auth-clerk.mjs +32 -0
  16. package/auth-workos.cjs +47 -0
  17. package/auth-workos.d.cts +30 -0
  18. package/auth-workos.d.mts +30 -0
  19. package/auth-workos.mjs +45 -0
  20. package/core-adapter.cjs +0 -0
  21. package/core-adapter.d.cts +2 -0
  22. package/core-adapter.d.mts +2 -0
  23. package/core-adapter.mjs +1 -0
  24. package/core-auth-adapter.cjs +3 -0
  25. package/core-auth-adapter.d.cts +2 -0
  26. package/core-auth-adapter.d.mts +2 -0
  27. package/core-auth-adapter.mjs +2 -0
  28. package/core-drizzle.cjs +32 -0
  29. package/core-drizzle.d.cts +21 -0
  30. package/core-drizzle.d.mts +21 -0
  31. package/core-drizzle.mjs +31 -0
  32. package/core-entity-config.cjs +13 -0
  33. package/core-entity-config.d.cts +2 -0
  34. package/core-entity-config.d.mts +2 -0
  35. package/core-entity-config.mjs +2 -0
  36. package/core-entity-heuristics.cjs +442 -0
  37. package/core-entity-heuristics.d.cts +68 -0
  38. package/core-entity-heuristics.d.mts +68 -0
  39. package/core-entity-heuristics.mjs +431 -0
  40. package/core-mapping-form.cjs +5 -0
  41. package/core-mapping-form.d.cts +2 -0
  42. package/core-mapping-form.d.mts +2 -0
  43. package/core-mapping-form.mjs +2 -0
  44. package/core-mapping-score.cjs +12 -0
  45. package/core-mapping-score.d.cts +2 -0
  46. package/core-mapping-score.d.mts +2 -0
  47. package/core-mapping-score.mjs +2 -0
  48. package/core-mappings-config.cjs +3 -0
  49. package/core-mappings-config.d.cts +2 -0
  50. package/core-mappings-config.d.mts +2 -0
  51. package/core-mappings-config.mjs +2 -0
  52. package/core-sync-engine.cjs +6 -0
  53. package/core-sync-engine.d.cts +2 -0
  54. package/core-sync-engine.d.mts +2 -0
  55. package/core-sync-engine.mjs +2 -0
  56. package/core-types.cjs +0 -0
  57. package/core-types.d.cts +2 -0
  58. package/core-types.d.mts +2 -0
  59. package/core-types.mjs +1 -0
  60. package/core-view-compiler.cjs +5 -0
  61. package/core-view-compiler.d.cts +2 -0
  62. package/core-view-compiler.d.mts +2 -0
  63. package/core-view-compiler.mjs +2 -0
  64. package/core.cjs +27 -0
  65. package/core.d.cts +9 -0
  66. package/core.d.mts +9 -0
  67. package/core.mjs +7 -0
  68. package/drizzle-meta-BGn0BqGb.cjs +9 -0
  69. package/drizzle-meta-CnC336L-.mjs +4 -0
  70. package/drizzle.cjs +107 -0
  71. package/drizzle.d.cts +28 -0
  72. package/drizzle.d.mts +28 -0
  73. package/drizzle.mjs +105 -0
  74. package/entity-config-Cvjo1cpv.cjs +4598 -0
  75. package/entity-config-D1DFyeUp.d.mts +2403 -0
  76. package/entity-config-DoR0khce.mjs +4533 -0
  77. package/entity-config-GczjD3y8.d.cts +2403 -0
  78. package/index.cjs +926 -0
  79. package/index.d.cts +56 -0
  80. package/index.d.mts +56 -0
  81. package/{dist/index.mjs → index.mjs} +430 -47
  82. package/mapping-form-BHtPexzI.mjs +47 -0
  83. package/mapping-form-CUlFVfG8.d.cts +35 -0
  84. package/mapping-form-DdmDebC3.cjs +64 -0
  85. package/mapping-form-kjSiJaCX.d.mts +35 -0
  86. package/mapping-score-A2lRnlpk.d.mts +49 -0
  87. package/mapping-score-BJNe0lFn.mjs +87 -0
  88. package/mapping-score-BcjC0ZQs.d.cts +49 -0
  89. package/mapping-score-D9gfhtRu.cjs +146 -0
  90. package/mappings-config-CYPrzB3K.mjs +99 -0
  91. package/mappings-config-DTu_2cyx.cjs +104 -0
  92. package/mappings-config-DjRLCb2Q.d.cts +42 -0
  93. package/mappings-config-GxiYsSBQ.d.mts +42 -0
  94. package/namespace-probe-BtS0nXcr.d.cts +299 -0
  95. package/namespace-probe-CYMie-BI.d.mts +299 -0
  96. package/package.json +223 -26
  97. package/sdk-BZUn6wzs.mjs +764 -0
  98. package/sdk-CP_mYZa5.cjs +793 -0
  99. package/sdk.cjs +7 -0
  100. package/sdk.d.cts +33 -0
  101. package/sdk.d.mts +33 -0
  102. package/sdk.mjs +2 -0
  103. package/sync-engine-DUc0qWgo.cjs +230 -0
  104. package/sync-engine-DWnQPjV3.d.mts +183 -0
  105. package/sync-engine-DedXngop.mjs +207 -0
  106. package/sync-engine-uhfzKM-e.d.cts +183 -0
  107. package/types-C4POUcvk.d.mts +158 -0
  108. package/types-LJTBhkcN.d.cts +158 -0
  109. package/view-compiler-B4UYTvRM.d.cts +71 -0
  110. package/view-compiler-BHzbI9b7.cjs +202 -0
  111. package/view-compiler-CosFGar8.mjs +185 -0
  112. package/view-compiler-i_xFf-L0.d.mts +71 -0
  113. package/vite.cjs +38 -0
  114. package/vite.d.cts +23 -0
  115. package/vite.d.mts +23 -0
  116. package/vite.mjs +37 -0
@@ -1,11 +1,83 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
- import { isAbsolute, join, relative, resolve } from "node:path";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
2
+ import { t as createClivlySDK } from "./sdk-BZUn6wzs.mjs";
3
+ import { t as CANONICAL_FIELDS } from "./entity-config-DoR0khce.mjs";
4
+ import "./mapping-score-BJNe0lFn.mjs";
5
+ import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./core-entity-heuristics.mjs";
6
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
7
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
8
  import { createJiti } from "jiti";
6
- import { createClivlySDK } from "@clivly/sdk";
7
- import { CANONICAL_FIELDS } from "@clivly/core/entity-config";
8
- import { matchesConcept, requiredFieldsFor, suggestCursorField, suggestFieldMap, suggestRelationships } from "@clivly/core/entity-heuristics";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { spawn } from "node:child_process";
11
+ //#region src/render.ts
12
+ function checkLine(ok, label, detail) {
13
+ return ` ${ok ? "✅" : "❌"} ${label}${detail ? ` — ${detail}` : ""}\n`;
14
+ }
15
+ //#endregion
16
+ //#region src/connect.ts
17
+ const REASON_HINT$1 = {
18
+ ok: "",
19
+ invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
20
+ forbidden: "The key lacks access (403).",
21
+ rate_limited: "Rate limited (429) — retry shortly.",
22
+ server_error: "Clivly returned a 5xx — retry shortly.",
23
+ client_error: "The request was rejected (4xx).",
24
+ network_error: "No response — check the network / apiUrl."
25
+ };
26
+ const defaultConnect$1 = (apiKey, apiUrl) => createClivlySDK({
27
+ apiKey,
28
+ apiUrl,
29
+ retry: { maxAttempts: 1 }
30
+ }).connect();
31
+ const defaultLoadConfig = async (path) => {
32
+ const mod = await createJiti(import.meta.url).import(path);
33
+ return mod.default ?? mod;
34
+ };
35
+ /**
36
+ * `clivly connect` — a read-only pass/fail report: API key present → heartbeat
37
+ * (real SDK connect, proving the cloud saw it) → config schema loadable →
38
+ * trigger-URL note. Exit 0 iff the first three pass; a missing trigger URL warns
39
+ * but does not fail (push-only setups are valid). Deps injectable for tests.
40
+ */
41
+ async function runConnect(options) {
42
+ const out = options.out ?? ((line) => process.stdout.write(line));
43
+ out("\nClivly connect\n");
44
+ if (!options.apiKey) {
45
+ out(checkLine(false, "CLIVLY_API_KEY is not set."));
46
+ out(" Set it to your sk_live_… key from Settings → API keys.\n");
47
+ return 1;
48
+ }
49
+ const result = await (options.connectFn ?? defaultConnect$1)(options.apiKey, options.apiUrl);
50
+ if (result.ok) out(checkLine(true, `Heartbeat reached Clivly — org: ${result.org?.name ?? "(unnamed org)"}${result.org?.id ? ` (${result.org.id})` : ""}`));
51
+ else {
52
+ const status = result.status === null ? "no response" : `HTTP ${result.status}`;
53
+ out(checkLine(false, `Heartbeat failed (${result.reason}, ${status})`));
54
+ out(` ${REASON_HINT$1[result.reason]}\n`);
55
+ return 1;
56
+ }
57
+ const path = resolve(options.cwd, options.configPath ?? "clivly.config.ts");
58
+ const loadConfig = options.loadConfigFn ?? defaultLoadConfig;
59
+ let config;
60
+ try {
61
+ config = await loadConfig(path);
62
+ } catch (error) {
63
+ out(checkLine(false, "Load clivly.config", error.message));
64
+ return 1;
65
+ }
66
+ const tables = Object.values(config.entities?.entities ?? {}).map((e) => e.source).filter((s) => Boolean(s));
67
+ if (tables.length === 0) {
68
+ out(checkLine(false, "No entities configured in clivly.config"));
69
+ return 1;
70
+ }
71
+ out(checkLine(true, `Schema: ${tables.length} entity table(s) — ${tables.join(", ")}`));
72
+ const triggerUrl = config.syncTrigger?.url;
73
+ if (triggerUrl) out(checkLine(true, `Trigger route configured — ${triggerUrl}`));
74
+ else {
75
+ out(checkLine(false, "No trigger URL — dashboard-triggered sync is not wired"));
76
+ out(" Set syncTrigger.url (CLIVLY_SYNC_TRIGGER_URL) to your public tick route.\n");
77
+ }
78
+ return 0;
79
+ }
80
+ //#endregion
9
81
  //#region src/detect.ts
10
82
  const FRAMEWORK_MARKERS = [
11
83
  {
@@ -59,6 +131,34 @@ function detectOrm(deps) {
59
131
  for (const { orm, deps: markers } of ORM_MARKERS) if (markers.some((dep) => dep in deps)) return orm;
60
132
  return "unknown";
61
133
  }
134
+ const LOCKFILES = [
135
+ {
136
+ file: "pnpm-lock.yaml",
137
+ pm: "pnpm"
138
+ },
139
+ {
140
+ file: "bun.lockb",
141
+ pm: "bun"
142
+ },
143
+ {
144
+ file: "yarn.lock",
145
+ pm: "yarn"
146
+ },
147
+ {
148
+ file: "package-lock.json",
149
+ pm: "npm"
150
+ }
151
+ ];
152
+ function detectPackageManager(cwd, fileExists = existsSync) {
153
+ for (const { file, pm } of LOCKFILES) if (fileExists(join(cwd, file))) return pm;
154
+ return "npm";
155
+ }
156
+ function addCommand(pm, packages) {
157
+ return {
158
+ command: pm,
159
+ args: [pm === "npm" ? "install" : "add", ...packages]
160
+ };
161
+ }
62
162
  //#endregion
63
163
  //#region src/discover-schema.ts
64
164
  const WINDOWS_SEP = /\\/g;
@@ -81,20 +181,60 @@ function defaultLoader(cwd) {
81
181
  const jiti = createJiti(pathToFileURL(join(cwd, "clivly.config.ts")).href);
82
182
  return (specifier) => jiti.import(specifier);
83
183
  }
184
+ async function loadPgCoreRuntime(load) {
185
+ try {
186
+ return await load("drizzle-orm/pg-core");
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+ function buildFkByColumn(pgCore, drizzle, table) {
192
+ const fkByColumn = /* @__PURE__ */ new Map();
193
+ if (!pgCore) return fkByColumn;
194
+ try {
195
+ for (const fk of pgCore.getTableConfig(table).foreignKeys) {
196
+ const ref = fk.reference();
197
+ const foreignTable = drizzle.getTableName(ref.foreignTable);
198
+ ref.columns.forEach((col, i) => {
199
+ const foreignCol = ref.foreignColumns[i];
200
+ if (foreignCol) fkByColumn.set(col.name, {
201
+ table: foreignTable,
202
+ column: foreignCol.name
203
+ });
204
+ });
205
+ }
206
+ } catch {}
207
+ return fkByColumn;
208
+ }
209
+ function toDiscoveredColumn(key, col, fkByColumn) {
210
+ let type;
211
+ try {
212
+ type = col.getSQLType?.();
213
+ } catch {
214
+ type = void 0;
215
+ }
216
+ const references = fkByColumn.get(col.name);
217
+ return {
218
+ key,
219
+ dbName: col.name,
220
+ isPrimary: Boolean(col.primary),
221
+ type,
222
+ nullable: !col.notNull,
223
+ ...references ? { references } : {}
224
+ };
225
+ }
84
226
  async function discoverSchema(opts) {
85
227
  const load = opts.load ?? defaultLoader(opts.cwd);
86
228
  const mod = await loadSchemaModule(opts.schemaPath, load);
87
229
  const drizzle = await load("drizzle-orm");
230
+ const pgCore = await loadPgCoreRuntime(load);
88
231
  const importPath = toImportPath(opts.schemaPath, opts.cwd);
89
232
  const tables = [];
90
233
  let db = null;
91
234
  for (const [moduleKey, value] of Object.entries(mod)) {
92
235
  if (drizzle.is(value, drizzle.Table)) {
93
- const columns = Object.entries(drizzle.getTableColumns(value)).map(([key, col]) => ({
94
- key,
95
- dbName: col.name,
96
- isPrimary: Boolean(col.primary)
97
- }));
236
+ const fkByColumn = buildFkByColumn(pgCore, drizzle, value);
237
+ const columns = Object.entries(drizzle.getTableColumns(value)).map(([key, col]) => toDiscoveredColumn(key, col, fkByColumn));
98
238
  tables.push({
99
239
  moduleKey,
100
240
  tableName: drizzle.getTableName(value),
@@ -114,11 +254,6 @@ async function discoverSchema(opts) {
114
254
  };
115
255
  }
116
256
  //#endregion
117
- //#region src/render.ts
118
- function checkLine(ok, label, detail) {
119
- return ` ${ok ? "✅" : "❌"} ${label}${detail ? ` — ${detail}` : ""}\n`;
120
- }
121
- //#endregion
122
257
  //#region src/doctor.ts
123
258
  async function loadSdkFromConfig(path) {
124
259
  const mod = await createJiti(import.meta.url).import(path);
@@ -155,6 +290,66 @@ async function runDoctor(options) {
155
290
  return report.ok ? 0 : 1;
156
291
  }
157
292
  //#endregion
293
+ //#region src/env-file.ts
294
+ function hasKey(content, key) {
295
+ return new RegExp(`^\\s*${key}=`, "m").test(content);
296
+ }
297
+ /**
298
+ * Append only the `entries` whose key is absent, each preceded by its comment.
299
+ * Existing keys/values are left exactly as-is. Returns the full new content.
300
+ */
301
+ function appendEnvKeys(existing, entries) {
302
+ const missing = entries.filter((e) => !hasKey(existing, e.key));
303
+ if (missing.length === 0) return existing;
304
+ const block = missing.map((e) => `${e.comment ? `# ${e.comment}\n` : ""}${e.key}=`).join("\n");
305
+ return `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${existing.length > 0 ? "\n" : ""}# Clivly\n${block}\n`;
306
+ }
307
+ //#endregion
308
+ //#region src/install.ts
309
+ const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject) => {
310
+ const child = spawn(command, args, {
311
+ cwd,
312
+ stdio: "inherit"
313
+ });
314
+ child.on("error", reject);
315
+ child.on("close", (code) => resolvePromise(code ?? 0));
316
+ });
317
+ /**
318
+ * Best-effort install: run `<pm> add <packages>`. Never throws — returns
319
+ * `{ ok, command }` so the caller can print the exact command on failure and
320
+ * continue scaffolding.
321
+ */
322
+ async function installPackages(options) {
323
+ const { command, args } = addCommand(options.pm, options.packages);
324
+ const printable = `${command} ${args.join(" ")}`;
325
+ const run = options.run ?? defaultRun;
326
+ try {
327
+ return {
328
+ ok: await run(command, args, options.cwd) === 0,
329
+ command: printable
330
+ };
331
+ } catch {
332
+ return {
333
+ ok: false,
334
+ command: printable
335
+ };
336
+ }
337
+ }
338
+ //#endregion
339
+ //#region src/is-entry.ts
340
+ function isEntrypoint(argv1, moduleUrl, realpath = realpathSync) {
341
+ if (!argv1) return false;
342
+ const modulePath = fileURLToPath(moduleUrl);
343
+ const resolve = (path) => {
344
+ try {
345
+ return realpath(path);
346
+ } catch {
347
+ return path;
348
+ }
349
+ };
350
+ return resolve(argv1) === resolve(modulePath);
351
+ }
352
+ //#endregion
158
353
  //#region src/ping.ts
159
354
  const REASON_HINT = {
160
355
  ok: "",
@@ -202,6 +397,20 @@ async function runPing(options) {
202
397
  }
203
398
  //#endregion
204
399
  //#region src/plan-init.ts
400
+ function toScoredTable(table) {
401
+ return {
402
+ name: table.tableName,
403
+ columns: table.columns.map((c) => c.dbName),
404
+ columnsMeta: table.columns.map((c) => ({
405
+ name: c.dbName,
406
+ type: c.type,
407
+ nullable: c.nullable,
408
+ isPrimaryKey: c.isPrimary,
409
+ isForeignKey: c.references !== void 0,
410
+ ...c.references ? { references: c.references } : {}
411
+ }))
412
+ };
413
+ }
205
414
  function rankCandidates(tables, concept) {
206
415
  return tables.map((t) => ({
207
416
  t,
@@ -243,7 +452,7 @@ function chooseSyncFields(table, todos) {
243
452
  }
244
453
  function buildEntity(table, concept, todos) {
245
454
  const canonical = CANONICAL_FIELDS[concept];
246
- const suggested = suggestFieldMap(concept, table.columns.map((c) => c.dbName));
455
+ const suggested = toFieldMap(scoreFieldMap(concept, toScoredTable(table)));
247
456
  const fields = {};
248
457
  for (const [key, column] of Object.entries(suggested)) if (canonical.includes(key)) fields[key] = column;
249
458
  for (const required of requiredFieldsFor(concept)) if (!fields[required]) todos.push(`Could not map required ${concept} field "${required}" on "${table.tableName}" — add it manually.`);
@@ -282,17 +491,13 @@ function planInit(schema) {
282
491
  if (matchesConcept(companyTable.tableName, "contact")) todos.push(`"${companyTable.tableName}" matches both contact and company naming — assigned to company. Swap it to your contact entity if that's wrong.`);
283
492
  }
284
493
  if (contactTable) {
285
- const suggestions = suggestRelationships({
286
- name: contactTable.tableName,
287
- columns: contactTable.columns.map((c) => c.dbName)
288
- }, schema.tables.map((t) => ({
289
- name: t.tableName,
290
- columns: t.columns.map((c) => c.dbName)
291
- })));
292
- const only = suggestions[0];
293
- if (suggestions.length === 1 && only) contact.relationship = {
294
- name: only.name,
295
- via: only.via
494
+ const scored = scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
495
+ const top = scored[0];
496
+ const runnerUp = scored[1];
497
+ const isClearWinner = top !== void 0 && (runnerUp === void 0 || top.score - runnerUp.score >= 15);
498
+ if (top && isClearWinner) contact.relationship = {
499
+ name: top.value.name,
500
+ via: top.value.via
296
501
  };
297
502
  }
298
503
  return {
@@ -306,6 +511,24 @@ function planInit(schema) {
306
511
  }
307
512
  //#endregion
308
513
  //#region src/render-config.ts
514
+ const CLIVLY_ENV_KEYS = [
515
+ {
516
+ key: "CLIVLY_API_KEY",
517
+ comment: "Org API key — Settings → API keys"
518
+ },
519
+ {
520
+ key: "CLIVLY_SYNC_TRIGGER_URL",
521
+ comment: "Public URL of your mounted tick route (e.g. https://app.example.com/api/clivly/tick)"
522
+ },
523
+ {
524
+ key: "CLIVLY_SYNC_TRIGGER_SECRET",
525
+ comment: "Signing secret — Integrations → Remote sync"
526
+ },
527
+ {
528
+ key: "CLIVLY_VERIFY_TOKEN",
529
+ comment: "Bearer token Clivly presents to /clivly/auth/verify"
530
+ }
531
+ ];
309
532
  function renderFields(fields) {
310
533
  return `{ ${Object.entries(fields).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
311
534
  }
@@ -357,6 +580,12 @@ export default createClivlySDK({
357
580
  source: {
358
581
  ${sources.join(",\n")},
359
582
  },
583
+ syncTrigger: {
584
+ // The public URL Clivly Cloud calls to trigger a sync — your mounted tick route.
585
+ url: process.env.CLIVLY_SYNC_TRIGGER_URL,
586
+ secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
587
+ },
588
+ verifyToken: process.env.CLIVLY_VERIFY_TOKEN,
360
589
  });
361
590
  `;
362
591
  }
@@ -403,6 +632,108 @@ function resolveSchemaPath(opts) {
403
632
  return null;
404
633
  }
405
634
  //#endregion
635
+ //#region src/scaffold.ts
636
+ const TANSTACK_ROUTE_PREFIX = /^src\/routes\//;
637
+ const TS_EXTENSION = /\.ts$/;
638
+ const ROUTE_PATHS = {
639
+ nextjs: {
640
+ tick: "app/api/clivly/tick/route.ts",
641
+ verify: "app/api/clivly/auth/verify/route.ts"
642
+ },
643
+ "tanstack-start": {
644
+ tick: "src/routes/api/clivly/tick.ts",
645
+ verify: "src/routes/api/clivly/auth/verify.ts"
646
+ },
647
+ sveltekit: {
648
+ tick: "src/routes/api/clivly/tick/+server.ts",
649
+ verify: "src/routes/api/clivly/auth/verify/+server.ts"
650
+ },
651
+ remix: {
652
+ tick: "app/routes/api.clivly.tick.ts",
653
+ verify: "app/routes/api.clivly.auth.verify.ts"
654
+ },
655
+ nuxt: {
656
+ tick: "server/api/clivly/tick.ts",
657
+ verify: "server/api/clivly/auth/verify.ts"
658
+ },
659
+ unknown: null
660
+ };
661
+ function routePath(framework, kind) {
662
+ const paths = ROUTE_PATHS[framework];
663
+ if (!paths) throw new Error(`No route path for framework: ${framework}`);
664
+ return paths[kind];
665
+ }
666
+ function configImport(routeFilePath) {
667
+ const rel = relative(dirname(routeFilePath), "clivly.config").replace(/\\/g, "/");
668
+ return rel.startsWith(".") ? rel : `./${rel}`;
669
+ }
670
+ function renderBody(framework, importPath, verify) {
671
+ const preamble = verify ? `import clivly from "${importPath}";
672
+
673
+ // TODO(clivly): resolve the signed-in host user from the request (your auth stack).
674
+ const resolveUser = (_req: Request) => null;
675
+ const handler = clivly.createAuthVerifyHandler(resolveUser);
676
+ ` : `import clivly from "${importPath}";
677
+
678
+ const handler = clivly.createClivlyHandler();
679
+ `;
680
+ switch (framework) {
681
+ case "nextjs": return `${preamble}
682
+ export const POST = (request: Request) => handler(request);
683
+ `;
684
+ case "tanstack-start": return `import { createAPIFileRoute } from "@tanstack/react-start/api";
685
+ ${preamble}
686
+ export const APIRoute = createAPIFileRoute("__ROUTE__")({
687
+ POST: ({ request }) => handler(request),
688
+ });
689
+ `;
690
+ case "sveltekit": return `import type { RequestHandler } from "./$types";
691
+ ${preamble}
692
+ export const POST: RequestHandler = ({ request }) => handler(request);
693
+ `;
694
+ case "remix": return `import type { ActionFunctionArgs } from "@remix-run/node";
695
+ ${preamble}
696
+ export const action = ({ request }: ActionFunctionArgs) => handler(request);
697
+ `;
698
+ case "nuxt": return `import { toWebRequest } from "h3";
699
+ ${preamble}
700
+ export default defineEventHandler((event) => handler(toWebRequest(event)));
701
+ `;
702
+ default: throw new Error(`No route template for framework: ${framework}`);
703
+ }
704
+ }
705
+ function renderRouteFile(framework, kind) {
706
+ const path = routePath(framework, kind);
707
+ let body = renderBody(framework, configImport(path), kind === "verify");
708
+ if (framework === "tanstack-start") {
709
+ const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
710
+ body = body.replace("__ROUTE__", url);
711
+ }
712
+ return body;
713
+ }
714
+ const COMMON_FILES = [{
715
+ path: "clivly.config.ts",
716
+ description: "Your Clivly configuration",
717
+ owned: false
718
+ }];
719
+ function planScaffold(framework) {
720
+ const paths = ROUTE_PATHS[framework];
721
+ if (!paths) return [];
722
+ return [
723
+ {
724
+ path: paths.tick,
725
+ description: "Clivly sync-trigger route (createClivlyHandler)",
726
+ owned: true
727
+ },
728
+ {
729
+ path: paths.verify,
730
+ description: "Clivly auth-verify route (createAuthVerifyHandler) — finish resolveUser",
731
+ owned: true
732
+ },
733
+ ...COMMON_FILES
734
+ ];
735
+ }
736
+ //#endregion
406
737
  //#region src/index.ts
407
738
  function flagValue(argv, flag) {
408
739
  const index = argv.indexOf(flag);
@@ -421,6 +752,13 @@ function readDeps(cwd) {
421
752
  return {};
422
753
  }
423
754
  }
755
+ function defaultReadEnv(cwd) {
756
+ const path = join(cwd, ".env");
757
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
758
+ }
759
+ function defaultWriteEnv(cwd, contents) {
760
+ writeFileSync(join(cwd, ".env"), contents, "utf8");
761
+ }
424
762
  function resolveInitDeps(options) {
425
763
  return {
426
764
  out: options.out ?? ((line) => process.stdout.write(line)),
@@ -434,7 +772,15 @@ function resolveInitDeps(options) {
434
772
  cwd
435
773
  })),
436
774
  configExists: options.configExists ?? existsSync,
437
- writeConfig: options.writeConfig ?? ((path, contents) => writeFileSync(path, contents, "utf8"))
775
+ writeConfig: options.writeConfig ?? ((path, contents) => {
776
+ mkdirSync(dirname(path), { recursive: true });
777
+ writeFileSync(path, contents, "utf8");
778
+ }),
779
+ detectPackageManager: options.detectPackageManager ?? detectPackageManager,
780
+ install: options.installPackages ?? installPackages,
781
+ readEnv: options.readEnv ?? defaultReadEnv,
782
+ writeEnv: options.writeEnv ?? defaultWriteEnv,
783
+ framework: "unknown"
438
784
  };
439
785
  }
440
786
  async function acquireSchema(options, deps) {
@@ -446,6 +792,7 @@ async function acquireSchema(options, deps) {
446
792
  }
447
793
  const framework = detectFramework(appDeps);
448
794
  const orm = detectOrm(appDeps);
795
+ deps.framework = framework;
449
796
  out("\nClivly init\n");
450
797
  out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
451
798
  out(` ORM: ${orm}\n\n`);
@@ -453,6 +800,10 @@ async function acquireSchema(options, deps) {
453
800
  out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
454
801
  return { code: 1 };
455
802
  }
803
+ if (framework === "unknown") {
804
+ out("clivly init doesn't recognise your framework yet (looked for Next.js, TanStack Start, SvelteKit, Remix, Nuxt). Route file generation needs one of these.\n");
805
+ return { code: 1 };
806
+ }
456
807
  const schemaPath = deps.resolvePath(options.cwd);
457
808
  if (!schemaPath) {
458
809
  out("Could not locate your Drizzle schema. Pass --schema <path>, or add a `schema` field to drizzle.config.ts.\n");
@@ -483,20 +834,48 @@ function printPlan(out, plan) {
483
834
  out("\n");
484
835
  }
485
836
  }
486
- function writePlan(options, deps, plan) {
837
+ function writeOwnedFile(options, deps, path, contents) {
838
+ const full = join(options.cwd, path);
839
+ if (deps.configExists(full) && !options.force) {
840
+ deps.out(` • ${path} exists — skipped (pass --force to overwrite)\n`);
841
+ return;
842
+ }
843
+ deps.writeConfig(full, contents);
844
+ deps.out(` ✓ ${path}\n`);
845
+ }
846
+ function writeScaffoldFiles(options, deps, plan) {
847
+ writeOwnedFile(options, deps, "clivly.config.ts", renderInitConfig(plan));
848
+ for (const file of planScaffold(deps.framework)) {
849
+ if (file.path === "clivly.config.ts") continue;
850
+ const kind = file.path.includes("verify") ? "verify" : "tick";
851
+ writeOwnedFile(options, deps, file.path, renderRouteFile(deps.framework, kind));
852
+ }
853
+ }
854
+ function writeEnvFile(options, deps) {
855
+ const nextEnv = appendEnvKeys(deps.readEnv(options.cwd), CLIVLY_ENV_KEYS);
856
+ deps.writeEnv(options.cwd, nextEnv);
857
+ deps.out(" ✓ .env placeholders\n");
858
+ }
859
+ function printNextSteps(out) {
860
+ out("\nNext steps:\n");
861
+ out(" 1. Set the CLIVLY_* values in .env\n");
862
+ out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
863
+ out(" 3. Finish resolveUser in the auth/verify route\n");
864
+ out(" 4. Run `clivly connect` to verify\n");
865
+ }
866
+ async function scaffold(options, deps, plan) {
487
867
  const { out } = deps;
488
- const configPath = join(options.cwd, "clivly.config.ts");
489
- if (!options.write) {
490
- out("Dry run. Re-run with --write to scaffold clivly.config.ts.\n");
491
- return plan.contactDetected ? 0 : 1;
492
- }
493
- if (deps.configExists(configPath) && !options.force) {
494
- out("clivly.config.ts already exists pass --force to overwrite.\n");
495
- return 0;
496
- }
497
- deps.writeConfig(configPath, renderInitConfig(plan));
498
- out("Created clivly.config.ts\n");
499
- out("Next: set CLIVLY_API_KEY, address any TODOs above, then run `clivly doctor`.\n");
868
+ const pm = deps.detectPackageManager(options.cwd);
869
+ const result = await deps.install({
870
+ cwd: options.cwd,
871
+ pm,
872
+ packages: ["@clivly/sdk", "@clivly/core"]
873
+ });
874
+ if (result.ok) out(`Installed @clivly/sdk @clivly/core (${pm})\n`);
875
+ else out(`Could not install automatically — run:\n ${result.command}\n`);
876
+ writeScaffoldFiles(options, deps, plan);
877
+ writeEnvFile(options, deps);
878
+ printNextSteps(out);
500
879
  return plan.contactDetected ? 0 : 1;
501
880
  }
502
881
  async function runInit(options) {
@@ -505,15 +884,13 @@ async function runInit(options) {
505
884
  if ("code" in acquired) return acquired.code;
506
885
  const plan = planInit(acquired);
507
886
  printPlan(deps.out, plan);
508
- return writePlan(options, deps, plan);
887
+ return scaffold(options, deps, plan);
509
888
  }
510
889
  async function main(argv) {
511
890
  const [command, ...rest] = argv;
512
- const write = rest.includes("--write");
513
891
  switch (command) {
514
892
  case "init": return await runInit({
515
893
  cwd: process.cwd(),
516
- write,
517
894
  force: rest.includes("--force"),
518
895
  schemaFlag: flagValue(rest, "--schema")
519
896
  });
@@ -525,17 +902,23 @@ async function main(argv) {
525
902
  cwd: process.cwd(),
526
903
  configPath: flagValue(rest, "--config")
527
904
  });
905
+ case "connect": return await runConnect({
906
+ cwd: process.cwd(),
907
+ apiKey: process.env.CLIVLY_API_KEY,
908
+ apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
909
+ configPath: flagValue(rest, "--config")
910
+ });
528
911
  case void 0:
529
912
  case "help":
530
913
  case "--help":
531
- process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--write] [--force] [--schema <path>] Detect your Drizzle schema and generate clivly.config.ts\n clivly ping [--api-url <url>] Verify CLIVLY_API_KEY + reachability\n clivly doctor [--config <path>] Verify your clivly.config setup end-to-end\n");
914
+ process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--schema <path>] Scaffold Clivly (config, routes, .env) for your app\n clivly ping [--api-url <url>] Verify CLIVLY_API_KEY + reachability\n clivly doctor [--config <path>] Verify your clivly.config setup end-to-end\n clivly connect [--config <path>] One pass/fail check that setup is wired\n");
532
915
  return 0;
533
916
  default:
534
917
  process.stdout.write(`Unknown command: ${command}\n`);
535
918
  return 1;
536
919
  }
537
920
  }
538
- if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2)).then((code) => {
921
+ if (isEntrypoint(process.argv[1], import.meta.url)) main(process.argv.slice(2)).then((code) => {
539
922
  process.exitCode = code;
540
923
  });
541
924
  //#endregion
@@ -0,0 +1,47 @@
1
+ //#region ../core/src/mapping-form.ts
2
+ /**
3
+ * Build the filter for a discriminated entity: rows where `column` equals
4
+ * `value` belong to this entity (e.g. `role = "owner"`). Returns `undefined`
5
+ * when either side is blank so the caller can omit the filter entirely.
6
+ */
7
+ function buildFilterFromDiscriminator(column, value) {
8
+ const col = column?.trim();
9
+ const val = value.trim();
10
+ if (!col || val === "") return;
11
+ return { [col]: { eq: val } };
12
+ }
13
+ /**
14
+ * A stable identity string for a relationship's `via`. Two candidates on the
15
+ * same source table always differ in their join (`fkOn`+column, join-table
16
+ * keys, or raw SQL), so this disambiguates candidates the heuristic gives the
17
+ * same `name` (every contact→company candidate is named "company").
18
+ */
19
+ function relationshipViaSignature(via) {
20
+ if ("fkOn" in via) return `fk:${via.fkOn}:${via.column}`;
21
+ if ("through" in via) return `through:${via.through}:${via.localKey}:${via.foreignKey}`;
22
+ return `raw:${via.$raw}`;
23
+ }
24
+ /**
25
+ * Compile the selected relationship candidates into the persisted
26
+ * `RelationshipsMap`. Returns `undefined` for an empty selection so the caller
27
+ * omits the field.
28
+ */
29
+ function buildRelationships(candidates) {
30
+ if (candidates.length === 0) return;
31
+ const map = {};
32
+ for (const candidate of candidates) {
33
+ let key = candidate.name;
34
+ let suffix = 2;
35
+ while (key in map) {
36
+ key = `${candidate.name}_${suffix}`;
37
+ suffix++;
38
+ }
39
+ map[key] = {
40
+ entity: candidate.entityTable,
41
+ via: candidate.via
42
+ };
43
+ }
44
+ return map;
45
+ }
46
+ //#endregion
47
+ export { buildRelationships as n, relationshipViaSignature as r, buildFilterFromDiscriminator as t };