clivly 0.2.1 → 0.3.0-next.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 (50) hide show
  1. package/README.md +7 -5
  2. package/{adapter-nLfZpgIG.d.cts → adapter-CXr12l4K.d.cts} +7 -1
  3. package/{adapter-xs04z0KX.d.mts → adapter-CvNBD_3y.d.mts} +7 -1
  4. package/{auth-adapter-FEOR1nx-.d.mts → auth-adapter-CRBW4elj.d.mts} +1 -1
  5. package/{auth-adapter-wvfGKsOJ.d.cts → auth-adapter-CdLODn2p.d.cts} +1 -1
  6. package/auth-better-auth.d.cts +1 -1
  7. package/auth-better-auth.d.mts +1 -1
  8. package/auth-clerk.d.cts +1 -1
  9. package/auth-clerk.d.mts +1 -1
  10. package/auth-workos.d.cts +1 -1
  11. package/auth-workos.d.mts +1 -1
  12. package/core-adapter.d.cts +1 -1
  13. package/core-adapter.d.mts +1 -1
  14. package/core-auth-adapter.d.cts +1 -1
  15. package/core-auth-adapter.d.mts +1 -1
  16. package/core-entity-config.cjs +1 -1
  17. package/core-entity-config.mjs +1 -1
  18. package/core-entity-heuristics.cjs +2 -2
  19. package/core-entity-heuristics.mjs +2 -2
  20. package/core-mapping-score.cjs +1 -1
  21. package/core-mapping-score.mjs +1 -1
  22. package/core-mappings-config.cjs +1 -1
  23. package/core-mappings-config.mjs +1 -1
  24. package/core-types.d.cts +1 -1
  25. package/core-types.d.mts +1 -1
  26. package/core.cjs +2 -2
  27. package/core.d.cts +3 -3
  28. package/core.d.mts +3 -3
  29. package/core.mjs +2 -2
  30. package/drizzle.cjs +1 -1
  31. package/drizzle.mjs +1 -1
  32. package/index.cjs +702 -272
  33. package/index.d.cts +11 -0
  34. package/index.d.mts +11 -0
  35. package/index.mjs +700 -270
  36. package/{mappings-config-CYPrzB3K.mjs → mappings-config-CNTZRD4g.mjs} +1 -1
  37. package/{mappings-config-DTu_2cyx.cjs → mappings-config-DYZ_uOOB.cjs} +1 -1
  38. package/package.json +30 -4
  39. package/{sdk-CP_mYZa5.cjs → sdk-BNdK3KgW.cjs} +1 -1
  40. package/{sdk-BZUn6wzs.mjs → sdk-vu_uej2q.mjs} +1 -1
  41. package/sdk.cjs +1 -1
  42. package/sdk.mjs +1 -1
  43. package/{types-LJTBhkcN.d.cts → types-BpsKVccL.d.cts} +1 -0
  44. package/{types-C4POUcvk.d.mts → types-_qbJXMt_.d.mts} +1 -0
  45. /package/{drizzle-meta-CnC336L-.mjs → drizzle-meta-1z6Apnt-.mjs} +0 -0
  46. /package/{drizzle-meta-BGn0BqGb.cjs → drizzle-meta-CM5M73Z4.cjs} +0 -0
  47. /package/{entity-config-DoR0khce.mjs → entity-config-D3YpQB0z.mjs} +0 -0
  48. /package/{entity-config-Cvjo1cpv.cjs → entity-config-DMuZpchz.cjs} +0 -0
  49. /package/{mapping-score-BJNe0lFn.mjs → mapping-score-CPPvwoZC.mjs} +0 -0
  50. /package/{mapping-score-D9gfhtRu.cjs → mapping-score-i3p8LWDF.cjs} +0 -0
package/index.mjs CHANGED
@@ -1,83 +1,14 @@
1
1
  #!/usr/bin/env node
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";
2
+ import { t as CANONICAL_FIELDS } from "./entity-config-D3YpQB0z.mjs";
3
+ import "./mapping-score-CPPvwoZC.mjs";
5
4
  import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./core-entity-heuristics.mjs";
5
+ import { t as createClivlySDK } from "./sdk-vu_uej2q.mjs";
6
+ import { createRequire } from "node:module";
6
7
  import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
7
8
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
8
- import { createJiti } from "jiti";
9
9
  import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { createJiti } from "jiti";
10
11
  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
81
12
  //#region src/detect.ts
