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/README.md +31 -1
- package/drizzle.cjs +2 -0
- package/drizzle.d.cts +1 -1
- package/drizzle.d.mts +1 -1
- package/drizzle.mjs +2 -0
- package/index.cjs +281 -28
- package/index.d.cts +2 -0
- package/index.d.mts +2 -0
- package/index.mjs +281 -28
- package/{namespace-probe-ouHKZas2.d.mts → namespace-probe-BmxtnOfc.d.mts} +16 -0
- package/{namespace-probe-lj_0bJrc.d.cts → namespace-probe-C3xMOqWE.d.cts} +16 -0
- package/package.json +1 -1
- package/{sdk-DbvqXNSh.cjs → sdk-BIYu0N3V.cjs} +113 -16
- package/{sdk-BTzaM8bF.mjs → sdk-DofHordq.mjs} +113 -16
- package/sdk.cjs +1 -1
- package/sdk.d.cts +1 -1
- package/sdk.d.mts +1 -1
- package/sdk.mjs +1 -1
- package/vite.d.cts +1 -1
- package/vite.d.mts +1 -1
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-
|
|
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
|
-
--
|
|
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
|
-
|
|
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
|
-
//
|
|
1956
|
-
//
|
|
1957
|
-
//
|
|
1958
|
-
//
|
|
1959
|
-
//
|
|
1960
|
-
//
|
|
1961
|
-
|
|
1962
|
-
|
|
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
|
|
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
|
-
|
|
2774
|
+
return JSON.parse(raw);
|
|
2571
2775
|
} catch {
|
|
2572
|
-
|
|
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
|
|
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
|
|
3916
|
-
|
|
3917
|
-
|
|
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(
|
|
3924
|
-
|
|
3925
|
-
|
|
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
|
}
|