@l4yercak3/cli 1.2.20 → 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,480 @@
1
+ /**
2
+ * Page/Screen Detector
3
+ * Detects pages in Next.js projects and screens in Expo/React Native projects
4
+ * Used for syncing application structure with L4YERCAK3 backend
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ class PageDetector {
11
+ /**
12
+ * Detect all pages/screens in a project
13
+ * @param {string} projectPath - Path to project root
14
+ * @param {string} frameworkType - 'nextjs', 'expo', or 'react-native'
15
+ * @param {object} frameworkMetadata - Additional framework info (routerType, etc.)
16
+ * @returns {Array<{path: string, name: string, pageType: string, filePath: string}>}
17
+ */
18
+ detect(projectPath, frameworkType, frameworkMetadata = {}) {
19
+ switch (frameworkType) {
20
+ case 'nextjs':
21
+ return this.detectNextJsPages(projectPath, frameworkMetadata);
22
+ case 'expo':
23
+ case 'react-native':
24
+ return this.detectExpoScreens(projectPath, frameworkMetadata);
25
+ default:
26
+ return [];
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Detect Next.js pages (supports both App Router and Pages Router)
32
+ */
33
+ detectNextJsPages(projectPath, metadata) {
34
+ const routerType = metadata.routerType || 'pages';
35
+ const pages = [];
36
+
37
+ if (routerType === 'app') {
38
+ // App Router: look for page.tsx/page.js files in app/ or src/app/
39
+ const appDirs = [
40
+ path.join(projectPath, 'app'),
41
+ path.join(projectPath, 'src', 'app'),
42
+ ];
43
+
44
+ for (const appDir of appDirs) {
45
+ if (fs.existsSync(appDir)) {
46
+ this.scanAppRouterDirectory(appDir, '', pages, appDir);
47
+ break; // Only use the first found app directory
48
+ }
49
+ }
50
+ } else {
51
+ // Pages Router: look for files in pages/ or src/pages/
52
+ const pagesDirs = [
53
+ path.join(projectPath, 'pages'),
54
+ path.join(projectPath, 'src', 'pages'),
55
+ ];
56
+
57
+ for (const pagesDir of pagesDirs) {
58
+ if (fs.existsSync(pagesDir)) {
59
+ this.scanPagesRouterDirectory(pagesDir, '', pages, pagesDir);
60
+ break;
61
+ }
62
+ }
63
+ }
64
+
65
+ return pages;
66
+ }
67
+
68
+ /**
69
+ * Scan App Router directory structure
70
+ * In App Router, pages are defined by page.tsx/page.js files
71
+ * API routes are defined by route.tsx/route.js files
72
+ */
73
+ scanAppRouterDirectory(dir, routePath, pages, baseDir) {
74
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
75
+
76
+ for (const entry of entries) {
77
+ const fullPath = path.join(dir, entry.name);
78
+
79
+ if (entry.isDirectory()) {
80
+ // Skip special directories
81
+ if (entry.name.startsWith('_') || entry.name.startsWith('.')) {
82
+ continue;
83
+ }
84
+
85
+ // Handle route groups (parentheses) - they don't add to URL
86
+ let newRoutePath = routePath;
87
+ if (!entry.name.startsWith('(') || !entry.name.endsWith(')')) {
88
+ // Handle dynamic segments [param] and catch-all [...param]
89
+ let segment = entry.name;
90
+ if (segment.startsWith('[') && segment.endsWith(']')) {
91
+ // Keep dynamic segment notation
92
+ newRoutePath = `${routePath}/${segment}`;
93
+ } else {
94
+ newRoutePath = `${routePath}/${segment}`;
95
+ }
96
+ }
97
+
98
+ this.scanAppRouterDirectory(fullPath, newRoutePath, pages, baseDir);
99
+ } else if (entry.isFile()) {
100
+ const ext = path.extname(entry.name);
101
+ const baseName = path.basename(entry.name, ext);
102
+
103
+ // Check for page files
104
+ if (baseName === 'page' && ['.tsx', '.ts', '.jsx', '.js'].includes(ext)) {
105
+ const pagePath = routePath || '/';
106
+ pages.push({
107
+ path: pagePath,
108
+ name: this.generatePageName(pagePath),
109
+ pageType: this.detectPageType(pagePath),
110
+ filePath: path.relative(baseDir, fullPath),
111
+ });
112
+ }
113
+
114
+ // Check for API route files
115
+ if (baseName === 'route' && ['.tsx', '.ts', '.jsx', '.js'].includes(ext)) {
116
+ const apiPath = routePath || '/';
117
+ const methods = this.detectHttpMethods(fullPath, 'app');
118
+ pages.push({
119
+ path: apiPath,
120
+ name: this.generatePageName(apiPath, true),
121
+ pageType: 'api_route',
122
+ filePath: path.relative(baseDir, fullPath),
123
+ methods,
124
+ });
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Detect HTTP methods exported from a route file
132
+ * @param {string} filePath - Absolute path to the route file
133
+ * @param {string} routerType - 'app' or 'pages'
134
+ * @returns {string[]} Array of HTTP methods (e.g., ['GET', 'POST'])
135
+ */
136
+ detectHttpMethods(filePath, routerType) {
137
+ let content;
138
+ try {
139
+ content = fs.readFileSync(filePath, 'utf8');
140
+ } catch {
141
+ return ['GET', 'POST']; // Default fallback
142
+ }
143
+
144
+ const methods = new Set();
145
+
146
+ if (routerType === 'app') {
147
+ // App Router: look for exported handler functions
148
+ // export async function GET(...) or export const GET = ...
149
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
150
+ for (const method of HTTP_METHODS) {
151
+ const funcPattern = new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\b`);
152
+ const constPattern = new RegExp(`export\\s+const\\s+${method}\\s*=`);
153
+ if (funcPattern.test(content) || constPattern.test(content)) {
154
+ methods.add(method);
155
+ }
156
+ }
157
+ } else {
158
+ // Pages Router: look for req.method checks
159
+ const methodCheckRegex = /req\.method\s*===?\s*['"](\w+)['"]/g;
160
+ let match;
161
+ while ((match = methodCheckRegex.exec(content)) !== null) {
162
+ methods.add(match[1].toUpperCase());
163
+ }
164
+
165
+ // Also check switch/case patterns: case 'GET':
166
+ const caseRegex = /case\s+['"](\w+)['"]\s*:/g;
167
+ while ((match = caseRegex.exec(content)) !== null) {
168
+ const val = match[1].toUpperCase();
169
+ if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(val)) {
170
+ methods.add(val);
171
+ }
172
+ }
173
+ }
174
+
175
+ // Default to GET/POST if no methods detected
176
+ if (methods.size === 0) {
177
+ return ['GET', 'POST'];
178
+ }
179
+
180
+ return Array.from(methods).sort();
181
+ }
182
+
183
+ /**
184
+ * Scan Pages Router directory structure
185
+ * In Pages Router, each file is a page, and files in api/ are API routes
186
+ */
187
+ scanPagesRouterDirectory(dir, routePath, pages, baseDir) {
188
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
189
+
190
+ for (const entry of entries) {
191
+ const fullPath = path.join(dir, entry.name);
192
+
193
+ if (entry.isDirectory()) {
194
+ // Skip special directories
195
+ if (entry.name.startsWith('_') || entry.name.startsWith('.')) {
196
+ continue;
197
+ }
198
+
199
+ const newRoutePath = `${routePath}/${entry.name}`;
200
+ this.scanPagesRouterDirectory(fullPath, newRoutePath, pages, baseDir);
201
+ } else if (entry.isFile()) {
202
+ const ext = path.extname(entry.name);
203
+ const baseName = path.basename(entry.name, ext);
204
+
205
+ // Only process TypeScript/JavaScript files
206
+ if (!['.tsx', '.ts', '.jsx', '.js'].includes(ext)) {
207
+ continue;
208
+ }
209
+
210
+ // Skip special files
211
+ if (baseName.startsWith('_')) {
212
+ continue;
213
+ }
214
+
215
+ // Determine the route path
216
+ let pagePath;
217
+ if (baseName === 'index') {
218
+ pagePath = routePath || '/';
219
+ } else {
220
+ pagePath = `${routePath}/${baseName}`;
221
+ }
222
+
223
+ // Check if this is an API route
224
+ const isApiRoute = routePath.startsWith('/api') || pagePath.startsWith('/api');
225
+
226
+ const entry_data = {
227
+ path: pagePath,
228
+ name: this.generatePageName(pagePath, isApiRoute),
229
+ pageType: isApiRoute ? 'api_route' : this.detectPageType(pagePath),
230
+ filePath: path.relative(baseDir, fullPath),
231
+ };
232
+
233
+ if (isApiRoute) {
234
+ entry_data.methods = this.detectHttpMethods(fullPath, 'pages');
235
+ }
236
+
237
+ pages.push(entry_data);
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Detect Expo/React Native screens
244
+ * Supports expo-router (file-based) and react-navigation
245
+ */
246
+ detectExpoScreens(projectPath, metadata) {
247
+ const screens = [];
248
+ const routerType = metadata.routerType || 'native';
249
+
250
+ if (routerType === 'expo-router') {
251
+ // Expo Router uses file-based routing similar to Next.js App Router
252
+ const appDirs = [
253
+ path.join(projectPath, 'app'),
254
+ path.join(projectPath, 'src', 'app'),
255
+ ];
256
+
257
+ for (const appDir of appDirs) {
258
+ if (fs.existsSync(appDir)) {
259
+ this.scanExpoRouterDirectory(appDir, '', screens, appDir);
260
+ break;
261
+ }
262
+ }
263
+ } else {
264
+ // React Navigation or native - scan for screen files
265
+ const screenDirs = [
266
+ path.join(projectPath, 'src', 'screens'),
267
+ path.join(projectPath, 'screens'),
268
+ path.join(projectPath, 'src', 'views'),
269
+ path.join(projectPath, 'views'),
270
+ path.join(projectPath, 'app', 'screens'),
271
+ ];
272
+
273
+ for (const screenDir of screenDirs) {
274
+ if (fs.existsSync(screenDir)) {
275
+ this.scanScreensDirectory(screenDir, screens, screenDir);
276
+ }
277
+ }
278
+ }
279
+
280
+ return screens;
281
+ }
282
+
283
+ /**
284
+ * Scan Expo Router directory structure
285
+ * Similar to Next.js App Router but with some differences
286
+ */
287
+ scanExpoRouterDirectory(dir, routePath, screens, baseDir) {
288
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
289
+
290
+ for (const entry of entries) {
291
+ const fullPath = path.join(dir, entry.name);
292
+
293
+ if (entry.isDirectory()) {
294
+ // Skip special directories
295
+ if (entry.name.startsWith('_') || entry.name.startsWith('.')) {
296
+ continue;
297
+ }
298
+
299
+ // Handle route groups (parentheses)
300
+ let newRoutePath = routePath;
301
+ if (!entry.name.startsWith('(') || !entry.name.endsWith(')')) {
302
+ newRoutePath = `${routePath}/${entry.name}`;
303
+ }
304
+
305
+ this.scanExpoRouterDirectory(fullPath, newRoutePath, screens, baseDir);
306
+ } else if (entry.isFile()) {
307
+ const ext = path.extname(entry.name);
308
+ const baseName = path.basename(entry.name, ext);
309
+
310
+ // Skip non-JS/TS files and special files
311
+ if (!['.tsx', '.ts', '.jsx', '.js'].includes(ext)) {
312
+ continue;
313
+ }
314
+ if (baseName.startsWith('_')) {
315
+ continue;
316
+ }
317
+
318
+ // Determine the route path
319
+ let screenPath;
320
+ if (baseName === 'index') {
321
+ screenPath = routePath || '/';
322
+ } else if (baseName === '+not-found') {
323
+ screenPath = '/*'; // Catch-all not found
324
+ } else if (baseName.startsWith('+')) {
325
+ // Skip Expo Router special files like +html.tsx, +native-intent.tsx
326
+ continue;
327
+ } else {
328
+ screenPath = `${routePath}/${baseName}`;
329
+ }
330
+
331
+ screens.push({
332
+ path: screenPath,
333
+ name: this.generatePageName(screenPath),
334
+ pageType: this.detectPageType(screenPath),
335
+ filePath: path.relative(baseDir, fullPath),
336
+ });
337
+ }
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Scan traditional screens directory for React Navigation setup
343
+ */
344
+ scanScreensDirectory(dir, screens, baseDir) {
345
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
346
+
347
+ for (const entry of entries) {
348
+ const fullPath = path.join(dir, entry.name);
349
+
350
+ if (entry.isDirectory()) {
351
+ // Recurse into subdirectories
352
+ this.scanScreensDirectory(fullPath, screens, baseDir);
353
+ } else if (entry.isFile()) {
354
+ const ext = path.extname(entry.name);
355
+ const baseName = path.basename(entry.name, ext);
356
+
357
+ // Only process screen files
358
+ if (!['.tsx', '.ts', '.jsx', '.js'].includes(ext)) {
359
+ continue;
360
+ }
361
+
362
+ // Skip index files and non-screen files
363
+ if (baseName === 'index' || baseName.startsWith('_')) {
364
+ continue;
365
+ }
366
+
367
+ // Try to extract a meaningful path from the file name
368
+ // e.g., HomeScreen.tsx -> /home, ProfileScreen.tsx -> /profile
369
+ let screenPath = this.fileNameToPath(baseName);
370
+
371
+ screens.push({
372
+ path: screenPath,
373
+ name: this.generatePageName(screenPath),
374
+ pageType: 'static',
375
+ filePath: path.relative(baseDir, fullPath),
376
+ });
377
+ }
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Convert a file name to a route path
383
+ * e.g., "HomeScreen" -> "/home", "UserProfileScreen" -> "/user-profile"
384
+ */
385
+ fileNameToPath(fileName) {
386
+ // Remove common suffixes
387
+ let name = fileName
388
+ .replace(/Screen$/, '')
389
+ .replace(/View$/, '')
390
+ .replace(/Page$/, '');
391
+
392
+ // Convert camelCase/PascalCase to kebab-case
393
+ name = name
394
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
395
+ .toLowerCase();
396
+
397
+ // Handle "Home" or "Index" specially
398
+ if (name === 'home' || name === 'index' || name === 'main') {
399
+ return '/';
400
+ }
401
+
402
+ return `/${name}`;
403
+ }
404
+
405
+ /**
406
+ * Generate a human-readable name from a path
407
+ */
408
+ generatePageName(routePath, isApiRoute = false) {
409
+ if (routePath === '/') {
410
+ return isApiRoute ? 'API: Root' : 'Home';
411
+ }
412
+
413
+ // Handle catch-all not found (Expo +not-found)
414
+ if (routePath === '/*') {
415
+ return 'Not Found';
416
+ }
417
+
418
+ // Remove leading slash and split
419
+ const segments = routePath.slice(1).split('/');
420
+
421
+ // Handle API routes
422
+ if (isApiRoute || segments[0] === 'api') {
423
+ const apiSegments = segments.filter(s => s !== 'api');
424
+ if (apiSegments.length === 0) {
425
+ return 'API: Root';
426
+ }
427
+ const name = this.formatSegment(apiSegments[apiSegments.length - 1]);
428
+ return `API: ${name}`;
429
+ }
430
+
431
+ // Get the last segment for the name
432
+ const lastSegment = segments[segments.length - 1];
433
+
434
+ // Check if it's a dynamic segment
435
+ if (lastSegment.startsWith('[') && lastSegment.endsWith(']')) {
436
+ // Use the parent segment + "Detail"
437
+ if (segments.length > 1) {
438
+ const parentSegment = segments[segments.length - 2];
439
+ return `${this.formatSegment(parentSegment)} Detail`;
440
+ }
441
+ return 'Detail';
442
+ }
443
+
444
+ // Check for catch-all
445
+ if (lastSegment.startsWith('[...')) {
446
+ return 'Catch All';
447
+ }
448
+
449
+ return this.formatSegment(lastSegment);
450
+ }
451
+
452
+ /**
453
+ * Format a URL segment into a readable name
454
+ */
455
+ formatSegment(segment) {
456
+ // Handle dynamic segments
457
+ if (segment.startsWith('[') && segment.endsWith(']')) {
458
+ segment = segment.slice(1, -1);
459
+ }
460
+
461
+ // Convert kebab-case to Title Case
462
+ return segment
463
+ .split('-')
464
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
465
+ .join(' ');
466
+ }
467
+
468
+ /**
469
+ * Detect page type based on path
470
+ */
471
+ detectPageType(routePath) {
472
+ // Check for dynamic segments
473
+ if (routePath.includes('[') && routePath.includes(']')) {
474
+ return 'dynamic';
475
+ }
476
+ return 'static';
477
+ }
478
+ }
479
+
480
+ module.exports = new PageDetector();
@@ -28,10 +28,10 @@ const sortedDetectors = [...detectors].sort((a, b) => b.priority - a.priority);
28
28
 
29
29
  /**
30
30
  * Detect project type
31
- *
31
+ *
32
32
  * Runs all detectors in priority order and returns the first match
33
33
  * with confidence > 0.8, or all results if no high-confidence match.
34
- *
34
+ *
35
35
  * @param {string} projectPath - Path to project directory
36
36
  * @returns {object} Detection results
37
37
  */
@@ -43,11 +43,24 @@ function detectProjectType(projectPath = process.cwd()) {
43
43
  allResults: [], // All detector results (for debugging)
44
44
  };
45
45
 
46
+ const debug = process.env.L4YERCAK3_DEBUG;
47
+
48
+ if (debug) {
49
+ console.log('\n[DEBUG] Registry: Running detectors in priority order:');
50
+ }
51
+
46
52
  // Run all detectors
47
53
  for (const detector of sortedDetectors) {
48
54
  try {
49
55
  const result = detector.detect(projectPath);
50
-
56
+
57
+ if (debug) {
58
+ console.log(` [${detector.name}] priority=${detector.priority}, detected=${result.detected}, confidence=${result.confidence}`);
59
+ if (result.metadata) {
60
+ console.log(` metadata: ${JSON.stringify(result.metadata)}`);
61
+ }
62
+ }
63
+
51
64
  results.allResults.push({
52
65
  detector: detector.name,
53
66
  priority: detector.priority,
@@ -57,9 +70,14 @@ function detectProjectType(projectPath = process.cwd()) {
57
70
  // If this detector found a match with high confidence, use it
58
71
  if (result.detected && result.confidence > 0.8) {
59
72
  if (result.confidence > results.confidence) {
73
+ if (debug) {
74
+ console.log(` → Selected as best match (confidence ${result.confidence} > ${results.confidence})`);
75
+ }
60
76
  results.detected = detector.name;
61
77
  results.confidence = result.confidence;
62
78
  results.metadata = result.metadata;
79
+ } else if (debug) {
80
+ console.log(` → Skipped (confidence ${result.confidence} <= ${results.confidence})`);
63
81
  }
64
82
  }
65
83
  } catch (error) {
@@ -68,6 +86,10 @@ function detectProjectType(projectPath = process.cwd()) {
68
86
  }
69
87
  }
70
88
 
89
+ if (debug) {
90
+ console.log(`\n[DEBUG] Registry: Final result: ${results.detected} (confidence: ${results.confidence})`);
91
+ }
92
+
71
93
  return results;
72
94
  }
73
95
 
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Manifest Generator
3
+ * Generates and manages the .l4yercak3.json project manifest file
4
+ *
5
+ * The manifest captures the project's structure (framework, models, routes)
6
+ * and is used by the platform to understand and integrate with the app.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const MANIFEST_FILENAME = '.l4yercak3.json';
13
+ const MANIFEST_VERSION = '1.0.0';
14
+
15
+ class ManifestGenerator {
16
+ /**
17
+ * Generate or update the .l4yercak3.json manifest
18
+ * @param {object} options
19
+ * @param {string} options.projectPath - Project root directory
20
+ * @param {object} options.detection - Detection results from ProjectDetector
21
+ * @param {Array} options.models - Detected models from model-detector
22
+ * @param {Array} options.routes - Detected routes from page-detector (with methods)
23
+ * @param {Array} options.mappings - Suggested mappings from mapping-suggestor
24
+ * @returns {string} Path to the generated manifest file
25
+ */
26
+ generate(options) {
27
+ const { projectPath, detection, models, routes, mappings } = options;
28
+ const manifestPath = path.join(projectPath, MANIFEST_FILENAME);
29
+
30
+ // Read existing manifest to preserve user overrides
31
+ const existing = this.loadManifest(projectPath);
32
+
33
+ const manifest = {
34
+ version: MANIFEST_VERSION,
35
+ framework: detection?.framework?.type || 'unknown',
36
+ routerType: detection?.framework?.metadata?.routerType || null,
37
+ typescript: detection?.framework?.metadata?.hasTypeScript || false,
38
+ database: detection?.database?.primary?.type || null,
39
+ detectedModels: (models || []).map(m => ({
40
+ name: m.name,
41
+ source: m.source,
42
+ fields: m.fields || [],
43
+ })),
44
+ detectedRoutes: (routes || [])
45
+ .filter(r => r.pageType === 'api_route')
46
+ .map(r => ({
47
+ path: r.path,
48
+ methods: r.methods || ['GET', 'POST'],
49
+ })),
50
+ suggestedMappings: (mappings || []).map(m => ({
51
+ localModel: m.localModel,
52
+ platformType: m.platformType,
53
+ confidence: m.confidence,
54
+ })),
55
+ lastSyncedAt: new Date().toISOString(),
56
+ };
57
+
58
+ // Preserve user overrides from existing manifest
59
+ if (existing && existing.userOverrides) {
60
+ manifest.userOverrides = existing.userOverrides;
61
+ }
62
+
63
+ // Preserve confirmed mappings from existing manifest
64
+ if (existing && existing.confirmedMappings) {
65
+ manifest.confirmedMappings = existing.confirmedMappings;
66
+ }
67
+
68
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
69
+ return manifestPath;
70
+ }
71
+
72
+ /**
73
+ * Load an existing manifest from a project
74
+ * @param {string} projectPath - Project root directory
75
+ * @returns {object|null} Parsed manifest or null if not found
76
+ */
77
+ loadManifest(projectPath) {
78
+ const manifestPath = path.join(projectPath, MANIFEST_FILENAME);
79
+ try {
80
+ if (!fs.existsSync(manifestPath)) {
81
+ return null;
82
+ }
83
+ const content = fs.readFileSync(manifestPath, 'utf8');
84
+ return JSON.parse(content);
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Check if a manifest exists in the project
92
+ * @param {string} projectPath
93
+ * @returns {boolean}
94
+ */
95
+ hasManifest(projectPath) {
96
+ return fs.existsSync(path.join(projectPath, MANIFEST_FILENAME));
97
+ }
98
+
99
+ /**
100
+ * Compute a diff between old and new manifest data
101
+ * @param {object} oldManifest - Previous manifest
102
+ * @param {object} newManifest - New manifest data
103
+ * @returns {object} Diff summary
104
+ */
105
+ diff(oldManifest, newManifest) {
106
+ const result = {
107
+ modelsAdded: [],
108
+ modelsRemoved: [],
109
+ routesAdded: [],
110
+ routesRemoved: [],
111
+ mappingsChanged: [],
112
+ hasChanges: false,
113
+ };
114
+
115
+ if (!oldManifest) {
116
+ result.modelsAdded = newManifest.detectedModels || [];
117
+ result.routesAdded = newManifest.detectedRoutes || [];
118
+ result.hasChanges = result.modelsAdded.length > 0 || result.routesAdded.length > 0;
119
+ return result;
120
+ }
121
+
122
+ // Compare models
123
+ const oldModelNames = new Set((oldManifest.detectedModels || []).map(m => m.name));
124
+ const newModelNames = new Set((newManifest.detectedModels || []).map(m => m.name));
125
+
126
+ for (const m of (newManifest.detectedModels || [])) {
127
+ if (!oldModelNames.has(m.name)) result.modelsAdded.push(m);
128
+ }
129
+ for (const m of (oldManifest.detectedModels || [])) {
130
+ if (!newModelNames.has(m.name)) result.modelsRemoved.push(m);
131
+ }
132
+
133
+ // Compare routes
134
+ const oldRoutePaths = new Set((oldManifest.detectedRoutes || []).map(r => r.path));
135
+ const newRoutePaths = new Set((newManifest.detectedRoutes || []).map(r => r.path));
136
+
137
+ for (const r of (newManifest.detectedRoutes || [])) {
138
+ if (!oldRoutePaths.has(r.path)) result.routesAdded.push(r);
139
+ }
140
+ for (const r of (oldManifest.detectedRoutes || [])) {
141
+ if (!newRoutePaths.has(r.path)) result.routesRemoved.push(r);
142
+ }
143
+
144
+ result.hasChanges =
145
+ result.modelsAdded.length > 0 ||
146
+ result.modelsRemoved.length > 0 ||
147
+ result.routesAdded.length > 0 ||
148
+ result.routesRemoved.length > 0;
149
+
150
+ return result;
151
+ }
152
+ }
153
+
154
+ module.exports = new ManifestGenerator();