clivly 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +63 -3
  2. package/adapter-nLfZpgIG.d.cts +40 -0
  3. package/adapter-xs04z0KX.d.mts +40 -0
  4. package/auth-adapter-CBOKDhlc.cjs +11 -0
  5. package/auth-adapter-FEOR1nx-.d.mts +30 -0
  6. package/auth-adapter-wqnnEjo3.mjs +6 -0
  7. package/auth-adapter-wvfGKsOJ.d.cts +30 -0
  8. package/auth-better-auth.cjs +28 -0
  9. package/auth-better-auth.d.cts +33 -0
  10. package/auth-better-auth.d.mts +33 -0
  11. package/auth-better-auth.mjs +26 -0
  12. package/auth-clerk.cjs +34 -0
  13. package/auth-clerk.d.cts +32 -0
  14. package/auth-clerk.d.mts +32 -0
  15. package/auth-clerk.mjs +32 -0
  16. package/auth-workos.cjs +47 -0
  17. package/auth-workos.d.cts +30 -0
  18. package/auth-workos.d.mts +30 -0
  19. package/auth-workos.mjs +45 -0
  20. package/core-adapter.cjs +0 -0
  21. package/core-adapter.d.cts +2 -0
  22. package/core-adapter.d.mts +2 -0
  23. package/core-adapter.mjs +1 -0
  24. package/core-auth-adapter.cjs +3 -0
  25. package/core-auth-adapter.d.cts +2 -0
  26. package/core-auth-adapter.d.mts +2 -0
  27. package/core-auth-adapter.mjs +2 -0
  28. package/core-drizzle.cjs +32 -0
  29. package/core-drizzle.d.cts +21 -0
  30. package/core-drizzle.d.mts +21 -0
  31. package/core-drizzle.mjs +31 -0
  32. package/core-entity-config.cjs +13 -0
  33. package/core-entity-config.d.cts +2 -0
  34. package/core-entity-config.d.mts +2 -0
  35. package/core-entity-config.mjs +2 -0
  36. package/core-entity-heuristics.cjs +442 -0
  37. package/core-entity-heuristics.d.cts +68 -0
  38. package/core-entity-heuristics.d.mts +68 -0
  39. package/core-entity-heuristics.mjs +431 -0
  40. package/core-mapping-form.cjs +5 -0
  41. package/core-mapping-form.d.cts +2 -0
  42. package/core-mapping-form.d.mts +2 -0
  43. package/core-mapping-form.mjs +2 -0
  44. package/core-mapping-score.cjs +12 -0
  45. package/core-mapping-score.d.cts +2 -0
  46. package/core-mapping-score.d.mts +2 -0
  47. package/core-mapping-score.mjs +2 -0
  48. package/core-mappings-config.cjs +3 -0
  49. package/core-mappings-config.d.cts +2 -0
  50. package/core-mappings-config.d.mts +2 -0
  51. package/core-mappings-config.mjs +2 -0
  52. package/core-sync-engine.cjs +6 -0
  53. package/core-sync-engine.d.cts +2 -0
  54. package/core-sync-engine.d.mts +2 -0
  55. package/core-sync-engine.mjs +2 -0
  56. package/core-types.cjs +0 -0
  57. package/core-types.d.cts +2 -0
  58. package/core-types.d.mts +2 -0
  59. package/core-types.mjs +1 -0
  60. package/core-view-compiler.cjs +5 -0
  61. package/core-view-compiler.d.cts +2 -0
  62. package/core-view-compiler.d.mts +2 -0
  63. package/core-view-compiler.mjs +2 -0
  64. package/core.cjs +27 -0
  65. package/core.d.cts +9 -0
  66. package/core.d.mts +9 -0
  67. package/core.mjs +7 -0
  68. package/drizzle-meta-BGn0BqGb.cjs +9 -0
  69. package/drizzle-meta-CnC336L-.mjs +4 -0
  70. package/drizzle.cjs +107 -0
  71. package/drizzle.d.cts +28 -0
  72. package/drizzle.d.mts +28 -0
  73. package/drizzle.mjs +105 -0
  74. package/entity-config-Cvjo1cpv.cjs +4598 -0
  75. package/entity-config-D1DFyeUp.d.mts +2403 -0
  76. package/entity-config-DoR0khce.mjs +4533 -0
  77. package/entity-config-GczjD3y8.d.cts +2403 -0
  78. package/index.cjs +926 -0
  79. package/index.d.cts +56 -0
  80. package/index.d.mts +56 -0
  81. package/index.mjs +925 -0
  82. package/mapping-form-BHtPexzI.mjs +47 -0
  83. package/mapping-form-CUlFVfG8.d.cts +35 -0
  84. package/mapping-form-DdmDebC3.cjs +64 -0
  85. package/mapping-form-kjSiJaCX.d.mts +35 -0
  86. package/mapping-score-A2lRnlpk.d.mts +49 -0
  87. package/mapping-score-BJNe0lFn.mjs +87 -0
  88. package/mapping-score-BcjC0ZQs.d.cts +49 -0
  89. package/mapping-score-D9gfhtRu.cjs +146 -0
  90. package/mappings-config-CYPrzB3K.mjs +99 -0
  91. package/mappings-config-DTu_2cyx.cjs +104 -0
  92. package/mappings-config-DjRLCb2Q.d.cts +42 -0
  93. package/mappings-config-GxiYsSBQ.d.mts +42 -0
  94. package/namespace-probe-BtS0nXcr.d.cts +299 -0
  95. package/namespace-probe-CYMie-BI.d.mts +299 -0
  96. package/package.json +224 -21
  97. package/sdk-BZUn6wzs.mjs +764 -0
  98. package/sdk-CP_mYZa5.cjs +793 -0
  99. package/sdk.cjs +7 -0
  100. package/sdk.d.cts +33 -0
  101. package/sdk.d.mts +33 -0
  102. package/sdk.mjs +2 -0
  103. package/sync-engine-DUc0qWgo.cjs +230 -0
  104. package/sync-engine-DWnQPjV3.d.mts +183 -0
  105. package/sync-engine-DedXngop.mjs +207 -0
  106. package/sync-engine-uhfzKM-e.d.cts +183 -0
  107. package/types-C4POUcvk.d.mts +158 -0
  108. package/types-LJTBhkcN.d.cts +158 -0
  109. package/view-compiler-B4UYTvRM.d.cts +71 -0
  110. package/view-compiler-BHzbI9b7.cjs +202 -0
  111. package/view-compiler-CosFGar8.mjs +185 -0
  112. package/view-compiler-i_xFf-L0.d.mts +71 -0
  113. package/vite.cjs +38 -0
  114. package/vite.d.cts +23 -0
  115. package/vite.d.mts +23 -0
  116. package/vite.mjs +37 -0
  117. package/dist/index.mjs +0 -193
