@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.
- 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 +68 -164
- 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/detectors/registry.js +25 -3
- package/src/generators/manifest-generator.js +154 -0
- package/src/generators/quickstart/index.js +6 -0
- package/src/utils/init-helpers.js +243 -0
- package/tests/page-detector.test.js +371 -0
|
@@ -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
|
+
};
|
package/src/detectors/index.js
CHANGED
|
@@ -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
|
+
};
|