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