@l4yercak3/cli 1.2.21 → 1.3.0

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,169 @@
1
+ /**
2
+ * Sync Command
3
+ * Re-detects project structure and syncs the manifest with the L4YERCAK3 platform
4
+ *
5
+ * Flow:
6
+ * 1. Re-run all detectors (models, routes, pages)
7
+ * 2. Re-compute suggested mappings
8
+ * 3. Diff old manifest vs new detection results
9
+ * 4. Write updated .l4yercak3.json
10
+ * 5. Push manifest to platform if connected
11
+ */
12
+
13
+ const configManager = require('../config/config-manager');
14
+ const backendClient = require('../api/backend-client');
15
+ const projectDetector = require('../detectors');
16
+ const pageDetector = require('../detectors/page-detector');
17
+ const { suggestMappings } = require('../detectors/mapping-suggestor');
18
+ const manifestGenerator = require('../generators/manifest-generator');
19
+ const { requireAuth } = require('../utils/init-helpers');
20
+ const chalk = require('chalk');
21
+
22
+ async function handleSync() {
23
+ requireAuth(configManager);
24
+
25
+ console.log(chalk.cyan(' 🔄 Syncing project structure...\n'));
26
+
27
+ try {
28
+ const projectPath = process.cwd();
29
+
30
+ // Step 1: Load existing manifest
31
+ const existingManifest = manifestGenerator.loadManifest(projectPath);
32
+ if (!existingManifest) {
33
+ console.log(chalk.yellow(' ⚠️ No .l4yercak3.json manifest found.'));
34
+ console.log(chalk.gray(' Run "l4yercak3 init" first to scan your project.\n'));
35
+ process.exit(1);
36
+ }
37
+
38
+ console.log(chalk.gray(' Loading existing manifest...'));
39
+ console.log(chalk.gray(` Framework: ${existingManifest.framework}`));
40
+ console.log(chalk.gray(` Models: ${existingManifest.detectedModels?.length || 0}`));
41
+ console.log(chalk.gray(` Routes: ${existingManifest.detectedRoutes?.length || 0}`));
42
+ console.log(chalk.gray(` Last synced: ${existingManifest.lastSyncedAt || 'never'}\n`));
43
+
44
+ // Step 2: Re-run detection
45
+ console.log(chalk.gray(' 🔍 Re-scanning project...\n'));
46
+ const detection = projectDetector.detect(projectPath);
47
+
48
+ // Step 3: Re-scan routes with methods
49
+ const detectedRoutes = pageDetector.detect(
50
+ projectPath,
51
+ detection.framework.type,
52
+ detection.framework.metadata || {}
53
+ );
54
+
55
+ // Step 4: Re-compute mappings
56
+ const models = detection.models ? detection.models.models : [];
57
+ const mappings = suggestMappings(models);
58
+
59
+ // Step 5: Build new manifest data for diff
60
+ const newManifestData = {
61
+ version: existingManifest.version || '1.0.0',
62
+ framework: detection.framework.type || existingManifest.framework,
63
+ routerType: detection.framework.metadata?.routerType || existingManifest.routerType,
64
+ typescript: detection.framework.metadata?.hasTypeScript || existingManifest.typescript,
65
+ database: detection.database?.primary?.type || existingManifest.database,
66
+ detectedModels: models.map(m => ({
67
+ name: m.name,
68
+ source: m.source,
69
+ fields: m.fields || [],
70
+ })),
71
+ detectedRoutes: detectedRoutes
72
+ .filter(r => r.pageType === 'api_route')
73
+ .map(r => ({
74
+ path: r.path,
75
+ methods: r.methods || ['GET', 'POST'],
76
+ })),
77
+ suggestedMappings: mappings.map(m => ({
78
+ localModel: m.localModel,
79
+ platformType: m.platformType,
80
+ confidence: m.confidence,
81
+ })),
82
+ };
83
+
84
+ // Step 6: Compute diff
85
+ const diff = manifestGenerator.diff(existingManifest, newManifestData);
86
+
87
+ if (!diff.hasChanges) {
88
+ console.log(chalk.green(' ✅ No changes detected. Your manifest is up to date.\n'));
89
+ } else {
90
+ console.log(chalk.cyan(' 📊 Changes detected:\n'));
91
+
92
+ if (diff.modelsAdded.length > 0) {
93
+ console.log(chalk.green(` + ${diff.modelsAdded.length} new model(s)`));
94
+ for (const m of diff.modelsAdded) {
95
+ console.log(chalk.gray(` • ${m.name} (${m.source})`));
96
+ }
97
+ }
98
+
99
+ if (diff.modelsRemoved.length > 0) {
100
+ console.log(chalk.red(` - ${diff.modelsRemoved.length} removed model(s)`));
101
+ for (const m of diff.modelsRemoved) {
102
+ console.log(chalk.gray(` • ${m.name} (${m.source})`));
103
+ }
104
+ }
105
+
106
+ if (diff.routesAdded.length > 0) {
107
+ console.log(chalk.green(` + ${diff.routesAdded.length} new route(s)`));
108
+ for (const r of diff.routesAdded) {
109
+ console.log(chalk.gray(` • ${r.path} [${(r.methods || []).join(', ')}]`));
110
+ }
111
+ }
112
+
113
+ if (diff.routesRemoved.length > 0) {
114
+ console.log(chalk.red(` - ${diff.routesRemoved.length} removed route(s)`));
115
+ for (const r of diff.routesRemoved) {
116
+ console.log(chalk.gray(` • ${r.path}`));
117
+ }
118
+ }
119
+
120
+ console.log('');
121
+ }
122
+
123
+ // Step 7: Write updated manifest
124
+ manifestGenerator.generate({
125
+ projectPath,
126
+ detection,
127
+ models,
128
+ routes: detectedRoutes,
129
+ mappings,
130
+ });
131
+ console.log(chalk.green(` ✅ Manifest updated: .l4yercak3.json`));
132
+
133
+ // Step 8: Summary
134
+ console.log(chalk.gray(` Models: ${newManifestData.detectedModels.length}`));
135
+ console.log(chalk.gray(` Routes: ${newManifestData.detectedRoutes.length}`));
136
+ console.log(chalk.gray(` Mappings: ${newManifestData.suggestedMappings.length}\n`));
137
+
138
+ // Step 9: Push to platform if connected
139
+ const projectConfig = configManager.getProjectConfig(projectPath);
140
+ if (projectConfig && projectConfig.applicationId) {
141
+ console.log(chalk.gray(' Syncing to platform...'));
142
+ try {
143
+ const updatedManifest = manifestGenerator.loadManifest(projectPath);
144
+ await backendClient.syncManifest(projectConfig.applicationId, updatedManifest);
145
+ console.log(chalk.green(' ✅ Manifest synced to L4YERCAK3 platform\n'));
146
+ } catch (syncError) {
147
+ console.log(chalk.yellow(` ⚠️ Could not sync to platform: ${syncError.message}`));
148
+ console.log(chalk.gray(' The manifest was updated locally. Platform sync may not be available yet.\n'));
149
+ }
150
+ } else {
151
+ console.log(chalk.gray(' ℹ️ Not connected to platform. Run "l4yercak3 connect" to enable remote sync.\n'));
152
+ }
153
+
154
+ console.log(chalk.cyan(' 🎉 Sync complete!\n'));
155
+
156
+ } catch (error) {
157
+ console.error(chalk.red(`\n ❌ Error: ${error.message}\n`));
158
+ if (process.env.L4YERCAK3_DEBUG && error.stack) {
159
+ console.error(chalk.gray(error.stack));
160
+ }
161
+ process.exit(1);
162
+ }
163
+ }
164
+
165
+ module.exports = {
166
+ command: 'sync',
167
+ description: 'Re-detect project structure and sync with L4YERCAK3',
168
+ handler: handleSync,
169
+ };
@@ -11,6 +11,7 @@ const githubDetector = require('./github-detector');
11
11
  const apiClientDetector = require('./api-client-detector');
