clivly 0.10.8 → 0.11.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +31 -1
  2. package/{adapter-1TN0zohk.d.cts → adapter-BQl13eS3.d.mts} +1 -1
  3. package/{adapter-uxirB88i.d.mts → adapter-CA8bA6o-.d.cts} +1 -1
  4. package/{auth-adapter-CXTiW3Mb.d.cts → auth-adapter-B4vVyUiX.d.mts} +1 -1
  5. package/{auth-adapter-DSuYwv1c.d.mts → auth-adapter-Cvc_sOpY.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-drizzle.d.cts +1 -1
  17. package/core-drizzle.d.mts +1 -1
  18. package/core-entity-config.cjs +1 -1
  19. package/core-entity-config.d.cts +1 -1
  20. package/core-entity-config.d.mts +1 -1
  21. package/core-entity-config.mjs +1 -1
  22. package/core-entity-heuristics.d.cts +1 -1
  23. package/core-entity-heuristics.d.mts +1 -1
  24. package/core-mapping-form.d.cts +1 -1
  25. package/core-mapping-form.d.mts +1 -1
  26. package/core-mappings-config.cjs +1 -1
  27. package/core-mappings-config.d.cts +1 -1
  28. package/core-mappings-config.d.mts +1 -1
  29. package/core-mappings-config.mjs +1 -1
  30. package/core-sync-engine.d.cts +1 -1
  31. package/core-sync-engine.d.mts +1 -1
  32. package/core-types.d.cts +1 -1
  33. package/core-types.d.mts +1 -1
  34. package/core-view-compiler.d.cts +1 -1
  35. package/core-view-compiler.d.mts +1 -1
  36. package/core.cjs +2 -2
  37. package/core.d.cts +9 -9
  38. package/core.d.mts +9 -9
  39. package/core.mjs +2 -2
  40. package/{drizzle-SD8WCvYj.d.cts → drizzle-C_0pIDpx.d.mts} +1 -1
  41. package/{drizzle-axugRJ0u.d.mts → drizzle-D3DcaC2d.d.cts} +1 -1
  42. package/drizzle.d.cts +3 -3
  43. package/drizzle.d.mts +3 -3
  44. package/{entity-config-bC3DX9f1.mjs → entity-config-CO0TEwZC.mjs} +1 -0
  45. package/{entity-config-w8oMBWf8.d.cts → entity-config-CbS2DGmA.d.cts} +1 -1
  46. package/{entity-config-w8oMBWf8.d.mts → entity-config-CbS2DGmA.d.mts} +1 -1
  47. package/{entity-config-C2MfIDL2.cjs → entity-config-EZg9BtqD.cjs} +1 -0
  48. package/index.cjs +207 -14
  49. package/index.d.cts +2 -0
  50. package/index.d.mts +2 -0
  51. package/index.mjs +207 -14
  52. package/{internal-CHvZMvxC.d.cts → internal-B8cq-xP-.d.cts} +1 -1
  53. package/{internal-WximMGmH.d.mts → internal-DEV1NCQc.d.mts} +1 -1
  54. package/internal-adapter.d.cts +1 -1
  55. package/internal-adapter.d.mts +1 -1
  56. package/internal-auth-adapter.d.cts +1 -1
  57. package/internal-auth-adapter.d.mts +1 -1
  58. package/internal-drizzle.d.cts +1 -1
  59. package/internal-drizzle.d.mts +1 -1
  60. package/internal-entity-config.cjs +1 -1
  61. package/internal-entity-config.d.cts +1 -1
  62. package/internal-entity-config.d.mts +1 -1
  63. package/internal-entity-config.mjs +1 -1
  64. package/internal-entity-heuristics.cjs +35 -18
  65. package/internal-entity-heuristics.d.cts +1 -1
  66. package/internal-entity-heuristics.d.mts +1 -1
  67. package/internal-entity-heuristics.mjs +35 -18
  68. package/internal-mapping-form.d.cts +1 -1
  69. package/internal-mapping-form.d.mts +1 -1
  70. package/internal-mappings-config.cjs +1 -1
  71. package/internal-mappings-config.d.cts +1 -1
  72. package/internal-mappings-config.d.mts +1 -1
  73. package/internal-mappings-config.mjs +1 -1
  74. package/internal-sync-engine.d.cts +1 -1
  75. package/internal-sync-engine.d.mts +1 -1
  76. package/internal-types.d.cts +1 -1
  77. package/internal-types.d.mts +1 -1
  78. package/internal-view-compiler.d.cts +1 -1
  79. package/internal-view-compiler.d.mts +1 -1
  80. package/internal.cjs +2 -2
  81. package/internal.d.cts +9 -9
  82. package/internal.d.mts +9 -9
  83. package/internal.mjs +2 -2
  84. package/{mapping-form-BsTxv8w3.d.cts → mapping-form-BBgP1iEf.d.cts} +1 -1
  85. package/{mapping-form-OTFwr1sN.d.mts → mapping-form-B_vX_kZJ.d.mts} +1 -1
  86. package/{mappings-config-Dpv08-x4.cjs → mappings-config-BRP_Dev5.cjs} +1 -1
  87. package/{mappings-config-DbznSjTX.d.cts → mappings-config-BXdSUl-K.d.mts} +1 -1
  88. package/{mappings-config-SXu_zlmp.d.mts → mappings-config-Cvw7RDSb.d.cts} +1 -1
  89. package/{mappings-config-CRSL9dzw.mjs → mappings-config-fAfUoVuD.mjs} +1 -1
  90. package/{namespace-probe-BmxtnOfc.d.mts → namespace-probe-BWZ7f1fg.d.mts} +1 -1
  91. package/{namespace-probe-C3xMOqWE.d.cts → namespace-probe-DC4YsI6m.d.cts} +1 -1
  92. package/package.json +1 -1
  93. package/{sdk-C0DU5KDS.mjs → sdk-CsR4W9QQ.mjs} +71 -14
  94. package/{sdk-BAHcnhm7.cjs → sdk-UCBezLoT.cjs} +71 -14
  95. package/sdk.cjs +2 -2
  96. package/sdk.d.cts +2 -2
  97. package/sdk.d.mts +2 -2
  98. package/sdk.mjs +2 -2
  99. package/{sync-engine-CUKWXUv7.d.mts → sync-engine-BQRApDAG.d.mts} +2 -2
  100. package/{sync-engine-D2YlIHZB.d.cts → sync-engine-qxbrOEJX.d.cts} +2 -2
  101. package/{types-Cq1uHUKp.d.cts → types-S0N0hqOV.d.cts} +3 -0
  102. package/{types-Cq1uHUKp.d.mts → types-S0N0hqOV.d.mts} +3 -0
  103. package/{view-compiler-BO4CKamn.d.cts → view-compiler-CdnLqPQx.d.mts} +1 -1
  104. package/{view-compiler-iEwUOuba.d.mts → view-compiler-P4GHpOHt.d.cts} +1 -1
  105. package/vite.d.cts +1 -1
  106. package/vite.d.mts +1 -1
package/index.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_entity_config = require("./entity-config-C2MfIDL2.cjs");
3
+ const require_entity_config = require("./entity-config-EZg9BtqD.cjs");
4
4
  require("./mapping-score-CaB3F4Uu.cjs");
5
5
  const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
6
- const require_sdk = require("./sdk-BAHcnhm7.cjs");
6
+ const require_sdk = require("./sdk-UCBezLoT.cjs");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_fs = require("node:fs");
9
9
  let node_module = require("node:module");
@@ -293,6 +293,124 @@ function addCommand(pm, packages) {
293
293
  };
294
294
  }
