clivly 0.10.7 → 0.10.9

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.
package/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { t as CANONICAL_FIELDS } from "./entity-config-bC3DX9f1.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-BTzaM8bF.mjs";
5
+ import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-DofHordq.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));
@@ -1952,14 +2112,19 @@ export async function register() {
1952
2112
  return;
1953
2113
  }
1954
2114
  try {
1955
- // Built at runtime so the bundler cannot statically trace it. Next
1956
- // compiles this file for BOTH runtimes, and the guard above only runs on
1957
- // the edge — it does not stop the edge BUILD from following a literal
1958
- // import into your config, and from there into your database driver. A
1959
- // node-postgres app fails that pass on \`require('path')\` and every route
1960
- // 500s before Clivly is ever reached.
1961
- const specifier = "${importPath}";
1962
- const { default: clivly } = await import(/* webpackIgnore: true */ specifier);
2115
+ // A plain import, deliberately. Next compiles this file for BOTH runtimes
2116
+ // and follows this into your config and on into your database driver, so a
2117
+ // Node-only driver breaks the EDGE build — the guard above is a runtime
2118
+ // check and cannot prevent that. The fix belongs in next.config, where
2119
+ // \`clivly init\` writes an edge-only \`config.externals\` entry; nothing here
2120
+ // ever executes on the edge, so the driver only has to resolve.
2121
+ //
2122
+ // Do NOT make this untraceable (a variable specifier, webpackIgnore).
2123
+ // That silences the build error and breaks the app in a worse way:
2124
+ // clivly.config is TypeScript, so an unbundled import cannot load it, and
2125
+ // the catch below turns that into a warning — the app boots, serves
2126
+ // traffic, and never checks in.
2127
+ const { default: clivly } = await import("${importPath}");
1963
2128
  await clivly.start();
1964
2129
  } catch (error) {
1965
2130
  console.warn("[clivly] could not check in:", error);
@@ -2012,6 +2177,46 @@ function nextNeedsInstrumentationFlag(versionRange) {
2012
2177
  }
2013
2178
  const NEXT_INSTRUMENTATION_FLAG_STEP = `Your Next.js version reads instrumentation.ts only when it is switched on. Add this to next.config, or Clivly will never be told your app started:
2014
2179
  experimental: { instrumentationHook: true }`;
2180
+ const NODE_ONLY_DRIVERS = /* @__PURE__ */ new Set([
2181
+ "pg",
2182
+ "mysql",
2183
+ "mysql2",
2184
+ "better-sqlite3",
2185
+ "sqlite3",
2186
+ "tedious",
2187
+ "oracledb",
2188
+ "ioredis"
2189
+ ]);
2190
+ /**
2191
+ * The Node-only database drivers a config's dependency list contains.
2192
+ *
2193
+ * Next compiles instrumentation.ts for the edge runtime whether or not the app
2194
+ * uses it, and follows the boot hook's config import into the driver. `pg`
2195
+ * pulls in `pgpass` → `split2` → `stream`, none of which resolve there, so the
2196
+ * build fails and every route 500s — before any Clivly code runs.
2197
+ *
2198
+ * `serverExternalPackages` does NOT fix this. It applies to the Node server
2199
+ * bundle only; the edge pass ignores it entirely, which is why adding package
2200
+ * after package to it never terminates. An edge-only `config.externals` is what
2201
+ * works, and it is safe precisely because nothing in that bundle ever executes
2202
+ * — the boot hook returns early unless NEXT_RUNTIME is "nodejs".
2203
+ */
2204
+ function nodeOnlyDrivers(dependencies) {
2205
+ if (!dependencies) return [];
2206
+ return Object.keys(dependencies).filter((name) => NODE_ONLY_DRIVERS.has(name)).sort();
2207
+ }
2208
+ /** The next.config step an app needs when its config reaches a Node driver. */
2209
+ function nextEdgeExternalsStep(drivers) {
2210
+ const list = drivers.map((d) => `"${d}"`).join(", ");
2211
+ return `Your database driver (${drivers.join(", ")}) cannot be bundled for Next's edge runtime, which compiles instrumentation.ts whether or not you use it. Without this, every route fails to build. Add to next.config:
2212
+ webpack: (config, { nextRuntime }) => {
2213
+ if (nextRuntime === "edge") {
2214
+ config.externals = [...(config.externals ?? []), ${list}];
2215
+ }
2216
+ return config;
2217
+ }
2218
+ Nothing runs on the edge — the boot hook exits early there — so the driver only has to resolve.`;
2219
+ }
2015
2220
  const REMIX_BOOT_STEP = `Start Clivly when your app boots. Add this near the top of app/entry.server.tsx:
2016
2221
  import clivly from "./clivly.config";
2017
2222
  clivly.start().catch((error) => console.warn("[clivly] could not check in:", error));`;
@@ -2562,21 +2767,32 @@ function planEnv(args) {
2562
2767
  action: envAction(addedKeys.length, existing.length)
2563
2768
  };
2564
2769
  }
