clivly 0.2.0 → 0.2.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 +52 -3
- package/adapter-nLfZpgIG.d.cts +40 -0
- package/adapter-xs04z0KX.d.mts +40 -0
- package/auth-adapter-CBOKDhlc.cjs +11 -0
- package/auth-adapter-FEOR1nx-.d.mts +30 -0
- package/auth-adapter-wqnnEjo3.mjs +6 -0
- package/auth-adapter-wvfGKsOJ.d.cts +30 -0
- package/auth-better-auth.cjs +28 -0
- package/auth-better-auth.d.cts +33 -0
- package/auth-better-auth.d.mts +33 -0
- package/auth-better-auth.mjs +26 -0
- package/auth-clerk.cjs +34 -0
- package/auth-clerk.d.cts +32 -0
- package/auth-clerk.d.mts +32 -0
- package/auth-clerk.mjs +32 -0
- package/auth-workos.cjs +47 -0
- package/auth-workos.d.cts +30 -0
- package/auth-workos.d.mts +30 -0
- package/auth-workos.mjs +45 -0
- package/core-adapter.cjs +0 -0
- package/core-adapter.d.cts +2 -0
- package/core-adapter.d.mts +2 -0
- package/core-adapter.mjs +1 -0
- package/core-auth-adapter.cjs +3 -0
- package/core-auth-adapter.d.cts +2 -0
- package/core-auth-adapter.d.mts +2 -0
- package/core-auth-adapter.mjs +2 -0
- package/core-drizzle.cjs +32 -0
- package/core-drizzle.d.cts +21 -0
- package/core-drizzle.d.mts +21 -0
- package/core-drizzle.mjs +31 -0
- package/core-entity-config.cjs +13 -0
- package/core-entity-config.d.cts +2 -0
- package/core-entity-config.d.mts +2 -0
- package/core-entity-config.mjs +2 -0
- package/core-entity-heuristics.cjs +442 -0
- package/core-entity-heuristics.d.cts +68 -0
- package/core-entity-heuristics.d.mts +68 -0
- package/core-entity-heuristics.mjs +431 -0
- package/core-mapping-form.cjs +5 -0
- package/core-mapping-form.d.cts +2 -0
- package/core-mapping-form.d.mts +2 -0
- package/core-mapping-form.mjs +2 -0
- package/core-mapping-score.cjs +12 -0
- package/core-mapping-score.d.cts +2 -0
- package/core-mapping-score.d.mts +2 -0
- package/core-mapping-score.mjs +2 -0
- package/core-mappings-config.cjs +3 -0
- package/core-mappings-config.d.cts +2 -0
- package/core-mappings-config.d.mts +2 -0
- package/core-mappings-config.mjs +2 -0
- package/core-sync-engine.cjs +6 -0
- package/core-sync-engine.d.cts +2 -0
- package/core-sync-engine.d.mts +2 -0
- package/core-sync-engine.mjs +2 -0
- package/core-types.cjs +0 -0
- package/core-types.d.cts +2 -0
- package/core-types.d.mts +2 -0
- package/core-types.mjs +1 -0
- package/core-view-compiler.cjs +5 -0
- package/core-view-compiler.d.cts +2 -0
- package/core-view-compiler.d.mts +2 -0
- package/core-view-compiler.mjs +2 -0
- package/core.cjs +27 -0
- package/core.d.cts +9 -0
- package/core.d.mts +9 -0
- package/core.mjs +7 -0
- package/drizzle-meta-BGn0BqGb.cjs +9 -0
- package/drizzle-meta-CnC336L-.mjs +4 -0
- package/drizzle.cjs +107 -0
- package/drizzle.d.cts +28 -0
- package/drizzle.d.mts +28 -0
- package/drizzle.mjs +105 -0
- package/entity-config-Cvjo1cpv.cjs +4598 -0
- package/entity-config-D1DFyeUp.d.mts +2403 -0
- package/entity-config-DoR0khce.mjs +4533 -0
- package/entity-config-GczjD3y8.d.cts +2403 -0
- package/index.cjs +926 -0
- package/index.d.cts +56 -0
- package/index.d.mts +56 -0
- package/{dist/index.mjs → index.mjs} +430 -47
- package/mapping-form-BHtPexzI.mjs +47 -0
- package/mapping-form-CUlFVfG8.d.cts +35 -0
- package/mapping-form-DdmDebC3.cjs +64 -0
- package/mapping-form-kjSiJaCX.d.mts +35 -0
- package/mapping-score-A2lRnlpk.d.mts +49 -0
- package/mapping-score-BJNe0lFn.mjs +87 -0
- package/mapping-score-BcjC0ZQs.d.cts +49 -0
- package/mapping-score-D9gfhtRu.cjs +146 -0
- package/mappings-config-CYPrzB3K.mjs +99 -0
- package/mappings-config-DTu_2cyx.cjs +104 -0
- package/mappings-config-DjRLCb2Q.d.cts +42 -0
- package/mappings-config-GxiYsSBQ.d.mts +42 -0
- package/namespace-probe-BtS0nXcr.d.cts +299 -0
- package/namespace-probe-CYMie-BI.d.mts +299 -0
- package/package.json +223 -26
- package/sdk-BZUn6wzs.mjs +764 -0
- package/sdk-CP_mYZa5.cjs +793 -0
- package/sdk.cjs +7 -0
- package/sdk.d.cts +33 -0
- package/sdk.d.mts +33 -0
- package/sdk.mjs +2 -0
- package/sync-engine-DUc0qWgo.cjs +230 -0
- package/sync-engine-DWnQPjV3.d.mts +183 -0
- package/sync-engine-DedXngop.mjs +207 -0
- package/sync-engine-uhfzKM-e.d.cts +183 -0
- package/types-C4POUcvk.d.mts +158 -0
- package/types-LJTBhkcN.d.cts +158 -0
- package/view-compiler-B4UYTvRM.d.cts +71 -0
- package/view-compiler-BHzbI9b7.cjs +202 -0
- package/view-compiler-CosFGar8.mjs +185 -0
- package/view-compiler-i_xFf-L0.d.mts +71 -0
- package/vite.cjs +38 -0
- package/vite.d.cts +23 -0
- package/vite.d.mts +23 -0
- package/vite.mjs +37 -0
|
@@ -1,11 +1,83 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { t as createClivlySDK } from "./sdk-BZUn6wzs.mjs";
|
|
3
|
+
import { t as CANONICAL_FIELDS } from "./entity-config-DoR0khce.mjs";
|
|
4
|
+
import "./mapping-score-BJNe0lFn.mjs";
|
|
5
|
+
import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./core-entity-heuristics.mjs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
8
|
import { createJiti } from "jiti";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
//#region src/render.ts
|
|
12
|
+
function checkLine(ok, label, detail) {
|
|
13
|
+
return ` ${ok ? "✅" : "❌"} ${label}${detail ? ` — ${detail}` : ""}\n`;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/connect.ts
|
|
17
|
+
const REASON_HINT$1 = {
|
|
18
|
+
ok: "",
|
|
19
|
+
invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
|
|
20
|
+
forbidden: "The key lacks access (403).",
|
|
21
|
+
rate_limited: "Rate limited (429) — retry shortly.",
|
|
22
|
+
server_error: "Clivly returned a 5xx — retry shortly.",
|
|
23
|
+
client_error: "The request was rejected (4xx).",
|
|
24
|
+
network_error: "No response — check the network / apiUrl."
|
|
25
|
+
};
|
|
26
|
+
const defaultConnect$1 = (apiKey, apiUrl) => createClivlySDK({
|
|
27
|
+
apiKey,
|
|
28
|
+
apiUrl,
|
|
29
|
+
retry: { maxAttempts: 1 }
|
|
30
|
+
}).connect();
|
|
31
|
+
const defaultLoadConfig = async (path) => {
|
|
32
|
+
const mod = await createJiti(import.meta.url).import(path);
|
|
33
|
+
return mod.default ?? mod;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* `clivly connect` — a read-only pass/fail report: API key present → heartbeat
|
|
37
|
+
* (real SDK connect, proving the cloud saw it) → config schema loadable →
|
|
38
|
+
* trigger-URL note. Exit 0 iff the first three pass; a missing trigger URL warns
|
|
39
|
+
* but does not fail (push-only setups are valid). Deps injectable for tests.
|
|
40
|
+
*/
|
|
41
|
+
async function runConnect(options) {
|
|
42
|
+
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
43
|
+
out("\nClivly connect\n");
|
|
44
|
+
if (!options.apiKey) {
|
|
45
|
+
out(checkLine(false, "CLIVLY_API_KEY is not set."));
|
|
46
|
+
out(" Set it to your sk_live_… key from Settings → API keys.\n");
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
const result = await (options.connectFn ?? defaultConnect$1)(options.apiKey, options.apiUrl);
|
|
50
|
+
if (result.ok) out(checkLine(true, `Heartbeat reached Clivly — org: ${result.org?.name ?? "(unnamed org)"}${result.org?.id ? ` (${result.org.id})` : ""}`));
|
|
51
|
+
else {
|
|
52
|
+
const status = result.status === null ? "no response" : `HTTP ${result.status}`;
|
|
53
|
+
out(checkLine(false, `Heartbeat failed (${result.reason}, ${status})`));
|
|
54
|
+
out(` ${REASON_HINT$1[result.reason]}\n`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const path = resolve(options.cwd, options.configPath ?? "clivly.config.ts");
|
|
58
|
+
const loadConfig = options.loadConfigFn ?? defaultLoadConfig;
|
|
59
|
+
let config;
|
|
60
|
+
try {
|
|
61
|
+
config = await loadConfig(path);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
out(checkLine(false, "Load clivly.config", error.message));
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
const tables = Object.values(config.entities?.entities ?? {}).map((e) => e.source).filter((s) => Boolean(s));
|
|
67
|
+
if (tables.length === 0) {
|
|
68
|
+
out(checkLine(false, "No entities configured in clivly.config"));
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
out(checkLine(true, `Schema: ${tables.length} entity table(s) — ${tables.join(", ")}`));
|
|
72
|
+
const triggerUrl = config.syncTrigger?.url;
|
|
73
|
+
if (triggerUrl) out(checkLine(true, `Trigger route configured — ${triggerUrl}`));
|
|
74
|
+
else {
|
|
75
|
+
out(checkLine(false, "No trigger URL — dashboard-triggered sync is not wired"));
|
|
76
|
+
out(" Set syncTrigger.url (CLIVLY_SYNC_TRIGGER_URL) to your public tick route.\n");
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
9
81
|
//#region src/detect.ts
|
|
10
82
|
const FRAMEWORK_MARKERS = [
|
|
11
83
|
{
|
|
@@ -59,6 +131,34 @@ function detectOrm(deps) {
|
|
|
59
131
|
for (const { orm, deps: markers } of ORM_MARKERS) if (markers.some((dep) => dep in deps)) return orm;
|
|
60
132
|
return "unknown";
|
|
61
133
|
}
|
|
134
|
+
const LOCKFILES = [
|
|
135
|
+
{
|
|
136
|
+
file: "pnpm-lock.yaml",
|
|
137
|
+
pm: "pnpm"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
file: "bun.lockb",
|
|
141
|
+
pm: "bun"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
file: "yarn.lock",
|
|
145
|
+
pm: "yarn"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
file: "package-lock.json",
|
|
149
|
+
pm: "npm"
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
function detectPackageManager(cwd, fileExists = existsSync) {
|
|
153
|
+
for (const { file, pm } of LOCKFILES) if (fileExists(join(cwd, file))) return pm;
|
|
154
|
+
return "npm";
|
|
155
|
+
}
|
|
156
|
+
function addCommand(pm, packages) {
|
|
157
|
+
return {
|
|
158
|
+
command: pm,
|
|
159
|
+
args: [pm === "npm" ? "install" : "add", ...packages]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
62
162
|
//#endregion
|
|
63
163
|
//#region src/discover-schema.ts
|
|
64
164
|
const WINDOWS_SEP = /\\/g;
|
|
@@ -81,20 +181,60 @@ function defaultLoader(cwd) {
|
|
|
81
181
|
const jiti = createJiti(pathToFileURL(join(cwd, "clivly.config.ts")).href);
|
|
82
182
|
return (specifier) => jiti.import(specifier);
|
|
83
183
|
}
|
|
184
|
+
async function loadPgCoreRuntime(load) {
|
|
185
|
+
try {
|
|
186
|
+
return await load("drizzle-orm/pg-core");
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function buildFkByColumn(pgCore, drizzle, table) {
|
|
192
|
+
const fkByColumn = /* @__PURE__ */ new Map();
|
|
193
|
+
if (!pgCore) return fkByColumn;
|
|
194
|
+
try {
|
|
195
|
+
for (const fk of pgCore.getTableConfig(table).foreignKeys) {
|
|
196
|
+
const ref = fk.reference();
|
|
197
|
+
const foreignTable = drizzle.getTableName(ref.foreignTable);
|
|
198
|
+
ref.columns.forEach((col, i) => {
|
|
199
|
+
const foreignCol = ref.foreignColumns[i];
|
|
200
|
+
if (foreignCol) fkByColumn.set(col.name, {
|
|
201
|
+
table: foreignTable,
|
|
202
|
+
column: foreignCol.name
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} catch {}
|
|
207
|
+
return fkByColumn;
|
|
208
|
+
}
|
|
209
|
+
function toDiscoveredColumn(key, col, fkByColumn) {
|
|
210
|
+
let type;
|
|
211
|
+
try {
|
|
212
|
+
type = col.getSQLType?.();
|
|
213
|
+
} catch {
|
|
214
|
+
type = void 0;
|
|
215
|
+
}
|
|
216
|
+
const references = fkByColumn.get(col.name);
|
|
217
|
+
return {
|
|
218
|
+
key,
|
|
219
|
+
dbName: col.name,
|
|
220
|
+
isPrimary: Boolean(col.primary),
|
|
221
|
+
type,
|
|
222
|
+
nullable: !col.notNull,
|
|
223
|
+
...references ? { references } : {}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
84
226
|
async function discoverSchema(opts) {
|
|
85
227
|
const load = opts.load ?? defaultLoader(opts.cwd);
|
|
86
228
|
const mod = await loadSchemaModule(opts.schemaPath, load);
|
|
87
229
|
const drizzle = await load("drizzle-orm");
|
|
230
|
+
const pgCore = await loadPgCoreRuntime(load);
|
|
88
231
|
const importPath = toImportPath(opts.schemaPath, opts.cwd);
|
|
89
232
|
const tables = [];
|
|
90
233
|
let db = null;
|
|
91
234
|
for (const [moduleKey, value] of Object.entries(mod)) {
|
|
92
235
|
if (drizzle.is(value, drizzle.Table)) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
dbName: col.name,
|
|
96
|
-
isPrimary: Boolean(col.primary)
|
|
97
|
-
}));
|
|
236
|
+
const fkByColumn = buildFkByColumn(pgCore, drizzle, value);
|
|
237
|
+
const columns = Object.entries(drizzle.getTableColumns(value)).map(([key, col]) => toDiscoveredColumn(key, col, fkByColumn));
|
|
98
238
|
tables.push({
|
|
99
239
|
moduleKey,
|
|
100
240
|
tableName: drizzle.getTableName(value),
|
|
@@ -114,11 +254,6 @@ async function discoverSchema(opts) {
|
|
|
114
254
|
};
|
|
115
255
|
}
|
|
116
256
|
//#endregion
|
|
117
|
-
//#region src/render.ts
|
|
118
|
-
function checkLine(ok, label, detail) {
|
|
119
|
-
return ` ${ok ? "✅" : "❌"} ${label}${detail ? ` — ${detail}` : ""}\n`;
|
|
120
|
-
}
|
|
121
|
-
//#endregion
|
|
122
257
|
//#region src/doctor.ts
|
|
123
258
|
async function loadSdkFromConfig(path) {
|
|
124
259
|
const mod = await createJiti(import.meta.url).import(path);
|
|
@@ -155,6 +290,66 @@ async function runDoctor(options) {
|
|
|
155
290
|
return report.ok ? 0 : 1;
|
|
156
291
|
}
|
|
157
292
|
//#endregion
|
|
293
|
+
//#region src/env-file.ts
|
|
294
|
+
function hasKey(content, key) {
|
|
295
|
+
return new RegExp(`^\\s*${key}=`, "m").test(content);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Append only the `entries` whose key is absent, each preceded by its comment.
|
|
299
|
+
* Existing keys/values are left exactly as-is. Returns the full new content.
|
|
300
|
+
*/
|
|
301
|
+
function appendEnvKeys(existing, entries) {
|
|
302
|
+
const missing = entries.filter((e) => !hasKey(existing, e.key));
|
|
303
|
+
if (missing.length === 0) return existing;
|
|
304
|
+
const block = missing.map((e) => `${e.comment ? `# ${e.comment}\n` : ""}${e.key}=`).join("\n");
|
|
305
|
+
return `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${existing.length > 0 ? "\n" : ""}# Clivly\n${block}\n`;
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/install.ts
|
|
309
|
+
const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject) => {
|
|
310
|
+
const child = spawn(command, args, {
|
|
311
|
+
cwd,
|
|
312
|
+
stdio: "inherit"
|
|
313
|
+
});
|
|
314
|
+
child.on("error", reject);
|
|
315
|
+
child.on("close", (code) => resolvePromise(code ?? 0));
|
|
316
|
+
});
|
|
317
|
+
/**
|
|
318
|
+
* Best-effort install: run `<pm> add <packages>`. Never throws — returns
|
|
319
|
+
* `{ ok, command }` so the caller can print the exact command on failure and
|
|
320
|
+
* continue scaffolding.
|
|
321
|
+
*/
|
|
322
|
+
async function installPackages(options) {
|
|
323
|
+
const { command, args } = addCommand(options.pm, options.packages);
|
|
324
|
+
const printable = `${command} ${args.join(" ")}`;
|
|
325
|
+
const run = options.run ?? defaultRun;
|
|
326
|
+
try {
|
|
327
|
+
return {
|
|
328
|
+
ok: await run(command, args, options.cwd) === 0,
|
|
329
|
+
command: printable
|
|
330
|
+
};
|
|
331
|
+
} catch {
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
command: printable
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/is-entry.ts
|
|
340
|
+
function isEntrypoint(argv1, moduleUrl, realpath = realpathSync) {
|
|
341
|
+
if (!argv1) return false;
|
|
342
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
343
|
+
const resolve = (path) => {
|
|
344
|
+
try {
|
|
345
|
+
return realpath(path);
|
|
346
|
+
} catch {
|
|
347
|
+
return path;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
return resolve(argv1) === resolve(modulePath);
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
158
353
|
//#region src/ping.ts
|
|
159
354
|
const REASON_HINT = {
|
|
160
355
|
ok: "",
|
|
@@ -202,6 +397,20 @@ async function runPing(options) {
|
|
|
202
397
|
}
|
|
203
398
|
//#endregion
|
|
204
399
|
//#region src/plan-init.ts
|
|
400
|
+
function toScoredTable(table) {
|
|
401
|
+
return {
|
|
402
|
+
name: table.tableName,
|
|
403
|
+
columns: table.columns.map((c) => c.dbName),
|
|
404
|
+
columnsMeta: table.columns.map((c) => ({
|
|
405
|
+
name: c.dbName,
|
|
406
|
+
type: c.type,
|
|
407
|
+
nullable: c.nullable,
|
|
408
|
+
isPrimaryKey: c.isPrimary,
|
|
409
|
+
isForeignKey: c.references !== void 0,
|
|
410
|
+
...c.references ? { references: c.references } : {}
|
|
411
|
+
}))
|
|
412
|
+
};
|
|
413
|
+
}
|
|
205
414
|
function rankCandidates(tables, concept) {
|
|
206
415
|
return tables.map((t) => ({
|
|
207
416
|
t,
|
|
@@ -243,7 +452,7 @@ function chooseSyncFields(table, todos) {
|
|
|
243
452
|
}
|
|
244
453
|
function buildEntity(table, concept, todos) {
|
|
245
454
|
const canonical = CANONICAL_FIELDS[concept];
|
|
246
|
-
const suggested =
|
|
455
|
+
const suggested = toFieldMap(scoreFieldMap(concept, toScoredTable(table)));
|
|
247
456
|
const fields = {};
|
|
248
457
|
for (const [key, column] of Object.entries(suggested)) if (canonical.includes(key)) fields[key] = column;
|
|
249
458
|
for (const required of requiredFieldsFor(concept)) if (!fields[required]) todos.push(`Could not map required ${concept} field "${required}" on "${table.tableName}" — add it manually.`);
|
|
@@ -282,17 +491,13 @@ function planInit(schema) {
|
|
|
282
491
|
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.`);
|
|
283
492
|
}
|
|
284
493
|
if (contactTable) {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const only = suggestions[0];
|
|
293
|
-
if (suggestions.length === 1 && only) contact.relationship = {
|
|
294
|
-
name: only.name,
|
|
295
|
-
via: only.via
|
|
494
|
+
const scored = scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
|
|
495
|
+
const top = scored[0];
|
|
496
|
+
const runnerUp = scored[1];
|
|
497
|
+
const isClearWinner = top !== void 0 && (runnerUp === void 0 || top.score - runnerUp.score >= 15);
|
|
498
|
+
if (top && isClearWinner) contact.relationship = {
|
|
499
|
+
name: top.value.name,
|
|
500
|
+
via: top.value.via
|
|
296
501
|
};
|
|
297
502
|
}
|
|
298
503
|
return {
|
|
@@ -306,6 +511,24 @@ function planInit(schema) {
|
|
|
306
511
|
}
|
|
307
512
|
//#endregion
|
|
308
513
|
//#region src/render-config.ts
|
|
514
|
+
const CLIVLY_ENV_KEYS = [
|
|
515
|
+
{
|
|
516
|
+
key: "CLIVLY_API_KEY",
|
|
517
|
+
comment: "Org API key — Settings → API keys"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
key: "CLIVLY_SYNC_TRIGGER_URL",
|
|
521
|
+
comment: "Public URL of your mounted tick route (e.g. https://app.example.com/api/clivly/tick)"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
key: "CLIVLY_SYNC_TRIGGER_SECRET",
|
|
525
|
+
comment: "Signing secret — Integrations → Remote sync"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
key: "CLIVLY_VERIFY_TOKEN",
|
|
529
|
+
comment: "Bearer token Clivly presents to /clivly/auth/verify"
|
|
530
|
+
}
|
|
531
|
+
];
|
|
309
532
|
function renderFields(fields) {
|
|
310
533
|
return `{ ${Object.entries(fields).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
|
|
311
534
|
}
|
|
@@ -357,6 +580,12 @@ export default createClivlySDK({
|
|
|
357
580
|
source: {
|
|
358
581
|
${sources.join(",\n")},
|
|
359
582
|
},
|
|
583
|
+
syncTrigger: {
|
|
584
|
+
// The public URL Clivly Cloud calls to trigger a sync — your mounted tick route.
|
|
585
|
+
url: process.env.CLIVLY_SYNC_TRIGGER_URL,
|
|
586
|
+
secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
|
|
587
|
+
},
|
|
588
|
+
verifyToken: process.env.CLIVLY_VERIFY_TOKEN,
|
|
360
589
|
});
|
|
361
590
|
`;
|
|
362
591
|
}
|
|
@@ -403,6 +632,108 @@ function resolveSchemaPath(opts) {
|
|
|
403
632
|
return null;
|
|
404
633
|
}
|
|
405
634
|
//#endregion
|
|
635
|
+
//#region src/scaffold.ts
|
|
636
|
+
const TANSTACK_ROUTE_PREFIX = /^src\/routes\//;
|
|
637
|
+
const TS_EXTENSION = /\.ts$/;
|
|
638
|
+
const ROUTE_PATHS = {
|
|
639
|
+
nextjs: {
|
|
640
|
+
tick: "app/api/clivly/tick/route.ts",
|
|
641
|
+
verify: "app/api/clivly/auth/verify/route.ts"
|
|
642
|
+
},
|
|
643
|
+
"tanstack-start": {
|
|
644
|
+
tick: "src/routes/api/clivly/tick.ts",
|
|
645
|
+
verify: "src/routes/api/clivly/auth/verify.ts"
|
|
646
|
+
},
|
|
647
|
+
sveltekit: {
|
|
648
|
+
tick: "src/routes/api/clivly/tick/+server.ts",
|
|
649
|
+
verify: "src/routes/api/clivly/auth/verify/+server.ts"
|
|
650
|
+
},
|
|
651
|
+
remix: {
|
|
652
|
+
tick: "app/routes/api.clivly.tick.ts",
|
|
653
|
+
verify: "app/routes/api.clivly.auth.verify.ts"
|
|
654
|
+
},
|
|
655
|
+
nuxt: {
|
|
656
|
+
tick: "server/api/clivly/tick.ts",
|
|
657
|
+
verify: "server/api/clivly/auth/verify.ts"
|
|
658
|
+
},
|
|
659
|
+
unknown: null
|
|
660
|
+
};
|
|
661
|
+
function routePath(framework, kind) {
|
|
662
|
+
const paths = ROUTE_PATHS[framework];
|
|
663
|
+
if (!paths) throw new Error(`No route path for framework: ${framework}`);
|
|
664
|
+
return paths[kind];
|
|
665
|
+
}
|
|
666
|
+
function configImport(routeFilePath) {
|
|
667
|
+
const rel = relative(dirname(routeFilePath), "clivly.config").replace(/\\/g, "/");
|
|
668
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
669
|
+
}
|
|
670
|
+
function renderBody(framework, importPath, verify) {
|
|
671
|
+
const preamble = verify ? `import clivly from "${importPath}";
|
|
672
|
+
|
|
673
|
+
// TODO(clivly): resolve the signed-in host user from the request (your auth stack).
|
|
674
|
+
const resolveUser = (_req: Request) => null;
|
|
675
|
+
const handler = clivly.createAuthVerifyHandler(resolveUser);
|
|
676
|
+
` : `import clivly from "${importPath}";
|
|
677
|
+
|
|
678
|
+
const handler = clivly.createClivlyHandler();
|
|
679
|
+
`;
|
|
680
|
+
switch (framework) {
|
|
681
|
+
case "nextjs": return `${preamble}
|
|
682
|
+
export const POST = (request: Request) => handler(request);
|
|
683
|
+
`;
|
|
684
|
+
case "tanstack-start": return `import { createAPIFileRoute } from "@tanstack/react-start/api";
|
|
685
|
+
${preamble}
|
|
686
|
+
export const APIRoute = createAPIFileRoute("__ROUTE__")({
|
|
687
|
+
POST: ({ request }) => handler(request),
|
|
688
|
+
});
|
|
689
|
+
`;
|
|
690
|
+
case "sveltekit": return `import type { RequestHandler } from "./$types";
|
|
691
|
+
${preamble}
|
|
692
|
+
export const POST: RequestHandler = ({ request }) => handler(request);
|
|
693
|
+
`;
|
|
694
|
+
case "remix": return `import type { ActionFunctionArgs } from "@remix-run/node";
|
|
695
|
+
${preamble}
|
|
696
|
+
export const action = ({ request }: ActionFunctionArgs) => handler(request);
|
|
697
|
+
`;
|
|
698
|
+
case "nuxt": return `import { toWebRequest } from "h3";
|
|
699
|
+
${preamble}
|
|
700
|
+
export default defineEventHandler((event) => handler(toWebRequest(event)));
|
|
701
|
+
`;
|
|
702
|
+
default: throw new Error(`No route template for framework: ${framework}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function renderRouteFile(framework, kind) {
|
|
706
|
+
const path = routePath(framework, kind);
|
|
707
|
+
let body = renderBody(framework, configImport(path), kind === "verify");
|
|
708
|
+
if (framework === "tanstack-start") {
|
|
709
|
+
const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
|
|
710
|
+
body = body.replace("__ROUTE__", url);
|
|
711
|
+
}
|
|
712
|
+
return body;
|
|
713
|
+
}
|
|
714
|
+
const COMMON_FILES = [{
|
|
715
|
+
path: "clivly.config.ts",
|
|
716
|
+
description: "Your Clivly configuration",
|
|
717
|
+
owned: false
|
|
718
|
+
}];
|
|
719
|
+
function planScaffold(framework) {
|
|
720
|
+
const paths = ROUTE_PATHS[framework];
|
|
721
|
+
if (!paths) return [];
|
|
722
|
+
return [
|
|
723
|
+
{
|
|
724
|
+
path: paths.tick,
|
|
725
|
+
description: "Clivly sync-trigger route (createClivlyHandler)",
|
|
726
|
+
owned: true
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
path: paths.verify,
|
|
730
|
+
description: "Clivly auth-verify route (createAuthVerifyHandler) — finish resolveUser",
|
|
731
|
+
owned: true
|
|
732
|
+
},
|
|
733
|
+
...COMMON_FILES
|
|
734
|
+
];
|
|
735
|
+
}
|
|
736
|
+
//#endregion
|
|
406
737
|
//#region src/index.ts
|
|
407
738
|
function flagValue(argv, flag) {
|
|
408
739
|
const index = argv.indexOf(flag);
|
|
@@ -421,6 +752,13 @@ function readDeps(cwd) {
|
|
|
421
752
|
return {};
|
|
422
753
|
}
|
|
423
754
|
}
|
|
755
|
+
function defaultReadEnv(cwd) {
|
|
756
|
+
const path = join(cwd, ".env");
|
|
757
|
+
return existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
758
|
+
}
|
|
759
|
+
function defaultWriteEnv(cwd, contents) {
|
|
760
|
+
writeFileSync(join(cwd, ".env"), contents, "utf8");
|
|
761
|
+
}
|
|
424
762
|
function resolveInitDeps(options) {
|
|
425
763
|
return {
|
|
426
764
|
out: options.out ?? ((line) => process.stdout.write(line)),
|
|
@@ -434,7 +772,15 @@ function resolveInitDeps(options) {
|
|
|
434
772
|
cwd
|
|
435
773
|
})),
|
|
436
774
|
configExists: options.configExists ?? existsSync,
|
|
437
|
-
writeConfig: options.writeConfig ?? ((path, contents) =>
|
|
775
|
+
writeConfig: options.writeConfig ?? ((path, contents) => {
|
|
776
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
777
|
+
writeFileSync(path, contents, "utf8");
|
|
778
|
+
}),
|
|
779
|
+
detectPackageManager: options.detectPackageManager ?? detectPackageManager,
|
|
780
|
+
install: options.installPackages ?? installPackages,
|
|
781
|
+
readEnv: options.readEnv ?? defaultReadEnv,
|
|
782
|
+
writeEnv: options.writeEnv ?? defaultWriteEnv,
|
|
783
|
+
framework: "unknown"
|
|
438
784
|
};
|
|
439
785
|
}
|
|
440
786
|
async function acquireSchema(options, deps) {
|
|
@@ -446,6 +792,7 @@ async function acquireSchema(options, deps) {
|
|
|
446
792
|
}
|
|
447
793
|
const framework = detectFramework(appDeps);
|
|
448
794
|
const orm = detectOrm(appDeps);
|
|
795
|
+
deps.framework = framework;
|
|
449
796
|
out("\nClivly init\n");
|
|
450
797
|
out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
|
|
451
798
|
out(` ORM: ${orm}\n\n`);
|
|
@@ -453,6 +800,10 @@ async function acquireSchema(options, deps) {
|
|
|
453
800
|
out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
|
|
454
801
|
return { code: 1 };
|
|
455
802
|
}
|
|
803
|
+
if (framework === "unknown") {
|
|
804
|
+
out("clivly init doesn't recognise your framework yet (looked for Next.js, TanStack Start, SvelteKit, Remix, Nuxt). Route file generation needs one of these.\n");
|
|
805
|
+
return { code: 1 };
|
|
806
|
+
}
|
|
456
807
|
const schemaPath = deps.resolvePath(options.cwd);
|
|
457
808
|
if (!schemaPath) {
|
|
458
809
|
out("Could not locate your Drizzle schema. Pass --schema <path>, or add a `schema` field to drizzle.config.ts.\n");
|
|
@@ -483,20 +834,48 @@ function printPlan(out, plan) {
|
|
|
483
834
|
out("\n");
|
|
484
835
|
}
|
|
485
836
|
}
|
|
486
|
-
function
|
|
837
|
+
function writeOwnedFile(options, deps, path, contents) {
|
|
838
|
+
const full = join(options.cwd, path);
|
|
839
|
+
if (deps.configExists(full) && !options.force) {
|
|
840
|
+
deps.out(` • ${path} exists — skipped (pass --force to overwrite)\n`);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
deps.writeConfig(full, contents);
|
|
844
|
+
deps.out(` ✓ ${path}\n`);
|
|
845
|
+
}
|
|
846
|
+
function writeScaffoldFiles(options, deps, plan) {
|
|
847
|
+
writeOwnedFile(options, deps, "clivly.config.ts", renderInitConfig(plan));
|
|
848
|
+
for (const file of planScaffold(deps.framework)) {
|
|
849
|
+
if (file.path === "clivly.config.ts") continue;
|
|
850
|
+
const kind = file.path.includes("verify") ? "verify" : "tick";
|
|
851
|
+
writeOwnedFile(options, deps, file.path, renderRouteFile(deps.framework, kind));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function writeEnvFile(options, deps) {
|
|
855
|
+
const nextEnv = appendEnvKeys(deps.readEnv(options.cwd), CLIVLY_ENV_KEYS);
|
|
856
|
+
deps.writeEnv(options.cwd, nextEnv);
|
|
857
|
+
deps.out(" ✓ .env placeholders\n");
|
|
858
|
+
}
|
|
859
|
+
function printNextSteps(out) {
|
|
860
|
+
out("\nNext steps:\n");
|
|
861
|
+
out(" 1. Set the CLIVLY_* values in .env\n");
|
|
862
|
+
out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
|
|
863
|
+
out(" 3. Finish resolveUser in the auth/verify route\n");
|
|
864
|
+
out(" 4. Run `clivly connect` to verify\n");
|
|
865
|
+
}
|
|
866
|
+
async function scaffold(options, deps, plan) {
|
|
487
867
|
const { out } = deps;
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
out
|
|
499
|
-
out("Next: set CLIVLY_API_KEY, address any TODOs above, then run `clivly doctor`.\n");
|
|
868
|
+
const pm = deps.detectPackageManager(options.cwd);
|
|
869
|
+
const result = await deps.install({
|
|
870
|
+
cwd: options.cwd,
|
|
871
|
+
pm,
|
|
872
|
+
packages: ["@clivly/sdk", "@clivly/core"]
|
|
873
|
+
});
|
|
874
|
+
if (result.ok) out(`Installed @clivly/sdk @clivly/core (${pm})\n`);
|
|
875
|
+
else out(`Could not install automatically — run:\n ${result.command}\n`);
|
|
876
|
+
writeScaffoldFiles(options, deps, plan);
|
|
877
|
+
writeEnvFile(options, deps);
|
|
878
|
+
printNextSteps(out);
|
|
500
879
|
return plan.contactDetected ? 0 : 1;
|
|
501
880
|
}
|
|
502
881
|
async function runInit(options) {
|
|
@@ -505,15 +884,13 @@ async function runInit(options) {
|
|
|
505
884
|
if ("code" in acquired) return acquired.code;
|
|
506
885
|
const plan = planInit(acquired);
|
|
507
886
|
printPlan(deps.out, plan);
|
|
508
|
-
return
|
|
887
|
+
return scaffold(options, deps, plan);
|
|
509
888
|
}
|
|
510
889
|
async function main(argv) {
|
|
511
890
|
const [command, ...rest] = argv;
|
|
512
|
-
const write = rest.includes("--write");
|
|
513
891
|
switch (command) {
|
|
514
892
|
case "init": return await runInit({
|
|
515
893
|
cwd: process.cwd(),
|
|
516
|
-
write,
|
|
517
894
|
force: rest.includes("--force"),
|
|
518
895
|
schemaFlag: flagValue(rest, "--schema")
|
|
519
896
|
});
|
|
@@ -525,17 +902,23 @@ async function main(argv) {
|
|
|
525
902
|
cwd: process.cwd(),
|
|
526
903
|
configPath: flagValue(rest, "--config")
|
|
527
904
|
});
|
|
905
|
+
case "connect": return await runConnect({
|
|
906
|
+
cwd: process.cwd(),
|
|
907
|
+
apiKey: process.env.CLIVLY_API_KEY,
|
|
908
|
+
apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
|
|
909
|
+
configPath: flagValue(rest, "--config")
|
|
910
|
+
});
|
|
528
911
|
case void 0:
|
|
529
912
|
case "help":
|
|
530
913
|
case "--help":
|
|
531
|
-
process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--
|
|
914
|
+
process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--schema <path>] Scaffold Clivly (config, routes, .env) for your app\n clivly ping [--api-url <url>] Verify CLIVLY_API_KEY + reachability\n clivly doctor [--config <path>] Verify your clivly.config setup end-to-end\n clivly connect [--config <path>] One pass/fail check that setup is wired\n");
|
|
532
915
|
return 0;
|
|
533
916
|
default:
|
|
534
917
|
process.stdout.write(`Unknown command: ${command}\n`);
|
|
535
918
|
return 1;
|
|
536
919
|
}
|
|
537
920
|
}
|
|
538
|
-
if (process.argv[1]
|
|
921
|
+
if (isEntrypoint(process.argv[1], import.meta.url)) main(process.argv.slice(2)).then((code) => {
|
|
539
922
|
process.exitCode = code;
|
|
540
923
|
});
|
|
541
924
|
//#endregion
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region ../core/src/mapping-form.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build the filter for a discriminated entity: rows where `column` equals
|
|
4
|
+
* `value` belong to this entity (e.g. `role = "owner"`). Returns `undefined`
|
|
5
|
+
* when either side is blank so the caller can omit the filter entirely.
|
|
6
|
+
*/
|
|
7
|
+
function buildFilterFromDiscriminator(column, value) {
|
|
8
|
+
const col = column?.trim();
|
|
9
|
+
const val = value.trim();
|
|
10
|
+
if (!col || val === "") return;
|
|
11
|
+
return { [col]: { eq: val } };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A stable identity string for a relationship's `via`. Two candidates on the
|
|
15
|
+
* same source table always differ in their join (`fkOn`+column, join-table
|
|
16
|
+
* keys, or raw SQL), so this disambiguates candidates the heuristic gives the
|
|
17
|
+
* same `name` (every contact→company candidate is named "company").
|
|
18
|
+
*/
|
|
19
|
+
function relationshipViaSignature(via) {
|
|
20
|
+
if ("fkOn" in via) return `fk:${via.fkOn}:${via.column}`;
|
|
21
|
+
if ("through" in via) return `through:${via.through}:${via.localKey}:${via.foreignKey}`;
|
|
22
|
+
return `raw:${via.$raw}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compile the selected relationship candidates into the persisted
|
|
26
|
+
* `RelationshipsMap`. Returns `undefined` for an empty selection so the caller
|
|
27
|
+
* omits the field.
|
|
28
|
+
*/
|
|
29
|
+
function buildRelationships(candidates) {
|
|
30
|
+
if (candidates.length === 0) return;
|
|
31
|
+
const map = {};
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
let key = candidate.name;
|
|
34
|
+
let suffix = 2;
|
|
35
|
+
while (key in map) {
|
|
36
|
+
key = `${candidate.name}_${suffix}`;
|
|
37
|
+
suffix++;
|
|
38
|
+
}
|
|
39
|
+
map[key] = {
|
|
40
|
+
entity: candidate.entityTable,
|
|
41
|
+
via: candidate.via
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return map;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { buildRelationships as n, relationshipViaSignature as r, buildFilterFromDiscriminator as t };
|