@l4yercak3/cli 1.2.21 → 1.3.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.
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Model/Type Scanner
3
+ * Detects data models and types across multiple sources:
4
+ * - Prisma schemas
5
+ * - TypeScript interfaces/types
6
+ * - Drizzle table definitions
7
+ * - Convex schemas
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ /**
14
+ * @typedef {Object} DetectedModel
15
+ * @property {string} name - Model name
16
+ * @property {string} source - Source identifier (e.g., 'prisma/schema.prisma')
17
+ * @property {string[]} fields - Field names
18
+ */
19
+
20
+ /**
21
+ * Check if a path exists
22
+ * @param {string} filePath
23
+ * @returns {boolean}
24
+ */
25
+ function exists(filePath) {
26
+ try {
27
+ return fs.existsSync(filePath);
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Safely read a file
35
+ * @param {string} filePath
36
+ * @returns {string|null}
37
+ */
38
+ function readFile(filePath) {
39
+ try {
40
+ return fs.readFileSync(filePath, 'utf8');
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Find files matching a pattern in a directory (non-recursive)
48
+ * @param {string} dir
49
+ * @param {RegExp} pattern
50
+ * @returns {string[]}
51
+ */
52
+ function findFiles(dir, pattern) {
53
+ if (!exists(dir)) return [];
54
+ try {
55
+ return fs.readdirSync(dir)
56
+ .filter(f => pattern.test(f))
57
+ .map(f => path.join(dir, f));
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Parse Prisma schema file for model definitions
65
+ * @param {string} filePath - Path to schema.prisma
66
+ * @returns {DetectedModel[]}
67
+ */
68
+ function parsePrismaSchema(filePath) {
69
+ const content = readFile(filePath);
70
+ if (!content) return [];
71
+
72
+ const models = [];
73
+ const modelRegex = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
74
+ let match;
75
+
76
+ while ((match = modelRegex.exec(content)) !== null) {
77
+ const name = match[1];
78
+ const body = match[2];
79
+ const fields = [];
80
+
81
+ for (const line of body.split('\n')) {
82
+ const trimmed = line.trim();
83
+ // Skip empty lines, comments, and @@ annotations
84
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
85
+ // Extract field name (first word on the line)
86
+ const fieldMatch = trimmed.match(/^(\w+)\s+/);
87
+ if (fieldMatch) {
88
+ fields.push(fieldMatch[1]);
89
+ }
90
+ }
91
+
92
+ models.push({
93
+ name,
94
+ source: 'prisma/schema.prisma',
95
+ fields,
96
+ });
97
+ }
98
+
99
+ return models;
100
+ }
101
+
102
+ /**
103
+ * Parse TypeScript files for interface and type definitions
104
+ * @param {string[]} dirs - Directories to scan
105
+ * @param {string} projectPath - Project root for relative paths
106
+ * @returns {DetectedModel[]}
107
+ */
108
+ function parseTypeScriptTypes(dirs, projectPath) {
109
+ const models = [];
110
+ const filePattern = /\.(ts|tsx)$/;
111
+
112
+ for (const dir of dirs) {
113
+ const files = findFiles(dir, filePattern);
114
+
115
+ for (const filePath of files) {
116
+ const content = readFile(filePath);
117
+ if (!content) continue;
118
+
119
+ const relativePath = path.relative(projectPath, filePath);
120
+
121
+ // Match interface declarations
122
+ const interfaceRegex = /^(?:export\s+)?interface\s+(\w+)\s*(?:extends\s+[\w,\s<>]+)?\s*\{([^}]+)\}/gm;
123
+ let match;
124
+
125
+ while ((match = interfaceRegex.exec(content)) !== null) {
126
+ const name = match[1];
127
+ const body = match[2];
128
+ const fields = extractTsFields(body);
129
+ if (fields.length > 0) {
130
+ models.push({ name, source: relativePath, fields });
131
+ }
132
+ }
133
+
134
+ // Match type aliases with object shapes
135
+ const typeRegex = /^(?:export\s+)?type\s+(\w+)\s*=\s*\{([^}]+)\}/gm;
136
+ while ((match = typeRegex.exec(content)) !== null) {
137
+ const name = match[1];
138
+ const body = match[2];
139
+ const fields = extractTsFields(body);
140
+ if (fields.length > 0) {
141
+ models.push({ name, source: relativePath, fields });
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ return models;
148
+ }
149
+
150
+ /**
151
+ * Extract field names from a TypeScript object body
152
+ * @param {string} body
153
+ * @returns {string[]}
154
+ */
155
+ function extractTsFields(body) {
156
+ const fields = [];
157
+ for (const line of body.split('\n')) {
158
+ const trimmed = line.trim();
159
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;
160
+ // Match: fieldName: type or fieldName?: type
161
+ const fieldMatch = trimmed.match(/^(\w+)\s*\??:/);
162
+ if (fieldMatch) {
163
+ fields.push(fieldMatch[1]);
164
+ }
165
+ }
166
+ return fields;
167
+ }
168
+
169
+ /**
170
+ * Parse Drizzle schema files for table definitions
171
+ * @param {string[]} dirs - Directories to scan
172
+ * @param {string} projectPath - Project root for relative paths
173
+ * @returns {DetectedModel[]}
174
+ */
175
+ function parseDrizzleSchemas(dirs, projectPath) {
176
+ const models = [];
177
+ const filePattern = /schema.*\.(ts|js)$/;
178
+
179
+ for (const dir of dirs) {
180
+ const files = findFiles(dir, filePattern);
181
+
182
+ for (const filePath of files) {
183
+ const content = readFile(filePath);
184
+ if (!content) continue;
185
+
186
+ const relativePath = path.relative(projectPath, filePath);
187
+
188
+ // Match: export const tableName = pgTable('table_name', { ... })
189
+ const tableRegex = /export\s+const\s+(\w+)\s*=\s*(?:pgTable|mysqlTable|sqliteTable)\s*\(\s*['"](\w+)['"]\s*,\s*\{([^}]+)\}/g;
190
+ let match;
191
+
192
+ while ((match = tableRegex.exec(content)) !== null) {
193
+ // match[1] is the variable name (e.g., 'users'), match[2] is the SQL table name
194
+ const tableName = match[2];
195
+ const body = match[3];
196
+ const fields = [];
197
+
198
+ for (const line of body.split('\n')) {
199
+ const trimmed = line.trim();
200
+ if (!trimmed || trimmed.startsWith('//')) continue;
201
+ // Match: columnName: type(...)
202
+ const colMatch = trimmed.match(/^(\w+)\s*:/);
203
+ if (colMatch) {
204
+ fields.push(colMatch[1]);
205
+ }
206
+ }
207
+
208
+ // Use PascalCase version of table name as model name
209
+ const name = tableName
210
+ .split('_')
211
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
212
+ .join('');
213
+
214
+ models.push({ name, source: relativePath, fields });
215
+ }
216
+ }
217
+ }
218
+
219
+ return models;
220
+ }
221
+
222
+ /**
223
+ * Parse Convex schema file
224
+ * @param {string} filePath - Path to convex/schema.ts
225
+ * @param {string} projectPath - Project root
226
+ * @returns {DetectedModel[]}
227
+ */
228
+ function parseConvexSchema(filePath, projectPath) {
229
+ const content = readFile(filePath);
230
+ if (!content) return [];
231
+
232
+ const models = [];
233
+ const relativePath = path.relative(projectPath, filePath);
234
+
235
+ // Match: tableName: defineTable({ ... })
236
+ const tableRegex = /(\w+)\s*:\s*defineTable\s*\(\s*\{([^}]+)\}/g;
237
+ let match;
238
+
239
+ while ((match = tableRegex.exec(content)) !== null) {
240
+ const tableName = match[1];
241
+ const body = match[2];
242
+ const fields = [];
243
+
244
+ for (const line of body.split('\n')) {
245
+ const trimmed = line.trim();
246
+ if (!trimmed || trimmed.startsWith('//')) continue;
247
+ const fieldMatch = trimmed.match(/^(\w+)\s*:/);
248
+ if (fieldMatch) {
249
+ fields.push(fieldMatch[1]);
250
+ }
251
+ }
252
+
253
+ const name = tableName.charAt(0).toUpperCase() + tableName.slice(1);
254
+ models.push({ name, source: relativePath, fields });
255
+ }
256
+
257
+ return models;
258
+ }
259
+
260
+ /**
261
+ * Detect all models/types in a project
262
+ * @param {string} projectPath - Project root path
263
+ * @returns {{ hasModels: boolean, models: DetectedModel[] }}
264
+ */
265
+ function detect(projectPath = process.cwd()) {
266
+ const allModels = [];
267
+
268
+ // 1. Prisma
269
+ const prismaPath = path.join(projectPath, 'prisma', 'schema.prisma');
270
+ if (exists(prismaPath)) {
271
+ allModels.push(...parsePrismaSchema(prismaPath));
272
+ }
273
+
274
+ // 2. TypeScript types/interfaces
275
+ const tsDirs = [
276
+ path.join(projectPath, 'types'),
277
+ path.join(projectPath, 'src', 'types'),
278
+ path.join(projectPath, 'models'),
279
+ path.join(projectPath, 'src', 'models'),
280
+ ].filter(d => exists(d));
281
+
282
+ if (tsDirs.length > 0) {
283
+ allModels.push(...parseTypeScriptTypes(tsDirs, projectPath));
284
+ }
285
+
286
+ // 3. Drizzle
287
+ const drizzleDirs = [
288
+ path.join(projectPath, 'drizzle'),
289
+ path.join(projectPath, 'src', 'db'),
290
+ path.join(projectPath, 'db'),
291
+ ].filter(d => exists(d));
292
+
293
+ if (drizzleDirs.length > 0) {
294
+ allModels.push(...parseDrizzleSchemas(drizzleDirs, projectPath));
295
+ }
296
+
297
+ // 4. Convex
298
+ const convexSchemaTs = path.join(projectPath, 'convex', 'schema.ts');
299
+ const convexSchemaJs = path.join(projectPath, 'convex', 'schema.js');
300
+ if (exists(convexSchemaTs)) {
301
+ allModels.push(...parseConvexSchema(convexSchemaTs, projectPath));
302
+ } else if (exists(convexSchemaJs)) {
303
+ allModels.push(...parseConvexSchema(convexSchemaJs, projectPath));
304
+ }
305
+
306
+ return {
307
+ hasModels: allModels.length > 0,
308
+ models: allModels,
309
+ };
310
+ }
311
+
312
+ module.exports = {
313
+ detect,
314
+ parsePrismaSchema,
315
+ parseTypeScriptTypes,
316
+ parseDrizzleSchemas,
317
+ parseConvexSchema,
318
+ };