2565
- function declaredNextVersion(args) {
2770
+ function hostManifest(args) {
2566
2771
  const raw = args.readFile(join(args.cwd, "package.json"));
2567
- if (!raw) return;
2568
- let pkg = {};
2772
+ if (!raw) return {};
2569
2773
  try {
2570
- pkg = JSON.parse(raw);
2774
+ return JSON.parse(raw);
2571
2775
  } catch {
2572
- pkg = {};
2776
+ return {};
2573
2777
  }
2778
+ }
2779
+ function declaredNextVersion(args) {
2780
+ const pkg = hostManifest(args);
2574
2781
  return pkg.dependencies?.next ?? pkg.devDependencies?.next;
2575
2782
  }
2783
+ function declaredDependencies(args) {
2784
+ return hostManifest(args).dependencies;
2785
+ }
2786
+ /** The edge-externals step, or null when no Node-only driver is declared. */
2787
+ function edgeExternalsStepFor(dependencies) {
2788
+ const drivers = nodeOnlyDrivers(dependencies);
2789
+ return drivers.length > 0 ? nextEdgeExternalsStep(drivers) : null;
2790
+ }
2576
2791
  function planBootPatch(args) {
2577
2792
  if (bootHookPath(args.framework, args.layout) !== null) return {
2578
2793
  file: null,
2579
- manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null
2794
+ manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null,
2795
+ extra: args.framework === "nextjs" ? edgeExternalsStepFor(declaredDependencies(args)) : null
2580
2796
  };
2581
2797
  if (args.framework === "remix") return {
2582
2798
  file: null,
@@ -2622,7 +2838,7 @@ function planMutations(args) {
2622
2838
  return {
2623
2839
  files,
2624
2840
  env: planEnv(args),
2625
- manualSteps: boot.manual ? [boot.manual] : []
2841
+ manualSteps: [boot.manual, boot.extra ?? null].filter((step) => step !== null)
2626
2842
  };
2627
2843
  }
2628
2844
  function planWidgetMutations(args) {
@@ -3905,6 +4121,18 @@ function resolveInitDeps(options) {
3905
4121
  authWiring: renderAuthWiring("unknown")
3906
4122
  };
3907
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
+ }
3908
4136
  async function acquireSchema(options, deps) {
3909
4137
  const { out } = deps;
3910
4138
  const appDeps = deps.readDepsFn(options.cwd);
@@ -3912,26 +4140,39 @@ async function acquireSchema(options, deps) {
3912
4140
  out(`No package.json found in ${options.cwd}. Run \`clivly init\` from inside an existing app — cd into your project first.\n`);
3913
4141
  return { code: 1 };
3914
4142
  }
3915
- const framework = detectFramework(appDeps);
3916
- const orm = detectOrm(appDeps);
3917
- 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;
3918
4150
  deps.framework = framework;
3919
4151
  const layout = deps.resolveAppLayout(options.cwd, deps.fileExists);
3920
4152
  deps.layout = layout;
3921
4153
  out("\nClivly init\n");
3922
4154
  deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
3923
- out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
3924
- out(` ORM: ${orm}\n`);
3925
- 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
+ }));
3926
4163
  if (framework === "nextjs") out(` Layout: ${layout}\n`);
4164
+ const frameworkCaution = options.frameworkFlag ? null : borrowedFrameworkWarning(framework, sources.framework);
4165
+ if (frameworkCaution) out(`\n${frameworkCaution}`);
3927
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");
3928
4167
  out("\n");
3929
- if (orm !== "drizzle") {
4168
+ if (orm !== "drizzle" && !options.schemaFlag) {
3930
4169
  out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
4170
+ out(workspaceHint("orm", workspacePackages.length));
3931
4171
  return { code: 1 };
3932
4172
  }
3933
4173
  if (framework === "unknown") {
3934
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));
3935
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");
3936
4177
  return { code: 1 };
3937
4178
  }
