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