295
295
  //#endregion
296
+ //#region src/detect-workspace.ts
297
+ const SELECTABLE_FRAMEWORKS = [
298
+ "nextjs",
299
+ "tanstack-start",
300
+ "sveltekit",
301
+ "remix",
302
+ "nuxt"
303
+ ];
304
+ /**
305
+ * Validate a `--framework` value.
306
+ *
307
+ * Rejecting a typo matters more than accepting one leniently: an unrecognised
308
+ * value silently falling back to detection would scaffold route files in the
309
+ * wrong shape, and a route in the wrong shape is not a build error — the
310
+ * generator simply skips it and the endpoint 404s at runtime.
311
+ */
312
+ function parseFrameworkFlag(value) {
313
+ if (value === void 0) return;
314
+ if (SELECTABLE_FRAMEWORKS.includes(value)) return value;
315
+ throw new Error(`"${value}" is not a supported framework. Accepted: ${SELECTABLE_FRAMEWORKS.join(", ")}.`);
316
+ }
317
+ /**
318
+ * A caution for when the framework came from a sibling package, or null.
319
+ *
320
+ * Borrowing the ORM or auth is unambiguous — one database and one auth instance
321
+ * serve the whole workspace. Borrowing the *framework* is not: route files are
322
+ * generated in that framework's shape and written into the directory `init` ran
323
+ * in, so if the framework lives next door the routes probably belong next door
324
+ * too. Writing TanStack `src/routes/` files into a Hono app is not a build
325
+ * error — the route generator simply never sees them and the endpoints 404.
326
+ */
327
+ function borrowedFrameworkWarning(framework, source) {
328
+ if (!source) return null;
329
+ return ` ⚠️ ${FRAMEWORK_LABELS[framework]} was detected in "${source}", not here.\n Route files are generated in that framework's shape. If this app
330
+ does not serve them, run init inside "${source}" instead, or pass\n --framework to say what this app actually is.
331
+ `;
332
+ }
333
+ /**
334
+ * Extra guidance for a detection failure inside a workspace, or "" outside one.
335
+ *
336
+ * Detection has already scanned every sibling by the time either failure fires,
337
+ * so the marker really is absent everywhere — which is worth saying, because
338
+ * the bare message sends a developer looking for a dependency that was never
339
+ * missing. Returning "" for the single-package case keeps the caller
340
+ * branch-free.
341
+ */
342
+ function workspaceHint(kind, packageCount) {
343
+ if (packageCount === 0) return "";
344
+ const scanned = `\nScanned ${packageCount} workspace package(s) and found no ${kind === "orm" ? "ORM" : "framework"} marker`;
345
+ return kind === "orm" ? `${scanned}.\nIf your schema is Drizzle, point at it and init will take your word:\n\n clivly init --schema <path-or-package>\n` : `${scanned}\nin any of them. If this app is served by one of the frameworks above,\nname it outright:\n\n clivly init --framework tanstack-start\n`;
346
+ }
347
+ /**
348
+ * Resolve one signal, preferring the app's own dependencies.
349
+ *
350
+ * The app's package.json is authoritative: a sibling on a different framework
351
+ * must never redefine what this app is. Only when the app itself is silent do
352
+ * we look outward, and then the first package by name wins.
353
+ *
354
+ * Sorting here rather than trusting the caller matters: the real package list
355
+ * comes from glob expansion, whose order is not guaranteed, so an unsorted scan
356
+ * would let the same repo detect differently between runs and report a
357
+ * different package as the source.
358
+ */
359
+ function resolveSignal(appDeps, packages, detect, unknown) {
360
+ const own = detect(appDeps);
361
+ if (own !== unknown) return {
362
+ source: null,
363
+ value: own
364
+ };
365
+ const ordered = [...packages].sort((a, b) => a.name.localeCompare(b.name));
366
+ for (const pkg of ordered) {
367
+ const found = detect(pkg.deps);
368
+ if (found !== unknown) return {
369
+ source: pkg.name,
370
+ value: found
371
+ };
372
+ }
373
+ return {
374
+ source: null,
375
+ value: unknown
376
+ };
377
+ }
378
+ /**
379
+ * Detect framework, ORM and auth across a whole workspace.
380
+ *
381
+ * A monorepo routinely splits these across packages — framework in the frontend
382
+ * app, ORM in a `db` package, auth in an `auth` package — so reading only the
383
+ * directory `init` was run from finds at most one of the three and fails
384
+ * whichever way the developer turns. Widening the search to sibling packages is
385
+ * what makes those layouts installable.
386
+ *
387
+ * Detection is deliberately a union rather than a per-package view: `init`
388
+ * generates one config for one app, and that app reaches its database and auth
389
+ * through workspace dependencies whether or not they are named in its own
390
+ * manifest.
391
+ *
392
+ * `packages` may include the app itself; that is harmless, since the app's own
393
+ * deps are consulted first anyway. Pass `[]` outside a workspace, which reduces
394
+ * this to a plain single-package read.
395
+ */
396
+ function detectAcrossWorkspace(appDeps, packages) {
397
+ const framework = resolveSignal(appDeps, packages, detectFramework, "unknown");
398
+ const orm = resolveSignal(appDeps, packages, detectOrm, "unknown");
399
+ const auth = resolveSignal(appDeps, packages, detectAuth, "unknown");
400
+ const sources = {
401
+ auth: auth.source,
402
+ framework: framework.source,
403
+ orm: orm.source
404
+ };
405
+ return {
406
+ auth: auth.value,
407
+ borrowed: Object.values(sources).some((source) => source !== null),
408
+ framework: framework.value,
409
+ orm: orm.value,
410
+ sources
411
+ };
412
+ }
413
+ //#endregion
296
414
  //#region src/tick-route.ts