@@ -4029,6 +4270,10 @@ function previewMutations(deps, mutations, options) {
4029
4270
  out(` ${mutations.env.action.padEnd(9)} .env\n`);
4030
4271
  for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
4031
4272
  }
4273
+ if (mutations.manualSteps.length > 0) {
4274
+ out("\nYou will also need to:\n");
4275
+ for (const step of mutations.manualSteps) out(`\n ! ${step}\n`);
4276
+ }
4032
4277
  out(options.diff ? "\nRe-run without --diff to apply; --force to overwrite the files marked conflict.\n" : "\nRe-run without --dry-run to apply.\n");
4033
4278
  }
4034
4279
  function printNextSteps(out, authComplete, loginRan, envFile, needsViteDotenv = false) {
@@ -4355,6 +4600,13 @@ function defaultLoginDeps() {
4355
4600
  };
4356
4601
  }
4357
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
+ }
4358
4610
  return runInit({
4359
4611
  cwd,
4360
4612
  force: argv.includes("--force"),
@@ -4364,6 +4616,7 @@ function runInitFromArgv(argv, cwd) {
4364
4616
  noLogin: argv.includes("--no-login"),
4365
4617
  openBrowser: !argv.includes("--no-browser"),
4366
4618
  schemaFlag: flagValue(argv, "--schema"),
4619
+ frameworkFlag,
4367
4620
  dbImportFlag: flagValue(argv, "--db-import"),
4368
4621
  authImportFlag: flagValue(argv, "--auth-import"),
4369
4622
  contactTableFlag: flagValue(argv, "--contact-table"),
@@ -332,6 +332,22 @@ interface SyncSource {
332
332
  * absent from another, and read by the runtime.
333
333
  */
334
334
  objectType?: string;
335
+ /**
336
+ * The `source` label a push carries, when it differs from `entity`.
337
+ *
338
+ * Only a projection sets this, and only because a projection entity has no
339
+ * choice but to carry two names: its entity key doubles as the custom-object
340
+ * slug and must be kebab-case, while `projection.entityKey` is validated by
341
+ * `/^[a-z][a-z0-9_]*$/` and rejects hyphens. The server resolves that by
342
+ * storing the mapping's `sourceTable` as `projection.baseTable` — so a push
343
+ * labelled with the entity key matched no mapping and every row was dropped,
344
+ * while `push-schema` reported the mapping created and exited 0.
345
+ *
346
+ * Deliberately NOT folded into `entity`: that value is also the cursor-store
347
+ * key, so redefining it would silently reset the sync cursor of every
348
+ * existing projection and re-push its whole history.
349
+ */
350
+ pushSource?: string;
335
351
  /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
336
352
  [DRIZZLE_META]?: DrizzleSourceMeta;
337
353
  }
@@ -332,6 +332,22 @@ interface SyncSource {
332
332
  * absent from another, and read by the runtime.
333
333
  */
334
334
  objectType?: string;
335
+ /**
336
+ * The `source` label a push carries, when it differs from `entity`.
337
+ *
338
+ * Only a projection sets this, and only because a projection entity has no
339
+ * choice but to carry two names: its entity key doubles as the custom-object
340
+ * slug and must be kebab-case, while `projection.entityKey` is validated by
341
+ * `/^[a-z][a-z0-9_]*$/` and rejects hyphens. The server resolves that by
342
+ * storing the mapping's `sourceTable` as `projection.baseTable` — so a push
343
+ * labelled with the entity key matched no mapping and every row was dropped,
344
+ * while `push-schema` reported the mapping created and exited 0.
345
+ *
346
+ * Deliberately NOT folded into `entity`: that value is also the cursor-store
347
+ * key, so redefining it would silently reset the sync cursor of every
348
+ * existing projection and re-push its whole history.
349
+ */
350
+ pushSource?: string;
335
351
  /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
336
352
  [DRIZZLE_META]?: DrizzleSourceMeta;
337
353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clivly",
3
- "version": "0.10.7",
3
+ "version": "0.10.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly CLI — scaffold a CRM into your app (clivly init).",