clivly 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -134
- package/core-drizzle.cjs +3 -1
- package/core-drizzle.d.cts +3 -2
- package/core-drizzle.d.mts +3 -2
- package/core-drizzle.mjs +2 -2
- package/core-entity-config.cjs +2 -1
- package/core-entity-config.d.cts +2 -2
- package/core-entity-config.d.mts +2 -2
- package/core-entity-config.mjs +2 -2
- package/core-entity-heuristics.d.cts +3 -2
- package/core-entity-heuristics.d.mts +3 -2
- package/core-mapping-form.d.cts +1 -1
- package/core-mapping-form.d.mts +1 -1
- package/core-mappings-config.cjs +1 -1
- package/core-mappings-config.d.cts +1 -1
- package/core-mappings-config.d.mts +1 -1
- package/core-mappings-config.mjs +1 -1
- package/core-sync-engine.cjs +1 -1
- package/core-sync-engine.d.cts +1 -1
- package/core-sync-engine.d.mts +1 -1
- package/core-sync-engine.mjs +1 -1
- package/core-view-compiler.cjs +2 -1
- package/core-view-compiler.d.cts +2 -2
- package/core-view-compiler.d.mts +2 -2
- package/core-view-compiler.mjs +2 -2
- package/core.cjs +21 -5
- package/core.d.cts +7 -6
- package/core.d.mts +7 -6
- package/core.mjs +7 -6
- package/drizzle-3kvhkwO7.mjs +110 -0
- package/drizzle-DooDUGqO.d.mts +41 -0
- package/drizzle-bfjVzYR6.cjs +127 -0
- package/drizzle-jz0lrAgl.d.cts +41 -0
- package/drizzle.cjs +12 -40
- package/drizzle.d.cts +3 -11
- package/drizzle.d.mts +3 -11
- package/drizzle.mjs +12 -41
- package/entity-config-D08zAeFY.mjs +657 -0
- package/{entity-config-DoUCXtq5.d.cts → entity-config-fKQhimtD.d.cts} +263 -2
- package/{entity-config-DoUCXtq5.d.mts → entity-config-fKQhimtD.d.mts} +263 -2
- package/entity-config-nuXge6rY.cjs +778 -0
- package/index.cjs +768 -122
- package/index.d.cts +12 -0
- package/index.d.mts +12 -0
- package/index.mjs +769 -123
- package/internal-BGVUG312.mjs +68 -0
- package/internal-BLCK81kB.d.mts +72 -0
- package/internal-C-YBW2DB.d.cts +72 -0
- package/internal-D4HDi2GC.cjs +97 -0
- package/internal-drizzle.cjs +3 -1
- package/internal-drizzle.d.cts +2 -2
- package/internal-drizzle.d.mts +2 -2
- package/internal-drizzle.mjs +2 -2
- package/internal-entity-config.cjs +2 -1
- package/internal-entity-config.d.cts +2 -2
- package/internal-entity-config.d.mts +2 -2
- package/internal-entity-config.mjs +2 -2
- package/internal-entity-heuristics.cjs +28 -8
- package/internal-entity-heuristics.d.cts +2 -12
- package/internal-entity-heuristics.d.mts +2 -12
- package/internal-entity-heuristics.mjs +28 -8
- package/internal-mapping-form.d.cts +1 -1
- package/internal-mapping-form.d.mts +1 -1
- package/internal-mappings-config.cjs +1 -1
- package/internal-mappings-config.d.cts +1 -1
- package/internal-mappings-config.d.mts +1 -1
- package/internal-mappings-config.mjs +1 -1
- package/internal-sync-engine.cjs +1 -1
- package/internal-sync-engine.d.cts +1 -1
- package/internal-sync-engine.d.mts +1 -1
- package/internal-sync-engine.mjs +1 -1
- package/internal-view-compiler.cjs +2 -1
- package/internal-view-compiler.d.cts +2 -2
- package/internal-view-compiler.d.mts +2 -2
- package/internal-view-compiler.mjs +2 -2
- package/internal.cjs +21 -4
- package/internal.d.cts +7 -6
- package/internal.d.mts +7 -6
- package/internal.mjs +7 -5
- package/{mapping-form-D93lhHEc.d.mts → mapping-form-DhY0iFmN.d.cts} +1 -1
- package/{mapping-form-lfyEE7EJ.d.cts → mapping-form-Du36VZr1.d.mts} +1 -1
- package/{mappings-config-DsUa7-1w.d.mts → mappings-config-B1qNM8VL.d.cts} +7 -1
- package/{mappings-config-XmzB4dag.d.cts → mappings-config-BChZsany.d.mts} +7 -1
- package/{mappings-config-BsRADNRv.cjs → mappings-config-C8ynKmDK.cjs} +32 -6
- package/{mappings-config-Dbgxx3Nv.mjs → mappings-config-DNaYGyKN.mjs} +32 -6
- package/namespace-probe-BkUs8raG.d.mts +381 -0
- package/namespace-probe-aRFmpU85.d.cts +381 -0
- package/package.json +1 -1
- package/{entity-config-va2bkH5o.mjs → projection-C5244vGa.cjs} +130 -309
- package/{entity-config-D87rV-tE.cjs → projection-D7xmQP4S.mjs} +60 -381
- package/{sdk-iqOXeWIp.cjs → sdk-Bj-ZDf4u.cjs} +126 -28
- package/{sdk-C6WANW8M.mjs → sdk-BvRD2jAp.mjs} +126 -28
- package/sdk.cjs +2 -2
- package/sdk.d.cts +36 -1
- package/sdk.d.mts +36 -1
- package/sdk.mjs +2 -2
- package/{sync-engine-DGk_AeHJ.d.mts → sync-engine-DHQ7rXZe.d.mts} +2 -2
- package/{sync-engine-B7QIbXro.d.cts → sync-engine-DiPjH53O.d.cts} +2 -2
- package/{sync-engine-Do5ru3WF.mjs → sync-engine-SuxSzbZ4.mjs} +9 -2
- package/{sync-engine-CzEbpQGv.cjs → sync-engine-abE94g6X.cjs} +9 -2
- package/{view-compiler-BIwvm-o2.cjs → view-compiler-Blu4OXJ7.cjs} +103 -12
- package/{view-compiler-ChWr_Lml.mjs → view-compiler-CQx8TRrW.mjs} +98 -13
- package/{view-compiler-BWz0sGdm.d.cts → view-compiler-CcUwzZut.d.mts} +12 -2
- package/{view-compiler-Bn_EJJMO.d.mts → view-compiler-DqAdVxLq.d.cts} +12 -2
- package/vite.d.cts +1 -1
- package/vite.d.mts +1 -1
- package/drizzle-B89tLo4T.mjs +0 -31
- package/drizzle-CFOr9dU9.d.mts +0 -20
- package/drizzle-DiReZf6d.cjs +0 -36
- package/drizzle-DmeKSqeF.d.cts +0 -20
- package/types-CsL0rFYh.d.cts +0 -155
- package/types-CsL0rFYh.d.mts +0 -155
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-
|
|
3
|
+
const require_entity_config = require("./entity-config-nuXge6rY.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-
|
|
6
|
+
const require_sdk = require("./sdk-Bj-ZDf4u.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");
|
|
@@ -12,6 +12,72 @@ let node_path = require("node:path");
|
|
|
12
12
|
let node_readline_promises = require("node:readline/promises");
|
|
13
13
|
let jiti = require("jiti");
|
|
14
14
|
let node_url = require("node:url");
|
|
15
|
+
//#region ../core/src/env-file.ts
|
|
16
|
+
const ENV_FILES = {
|
|
17
|
+
next: ".env.local",
|
|
18
|
+
"tanstack-start": ".env",
|
|
19
|
+
sveltekit: ".env",
|
|
20
|
+
remix: ".env",
|
|
21
|
+
nuxt: ".env",
|
|
22
|
+
"hono-workers": ".dev.vars"
|
|
23
|
+
};
|
|
24
|
+
/** The conventional env file for a framework. `.env` when we do not know. */
|
|
25
|
+
function envFileFor(framework) {
|
|
26
|
+
return ENV_FILES[normalizeFramework(framework)] ?? ".env";
|
|
27
|
+
}
|
|
28
|
+
const ALIASES = {
|
|
29
|
+
next: "next",
|
|
30
|
+
nextjs: "next",
|
|
31
|
+
"next.js": "next",
|
|
32
|
+
"tanstack-start": "tanstack-start",
|
|
33
|
+
tanstack: "tanstack-start",
|
|
34
|
+
sveltekit: "sveltekit",
|
|
35
|
+
remix: "remix",
|
|
36
|
+
nuxt: "nuxt",
|
|
37
|
+
"hono-workers": "hono-workers",
|
|
38
|
+
hono: "hono-workers",
|
|
39
|
+
workers: "hono-workers"
|
|
40
|
+
};
|
|
41
|
+
/** The canonical id for a framework name, or null when unrecognised. */
|
|
42
|
+
function normalizeFramework(framework) {
|
|
43
|
+
if (!framework) return null;
|
|
44
|
+
return ALIASES[framework.toLowerCase()] ?? null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Which env file to actually write to.
|
|
48
|
+
*
|
|
49
|
+
* Prefers a file that already holds the key over the conventional one: a
|
|
50
|
+
* project that has been running with credentials in `.env` must not suddenly
|
|
51
|
+
* get a second copy in `.env.local`, leaving two files disagreeing and the
|
|
52
|
+
* stale one still loading. Convention applies to new setups, not to a working
|
|
53
|
+
* one someone already has.
|
|
54
|
+
*/
|
|
55
|
+
function resolveEnvFile(input) {
|
|
56
|
+
const conventional = envFileFor(input.framework);
|
|
57
|
+
if (input.existing.includes(conventional)) return conventional;
|
|
58
|
+
return input.existing[0] ?? conventional;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/app-layout.ts
|
|
62
|
+
function resolveAppLayout(cwd, fileExists = node_fs.existsSync) {
|
|
63
|
+
if (fileExists((0, node_path.join)(cwd, "src", "app"))) return "src/app";
|
|
64
|
+
if (fileExists((0, node_path.join)(cwd, "app"))) return "app";
|
|
65
|
+
return fileExists((0, node_path.join)(cwd, "src")) ? "src/app" : "app";
|
|
66
|
+
}
|
|
67
|
+
function shadowedAppDirError(cwd, framework, plannedPaths, fileExists = node_fs.existsSync) {
|
|
68
|
+
if (framework !== "nextjs") return null;
|
|
69
|
+
if (!plannedPaths.some((path) => path.startsWith("app/"))) return null;
|
|
70
|
+
if (!fileExists((0, node_path.join)(cwd, "src", "app"))) return null;
|
|
71
|
+
return `This project has a src/app directory, so writing routes to app/ would shadow it.
|
|
72
|
+
|
|
73
|
+
Next.js reads either app/ or src/app/, never both. A root app/ directory wins,
|
|
74
|
+
and every route under src/app/ disappears from the build.
|
|
75
|
+
|
|
76
|
+
Nothing was written. Please report this at https://github.com/amani-joseph/clivly/issues
|
|
77
|
+
— Clivly should have detected the src/app layout automatically.
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
15
81
|
//#region src/load-config.ts
|
|
16
82
|
const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
|
|
17
83
|
function resolveConfigPath(cwd, configPath) {
|
|
@@ -109,6 +175,14 @@ async function loadCliConfig(cwd) {
|
|
|
109
175
|
return envFiles ? { envFiles } : {};
|
|
110
176
|
}
|
|
111
177
|
//#endregion
|
|
178
|
+
//#region src/dashboard-locations.ts
|
|
179
|
+
/** Secret + publishable keys. The Connect dialog mints them and shows the secret once. */
|
|
180
|
+
const CREDENTIALS_LOCATION = "Clivly dashboard → Connect app (or Settings → API keys)";
|
|
181
|
+
/** The signing secret Clivly Cloud uses on sync-trigger callbacks. */
|
|
182
|
+
const SYNC_SECRET_LOCATION = "Clivly dashboard → Integrations → Remote sync";
|
|
183
|
+
/** Chat widget records and their allowed origins. */
|
|
184
|
+
const WIDGET_LOCATION = "Clivly dashboard → Settings → Chat widget";
|
|
185
|
+
//#endregion
|
|
112
186
|
//#region src/detect.ts
|
|
113
187
|
const FRAMEWORK_MARKERS = [
|
|
114
188
|
{
|
|
@@ -254,8 +328,19 @@ const OVERRIDE_FAILURE_CAUSES = {
|
|
|
254
328
|
not_enabled: "The target backend does not have the feature-flagged trigger-url endpoint enabled (likely the wrong backend, or the flag is off).",
|
|
255
329
|
unreachable: "Could not reach Clivly at the selected API base URL."
|
|
256
330
|
};
|
|
257
|
-
|
|
258
|
-
|
|
331
|
+
/**
|
|
332
|
+
* `status` is required in practice for `unexpected_response` and meaningless
|
|
333
|
+
* for the others.
|
|
334
|
+
*
|
|
335
|
+
* Every non-401/404 status used to collapse into `unreachable`, so the CLI told
|
|
336
|
+
* developers it "could not reach Clivly" while Clivly was answering. That is
|
|
337
|
+
* not a wording nit: when neon-http broke these endpoints in production the CLI
|
|
338
|
+
* discarded the one honest signal it had (HTTP 500) and sent people to check
|
|
339
|
+
* their network and API URL, where nothing was wrong. A response is proof of
|
|
340
|
+
* reachability — so say what came back.
|
|
341
|
+
*/
|
|
342
|
+
function overrideFailureMessage(kind, status) {
|
|
343
|
+
return `Warning: could not register the tunnel URL — ${kind === "unexpected_response" ? `Clivly answered HTTP ${status ?? "an error"} — the request reached Clivly but was rejected, so this is a Clivly-side or request problem rather than a network one.` : OVERRIDE_FAILURE_CAUSES[kind]} Dashboard-triggered sync is unavailable this session. Your app is still running.`;
|
|
259
344
|
}
|
|
260
345
|
function cloudflaredMissingMessage() {
|
|
261
346
|
return [
|
|
@@ -310,12 +395,16 @@ async function runDev(options) {
|
|
|
310
395
|
deps.out(`Tunnel: ${triggerUrl}`);
|
|
311
396
|
const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
|
|
312
397
|
if (result.ok) deps.out("Dashboard-triggered sync now points at this local session.");
|
|
313
|
-
else deps.out(overrideFailureMessage(result.kind));
|
|
398
|
+
else deps.out(overrideFailureMessage(result.kind, result.status));
|
|
314
399
|
});
|
|
400
|
+
const stopWatching = deps.watchConfig?.(options.cwd, () => {
|
|
401
|
+
deps.out("clivly.config.ts changed — restart your app for it to take effect.");
|
|
402
|
+
}) ?? null;
|
|
315
403
|
return await new Promise((resolve) => {
|
|
316
404
|
const shutdown = async (code) => {
|
|
317
405
|
if (shuttingDown) return;
|
|
318
406
|
shuttingDown = true;
|
|
407
|
+
stopWatching?.();
|
|
319
408
|
tunnel.kill();
|
|
320
409
|
app?.kill();
|
|
321
410
|
if (!(await deps.clear().catch(() => UNREACHABLE_RESULT)).ok) deps.out("Warning: could not clear the tunnel override on exit.");
|
|
@@ -337,7 +426,18 @@ const HTTP_NOT_FOUND = 404;
|
|
|
337
426
|
function kindForStatus(status) {
|
|
338
427
|
if (status === HTTP_UNAUTHORIZED) return "unauthorized";
|
|
339
428
|
if (status === HTTP_NOT_FOUND) return "not_enabled";
|
|
340
|
-
return "
|
|
429
|
+
return "unexpected_response";
|
|
430
|
+
}
|
|
431
|
+
function failureFor(status) {
|
|
432
|
+
const kind = kindForStatus(status);
|
|
433
|
+
return kind === "unexpected_response" ? {
|
|
434
|
+
ok: false,
|
|
435
|
+
kind,
|
|
436
|
+
status
|
|
437
|
+
} : {
|
|
438
|
+
ok: false,
|
|
439
|
+
kind
|
|
440
|
+
};
|
|
341
441
|
}
|
|
342
442
|
async function registerTunnelOverride(deps) {
|
|
343
443
|
const res = await (deps.fetchImpl ?? globalThis.fetch)(`${deps.apiUrl}${TRIGGER_URL_PATH}`, {
|
|
@@ -349,10 +449,7 @@ async function registerTunnelOverride(deps) {
|
|
|
349
449
|
body: JSON.stringify({ url: deps.triggerUrl })
|
|
350
450
|
});
|
|
351
451
|
if (res.ok) return { ok: true };
|
|
352
|
-
return
|
|
353
|
-
ok: false,
|
|
354
|
-
kind: kindForStatus(res.status)
|
|
355
|
-
};
|
|
452
|
+
return failureFor(res.status);
|
|
356
453
|
}
|
|
357
454
|
async function clearTunnelOverride(deps) {
|
|
358
455
|
const res = await (deps.fetchImpl ?? globalThis.fetch)(`${deps.apiUrl}${TRIGGER_URL_PATH}`, {
|
|
@@ -360,10 +457,7 @@ async function clearTunnelOverride(deps) {
|
|
|
360
457
|
headers: { authorization: `Bearer ${deps.apiKey}` }
|
|
361
458
|
});
|
|
362
459
|
if (res.ok) return { ok: true };
|
|
363
|
-
return
|
|
364
|
-
ok: false,
|
|
365
|
-
kind: kindForStatus(res.status)
|
|
366
|
-
};
|
|
460
|
+
return failureFor(res.status);
|
|
367
461
|
}
|
|
368
462
|
//#endregion
|
|
369
463
|
//#region src/probe-module.ts
|
|
@@ -688,6 +782,30 @@ function buildFkByColumn(pgCore, drizzle, table) {
|
|
|
688
782
|
} catch {}
|
|
689
783
|
return fkByColumn;
|
|
690
784
|
}
|
|
785
|
+
function buildUniqueConstraints(pgCore, table) {
|
|
786
|
+
if (!pgCore) return;
|
|
787
|
+
const keys = [];
|
|
788
|
+
try {
|
|
789
|
+
const config = pgCore.getTableConfig(table);
|
|
790
|
+
for (const column of config.columns ?? []) if (column.isUnique || column.primary) keys.push([column.name]);
|
|
791
|
+
for (const pk of config.primaryKeys ?? []) keys.push(pk.columns.map((c) => c.name));
|
|
792
|
+
for (const constraint of config.uniqueConstraints ?? []) keys.push(constraint.columns.map((c) => c.name));
|
|
793
|
+
for (const index of config.indexes ?? []) {
|
|
794
|
+
if (!index.config?.unique) continue;
|
|
795
|
+
const columns = (index.config.columns ?? []).map((c) => c?.name).filter((name) => Boolean(name));
|
|
796
|
+
if (columns.length > 0) keys.push(columns);
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const seen = /* @__PURE__ */ new Set();
|
|
802
|
+
return keys.filter((key) => {
|
|
803
|
+
const id = [...key].sort().join("\0");
|
|
804
|
+
if (seen.has(id) || key.length === 0) return false;
|
|
805
|
+
seen.add(id);
|
|
806
|
+
return true;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
691
809
|
function toDiscoveredColumn(key, col, fkByColumn) {
|
|
692
810
|
let type;
|
|
693
811
|
try {
|
|
@@ -735,10 +853,12 @@ async function discoverSchema(opts) {
|
|
|
735
853
|
if (drizzle.is(value, drizzle.Table)) {
|
|
736
854
|
const fkByColumn = buildFkByColumn(pgCore, drizzle, value);
|
|
737
855
|
const columns = Object.entries(drizzle.getTableColumns(value)).map(([key, col]) => toDiscoveredColumn(key, col, fkByColumn));
|
|
856
|
+
const uniqueConstraints = buildUniqueConstraints(pgCore, value);
|
|
738
857
|
tables.push({
|
|
739
858
|
moduleKey,
|
|
740
859
|
tableName: drizzle.getTableName(value),
|
|
741
|
-
columns
|
|
860
|
+
columns,
|
|
861
|
+
...uniqueConstraints === void 0 ? {} : { uniqueConstraints }
|
|
742
862
|
});
|
|
743
863
|
continue;
|
|
744
864
|
}
|
|
@@ -768,6 +888,29 @@ async function discoverSchema(opts) {
|
|
|
768
888
|
};
|
|
769
889
|
}
|
|
770
890
|
//#endregion
|
|
891
|
+
//#region src/engine-check.ts
|
|
892
|
+
const MAJOR = /^v?(\d+)/;
|
|
893
|
+
const FIRST_NUMBER = /(\d+)/;
|
|
894
|
+
function currentMajor(version) {
|
|
895
|
+
const match = MAJOR.exec(version);
|
|
896
|
+
return match ? Number(match[1]) : null;
|
|
897
|
+
}
|
|
898
|
+
function requiredMajor(range) {
|
|
899
|
+
if (!range) return null;
|
|
900
|
+
const match = FIRST_NUMBER.exec(range);
|
|
901
|
+
return match ? Number(match[1]) : null;
|
|
902
|
+
}
|
|
903
|
+
function engineError(currentVersion, enginesRange) {
|
|
904
|
+
const have = currentMajor(currentVersion);
|
|
905
|
+
const need = requiredMajor(enginesRange);
|
|
906
|
+
if (have === null || need === null || have >= need) return null;
|
|
907
|
+
return `Clivly requires Node.js ${need} or newer. You are running ${currentVersion}.
|
|
908
|
+
|
|
909
|
+
Upgrade with \`nvm install ${need}\` (or your version manager of choice), then re-run.
|
|
910
|
+
Set CLIVLY_SKIP_ENGINE_CHECK=1 to run anyway — expect unrelated-looking failures.
|
|
911
|
+
`;
|
|
912
|
+
}
|
|
913
|
+
//#endregion
|
|
771
914
|
//#region src/env-file.ts
|
|
772
915
|
const REGEX_METACHARACTERS = /[.*+?^${}()|[\]\\]/g;
|
|
773
916
|
function escapeForRegExp(key) {
|
|
@@ -852,6 +995,123 @@ function upsertEnvValues(existing, entries, options = {}) {
|
|
|
852
995
|
};
|
|
853
996
|
}
|
|
854
997
|
//#endregion
|
|
998
|
+
//#region src/help.ts
|
|
999
|
+
const ROOT_HELP = `clivly — embed a CRM in your app
|
|
1000
|
+
|
|
1001
|
+
Usage:
|
|
1002
|
+
clivly init [options] Scaffold Clivly (config, routes, env file) for your app
|
|
1003
|
+
clivly add widget [options] Scaffold the chat-session route
|
|
1004
|
+
clivly status [options] Check your setup, step by step
|
|
1005
|
+
clivly push-schema [options] Report your schema to Clivly without deploying
|
|
1006
|
+
clivly dev [options] Tunnel localhost so remote sync can reach you
|
|
1007
|
+
clivly login [options] Pair this machine and write credentials to your env file
|
|
1008
|
+
clivly --version Print the installed clivly version
|
|
1009
|
+
|
|
1010
|
+
Deprecated aliases — each runs part of \`clivly status\` and still works:
|
|
1011
|
+
clivly ping credential + reachability
|
|
1012
|
+
clivly connect the above, plus config and trigger URL
|
|
1013
|
+
clivly doctor config, setup checks and edge safety
|
|
1014
|
+
|
|
1015
|
+
Prefer \`clivly status\`: it runs the whole ladder and tells you which step you
|
|
1016
|
+
are on. The aliases are kept because output and docs have recommended them.
|
|
1017
|
+
|
|
1018
|
+
Run \`clivly <command> --help\` for details on a command.
|
|
1019
|
+
`;
|
|
1020
|
+
const COMMAND_HELP = {
|
|
1021
|
+
init: `clivly init — scaffold Clivly into this app
|
|
1022
|
+
|
|
1023
|
+
Usage:
|
|
1024
|
+
clivly init [--force] [--dry-run] [--diff] [--yes] [--no-login] [--no-browser]
|
|
1025
|
+
[--schema <path|package>] [--db-import <specifier>]
|
|
1026
|
+
[--auth-import <specifier>] [--contact-table <table>]
|
|
1027
|
+
[--company-table <table>]
|
|
1028
|
+
|
|
1029
|
+
Scaffolds clivly.config.ts, the Clivly route files, and .env placeholders.
|
|
1030
|
+
Detects your framework, ORM, auth adapter and app layout from the project.
|
|
1031
|
+
|
|
1032
|
+
Options:
|
|
1033
|
+
--dry-run Show what would be written; write nothing
|
|
1034
|
+
--diff Show a diff for files that already exist
|
|
1035
|
+
--force Overwrite files Clivly owns
|
|
1036
|
+
--yes Skip prompts
|
|
1037
|
+
--no-login Skip browser pairing
|
|
1038
|
+
--schema <p> Path or package holding your ORM schema
|
|
1039
|
+
--db-import <s> Module specifier exporting your database instance
|
|
1040
|
+
--auth-import <s> Module specifier exporting your auth instance
|
|
1041
|
+
--contact-table <t> Table to map as contacts
|
|
1042
|
+
--company-table <t> Table to map as companies
|
|
1043
|
+
`,
|
|
1044
|
+
add: `clivly add widget — scaffold the chat-session route
|
|
1045
|
+
|
|
1046
|
+
Usage:
|
|
1047
|
+
clivly add widget [--force] [--dry-run]
|
|
1048
|
+
|
|
1049
|
+
Writes the route that mints chat-widget sessions. Creates no widget record —
|
|
1050
|
+
that lives in the Clivly dashboard.
|
|
1051
|
+
|
|
1052
|
+
Options:
|
|
1053
|
+
--dry-run Show what would be written; write nothing
|
|
1054
|
+
--force Overwrite the route if it already exists
|
|
1055
|
+
`,
|
|
1056
|
+
status: `clivly status — check your setup, step by step
|
|
1057
|
+
|
|
1058
|
+
Usage:
|
|
1059
|
+
clivly status [--verbose] [--config <path>] [--env-file <path>]
|
|
1060
|
+
|
|
1061
|
+
Runs the setup ladder: credential, reachability, config load, edge-safety,
|
|
1062
|
+
setup checks, sync trigger. Read-only — writes nothing.
|
|
1063
|
+
|
|
1064
|
+
Options:
|
|
1065
|
+
--verbose Show every check, including those that passed
|
|
1066
|
+
--config <path> Path to clivly.config.ts
|
|
1067
|
+
--env-file <path> Extra env file to load
|
|
1068
|
+
`,
|
|
1069
|
+
"push-schema": `clivly push-schema — report your schema without deploying
|
|
1070
|
+
|
|
1071
|
+
Usage:
|
|
1072
|
+
clivly push-schema [--config <path>]
|
|
1073
|
+
|
|
1074
|
+
Sends your discovered tables to Clivly so the dashboard can offer them for
|
|
1075
|
+
mapping. Reads your config; writes nothing locally.
|
|
1076
|
+
`,
|
|
1077
|
+
dev: `clivly dev — tunnel localhost so remote sync can reach you
|
|
1078
|
+
|
|
1079
|
+
Usage:
|
|
1080
|
+
clivly dev [--port <n>] [--api-url <url>] [--route-path <path>] [-- <cmd>]
|
|
1081
|
+
|
|
1082
|
+
Opens a tunnel to your local app and points Clivly's sync trigger at it.
|
|
1083
|
+
Everything after \`--\` runs as your app's dev command.
|
|
1084
|
+
|
|
1085
|
+
Options:
|
|
1086
|
+
--port <n> Local port your app listens on
|
|
1087
|
+
--route-path <path> Path to your mounted tick route
|
|
1088
|
+
`,
|
|
1089
|
+
login: `clivly login — pair this machine with Clivly
|
|
1090
|
+
|
|
1091
|
+
Usage:
|
|
1092
|
+
clivly login [--force] [--rotate] [--api-url <url>] [--no-browser]
|
|
1093
|
+
|
|
1094
|
+
Opens a browser to approve this machine, then writes CLIVLY_SECRET_KEY and
|
|
1095
|
+
CLIVLY_SYNC_TRIGGER_SECRET to .env.
|
|
1096
|
+
|
|
1097
|
+
Options:
|
|
1098
|
+
--force Re-pair even if credentials already exist
|
|
1099
|
+
--rotate Replace the existing credentials
|
|
1100
|
+
--no-browser Print the URL instead of opening a browser
|
|
1101
|
+
`
|
|
1102
|
+
};
|
|
1103
|
+
function isHelpRequest(argv) {
|
|
1104
|
+
for (const arg of argv) {
|
|
1105
|
+
if (arg === "--") return false;
|
|
1106
|
+
if (arg === "--help" || arg === "-h") return true;
|
|
1107
|
+
}
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
function helpFor(command) {
|
|
1111
|
+
if (!command) return ROOT_HELP;
|
|
1112
|
+
return COMMAND_HELP[command] ?? ROOT_HELP;
|
|
1113
|
+
}
|
|
1114
|
+
//#endregion
|
|
855
1115
|
//#region src/install.ts
|
|
856
1116
|
const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject) => {
|
|
857
1117
|
const child = (0, node_child_process.spawn)(command, args, {
|
|
@@ -1091,11 +1351,11 @@ function credentialEntries(credentials) {
|
|
|
1091
1351
|
return [{
|
|
1092
1352
|
key: SECRET_KEY_ENV,
|
|
1093
1353
|
value: credentials.apiKey,
|
|
1094
|
-
comment:
|
|
1354
|
+
comment: `Org secret key — ${CREDENTIALS_LOCATION}`
|
|
1095
1355
|
}, {
|
|
1096
1356
|
key: "CLIVLY_SYNC_TRIGGER_SECRET",
|
|
1097
1357
|
value: credentials.triggerSecret,
|
|
1098
|
-
comment:
|
|
1358
|
+
comment: `Signing secret — ${SYNC_SECRET_LOCATION}`
|
|
1099
1359
|
}];
|
|
1100
1360
|
}
|
|
1101
1361
|
function writeCredentials(options, credentials) {
|
|
@@ -1182,12 +1442,19 @@ function chooseSyncFields(table, todos) {
|
|
|
1182
1442
|
idField
|
|
1183
1443
|
};
|
|
1184
1444
|
}
|
|
1445
|
+
const SPLIT_NAME_COLUMNS = /* @__PURE__ */ new Set([
|
|
1446
|
+
"first_name",
|
|
1447
|
+
"firstname",
|
|
1448
|
+
"given_name"
|
|
1449
|
+
]);
|
|
1185
1450
|
function buildEntity(table, concept, todos) {
|
|
1186
1451
|
const canonical = require_entity_config.CANONICAL_FIELDS[concept];
|
|
1187
1452
|
const suggested = require_internal_entity_heuristics.toFieldMap(require_internal_entity_heuristics.scoreFieldMap(concept, toScoredTable(table)));
|
|
1188
1453
|
const fields = {};
|
|
1189
1454
|
for (const [key, column] of Object.entries(suggested)) if (canonical.includes(key)) fields[key] = column;
|
|
1190
1455
|
for (const required of require_internal_entity_heuristics.requiredFieldsFor(concept)) if (!fields[required]) todos.push(`Could not map required ${concept} field "${required}" on "${table.tableName}" — add it manually.`);
|
|
1456
|
+
const nameColumn = fields.name?.toLowerCase();
|
|
1457
|
+
if (nameColumn && SPLIT_NAME_COLUMNS.has(nameColumn)) todos.push(`Mapped ${concept} "name" to "${fields.name}" on "${table.tableName}" — that is the given name only. Clivly mirrors one column per field, so add a full-name column if you want surnames.`);
|
|
1191
1458
|
const { cursorField, idField } = chooseSyncFields(table, todos);
|
|
1192
1459
|
return {
|
|
1193
1460
|
moduleKey: table.moduleKey,
|
|
@@ -1236,8 +1503,10 @@ function planInit(schema, overrides = {}) {
|
|
|
1236
1503
|
if (built && !contact) todos.push(`"${built.tableName}" was chosen as the contact entity but none of its columns could be mapped to name/email.`);
|
|
1237
1504
|
let company = null;
|
|
1238
1505
|
if (companyTable) {
|
|
1239
|
-
const
|
|
1506
|
+
const candidateTodos = [];
|
|
1507
|
+
const candidate = buildEntity(companyTable, "company", candidateTodos);
|
|
1240
1508
|
if (hasMappedFields(candidate)) {
|
|
1509
|
+
todos.push(...candidateTodos);
|
|
1241
1510
|
company = candidate;
|
|
1242
1511
|
if (require_internal_entity_heuristics.matchesConcept(companyTable.tableName, "contact")) todos.push(`"${companyTable.tableName}" matches both contact and company naming — assigned to company. Swap it to your contact entity if that's wrong.`);
|
|
1243
1512
|
} else todos.push(`"${companyTable.tableName}" looked like a company but no fields could be mapped — left it out. Add it to \`entities\` once you know which columns map to name/domain.`);
|
|
@@ -1258,15 +1527,99 @@ function planInit(schema, overrides = {}) {
|
|
|
1258
1527
|
contact,
|
|
1259
1528
|
company,
|
|
1260
1529
|
tableNames: schema.tables.map((table) => table.tableName),
|
|
1530
|
+
contactCandidates: rankContactCandidates(schema.tables),
|
|
1261
1531
|
todos
|
|
1262
1532
|
};
|
|
1263
1533
|
}
|
|
1534
|
+
const CONTACT_COLUMN_EVIDENCE = [
|
|
1535
|
+
{
|
|
1536
|
+
label: "email",
|
|
1537
|
+
names: ["email", "email_address"],
|
|
1538
|
+
weight: 3
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
label: "phone",
|
|
1542
|
+
names: [
|
|
1543
|
+
"phone",
|
|
1544
|
+
"phone_number",
|
|
1545
|
+
"mobile"
|
|
1546
|
+
],
|
|
1547
|
+
weight: 2
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
label: "first name",
|
|
1551
|
+
names: [
|
|
1552
|
+
"first_name",
|
|
1553
|
+
"firstname",
|
|
1554
|
+
"given_name"
|
|
1555
|
+
],
|
|
1556
|
+
weight: 2
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
label: "last name",
|
|
1560
|
+
names: [
|
|
1561
|
+
"last_name",
|
|
1562
|
+
"lastname",
|
|
1563
|
+
"surname",
|
|
1564
|
+
"family_name"
|
|
1565
|
+
],
|
|
1566
|
+
weight: 2
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
label: "name",
|
|
1570
|
+
names: [
|
|
1571
|
+
"name",
|
|
1572
|
+
"full_name",
|
|
1573
|
+
"display_name"
|
|
1574
|
+
],
|
|
1575
|
+
weight: 1
|
|
1576
|
+
}
|
|
1577
|
+
];
|
|
1578
|
+
const NAME_MATCH_WEIGHT = 4;
|
|
1579
|
+
/**
|
|
1580
|
+
* Rank the schema's tables by how much they look like people, best first.
|
|
1581
|
+
*
|
|
1582
|
+
* Used only on the failure path, where detection found nothing mappable and the
|
|
1583
|
+
* developer has to choose. `init` previously suggested `tableNames[0]` — the
|
|
1584
|
+
* first table in schema order — so a rental app was told to map its `lease`
|
|
1585
|
+
* table as contacts. Wrong guidance is worse than none: it is confident, and
|
|
1586
|
+
* the developer has no way to see it is wrong.
|
|
1587
|
+
*
|
|
1588
|
+
* Deliberately evidence-based rather than name-based alone, so a table whose
|
|
1589
|
+
* name Clivly has never heard of still surfaces on the strength of its columns.
|
|
1590
|
+
*/
|
|
1591
|
+
function rankContactCandidates(tables) {
|
|
1592
|
+
return tables.map((table) => {
|
|
1593
|
+
const columns = new Set(table.columns.flatMap((c) => [c.dbName.toLowerCase(), c.key.toLowerCase()]));
|
|
1594
|
+
const reasons = [];
|
|
1595
|
+
let score = 0;
|
|
1596
|
+
if (require_internal_entity_heuristics.matchesConcept(table.tableName, "contact")) {
|
|
1597
|
+
score += NAME_MATCH_WEIGHT;
|
|
1598
|
+
reasons.push("the name looks like a people table");
|
|
1599
|
+
}
|
|
1600
|
+
for (const evidence of CONTACT_COLUMN_EVIDENCE) {
|
|
1601
|
+
const hit = evidence.names.find((name) => columns.has(name));
|
|
1602
|
+
if (hit) {
|
|
1603
|
+
score += evidence.weight;
|
|
1604
|
+
reasons.push(hit);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
reasons,
|
|
1609
|
+
score,
|
|
1610
|
+
table: table.tableName
|
|
1611
|
+
};
|
|
1612
|
+
}).filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score || a.table.localeCompare(b.table)).map(({ reasons, table }) => ({
|
|
1613
|
+
reasons,
|
|
1614
|
+
table
|
|
1615
|
+
}));
|
|
1616
|
+
}
|
|
1264
1617
|
//#endregion
|
|
1265
1618
|
//#region src/render-config.ts
|
|
1266
1619
|
const CLIVLY_ENV_KEYS = [
|
|
1267
1620
|
{
|
|
1268
1621
|
key: "CLIVLY_SECRET_KEY",
|
|
1269
|
-
comment:
|
|
1622
|
+
comment: `Org secret key — ${CREDENTIALS_LOCATION}`
|
|
1270
1623
|
},
|
|
1271
1624
|
{
|
|
1272
1625
|
key: "CLIVLY_SYNC_TRIGGER_URL",
|
|
@@ -1274,7 +1627,7 @@ const CLIVLY_ENV_KEYS = [
|
|
|
1274
1627
|
},
|
|
1275
1628
|
{
|
|
1276
1629
|
key: "CLIVLY_SYNC_TRIGGER_SECRET",
|
|
1277
|
-
comment:
|
|
1630
|
+
comment: `Signing secret — ${SYNC_SECRET_LOCATION}`
|
|
1278
1631
|
}
|
|
1279
1632
|
];
|
|
1280
1633
|
function renderFields(fields) {
|
|
@@ -1303,6 +1656,28 @@ function renderDbExpr(plan) {
|
|
|
1303
1656
|
if (!plan.db) return "db";
|
|
1304
1657
|
return dbIsInSchemaModule(plan) ? `schema.${plan.db.moduleKey}` : plan.db.moduleKey;
|
|
1305
1658
|
}
|
|
1659
|
+
const TRAILING_SCHEMA_SEGMENT = /\/schema(\/index)?$/;
|
|
1660
|
+
/**
|
|
1661
|
+
* Where the `db` instance most likely lives, given where the schema was found.
|
|
1662
|
+
*
|
|
1663
|
+
* Only reached when discovery could not confirm one. Discovery *evaluates* a
|
|
1664
|
+
* candidate module and duck-types its exports for `.select`, which is the right
|
|
1665
|
+
* check but fails for the most common real-world db module: `drizzle(process.
|
|
1666
|
+
* env.DATABASE_URL!)` throws at import time when the env var is absent, and it
|
|
1667
|
+
* is absent precisely when a developer is running `clivly init` for the first
|
|
1668
|
+
* time. So the probe misses, through no fault of the project.
|
|
1669
|
+
*
|
|
1670
|
+
* The old fallback emitted `<YOUR_DB_MODULE>` — not a module specifier, not
|
|
1671
|
+
* valid TypeScript, and invisible until the config failed to load at runtime.
|
|
1672
|
+
* A best-guess relative path is strictly better: right for the conventional
|
|
1673
|
+
* layout (`lib/db/schema.ts` beside `lib/db/index.ts`), and when it is wrong it
|
|
1674
|
+
* is a module-not-found error at build time, naming a real path the developer
|
|
1675
|
+
* can correct.
|
|
1676
|
+
*/
|
|
1677
|
+
function guessDbImportPath(schemaImportPath) {
|
|
1678
|
+
const trimmed = schemaImportPath.replace(TRAILING_SCHEMA_SEGMENT, "");
|
|
1679
|
+
return trimmed === schemaImportPath || trimmed === "." || trimmed === "" ? "./db" : trimmed;
|
|
1680
|
+
}
|
|
1306
1681
|
function renderImports(plan) {
|
|
1307
1682
|
const lines = [
|
|
1308
1683
|
"import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
|
|
@@ -1310,8 +1685,10 @@ function renderImports(plan) {
|
|
|
1310
1685
|
"import { discoverFromDrizzle } from \"clivly/drizzle\";",
|
|
1311
1686
|
`import * as schema from "${plan.importPath}";`
|
|
1312
1687
|
];
|
|
1313
|
-
if (
|
|
1314
|
-
|
|
1688
|
+
if (plan.db === null) {
|
|
1689
|
+
const guess = guessDbImportPath(plan.importPath);
|
|
1690
|
+
lines.push(`import { db } from "${guess}"; // TODO(clivly): verify this path points at your Drizzle \`db\` instance — NOT your schema file`);
|
|
1691
|
+
} else if (!dbIsInSchemaModule(plan)) lines.push(`import { ${plan.db.moduleKey} } from "${plan.db.importPath}";`);
|
|
1315
1692
|
return lines.join("\n");
|
|
1316
1693
|
}
|
|
1317
1694
|
function renderTodoHeader(todos) {
|
|
@@ -1429,6 +1806,83 @@ function renderAuthWiring(auth, authInstance = null) {
|
|
|
1429
1806
|
}
|
|
1430
1807
|
}
|
|
1431
1808
|
//#endregion
|
|
1809
|
+
//#region src/tsconfig-alias.ts
|
|
1810
|
+
const LEADING_DOT_SLASH$1 = /^\.\//;
|
|
1811
|
+
const TRAILING_WILDCARD = /\/?\*$/;
|
|
1812
|
+
const TRAILING_SLASH = /\/$/;
|
|
1813
|
+
const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
|
|
1814
|
+
const LINE_COMMENT = /(^|[^:])\/\/.*$/gm;
|
|
1815
|
+
const TRAILING_COMMA = /,(\s*[}\]])/g;
|
|
1816
|
+
/**
|
|
1817
|
+
* `compilerOptions` from a tsconfig's text, or null when it cannot be read.
|
|
1818
|
+
*
|
|
1819
|
+
* tsconfig is JSONC — comments and trailing commas are legal and common, and
|
|
1820
|
+
* `JSON.parse` rejects both. Stripping them is enough here: this only ever reads
|
|
1821
|
+
* two string-valued fields, so the cost of being wrong is falling back to a
|
|
1822
|
+
* relative import, which already works.
|
|
1823
|
+
*/
|
|
1824
|
+
function parseTsconfig(source) {
|
|
1825
|
+
try {
|
|
1826
|
+
const stripped = source.replace(BLOCK_COMMENT, "").replace(LINE_COMMENT, "$1").replace(TRAILING_COMMA, "$1");
|
|
1827
|
+
return JSON.parse(stripped).compilerOptions ?? null;
|
|
1828
|
+
} catch {
|
|
1829
|
+
return null;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Strip a leading "./" and any trailing wildcard so two paths can be compared.
|
|
1834
|
+
*
|
|
1835
|
+
* `"./*"` — an alias rooted at the app itself — must reduce to the empty string,
|
|
1836
|
+
* not to a stray `"*"`; that is the mapping which makes a root-level
|
|
1837
|
+
* clivly.config alias-reachable.
|
|
1838
|
+
*/
|
|
1839
|
+
function normalize(value) {
|
|
1840
|
+
return value.replace(LEADING_DOT_SLASH$1, "").replace(TRAILING_WILDCARD, "").replace(TRAILING_SLASH, "");
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* The alias specifier that reaches `target`, or null when none does.
|
|
1844
|
+
*
|
|
1845
|
+
* `target` is app-root-relative and extensionless, e.g. `lib/auth` or
|
|
1846
|
+
* `clivly.config`. Only wildcard patterns are considered — an exact mapping
|
|
1847
|
+
* names one module and is not a prefix anything else can be rebased onto.
|
|
1848
|
+
*
|
|
1849
|
+
* Longest prefix wins, so `"@components/*": ["./src/components/*"]` beats
|
|
1850
|
+
* `"@/*": ["./src/*"]` for a module under `src/components`, matching how
|
|
1851
|
+
* TypeScript itself resolves.
|
|
1852
|
+
*/
|
|
1853
|
+
function aliasFor(input) {
|
|
1854
|
+
if (!input.paths) return null;
|
|
1855
|
+
const target = normalize(input.target);
|
|
1856
|
+
let best = null;
|
|
1857
|
+
for (const [pattern, targets] of Object.entries(input.paths)) {
|
|
1858
|
+
if (!pattern.endsWith("/*")) continue;
|
|
1859
|
+
const prefix = pattern.slice(0, -2);
|
|
1860
|
+
for (const mapped of targets) {
|
|
1861
|
+
const base = baseReaching(mapped, target);
|
|
1862
|
+
if (base === null) continue;
|
|
1863
|
+
const rest = base === "" ? target : target.slice(base.length + 1);
|
|
1864
|
+
if (!best || base.length > best.length) best = {
|
|
1865
|
+
alias: `${prefix}/${rest}`,
|
|
1866
|
+
length: base.length
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return best?.alias ?? null;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* The normalized base of `mapped` when it contains `target`, else null.
|
|
1874
|
+
*
|
|
1875
|
+
* A prefix must end at a segment boundary — `src` must not match `srcinternal`,
|
|
1876
|
+
* a different directory — and the target must be strictly inside the base, since
|
|
1877
|
+
* a wildcard needs a remainder to substitute.
|
|
1878
|
+
*/
|
|
1879
|
+
function baseReaching(mapped, target) {
|
|
1880
|
+
if (!mapped.endsWith("/*")) return null;
|
|
1881
|
+
const base = normalize(mapped);
|
|
1882
|
+
if (base === "") return target === "" ? null : "";
|
|
1883
|
+
return target.startsWith(`${base}/`) ? base : null;
|
|
1884
|
+
}
|
|
1885
|
+
//#endregion
|
|
1432
1886
|
//#region src/scaffold.ts
|
|
1433
1887
|
const TANSTACK_ROUTE_PREFIX = /^src\/routes\//;
|
|
1434
1888
|
const TS_EXTENSION = /\.ts$/;
|
|
@@ -1460,10 +1914,14 @@ const ROUTE_PATHS = {
|
|
|
1460
1914
|
},
|
|
1461
1915
|
unknown: null
|
|
1462
1916
|
};
|
|
1463
|
-
function
|
|
1917
|
+
function applyLayout(framework, path, layout) {
|
|
1918
|
+
if (framework !== "nextjs" || layout === "app") return path;
|
|
1919
|
+
return `src/${path}`;
|
|
1920
|
+
}
|
|
1921
|
+
function routePath(framework, kind, layout = "app") {
|
|
1464
1922
|
const paths = ROUTE_PATHS[framework];
|
|
1465
1923
|
if (!paths) throw new Error(`No route path for framework: ${framework}`);
|
|
1466
|
-
return paths[kind];
|
|
1924
|
+
return applyLayout(framework, paths[kind], layout);
|
|
1467
1925
|
}
|
|
1468
1926
|
const WINDOWS_SEP = /\\/g;
|
|
1469
1927
|
const LEADING_DOT_SLASH = /^\.\//;
|
|
@@ -1471,22 +1929,27 @@ function configImport(routeFilePath) {
|
|
|
1471
1929
|
const rel = (0, node_path.relative)((0, node_path.dirname)(routeFilePath), "clivly.config").replace(WINDOWS_SEP, "/");
|
|
1472
1930
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1473
1931
|
}
|
|
1474
|
-
function hostImportPath(routeFilePath, appRelative) {
|
|
1932
|
+
function hostImportPath(routeFilePath, appRelative, paths) {
|
|
1475
1933
|
const fromRoot = appRelative.replace(LEADING_DOT_SLASH, "");
|
|
1934
|
+
const alias = aliasFor({
|
|
1935
|
+
paths,
|
|
1936
|
+
target: fromRoot
|
|
1937
|
+
});
|
|
1938
|
+
if (alias) return alias;
|
|
1476
1939
|
const rel = (0, node_path.relative)((0, node_path.dirname)(routeFilePath), fromRoot).replace(WINDOWS_SEP, "/");
|
|
1477
1940
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1478
1941
|
}
|
|
1479
|
-
function renderAuthPreamble(routeFilePath, auth) {
|
|
1942
|
+
function renderAuthPreamble(routeFilePath, auth, paths) {
|
|
1480
1943
|
const lines = [...auth.imports];
|
|
1481
1944
|
if (auth.hostImport) {
|
|
1482
1945
|
if (auth.hostImport.todo) lines.push(`// ${auth.hostImport.todo}`);
|
|
1483
|
-
const path = hostImportPath(routeFilePath, auth.hostImport.path);
|
|
1946
|
+
const path = hostImportPath(routeFilePath, auth.hostImport.path, paths);
|
|
1484
1947
|
lines.push(`import { ${auth.hostImport.binding} } from "${path}";`);
|
|
1485
1948
|
}
|
|
1486
1949
|
lines.push("", auth.body);
|
|
1487
1950
|
return lines.join("\n");
|
|
1488
1951
|
}
|
|
1489
|
-
function renderPreamble(kind, importPath, auth, routeFilePath) {
|
|
1952
|
+
function renderPreamble(kind, importPath, auth, paths, routeFilePath) {
|
|
1490
1953
|
if (kind === "session") return `import { createChatSessionHandler } from "clivly/sdk";
|
|
1491
1954
|
|
|
1492
1955
|
const handler = createChatSessionHandler({
|
|
@@ -1497,7 +1960,7 @@ const handler = createChatSessionHandler({
|
|
|
1497
1960
|
});
|
|
1498
1961
|
`;
|
|
1499
1962
|
if (kind === "verify") return `import clivly from "${importPath}";
|
|
1500
|
-
${renderAuthPreamble(routeFilePath, auth)}
|
|
1963
|
+
${renderAuthPreamble(routeFilePath, auth, paths)}
|
|
1501
1964
|
const handler = clivly.createAuthVerifyHandler(resolveUser);
|
|
1502
1965
|
`;
|
|
1503
1966
|
return `import clivly from "${importPath}";
|
|
@@ -1505,8 +1968,8 @@ const handler = clivly.createAuthVerifyHandler(resolveUser);
|
|
|
1505
1968
|
const handler = clivly.createClivlyHandler();
|
|
1506
1969
|
`;
|
|
1507
1970
|
}
|
|
1508
|
-
function renderBody(framework, importPath, kind, auth, routeFilePath) {
|
|
1509
|
-
const preamble = renderPreamble(kind, importPath, auth, routeFilePath);
|
|
1971
|
+
function renderBody(framework, importPath, kind, auth, routeFilePath, paths) {
|
|
1972
|
+
const preamble = renderPreamble(kind, importPath, auth, paths, routeFilePath);
|
|
1510
1973
|
switch (framework) {
|
|
1511
1974
|
case "nextjs": return `${preamble}
|
|
1512
1975
|
export const POST = (request: Request) => handler(request);
|
|
@@ -1536,9 +1999,9 @@ export default defineEventHandler((event) => handler(toWebRequest(event)));
|
|
|
1536
1999
|
default: throw new Error(`No route template for framework: ${framework}`);
|
|
1537
2000
|
}
|
|
1538
2001
|
}
|
|
1539
|
-
function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown")) {
|
|
1540
|
-
const path = routePath(framework, kind);
|
|
1541
|
-
let body = renderBody(framework, configImport(path), kind, auth, path);
|
|
2002
|
+
function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown"), layout = "app", paths) {
|
|
2003
|
+
const path = routePath(framework, kind, layout);
|
|
2004
|
+
let body = renderBody(framework, configImport(path), kind, auth, path, paths);
|
|
1542
2005
|
if (framework === "tanstack-start") {
|
|
1543
2006
|
const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
|
|
1544
2007
|
body = body.replace("__ROUTE__", url);
|
|
@@ -1550,28 +2013,28 @@ const COMMON_FILES = [{
|
|
|
1550
2013
|
description: "Your Clivly configuration",
|
|
1551
2014
|
owned: false
|
|
1552
2015
|
}];
|
|
1553
|
-
function planScaffold(framework) {
|
|
2016
|
+
function planScaffold(framework, layout = "app") {
|
|
1554
2017
|
const paths = ROUTE_PATHS[framework];
|
|
1555
2018
|
if (!paths) return [];
|
|
1556
2019
|
return [
|
|
1557
2020
|
{
|
|
1558
|
-
path: paths.tick,
|
|
2021
|
+
path: applyLayout(framework, paths.tick, layout),
|
|
1559
2022
|
description: "Clivly sync-trigger route (createClivlyHandler)",
|
|
1560
2023
|
owned: true
|
|
1561
2024
|
},
|
|
1562
2025
|
{
|
|
1563
|
-
path: paths.verify,
|
|
2026
|
+
path: applyLayout(framework, paths.verify, layout),
|
|
1564
2027
|
description: "Clivly auth-verify route (createAuthVerifyHandler) — finish resolveUser",
|
|
1565
2028
|
owned: true
|
|
1566
2029
|
},
|
|
1567
2030
|
...COMMON_FILES
|
|
1568
2031
|
];
|
|
1569
2032
|
}
|
|
1570
|
-
function planWidgetScaffold(framework) {
|
|
2033
|
+
function planWidgetScaffold(framework, layout = "app") {
|
|
1571
2034
|
const paths = ROUTE_PATHS[framework];
|
|
1572
2035
|
if (!paths) return [];
|
|
1573
2036
|
return [{
|
|
1574
|
-
path: paths.session,
|
|
2037
|
+
path: applyLayout(framework, paths.session, layout),
|
|
1575
2038
|
description: "Clivly chat-session route (createChatSessionHandler) — add allowedOrigins",
|
|
1576
2039
|
owned: true
|
|
1577
2040
|
}];
|
|
@@ -1591,9 +2054,9 @@ function planFiles(args) {
|
|
|
1591
2054
|
const render = (path) => {
|
|
1592
2055
|
if (path === "clivly.config.ts") return renderInitConfig(args.plan);
|
|
1593
2056
|
const kind = path.includes("verify") ? "verify" : "tick";
|
|
1594
|
-
return renderRouteFile(args.framework, kind, args.authWiring);
|
|
2057
|
+
return renderRouteFile(args.framework, kind, args.authWiring, args.layout, parseTsconfig(args.readFile((0, node_path.join)(args.cwd, "tsconfig.json")) ?? "")?.paths);
|
|
1595
2058
|
};
|
|
1596
|
-
for (const file of planScaffold(args.framework)) {
|
|
2059
|
+
for (const file of planScaffold(args.framework, args.layout)) {
|
|
1597
2060
|
const contents = render(file.path);
|
|
1598
2061
|
mutations.push({
|
|
1599
2062
|
path: file.path,
|
|
@@ -1624,8 +2087,8 @@ function planMutations(args) {
|
|
|
1624
2087
|
};
|
|
1625
2088
|
}
|
|
1626
2089
|
function planWidgetMutations(args) {
|
|
1627
|
-
return planWidgetScaffold(args.framework).map((file) => {
|
|
1628
|
-
const contents = renderRouteFile(args.framework, "session");
|
|
2090
|
+
return planWidgetScaffold(args.framework, args.layout).map((file) => {
|
|
2091
|
+
const contents = renderRouteFile(args.framework, "session", void 0, args.layout);
|
|
1629
2092
|
return {
|
|
1630
2093
|
path: file.path,
|
|
1631
2094
|
contents,
|
|
@@ -1950,7 +2413,17 @@ const ALLOWED_CALLS = /* @__PURE__ */ new Set([
|
|
|
1950
2413
|
"while"
|
|
1951
2414
|
]);
|
|
1952
2415
|
const SIDE_EFFECT_CALLS = /^(dotenv\.config|config|configDotenv|fileURLToPath|readFileSync|require|process\.chdir)$/;
|
|
2416
|
+
const NATIVE_PACKAGES = /* @__PURE__ */ new Set([
|
|
2417
|
+
"bcrypt",
|
|
2418
|
+
"better-sqlite3",
|
|
2419
|
+
"canvas",
|
|
2420
|
+
"node-gyp",
|
|
2421
|
+
"pg-native",
|
|
2422
|
+
"sharp",
|
|
2423
|
+
"sqlite3"
|
|
2424
|
+
]);
|
|
1953
2425
|
const ENV_HINT = "Move it to clivly.cli.ts (`envFiles`) or pass --env-file — the CLI reads those in Node, so your config stays edge-safe.";
|
|
2426
|
+
const NATIVE_HINT = "A compiled binary cannot load on an edge runtime — this is the \"Could not locate the bindings file\" / ERR_DLOPEN_FAILED failure. Import your db lazily inside a function, or use a driver that speaks HTTP (e.g. @neondatabase/serverless) in the config.";
|
|
1954
2427
|
function isNodeBuiltin(specifier) {
|
|
1955
2428
|
if (specifier.startsWith("node:")) return true;
|
|
1956
2429
|
return NODE_BUILTINS.has(specifier.split("/")[0] ?? "");
|
|
@@ -1967,6 +2440,12 @@ function specifierIssue(specifier, line) {
|
|
|
1967
2440
|
hint: ENV_HINT,
|
|
1968
2441
|
line
|
|
1969
2442
|
};
|
|
2443
|
+
if (NATIVE_PACKAGES.has(specifier.split("/")[0] ?? "")) return {
|
|
2444
|
+
code: "native_module_in_edge_config",
|
|
2445
|
+
detail: `imports "${specifier}", which ships a compiled binary`,
|
|
2446
|
+
hint: NATIVE_HINT,
|
|
2447
|
+
line
|
|
2448
|
+
};
|
|
1970
2449
|
if (isNodeBuiltin(specifier)) return {
|
|
1971
2450
|
code: "node_builtin_in_edge_config",
|
|
1972
2451
|
detail: `imports "${specifier}", which does not exist on an edge runtime`,
|
|
@@ -2006,8 +2485,56 @@ function analyzeEdgeSafety(source) {
|
|
|
2006
2485
|
});
|
|
2007
2486
|
return issues;
|
|
2008
2487
|
}
|
|
2488
|
+
/** Every relative specifier `source` imports, in first-seen order, deduped. */
|
|
2489
|
+
function relativeImports(source) {
|
|
2490
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2491
|
+
for (const rawLine of source.split(NEWLINE)) {
|
|
2492
|
+
const line = stripComment(rawLine);
|
|
2493
|
+
const specifier = BARE_IMPORT.exec(line)?.[1] ?? IMPORT_FROM.exec(line)?.[1] ?? REQUIRE_CALL.exec(line)?.[1];
|
|
2494
|
+
if (specifier?.startsWith(".")) seen.add(specifier);
|
|
2495
|
+
}
|
|
2496
|
+
return [...seen];
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Edge-safety for the config *and* the modules it imports directly.
|
|
2500
|
+
*
|
|
2501
|
+
* The dominant real-world shape is a clean-looking config with
|
|
2502
|
+
* `import { db } from "./lib/db"`, where the problem is one hop away and
|
|
2503
|
+
* invisible to a scan that reads only the config file.
|
|
2504
|
+
*
|
|
2505
|
+
* One hop, not more: each additional level multiplies the files read for a
|
|
2506
|
+
* shrinking return, and the config's own imports are where a host app puts its
|
|
2507
|
+
* database. Bare specifiers are never followed — resolving into node_modules is
|
|
2508
|
+
* a different job with different failure modes, and the denylist already covers
|
|
2509
|
+
* the packages that matter.
|
|
2510
|
+
*
|
|
2511
|
+
* `readModule` returns source text, or null when the module cannot be read;
|
|
2512
|
+
* unreadable is not a finding, since a scan is not a build.
|
|
2513
|
+
*/
|
|
2514
|
+
function analyzeConfigTree(input) {
|
|
2515
|
+
const issues = analyzeEdgeSafety(input.source);
|
|
2516
|
+
for (const specifier of relativeImports(input.source)) {
|
|
2517
|
+
const source = input.readModule(specifier);
|
|
2518
|
+
if (source === null) continue;
|
|
2519
|
+
for (const issue of analyzeEdgeSafety(source)) issues.push({
|
|
2520
|
+
...issue,
|
|
2521
|
+
module: specifier
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
return issues;
|
|
2525
|
+
}
|
|
2009
2526
|
//#endregion
|
|
2010
2527
|
//#region src/status-stages.ts
|
|
2528
|
+
const MODULE_CANDIDATES = [
|
|
2529
|
+
".ts",
|
|
2530
|
+
".tsx",
|
|
2531
|
+
".js",
|
|
2532
|
+
".mjs",
|
|
2533
|
+
".cjs",
|
|
2534
|
+
"/index.ts",
|
|
2535
|
+
"/index.js",
|
|
2536
|
+
""
|
|
2537
|
+
];
|
|
2011
2538
|
function defaultReadConfigSource(path) {
|
|
2012
2539
|
try {
|
|
2013
2540
|
return (0, node_fs.readFileSync)(path, "utf8");
|
|
@@ -2022,10 +2549,11 @@ const REASON_HINT = {
|
|
|
2022
2549
|
rate_limited: "Rate limited (429). Wait a moment and retry.",
|
|
2023
2550
|
server_error: "Clivly returned a server error (5xx). Retry shortly.",
|
|
2024
2551
|
client_error: "The request was rejected (4xx). Check your configuration.",
|
|
2552
|
+
server_too_old: "This Clivly server is older than your CLI and has no /clivly/sdk/ping. Upgrade the server, or pin the CLI to match it.",
|
|
2025
2553
|
network_error: "Could not reach Clivly. Check your network or --api-url."
|
|
2026
2554
|
};
|
|
2027
2555
|
const apiKeyStage = {
|
|
2028
|
-
id: "
|
|
2556
|
+
id: "credential",
|
|
2029
2557
|
label: "API key present",
|
|
2030
2558
|
run(ctx) {
|
|
2031
2559
|
if (ctx.apiKey) return Promise.resolve({
|
|
@@ -2035,20 +2563,29 @@ const apiKeyStage = {
|
|
|
2035
2563
|
return Promise.resolve({
|
|
2036
2564
|
state: "fail",
|
|
2037
2565
|
detail: "Secret key is not set.",
|
|
2038
|
-
hint:
|
|
2566
|
+
hint: `Copy your secret key from ${CREDENTIALS_LOCATION} into CLIVLY_SECRET_KEY in .env.`
|
|
2039
2567
|
});
|
|
2040
2568
|
}
|
|
2041
2569
|
};
|
|
2042
|
-
const
|
|
2043
|
-
id: "
|
|
2570
|
+
const reachabilityStage = {
|
|
2571
|
+
id: "application",
|
|
2044
2572
|
label: "Clivly reachable",
|
|
2045
2573
|
needs: [apiKeyStage.id],
|
|
2046
2574
|
async run(ctx) {
|
|
2047
2575
|
const result = await ctx.connect(ctx.apiKey, ctx.apiUrl);
|
|
2048
|
-
if (result.ok)
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2576
|
+
if (result.ok) {
|
|
2577
|
+
const name = result.org?.name ?? "(unnamed org)";
|
|
2578
|
+
const id = result.org?.id ? ` (${result.org.id})` : "";
|
|
2579
|
+
if (result.appLastSeenAt === null) return {
|
|
2580
|
+
state: "warn",
|
|
2581
|
+
detail: `Reached org: ${name}${id} — your app has not connected yet`,
|
|
2582
|
+
hint: "Start your app with the Clivly SDK mounted; it will appear in the dashboard within a minute."
|
|
2583
|
+
};
|
|
2584
|
+
return {
|
|
2585
|
+
state: "pass",
|
|
2586
|
+
detail: `Reached org: ${name}${id}`
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2052
2589
|
const status = result.status === null ? "no response" : `HTTP ${result.status}`;
|
|
2053
2590
|
return {
|
|
2054
2591
|
state: "fail",
|
|
@@ -2076,22 +2613,76 @@ const configStage = {
|
|
|
2076
2613
|
}
|
|
2077
2614
|
}
|
|
2078
2615
|
};
|
|
2079
|
-
const LADDER_OWNED_CHECKS = /* @__PURE__ */ new Set(["API key present", "
|
|
2616
|
+
const LADDER_OWNED_CHECKS = /* @__PURE__ */ new Set(["API key present", "Clivly reachable"]);
|
|
2617
|
+
const sdkChecksStage = {
|
|
2618
|
+
id: "setup-checks",
|
|
2619
|
+
label: "Setup checks",
|
|
2620
|
+
needs: [configStage.id],
|
|
2621
|
+
async run(ctx) {
|
|
2622
|
+
const owned = (await ctx.sdk.doctor()).checks.filter((check) => !LADDER_OWNED_CHECKS.has(check.name));
|
|
2623
|
+
const counted = owned.filter((check) => !check.skipped);
|
|
2624
|
+
const passed = counted.filter((check) => check.ok).length;
|
|
2625
|
+
const ok = counted.every((check) => check.ok);
|
|
2626
|
+
const warned = counted.filter((check) => check.warn).length;
|
|
2627
|
+
const summary = warned > 0 ? `${passed} of ${counted.length} passed, ${warned} with warnings` : `${passed} of ${counted.length} passed`;
|
|
2628
|
+
return {
|
|
2629
|
+
state: ok ? "pass" : "fail",
|
|
2630
|
+
detail: summary,
|
|
2631
|
+
sub: owned.map((check) => ({
|
|
2632
|
+
label: check.name,
|
|
2633
|
+
ok: check.ok,
|
|
2634
|
+
skipped: check.skipped,
|
|
2635
|
+
warn: check.warn,
|
|
2636
|
+
detail: check.detail
|
|
2637
|
+
}))
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
};
|
|
2641
|
+
const triggerUrlStage = {
|
|
2642
|
+
id: "trigger-url",
|
|
2643
|
+
label: "Sync trigger URL",
|
|
2644
|
+
needs: [configStage.id],
|
|
2645
|
+
run(ctx) {
|
|
2646
|
+
const url = ctx.sdk?.syncTrigger?.url;
|
|
2647
|
+
if (url) return Promise.resolve({
|
|
2648
|
+
state: "pass",
|
|
2649
|
+
detail: url
|
|
2650
|
+
});
|
|
2651
|
+
return Promise.resolve({
|
|
2652
|
+
state: "warn",
|
|
2653
|
+
detail: "Not set — remote sync cannot call back into this app.",
|
|
2654
|
+
hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. For local development run `clivly dev` — it opens a tunnel and registers the URL for you. Push-only setups can ignore this."
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2080
2658
|
/** The full ladder, in the order a developer completes setup. */
|
|
2081
2659
|
const STATUS_STAGES = [
|
|
2082
2660
|
apiKeyStage,
|
|
2083
|
-
|
|
2661
|
+
reachabilityStage,
|
|
2084
2662
|
configStage,
|
|
2085
2663
|
{
|
|
2086
2664
|
id: "runtime",
|
|
2087
2665
|
label: "clivly.config is edge-safe",
|
|
2666
|
+
needs: [configStage.id],
|
|
2088
2667
|
run(ctx) {
|
|
2089
2668
|
const source = (ctx.readConfigSource ?? defaultReadConfigSource)(ctx.configPath);
|
|
2090
2669
|
if (source === null) return Promise.resolve({
|
|
2091
2670
|
state: "skip",
|
|
2092
2671
|
detail: "config not readable"
|
|
2093
2672
|
});
|
|
2094
|
-
const
|
|
2673
|
+
const read = ctx.readConfigSource ?? defaultReadConfigSource;
|
|
2674
|
+
const base = (0, node_path.dirname)(ctx.configPath);
|
|
2675
|
+
const issues = analyzeConfigTree({
|
|
2676
|
+
source,
|
|
2677
|
+
readModule: (specifier) => {
|
|
2678
|
+
const target = (0, node_path.resolve)(base, specifier);
|
|
2679
|
+
for (const candidate of MODULE_CANDIDATES) {
|
|
2680
|
+
const found = read(`${target}${candidate}`);
|
|
2681
|
+
if (found !== null) return found;
|
|
2682
|
+
}
|
|
2683
|
+
return null;
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
2095
2686
|
if (issues.length === 0) return Promise.resolve({
|
|
2096
2687
|
state: "pass",
|
|
2097
2688
|
detail: "no Node-only code at module scope"
|
|
@@ -2108,43 +2699,8 @@ const STATUS_STAGES = [
|
|
|
2108
2699
|
});
|
|
2109
2700
|
}
|
|
2110
2701
|
},
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
label: "Setup checks",
|
|
2114
|
-
needs: [configStage.id],
|
|
2115
|
-
async run(ctx) {
|
|
2116
|
-
const owned = (await ctx.sdk.doctor()).checks.filter((check) => !LADDER_OWNED_CHECKS.has(check.name));
|
|
2117
|
-
const counted = owned.filter((check) => !check.skipped);
|
|
2118
|
-
const passed = counted.filter((check) => check.ok).length;
|
|
2119
|
-
return {
|
|
2120
|
-
state: counted.every((check) => check.ok) ? "pass" : "fail",
|
|
2121
|
-
detail: `${passed} of ${counted.length} passed`,
|
|
2122
|
-
sub: owned.map((check) => ({
|
|
2123
|
-
label: check.name,
|
|
2124
|
-
ok: check.ok,
|
|
2125
|
-
skipped: check.skipped,
|
|
2126
|
-
detail: check.detail
|
|
2127
|
-
}))
|
|
2128
|
-
};
|
|
2129
|
-
}
|
|
2130
|
-
},
|
|
2131
|
-
{
|
|
2132
|
-
id: "trigger-url",
|
|
2133
|
-
label: "Sync trigger URL",
|
|
2134
|
-
needs: [configStage.id],
|
|
2135
|
-
run(ctx) {
|
|
2136
|
-
const url = ctx.sdk?.syncTrigger?.url;
|
|
2137
|
-
if (url) return Promise.resolve({
|
|
2138
|
-
state: "pass",
|
|
2139
|
-
detail: url
|
|
2140
|
-
});
|
|
2141
|
-
return Promise.resolve({
|
|
2142
|
-
state: "warn",
|
|
2143
|
-
detail: "Not set — remote sync cannot call back into this app.",
|
|
2144
|
-
hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. For local development run `clivly dev` — it opens a tunnel and registers the URL for you. Push-only setups can ignore this."
|
|
2145
|
-
});
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2702
|
+
sdkChecksStage,
|
|
2703
|
+
triggerUrlStage
|
|
2148
2704
|
];
|
|
2149
2705
|
function defaultLoadSdk(path) {
|
|
2150
2706
|
return loadConfigSdk(path, "doctor");
|
|
@@ -2154,15 +2710,15 @@ function defaultLoadSdk(path) {
|
|
|
2154
2710
|
/** Stage ids each legacy command maps onto. */
|
|
2155
2711
|
const ALIAS_STAGE_IDS = {
|
|
2156
2712
|
status: STATUS_STAGES.map((stage) => stage.id),
|
|
2157
|
-
ping: ["
|
|
2713
|
+
ping: ["credential", "application"],
|
|
2158
2714
|
doctor: [
|
|
2159
2715
|
"config",
|
|
2160
2716
|
"runtime",
|
|
2161
2717
|
"setup-checks"
|
|
2162
2718
|
],
|
|
2163
2719
|
connect: [
|
|
2164
|
-
"
|
|
2165
|
-
"
|
|
2720
|
+
"credential",
|
|
2721
|
+
"application",
|
|
2166
2722
|
"config",
|
|
2167
2723
|
"trigger-url"
|
|
2168
2724
|
]
|
|
@@ -2178,9 +2734,9 @@ const defaultConnect = (apiKey, apiUrl) => require_sdk.createClivlySDK({
|
|
|
2178
2734
|
apiKey,
|
|
2179
2735
|
apiUrl,
|
|
2180
2736
|
retry: { maxAttempts: 1 }
|
|
2181
|
-
}).
|
|
2737
|
+
}).ping();
|
|
2182
2738
|
function subMark(sub) {
|
|
2183
|
-
if (sub.skipped) return "⚠️ ";
|
|
2739
|
+
if (sub.skipped || sub.warn) return "⚠️ ";
|
|
2184
2740
|
return sub.ok ? "✅" : "❌";
|
|
2185
2741
|
}
|
|
2186
2742
|
function renderStage(result, verbose, out) {
|
|
@@ -2188,7 +2744,7 @@ function renderStage(result, verbose, out) {
|
|
|
2188
2744
|
const detail = result.detail ? ` — ${result.detail}` : "";
|
|
2189
2745
|
out(` ${MARK[result.state]} ${step} · ${result.label}${detail}\n`);
|
|
2190
2746
|
for (const sub of result.sub ?? []) {
|
|
2191
|
-
if (!(verbose || sub.skipped || !sub.ok)) continue;
|
|
2747
|
+
if (!(verbose || sub.skipped || sub.warn || !sub.ok)) continue;
|
|
2192
2748
|
const subDetail = sub.detail ? ` — ${sub.detail}` : "";
|
|
2193
2749
|
out(` ${subMark(sub)} ${sub.label}${subDetail}\n`);
|
|
2194
2750
|
}
|
|
@@ -2214,6 +2770,12 @@ async function runStatus(options) {
|
|
|
2214
2770
|
}
|
|
2215
2771
|
//#endregion
|
|
2216
2772
|
//#region src/index.ts
|
|
2773
|
+
const ENV_FILE_CANDIDATES = [
|
|
2774
|
+
".env",
|
|
2775
|
+
".env.local",
|
|
2776
|
+
".dev.vars"
|
|
2777
|
+
];
|
|
2778
|
+
const CONFIG_WATCH_DEBOUNCE_MS = 150;
|
|
2217
2779
|
function flagValue(argv, flag) {
|
|
2218
2780
|
const index = argv.indexOf(flag);
|
|
2219
2781
|
return index >= 0 ? argv[index + 1] : void 0;
|
|
@@ -2232,7 +2794,10 @@ const KNOWN_COMMANDS = [
|
|
|
2232
2794
|
"push-schema",
|
|
2233
2795
|
"dev",
|
|
2234
2796
|
"login",
|
|
2235
|
-
"help"
|
|
2797
|
+
"help",
|
|
2798
|
+
"ping",
|
|
2799
|
+
"doctor",
|
|
2800
|
+
"connect"
|
|
2236
2801
|
];
|
|
2237
2802
|
function editDistance(a, b) {
|
|
2238
2803
|
const m = a.length;
|
|
@@ -2275,6 +2840,7 @@ function readDeps(cwd) {
|
|
|
2275
2840
|
}
|
|
2276
2841
|
const ADD_USAGE = "Usage: clivly add widget [--force] [--dry-run]";
|
|
2277
2842
|
const defaultAddDeps = {
|
|
2843
|
+
fileExists: node_fs.existsSync,
|
|
2278
2844
|
out: (line) => process.stdout.write(line),
|
|
2279
2845
|
readFile: (path) => (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : null,
|
|
2280
2846
|
writeFile: (path, contents) => {
|
|
@@ -2294,14 +2860,21 @@ function runAdd(argv, cwd, deps = defaultAddDeps) {
|
|
|
2294
2860
|
deps.out("Couldn't detect your framework from package.json. `clivly add widget` supports Next.js, TanStack Start, SvelteKit, Remix, and Nuxt.\n");
|
|
2295
2861
|
return 1;
|
|
2296
2862
|
}
|
|
2863
|
+
const layout = resolveAppLayout(cwd, deps.fileExists);
|
|
2297
2864
|
const force = argv.includes("--force");
|
|
2298
2865
|
const dryRun = argv.includes("--dry-run");
|
|
2299
2866
|
const files = planWidgetMutations({
|
|
2300
2867
|
framework,
|
|
2301
2868
|
cwd,
|
|
2302
2869
|
force,
|
|
2870
|
+
layout,
|
|
2303
2871
|
readFile: (path) => deps.readFile(path)
|
|
2304
2872
|
});
|
|
2873
|
+
const shadowing = shadowedAppDirError(cwd, framework, files.map((file) => file.path), deps.fileExists);
|
|
2874
|
+
if (shadowing) {
|
|
2875
|
+
deps.out(shadowing);
|
|
2876
|
+
return 1;
|
|
2877
|
+
}
|
|
2305
2878
|
deps.out(`\nClivly add widget — ${FRAMEWORK_LABELS[framework]}\n\n`);
|
|
2306
2879
|
if (dryRun) deps.out("Dry run — nothing was written.\n\n");
|
|
2307
2880
|
for (const file of files) {
|
|
@@ -2322,7 +2895,7 @@ function runAdd(argv, cwd, deps = defaultAddDeps) {
|
|
|
2322
2895
|
}
|
|
2323
2896
|
if (!dryRun) {
|
|
2324
2897
|
const sessionPath = files[0]?.path ?? "";
|
|
2325
|
-
deps.out(`\nNext steps:\n 1. Add your site origins to allowedOrigins in ${sessionPath}\n 2. Create the widget + set allowed origins
|
|
2898
|
+
deps.out(`\nNext steps:\n 1. Add your site origins to allowedOrigins in ${sessionPath}\n 2. Create the widget + set allowed origins at ${WIDGET_LOCATION}\n 3. Embed it: <ClivlyChatWidget widgetId="…" />\n`);
|
|
2326
2899
|
}
|
|
2327
2900
|
return 0;
|
|
2328
2901
|
}
|
|
@@ -2333,6 +2906,13 @@ function readOwnVersion() {
|
|
|
2333
2906
|
if (pkg.name === "clivly" && pkg.version) return pkg.version;
|
|
2334
2907
|
} catch {}
|
|
2335
2908
|
}
|
|
2909
|
+
function readOwnEngines() {
|
|
2910
|
+
const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
2911
|
+
for (const rel of ["./package.json", "../package.json"]) try {
|
|
2912
|
+
const pkg = req(rel);
|
|
2913
|
+
if (pkg.name === "clivly") return pkg.engines?.node;
|
|
2914
|
+
} catch {}
|
|
2915
|
+
}
|
|
2336
2916
|
/**
|
|
2337
2917
|
* Parse a `--db-import` / `--auth-import` value into a discovery-shaped override.
|
|
2338
2918
|
*
|
|
@@ -2362,12 +2942,24 @@ function authInstanceFromFlag(flag) {
|
|
|
2362
2942
|
function defaultReadFile(path) {
|
|
2363
2943
|
return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : null;
|
|
2364
2944
|
}
|
|
2945
|
+
function resolveEnvPath(cwd) {
|
|
2946
|
+
const framework = detectFramework(readDeps(cwd));
|
|
2947
|
+
return (0, node_path.join)(cwd, resolveEnvFile({
|
|
2948
|
+
existing: ENV_FILE_CANDIDATES.filter((name) => {
|
|
2949
|
+
const path = (0, node_path.join)(cwd, name);
|
|
2950
|
+
if (!(0, node_fs.existsSync)(path)) return false;
|
|
2951
|
+
const contents = (0, node_fs.readFileSync)(path, "utf8");
|
|
2952
|
+
return isKeySet(contents, "CLIVLY_SECRET_KEY") || isKeySet(contents, "CLIVLY_API_KEY");
|
|
2953
|
+
}),
|
|
2954
|
+
framework
|
|
2955
|
+
}));
|
|
2956
|
+
}
|
|
2365
2957
|
function defaultReadEnv(cwd) {
|
|
2366
|
-
const path = (
|
|
2958
|
+
const path = resolveEnvPath(cwd);
|
|
2367
2959
|
return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : "";
|
|
2368
2960
|
}
|
|
2369
2961
|
function defaultWriteEnv(cwd, contents) {
|
|
2370
|
-
(0, node_fs.writeFileSync)((
|
|
2962
|
+
(0, node_fs.writeFileSync)(resolveEnvPath(cwd), contents, "utf8");
|
|
2371
2963
|
}
|
|
2372
2964
|
function defaultIsInteractive() {
|
|
2373
2965
|
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
@@ -2394,6 +2986,8 @@ function resolveInitDeps(options) {
|
|
|
2394
2986
|
graph: graphFor(cwd)
|
|
2395
2987
|
})),
|
|
2396
2988
|
readFile: options.readFile ?? defaultReadFile,
|
|
2989
|
+
fileExists: options.fileExists ?? node_fs.existsSync,
|
|
2990
|
+
resolveAppLayout: options.resolveAppLayout ?? resolveAppLayout,
|
|
2397
2991
|
writeConfig: options.writeConfig ?? ((path, contents) => {
|
|
2398
2992
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
2399
2993
|
(0, node_fs.writeFileSync)(path, contents, "utf8");
|
|
@@ -2414,6 +3008,7 @@ function resolveInitDeps(options) {
|
|
|
2414
3008
|
rotate: false
|
|
2415
3009
|
})),
|
|
2416
3010
|
framework: "unknown",
|
|
3011
|
+
layout: "app",
|
|
2417
3012
|
authWiring: renderAuthWiring("unknown")
|
|
2418
3013
|
};
|
|
2419
3014
|
}
|
|
@@ -2428,11 +3023,15 @@ async function acquireSchema(options, deps) {
|
|
|
2428
3023
|
const orm = detectOrm(appDeps);
|
|
2429
3024
|
const auth = detectAuth(appDeps);
|
|
2430
3025
|
deps.framework = framework;
|
|
2431
|
-
|
|
3026
|
+
const layout = deps.resolveAppLayout(options.cwd, deps.fileExists);
|
|
3027
|
+
deps.layout = layout;
|
|
2432
3028
|
out("\nClivly init\n");
|
|
3029
|
+
deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
|
|
2433
3030
|
out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
|
|
2434
3031
|
out(` ORM: ${orm}\n`);
|
|
2435
|
-
out(` Auth: ${auth}${deps.authWiring.complete ? " (wired)" : ""}\n
|
|
3032
|
+
out(` Auth: ${auth}${deps.authWiring.complete ? " (wired)" : ""}\n`);
|
|
3033
|
+
if (framework === "nextjs") out(` Layout: ${layout}\n`);
|
|
3034
|
+
out("\n");
|
|
2436
3035
|
if (orm !== "drizzle") {
|
|
2437
3036
|
out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
|
|
2438
3037
|
return { code: 1 };
|
|
@@ -2476,6 +3075,7 @@ function mutationsFor(options, deps, plan) {
|
|
|
2476
3075
|
cwd: options.cwd,
|
|
2477
3076
|
force: options.force,
|
|
2478
3077
|
framework: deps.framework,
|
|
3078
|
+
layout: deps.layout,
|
|
2479
3079
|
authWiring: deps.authWiring,
|
|
2480
3080
|
plan,
|
|
2481
3081
|
readFile: deps.readFile,
|
|
@@ -2535,14 +3135,15 @@ function previewMutations(deps, mutations, options) {
|
|
|
2535
3135
|
}
|
|
2536
3136
|
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");
|
|
2537
3137
|
}
|
|
2538
|
-
function printNextSteps(out, authComplete, loginRan) {
|
|
3138
|
+
function printNextSteps(out, authComplete, loginRan, envFile) {
|
|
2539
3139
|
out("\nNext steps:\n");
|
|
2540
|
-
out(loginRan ? " 1. Credentials written by `clivly login`\n" :
|
|
3140
|
+
out(loginRan ? " 1. Credentials written by `clivly login`\n" : ` 1. Add CLIVLY_SECRET_KEY to ${envFile} — create one at\n ${CREDENTIALS_LOCATION}\n (the legacy CLIVLY_API_KEY is still accepted)\n`);
|
|
2541
3141
|
out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
|
|
2542
3142
|
out(" Local dev? Run `clivly dev` to open a tunnel, or skip this step until you deploy (remote sync is deploy-only locally).\n");
|
|
2543
3143
|
const step = authComplete ? 3 : 4;
|
|
2544
3144
|
if (!authComplete) out(" 3. Finish resolveUser in the auth/verify route\n");
|
|
2545
3145
|
out(` ${step}. Run \`clivly status\` to see where you are\n`);
|
|
3146
|
+
out("\nAfter editing clivly.config.ts, restart your app — it loads the config\nat boot, so a running server keeps using the old one.\n");
|
|
2546
3147
|
}
|
|
2547
3148
|
function credentialsMissing(envContents) {
|
|
2548
3149
|
return !(isKeySet(envContents, "CLIVLY_SECRET_KEY") || isKeySet(envContents, "CLIVLY_API_KEY"));
|
|
@@ -2563,6 +3164,11 @@ async function scaffold(options, deps, plan) {
|
|
|
2563
3164
|
const pm = deps.detectPackageManager(options.cwd);
|
|
2564
3165
|
const packages = [resolveRuntimeInstallSpec(deps), ...deps.authWiring.packages];
|
|
2565
3166
|
const mutations = mutationsFor(options, deps, plan);
|
|
3167
|
+
const shadowing = shadowedAppDirError(options.cwd, deps.framework, mutations.files.map((file) => file.path), deps.fileExists);
|
|
3168
|
+
if (shadowing) {
|
|
3169
|
+
out(shadowing);
|
|
3170
|
+
return 1;
|
|
3171
|
+
}
|
|
2566
3172
|
if (options.dryRun || options.diff) {
|
|
2567
3173
|
out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
|
|
2568
3174
|
previewMutations(deps, mutations, {
|
|
@@ -2587,7 +3193,7 @@ async function scaffold(options, deps, plan) {
|
|
|
2587
3193
|
out(" Run `clivly login` from a terminal to write CLIVLY_SECRET_KEY and CLIVLY_SYNC_TRIGGER_SECRET.\n");
|
|
2588
3194
|
} else loginRan = await deps.login(options.cwd) === 0;
|
|
2589
3195
|
}
|
|
2590
|
-
printNextSteps(out, deps.authWiring.complete, loginRan);
|
|
3196
|
+
printNextSteps(out, deps.authWiring.complete, loginRan, envFileFor(detectFramework(deps.readDepsFn(options.cwd))));
|
|
2591
3197
|
return 0;
|
|
2592
3198
|
}
|
|
2593
3199
|
function normalizeAppRoot(options) {
|
|
@@ -2654,8 +3260,18 @@ function printNoContactGuidance(out, plan, options) {
|
|
|
2654
3260
|
out(`\n Tables in ${plan.importPath}:\n`);
|
|
2655
3261
|
for (const name of plan.tableNames) out(` ${name}\n`);
|
|
2656
3262
|
const schemaArg = options.schemaFlag ? ` --schema ${options.schemaFlag}` : "";
|
|
2657
|
-
|
|
2658
|
-
|
|
3263
|
+
const best = plan.contactCandidates[0];
|
|
3264
|
+
if (best) {
|
|
3265
|
+
out(`\n Best guess: ${best.table} — ${best.reasons.join(", ")}\n`);
|
|
3266
|
+
const runnerUp = plan.contactCandidates.slice(1, 3).map((candidate) => candidate.table);
|
|
3267
|
+
if (runnerUp.length > 0) out(` Also possible: ${runnerUp.join(", ")}\n`);
|
|
3268
|
+
out("\n Confirm it and re-run:\n");
|
|
3269
|
+
out(` clivly init${schemaArg} --contact-table ${best.table}\n\n`);
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
out("\n Nothing here looks like a people table — no email, phone or name\n");
|
|
3273
|
+
out(" columns turned up. Pick the one that holds your people and re-run:\n");
|
|
3274
|
+
out(` clivly init${schemaArg} --contact-table ${plan.tableNames[0] ?? "<table>"}\n\n`);
|
|
2659
3275
|
out(" Nothing was written — Clivly needs one entity before a config can load.\n\n");
|
|
2660
3276
|
}
|
|
2661
3277
|
function hasCloudflaredOnPath() {
|
|
@@ -2751,6 +3367,22 @@ function defaultDevDeps(options) {
|
|
|
2751
3367
|
},
|
|
2752
3368
|
out: (line) => {
|
|
2753
3369
|
process.stdout.write(`${line}\n`);
|
|
3370
|
+
},
|
|
3371
|
+
watchConfig: (cwd, onChange) => {
|
|
3372
|
+
const target = resolveConfigPath(cwd);
|
|
3373
|
+
try {
|
|
3374
|
+
let timer = null;
|
|
3375
|
+
const watcher = (0, node_fs.watch)(target, () => {
|
|
3376
|
+
if (timer) clearTimeout(timer);
|
|
3377
|
+
timer = setTimeout(onChange, CONFIG_WATCH_DEBOUNCE_MS);
|
|
3378
|
+
});
|
|
3379
|
+
return () => {
|
|
3380
|
+
if (timer) clearTimeout(timer);
|
|
3381
|
+
watcher.close();
|
|
3382
|
+
};
|
|
3383
|
+
} catch {
|
|
3384
|
+
return null;
|
|
3385
|
+
}
|
|
2754
3386
|
}
|
|
2755
3387
|
};
|
|
2756
3388
|
}
|
|
@@ -2829,6 +3461,21 @@ function runInitFromArgv(argv, cwd) {
|
|
|
2829
3461
|
}
|
|
2830
3462
|
async function main(argv) {
|
|
2831
3463
|
const [command, ...rest] = argv;
|
|
3464
|
+
if (command === "-h" || command === "--help" || isHelpRequest(rest)) {
|
|
3465
|
+
process.stdout.write(helpFor(command));
|
|
3466
|
+
return 0;
|
|
3467
|
+
}
|
|
3468
|
+
if (command === "--version" || command === "-v") {
|
|
3469
|
+
process.stdout.write(`${readOwnVersion() ?? "unknown"}\n`);
|
|
3470
|
+
return 0;
|
|
3471
|
+
}
|
|
3472
|
+
if (!process.env.CLIVLY_SKIP_ENGINE_CHECK) {
|
|
3473
|
+
const engineProblem = engineError(process.version, readOwnEngines());
|
|
3474
|
+
if (engineProblem) {
|
|
3475
|
+
process.stderr.write(engineProblem);
|
|
3476
|
+
return 1;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
2832
3479
|
let cliEnvFiles = [];
|
|
2833
3480
|
try {
|
|
2834
3481
|
cliEnvFiles = (await loadCliConfig(process.cwd()))?.envFiles ?? [];
|
|
@@ -2847,14 +3494,16 @@ async function main(argv) {
|
|
|
2847
3494
|
case "status":
|
|
2848
3495
|
case "ping":
|
|
2849
3496
|
case "doctor":
|
|
2850
|
-
case "connect":
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
3497
|
+
case "connect":
|
|
3498
|
+
if (command !== "status") process.stderr.write(`\`clivly ${command}\` is a deprecated alias that runs part of \`clivly status\`. Prefer \`clivly status\`.\n`);
|
|
3499
|
+
return await runStatus({
|
|
3500
|
+
cwd: process.cwd(),
|
|
3501
|
+
apiKey: readSecretKeyEnv(process.env),
|
|
3502
|
+
apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
|
|
3503
|
+
configPath: flagValue(rest, "--config"),
|
|
3504
|
+
only: ALIAS_STAGE_IDS[command],
|
|
3505
|
+
verbose: rest.includes("--verbose")
|
|
3506
|
+
});
|
|
2858
3507
|
case "dev": {
|
|
2859
3508
|
const apiKey = readSecretKeyEnv(process.env);
|
|
2860
3509
|
const apiUrl = flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL ?? "https://api.clivly.com";
|
|
@@ -2877,14 +3526,11 @@ async function main(argv) {
|
|
|
2877
3526
|
openBrowser: !rest.includes("--no-browser"),
|
|
2878
3527
|
deps: defaultLoginDeps()
|
|
2879
3528
|
});
|
|
2880
|
-
case "--version":
|
|
2881
|
-
case "-v":
|
|
2882
|
-
process.stdout.write(`${readOwnVersion() ?? "unknown"}\n`);
|
|
2883
|
-
return 0;
|
|
2884
3529
|
case void 0:
|
|
3530
|
+
process.stdout.write(ROOT_HELP);
|
|
3531
|
+
return 0;
|
|
2885
3532
|
case "help":
|
|
2886
|
-
|
|
2887
|
-
process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--dry-run] [--diff] [--yes] [--no-login] [--no-browser]\n [--schema <path|package>] [--db-import <specifier>] [--auth-import <specifier>]\n Scaffold Clivly (config, routes, .env) for your app\n clivly add widget [--force] [--dry-run] Scaffold the chat-session route\n clivly status [--verbose] [--config <path>] [--env-file <path>] Check your setup, step by step\n clivly push-schema [--config <path>] Report your schema to Clivly without deploying\n clivly dev [--port <n>] [--api-url <url>] [--route-path <path>] [-- <cmd>] Tunnel localhost for remote sync\n clivly login [--force] [--rotate] [--api-url <url>] [--no-browser] Pair this machine and write CLIVLY_SECRET_KEY / CLIVLY_SYNC_TRIGGER_SECRET to .env\n clivly --version Print the installed clivly version\n");
|
|
3533
|
+
process.stdout.write(helpFor(rest[0]));
|
|
2888
3534
|
return 0;
|
|
2889
3535
|
default: {
|
|
2890
3536
|
process.stdout.write(`Unknown command: ${command}\n`);
|