297
415
  /**
298
416
  * The tick route's public URL path, and the template `clivly login` writes for
@@ -1258,10 +1376,12 @@ Usage:
1258
1376
  clivly init [--force] [--dry-run] [--diff] [--yes] [--no-login] [--no-browser]
1259
1377
  [--schema <path|package>] [--db-import <specifier>]
1260
1378
  [--auth-import <specifier>] [--contact-table <table>]
1261
- [--company-table <table>]
1379
+ [--company-table <table>] [--framework <name>]
1262
1380
 
1263
1381
  Scaffolds clivly.config.ts, the Clivly route files, and .env placeholders.
1264
1382
  Detects your framework, ORM, auth adapter and app layout from the project.
1383
+ In a workspace the whole package graph is scanned, so a monorepo that keeps
1384
+ its framework, ORM and auth in separate packages is detected from any app.
1265
1385
 
1266
1386
  Options:
1267
1387
  --dry-run Show what would be written; write nothing
@@ -1269,7 +1389,8 @@ Options:
1269
1389
  --force Overwrite files Clivly owns
1270
1390
  --yes Skip prompts
1271
1391
  --no-login Skip browser pairing
1272
- --schema <p> Path or package holding your ORM schema
1392
+ --framework <n> nextjs | tanstack-start | sveltekit | remix | nuxt
1393
+ --schema <p> Path or package holding your ORM schema (implies Drizzle)
1273
1394
  --db-import <s> Module specifier exporting your database instance
1274
1395
  --auth-import <s> Module specifier exporting your auth instance
1275
1396
  --contact-table <t> Table to map as contacts
@@ -1701,13 +1822,52 @@ function toScoredTable(table) {
1701
1822
  function columnNames(table) {
1702
1823
  return table.columns.map((column) => column.dbName);
1703
1824
  }
1704
- function rankCandidates$1(tables, concept) {
1825
+ /**
1826
+ * Company candidates the vocabulary cannot name, promoted on foreign-key
1827
+ * evidence.
1828
+ *
1829
+ * The company vocabulary is seven generic business words (companies,
1830
+ * organizations, teams, accounts…). A domain word for the same idea —
1831
+ * `campuses`, `branches`, `sites`, `practices`, `chapters` — matches none of
1832
+ * them, so such a table was never a candidate at all and no amount of ranking
1833
+ * could reach it. On `vue-academy` that left Better Auth's `organization` as
1834
+ * the only company candidate, and init duly scaffolded the auth table while
1835
+ * `campuses`, which every student row references, was invisible.
1836
+ *
1837
+ * Guessing more words does not fix this; the next unlisted word fails
1838
+ * identically. The schema already states the answer: when the CONTACT table
1839
+ * holds a real foreign key to some table, that target is what a contact
1840
+ * belongs to. `references` is reflected metadata, so this is a fact rather
1841
+ * than a name guess.
1842
+ *
1843
+ * Two guards keep it honest. The target must pass `hasDefiningColumn`, the
1844
+ * same "can this table BE a company" test every other candidate passes — a
1845
+ * join table with no `name` is not promoted. And framework tables are skipped,
1846
+ * because `contacts.organization_id → organization` is exactly the auth
1847
+ * plumbing this is meant to see past.
1848
+ */
1849
+ function fkPromotedCompanies(tables, contact) {
1850
+ if (!contact) return [];
1851
+ const targets = /* @__PURE__ */ new Set();
1852
+ for (const column of contact.columns) {
1853
+ const table = column.references?.table;
1854
+ if (table && !require_internal_entity_heuristics.isFrameworkTable(table)) targets.add(table.toLowerCase());
1855
+ }
1856
+ return tables.filter((t) => targets.has(t.tableName.toLowerCase()) && t.tableName !== contact.tableName && require_internal_entity_heuristics.hasDefiningColumn("company", columnNames(t)));
1857
+ }
1858
+ function rankCandidates$1(tables, concept, contact = null) {
1859
+ const promoted = concept === "company" ? fkPromotedCompanies(tables, contact) : [];
1860
+ const promotedNames = new Set(promoted.map((t) => t.tableName));
1705
1861
  return tables.map((t) => ({
1706
1862
  t,
1707
- m: require_internal_entity_heuristics.matchesConcept(t.tableName, concept)
1863
+ m: require_internal_entity_heuristics.matchesConcept(t.tableName, concept) ?? (promotedNames.has(t.tableName) ? { exact: false } : null)
1708
1864
  })).filter((c) => Boolean(c.m)).sort((a, b) => {
1709
1865
  const usable = Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(b.t))) - Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(a.t)));
