clivly 0.2.0 → 0.2.2
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 +54 -3
- package/adapter-Cd6D_3JD.d.mts +46 -0
- package/adapter-Cy4q2CLs.d.cts +46 -0
- package/auth-adapter-CBOKDhlc.cjs +11 -0
- package/auth-adapter-CRBW4elj.d.mts +30 -0
- package/auth-adapter-CdLODn2p.d.cts +30 -0
- package/auth-adapter-wqnnEjo3.mjs +6 -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 +961 -0
- package/index.d.cts +57 -0
- package/index.d.mts +57 -0
- package/index.mjs +960 -0
- 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 +244 -21
- 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-BpsKVccL.d.cts +159 -0
- package/types-_qbJXMt_.d.mts +159 -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
- package/dist/index.mjs +0 -542
package/dist/index.mjs
DELETED
|
@@ -1,542 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
import { createJiti } from "jiti";
|
|
6
|
-
import { createClivlySDK } from "@clivly/sdk";
|
|
7
|
-
import { CANONICAL_FIELDS } from "@clivly/core/entity-config";
|
|
8
|
-
import { matchesConcept, requiredFieldsFor, suggestCursorField, suggestFieldMap, suggestRelationships } from "@clivly/core/entity-heuristics";
|
|
9
|
-
//#region src/detect.ts
|
|
10
|
-
const FRAMEWORK_MARKERS = [
|
|
11
|
-
{
|
|
12
|
-
framework: "nextjs",
|
|
13
|
-
deps: ["next"]
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
framework: "tanstack-start",
|
|
17
|
-
deps: ["@tanstack/react-start"]
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
framework: "sveltekit",
|
|
21
|
-
deps: ["@sveltejs/kit"]
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
framework: "remix",
|
|
25
|
-
deps: ["@remix-run/react", "@remix-run/node"]
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
framework: "nuxt",
|
|
29
|
-
deps: ["nuxt"]
|
|
30
|
-
}
|
|
31
|
-
];
|
|
32
|
-
const ORM_MARKERS = [
|
|
33
|
-
{
|
|
34
|
-
orm: "drizzle",
|
|
35
|
-
deps: ["drizzle-orm"]
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
orm: "prisma",
|
|
39
|
-
deps: ["@prisma/client", "prisma"]
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
orm: "kysely",
|
|
43
|
-
deps: ["kysely"]
|
|
44
|
-
}
|
|
45
|
-
];
|
|
46
|
-
const FRAMEWORK_LABELS = {
|
|
47
|
-
nextjs: "Next.js",
|
|
48
|
-
"tanstack-start": "TanStack Start",
|
|
49
|
-
sveltekit: "SvelteKit",
|
|
50
|
-
remix: "Remix",
|
|
51
|
-
nuxt: "Nuxt",
|
|
52
|
-
unknown: "Unknown"
|
|
53
|
-
};
|
|
54
|
-
function detectFramework(deps) {
|
|
55
|
-
for (const { framework, deps: markers } of FRAMEWORK_MARKERS) if (markers.some((dep) => dep in deps)) return framework;
|
|
56
|
-
return "unknown";
|
|
57
|
-
}
|
|
58
|
-
function detectOrm(deps) {
|
|
59
|
-
for (const { orm, deps: markers } of ORM_MARKERS) if (markers.some((dep) => dep in deps)) return orm;
|
|
60
|
-
return "unknown";
|
|
61
|
-
}
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region src/discover-schema.ts
|
|
64
|
-
const WINDOWS_SEP = /\\/g;
|
|
65
|
-
const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
|
|
66
|
-
function toImportPath(schemaPath, cwd) {
|
|
67
|
-
const rel = relative(cwd, schemaPath).replace(WINDOWS_SEP, "/").replace(MODULE_EXT, "");
|
|
68
|
-
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
69
|
-
}
|
|
70
|
-
function isDirectory(path) {
|
|
71
|
-
try {
|
|
72
|
-
return statSync(path).isDirectory();
|
|
73
|
-
} catch {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
function loadSchemaModule(schemaPath, load) {
|
|
78
|
-
return load(isDirectory(schemaPath) ? join(schemaPath, "index.ts") : schemaPath);
|
|
79
|
-
}
|
|
80
|
-
function defaultLoader(cwd) {
|
|
81
|
-
const jiti = createJiti(pathToFileURL(join(cwd, "clivly.config.ts")).href);
|
|
82
|
-
return (specifier) => jiti.import(specifier);
|
|
83
|
-
}
|
|
84
|
-
async function discoverSchema(opts) {
|
|
85
|
-
const load = opts.load ?? defaultLoader(opts.cwd);
|
|
86
|
-
const mod = await loadSchemaModule(opts.schemaPath, load);
|
|
87
|
-
const drizzle = await load("drizzle-orm");
|
|
88
|
-
const importPath = toImportPath(opts.schemaPath, opts.cwd);
|
|
89
|
-
const tables = [];
|
|
90
|
-
let db = null;
|
|
91
|
-
for (const [moduleKey, value] of Object.entries(mod)) {
|
|
92
|
-
if (drizzle.is(value, drizzle.Table)) {
|
|
93
|
-
const columns = Object.entries(drizzle.getTableColumns(value)).map(([key, col]) => ({
|
|
94
|
-
key,
|
|
95
|
-
dbName: col.name,
|
|
96
|
-
isPrimary: Boolean(col.primary)
|
|
97
|
-
}));
|
|
98
|
-
tables.push({
|
|
99
|
-
moduleKey,
|
|
100
|
-
tableName: drizzle.getTableName(value),
|
|
101
|
-
columns
|
|
102
|
-
});
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (db === null && value && typeof value.select === "function") db = {
|
|
106
|
-
moduleKey,
|
|
107
|
-
importPath
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
tables,
|
|
112
|
-
importPath,
|
|
113
|
-
db
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
//#endregion
|
|
117
|
-
//#region src/render.ts
|
|
118
|
-
function checkLine(ok, label, detail) {
|
|
119
|
-
return ` ${ok ? "✅" : "❌"} ${label}${detail ? ` — ${detail}` : ""}\n`;
|
|
120
|
-
}
|
|
121
|
-
//#endregion
|
|
122
|
-
//#region src/doctor.ts
|
|
123
|
-
async function loadSdkFromConfig(path) {
|
|
124
|
-
const mod = await createJiti(import.meta.url).import(path);
|
|
125
|
-
const sdk = mod.default ?? mod;
|
|
126
|
-
if (typeof sdk.doctor !== "function") throw new Error("default export is not a Clivly SDK instance");
|
|
127
|
-
return sdk;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* `clivly doctor` — load the app's clivly.config, run the SDK's read-only setup
|
|
131
|
-
* checks, and print a ✅/❌ checklist. Exit 0 when every check passes. `loadSdk`
|
|
132
|
-
* and `out` are injectable for tests.
|
|
133
|
-
*/
|
|
134
|
-
async function runDoctor(options) {
|
|
135
|
-
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
136
|
-
const path = resolve(options.cwd, options.configPath ?? "clivly.config.ts");
|
|
137
|
-
const loadSdk = options.loadSdk ?? loadSdkFromConfig;
|
|
138
|
-
out("\nClivly doctor\n");
|
|
139
|
-
let sdk;
|
|
140
|
-
try {
|
|
141
|
-
sdk = await loadSdk(path);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
out(checkLine(false, "Load clivly.config", error.message));
|
|
144
|
-
out(" Expected clivly.config.ts at your app root with `export default createClivlySDK({...})`.\n");
|
|
145
|
-
return 1;
|
|
146
|
-
}
|
|
147
|
-
const report = await sdk.doctor();
|
|
148
|
-
for (const check of report.checks) {
|
|
149
|
-
if (check.skipped) {
|
|
150
|
-
out(` ⚠️ ${check.name} — ${check.detail}\n`);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
out(checkLine(check.ok, check.name, check.detail));
|
|
154
|
-
}
|
|
155
|
-
return report.ok ? 0 : 1;
|
|
156
|
-
}
|
|
157
|
-
//#endregion
|
|
158
|
-
//#region src/ping.ts
|
|
159
|
-
const REASON_HINT = {
|
|
160
|
-
ok: "",
|
|
161
|
-
invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
|
|
162
|
-
forbidden: "The key is valid but lacks access to this org (403).",
|
|
163
|
-
rate_limited: "Rate limited (429). Wait a moment and retry.",
|
|
164
|
-
server_error: "Clivly returned a server error (5xx). Retry shortly.",
|
|
165
|
-
client_error: "The request was rejected (4xx). Check your configuration.",
|
|
166
|
-
network_error: "Could not reach Clivly. Check your network or --api-url."
|
|
167
|
-
};
|
|
168
|
-
const defaultConnect = (apiKey, apiUrl) => createClivlySDK({
|
|
169
|
-
apiKey,
|
|
170
|
-
apiUrl,
|
|
171
|
-
retry: { maxAttempts: 1 }
|
|
172
|
-
}).connect();
|
|
173
|
-
function resultLines(result) {
|
|
174
|
-
if (result.ok) {
|
|
175
|
-
const lines = [checkLine(true, "Connected to Clivly")];
|
|
176
|
-
if (result.org) {
|
|
177
|
-
const name = result.org.name ?? "(unnamed org)";
|
|
178
|
-
const id = result.org.id ? ` (${result.org.id})` : "";
|
|
179
|
-
lines.push(` Org: ${name}${id}\n`);
|
|
180
|
-
}
|
|
181
|
-
return lines;
|
|
182
|
-
}
|
|
183
|
-
const status = result.status === null ? "no response" : `HTTP ${result.status}`;
|
|
184
|
-
return [checkLine(false, `Could not connect (${result.reason}, ${status})`), ` ${REASON_HINT[result.reason]}\n`];
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* `clivly ping` — send one heartbeat with the configured key and print a clear
|
|
188
|
-
* ✅/❌ with the resolved org or the failure reason. Verifies key + reachability
|
|
189
|
-
* from the terminal with zero code. `connect`/`out` are injectable for tests.
|
|
190
|
-
*/
|
|
191
|
-
async function runPing(options) {
|
|
192
|
-
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
193
|
-
out("\nClivly ping\n");
|
|
194
|
-
if (!options.apiKey) {
|
|
195
|
-
out(checkLine(false, "CLIVLY_API_KEY is not set."));
|
|
196
|
-
out(" Set it to your sk_live_… key from Settings → API keys.\n");
|
|
197
|
-
return 1;
|
|
198
|
-
}
|
|
199
|
-
const result = await (options.connect ?? defaultConnect)(options.apiKey, options.apiUrl);
|
|
200
|
-
for (const line of resultLines(result)) out(line);
|
|
201
|
-
return result.ok ? 0 : 1;
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region src/plan-init.ts
|
|
205
|
-
function rankCandidates(tables, concept) {
|
|
206
|
-
return tables.map((t) => ({
|
|
207
|
-
t,
|
|
208
|
-
m: matchesConcept(t.tableName, concept)
|
|
209
|
-
})).filter((c) => Boolean(c.m)).sort((a, b) => Number(b.m.exact) - Number(a.m.exact)).map((c) => c.t);
|
|
210
|
-
}
|
|
211
|
-
function selectEntities(tables) {
|
|
212
|
-
const contactCands = rankCandidates(tables, "contact");
|
|
213
|
-
const companyCands = rankCandidates(tables, "company");
|
|
214
|
-
const contactNames = new Set(contactCands.map((t) => t.tableName));
|
|
215
|
-
const companyNames = new Set(companyCands.map((t) => t.tableName));
|
|
216
|
-
const contactExclusive = contactCands.filter((t) => !companyNames.has(t.tableName));
|
|
217
|
-
const companyExclusive = companyCands.filter((t) => !contactNames.has(t.tableName));
|
|
218
|
-
const contact = contactExclusive[0] ?? contactCands[0] ?? null;
|
|
219
|
-
const companyPreferred = companyExclusive[0] ?? companyCands[0] ?? null;
|
|
220
|
-
return {
|
|
221
|
-
contact,
|
|
222
|
-
company: companyPreferred && companyPreferred.tableName !== contact?.tableName ? companyPreferred : companyCands.find((t) => t.tableName !== contact?.tableName) ?? null
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
function chooseSyncFields(table, todos) {
|
|
226
|
-
const columnKeys = table.columns.map((c) => c.key);
|
|
227
|
-
let cursorField = suggestCursorField(columnKeys);
|
|
228
|
-
if (!cursorField) {
|
|
229
|
-
cursorField = table.columns.find((c) => c.isPrimary)?.key ?? columnKeys[0] ?? "id";
|
|
230
|
-
todos.push(`No updated_at-like column on "${table.tableName}" — using "${cursorField}" as the sync cursor. Confirm it is monotonic.`);
|
|
231
|
-
}
|
|
232
|
-
const pkCols = table.columns.filter((c) => c.isPrimary);
|
|
233
|
-
if (pkCols.length === 1) return {
|
|
234
|
-
cursorField,
|
|
235
|
-
idField: null
|
|
236
|
-
};
|
|
237
|
-
const idField = pkCols[0]?.key ?? columnKeys[0] ?? "id";
|
|
238
|
-
todos.push(`Table "${table.tableName}" has ${pkCols.length === 0 ? "no" : "a composite"} primary key — set idField (guessed "${idField}").`);
|
|
239
|
-
return {
|
|
240
|
-
cursorField,
|
|
241
|
-
idField
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
function buildEntity(table, concept, todos) {
|
|
245
|
-
const canonical = CANONICAL_FIELDS[concept];
|
|
246
|
-
const suggested = suggestFieldMap(concept, table.columns.map((c) => c.dbName));
|
|
247
|
-
const fields = {};
|
|
248
|
-
for (const [key, column] of Object.entries(suggested)) if (canonical.includes(key)) fields[key] = column;
|
|
249
|
-
for (const required of requiredFieldsFor(concept)) if (!fields[required]) todos.push(`Could not map required ${concept} field "${required}" on "${table.tableName}" — add it manually.`);
|
|
250
|
-
const { cursorField, idField } = chooseSyncFields(table, todos);
|
|
251
|
-
return {
|
|
252
|
-
moduleKey: table.moduleKey,
|
|
253
|
-
tableName: table.tableName,
|
|
254
|
-
concept,
|
|
255
|
-
fields,
|
|
256
|
-
cursorField,
|
|
257
|
-
idField
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
function placeholderContact(todos) {
|
|
261
|
-
todos.push("No contact table detected. Filled in a placeholder — set `source`, `fields`, and the fromDrizzle table below, then run `clivly doctor`.");
|
|
262
|
-
return {
|
|
263
|
-
moduleKey: "contacts",
|
|
264
|
-
tableName: "contacts",
|
|
265
|
-
concept: "contact",
|
|
266
|
-
fields: {
|
|
267
|
-
name: "name",
|
|
268
|
-
email: "email"
|
|
269
|
-
},
|
|
270
|
-
cursorField: "updatedAt",
|
|
271
|
-
idField: null
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
function planInit(schema) {
|
|
275
|
-
const todos = [];
|
|
276
|
-
const { contact: contactTable, company: companyTable } = selectEntities(schema.tables);
|
|
277
|
-
const contactDetected = contactTable !== null;
|
|
278
|
-
const contact = contactTable ? buildEntity(contactTable, "contact", todos) : placeholderContact(todos);
|
|
279
|
-
let company = null;
|
|
280
|
-
if (companyTable) {
|
|
281
|
-
company = buildEntity(companyTable, "company", todos);
|
|
282
|
-
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
|
-
}
|
|
284
|
-
if (contactTable) {
|
|
285
|
-
const suggestions = suggestRelationships({
|
|
286
|
-
name: contactTable.tableName,
|
|
287
|
-
columns: contactTable.columns.map((c) => c.dbName)
|
|
288
|
-
}, schema.tables.map((t) => ({
|
|
289
|
-
name: t.tableName,
|
|
290
|
-
columns: t.columns.map((c) => c.dbName)
|
|
291
|
-
})));
|
|
292
|
-
const only = suggestions[0];
|
|
293
|
-
if (suggestions.length === 1 && only) contact.relationship = {
|
|
294
|
-
name: only.name,
|
|
295
|
-
via: only.via
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
299
|
-
importPath: schema.importPath,
|
|
300
|
-
db: schema.db,
|
|
301
|
-
contact,
|
|
302
|
-
company,
|
|
303
|
-
todos,
|
|
304
|
-
contactDetected
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
//#endregion
|
|
308
|
-
//#region src/render-config.ts
|
|
309
|
-
function renderFields(fields) {
|
|
310
|
-
return `{ ${Object.entries(fields).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
|
|
311
|
-
}
|
|
312
|
-
function renderRelationships(entity) {
|
|
313
|
-
if (!entity.relationship) return "";
|
|
314
|
-
const { name, via } = entity.relationship;
|
|
315
|
-
return `,\n relationships: { ${name}: { entity: "${name}", via: ${JSON.stringify(via)} } }`;
|
|
316
|
-
}
|
|
317
|
-
function renderEntity(key, entity) {
|
|
318
|
-
return ` ${key}: {
|
|
319
|
-
concept: "${entity.concept}",
|
|
320
|
-
source: "${entity.tableName}",
|
|
321
|
-
fields: ${renderFields(entity.fields)}${renderRelationships(entity)},
|
|
322
|
-
}`;
|
|
323
|
-
}
|
|
324
|
-
function renderSource(role, entity) {
|
|
325
|
-
const opts = entity.idField === null ? `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}" }` : `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}", idField: "${entity.idField}" }`;
|
|
326
|
-
return ` ${role}: fromDrizzle(db, ${entity.moduleKey}, ${opts})`;
|
|
327
|
-
}
|
|
328
|
-
function renderTableImport(plan) {
|
|
329
|
-
const tableKeys = [plan.contact.moduleKey, plan.company?.moduleKey].filter(Boolean);
|
|
330
|
-
if (plan.db) return `import { db, ${tableKeys.join(", ")} } from "${plan.importPath}";`;
|
|
331
|
-
return `import { ${tableKeys.join(", ")} } from "${plan.importPath}";
|
|
332
|
-
import { db } from "./db"; // TODO: point at your Drizzle instance`;
|
|
333
|
-
}
|
|
334
|
-
function renderTodoHeader(todos) {
|
|
335
|
-
if (todos.length === 0) return "";
|
|
336
|
-
return `// TODO(clivly): review before running \`clivly doctor\`:\n${todos.map((t) => `// - ${t}`).join("\n")}\n\n`;
|
|
337
|
-
}
|
|
338
|
-
function renderInitConfig(plan) {
|
|
339
|
-
const entities = [renderEntity(plan.contact.moduleKey, plan.contact)];
|
|
340
|
-
if (plan.company) entities.push(renderEntity(plan.company.moduleKey, plan.company));
|
|
341
|
-
const sources = [renderSource("contacts", plan.contact)];
|
|
342
|
-
if (plan.company) sources.push(renderSource("companies", plan.company));
|
|
343
|
-
return `${renderTodoHeader(plan.todos)}import { createClivlySDK } from "@clivly/sdk";
|
|
344
|
-
import { fromDrizzle } from "@clivly/sdk/drizzle";
|
|
345
|
-
import { defineClivlyConfig } from "@clivly/core";
|
|
346
|
-
${renderTableImport(plan)}
|
|
347
|
-
|
|
348
|
-
const entities = defineClivlyConfig({
|
|
349
|
-
entities: {
|
|
350
|
-
${entities.join(",\n")},
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
export default createClivlySDK({
|
|
355
|
-
apiKey: process.env.CLIVLY_API_KEY!,
|
|
356
|
-
entities,
|
|
357
|
-
source: {
|
|
358
|
-
${sources.join(",\n")},
|
|
359
|
-
},
|
|
360
|
-
});
|
|
361
|
-
`;
|
|
362
|
-
}
|
|
363
|
-
//#endregion
|
|
364
|
-
//#region src/resolve-schema-path.ts
|
|
365
|
-
const COMMON_PATHS = [
|
|
366
|
-
"src/db/schema.ts",
|
|
367
|
-
"src/db/schema",
|
|
368
|
-
"db/schema.ts",
|
|
369
|
-
"db/schema",
|
|
370
|
-
"src/schema.ts",
|
|
371
|
-
"src/schema",
|
|
372
|
-
"drizzle/schema.ts",
|
|
373
|
-
"drizzle/schema"
|
|
374
|
-
];
|
|
375
|
-
const DRIZZLE_CONFIG_NAMES = [
|
|
376
|
-
"drizzle.config.ts",
|
|
377
|
-
"drizzle.config.js",
|
|
378
|
-
"drizzle.config.mjs"
|
|
379
|
-
];
|
|
380
|
-
function defaultReadDrizzleConfigSchema(cwd) {
|
|
381
|
-
for (const name of DRIZZLE_CONFIG_NAMES) {
|
|
382
|
-
const path = resolve(cwd, name);
|
|
383
|
-
if (!existsSync(path)) continue;
|
|
384
|
-
try {
|
|
385
|
-
const schema = createJiti(pathToFileURL(path).href)(path).default?.schema;
|
|
386
|
-
const first = Array.isArray(schema) ? schema[0] : schema;
|
|
387
|
-
if (typeof first === "string") return first;
|
|
388
|
-
} catch {}
|
|
389
|
-
}
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
function resolveSchemaPath(opts) {
|
|
393
|
-
const exists = opts.deps?.exists ?? existsSync;
|
|
394
|
-
const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
|
|
395
|
-
const toAbs = (p) => isAbsolute(p) ? p : resolve(opts.cwd, p);
|
|
396
|
-
if (opts.flag) return toAbs(opts.flag);
|
|
397
|
-
const fromConfig = readSchema(opts.cwd);
|
|
398
|
-
if (fromConfig) return toAbs(fromConfig);
|
|
399
|
-
for (const candidate of COMMON_PATHS) {
|
|
400
|
-
const abs = toAbs(candidate);
|
|
401
|
-
if (exists(abs)) return abs;
|
|
402
|
-
}
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
//#endregion
|
|
406
|
-
//#region src/index.ts
|
|
407
|
-
function flagValue(argv, flag) {
|
|
408
|
-
const index = argv.indexOf(flag);
|
|
409
|
-
return index >= 0 ? argv[index + 1] : void 0;
|
|
410
|
-
}
|
|
411
|
-
function readDeps(cwd) {
|
|
412
|
-
const pkgPath = join(cwd, "package.json");
|
|
413
|
-
if (!existsSync(pkgPath)) return {};
|
|
414
|
-
try {
|
|
415
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
416
|
-
return {
|
|
417
|
-
...pkg.dependencies,
|
|
418
|
-
...pkg.devDependencies
|
|
419
|
-
};
|
|
420
|
-
} catch {
|
|
421
|
-
return {};
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
function resolveInitDeps(options) {
|
|
425
|
-
return {
|
|
426
|
-
out: options.out ?? ((line) => process.stdout.write(line)),
|
|
427
|
-
readDepsFn: options.readDeps ?? readDeps,
|
|
428
|
-
resolvePath: options.resolveSchemaPath ?? ((cwd) => resolveSchemaPath({
|
|
429
|
-
cwd,
|
|
430
|
-
flag: options.schemaFlag
|
|
431
|
-
})),
|
|
432
|
-
discover: options.discoverSchema ?? ((schemaPath, cwd) => discoverSchema({
|
|
433
|
-
schemaPath,
|
|
434
|
-
cwd
|
|
435
|
-
})),
|
|
436
|
-
configExists: options.configExists ?? existsSync,
|
|
437
|
-
writeConfig: options.writeConfig ?? ((path, contents) => writeFileSync(path, contents, "utf8"))
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
async function acquireSchema(options, deps) {
|
|
441
|
-
const { out } = deps;
|
|
442
|
-
const appDeps = deps.readDepsFn(options.cwd);
|
|
443
|
-
if (Object.keys(appDeps).length === 0) {
|
|
444
|
-
out("No package.json found here. Run `clivly init` from your app root.\n");
|
|
445
|
-
return { code: 1 };
|
|
446
|
-
}
|
|
447
|
-
const framework = detectFramework(appDeps);
|
|
448
|
-
const orm = detectOrm(appDeps);
|
|
449
|
-
out("\nClivly init\n");
|
|
450
|
-
out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
|
|
451
|
-
out(` ORM: ${orm}\n\n`);
|
|
452
|
-
if (orm !== "drizzle") {
|
|
453
|
-
out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
|
|
454
|
-
return { code: 1 };
|
|
455
|
-
}
|
|
456
|
-
const schemaPath = deps.resolvePath(options.cwd);
|
|
457
|
-
if (!schemaPath) {
|
|
458
|
-
out("Could not locate your Drizzle schema. Pass --schema <path>, or add a `schema` field to drizzle.config.ts.\n");
|
|
459
|
-
return { code: 1 };
|
|
460
|
-
}
|
|
461
|
-
out(` Schema: ${schemaPath}\n\n`);
|
|
462
|
-
try {
|
|
463
|
-
const schema = await deps.discover(schemaPath, options.cwd);
|
|
464
|
-
if (schema.tables.length === 0) {
|
|
465
|
-
out(`No Drizzle tables found in ${schemaPath}. Is this the right path?\n`);
|
|
466
|
-
return { code: 1 };
|
|
467
|
-
}
|
|
468
|
-
return schema;
|
|
469
|
-
} catch (error) {
|
|
470
|
-
out(`Failed to load the schema module: ${error.message}\n`);
|
|
471
|
-
out("The schema must import cleanly without a live DB connection.\n");
|
|
472
|
-
return { code: 1 };
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
function printPlan(out, plan) {
|
|
476
|
-
out("Detected:\n");
|
|
477
|
-
out(` contact ← ${plan.contact.tableName} fields ${JSON.stringify(plan.contact.fields)}\n`);
|
|
478
|
-
if (plan.company) out(` company ← ${plan.company.tableName} fields ${JSON.stringify(plan.company.fields)}\n`);
|
|
479
|
-
out("\n");
|
|
480
|
-
if (plan.todos.length > 0) {
|
|
481
|
-
out("Review after generation:\n");
|
|
482
|
-
for (const todo of plan.todos) out(` • ${todo}\n`);
|
|
483
|
-
out("\n");
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
function writePlan(options, deps, plan) {
|
|
487
|
-
const { out } = deps;
|
|
488
|
-
const configPath = join(options.cwd, "clivly.config.ts");
|
|
489
|
-
if (!options.write) {
|
|
490
|
-
out("Dry run. Re-run with --write to scaffold clivly.config.ts.\n");
|
|
491
|
-
return plan.contactDetected ? 0 : 1;
|
|
492
|
-
}
|
|
493
|
-
if (deps.configExists(configPath) && !options.force) {
|
|
494
|
-
out("clivly.config.ts already exists — pass --force to overwrite.\n");
|
|
495
|
-
return 0;
|
|
496
|
-
}
|
|
497
|
-
deps.writeConfig(configPath, renderInitConfig(plan));
|
|
498
|
-
out("Created clivly.config.ts\n");
|
|
499
|
-
out("Next: set CLIVLY_API_KEY, address any TODOs above, then run `clivly doctor`.\n");
|
|
500
|
-
return plan.contactDetected ? 0 : 1;
|
|
501
|
-
}
|
|
502
|
-
async function runInit(options) {
|
|
503
|
-
const deps = resolveInitDeps(options);
|
|
504
|
-
const acquired = await acquireSchema(options, deps);
|
|
505
|
-
if ("code" in acquired) return acquired.code;
|
|
506
|
-
const plan = planInit(acquired);
|
|
507
|
-
printPlan(deps.out, plan);
|
|
508
|
-
return writePlan(options, deps, plan);
|
|
509
|
-
}
|
|
510
|
-
async function main(argv) {
|
|
511
|
-
const [command, ...rest] = argv;
|
|
512
|
-
const write = rest.includes("--write");
|
|
513
|
-
switch (command) {
|
|
514
|
-
case "init": return await runInit({
|
|
515
|
-
cwd: process.cwd(),
|
|
516
|
-
write,
|
|
517
|
-
force: rest.includes("--force"),
|
|
518
|
-
schemaFlag: flagValue(rest, "--schema")
|
|
519
|
-
});
|
|
520
|
-
case "ping": return await runPing({
|
|
521
|
-
apiKey: process.env.CLIVLY_API_KEY,
|
|
522
|
-
apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL
|
|
523
|
-
});
|
|
524
|
-
case "doctor": return await runDoctor({
|
|
525
|
-
cwd: process.cwd(),
|
|
526
|
-
configPath: flagValue(rest, "--config")
|
|
527
|
-
});
|
|
528
|
-
case void 0:
|
|
529
|
-
case "help":
|
|
530
|
-
case "--help":
|
|
531
|
-
process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--write] [--force] [--schema <path>] Detect your Drizzle schema and generate clivly.config.ts\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");
|
|
532
|
-
return 0;
|
|
533
|
-
default:
|
|
534
|
-
process.stdout.write(`Unknown command: ${command}\n`);
|
|
535
|
-
return 1;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2)).then((code) => {
|
|
539
|
-
process.exitCode = code;
|
|
540
|
-
});
|
|
541
|
-
//#endregion
|
|
542
|
-
export { runInit };
|