clivly 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -6
- package/{adapter-CXr12l4K.d.cts → adapter-C2eRbBfo.d.cts} +1 -2
- package/{adapter-CvNBD_3y.d.mts → adapter-D5LRa-NA.d.mts} +1 -2
- package/{auth-adapter-CdLODn2p.d.cts → auth-adapter-Bjgo8LhN.d.mts} +1 -2
- package/{auth-adapter-CRBW4elj.d.mts → auth-adapter-DWzgl5Yq.d.cts} +1 -2
- package/auth-better-auth.d.cts +1 -2
- package/auth-better-auth.d.mts +1 -2
- package/auth-clerk.d.cts +1 -2
- package/auth-clerk.d.mts +1 -2
- package/auth-workos.d.cts +1 -2
- package/auth-workos.d.mts +1 -2
- package/core-adapter.cjs +5 -0
- package/core-adapter.d.cts +1 -1
- package/core-adapter.d.mts +1 -1
- package/core-adapter.mjs +5 -0
- package/core-auth-adapter.cjs +6 -1
- package/core-auth-adapter.d.cts +1 -1
- package/core-auth-adapter.d.mts +1 -1
- package/core-auth-adapter.mjs +6 -1
- package/core-drizzle.cjs +6 -30
- package/core-drizzle.d.cts +1 -20
- package/core-drizzle.d.mts +1 -20
- package/core-drizzle.mjs +5 -29
- package/core-entity-config.cjs +5 -1
- package/core-entity-config.d.cts +1 -1
- package/core-entity-config.d.mts +1 -1
- package/core-entity-config.mjs +5 -1
- package/core-entity-heuristics.cjs +15 -453
- package/core-entity-heuristics.d.cts +1 -67
- package/core-entity-heuristics.d.mts +1 -67
- package/core-entity-heuristics.mjs +4 -442
- package/core-mapping-form.cjs +6 -1
- package/core-mapping-form.d.cts +1 -1
- package/core-mapping-form.d.mts +1 -1
- package/core-mapping-form.mjs +6 -1
- package/core-mapping-score.cjs +5 -1
- package/core-mapping-score.d.cts +1 -1
- package/core-mapping-score.d.mts +1 -1
- package/core-mapping-score.mjs +5 -1
- package/core-mappings-config.cjs +6 -1
- package/core-mappings-config.d.cts +1 -1
- package/core-mappings-config.d.mts +1 -1
- package/core-mappings-config.mjs +6 -1
- package/core-sync-engine.cjs +6 -1
- package/core-sync-engine.d.cts +1 -1
- package/core-sync-engine.d.mts +1 -1
- package/core-sync-engine.mjs +6 -1
- package/core-types.cjs +5 -0
- package/core-types.d.cts +1 -1
- package/core-types.d.mts +1 -1
- package/core-types.mjs +5 -0
- package/core-view-compiler.cjs +6 -1
- package/core-view-compiler.d.cts +1 -1
- package/core-view-compiler.d.mts +1 -1
- package/core-view-compiler.mjs +6 -1
- package/core.cjs +11 -6
- package/core.d.cts +9 -9
- package/core.d.mts +9 -9
- package/core.mjs +11 -6
- package/deprecate-subpath-B9ngvizt.cjs +30 -0
- package/deprecate-subpath-CmvZtCpE.mjs +25 -0
- package/drizzle-B89tLo4T.mjs +31 -0
- package/drizzle-CFOr9dU9.d.mts +20 -0
- package/drizzle-DiReZf6d.cjs +36 -0
- package/drizzle-DmeKSqeF.d.cts +20 -0
- package/drizzle.cjs +3 -1
- package/drizzle.d.cts +11 -3
- package/drizzle.d.mts +11 -3
- package/drizzle.mjs +3 -2
- package/{entity-config-DMuZpchz.cjs → entity-config-D87rV-tE.cjs} +194 -192
- package/{entity-config-D1DFyeUp.d.mts → entity-config-DoUCXtq5.d.cts} +48 -35
- package/{entity-config-GczjD3y8.d.cts → entity-config-DoUCXtq5.d.mts} +48 -35
- package/{entity-config-D3YpQB0z.mjs → entity-config-va2bkH5o.mjs} +194 -192
- package/index.cjs +943 -228
- package/index.d.cts +32 -2
- package/index.d.mts +32 -2
- package/index.mjs +932 -221
- package/internal-adapter.cjs +0 -0
- package/internal-adapter.d.cts +2 -0
- package/internal-adapter.d.mts +2 -0
- package/internal-adapter.mjs +1 -0
- package/internal-auth-adapter.cjs +3 -0
- package/internal-auth-adapter.d.cts +2 -0
- package/internal-auth-adapter.d.mts +2 -0
- package/internal-auth-adapter.mjs +2 -0
- package/internal-drizzle.cjs +3 -0
- package/internal-drizzle.d.cts +2 -0
- package/internal-drizzle.d.mts +2 -0
- package/internal-drizzle.mjs +2 -0
- package/internal-entity-config.cjs +13 -0
- package/internal-entity-config.d.cts +2 -0
- package/internal-entity-config.d.mts +2 -0
- package/internal-entity-config.mjs +2 -0
- package/internal-entity-heuristics.cjs +455 -0
- package/internal-entity-heuristics.d.cts +67 -0
- package/internal-entity-heuristics.d.mts +67 -0
- package/internal-entity-heuristics.mjs +444 -0
- package/internal-mapping-form.cjs +5 -0
- package/internal-mapping-form.d.cts +2 -0
- package/internal-mapping-form.d.mts +2 -0
- package/internal-mapping-form.mjs +2 -0
- package/internal-mapping-score.cjs +12 -0
- package/internal-mapping-score.d.cts +2 -0
- package/internal-mapping-score.d.mts +2 -0
- package/internal-mapping-score.mjs +2 -0
- package/internal-mappings-config.cjs +3 -0
- package/internal-mappings-config.d.cts +2 -0
- package/internal-mappings-config.d.mts +2 -0
- package/internal-mappings-config.mjs +2 -0
- package/internal-sync-engine.cjs +6 -0
- package/internal-sync-engine.d.cts +2 -0
- package/internal-sync-engine.d.mts +2 -0
- package/internal-sync-engine.mjs +2 -0
- package/internal-types.cjs +0 -0
- package/internal-types.d.cts +2 -0
- package/internal-types.d.mts +2 -0
- package/internal-types.mjs +1 -0
- package/internal-view-compiler.cjs +6 -0
- package/internal-view-compiler.d.cts +2 -0
- package/internal-view-compiler.d.mts +2 -0
- package/internal-view-compiler.mjs +2 -0
- package/internal.cjs +28 -0
- package/internal.d.cts +9 -0
- package/internal.d.mts +9 -0
- package/internal.mjs +7 -0
- package/{mapping-form-CUlFVfG8.d.cts → mapping-form-D93lhHEc.d.mts} +1 -2
- package/{mapping-form-kjSiJaCX.d.mts → mapping-form-lfyEE7EJ.d.cts} +1 -2
- package/{mappings-config-DYZ_uOOB.cjs → mappings-config-BsRADNRv.cjs} +1 -1
- package/{mappings-config-CNTZRD4g.mjs → mappings-config-Dbgxx3Nv.mjs} +1 -1
- package/{mappings-config-DjRLCb2Q.d.cts → mappings-config-DsUa7-1w.d.mts} +1 -2
- package/{mappings-config-GxiYsSBQ.d.mts → mappings-config-XmzB4dag.d.cts} +1 -2
- package/package.json +156 -3
- package/{sdk-DdzBxgMx.mjs → sdk-C_1sGiH8.mjs} +15 -11
- package/{sdk-D1BHQDf5.cjs → sdk-CzOf596t.cjs} +15 -11
- package/sdk.cjs +3 -1
- package/sdk.d.cts +1 -41
- package/sdk.d.mts +1 -41
- package/sdk.mjs +3 -2
- package/{sync-engine-DIVMG9EV.d.cts → sync-engine-B7QIbXro.d.cts} +6 -5
- package/{sync-engine-BtzkzKmY.cjs → sync-engine-CzEbpQGv.cjs} +1 -1
- package/{sync-engine-DLx243iw.d.mts → sync-engine-DGk_AeHJ.d.mts} +6 -5
- package/{sync-engine-D9vghQab.mjs → sync-engine-Do5ru3WF.mjs} +1 -1
- package/types-CsL0rFYh.d.cts +155 -0
- package/types-CsL0rFYh.d.mts +155 -0
- package/{view-compiler-4HsRUTWH.d.cts → view-compiler-BWz0sGdm.d.cts} +1 -2
- package/{view-compiler-BpIDExjd.d.mts → view-compiler-Bn_EJJMO.d.mts} +1 -2
- package/vite.d.cts +1 -2
- package/vite.d.mts +1 -2
- package/namespace-probe-CA4Vj40g.d.mts +0 -331
- package/namespace-probe-CkHCj9tQ.d.cts +0 -331
- /package/{auth-adapter-CBOKDhlc.cjs → auth-adapter-CiEXcweZ.cjs} +0 -0
- /package/{auth-adapter-wqnnEjo3.mjs → auth-adapter-gfVDxf3d.mjs} +0 -0
- /package/{drizzle-meta-BVR1qiyu.cjs → drizzle-meta-Bb1JWYqf.cjs} +0 -0
- /package/{drizzle-meta-DpMKataU.mjs → drizzle-meta-D-5k3n0i.mjs} +0 -0
- /package/{mapping-form-DdmDebC3.cjs → mapping-form-BpfON_ME.cjs} +0 -0
- /package/{mapping-form-BHtPexzI.mjs → mapping-form-DigkuvFc.mjs} +0 -0
- /package/{mapping-score-CPPvwoZC.mjs → mapping-score-C68uAqGB.mjs} +0 -0
- /package/{mapping-score-i3p8LWDF.cjs → mapping-score-CaB3F4Uu.cjs} +0 -0
- /package/{mapping-score-A2lRnlpk.d.mts → mapping-score-D7Gzg7Ii.d.cts} +0 -0
- /package/{mapping-score-BcjC0ZQs.d.cts → mapping-score-D7Gzg7Ii.d.mts} +0 -0
- /package/{types-BpsKVccL.d.cts → types-B1oRXdNP.d.cts} +0 -0
- /package/{types-_qbJXMt_.d.mts → types-B1oRXdNP.d.mts} +0 -0
- /package/{view-compiler-BcgJINr7.cjs → view-compiler-BIwvm-o2.cjs} +0 -0
- /package/{view-compiler-DeZzx_jn.mjs → view-compiler-ChWr_Lml.mjs} +0 -0
package/index.cjs
CHANGED
|
@@ -1,17 +1,117 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const require_entity_config = require("./entity-config-
|
|
4
|
-
require("./
|
|
5
|
-
|
|
6
|
-
const
|
|
3
|
+
const require_entity_config = require("./entity-config-D87rV-tE.cjs");
|
|
4
|
+
require("./core-entity-config.cjs");
|
|
5
|
+
require("./mapping-score-CaB3F4Uu.cjs");
|
|
6
|
+
const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
|
|
7
|
+
require("./core-entity-heuristics.cjs");
|
|
8
|
+
require("./core-mapping-score.cjs");
|
|
9
|
+
const require_sdk = require("./sdk-CzOf596t.cjs");
|
|
7
10
|
let node_child_process = require("node:child_process");
|
|
8
11
|
let node_fs = require("node:fs");
|
|
9
12
|
let node_module = require("node:module");
|
|
10
13
|
let node_os = require("node:os");
|
|
11
14
|
let node_path = require("node:path");
|
|
12
15
|
let node_readline_promises = require("node:readline/promises");
|
|
13
|
-
let node_url = require("node:url");
|
|
14
16
|
let jiti = require("jiti");
|
|
17
|
+
let node_url = require("node:url");
|
|
18
|
+
//#region src/load-config.ts
|
|
19
|
+
const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
|
|
20
|
+
function resolveConfigPath(cwd, configPath) {
|
|
21
|
+
return (0, node_path.resolve)(cwd, configPath ?? "clivly.config.ts");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Load the config and return its default export, checking it carries the method
|
|
25
|
+
* the caller needs. The check is by capability rather than by type: the config
|
|
26
|
+
* is the developer's own SDK instance, and a clear "this isn't an SDK instance"
|
|
27
|
+
* beats a TypeError deep inside a command.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The first meaningful line of an error message. jiti reports a failed
|
|
31
|
+
* resolution with a multi-line "Require stack:" dump naming Clivly's own
|
|
32
|
+
* internal files — noise that tells the developer nothing about their app and
|
|
33
|
+
* buries the one line that does.
|
|
34
|
+
*/
|
|
35
|
+
function firstLine(message) {
|
|
36
|
+
return message.split("\n")[0]?.trim() ?? "";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Describe why the config failed to load, in one line the developer can act on.
|
|
40
|
+
*
|
|
41
|
+
* A config that fails its own schema validation throws a zod error carrying
|
|
42
|
+
* structured `issues`, but whose `message` is a multi-line JSON dump — its
|
|
43
|
+
* first line is a bare "[", so `firstLine` alone reduced the entire diagnosis
|
|
44
|
+
* to a single bracket. Render the issues as `path: message` instead, which is
|
|
45
|
+
* what actually names the entity to fix. Anything else keeps the require-stack
|
|
46
|
+
* trimming `firstLine` exists for.
|
|
47
|
+
*/
|
|
48
|
+
function describeLoadError(error) {
|
|
49
|
+
const issues = error.issues;
|
|
50
|
+
if (Array.isArray(issues) && issues.length > 0) return issues.map((raw) => {
|
|
51
|
+
const issue = raw;
|
|
52
|
+
const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
|
|
53
|
+
const message = typeof issue.message === "string" ? issue.message : "invalid value";
|
|
54
|
+
return path ? `${path}: ${message}` : message;
|
|
55
|
+
}).join("; ");
|
|
56
|
+
return firstLine(error?.message ?? String(error));
|
|
57
|
+
}
|
|
58
|
+
async function loadConfigSdk(path, requiredMethod) {
|
|
59
|
+
const jiti$3 = (0, jiti.createJiti)(require("url").pathToFileURL(__filename).href);
|
|
60
|
+
let mod;
|
|
61
|
+
try {
|
|
62
|
+
mod = await jiti$3.import(path);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(describeLoadError(error));
|
|
65
|
+
}
|
|
66
|
+
const sdk = mod.default ?? mod;
|
|
67
|
+
if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
|
|
68
|
+
return sdk;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/cli-config.ts
|
|
72
|
+
const CLI_CONFIG_NAMES = [
|
|
73
|
+
"clivly.cli.ts",
|
|
74
|
+
"clivly.cli.js",
|
|
75
|
+
"clivly.cli.mjs"
|
|
76
|
+
];
|
|
77
|
+
/** The app's CLI config, or null when it has none — which is the normal case. */
|
|
78
|
+
function resolveCliConfigPath(cwd) {
|
|
79
|
+
for (const name of CLI_CONFIG_NAMES) {
|
|
80
|
+
const path = (0, node_path.join)(cwd, name);
|
|
81
|
+
if ((0, node_fs.existsSync)(path)) return path;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function readEnvFiles(config) {
|
|
86
|
+
const envFiles = config.envFiles;
|
|
87
|
+
if (envFiles === void 0) return;
|
|
88
|
+
if (!Array.isArray(envFiles)) throw new Error("clivly.cli.ts: envFiles must be an array of paths, e.g. [\"../server/.env\"]");
|
|
89
|
+
if (envFiles.some((entry) => typeof entry !== "string")) throw new Error("clivly.cli.ts: every envFiles entry must be a string");
|
|
90
|
+
return envFiles;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Load `clivly.cli.ts` if the app has one.
|
|
94
|
+
*
|
|
95
|
+
* A malformed config throws rather than being silently ignored: a developer who
|
|
96
|
+
* wrote `envFiles` and sees nothing happen has no way to tell the difference
|
|
97
|
+
* between "not supported" and "I typed it wrong", and the symptom they get
|
|
98
|
+
* instead is a missing API key several steps later.
|
|
99
|
+
*/
|
|
100
|
+
async function loadCliConfig(cwd) {
|
|
101
|
+
const path = resolveCliConfigPath(cwd);
|
|
102
|
+
if (!path) return null;
|
|
103
|
+
let mod;
|
|
104
|
+
try {
|
|
105
|
+
mod = await (0, jiti.createJiti)(require("url").pathToFileURL(__filename).href).import(path);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`clivly.cli.ts: ${describeLoadError(error)}`);
|
|
108
|
+
}
|
|
109
|
+
const config = mod?.default ?? mod;
|
|
110
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) throw new Error("clivly.cli.ts must export an object as its default export");
|
|
111
|
+
const envFiles = readEnvFiles(config);
|
|
112
|
+
return envFiles ? { envFiles } : {};
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
15
115
|
//#region src/detect.ts
|
|
16
116
|
const FRAMEWORK_MARKERS = [
|
|
17
117
|
{
|
|
@@ -153,7 +253,7 @@ function detectDevCommand(scripts, explicit) {
|
|
|
153
253
|
return null;
|
|
154
254
|
}
|
|
155
255
|
const OVERRIDE_FAILURE_CAUSES = {
|
|
156
|
-
unauthorized: "
|
|
256
|
+
unauthorized: "CLIVLY_SECRET_KEY is invalid for this workspace/backend.",
|
|
157
257
|
not_enabled: "The target backend does not have the feature-flagged trigger-url endpoint enabled (likely the wrong backend, or the flag is off).",
|
|
158
258
|
unreachable: "Could not reach Clivly at the selected API base URL."
|
|
159
259
|
};
|
|
@@ -201,7 +301,7 @@ async function runDev(options) {
|
|
|
201
301
|
return 0;
|
|
202
302
|
}
|
|
203
303
|
if (!options.apiKey) {
|
|
204
|
-
deps.out("
|
|
304
|
+
deps.out("CLIVLY_SECRET_KEY is not set — cannot register the tunnel URL. Set it and re-run.");
|
|
205
305
|
return 1;
|
|
206
306
|
}
|
|
207
307
|
const app = command ? deps.spawnApp(command) : null;
|
|
@@ -280,16 +380,275 @@ async function probeModule(opts) {
|
|
|
280
380
|
}
|
|
281
381
|
for (const [moduleKey, value] of Object.entries(mod)) if (opts.match(value)) return {
|
|
282
382
|
moduleKey,
|
|
283
|
-
importPath: toImportPath(candidate, opts.cwd)
|
|
383
|
+
importPath: toImportPath(candidate, opts.cwd, opts.graph)
|
|
284
384
|
};
|
|
285
385
|
}
|
|
286
386
|
return null;
|
|
287
387
|
}
|
|
288
388
|
//#endregion
|
|
389
|
+
//#region src/workspace-graph.ts
|
|
390
|
+
const WINDOWS_SEP$2 = /\\/g;
|
|
391
|
+
const MODULE_EXT$1 = /\.(ts|tsx|js|jsx|mjs|cjs|cts|mts)$/;
|
|
392
|
+
const BRACE_GROUP = /\{([^{}]*)\}/;
|
|
393
|
+
const MAX_GLOB_DEPTH = 8;
|
|
394
|
+
const IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
395
|
+
"node_modules",
|
|
396
|
+
"dist",
|
|
397
|
+
"build",
|
|
398
|
+
"coverage"
|
|
399
|
+
]);
|
|
400
|
+
function isDirectory$1(path) {
|
|
401
|
+
try {
|
|
402
|
+
return (0, node_fs.statSync)(path).isDirectory();
|
|
403
|
+
} catch {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const CONVENTIONAL_ENTRIES = [
|
|
408
|
+
"src/index.ts",
|
|
409
|
+
"src/index.tsx",
|
|
410
|
+
"index.ts",
|
|
411
|
+
"index.tsx",
|
|
412
|
+
"src/index.js",
|
|
413
|
+
"index.js"
|
|
414
|
+
];
|
|
415
|
+
function readPackageManifest(dir) {
|
|
416
|
+
const path = (0, node_path.join)(dir, "package.json");
|
|
417
|
+
if (!(0, node_fs.existsSync)(path)) return null;
|
|
418
|
+
try {
|
|
419
|
+
return JSON.parse((0, node_fs.readFileSync)(path, "utf8"));
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function parentDir$1(dir) {
|
|
425
|
+
const parent = (0, node_path.dirname)(dir);
|
|
426
|
+
return parent === dir ? null : parent;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* The workspace globs declared at `dir`, from either `package.json#workspaces`
|
|
430
|
+
* (npm, yarn, bun) or `pnpm-workspace.yaml` (pnpm). Negations are dropped —
|
|
431
|
+
* they only ever narrow the set, and treating one as a literal path would
|
|
432
|
+
* invent a package directory named `!something`.
|
|
433
|
+
*/
|
|
434
|
+
function readWorkspaceGlobs(dir) {
|
|
435
|
+
const manifest = readPackageManifest(dir);
|
|
436
|
+
const declared = Array.isArray(manifest?.workspaces) ? manifest.workspaces : manifest?.workspaces?.packages;
|
|
437
|
+
if (Array.isArray(declared) && declared.length > 0) return declared.filter((glob) => !glob.startsWith("!"));
|
|
438
|
+
const pnpmPath = (0, node_path.join)(dir, "pnpm-workspace.yaml");
|
|
439
|
+
if (!(0, node_fs.existsSync)(pnpmPath)) return [];
|
|
440
|
+
try {
|
|
441
|
+
return (0, node_fs.readFileSync)(pnpmPath, "utf8").split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim().replace(/^['"]|['"]$/g, "")).filter((glob) => glob.length > 0 && !glob.startsWith("!"));
|
|
442
|
+
} catch {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* `{apps,packages}/*` -> [`apps/*`, `packages/*`].
|
|
448
|
+
*
|
|
449
|
+
* Expands one group per pass and recurses, so nested and repeated groups all
|
|
450
|
+
* resolve. A manifest that declares a brace glob is a workspace like any other,
|
|
451
|
+
* and failing to expand it silently yields an empty graph — which downgrades
|
|
452
|
+
* every generated specifier to the relative form this module exists to avoid.
|
|
453
|
+
*/
|
|
454
|
+
function expandBraces(pattern) {
|
|
455
|
+
const group = BRACE_GROUP.exec(pattern);
|
|
456
|
+
if (!group) return [pattern];
|
|
457
|
+
return (group[1] ?? "").split(",").flatMap((option) => expandBraces(pattern.replace(group[0], option.trim())));
|
|
458
|
+
}
|
|
459
|
+
function childDirectories(dir) {
|
|
460
|
+
try {
|
|
461
|
+
return (0, node_fs.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !(IGNORED_DIRS.has(entry.name) || entry.name.startsWith("."))).map((entry) => (0, node_path.join)(dir, entry.name));
|
|
462
|
+
} catch {
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/** `dir` and every directory beneath it, up to `depth` levels down. */
|
|
467
|
+
function withDescendants(dir, depth) {
|
|
468
|
+
if (depth <= 0) return [dir];
|
|
469
|
+
return [dir, ...childDirectories(dir).flatMap((child) => withDescendants(child, depth - 1))];
|
|
470
|
+
}
|
|
471
|
+
/** Resolve the remaining glob segments against the directories matched so far. */
|
|
472
|
+
function matchSegments(dirs, segments) {
|
|
473
|
+
const [segment, ...rest] = segments;
|
|
474
|
+
if (segment === void 0) return dirs;
|
|
475
|
+
let next;
|
|
476
|
+
if (segment === "**") next = dirs.flatMap((dir) => withDescendants(dir, MAX_GLOB_DEPTH));
|
|
477
|
+
else if (segment === "*") next = dirs.flatMap(childDirectories);
|
|
478
|
+
else next = dirs.map((dir) => (0, node_path.join)(dir, segment)).filter(isDirectory$1);
|
|
479
|
+
return matchSegments(next, rest);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Expand one workspace glob into the package directories it names.
|
|
483
|
+
*
|
|
484
|
+
* Handles exact paths, `*`, `**`, and brace groups — the shapes that actually
|
|
485
|
+
* appear in pnpm/npm/yarn/bun manifests. `node_modules` and dot-directories are
|
|
486
|
+
* never descended into: a dependency's own package.json is not a workspace
|
|
487
|
+
* member, and treating one as such would put a third-party name in the graph.
|
|
488
|
+
*/
|
|
489
|
+
function expandWorkspaceGlob(root, pattern) {
|
|
490
|
+
return expandBraces(pattern).flatMap((expanded) => matchSegments([root], expanded.split("/").filter((segment) => segment.length > 0))).filter((dir) => (0, node_fs.existsSync)((0, node_path.join)(dir, "package.json")));
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Walk up from `start` looking for the directory that declares workspace globs.
|
|
494
|
+
* Stops *before* `boundary` (the user's home directory by default) so a stray
|
|
495
|
+
* manifest above the project is never mistaken for a workspace root.
|
|
496
|
+
*/
|
|
497
|
+
function findWorkspaceRoot(start, boundary = (0, node_os.homedir)()) {
|
|
498
|
+
const stop = (0, node_path.resolve)(boundary);
|
|
499
|
+
let current = (0, node_path.resolve)(start);
|
|
500
|
+
while (current !== stop) {
|
|
501
|
+
if (readWorkspaceGlobs(current).length > 0) return current;
|
|
502
|
+
const parent = parentDir$1(current);
|
|
503
|
+
if (!parent) return null;
|
|
504
|
+
current = parent;
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Build the package graph containing `start`, or null when `start` is not inside
|
|
510
|
+
* a workspace (the single-package case, where relative imports are correct).
|
|
511
|
+
*/
|
|
512
|
+
function loadWorkspaceGraph(start, boundary = (0, node_os.homedir)()) {
|
|
513
|
+
const root = findWorkspaceRoot(start, boundary);
|
|
514
|
+
if (!root) return null;
|
|
515
|
+
const seen = /* @__PURE__ */ new Set();
|
|
516
|
+
const packages = [];
|
|
517
|
+
for (const pattern of readWorkspaceGlobs(root)) for (const dir of expandWorkspaceGlob(root, pattern)) {
|
|
518
|
+
const absolute = (0, node_path.resolve)(dir);
|
|
519
|
+
if (seen.has(absolute)) continue;
|
|
520
|
+
seen.add(absolute);
|
|
521
|
+
const name = readPackageManifest(absolute)?.name;
|
|
522
|
+
if (typeof name === "string" && name.length > 0) packages.push({
|
|
523
|
+
dir: absolute,
|
|
524
|
+
name
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
root,
|
|
529
|
+
packages
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* The workspace package that owns `absPath`.
|
|
534
|
+
*
|
|
535
|
+
* Compares against `dir + sep` so a sibling sharing a name prefix
|
|
536
|
+
* (`packages/database-legacy` vs `packages/database`) can't match, and returns
|
|
537
|
+
* the longest match so a nested package beats its parent.
|
|
538
|
+
*/
|
|
539
|
+
function findPackageContaining(graph, absPath) {
|
|
540
|
+
if (!graph) return null;
|
|
541
|
+
const target = (0, node_path.resolve)(absPath);
|
|
542
|
+
let best = null;
|
|
543
|
+
for (const pkg of graph.packages) {
|
|
544
|
+
const withSep = pkg.dir.endsWith(node_path.sep) ? pkg.dir : `${pkg.dir}${node_path.sep}`;
|
|
545
|
+
if ((target === pkg.dir || target.startsWith(withSep)) && (!best || pkg.dir.length > best.dir.length)) best = pkg;
|
|
546
|
+
}
|
|
547
|
+
return best;
|
|
548
|
+
}
|
|
549
|
+
/** The first string buried in an `exports` value, following conditions in order. */
|
|
550
|
+
function firstExportString(value) {
|
|
551
|
+
if (typeof value === "string") return value;
|
|
552
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
553
|
+
for (const nested of Object.values(value)) {
|
|
554
|
+
const found = firstExportString(nested);
|
|
555
|
+
if (found) return found;
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
function isFile(path) {
|
|
560
|
+
try {
|
|
561
|
+
return (0, node_fs.statSync)(path).isFile();
|
|
562
|
+
} catch {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* The file a bare import of this package resolves to, or null if we can't tell.
|
|
568
|
+
*
|
|
569
|
+
* Null rather than a guess on purpose: an entry declared in the manifest but
|
|
570
|
+
* absent on disk (a `dist/` build that hasn't run) would otherwise make `init`
|
|
571
|
+
* generate an import of a file that does not exist.
|
|
572
|
+
*/
|
|
573
|
+
function resolvePackageEntry(packageDir) {
|
|
574
|
+
const manifest = readPackageManifest(packageDir);
|
|
575
|
+
const declared = [
|
|
576
|
+
firstExportString((manifest?.exports)?.["."]),
|
|
577
|
+
manifest?.module,
|
|
578
|
+
manifest?.main
|
|
579
|
+
].filter((entry) => typeof entry === "string");
|
|
580
|
+
for (const entry of [...declared, ...CONVENTIONAL_ENTRIES]) {
|
|
581
|
+
const candidate = (0, node_path.resolve)(packageDir, entry);
|
|
582
|
+
if (isFile(candidate)) return candidate;
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Whether `./subpath` is something an importer outside the package may request.
|
|
588
|
+
*
|
|
589
|
+
* A package with no `exports` field uses legacy resolution, where any file is
|
|
590
|
+
* reachable. Once `exports` exists it is an allow-list: Node and TypeScript's
|
|
591
|
+
* `bundler`/`node16` resolution both reject anything it does not name, so
|
|
592
|
+
* emitting such a specifier scaffolds a config that cannot compile.
|
|
593
|
+
*/
|
|
594
|
+
function exportsAllowSubpath(packageDir, subpath) {
|
|
595
|
+
const exports = readPackageManifest(packageDir)?.exports;
|
|
596
|
+
if (exports === void 0 || exports === null) return true;
|
|
597
|
+
if (typeof exports !== "object" || Array.isArray(exports)) return false;
|
|
598
|
+
const request = `./${subpath}`;
|
|
599
|
+
return Object.keys(exports).some((key) => {
|
|
600
|
+
if (!key.startsWith(".")) return false;
|
|
601
|
+
const star = key.indexOf("*");
|
|
602
|
+
if (star === -1) return key === request;
|
|
603
|
+
const prefix = key.slice(0, star);
|
|
604
|
+
const suffix = key.slice(star + 1);
|
|
605
|
+
return request.length >= prefix.length + suffix.length && request.startsWith(prefix) && request.endsWith(suffix);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The specifier that imports `absPath` from outside its package: the bare name
|
|
610
|
+
* when the file is the package entry, `name/subpath` when the package's
|
|
611
|
+
* `exports` permits that subpath, and **null** when it does not.
|
|
612
|
+
*
|
|
613
|
+
* Null is the honest answer rather than a specifier that looks right and fails
|
|
614
|
+
* to resolve. The common shape that hits it is a package exporting only `"."`
|
|
615
|
+
* while the Drizzle instance lives in `src/db.ts` — callers should fall back to
|
|
616
|
+
* a relative import, or ask the developer via `--db-import` / `--auth-import`.
|
|
617
|
+
*/
|
|
618
|
+
function toPackageSpecifier(pkg, absPath) {
|
|
619
|
+
const target = (0, node_path.resolve)(absPath);
|
|
620
|
+
if (resolvePackageEntry(pkg.dir) === target) return pkg.name;
|
|
621
|
+
const subpath = (0, node_path.relative)(pkg.dir, target).replace(WINDOWS_SEP$2, "/").replace(MODULE_EXT$1, "");
|
|
622
|
+
if (subpath.length === 0) return pkg.name;
|
|
623
|
+
return exportsAllowSubpath(pkg.dir, subpath) ? `${pkg.name}/${subpath}` : null;
|
|
624
|
+
}
|
|
625
|
+
//#endregion
|
|
289
626
|
//#region src/discover-schema.ts
|
|
290
627
|
const WINDOWS_SEP$1 = /\\/g;
|
|
291
628
|
const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
|
|
292
|
-
|
|
629
|
+
/**
|
|
630
|
+
* Turn an absolute module path into the specifier the generated config imports
|
|
631
|
+
* it by. Every import `init` emits — schema, db instance, auth instance — is
|
|
632
|
+
* minted here, so this is the one place the monorepo fix has to land.
|
|
633
|
+
*
|
|
634
|
+
* "/app/src/db/schema.ts" (cwd "/app") -> "./src/db/schema"
|
|
635
|
+
*
|
|
636
|
+
* With a workspace `graph`, a file owned by a *different* workspace package
|
|
637
|
+
* becomes a package specifier instead: a schema in `packages/database` imports
|
|
638
|
+
* as `@acme/database`, not `../../packages/database/src/index`. The relative
|
|
639
|
+
* form escapes the app's tsconfig rootDir and sidesteps the package boundary the
|
|
640
|
+
* repo already maintains, which is what made generated configs unusable in
|
|
641
|
+
* shared-package layouts.
|
|
642
|
+
*
|
|
643
|
+
* A file inside the app itself still gets a relative specifier — the app is a
|
|
644
|
+
* workspace package too, and importing it by its own name would be a cycle.
|
|
645
|
+
*/
|
|
646
|
+
function toImportPath(schemaPath, cwd, graph) {
|
|
647
|
+
const owner = findPackageContaining(graph ?? null, schemaPath);
|
|
648
|
+
if (owner && (0, node_path.resolve)(owner.dir) !== (0, node_path.resolve)(cwd)) {
|
|
649
|
+
const specifier = toPackageSpecifier(owner, schemaPath);
|
|
650
|
+
if (specifier) return specifier;
|
|
651
|
+
}
|
|
293
652
|
const rel = (0, node_path.relative)(cwd, schemaPath).replace(WINDOWS_SEP$1, "/").replace(MODULE_EXT, "");
|
|
294
653
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
295
654
|
}
|
|
@@ -304,8 +663,8 @@ function loadSchemaModule(schemaPath, load) {
|
|
|
304
663
|
return load(isDirectory(schemaPath) ? (0, node_path.join)(schemaPath, "index.ts") : schemaPath);
|
|
305
664
|
}
|
|
306
665
|
function defaultLoader$1(cwd) {
|
|
307
|
-
const jiti$
|
|
308
|
-
return (specifier) => jiti$
|
|
666
|
+
const jiti$2 = (0, jiti.createJiti)((0, node_url.pathToFileURL)((0, node_path.join)(cwd, "clivly.config.ts")).href);
|
|
667
|
+
return (specifier) => jiti$2.import(specifier);
|
|
309
668
|
}
|
|
310
669
|
async function loadPgCoreRuntime(load) {
|
|
311
670
|
try {
|
|
@@ -366,7 +725,7 @@ async function discoverSchema(opts) {
|
|
|
366
725
|
const mod = await loadSchemaModule(opts.schemaPath, load);
|
|
367
726
|
const drizzle = await load("drizzle-orm");
|
|
368
727
|
const pgCore = await loadPgCoreRuntime(load);
|
|
369
|
-
const importPath = toImportPath(opts.schemaPath, opts.cwd);
|
|
728
|
+
const importPath = toImportPath(opts.schemaPath, opts.cwd, opts.graph);
|
|
370
729
|
const tables = [];
|
|
371
730
|
let db = null;
|
|
372
731
|
for (const [moduleKey, value] of Object.entries(mod)) {
|
|
@@ -388,6 +747,7 @@ async function discoverSchema(opts) {
|
|
|
388
747
|
if (db === null) db = await probeModule({
|
|
389
748
|
candidates: siblingDbCandidates(opts.schemaPath),
|
|
390
749
|
cwd: opts.cwd,
|
|
750
|
+
graph: opts.graph,
|
|
391
751
|
load,
|
|
392
752
|
match: isDrizzleDb
|
|
393
753
|
});
|
|
@@ -540,7 +900,7 @@ function isEntrypoint(argv1, moduleUrl, realpath = node_fs.realpathSync) {
|
|
|
540
900
|
//#endregion
|
|
541
901
|
//#region src/load-env.ts
|
|
542
902
|
const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
543
|
-
const NEWLINE = /\r?\n/;
|
|
903
|
+
const NEWLINE$1 = /\r?\n/;
|
|
544
904
|
const ESCAPED_NEWLINE = /\\n/g;
|
|
545
905
|
const ESCAPED_CARRIAGE = /\\r/g;
|
|
546
906
|
const TRAILING_COMMENT = /\s#/;
|
|
@@ -552,7 +912,7 @@ const TRAILING_COMMENT = /\s#/;
|
|
|
552
912
|
*/
|
|
553
913
|
function parseEnv(content) {
|
|
554
914
|
const out = {};
|
|
555
|
-
for (const rawLine of content.split(NEWLINE)) {
|
|
915
|
+
for (const rawLine of content.split(NEWLINE$1)) {
|
|
556
916
|
const line = rawLine.trimStart();
|
|
557
917
|
if (line === "" || line.startsWith("#")) continue;
|
|
558
918
|
const match = LINE.exec(line);
|
|
@@ -577,15 +937,26 @@ function parseValue(raw) {
|
|
|
577
937
|
/**
|
|
578
938
|
* Load `.env` then `.env.local` from `cwd` into `env` (default `process.env`),
|
|
579
939
|
* WITHOUT overriding anything already set. Precedence, strongest first:
|
|
580
|
-
* real environment (shell) > `.env.local` > `.env`.
|
|
581
|
-
*
|
|
582
|
-
* `CLIVLY_API_URL` at a staging host from the shell still wins
|
|
940
|
+
* real environment (shell) > explicit `options.files` > `.env.local` > `.env`.
|
|
941
|
+
* The conventional part mirrors what developers already expect (Next.js, Vite),
|
|
942
|
+
* so pointing `CLIVLY_API_URL` at a staging host from the shell still wins.
|
|
943
|
+
*
|
|
944
|
+
* Explicit files exist for split workspaces, where the app that holds
|
|
945
|
+
* `clivly.config.ts` is not the package that holds the credentials. Reading
|
|
946
|
+
* them here — in the CLI process — is what keeps that need out of
|
|
947
|
+
* `clivly.config.ts`, which the app imports and which therefore has to survive
|
|
948
|
+
* an edge runtime with no filesystem.
|
|
583
949
|
*/
|
|
584
|
-
function loadEnv(cwd, env = process.env) {
|
|
950
|
+
function loadEnv(cwd, env = process.env, options = {}) {
|
|
585
951
|
const files = [];
|
|
586
952
|
const merged = {};
|
|
587
|
-
|
|
588
|
-
|
|
953
|
+
const candidates = [
|
|
954
|
+
".env",
|
|
955
|
+
".env.local",
|
|
956
|
+
...options.files ?? []
|
|
957
|
+
];
|
|
958
|
+
for (const name of candidates) {
|
|
959
|
+
const path = (0, node_path.resolve)(cwd, name);
|
|
589
960
|
if (!(0, node_fs.existsSync)(path)) continue;
|
|
590
961
|
files.push(name);
|
|
591
962
|
try {
|
|
@@ -603,6 +974,15 @@ function loadEnv(cwd, env = process.env) {
|
|
|
603
974
|
};
|
|
604
975
|
}
|
|
605
976
|
//#endregion
|
|
977
|
+
//#region src/secret-key-env.ts
|
|
978
|
+
const SECRET_KEY_ENV = "CLIVLY_SECRET_KEY";
|
|
979
|
+
const LEGACY_SECRET_KEY_ENV = "CLIVLY_API_KEY";
|
|
980
|
+
function readSecretKeyEnv(env) {
|
|
981
|
+
const next = env[SECRET_KEY_ENV];
|
|
982
|
+
if (next && next.length > 0) return next;
|
|
983
|
+
return env[LEGACY_SECRET_KEY_ENV];
|
|
984
|
+
}
|
|
985
|
+
//#endregion
|
|
606
986
|
//#region src/login.ts
|
|
607
987
|
const POLL_INTERVAL_MS = 2e3;
|
|
608
988
|
const POLL_BACKOFF_MULTIPLIER = 1.5;
|
|
@@ -614,7 +994,7 @@ const MASK_VISIBLE_CHARS = 4;
|
|
|
614
994
|
const MASK_PREFIX = "••••";
|
|
615
995
|
function maskSecret(value) {
|
|
616
996
|
if (value.length <= MASK_VISIBLE_CHARS) return MASK_PREFIX;
|
|
617
|
-
return `${MASK_PREFIX}${value.slice(-
|
|
997
|
+
return `${MASK_PREFIX}${value.slice(-4)}`;
|
|
618
998
|
}
|
|
619
999
|
async function createPairingSession(apiUrl, deps, rotate) {
|
|
620
1000
|
const res = await deps.fetchImpl(`${apiUrl}/clivly/cli/pair`, {
|
|
@@ -626,7 +1006,7 @@ async function createPairingSession(apiUrl, deps, rotate) {
|
|
|
626
1006
|
})
|
|
627
1007
|
});
|
|
628
1008
|
if (res.status === 404) {
|
|
629
|
-
deps.out("clivly login isn't available yet on this server — CLI pairing is not enabled. Ask whoever runs your Clivly backend to enable it, or set
|
|
1009
|
+
deps.out("clivly login isn't available yet on this server — CLI pairing is not enabled. Ask whoever runs your Clivly backend to enable it, or set CLIVLY_SECRET_KEY / CLIVLY_SYNC_TRIGGER_SECRET manually.");
|
|
630
1010
|
return { exitCode: 1 };
|
|
631
1011
|
}
|
|
632
1012
|
if (!res.ok) {
|
|
@@ -699,9 +1079,9 @@ async function confirmWorkspace(deps, credentials) {
|
|
|
699
1079
|
}
|
|
700
1080
|
function credentialEntries(credentials) {
|
|
701
1081
|
return [{
|
|
702
|
-
key:
|
|
1082
|
+
key: SECRET_KEY_ENV,
|
|
703
1083
|
value: credentials.apiKey,
|
|
704
|
-
comment: "Org
|
|
1084
|
+
comment: "Org secret key — Settings → API keys"
|
|
705
1085
|
}, {
|
|
706
1086
|
key: "CLIVLY_SYNC_TRIGGER_SECRET",
|
|
707
1087
|
value: credentials.triggerSecret,
|
|
@@ -715,7 +1095,7 @@ function writeCredentials(options, credentials) {
|
|
|
715
1095
|
deps.writeEnv(options.cwd, result.contents);
|
|
716
1096
|
deps.out("\nLogin successful.\n");
|
|
717
1097
|
const valuesByKey = {
|
|
718
|
-
|
|
1098
|
+
[SECRET_KEY_ENV]: credentials.apiKey,
|
|
719
1099
|
CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
|
|
720
1100
|
};
|
|
721
1101
|
for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
|
|
@@ -756,7 +1136,7 @@ function toScoredTable(table) {
|
|
|
756
1136
|
function rankCandidates$1(tables, concept) {
|
|
757
1137
|
return tables.map((t) => ({
|
|
758
1138
|
t,
|
|
759
|
-
m:
|
|
1139
|
+
m: require_internal_entity_heuristics.matchesConcept(t.tableName, concept)
|
|
760
1140
|
})).filter((c) => Boolean(c.m)).sort((a, b) => Number(b.m.exact) - Number(a.m.exact)).map((c) => c.t);
|
|
761
1141
|
}
|
|
762
1142
|
function selectEntities(tables) {
|
|
@@ -775,7 +1155,7 @@ function selectEntities(tables) {
|
|
|
775
1155
|
}
|
|
776
1156
|
function chooseSyncFields(table, todos) {
|
|
777
1157
|
const columnKeys = table.columns.map((c) => c.key);
|
|
778
|
-
let cursorField =
|
|
1158
|
+
let cursorField = require_internal_entity_heuristics.suggestCursorField(columnKeys);
|
|
779
1159
|
if (!cursorField) {
|
|
780
1160
|
cursorField = table.columns.find((c) => c.isPrimary)?.key ?? columnKeys[0] ?? "id";
|
|
781
1161
|
todos.push(`No updated_at-like column on "${table.tableName}" — using "${cursorField}" as the sync cursor. Confirm it is monotonic.`);
|
|
@@ -794,10 +1174,10 @@ function chooseSyncFields(table, todos) {
|
|
|
794
1174
|
}
|
|
795
1175
|
function buildEntity(table, concept, todos) {
|
|
796
1176
|
const canonical = require_entity_config.CANONICAL_FIELDS[concept];
|
|
797
|
-
const suggested =
|
|
1177
|
+
const suggested = require_internal_entity_heuristics.toFieldMap(require_internal_entity_heuristics.scoreFieldMap(concept, toScoredTable(table)));
|
|
798
1178
|
const fields = {};
|
|
799
1179
|
for (const [key, column] of Object.entries(suggested)) if (canonical.includes(key)) fields[key] = column;
|
|
800
|
-
for (const required of
|
|
1180
|
+
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.`);
|
|
801
1181
|
const { cursorField, idField } = chooseSyncFields(table, todos);
|
|
802
1182
|
return {
|
|
803
1183
|
moduleKey: table.moduleKey,
|
|
@@ -808,42 +1188,52 @@ function buildEntity(table, concept, todos) {
|
|
|
808
1188
|
idField
|
|
809
1189
|
};
|
|
810
1190
|
}
|
|
811
|
-
function placeholderContact(todos) {
|
|
812
|
-
todos.push("No contact table detected. Filled in a placeholder — set `source`, `fields`, and the fromDrizzle table below, then run `clivly doctor`.");
|
|
813
|
-
return {
|
|
814
|
-
moduleKey: "contacts",
|
|
815
|
-
tableName: "contacts",
|
|
816
|
-
concept: "contact",
|
|
817
|
-
fields: {
|
|
818
|
-
name: "name",
|
|
819
|
-
email: "email"
|
|
820
|
-
},
|
|
821
|
-
cursorField: "updatedAt",
|
|
822
|
-
idField: null
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
1191
|
function hasMappedFields(entity) {
|
|
826
1192
|
return Object.keys(entity.fields).length > 0;
|
|
827
1193
|
}
|
|
828
|
-
|
|
1194
|
+
/** A table by its SQL name or its schema-module export name — either is what a developer would type. */
|
|
1195
|
+
function findTable(tables, name) {
|
|
1196
|
+
return tables.find((table) => table.tableName === name || table.moduleKey === name) ?? null;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Which table serves each concept: an explicit override when given, else the
|
|
1200
|
+
* heuristic pick.
|
|
1201
|
+
*
|
|
1202
|
+
* Split out of `planInit` because resolving two independent overrides, each with
|
|
1203
|
+
* its own unknown-name path, is most of the branching — and keeps `planInit`
|
|
1204
|
+
* inside the cognitive-complexity budget.
|
|
1205
|
+
*/
|
|
1206
|
+
function selectTables(tables, overrides, todos) {
|
|
1207
|
+
const heuristic = selectEntities(tables);
|
|
1208
|
+
const pick = (flag, fallback, label) => {
|
|
1209
|
+
if (!flag) return fallback;
|
|
1210
|
+
const found = findTable(tables, flag);
|
|
1211
|
+
if (!found) todos.push(`--${label}-table "${flag}" is not a table in this schema.`);
|
|
1212
|
+
return found;
|
|
1213
|
+
};
|
|
1214
|
+
const contact = pick(overrides.contactTable, heuristic.contact, "contact");
|
|
1215
|
+
const company = pick(overrides.companyTable, heuristic.company, "company");
|
|
1216
|
+
return {
|
|
1217
|
+
contact,
|
|
1218
|
+
company: company?.tableName === contact?.tableName ? null : company
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function planInit(schema, overrides = {}) {
|
|
829
1222
|
const todos = [];
|
|
830
|
-
const { contact: contactTable, company: companyTable } =
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
if (!
|
|
834
|
-
contact = placeholderContact(todos);
|
|
835
|
-
contactDetected = false;
|
|
836
|
-
}
|
|
1223
|
+
const { contact: contactTable, company: companyTable } = selectTables(schema.tables, overrides, todos);
|
|
1224
|
+
const built = contactTable ? buildEntity(contactTable, "contact", todos) : null;
|
|
1225
|
+
const contact = built && hasMappedFields(built) ? built : null;
|
|
1226
|
+
if (built && !contact) todos.push(`"${built.tableName}" was chosen as the contact entity but none of its columns could be mapped to name/email.`);
|
|
837
1227
|
let company = null;
|
|
838
1228
|
if (companyTable) {
|
|
839
1229
|
const candidate = buildEntity(companyTable, "company", todos);
|
|
840
1230
|
if (hasMappedFields(candidate)) {
|
|
841
1231
|
company = candidate;
|
|
842
|
-
if (
|
|
1232
|
+
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.`);
|
|
843
1233
|
} 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.`);
|
|
844
1234
|
}
|
|
845
|
-
if (contactTable) {
|
|
846
|
-
const scored =
|
|
1235
|
+
if (contactTable && contact) {
|
|
1236
|
+
const scored = require_internal_entity_heuristics.scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
|
|
847
1237
|
const top = scored[0];
|
|
848
1238
|
const runnerUp = scored[1];
|
|
849
1239
|
const isClearWinner = top !== void 0 && (runnerUp === void 0 || top.score - runnerUp.score >= 15);
|
|
@@ -857,16 +1247,16 @@ function planInit(schema) {
|
|
|
857
1247
|
db: schema.db,
|
|
858
1248
|
contact,
|
|
859
1249
|
company,
|
|
860
|
-
|
|
861
|
-
|
|
1250
|
+
tableNames: schema.tables.map((table) => table.tableName),
|
|
1251
|
+
todos
|
|
862
1252
|
};
|
|
863
1253
|
}
|
|
864
1254
|
//#endregion
|
|
865
1255
|
//#region src/render-config.ts
|
|
866
1256
|
const CLIVLY_ENV_KEYS = [
|
|
867
1257
|
{
|
|
868
|
-
key: "
|
|
869
|
-
comment: "Org
|
|
1258
|
+
key: "CLIVLY_SECRET_KEY",
|
|
1259
|
+
comment: "Org secret key — Settings → API keys"
|
|
870
1260
|
},
|
|
871
1261
|
{
|
|
872
1262
|
key: "CLIVLY_SYNC_TRIGGER_URL",
|
|
@@ -875,10 +1265,6 @@ const CLIVLY_ENV_KEYS = [
|
|
|
875
1265
|
{
|
|
876
1266
|
key: "CLIVLY_SYNC_TRIGGER_SECRET",
|
|
877
1267
|
comment: "Signing secret — Integrations → Remote sync"
|
|
878
|
-
},
|
|
879
|
-
{
|
|
880
|
-
key: "CLIVLY_VERIFY_TOKEN",
|
|
881
|
-
comment: "Bearer token Clivly presents to /clivly/auth/verify"
|
|
882
1268
|
}
|
|
883
1269
|
];
|
|
884
1270
|
function renderFields(fields) {
|
|
@@ -913,10 +1299,9 @@ function renderDbExpr(plan) {
|
|
|
913
1299
|
}
|
|
914
1300
|
function renderImports(plan) {
|
|
915
1301
|
const lines = [
|
|
916
|
-
"import { createClivlySDK } from \"clivly/sdk\";",
|
|
1302
|
+
"import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
|
|
917
1303
|
"import { fromDrizzle } from \"clivly/drizzle\";",
|
|
918
|
-
"import {
|
|
919
|
-
"import { discoverFromDrizzle } from \"clivly/core/drizzle\";",
|
|
1304
|
+
"import { discoverFromDrizzle } from \"clivly/drizzle\";",
|
|
920
1305
|
`import * as schema from "${plan.importPath}";`
|
|
921
1306
|
];
|
|
922
1307
|
if (!plan.db) lines.push(`import { db } from "${fallbackDbImportPath(plan.importPath)}"; // TODO: point at your Drizzle instance`);
|
|
@@ -942,7 +1327,7 @@ ${entities.join(",\n")},
|
|
|
942
1327
|
});
|
|
943
1328
|
|
|
944
1329
|
export default createClivlySDK({
|
|
945
|
-
apiKey: process.env.CLIVLY_API_KEY!,
|
|
1330
|
+
apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY!,
|
|
946
1331
|
entities,
|
|
947
1332
|
// Every table in your Drizzle schema, so the Clivly dashboard can offer them
|
|
948
1333
|
// all when you add a mapping — not just the ones wired up as sources below.
|
|
@@ -955,7 +1340,8 @@ ${sources.join(",\n")},
|
|
|
955
1340
|
url: process.env.CLIVLY_SYNC_TRIGGER_URL,
|
|
956
1341
|
secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
|
|
957
1342
|
},
|
|
958
|
-
verifyToken
|
|
1343
|
+
// verifyToken is optional and only needed once a Clivly Cloud consumer calls
|
|
1344
|
+
// your /clivly/auth/verify endpoint; leave it unset otherwise.
|
|
959
1345
|
});
|
|
960
1346
|
`;
|
|
961
1347
|
}
|
|
@@ -1041,23 +1427,28 @@ const TS_EXTENSION = /\.ts$/;
|
|
|
1041
1427
|
const ROUTE_PATHS = {
|
|
1042
1428
|
nextjs: {
|
|
1043
1429
|
tick: "app/api/clivly/tick/route.ts",
|
|
1044
|
-
verify: "app/api/clivly/auth/verify/route.ts"
|
|
1430
|
+
verify: "app/api/clivly/auth/verify/route.ts",
|
|
1431
|
+
session: "app/api/clivly/widget/session/route.ts"
|
|
1045
1432
|
},
|
|
1046
1433
|
"tanstack-start": {
|
|
1047
1434
|
tick: "src/routes/api/clivly/tick.ts",
|
|
1048
|
-
verify: "src/routes/api/clivly/auth/verify.ts"
|
|
1435
|
+
verify: "src/routes/api/clivly/auth/verify.ts",
|
|
1436
|
+
session: "src/routes/api/clivly/widget/session.ts"
|
|
1049
1437
|
},
|
|
1050
1438
|
sveltekit: {
|
|
1051
1439
|
tick: "src/routes/api/clivly/tick/+server.ts",
|
|
1052
|
-
verify: "src/routes/api/clivly/auth/verify/+server.ts"
|
|
1440
|
+
verify: "src/routes/api/clivly/auth/verify/+server.ts",
|
|
1441
|
+
session: "src/routes/api/clivly/widget/session/+server.ts"
|
|
1053
1442
|
},
|
|
1054
1443
|
remix: {
|
|
1055
1444
|
tick: "app/routes/api.clivly.tick.ts",
|
|
1056
|
-
verify: "app/routes/api.clivly.auth.verify.ts"
|
|
1445
|
+
verify: "app/routes/api.clivly.auth.verify.ts",
|
|
1446
|
+
session: "app/routes/api.clivly.widget.session.ts"
|
|
1057
1447
|
},
|
|
1058
1448
|
nuxt: {
|
|
1059
1449
|
tick: "server/api/clivly/tick.ts",
|
|
1060
|
-
verify: "server/api/clivly/auth/verify.ts"
|
|
1450
|
+
verify: "server/api/clivly/auth/verify.ts",
|
|
1451
|
+
session: "server/api/clivly/widget/session.ts"
|
|
1061
1452
|
},
|
|
1062
1453
|
unknown: null
|
|
1063
1454
|
};
|
|
@@ -1087,14 +1478,27 @@ function renderAuthPreamble(routeFilePath, auth) {
|
|
|
1087
1478
|
lines.push("", auth.body);
|
|
1088
1479
|
return lines.join("\n");
|
|
1089
1480
|
}
|
|
1090
|
-
function
|
|
1091
|
-
|
|
1481
|
+
function renderPreamble(kind, importPath, auth, routeFilePath) {
|
|
1482
|
+
if (kind === "session") return `import { createChatSessionHandler } from "clivly/sdk";
|
|
1483
|
+
|
|
1484
|
+
const handler = createChatSessionHandler({
|
|
1485
|
+
apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY ?? "",
|
|
1486
|
+
allowedOrigins: [
|
|
1487
|
+
// TODO: add the origins your widget loads from, e.g. "https://example.com"
|
|
1488
|
+
],
|
|
1489
|
+
});
|
|
1490
|
+
`;
|
|
1491
|
+
if (kind === "verify") return `import clivly from "${importPath}";
|
|
1092
1492
|
${renderAuthPreamble(routeFilePath, auth)}
|
|
1093
1493
|
const handler = clivly.createAuthVerifyHandler(resolveUser);
|
|
1094
|
-
|
|
1494
|
+
`;
|
|
1495
|
+
return `import clivly from "${importPath}";
|
|
1095
1496
|
|
|
1096
1497
|
const handler = clivly.createClivlyHandler();
|
|
1097
1498
|
`;
|
|
1499
|
+
}
|
|
1500
|
+
function renderBody(framework, importPath, kind, auth, routeFilePath) {
|
|
1501
|
+
const preamble = renderPreamble(kind, importPath, auth, routeFilePath);
|
|
1098
1502
|
switch (framework) {
|
|
1099
1503
|
case "nextjs": return `${preamble}
|
|
1100
1504
|
export const POST = (request: Request) => handler(request);
|
|
@@ -1126,7 +1530,7 @@ export default defineEventHandler((event) => handler(toWebRequest(event)));
|
|
|
1126
1530
|
}
|
|
1127
1531
|
function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown")) {
|
|
1128
1532
|
const path = routePath(framework, kind);
|
|
1129
|
-
let body = renderBody(framework, configImport(path), kind
|
|
1533
|
+
let body = renderBody(framework, configImport(path), kind, auth, path);
|
|
1130
1534
|
if (framework === "tanstack-start") {
|
|
1131
1535
|
const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
|
|
1132
1536
|
body = body.replace("__ROUTE__", url);
|
|
@@ -1155,11 +1559,21 @@ function planScaffold(framework) {
|
|
|
1155
1559
|
...COMMON_FILES
|
|
1156
1560
|
];
|
|
1157
1561
|
}
|
|
1562
|
+
function planWidgetScaffold(framework) {
|
|
1563
|
+
const paths = ROUTE_PATHS[framework];
|
|
1564
|
+
if (!paths) return [];
|
|
1565
|
+
return [{
|
|
1566
|
+
path: paths.session,
|
|
1567
|
+
description: "Clivly chat-session route (createChatSessionHandler) — add allowedOrigins",
|
|
1568
|
+
owned: true
|
|
1569
|
+
}];
|
|
1570
|
+
}
|
|
1158
1571
|
//#endregion
|
|
1159
1572
|
//#region src/plan-mutations.ts
|
|
1160
|
-
function fileAction(
|
|
1161
|
-
if (
|
|
1162
|
-
|
|
1573
|
+
function fileAction(existing, next, force) {
|
|
1574
|
+
if (existing === null) return "create";
|
|
1575
|
+
if (existing === next) return "unchanged";
|
|
1576
|
+
return force ? "overwrite" : "conflict";
|
|
1163
1577
|
}
|
|
1164
1578
|
function hasKey(content, key) {
|
|
1165
1579
|
return new RegExp(`^\\s*${key}=`, "m").test(content);
|
|
@@ -1171,11 +1585,14 @@ function planFiles(args) {
|
|
|
1171
1585
|
const kind = path.includes("verify") ? "verify" : "tick";
|
|
1172
1586
|
return renderRouteFile(args.framework, kind, args.authWiring);
|
|
1173
1587
|
};
|
|
1174
|
-
for (const file of planScaffold(args.framework))
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1588
|
+
for (const file of planScaffold(args.framework)) {
|
|
1589
|
+
const contents = render(file.path);
|
|
1590
|
+
mutations.push({
|
|
1591
|
+
path: file.path,
|
|
1592
|
+
contents,
|
|
1593
|
+
action: fileAction(args.readFile((0, node_path.join)(args.cwd, file.path)), contents, args.force)
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1179
1596
|
return mutations;
|
|
1180
1597
|
}
|
|
1181
1598
|
function envAction(addedCount, existingLength) {
|
|
@@ -1198,6 +1615,16 @@ function planMutations(args) {
|
|
|
1198
1615
|
env: planEnv(args)
|
|
1199
1616
|
};
|
|
1200
1617
|
}
|
|
1618
|
+
function planWidgetMutations(args) {
|
|
1619
|
+
return planWidgetScaffold(args.framework).map((file) => {
|
|
1620
|
+
const contents = renderRouteFile(args.framework, "session");
|
|
1621
|
+
return {
|
|
1622
|
+
path: file.path,
|
|
1623
|
+
contents,
|
|
1624
|
+
action: fileAction(args.readFile((0, node_path.join)(args.cwd, file.path)), contents, args.force)
|
|
1625
|
+
};
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1201
1628
|
//#endregion
|
|
1202
1629
|
//#region src/probe-auth.ts
|
|
1203
1630
|
const AUTH_MODULE_PATHS = [
|
|
@@ -1217,8 +1644,8 @@ function authInstanceCandidates(cwd) {
|
|
|
1217
1644
|
return AUTH_MODULE_PATHS.map((path) => (0, node_path.join)(cwd, path));
|
|
1218
1645
|
}
|
|
1219
1646
|
function defaultLoader(cwd) {
|
|
1220
|
-
const jiti$
|
|
1221
|
-
return (specifier) => jiti$
|
|
1647
|
+
const jiti$1 = (0, jiti.createJiti)((0, node_url.pathToFileURL)((0, node_path.join)(cwd, "clivly.config.ts")).href);
|
|
1648
|
+
return (specifier) => jiti$1.import(specifier);
|
|
1222
1649
|
}
|
|
1223
1650
|
function probeAuthInstance(cwd, load) {
|
|
1224
1651
|
return probeModule({
|
|
@@ -1229,59 +1656,6 @@ function probeAuthInstance(cwd, load) {
|
|
|
1229
1656
|
});
|
|
1230
1657
|
}
|
|
1231
1658
|
//#endregion
|
|
1232
|
-
//#region src/load-config.ts
|
|
1233
|
-
const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
|
|
1234
|
-
function resolveConfigPath(cwd, configPath) {
|
|
1235
|
-
return (0, node_path.resolve)(cwd, configPath ?? "clivly.config.ts");
|
|
1236
|
-
}
|
|
1237
|
-
/**
|
|
1238
|
-
* Load the config and return its default export, checking it carries the method
|
|
1239
|
-
* the caller needs. The check is by capability rather than by type: the config
|
|
1240
|
-
* is the developer's own SDK instance, and a clear "this isn't an SDK instance"
|
|
1241
|
-
* beats a TypeError deep inside a command.
|
|
1242
|
-
*/
|
|
1243
|
-
/**
|
|
1244
|
-
* The first meaningful line of an error message. jiti reports a failed
|
|
1245
|
-
* resolution with a multi-line "Require stack:" dump naming Clivly's own
|
|
1246
|
-
* internal files — noise that tells the developer nothing about their app and
|
|
1247
|
-
* buries the one line that does.
|
|
1248
|
-
*/
|
|
1249
|
-
function firstLine(message) {
|
|
1250
|
-
return message.split("\n")[0]?.trim() ?? "";
|
|
1251
|
-
}
|
|
1252
|
-
/**
|
|
1253
|
-
* Describe why the config failed to load, in one line the developer can act on.
|
|
1254
|
-
*
|
|
1255
|
-
* A config that fails its own schema validation throws a zod error carrying
|
|
1256
|
-
* structured `issues`, but whose `message` is a multi-line JSON dump — its
|
|
1257
|
-
* first line is a bare "[", so `firstLine` alone reduced the entire diagnosis
|
|
1258
|
-
* to a single bracket. Render the issues as `path: message` instead, which is
|
|
1259
|
-
* what actually names the entity to fix. Anything else keeps the require-stack
|
|
1260
|
-
* trimming `firstLine` exists for.
|
|
1261
|
-
*/
|
|
1262
|
-
function describeLoadError(error) {
|
|
1263
|
-
const issues = error.issues;
|
|
1264
|
-
if (Array.isArray(issues) && issues.length > 0) return issues.map((raw) => {
|
|
1265
|
-
const issue = raw;
|
|
1266
|
-
const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
|
|
1267
|
-
const message = typeof issue.message === "string" ? issue.message : "invalid value";
|
|
1268
|
-
return path ? `${path}: ${message}` : message;
|
|
1269
|
-
}).join("; ");
|
|
1270
|
-
return firstLine(error?.message ?? String(error));
|
|
1271
|
-
}
|
|
1272
|
-
async function loadConfigSdk(path, requiredMethod) {
|
|
1273
|
-
const jiti$1 = (0, jiti.createJiti)(require("url").pathToFileURL(__filename).href);
|
|
1274
|
-
let mod;
|
|
1275
|
-
try {
|
|
1276
|
-
mod = await jiti$1.import(path);
|
|
1277
|
-
} catch (error) {
|
|
1278
|
-
throw new Error(describeLoadError(error));
|
|
1279
|
-
}
|
|
1280
|
-
const sdk = mod.default ?? mod;
|
|
1281
|
-
if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
|
|
1282
|
-
return sdk;
|
|
1283
|
-
}
|
|
1284
|
-
//#endregion
|
|
1285
1659
|
//#region src/push-schema.ts
|
|
1286
1660
|
async function runPushSchema(options) {
|
|
1287
1661
|
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
@@ -1350,11 +1724,40 @@ function defaultReadDrizzleConfigSchema(cwd) {
|
|
|
1350
1724
|
}
|
|
1351
1725
|
return null;
|
|
1352
1726
|
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Resolve a `--schema` value that names a workspace package rather than a file.
|
|
1729
|
+
*
|
|
1730
|
+
* Matches the longest package name that prefixes the specifier, so both
|
|
1731
|
+
* `@acme/database` and `@acme/database/src/schema.ts` work. Returns `undefined`
|
|
1732
|
+
* when nothing matched (caller falls back to path handling) and `null` when a
|
|
1733
|
+
* package matched but its entry could not be resolved — those are different
|
|
1734
|
+
* outcomes and the caller reports them differently.
|
|
1735
|
+
*/
|
|
1736
|
+
function resolveFromWorkspace(specifier, graph, deps) {
|
|
1737
|
+
const pkg = graph.packages.filter((pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)).sort((a, b) => b.name.length - a.name.length)[0];
|
|
1738
|
+
if (!pkg) return;
|
|
1739
|
+
if (specifier === pkg.name) return deps.resolveEntry(pkg.dir);
|
|
1740
|
+
const subpath = specifier.slice(pkg.name.length + 1);
|
|
1741
|
+
const candidate = (0, node_path.resolve)(pkg.dir, subpath);
|
|
1742
|
+
return deps.exists(candidate) ? candidate : deps.resolveEntry(pkg.dir);
|
|
1743
|
+
}
|
|
1353
1744
|
function resolveSchemaPath(opts) {
|
|
1354
1745
|
const exists = opts.deps?.exists ?? node_fs.existsSync;
|
|
1355
1746
|
const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
|
|
1747
|
+
const resolveEntry = opts.deps?.resolvePackageEntry ?? resolvePackageEntry;
|
|
1356
1748
|
const toAbs = (p) => (0, node_path.isAbsolute)(p) ? p : (0, node_path.resolve)(opts.cwd, p);
|
|
1357
|
-
if (opts.flag)
|
|
1749
|
+
if (opts.flag) {
|
|
1750
|
+
const asPath = toAbs(opts.flag);
|
|
1751
|
+
if (exists(asPath)) return asPath;
|
|
1752
|
+
if (opts.graph) {
|
|
1753
|
+
const fromWorkspace = resolveFromWorkspace(opts.flag, opts.graph, {
|
|
1754
|
+
exists,
|
|
1755
|
+
resolveEntry
|
|
1756
|
+
});
|
|
1757
|
+
if (fromWorkspace !== void 0) return fromWorkspace;
|
|
1758
|
+
}
|
|
1759
|
+
return asPath;
|
|
1760
|
+
}
|
|
1358
1761
|
const fromConfig = readSchema(opts.cwd);
|
|
1359
1762
|
if (fromConfig) return toAbs(fromConfig);
|
|
1360
1763
|
for (const candidate of COMMON_PATHS) {
|
|
@@ -1365,17 +1768,8 @@ function resolveSchemaPath(opts) {
|
|
|
1365
1768
|
}
|
|
1366
1769
|
//#endregion
|
|
1367
1770
|
//#region src/resolve-target-app.ts
|
|
1368
|
-
function readPackageJson(dir) {
|
|
1369
|
-
const path = (0, node_path.join)(dir, "package.json");
|
|
1370
|
-
if (!(0, node_fs.existsSync)(path)) return null;
|
|
1371
|
-
try {
|
|
1372
|
-
return JSON.parse((0, node_fs.readFileSync)(path, "utf8"));
|
|
1373
|
-
} catch {
|
|
1374
|
-
return null;
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
1771
|
function packageDeps(dir) {
|
|
1378
|
-
const pkg =
|
|
1772
|
+
const pkg = readPackageManifest(dir);
|
|
1379
1773
|
return {
|
|
1380
1774
|
...pkg?.dependencies,
|
|
1381
1775
|
...pkg?.devDependencies
|
|
@@ -1389,49 +1783,16 @@ function parentDir(dir) {
|
|
|
1389
1783
|
const parent = (0, node_path.dirname)(dir);
|
|
1390
1784
|
return parent === dir ? null : parent;
|
|
1391
1785
|
}
|
|
1392
|
-
function findNearestPackageDir(start) {
|
|
1786
|
+
function findNearestPackageDir(start, boundary) {
|
|
1393
1787
|
let current = (0, node_path.resolve)(start);
|
|
1394
1788
|
while (true) {
|
|
1789
|
+
if (current === boundary) return null;
|
|
1395
1790
|
if ((0, node_fs.existsSync)((0, node_path.join)(current, "package.json"))) return current;
|
|
1396
1791
|
const parent = parentDir(current);
|
|
1397
1792
|
if (!parent) return null;
|
|
1398
1793
|
current = parent;
|
|
1399
1794
|
}
|
|
1400
1795
|
}
|
|
1401
|
-
function readWorkspaceGlobs(dir) {
|
|
1402
|
-
const pkg = readPackageJson(dir);
|
|
1403
|
-
const workspaces = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
|
|
1404
|
-
if (Array.isArray(workspaces) && workspaces.length > 0) return workspaces;
|
|
1405
|
-
const pnpmWorkspacePath = (0, node_path.join)(dir, "pnpm-workspace.yaml");
|
|
1406
|
-
if (!(0, node_fs.existsSync)(pnpmWorkspacePath)) return [];
|
|
1407
|
-
try {
|
|
1408
|
-
return (0, node_fs.readFileSync)(pnpmWorkspacePath, "utf8").split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim().replace(/^['"]|['"]$/g, "")).filter((glob) => glob.length > 0 && !glob.startsWith("!"));
|
|
1409
|
-
} catch {
|
|
1410
|
-
return [];
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
function expandWorkspaceGlob(root, pattern) {
|
|
1414
|
-
if (pattern.endsWith("/*")) {
|
|
1415
|
-
const base = (0, node_path.join)(root, pattern.slice(0, -2));
|
|
1416
|
-
if (!(0, node_fs.existsSync)(base)) return [];
|
|
1417
|
-
try {
|
|
1418
|
-
return (0, node_fs.readdirSync)(base, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, node_path.join)(base, entry.name)).filter((dir) => (0, node_fs.existsSync)((0, node_path.join)(dir, "package.json")));
|
|
1419
|
-
} catch {
|
|
1420
|
-
return [];
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
const exact = (0, node_path.join)(root, pattern);
|
|
1424
|
-
return (0, node_fs.existsSync)((0, node_path.join)(exact, "package.json")) ? [exact] : [];
|
|
1425
|
-
}
|
|
1426
|
-
function findWorkspaceRoot(start) {
|
|
1427
|
-
let current = (0, node_path.resolve)(start);
|
|
1428
|
-
while (true) {
|
|
1429
|
-
if (readWorkspaceGlobs(current).length > 0) return current;
|
|
1430
|
-
const parent = parentDir(current);
|
|
1431
|
-
if (!parent) return null;
|
|
1432
|
-
current = parent;
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
1796
|
function uniquePaths(paths) {
|
|
1436
1797
|
return [...new Set(paths.map((path) => (0, node_path.resolve)(path)))];
|
|
1437
1798
|
}
|
|
@@ -1444,13 +1805,14 @@ function rankCandidates(paths, start) {
|
|
|
1444
1805
|
return distance(a) - distance(b) || a.localeCompare(b);
|
|
1445
1806
|
});
|
|
1446
1807
|
}
|
|
1447
|
-
function resolveTargetApp(start) {
|
|
1448
|
-
const
|
|
1808
|
+
function resolveTargetApp(start, boundary = (0, node_os.homedir)()) {
|
|
1809
|
+
const stop = (0, node_path.resolve)(boundary);
|
|
1810
|
+
const nearestPackageDir = findNearestPackageDir(start, stop);
|
|
1449
1811
|
if (nearestPackageDir && isDrizzleApp(nearestPackageDir)) return {
|
|
1450
1812
|
cwd: nearestPackageDir,
|
|
1451
1813
|
reason: "direct"
|
|
1452
1814
|
};
|
|
1453
|
-
const workspaceRoot = findWorkspaceRoot(start);
|
|
1815
|
+
const workspaceRoot = findWorkspaceRoot(start, stop);
|
|
1454
1816
|
if (!workspaceRoot) return {
|
|
1455
1817
|
cwd: nearestPackageDir ?? (0, node_path.resolve)(start),
|
|
1456
1818
|
reason: "direct"
|
|
@@ -1486,7 +1848,7 @@ function resolveTargetApp(start) {
|
|
|
1486
1848
|
}
|
|
1487
1849
|
//#endregion
|
|
1488
1850
|
//#region src/status-ladder.ts
|
|
1489
|
-
const SATISFYING = new Set(["pass", "warn"]);
|
|
1851
|
+
const SATISFYING = /* @__PURE__ */ new Set(["pass", "warn"]);
|
|
1490
1852
|
function unmetPrerequisites(stage, satisfied) {
|
|
1491
1853
|
return (stage.needs ?? []).filter((id) => !satisfied.has(id));
|
|
1492
1854
|
}
|
|
@@ -1536,10 +1898,118 @@ function ladderExitCode(results) {
|
|
|
1536
1898
|
return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
|
|
1537
1899
|
}
|
|
1538
1900
|
//#endregion
|
|
1901
|
+
//#region src/edge-safety.ts
|
|
1902
|
+
const NODE_BUILTINS = /* @__PURE__ */ new Set([
|
|
1903
|
+
"assert",
|
|
1904
|
+
"buffer",
|
|
1905
|
+
"child_process",
|
|
1906
|
+
"cluster",
|
|
1907
|
+
"crypto",
|
|
1908
|
+
"dns",
|
|
1909
|
+
"events",
|
|
1910
|
+
"fs",
|
|
1911
|
+
"http",
|
|
1912
|
+
"http2",
|
|
1913
|
+
"https",
|
|
1914
|
+
"module",
|
|
1915
|
+
"net",
|
|
1916
|
+
"os",
|
|
1917
|
+
"path",
|
|
1918
|
+
"perf_hooks",
|
|
1919
|
+
"process",
|
|
1920
|
+
"readline",
|
|
1921
|
+
"stream",
|
|
1922
|
+
"timers",
|
|
1923
|
+
"tls",
|
|
1924
|
+
"url",
|
|
1925
|
+
"util",
|
|
1926
|
+
"v8",
|
|
1927
|
+
"vm",
|
|
1928
|
+
"worker_threads",
|
|
1929
|
+
"zlib"
|
|
1930
|
+
]);
|
|
1931
|
+
const SIDE_EFFECT_MODULES = [/^dotenv\/config$/, /^dotenv\/lib\/env-options$/];
|
|
1932
|
+
const IMPORT_FROM = /(?:^|\s)(?:import|export)\b[^"']*from\s*["']([^"']+)["']/;
|
|
1933
|
+
const BARE_IMPORT = /^\s*import\s*["']([^"']+)["']\s*;?\s*$/;
|
|
1934
|
+
const REQUIRE_CALL = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/;
|
|
1935
|
+
const NEWLINE = /\r?\n/;
|
|
1936
|
+
const MODULE_SCOPE_CALL = /^([A-Za-z_$][\w$.]*)\s*\(/;
|
|
1937
|
+
const ALLOWED_CALLS = /* @__PURE__ */ new Set([
|
|
1938
|
+
"export",
|
|
1939
|
+
"return",
|
|
1940
|
+
"if",
|
|
1941
|
+
"for",
|
|
1942
|
+
"while"
|
|
1943
|
+
]);
|
|
1944
|
+
const SIDE_EFFECT_CALLS = /^(dotenv\.config|config|configDotenv|fileURLToPath|readFileSync|require|process\.chdir)$/;
|
|
1945
|
+
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.";
|
|
1946
|
+
function isNodeBuiltin(specifier) {
|
|
1947
|
+
if (specifier.startsWith("node:")) return true;
|
|
1948
|
+
return NODE_BUILTINS.has(specifier.split("/")[0] ?? "");
|
|
1949
|
+
}
|
|
1950
|
+
function stripComment(line) {
|
|
1951
|
+
const trimmed = line.trimStart();
|
|
1952
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) return "";
|
|
1953
|
+
return line;
|
|
1954
|
+
}
|
|
1955
|
+
function specifierIssue(specifier, line) {
|
|
1956
|
+
if (SIDE_EFFECT_MODULES.some((pattern) => pattern.test(specifier))) return {
|
|
1957
|
+
code: "module_scope_side_effect",
|
|
1958
|
+
detail: `imports "${specifier}" for its side effect`,
|
|
1959
|
+
hint: ENV_HINT,
|
|
1960
|
+
line
|
|
1961
|
+
};
|
|
1962
|
+
if (isNodeBuiltin(specifier)) return {
|
|
1963
|
+
code: "node_builtin_in_edge_config",
|
|
1964
|
+
detail: `imports "${specifier}", which does not exist on an edge runtime`,
|
|
1965
|
+
hint: ENV_HINT,
|
|
1966
|
+
line
|
|
1967
|
+
};
|
|
1968
|
+
return null;
|
|
1969
|
+
}
|
|
1970
|
+
function callIssue(source, line) {
|
|
1971
|
+
const name = MODULE_SCOPE_CALL.exec(source)?.[1];
|
|
1972
|
+
if (!name || ALLOWED_CALLS.has(name) || !SIDE_EFFECT_CALLS.test(name)) return null;
|
|
1973
|
+
return {
|
|
1974
|
+
code: "module_scope_side_effect",
|
|
1975
|
+
detail: `calls ${name}() at module scope`,
|
|
1976
|
+
hint: ENV_HINT,
|
|
1977
|
+
line
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Every reason `source` would fail to import on an edge runtime, in line order.
|
|
1982
|
+
* An empty array means the config is safe to import from the app.
|
|
1983
|
+
*/
|
|
1984
|
+
function analyzeEdgeSafety(source) {
|
|
1985
|
+
const issues = [];
|
|
1986
|
+
source.split(NEWLINE).forEach((rawLine, index) => {
|
|
1987
|
+
const line = stripComment(rawLine);
|
|
1988
|
+
if (line.trim() === "") return;
|
|
1989
|
+
const lineNumber = index + 1;
|
|
1990
|
+
const specifier = BARE_IMPORT.exec(line)?.[1] ?? IMPORT_FROM.exec(line)?.[1] ?? REQUIRE_CALL.exec(line)?.[1];
|
|
1991
|
+
if (specifier) {
|
|
1992
|
+
const issue = specifierIssue(specifier, lineNumber);
|
|
1993
|
+
if (issue) issues.push(issue);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const call = callIssue(line, lineNumber);
|
|
1997
|
+
if (call) issues.push(call);
|
|
1998
|
+
});
|
|
1999
|
+
return issues;
|
|
2000
|
+
}
|
|
2001
|
+
//#endregion
|
|
1539
2002
|
//#region src/status-stages.ts
|
|
2003
|
+
function defaultReadConfigSource(path) {
|
|
2004
|
+
try {
|
|
2005
|
+
return (0, node_fs.readFileSync)(path, "utf8");
|
|
2006
|
+
} catch {
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
1540
2010
|
const REASON_HINT = {
|
|
1541
2011
|
ok: "",
|
|
1542
|
-
invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
|
|
2012
|
+
invalid_api_key: "The key was rejected (401). Check CLIVLY_SECRET_KEY (or legacy CLIVLY_API_KEY).",
|
|
1543
2013
|
forbidden: "The key is valid but lacks access to this org (403).",
|
|
1544
2014
|
rate_limited: "Rate limited (429). Wait a moment and retry.",
|
|
1545
2015
|
server_error: "Clivly returned a server error (5xx). Retry shortly.",
|
|
@@ -1552,12 +2022,12 @@ const apiKeyStage = {
|
|
|
1552
2022
|
run(ctx) {
|
|
1553
2023
|
if (ctx.apiKey) return Promise.resolve({
|
|
1554
2024
|
state: "pass",
|
|
1555
|
-
detail: "
|
|
2025
|
+
detail: "Secret key is set."
|
|
1556
2026
|
});
|
|
1557
2027
|
return Promise.resolve({
|
|
1558
2028
|
state: "fail",
|
|
1559
|
-
detail: "
|
|
1560
|
-
hint: "Copy your
|
|
2029
|
+
detail: "Secret key is not set.",
|
|
2030
|
+
hint: "Copy your secret key from Settings → API keys into CLIVLY_SECRET_KEY in .env."
|
|
1561
2031
|
});
|
|
1562
2032
|
}
|
|
1563
2033
|
};
|
|
@@ -1598,12 +2068,38 @@ const configStage = {
|
|
|
1598
2068
|
}
|
|
1599
2069
|
}
|
|
1600
2070
|
};
|
|
1601
|
-
const LADDER_OWNED_CHECKS = new Set(["API key present", "Heartbeat"]);
|
|
2071
|
+
const LADDER_OWNED_CHECKS = /* @__PURE__ */ new Set(["API key present", "Heartbeat"]);
|
|
1602
2072
|
/** The full ladder, in the order a developer completes setup. */
|
|
1603
2073
|
const STATUS_STAGES = [
|
|
1604
2074
|
apiKeyStage,
|
|
1605
2075
|
heartbeatStage,
|
|
1606
2076
|
configStage,
|
|
2077
|
+
{
|
|
2078
|
+
id: "runtime",
|
|
2079
|
+
label: "clivly.config is edge-safe",
|
|
2080
|
+
run(ctx) {
|
|
2081
|
+
const source = (ctx.readConfigSource ?? defaultReadConfigSource)(ctx.configPath);
|
|
2082
|
+
if (source === null) return Promise.resolve({
|
|
2083
|
+
state: "skip",
|
|
2084
|
+
detail: "config not readable"
|
|
2085
|
+
});
|
|
2086
|
+
const issues = analyzeEdgeSafety(source);
|
|
2087
|
+
if (issues.length === 0) return Promise.resolve({
|
|
2088
|
+
state: "pass",
|
|
2089
|
+
detail: "no Node-only code at module scope"
|
|
2090
|
+
});
|
|
2091
|
+
return Promise.resolve({
|
|
2092
|
+
state: "fail",
|
|
2093
|
+
detail: issues.map((issue) => issue.detail).join("; "),
|
|
2094
|
+
hint: issues[0]?.hint,
|
|
2095
|
+
sub: issues.map((issue) => ({
|
|
2096
|
+
label: `${issue.code} (line ${issue.line})`,
|
|
2097
|
+
ok: false,
|
|
2098
|
+
detail: issue.detail
|
|
2099
|
+
}))
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
},
|
|
1607
2103
|
{
|
|
1608
2104
|
id: "setup-checks",
|
|
1609
2105
|
label: "Setup checks",
|
|
@@ -1651,7 +2147,11 @@ function defaultLoadSdk(path) {
|
|
|
1651
2147
|
const ALIAS_STAGE_IDS = {
|
|
1652
2148
|
status: STATUS_STAGES.map((stage) => stage.id),
|
|
1653
2149
|
ping: ["api-key", "heartbeat"],
|
|
1654
|
-
doctor: [
|
|
2150
|
+
doctor: [
|
|
2151
|
+
"config",
|
|
2152
|
+
"runtime",
|
|
2153
|
+
"setup-checks"
|
|
2154
|
+
],
|
|
1655
2155
|
connect: [
|
|
1656
2156
|
"api-key",
|
|
1657
2157
|
"heartbeat",
|
|
@@ -1710,6 +2210,48 @@ function flagValue(argv, flag) {
|
|
|
1710
2210
|
const index = argv.indexOf(flag);
|
|
1711
2211
|
return index >= 0 ? argv[index + 1] : void 0;
|
|
1712
2212
|
}
|
|
2213
|
+
function flagValues(argv, flag) {
|
|
2214
|
+
const values = [];
|
|
2215
|
+
for (const [index, entry] of argv.entries()) {
|
|
2216
|
+
const value = entry === flag ? argv[index + 1] : void 0;
|
|
2217
|
+
if (value !== void 0 && !value.startsWith("--")) values.push(value);
|
|
2218
|
+
}
|
|
2219
|
+
return values;
|
|
2220
|
+
}
|
|
2221
|
+
const KNOWN_COMMANDS = [
|
|
2222
|
+
"init",
|
|
2223
|
+
"status",
|
|
2224
|
+
"push-schema",
|
|
2225
|
+
"dev",
|
|
2226
|
+
"login",
|
|
2227
|
+
"help"
|
|
2228
|
+
];
|
|
2229
|
+
function editDistance(a, b) {
|
|
2230
|
+
const m = a.length;
|
|
2231
|
+
const n = b.length;
|
|
2232
|
+
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
2233
|
+
let curr = Array.from({ length: n + 1 }, () => 0);
|
|
2234
|
+
for (let i = 1; i <= m; i++) {
|
|
2235
|
+
curr[0] = i;
|
|
2236
|
+
for (let j = 1; j <= n; j++) {
|
|
2237
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2238
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
2239
|
+
}
|
|
2240
|
+
[prev, curr] = [curr, prev];
|
|
2241
|
+
}
|
|
2242
|
+
return prev[n] ?? 0;
|
|
2243
|
+
}
|
|
2244
|
+
function nearestCommand(input) {
|
|
2245
|
+
let best;
|
|
2246
|
+
for (const command of KNOWN_COMMANDS) {
|
|
2247
|
+
const distance = editDistance(input, command);
|
|
2248
|
+
if (distance <= 2 && (!best || distance < best.distance)) best = {
|
|
2249
|
+
command,
|
|
2250
|
+
distance
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
return best?.command;
|
|
2254
|
+
}
|
|
1713
2255
|
function readDeps(cwd) {
|
|
1714
2256
|
const pkgPath = (0, node_path.join)(cwd, "package.json");
|
|
1715
2257
|
if (!(0, node_fs.existsSync)(pkgPath)) return {};
|
|
@@ -1723,6 +2265,59 @@ function readDeps(cwd) {
|
|
|
1723
2265
|
return {};
|
|
1724
2266
|
}
|
|
1725
2267
|
}
|
|
2268
|
+
const ADD_USAGE = "Usage: clivly add widget [--force] [--dry-run]";
|
|
2269
|
+
const defaultAddDeps = {
|
|
2270
|
+
out: (line) => process.stdout.write(line),
|
|
2271
|
+
readFile: (path) => (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : null,
|
|
2272
|
+
writeFile: (path, contents) => {
|
|
2273
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
2274
|
+
(0, node_fs.writeFileSync)(path, contents, "utf8");
|
|
2275
|
+
},
|
|
2276
|
+
readDeps
|
|
2277
|
+
};
|
|
2278
|
+
function runAdd(argv, cwd, deps = defaultAddDeps) {
|
|
2279
|
+
const target = argv[0];
|
|
2280
|
+
if (target !== "widget") {
|
|
2281
|
+
deps.out(`${target ? `Unknown target "${target}". ` : "Missing target. "}Only \`widget\` is supported.\n${ADD_USAGE}\n`);
|
|
2282
|
+
return 1;
|
|
2283
|
+
}
|
|
2284
|
+
const framework = detectFramework(deps.readDeps(cwd));
|
|
2285
|
+
if (framework === "unknown") {
|
|
2286
|
+
deps.out("Couldn't detect your framework from package.json. `clivly add widget` supports Next.js, TanStack Start, SvelteKit, Remix, and Nuxt.\n");
|
|
2287
|
+
return 1;
|
|
2288
|
+
}
|
|
2289
|
+
const force = argv.includes("--force");
|
|
2290
|
+
const dryRun = argv.includes("--dry-run");
|
|
2291
|
+
const files = planWidgetMutations({
|
|
2292
|
+
framework,
|
|
2293
|
+
cwd,
|
|
2294
|
+
force,
|
|
2295
|
+
readFile: (path) => deps.readFile(path)
|
|
2296
|
+
});
|
|
2297
|
+
deps.out(`\nClivly add widget — ${FRAMEWORK_LABELS[framework]}\n\n`);
|
|
2298
|
+
if (dryRun) deps.out("Dry run — nothing was written.\n\n");
|
|
2299
|
+
for (const file of files) {
|
|
2300
|
+
if (dryRun) {
|
|
2301
|
+
deps.out(` ${file.action.padEnd(9)} ${file.path}\n`);
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
if (file.action === "conflict") {
|
|
2305
|
+
deps.out(` ! ${file.path} differs from what Clivly would write — left alone (--force to overwrite)\n`);
|
|
2306
|
+
continue;
|
|
2307
|
+
}
|
|
2308
|
+
if (file.action === "unchanged") {
|
|
2309
|
+
deps.out(` unchanged ${file.path} (already up to date)\n`);
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
deps.writeFile((0, node_path.join)(cwd, file.path), file.contents);
|
|
2313
|
+
deps.out(` ${file.action.padEnd(9)} ${file.path}\n`);
|
|
2314
|
+
}
|
|
2315
|
+
if (!dryRun) {
|
|
2316
|
+
const sessionPath = files[0]?.path ?? "";
|
|
2317
|
+
deps.out(`\nNext steps:\n 1. Add your site origins to allowedOrigins in ${sessionPath}\n 2. Create the widget + set allowed origins in the Clivly dashboard (Settings → Chat widget)\n 3. Embed it: <ClivlyChatWidget widgetId="…" />\n`);
|
|
2318
|
+
}
|
|
2319
|
+
return 0;
|
|
2320
|
+
}
|
|
1726
2321
|
function readOwnVersion() {
|
|
1727
2322
|
const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
1728
2323
|
for (const rel of ["./package.json", "../package.json"]) try {
|
|
@@ -1730,6 +2325,35 @@ function readOwnVersion() {
|
|
|
1730
2325
|
if (pkg.name === "clivly" && pkg.version) return pkg.version;
|
|
1731
2326
|
} catch {}
|
|
1732
2327
|
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Parse a `--db-import` / `--auth-import` value into a discovery-shaped override.
|
|
2330
|
+
*
|
|
2331
|
+
* `@acme/database` -> importPath "@acme/database", moduleKey <fallback>
|
|
2332
|
+
* `@acme/database#database` -> importPath "@acme/database", moduleKey "database"
|
|
2333
|
+
*
|
|
2334
|
+
* The optional `#exportName` suffix exists because a specifier alone doesn't say
|
|
2335
|
+
* which export holds the instance. Guessing wrong emits an import of a name that
|
|
2336
|
+
* isn't there, so the flag lets the developer say it outright while keeping the
|
|
2337
|
+
* conventional name as the default.
|
|
2338
|
+
*/
|
|
2339
|
+
function importOverride(flag, fallbackModuleKey) {
|
|
2340
|
+
const trimmed = flag?.trim();
|
|
2341
|
+
if (!trimmed) return null;
|
|
2342
|
+
const separator = trimmed.lastIndexOf("#");
|
|
2343
|
+
const importPath = separator === -1 ? trimmed : trimmed.slice(0, separator);
|
|
2344
|
+
const moduleKey = separator === -1 ? "" : trimmed.slice(separator + 1);
|
|
2345
|
+
if (importPath.length === 0) return null;
|
|
2346
|
+
return {
|
|
2347
|
+
importPath,
|
|
2348
|
+
moduleKey: moduleKey || fallbackModuleKey
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
function authInstanceFromFlag(flag) {
|
|
2352
|
+
return importOverride(flag, "auth");
|
|
2353
|
+
}
|
|
2354
|
+
function defaultReadFile(path) {
|
|
2355
|
+
return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : null;
|
|
2356
|
+
}
|
|
1733
2357
|
function defaultReadEnv(cwd) {
|
|
1734
2358
|
const path = (0, node_path.join)(cwd, ".env");
|
|
1735
2359
|
return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : "";
|
|
@@ -1741,18 +2365,27 @@ function defaultIsInteractive() {
|
|
|
1741
2365
|
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
1742
2366
|
}
|
|
1743
2367
|
function resolveInitDeps(options) {
|
|
2368
|
+
const loadGraph = options.loadWorkspaceGraph ?? loadWorkspaceGraph;
|
|
2369
|
+
const graphCache = /* @__PURE__ */ new Map();
|
|
2370
|
+
const graphFor = (cwd) => {
|
|
2371
|
+
if (!graphCache.has(cwd)) graphCache.set(cwd, loadGraph(cwd));
|
|
2372
|
+
return graphCache.get(cwd) ?? null;
|
|
2373
|
+
};
|
|
1744
2374
|
return {
|
|
1745
2375
|
out: options.out ?? ((line) => process.stdout.write(line)),
|
|
1746
2376
|
readDepsFn: options.readDeps ?? readDeps,
|
|
2377
|
+
graphFor,
|
|
1747
2378
|
resolvePath: options.resolveSchemaPath ?? ((cwd) => resolveSchemaPath({
|
|
1748
2379
|
cwd,
|
|
1749
|
-
flag: options.schemaFlag
|
|
2380
|
+
flag: options.schemaFlag,
|
|
2381
|
+
graph: graphFor(cwd)
|
|
1750
2382
|
})),
|
|
1751
2383
|
discover: options.discoverSchema ?? ((schemaPath, cwd) => discoverSchema({
|
|
1752
2384
|
schemaPath,
|
|
1753
|
-
cwd
|
|
2385
|
+
cwd,
|
|
2386
|
+
graph: graphFor(cwd)
|
|
1754
2387
|
})),
|
|
1755
|
-
|
|
2388
|
+
readFile: options.readFile ?? defaultReadFile,
|
|
1756
2389
|
writeConfig: options.writeConfig ?? ((path, contents) => {
|
|
1757
2390
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
1758
2391
|
(0, node_fs.writeFileSync)(path, contents, "utf8");
|
|
@@ -1763,7 +2396,7 @@ function resolveInitDeps(options) {
|
|
|
1763
2396
|
readEnv: options.readEnv ?? defaultReadEnv,
|
|
1764
2397
|
readOwnVersion: options.readOwnVersion ?? readOwnVersion,
|
|
1765
2398
|
writeEnv: options.writeEnv ?? defaultWriteEnv,
|
|
1766
|
-
isInteractive: options.isInteractive ?? defaultIsInteractive,
|
|
2399
|
+
isInteractive: options.yes ? () => false : options.isInteractive ?? defaultIsInteractive,
|
|
1767
2400
|
login: options.login ?? ((cwd) => runLogin({
|
|
1768
2401
|
apiUrl: process.env.CLIVLY_API_URL ?? "https://api.clivly.com",
|
|
1769
2402
|
cwd,
|
|
@@ -1780,14 +2413,14 @@ async function acquireSchema(options, deps) {
|
|
|
1780
2413
|
const { out } = deps;
|
|
1781
2414
|
const appDeps = deps.readDepsFn(options.cwd);
|
|
1782
2415
|
if (Object.keys(appDeps).length === 0) {
|
|
1783
|
-
out(
|
|
2416
|
+
out(`No package.json found in ${options.cwd}. Run \`clivly init\` from inside an existing app — cd into your project first.\n`);
|
|
1784
2417
|
return { code: 1 };
|
|
1785
2418
|
}
|
|
1786
2419
|
const framework = detectFramework(appDeps);
|
|
1787
2420
|
const orm = detectOrm(appDeps);
|
|
1788
2421
|
const auth = detectAuth(appDeps);
|
|
1789
2422
|
deps.framework = framework;
|
|
1790
|
-
deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? await deps.probeAuth(options.cwd) : null);
|
|
2423
|
+
deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
|
|
1791
2424
|
out("\nClivly init\n");
|
|
1792
2425
|
out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
|
|
1793
2426
|
out(` ORM: ${orm}\n`);
|
|
@@ -1837,14 +2470,18 @@ function mutationsFor(options, deps, plan) {
|
|
|
1837
2470
|
framework: deps.framework,
|
|
1838
2471
|
authWiring: deps.authWiring,
|
|
1839
2472
|
plan,
|
|
1840
|
-
|
|
2473
|
+
readFile: deps.readFile,
|
|
1841
2474
|
readEnv: deps.readEnv
|
|
1842
2475
|
});
|
|
1843
2476
|
}
|
|
1844
2477
|
function applyMutations(options, deps, mutations) {
|
|
1845
2478
|
for (const file of mutations.files) {
|
|
1846
|
-
if (file.action === "
|
|
1847
|
-
deps.out(`
|
|
2479
|
+
if (file.action === "unchanged") {
|
|
2480
|
+
deps.out(` · ${file.path} already up to date\n`);
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
if (file.action === "conflict") {
|
|
2484
|
+
deps.out(` ! ${file.path} differs from what init would write — left alone (--diff to see, --force to overwrite)\n`);
|
|
1848
2485
|
continue;
|
|
1849
2486
|
}
|
|
1850
2487
|
deps.writeConfig((0, node_path.join)(options.cwd, file.path), file.contents);
|
|
@@ -1857,29 +2494,42 @@ function applyMutations(options, deps, mutations) {
|
|
|
1857
2494
|
deps.writeEnv(options.cwd, mutations.env.contents);
|
|
1858
2495
|
deps.out(" ✓ .env placeholders\n");
|
|
1859
2496
|
}
|
|
1860
|
-
|
|
2497
|
+
const DIFF_CONTEXT_LIMIT = 40;
|
|
2498
|
+
function renderDiff(existing, next) {
|
|
2499
|
+
const before = existing.split("\n");
|
|
2500
|
+
const after = next.split("\n");
|
|
2501
|
+
const lines = [];
|
|
2502
|
+
for (const line of before.filter((l) => !after.includes(l))) lines.push(` - ${line}`);
|
|
2503
|
+
for (const line of after.filter((l) => !before.includes(l))) lines.push(` + ${line}`);
|
|
2504
|
+
return lines.length > DIFF_CONTEXT_LIMIT ? [...lines.slice(0, DIFF_CONTEXT_LIMIT), ` … ${lines.length - DIFF_CONTEXT_LIMIT} more lines`] : lines;
|
|
2505
|
+
}
|
|
2506
|
+
function previewMutations(deps, mutations, options) {
|
|
1861
2507
|
const { out } = deps;
|
|
1862
2508
|
out("\nDry run — nothing was written.\n\n");
|
|
1863
2509
|
for (const file of mutations.files) {
|
|
1864
|
-
const verb = {
|
|
1865
|
-
create: "create",
|
|
1866
|
-
overwrite: "overwrite",
|
|
1867
|
-
skip: "skip"
|
|
1868
|
-
}[file.action];
|
|
1869
2510
|
const lines = file.contents.split("\n").length;
|
|
1870
|
-
const suffix =
|
|
1871
|
-
|
|
2511
|
+
const suffix = {
|
|
2512
|
+
conflict: " (differs — --diff to see, --force to overwrite)",
|
|
2513
|
+
create: ` (${lines} lines)`,
|
|
2514
|
+
overwrite: ` (${lines} lines)`,
|
|
2515
|
+
unchanged: " (already up to date)"
|
|
2516
|
+
}[file.action];
|
|
2517
|
+
out(` ${file.action.padEnd(9)} ${file.path}${suffix}\n`);
|
|
2518
|
+
if (options.diff && file.action === "conflict") {
|
|
2519
|
+
const existing = deps.readFile((0, node_path.join)(options.cwd, file.path)) ?? "";
|
|
2520
|
+
for (const line of renderDiff(existing, file.contents)) out(`${line}\n`);
|
|
2521
|
+
}
|
|
1872
2522
|
}
|
|
1873
2523
|
if (mutations.env.addedKeys.length === 0) out(" skip .env (every CLIVLY_* key already present)\n");
|
|
1874
2524
|
else {
|
|
1875
2525
|
out(` ${mutations.env.action.padEnd(9)} .env\n`);
|
|
1876
2526
|
for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
|
|
1877
2527
|
}
|
|
1878
|
-
out("\nRe-run without --dry-run to apply.\n");
|
|
2528
|
+
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");
|
|
1879
2529
|
}
|
|
1880
2530
|
function printNextSteps(out, authComplete, loginRan) {
|
|
1881
2531
|
out("\nNext steps:\n");
|
|
1882
|
-
out(loginRan ? " 1. Credentials written by `clivly login`\n" : " 1. Add
|
|
2532
|
+
out(loginRan ? " 1. Credentials written by `clivly login`\n" : " 1. Add CLIVLY_SECRET_KEY to .env — create one in the Clivly dashboard\n under Integrations → Create API Key (or Settings → API keys)\n (the legacy CLIVLY_API_KEY is still accepted)\n");
|
|
1883
2533
|
out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
|
|
1884
2534
|
out(" Local dev? Run `clivly dev` to open a tunnel, or skip this step until you deploy (remote sync is deploy-only locally).\n");
|
|
1885
2535
|
const step = authComplete ? 3 : 4;
|
|
@@ -1887,7 +2537,7 @@ function printNextSteps(out, authComplete, loginRan) {
|
|
|
1887
2537
|
out(` ${step}. Run \`clivly status\` to see where you are\n`);
|
|
1888
2538
|
}
|
|
1889
2539
|
function credentialsMissing(envContents) {
|
|
1890
|
-
return !isKeySet(envContents, "CLIVLY_API_KEY");
|
|
2540
|
+
return !(isKeySet(envContents, "CLIVLY_SECRET_KEY") || isKeySet(envContents, "CLIVLY_API_KEY"));
|
|
1891
2541
|
}
|
|
1892
2542
|
function pairingSkipReason(options, deps) {
|
|
1893
2543
|
if (options.noLogin) return "--no-login";
|
|
@@ -1905,9 +2555,12 @@ async function scaffold(options, deps, plan) {
|
|
|
1905
2555
|
const pm = deps.detectPackageManager(options.cwd);
|
|
1906
2556
|
const packages = [resolveRuntimeInstallSpec(deps), ...deps.authWiring.packages];
|
|
1907
2557
|
const mutations = mutationsFor(options, deps, plan);
|
|
1908
|
-
if (options.dryRun) {
|
|
2558
|
+
if (options.dryRun || options.diff) {
|
|
1909
2559
|
out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
|
|
1910
|
-
previewMutations(deps, mutations
|
|
2560
|
+
previewMutations(deps, mutations, {
|
|
2561
|
+
cwd: options.cwd,
|
|
2562
|
+
diff: options.diff
|
|
2563
|
+
});
|
|
1911
2564
|
return 0;
|
|
1912
2565
|
}
|
|
1913
2566
|
const result = await deps.install({
|
|
@@ -1923,11 +2576,11 @@ async function scaffold(options, deps, plan) {
|
|
|
1923
2576
|
const skipReason = pairingSkipReason(options, deps);
|
|
1924
2577
|
if (skipReason) {
|
|
1925
2578
|
out(`\nSkipped browser pairing (${skipReason}).\n`);
|
|
1926
|
-
out(" Run `clivly login` from a terminal to write
|
|
2579
|
+
out(" Run `clivly login` from a terminal to write CLIVLY_SECRET_KEY and CLIVLY_SYNC_TRIGGER_SECRET.\n");
|
|
1927
2580
|
} else loginRan = await deps.login(options.cwd) === 0;
|
|
1928
2581
|
}
|
|
1929
2582
|
printNextSteps(out, deps.authWiring.complete, loginRan);
|
|
1930
|
-
return
|
|
2583
|
+
return 0;
|
|
1931
2584
|
}
|
|
1932
2585
|
function normalizeAppRoot(options) {
|
|
1933
2586
|
const resolution = (options.resolveTargetApp ?? resolveTargetApp)(options.cwd);
|
|
@@ -1957,9 +2610,45 @@ async function runInit(options) {
|
|
|
1957
2610
|
const deps = resolveInitDeps(resolvedOptions);
|
|
1958
2611
|
const acquired = await acquireSchema(resolvedOptions, deps);
|
|
1959
2612
|
if ("code" in acquired) return acquired.code;
|
|
1960
|
-
const
|
|
1961
|
-
|
|
1962
|
-
|
|
2613
|
+
const dbOverride = importOverride(resolvedOptions.dbImportFlag, "db");
|
|
2614
|
+
const plan = planInit(dbOverride ? {
|
|
2615
|
+
...acquired,
|
|
2616
|
+
db: dbOverride
|
|
2617
|
+
} : acquired, {
|
|
2618
|
+
contactTable: resolvedOptions.contactTableFlag,
|
|
2619
|
+
companyTable: resolvedOptions.companyTableFlag
|
|
2620
|
+
});
|
|
2621
|
+
const { contact } = plan;
|
|
2622
|
+
if (!contact) {
|
|
2623
|
+
printNoContactGuidance(deps.out, plan, resolvedOptions);
|
|
2624
|
+
return resolvedOptions.dryRun ? 0 : 1;
|
|
2625
|
+
}
|
|
2626
|
+
const renderable = {
|
|
2627
|
+
...plan,
|
|
2628
|
+
contact
|
|
2629
|
+
};
|
|
2630
|
+
printPlan(deps.out, renderable);
|
|
2631
|
+
return scaffold(resolvedOptions, deps, renderable);
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* What to do when no contact entity could be mapped. Lists the tables actually
|
|
2635
|
+
* discovered and the flag that picks one, so the next command succeeds rather
|
|
2636
|
+
* than leaving the developer to guess.
|
|
2637
|
+
*/
|
|
2638
|
+
function printNoContactGuidance(out, plan, options) {
|
|
2639
|
+
out("No usable contact entity\n\n");
|
|
2640
|
+
for (const todo of plan.todos) out(` • ${todo}\n`);
|
|
2641
|
+
if (plan.tableNames.length === 0) {
|
|
2642
|
+
out(`\n No tables were found in ${plan.importPath}.\n`);
|
|
2643
|
+
out(" Check that --schema points at a module exporting Drizzle tables.\n\n");
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
out(`\n Tables in ${plan.importPath}:\n`);
|
|
2647
|
+
for (const name of plan.tableNames) out(` ${name}\n`);
|
|
2648
|
+
const schemaArg = options.schemaFlag ? ` --schema ${options.schemaFlag}` : "";
|
|
2649
|
+
out("\n Pick the one that holds your people and re-run:\n");
|
|
2650
|
+
out(` clivly init${schemaArg} --contact-table ${plan.tableNames[0]}\n\n`);
|
|
2651
|
+
out(" Nothing was written — Clivly needs one entity before a config can load.\n\n");
|
|
1963
2652
|
}
|
|
1964
2653
|
function hasCloudflaredOnPath() {
|
|
1965
2654
|
const pathEnv = process.env.PATH ?? "";
|
|
@@ -2119,16 +2808,30 @@ function runInitFromArgv(argv, cwd) {
|
|
|
2119
2808
|
cwd,
|
|
2120
2809
|
force: argv.includes("--force"),
|
|
2121
2810
|
dryRun: argv.includes("--dry-run"),
|
|
2811
|
+
diff: argv.includes("--diff"),
|
|
2812
|
+
yes: argv.includes("--yes"),
|
|
2122
2813
|
noLogin: argv.includes("--no-login"),
|
|
2123
2814
|
openBrowser: !argv.includes("--no-browser"),
|
|
2124
|
-
schemaFlag: flagValue(argv, "--schema")
|
|
2815
|
+
schemaFlag: flagValue(argv, "--schema"),
|
|
2816
|
+
dbImportFlag: flagValue(argv, "--db-import"),
|
|
2817
|
+
authImportFlag: flagValue(argv, "--auth-import"),
|
|
2818
|
+
contactTableFlag: flagValue(argv, "--contact-table"),
|
|
2819
|
+
companyTableFlag: flagValue(argv, "--company-table")
|
|
2125
2820
|
});
|
|
2126
2821
|
}
|
|
2127
2822
|
async function main(argv) {
|
|
2128
2823
|
const [command, ...rest] = argv;
|
|
2129
|
-
|
|
2824
|
+
let cliEnvFiles = [];
|
|
2825
|
+
try {
|
|
2826
|
+
cliEnvFiles = (await loadCliConfig(process.cwd()))?.envFiles ?? [];
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
process.stderr.write(`${error.message}\n`);
|
|
2829
|
+
return 1;
|
|
2830
|
+
}
|
|
2831
|
+
loadEnv(process.cwd(), process.env, { files: [...cliEnvFiles, ...flagValues(rest, "--env-file")] });
|
|
2130
2832
|
switch (command) {
|
|
2131
2833
|
case "init": return await runInitFromArgv(rest, process.cwd());
|
|
2834
|
+
case "add": return runAdd(rest, process.cwd());
|
|
2132
2835
|
case "push-schema": return await runPushSchema({
|
|
2133
2836
|
cwd: process.cwd(),
|
|
2134
2837
|
configPath: flagValue(rest, "--config")
|
|
@@ -2138,14 +2841,14 @@ async function main(argv) {
|
|
|
2138
2841
|
case "doctor":
|
|
2139
2842
|
case "connect": return await runStatus({
|
|
2140
2843
|
cwd: process.cwd(),
|
|
2141
|
-
apiKey: process.env
|
|
2844
|
+
apiKey: readSecretKeyEnv(process.env),
|
|
2142
2845
|
apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
|
|
2143
2846
|
configPath: flagValue(rest, "--config"),
|
|
2144
2847
|
only: ALIAS_STAGE_IDS[command],
|
|
2145
2848
|
verbose: rest.includes("--verbose")
|
|
2146
2849
|
});
|
|
2147
2850
|
case "dev": {
|
|
2148
|
-
const apiKey = process.env
|
|
2851
|
+
const apiKey = readSecretKeyEnv(process.env);
|
|
2149
2852
|
const apiUrl = flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL ?? "https://api.clivly.com";
|
|
2150
2853
|
return await runDev({
|
|
2151
2854
|
cwd: process.cwd(),
|
|
@@ -2166,19 +2869,31 @@ async function main(argv) {
|
|
|
2166
2869
|
openBrowser: !rest.includes("--no-browser"),
|
|
2167
2870
|
deps: defaultLoginDeps()
|
|
2168
2871
|
});
|
|
2872
|
+
case "--version":
|
|
2873
|
+
case "-v":
|
|
2874
|
+
process.stdout.write(`${readOwnVersion() ?? "unknown"}\n`);
|
|
2875
|
+
return 0;
|
|
2169
2876
|
case void 0:
|
|
2170
2877
|
case "help":
|
|
2171
2878
|
case "--help":
|
|
2172
|
-
process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--dry-run] [--
|
|
2879
|
+
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");
|
|
2173
2880
|
return 0;
|
|
2174
|
-
default:
|
|
2881
|
+
default: {
|
|
2175
2882
|
process.stdout.write(`Unknown command: ${command}\n`);
|
|
2883
|
+
const suggestion = nearestCommand(command);
|
|
2884
|
+
if (suggestion) process.stdout.write(`Did you mean \`clivly ${suggestion}\`?\n`);
|
|
2885
|
+
process.stdout.write("Run `clivly --help` to see available commands.\n");
|
|
2176
2886
|
return 1;
|
|
2887
|
+
}
|
|
2177
2888
|
}
|
|
2178
2889
|
}
|
|
2179
2890
|
if (isEntrypoint(process.argv[1], require("url").pathToFileURL(__filename).href)) main(process.argv.slice(2)).then((code) => {
|
|
2180
2891
|
process.exitCode = code;
|
|
2181
2892
|
});
|
|
2182
2893
|
//#endregion
|
|
2894
|
+
exports.credentialsMissing = credentialsMissing;
|
|
2895
|
+
exports.main = main;
|
|
2896
|
+
exports.nearestCommand = nearestCommand;
|
|
2897
|
+
exports.runAdd = runAdd;
|
|
2183
2898
|
exports.runInit = runInit;
|
|
2184
2899
|
exports.runInitFromArgv = runInitFromArgv;
|