clivly 0.4.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -10
- 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 +4 -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 +14 -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 +4 -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 +910 -234
- package/index.d.cts +30 -2
- package/index.d.mts +30 -2
- package/index.mjs +900 -226
- 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 +152 -4
- package/{sdk-DdzBxgMx.mjs → sdk-C6WANW8M.mjs} +173 -34
- package/{sdk-D1BHQDf5.cjs → sdk-iqOXeWIp.cjs} +173 -34
- 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,114 @@
|
|
|
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("./mapping-score-
|
|
5
|
-
const
|
|
6
|
-
const require_sdk = require("./sdk-
|
|
3
|
+
const require_entity_config = require("./entity-config-D87rV-tE.cjs");
|
|
4
|
+
require("./mapping-score-CaB3F4Uu.cjs");
|
|
5
|
+
const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
|
|
6
|
+
const require_sdk = require("./sdk-iqOXeWIp.cjs");
|
|
7
7
|
let node_child_process = require("node:child_process");
|
|
8
8
|
let node_fs = require("node:fs");
|
|
9
9
|
let node_module = require("node:module");
|
|
10
10
|
let node_os = require("node:os");
|
|
11
11
|
let node_path = require("node:path");
|
|
12
12
|
let node_readline_promises = require("node:readline/promises");
|
|
13
|
-
let node_url = require("node:url");
|
|
14
13
|
let jiti = require("jiti");
|
|
14
|
+
let node_url = require("node:url");
|
|
15
|
+
//#region src/load-config.ts
|
|
16
|
+
const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
|
|
17
|
+
function resolveConfigPath(cwd, configPath) {
|
|
18
|
+
return (0, node_path.resolve)(cwd, configPath ?? "clivly.config.ts");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load the config and return its default export, checking it carries the method
|
|
22
|
+
* the caller needs. The check is by capability rather than by type: the config
|
|
23
|
+
* is the developer's own SDK instance, and a clear "this isn't an SDK instance"
|
|
24
|
+
* beats a TypeError deep inside a command.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The first meaningful line of an error message. jiti reports a failed
|
|
28
|
+
* resolution with a multi-line "Require stack:" dump naming Clivly's own
|
|
29
|
+
* internal files — noise that tells the developer nothing about their app and
|
|
30
|
+
* buries the one line that does.
|
|
31
|
+
*/
|
|
32
|
+
function firstLine(message) {
|
|
33
|
+
return message.split("\n")[0]?.trim() ?? "";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Describe why the config failed to load, in one line the developer can act on.
|
|
37
|
+
*
|
|
38
|
+
* A config that fails its own schema validation throws a zod error carrying
|
|
39
|
+
* structured `issues`, but whose `message` is a multi-line JSON dump — its
|
|
40
|
+
* first line is a bare "[", so `firstLine` alone reduced the entire diagnosis
|
|
41
|
+
* to a single bracket. Render the issues as `path: message` instead, which is
|
|
42
|
+
* what actually names the entity to fix. Anything else keeps the require-stack
|
|
43
|
+
* trimming `firstLine` exists for.
|
|
44
|
+
*/
|
|
45
|
+
function describeLoadError(error) {
|
|
46
|
+
const issues = error.issues;
|
|
47
|
+
if (Array.isArray(issues) && issues.length > 0) return issues.map((raw) => {
|
|
48
|
+
const issue = raw;
|
|
49
|
+
const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
|
|
50
|
+
const message = typeof issue.message === "string" ? issue.message : "invalid value";
|
|
51
|
+
return path ? `${path}: ${message}` : message;
|
|
52
|
+
}).join("; ");
|
|
53
|
+
return firstLine(error?.message ?? String(error));
|
|
54
|
+
}
|
|
55
|
+
async function loadConfigSdk(path, requiredMethod) {
|
|
56
|
+
const jiti$3 = (0, jiti.createJiti)(require("url").pathToFileURL(__filename).href);
|
|
57
|
+
let mod;
|
|
58
|
+
try {
|
|
59
|
+
mod = await jiti$3.import(path);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new Error(describeLoadError(error));
|
|
62
|
+
}
|
|
63
|
+
const sdk = mod.default ?? mod;
|
|
64
|
+
if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
|
|
65
|
+
return sdk;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/cli-config.ts
|
|
69
|
+
const CLI_CONFIG_NAMES = [
|
|
70
|
+
"clivly.cli.ts",
|
|
71
|
+
"clivly.cli.js",
|
|
72
|
+
"clivly.cli.mjs"
|
|
73
|
+
];
|
|
74
|
+
/** The app's CLI config, or null when it has none — which is the normal case. */
|
|
75
|
+
function resolveCliConfigPath(cwd) {
|
|
76
|
+
for (const name of CLI_CONFIG_NAMES) {
|
|
77
|
+
const path = (0, node_path.join)(cwd, name);
|
|
78
|
+
if ((0, node_fs.existsSync)(path)) return path;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function readEnvFiles(config) {
|
|
83
|
+
const envFiles = config.envFiles;
|
|
84
|
+
if (envFiles === void 0) return;
|
|
85
|
+
if (!Array.isArray(envFiles)) throw new Error("clivly.cli.ts: envFiles must be an array of paths, e.g. [\"../server/.env\"]");
|
|
86
|
+
if (envFiles.some((entry) => typeof entry !== "string")) throw new Error("clivly.cli.ts: every envFiles entry must be a string");
|
|
87
|
+
return envFiles;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Load `clivly.cli.ts` if the app has one.
|
|
91
|
+
*
|
|
92
|
+
* A malformed config throws rather than being silently ignored: a developer who
|
|
93
|
+
* wrote `envFiles` and sees nothing happen has no way to tell the difference
|
|
94
|
+
* between "not supported" and "I typed it wrong", and the symptom they get
|
|
95
|
+
* instead is a missing API key several steps later.
|
|
96
|
+
*/
|
|
97
|
+
async function loadCliConfig(cwd) {
|
|
98
|
+
const path = resolveCliConfigPath(cwd);
|
|
99
|
+
if (!path) return null;
|
|
100
|
+
let mod;
|
|
101
|
+
try {
|
|
102
|
+
mod = await (0, jiti.createJiti)(require("url").pathToFileURL(__filename).href).import(path);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(`clivly.cli.ts: ${describeLoadError(error)}`);
|
|
105
|
+
}
|
|
106
|
+
const config = mod?.default ?? mod;
|
|
107
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) throw new Error("clivly.cli.ts must export an object as its default export");
|
|
108
|
+
const envFiles = readEnvFiles(config);
|
|
109
|
+
return envFiles ? { envFiles } : {};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
15
112
|
//#region src/detect.ts
|
|
16
113
|
const FRAMEWORK_MARKERS = [
|
|
17
114
|
{
|
|
@@ -153,7 +250,7 @@ function detectDevCommand(scripts, explicit) {
|
|
|
153
250
|
return null;
|
|
154
251
|
}
|
|
155
252
|
const OVERRIDE_FAILURE_CAUSES = {
|
|
156
|
-
unauthorized: "
|
|
253
|
+
unauthorized: "CLIVLY_SECRET_KEY is invalid for this workspace/backend.",
|
|
157
254
|
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
255
|
unreachable: "Could not reach Clivly at the selected API base URL."
|
|
159
256
|
};
|
|
@@ -201,7 +298,7 @@ async function runDev(options) {
|
|
|
201
298
|
return 0;
|
|
202
299
|
}
|
|
203
300
|
if (!options.apiKey) {
|
|
204
|
-
deps.out("
|
|
301
|
+
deps.out("CLIVLY_SECRET_KEY is not set — cannot register the tunnel URL. Set it and re-run.");
|
|
205
302
|
return 1;
|
|
206
303
|
}
|
|
207
304
|
const app = command ? deps.spawnApp(command) : null;
|
|
@@ -280,16 +377,275 @@ async function probeModule(opts) {
|
|
|
280
377
|
}
|
|
281
378
|
for (const [moduleKey, value] of Object.entries(mod)) if (opts.match(value)) return {
|
|
282
379
|
moduleKey,
|
|
283
|
-
importPath: toImportPath(candidate, opts.cwd)
|
|
380
|
+
importPath: toImportPath(candidate, opts.cwd, opts.graph)
|
|
284
381
|
};
|
|
285
382
|
}
|
|
286
383
|
return null;
|
|
287
384
|
}
|
|
288
385
|
//#endregion
|
|
386
|
+
//#region src/workspace-graph.ts
|
|
387
|
+
const WINDOWS_SEP$2 = /\\/g;
|
|
388
|
+
const MODULE_EXT$1 = /\.(ts|tsx|js|jsx|mjs|cjs|cts|mts)$/;
|
|
389
|
+
const BRACE_GROUP = /\{([^{}]*)\}/;
|
|
390
|
+
const MAX_GLOB_DEPTH = 8;
|
|
391
|
+
const IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
392
|
+
"node_modules",
|
|
393
|
+
"dist",
|
|
394
|
+
"build",
|
|
395
|
+
"coverage"
|
|
396
|
+
]);
|
|
397
|
+
function isDirectory$1(path) {
|
|
398
|
+
try {
|
|
399
|
+
return (0, node_fs.statSync)(path).isDirectory();
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const CONVENTIONAL_ENTRIES = [
|
|
405
|
+
"src/index.ts",
|
|
406
|
+
"src/index.tsx",
|
|
407
|
+
"index.ts",
|
|
408
|
+
"index.tsx",
|
|
409
|
+
"src/index.js",
|
|
410
|
+
"index.js"
|
|
411
|
+
];
|
|
412
|
+
function readPackageManifest(dir) {
|
|
413
|
+
const path = (0, node_path.join)(dir, "package.json");
|
|
414
|
+
if (!(0, node_fs.existsSync)(path)) return null;
|
|
415
|
+
try {
|
|
416
|
+
return JSON.parse((0, node_fs.readFileSync)(path, "utf8"));
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function parentDir$1(dir) {
|
|
422
|
+
const parent = (0, node_path.dirname)(dir);
|
|
423
|
+
return parent === dir ? null : parent;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The workspace globs declared at `dir`, from either `package.json#workspaces`
|
|
427
|
+
* (npm, yarn, bun) or `pnpm-workspace.yaml` (pnpm). Negations are dropped —
|
|
428
|
+
* they only ever narrow the set, and treating one as a literal path would
|
|
429
|
+
* invent a package directory named `!something`.
|
|
430
|
+
*/
|
|
431
|
+
function readWorkspaceGlobs(dir) {
|
|
432
|
+
const manifest = readPackageManifest(dir);
|
|
433
|
+
const declared = Array.isArray(manifest?.workspaces) ? manifest.workspaces : manifest?.workspaces?.packages;
|
|
434
|
+
if (Array.isArray(declared) && declared.length > 0) return declared.filter((glob) => !glob.startsWith("!"));
|
|
435
|
+
const pnpmPath = (0, node_path.join)(dir, "pnpm-workspace.yaml");
|
|
436
|
+
if (!(0, node_fs.existsSync)(pnpmPath)) return [];
|
|
437
|
+
try {
|
|
438
|
+
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("!"));
|
|
439
|
+
} catch {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* `{apps,packages}/*` -> [`apps/*`, `packages/*`].
|
|
445
|
+
*
|
|
446
|
+
* Expands one group per pass and recurses, so nested and repeated groups all
|
|
447
|
+
* resolve. A manifest that declares a brace glob is a workspace like any other,
|
|
448
|
+
* and failing to expand it silently yields an empty graph — which downgrades
|
|
449
|
+
* every generated specifier to the relative form this module exists to avoid.
|
|
450
|
+
*/
|
|
451
|
+
function expandBraces(pattern) {
|
|
452
|
+
const group = BRACE_GROUP.exec(pattern);
|
|
453
|
+
if (!group) return [pattern];
|
|
454
|
+
return (group[1] ?? "").split(",").flatMap((option) => expandBraces(pattern.replace(group[0], option.trim())));
|
|
455
|
+
}
|
|
456
|
+
function childDirectories(dir) {
|
|
457
|
+
try {
|
|
458
|
+
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));
|
|
459
|
+
} catch {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** `dir` and every directory beneath it, up to `depth` levels down. */
|
|
464
|
+
function withDescendants(dir, depth) {
|
|
465
|
+
if (depth <= 0) return [dir];
|
|
466
|
+
return [dir, ...childDirectories(dir).flatMap((child) => withDescendants(child, depth - 1))];
|
|
467
|
+
}
|
|
468
|
+
/** Resolve the remaining glob segments against the directories matched so far. */
|
|
469
|
+
function matchSegments(dirs, segments) {
|
|
470
|
+
const [segment, ...rest] = segments;
|
|
471
|
+
if (segment === void 0) return dirs;
|
|
472
|
+
let next;
|
|
473
|
+
if (segment === "**") next = dirs.flatMap((dir) => withDescendants(dir, MAX_GLOB_DEPTH));
|
|
474
|
+
else if (segment === "*") next = dirs.flatMap(childDirectories);
|
|
475
|
+
else next = dirs.map((dir) => (0, node_path.join)(dir, segment)).filter(isDirectory$1);
|
|
476
|
+
return matchSegments(next, rest);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Expand one workspace glob into the package directories it names.
|
|
480
|
+
*
|
|
481
|
+
* Handles exact paths, `*`, `**`, and brace groups — the shapes that actually
|
|
482
|
+
* appear in pnpm/npm/yarn/bun manifests. `node_modules` and dot-directories are
|
|
483
|
+
* never descended into: a dependency's own package.json is not a workspace
|
|
484
|
+
* member, and treating one as such would put a third-party name in the graph.
|
|
485
|
+
*/
|
|
486
|
+
function expandWorkspaceGlob(root, pattern) {
|
|
487
|
+
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")));
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Walk up from `start` looking for the directory that declares workspace globs.
|
|
491
|
+
* Stops *before* `boundary` (the user's home directory by default) so a stray
|
|
492
|
+
* manifest above the project is never mistaken for a workspace root.
|
|
493
|
+
*/
|
|
494
|
+
function findWorkspaceRoot(start, boundary = (0, node_os.homedir)()) {
|
|
495
|
+
const stop = (0, node_path.resolve)(boundary);
|
|
496
|
+
let current = (0, node_path.resolve)(start);
|
|
497
|
+
while (current !== stop) {
|
|
498
|
+
if (readWorkspaceGlobs(current).length > 0) return current;
|
|
499
|
+
const parent = parentDir$1(current);
|
|
500
|
+
if (!parent) return null;
|
|
501
|
+
current = parent;
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Build the package graph containing `start`, or null when `start` is not inside
|
|
507
|
+
* a workspace (the single-package case, where relative imports are correct).
|
|
508
|
+
*/
|
|
509
|
+
function loadWorkspaceGraph(start, boundary = (0, node_os.homedir)()) {
|
|
510
|
+
const root = findWorkspaceRoot(start, boundary);
|
|
511
|
+
if (!root) return null;
|
|
512
|
+
const seen = /* @__PURE__ */ new Set();
|
|
513
|
+
const packages = [];
|
|
514
|
+
for (const pattern of readWorkspaceGlobs(root)) for (const dir of expandWorkspaceGlob(root, pattern)) {
|
|
515
|
+
const absolute = (0, node_path.resolve)(dir);
|
|
516
|
+
if (seen.has(absolute)) continue;
|
|
517
|
+
seen.add(absolute);
|
|
518
|
+
const name = readPackageManifest(absolute)?.name;
|
|
519
|
+
if (typeof name === "string" && name.length > 0) packages.push({
|
|
520
|
+
dir: absolute,
|
|
521
|
+
name
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
root,
|
|
526
|
+
packages
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* The workspace package that owns `absPath`.
|
|
531
|
+
*
|
|
532
|
+
* Compares against `dir + sep` so a sibling sharing a name prefix
|
|
533
|
+
* (`packages/database-legacy` vs `packages/database`) can't match, and returns
|
|
534
|
+
* the longest match so a nested package beats its parent.
|
|
535
|
+
*/
|
|
536
|
+
function findPackageContaining(graph, absPath) {
|
|
537
|
+
if (!graph) return null;
|
|
538
|
+
const target = (0, node_path.resolve)(absPath);
|
|
539
|
+
let best = null;
|
|
540
|
+
for (const pkg of graph.packages) {
|
|
541
|
+
const withSep = pkg.dir.endsWith(node_path.sep) ? pkg.dir : `${pkg.dir}${node_path.sep}`;
|
|
542
|
+
if ((target === pkg.dir || target.startsWith(withSep)) && (!best || pkg.dir.length > best.dir.length)) best = pkg;
|
|
543
|
+
}
|
|
544
|
+
return best;
|
|
545
|
+
}
|
|
546
|
+
/** The first string buried in an `exports` value, following conditions in order. */
|
|
547
|
+
function firstExportString(value) {
|
|
548
|
+
if (typeof value === "string") return value;
|
|
549
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
550
|
+
for (const nested of Object.values(value)) {
|
|
551
|
+
const found = firstExportString(nested);
|
|
552
|
+
if (found) return found;
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
function isFile(path) {
|
|
557
|
+
try {
|
|
558
|
+
return (0, node_fs.statSync)(path).isFile();
|
|
559
|
+
} catch {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* The file a bare import of this package resolves to, or null if we can't tell.
|
|
565
|
+
*
|
|
566
|
+
* Null rather than a guess on purpose: an entry declared in the manifest but
|
|
567
|
+
* absent on disk (a `dist/` build that hasn't run) would otherwise make `init`
|
|
568
|
+
* generate an import of a file that does not exist.
|
|
569
|
+
*/
|
|
570
|
+
function resolvePackageEntry(packageDir) {
|
|
571
|
+
const manifest = readPackageManifest(packageDir);
|
|
572
|
+
const declared = [
|
|
573
|
+
firstExportString((manifest?.exports)?.["."]),
|
|
574
|
+
manifest?.module,
|
|
575
|
+
manifest?.main
|
|
576
|
+
].filter((entry) => typeof entry === "string");
|
|
577
|
+
for (const entry of [...declared, ...CONVENTIONAL_ENTRIES]) {
|
|
578
|
+
const candidate = (0, node_path.resolve)(packageDir, entry);
|
|
579
|
+
if (isFile(candidate)) return candidate;
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Whether `./subpath` is something an importer outside the package may request.
|
|
585
|
+
*
|
|
586
|
+
* A package with no `exports` field uses legacy resolution, where any file is
|
|
587
|
+
* reachable. Once `exports` exists it is an allow-list: Node and TypeScript's
|
|
588
|
+
* `bundler`/`node16` resolution both reject anything it does not name, so
|
|
589
|
+
* emitting such a specifier scaffolds a config that cannot compile.
|
|
590
|
+
*/
|
|
591
|
+
function exportsAllowSubpath(packageDir, subpath) {
|
|
592
|
+
const exports = readPackageManifest(packageDir)?.exports;
|
|
593
|
+
if (exports === void 0 || exports === null) return true;
|
|
594
|
+
if (typeof exports !== "object" || Array.isArray(exports)) return false;
|
|
595
|
+
const request = `./${subpath}`;
|
|
596
|
+
return Object.keys(exports).some((key) => {
|
|
597
|
+
if (!key.startsWith(".")) return false;
|
|
598
|
+
const star = key.indexOf("*");
|
|
599
|
+
if (star === -1) return key === request;
|
|
600
|
+
const prefix = key.slice(0, star);
|
|
601
|
+
const suffix = key.slice(star + 1);
|
|
602
|
+
return request.length >= prefix.length + suffix.length && request.startsWith(prefix) && request.endsWith(suffix);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* The specifier that imports `absPath` from outside its package: the bare name
|
|
607
|
+
* when the file is the package entry, `name/subpath` when the package's
|
|
608
|
+
* `exports` permits that subpath, and **null** when it does not.
|
|
609
|
+
*
|
|
610
|
+
* Null is the honest answer rather than a specifier that looks right and fails
|
|
611
|
+
* to resolve. The common shape that hits it is a package exporting only `"."`
|
|
612
|
+
* while the Drizzle instance lives in `src/db.ts` — callers should fall back to
|
|
613
|
+
* a relative import, or ask the developer via `--db-import` / `--auth-import`.
|
|
614
|
+
*/
|
|
615
|
+
function toPackageSpecifier(pkg, absPath) {
|
|
616
|
+
const target = (0, node_path.resolve)(absPath);
|
|
617
|
+
if (resolvePackageEntry(pkg.dir) === target) return pkg.name;
|
|
618
|
+
const subpath = (0, node_path.relative)(pkg.dir, target).replace(WINDOWS_SEP$2, "/").replace(MODULE_EXT$1, "");
|
|
619
|
+
if (subpath.length === 0) return pkg.name;
|
|
620
|
+
return exportsAllowSubpath(pkg.dir, subpath) ? `${pkg.name}/${subpath}` : null;
|
|
621
|
+
}
|
|
622
|
+
//#endregion
|
|
289
623
|
//#region src/discover-schema.ts
|
|
290
624
|
const WINDOWS_SEP$1 = /\\/g;
|
|
291
625
|
const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
|
|
292
|
-
|
|
626
|
+
/**
|
|
627
|
+
* Turn an absolute module path into the specifier the generated config imports
|
|
628
|
+
* it by. Every import `init` emits — schema, db instance, auth instance — is
|
|
629
|
+
* minted here, so this is the one place the monorepo fix has to land.
|
|
630
|
+
*
|
|
631
|
+
* "/app/src/db/schema.ts" (cwd "/app") -> "./src/db/schema"
|
|
632
|
+
*
|
|
633
|
+
* With a workspace `graph`, a file owned by a *different* workspace package
|
|
634
|
+
* becomes a package specifier instead: a schema in `packages/database` imports
|
|
635
|
+
* as `@acme/database`, not `../../packages/database/src/index`. The relative
|
|
636
|
+
* form escapes the app's tsconfig rootDir and sidesteps the package boundary the
|
|
637
|
+
* repo already maintains, which is what made generated configs unusable in
|
|
638
|
+
* shared-package layouts.
|
|
639
|
+
*
|
|
640
|
+
* A file inside the app itself still gets a relative specifier — the app is a
|
|
641
|
+
* workspace package too, and importing it by its own name would be a cycle.
|
|
642
|
+
*/
|
|
643
|
+
function toImportPath(schemaPath, cwd, graph) {
|
|
644
|
+
const owner = findPackageContaining(graph ?? null, schemaPath);
|
|
645
|
+
if (owner && (0, node_path.resolve)(owner.dir) !== (0, node_path.resolve)(cwd)) {
|
|
646
|
+
const specifier = toPackageSpecifier(owner, schemaPath);
|
|
647
|
+
if (specifier) return specifier;
|
|
648
|
+
}
|
|
293
649
|
const rel = (0, node_path.relative)(cwd, schemaPath).replace(WINDOWS_SEP$1, "/").replace(MODULE_EXT, "");
|
|
294
650
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
295
651
|
}
|
|
@@ -304,8 +660,8 @@ function loadSchemaModule(schemaPath, load) {
|
|
|
304
660
|
return load(isDirectory(schemaPath) ? (0, node_path.join)(schemaPath, "index.ts") : schemaPath);
|
|
305
661
|
}
|
|
306
662
|
function defaultLoader$1(cwd) {
|
|
307
|
-
const jiti$
|
|
308
|
-
return (specifier) => jiti$
|
|
663
|
+
const jiti$2 = (0, jiti.createJiti)((0, node_url.pathToFileURL)((0, node_path.join)(cwd, "clivly.config.ts")).href);
|
|
664
|
+
return (specifier) => jiti$2.import(specifier);
|
|
309
665
|
}
|
|
310
666
|
async function loadPgCoreRuntime(load) {
|
|
311
667
|
try {
|
|
@@ -361,12 +717,18 @@ function siblingDbCandidates(schemaPath) {
|
|
|
361
717
|
const dir = isDirectory(schemaPath) ? schemaPath : (0, node_path.dirname)(schemaPath);
|
|
362
718
|
return SIBLING_DB_NAMES.map((name) => (0, node_path.join)(dir, name));
|
|
363
719
|
}
|
|
720
|
+
function parentDbCandidates(schemaPath, cwd) {
|
|
721
|
+
const parent = (0, node_path.dirname)(isDirectory(schemaPath) ? schemaPath : (0, node_path.dirname)(schemaPath));
|
|
722
|
+
const relativeToRoot = (0, node_path.relative)((0, node_path.resolve)(cwd), (0, node_path.resolve)(parent));
|
|
723
|
+
if (!(relativeToRoot === "" || !relativeToRoot.startsWith(".."))) return [];
|
|
724
|
+
return SIBLING_DB_NAMES.map((name) => (0, node_path.join)(parent, name));
|
|
725
|
+
}
|
|
364
726
|
async function discoverSchema(opts) {
|
|
365
727
|
const load = opts.load ?? defaultLoader$1(opts.cwd);
|
|
366
728
|
const mod = await loadSchemaModule(opts.schemaPath, load);
|
|
367
729
|
const drizzle = await load("drizzle-orm");
|
|
368
730
|
const pgCore = await loadPgCoreRuntime(load);
|
|
369
|
-
const importPath = toImportPath(opts.schemaPath, opts.cwd);
|
|
731
|
+
const importPath = toImportPath(opts.schemaPath, opts.cwd, opts.graph);
|
|
370
732
|
const tables = [];
|
|
371
733
|
let db = null;
|
|
372
734
|
for (const [moduleKey, value] of Object.entries(mod)) {
|
|
@@ -388,6 +750,14 @@ async function discoverSchema(opts) {
|
|
|
388
750
|
if (db === null) db = await probeModule({
|
|
389
751
|
candidates: siblingDbCandidates(opts.schemaPath),
|
|
390
752
|
cwd: opts.cwd,
|
|
753
|
+
graph: opts.graph,
|
|
754
|
+
load,
|
|
755
|
+
match: isDrizzleDb
|
|
756
|
+
});
|
|
757
|
+
if (db === null) db = await probeModule({
|
|
758
|
+
candidates: parentDbCandidates(opts.schemaPath, opts.cwd),
|
|
759
|
+
cwd: opts.cwd,
|
|
760
|
+
graph: opts.graph,
|
|
391
761
|
load,
|
|
392
762
|
match: isDrizzleDb
|
|
393
763
|
});
|
|
@@ -540,7 +910,7 @@ function isEntrypoint(argv1, moduleUrl, realpath = node_fs.realpathSync) {
|
|
|
540
910
|
//#endregion
|
|
541
911
|
//#region src/load-env.ts
|
|
542
912
|
const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
543
|
-
const NEWLINE = /\r?\n/;
|
|
913
|
+
const NEWLINE$1 = /\r?\n/;
|
|
544
914
|
const ESCAPED_NEWLINE = /\\n/g;
|
|
545
915
|
const ESCAPED_CARRIAGE = /\\r/g;
|
|
546
916
|
const TRAILING_COMMENT = /\s#/;
|
|
@@ -552,7 +922,7 @@ const TRAILING_COMMENT = /\s#/;
|
|
|
552
922
|
*/
|
|
553
923
|
function parseEnv(content) {
|
|
554
924
|
const out = {};
|
|
555
|
-
for (const rawLine of content.split(NEWLINE)) {
|
|
925
|
+
for (const rawLine of content.split(NEWLINE$1)) {
|
|
556
926
|
const line = rawLine.trimStart();
|
|
557
927
|
if (line === "" || line.startsWith("#")) continue;
|
|
558
928
|
const match = LINE.exec(line);
|
|
@@ -577,15 +947,26 @@ function parseValue(raw) {
|
|
|
577
947
|
/**
|
|
578
948
|
* Load `.env` then `.env.local` from `cwd` into `env` (default `process.env`),
|
|
579
949
|
* 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
|
|
950
|
+
* real environment (shell) > explicit `options.files` > `.env.local` > `.env`.
|
|
951
|
+
* The conventional part mirrors what developers already expect (Next.js, Vite),
|
|
952
|
+
* so pointing `CLIVLY_API_URL` at a staging host from the shell still wins.
|
|
953
|
+
*
|
|
954
|
+
* Explicit files exist for split workspaces, where the app that holds
|
|
955
|
+
* `clivly.config.ts` is not the package that holds the credentials. Reading
|
|
956
|
+
* them here — in the CLI process — is what keeps that need out of
|
|
957
|
+
* `clivly.config.ts`, which the app imports and which therefore has to survive
|
|
958
|
+
* an edge runtime with no filesystem.
|
|
583
959
|
*/
|
|
584
|
-
function loadEnv(cwd, env = process.env) {
|
|
960
|
+
function loadEnv(cwd, env = process.env, options = {}) {
|
|
585
961
|
const files = [];
|
|
586
962
|
const merged = {};
|
|
587
|
-
|
|
588
|
-
|
|
963
|
+
const candidates = [
|
|
964
|
+
".env",
|
|
965
|
+
".env.local",
|
|
966
|
+
...options.files ?? []
|
|
967
|
+
];
|
|
968
|
+
for (const name of candidates) {
|
|
969
|
+
const path = (0, node_path.resolve)(cwd, name);
|
|
589
970
|
if (!(0, node_fs.existsSync)(path)) continue;
|
|
590
971
|
files.push(name);
|
|
591
972
|
try {
|
|
@@ -603,6 +984,15 @@ function loadEnv(cwd, env = process.env) {
|
|
|
603
984
|
};
|
|
604
985
|
}
|
|
605
986
|
//#endregion
|
|
987
|
+
//#region src/secret-key-env.ts
|
|
988
|
+
const SECRET_KEY_ENV = "CLIVLY_SECRET_KEY";
|
|
989
|
+
const LEGACY_SECRET_KEY_ENV = "CLIVLY_API_KEY";
|
|
990
|
+
function readSecretKeyEnv(env) {
|
|
991
|
+
const next = env[SECRET_KEY_ENV];
|
|
992
|
+
if (next && next.length > 0) return next;
|
|
993
|
+
return env[LEGACY_SECRET_KEY_ENV];
|
|
994
|
+
}
|
|
995
|
+
//#endregion
|
|
606
996
|
//#region src/login.ts
|
|
607
997
|
const POLL_INTERVAL_MS = 2e3;
|
|
608
998
|
const POLL_BACKOFF_MULTIPLIER = 1.5;
|
|
@@ -614,7 +1004,7 @@ const MASK_VISIBLE_CHARS = 4;
|
|
|
614
1004
|
const MASK_PREFIX = "••••";
|
|
615
1005
|
function maskSecret(value) {
|
|
616
1006
|
if (value.length <= MASK_VISIBLE_CHARS) return MASK_PREFIX;
|
|
617
|
-
return `${MASK_PREFIX}${value.slice(-
|
|
1007
|
+
return `${MASK_PREFIX}${value.slice(-4)}`;
|
|
618
1008
|
}
|
|
619
1009
|
async function createPairingSession(apiUrl, deps, rotate) {
|
|
620
1010
|
const res = await deps.fetchImpl(`${apiUrl}/clivly/cli/pair`, {
|
|
@@ -626,7 +1016,7 @@ async function createPairingSession(apiUrl, deps, rotate) {
|
|
|
626
1016
|
})
|
|
627
1017
|
});
|
|
628
1018
|
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
|
|
1019
|
+
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
1020
|
return { exitCode: 1 };
|
|
631
1021
|
}
|
|
632
1022
|
if (!res.ok) {
|
|
@@ -699,9 +1089,9 @@ async function confirmWorkspace(deps, credentials) {
|
|
|
699
1089
|
}
|
|
700
1090
|
function credentialEntries(credentials) {
|
|
701
1091
|
return [{
|
|
702
|
-
key:
|
|
1092
|
+
key: SECRET_KEY_ENV,
|
|
703
1093
|
value: credentials.apiKey,
|
|
704
|
-
comment: "Org
|
|
1094
|
+
comment: "Org secret key — Settings → API keys"
|
|
705
1095
|
}, {
|
|
706
1096
|
key: "CLIVLY_SYNC_TRIGGER_SECRET",
|
|
707
1097
|
value: credentials.triggerSecret,
|
|
@@ -715,7 +1105,7 @@ function writeCredentials(options, credentials) {
|
|
|
715
1105
|
deps.writeEnv(options.cwd, result.contents);
|
|
716
1106
|
deps.out("\nLogin successful.\n");
|
|
717
1107
|
const valuesByKey = {
|
|
718
|
-
|
|
1108
|
+
[SECRET_KEY_ENV]: credentials.apiKey,
|
|
719
1109
|
CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
|
|
720
1110
|
};
|
|
721
1111
|
for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
|
|
@@ -756,7 +1146,7 @@ function toScoredTable(table) {
|
|
|
756
1146
|
function rankCandidates$1(tables, concept) {
|
|
757
1147
|
return tables.map((t) => ({
|
|
758
1148
|
t,
|
|
759
|
-
m:
|
|
1149
|
+
m: require_internal_entity_heuristics.matchesConcept(t.tableName, concept)
|
|
760
1150
|
})).filter((c) => Boolean(c.m)).sort((a, b) => Number(b.m.exact) - Number(a.m.exact)).map((c) => c.t);
|
|
761
1151
|
}
|
|
762
1152
|
function selectEntities(tables) {
|
|
@@ -775,7 +1165,7 @@ function selectEntities(tables) {
|
|
|
775
1165
|
}
|
|
776
1166
|
function chooseSyncFields(table, todos) {
|
|
777
1167
|
const columnKeys = table.columns.map((c) => c.key);
|
|
778
|
-
let cursorField =
|
|
1168
|
+
let cursorField = require_internal_entity_heuristics.suggestCursorField(columnKeys);
|
|
779
1169
|
if (!cursorField) {
|
|
780
1170
|
cursorField = table.columns.find((c) => c.isPrimary)?.key ?? columnKeys[0] ?? "id";
|
|
781
1171
|
todos.push(`No updated_at-like column on "${table.tableName}" — using "${cursorField}" as the sync cursor. Confirm it is monotonic.`);
|
|
@@ -794,10 +1184,10 @@ function chooseSyncFields(table, todos) {
|
|
|
794
1184
|
}
|
|
795
1185
|
function buildEntity(table, concept, todos) {
|
|
796
1186
|
const canonical = require_entity_config.CANONICAL_FIELDS[concept];
|
|
797
|
-
const suggested =
|
|
1187
|
+
const suggested = require_internal_entity_heuristics.toFieldMap(require_internal_entity_heuristics.scoreFieldMap(concept, toScoredTable(table)));
|
|
798
1188
|
const fields = {};
|
|
799
1189
|
for (const [key, column] of Object.entries(suggested)) if (canonical.includes(key)) fields[key] = column;
|
|
800
|
-
for (const required of
|
|
1190
|
+
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
1191
|
const { cursorField, idField } = chooseSyncFields(table, todos);
|
|
802
1192
|
return {
|
|
803
1193
|
moduleKey: table.moduleKey,
|
|
@@ -808,42 +1198,52 @@ function buildEntity(table, concept, todos) {
|
|
|
808
1198
|
idField
|
|
809
1199
|
};
|
|
810
1200
|
}
|
|
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
1201
|
function hasMappedFields(entity) {
|
|
826
1202
|
return Object.keys(entity.fields).length > 0;
|
|
827
1203
|
}
|
|
828
|
-
|
|
1204
|
+
/** A table by its SQL name or its schema-module export name — either is what a developer would type. */
|
|
1205
|
+
function findTable(tables, name) {
|
|
1206
|
+
return tables.find((table) => table.tableName === name || table.moduleKey === name) ?? null;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Which table serves each concept: an explicit override when given, else the
|
|
1210
|
+
* heuristic pick.
|
|
1211
|
+
*
|
|
1212
|
+
* Split out of `planInit` because resolving two independent overrides, each with
|
|
1213
|
+
* its own unknown-name path, is most of the branching — and keeps `planInit`
|
|
1214
|
+
* inside the cognitive-complexity budget.
|
|
1215
|
+
*/
|
|
1216
|
+
function selectTables(tables, overrides, todos) {
|
|
1217
|
+
const heuristic = selectEntities(tables);
|
|
1218
|
+
const pick = (flag, fallback, label) => {
|
|
1219
|
+
if (!flag) return fallback;
|
|
1220
|
+
const found = findTable(tables, flag);
|
|
1221
|
+
if (!found) todos.push(`--${label}-table "${flag}" is not a table in this schema.`);
|
|
1222
|
+
return found;
|
|
1223
|
+
};
|
|
1224
|
+
const contact = pick(overrides.contactTable, heuristic.contact, "contact");
|
|
1225
|
+
const company = pick(overrides.companyTable, heuristic.company, "company");
|
|
1226
|
+
return {
|
|
1227
|
+
contact,
|
|
1228
|
+
company: company?.tableName === contact?.tableName ? null : company
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function planInit(schema, overrides = {}) {
|
|
829
1232
|
const todos = [];
|
|
830
|
-
const { contact: contactTable, company: companyTable } =
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
if (!
|
|
834
|
-
contact = placeholderContact(todos);
|
|
835
|
-
contactDetected = false;
|
|
836
|
-
}
|
|
1233
|
+
const { contact: contactTable, company: companyTable } = selectTables(schema.tables, overrides, todos);
|
|
1234
|
+
const built = contactTable ? buildEntity(contactTable, "contact", todos) : null;
|
|
1235
|
+
const contact = built && hasMappedFields(built) ? built : null;
|
|
1236
|
+
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
1237
|
let company = null;
|
|
838
1238
|
if (companyTable) {
|
|
839
1239
|
const candidate = buildEntity(companyTable, "company", todos);
|
|
840
1240
|
if (hasMappedFields(candidate)) {
|
|
841
1241
|
company = candidate;
|
|
842
|
-
if (
|
|
1242
|
+
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
1243
|
} 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
1244
|
}
|
|
845
|
-
if (contactTable) {
|
|
846
|
-
const scored =
|
|
1245
|
+
if (contactTable && contact) {
|
|
1246
|
+
const scored = require_internal_entity_heuristics.scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
|
|
847
1247
|
const top = scored[0];
|
|
848
1248
|
const runnerUp = scored[1];
|
|
849
1249
|
const isClearWinner = top !== void 0 && (runnerUp === void 0 || top.score - runnerUp.score >= 15);
|
|
@@ -857,16 +1257,16 @@ function planInit(schema) {
|
|
|
857
1257
|
db: schema.db,
|
|
858
1258
|
contact,
|
|
859
1259
|
company,
|
|
860
|
-
|
|
861
|
-
|
|
1260
|
+
tableNames: schema.tables.map((table) => table.tableName),
|
|
1261
|
+
todos
|
|
862
1262
|
};
|
|
863
1263
|
}
|
|
864
1264
|
//#endregion
|
|
865
1265
|
//#region src/render-config.ts
|
|
866
1266
|
const CLIVLY_ENV_KEYS = [
|
|
867
1267
|
{
|
|
868
|
-
key: "
|
|
869
|
-
comment: "Org
|
|
1268
|
+
key: "CLIVLY_SECRET_KEY",
|
|
1269
|
+
comment: "Org secret key — Settings → API keys"
|
|
870
1270
|
},
|
|
871
1271
|
{
|
|
872
1272
|
key: "CLIVLY_SYNC_TRIGGER_URL",
|
|
@@ -875,10 +1275,6 @@ const CLIVLY_ENV_KEYS = [
|
|
|
875
1275
|
{
|
|
876
1276
|
key: "CLIVLY_SYNC_TRIGGER_SECRET",
|
|
877
1277
|
comment: "Signing secret — Integrations → Remote sync"
|
|
878
|
-
},
|
|
879
|
-
{
|
|
880
|
-
key: "CLIVLY_VERIFY_TOKEN",
|
|
881
|
-
comment: "Bearer token Clivly presents to /clivly/auth/verify"
|
|
882
1278
|
}
|
|
883
1279
|
];
|
|
884
1280
|
function renderFields(fields) {
|
|
@@ -900,10 +1296,6 @@ function renderSource(role, entity, dbExpr) {
|
|
|
900
1296
|
const opts = entity.idField === null ? `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}" }` : `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}", idField: "${entity.idField}" }`;
|
|
901
1297
|
return ` ${role}: fromDrizzle(${dbExpr}, schema.${entity.moduleKey}, ${opts})`;
|
|
902
1298
|
}
|
|
903
|
-
function fallbackDbImportPath(schemaImportPath) {
|
|
904
|
-
const slash = schemaImportPath.lastIndexOf("/");
|
|
905
|
-
return `${slash >= 0 ? schemaImportPath.slice(0, slash) : "."}/db`;
|
|
906
|
-
}
|
|
907
1299
|
function dbIsInSchemaModule(plan) {
|
|
908
1300
|
return plan.db !== null && plan.db.importPath === plan.importPath;
|
|
909
1301
|
}
|
|
@@ -913,13 +1305,12 @@ function renderDbExpr(plan) {
|
|
|
913
1305
|
}
|
|
914
1306
|
function renderImports(plan) {
|
|
915
1307
|
const lines = [
|
|
916
|
-
"import { createClivlySDK } from \"clivly/sdk\";",
|
|
1308
|
+
"import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
|
|
917
1309
|
"import { fromDrizzle } from \"clivly/drizzle\";",
|
|
918
|
-
"import {
|
|
919
|
-
"import { discoverFromDrizzle } from \"clivly/core/drizzle\";",
|
|
1310
|
+
"import { discoverFromDrizzle } from \"clivly/drizzle\";",
|
|
920
1311
|
`import * as schema from "${plan.importPath}";`
|
|
921
1312
|
];
|
|
922
|
-
if (!plan.db) lines.push(
|
|
1313
|
+
if (!plan.db) lines.push("import { db } from \"<YOUR_DB_MODULE>\"; // TODO(clivly): replace with the path to your Drizzle `db` instance — NOT your schema file");
|
|
923
1314
|
else if (!dbIsInSchemaModule(plan)) lines.push(`import { ${plan.db.moduleKey} } from "${plan.db.importPath}";`);
|
|
924
1315
|
return lines.join("\n");
|
|
925
1316
|
}
|
|
@@ -942,12 +1333,14 @@ ${entities.join(",\n")},
|
|
|
942
1333
|
});
|
|
943
1334
|
|
|
944
1335
|
export default createClivlySDK({
|
|
945
|
-
apiKey: process.env.CLIVLY_API_KEY!,
|
|
1336
|
+
apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY!,
|
|
946
1337
|
entities,
|
|
947
1338
|
// Every table in your Drizzle schema, so the Clivly dashboard can offer them
|
|
948
1339
|
// all when you add a mapping — not just the ones wired up as sources below.
|
|
949
1340
|
schema: discoverFromDrizzle(schema),
|
|
950
1341
|
source: {
|
|
1342
|
+
// Keyed by CRM concept — contacts/companies/deals/custom.
|
|
1343
|
+
// NOT by your entity key. The entity's own \`source:\` above is a table name.
|
|
951
1344
|
${sources.join(",\n")},
|
|
952
1345
|
},
|
|
953
1346
|
syncTrigger: {
|
|
@@ -955,7 +1348,8 @@ ${sources.join(",\n")},
|
|
|
955
1348
|
url: process.env.CLIVLY_SYNC_TRIGGER_URL,
|
|
956
1349
|
secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
|
|
957
1350
|
},
|
|
958
|
-
verifyToken
|
|
1351
|
+
// verifyToken is optional and only needed once a Clivly Cloud consumer calls
|
|
1352
|
+
// your /clivly/auth/verify endpoint; leave it unset otherwise.
|
|
959
1353
|
});
|
|
960
1354
|
`;
|
|
961
1355
|
}
|
|
@@ -1041,23 +1435,28 @@ const TS_EXTENSION = /\.ts$/;
|
|
|
1041
1435
|
const ROUTE_PATHS = {
|
|
1042
1436
|
nextjs: {
|
|
1043
1437
|
tick: "app/api/clivly/tick/route.ts",
|
|
1044
|
-
verify: "app/api/clivly/auth/verify/route.ts"
|
|
1438
|
+
verify: "app/api/clivly/auth/verify/route.ts",
|
|
1439
|
+
session: "app/api/clivly/widget/session/route.ts"
|
|
1045
1440
|
},
|
|
1046
1441
|
"tanstack-start": {
|
|
1047
1442
|
tick: "src/routes/api/clivly/tick.ts",
|
|
1048
|
-
verify: "src/routes/api/clivly/auth/verify.ts"
|
|
1443
|
+
verify: "src/routes/api/clivly/auth/verify.ts",
|
|
1444
|
+
session: "src/routes/api/clivly/widget/session.ts"
|
|
1049
1445
|
},
|
|
1050
1446
|
sveltekit: {
|
|
1051
1447
|
tick: "src/routes/api/clivly/tick/+server.ts",
|
|
1052
|
-
verify: "src/routes/api/clivly/auth/verify/+server.ts"
|
|
1448
|
+
verify: "src/routes/api/clivly/auth/verify/+server.ts",
|
|
1449
|
+
session: "src/routes/api/clivly/widget/session/+server.ts"
|
|
1053
1450
|
},
|
|
1054
1451
|
remix: {
|
|
1055
1452
|
tick: "app/routes/api.clivly.tick.ts",
|
|
1056
|
-
verify: "app/routes/api.clivly.auth.verify.ts"
|
|
1453
|
+
verify: "app/routes/api.clivly.auth.verify.ts",
|
|
1454
|
+
session: "app/routes/api.clivly.widget.session.ts"
|
|
1057
1455
|
},
|
|
1058
1456
|
nuxt: {
|
|
1059
1457
|
tick: "server/api/clivly/tick.ts",
|
|
1060
|
-
verify: "server/api/clivly/auth/verify.ts"
|
|
1458
|
+
verify: "server/api/clivly/auth/verify.ts",
|
|
1459
|
+
session: "server/api/clivly/widget/session.ts"
|
|
1061
1460
|
},
|
|
1062
1461
|
unknown: null
|
|
1063
1462
|
};
|
|
@@ -1087,14 +1486,27 @@ function renderAuthPreamble(routeFilePath, auth) {
|
|
|
1087
1486
|
lines.push("", auth.body);
|
|
1088
1487
|
return lines.join("\n");
|
|
1089
1488
|
}
|
|
1090
|
-
function
|
|
1091
|
-
|
|
1489
|
+
function renderPreamble(kind, importPath, auth, routeFilePath) {
|
|
1490
|
+
if (kind === "session") return `import { createChatSessionHandler } from "clivly/sdk";
|
|
1491
|
+
|
|
1492
|
+
const handler = createChatSessionHandler({
|
|
1493
|
+
apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY ?? "",
|
|
1494
|
+
allowedOrigins: [
|
|
1495
|
+
// TODO: add the origins your widget loads from, e.g. "https://example.com"
|
|
1496
|
+
],
|
|
1497
|
+
});
|
|
1498
|
+
`;
|
|
1499
|
+
if (kind === "verify") return `import clivly from "${importPath}";
|
|
1092
1500
|
${renderAuthPreamble(routeFilePath, auth)}
|
|
1093
1501
|
const handler = clivly.createAuthVerifyHandler(resolveUser);
|
|
1094
|
-
|
|
1502
|
+
`;
|
|
1503
|
+
return `import clivly from "${importPath}";
|
|
1095
1504
|
|
|
1096
1505
|
const handler = clivly.createClivlyHandler();
|
|
1097
1506
|
`;
|
|
1507
|
+
}
|
|
1508
|
+
function renderBody(framework, importPath, kind, auth, routeFilePath) {
|
|
1509
|
+
const preamble = renderPreamble(kind, importPath, auth, routeFilePath);
|
|
1098
1510
|
switch (framework) {
|
|
1099
1511
|
case "nextjs": return `${preamble}
|
|
1100
1512
|
export const POST = (request: Request) => handler(request);
|
|
@@ -1126,7 +1538,7 @@ export default defineEventHandler((event) => handler(toWebRequest(event)));
|
|
|
1126
1538
|
}
|
|
1127
1539
|
function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown")) {
|
|
1128
1540
|
const path = routePath(framework, kind);
|
|
1129
|
-
let body = renderBody(framework, configImport(path), kind
|
|
1541
|
+
let body = renderBody(framework, configImport(path), kind, auth, path);
|
|
1130
1542
|
if (framework === "tanstack-start") {
|
|
1131
1543
|
const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
|
|
1132
1544
|
body = body.replace("__ROUTE__", url);
|
|
@@ -1155,11 +1567,21 @@ function planScaffold(framework) {
|
|
|
1155
1567
|
...COMMON_FILES
|
|
1156
1568
|
];
|
|
1157
1569
|
}
|
|
1570
|
+
function planWidgetScaffold(framework) {
|
|
1571
|
+
const paths = ROUTE_PATHS[framework];
|
|
1572
|
+
if (!paths) return [];
|
|
1573
|
+
return [{
|
|
1574
|
+
path: paths.session,
|
|
1575
|
+
description: "Clivly chat-session route (createChatSessionHandler) — add allowedOrigins",
|
|
1576
|
+
owned: true
|
|
1577
|
+
}];
|
|
1578
|
+
}
|
|
1158
1579
|
//#endregion
|
|
1159
1580
|
//#region src/plan-mutations.ts
|
|
1160
|
-
function fileAction(
|
|
1161
|
-
if (
|
|
1162
|
-
|
|
1581
|
+
function fileAction(existing, next, force) {
|
|
1582
|
+
if (existing === null) return "create";
|
|
1583
|
+
if (existing === next) return "unchanged";
|
|
1584
|
+
return force ? "overwrite" : "conflict";
|
|
1163
1585
|
}
|
|
1164
1586
|
function hasKey(content, key) {
|
|
1165
1587
|
return new RegExp(`^\\s*${key}=`, "m").test(content);
|
|
@@ -1171,11 +1593,14 @@ function planFiles(args) {
|
|
|
1171
1593
|
const kind = path.includes("verify") ? "verify" : "tick";
|
|
1172
1594
|
return renderRouteFile(args.framework, kind, args.authWiring);
|
|
1173
1595
|
};
|
|
1174
|
-
for (const file of planScaffold(args.framework))
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1596
|
+
for (const file of planScaffold(args.framework)) {
|
|
1597
|
+
const contents = render(file.path);
|
|
1598
|
+
mutations.push({
|
|
1599
|
+
path: file.path,
|
|
1600
|
+
contents,
|
|
1601
|
+
action: fileAction(args.readFile((0, node_path.join)(args.cwd, file.path)), contents, args.force)
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1179
1604
|
return mutations;
|
|
1180
1605
|
}
|
|
1181
1606
|
function envAction(addedCount, existingLength) {
|
|
@@ -1198,6 +1623,16 @@ function planMutations(args) {
|
|
|
1198
1623
|
env: planEnv(args)
|
|
1199
1624
|
};
|
|
1200
1625
|
}
|
|
1626
|
+
function planWidgetMutations(args) {
|
|
1627
|
+
return planWidgetScaffold(args.framework).map((file) => {
|
|
1628
|
+
const contents = renderRouteFile(args.framework, "session");
|
|
1629
|
+
return {
|
|
1630
|
+
path: file.path,
|
|
1631
|
+
contents,
|
|
1632
|
+
action: fileAction(args.readFile((0, node_path.join)(args.cwd, file.path)), contents, args.force)
|
|
1633
|
+
};
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1201
1636
|
//#endregion
|
|
1202
1637
|
//#region src/probe-auth.ts
|
|
1203
1638
|
const AUTH_MODULE_PATHS = [
|
|
@@ -1217,8 +1652,8 @@ function authInstanceCandidates(cwd) {
|
|
|
1217
1652
|
return AUTH_MODULE_PATHS.map((path) => (0, node_path.join)(cwd, path));
|
|
1218
1653
|
}
|
|
1219
1654
|
function defaultLoader(cwd) {
|
|
1220
|
-
const jiti$
|
|
1221
|
-
return (specifier) => jiti$
|
|
1655
|
+
const jiti$1 = (0, jiti.createJiti)((0, node_url.pathToFileURL)((0, node_path.join)(cwd, "clivly.config.ts")).href);
|
|
1656
|
+
return (specifier) => jiti$1.import(specifier);
|
|
1222
1657
|
}
|
|
1223
1658
|
function probeAuthInstance(cwd, load) {
|
|
1224
1659
|
return probeModule({
|
|
@@ -1229,59 +1664,6 @@ function probeAuthInstance(cwd, load) {
|
|
|
1229
1664
|
});
|
|
1230
1665
|
}
|
|
1231
1666
|
//#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
1667
|
//#region src/push-schema.ts
|
|
1286
1668
|
async function runPushSchema(options) {
|
|
1287
1669
|
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
@@ -1350,11 +1732,40 @@ function defaultReadDrizzleConfigSchema(cwd) {
|
|
|
1350
1732
|
}
|
|
1351
1733
|
return null;
|
|
1352
1734
|
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Resolve a `--schema` value that names a workspace package rather than a file.
|
|
1737
|
+
*
|
|
1738
|
+
* Matches the longest package name that prefixes the specifier, so both
|
|
1739
|
+
* `@acme/database` and `@acme/database/src/schema.ts` work. Returns `undefined`
|
|
1740
|
+
* when nothing matched (caller falls back to path handling) and `null` when a
|
|
1741
|
+
* package matched but its entry could not be resolved — those are different
|
|
1742
|
+
* outcomes and the caller reports them differently.
|
|
1743
|
+
*/
|
|
1744
|
+
function resolveFromWorkspace(specifier, graph, deps) {
|
|
1745
|
+
const pkg = graph.packages.filter((pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)).sort((a, b) => b.name.length - a.name.length)[0];
|
|
1746
|
+
if (!pkg) return;
|
|
1747
|
+
if (specifier === pkg.name) return deps.resolveEntry(pkg.dir);
|
|
1748
|
+
const subpath = specifier.slice(pkg.name.length + 1);
|
|
1749
|
+
const candidate = (0, node_path.resolve)(pkg.dir, subpath);
|
|
1750
|
+
return deps.exists(candidate) ? candidate : deps.resolveEntry(pkg.dir);
|
|
1751
|
+
}
|
|
1353
1752
|
function resolveSchemaPath(opts) {
|
|
1354
1753
|
const exists = opts.deps?.exists ?? node_fs.existsSync;
|
|
1355
1754
|
const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
|
|
1755
|
+
const resolveEntry = opts.deps?.resolvePackageEntry ?? resolvePackageEntry;
|
|
1356
1756
|
const toAbs = (p) => (0, node_path.isAbsolute)(p) ? p : (0, node_path.resolve)(opts.cwd, p);
|
|
1357
|
-
if (opts.flag)
|
|
1757
|
+
if (opts.flag) {
|
|
1758
|
+
const asPath = toAbs(opts.flag);
|
|
1759
|
+
if (exists(asPath)) return asPath;
|
|
1760
|
+
if (opts.graph) {
|
|
1761
|
+
const fromWorkspace = resolveFromWorkspace(opts.flag, opts.graph, {
|
|
1762
|
+
exists,
|
|
1763
|
+
resolveEntry
|
|
1764
|
+
});
|
|
1765
|
+
if (fromWorkspace !== void 0) return fromWorkspace;
|
|
1766
|
+
}
|
|
1767
|
+
return asPath;
|
|
1768
|
+
}
|
|
1358
1769
|
const fromConfig = readSchema(opts.cwd);
|
|
1359
1770
|
if (fromConfig) return toAbs(fromConfig);
|
|
1360
1771
|
for (const candidate of COMMON_PATHS) {
|
|
@@ -1365,17 +1776,8 @@ function resolveSchemaPath(opts) {
|
|
|
1365
1776
|
}
|
|
1366
1777
|
//#endregion
|
|
1367
1778
|
//#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
1779
|
function packageDeps(dir) {
|
|
1378
|
-
const pkg =
|
|
1780
|
+
const pkg = readPackageManifest(dir);
|
|
1379
1781
|
return {
|
|
1380
1782
|
...pkg?.dependencies,
|
|
1381
1783
|
...pkg?.devDependencies
|
|
@@ -1399,41 +1801,6 @@ function findNearestPackageDir(start, boundary) {
|
|
|
1399
1801
|
current = parent;
|
|
1400
1802
|
}
|
|
1401
1803
|
}
|
|
1402
|
-
function readWorkspaceGlobs(dir) {
|
|
1403
|
-
const pkg = readPackageJson(dir);
|
|
1404
|
-
const workspaces = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
|
|
1405
|
-
if (Array.isArray(workspaces) && workspaces.length > 0) return workspaces;
|
|
1406
|
-
const pnpmWorkspacePath = (0, node_path.join)(dir, "pnpm-workspace.yaml");
|
|
1407
|
-
if (!(0, node_fs.existsSync)(pnpmWorkspacePath)) return [];
|
|
1408
|
-
try {
|
|
1409
|
-
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("!"));
|
|
1410
|
-
} catch {
|
|
1411
|
-
return [];
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
function expandWorkspaceGlob(root, pattern) {
|
|
1415
|
-
if (pattern.endsWith("/*")) {
|
|
1416
|
-
const base = (0, node_path.join)(root, pattern.slice(0, -2));
|
|
1417
|
-
if (!(0, node_fs.existsSync)(base)) return [];
|
|
1418
|
-
try {
|
|
1419
|
-
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")));
|
|
1420
|
-
} catch {
|
|
1421
|
-
return [];
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
const exact = (0, node_path.join)(root, pattern);
|
|
1425
|
-
return (0, node_fs.existsSync)((0, node_path.join)(exact, "package.json")) ? [exact] : [];
|
|
1426
|
-
}
|
|
1427
|
-
function findWorkspaceRoot(start, boundary) {
|
|
1428
|
-
let current = (0, node_path.resolve)(start);
|
|
1429
|
-
while (true) {
|
|
1430
|
-
if (current === boundary) return null;
|
|
1431
|
-
if (readWorkspaceGlobs(current).length > 0) return current;
|
|
1432
|
-
const parent = parentDir(current);
|
|
1433
|
-
if (!parent) return null;
|
|
1434
|
-
current = parent;
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
1804
|
function uniquePaths(paths) {
|
|
1438
1805
|
return [...new Set(paths.map((path) => (0, node_path.resolve)(path)))];
|
|
1439
1806
|
}
|
|
@@ -1489,7 +1856,7 @@ function resolveTargetApp(start, boundary = (0, node_os.homedir)()) {
|
|
|
1489
1856
|
}
|
|
1490
1857
|
//#endregion
|
|
1491
1858
|
//#region src/status-ladder.ts
|
|
1492
|
-
const SATISFYING = new Set(["pass", "warn"]);
|
|
1859
|
+
const SATISFYING = /* @__PURE__ */ new Set(["pass", "warn"]);
|
|
1493
1860
|
function unmetPrerequisites(stage, satisfied) {
|
|
1494
1861
|
return (stage.needs ?? []).filter((id) => !satisfied.has(id));
|
|
1495
1862
|
}
|
|
@@ -1539,10 +1906,118 @@ function ladderExitCode(results) {
|
|
|
1539
1906
|
return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
|
|
1540
1907
|
}
|
|
1541
1908
|
//#endregion
|
|
1909
|
+
//#region src/edge-safety.ts
|
|
1910
|
+
const NODE_BUILTINS = /* @__PURE__ */ new Set([
|
|
1911
|
+
"assert",
|
|
1912
|
+
"buffer",
|
|
1913
|
+
"child_process",
|
|
1914
|
+
"cluster",
|
|
1915
|
+
"crypto",
|
|
1916
|
+
"dns",
|
|
1917
|
+
"events",
|
|
1918
|
+
"fs",
|
|
1919
|
+
"http",
|
|
1920
|
+
"http2",
|
|
1921
|
+
"https",
|
|
1922
|
+
"module",
|
|
1923
|
+
"net",
|
|
1924
|
+
"os",
|
|
1925
|
+
"path",
|
|
1926
|
+
"perf_hooks",
|
|
1927
|
+
"process",
|
|
1928
|
+
"readline",
|
|
1929
|
+
"stream",
|
|
1930
|
+
"timers",
|
|
1931
|
+
"tls",
|
|
1932
|
+
"url",
|
|
1933
|
+
"util",
|
|
1934
|
+
"v8",
|
|
1935
|
+
"vm",
|
|
1936
|
+
"worker_threads",
|
|
1937
|
+
"zlib"
|
|
1938
|
+
]);
|
|
1939
|
+
const SIDE_EFFECT_MODULES = [/^dotenv\/config$/, /^dotenv\/lib\/env-options$/];
|
|
1940
|
+
const IMPORT_FROM = /(?:^|\s)(?:import|export)\b[^"']*from\s*["']([^"']+)["']/;
|
|
1941
|
+
const BARE_IMPORT = /^\s*import\s*["']([^"']+)["']\s*;?\s*$/;
|
|
1942
|
+
const REQUIRE_CALL = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/;
|
|
1943
|
+
const NEWLINE = /\r?\n/;
|
|
1944
|
+
const MODULE_SCOPE_CALL = /^([A-Za-z_$][\w$.]*)\s*\(/;
|
|
1945
|
+
const ALLOWED_CALLS = /* @__PURE__ */ new Set([
|
|
1946
|
+
"export",
|
|
1947
|
+
"return",
|
|
1948
|
+
"if",
|
|
1949
|
+
"for",
|
|
1950
|
+
"while"
|
|
1951
|
+
]);
|
|
1952
|
+
const SIDE_EFFECT_CALLS = /^(dotenv\.config|config|configDotenv|fileURLToPath|readFileSync|require|process\.chdir)$/;
|
|
1953
|
+
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.";
|
|
1954
|
+
function isNodeBuiltin(specifier) {
|
|
1955
|
+
if (specifier.startsWith("node:")) return true;
|
|
1956
|
+
return NODE_BUILTINS.has(specifier.split("/")[0] ?? "");
|
|
1957
|
+
}
|
|
1958
|
+
function stripComment(line) {
|
|
1959
|
+
const trimmed = line.trimStart();
|
|
1960
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) return "";
|
|
1961
|
+
return line;
|
|
1962
|
+
}
|
|
1963
|
+
function specifierIssue(specifier, line) {
|
|
1964
|
+
if (SIDE_EFFECT_MODULES.some((pattern) => pattern.test(specifier))) return {
|
|
1965
|
+
code: "module_scope_side_effect",
|
|
1966
|
+
detail: `imports "${specifier}" for its side effect`,
|
|
1967
|
+
hint: ENV_HINT,
|
|
1968
|
+
line
|
|
1969
|
+
};
|
|
1970
|
+
if (isNodeBuiltin(specifier)) return {
|
|
1971
|
+
code: "node_builtin_in_edge_config",
|
|
1972
|
+
detail: `imports "${specifier}", which does not exist on an edge runtime`,
|
|
1973
|
+
hint: ENV_HINT,
|
|
1974
|
+
line
|
|
1975
|
+
};
|
|
1976
|
+
return null;
|
|
1977
|
+
}
|
|
1978
|
+
function callIssue(source, line) {
|
|
1979
|
+
const name = MODULE_SCOPE_CALL.exec(source)?.[1];
|
|
1980
|
+
if (!name || ALLOWED_CALLS.has(name) || !SIDE_EFFECT_CALLS.test(name)) return null;
|
|
1981
|
+
return {
|
|
1982
|
+
code: "module_scope_side_effect",
|
|
1983
|
+
detail: `calls ${name}() at module scope`,
|
|
1984
|
+
hint: ENV_HINT,
|
|
1985
|
+
line
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Every reason `source` would fail to import on an edge runtime, in line order.
|
|
1990
|
+
* An empty array means the config is safe to import from the app.
|
|
1991
|
+
*/
|
|
1992
|
+
function analyzeEdgeSafety(source) {
|
|
1993
|
+
const issues = [];
|
|
1994
|
+
source.split(NEWLINE).forEach((rawLine, index) => {
|
|
1995
|
+
const line = stripComment(rawLine);
|
|
1996
|
+
if (line.trim() === "") return;
|
|
1997
|
+
const lineNumber = index + 1;
|
|
1998
|
+
const specifier = BARE_IMPORT.exec(line)?.[1] ?? IMPORT_FROM.exec(line)?.[1] ?? REQUIRE_CALL.exec(line)?.[1];
|
|
1999
|
+
if (specifier) {
|
|
2000
|
+
const issue = specifierIssue(specifier, lineNumber);
|
|
2001
|
+
if (issue) issues.push(issue);
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
const call = callIssue(line, lineNumber);
|
|
2005
|
+
if (call) issues.push(call);
|
|
2006
|
+
});
|
|
2007
|
+
return issues;
|
|
2008
|
+
}
|
|
2009
|
+
//#endregion
|
|
1542
2010
|
//#region src/status-stages.ts
|
|
2011
|
+
function defaultReadConfigSource(path) {
|
|
2012
|
+
try {
|
|
2013
|
+
return (0, node_fs.readFileSync)(path, "utf8");
|
|
2014
|
+
} catch {
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
1543
2018
|
const REASON_HINT = {
|
|
1544
2019
|
ok: "",
|
|
1545
|
-
invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
|
|
2020
|
+
invalid_api_key: "The key was rejected (401). Check CLIVLY_SECRET_KEY (or legacy CLIVLY_API_KEY).",
|
|
1546
2021
|
forbidden: "The key is valid but lacks access to this org (403).",
|
|
1547
2022
|
rate_limited: "Rate limited (429). Wait a moment and retry.",
|
|
1548
2023
|
server_error: "Clivly returned a server error (5xx). Retry shortly.",
|
|
@@ -1555,12 +2030,12 @@ const apiKeyStage = {
|
|
|
1555
2030
|
run(ctx) {
|
|
1556
2031
|
if (ctx.apiKey) return Promise.resolve({
|
|
1557
2032
|
state: "pass",
|
|
1558
|
-
detail: "
|
|
2033
|
+
detail: "Secret key is set."
|
|
1559
2034
|
});
|
|
1560
2035
|
return Promise.resolve({
|
|
1561
2036
|
state: "fail",
|
|
1562
|
-
detail: "
|
|
1563
|
-
hint: "Copy your
|
|
2037
|
+
detail: "Secret key is not set.",
|
|
2038
|
+
hint: "Copy your secret key from Settings → API keys into CLIVLY_SECRET_KEY in .env."
|
|
1564
2039
|
});
|
|
1565
2040
|
}
|
|
1566
2041
|
};
|
|
@@ -1601,12 +2076,38 @@ const configStage = {
|
|
|
1601
2076
|
}
|
|
1602
2077
|
}
|
|
1603
2078
|
};
|
|
1604
|
-
const LADDER_OWNED_CHECKS = new Set(["API key present", "Heartbeat"]);
|
|
2079
|
+
const LADDER_OWNED_CHECKS = /* @__PURE__ */ new Set(["API key present", "Heartbeat"]);
|
|
1605
2080
|
/** The full ladder, in the order a developer completes setup. */
|
|
1606
2081
|
const STATUS_STAGES = [
|
|
1607
2082
|
apiKeyStage,
|
|
1608
2083
|
heartbeatStage,
|
|
1609
2084
|
configStage,
|
|
2085
|
+
{
|
|
2086
|
+
id: "runtime",
|
|
2087
|
+
label: "clivly.config is edge-safe",
|
|
2088
|
+
run(ctx) {
|
|
2089
|
+
const source = (ctx.readConfigSource ?? defaultReadConfigSource)(ctx.configPath);
|
|
2090
|
+
if (source === null) return Promise.resolve({
|
|
2091
|
+
state: "skip",
|
|
2092
|
+
detail: "config not readable"
|
|
2093
|
+
});
|
|
2094
|
+
const issues = analyzeEdgeSafety(source);
|
|
2095
|
+
if (issues.length === 0) return Promise.resolve({
|
|
2096
|
+
state: "pass",
|
|
2097
|
+
detail: "no Node-only code at module scope"
|
|
2098
|
+
});
|
|
2099
|
+
return Promise.resolve({
|
|
2100
|
+
state: "fail",
|
|
2101
|
+
detail: issues.map((issue) => issue.detail).join("; "),
|
|
2102
|
+
hint: issues[0]?.hint,
|
|
2103
|
+
sub: issues.map((issue) => ({
|
|
2104
|
+
label: `${issue.code} (line ${issue.line})`,
|
|
2105
|
+
ok: false,
|
|
2106
|
+
detail: issue.detail
|
|
2107
|
+
}))
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
},
|
|
1610
2111
|
{
|
|
1611
2112
|
id: "setup-checks",
|
|
1612
2113
|
label: "Setup checks",
|
|
@@ -1640,7 +2141,7 @@ const STATUS_STAGES = [
|
|
|
1640
2141
|
return Promise.resolve({
|
|
1641
2142
|
state: "warn",
|
|
1642
2143
|
detail: "Not set — remote sync cannot call back into this app.",
|
|
1643
|
-
hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. Push-only setups can ignore this."
|
|
2144
|
+
hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. For local development run `clivly dev` — it opens a tunnel and registers the URL for you. Push-only setups can ignore this."
|
|
1644
2145
|
});
|
|
1645
2146
|
}
|
|
1646
2147
|
}
|
|
@@ -1654,7 +2155,11 @@ function defaultLoadSdk(path) {
|
|
|
1654
2155
|
const ALIAS_STAGE_IDS = {
|
|
1655
2156
|
status: STATUS_STAGES.map((stage) => stage.id),
|
|
1656
2157
|
ping: ["api-key", "heartbeat"],
|
|
1657
|
-
doctor: [
|
|
2158
|
+
doctor: [
|
|
2159
|
+
"config",
|
|
2160
|
+
"runtime",
|
|
2161
|
+
"setup-checks"
|
|
2162
|
+
],
|
|
1658
2163
|
connect: [
|
|
1659
2164
|
"api-key",
|
|
1660
2165
|
"heartbeat",
|
|
@@ -1713,6 +2218,14 @@ function flagValue(argv, flag) {
|
|
|
1713
2218
|
const index = argv.indexOf(flag);
|
|
1714
2219
|
return index >= 0 ? argv[index + 1] : void 0;
|
|
1715
2220
|
}
|
|
2221
|
+
function flagValues(argv, flag) {
|
|
2222
|
+
const values = [];
|
|
2223
|
+
for (const [index, entry] of argv.entries()) {
|
|
2224
|
+
const value = entry === flag ? argv[index + 1] : void 0;
|
|
2225
|
+
if (value !== void 0 && !value.startsWith("--")) values.push(value);
|
|
2226
|
+
}
|
|
2227
|
+
return values;
|
|
2228
|
+
}
|
|
1716
2229
|
const KNOWN_COMMANDS = [
|
|
1717
2230
|
"init",
|
|
1718
2231
|
"status",
|
|
@@ -1724,17 +2237,17 @@ const KNOWN_COMMANDS = [
|
|
|
1724
2237
|
function editDistance(a, b) {
|
|
1725
2238
|
const m = a.length;
|
|
1726
2239
|
const n = b.length;
|
|
1727
|
-
|
|
1728
|
-
|
|
2240
|
+
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
2241
|
+
let curr = Array.from({ length: n + 1 }, () => 0);
|
|
1729
2242
|
for (let i = 1; i <= m; i++) {
|
|
1730
2243
|
curr[0] = i;
|
|
1731
2244
|
for (let j = 1; j <= n; j++) {
|
|
1732
2245
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1733
|
-
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
2246
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
1734
2247
|
}
|
|
1735
|
-
|
|
2248
|
+
[prev, curr] = [curr, prev];
|
|
1736
2249
|
}
|
|
1737
|
-
return
|
|
2250
|
+
return prev[n] ?? 0;
|
|
1738
2251
|
}
|
|
1739
2252
|
function nearestCommand(input) {
|
|
1740
2253
|
let best;
|
|
@@ -1760,6 +2273,59 @@ function readDeps(cwd) {
|
|
|
1760
2273
|
return {};
|
|
1761
2274
|
}
|
|
1762
2275
|
}
|
|
2276
|
+
const ADD_USAGE = "Usage: clivly add widget [--force] [--dry-run]";
|
|
2277
|
+
const defaultAddDeps = {
|
|
2278
|
+
out: (line) => process.stdout.write(line),
|
|
2279
|
+
readFile: (path) => (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : null,
|
|
2280
|
+
writeFile: (path, contents) => {
|
|
2281
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
2282
|
+
(0, node_fs.writeFileSync)(path, contents, "utf8");
|
|
2283
|
+
},
|
|
2284
|
+
readDeps
|
|
2285
|
+
};
|
|
2286
|
+
function runAdd(argv, cwd, deps = defaultAddDeps) {
|
|
2287
|
+
const target = argv[0];
|
|
2288
|
+
if (target !== "widget") {
|
|
2289
|
+
deps.out(`${target ? `Unknown target "${target}". ` : "Missing target. "}Only \`widget\` is supported.\n${ADD_USAGE}\n`);
|
|
2290
|
+
return 1;
|
|
2291
|
+
}
|
|
2292
|
+
const framework = detectFramework(deps.readDeps(cwd));
|
|
2293
|
+
if (framework === "unknown") {
|
|
2294
|
+
deps.out("Couldn't detect your framework from package.json. `clivly add widget` supports Next.js, TanStack Start, SvelteKit, Remix, and Nuxt.\n");
|
|
2295
|
+
return 1;
|
|
2296
|
+
}
|
|
2297
|
+
const force = argv.includes("--force");
|
|
2298
|
+
const dryRun = argv.includes("--dry-run");
|
|
2299
|
+
const files = planWidgetMutations({
|
|
2300
|
+
framework,
|
|
2301
|
+
cwd,
|
|
2302
|
+
force,
|
|
2303
|
+
readFile: (path) => deps.readFile(path)
|
|
2304
|
+
});
|
|
2305
|
+
deps.out(`\nClivly add widget — ${FRAMEWORK_LABELS[framework]}\n\n`);
|
|
2306
|
+
if (dryRun) deps.out("Dry run — nothing was written.\n\n");
|
|
2307
|
+
for (const file of files) {
|
|
2308
|
+
if (dryRun) {
|
|
2309
|
+
deps.out(` ${file.action.padEnd(9)} ${file.path}\n`);
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
if (file.action === "conflict") {
|
|
2313
|
+
deps.out(` ! ${file.path} differs from what Clivly would write — left alone (--force to overwrite)\n`);
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
if (file.action === "unchanged") {
|
|
2317
|
+
deps.out(` unchanged ${file.path} (already up to date)\n`);
|
|
2318
|
+
continue;
|
|
2319
|
+
}
|
|
2320
|
+
deps.writeFile((0, node_path.join)(cwd, file.path), file.contents);
|
|
2321
|
+
deps.out(` ${file.action.padEnd(9)} ${file.path}\n`);
|
|
2322
|
+
}
|
|
2323
|
+
if (!dryRun) {
|
|
2324
|
+
const sessionPath = files[0]?.path ?? "";
|
|
2325
|
+
deps.out(`\nNext steps:\n 1. Add your site origins to allowedOrigins in ${sessionPath}\n 2. Create the widget + set allowed origins in the Clivly dashboard (Settings → Chat widget)\n 3. Embed it: <ClivlyChatWidget widgetId="…" />\n`);
|
|
2326
|
+
}
|
|
2327
|
+
return 0;
|
|
2328
|
+
}
|
|
1763
2329
|
function readOwnVersion() {
|
|
1764
2330
|
const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
1765
2331
|
for (const rel of ["./package.json", "../package.json"]) try {
|
|
@@ -1767,6 +2333,35 @@ function readOwnVersion() {
|
|
|
1767
2333
|
if (pkg.name === "clivly" && pkg.version) return pkg.version;
|
|
1768
2334
|
} catch {}
|
|
1769
2335
|
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Parse a `--db-import` / `--auth-import` value into a discovery-shaped override.
|
|
2338
|
+
*
|
|
2339
|
+
* `@acme/database` -> importPath "@acme/database", moduleKey <fallback>
|
|
2340
|
+
* `@acme/database#database` -> importPath "@acme/database", moduleKey "database"
|
|
2341
|
+
*
|
|
2342
|
+
* The optional `#exportName` suffix exists because a specifier alone doesn't say
|
|
2343
|
+
* which export holds the instance. Guessing wrong emits an import of a name that
|
|
2344
|
+
* isn't there, so the flag lets the developer say it outright while keeping the
|
|
2345
|
+
* conventional name as the default.
|
|
2346
|
+
*/
|
|
2347
|
+
function importOverride(flag, fallbackModuleKey) {
|
|
2348
|
+
const trimmed = flag?.trim();
|
|
2349
|
+
if (!trimmed) return null;
|
|
2350
|
+
const separator = trimmed.lastIndexOf("#");
|
|
2351
|
+
const importPath = separator === -1 ? trimmed : trimmed.slice(0, separator);
|
|
2352
|
+
const moduleKey = separator === -1 ? "" : trimmed.slice(separator + 1);
|
|
2353
|
+
if (importPath.length === 0) return null;
|
|
2354
|
+
return {
|
|
2355
|
+
importPath,
|
|
2356
|
+
moduleKey: moduleKey || fallbackModuleKey
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function authInstanceFromFlag(flag) {
|
|
2360
|
+
return importOverride(flag, "auth");
|
|
2361
|
+
}
|
|
2362
|
+
function defaultReadFile(path) {
|
|
2363
|
+
return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : null;
|
|
2364
|
+
}
|
|
1770
2365
|
function defaultReadEnv(cwd) {
|
|
1771
2366
|
const path = (0, node_path.join)(cwd, ".env");
|
|
1772
2367
|
return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : "";
|
|
@@ -1778,18 +2373,27 @@ function defaultIsInteractive() {
|
|
|
1778
2373
|
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
1779
2374
|
}
|
|
1780
2375
|
function resolveInitDeps(options) {
|
|
2376
|
+
const loadGraph = options.loadWorkspaceGraph ?? loadWorkspaceGraph;
|
|
2377
|
+
const graphCache = /* @__PURE__ */ new Map();
|
|
2378
|
+
const graphFor = (cwd) => {
|
|
2379
|
+
if (!graphCache.has(cwd)) graphCache.set(cwd, loadGraph(cwd));
|
|
2380
|
+
return graphCache.get(cwd) ?? null;
|
|
2381
|
+
};
|
|
1781
2382
|
return {
|
|
1782
2383
|
out: options.out ?? ((line) => process.stdout.write(line)),
|
|
1783
2384
|
readDepsFn: options.readDeps ?? readDeps,
|
|
2385
|
+
graphFor,
|
|
1784
2386
|
resolvePath: options.resolveSchemaPath ?? ((cwd) => resolveSchemaPath({
|
|
1785
2387
|
cwd,
|
|
1786
|
-
flag: options.schemaFlag
|
|
2388
|
+
flag: options.schemaFlag,
|
|
2389
|
+
graph: graphFor(cwd)
|
|
1787
2390
|
})),
|
|
1788
2391
|
discover: options.discoverSchema ?? ((schemaPath, cwd) => discoverSchema({
|
|
1789
2392
|
schemaPath,
|
|
1790
|
-
cwd
|
|
2393
|
+
cwd,
|
|
2394
|
+
graph: graphFor(cwd)
|
|
1791
2395
|
})),
|
|
1792
|
-
|
|
2396
|
+
readFile: options.readFile ?? defaultReadFile,
|
|
1793
2397
|
writeConfig: options.writeConfig ?? ((path, contents) => {
|
|
1794
2398
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
1795
2399
|
(0, node_fs.writeFileSync)(path, contents, "utf8");
|
|
@@ -1800,7 +2404,7 @@ function resolveInitDeps(options) {
|
|
|
1800
2404
|
readEnv: options.readEnv ?? defaultReadEnv,
|
|
1801
2405
|
readOwnVersion: options.readOwnVersion ?? readOwnVersion,
|
|
1802
2406
|
writeEnv: options.writeEnv ?? defaultWriteEnv,
|
|
1803
|
-
isInteractive: options.isInteractive ?? defaultIsInteractive,
|
|
2407
|
+
isInteractive: options.yes ? () => false : options.isInteractive ?? defaultIsInteractive,
|
|
1804
2408
|
login: options.login ?? ((cwd) => runLogin({
|
|
1805
2409
|
apiUrl: process.env.CLIVLY_API_URL ?? "https://api.clivly.com",
|
|
1806
2410
|
cwd,
|
|
@@ -1824,7 +2428,7 @@ async function acquireSchema(options, deps) {
|
|
|
1824
2428
|
const orm = detectOrm(appDeps);
|
|
1825
2429
|
const auth = detectAuth(appDeps);
|
|
1826
2430
|
deps.framework = framework;
|
|
1827
|
-
deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? await deps.probeAuth(options.cwd) : null);
|
|
2431
|
+
deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? authInstanceFromFlag(options.authImportFlag) ?? await deps.probeAuth(options.cwd) : null);
|
|
1828
2432
|
out("\nClivly init\n");
|
|
1829
2433
|
out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
|
|
1830
2434
|
out(` ORM: ${orm}\n`);
|
|
@@ -1874,14 +2478,18 @@ function mutationsFor(options, deps, plan) {
|
|
|
1874
2478
|
framework: deps.framework,
|
|
1875
2479
|
authWiring: deps.authWiring,
|
|
1876
2480
|
plan,
|
|
1877
|
-
|
|
2481
|
+
readFile: deps.readFile,
|
|
1878
2482
|
readEnv: deps.readEnv
|
|
1879
2483
|
});
|
|
1880
2484
|
}
|
|
1881
2485
|
function applyMutations(options, deps, mutations) {
|
|
1882
2486
|
for (const file of mutations.files) {
|
|
1883
|
-
if (file.action === "
|
|
1884
|
-
deps.out(`
|
|
2487
|
+
if (file.action === "unchanged") {
|
|
2488
|
+
deps.out(` · ${file.path} already up to date\n`);
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
if (file.action === "conflict") {
|
|
2492
|
+
deps.out(` ! ${file.path} differs from what init would write — left alone (--diff to see, --force to overwrite)\n`);
|
|
1885
2493
|
continue;
|
|
1886
2494
|
}
|
|
1887
2495
|
deps.writeConfig((0, node_path.join)(options.cwd, file.path), file.contents);
|
|
@@ -1894,29 +2502,42 @@ function applyMutations(options, deps, mutations) {
|
|
|
1894
2502
|
deps.writeEnv(options.cwd, mutations.env.contents);
|
|
1895
2503
|
deps.out(" ✓ .env placeholders\n");
|
|
1896
2504
|
}
|
|
1897
|
-
|
|
2505
|
+
const DIFF_CONTEXT_LIMIT = 40;
|
|
2506
|
+
function renderDiff(existing, next) {
|
|
2507
|
+
const before = existing.split("\n");
|
|
2508
|
+
const after = next.split("\n");
|
|
2509
|
+
const lines = [];
|
|
2510
|
+
for (const line of before.filter((l) => !after.includes(l))) lines.push(` - ${line}`);
|
|
2511
|
+
for (const line of after.filter((l) => !before.includes(l))) lines.push(` + ${line}`);
|
|
2512
|
+
return lines.length > DIFF_CONTEXT_LIMIT ? [...lines.slice(0, DIFF_CONTEXT_LIMIT), ` … ${lines.length - DIFF_CONTEXT_LIMIT} more lines`] : lines;
|
|
2513
|
+
}
|
|
2514
|
+
function previewMutations(deps, mutations, options) {
|
|
1898
2515
|
const { out } = deps;
|
|
1899
2516
|
out("\nDry run — nothing was written.\n\n");
|
|
1900
2517
|
for (const file of mutations.files) {
|
|
1901
|
-
const verb = {
|
|
1902
|
-
create: "create",
|
|
1903
|
-
overwrite: "overwrite",
|
|
1904
|
-
skip: "skip"
|
|
1905
|
-
}[file.action];
|
|
1906
2518
|
const lines = file.contents.split("\n").length;
|
|
1907
|
-
const suffix =
|
|
1908
|
-
|
|
2519
|
+
const suffix = {
|
|
2520
|
+
conflict: " (differs — --diff to see, --force to overwrite)",
|
|
2521
|
+
create: ` (${lines} lines)`,
|
|
2522
|
+
overwrite: ` (${lines} lines)`,
|
|
2523
|
+
unchanged: " (already up to date)"
|
|
2524
|
+
}[file.action];
|
|
2525
|
+
out(` ${file.action.padEnd(9)} ${file.path}${suffix}\n`);
|
|
2526
|
+
if (options.diff && file.action === "conflict") {
|
|
2527
|
+
const existing = deps.readFile((0, node_path.join)(options.cwd, file.path)) ?? "";
|
|
2528
|
+
for (const line of renderDiff(existing, file.contents)) out(`${line}\n`);
|
|
2529
|
+
}
|
|
1909
2530
|
}
|
|
1910
2531
|
if (mutations.env.addedKeys.length === 0) out(" skip .env (every CLIVLY_* key already present)\n");
|
|
1911
2532
|
else {
|
|
1912
2533
|
out(` ${mutations.env.action.padEnd(9)} .env\n`);
|
|
1913
2534
|
for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
|
|
1914
2535
|
}
|
|
1915
|
-
out("\nRe-run without --dry-run to apply.\n");
|
|
2536
|
+
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");
|
|
1916
2537
|
}
|
|
1917
2538
|
function printNextSteps(out, authComplete, loginRan) {
|
|
1918
2539
|
out("\nNext steps:\n");
|
|
1919
|
-
out(loginRan ? " 1. Credentials written by `clivly login`\n" : " 1. Add
|
|
2540
|
+
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");
|
|
1920
2541
|
out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
|
|
1921
2542
|
out(" Local dev? Run `clivly dev` to open a tunnel, or skip this step until you deploy (remote sync is deploy-only locally).\n");
|
|
1922
2543
|
const step = authComplete ? 3 : 4;
|
|
@@ -1924,7 +2545,7 @@ function printNextSteps(out, authComplete, loginRan) {
|
|
|
1924
2545
|
out(` ${step}. Run \`clivly status\` to see where you are\n`);
|
|
1925
2546
|
}
|
|
1926
2547
|
function credentialsMissing(envContents) {
|
|
1927
|
-
return !isKeySet(envContents, "CLIVLY_API_KEY");
|
|
2548
|
+
return !(isKeySet(envContents, "CLIVLY_SECRET_KEY") || isKeySet(envContents, "CLIVLY_API_KEY"));
|
|
1928
2549
|
}
|
|
1929
2550
|
function pairingSkipReason(options, deps) {
|
|
1930
2551
|
if (options.noLogin) return "--no-login";
|
|
@@ -1942,9 +2563,12 @@ async function scaffold(options, deps, plan) {
|
|
|
1942
2563
|
const pm = deps.detectPackageManager(options.cwd);
|
|
1943
2564
|
const packages = [resolveRuntimeInstallSpec(deps), ...deps.authWiring.packages];
|
|
1944
2565
|
const mutations = mutationsFor(options, deps, plan);
|
|
1945
|
-
if (options.dryRun) {
|
|
2566
|
+
if (options.dryRun || options.diff) {
|
|
1946
2567
|
out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
|
|
1947
|
-
previewMutations(deps, mutations
|
|
2568
|
+
previewMutations(deps, mutations, {
|
|
2569
|
+
cwd: options.cwd,
|
|
2570
|
+
diff: options.diff
|
|
2571
|
+
});
|
|
1948
2572
|
return 0;
|
|
1949
2573
|
}
|
|
1950
2574
|
const result = await deps.install({
|
|
@@ -1960,11 +2584,11 @@ async function scaffold(options, deps, plan) {
|
|
|
1960
2584
|
const skipReason = pairingSkipReason(options, deps);
|
|
1961
2585
|
if (skipReason) {
|
|
1962
2586
|
out(`\nSkipped browser pairing (${skipReason}).\n`);
|
|
1963
|
-
out(" Run `clivly login` from a terminal to write
|
|
2587
|
+
out(" Run `clivly login` from a terminal to write CLIVLY_SECRET_KEY and CLIVLY_SYNC_TRIGGER_SECRET.\n");
|
|
1964
2588
|
} else loginRan = await deps.login(options.cwd) === 0;
|
|
1965
2589
|
}
|
|
1966
2590
|
printNextSteps(out, deps.authWiring.complete, loginRan);
|
|
1967
|
-
return
|
|
2591
|
+
return 0;
|
|
1968
2592
|
}
|
|
1969
2593
|
function normalizeAppRoot(options) {
|
|
1970
2594
|
const resolution = (options.resolveTargetApp ?? resolveTargetApp)(options.cwd);
|
|
@@ -1994,9 +2618,45 @@ async function runInit(options) {
|
|
|
1994
2618
|
const deps = resolveInitDeps(resolvedOptions);
|
|
1995
2619
|
const acquired = await acquireSchema(resolvedOptions, deps);
|
|
1996
2620
|
if ("code" in acquired) return acquired.code;
|
|
1997
|
-
const
|
|
1998
|
-
|
|
1999
|
-
|
|
2621
|
+
const dbOverride = importOverride(resolvedOptions.dbImportFlag, "db");
|
|
2622
|
+
const plan = planInit(dbOverride ? {
|
|
2623
|
+
...acquired,
|
|
2624
|
+
db: dbOverride
|
|
2625
|
+
} : acquired, {
|
|
2626
|
+
contactTable: resolvedOptions.contactTableFlag,
|
|
2627
|
+
companyTable: resolvedOptions.companyTableFlag
|
|
2628
|
+
});
|
|
2629
|
+
const { contact } = plan;
|
|
2630
|
+
if (!contact) {
|
|
2631
|
+
printNoContactGuidance(deps.out, plan, resolvedOptions);
|
|
2632
|
+
return resolvedOptions.dryRun ? 0 : 1;
|
|
2633
|
+
}
|
|
2634
|
+
const renderable = {
|
|
2635
|
+
...plan,
|
|
2636
|
+
contact
|
|
2637
|
+
};
|
|
2638
|
+
printPlan(deps.out, renderable);
|
|
2639
|
+
return scaffold(resolvedOptions, deps, renderable);
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* What to do when no contact entity could be mapped. Lists the tables actually
|
|
2643
|
+
* discovered and the flag that picks one, so the next command succeeds rather
|
|
2644
|
+
* than leaving the developer to guess.
|
|
2645
|
+
*/
|
|
2646
|
+
function printNoContactGuidance(out, plan, options) {
|
|
2647
|
+
out("No usable contact entity\n\n");
|
|
2648
|
+
for (const todo of plan.todos) out(` • ${todo}\n`);
|
|
2649
|
+
if (plan.tableNames.length === 0) {
|
|
2650
|
+
out(`\n No tables were found in ${plan.importPath}.\n`);
|
|
2651
|
+
out(" Check that --schema points at a module exporting Drizzle tables.\n\n");
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
out(`\n Tables in ${plan.importPath}:\n`);
|
|
2655
|
+
for (const name of plan.tableNames) out(` ${name}\n`);
|
|
2656
|
+
const schemaArg = options.schemaFlag ? ` --schema ${options.schemaFlag}` : "";
|
|
2657
|
+
out("\n Pick the one that holds your people and re-run:\n");
|
|
2658
|
+
out(` clivly init${schemaArg} --contact-table ${plan.tableNames[0]}\n\n`);
|
|
2659
|
+
out(" Nothing was written — Clivly needs one entity before a config can load.\n\n");
|
|
2000
2660
|
}
|
|
2001
2661
|
function hasCloudflaredOnPath() {
|
|
2002
2662
|
const pathEnv = process.env.PATH ?? "";
|
|
@@ -2156,16 +2816,30 @@ function runInitFromArgv(argv, cwd) {
|
|
|
2156
2816
|
cwd,
|
|
2157
2817
|
force: argv.includes("--force"),
|
|
2158
2818
|
dryRun: argv.includes("--dry-run"),
|
|
2819
|
+
diff: argv.includes("--diff"),
|
|
2820
|
+
yes: argv.includes("--yes"),
|
|
2159
2821
|
noLogin: argv.includes("--no-login"),
|
|
2160
2822
|
openBrowser: !argv.includes("--no-browser"),
|
|
2161
|
-
schemaFlag: flagValue(argv, "--schema")
|
|
2823
|
+
schemaFlag: flagValue(argv, "--schema"),
|
|
2824
|
+
dbImportFlag: flagValue(argv, "--db-import"),
|
|
2825
|
+
authImportFlag: flagValue(argv, "--auth-import"),
|
|
2826
|
+
contactTableFlag: flagValue(argv, "--contact-table"),
|
|
2827
|
+
companyTableFlag: flagValue(argv, "--company-table")
|
|
2162
2828
|
});
|
|
2163
2829
|
}
|
|
2164
2830
|
async function main(argv) {
|
|
2165
2831
|
const [command, ...rest] = argv;
|
|
2166
|
-
|
|
2832
|
+
let cliEnvFiles = [];
|
|
2833
|
+
try {
|
|
2834
|
+
cliEnvFiles = (await loadCliConfig(process.cwd()))?.envFiles ?? [];
|
|
2835
|
+
} catch (error) {
|
|
2836
|
+
process.stderr.write(`${error.message}\n`);
|
|
2837
|
+
return 1;
|
|
2838
|
+
}
|
|
2839
|
+
loadEnv(process.cwd(), process.env, { files: [...cliEnvFiles, ...flagValues(rest, "--env-file")] });
|
|
2167
2840
|
switch (command) {
|
|
2168
2841
|
case "init": return await runInitFromArgv(rest, process.cwd());
|
|
2842
|
+
case "add": return runAdd(rest, process.cwd());
|
|
2169
2843
|
case "push-schema": return await runPushSchema({
|
|
2170
2844
|
cwd: process.cwd(),
|
|
2171
2845
|
configPath: flagValue(rest, "--config")
|
|
@@ -2175,14 +2849,14 @@ async function main(argv) {
|
|
|
2175
2849
|
case "doctor":
|
|
2176
2850
|
case "connect": return await runStatus({
|
|
2177
2851
|
cwd: process.cwd(),
|
|
2178
|
-
apiKey: process.env
|
|
2852
|
+
apiKey: readSecretKeyEnv(process.env),
|
|
2179
2853
|
apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
|
|
2180
2854
|
configPath: flagValue(rest, "--config"),
|
|
2181
2855
|
only: ALIAS_STAGE_IDS[command],
|
|
2182
2856
|
verbose: rest.includes("--verbose")
|
|
2183
2857
|
});
|
|
2184
2858
|
case "dev": {
|
|
2185
|
-
const apiKey = process.env
|
|
2859
|
+
const apiKey = readSecretKeyEnv(process.env);
|
|
2186
2860
|
const apiUrl = flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL ?? "https://api.clivly.com";
|
|
2187
2861
|
return await runDev({
|
|
2188
2862
|
cwd: process.cwd(),
|
|
@@ -2210,7 +2884,7 @@ async function main(argv) {
|
|
|
2210
2884
|
case void 0:
|
|
2211
2885
|
case "help":
|
|
2212
2886
|
case "--help":
|
|
2213
|
-
process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--dry-run] [--
|
|
2887
|
+
process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--dry-run] [--diff] [--yes] [--no-login] [--no-browser]\n [--schema <path|package>] [--db-import <specifier>] [--auth-import <specifier>]\n Scaffold Clivly (config, routes, .env) for your app\n clivly add widget [--force] [--dry-run] Scaffold the chat-session route\n clivly status [--verbose] [--config <path>] [--env-file <path>] Check your setup, step by step\n clivly push-schema [--config <path>] Report your schema to Clivly without deploying\n clivly dev [--port <n>] [--api-url <url>] [--route-path <path>] [-- <cmd>] Tunnel localhost for remote sync\n clivly login [--force] [--rotate] [--api-url <url>] [--no-browser] Pair this machine and write CLIVLY_SECRET_KEY / CLIVLY_SYNC_TRIGGER_SECRET to .env\n clivly --version Print the installed clivly version\n");
|
|
2214
2888
|
return 0;
|
|
2215
2889
|
default: {
|
|
2216
2890
|
process.stdout.write(`Unknown command: ${command}\n`);
|
|
@@ -2225,7 +2899,9 @@ if (isEntrypoint(process.argv[1], require("url").pathToFileURL(__filename).href)
|
|
|
2225
2899
|
process.exitCode = code;
|
|
2226
2900
|
});
|
|
2227
2901
|
//#endregion
|
|
2902
|
+
exports.credentialsMissing = credentialsMissing;
|
|
2228
2903
|
exports.main = main;
|
|
2229
2904
|
exports.nearestCommand = nearestCommand;
|
|
2905
|
+
exports.runAdd = runAdd;
|
|
2230
2906
|
exports.runInit = runInit;
|
|
2231
2907
|
exports.runInitFromArgv = runInitFromArgv;
|