@c15t/cli 0.0.1-rc.11

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 (3) hide show
  1. package/LICENSE.md +595 -0
  2. package/dist/index.mjs +1102 -0
  3. package/package.json +55 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1102 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import fs$1, { existsSync } from 'node:fs';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { getAdapter } from '@c15t/backend/pkgs/db-adapters';
7
+ import chalk from 'chalk';
8
+ import prompts from 'prompts';
9
+ import yoctoSpinner from 'yocto-spinner';
10
+ import { z } from 'zod';
11
+ import { createLogger } from '@c15t/backend/pkgs/logger';
12
+ import { getConsentTables } from '@c15t/backend/schema';
13
+ import { getMigrations } from '@c15t/backend/pkgs/migrations';
14
+ import { produceSchema } from '@mrleebo/prisma-ast';
15
+ import babelPresetReact from '@babel/preset-react';
16
+ import babelPresetTypescript from '@babel/preset-typescript';
17
+ import { DoubleTieError } from '@c15t/backend/pkgs/results';
18
+ import { loadConfig } from 'c12';
19
+ import 'dotenv/config';
20
+ import Crypto from 'node:crypto';
21
+ import fs$2 from 'fs-extra';
22
+
23
+ const logger = createLogger({
24
+ level: "info",
25
+ appName: "c15t"
26
+ });
27
+
28
+ function convertToSnakeCase(str) {
29
+ if (str === void 0 || str === null) {
30
+ return "";
31
+ }
32
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
33
+ }
34
+ const generateDrizzleSchema = async ({
35
+ options,
36
+ file,
37
+ adapter
38
+ }) => {
39
+ const tables = getConsentTables(options);
40
+ const filePath = file || "./auth-schema.ts";
41
+ const databaseType = adapter.options?.provider;
42
+ const usePlural = adapter.options?.usePlural;
43
+ const timestampAndBoolean = databaseType !== "sqlite" ? "timestamp, boolean" : "";
44
+ const int = databaseType === "mysql" ? "int" : "integer";
45
+ const hasBigint = Object.values(tables).some(
46
+ (table) => Object.values(table.fields).some(
47
+ (field) => "bigint" in field && field.bigint
48
+ )
49
+ );
50
+ const bigint = databaseType !== "sqlite" ? "bigint" : "";
51
+ const text = databaseType === "mysql" ? "varchar, text" : "text";
52
+ const jsonType = ["mysql", "pg"].includes(databaseType || "") ? ", json" : "";
53
+ let code = `import { ${databaseType}Table, ${text}, ${int}${hasBigint ? `, ${bigint}` : ""}, ${timestampAndBoolean}${jsonType} } from "drizzle-orm/${databaseType}-core";`;
54
+ const fileExist = existsSync(filePath);
55
+ let isFirstTable = true;
56
+ for (const table in tables) {
57
+ if (Object.prototype.hasOwnProperty.call(tables, table)) {
58
+ let getMySQLStringType = function(field, name) {
59
+ if (field.unique) {
60
+ return `varchar('${name}', { length: 255 })`;
61
+ }
62
+ if (field.references) {
63
+ return `varchar('${name}', { length: 36 })`;
64
+ }
65
+ return `text('${name}')`;
66
+ }, getType = function(fieldName, field) {
67
+ const snakeCaseName = convertToSnakeCase(fieldName);
68
+ const type = field.type;
69
+ const typeMap = {
70
+ string: {
71
+ sqlite: `text('${snakeCaseName}')`,
72
+ pg: `text('${snakeCaseName}')`,
73
+ mysql: getMySQLStringType(field, snakeCaseName)
74
+ },
75
+ boolean: {
76
+ sqlite: `integer('${snakeCaseName}', { mode: 'boolean' })`,
77
+ pg: `boolean('${snakeCaseName}')`,
78
+ mysql: `boolean('${snakeCaseName}')`
79
+ },
80
+ number: {
81
+ sqlite: `integer('${snakeCaseName}')`,
82
+ pg: "bigint" in field && field.bigint ? `bigint('${snakeCaseName}', { mode: 'number' })` : `integer('${snakeCaseName}')`,
83
+ mysql: "bigint" in field && field.bigint ? `bigint('${snakeCaseName}', { mode: 'number' })` : `int('${snakeCaseName}')`
84
+ },
85
+ date: {
86
+ sqlite: `integer('${snakeCaseName}', { mode: 'timestamp' })`,
87
+ pg: `timestamp('${snakeCaseName}')`,
88
+ mysql: `timestamp('${snakeCaseName}')`
89
+ },
90
+ // Add JSON type support
91
+ json: {
92
+ sqlite: `text('${snakeCaseName}')`,
93
+ // SQLite uses TEXT for JSON
94
+ pg: `json('${snakeCaseName}')`,
95
+ // PostgreSQL native JSON
96
+ mysql: `json('${snakeCaseName}')`
97
+ // MySQL native JSON
98
+ }
99
+ };
100
+ if (!typeMap[type]) {
101
+ return `text('${snakeCaseName}')`;
102
+ }
103
+ const dbType = databaseType && ["sqlite", "pg", "mysql"].includes(databaseType) ? databaseType : "sqlite";
104
+ return typeMap[type][dbType];
105
+ };
106
+ let modelName = usePlural ? `${tables[table].modelName}s` : tables[table].modelName;
107
+ if (!modelName) {
108
+ modelName = table;
109
+ }
110
+ const fields = tables[table].fields;
111
+ const id = databaseType === "mysql" ? `varchar("id", { length: 36 }).primaryKey()` : `text("id").primaryKey()`;
112
+ const tableNameForSQL = convertToSnakeCase(modelName);
113
+ if (isFirstTable) {
114
+ code += "\n\n";
115
+ isFirstTable = false;
116
+ } else {
117
+ code += "\n\n";
118
+ }
119
+ const schema = `export const ${modelName} = ${databaseType}Table("${tableNameForSQL}", {
120
+ id: ${id},
121
+ ${Object.keys(fields).map((field) => {
122
+ if (Object.prototype.hasOwnProperty.call(fields, field)) {
123
+ const attr = fields[field];
124
+ return ` ${field}: ${getType(field, attr)}${attr.required ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${usePlural ? `${attr.references.model}s` : attr.references.model}.${attr.references.field}, { onDelete: 'cascade' })` : ""}`;
125
+ }
126
+ return "";
127
+ }).filter(Boolean).join(",\n")}
128
+ });`;
129
+ code += schema;
130
+ }
131
+ }
132
+ return {
133
+ code,
134
+ fileName: filePath,
135
+ overwrite: fileExist
136
+ };
137
+ };
138
+
139
+ const CREATE_TABLE_REGEX = /create\s+table\s+"([^"]+)"\s+\((.*)\)/i;
140
+ const CREATE_INDEX_REGEX = /create\s+index\s+"?([^"\s]+)"?\s+on\s+"?([^"\s]+)"?/i;
141
+ const NOT_NULL_REGEX = /\bnot null\b/gi;
142
+ const PRIMARY_KEY_REGEX = /\bprimary key\b/gi;
143
+ const REFERENCES_REGEX = /\breferences\b/gi;
144
+ const UNIQUE_REGEX = /\bunique\b/gi;
145
+ const CREATE_TABLE_KEYWORD_REGEX = /\bcreate\s+table\b/gi;
146
+ const CREATE_INDEX_KEYWORD_REGEX = /\bcreate\s+index\b/gi;
147
+ const ALTER_TABLE_REGEX = /\balter\s+table\b/gi;
148
+ const INSERT_INTO_REGEX = /\binsert\s+into\b/gi;
149
+ const UPDATE_REGEX = /\bupdate\b/gi;
150
+ const DELETE_FROM_REGEX = /\bdelete\s+from\b/gi;
151
+ const SELECT_REGEX = /\bselect\b/gi;
152
+ const FROM_REGEX = /\bfrom\b/gi;
153
+ const WHERE_REGEX = /\bwhere\b/gi;
154
+ const JOIN_REGEX = /\bjoin\b/gi;
155
+ const ON_REGEX = /\bon\b/gi;
156
+ const AND_REGEX = /\band\b/gi;
157
+ const OR_REGEX = /\bor\b/gi;
158
+ const BOOLEAN_FIELD_REGEX = /("is[A-Z][a-zA-Z0-9]*")\s+integer/g;
159
+ const DATE_FIELD_REGEX = /("(?:created|updated|expires)At")\s+date/gi;
160
+ const TEXT_FIELD_REGEX = /("(?:name|code|description|id)")\s+text/gi;
161
+ const JSON_FIELD_REGEX = /("(?:metadata|config|data|settings|options|preferences|attributes)")\s+text/gi;
162
+ function formatSQL(sql, databaseType = "sqlite", options) {
163
+ const dbType = databaseType === "pg" ? "postgresql" : databaseType;
164
+ const statements = sql.split(";").filter((stmt) => stmt.trim());
165
+ const rollbackStatements = [];
166
+ const formattedStatements = statements.map((statement) => {
167
+ const trimmedStmt = statement.trim().toLowerCase();
168
+ if (trimmedStmt.startsWith("create table")) {
169
+ const match = statement.match(CREATE_TABLE_REGEX);
170
+ if (match) {
171
+ const [_, tableName, columnsStr] = match;
172
+ rollbackStatements.unshift(`DROP TABLE IF EXISTS "${tableName}"`);
173
+ const columns = columnsStr.split(",").map((col) => col.trim());
174
+ const formattedColumns = columns.map((col) => {
175
+ let formattedCol = col.replace(NOT_NULL_REGEX, "NOT NULL").replace(PRIMARY_KEY_REGEX, "PRIMARY KEY").replace(REFERENCES_REGEX, "REFERENCES").replace(UNIQUE_REGEX, "UNIQUE");
176
+ if (dbType === "postgresql") {
177
+ formattedCol = formattedCol.replace(BOOLEAN_FIELD_REGEX, "$1 boolean").replace(DATE_FIELD_REGEX, "$1 timestamp with time zone").replace(TEXT_FIELD_REGEX, "$1 varchar(255)").replace(JSON_FIELD_REGEX, "$1 jsonb");
178
+ } else if (dbType === "mysql") {
179
+ formattedCol = formattedCol.replace(BOOLEAN_FIELD_REGEX, "$1 TINYINT(1)").replace(DATE_FIELD_REGEX, "$1 DATETIME").replace(TEXT_FIELD_REGEX, "$1 VARCHAR(255)").replace(JSON_FIELD_REGEX, "$1 JSON");
180
+ } else if (dbType === "sqlite") {
181
+ formattedCol = formattedCol.replace(
182
+ JSON_FIELD_REGEX,
183
+ "$1 text -- stored as JSON"
184
+ );
185
+ }
186
+ return formattedCol;
187
+ }).map((col) => ` ${col}`).join(",\n");
188
+ return `CREATE TABLE IF NOT EXISTS "${tableName}" (
189
+ ${formattedColumns}
190
+ );`;
191
+ }
192
+ }
193
+ if (trimmedStmt.startsWith("create index")) {
194
+ const indexMatch = statement.match(CREATE_INDEX_REGEX);
195
+ if (indexMatch) {
196
+ const [_, indexName] = indexMatch;
197
+ rollbackStatements.unshift(`DROP INDEX IF EXISTS "${indexName}"`);
198
+ return `CREATE INDEX IF NOT EXISTS "${indexName}" ${statement.substring(statement.toLowerCase().indexOf("on")).trim()};`;
199
+ }
200
+ }
201
+ return `${statement.trim().replace(CREATE_TABLE_KEYWORD_REGEX, "CREATE TABLE").replace(CREATE_INDEX_KEYWORD_REGEX, "CREATE INDEX").replace(ALTER_TABLE_REGEX, "ALTER TABLE").replace(INSERT_INTO_REGEX, "INSERT INTO").replace(UPDATE_REGEX, "UPDATE").replace(DELETE_FROM_REGEX, "DELETE FROM").replace(SELECT_REGEX, "SELECT").replace(FROM_REGEX, "FROM").replace(WHERE_REGEX, "WHERE").replace(JOIN_REGEX, "JOIN").replace(ON_REGEX, "ON").replace(AND_REGEX, "AND").replace(OR_REGEX, "OR")};`;
202
+ }).join("\n\n");
203
+ const useTransactions = dbType !== "d1";
204
+ let transactionStart = "";
205
+ if (useTransactions) {
206
+ if (dbType === "mysql") {
207
+ transactionStart = "START TRANSACTION;";
208
+ } else {
209
+ transactionStart = "BEGIN;";
210
+ }
211
+ }
212
+ const transactionEnd = useTransactions ? "COMMIT;" : "";
213
+ const timestamp = options?.timestamp || (/* @__PURE__ */ new Date()).toISOString();
214
+ return `-- Migration generated by C15T (${timestamp})
215
+ -- Database type: ${dbType}
216
+ -- Description: Automatically generated schema migration
217
+ --
218
+ -- Wrapped in a transaction for atomicity
219
+ -- To roll back this migration, use the ROLLBACK section below
220
+
221
+ ${transactionStart}
222
+ -- MIGRATION
223
+ ${formattedStatements}
224
+ ${transactionEnd}
225
+
226
+ -- ROLLBACK
227
+ -- Uncomment the section below to roll back this migration
228
+ /*
229
+ ${transactionStart}
230
+
231
+ ${rollbackStatements.join(";\n\n")};
232
+
233
+ ${transactionEnd}
234
+ */`;
235
+ }
236
+ const generateMigrations = async ({
237
+ options,
238
+ file,
239
+ adapter
240
+ }) => {
241
+ const { compileMigrations } = await getMigrations(options);
242
+ const migrations = await compileMigrations();
243
+ let databaseType = "sqlite";
244
+ if (adapter?.options?.provider) {
245
+ databaseType = adapter.options.provider;
246
+ } else if (options.database && "options" in options.database && options.database.options && typeof options.database.options === "object" && "provider" in options.database.options) {
247
+ databaseType = options.database.options.provider;
248
+ }
249
+ const isTest = process.env.NODE_ENV === "test" || file?.includes("test");
250
+ const testTimestamp = options?._testTimestamp;
251
+ const formatOptions = {
252
+ timestamp: testTimestamp || (isTest ? "2023-01-01T00:00:00.000Z" : void 0)
253
+ };
254
+ const formattedMigrations = formatSQL(
255
+ migrations,
256
+ databaseType,
257
+ formatOptions
258
+ );
259
+ const generatedFileName = file || `./c15t_migrations/${Date.now()}_create_tables.sql`;
260
+ return {
261
+ code: formattedMigrations,
262
+ fileName: generatedFileName
263
+ };
264
+ };
265
+
266
+ function capitalizeFirstLetter(str) {
267
+ return str.charAt(0).toUpperCase() + str.slice(1);
268
+ }
269
+
270
+ const generatePrismaSchema = async ({
271
+ adapter,
272
+ options,
273
+ file
274
+ }) => {
275
+ const provider = adapter.options?.provider || "postgresql";
276
+ const tables = getConsentTables(options);
277
+ const filePath = file || "./prisma/schema.prisma";
278
+ const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
279
+ let schemaPrisma = "";
280
+ if (schemaPrismaExist) {
281
+ schemaPrisma = await fs.readFile(
282
+ path.join(process.cwd(), filePath),
283
+ "utf-8"
284
+ );
285
+ } else {
286
+ schemaPrisma = getNewPrisma(provider);
287
+ }
288
+ const manyToManyRelations = /* @__PURE__ */ new Map();
289
+ for (const table in tables) {
290
+ if (Object.hasOwn(tables, table)) {
291
+ const fields = tables[table]?.fields;
292
+ for (const field in fields) {
293
+ if (Object.hasOwn(fields, field)) {
294
+ const attr = fields[field];
295
+ if (attr?.references) {
296
+ const referencedModel = capitalizeFirstLetter(
297
+ attr.references.model
298
+ );
299
+ if (!manyToManyRelations.has(referencedModel)) {
300
+ manyToManyRelations.set(referencedModel, /* @__PURE__ */ new Set());
301
+ }
302
+ manyToManyRelations.get(referencedModel).add(capitalizeFirstLetter(table));
303
+ }
304
+ }
305
+ }
306
+ }
307
+ }
308
+ const schema = produceSchema(schemaPrisma, (builder) => {
309
+ function getPrismaType(type, isOptional, isBigint) {
310
+ const isJsonField = type === "json" || type === "jsonb";
311
+ if (isJsonField) {
312
+ if (provider === "postgresql" || provider === "mysql") {
313
+ return isOptional ? "Json?" : "Json";
314
+ }
315
+ return isOptional ? "String?" : 'String @map("json_as_text")';
316
+ }
317
+ if (type === "string") {
318
+ return isOptional ? "String?" : "String";
319
+ }
320
+ if (type === "number" && isBigint) {
321
+ return isOptional ? "BigInt?" : "BigInt";
322
+ }
323
+ if (type === "number") {
324
+ return isOptional ? "Int?" : "Int";
325
+ }
326
+ if (type === "boolean") {
327
+ return isOptional ? "Boolean?" : "Boolean";
328
+ }
329
+ if (type === "date") {
330
+ return isOptional ? "DateTime?" : "DateTime";
331
+ }
332
+ if (type === "string[]") {
333
+ return "String[]";
334
+ }
335
+ if (type === "number[]") {
336
+ return "Int[]";
337
+ }
338
+ return "String";
339
+ }
340
+ for (const table in tables) {
341
+ if (Object.hasOwn(tables, table)) {
342
+ const fields = tables[table]?.fields;
343
+ const originalTable = tables[table]?.modelName;
344
+ const modelName = capitalizeFirstLetter(originalTable || table);
345
+ const prismaModel = builder.findByType("model", { name: modelName });
346
+ if (!prismaModel) {
347
+ if (provider === "mongodb") {
348
+ builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`);
349
+ } else {
350
+ builder.model(modelName).field("id", "String").attribute("id");
351
+ }
352
+ }
353
+ for (const field in fields) {
354
+ if (Object.hasOwn(fields, field)) {
355
+ const attr = fields[field];
356
+ const existingField = builder.findByType("field", {
357
+ name: field,
358
+ within: prismaModel?.properties
359
+ });
360
+ if (existingField) {
361
+ continue;
362
+ }
363
+ builder.model(modelName).field(
364
+ field,
365
+ getPrismaType(attr.type, !attr?.required, attr?.bigint || false)
366
+ );
367
+ if (attr.unique) {
368
+ builder.model(modelName).blockAttribute(`unique([${field}])`);
369
+ }
370
+ if (attr.references) {
371
+ builder.model(modelName).field(
372
+ `${attr.references.model.toLowerCase()}`,
373
+ capitalizeFirstLetter(attr.references.model)
374
+ ).attribute(
375
+ `relation(fields: [${field}], references: [${attr.references.field}], onDelete: Cascade)`
376
+ );
377
+ }
378
+ if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") {
379
+ builder.model(modelName).field(field).attribute("db.Text");
380
+ }
381
+ }
382
+ }
383
+ if (originalTable && originalTable !== modelName) {
384
+ const hasMapAttribute = builder.findByType("attribute", {
385
+ name: "map",
386
+ within: prismaModel?.properties
387
+ });
388
+ if (!hasMapAttribute) {
389
+ builder.model(modelName).blockAttribute("map", originalTable);
390
+ }
391
+ }
392
+ }
393
+ }
394
+ for (const [
395
+ referencedModel,
396
+ relatedModels
397
+ ] of manyToManyRelations.entries()) {
398
+ for (const relatedModel of relatedModels) {
399
+ const fieldName = `${relatedModel.toLowerCase()}s`;
400
+ const model = builder.findByType("model", { name: referencedModel });
401
+ if (model) {
402
+ const existingField = builder.findByType("field", {
403
+ name: fieldName,
404
+ within: model.properties
405
+ });
406
+ if (!existingField) {
407
+ builder.model(referencedModel).field(fieldName, `${relatedModel}[]`);
408
+ }
409
+ }
410
+ }
411
+ }
412
+ });
413
+ return {
414
+ code: schema.trim() === schemaPrisma.trim() ? "" : schema,
415
+ fileName: filePath
416
+ };
417
+ };
418
+ const getNewPrisma = (provider) => `generator client {
419
+ provider = "prisma-client-js"
420
+ }
421
+
422
+ datasource db {
423
+ provider = "${provider}"
424
+ url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`}
425
+ }`;
426
+
427
+ const adapters = {
428
+ prisma: generatePrismaSchema,
429
+ drizzle: generateDrizzleSchema,
430
+ kysely: generateMigrations
431
+ };
432
+ const getGenerator = (opts) => {
433
+ const adapter = opts.adapter;
434
+ const generator = adapter.id in adapters ? adapters[adapter.id] : null;
435
+ if (!generator) {
436
+ logger.error(`${adapter.id} is not supported.`);
437
+ process.exit(1);
438
+ }
439
+ return generator(opts);
440
+ };
441
+
442
+ function addSvelteKitEnvModules(aliases) {
443
+ aliases["$env/dynamic/private"] = createDataUriModule(
444
+ createDynamicEnvModule()
445
+ );
446
+ aliases["$env/dynamic/public"] = createDataUriModule(
447
+ createDynamicEnvModule()
448
+ );
449
+ aliases["$env/static/private"] = createDataUriModule(
450
+ createStaticEnvModule(filterPrivateEnv("PUBLIC_", ""))
451
+ );
452
+ aliases["$env/static/public"] = createDataUriModule(
453
+ createStaticEnvModule(filterPublicEnv("PUBLIC_", ""))
454
+ );
455
+ }
456
+ function createDataUriModule(module) {
457
+ return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`;
458
+ }
459
+ function createStaticEnvModule(env) {
460
+ const declarations = Object.keys(env).filter((k) => validIdentifier.test(k) && !reserved.has(k)).map((k) => `export const ${k} = ${JSON.stringify(env[k])};`);
461
+ return `
462
+ ${declarations.join("\n")}
463
+ // jiti dirty hack: .unknown
464
+ `;
465
+ }
466
+ function createDynamicEnvModule() {
467
+ return `
468
+ export const env = process.env;
469
+ // jiti dirty hack: .unknown
470
+ `;
471
+ }
472
+ function filterPrivateEnv(publicPrefix, privatePrefix) {
473
+ return Object.fromEntries(
474
+ Object.entries(process.env).filter(
475
+ ([k]) => k.startsWith(privatePrefix) && (!k.startsWith(publicPrefix))
476
+ )
477
+ );
478
+ }
479
+ function filterPublicEnv(publicPrefix, privatePrefix) {
480
+ return Object.fromEntries(
481
+ Object.entries(process.env).filter(
482
+ ([k]) => k.startsWith(publicPrefix) && (privatePrefix === "")
483
+ )
484
+ );
485
+ }
486
+ const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
487
+ const reserved = /* @__PURE__ */ new Set([
488
+ "do",
489
+ "if",
490
+ "in",
491
+ "for",
492
+ "let",
493
+ "new",
494
+ "try",
495
+ "var",
496
+ "case",
497
+ "else",
498
+ "enum",
499
+ "eval",
500
+ "null",
501
+ "this",
502
+ "true",
503
+ "void",
504
+ "with",
505
+ "await",
506
+ "break",
507
+ "catch",
508
+ "class",
509
+ "const",
510
+ "false",
511
+ "super",
512
+ "throw",
513
+ "while",
514
+ "yield",
515
+ "delete",
516
+ "export",
517
+ "import",
518
+ "public",
519
+ "return",
520
+ "static",
521
+ "switch",
522
+ "typeof",
523
+ "default",
524
+ "extends",
525
+ "finally",
526
+ "package",
527
+ "private",
528
+ "continue",
529
+ "debugger",
530
+ "function",
531
+ "arguments",
532
+ "interface",
533
+ "protected",
534
+ "implements",
535
+ "instanceof"
536
+ ]);
537
+
538
+ const configFileNames = ["c15t", "consent", "cmp"];
539
+ const extensions = [
540
+ ".js",
541
+ ".jsx",
542
+ ".ts",
543
+ ".tsx",
544
+ ".cjs",
545
+ ".cts",
546
+ ".mjs",
547
+ ".mts",
548
+ ".server.cjs",
549
+ ".server.cts",
550
+ ".server.js",
551
+ ".server.jsx",
552
+ ".server.mjs",
553
+ ".server.mts",
554
+ ".server.ts",
555
+ ".server.tsx"
556
+ ];
557
+ let possiblePaths = configFileNames.flatMap(
558
+ (name) => extensions.map((ext) => `${name}${ext}`)
559
+ );
560
+ const directories = [
561
+ "",
562
+ "lib/server/",
563
+ "server/",
564
+ "lib/",
565
+ "utils/",
566
+ "config/",
567
+ "src/",
568
+ "app/"
569
+ ];
570
+ possiblePaths = directories.flatMap(
571
+ (dir) => possiblePaths.map((file) => `${dir}${file}`)
572
+ );
573
+ const monorepoSubdirs = ["packages/*", "apps/*"];
574
+ function stripJsonComments(jsonString) {
575
+ return jsonString.replace(
576
+ /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
577
+ (m, g) => g ? "" : m
578
+ ).replace(/,(?=\s*[}\]])/g, "");
579
+ }
580
+ function getPathAliases(cwd) {
581
+ const tsConfigPath = path.join(cwd, "tsconfig.json");
582
+ if (!fs$1.existsSync(tsConfigPath)) {
583
+ const jsConfigPath = path.join(cwd, "jsconfig.json");
584
+ if (!fs$1.existsSync(jsConfigPath)) {
585
+ return null;
586
+ }
587
+ return extractAliasesFromConfigFile(jsConfigPath, cwd);
588
+ }
589
+ return extractAliasesFromConfigFile(tsConfigPath, cwd);
590
+ }
591
+ function extractAliasesFromConfigFile(configPath, cwd) {
592
+ try {
593
+ const configContent = fs$1.readFileSync(configPath, "utf8");
594
+ const strippedConfigContent = stripJsonComments(configContent);
595
+ const config = JSON.parse(strippedConfigContent);
596
+ const { paths = {}, baseUrl = "." } = config.compilerOptions || {};
597
+ const result = {};
598
+ const obj = Object.entries(paths);
599
+ for (const [alias, aliasPaths] of obj) {
600
+ for (const aliasedPath of aliasPaths) {
601
+ const resolvedBaseUrl = path.join(cwd, baseUrl);
602
+ const finalAlias = alias.slice(-1) === "*" ? alias.slice(0, -1) : alias;
603
+ const finalAliasedPath = aliasedPath.slice(-1) === "*" ? aliasedPath.slice(0, -1) : aliasedPath;
604
+ result[finalAlias || ""] = path.join(resolvedBaseUrl, finalAliasedPath);
605
+ }
606
+ }
607
+ addSvelteKitEnvModules(result);
608
+ return result;
609
+ } catch (error) {
610
+ logger.warn(`Error parsing config file ${configPath}`, error);
611
+ return null;
612
+ }
613
+ }
614
+ const jitiOptions = (cwd) => {
615
+ const alias = getPathAliases(cwd) || {};
616
+ return {
617
+ transformOptions: {
618
+ babel: {
619
+ presets: [
620
+ [
621
+ babelPresetTypescript,
622
+ {
623
+ isTSX: true,
624
+ allExtensions: true
625
+ }
626
+ ],
627
+ [babelPresetReact, { runtime: "automatic" }]
628
+ ]
629
+ }
630
+ },
631
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
632
+ alias
633
+ };
634
+ };
635
+ function extractOptionsFromConfig(config) {
636
+ if (config.c15t && typeof config.c15t === "function") {
637
+ return config.c15t;
638
+ }
639
+ if (config.default && typeof config.default === "function") {
640
+ return config.default;
641
+ }
642
+ if (config.c15tInstance && typeof config.c15tInstance === "function") {
643
+ return config.c15tInstance;
644
+ }
645
+ if (config.consent && typeof config.consent === "function") {
646
+ return config.consent;
647
+ }
648
+ return config.c15t?.options || config.default?.options || config.c15tInstance?.options || config.instance?.options || config.consent?.options || config.config?.options || // Also check for direct exports of options objects
649
+ (config.default && typeof config.default === "object" && "appName" in config.default ? config.default : null) || // Finally check for direct exports of the instance
650
+ (config.c15t && typeof config.c15t === "object" && "appName" in config.c15t ? config.c15t : null) || null;
651
+ }
652
+ function findDirectories(cwd, patterns) {
653
+ const results = [];
654
+ for (const pattern of patterns) {
655
+ if (pattern.includes("*")) {
656
+ const [prefix, _] = pattern.split("*");
657
+ const basePath = path.join(cwd, prefix);
658
+ try {
659
+ if (fs$1.existsSync(basePath)) {
660
+ const entries = fs$1.readdirSync(basePath, { withFileTypes: true });
661
+ for (const entry of entries) {
662
+ if (entry.isDirectory()) {
663
+ results.push(path.join(prefix, entry.name));
664
+ }
665
+ }
666
+ }
667
+ } catch {
668
+ }
669
+ } else if (fs$1.existsSync(path.join(cwd, pattern)) && fs$1.statSync(path.join(cwd, pattern)).isDirectory()) {
670
+ results.push(pattern);
671
+ }
672
+ }
673
+ return results;
674
+ }
675
+ function validateConfig(config) {
676
+ if (!config) {
677
+ return false;
678
+ }
679
+ return typeof config === "object";
680
+ }
681
+ async function getConfig({
682
+ cwd,
683
+ configPath
684
+ }) {
685
+ const foundPaths = [];
686
+ const failedImports = [];
687
+ try {
688
+ let configFile = null;
689
+ if (configPath) {
690
+ const resolvedPath = path.join(cwd, configPath);
691
+ try {
692
+ if (!fs$1.existsSync(resolvedPath)) {
693
+ throw new DoubleTieError(
694
+ `Configuration file not found: ${resolvedPath}
695
+ Make sure the path is correct and the file exists.`,
696
+ {
697
+ code: "CONFIG_FILE_NOT_FOUND",
698
+ status: 404,
699
+ category: "CONFIG_FILE_NOT_FOUND"
700
+ }
701
+ );
702
+ }
703
+ foundPaths.push(resolvedPath);
704
+ const { config } = await loadConfig({
705
+ configFile: resolvedPath,
706
+ dotenv: true,
707
+ jitiOptions: jitiOptions(cwd)
708
+ });
709
+ configFile = extractOptionsFromConfig(config);
710
+ if (!configFile) {
711
+ throw new DoubleTieError(
712
+ // biome-ignore lint/style/useTemplate: keep it split so its easier to read
713
+ `Found config file at ${resolvedPath} but couldn't extract c15t options.
714
+ Make sure you're exporting c15t with one of these patterns:
715
+ - export const c15t = c15tInstance({...})
716
+ - export const consent = c15tInstance({...})
717
+ - export const c15tInstance = c15tInstance({...})
718
+ - export default c15tInstance({...})`,
719
+ {
720
+ code: "CONFIG_FILE_LOAD_ERROR",
721
+ status: 500,
722
+ category: "CONFIG_FILE_LOAD_ERROR"
723
+ }
724
+ );
725
+ }
726
+ } catch (e) {
727
+ if (fs$1.existsSync(resolvedPath)) {
728
+ failedImports.push(resolvedPath);
729
+ if (e instanceof DoubleTieError) {
730
+ throw e;
731
+ }
732
+ throw new DoubleTieError(
733
+ // biome-ignore lint/style/useTemplate: keep it split so its easier to read
734
+ `Config file found at ${resolvedPath} but failed to load.
735
+ This usually happens because of import problems:
736
+ - Check for invalid import paths
737
+ - Ensure all dependencies are installed
738
+ - Verify path aliases in tsconfig.json
739
+
740
+ Error details: ${e instanceof Error ? e.message : String(e)}`,
741
+ {
742
+ code: "CONFIG_FILE_LOAD_ERROR",
743
+ status: 500,
744
+ category: "CONFIG_FILE_LOAD_ERROR",
745
+ cause: e
746
+ }
747
+ );
748
+ }
749
+ throw e;
750
+ }
751
+ }
752
+ if (!configFile) {
753
+ const searchDirs = [""];
754
+ searchDirs.push(...findDirectories(cwd, monorepoSubdirs));
755
+ for (const dir of searchDirs) {
756
+ for (const possiblePath of possiblePaths) {
757
+ const configPath2 = path.join(dir, possiblePath);
758
+ const fullPath = path.join(cwd, configPath2);
759
+ if (!fs$1.existsSync(fullPath)) {
760
+ continue;
761
+ }
762
+ foundPaths.push(fullPath);
763
+ try {
764
+ const { config } = await loadConfig({
765
+ configFile: configPath2,
766
+ jitiOptions: jitiOptions(cwd)
767
+ });
768
+ if (Object.keys(config).length > 0) {
769
+ configFile = extractOptionsFromConfig(config);
770
+ if (configFile && validateConfig(configFile)) {
771
+ logger.info(`\u2705 Using c15t config from ${fullPath}`);
772
+ break;
773
+ }
774
+ }
775
+ } catch (e) {
776
+ if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes(
777
+ "This module cannot be imported from a Client Component module"
778
+ )) {
779
+ throw new DoubleTieError(
780
+ // biome-ignore lint/style/useTemplate: keep it split so its easier to read
781
+ `Found config file at ${fullPath}, but it imports 'server-only'.
782
+ Please temporarily remove the 'server-only' import while using the CLI,
783
+ and you can add it back afterwards.`,
784
+ {
785
+ code: "SERVER_ONLY_IMPORT_DETECTED",
786
+ status: 500,
787
+ category: "SERVER_ONLY_IMPORT_DETECTED"
788
+ }
789
+ );
790
+ }
791
+ failedImports.push(fullPath);
792
+ }
793
+ }
794
+ if (configFile) {
795
+ break;
796
+ }
797
+ }
798
+ }
799
+ if (!configFile) {
800
+ if (foundPaths.length > 0) {
801
+ logger.error(
802
+ `\u274C Found ${foundPaths.length} potential config files, but couldn't load any of them:`
803
+ );
804
+ for (const filePath of foundPaths.slice(0, 3)) {
805
+ logger.error(` - ${filePath}`);
806
+ }
807
+ if (foundPaths.length > 3) {
808
+ logger.error(` - ...and ${foundPaths.length - 3} more`);
809
+ }
810
+ if (failedImports.length > 0) {
811
+ logger.error("\n\u2753 Common issues that prevent loading config files:");
812
+ logger.error(" - Missing dependencies (check your package.json)");
813
+ logger.error(
814
+ " - Import path issues (check your import statements)"
815
+ );
816
+ logger.error(
817
+ " - Path alias configuration (check your tsconfig.json)"
818
+ );
819
+ logger.error(
820
+ " - Export format (make sure you're exporting c15t, c15tInstance, consent, or default)"
821
+ );
822
+ }
823
+ throw new DoubleTieError("Unable to load any c15t configuration file", {
824
+ code: "CONFIG_FILE_LOAD_ERROR",
825
+ status: 500,
826
+ category: "CONFIG_FILE_LOAD_ERROR"
827
+ });
828
+ }
829
+ logger.error(
830
+ "\u274C No c15t configuration files found in standard locations"
831
+ );
832
+ logger.info("\n\u{1F4DD} Create a c15t.ts file with your configuration:");
833
+ logger.info(`
834
+ import { c15tInstance } from '@c15t/backend';
835
+
836
+ export const c15t = c15tInstance({
837
+ appName: 'My App',
838
+ basePath: '/api/c15t',
839
+ // Add your configuration here
840
+ });
841
+ `);
842
+ throw new DoubleTieError(
843
+ "No c15t config file found. Create a c15t.ts file or specify with --config",
844
+ {
845
+ code: "CONFIG_FILE_NOT_FOUND",
846
+ status: 404,
847
+ category: "CONFIG_FILE_NOT_FOUND"
848
+ }
849
+ );
850
+ }
851
+ return configFile;
852
+ } catch (e) {
853
+ if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes(
854
+ "This module cannot be imported from a Client Component module"
855
+ )) {
856
+ logger.error(
857
+ "\u274C Server-only import detected in config file\nPlease temporarily remove the 'server-only' import while using the CLI,\nand you can add it back afterwards."
858
+ );
859
+ process.exit(1);
860
+ }
861
+ if (e instanceof DoubleTieError) {
862
+ logger.error(`\u274C ${e.message}`);
863
+ } else {
864
+ logger.error(`\u274C Couldn't read your c15t configuration`);
865
+ logger.error(` Error: ${e instanceof Error ? e.message : String(e)}`);
866
+ }
867
+ if (failedImports.length > 0) {
868
+ logger.info(
869
+ "\n\u{1F4A1} Tip: If you're having import issues, try running with verbose logging:"
870
+ );
871
+ logger.info(" DEBUG=c15t* npx c15t@latest <command>");
872
+ }
873
+ process.exit(1);
874
+ }
875
+ }
876
+
877
+ async function generateAction(opts) {
878
+ const options = z.object({
879
+ cwd: z.string(),
880
+ config: z.string().optional(),
881
+ output: z.string().optional(),
882
+ y: z.boolean().optional()
883
+ }).parse(opts);
884
+ const cwd = path.resolve(options.cwd);
885
+ if (!existsSync(cwd)) {
886
+ logger.error(`The directory "${cwd}" does not exist.`);
887
+ process.exit(1);
888
+ }
889
+ const config = await getConfig({
890
+ cwd,
891
+ configPath: options.config
892
+ });
893
+ if (!config) {
894
+ logger.error(
895
+ "No configuration file found. Add a `c15t.ts` file to your project or pass the path to the configuration file using the `--config` flag."
896
+ );
897
+ return;
898
+ }
899
+ const adapter = await getAdapter(config).catch((e) => {
900
+ logger.error(e.message);
901
+ process.exit(1);
902
+ });
903
+ const spinner = yoctoSpinner({ text: "preparing schema..." }).start();
904
+ const schema = await getGenerator({
905
+ adapter,
906
+ file: options.output,
907
+ options: config
908
+ });
909
+ spinner.stop();
910
+ if (!schema.code) {
911
+ logger.info("Your schema is already up to date.");
912
+ process.exit(0);
913
+ }
914
+ if (schema.append || schema.overwrite) {
915
+ let confirm2 = options.y;
916
+ if (!confirm2) {
917
+ const response = await prompts({
918
+ type: "confirm",
919
+ name: "confirm",
920
+ message: `The file ${schema.fileName} already exists. Do you want to ${chalk.yellow(
921
+ `${schema.overwrite ? "overwrite" : "append"}`
922
+ )} the schema to the file?`
923
+ });
924
+ confirm2 = response.confirm;
925
+ }
926
+ if (confirm2) {
927
+ const exist = existsSync(path.join(cwd, schema.fileName));
928
+ if (!exist) {
929
+ await fs.mkdir(path.dirname(path.join(cwd, schema.fileName)), {
930
+ recursive: true
931
+ });
932
+ }
933
+ if (schema.overwrite) {
934
+ await fs.writeFile(path.join(cwd, schema.fileName), schema.code);
935
+ } else {
936
+ await fs.appendFile(path.join(cwd, schema.fileName), schema.code);
937
+ }
938
+ logger.success(
939
+ `\u{1F680} Schema was ${schema.overwrite ? "overwritten" : "appended"} successfully!`
940
+ );
941
+ process.exit(0);
942
+ } else {
943
+ logger.error("Schema generation aborted.");
944
+ process.exit(1);
945
+ }
946
+ }
947
+ let confirm = options.y;
948
+ if (!confirm) {
949
+ const response = await prompts({
950
+ type: "confirm",
951
+ name: "confirm",
952
+ message: `Do you want to generate the schema to ${chalk.yellow(
953
+ schema.fileName
954
+ )}?`
955
+ });
956
+ confirm = response.confirm;
957
+ }
958
+ if (!confirm) {
959
+ logger.error("Schema generation aborted.");
960
+ process.exit(1);
961
+ }
962
+ if (!options.output) {
963
+ const dirExist = existsSync(path.dirname(path.join(cwd, schema.fileName)));
964
+ if (!dirExist) {
965
+ await fs.mkdir(path.dirname(path.join(cwd, schema.fileName)), {
966
+ recursive: true
967
+ });
968
+ }
969
+ }
970
+ await fs.writeFile(
971
+ options.output || path.join(cwd, schema.fileName),
972
+ schema.code
973
+ );
974
+ logger.success("\u{1F680} Schema was generated successfully!");
975
+ process.exit(0);
976
+ }
977
+ const generate = new Command("generate").option(
978
+ "-c, --cwd <cwd>",
979
+ "the working directory. defaults to the current directory.",
980
+ process.cwd()
981
+ ).option(
982
+ "--config <config>",
983
+ "the path to the configuration file. defaults to the first configuration file found."
984
+ ).option("--output <output>", "the file to output to the generated schema").option("-y, --y", "automatically answer yes to all prompts", false).action(generateAction);
985
+
986
+ async function migrateAction(opts) {
987
+ const options = z.object({
988
+ cwd: z.string(),
989
+ config: z.string().optional(),
990
+ y: z.boolean().optional()
991
+ }).parse(opts);
992
+ const cwd = path.resolve(options.cwd);
993
+ if (!existsSync(cwd)) {
994
+ logger.error(`The directory "${cwd}" does not exist.`);
995
+ process.exit(1);
996
+ }
997
+ const config = await getConfig({
998
+ cwd,
999
+ configPath: options.config
1000
+ });
1001
+ if (!config) {
1002
+ logger.error(
1003
+ "No configuration file found. Add a `c15t.ts` file to your project or pass the path to the configuration file using the `--config` flag."
1004
+ );
1005
+ return;
1006
+ }
1007
+ const db = await getAdapter(config);
1008
+ if (!db) {
1009
+ logger.error(
1010
+ "Invalid database configuration. Make sure you're not using adapters. Migrate command only works with built-in Kysely adapter."
1011
+ );
1012
+ process.exit(1);
1013
+ }
1014
+ if (db.id !== "kysely") {
1015
+ if (db.id === "prisma") {
1016
+ logger.error(
1017
+ "The migrate command only works with the built-in Kysely adapter. For Prisma, run `npx @c15t/cli generate` to create the schema, then use Prisma\u2019s migrate or push to apply it."
1018
+ );
1019
+ process.exit(0);
1020
+ }
1021
+ if (db.id === "drizzle") {
1022
+ logger.error(
1023
+ "The migrate command only works with the built-in Kysely adapter. For Drizzle, run `npx @c15t/cli generate` to create the schema, then use Drizzle\u2019s migrate or push to apply it."
1024
+ );
1025
+ process.exit(0);
1026
+ }
1027
+ logger.error("Migrate command isn't supported for this adapter.");
1028
+ process.exit(1);
1029
+ }
1030
+ const spinner = yoctoSpinner({ text: "preparing migration..." }).start();
1031
+ const { toBeAdded, toBeCreated, runMigrations } = await getMigrations(config);
1032
+ if (!toBeAdded.length && !toBeCreated.length) {
1033
+ spinner.stop();
1034
+ logger.info("\u{1F680} No migrations needed.");
1035
+ process.exit(0);
1036
+ }
1037
+ spinner.stop();
1038
+ logger.info("\u{1F511} The migration will affect the following:");
1039
+ for (const table of [...toBeCreated, ...toBeAdded]) {
1040
+ console.log(
1041
+ "->",
1042
+ chalk.magenta(Object.keys(table.fields).join(", ")),
1043
+ chalk.white("fields on"),
1044
+ chalk.yellow(`${table.table}`),
1045
+ chalk.white("table.")
1046
+ );
1047
+ }
1048
+ let migrate2 = options.y;
1049
+ if (!migrate2) {
1050
+ const response = await prompts({
1051
+ type: "confirm",
1052
+ name: "migrate",
1053
+ message: "Are you sure you want to run these migrations?",
1054
+ initial: false
1055
+ });
1056
+ migrate2 = response.migrate;
1057
+ }
1058
+ if (!migrate2) {
1059
+ logger.info("Migration cancelled.");
1060
+ process.exit(0);
1061
+ }
1062
+ spinner?.start("migrating...");
1063
+ await runMigrations();
1064
+ spinner.stop();
1065
+ logger.info("\u{1F680} migration was completed successfully!");
1066
+ process.exit(0);
1067
+ }
1068
+ const migrate = new Command("migrate").option(
1069
+ "-c, --cwd <cwd>",
1070
+ "the working directory. defaults to the current directory.",
1071
+ process.cwd()
1072
+ ).option(
1073
+ "--config <config>",
1074
+ "the path to the configuration file. defaults to the first configuration file found."
1075
+ ).option(
1076
+ "-y, --y",
1077
+ "automatically accept and run migrations without prompting",
1078
+ false
1079
+ ).action(migrateAction);
1080
+
1081
+ const generateSecret = new Command("secret").action(() => {
1082
+ const secret = Crypto.randomBytes(32).toString("hex");
1083
+ logger.info(`
1084
+ Add the following to your .env file:
1085
+ ${chalk.gray("# C15T Secret") + chalk.green(`
1086
+ C15T_SECRET=${secret}`)}`);
1087
+ });
1088
+
1089
+ function getPackageInfo() {
1090
+ const packageJsonPath = path.join("package.json");
1091
+ return fs$2.readJSONSync(packageJsonPath);
1092
+ }
1093
+
1094
+ process.on("SIGINT", () => process.exit(0));
1095
+ process.on("SIGTERM", () => process.exit(0));
1096
+ async function main() {
1097
+ const program = new Command("c15t");
1098
+ const packageInfo = await getPackageInfo();
1099
+ program.addCommand(migrate).addCommand(generate).addCommand(generateSecret).version(packageInfo.version || "1.1.2").description("c15t CLI");
1100
+ program.parse();
1101
+ }
1102
+ main();