1710
1866
  if (usable !== 0) return usable;
1867
+ const aPromoted = promotedNames.has(a.t.tableName);
1868
+ if (aPromoted !== promotedNames.has(b.t.tableName)) {
1869
+ if (require_internal_entity_heuristics.isFrameworkTable((aPromoted ? b.t : a.t).tableName)) return aPromoted ? -1 : 1;
1870
+ }
1711
1871
  const exact = Number(b.m.exact) - Number(a.m.exact);
1712
1872
  if (exact !== 0) return exact;
1713
1873
  return Number(require_internal_entity_heuristics.isFrameworkTable(a.t.tableName)) - Number(require_internal_entity_heuristics.isFrameworkTable(b.t.tableName));
@@ -1715,7 +1875,7 @@ function rankCandidates$1(tables, concept) {
1715
1875
  }
1716
1876
  function selectEntities(tables) {
1717
1877
  const contactCands = rankCandidates$1(tables, "contact");
1718
- const companyCands = rankCandidates$1(tables, "company");
1878
+ const companyCands = rankCandidates$1(tables, "company", contactCands[0] ?? null);
1719
1879
  const contactNames = new Set(contactCands.map((t) => t.tableName));
1720
1880
  const companyNames = new Set(companyCands.map((t) => t.tableName));
1721
1881
  const contactExclusive = contactCands.filter((t) => !companyNames.has(t.tableName));
@@ -3962,6 +4122,18 @@ function resolveInitDeps(options) {
3962
4122
  authWiring: renderAuthWiring("unknown")
3963
4123
  };
3964
4124
  }
