@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.
- package/.claude/settings.local.json +8 -1
- package/bin/cli.js +25 -0
- package/docs/CLI_PAGE_DETECTION_REQUIREMENTS.md +519 -0
- package/package.json +1 -1
- package/src/api/backend-client.js +149 -0
- package/src/commands/connect.js +243 -0
- package/src/commands/pages.js +317 -0
- package/src/commands/scaffold.js +409 -0
- package/src/commands/spread.js +89 -190
- package/src/commands/sync.js +169 -0
- package/src/detectors/index.js +13 -0
- package/src/detectors/mapping-suggestor.js +119 -0
- package/src/detectors/model-detector.js +318 -0
- package/src/detectors/page-detector.js +480 -0
- package/src/generators/manifest-generator.js +154 -0
- package/src/utils/init-helpers.js +243 -0
- package/tests/page-detector.test.js +371 -0
|
@@ -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
|
+
};
|