clivly 0.2.2 → 0.3.0-next.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/{adapter-Cy4q2CLs.d.cts → adapter-CXr12l4K.d.cts} +2 -2
- package/{adapter-Cd6D_3JD.d.mts → adapter-CvNBD_3y.d.mts} +2 -2
- package/core-adapter.d.cts +1 -1
- package/core-adapter.d.mts +1 -1
- package/core-entity-config.cjs +1 -1
- package/core-entity-config.mjs +1 -1
- package/core-entity-heuristics.cjs +2 -2
- package/core-entity-heuristics.mjs +2 -2
- package/core-mapping-score.cjs +1 -1
- package/core-mapping-score.mjs +1 -1
- package/core-mappings-config.cjs +1 -1
- package/core-mappings-config.mjs +1 -1
- package/core.cjs +2 -2
- package/core.d.cts +1 -1
- package/core.d.mts +1 -1
- package/core.mjs +2 -2
- package/drizzle.cjs +1 -1
- package/drizzle.mjs +1 -1
- package/index.cjs +653 -258
- package/index.d.cts +10 -0
- package/index.d.mts +10 -0
- package/index.mjs +651 -256
- package/{mappings-config-CYPrzB3K.mjs → mappings-config-CNTZRD4g.mjs} +1 -1
- package/{mappings-config-DTu_2cyx.cjs → mappings-config-DYZ_uOOB.cjs} +1 -1
- package/package.json +1 -1
- package/{sdk-CP_mYZa5.cjs → sdk-BNdK3KgW.cjs} +1 -1
- package/{sdk-BZUn6wzs.mjs → sdk-vu_uej2q.mjs} +1 -1
- package/sdk.cjs +1 -1
- package/sdk.mjs +1 -1
- /package/{drizzle-meta-CnC336L-.mjs → drizzle-meta-1z6Apnt-.mjs} +0 -0
- /package/{drizzle-meta-BGn0BqGb.cjs → drizzle-meta-CM5M73Z4.cjs} +0 -0
- /package/{entity-config-DoR0khce.mjs → entity-config-D3YpQB0z.mjs} +0 -0
- /package/{entity-config-Cvjo1cpv.cjs → entity-config-DMuZpchz.cjs} +0 -0
- /package/{mapping-score-BJNe0lFn.mjs → mapping-score-CPPvwoZC.mjs} +0 -0
- /package/{mapping-score-D9gfhtRu.cjs → mapping-score-i3p8LWDF.cjs} +0 -0
package/index.mjs
CHANGED
|
@@ -1,84 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as
|
|
3
|
-
import
|
|
4
|
-
import "./mapping-score-BJNe0lFn.mjs";
|
|
2
|
+
import { t as CANONICAL_FIELDS } from "./entity-config-D3YpQB0z.mjs";
|
|
3
|
+
import "./mapping-score-CPPvwoZC.mjs";
|
|
5
4
|
import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./core-entity-heuristics.mjs";
|
|
5
|
+
import { t as createClivlySDK } from "./sdk-vu_uej2q.mjs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
|
-
import { createJiti } from "jiti";
|
|
10
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import { createJiti } from "jiti";
|
|
11
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
12
|
//#region src/detect.ts
|
|
83
13
|
const FRAMEWORK_MARKERS = [
|
|
84
14
|
{
|
|
@@ -116,6 +46,24 @@ const ORM_MARKERS = [
|
|
|
116
46
|
deps: ["kysely"]
|
|
117
47
|
}
|
|
118
48
|
];
|
|
49
|
+
const AUTH_MARKERS = [
|
|
50
|
+
{
|
|
51
|
+
auth: "clerk",
|
|
52
|
+
deps: [
|
|
53
|
+
"@clerk/nextjs",
|
|
54
|
+
"@clerk/backend",
|
|
55
|
+
"@clerk/tanstack-react-start"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
auth: "better-auth",
|
|
60
|
+
deps: ["better-auth"]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
auth: "workos",
|
|
64
|
+
deps: ["@workos-inc/node", "@workos-inc/authkit-nextjs"]
|
|
65
|
+
}
|
|
66
|
+
];
|
|
119
67
|
const FRAMEWORK_LABELS = {
|
|
120
68
|
nextjs: "Next.js",
|
|
121
69
|
"tanstack-start": "TanStack Start",
|
|
@@ -132,6 +80,10 @@ function detectOrm(deps) {
|
|
|
132
80
|
for (const { orm, deps: markers } of ORM_MARKERS) if (markers.some((dep) => dep in deps)) return orm;
|
|
133
81
|
return "unknown";
|
|
134
82
|
}
|
|
83
|
+
function detectAuth(deps) {
|
|
84
|
+
for (const { auth, deps: markers } of AUTH_MARKERS) if (markers.some((dep) => dep in deps)) return auth;
|
|
85
|
+
return "unknown";
|
|
86
|
+
}
|
|
135
87
|
const LOCKFILES = [
|
|
136
88
|
{
|
|
137
89
|
file: "pnpm-lock.yaml",
|
|
@@ -161,11 +113,28 @@ function addCommand(pm, packages) {
|
|
|
161
113
|
};
|
|
162
114
|
}
|
|
163
115
|
//#endregion
|
|
116
|
+
//#region src/probe-module.ts
|
|
117
|
+
async function probeModule(opts) {
|
|
118
|
+
for (const candidate of opts.candidates) {
|
|
119
|
+
let mod;
|
|
120
|
+
try {
|
|
121
|
+
mod = await opts.load(candidate);
|
|
122
|
+
} catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const [moduleKey, value] of Object.entries(mod)) if (opts.match(value)) return {
|
|
126
|
+
moduleKey,
|
|
127
|
+
importPath: toImportPath(candidate, opts.cwd)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
164
133
|
//#region src/discover-schema.ts
|
|
165
|
-
const WINDOWS_SEP = /\\/g;
|
|
134
|
+
const WINDOWS_SEP$1 = /\\/g;
|
|
166
135
|
const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
|
|
167
136
|
function toImportPath(schemaPath, cwd) {
|
|
168
|
-
const rel = relative(cwd, schemaPath).replace(WINDOWS_SEP, "/").replace(MODULE_EXT, "");
|
|
137
|
+
const rel = relative(cwd, schemaPath).replace(WINDOWS_SEP$1, "/").replace(MODULE_EXT, "");
|
|
169
138
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
170
139
|
}
|
|
171
140
|
function isDirectory(path) {
|
|
@@ -178,7 +147,7 @@ function isDirectory(path) {
|
|
|
178
147
|
function loadSchemaModule(schemaPath, load) {
|
|
179
148
|
return load(isDirectory(schemaPath) ? join(schemaPath, "index.ts") : schemaPath);
|
|
180
149
|
}
|
|
181
|
-
function defaultLoader(cwd) {
|
|
150
|
+
function defaultLoader$1(cwd) {
|
|
182
151
|
const jiti = createJiti(pathToFileURL(join(cwd, "clivly.config.ts")).href);
|
|
183
152
|
return (specifier) => jiti.import(specifier);
|
|
184
153
|
}
|
|
@@ -224,8 +193,20 @@ function toDiscoveredColumn(key, col, fkByColumn) {
|
|
|
224
193
|
...references ? { references } : {}
|
|
225
194
|
};
|
|
226
195
|
}
|
|
196
|
+
function isDrizzleDb(value) {
|
|
197
|
+
return typeof value?.select === "function";
|
|
198
|
+
}
|
|
199
|
+
const SIBLING_DB_NAMES = [
|
|
200
|
+
"db",
|
|
201
|
+
"client",
|
|
202
|
+
"index"
|
|
203
|
+
];
|
|
204
|
+
function siblingDbCandidates(schemaPath) {
|
|
205
|
+
const dir = isDirectory(schemaPath) ? schemaPath : dirname(schemaPath);
|
|
206
|
+
return SIBLING_DB_NAMES.map((name) => join(dir, name));
|
|
207
|
+
}
|
|
227
208
|
async function discoverSchema(opts) {
|
|
228
|
-
const load = opts.load ?? defaultLoader(opts.cwd);
|
|
209
|
+
const load = opts.load ?? defaultLoader$1(opts.cwd);
|
|
229
210
|
const mod = await loadSchemaModule(opts.schemaPath, load);
|
|
230
211
|
const drizzle = await load("drizzle-orm");
|
|
231
212
|
const pgCore = await loadPgCoreRuntime(load);
|
|
@@ -248,6 +229,12 @@ async function discoverSchema(opts) {
|
|
|
248
229
|
importPath
|
|
249
230
|
};
|
|
250
231
|
}
|
|
232
|
+
if (db === null) db = await probeModule({
|
|
233
|
+
candidates: siblingDbCandidates(opts.schemaPath),
|
|
234
|
+
cwd: opts.cwd,
|
|
235
|
+
load,
|
|
236
|
+
match: isDrizzleDb
|
|
237
|
+
});
|
|
251
238
|
return {
|
|
252
239
|
tables,
|
|
253
240
|
importPath,
|
|
@@ -255,57 +242,6 @@ async function discoverSchema(opts) {
|
|
|
255
242
|
};
|
|
256
243
|
}
|
|
257
244
|
//#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
245
|
//#region src/install.ts
|
|
310
246
|
const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject) => {
|
|
311
247
|
const child = spawn(command, args, {
|
|
@@ -362,52 +298,6 @@ function isEntrypoint(argv1, moduleUrl, realpath = realpathSync) {
|
|
|
362
298
|
return resolve(argv1) === resolve(modulePath);
|
|
363
299
|
}
|
|
364
300
|
//#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
301
|
//#region src/plan-init.ts
|
|
412
302
|
function toScoredTable(table) {
|
|
413
303
|
return {
|
|
@@ -522,6 +412,21 @@ function planInit(schema) {
|
|
|
522
412
|
};
|
|
523
413
|
}
|
|
524
414
|
//#endregion
|
|
415
|
+
//#region src/env-file.ts
|
|
416
|
+
function hasKey$1(content, key) {
|
|
417
|
+
return new RegExp(`^\\s*${key}=`, "m").test(content);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Append only the `entries` whose key is absent, each preceded by its comment.
|
|
421
|
+
* Existing keys/values are left exactly as-is. Returns the full new content.
|
|
422
|
+
*/
|
|
423
|
+
function appendEnvKeys(existing, entries) {
|
|
424
|
+
const missing = entries.filter((e) => !hasKey$1(existing, e.key));
|
|
425
|
+
if (missing.length === 0) return existing;
|
|
426
|
+
const block = missing.map((e) => `${e.comment ? `# ${e.comment}\n` : ""}${e.key}=`).join("\n");
|
|
427
|
+
return `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${existing.length > 0 ? "\n" : ""}# Clivly\n${block}\n`;
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
525
430
|
//#region src/render-config.ts
|
|
526
431
|
const CLIVLY_ENV_KEYS = [
|
|
527
432
|
{
|
|
@@ -564,8 +469,12 @@ function fallbackDbImportPath(schemaImportPath) {
|
|
|
564
469
|
const slash = schemaImportPath.lastIndexOf("/");
|
|
565
470
|
return `${slash >= 0 ? schemaImportPath.slice(0, slash) : "."}/db`;
|
|
566
471
|
}
|
|
472
|
+
function dbIsInSchemaModule(plan) {
|
|
473
|
+
return plan.db !== null && plan.db.importPath === plan.importPath;
|
|
474
|
+
}
|
|
567
475
|
function renderDbExpr(plan) {
|
|
568
|
-
|
|
476
|
+
if (!plan.db) return "db";
|
|
477
|
+
return dbIsInSchemaModule(plan) ? `schema.${plan.db.moduleKey}` : plan.db.moduleKey;
|
|
569
478
|
}
|
|
570
479
|
function renderImports(plan) {
|
|
571
480
|
const lines = [
|
|
@@ -576,6 +485,7 @@ function renderImports(plan) {
|
|
|
576
485
|
`import * as schema from "${plan.importPath}";`
|
|
577
486
|
];
|
|
578
487
|
if (!plan.db) lines.push(`import { db } from "${fallbackDbImportPath(plan.importPath)}"; // TODO: point at your Drizzle instance`);
|
|
488
|
+
else if (!dbIsInSchemaModule(plan)) lines.push(`import { ${plan.db.moduleKey} } from "${plan.db.importPath}";`);
|
|
579
489
|
return lines.join("\n");
|
|
580
490
|
}
|
|
581
491
|
function renderTodoHeader(todos) {
|
|
@@ -615,46 +525,79 @@ ${sources.join(",\n")},
|
|
|
615
525
|
`;
|
|
616
526
|
}
|
|
617
527
|
//#endregion
|
|
618
|
-
//#region src/
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
} catch {}
|
|
643
|
-
}
|
|
644
|
-
return null;
|
|
528
|
+
//#region src/render-auth.ts
|
|
529
|
+
const RESOLVE_USER = "const resolveUser = (req: Request) => adapter.getUser(req);";
|
|
530
|
+
const STUB = {
|
|
531
|
+
complete: false,
|
|
532
|
+
envKeys: [],
|
|
533
|
+
hostImport: null,
|
|
534
|
+
imports: [],
|
|
535
|
+
packages: [],
|
|
536
|
+
body: `// TODO(clivly): resolve the signed-in host user from the request (your auth stack).
|
|
537
|
+
const resolveUser = (_req: Request) => null;`
|
|
538
|
+
};
|
|
539
|
+
function clerkWiring() {
|
|
540
|
+
return {
|
|
541
|
+
complete: true,
|
|
542
|
+
envKeys: ["CLERK_SECRET_KEY", "CLERK_PUBLISHABLE_KEY"],
|
|
543
|
+
hostImport: null,
|
|
544
|
+
imports: ["import { createClerkAuthAdapter } from \"clivly/auth/clerk\";"],
|
|
545
|
+
packages: ["@clerk/backend"],
|
|
546
|
+
body: `const adapter = createClerkAuthAdapter({
|
|
547
|
+
secretKey: process.env.CLERK_SECRET_KEY ?? "",
|
|
548
|
+
publishableKey: process.env.CLERK_PUBLISHABLE_KEY,
|
|
549
|
+
});
|
|
550
|
+
${RESOLVE_USER}`
|
|
551
|
+
};
|
|
645
552
|
}
|
|
646
|
-
function
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
553
|
+
function workosWiring() {
|
|
554
|
+
return {
|
|
555
|
+
complete: true,
|
|
556
|
+
envKeys: [
|
|
557
|
+
"WORKOS_API_KEY",
|
|
558
|
+
"WORKOS_CLIENT_ID",
|
|
559
|
+
"WORKOS_COOKIE_PASSWORD"
|
|
560
|
+
],
|
|
561
|
+
hostImport: null,
|
|
562
|
+
imports: ["import { createWorkOSAuthAdapter } from \"clivly/auth/workos\";"],
|
|
563
|
+
packages: ["@workos-inc/node"],
|
|
564
|
+
body: `const adapter = createWorkOSAuthAdapter({
|
|
565
|
+
apiKey: process.env.WORKOS_API_KEY ?? "",
|
|
566
|
+
clientId: process.env.WORKOS_CLIENT_ID ?? "",
|
|
567
|
+
cookiePassword: process.env.WORKOS_COOKIE_PASSWORD ?? "",
|
|
568
|
+
});
|
|
569
|
+
${RESOLVE_USER}`
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function hostImportFor(instance) {
|
|
573
|
+
if (instance === null) return {
|
|
574
|
+
binding: "auth",
|
|
575
|
+
path: "./lib/auth",
|
|
576
|
+
todo: "TODO(clivly): point this at your better-auth instance."
|
|
577
|
+
};
|
|
578
|
+
return {
|
|
579
|
+
binding: instance.moduleKey === "auth" ? "auth" : `${instance.moduleKey} as auth`,
|
|
580
|
+
path: instance.importPath
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function betterAuthWiring(instance) {
|
|
584
|
+
return {
|
|
585
|
+
complete: instance !== null,
|
|
586
|
+
envKeys: [],
|
|
587
|
+
hostImport: hostImportFor(instance),
|
|
588
|
+
imports: ["import { createBetterAuthAdapter } from \"clivly/auth/better-auth\";"],
|
|
589
|
+
packages: [],
|
|
590
|
+
body: `const adapter = createBetterAuthAdapter({ auth });
|
|
591
|
+
${RESOLVE_USER}`
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function renderAuthWiring(auth, authInstance = null) {
|
|
595
|
+
switch (auth) {
|
|
596
|
+
case "clerk": return clerkWiring();
|
|
597
|
+
case "workos": return workosWiring();
|
|
598
|
+
case "better-auth": return betterAuthWiring(authInstance);
|
|
599
|
+
default: return STUB;
|
|
656
600
|
}
|
|
657
|
-
return null;
|
|
658
601
|
}
|
|
659
602
|
//#endregion
|
|
660
603
|
//#region src/scaffold.ts
|
|
@@ -688,15 +631,30 @@ function routePath(framework, kind) {
|
|
|
688
631
|
if (!paths) throw new Error(`No route path for framework: ${framework}`);
|
|
689
632
|
return paths[kind];
|
|
690
633
|
}
|
|
634
|
+
const WINDOWS_SEP = /\\/g;
|
|
635
|
+
const LEADING_DOT_SLASH = /^\.\//;
|
|
691
636
|
function configImport(routeFilePath) {
|
|
692
|
-
const rel = relative(dirname(routeFilePath), "clivly.config").replace(
|
|
637
|
+
const rel = relative(dirname(routeFilePath), "clivly.config").replace(WINDOWS_SEP, "/");
|
|
693
638
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
694
639
|
}
|
|
695
|
-
function
|
|
640
|
+
function hostImportPath(routeFilePath, appRelative) {
|
|
641
|
+
const fromRoot = appRelative.replace(LEADING_DOT_SLASH, "");
|
|
642
|
+
const rel = relative(dirname(routeFilePath), fromRoot).replace(WINDOWS_SEP, "/");
|
|
643
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
644
|
+
}
|
|
645
|
+
function renderAuthPreamble(routeFilePath, auth) {
|
|
646
|
+
const lines = [...auth.imports];
|
|
647
|
+
if (auth.hostImport) {
|
|
648
|
+
if (auth.hostImport.todo) lines.push(`// ${auth.hostImport.todo}`);
|
|
649
|
+
const path = hostImportPath(routeFilePath, auth.hostImport.path);
|
|
650
|
+
lines.push(`import { ${auth.hostImport.binding} } from "${path}";`);
|
|
651
|
+
}
|
|
652
|
+
lines.push("", auth.body);
|
|
653
|
+
return lines.join("\n");
|
|
654
|
+
}
|
|
655
|
+
function renderBody(framework, importPath, verify, auth, routeFilePath) {
|
|
696
656
|
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;
|
|
657
|
+
${renderAuthPreamble(routeFilePath, auth)}
|
|
700
658
|
const handler = clivly.createAuthVerifyHandler(resolveUser);
|
|
701
659
|
` : `import clivly from "${importPath}";
|
|
702
660
|
|
|
@@ -727,9 +685,9 @@ export default defineEventHandler((event) => handler(toWebRequest(event)));
|
|
|
727
685
|
default: throw new Error(`No route template for framework: ${framework}`);
|
|
728
686
|
}
|
|
729
687
|
}
|
|
730
|
-
function renderRouteFile(framework, kind) {
|
|
688
|
+
function renderRouteFile(framework, kind, auth = renderAuthWiring("unknown")) {
|
|
731
689
|
const path = routePath(framework, kind);
|
|
732
|
-
let body = renderBody(framework, configImport(path), kind === "verify");
|
|
690
|
+
let body = renderBody(framework, configImport(path), kind === "verify", auth, path);
|
|
733
691
|
if (framework === "tanstack-start") {
|
|
734
692
|
const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
|
|
735
693
|
body = body.replace("__ROUTE__", url);
|
|
@@ -759,6 +717,405 @@ function planScaffold(framework) {
|
|
|
759
717
|
];
|
|
760
718
|
}
|
|
761
719
|
//#endregion
|
|
720
|
+
//#region src/plan-mutations.ts
|
|
721
|
+
function fileAction(present, force) {
|
|
722
|
+
if (!present) return "create";
|
|
723
|
+
return force ? "overwrite" : "skip";
|
|
724
|
+
}
|
|
725
|
+
function hasKey(content, key) {
|
|
726
|
+
return new RegExp(`^\\s*${key}=`, "m").test(content);
|
|
727
|
+
}
|
|
728
|
+
function planFiles(args) {
|
|
729
|
+
const mutations = [];
|
|
730
|
+
const render = (path) => {
|
|
731
|
+
if (path === "clivly.config.ts") return renderInitConfig(args.plan);
|
|
732
|
+
const kind = path.includes("verify") ? "verify" : "tick";
|
|
733
|
+
return renderRouteFile(args.framework, kind, args.authWiring);
|
|
734
|
+
};
|
|
735
|
+
for (const file of planScaffold(args.framework)) mutations.push({
|
|
736
|
+
path: file.path,
|
|
737
|
+
contents: render(file.path),
|
|
738
|
+
action: fileAction(args.exists(join(args.cwd, file.path)), args.force)
|
|
739
|
+
});
|
|
740
|
+
return mutations;
|
|
741
|
+
}
|
|
742
|
+
function envAction(addedCount, existingLength) {
|
|
743
|
+
if (addedCount === 0) return "skip";
|
|
744
|
+
return existingLength === 0 ? "create" : "update";
|
|
745
|
+
}
|
|
746
|
+
function planEnv(args) {
|
|
747
|
+
const existing = args.readEnv(args.cwd);
|
|
748
|
+
const addedKeys = CLIVLY_ENV_KEYS.filter((entry) => !hasKey(existing, entry.key)).map((entry) => entry.key);
|
|
749
|
+
return {
|
|
750
|
+
path: ".env",
|
|
751
|
+
addedKeys,
|
|
752
|
+
contents: appendEnvKeys(existing, CLIVLY_ENV_KEYS),
|
|
753
|
+
action: envAction(addedKeys.length, existing.length)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function planMutations(args) {
|
|
757
|
+
return {
|
|
758
|
+
files: planFiles(args),
|
|
759
|
+
env: planEnv(args)
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region src/probe-auth.ts
|
|
764
|
+
const AUTH_MODULE_PATHS = [
|
|
765
|
+
"auth",
|
|
766
|
+
"lib/auth",
|
|
767
|
+
"src/auth",
|
|
768
|
+
"src/lib/auth",
|
|
769
|
+
"app/lib/auth",
|
|
770
|
+
"server/auth",
|
|
771
|
+
"src/server/auth"
|
|
772
|
+
];
|
|
773
|
+
/** A better-auth instance, duck-typed by the surface the adapter calls. */
|
|
774
|
+
function isBetterAuthInstance(value) {
|
|
775
|
+
return typeof value?.api?.getSession === "function";
|
|
776
|
+
}
|
|
777
|
+
function authInstanceCandidates(cwd) {
|
|
778
|
+
return AUTH_MODULE_PATHS.map((path) => join(cwd, path));
|
|
779
|
+
}
|
|
780
|
+
function defaultLoader(cwd) {
|
|
781
|
+
const jiti = createJiti(pathToFileURL(join(cwd, "clivly.config.ts")).href);
|
|
782
|
+
return (specifier) => jiti.import(specifier);
|
|
783
|
+
}
|
|
784
|
+
function probeAuthInstance(cwd, load) {
|
|
785
|
+
return probeModule({
|
|
786
|
+
candidates: authInstanceCandidates(cwd),
|
|
787
|
+
cwd,
|
|
788
|
+
load: load ?? defaultLoader(cwd),
|
|
789
|
+
match: isBetterAuthInstance
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/load-config.ts
|
|
794
|
+
const CONFIG_HINT = "Expected clivly.config.ts at your app root exporting `createClivlySDK({...})`.";
|
|
795
|
+
function resolveConfigPath(cwd, configPath) {
|
|
796
|
+
return resolve(cwd, configPath ?? "clivly.config.ts");
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Load the config and return its default export, checking it carries the method
|
|
800
|
+
* the caller needs. The check is by capability rather than by type: the config
|
|
801
|
+
* is the developer's own SDK instance, and a clear "this isn't an SDK instance"
|
|
802
|
+
* beats a TypeError deep inside a command.
|
|
803
|
+
*/
|
|
804
|
+
/**
|
|
805
|
+
* The first meaningful line of an error message. jiti reports a failed
|
|
806
|
+
* resolution with a multi-line "Require stack:" dump naming Clivly's own
|
|
807
|
+
* internal files — noise that tells the developer nothing about their app and
|
|
808
|
+
* buries the one line that does.
|
|
809
|
+
*/
|
|
810
|
+
function firstLine(message) {
|
|
811
|
+
return message.split("\n")[0]?.trim() ?? "";
|
|
812
|
+
}
|
|
813
|
+
async function loadConfigSdk(path, requiredMethod) {
|
|
814
|
+
const jiti = createJiti(import.meta.url);
|
|
815
|
+
let mod;
|
|
816
|
+
try {
|
|
817
|
+
mod = await jiti.import(path);
|
|
818
|
+
} catch (error) {
|
|
819
|
+
throw new Error(firstLine(error.message));
|
|
820
|
+
}
|
|
821
|
+
const sdk = mod.default ?? mod;
|
|
822
|
+
if (typeof sdk?.[requiredMethod] !== "function") throw new Error("default export is not a Clivly SDK instance");
|
|
823
|
+
return sdk;
|
|
824
|
+
}
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region src/push-schema.ts
|
|
827
|
+
async function runPushSchema(options) {
|
|
828
|
+
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
829
|
+
const path = resolveConfigPath(options.cwd, options.configPath);
|
|
830
|
+
const loadSdk = options.loadSdk ?? ((configPath) => loadConfigSdk(configPath, "heartbeat"));
|
|
831
|
+
out("\nClivly push-schema\n");
|
|
832
|
+
let sdk;
|
|
833
|
+
try {
|
|
834
|
+
sdk = await loadSdk(path);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
out(` ❌ Could not load config — ${error.message}\n`);
|
|
837
|
+
out(` ${CONFIG_HINT}\n`);
|
|
838
|
+
return 1;
|
|
839
|
+
}
|
|
840
|
+
let delivered;
|
|
841
|
+
try {
|
|
842
|
+
delivered = await sdk.heartbeat();
|
|
843
|
+
} catch (error) {
|
|
844
|
+
out(` ❌ Push failed — ${error.message}\n`);
|
|
845
|
+
return 1;
|
|
846
|
+
}
|
|
847
|
+
if (!delivered) {
|
|
848
|
+
out(" ❌ Clivly rejected the push.\n");
|
|
849
|
+
out(" Run `clivly status` to check your key and reachability.\n");
|
|
850
|
+
return 1;
|
|
851
|
+
}
|
|
852
|
+
out(" ✅ Schema reported to Clivly.\n");
|
|
853
|
+
out(" Integration liveness and trigger URL were refreshed too.\n");
|
|
854
|
+
out(" Open Integrations → Mapping to map your tables.\n");
|
|
855
|
+
return 0;
|
|
856
|
+
}
|
|
857
|
+
//#endregion
|
|
858
|
+
//#region src/resolve-schema-path.ts
|
|
859
|
+
const COMMON_PATHS = [
|
|
860
|
+
"src/db/schema.ts",
|
|
861
|
+
"src/db/schema",
|
|
862
|
+
"db/schema.ts",
|
|
863
|
+
"db/schema",
|
|
864
|
+
"src/schema.ts",
|
|
865
|
+
"src/schema",
|
|
866
|
+
"drizzle/schema.ts",
|
|
867
|
+
"drizzle/schema"
|
|
868
|
+
];
|
|
869
|
+
const DRIZZLE_CONFIG_NAMES = [
|
|
870
|
+
"drizzle.config.ts",
|
|
871
|
+
"drizzle.config.js",
|
|
872
|
+
"drizzle.config.mjs"
|
|
873
|
+
];
|
|
874
|
+
function defaultReadDrizzleConfigSchema(cwd) {
|
|
875
|
+
for (const name of DRIZZLE_CONFIG_NAMES) {
|
|
876
|
+
const path = resolve(cwd, name);
|
|
877
|
+
if (!existsSync(path)) continue;
|
|
878
|
+
try {
|
|
879
|
+
const schema = createJiti(pathToFileURL(path).href)(path).default?.schema;
|
|
880
|
+
const first = Array.isArray(schema) ? schema[0] : schema;
|
|
881
|
+
if (typeof first === "string") return first;
|
|
882
|
+
} catch {}
|
|
883
|
+
}
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
function resolveSchemaPath(opts) {
|
|
887
|
+
const exists = opts.deps?.exists ?? existsSync;
|
|
888
|
+
const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
|
|
889
|
+
const toAbs = (p) => isAbsolute(p) ? p : resolve(opts.cwd, p);
|
|
890
|
+
if (opts.flag) return toAbs(opts.flag);
|
|
891
|
+
const fromConfig = readSchema(opts.cwd);
|
|
892
|
+
if (fromConfig) return toAbs(fromConfig);
|
|
893
|
+
for (const candidate of COMMON_PATHS) {
|
|
894
|
+
const abs = toAbs(candidate);
|
|
895
|
+
if (exists(abs)) return abs;
|
|
896
|
+
}
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/status-ladder.ts
|
|
901
|
+
const SATISFYING = new Set(["pass", "warn"]);
|
|
902
|
+
function unmetPrerequisites(stage, satisfied) {
|
|
903
|
+
return (stage.needs ?? []).filter((id) => !satisfied.has(id));
|
|
904
|
+
}
|
|
905
|
+
async function runLadder(stages, ctx) {
|
|
906
|
+
const results = [];
|
|
907
|
+
const satisfied = /* @__PURE__ */ new Set();
|
|
908
|
+
const total = stages.length;
|
|
909
|
+
for (const [index, stage] of stages.entries()) {
|
|
910
|
+
const position = {
|
|
911
|
+
step: index + 1,
|
|
912
|
+
total,
|
|
913
|
+
id: stage.id,
|
|
914
|
+
label: stage.label
|
|
915
|
+
};
|
|
916
|
+
const blockedBy = unmetPrerequisites(stage, satisfied);
|
|
917
|
+
if (blockedBy.length > 0) {
|
|
918
|
+
results.push({
|
|
919
|
+
...position,
|
|
920
|
+
state: "pending",
|
|
921
|
+
detail: `Waiting on: ${blockedBy.join(", ")}.`
|
|
922
|
+
});
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
let outcome;
|
|
926
|
+
try {
|
|
927
|
+
outcome = await stage.run(ctx);
|
|
928
|
+
} catch (error) {
|
|
929
|
+
outcome = {
|
|
930
|
+
state: "fail",
|
|
931
|
+
detail: error.message
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (SATISFYING.has(outcome.state)) satisfied.add(stage.id);
|
|
935
|
+
results.push({
|
|
936
|
+
...position,
|
|
937
|
+
...outcome
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
return results;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Exit 0 only when the setup actually works. `pending` is still non-zero —
|
|
944
|
+
* incomplete is not working, and scripts must see that — even though the
|
|
945
|
+
* rendering frames it as progress rather than breakage.
|
|
946
|
+
*/
|
|
947
|
+
function ladderExitCode(results) {
|
|
948
|
+
return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
|
|
949
|
+
}
|
|
950
|
+
//#endregion
|
|
951
|
+
//#region src/status-stages.ts
|
|
952
|
+
const REASON_HINT = {
|
|
953
|
+
ok: "",
|
|
954
|
+
invalid_api_key: "The key was rejected (401). Check CLIVLY_API_KEY.",
|
|
955
|
+
forbidden: "The key is valid but lacks access to this org (403).",
|
|
956
|
+
rate_limited: "Rate limited (429). Wait a moment and retry.",
|
|
957
|
+
server_error: "Clivly returned a server error (5xx). Retry shortly.",
|
|
958
|
+
client_error: "The request was rejected (4xx). Check your configuration.",
|
|
959
|
+
network_error: "Could not reach Clivly. Check your network or --api-url."
|
|
960
|
+
};
|
|
961
|
+
const apiKeyStage = {
|
|
962
|
+
id: "api-key",
|
|
963
|
+
label: "API key present",
|
|
964
|
+
run(ctx) {
|
|
965
|
+
if (ctx.apiKey) return Promise.resolve({
|
|
966
|
+
state: "pass",
|
|
967
|
+
detail: "CLIVLY_API_KEY is set."
|
|
968
|
+
});
|
|
969
|
+
return Promise.resolve({
|
|
970
|
+
state: "fail",
|
|
971
|
+
detail: "CLIVLY_API_KEY is not set.",
|
|
972
|
+
hint: "Copy your API key from Settings → API keys into .env."
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
const heartbeatStage = {
|
|
977
|
+
id: "heartbeat",
|
|
978
|
+
label: "Clivly reachable",
|
|
979
|
+
needs: [apiKeyStage.id],
|
|
980
|
+
async run(ctx) {
|
|
981
|
+
const result = await ctx.connect(ctx.apiKey, ctx.apiUrl);
|
|
982
|
+
if (result.ok) return {
|
|
983
|
+
state: "pass",
|
|
984
|
+
detail: `Reached org: ${result.org?.name ?? "(unnamed org)"}${result.org?.id ? ` (${result.org.id})` : ""}`
|
|
985
|
+
};
|
|
986
|
+
const status = result.status === null ? "no response" : `HTTP ${result.status}`;
|
|
987
|
+
return {
|
|
988
|
+
state: "fail",
|
|
989
|
+
detail: `${result.reason} (${status})`,
|
|
990
|
+
hint: REASON_HINT[result.reason]
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
const configStage = {
|
|
995
|
+
id: "config",
|
|
996
|
+
label: "clivly.config loads",
|
|
997
|
+
async run(ctx) {
|
|
998
|
+
try {
|
|
999
|
+
ctx.sdk = await ctx.loadSdk(ctx.configPath);
|
|
1000
|
+
return {
|
|
1001
|
+
state: "pass",
|
|
1002
|
+
detail: ctx.configPath
|
|
1003
|
+
};
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
return {
|
|
1006
|
+
state: "fail",
|
|
1007
|
+
detail: error.message,
|
|
1008
|
+
hint: CONFIG_HINT
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
/** The full ladder, in the order a developer completes setup. */
|
|
1014
|
+
const STATUS_STAGES = [
|
|
1015
|
+
apiKeyStage,
|
|
1016
|
+
heartbeatStage,
|
|
1017
|
+
configStage,
|
|
1018
|
+
{
|
|
1019
|
+
id: "setup-checks",
|
|
1020
|
+
label: "Setup checks",
|
|
1021
|
+
needs: [configStage.id],
|
|
1022
|
+
async run(ctx) {
|
|
1023
|
+
const report = await ctx.sdk.doctor();
|
|
1024
|
+
const counted = report.checks.filter((check) => !check.skipped);
|
|
1025
|
+
const passed = counted.filter((check) => check.ok).length;
|
|
1026
|
+
return {
|
|
1027
|
+
state: report.ok ? "pass" : "fail",
|
|
1028
|
+
detail: `${passed} of ${counted.length} passed`,
|
|
1029
|
+
sub: report.checks.map((check) => ({
|
|
1030
|
+
label: check.name,
|
|
1031
|
+
ok: check.ok,
|
|
1032
|
+
skipped: check.skipped,
|
|
1033
|
+
detail: check.detail
|
|
1034
|
+
}))
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
id: "trigger-url",
|
|
1040
|
+
label: "Sync trigger URL",
|
|
1041
|
+
needs: [configStage.id],
|
|
1042
|
+
run(ctx) {
|
|
1043
|
+
const url = ctx.sdk?.syncTrigger?.url;
|
|
1044
|
+
if (url) return Promise.resolve({
|
|
1045
|
+
state: "pass",
|
|
1046
|
+
detail: url
|
|
1047
|
+
});
|
|
1048
|
+
return Promise.resolve({
|
|
1049
|
+
state: "warn",
|
|
1050
|
+
detail: "Not set — remote sync cannot call back into this app.",
|
|
1051
|
+
hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. Push-only setups can ignore this."
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
];
|
|
1056
|
+
function defaultLoadSdk(path) {
|
|
1057
|
+
return loadConfigSdk(path, "doctor");
|
|
1058
|
+
}
|
|
1059
|
+
//#endregion
|
|
1060
|
+
//#region src/status.ts
|
|
1061
|
+
/** Stage ids each legacy command maps onto. */
|
|
1062
|
+
const ALIAS_STAGE_IDS = {
|
|
1063
|
+
status: STATUS_STAGES.map((stage) => stage.id),
|
|
1064
|
+
ping: ["api-key", "heartbeat"],
|
|
1065
|
+
doctor: ["config", "setup-checks"],
|
|
1066
|
+
connect: [
|
|
1067
|
+
"api-key",
|
|
1068
|
+
"heartbeat",
|
|
1069
|
+
"config",
|
|
1070
|
+
"trigger-url"
|
|
1071
|
+
]
|
|
1072
|
+
};
|
|
1073
|
+
const MARK = {
|
|
1074
|
+
pass: "✅",
|
|
1075
|
+
fail: "❌",
|
|
1076
|
+
warn: "⚠️ ",
|
|
1077
|
+
skip: "⏭️ ",
|
|
1078
|
+
pending: "⏳"
|
|
1079
|
+
};
|
|
1080
|
+
const defaultConnect = (apiKey, apiUrl) => createClivlySDK({
|
|
1081
|
+
apiKey,
|
|
1082
|
+
apiUrl,
|
|
1083
|
+
retry: { maxAttempts: 1 }
|
|
1084
|
+
}).connect();
|
|
1085
|
+
function subMark(sub) {
|
|
1086
|
+
if (sub.skipped) return "⚠️ ";
|
|
1087
|
+
return sub.ok ? "✅" : "❌";
|
|
1088
|
+
}
|
|
1089
|
+
function renderStage(result, verbose, out) {
|
|
1090
|
+
const step = `Step ${result.step} of ${result.total}`;
|
|
1091
|
+
const detail = result.detail ? ` — ${result.detail}` : "";
|
|
1092
|
+
out(` ${MARK[result.state]} ${step} · ${result.label}${detail}\n`);
|
|
1093
|
+
for (const sub of result.sub ?? []) {
|
|
1094
|
+
if (!(verbose || sub.skipped || !sub.ok)) continue;
|
|
1095
|
+
const subDetail = sub.detail ? ` — ${sub.detail}` : "";
|
|
1096
|
+
out(` ${subMark(sub)} ${sub.label}${subDetail}\n`);
|
|
1097
|
+
}
|
|
1098
|
+
if (result.hint && result.state !== "pass") out(` ${result.hint}\n`);
|
|
1099
|
+
}
|
|
1100
|
+
async function runStatus(options) {
|
|
1101
|
+
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
1102
|
+
const selected = new Set(options.only ?? ALIAS_STAGE_IDS.status);
|
|
1103
|
+
const stages = STATUS_STAGES.filter((stage) => selected.has(stage.id));
|
|
1104
|
+
const ctx = {
|
|
1105
|
+
apiKey: options.apiKey,
|
|
1106
|
+
apiUrl: options.apiUrl,
|
|
1107
|
+
configPath: resolveConfigPath(options.cwd, options.configPath),
|
|
1108
|
+
connect: options.connect ?? defaultConnect,
|
|
1109
|
+
loadSdk: options.loadSdk ?? defaultLoadSdk
|
|
1110
|
+
};
|
|
1111
|
+
out("\nClivly status\n");
|
|
1112
|
+
const results = await runLadder(stages, ctx);
|
|
1113
|
+
for (const result of results) renderStage(result, options.verbose ?? false, out);
|
|
1114
|
+
const code = ladderExitCode(results);
|
|
1115
|
+
if (code !== 0 && results.some((result) => result.state === "pending")) out("\nSetup is incomplete — finish the steps above and re-run.\n");
|
|
1116
|
+
return code;
|
|
1117
|
+
}
|
|
1118
|
+
//#endregion
|
|
762
1119
|
//#region src/index.ts
|
|
763
1120
|
function flagValue(argv, flag) {
|
|
764
1121
|
const index = argv.indexOf(flag);
|
|
@@ -810,10 +1167,12 @@ function resolveInitDeps(options) {
|
|
|
810
1167
|
}),
|
|
811
1168
|
detectPackageManager: options.detectPackageManager ?? detectPackageManager,
|
|
812
1169
|
install: options.installPackages ?? installPackages,
|
|
1170
|
+
probeAuth: options.probeAuthInstance ?? probeAuthInstance,
|
|
813
1171
|
readEnv: options.readEnv ?? defaultReadEnv,
|
|
814
1172
|
readOwnVersion: options.readOwnVersion ?? readOwnVersion,
|
|
815
1173
|
writeEnv: options.writeEnv ?? defaultWriteEnv,
|
|
816
|
-
framework: "unknown"
|
|
1174
|
+
framework: "unknown",
|
|
1175
|
+
authWiring: renderAuthWiring("unknown")
|
|
817
1176
|
};
|
|
818
1177
|
}
|
|
819
1178
|
async function acquireSchema(options, deps) {
|
|
@@ -825,10 +1184,13 @@ async function acquireSchema(options, deps) {
|
|
|
825
1184
|
}
|
|
826
1185
|
const framework = detectFramework(appDeps);
|
|
827
1186
|
const orm = detectOrm(appDeps);
|
|
1187
|
+
const auth = detectAuth(appDeps);
|
|
828
1188
|
deps.framework = framework;
|
|
1189
|
+
deps.authWiring = renderAuthWiring(auth, auth === "better-auth" ? await deps.probeAuth(options.cwd) : null);
|
|
829
1190
|
out("\nClivly init\n");
|
|
830
1191
|
out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
|
|
831
|
-
out(` ORM: ${orm}\n
|
|
1192
|
+
out(` ORM: ${orm}\n`);
|
|
1193
|
+
out(` Auth: ${auth}${deps.authWiring.complete ? " (wired)" : ""}\n\n`);
|
|
832
1194
|
if (orm !== "drizzle") {
|
|
833
1195
|
out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
|
|
834
1196
|
return { code: 1 };
|
|
@@ -867,50 +1229,81 @@ function printPlan(out, plan) {
|
|
|
867
1229
|
out("\n");
|
|
868
1230
|
}
|
|
869
1231
|
}
|
|
870
|
-
function
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1232
|
+
function mutationsFor(options, deps, plan) {
|
|
1233
|
+
return planMutations({
|
|
1234
|
+
cwd: options.cwd,
|
|
1235
|
+
force: options.force,
|
|
1236
|
+
framework: deps.framework,
|
|
1237
|
+
authWiring: deps.authWiring,
|
|
1238
|
+
plan,
|
|
1239
|
+
exists: deps.configExists,
|
|
1240
|
+
readEnv: deps.readEnv
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
function applyMutations(options, deps, mutations) {
|
|
1244
|
+
for (const file of mutations.files) {
|
|
1245
|
+
if (file.action === "skip") {
|
|
1246
|
+
deps.out(` • ${file.path} exists — skipped (pass --force to overwrite)\n`);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
deps.writeConfig(join(options.cwd, file.path), file.contents);
|
|
1250
|
+
deps.out(` ✓ ${file.path}\n`);
|
|
1251
|
+
}
|
|
1252
|
+
if (mutations.env.action === "skip") {
|
|
1253
|
+
deps.out(" • .env already has every CLIVLY_* key — skipped\n");
|
|
874
1254
|
return;
|
|
875
1255
|
}
|
|
876
|
-
deps.
|
|
877
|
-
deps.out(
|
|
1256
|
+
deps.writeEnv(options.cwd, mutations.env.contents);
|
|
1257
|
+
deps.out(" ✓ .env placeholders\n");
|
|
878
1258
|
}
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const
|
|
884
|
-
|
|
1259
|
+
function previewMutations(deps, mutations) {
|
|
1260
|
+
const { out } = deps;
|
|
1261
|
+
out("\nDry run — nothing was written.\n\n");
|
|
1262
|
+
for (const file of mutations.files) {
|
|
1263
|
+
const verb = {
|
|
1264
|
+
create: "create",
|
|
1265
|
+
overwrite: "overwrite",
|
|
1266
|
+
skip: "skip"
|
|
1267
|
+
}[file.action];
|
|
1268
|
+
const lines = file.contents.split("\n").length;
|
|
1269
|
+
const suffix = file.action === "skip" ? " (exists — pass --force to overwrite)" : ` (${lines} lines)`;
|
|
1270
|
+
out(` ${verb.padEnd(9)} ${file.path}${suffix}\n`);
|
|
885
1271
|
}
|
|
1272
|
+
if (mutations.env.addedKeys.length === 0) out(" skip .env (every CLIVLY_* key already present)\n");
|
|
1273
|
+
else {
|
|
1274
|
+
out(` ${mutations.env.action.padEnd(9)} .env\n`);
|
|
1275
|
+
for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
|
|
1276
|
+
}
|
|
1277
|
+
out("\nRe-run without --dry-run to apply.\n");
|
|
886
1278
|
}
|
|
887
|
-
function
|
|
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) {
|
|
1279
|
+
function printNextSteps(out, authComplete) {
|
|
893
1280
|
out("\nNext steps:\n");
|
|
894
1281
|
out(" 1. Set the CLIVLY_* values in .env\n");
|
|
895
1282
|
out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
|
|
896
|
-
|
|
897
|
-
out("
|
|
1283
|
+
const step = authComplete ? 3 : 4;
|
|
1284
|
+
if (!authComplete) out(" 3. Finish resolveUser in the auth/verify route\n");
|
|
1285
|
+
out(` ${step}. Run \`clivly status\` to see where you are\n`);
|
|
898
1286
|
}
|
|
899
1287
|
async function scaffold(options, deps, plan) {
|
|
900
1288
|
const { out } = deps;
|
|
901
1289
|
const pm = deps.detectPackageManager(options.cwd);
|
|
902
1290
|
const version = deps.readOwnVersion();
|
|
903
|
-
const
|
|
1291
|
+
const packages = [version ? `clivly@${version}` : "clivly", ...deps.authWiring.packages];
|
|
1292
|
+
const mutations = mutationsFor(options, deps, plan);
|
|
1293
|
+
if (options.dryRun) {
|
|
1294
|
+
out(`\nWould install (${pm}): ${packages.join(", ")}\n`);
|
|
1295
|
+
previewMutations(deps, mutations);
|
|
1296
|
+
return plan.contactDetected ? 0 : 1;
|
|
1297
|
+
}
|
|
904
1298
|
const result = await deps.install({
|
|
905
1299
|
cwd: options.cwd,
|
|
906
1300
|
pm,
|
|
907
|
-
packages
|
|
1301
|
+
packages
|
|
908
1302
|
});
|
|
909
1303
|
if (result.ok) out(`Installed clivly (${pm})\n`);
|
|
910
1304
|
else out(`Could not install automatically — run:\n ${result.command}\n`);
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
printNextSteps(out);
|
|
1305
|
+
applyMutations(options, deps, mutations);
|
|
1306
|
+
printNextSteps(out, deps.authWiring.complete);
|
|
914
1307
|
return plan.contactDetected ? 0 : 1;
|
|
915
1308
|
}
|
|
916
1309
|
async function runInit(options) {
|
|
@@ -927,26 +1320,28 @@ async function main(argv) {
|
|
|
927
1320
|
case "init": return await runInit({
|
|
928
1321
|
cwd: process.cwd(),
|
|
929
1322
|
force: rest.includes("--force"),
|
|
1323
|
+
dryRun: rest.includes("--dry-run"),
|
|
930
1324
|
schemaFlag: flagValue(rest, "--schema")
|
|
931
1325
|
});
|
|
932
|
-
case "
|
|
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({
|
|
1326
|
+
case "push-schema": return await runPushSchema({
|
|
937
1327
|
cwd: process.cwd(),
|
|
938
1328
|
configPath: flagValue(rest, "--config")
|
|
939
1329
|
});
|
|
940
|
-
case "
|
|
1330
|
+
case "status":
|
|
1331
|
+
case "ping":
|
|
1332
|
+
case "doctor":
|
|
1333
|
+
case "connect": return await runStatus({
|
|
941
1334
|
cwd: process.cwd(),
|
|
942
1335
|
apiKey: process.env.CLIVLY_API_KEY,
|
|
943
1336
|
apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
|
|
944
|
-
configPath: flagValue(rest, "--config")
|
|
1337
|
+
configPath: flagValue(rest, "--config"),
|
|
1338
|
+
only: ALIAS_STAGE_IDS[command],
|
|
1339
|
+
verbose: rest.includes("--verbose")
|
|
945
1340
|
});
|
|
946
1341
|
case void 0:
|
|
947
1342
|
case "help":
|
|
948
1343
|
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
|
|
1344
|
+
process.stdout.write("clivly — embed a CRM in your app\n\nUsage:\n clivly init [--force] [--dry-run] [--schema <path>] Scaffold Clivly (config, routes, .env) for your app\n clivly status [--verbose] [--config <path>] Check your setup, step by step\n clivly push-schema [--config <path>] Report your schema to Clivly without deploying\n");
|
|
950
1345
|
return 0;
|
|
951
1346
|
default:
|
|
952
1347
|
process.stdout.write(`Unknown command: ${command}\n`);
|