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