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/index.mjs
ADDED
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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 { createRequire } from "node:module";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
|
+
import { createJiti } from "jiti";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
//#region src/render.ts
|
|
13
|
+
function checkLine(ok, label, detail) {
|
|
14
|
+
return ` ${ok ? "✅" : "❌"} ${label}${detail ? ` — ${detail}` : ""}\n`;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/connect.ts
|
|
18
|
+
const REASON_HINT$1 = {
|
|
19
|
+
ok: "",
|
|
20
|
+
invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
|
|
21
|
+
forbidden: "The key lacks access (403).",
|
|
22
|
+
rate_limited: "Rate limited (429) — retry shortly.",
|
|
23
|
+
server_error: "Clivly returned a 5xx — retry shortly.",
|
|
24
|
+
client_error: "The request was rejected (4xx).",
|
|
25
|
+
network_error: "No response — check the network / apiUrl."
|
|
26
|
+
};
|
|
27
|
+
const defaultConnect$1 = (apiKey, apiUrl) => createClivlySDK({
|
|
28
|
+
apiKey,
|
|
29
|
+
apiUrl,
|
|
30
|
+
retry: { maxAttempts: 1 }
|
|
31
|
+
}).connect();
|
|
32
|
+
const defaultLoadConfig = async (path) => {
|
|
33
|
+
const mod = await createJiti(import.meta.url).import(path);
|
|
34
|
+
return mod.default ?? mod;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* `clivly connect` — a read-only pass/fail report: API key present → heartbeat
|
|
38
|
+
* (real SDK connect, proving the cloud saw it) → config schema loadable →
|
|
39
|
+
* trigger-URL note. Exit 0 iff the first three pass; a missing trigger URL warns
|
|
40
|
+
* but does not fail (push-only setups are valid). Deps injectable for tests.
|
|
41
|
+
*/
|
|
42
|
+
async function runConnect(options) {
|
|
43
|
+
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
44
|
+
out("\nClivly connect\n");
|
|
45
|
+
if (!options.apiKey) {
|
|
46
|
+
out(checkLine(false, "CLIVLY_API_KEY is not set."));
|
|
47
|
+
out(" Set it to your sk_live_… key from Settings → API keys.\n");
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const result = await (options.connectFn ?? defaultConnect$1)(options.apiKey, options.apiUrl);
|
|
51
|
+
if (result.ok) out(checkLine(true, `Heartbeat reached Clivly — org: ${result.org?.name ?? "(unnamed org)"}${result.org?.id ? ` (${result.org.id})` : ""}`));
|
|
52
|
+
else {
|
|
53
|
+
const status = result.status === null ? "no response" : `HTTP ${result.status}`;
|
|
54
|
+
out(checkLine(false, `Heartbeat failed (${result.reason}, ${status})`));
|
|
55
|
+
out(` ${REASON_HINT$1[result.reason]}\n`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
const path = resolve(options.cwd, options.configPath ?? "clivly.config.ts");
|
|
59
|
+
const loadConfig = options.loadConfigFn ?? defaultLoadConfig;
|
|
60
|
+
let config;
|
|
61
|
+
try {
|
|
62
|
+
config = await loadConfig(path);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
out(checkLine(false, "Load clivly.config", error.message));
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const tables = Object.values(config.entities?.entities ?? {}).map((e) => e.source).filter((s) => Boolean(s));
|
|
68
|
+
if (tables.length === 0) {
|
|
69
|
+
out(checkLine(false, "No entities configured in clivly.config"));
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
out(checkLine(true, `Schema: ${tables.length} entity table(s) — ${tables.join(", ")}`));
|
|
73
|
+
const triggerUrl = config.syncTrigger?.url;
|
|
74
|
+
if (triggerUrl) out(checkLine(true, `Trigger route configured — ${triggerUrl}`));
|
|
75
|
+
else {
|
|
76
|
+
out(checkLine(false, "No trigger URL — dashboard-triggered sync is not wired"));
|
|
77
|
+
out(" Set syncTrigger.url (CLIVLY_SYNC_TRIGGER_URL) to your public tick route.\n");
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/detect.ts
|
|
83
|
+
const FRAMEWORK_MARKERS = [
|
|
84
|
+
{
|
|
85
|
+
framework: "nextjs",
|
|
86
|
+
deps: ["next"]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
framework: "tanstack-start",
|
|
90
|
+
deps: ["@tanstack/react-start"]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
framework: "sveltekit",
|
|
94
|
+
deps: ["@sveltejs/kit"]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
framework: "remix",
|
|
98
|
+
deps: ["@remix-run/react", "@remix-run/node"]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
framework: "nuxt",
|
|
102
|
+
deps: ["nuxt"]
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
const ORM_MARKERS = [
|
|
106
|
+
{
|
|
107
|
+
orm: "drizzle",
|
|
108
|
+
deps: ["drizzle-orm"]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
orm: "prisma",
|
|
112
|
+
deps: ["@prisma/client", "prisma"]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
orm: "kysely",
|
|
116
|
+
deps: ["kysely"]
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
const FRAMEWORK_LABELS = {
|
|
120
|
+
nextjs: "Next.js",
|
|
121
|
+
"tanstack-start": "TanStack Start",
|
|
122
|
+
sveltekit: "SvelteKit",
|
|
123
|
+
remix: "Remix",
|
|
124
|
+
nuxt: "Nuxt",
|
|
125
|
+
unknown: "Unknown"
|
|
126
|
+
};
|
|
127
|
+
function detectFramework(deps) {
|
|
128
|
+
for (const { framework, deps: markers } of FRAMEWORK_MARKERS) if (markers.some((dep) => dep in deps)) return framework;
|
|
129
|
+
return "unknown";
|
|
130
|
+
}
|
|
131
|
+
function detectOrm(deps) {
|
|
132
|
+
for (const { orm, deps: markers } of ORM_MARKERS) if (markers.some((dep) => dep in deps)) return orm;
|
|
133
|
+
return "unknown";
|
|
134
|
+
}
|
|
135
|
+
const LOCKFILES = [
|
|
136
|
+
{
|
|
137
|
+
file: "pnpm-lock.yaml",
|
|
138
|
+
pm: "pnpm"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
file: "bun.lockb",
|
|
142
|
+
pm: "bun"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
file: "yarn.lock",
|
|
146
|
+
pm: "yarn"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
file: "package-lock.json",
|
|
150
|
+
pm: "npm"
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
function detectPackageManager(cwd, fileExists = existsSync) {
|
|
154
|
+
for (const { file, pm } of LOCKFILES) if (fileExists(join(cwd, file))) return pm;
|
|
155
|
+
return "npm";
|
|
156
|
+
}
|
|
157
|
+
function addCommand(pm, packages) {
|
|
158
|
+
return {
|
|
159
|
+
command: pm,
|
|
160
|
+
args: [pm === "npm" ? "install" : "add", ...packages]
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/discover-schema.ts
|
|
165
|
+
const WINDOWS_SEP = /\\/g;
|
|
166
|
+
const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
|
|
167
|
+
function toImportPath(schemaPath, cwd) {
|
|
168
|
+
const rel = relative(cwd, schemaPath).replace(WINDOWS_SEP, "/").replace(MODULE_EXT, "");
|
|
169
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
170
|
+
}
|
|
171
|
+
function isDirectory(path) {
|
|
172
|
+
try {
|
|
173
|
+
return statSync(path).isDirectory();
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function loadSchemaModule(schemaPath, load) {
|
|
179
|
+
return load(isDirectory(schemaPath) ? join(schemaPath, "index.ts") : schemaPath);
|
|
180
|
+
}
|
|
181
|
+
function defaultLoader(cwd) {
|
|
182
|
+
const jiti = createJiti(pathToFileURL(join(cwd, "clivly.config.ts")).href);
|
|
183
|
+
return (specifier) => jiti.import(specifier);
|
|
184
|
+
}
|
|
185
|
+
async function loadPgCoreRuntime(load) {
|
|
186
|
+
try {
|
|
187
|
+
return await load("drizzle-orm/pg-core");
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function buildFkByColumn(pgCore, drizzle, table) {
|
|
193
|
+
const fkByColumn = /* @__PURE__ */ new Map();
|
|
194
|
+
if (!pgCore) return fkByColumn;
|
|
195
|
+
try {
|
|
196
|
+
for (const fk of pgCore.getTableConfig(table).foreignKeys) {
|
|
197
|
+
const ref = fk.reference();
|
|
198
|
+
const foreignTable = drizzle.getTableName(ref.foreignTable);
|
|
199
|
+
ref.columns.forEach((col, i) => {
|
|
200
|
+
const foreignCol = ref.foreignColumns[i];
|
|
201
|
+
if (foreignCol) fkByColumn.set(col.name, {
|
|
202
|
+
table: foreignTable,
|
|
203
|
+
column: foreignCol.name
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} catch {}
|
|
208
|
+
return fkByColumn;
|
|
209
|
+
}
|
|
210
|
+
function toDiscoveredColumn(key, col, fkByColumn) {
|
|
211
|
+
let type;
|
|
212
|
+
try {
|
|
213
|
+
type = col.getSQLType?.();
|
|
214
|
+
} catch {
|
|
215
|
+
type = void 0;
|
|
216
|
+
}
|
|
217
|
+
const references = fkByColumn.get(col.name);
|
|
218
|
+
return {
|
|
219
|
+
key,
|
|
220
|
+
dbName: col.name,
|
|
221
|
+
isPrimary: Boolean(col.primary),
|
|
222
|
+
type,
|
|
223
|
+
nullable: !col.notNull,
|
|
224
|
+
...references ? { references } : {}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function discoverSchema(opts) {
|
|
228
|
+
const load = opts.load ?? defaultLoader(opts.cwd);
|
|
229
|
+
const mod = await loadSchemaModule(opts.schemaPath, load);
|
|
230
|
+
const drizzle = await load("drizzle-orm");
|
|
231
|
+
const pgCore = await loadPgCoreRuntime(load);
|
|
232
|
+
const importPath = toImportPath(opts.schemaPath, opts.cwd);
|
|
233
|
+
const tables = [];
|
|
234
|
+
let db = null;
|
|
235
|
+
for (const [moduleKey, value] of Object.entries(mod)) {
|
|
236
|
+
if (drizzle.is(value, drizzle.Table)) {
|
|
237
|
+
const fkByColumn = buildFkByColumn(pgCore, drizzle, value);
|
|
238
|
+
const columns = Object.entries(drizzle.getTableColumns(value)).map(([key, col]) => toDiscoveredColumn(key, col, fkByColumn));
|
|
239
|
+
tables.push({
|
|
240
|
+
moduleKey,
|
|
241
|
+
tableName: drizzle.getTableName(value),
|
|
242
|
+
columns
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (db === null && value && typeof value.select === "function") db = {
|
|
247
|
+
moduleKey,
|
|
248
|
+
importPath
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
tables,
|
|
253
|
+
importPath,
|
|
254
|
+
db
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/doctor.ts
|
|
259
|
+
async function loadSdkFromConfig(path) {
|
|
260
|
+
const mod = await createJiti(import.meta.url).import(path);
|
|
261
|
+
const sdk = mod.default ?? mod;
|
|
262
|
+
if (typeof sdk.doctor !== "function") throw new Error("default export is not a Clivly SDK instance");
|
|
263
|
+
return sdk;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* `clivly doctor` — load the app's clivly.config, run the SDK's read-only setup
|
|
267
|
+
* checks, and print a ✅/❌ checklist. Exit 0 when every check passes. `loadSdk`
|
|
268
|
+
* and `out` are injectable for tests.
|
|
269
|
+
*/
|
|
270
|
+
async function runDoctor(options) {
|
|
271
|
+
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
272
|
+
const path = resolve(options.cwd, options.configPath ?? "clivly.config.ts");
|
|
273
|
+
const loadSdk = options.loadSdk ?? loadSdkFromConfig;
|
|
274
|
+
out("\nClivly doctor\n");
|
|
275
|
+
let sdk;
|
|
276
|
+
try {
|
|
277
|
+
sdk = await loadSdk(path);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
out(checkLine(false, "Load clivly.config", error.message));
|
|
280
|
+
out(" Expected clivly.config.ts at your app root with `export default createClivlySDK({...})`.\n");
|
|
281
|
+
return 1;
|
|
282
|
+
}
|
|
283
|
+
const report = await sdk.doctor();
|
|
284
|
+
for (const check of report.checks) {
|
|
285
|
+
if (check.skipped) {
|
|
286
|
+
out(` ⚠️ ${check.name} — ${check.detail}\n`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
out(checkLine(check.ok, check.name, check.detail));
|
|
290
|
+
}
|
|
291
|
+
return report.ok ? 0 : 1;
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/env-file.ts
|
|
295
|
+
function hasKey(content, key) {
|
|
296
|
+
return new RegExp(`^\\s*${key}=`, "m").test(content);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Append only the `entries` whose key is absent, each preceded by its comment.
|
|
300
|
+
* Existing keys/values are left exactly as-is. Returns the full new content.
|
|
301
|
+
*/
|
|
302
|
+
function appendEnvKeys(existing, entries) {
|
|
303
|
+
const missing = entries.filter((e) => !hasKey(existing, e.key));
|
|
304
|
+
if (missing.length === 0) return existing;
|
|
305
|
+
const block = missing.map((e) => `${e.comment ? `# ${e.comment}\n` : ""}${e.key}=`).join("\n");
|
|
306
|
+
return `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${existing.length > 0 ? "\n" : ""}# Clivly\n${block}\n`;
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/install.ts
|
|
310
|
+
const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject) => {
|
|
311
|
+
const child = spawn(command, args, {
|
|
312
|
+
cwd,
|
|
313
|
+
stdio: "inherit"
|
|
314
|
+
});
|
|
315
|
+
child.on("error", reject);
|
|
316
|
+
child.on("close", (code) => resolvePromise(code ?? 0));
|
|
317
|
+
});
|
|
318
|
+
const QUIET_FLAGS = {
|
|
319
|
+
npm: [
|
|
320
|
+
"--no-audit",
|
|
321
|
+
"--no-fund",
|
|
322
|
+
"--loglevel=error"
|
|
323
|
+
],
|
|
324
|
+
pnpm: ["--reporter=silent"],
|
|
325
|
+
yarn: [],
|
|
326
|
+
bun: []
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* Best-effort install: run `<pm> add <packages>`. Never throws — returns
|
|
330
|
+
* `{ ok, command }` so the caller can print the exact command on failure and
|
|
331
|
+
* continue scaffolding.
|
|
332
|
+
*/
|
|
333
|
+
async function installPackages(options) {
|
|
334
|
+
const { command, args } = addCommand(options.pm, options.packages);
|
|
335
|
+
const fullArgs = [...args, ...QUIET_FLAGS[options.pm]];
|
|
336
|
+
const printable = `${command} ${fullArgs.join(" ")}`;
|
|
337
|
+
const run = options.run ?? defaultRun;
|
|
338
|
+
try {
|
|
339
|
+
return {
|
|
340
|
+
ok: await run(command, fullArgs, options.cwd) === 0,
|
|
341
|
+
command: printable
|
|
342
|
+
};
|
|
343
|
+
} catch {
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
command: printable
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/is-entry.ts
|
|
352
|
+
function isEntrypoint(argv1, moduleUrl, realpath = realpathSync) {
|
|
353
|
+
if (!argv1) return false;
|
|
354
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
355
|
+
const resolve = (path) => {
|
|
356
|
+
try {
|
|
357
|
+
return realpath(path);
|
|
358
|
+
} catch {
|
|
359
|
+
return path;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
return resolve(argv1) === resolve(modulePath);
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/ping.ts
|
|
366
|
+
const REASON_HINT = {
|
|
367
|
+
ok: "",
|
|
368
|
+
invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
|
|
369
|
+
forbidden: "The key is valid but lacks access to this org (403).",
|
|
370
|
+
rate_limited: "Rate limited (429). Wait a moment and retry.",
|
|
371
|
+
server_error: "Clivly returned a server error (5xx). Retry shortly.",
|
|
372
|
+
client_error: "The request was rejected (4xx). Check your configuration.",
|
|
373
|
+
network_error: "Could not reach Clivly. Check your network or --api-url."
|
|
374
|
+
};
|
|
375
|
+
const defaultConnect = (apiKey, apiUrl) => createClivlySDK({
|
|
376
|
+
apiKey,
|
|
377
|
+
apiUrl,
|
|
378
|
+
retry: { maxAttempts: 1 }
|
|
379
|
+
}).connect();
|
|
380
|
+
function resultLines(result) {
|
|
381
|
+
if (result.ok) {
|
|
382
|
+
const lines = [checkLine(true, "Connected to Clivly")];
|
|
383
|
+
if (result.org) {
|
|
384
|
+
const name = result.org.name ?? "(unnamed org)";
|
|
385
|
+
const id = result.org.id ? ` (${result.org.id})` : "";
|
|
386
|
+
lines.push(` Org: ${name}${id}\n`);
|
|
387
|
+
}
|
|
388
|
+
return lines;
|
|
389
|
+
}
|
|
390
|
+
const status = result.status === null ? "no response" : `HTTP ${result.status}`;
|
|
391
|
+
return [checkLine(false, `Could not connect (${result.reason}, ${status})`), ` ${REASON_HINT[result.reason]}\n`];
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* `clivly ping` — send one heartbeat with the configured key and print a clear
|
|
395
|
+
* ✅/❌ with the resolved org or the failure reason. Verifies key + reachability
|
|
396
|
+
* from the terminal with zero code. `connect`/`out` are injectable for tests.
|
|
397
|
+
*/
|
|
398
|
+
async function runPing(options) {
|
|
399
|
+
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
400
|
+
out("\nClivly ping\n");
|
|
401
|
+
if (!options.apiKey) {
|
|
402
|
+
out(checkLine(false, "CLIVLY_API_KEY is not set."));
|
|
403
|
+
out(" Set it to your sk_live_… key from Settings → API keys.\n");
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
const result = await (options.connect ?? defaultConnect)(options.apiKey, options.apiUrl);
|
|
407
|
+
for (const line of resultLines(result)) out(line);
|
|
408
|
+
return result.ok ? 0 : 1;
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/plan-init.ts
|
|
412
|
+
function toScoredTable(table) {
|
|
413
|
+
return {
|
|
414
|
+
name: table.tableName,
|
|
415
|
+
columns: table.columns.map((c) => c.dbName),
|
|
416
|
+
columnsMeta: table.columns.map((c) => ({
|
|
417
|
+
name: c.dbName,
|
|
418
|
+
type: c.type,
|
|
419
|
+
nullable: c.nullable,
|
|
420
|
+
isPrimaryKey: c.isPrimary,
|
|
421
|
+
isForeignKey: c.references !== void 0,
|
|
422
|
+
...c.references ? { references: c.references } : {}
|
|
423
|
+
}))
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function rankCandidates(tables, concept) {
|
|
427
|
+
return tables.map((t) => ({
|
|
428
|
+
t,
|
|
429
|
+
m: matchesConcept(t.tableName, concept)
|
|
430
|
+
})).filter((c) => Boolean(c.m)).sort((a, b) => Number(b.m.exact) - Number(a.m.exact)).map((c) => c.t);
|
|
431
|
+
}
|
|
432
|
+
function selectEntities(tables) {
|
|
433
|
+
const contactCands = rankCandidates(tables, "contact");
|
|
434
|
+
const companyCands = rankCandidates(tables, "company");
|
|
435
|
+
const contactNames = new Set(contactCands.map((t) => t.tableName));
|
|
436
|
+
const companyNames = new Set(companyCands.map((t) => t.tableName));
|
|
437
|
+
const contactExclusive = contactCands.filter((t) => !companyNames.has(t.tableName));
|
|
438
|
+
const companyExclusive = companyCands.filter((t) => !contactNames.has(t.tableName));
|
|
439
|
+
const contact = contactExclusive[0] ?? contactCands[0] ?? null;
|
|
440
|
+
const companyPreferred = companyExclusive[0] ?? companyCands[0] ?? null;
|
|
441
|
+
return {
|
|
442
|
+
contact,
|
|
443
|
+
company: companyPreferred && companyPreferred.tableName !== contact?.tableName ? companyPreferred : companyCands.find((t) => t.tableName !== contact?.tableName) ?? null
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function chooseSyncFields(table, todos) {
|
|
447
|
+
const columnKeys = table.columns.map((c) => c.key);
|
|
448
|
+
let cursorField = suggestCursorField(columnKeys);
|
|
449
|
+
if (!cursorField) {
|
|
450
|
+
cursorField = table.columns.find((c) => c.isPrimary)?.key ?? columnKeys[0] ?? "id";
|
|
451
|
+
todos.push(`No updated_at-like column on "${table.tableName}" — using "${cursorField}" as the sync cursor. Confirm it is monotonic.`);
|
|
452
|
+
}
|
|
453
|
+
const pkCols = table.columns.filter((c) => c.isPrimary);
|
|
454
|
+
if (pkCols.length === 1) return {
|
|
455
|
+
cursorField,
|
|
456
|
+
idField: null
|
|
457
|
+
};
|
|
458
|
+
const idField = pkCols[0]?.key ?? columnKeys[0] ?? "id";
|
|
459
|
+
todos.push(`Table "${table.tableName}" has ${pkCols.length === 0 ? "no" : "a composite"} primary key — set idField (guessed "${idField}").`);
|
|
460
|
+
return {
|
|
461
|
+
cursorField,
|
|
462
|
+
idField
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function buildEntity(table, concept, todos) {
|
|
466
|
+
const canonical = CANONICAL_FIELDS[concept];
|
|
467
|
+
const suggested = toFieldMap(scoreFieldMap(concept, toScoredTable(table)));
|
|
468
|
+
const fields = {};
|
|
469
|
+
for (const [key, column] of Object.entries(suggested)) if (canonical.includes(key)) fields[key] = column;
|
|
470
|
+
for (const required of requiredFieldsFor(concept)) if (!fields[required]) todos.push(`Could not map required ${concept} field "${required}" on "${table.tableName}" — add it manually.`);
|
|
471
|
+
const { cursorField, idField } = chooseSyncFields(table, todos);
|
|
472
|
+
return {
|
|
473
|
+
moduleKey: table.moduleKey,
|
|
474
|
+
tableName: table.tableName,
|
|
475
|
+
concept,
|
|
476
|
+
fields,
|
|
477
|
+
cursorField,
|
|
478
|
+
idField
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function placeholderContact(todos) {
|
|
482
|
+
todos.push("No contact table detected. Filled in a placeholder — set `source`, `fields`, and the fromDrizzle table below, then run `clivly doctor`.");
|
|
483
|
+
return {
|
|
484
|
+
moduleKey: "contacts",
|
|
485
|
+
tableName: "contacts",
|
|
486
|
+
concept: "contact",
|
|
487
|
+
fields: {
|
|
488
|
+
name: "name",
|
|
489
|
+
email: "email"
|
|
490
|
+
},
|
|
491
|
+
cursorField: "updatedAt",
|
|
492
|
+
idField: null
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function planInit(schema) {
|
|
496
|
+
const todos = [];
|
|
497
|
+
const { contact: contactTable, company: companyTable } = selectEntities(schema.tables);
|
|
498
|
+
const contactDetected = contactTable !== null;
|
|
499
|
+
const contact = contactTable ? buildEntity(contactTable, "contact", todos) : placeholderContact(todos);
|
|
500
|
+
let company = null;
|
|
501
|
+
if (companyTable) {
|
|
502
|
+
company = buildEntity(companyTable, "company", todos);
|
|
503
|
+
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.`);
|
|
504
|
+
}
|
|
505
|
+
if (contactTable) {
|
|
506
|
+
const scored = scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
|
|
507
|
+
const top = scored[0];
|
|
508
|
+
const runnerUp = scored[1];
|
|
509
|
+
const isClearWinner = top !== void 0 && (runnerUp === void 0 || top.score - runnerUp.score >= 15);
|
|
510
|
+
if (top && isClearWinner) contact.relationship = {
|
|
511
|
+
name: top.value.name,
|
|
512
|
+
via: top.value.via
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
importPath: schema.importPath,
|
|
517
|
+
db: schema.db,
|
|
518
|
+
contact,
|
|
519
|
+
company,
|
|
520
|
+
todos,
|
|
521
|
+
contactDetected
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/render-config.ts
|
|
526
|
+
const CLIVLY_ENV_KEYS = [
|
|
527
|
+
{
|
|
528
|
+
key: "CLIVLY_API_KEY",
|
|
529
|
+
comment: "Org API key — Settings → API keys"
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
key: "CLIVLY_SYNC_TRIGGER_URL",
|
|
533
|
+
comment: "Public URL of your mounted tick route (e.g. https://app.example.com/api/clivly/tick)"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
key: "CLIVLY_SYNC_TRIGGER_SECRET",
|
|
537
|
+
comment: "Signing secret — Integrations → Remote sync"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
key: "CLIVLY_VERIFY_TOKEN",
|
|
541
|
+
comment: "Bearer token Clivly presents to /clivly/auth/verify"
|
|
542
|
+
}
|
|
543
|
+
];
|
|
544
|
+
function renderFields(fields) {
|
|
545
|
+
return `{ ${Object.entries(fields).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
|
|
546
|
+
}
|
|
547
|
+
function renderRelationships(entity) {
|
|
548
|
+
if (!entity.relationship) return "";
|
|
549
|
+
const { name, via } = entity.relationship;
|
|
550
|
+
return `,\n relationships: { ${name}: { entity: "${name}", via: ${JSON.stringify(via)} } }`;
|
|
551
|
+
}
|
|
552
|
+
function renderEntity(key, entity) {
|
|
553
|
+
return ` ${key}: {
|
|
554
|
+
concept: "${entity.concept}",
|
|
555
|
+
source: "${entity.tableName}",
|
|
556
|
+
fields: ${renderFields(entity.fields)}${renderRelationships(entity)},
|
|
557
|
+
}`;
|
|
558
|
+
}
|
|
559
|
+
function renderSource(role, entity, dbExpr) {
|
|
560
|
+
const opts = entity.idField === null ? `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}" }` : `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}", idField: "${entity.idField}" }`;
|
|
561
|
+
return ` ${role}: fromDrizzle(${dbExpr}, schema.${entity.moduleKey}, ${opts})`;
|
|
562
|
+
}
|
|
563
|
+
function fallbackDbImportPath(schemaImportPath) {
|
|
564
|
+
const slash = schemaImportPath.lastIndexOf("/");
|
|
565
|
+
return `${slash >= 0 ? schemaImportPath.slice(0, slash) : "."}/db`;
|
|
566
|
+
}
|
|
567
|
+
function renderDbExpr(plan) {
|
|
568
|
+
return plan.db ? `schema.${plan.db.moduleKey}` : "db";
|
|
569
|
+
}
|
|
570
|
+
function renderImports(plan) {
|
|
571
|
+
const lines = [
|
|
572
|
+
"import { createClivlySDK } from \"clivly/sdk\";",
|
|
573
|
+
"import { fromDrizzle } from \"clivly/drizzle\";",
|
|
574
|
+
"import { defineClivlyConfig } from \"clivly/core\";",
|
|
575
|
+
"import { discoverFromDrizzle } from \"clivly/core/drizzle\";",
|
|
576
|
+
`import * as schema from "${plan.importPath}";`
|
|
577
|
+
];
|
|
578
|
+
if (!plan.db) lines.push(`import { db } from "${fallbackDbImportPath(plan.importPath)}"; // TODO: point at your Drizzle instance`);
|
|
579
|
+
return lines.join("\n");
|
|
580
|
+
}
|
|
581
|
+
function renderTodoHeader(todos) {
|
|
582
|
+
if (todos.length === 0) return "";
|
|
583
|
+
return `// TODO(clivly): review before running \`clivly doctor\`:\n${todos.map((t) => `// - ${t}`).join("\n")}\n\n`;
|
|
584
|
+
}
|
|
585
|
+
function renderInitConfig(plan) {
|
|
586
|
+
const dbExpr = renderDbExpr(plan);
|
|
587
|
+
const entities = [renderEntity(plan.contact.moduleKey, plan.contact)];
|
|
588
|
+
if (plan.company) entities.push(renderEntity(plan.company.moduleKey, plan.company));
|
|
589
|
+
const sources = [renderSource("contacts", plan.contact, dbExpr)];
|
|
590
|
+
if (plan.company) sources.push(renderSource("companies", plan.company, dbExpr));
|
|
591
|
+
return `${renderTodoHeader(plan.todos)}${renderImports(plan)}
|
|
592
|
+
|
|
593
|
+
const entities = defineClivlyConfig({
|
|
594
|
+
entities: {
|
|
595
|
+
${entities.join(",\n")},
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
export default createClivlySDK({
|
|
600
|
+
apiKey: process.env.CLIVLY_API_KEY!,
|
|
601
|
+
entities,
|
|
602
|
+
// Every table in your Drizzle schema, so the Clivly dashboard can offer them
|
|
603
|
+
// all when you add a mapping — not just the ones wired up as sources below.
|
|
604
|
+
schema: discoverFromDrizzle(schema),
|
|
605
|
+
source: {
|
|
606
|
+
${sources.join(",\n")},
|
|
607
|
+
},
|
|
608
|
+
syncTrigger: {
|
|
609
|
+
// The public URL Clivly Cloud calls to trigger a sync — your mounted tick route.
|
|
610
|
+
url: process.env.CLIVLY_SYNC_TRIGGER_URL,
|
|
611
|
+
secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
|
|
612
|
+
},
|
|
613
|
+
verifyToken: process.env.CLIVLY_VERIFY_TOKEN,
|
|
614
|
+
});
|
|
615
|
+
`;
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/resolve-schema-path.ts
|
|
619
|
+
const COMMON_PATHS = [
|
|
620
|
+
"src/db/schema.ts",
|
|
621
|
+
"src/db/schema",
|
|
622
|
+
"db/schema.ts",
|
|
623
|
+
"db/schema",
|
|
624
|
+
"src/schema.ts",
|
|
625
|
+
"src/schema",
|
|
626
|
+
"drizzle/schema.ts",
|
|
627
|
+
"drizzle/schema"
|
|
628
|
+
];
|
|
629
|
+
const DRIZZLE_CONFIG_NAMES = [
|
|
630
|
+
"drizzle.config.ts",
|
|
631
|
+
"drizzle.config.js",
|
|
632
|
+
"drizzle.config.mjs"
|
|
633
|
+
];
|
|
634
|
+
function defaultReadDrizzleConfigSchema(cwd) {
|
|
635
|
+
for (const name of DRIZZLE_CONFIG_NAMES) {
|
|
636
|
+
const path = resolve(cwd, name);
|
|
637
|
+
if (!existsSync(path)) continue;
|
|
638
|
+
try {
|
|
639
|
+
const schema = createJiti(pathToFileURL(path).href)(path).default?.schema;
|
|
640
|
+
const first = Array.isArray(schema) ? schema[0] : schema;
|
|
641
|
+
if (typeof first === "string") return first;
|
|
642
|
+
} catch {}
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function resolveSchemaPath(opts) {
|
|
647
|
+
const exists = opts.deps?.exists ?? existsSync;
|
|
648
|
+
const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
|
|
649
|
+
const toAbs = (p) => isAbsolute(p) ? p : resolve(opts.cwd, p);
|
|
650
|
+
if (opts.flag) return toAbs(opts.flag);
|
|
651
|
+
const fromConfig = readSchema(opts.cwd);
|
|
652
|
+
if (fromConfig) return toAbs(fromConfig);
|
|
653
|
+
for (const candidate of COMMON_PATHS) {
|
|
654
|
+
const abs = toAbs(candidate);
|
|
655
|
+
if (exists(abs)) return abs;
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/scaffold.ts
|
|
661
|
+
const TANSTACK_ROUTE_PREFIX = /^src\/routes\//;
|
|
662
|
+
const TS_EXTENSION = /\.ts$/;
|
|
663
|
+
const ROUTE_PATHS = {
|
|
664
|
+
nextjs: {
|
|
665
|
+
tick: "app/api/clivly/tick/route.ts",
|
|
666
|
+
verify: "app/api/clivly/auth/verify/route.ts"
|
|
667
|
+
},
|
|
668
|
+
"tanstack-start": {
|
|
669
|
+
tick: "src/routes/api/clivly/tick.ts",
|
|
670
|
+
verify: "src/routes/api/clivly/auth/verify.ts"
|
|
671
|
+
},
|
|
672
|
+
sveltekit: {
|
|
673
|
+
tick: "src/routes/api/clivly/tick/+server.ts",
|
|
674
|
+
verify: "src/routes/api/clivly/auth/verify/+server.ts"
|
|
675
|
+
},
|
|
676
|
+
remix: {
|
|
677
|
+
tick: "app/routes/api.clivly.tick.ts",
|
|
678
|
+
verify: "app/routes/api.clivly.auth.verify.ts"
|
|
679
|
+
},
|
|
680
|
+
nuxt: {
|
|
681
|
+
tick: "server/api/clivly/tick.ts",
|
|
682
|
+
verify: "server/api/clivly/auth/verify.ts"
|
|
683
|
+
},
|
|
684
|
+
unknown: null
|
|
685
|
+
};
|
|
686
|
+
function routePath(framework, kind) {
|
|
687
|
+
const paths = ROUTE_PATHS[framework];
|
|
688
|
+
if (!paths) throw new Error(`No route path for framework: ${framework}`);
|
|
689
|
+
return paths[kind];
|
|
690
|
+
}
|
|
691
|
+
function configImport(routeFilePath) {
|
|
692
|
+
const rel = relative(dirname(routeFilePath), "clivly.config").replace(/\\/g, "/");
|
|
693
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
694
|
+
}
|
|
695
|
+
function renderBody(framework, importPath, verify) {
|
|
696
|
+
const preamble = verify ? `import clivly from "${importPath}";
|
|
697
|
+
|
|
698
|
+
// TODO(clivly): resolve the signed-in host user from the request (your auth stack).
|
|
699
|
+
const resolveUser = (_req: Request) => null;
|
|
700
|
+
const handler = clivly.createAuthVerifyHandler(resolveUser);
|
|
701
|
+
` : `import clivly from "${importPath}";
|
|
702
|
+
|
|
703
|
+
const handler = clivly.createClivlyHandler();
|
|
704
|
+
`;
|
|
705
|
+
switch (framework) {
|
|
706
|
+
case "nextjs": return `${preamble}
|
|
707
|
+
export const POST = (request: Request) => handler(request);
|
|
708
|
+
`;
|
|
709
|
+
case "tanstack-start": return `import { createAPIFileRoute } from "@tanstack/react-start/api";
|
|
710
|
+
${preamble}
|
|
711
|
+
export const APIRoute = createAPIFileRoute("__ROUTE__")({
|
|
712
|
+
POST: ({ request }) => handler(request),
|
|
713
|
+
});
|
|
714
|
+
`;
|
|
715
|
+
case "sveltekit": return `import type { RequestHandler } from "./$types";
|
|
716
|
+
${preamble}
|
|
717
|
+
export const POST: RequestHandler = ({ request }) => handler(request);
|
|
718
|
+
`;
|
|
719
|
+
case "remix": return `import type { ActionFunctionArgs } from "@remix-run/node";
|
|
720
|
+
${preamble}
|
|
721
|
+
export const action = ({ request }: ActionFunctionArgs) => handler(request);
|
|
722
|
+
`;
|
|
723
|
+
case "nuxt": return `import { toWebRequest } from "h3";
|
|
724
|
+
${preamble}
|
|
725
|
+
export default defineEventHandler((event) => handler(toWebRequest(event)));
|
|
726
|
+
`;
|
|
727
|
+
default: throw new Error(`No route template for framework: ${framework}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function renderRouteFile(framework, kind) {
|
|
731
|
+
const path = routePath(framework, kind);
|
|
732
|
+
let body = renderBody(framework, configImport(path), kind === "verify");
|
|
733
|
+
if (framework === "tanstack-start") {
|
|
734
|
+
const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
|
|
735
|
+
body = body.replace("__ROUTE__", url);
|
|
736
|
+
}
|
|
737
|
+
return body;
|
|
738
|
+
}
|
|
739
|
+
const COMMON_FILES = [{
|
|
740
|
+
path: "clivly.config.ts",
|
|
741
|
+
description: "Your Clivly configuration",
|
|
742
|
+
owned: false
|
|
743
|
+
}];
|
|
744
|
+
function planScaffold(framework) {
|
|
745
|
+
const paths = ROUTE_PATHS[framework];
|
|
746
|
+
if (!paths) return [];
|
|
747
|
+
return [
|
|
748
|
+
{
|
|
749
|
+
path: paths.tick,
|
|
750
|
+
description: "Clivly sync-trigger route (createClivlyHandler)",
|
|
751
|
+
owned: true
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
path: paths.verify,
|
|
755
|
+
description: "Clivly auth-verify route (createAuthVerifyHandler) — finish resolveUser",
|
|
756
|
+
owned: true
|
|
757
|
+
},
|
|
758
|
+
...COMMON_FILES
|
|
759
|
+
];
|
|
760
|
+
}
|
|
761
|
+
//#endregion
|
|
762
|
+
//#region src/index.ts
|
|
763
|
+
function flagValue(argv, flag) {
|
|
764
|
+
const index = argv.indexOf(flag);
|
|
765
|
+
return index >= 0 ? argv[index + 1] : void 0;
|
|
766
|
+
}
|
|
767
|
+
function readDeps(cwd) {
|
|
768
|
+
const pkgPath = join(cwd, "package.json");
|
|
769
|
+
if (!existsSync(pkgPath)) return {};
|
|
770
|
+
try {
|
|
771
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
772
|
+
return {
|
|
773
|
+
...pkg.dependencies,
|
|
774
|
+
...pkg.devDependencies
|
|
775
|
+
};
|
|
776
|
+
} catch {
|
|
777
|
+
return {};
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function readOwnVersion() {
|
|
781
|
+
const req = createRequire(import.meta.url);
|
|
782
|
+
for (const rel of ["./package.json", "../package.json"]) try {
|
|
783
|
+
const pkg = req(rel);
|
|
784
|
+
if (pkg.name === "clivly" && pkg.version) return pkg.version;
|
|
785
|
+
} catch {}
|
|
786
|
+
}
|
|
787
|
+
function defaultReadEnv(cwd) {
|
|
788
|
+
const path = join(cwd, ".env");
|
|
789
|
+
return existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
790
|
+
}
|
|
791
|
+
function defaultWriteEnv(cwd, contents) {
|
|
792
|
+
writeFileSync(join(cwd, ".env"), contents, "utf8");
|
|
793
|
+
}
|
|
794
|
+
function resolveInitDeps(options) {
|
|
795
|
+
return {
|
|
796
|
+
out: options.out ?? ((line) => process.stdout.write(line)),
|
|
797
|
+
readDepsFn: options.readDeps ?? readDeps,
|
|
798
|
+
resolvePath: options.resolveSchemaPath ?? ((cwd) => resolveSchemaPath({
|
|
799
|
+
cwd,
|
|
800
|
+
flag: options.schemaFlag
|
|
801
|
+
})),
|
|
802
|
+
discover: options.discoverSchema ?? ((schemaPath, cwd) => discoverSchema({
|
|
803
|
+
schemaPath,
|
|
804
|
+
cwd
|
|
805
|
+
})),
|
|
806
|
+
configExists: options.configExists ?? existsSync,
|
|
807
|
+
writeConfig: options.writeConfig ?? ((path, contents) => {
|
|
808
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
809
|
+
writeFileSync(path, contents, "utf8");
|
|
810
|
+
}),
|
|
811
|
+
detectPackageManager: options.detectPackageManager ?? detectPackageManager,
|
|
812
|
+
install: options.installPackages ?? installPackages,
|
|
813
|
+
readEnv: options.readEnv ?? defaultReadEnv,
|
|
814
|
+
readOwnVersion: options.readOwnVersion ?? readOwnVersion,
|
|
815
|
+
writeEnv: options.writeEnv ?? defaultWriteEnv,
|
|
816
|
+
framework: "unknown"
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
async function acquireSchema(options, deps) {
|
|
820
|
+
const { out } = deps;
|
|
821
|
+
const appDeps = deps.readDepsFn(options.cwd);
|
|
822
|
+
if (Object.keys(appDeps).length === 0) {
|
|
823
|
+
out("No package.json found here. Run `clivly init` from your app root.\n");
|
|
824
|
+
return { code: 1 };
|
|
825
|
+
}
|
|
826
|
+
const framework = detectFramework(appDeps);
|
|
827
|
+
const orm = detectOrm(appDeps);
|
|
828
|
+
deps.framework = framework;
|
|
829
|
+
out("\nClivly init\n");
|
|
830
|
+
out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
|
|
831
|
+
out(` ORM: ${orm}\n\n`);
|
|
832
|
+
if (orm !== "drizzle") {
|
|
833
|
+
out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
|
|
834
|
+
return { code: 1 };
|
|
835
|
+
}
|
|
836
|
+
if (framework === "unknown") {
|
|
837
|
+
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");
|
|
838
|
+
return { code: 1 };
|
|
839
|
+
}
|
|
840
|
+
const schemaPath = deps.resolvePath(options.cwd);
|
|
841
|
+
if (!schemaPath) {
|
|
842
|
+
out("Could not locate your Drizzle schema. Pass --schema <path>, or add a `schema` field to drizzle.config.ts.\n");
|
|
843
|
+
return { code: 1 };
|
|
844
|
+
}
|
|
845
|
+
out(` Schema: ${schemaPath}\n\n`);
|
|
846
|
+
try {
|
|
847
|
+
const schema = await deps.discover(schemaPath, options.cwd);
|
|
848
|
+
if (schema.tables.length === 0) {
|
|
849
|
+
out(`No Drizzle tables found in ${schemaPath}. Is this the right path?\n`);
|
|
850
|
+
return { code: 1 };
|
|
851
|
+
}
|
|
852
|
+
return schema;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
out(`Failed to load the schema module: ${error.message}\n`);
|
|
855
|
+
out("The schema must import cleanly without a live DB connection.\n");
|
|
856
|
+
return { code: 1 };
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function printPlan(out, plan) {
|
|
860
|
+
out("Detected:\n");
|
|
861
|
+
out(` contact ← ${plan.contact.tableName} fields ${JSON.stringify(plan.contact.fields)}\n`);
|
|
862
|
+
if (plan.company) out(` company ← ${plan.company.tableName} fields ${JSON.stringify(plan.company.fields)}\n`);
|
|
863
|
+
out("\n");
|
|
864
|
+
if (plan.todos.length > 0) {
|
|
865
|
+
out("Review after generation:\n");
|
|
866
|
+
for (const todo of plan.todos) out(` • ${todo}\n`);
|
|
867
|
+
out("\n");
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function writeOwnedFile(options, deps, path, contents) {
|
|
871
|
+
const full = join(options.cwd, path);
|
|
872
|
+
if (deps.configExists(full) && !options.force) {
|
|
873
|
+
deps.out(` • ${path} exists — skipped (pass --force to overwrite)\n`);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
deps.writeConfig(full, contents);
|
|
877
|
+
deps.out(` ✓ ${path}\n`);
|
|
878
|
+
}
|
|
879
|
+
function writeScaffoldFiles(options, deps, plan) {
|
|
880
|
+
writeOwnedFile(options, deps, "clivly.config.ts", renderInitConfig(plan));
|
|
881
|
+
for (const file of planScaffold(deps.framework)) {
|
|
882
|
+
if (file.path === "clivly.config.ts") continue;
|
|
883
|
+
const kind = file.path.includes("verify") ? "verify" : "tick";
|
|
884
|
+
writeOwnedFile(options, deps, file.path, renderRouteFile(deps.framework, kind));
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
function writeEnvFile(options, deps) {
|
|
888
|
+
const nextEnv = appendEnvKeys(deps.readEnv(options.cwd), CLIVLY_ENV_KEYS);
|
|
889
|
+
deps.writeEnv(options.cwd, nextEnv);
|
|
890
|
+
deps.out(" ✓ .env placeholders\n");
|
|
891
|
+
}
|
|
892
|
+
function printNextSteps(out) {
|
|
893
|
+
out("\nNext steps:\n");
|
|
894
|
+
out(" 1. Set the CLIVLY_* values in .env\n");
|
|
895
|
+
out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
|
|
896
|
+
out(" 3. Finish resolveUser in the auth/verify route\n");
|
|
897
|
+
out(" 4. Run `clivly connect` to verify\n");
|
|
898
|
+
}
|
|
899
|
+
async function scaffold(options, deps, plan) {
|
|
900
|
+
const { out } = deps;
|
|
901
|
+
const pm = deps.detectPackageManager(options.cwd);
|
|
902
|
+
const version = deps.readOwnVersion();
|
|
903
|
+
const spec = version ? `clivly@${version}` : "clivly";
|
|
904
|
+
const result = await deps.install({
|
|
905
|
+
cwd: options.cwd,
|
|
906
|
+
pm,
|
|
907
|
+
packages: [spec]
|
|
908
|
+
});
|
|
909
|
+
if (result.ok) out(`Installed clivly (${pm})\n`);
|
|
910
|
+
else out(`Could not install automatically — run:\n ${result.command}\n`);
|
|
911
|
+
writeScaffoldFiles(options, deps, plan);
|
|
912
|
+
writeEnvFile(options, deps);
|
|
913
|
+
printNextSteps(out);
|
|
914
|
+
return plan.contactDetected ? 0 : 1;
|
|
915
|
+
}
|
|
916
|
+
async function runInit(options) {
|
|
917
|
+
const deps = resolveInitDeps(options);
|
|
918
|
+
const acquired = await acquireSchema(options, deps);
|
|
919
|
+
if ("code" in acquired) return acquired.code;
|
|
920
|
+
const plan = planInit(acquired);
|
|
921
|
+
printPlan(deps.out, plan);
|
|
922
|
+
return scaffold(options, deps, plan);
|
|
923
|
+
}
|
|
924
|
+
async function main(argv) {
|
|
925
|
+
const [command, ...rest] = argv;
|
|
926
|
+
switch (command) {
|
|
927
|
+
case "init": return await runInit({
|
|
928
|
+
cwd: process.cwd(),
|
|
929
|
+
force: rest.includes("--force"),
|
|
930
|
+
schemaFlag: flagValue(rest, "--schema")
|
|
931
|
+
});
|
|
932
|
+
case "ping": return await runPing({
|
|
933
|
+
apiKey: process.env.CLIVLY_API_KEY,
|
|
934
|
+
apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL
|
|
935
|
+
});
|
|
936
|
+
case "doctor": return await runDoctor({
|
|
937
|
+
cwd: process.cwd(),
|
|
938
|
+
configPath: flagValue(rest, "--config")
|
|
939
|
+
});
|
|
940
|
+
case "connect": return await runConnect({
|
|
941
|
+
cwd: process.cwd(),
|
|
942
|
+
apiKey: process.env.CLIVLY_API_KEY,
|
|
943
|
+
apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
|
|
944
|
+
configPath: flagValue(rest, "--config")
|
|
945
|
+
});
|
|
946
|
+
case void 0:
|
|
947
|
+
case "help":
|
|
948
|
+
case "--help":
|
|
949
|
+
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");
|
|
950
|
+
return 0;
|
|
951
|
+
default:
|
|
952
|
+
process.stdout.write(`Unknown command: ${command}\n`);
|
|
953
|
+
return 1;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (isEntrypoint(process.argv[1], import.meta.url)) main(process.argv.slice(2)).then((code) => {
|
|
957
|
+
process.exitCode = code;
|
|
958
|
+
});
|
|
959
|
+
//#endregion
|
|
960
|
+
export { runInit };
|