4125
+ /**
4126
+ * The three detection lines, annotating any signal borrowed from a sibling.
4127
+ *
4128
+ * Naming the source package is what makes a workspace detection auditable:
4129
+ * without it the banner claims an ORM the developer cannot find in the
4130
+ * directory they are standing in.
4131
+ */
4132
+ function renderDetectionLines(detection) {
4133
+ const from = (source) => source ? ` (from ${source})` : "";
4134
+ const frameworkNote = detection.frameworkForced ? " (--framework)" : from(detection.sources.framework);
4135
+ return ` Framework: ${FRAMEWORK_LABELS[detection.framework]}${frameworkNote}\n ORM: ${detection.orm}${from(detection.sources.orm)}\n Auth: ${detection.auth}${from(detection.sources.auth)}${detection.authWired ? " (wired)" : ""}\n`;
4136
+ }
3965
4137
  async function acquireSchema(options, deps) {
3966
4138
  const { out } = deps;
3967
4139
  const appDeps = deps.readDepsFn(options.cwd);
@@ -3969,26 +4141,39 @@ async function acquireSchema(options, deps) {
3969
4141
  out(`No package.json found in ${options.cwd}. Run \`clivly init\` from inside an existing app — cd into your project first.\n`);
3970
4142
  return { code: 1 };
3971
4143
  }
3972
- const framework = detectFramework(appDeps);
3973
- const orm = detectOrm(appDeps);
3974
- const auth = detectAuth(appDeps);
4144
+ const workspacePackages = (deps.graphFor(options.cwd)?.packages ?? []).map((pkg) => ({
4145
+ deps: deps.readDepsFn(pkg.dir),
4146
+ name: pkg.name
4147
+ }));
4148
+ const detected = detectAcrossWorkspace(appDeps, workspacePackages);
4149
+ const framework = options.frameworkFlag ?? detected.framework;
4150
+ const { auth, orm, sources } = detected;
3975
4151
  deps.framework = framework;
3976
4152
  const layout = deps.resolveAppLayout(options.cwd, deps.fileExists);
3977
4153
  deps.layout = layout;
3978
4154
  out("\nClivly init\n");
3979
4155
  deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
3980
- out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
3981
- out(` ORM: ${orm}\n`);
3982
- out(` Auth: ${auth}${deps.authWiring.complete ? " (wired)" : ""}\n`);
4156
+ out(renderDetectionLines({
4157
+ auth,
4158
+ authWired: deps.authWiring.complete,
4159
+ framework,
4160
+ frameworkForced: options.frameworkFlag !== void 0,
4161
+ orm,
4162
+ sources
4163
+ }));
3983
4164
  if (framework === "nextjs") out(` Layout: ${layout}\n`);
4165
+ const frameworkCaution = options.frameworkFlag ? null : borrowedFrameworkWarning(framework, sources.framework);
4166
+ if (frameworkCaution) out(`\n${frameworkCaution}`);
3984
4167
  if (auth === "better-auth" && !deps.authWiring.complete) out("\n ⚠️ Could not locate your better-auth instance.\n The verify route will import a placeholder path that does not\n resolve. Point it at the real module with:\n\n clivly init --force --auth-import <specifier>\n\n Accepts an alias ($lib/server/auth, @/lib/auth), a package name,\n or a project-relative path (src/lib/server/auth).\n");
3985
4168
  out("\n");
3986
- if (orm !== "drizzle") {
4169
+ if (orm !== "drizzle" && !options.schemaFlag) {
3987
4170
  out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
4171
+ out(workspaceHint("orm", workspacePackages.length));
3988
4172
  return { code: 1 };
3989
4173
  }
3990
4174
  if (framework === "unknown") {
3991
4175
  out("clivly init doesn't recognise your framework yet (looked for Next.js, TanStack Start, SvelteKit, Remix, Nuxt). Route file generation needs one of these.\n");
4176
+ out(workspaceHint("framework", workspacePackages.length));
3992
4177
  if ("vue" in appDeps) out("\nUsing Vue? Clivly supports Vue through Nuxt. A Vite SPA has no server\nto mount the sync route on, so the route files need Nuxt's server\ndirectory (or any of the frameworks above).\n");
3993
4178
  return { code: 1 };
3994
4179
  }
@@ -4416,6 +4601,13 @@ function defaultLoginDeps() {
4416
4601
  };
4417
4602
  }
4418
4603
  function runInitFromArgv(argv, cwd) {
4604
+ let frameworkFlag;
4605
+ try {
4606
+ frameworkFlag = parseFrameworkFlag(flagValue(argv, "--framework"));
4607
+ } catch (error) {
4608
+ process.stdout.write(`${error.message}\n`);
4609
+ return Promise.resolve(1);
4610
+ }
4419
4611
  return runInit({
4420
4612
  cwd,
4421
4613
  force: argv.includes("--force"),
@@ -4425,6 +4617,7 @@ function runInitFromArgv(argv, cwd) {
4425
4617
  noLogin: argv.includes("--no-login"),
4426
4618
  openBrowser: !argv.includes("--no-browser"),
4427
4619
  schemaFlag: flagValue(argv, "--schema"),
4620
+ frameworkFlag,
4428
4621
  dbImportFlag: flagValue(argv, "--db-import"),
4429
4622
  authImportFlag: flagValue(argv, "--auth-import"),
4430
4623
  contactTableFlag: flagValue(argv, "--contact-table"),
package/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  //#region src/detect.d.ts
2
+ type Framework = "nextjs" | "tanstack-start" | "sveltekit" | "remix" | "nuxt" | "unknown";
2
3
  type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
3
4
  //#endregion
4
5
  //#region src/app-layout.d.ts
@@ -84,6 +85,7 @@ interface RunInitOptions {
84
85
  dryRun?: boolean;
85
86
  fileExists?: (path: string) => boolean;
86
87
  force: boolean;
88
+ frameworkFlag?: Framework;
87
89
  installPackages?: (options: {
88
90
  cwd: string;
89
91
  packages: string[];
package/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  //#region src/detect.d.ts
2
+ type Framework = "nextjs" | "tanstack-start" | "sveltekit" | "remix" | "nuxt" | "unknown";
2
3
  type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
3
4
  //#endregion
4
5
  //#region src/app-layout.d.ts
@@ -84,6 +85,7 @@ interface RunInitOptions {
84
85
  dryRun?: boolean;
85
86
  fileExists?: (path: string) => boolean;
86
87
  force: boolean;
88
+ frameworkFlag?: Framework;
87
89
  installPackages?: (options: {
88
90
  cwd: string;
89
91
  packages: string[];
package/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { t as CANONICAL_FIELDS } from "./entity-config-bC3DX9f1.mjs";
2
+ import { t as CANONICAL_FIELDS } from "./entity-config-CO0TEwZC.mjs";
3
3
  import "./mapping-score-C68uAqGB.mjs";
4
4
  import { hasDefiningColumn, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
5
- import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-C0DU5KDS.mjs";
5
+ import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-CsR4W9QQ.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { spawn } from "node:child_process";
8
8
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
@@ -292,6 +292,124 @@ function addCommand(pm, packages) {
292
292
  };
293
293
  }
294
294
  //#endregion
295
+ //#region src/detect-workspace.ts
296
+ const SELECTABLE_FRAMEWORKS = [
297
+ "nextjs",
298
+ "tanstack-start",
299
+ "sveltekit",
300
+ "remix",
301
+ "nuxt"
302
+ ];
303
+ /**
304
+ * Validate a `--framework` value.
305
+ *
306
+ * Rejecting a typo matters more than accepting one leniently: an unrecognised
307
+ * value silently falling back to detection would scaffold route files in the
308
+ * wrong shape, and a route in the wrong shape is not a build error — the
309
+ * generator simply skips it and the endpoint 404s at runtime.
310
+ */
311
+ function parseFrameworkFlag(value) {
312
+ if (value === void 0) return;
313
+ if (SELECTABLE_FRAMEWORKS.includes(value)) return value;
314
+ throw new Error(`"${value}" is not a supported framework. Accepted: ${SELECTABLE_FRAMEWORKS.join(", ")}.`);
315
+ }
316
+ /**
317
+ * A caution for when the framework came from a sibling package, or null.
318
+ *
319
+ * Borrowing the ORM or auth is unambiguous — one database and one auth instance
320
+ * serve the whole workspace. Borrowing the *framework* is not: route files are
321
+ * generated in that framework's shape and written into the directory `init` ran
322
+ * in, so if the framework lives next door the routes probably belong next door
323
+ * too. Writing TanStack `src/routes/` files into a Hono app is not a build
324
+ * error — the route generator simply never sees them and the endpoints 404.
325
+ */
326
+ function borrowedFrameworkWarning(framework, source) {
327
+ if (!source) return null;
328
+ return ` ⚠️ ${FRAMEWORK_LABELS[framework]} was detected in "${source}", not here.\n Route files are generated in that framework's shape. If this app
329
+ does not serve them, run init inside "${source}" instead, or pass\n --framework to say what this app actually is.
330
+ `;
331
+ }
332
+ /**
333
+ * Extra guidance for a detection failure inside a workspace, or "" outside one.
334
+ *
335
+ * Detection has already scanned every sibling by the time either failure fires,
336
+ * so the marker really is absent everywhere — which is worth saying, because
337
+ * the bare message sends a developer looking for a dependency that was never
338
+ * missing. Returning "" for the single-package case keeps the caller
339
+ * branch-free.
340
+ */
341
+ function workspaceHint(kind, packageCount) {
342
+ if (packageCount === 0) return "";
343
+ const scanned = `\nScanned ${packageCount} workspace package(s) and found no ${kind === "orm" ? "ORM" : "framework"} marker`;
344
+ return kind === "orm" ? `${scanned}.\nIf your schema is Drizzle, point at it and init will take your word:\n\n clivly init --schema <path-or-package>\n` : `${scanned}\nin any of them. If this app is served by one of the frameworks above,\nname it outright:\n\n clivly init --framework tanstack-start\n`;
345
+ }
346
+ /**
347
+ * Resolve one signal, preferring the app's own dependencies.
348
+ *
349
+ * The app's package.json is authoritative: a sibling on a different framework
350
+ * must never redefine what this app is. Only when the app itself is silent do
351
+ * we look outward, and then the first package by name wins.
352
+ *
353
+ * Sorting here rather than trusting the caller matters: the real package list
354
+ * comes from glob expansion, whose order is not guaranteed, so an unsorted scan
355
+ * would let the same repo detect differently between runs and report a
356
+ * different package as the source.
357
+ */
358
+ function resolveSignal(appDeps, packages, detect, unknown) {
359
+ const own = detect(appDeps);
360
+ if (own !== unknown) return {
361
+ source: null,
362
+ value: own
363
+ };
364
+ const ordered = [...packages].sort((a, b) => a.name.localeCompare(b.name));
365
+ for (const pkg of ordered) {
366
+ const found = detect(pkg.deps);
367
+ if (found !== unknown) return {
368
+ source: pkg.name,
369
+ value: found
370
+ };
371
+ }
372
+ return {
373
+ source: null,
374
+ value: unknown
375
+ };
376
+ }
377
+ /**
378
+ * Detect framework, ORM and auth across a whole workspace.
379
+ *
380
+ * A monorepo routinely splits these across packages — framework in the frontend
381
+ * app, ORM in a `db` package, auth in an `auth` package — so reading only the
382
+ * directory `init` was run from finds at most one of the three and fails
383
+ * whichever way the developer turns. Widening the search to sibling packages is
384
+ * what makes those layouts installable.
385
+ *
386
+ * Detection is deliberately a union rather than a per-package view: `init`
387
+ * generates one config for one app, and that app reaches its database and auth
388
+ * through workspace dependencies whether or not they are named in its own
389
+ * manifest.
390
+ *
391
+ * `packages` may include the app itself; that is harmless, since the app's own
392
+ * deps are consulted first anyway. Pass `[]` outside a workspace, which reduces
393
+ * this to a plain single-package read.
394
+ */
395
+ function detectAcrossWorkspace(appDeps, packages) {
396
+ const framework = resolveSignal(appDeps, packages, detectFramework, "unknown");
397
+ const orm = resolveSignal(appDeps, packages, detectOrm, "unknown");
398
+ const auth = resolveSignal(appDeps, packages, detectAuth, "unknown");
399
+ const sources = {
400
+ auth: auth.source,
401
+ framework: framework.source,
402
+ orm: orm.source
403
+ };
404
+ return {
405
+ auth: auth.value,
406
+ borrowed: Object.values(sources).some((source) => source !== null),
407
+ framework: framework.value,
408
+ orm: orm.value,
409
+ sources
410
+ };
411
+ }
412
+ //#endregion
295
413
  //#region src/tick-route.ts
296
414
  /**
297
415
  * The tick route's public URL path, and the template `clivly login` writes for
@@ -1257,10 +1375,12 @@ Usage:
1257
1375
  clivly init [--force] [--dry-run] [--diff] [--yes] [--no-login] [--no-browser]
1258
1376
  [--schema <path|package>] [--db-import <specifier>]
1259
1377
  [--auth-import <specifier>] [--contact-table <table>]
1260
- [--company-table <table>]
1378
+ [--company-table <table>] [--framework <name>]
1261
1379
 
1262
1380
  Scaffolds clivly.config.ts, the Clivly route files, and .env placeholders.
1263
1381
  Detects your framework, ORM, auth adapter and app layout from the project.
1382
+ In a workspace the whole package graph is scanned, so a monorepo that keeps
1383
+ its framework, ORM and auth in separate packages is detected from any app.
1264
1384
 
1265
1385
  Options:
1266
1386
  --dry-run Show what would be written; write nothing
@@ -1268,7 +1388,8 @@ Options:
1268
1388
  --force Overwrite files Clivly owns
1269
1389
  --yes Skip prompts
1270
1390
  --no-login Skip browser pairing
1271
- --schema <p> Path or package holding your ORM schema
1391
+ --framework <n> nextjs | tanstack-start | sveltekit | remix | nuxt
1392
+ --schema <p> Path or package holding your ORM schema (implies Drizzle)
1272
1393
  --db-import <s> Module specifier exporting your database instance
1273
1394
  --auth-import <s> Module specifier exporting your auth instance
1274
1395
  --contact-table <t> Table to map as contacts
@@ -1700,13 +1821,52 @@ function toScoredTable(table) {
1700
1821
  function columnNames(table) {
1701
1822
  return table.columns.map((column) => column.dbName);
1702
1823
  }
1703
- function rankCandidates$1(tables, concept) {
1824
+ /**
1825
+ * Company candidates the vocabulary cannot name, promoted on foreign-key
1826
+ * evidence.
1827
+ *
1828
+ * The company vocabulary is seven generic business words (companies,
1829
+ * organizations, teams, accounts…). A domain word for the same idea —
1830
+ * `campuses`, `branches`, `sites`, `practices`, `chapters` — matches none of
1831
+ * them, so such a table was never a candidate at all and no amount of ranking
1832
+ * could reach it. On `vue-academy` that left Better Auth's `organization` as
1833
+ * the only company candidate, and init duly scaffolded the auth table while
1834
+ * `campuses`, which every student row references, was invisible.
1835
+ *
1836
+ * Guessing more words does not fix this; the next unlisted word fails
1837
+ * identically. The schema already states the answer: when the CONTACT table
1838
+ * holds a real foreign key to some table, that target is what a contact
1839
+ * belongs to. `references` is reflected metadata, so this is a fact rather
1840
+ * than a name guess.
1841
+ *
1842
+ * Two guards keep it honest. The target must pass `hasDefiningColumn`, the
1843
+ * same "can this table BE a company" test every other candidate passes — a
1844
+ * join table with no `name` is not promoted. And framework tables are skipped,
1845
+ * because `contacts.organization_id → organization` is exactly the auth
1846
+ * plumbing this is meant to see past.
1847
+ */
1848
+ function fkPromotedCompanies(tables, contact) {
1849
+ if (!contact) return [];
1850
+ const targets = /* @__PURE__ */ new Set();
1851
+ for (const column of contact.columns) {
1852
+ const table = column.references?.table;
1853
+ if (table && !isFrameworkTable(table)) targets.add(table.toLowerCase());
1854
+ }
1855
+ return tables.filter((t) => targets.has(t.tableName.toLowerCase()) && t.tableName !== contact.tableName && hasDefiningColumn("company", columnNames(t)));
1856
+ }
1857
+ function rankCandidates$1(tables, concept, contact = null) {
1858
+ const promoted = concept === "company" ? fkPromotedCompanies(tables, contact) : [];
1859
+ const promotedNames = new Set(promoted.map((t) => t.tableName));
1704
1860
  return tables.map((t) => ({
1705
1861
  t,
1706
- m: matchesConcept(t.tableName, concept)
1862
+ m: matchesConcept(t.tableName, concept) ?? (promotedNames.has(t.tableName) ? { exact: false } : null)
1707
1863
  })).filter((c) => Boolean(c.m)).sort((a, b) => {
1708
1864
  const usable = Number(hasDefiningColumn(concept, columnNames(b.t))) - Number(hasDefiningColumn(concept, columnNames(a.t)));
1709
1865
  if (usable !== 0) return usable;
1866
+ const aPromoted = promotedNames.has(a.t.tableName);
1867
+ if (aPromoted !== promotedNames.has(b.t.tableName)) {
1868
+ if (isFrameworkTable((aPromoted ? b.t : a.t).tableName)) return aPromoted ? -1 : 1;
1869
+ }
1710
1870
  const exact = Number(b.m.exact) - Number(a.m.exact);
1711
1871
  if (exact !== 0) return exact;
1712
1872
  return Number(isFrameworkTable(a.t.tableName)) - Number(isFrameworkTable(b.t.tableName));
@@ -1714,7 +1874,7 @@ function rankCandidates$1(tables, concept) {
1714
1874
  }
1715
1875
  function selectEntities(tables) {
1716
1876
  const contactCands = rankCandidates$1(tables, "contact");
1717
- const companyCands = rankCandidates$1(tables, "company");
1877
+ const companyCands = rankCandidates$1(tables, "company", contactCands[0] ?? null);
1718
1878
  const contactNames = new Set(contactCands.map((t) => t.tableName));
1719
1879
  const companyNames = new Set(companyCands.map((t) => t.tableName));
1720
1880
  const contactExclusive = contactCands.filter((t) => !companyNames.has(t.tableName));
@@ -3961,6 +4121,18 @@ function resolveInitDeps(options) {
3961
4121
  authWiring: renderAuthWiring("unknown")
3962
4122
  };
3963
4123
  }
4124
+ /**
4125
+ * The three detection lines, annotating any signal borrowed from a sibling.
4126
+ *
4127
+ * Naming the source package is what makes a workspace detection auditable:
4128
+ * without it the banner claims an ORM the developer cannot find in the
4129
+ * directory they are standing in.
4130
+ */
4131
+ function renderDetectionLines(detection) {
4132
+ const from = (source) => source ? ` (from ${source})` : "";
4133
+ const frameworkNote = detection.frameworkForced ? " (--framework)" : from(detection.sources.framework);
4134
+ return ` Framework: ${FRAMEWORK_LABELS[detection.framework]}${frameworkNote}\n ORM: ${detection.orm}${from(detection.sources.orm)}\n Auth: ${detection.auth}${from(detection.sources.auth)}${detection.authWired ? " (wired)" : ""}\n`;
4135
+ }
3964
4136
  async function acquireSchema(options, deps) {
3965
4137
  const { out } = deps;
3966
4138
  const appDeps = deps.readDepsFn(options.cwd);
@@ -3968,26 +4140,39 @@ async function acquireSchema(options, deps) {
3968
4140
  out(`No package.json found in ${options.cwd}. Run \`clivly init\` from inside an existing app — cd into your project first.\n`);
3969
4141
  return { code: 1 };
3970
4142
  }
3971
- const framework = detectFramework(appDeps);
3972
- const orm = detectOrm(appDeps);
3973
- const auth = detectAuth(appDeps);
4143
+ const workspacePackages = (deps.graphFor(options.cwd)?.packages ?? []).map((pkg) => ({
4144
+ deps: deps.readDepsFn(pkg.dir),
4145
+ name: pkg.name
4146
+ }));
4147
+ const detected = detectAcrossWorkspace(appDeps, workspacePackages);
4148
+ const framework = options.frameworkFlag ?? detected.framework;
4149
+ const { auth, orm, sources } = detected;
3974
4150
  deps.framework = framework;
3975
4151
  const layout = deps.resolveAppLayout(options.cwd, deps.fileExists);
3976
4152
  deps.layout = layout;
3977
4153
  out("\nClivly init\n");
3978
4154
  deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
3979
- out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
3980
- out(` ORM: ${orm}\n`);
3981
- out(` Auth: ${auth}${deps.authWiring.complete ? " (wired)" : ""}\n`);
4155
+ out(renderDetectionLines({
4156
+ auth,
4157
+ authWired: deps.authWiring.complete,
4158
+ framework,
4159
+ frameworkForced: options.frameworkFlag !== void 0,
4160
+ orm,
4161
+ sources
4162
+ }));
3982
4163
  if (framework === "nextjs") out(` Layout: ${layout}\n`);
4164
+ const frameworkCaution = options.frameworkFlag ? null : borrowedFrameworkWarning(framework, sources.framework);
4165
+ if (frameworkCaution) out(`\n${frameworkCaution}`);
3983
4166
  if (auth === "better-auth" && !deps.authWiring.complete) out("\n ⚠️ Could not locate your better-auth instance.\n The verify route will import a placeholder path that does not\n resolve. Point it at the real module with:\n\n clivly init --force --auth-import <specifier>\n\n Accepts an alias ($lib/server/auth, @/lib/auth), a package name,\n or a project-relative path (src/lib/server/auth).\n");
3984
4167
  out("\n");
3985
- if (orm !== "drizzle") {
4168
+ if (orm !== "drizzle" && !options.schemaFlag) {
3986
4169
  out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
4170
+ out(workspaceHint("orm", workspacePackages.length));
3987
4171
  return { code: 1 };
3988
4172
  }
3989
4173
  if (framework === "unknown") {
3990
4174
  out("clivly init doesn't recognise your framework yet (looked for Next.js, TanStack Start, SvelteKit, Remix, Nuxt). Route file generation needs one of these.\n");
4175
+ out(workspaceHint("framework", workspacePackages.length));
3991
4176
  if ("vue" in appDeps) out("\nUsing Vue? Clivly supports Vue through Nuxt. A Vite SPA has no server\nto mount the sync route on, so the route files need Nuxt's server\ndirectory (or any of the frameworks above).\n");
3992
4177
  return { code: 1 };
3993
4178
  }
@@ -4415,6 +4600,13 @@ function defaultLoginDeps() {
4415
4600
  };
4416
4601
  }
4417
4602
  function runInitFromArgv(argv, cwd) {
4603
+ let frameworkFlag;
4604
+ try {
4605
+ frameworkFlag = parseFrameworkFlag(flagValue(argv, "--framework"));
4606
+ } catch (error) {
4607
+ process.stdout.write(`${error.message}\n`);
4608
+ return Promise.resolve(1);
4609
+ }
4418
4610
  return runInit({
4419
4611
  cwd,
4420
4612
  force: argv.includes("--force"),
@@ -4424,6 +4616,7 @@ function runInitFromArgv(argv, cwd) {
4424
4616
  noLogin: argv.includes("--no-login"),
4425
4617
  openBrowser: !argv.includes("--no-browser"),
4426
4618
  schemaFlag: flagValue(argv, "--schema"),
4619
+ frameworkFlag,
4427
4620
  dbImportFlag: flagValue(argv, "--db-import"),
4428
4621
  authImportFlag: flagValue(argv, "--auth-import"),
4429
4622
  contactTableFlag: flagValue(argv, "--contact-table"),
@@ -1,4 +1,4 @@
1
- import { L as ProjectionJoin, R as ProjectionMapping, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-w8oMBWf8.cjs";
1
+ import { L as ProjectionJoin, R as ProjectionMapping, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-CbS2DGmA.cjs";
2
2
  //#region ../core/src/projection-cardinality.d.ts
3
3
  declare function joinFansOut(join: ProjectionJoin, joinTable: DiscoveredSchemaTable): boolean;
4
4
  /** Every join in the projection that can multiply rows. */
@@ -1,4 +1,4 @@
1
- import { L as ProjectionJoin, R as ProjectionMapping, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-w8oMBWf8.mjs";
1
+ import { L as ProjectionJoin, R as ProjectionMapping, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-CbS2DGmA.mjs";
2
2
  //#region ../core/src/projection-cardinality.d.ts
3
3
  declare function joinFansOut(join: ProjectionJoin, joinTable: DiscoveredSchemaTable): boolean;
4
4
  /** Every join in the projection that can multiply rows. */
@@ -1,2 +1,2 @@
1
- import { t as CRMAdapter } from "./adapter-1TN0zohk.cjs";
1
+ import { t as CRMAdapter } from "./adapter-CA8bA6o-.cjs";
2
2
  export { CRMAdapter };