package/index.cjs ADDED
@@ -0,0 +1,926 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_sdk = require("./sdk-CP_mYZa5.cjs");
4
+ const require_entity_config = require("./entity-config-Cvjo1cpv.cjs");
5
+ require("./mapping-score-D9gfhtRu.cjs");
6
+ const require_core_entity_heuristics = require("./core-entity-heuristics.cjs");
7
+ let node_fs = require("node:fs");
8
+ let node_path = require("node:path");
9
+ let jiti = require("jiti");
10
+ let node_url = require("node:url");
11
+ 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
+ //#region src/detect.ts
83
+ const FRAMEWORK_MARKERS = [
84
+ {
85
+ framework: "nextjs",
86
+ deps: ["next"]
87
+ },
88
+ {
89
+ framework: "tanstack-start",
90
+ deps: ["@tanstack/react-start"]
91
+ },
92
+ {
93
+ framework: "sveltekit",
94
+ deps: ["@sveltejs/kit"]
95
+ },
96
+ {
97
+ framework: "remix",
98
+ deps: ["@remix-run/react", "@remix-run/node"]
99
+ },
100
+ {
101
+ framework: "nuxt",
102
+ deps: ["nuxt"]
103
+ }
104
+ ];
105
+ const ORM_MARKERS = [
106
+ {
107
+ orm: "drizzle",
108
+ deps: ["drizzle-orm"]
109
+ },
110
+ {
111
+ orm: "prisma",
112
+ deps: ["@prisma/client", "prisma"]
113
+ },
114
+ {
115
+ orm: "kysely",
116
+ deps: ["kysely"]
117
+ }
118
+ ];
119
+ const FRAMEWORK_LABELS = {
120
+ nextjs: "Next.js",
121
+ "tanstack-start": "TanStack Start",
122
+ sveltekit: "SvelteKit",
123
+ remix: "Remix",
124
+ nuxt: "Nuxt",
125
+ unknown: "Unknown"
126
+ };
127
+ function detectFramework(deps) {
128
+ for (const { framework, deps: markers } of FRAMEWORK_MARKERS) if (markers.some((dep) => dep in deps)) return framework;
129
+ return "unknown";
130
+ }
131
+ function detectOrm(deps) {
132
+ for (const { orm, deps: markers } of ORM_MARKERS) if (markers.some((dep) => dep in deps)) return orm;
133
+ return "unknown";
134
+ }
135
+ const LOCKFILES = [
136
+ {
137
+ file: "pnpm-lock.yaml",
138
+ pm: "pnpm"
139
+ },
140
+ {
141
+ file: "bun.lockb",
142
+ pm: "bun"
143
+ },
144
+ {
145
+ file: "yarn.lock",
146
+ pm: "yarn"
147
+ },
148
+ {
149
+ file: "package-lock.json",
150
+ pm: "npm"
151
+ }
152
+ ];
153
+ function detectPackageManager(cwd, fileExists = node_fs.existsSync) {
154
+ for (const { file, pm } of LOCKFILES) if (fileExists((0, node_path.join)(cwd, file))) return pm;
155
+ return "npm";
156
+ }
157
+ function addCommand(pm, packages) {
158
+ return {
159
+ command: pm,
160
+ args: [pm === "npm" ? "install" : "add", ...packages]
161
+ };
162
+ }
163
+ //#endregion
164
+ //#region src/discover-schema.ts
165
+ const WINDOWS_SEP = /\\/g;
166
+ const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
167
+ function toImportPath(schemaPath, cwd) {
168
+ const rel = (0, node_path.relative)(cwd, schemaPath).replace(WINDOWS_SEP, "/").replace(MODULE_EXT, "");
169
+ return rel.startsWith(".") ? rel : `./${rel}`;
170
+ }
171
+ function isDirectory(path) {
172
+ try {
173
+ return (0, node_fs.statSync)(path).isDirectory();
174
+ } catch {
175
+ return false;
176
+ }
177
+ }
178
+ function loadSchemaModule(schemaPath, load) {
179
+ return load(isDirectory(schemaPath) ? (0, node_path.join)(schemaPath, "index.ts") : schemaPath);
180
+ }
181
+ function defaultLoader(cwd) {
182
+ const jiti$1 = (0, jiti.createJiti)((0, node_url.pathToFileURL)((0, node_path.join)(cwd, "clivly.config.ts")).href);
183
+ return (specifier) => jiti$1.import(specifier);
184
+ }
185
+ async function loadPgCoreRuntime(load) {
186
+ try {
187
+ return await load("drizzle-orm/pg-core");
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+ function buildFkByColumn(pgCore, drizzle, table) {
193
+ const fkByColumn = /* @__PURE__ */ new Map();
194
+ if (!pgCore) return fkByColumn;
195
+ try {
196
+ for (const fk of pgCore.getTableConfig(table).foreignKeys) {
197
+ const ref = fk.reference();
198
+ const foreignTable = drizzle.getTableName(ref.foreignTable);
199
+ ref.columns.forEach((col, i) => {
200
+ const foreignCol = ref.foreignColumns[i];
201
+ if (foreignCol) fkByColumn.set(col.name, {
202
+ table: foreignTable,
203
+ column: foreignCol.name
204
+ });
205
+ });
206
+ }
207
+ } catch {}
208
+ return fkByColumn;
209
+ }
210
+ function toDiscoveredColumn(key, col, fkByColumn) {
211
+ let type;
212
+ try {
213
+ type = col.getSQLType?.();
214
+ } catch {
215
+ type = void 0;
216
+ }
217
+ const references = fkByColumn.get(col.name);
218
+ return {
219
+ key,
220
+ dbName: col.name,
221
+ isPrimary: Boolean(col.primary),
222
+ type,
223
+ nullable: !col.notNull,
224
+ ...references ? { references } : {}
225
+ };
226
+ }
227
+ async function discoverSchema(opts) {
228
+ const load = opts.load ?? defaultLoader(opts.cwd);
229
+ const mod = await loadSchemaModule(opts.schemaPath, load);
230
+ const drizzle = await load("drizzle-orm");
231
+ const pgCore = await loadPgCoreRuntime(load);
232
+ const importPath = toImportPath(opts.schemaPath, opts.cwd);
233
+ const tables = [];
234
+ let db = null;
235
+ for (const [moduleKey, value] of Object.entries(mod)) {
236
+ if (drizzle.is(value, drizzle.Table)) {
237
+ const fkByColumn = buildFkByColumn(pgCore, drizzle, value);
238
+ const columns = Object.entries(drizzle.getTableColumns(value)).map(([key, col]) => toDiscoveredColumn(key, col, fkByColumn));
239
+ tables.push({
240
+ moduleKey,
241
+ tableName: drizzle.getTableName(value),
242
+ columns
243
+ });
244
+ continue;
245
+ }
246
+ if (db === null && value && typeof value.select === "function") db = {
247
+ moduleKey,
248
+ importPath
249
+ };
250
+ }
251
+ return {
252
+ tables,
253
+ importPath,
254
+ db
255
+ };
256
+ }
257
+ //#endregion
258
+ //#region src/doctor.ts
259
+ async function loadSdkFromConfig(path) {
260
+ const mod = await (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
+ //#region src/install.ts
310
+ const defaultRun = (command, args, cwd) => new Promise((resolvePromise, reject) => {
311
+ const child = (0, node_child_process.spawn)(command, args, {
312
+ cwd,
313
+ stdio: "inherit"
314
+ });
315
+ child.on("error", reject);
316
+ child.on("close", (code) => resolvePromise(code ?? 0));
317
+ });
318
+ /**
319
+ * Best-effort install: run `<pm> add <packages>`. Never throws — returns
320
+ * `{ ok, command }` so the caller can print the exact command on failure and
321
+ * continue scaffolding.
322
+ */
323
+ async function installPackages(options) {
324
+ const { command, args } = addCommand(options.pm, options.packages);
325
+ const printable = `${command} ${args.join(" ")}`;
326
+ const run = options.run ?? defaultRun;
327
+ try {
328
+ return {
329
+ ok: await run(command, args, options.cwd) === 0,
330
+ command: printable
331
+ };
332
+ } catch {
333
+ return {
334
+ ok: false,
335
+ command: printable
336
+ };
337
+ }
338
+ }
339
+ //#endregion
340
+ //#region src/is-entry.ts
341
+ function isEntrypoint(argv1, moduleUrl, realpath = node_fs.realpathSync) {
342
+ if (!argv1) return false;
343
+ const modulePath = (0, node_url.fileURLToPath)(moduleUrl);
344
+ const resolve = (path) => {
345
+ try {
346
+ return realpath(path);
347
+ } catch {
348
+ return path;
349
+ }
350
+ };
351
+ return resolve(argv1) === resolve(modulePath);
352
+ }
353
+ //#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
+ //#region src/plan-init.ts
401
+ function toScoredTable(table) {
402
+ return {
403
+ name: table.tableName,
404
+ columns: table.columns.map((c) => c.dbName),
405
+ columnsMeta: table.columns.map((c) => ({
406
+ name: c.dbName,
407
+ type: c.type,
408
+ nullable: c.nullable,
409
+ isPrimaryKey: c.isPrimary,
410
+ isForeignKey: c.references !== void 0,
411
+ ...c.references ? { references: c.references } : {}
412
+ }))
413
+ };
414
+ }
415
+ function rankCandidates(tables, concept) {
416
+ return tables.map((t) => ({
417
+ t,
418
+ m: require_core_entity_heuristics.matchesConcept(t.tableName, concept)
419
+ })).filter((c) => Boolean(c.m)).sort((a, b) => Number(b.m.exact) - Number(a.m.exact)).map((c) => c.t);
420
+ }
421
+ function selectEntities(tables) {
422
+ const contactCands = rankCandidates(tables, "contact");
423
+ const companyCands = rankCandidates(tables, "company");
424
+ const contactNames = new Set(contactCands.map((t) => t.tableName));
425
+ const companyNames = new Set(companyCands.map((t) => t.tableName));
426
+ const contactExclusive = contactCands.filter((t) => !companyNames.has(t.tableName));
427
+ const companyExclusive = companyCands.filter((t) => !contactNames.has(t.tableName));
428
+ const contact = contactExclusive[0] ?? contactCands[0] ?? null;
429
+ const companyPreferred = companyExclusive[0] ?? companyCands[0] ?? null;
430
+ return {
431
+ contact,
432
+ company: companyPreferred && companyPreferred.tableName !== contact?.tableName ? companyPreferred : companyCands.find((t) => t.tableName !== contact?.tableName) ?? null
433
+ };
434
+ }
435
+ function chooseSyncFields(table, todos) {
436
+ const columnKeys = table.columns.map((c) => c.key);
437
+ let cursorField = require_core_entity_heuristics.suggestCursorField(columnKeys);
438
+ if (!cursorField) {
439
+ cursorField = table.columns.find((c) => c.isPrimary)?.key ?? columnKeys[0] ?? "id";
440
+ todos.push(`No updated_at-like column on "${table.tableName}" — using "${cursorField}" as the sync cursor. Confirm it is monotonic.`);
441
+ }
442
+ const pkCols = table.columns.filter((c) => c.isPrimary);
443
+ if (pkCols.length === 1) return {
444
+ cursorField,
445
+ idField: null
446
+ };
447
+ const idField = pkCols[0]?.key ?? columnKeys[0] ?? "id";
448
+ todos.push(`Table "${table.tableName}" has ${pkCols.length === 0 ? "no" : "a composite"} primary key — set idField (guessed "${idField}").`);
449
+ return {
450
+ cursorField,
451
+ idField
452
+ };
453
+ }
454
+ function buildEntity(table, concept, todos) {
455
+ const canonical = require_entity_config.CANONICAL_FIELDS[concept];
456
+ const suggested = require_core_entity_heuristics.toFieldMap(require_core_entity_heuristics.scoreFieldMap(concept, toScoredTable(table)));
457
+ const fields = {};
458
+ for (const [key, column] of Object.entries(suggested)) if (canonical.includes(key)) fields[key] = column;
459
+ for (const required of require_core_entity_heuristics.requiredFieldsFor(concept)) if (!fields[required]) todos.push(`Could not map required ${concept} field "${required}" on "${table.tableName}" — add it manually.`);
460
+ const { cursorField, idField } = chooseSyncFields(table, todos);
461
+ return {
462
+ moduleKey: table.moduleKey,
463
+ tableName: table.tableName,
464
+ concept,
465
+ fields,
466
+ cursorField,
467
+ idField
468
+ };
469
+ }
470
+ function placeholderContact(todos) {
471
+ todos.push("No contact table detected. Filled in a placeholder — set `source`, `fields`, and the fromDrizzle table below, then run `clivly doctor`.");
472
+ return {
473
+ moduleKey: "contacts",
474
+ tableName: "contacts",
475
+ concept: "contact",
476
+ fields: {
477
+ name: "name",
478
+ email: "email"
479
+ },
480
+ cursorField: "updatedAt",
481
+ idField: null
482
+ };
483
+ }
484
+ function planInit(schema) {
485
+ const todos = [];
486
+ const { contact: contactTable, company: companyTable } = selectEntities(schema.tables);
487
+ const contactDetected = contactTable !== null;
488
+ const contact = contactTable ? buildEntity(contactTable, "contact", todos) : placeholderContact(todos);
489
+ let company = null;
490
+ if (companyTable) {
491
+ company = buildEntity(companyTable, "company", todos);
492
+ if (require_core_entity_heuristics.matchesConcept(companyTable.tableName, "contact")) todos.push(`"${companyTable.tableName}" matches both contact and company naming — assigned to company. Swap it to your contact entity if that's wrong.`);
493
+ }
494
+ if (contactTable) {
495
+ const scored = require_core_entity_heuristics.scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
496
+ const top = scored[0];
497
+ const runnerUp = scored[1];
498
+ const isClearWinner = top !== void 0 && (runnerUp === void 0 || top.score - runnerUp.score >= 15);
499
+ if (top && isClearWinner) contact.relationship = {
500
+ name: top.value.name,
501
+ via: top.value.via
502
+ };
503
+ }
504
+ return {
505
+ importPath: schema.importPath,
506
+ db: schema.db,
507
+ contact,
508
+ company,
509
+ todos,
510
+ contactDetected
511
+ };
512
+ }
513
+ //#endregion
514
+ //#region src/render-config.ts
515
+ const CLIVLY_ENV_KEYS = [
516
+ {
517
+ key: "CLIVLY_API_KEY",
518
+ comment: "Org API key — Settings → API keys"
519
+ },
520
+ {
521
+ key: "CLIVLY_SYNC_TRIGGER_URL",
522
+ comment: "Public URL of your mounted tick route (e.g. https://app.example.com/api/clivly/tick)"
523
+ },
524
+ {
525
+ key: "CLIVLY_SYNC_TRIGGER_SECRET",
526
+ comment: "Signing secret — Integrations → Remote sync"
527
+ },
528
+ {
529
+ key: "CLIVLY_VERIFY_TOKEN",
530
+ comment: "Bearer token Clivly presents to /clivly/auth/verify"
531
+ }
532
+ ];
533
+ function renderFields(fields) {
534
+ return `{ ${Object.entries(fields).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
535
+ }
536
+ function renderRelationships(entity) {
537
+ if (!entity.relationship) return "";
538
+ const { name, via } = entity.relationship;
539
+ return `,\n relationships: { ${name}: { entity: "${name}", via: ${JSON.stringify(via)} } }`;
540
+ }
541
+ function renderEntity(key, entity) {
542
+ return ` ${key}: {
543
+ concept: "${entity.concept}",
544
+ source: "${entity.tableName}",
545
+ fields: ${renderFields(entity.fields)}${renderRelationships(entity)},
546
+ }`;
547
+ }
548
+ function renderSource(role, entity) {
549
+ const opts = entity.idField === null ? `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}" }` : `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}", idField: "${entity.idField}" }`;
550
+ return ` ${role}: fromDrizzle(db, ${entity.moduleKey}, ${opts})`;
551
+ }
552
+ function renderTableImport(plan) {
553
+ const tableKeys = [plan.contact.moduleKey, plan.company?.moduleKey].filter(Boolean);
554
+ if (plan.db) return `import { db, ${tableKeys.join(", ")} } from "${plan.importPath}";`;
555
+ return `import { ${tableKeys.join(", ")} } from "${plan.importPath}";
556
+ import { db } from "./db"; // TODO: point at your Drizzle instance`;
557
+ }
558
+ function renderTodoHeader(todos) {
559
+ if (todos.length === 0) return "";
560
+ return `// TODO(clivly): review before running \`clivly doctor\`:\n${todos.map((t) => `// - ${t}`).join("\n")}\n\n`;
561
+ }
562
+ function renderInitConfig(plan) {
563
+ const entities = [renderEntity(plan.contact.moduleKey, plan.contact)];
564
+ 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)}import { createClivlySDK } from "@clivly/sdk";
568
+ import { fromDrizzle } from "@clivly/sdk/drizzle";
569
+ import { defineClivlyConfig } from "@clivly/core";
570
+ ${renderTableImport(plan)}
571
+
572
+ const entities = defineClivlyConfig({
573
+ entities: {
574
+ ${entities.join(",\n")},
575
+ },
576
+ });
577
+
578
+ export default createClivlySDK({
579
+ apiKey: process.env.CLIVLY_API_KEY!,
580
+ entities,
581
+ source: {
582
+ ${sources.join(",\n")},
583
+ },
584
+ syncTrigger: {
585
+ // The public URL Clivly Cloud calls to trigger a sync — your mounted tick route.
586
+ url: process.env.CLIVLY_SYNC_TRIGGER_URL,
587
+ secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
588
+ },
589
+ verifyToken: process.env.CLIVLY_VERIFY_TOKEN,
590
+ });
591
+ `;
592
+ }
593
+ //#endregion
594
+ //#region src/resolve-schema-path.ts
595
+ const COMMON_PATHS = [
596
+ "src/db/schema.ts",
597
+ "src/db/schema",
598
+ "db/schema.ts",
599
+ "db/schema",
600
+ "src/schema.ts",
601
+ "src/schema",
602
+ "drizzle/schema.ts",
603
+ "drizzle/schema"
604
+ ];
605
+ const DRIZZLE_CONFIG_NAMES = [
606
+ "drizzle.config.ts",
607
+ "drizzle.config.js",
608
+ "drizzle.config.mjs"
609
+ ];
610
+ function defaultReadDrizzleConfigSchema(cwd) {
611
+ for (const name of DRIZZLE_CONFIG_NAMES) {
612
+ const path = (0, node_path.resolve)(cwd, name);
613
+ if (!(0, node_fs.existsSync)(path)) continue;
614
+ try {
615
+ const schema = (0, jiti.createJiti)((0, node_url.pathToFileURL)(path).href)(path).default?.schema;
616
+ const first = Array.isArray(schema) ? schema[0] : schema;
617
+ if (typeof first === "string") return first;
618
+ } catch {}
619
+ }
620
+ return null;
621
+ }
622
+ function resolveSchemaPath(opts) {
623
+ const exists = opts.deps?.exists ?? node_fs.existsSync;
624
+ const readSchema = opts.deps?.readDrizzleConfigSchema ?? defaultReadDrizzleConfigSchema;
625
+ const toAbs = (p) => (0, node_path.isAbsolute)(p) ? p : (0, node_path.resolve)(opts.cwd, p);
626
+ if (opts.flag) return toAbs(opts.flag);
627
+ const fromConfig = readSchema(opts.cwd);
628
+ if (fromConfig) return toAbs(fromConfig);
629
+ for (const candidate of COMMON_PATHS) {
630
+ const abs = toAbs(candidate);
631
+ if (exists(abs)) return abs;
632
+ }
633
+ return null;
634
+ }
635
+ //#endregion
636
+ //#region src/scaffold.ts
637
+ const TANSTACK_ROUTE_PREFIX = /^src\/routes\//;
638
+ const TS_EXTENSION = /\.ts$/;
639
+ const ROUTE_PATHS = {
640
+ nextjs: {
641
+ tick: "app/api/clivly/tick/route.ts",
642
+ verify: "app/api/clivly/auth/verify/route.ts"
643
+ },
644
+ "tanstack-start": {
645
+ tick: "src/routes/api/clivly/tick.ts",
646
+ verify: "src/routes/api/clivly/auth/verify.ts"
647
+ },
648
+ sveltekit: {
649
+ tick: "src/routes/api/clivly/tick/+server.ts",
650
+ verify: "src/routes/api/clivly/auth/verify/+server.ts"
651
+ },
652
+ remix: {
653
+ tick: "app/routes/api.clivly.tick.ts",
654
+ verify: "app/routes/api.clivly.auth.verify.ts"
655
+ },
656
+ nuxt: {
657
+ tick: "server/api/clivly/tick.ts",
658
+ verify: "server/api/clivly/auth/verify.ts"
659
+ },
660
+ unknown: null
661
+ };
662
+ function routePath(framework, kind) {
663
+ const paths = ROUTE_PATHS[framework];
664
+ if (!paths) throw new Error(`No route path for framework: ${framework}`);
665
+ return paths[kind];
666
+ }
667
+ function configImport(routeFilePath) {
668
+ const rel = (0, node_path.relative)((0, node_path.dirname)(routeFilePath), "clivly.config").replace(/\\/g, "/");
669
+ return rel.startsWith(".") ? rel : `./${rel}`;
670
+ }
671
+ function renderBody(framework, importPath, verify) {
672
+ 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;
676
+ const handler = clivly.createAuthVerifyHandler(resolveUser);
677
+ ` : `import clivly from "${importPath}";
678
+
679
+ const handler = clivly.createClivlyHandler();
680
+ `;
681
+ switch (framework) {
682
+ case "nextjs": return `${preamble}
683
+ export const POST = (request: Request) => handler(request);
684
+ `;
685
+ case "tanstack-start": return `import { createAPIFileRoute } from "@tanstack/react-start/api";
686
+ ${preamble}
687
+ export const APIRoute = createAPIFileRoute("__ROUTE__")({
688
+ POST: ({ request }) => handler(request),
689
+ });
690
+ `;
691
+ case "sveltekit": return `import type { RequestHandler } from "./$types";
692
+ ${preamble}
693
+ export const POST: RequestHandler = ({ request }) => handler(request);
694
+ `;
695
+ case "remix": return `import type { ActionFunctionArgs } from "@remix-run/node";
696
+ ${preamble}
697
+ export const action = ({ request }: ActionFunctionArgs) => handler(request);
698
+ `;
699
+ case "nuxt": return `import { toWebRequest } from "h3";
700
+ ${preamble}
701
+ export default defineEventHandler((event) => handler(toWebRequest(event)));
702
+ `;
703
+ default: throw new Error(`No route template for framework: ${framework}`);
704
+ }
705
+ }
706
+ function renderRouteFile(framework, kind) {
707
+ const path = routePath(framework, kind);
708
+ let body = renderBody(framework, configImport(path), kind === "verify");
709
+ if (framework === "tanstack-start") {
710
+ const url = `/${path.replace(TANSTACK_ROUTE_PREFIX, "").replace(TS_EXTENSION, "")}`;
711
+ body = body.replace("__ROUTE__", url);
712
+ }
713
+ return body;
714
+ }
715
+ const COMMON_FILES = [{
716
+ path: "clivly.config.ts",
717
+ description: "Your Clivly configuration",
718
+ owned: false
719
+ }];
720
+ function planScaffold(framework) {
721
+ const paths = ROUTE_PATHS[framework];
722
+ if (!paths) return [];
723
+ return [
724
+ {
725
+ path: paths.tick,
726
+ description: "Clivly sync-trigger route (createClivlyHandler)",
727
+ owned: true
728
+ },
729
+ {
730
+ path: paths.verify,
731
+ description: "Clivly auth-verify route (createAuthVerifyHandler) — finish resolveUser",
732
+ owned: true
733
+ },
734
+ ...COMMON_FILES
735
+ ];
736
+ }
737
+ //#endregion
738
+ //#region src/index.ts
739
+ function flagValue(argv, flag) {
740
+ const index = argv.indexOf(flag);
741
+ return index >= 0 ? argv[index + 1] : void 0;
742
+ }
743
+ function readDeps(cwd) {
744
+ const pkgPath = (0, node_path.join)(cwd, "package.json");
745
+ if (!(0, node_fs.existsSync)(pkgPath)) return {};
746
+ try {
747
+ const pkg = JSON.parse((0, node_fs.readFileSync)(pkgPath, "utf8"));
748
+ return {
749
+ ...pkg.dependencies,
750
+ ...pkg.devDependencies
751
+ };
752
+ } catch {
753
+ return {};
754
+ }
755
+ }
756
+ function defaultReadEnv(cwd) {
757
+ const path = (0, node_path.join)(cwd, ".env");
758
+ return (0, node_fs.existsSync)(path) ? (0, node_fs.readFileSync)(path, "utf8") : "";
759
+ }
760
+ function defaultWriteEnv(cwd, contents) {
761
+ (0, node_fs.writeFileSync)((0, node_path.join)(cwd, ".env"), contents, "utf8");
762
+ }
763
+ function resolveInitDeps(options) {
764
+ return {
765
+ out: options.out ?? ((line) => process.stdout.write(line)),
766
+ readDepsFn: options.readDeps ?? readDeps,
767
+ resolvePath: options.resolveSchemaPath ?? ((cwd) => resolveSchemaPath({
768
+ cwd,
769
+ flag: options.schemaFlag
770
+ })),
771
+ discover: options.discoverSchema ?? ((schemaPath, cwd) => discoverSchema({
772
+ schemaPath,
773
+ cwd
774
+ })),
775
+ configExists: options.configExists ?? node_fs.existsSync,
776
+ writeConfig: options.writeConfig ?? ((path, contents) => {
777
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
778
+ (0, node_fs.writeFileSync)(path, contents, "utf8");
779
+ }),
780
+ detectPackageManager: options.detectPackageManager ?? detectPackageManager,
781
+ install: options.installPackages ?? installPackages,
782
+ readEnv: options.readEnv ?? defaultReadEnv,
783
+ writeEnv: options.writeEnv ?? defaultWriteEnv,
784
+ framework: "unknown"
785
+ };
786
+ }
787
+ async function acquireSchema(options, deps) {
788
+ const { out } = deps;
789
+ const appDeps = deps.readDepsFn(options.cwd);
790
+ if (Object.keys(appDeps).length === 0) {
791
+ out("No package.json found here. Run `clivly init` from your app root.\n");
792
+ return { code: 1 };
793
+ }
794
+ const framework = detectFramework(appDeps);
795
+ const orm = detectOrm(appDeps);
796
+ deps.framework = framework;
797
+ out("\nClivly init\n");
798
+ out(` Framework: ${FRAMEWORK_LABELS[framework]}\n`);
799
+ out(` ORM: ${orm}\n\n`);
800
+ if (orm !== "drizzle") {
801
+ out(`clivly init currently generates config for Drizzle only. Detected ORM: ${orm}\n`);
802
+ return { code: 1 };
803
+ }
804
+ if (framework === "unknown") {
805
+ out("clivly init doesn't recognise your framework yet (looked for Next.js, TanStack Start, SvelteKit, Remix, Nuxt). Route file generation needs one of these.\n");
806
+ return { code: 1 };
807
+ }
808
+ const schemaPath = deps.resolvePath(options.cwd);
809
+ if (!schemaPath) {
810
+ out("Could not locate your Drizzle schema. Pass --schema <path>, or add a `schema` field to drizzle.config.ts.\n");
811
+ return { code: 1 };
812
+ }
813
+ out(` Schema: ${schemaPath}\n\n`);
814
+ try {
815
+ const schema = await deps.discover(schemaPath, options.cwd);
816
+ if (schema.tables.length === 0) {
817
+ out(`No Drizzle tables found in ${schemaPath}. Is this the right path?\n`);
818
+ return { code: 1 };
819
+ }
820
+ return schema;
821
+ } catch (error) {
822
+ out(`Failed to load the schema module: ${error.message}\n`);
823
+ out("The schema must import cleanly without a live DB connection.\n");
824
+ return { code: 1 };
825
+ }
826
+ }
827
+ function printPlan(out, plan) {
828
+ out("Detected:\n");
829
+ out(` contact ← ${plan.contact.tableName} fields ${JSON.stringify(plan.contact.fields)}\n`);
830
+ if (plan.company) out(` company ← ${plan.company.tableName} fields ${JSON.stringify(plan.company.fields)}\n`);
831
+ out("\n");
832
+ if (plan.todos.length > 0) {
833
+ out("Review after generation:\n");
834
+ for (const todo of plan.todos) out(` • ${todo}\n`);
835
+ out("\n");
836
+ }
837
+ }
838
+ function writeOwnedFile(options, deps, path, contents) {
839
+ const full = (0, node_path.join)(options.cwd, path);
840
+ if (deps.configExists(full) && !options.force) {
841
+ deps.out(` • ${path} exists — skipped (pass --force to overwrite)\n`);
842
+ return;
843
+ }
844
+ deps.writeConfig(full, contents);
845
+ deps.out(` ✓ ${path}\n`);
846
+ }
847
+ function writeScaffoldFiles(options, deps, plan) {
848
+ writeOwnedFile(options, deps, "clivly.config.ts", renderInitConfig(plan));
849
+ for (const file of planScaffold(deps.framework)) {
850
+ if (file.path === "clivly.config.ts") continue;
851
+ const kind = file.path.includes("verify") ? "verify" : "tick";
852
+ writeOwnedFile(options, deps, file.path, renderRouteFile(deps.framework, kind));
853
+ }
854
+ }
855
+ function writeEnvFile(options, deps) {
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) {
861
+ out("\nNext steps:\n");
862
+ out(" 1. Set the CLIVLY_* values in .env\n");
863
+ out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
864
+ out(" 3. Finish resolveUser in the auth/verify route\n");
865
+ out(" 4. Run `clivly connect` to verify\n");
866
+ }
867
+ async function scaffold(options, deps, plan) {
868
+ const { out } = deps;
869
+ const pm = deps.detectPackageManager(options.cwd);
870
+ const result = await deps.install({
871
+ cwd: options.cwd,
872
+ pm,
873
+ packages: ["@clivly/sdk", "@clivly/core"]
874
+ });
875
+ if (result.ok) out(`Installed @clivly/sdk @clivly/core (${pm})\n`);
876
+ else out(`Could not install automatically — run:\n ${result.command}\n`);
877
+ writeScaffoldFiles(options, deps, plan);
878
+ writeEnvFile(options, deps);
879
+ printNextSteps(out);
880
+ return plan.contactDetected ? 0 : 1;
881
+ }
882
+ async function runInit(options) {
883
+ const deps = resolveInitDeps(options);
884
+ const acquired = await acquireSchema(options, deps);
885
+ if ("code" in acquired) return acquired.code;
886
+ const plan = planInit(acquired);
887
+ printPlan(deps.out, plan);
888
+ return scaffold(options, deps, plan);
889
+ }
890
+ async function main(argv) {
891
+ const [command, ...rest] = argv;
892
+ switch (command) {
893
+ case "init": return await runInit({
894
+ cwd: process.cwd(),
895
+ force: rest.includes("--force"),
896
+ schemaFlag: flagValue(rest, "--schema")
897
+ });
898
+ case "ping": return await runPing({
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({
903
+ cwd: process.cwd(),
904
+ configPath: flagValue(rest, "--config")
905
+ });
906
+ case "connect": return await runConnect({
907
+ cwd: process.cwd(),
908
+ apiKey: process.env.CLIVLY_API_KEY,
909
+ apiUrl: flagValue(rest, "--api-url") ?? process.env.CLIVLY_API_URL,
910
+ configPath: flagValue(rest, "--config")
911
+ });
912
+ case void 0:
913
+ case "help":
914
+ 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 ping [--api-url <url>] Verify CLIVLY_API_KEY + reachability\n clivly doctor [--config <path>] Verify your clivly.config setup end-to-end\n clivly connect [--config <path>] One pass/fail check that setup is wired\n");
916
+ return 0;
917
+ default:
918
+ process.stdout.write(`Unknown command: ${command}\n`);
919
+ return 1;
920
+ }
921
+ }
922
+ if (isEntrypoint(process.argv[1], require("url").pathToFileURL(__filename).href)) main(process.argv.slice(2)).then((code) => {
923
+ process.exitCode = code;
924
+ });
925
+ //#endregion
926
+ exports.runInit = runInit;