12
12
  const oauthDetector = require('./oauth-detector');
13
13
  const databaseDetector = require('./database-detector');
14
+ const modelDetector = require('./model-detector');
14
15
 
15
16
  class ProjectDetector {
16
17
  /**
@@ -28,6 +29,7 @@ class ProjectDetector {
28
29
  const apiClientInfo = apiClientDetector.detect(projectPath);
29
30
  const oauthInfo = oauthDetector.detect(projectPath);
30
31
  const databaseInfo = databaseDetector.detect(projectPath);
32
+ const modelInfo = modelDetector.detect(projectPath);
31
33
 
32
34
  // Get detector instance if we have a match
33
35
  const detector = frameworkDetection.detected
@@ -49,6 +51,7 @@ class ProjectDetector {
49
51
  apiClient: apiClientInfo,
50
52
  oauth: oauthInfo,
51
53
  database: databaseInfo,
54
+ models: modelInfo,
52
55
 
53
56
  // Raw detection results (for debugging)
54
57
  _raw: {
@@ -60,6 +63,16 @@ class ProjectDetector {
60
63
  };
61
64
  }
62
65
 
66
+ /**
67
+ * Detect models/types in a project
68
+ *
69
+ * @param {string} projectPath - Path to project directory
70
+ * @returns {object} Model detection results
71
+ */
72
+ detectModels(projectPath = process.cwd()) {
73
+ return modelDetector.detect(projectPath);
74
+ }
75
+
63
76
  /**
64
77
  * Detect database configuration in a project
65
78
  *
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Mapping Suggestor
3
+ * Suggests mappings between local data models and L4YERCAK3 platform types
4
+ * Pure logic module — no I/O
5
+ */
6
+
7
+ /**
8
+ * Platform types supported by L4YERCAK3
9
+ */
10
+ const PLATFORM_TYPES = [
11
+ 'contact',
12
+ 'booking',
13
+ 'event',
14
+ 'product',
15
+ 'invoice',
16
+ 'project',
17
+ 'form',
18
+ 'certificate',
19
+ 'benefit',
20
+ ];
21
+
22
+ /**
23
+ * Mapping rules: keyword patterns → platform type + base confidence
24
+ * Patterns are matched against lowercase model names
25
+ */
26
+ const MAPPING_RULES = [
27
+ {
28
+ patterns: ['user', 'customer', 'client', 'member', 'contact', 'person', 'lead', 'subscriber', 'account', 'profile'],
29
+ type: 'contact',
30
+ confidence: 90,
31
+ },
32
+ {
33
+ patterns: ['appointment', 'booking', 'reservation', 'schedule', 'slot'],
34
+ type: 'booking',
35
+ confidence: 85,
36
+ },
37
+ {
38
+ patterns: ['event', 'meeting', 'conference', 'webinar', 'session', 'workshop'],
39
+ type: 'event',
40
+ confidence: 85,
41
+ },
42
+ {
43
+ patterns: ['product', 'item', 'sku', 'merchandise', 'listing', 'catalog'],
44
+ type: 'product',
45
+ confidence: 85,
46
+ },
47
+ {
48
+ patterns: ['invoice', 'bill', 'receipt', 'charge', 'payment', 'order', 'transaction'],
49
+ type: 'invoice',
50
+ confidence: 80,
51
+ },
52
+ {
53
+ patterns: ['project', 'task', 'ticket', 'issue', 'sprint', 'milestone'],
54
+ type: 'project',
55
+ confidence: 80,
56
+ },
57
+ {
58
+ patterns: ['form', 'survey', 'questionnaire', 'submission', 'response'],
59
+ type: 'form',
60
+ confidence: 80,
61
+ },
62
+ {
63
+ patterns: ['certificate', 'credential', 'badge', 'diploma', 'cme', 'license'],
64
+ type: 'certificate',
65
+ confidence: 75,
66
+ },
67
+ {
68
+ patterns: ['benefit', 'claim', 'commission', 'payout', 'reward', 'bonus'],
69
+ type: 'benefit',
70
+ confidence: 75,
71
+ },
72
+ ];
73
+
74
+ /**
75
+ * Suggest platform type mappings for detected models
76
+ * @param {Array<{name: string, source: string, fields: string[]}>} models
77
+ * @returns {Array<{localModel: string, platformType: string, confidence: number}>}
78
+ */
79
+ function suggestMappings(models) {
80
+ if (!models || models.length === 0) return [];
81
+
82
+ const mappings = [];
83
+
84
+ for (const model of models) {
85
+ const lowerName = model.name.toLowerCase();
86
+ let bestMatch = null;
87
+
88
+ for (const rule of MAPPING_RULES) {
89
+ for (const pattern of rule.patterns) {
90
+ if (lowerName.includes(pattern)) {
91
+ // Exact match gets higher confidence
92
+ const isExact = lowerName === pattern;
93
+ const confidence = isExact ? Math.min(rule.confidence + 5, 99) : rule.confidence;
94
+
95
+ if (!bestMatch || confidence > bestMatch.confidence) {
96
+ bestMatch = {
97
+ localModel: model.name,
98
+ platformType: rule.type,
99
+ confidence,
100
+ };
101
+ }
102
+ break; // Only need first pattern match per rule
103
+ }
104
+ }
105
+ }
106
+
107
+ if (bestMatch) {
108
+ mappings.push(bestMatch);
109
+ }
110
+ }
111
+
112
+ return mappings;
113
+ }
114
+
115
+ module.exports = {
116
+ suggestMappings,
117
+ PLATFORM_TYPES,
118
+ MAPPING_RULES,
119
+ };
@@ -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
+ };