82
13
  const FRAMEWORK_MARKERS = [
83
14
  {
@@ -115,6 +46,24 @@ const ORM_MARKERS = [
115
46
  deps: ["kysely"]
116
47
  }
117
48
  ];
49
+ const AUTH_MARKERS = [
50
+ {
51
+ auth: "clerk",
52
+ deps: [
53
+ "@clerk/nextjs",
54
+ "@clerk/backend",
55
+ "@clerk/tanstack-react-start"
56
+ ]
57
+ },
58
+ {
59
+ auth: "better-auth",
60
+ deps: ["better-auth"]
61
+ },
62
+ {
63
+ auth: "workos",
64
+ deps: ["@workos-inc/node", "@workos-inc/authkit-nextjs"]
65
+ }
66
+ ];
118
67
  const FRAMEWORK_LABELS = {
119
68
  nextjs: "Next.js",
120
69
  "tanstack-start": "TanStack Start",
@@ -131,6 +80,10 @@ function detectOrm(deps) {
131
80
  for (const { orm, deps: markers } of ORM_MARKERS) if (markers.some((dep) => dep in deps)) return orm;
132
81
  return "unknown";
133
82
  }
83
+ function detectAuth(deps) {
84
+ for (const { auth, deps: markers } of AUTH_MARKERS) if (markers.some((dep) => dep in deps)) return auth;
85
+ return "unknown";
86
+ }
134
87
  const LOCKFILES = [
135
88
  {
136
89
  file: "pnpm-lock.yaml",
@@ -160,11 +113,28 @@ function addCommand(pm, packages) {
160
113
  };
161
114
  }
162
115
  //#endregion
116
+ //#region src/probe-module.ts
117
+ async function probeModule(opts) {
118
+ for (const candidate of opts.candidates) {
119
+ let mod;
120
+ try {
121
+ mod = await opts.load(candidate);
122
+ } catch {
123
+ continue;
124
+ }
125
+ for (const [moduleKey, value] of Object.entries(mod)) if (opts.match(value)) return {
126
+ moduleKey,
127
+ importPath: toImportPath(candidate, opts.cwd)
128
+ };
129
+ }
130
+ return null;
131
+ }
132
+ //#endregion
163
133
  //#region src/discover-schema.ts
164
- const WINDOWS_SEP = /\\/g;
134
+ const WINDOWS_SEP$1 = /\\/g;
165
135
  const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
166
136
  function toImportPath(schemaPath, cwd) {
167
- const rel = relative(cwd, schemaPath).replace(WINDOWS_SEP, "/").replace(MODULE_EXT, "");
137
+ const rel = relative(cwd, schemaPath).replace(WINDOWS_SEP$1, "/").replace(MODULE_EXT, "");
168
138
  return rel.startsWith(".") ? rel : `./${rel}`;
169
139
  }
170
140
  function isDirectory(path) {
@@ -177,7 +147,7 @@ function isDirectory(path) {
177
147
  function loadSchemaModule(schemaPath, load) {
178
148
  return load(isDirectory(schemaPath) ? join(schemaPath, "index.ts") : schemaPath);
179
149
  }
180
- function defaultLoader(cwd) {
150
+ function defaultLoader$1(cwd) {
181
151
  const jiti = createJiti(pathToFileURL(join(cwd, "clivly.config.ts")).href);
182
152
  return (specifier) => jiti.import(specifier);
183
153
  }
@@ -223,8 +193,20 @@ function toDiscoveredColumn(key, col, fkByColumn) {
223
193
  ...references ? { references } : {}
224
194
  };
225
195
  }
196
+ function isDrizzleDb(value) {
197
+ return typeof value?.select === "function";
198
+ }
199
+ const SIBLING_DB_NAMES = [
200
+ "db",
201
+ "client",
202
+ "index"
203
+ ];
204
+ function siblingDbCandidates(schemaPath) {
205
+ const dir = isDirectory(schemaPath) ? schemaPath : dirname(schemaPath);
206
+ return SIBLING_DB_NAMES.map((name) => join(dir, name));
207
+ }
226
208
  async function discoverSchema(opts) {
227
- const load = opts.load ?? defaultLoader(opts.cwd);
209
+ const load = opts.load ?? defaultLoader$1(opts.cwd);
228
210
  const mod = await loadSchemaModule(opts.schemaPath, load);
229
211
  const drizzle = await load("drizzle-orm");
230
212
  const pgCore = await loadPgCoreRuntime(load);
@@ -247,6 +229,12 @@ async function discoverSchema(opts) {
247
229
  importPath
248
230
  };
249
231
  }
232
+ if (db === null) db = await probeModule({
233
+ candidates: siblingDbCandidates(opts.schemaPath),
234
+ cwd: opts.cwd,
235
+ load,
236
+ match: isDrizzleDb
237
+ });
250
238
  return {
251
239
  tables,
252
240
  importPath,
@@ -254,57 +242,6 @@ async function discoverSchema(opts) {
254
242
  };
255
243
  }
256
244
  //#endregion
257
- //#region src/doctor.ts
258
- async function loadSdkFromConfig(path) {
259
- const mod = await createJiti(import.meta.url).import(path);
260
- const sdk = mod.default ?? mod;
261
- if (typeof sdk.doctor !== "function") throw new Error("default export is not a Clivly SDK instance");
262
- return sdk;
263
- }
264
- /**
265
- * `clivly doctor` — load the app's clivly.config, run the SDK's read-only setup
266
- * checks, and print a ✅/❌ checklist. Exit 0 when every check passes. `loadSdk`
267
- * and `out` are injectable for tests.
268
- */
269
- async function runDoctor(options) {
270
- const out = options.out ?? ((line) => process.stdout.write(line));
271
- const path = resolve(options.cwd, options.configPath ?? "clivly.config.ts");
272
- const loadSdk = options.loadSdk ?? loadSdkFromConfig;
273
- out("\nClivly doctor\n");
274
- let sdk;
275
- try {
276
- sdk = await loadSdk(path);
277
- } catch (error) {
278
- out(checkLine(false, "Load clivly.config", error.message));
279
- out(" Expected clivly.config.ts at your app root with `export default createClivlySDK({...})`.\n");
280
- return 1;
281
- }
282
- const report = await sdk.doctor();
283
- for (const check of report.checks) {
284
- if (check.skipped) {
285
- out(` ⚠️ ${check.name} — ${check.detail}\n`);
286
- continue;
287
- }
288
- out(checkLine(check.ok, check.name, check.detail));
289
- }
290
- return report.ok ? 0 : 1;
291
- }
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
245
  //#region src/install.ts
309
246
  const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject) => {
310
247
  const child = spawn(command, args, {
@@ -314,6 +251,16 @@ const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject)
314
251
  child.on("error", reject);
315
252
  child.on("close", (code) => resolvePromise(code ?? 0));
316
253
  });
254
+ const QUIET_FLAGS = {
255
+ npm: [
256
+ "--no-audit",
257
+ "--no-fund",
258
+ "--loglevel=error"
259
+ ],
260
+ pnpm: ["--reporter=silent"],
261
+ yarn: [],
262
+ bun: []
263
+ };
317
264
  /**
318
265
  * Best-effort install: run `<pm> add <packages>`. Never throws — returns
319
266
  * `{ ok, command }` so the caller can print the exact command on failure and
@@ -321,11 +268,12 @@ const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject)
321
268
  */
322
269
  async function installPackages(options) {
323
270
  const { command, args } = addCommand(options.pm, options.packages);
324
- const printable = `${command} ${args.join(" ")}`;
271
+ const fullArgs = [...args, ...QUIET_FLAGS[options.pm]];
272
+ const printable = `${command} ${fullArgs.join(" ")}`;
325
273
  const run = options.run ?? defaultRun;
326
274
  try {
327
275
  return {
328
- ok: await run(command, args, options.cwd) === 0,
276
+ ok: await run(command, fullArgs, options.cwd) === 0,
329
277
  command: printable
330
278
  };
331
279
  } catch {
@@ -350,52 +298,6 @@ function isEntrypoint(argv1, moduleUrl, realpath = realpathSync) {
350
298
  return resolve(argv1) === resolve(modulePath);
351
299
  }
352
300
  //#endregion
353
- //#region src/ping.ts
354
- const REASON_HINT = {
355
- ok: "",
356
- invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
357
- forbidden: "The key is valid but lacks access to this org (403).",
358
- rate_limited: "Rate limited (429). Wait a moment and retry.",
359
- server_error: "Clivly returned a server error (5xx). Retry shortly.",
360
- client_error: "The request was rejected (4xx). Check your configuration.",
361
- network_error: "Could not reach Clivly. Check your network or --api-url."
362
- };
363
- const defaultConnect = (apiKey, apiUrl) => createClivlySDK({
364
- apiKey,
365
- apiUrl,
366
- retry: { maxAttempts: 1 }
367
- }).connect();
368
- function resultLines(result) {
369
- if (result.ok) {
370
- const lines = [checkLine(true, "Connected to Clivly")];
371
- if (result.org) {
372
- const name = result.org.name ?? "(unnamed org)";
373
- const id = result.org.id ? ` (${result.org.id})` : "";
374
- lines.push(` Org: ${name}${id}\n`);
375
- }
376
- return lines;
377
- }
378
- const status = result.status === null ? "no response" : `HTTP ${result.status}`;
379
- return [checkLine(false, `Could not connect (${result.reason}, ${status})`), ` ${REASON_HINT[result.reason]}\n`];
380
- }
381
- /**
382
- * `clivly ping` — send one heartbeat with the configured key and print a clear
383
- * ✅/❌ with the resolved org or the failure reason. Verifies key + reachability
384
- * from the terminal with zero code. `connect`/`out` are injectable for tests.
385
- */
386
- async function runPing(options) {
387
- const out = options.out ?? ((line) => process.stdout.write(line));
388
- out("\nClivly ping\n");
389
- if (!options.apiKey) {
390
- out(checkLine(false, "CLIVLY_API_KEY is not set."));
391
- out(" Set it to your sk_live_… key from Settings → API keys.\n");
392
- return 1;
393
- }
394
- const result = await (options.connect ?? defaultConnect)(options.apiKey, options.apiUrl);
395
- for (const line of resultLines(result)) out(line);
396
- return result.ok ? 0 : 1;
397
- }
398
- //#endregion
399
301
  //#region src/plan-init.ts
400
302
  function toScoredTable(table) {
401
303
  return {
@@ -510,6 +412,21 @@ function planInit(schema) {
510
412
  };
511
413
  }
512
414
  //#endregion
415
+ //#region src/env-file.ts
416
+ function hasKey$1(content, key) {
417
+ return new RegExp(`^\\s*${key}=`, "m").test(content);
418
+ }
419
+ /**
420
+ * Append only the `entries` whose key is absent, each preceded by its comment.
421
+ * Existing keys/values are left exactly as-is. Returns the full new content.
422
+ */
423
+ function appendEnvKeys(existing, entries) {
424
+ const missing = entries.filter((e) => !hasKey$1(existing, e.key));
425
+ if (missing.length === 0) return existing;
426
+ const block = missing.map((e) => `${e.comment ? `# ${e.comment}\n` : ""}${e.key}=`).join("\n");
427
+ return `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${existing.length > 0 ? "\n" : ""}# Clivly\n${block}\n`;
428
+ }
429
+ //#endregion
513
430
  //#region src/render-config.ts
514
431
  const CLIVLY_ENV_KEYS = [
515
432
  {
@@ -544,29 +461,44 @@ function renderEntity(key, entity) {
544
461
  fields: ${renderFields(entity.fields)}${renderRelationships(entity)},
545
462
  }`;
546
463
  }
547
- function renderSource(role, entity) {
464
+ function renderSource(role, entity, dbExpr) {
548
465
  const opts = entity.idField === null ? `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}" }` : `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}", idField: "${entity.idField}" }`;
549
- return ` ${role}: fromDrizzle(db, ${entity.moduleKey}, ${opts})`;
466
+ return ` ${role}: fromDrizzle(${dbExpr}, schema.${entity.moduleKey}, ${opts})`;
467
+ }
468
+ function fallbackDbImportPath(schemaImportPath) {
469
+ const slash = schemaImportPath.lastIndexOf("/");
470
+ return `${slash >= 0 ? schemaImportPath.slice(0, slash) : "."}/db`;
550
471
  }
551
- function renderTableImport(plan) {
552
- const tableKeys = [plan.contact.moduleKey, plan.company?.moduleKey].filter(Boolean);
553
- if (plan.db) return `import { db, ${tableKeys.join(", ")} } from "${plan.importPath}";`;
554
- return `import { ${tableKeys.join(", ")} } from "${plan.importPath}";
555
- import { db } from "./db"; // TODO: point at your Drizzle instance`;
472
+ function dbIsInSchemaModule(plan) {
473
+ return plan.db !== null && plan.db.importPath === plan.importPath;
474
+ }
475
+ function renderDbExpr(plan) {
476
+ if (!plan.db) return "db";
477
+ return dbIsInSchemaModule(plan) ? `schema.${plan.db.moduleKey}` : plan.db.moduleKey;
478
+ }
479
+ function renderImports(plan) {
480
+ const lines = [
481
+ "import { createClivlySDK } from \"clivly/sdk\";",
482
+ "import { fromDrizzle } from \"clivly/drizzle\";",
483
+ "import { defineClivlyConfig } from \"clivly/core\";",
484
+ "import { discoverFromDrizzle } from \"clivly/core/drizzle\";",
485
+ `import * as schema from "${plan.importPath}";`
486
+ ];
487
+ if (!plan.db) lines.push(`import { db } from "${fallbackDbImportPath(plan.importPath)}"; // TODO: point at your Drizzle instance`);
488
+ else if (!dbIsInSchemaModule(plan)) lines.push(`import { ${plan.db.moduleKey} } from "${plan.db.importPath}";`);
489
+ return lines.join("\n");
556
490
  }
557
491
  function renderTodoHeader(todos) {
558
492
  if (todos.length === 0) return "";
559
493
  return `// TODO(clivly): review before running \`clivly doctor\`:\n${todos.map((t) => `// - ${t}`).join("\n")}\n\n`;
560
494
  }
561
495
  function renderInitConfig(plan) {
496
+ const dbExpr = renderDbExpr(plan);
562
497
  const entities = [renderEntity(plan.contact.moduleKey, plan.contact)];
563
498
  if (plan.company) entities.push(renderEntity(plan.company.moduleKey, plan.company));
564
- const sources = [renderSource("contacts", plan.contact)];
565
- if (plan.company) sources.push(renderSource("companies", plan.company));
566
- return `${renderTodoHeader(plan.todos)}import { createClivlySDK } from "@clivly/sdk";
567
- import { fromDrizzle } from "@clivly/sdk/drizzle";
568
- import { defineClivlyConfig } from "@clivly/core";
569
- ${renderTableImport(plan)}
499
+ const sources = [renderSource("contacts", plan.contact, dbExpr)];
500
+ if (plan.company) sources.push(renderSource("companies", plan.company, dbExpr));
501
+ return `${renderTodoHeader(plan.todos)}${renderImports(plan)}
570
502
 
571
503
  const entities = defineClivlyConfig({
572
504
  entities: {
@@ -577,6 +509,9 @@ ${entities.join(",\n")},
577
509
  export default createClivlySDK({
578
510
  apiKey: process.env.CLIVLY_API_KEY!,
579
511
  entities,
512
+ // Every table in your Drizzle schema, so the Clivly dashboard can offer them
513
+ // all when you add a mapping — not just the ones wired up as sources below.
514
+ schema: discoverFromDrizzle(schema),
580
515
  source: {
581
516
  ${sources.join(",\n")},
582
517
  },
@@ -590,46 +525,79 @@ ${sources.join(",\n")},
590
525
  `;
591
526
  }
592
527
  //#endregion
593
- //#region src/resolve-schema-path.ts
594
- const COMMON_PATHS = [
595
- "src/db/schema.ts",
596
- "src/db/schema",
597
- "db/schema.ts",
598
- "db/schema",
599
- "src/schema.ts",
600
- "src/schema",
601
- "drizzle/schema.ts",
602
- "drizzle/schema"
603
- ];
604
- const DRIZZLE_CONFIG_NAMES = [
605
- "drizzle.config.ts",
606
- "drizzle.config.js",
607
- "drizzle.config.mjs"
608
- ];
609
- function defaultReadDrizzleConfigSchema(cwd) {
610
- for (const name of DRIZZLE_CONFIG_NAMES) {
611
- const path = resolve(cwd, name);
612
- if (!existsSync(path)) continue;
613
- try {
614
- const schema = createJiti(pathToFileURL(path).href)(path).default?.schema;
615
- const first = Array.isArray(schema) ? schema[0] : schema;
616
- if (typeof first === "string") return first;
617
- } catch {}
618
- }
619
- return null;
528
+ //#region src/render-auth.ts
529
+ const RESOLVE_USER = "const resolveUser = (req: Request) => adapter.getUser(req);";
530
+ const STUB = {
531
+ complete: false,
532
+ envKeys: [],
533
+ hostImport: null,
534
+ imports: [],
535
+ packages: [],
536
+ body: `// TODO(clivly): resolve the signed-in host user from the request (your auth stack).
537
+ const resolveUser = (_req: Request) => null;`
538
+ };
539
+ function clerkWiring() {
540
+ return {
541
+ complete: true,
542
+ envKeys: ["CLERK_SECRET_KEY", "CLERK_PUBLISHABLE_KEY"],
543
+ hostImport: null,
544
+ imports: ["import { createClerkAuthAdapter } from \"clivly/auth/clerk\";"],
545
+ packages: ["@clerk/backend"],
546
+ body: `const adapter = createClerkAuthAdapter({
547
+ secretKey: process.env.CLERK_SECRET_KEY ?? "",
548
+ publishableKey: process.env.CLERK_PUBLISHABLE_KEY,
549
+ });
550
+ ${RESOLVE_USER}`
551
+ };
620
552
  }
621
- function resolveSchemaPath(opts) {
622
- const exists = opts.deps?.exists ?? existsSync;
623
- const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
624
- const toAbs = (p) => isAbsolute(p) ? p : resolve(opts.cwd, p);
625
- if (opts.flag) return toAbs(opts.flag);
626
- const fromConfig = readSchema(opts.cwd);
627
- if (fromConfig) return toAbs(fromConfig);
628
- for (const candidate of COMMON_PATHS) {
629
- const abs = toAbs(candidate);
630
- if (exists(abs)) return abs;
553
+ function workosWiring() {
554
+ return {
555
+ complete: true,
556
+ envKeys: [
557
+ "WORKOS_API_KEY",
558
+ "WORKOS_CLIENT_ID",
559
+ "WORKOS_COOKIE_PASSWORD"
560
+ ],
561
+ hostImport: null,
562
+ imports: ["import { createWorkOSAuthAdapter } from \"clivly/auth/workos\";"],
563
+ packages: ["@workos-inc/node"],
564
+ body: `const adapter = createWorkOSAuthAdapter({
565
+ apiKey: process.env.WORKOS_API_KEY ?? "",
566
+ clientId: process.env.WORKOS_CLIENT_ID ?? "",
567
+ cookiePassword: process.env.WORKOS_COOKIE_PASSWORD ?? "",
568
+ });
569
+ ${RESOLVE_USER}`
570
+ };
571
+ }
572
+ function hostImportFor(instance) {
573
+ if (instance === null) return {
574
+ binding: "auth",
575
+ path: "./lib/auth",
576
+ todo: "TODO(clivly): point this at your better-auth instance."
577
+ };
578
+ return {
579
+ binding: instance.moduleKey === "auth" ? "auth" : `${instance.moduleKey} as auth`,
580
+ path: instance.importPath
581
+ };
582
+ }
583
+ function betterAuthWiring(instance) {
584
+ return {
585
+ complete: instance !== null,
586
+ envKeys: [],
587
+ hostImport: hostImportFor(instance),
588
+ imports: ["import { createBetterAuthAdapter } from \"clivly/auth/better-auth\";"],
589
+ packages: [],
590
+ body: `const adapter = createBetterAuthAdapter({ auth });
591
+ ${RESOLVE_USER}`
592
+ };
593
+ }
594
+ function renderAuthWiring(auth, authInstance = null) {
595
+ switch (auth) {
596
+ case "clerk": return clerkWiring();
597
+ case "workos": return workosWiring();
598
+ case "better-auth": return betterAuthWiring(authInstance);
599
+ default: return STUB;
631
600
  }
632
- return null;
633
601
  }
634
602
  //#endregion
635
603
  //#region src/scaffold.ts
@@ -663,15 +631,30 @@ function routePath(framework, kind) {
663
631
  if (!paths) throw new Error(`No route path for framework: ${framework}`);
664
632
  return paths[kind];
665
633
  }
634
+ const WINDOWS_SEP = /\\/g;
635
+ const LEADING_DOT_SLASH = /^\.\//;
666
636
  function configImport(routeFilePath) {
667
- const rel = relative(dirname(routeFilePath), "clivly.config").replace(/\\/g, "/");
637
+ const rel = relative(dirname(routeFilePath), "clivly.config").replace(WINDOWS_SEP, "/");
638
+ return rel.startsWith(".") ? rel : `./${rel}`;
639
+ }
640
+ function hostImportPath(routeFilePath, appRelative) {
641
+ const fromRoot = appRelative.replace(LEADING_DOT_SLASH, "");
642
+ const rel = relative(dirname(routeFilePath), fromRoot).replace(WINDOWS_SEP, "/");
668
643
  return rel.startsWith(".") ? rel : `./${rel}`;
669
644
  }
670
- function renderBody(framework, importPath, verify) {
645
+ function renderAuthPreamble(routeFilePath, auth) {
646
+ const lines = [...auth.imports];
647
+ if (auth.hostImport) {
648
+ if (auth.hostImport.todo) lines.push(`// ${auth.hostImport.todo}`);
649
+ const path = hostImportPath(routeFilePath, auth.hostImport.path);
650
+ lines.push(`import { ${auth.hostImport.binding} } from "${path}";`);
651
+ }
652
+ lines.push("", auth.body);
653
+ return lines.join("\n");
654
+ }
655
+ function renderBody(framework, importPath, verify, auth, routeFilePath) {
671
656
  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;
657
+ ${renderAuthPreamble(routeFilePath, auth)}
675
658
  const handler = clivly.createAuthVerifyHandler(resolveUser);
676
659
  ` : `import clivly from "${importPath}";
677
660
 
@@ -702,9 +685,9 @@ export default defineEventHandler((event) => handler(toWebRequest(event)));
702
685
  default: throw new Error(`No route template for framework: ${framework}`);
703
686
  }
704
687
  }
705
- function renderRouteFile(framework, kind) {
688
+ function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown")) {
706
689
  const path = routePath(framework, kind);
707
- let body = renderBody(framework, configImport(path), kind === "verify");
690
+ let body = renderBody(framework, configImport(path), kind === "verify", auth, path);
708
691
  if (framework === "tanstack-start") {
709
692
  const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
710
693
  body = body.replace("__ROUTE__", url);
@@ -734,6 +717,405 @@ function planScaffold(framework) {
734
717
  ];
735
718
  }
736
719
  //#endregion
720
+ //#region src/plan-mutations.ts
721
+ function fileAction(present, force) {
722
+ if (!present) return "create";
723
+ return force ? "overwrite" : "skip";
724
+ }
725
+ function hasKey(content, key) {
726
+ return new RegExp(`^\\s*${key}=`, "m").test(content);
727
+ }
728
+ function planFiles(args) {
729
+ const mutations = [];
730
+ const render = (path) => {
731
+ if (path === "clivly.config.ts") return renderInitConfig(args.plan);
732
+ const kind = path.includes("verify") ? "verify" : "tick";
733
+ return renderRouteFile(args.framework, kind, args.authWiring);
734
+ };
735
+ for (const file of planScaffold(args.framework)) mutations.push({
736
+ path: file.path,
737
+ contents: render(file.path),
738
+ action: fileAction(args.exists(join(args.cwd, file.path)), args.force)
739
+ });
740
+ return mutations;
741
+ }
742
+ function envAction(addedCount, existingLength) {
743
+ if (addedCount === 0) return "skip";
744
+ return existingLength === 0 ? "create" : "update";
745
+ }
746
+ function planEnv(args) {
747
+ const existing = args.readEnv(args.cwd);
748
+ const addedKeys = CLIVLY_ENV_KEYS.filter((entry) => !hasKey(existing, entry.key)).map((entry) => entry.key);
749
+ return {
750
+ path: ".env",
751
+ addedKeys,
752
+ contents: appendEnvKeys(existing, CLIVLY_ENV_KEYS),
753
+ action: envAction(addedKeys.length, existing.length)
754
+ };
755
+ }
756
+ function planMutations(args) {
757
+ return {
758
+ files: planFiles(args),
759
+ env: planEnv(args)
760
+ };
761
+ }
762
+ //#endregion
763
+ //#region src/probe-auth.ts
764
+ const AUTH_MODULE_PATHS = [
765
+ "auth",
766
+ "lib/auth",
767
+ "src/auth",
768
+ "src/lib/auth",
769
+ "app/lib/auth",
770
+ "server/auth",
771
+ "src/server/auth"
772
+ ];
773
+ /** A better-auth instance, duck-typed by the surface the adapter calls. */
774
+ function isBetterAuthInstance(value) {
775
+ return typeof value?.api?.getSession === "function";
776
+ }
777
+ function authInstanceCandidates(cwd) {
778
+ return AUTH_MODULE_PATHS.map((path) => join(cwd, path));
779
+ }
780
+ function defaultLoader(cwd) {
781
+ const jiti = createJiti(pathToFileURL(join(cwd, "clivly.config.ts")).href);
782
+ return (specifier) => jiti.import(specifier);
783
+ }
784
+ function probeAuthInstance(cwd, load) {
785
+ return probeModule({
786
+ candidates: authInstanceCandidates(cwd),
787
+ cwd,
788
+ load: load ?? defaultLoader(cwd),
789
+ match: isBetterAuthInstance
790
+ });
791
+ }
792
+ //#endregion
793
+ //#region src/load-config.ts
794
+ const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
795
+ function resolveConfigPath(cwd, configPath) {
796
+ return resolve(cwd, configPath ?? "clivly.config.ts");
797
+ }
798
+ /**
799
+ * Load the config and return its default export, checking it carries the method
800
+ * the caller needs. The check is by capability rather than by type: the config
801
+ * is the developer's own SDK instance, and a clear "this isn't an SDK instance"
802
+ * beats a TypeError deep inside a command.
803
+ */
804
+ /**
805
+ * The first meaningful line of an error message. jiti reports a failed
806
+ * resolution with a multi-line "Require stack:" dump naming Clivly's own
807
+ * internal files — noise that tells the developer nothing about their app and
808
+ * buries the one line that does.
809
+ */
810
+ function firstLine(message) {
811
+ return message.split("\n")[0]?.trim() ?? "";
812
+ }
813
+ async function loadConfigSdk(path, requiredMethod) {
814
+ const jiti = createJiti(import.meta.url);
815
+ let mod;
816
+ try {
817
+ mod = await jiti.import(path);
818
+ } catch (error) {
819
+ throw new Error(firstLine(error.message));
820
+ }
821
+ const sdk = mod.default ?? mod;
822
+ if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
823
+ return sdk;
824
+ }
825
+ //#endregion
826
+ //#region src/push-schema.ts
827
+ async function runPushSchema(options) {
828
+ const out = options.out ?? ((line) => process.stdout.write(line));
829
+ const path = resolveConfigPath(options.cwd, options.configPath);
830
+ const loadSdk = options.loadSdk ?? ((configPath) => loadConfigSdk(configPath, "heartbeat"));
831
+ out("\nClivly push-schema\n");
832
+ let sdk;
833
+ try {
834
+ sdk = await loadSdk(path);
835
+ } catch (error) {
836
+ out(` ❌ Could not load config — ${error.message}\n`);
837
+ out(` ${CONFIG_HINT}\n`);
838
+ return 1;
839
+ }
840
+ let delivered;
841
+ try {
842
+ delivered = await sdk.heartbeat();
843
+ } catch (error) {
844
+ out(` ❌ Push failed — ${error.message}\n`);
845
+ return 1;
846
+ }
847
+ if (!delivered) {
848
+ out(" ❌ Clivly rejected the push.\n");
849
+ out(" Run `clivly status` to check your key and reachability.\n");
850
+ return 1;
851
+ }
852
+ out(" ✅ Schema reported to Clivly.\n");
853
+ out(" Integration liveness and trigger URL were refreshed too.\n");
854
+ out(" Open Integrations → Mapping to map your tables.\n");
855
+ return 0;
856
+ }
857
+ //#endregion
858
+ //#region src/resolve-schema-path.ts
859
+ const COMMON_PATHS = [
860
+ "src/db/schema.ts",
861
+ "src/db/schema",
862
+ "db/schema.ts",
863
+ "db/schema",
864
+ "src/schema.ts",
865
+ "src/schema",
866
+ "drizzle/schema.ts",
867
+ "drizzle/schema"
868
+ ];
869
+ const DRIZZLE_CONFIG_NAMES = [
870
+ "drizzle.config.ts",
871
+ "drizzle.config.js",
872
+ "drizzle.config.mjs"
873
+ ];
874
+ function defaultReadDrizzleConfigSchema(cwd) {
875
+ for (const name of DRIZZLE_CONFIG_NAMES) {
876
+ const path = resolve(cwd, name);
877
+ if (!existsSync(path)) continue;
878
+ try {
879
+ const schema = createJiti(pathToFileURL(path).href)(path).default?.schema;
880
+ const first = Array.isArray(schema) ? schema[0] : schema;
881
+ if (typeof first === "string") return first;
882
+ } catch {}
883
+ }
884
+ return null;
885
+ }
886
+ function resolveSchemaPath(opts) {
887
+ const exists = opts.deps?.exists ?? existsSync;
888
+ const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
889
+ const toAbs = (p) => isAbsolute(p) ? p : resolve(opts.cwd, p);
890
+ if (opts.flag) return toAbs(opts.flag);
891
+ const fromConfig = readSchema(opts.cwd);
892
+ if (fromConfig) return toAbs(fromConfig);
893
+ for (const candidate of COMMON_PATHS) {
894
+ const abs = toAbs(candidate);
895
+ if (exists(abs)) return abs;
896
+ }
897
+ return null;
898
+ }
899
+ //#endregion
900
+ //#region src/status-ladder.ts
901
+ const SATISFYING = new Set(["pass", "warn"]);
902
+ function unmetPrerequisites(stage, satisfied) {
903
+ return (stage.needs ?? []).filter((id) => !satisfied.has(id));
904
+ }
905
+ async function runLadder(stages, ctx) {
906
+ const results = [];
907
+ const satisfied = /* @__PURE__ */ new Set();
908
+ const total = stages.length;
909
+ for (const [index, stage] of stages.entries()) {
910
+ const position = {
911
+ step: index + 1,
912
+ total,
913
+ id: stage.id,
914
+ label: stage.label
915
+ };
916
+ const blockedBy = unmetPrerequisites(stage, satisfied);
917
+ if (blockedBy.length > 0) {
918
+ results.push({
919
+ ...position,
920
+ state: "pending",
921
+ detail: `Waiting on: ${blockedBy.join(", ")}.`
922
+ });
923
+ continue;
924
+ }
925
+ let outcome;
926
+ try {
927
+ outcome = await stage.run(ctx);
928
+ } catch (error) {
929
+ outcome = {
930
+ state: "fail",
931
+ detail: error.message
932
+ };
933
+ }
934
+ if (SATISFYING.has(outcome.state)) satisfied.add(stage.id);
935
+ results.push({
936
+ ...position,
937
+ ...outcome
938
+ });
939
+ }
940
+ return results;
941
+ }
942
+ /**
943
+ * Exit 0 only when the setup actually works. `pending` is still non-zero —
944
+ * incomplete is not working, and scripts must see that — even though the
945
+ * rendering frames it as progress rather than breakage.
946
+ */
947
+ function ladderExitCode(results) {
948
+ return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
949
+ }
950
+ //#endregion
951
+ //#region src/status-stages.ts
952
+ const REASON_HINT = {
953
+ ok: "",
954
+ invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
955
+ forbidden: "The key is valid but lacks access to this org (403).",
956
+ rate_limited: "Rate limited (429). Wait a moment and retry.",
957
+ server_error: "Clivly returned a server error (5xx). Retry shortly.",
958
+ client_error: "The request was rejected (4xx). Check your configuration.",
959
+ network_error: "Could not reach Clivly. Check your network or --api-url."
960
+ };
961
+ const apiKeyStage = {
962
+ id: "api-key",
963
+ label: "API key present",
964
+ run(ctx) {
965
+ if (ctx.apiKey) return Promise.resolve({
966
+ state: "pass",
967
+ detail: "CLIVLY_API_KEY is set."
968
+ });
969
+ return Promise.resolve({
970
+ state: "fail",
971
+ detail: "CLIVLY_API_KEY is not set.",
972
+ hint: "Copy your API key from Settings → API keys into .env."
973
+ });
974
+ }
975
+ };
976
+ const heartbeatStage = {
977
+ id: "heartbeat",
978
+ label: "Clivly reachable",
979
+ needs: [apiKeyStage.id],
980
+ async run(ctx) {
981
+ const result = await ctx.connect(ctx.apiKey, ctx.apiUrl);
982
+ if (result.ok) return {
983
+ state: "pass",
984
+ detail: `Reached org: ${result.org?.name ?? "(unnamed org)"}${result.org?.id ? ` (${result.org.id})` : ""}`
985
+ };
986
+ const status = result.status === null ? "no response" : `HTTP ${result.status}`;
987
+ return {
988
+ state: "fail",
989
+ detail: `${result.reason} (${status})`,
990
+ hint: REASON_HINT[result.reason]
991
+ };
992
+ }
993
+ };
994
+ const configStage = {
995
+ id: "config",
996
+ label: "clivly.config loads",
997
+ async run(ctx) {
998
+ try {
999
+ ctx.sdk = await ctx.loadSdk(ctx.configPath);
1000
+ return {
1001
+ state: "pass",
1002
+ detail: ctx.configPath
1003
+ };
1004
+ } catch (error) {
1005
+ return {
1006
+ state: "fail",
1007
+ detail: error.message,
1008
+ hint: CONFIG_HINT
1009
+ };
1010
+ }
1011
+ }
1012
+ };
1013
+ /** The full ladder, in the order a developer completes setup. */
1014
+ const STATUS_STAGES = [
1015
+ apiKeyStage,
1016
+ heartbeatStage,
1017
+ configStage,
1018
+ {
1019
+ id: "setup-checks",
1020
+ label: "Setup checks",
1021
+ needs: [configStage.id],
1022
+ async run(ctx) {
1023
+ const report = await ctx.sdk.doctor();
1024
+ const counted = report.checks.filter((check) => !check.skipped);
1025
+ const passed = counted.filter((check) => check.ok).length;
1026
+ return {
1027
+ state: report.ok ? "pass" : "fail",
1028
+ detail: `${passed} of ${counted.length} passed`,
1029
+ sub: report.checks.map((check) => ({
1030
+ label: check.name,
1031
+ ok: check.ok,
1032
+ skipped: check.skipped,
1033
+ detail: check.detail
1034
+ }))
1035
+ };
1036
+ }
1037
+ },
1038
+ {
1039
+ id: "trigger-url",
1040
+ label: "Sync trigger URL",
1041
+ needs: [configStage.id],
1042
+ run(ctx) {
1043
+ const url = ctx.sdk?.syncTrigger?.url;
1044
+ if (url) return Promise.resolve({
1045
+ state: "pass",
1046
+ detail: url
1047
+ });
1048
+ return Promise.resolve({
1049
+ state: "warn",
1050
+ detail: "Not set — remote sync cannot call back into this app.",
1051
+ hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. Push-only setups can ignore this."
1052
+ });
1053
+ }
1054
+ }
1055
+ ];
1056
+ function defaultLoadSdk(path) {
1057
+ return loadConfigSdk(path, "doctor");
1058
+ }
1059
+ //#endregion
1060
+ //#region src/status.ts
1061
+ /** Stage ids each legacy command maps onto. */
1062
+ const ALIAS_STAGE_IDS = {
1063
+ status: STATUS_STAGES.map((stage) => stage.id),
1064
+ ping: ["api-key", "heartbeat"],
1065
+ doctor: ["config", "setup-checks"],
1066
+ connect: [
1067
+ "api-key",
1068
+ "heartbeat",
1069
+ "config",
1070
+ "trigger-url"
1071
+ ]
1072
+ };
1073
+ const MARK = {
1074
+ pass: "✅",
1075
+ fail: "❌",
1076
+ warn: "⚠️ ",
1077
+ skip: "⏭️ ",
1078
+ pending: "⏳"
1079
+ };
1080
+ const defaultConnect = (apiKey, apiUrl) => createClivlySDK({
1081
+ apiKey,
1082
+ apiUrl,
1083
+ retry: { maxAttempts: 1 }
1084
+ }).connect();
1085
+ function subMark(sub) {
1086
+ if (sub.skipped) return "⚠️ ";
1087
+ return sub.ok ? "✅" : "❌";
1088
+ }
1089
+ function renderStage(result, verbose, out) {
1090
+ const step = `Step ${result.step} of ${result.total}`;
1091
+ const detail = result.detail ? ` — ${result.detail}` : "";
1092
+ out(` ${MARK[result.state]} ${step} · ${result.label}${detail}\n`);
1093
+ for (const sub of result.sub ?? []) {
1094
+ if (!(verbose || sub.skipped || !sub.ok)) continue;
1095
+ const subDetail = sub.detail ? ` — ${sub.detail}` : "";
1096
+ out(` ${subMark(sub)} ${sub.label}${subDetail}\n`);
1097
+ }
1098
+ if (result.hint && result.state !== "pass") out(` ${result.hint}\n`);
1099
+ }
1100
+ async function runStatus(options) {
1101
+ const out = options.out ?? ((line) => process.stdout.write(line));
1102
+ const selected = new Set(options.only ?? ALIAS_STAGE_IDS.status);
1103
+ const stages = STATUS_STAGES.filter((stage) => selected.has(stage.id));
1104
+ const ctx = {
1105
+ apiKey: options.apiKey,
1106
+ apiUrl: options.apiUrl,
1107
+ configPath: resolveConfigPath(options.cwd, options.configPath),
1108
+ connect: options.connect ?? defaultConnect,
1109
+ loadSdk: options.loadSdk ?? defaultLoadSdk
1110
+ };
1111
+ out("\nClivly status\n");
1112
+ const results = await runLadder(stages, ctx);
1113
+ for (const result of results) renderStage(result, options.verbose ?? false, out);
1114
+ const code = ladderExitCode(results);
1115
+ if (code !== 0 && results.some((result) => result.state === "pending")) out("\nSetup is incomplete — finish the steps above and re-run.\n");
1116
+ return code;
1117
+ }
1118
+ //#endregion
737
1119
  //#region src/index.ts
738
1120
  function flagValue(argv, flag) {
739
1121
  const index = argv.indexOf(flag);
@@ -752,6 +1134,13 @@ function readDeps(cwd) {
752
1134
  return {};
753
1135
  }
754
1136
  }
1137
+ function readOwnVersion() {
1138
+ const req = createRequire(import.meta.url);
1139
+ for (const rel of ["./package.json", "../package.json"]) try {
1140
+ const pkg = req(rel);
1141
+ if (pkg.name === "clivly" && pkg.version) return pkg.version;
1142
+ } catch {}
1143
+ }
755
1144
  function defaultReadEnv(cwd) {
756
1145
  const path = join(cwd, ".env");
757
1146
  return existsSync(path) ? readFileSync(path, "utf8") : "";
@@ -778,9 +1167,12 @@ function resolveInitDeps(options) {
778
1167
  }),
779
1168
  detectPackageManager: options.detectPackageManager ?? detectPackageManager,
780
1169
  install: options.installPackages ?? installPackages,
1170
+ probeAuth: options.probeAuthInstance ?? probeAuthInstance,
781
1171
  readEnv: options.readEnv ?? defaultReadEnv,
1172
+ readOwnVersion: options.readOwnVersion ?? readOwnVersion,
782
1173
  writeEnv: options.writeEnv ?? defaultWriteEnv,
783
- framework: "unknown"
1174
+ framework: "unknown",
1175
+ authWiring: renderAuthWiring("unknown")
784
1176
  };
785
1177
  }
786
1178
  async function acquireSchema(options, deps) {
@@ -792,10 +1184,13 @@ async function acquireSchema(options, deps) {
792
1184
  }
793
1185
  const framework = detectFramework(appDeps);
794
1186
  const orm = detectOrm(appDeps);
1187
+ const auth = detectAuth(appDeps);
795
1188
  deps.framework = framework;
1189
+ deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? await deps.probeAuth(options.cwd) : null);
796
1190
  out("\nClivly init\n");
797
1191
  out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
798
- out(` ORM: ${orm}\n\n`);
1192
+ out(` ORM: ${orm}\n`);
1193
+ out(` Auth: ${auth}${deps.authWiring.complete ? " (wired)" : ""}\n\n`);
799
1194
  if (orm !== "drizzle") {
800
1195
  out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
801
1196
  return { code: 1 };
@@ -834,48 +1229,81 @@ function printPlan(out, plan) {
834
1229
  out("\n");
835
1230
  }
836
1231
  }
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`);
1232
+ function mutationsFor(options, deps, plan) {
1233
+ return planMutations({
1234
+ cwd: options.cwd,
1235
+ force: options.force,
1236
+ framework: deps.framework,
1237
+ authWiring: deps.authWiring,
1238
+ plan,
1239
+ exists: deps.configExists,
1240
+ readEnv: deps.readEnv
1241
+ });
1242
+ }
1243
+ function applyMutations(options, deps, mutations) {
1244
+ for (const file of mutations.files) {
1245
+ if (file.action === "skip") {
1246
+ deps.out(` • ${file.path} exists — skipped (pass --force to overwrite)\n`);
1247
+ continue;
1248
+ }
1249
+ deps.writeConfig(join(options.cwd, file.path), file.contents);
1250
+ deps.out(` ✓ ${file.path}\n`);
1251
+ }
1252
+ if (mutations.env.action === "skip") {
1253
+ deps.out(" • .env already has every CLIVLY_* key — skipped\n");
841
1254
  return;
842
1255
  }
843
- deps.writeConfig(full, contents);
844
- deps.out(`${path}\n`);
1256
+ deps.writeEnv(options.cwd, mutations.env.contents);
1257
+ deps.out(".env placeholders\n");
845
1258
  }
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));
1259
+ function previewMutations(deps, mutations) {
1260
+ const { out } = deps;
1261
+ out("\nDry run nothing was written.\n\n");
1262
+ for (const file of mutations.files) {
1263
+ const verb = {
1264
+ create: "create",
1265
+ overwrite: "overwrite",
1266
+ skip: "skip"
1267
+ }[file.action];
1268
+ const lines = file.contents.split("\n").length;
1269
+ const suffix = file.action === "skip" ? " (exists — pass --force to overwrite)" : ` (${lines} lines)`;
1270
+ out(` ${verb.padEnd(9)} ${file.path}${suffix}\n`);
852
1271
  }
1272
+ if (mutations.env.addedKeys.length === 0) out(" skip .env (every CLIVLY_* key already present)\n");
1273
+ else {
1274
+ out(` ${mutations.env.action.padEnd(9)} .env\n`);
1275
+ for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
1276
+ }
1277
+ out("\nRe-run without --dry-run to apply.\n");
853
1278
  }
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) {
1279
+ function printNextSteps(out, authComplete) {
860
1280
  out("\nNext steps:\n");
861
1281
  out(" 1. Set the CLIVLY_* values in .env\n");
862
1282
  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");
1283
+ const step = authComplete ? 3 : 4;
1284
+ if (!authComplete) out(" 3. Finish resolveUser in the auth/verify route\n");
1285
+ out(` ${step}. Run \`clivly status\` to see where you are\n`);
865
1286
  }
866
1287
  async function scaffold(options, deps, plan) {
867
1288
  const { out } = deps;
868
1289
  const pm = deps.detectPackageManager(options.cwd);
1290
+ const version = deps.readOwnVersion();
1291
+ const packages = [version ? `clivly@${version}` : "clivly", ...deps.authWiring.packages];
1292
+ const mutations = mutationsFor(options, deps, plan);
1293
+ if (options.dryRun) {
1294
+ out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
1295
+ previewMutations(deps, mutations);
1296
+ return plan.contactDetected ? 0 : 1;
1297
+ }
869
1298
  const result = await deps.install({
870
1299
  cwd: options.cwd,
871
1300
  pm,
872
- packages: ["@clivly/sdk", "@clivly/core"]
1301
+ packages
873
1302
  });
874
- if (result.ok) out(`Installed @clivly/sdk @clivly/core (${pm})\n`);
1303
+ if (result.ok) out(`Installed clivly (${pm})\n`);
875
1304
  else out(`Could not install automatically — run:\n ${result.command}\n`);
876
- writeScaffoldFiles(options, deps, plan);
877
- writeEnvFile(options, deps);
878
- printNextSteps(out);
1305
+ applyMutations(options, deps, mutations);
1306
+ printNextSteps(out, deps.authWiring.complete);
879
1307
  return plan.contactDetected ? 0 : 1;
880
1308
  }
881
1309
  async function runInit(options) {
@@ -892,26 +1320,28 @@ async function main(argv) {
892
1320
  case "init": return await runInit({
893
1321
  cwd: process.cwd(),
894
1322
  force: rest.includes("--force"),
1323
+ dryRun: rest.includes("--dry-run"),
895
1324
  schemaFlag: flagValue(rest, "--schema")
896
1325
  });
897
- case "ping": return await runPing({
898
- apiKey: process.env.CLIVLY_API_KEY,
899
- apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL
900
- });
901
- case "doctor": return await runDoctor({
1326
+ case "push-schema": return await runPushSchema({
902
1327
  cwd: process.cwd(),
903
1328
  configPath: flagValue(rest, "--config")
904
1329
  });
905
- case "connect": return await runConnect({
1330
+ case "status":
1331
+ case "ping":
1332
+ case "doctor":
1333
+ case "connect": return await runStatus({
906
1334
  cwd: process.cwd(),
907
1335
  apiKey: process.env.CLIVLY_API_KEY,
908
1336
  apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
909
- configPath: flagValue(rest, "--config")
1337
+ configPath: flagValue(rest, "--config"),
1338
+ only: ALIAS_STAGE_IDS[command],
1339
+ verbose: rest.includes("--verbose")
910
1340
  });
911
1341
  case void 0:
912
1342
  case "help":
913
1343
  case "--help":
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");
1344
+ process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--dry-run] [--schema <path>] 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");
915
1345
  return 0;
916
1346
  default:
917
1347
  process.stdout.write(`Unknown command: ${command}\n`);