@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
package/src/commands/spread.js
CHANGED
|
@@ -5,198 +5,27 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
-
const { execSync } = require('child_process');
|
|
9
8
|
const configManager = require('../config/config-manager');
|
|
10
9
|
const backendClient = require('../api/backend-client');
|
|
11
10
|
const projectDetector = require('../detectors');
|
|
12
11
|
const fileGenerator = require('../generators');
|
|
12
|
+
const manifestGenerator = require('../generators/manifest-generator');
|
|
13
|
+
const pageDetector = require('../detectors/page-detector');
|
|
14
|
+
const { suggestMappings } = require('../detectors/mapping-suggestor');
|
|
13
15
|
const { generateProjectPathHash } = require('../utils/file-utils');
|
|
16
|
+
const {
|
|
17
|
+
createOrganization,
|
|
18
|
+
generateNewApiKey,
|
|
19
|
+
checkGitStatusBeforeGeneration,
|
|
20
|
+
requireAuth,
|
|
21
|
+
} = require('../utils/init-helpers');
|
|
14
22
|
const inquirer = require('inquirer');
|
|
15
23
|
const chalk = require('chalk');
|
|
16
24
|
const pkg = require('../../package.json');
|
|
17
25
|
const { showMainMenu, executeMenuAction } = require('../utils/prompt-utils');
|
|
18
26
|
|
|
19
|
-
/**
|
|
20
|
-
* Helper function to create an organization
|
|
21
|
-
*/
|
|
22
|
-
async function createOrganization(orgName) {
|
|
23
|
-
console.log(chalk.gray(` Creating organization "${orgName}"...`));
|
|
24
|
-
const newOrg = await backendClient.createOrganization(orgName);
|
|
25
|
-
// Handle different response formats
|
|
26
|
-
const organizationId = newOrg.organizationId || newOrg.id || newOrg.data?.organizationId || newOrg.data?.id;
|
|
27
|
-
const organizationName = newOrg.name || orgName;
|
|
28
|
-
|
|
29
|
-
if (!organizationId) {
|
|
30
|
-
throw new Error('Organization ID not found in response. Please check backend API endpoint.');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
console.log(chalk.green(` ✅ Organization created: ${organizationName}\n`));
|
|
34
|
-
return { organizationId, organizationName };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Helper function to generate a new API key
|
|
39
|
-
*/
|
|
40
|
-
async function generateNewApiKey(organizationId) {
|
|
41
|
-
console.log(chalk.gray(' Generating API key...'));
|
|
42
|
-
const apiKeyResponse = await backendClient.generateApiKey(
|
|
43
|
-
organizationId,
|
|
44
|
-
'CLI Generated Key',
|
|
45
|
-
['*']
|
|
46
|
-
);
|
|
47
|
-
// Handle different response formats
|
|
48
|
-
const apiKey = apiKeyResponse.key || apiKeyResponse.apiKey || apiKeyResponse.data?.key || apiKeyResponse.data?.apiKey;
|
|
49
|
-
|
|
50
|
-
if (!apiKey) {
|
|
51
|
-
throw new Error('API key not found in response. Please check backend API endpoint.');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
console.log(chalk.green(` ✅ API key generated\n`));
|
|
55
|
-
return apiKey;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Check if the project is a git repository
|
|
60
|
-
*/
|
|
61
|
-
function isGitRepo(projectPath) {
|
|
62
|
-
try {
|
|
63
|
-
execSync('git rev-parse --is-inside-work-tree', {
|
|
64
|
-
cwd: projectPath,
|
|
65
|
-
stdio: 'pipe',
|
|
66
|
-
});
|
|
67
|
-
return true;
|
|
68
|
-
} catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Get git status (uncommitted changes)
|
|
75
|
-
*/
|
|
76
|
-
function getGitStatus(projectPath) {
|
|
77
|
-
try {
|
|
78
|
-
const status = execSync('git status --porcelain', {
|
|
79
|
-
cwd: projectPath,
|
|
80
|
-
encoding: 'utf8',
|
|
81
|
-
});
|
|
82
|
-
return status.trim();
|
|
83
|
-
} catch {
|
|
84
|
-
return '';
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Check for uncommitted changes and prompt user to commit first
|
|
90
|
-
* Returns true if we should proceed, false if user wants to abort
|
|
91
|
-
*/
|
|
92
|
-
async function checkGitStatusBeforeGeneration(projectPath) {
|
|
93
|
-
const debug = process.env.L4YERCAK3_DEBUG;
|
|
94
|
-
|
|
95
|
-
if (debug) {
|
|
96
|
-
console.log('\n[DEBUG] Git status check:');
|
|
97
|
-
console.log(` projectPath: "${projectPath}"`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Skip if not a git repo
|
|
101
|
-
if (!isGitRepo(projectPath)) {
|
|
102
|
-
if (debug) {
|
|
103
|
-
console.log(' → Not a git repo, skipping check');
|
|
104
|
-
}
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const status = getGitStatus(projectPath);
|
|
109
|
-
|
|
110
|
-
if (debug) {
|
|
111
|
-
console.log(` → Git status: "${status.substring(0, 100)}${status.length > 100 ? '...' : ''}"`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// No uncommitted changes - proceed
|
|
115
|
-
if (!status) {
|
|
116
|
-
if (debug) {
|
|
117
|
-
console.log(' → No uncommitted changes, proceeding');
|
|
118
|
-
}
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Count changes
|
|
123
|
-
const changes = status.split('\n').filter(line => line.trim());
|
|
124
|
-
const modifiedCount = changes.filter(line => line.startsWith(' M') || line.startsWith('M ')).length;
|
|
125
|
-
const untrackedCount = changes.filter(line => line.startsWith('??')).length;
|
|
126
|
-
const stagedCount = changes.filter(line => /^[MADRC]/.test(line)).length;
|
|
127
|
-
|
|
128
|
-
console.log(chalk.yellow(' ⚠️ Uncommitted changes detected\n'));
|
|
129
|
-
|
|
130
|
-
if (modifiedCount > 0) {
|
|
131
|
-
console.log(chalk.gray(` ${modifiedCount} modified file(s)`));
|
|
132
|
-
}
|
|
133
|
-
if (untrackedCount > 0) {
|
|
134
|
-
console.log(chalk.gray(` ${untrackedCount} untracked file(s)`));
|
|
135
|
-
}
|
|
136
|
-
if (stagedCount > 0) {
|
|
137
|
-
console.log(chalk.gray(` ${stagedCount} staged file(s)`));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
console.log('');
|
|
141
|
-
console.log(chalk.gray(' We recommend committing your changes before generating'));
|
|
142
|
-
console.log(chalk.gray(' new files, so you can easily revert if needed.\n'));
|
|
143
|
-
|
|
144
|
-
const { action } = await inquirer.prompt([
|
|
145
|
-
{
|
|
146
|
-
type: 'list',
|
|
147
|
-
name: 'action',
|
|
148
|
-
message: 'How would you like to proceed?',
|
|
149
|
-
choices: [
|
|
150
|
-
{
|
|
151
|
-
name: 'Continue anyway - I\'ll handle it later',
|
|
152
|
-
value: 'continue',
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
name: 'Commit changes now - Create a checkpoint commit',
|
|
156
|
-
value: 'commit',
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
name: 'Abort - I\'ll commit manually first',
|
|
160
|
-
value: 'abort',
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
},
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
if (action === 'abort') {
|
|
167
|
-
console.log(chalk.gray('\n No worries! Run "l4yercak3 spread" again after committing.\n'));
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (action === 'commit') {
|
|
172
|
-
try {
|
|
173
|
-
// Stage all changes
|
|
174
|
-
execSync('git add -A', { cwd: projectPath, stdio: 'pipe' });
|
|
175
|
-
|
|
176
|
-
// Create commit
|
|
177
|
-
const commitMessage = 'chore: checkpoint before L4YERCAK3 integration';
|
|
178
|
-
execSync(`git commit -m "${commitMessage}"`, { cwd: projectPath, stdio: 'pipe' });
|
|
179
|
-
|
|
180
|
-
console.log(chalk.green('\n ✅ Changes committed successfully'));
|
|
181
|
-
console.log(chalk.gray(` Message: "${commitMessage}"`));
|
|
182
|
-
console.log(chalk.gray(' You can revert with: git reset --soft HEAD~1\n'));
|
|
183
|
-
} catch (error) {
|
|
184
|
-
console.log(chalk.yellow('\n ⚠️ Could not create commit automatically'));
|
|
185
|
-
console.log(chalk.gray(` ${error.message}`));
|
|
186
|
-
console.log(chalk.gray(' Proceeding with file generation anyway...\n'));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
27
|
async function handleSpread() {
|
|
194
|
-
|
|
195
|
-
if (!configManager.isLoggedIn()) {
|
|
196
|
-
console.log(chalk.yellow(' ⚠️ You must be logged in first'));
|
|
197
|
-
console.log(chalk.gray('\n Run "l4yercak3 login" to authenticate\n'));
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
28
|
+
requireAuth(configManager);
|
|
200
29
|
|
|
201
30
|
console.log(chalk.cyan(' 🍰 Setting up your Layer Cake integration...\n'));
|
|
202
31
|
|
|
@@ -293,6 +122,45 @@ async function handleSpread() {
|
|
|
293
122
|
}
|
|
294
123
|
}
|
|
295
124
|
|
|
125
|
+
// Display model detection
|
|
126
|
+
if (detection.models && detection.models.hasModels) {
|
|
127
|
+
console.log(chalk.green(` ✅ Detected ${detection.models.models.length} model(s)`));
|
|
128
|
+
for (const model of detection.models.models.slice(0, 5)) {
|
|
129
|
+
console.log(chalk.gray(` • ${model.name} (${model.source}) [${model.fields.length} fields]`));
|
|
130
|
+
}
|
|
131
|
+
if (detection.models.models.length > 5) {
|
|
132
|
+
console.log(chalk.gray(` ... and ${detection.models.models.length - 5} more`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Scan routes with HTTP methods
|
|
137
|
+
const detectedRoutes = pageDetector.detect(
|
|
138
|
+
detection.projectPath,
|
|
139
|
+
detection.framework.type,
|
|
140
|
+
detection.framework.metadata || {}
|
|
141
|
+
);
|
|
142
|
+
const apiRoutes = detectedRoutes.filter(r => r.pageType === 'api_route');
|
|
143
|
+
if (apiRoutes.length > 0) {
|
|
144
|
+
console.log(chalk.green(` ✅ Detected ${apiRoutes.length} API route(s)`));
|
|
145
|
+
for (const route of apiRoutes.slice(0, 5)) {
|
|
146
|
+
const methods = route.methods ? route.methods.join(', ') : 'GET, POST';
|
|
147
|
+
console.log(chalk.gray(` • ${route.path} [${methods}]`));
|
|
148
|
+
}
|
|
149
|
+
if (apiRoutes.length > 5) {
|
|
150
|
+
console.log(chalk.gray(` ... and ${apiRoutes.length - 5} more`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Compute suggested mappings
|
|
155
|
+
const models = detection.models ? detection.models.models : [];
|
|
156
|
+
const mappings = suggestMappings(models);
|
|
157
|
+
if (mappings.length > 0) {
|
|
158
|
+
console.log(chalk.green(` ✅ ${mappings.length} suggested mapping(s)`));
|
|
159
|
+
for (const mapping of mappings) {
|
|
160
|
+
console.log(chalk.gray(` • ${mapping.localModel} → ${mapping.platformType} (${mapping.confidence}% confidence)`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
296
164
|
console.log('');
|
|
297
165
|
|
|
298
166
|
// Step 1.5: Project name
|
|
@@ -553,22 +421,43 @@ async function handleSpread() {
|
|
|
553
421
|
}
|
|
554
422
|
|
|
555
423
|
// Step 4: Feature selection
|
|
424
|
+
// Auto-check features based on detected model mappings
|
|
425
|
+
const platformTypeToFeature = {
|
|
426
|
+
contact: 'crm',
|
|
427
|
+
booking: 'events',
|
|
428
|
+
event: 'events',
|
|
429
|
+
product: 'products',
|
|
430
|
+
invoice: 'invoicing',
|
|
431
|
+
project: 'projects',
|
|
432
|
+
form: 'forms',
|
|
433
|
+
certificate: 'certificates',
|
|
434
|
+
benefit: 'benefits',
|
|
435
|
+
};
|
|
436
|
+
const autoCheckedFeatures = new Set(['crm']); // CRM always default
|
|
437
|
+
for (const mapping of mappings) {
|
|
438
|
+
const feature = platformTypeToFeature[mapping.platformType];
|
|
439
|
+
if (feature) autoCheckedFeatures.add(feature);
|
|
440
|
+
}
|
|
441
|
+
|
|
556
442
|
console.log(chalk.cyan(' ⚙️ Feature Selection\n'));
|
|
443
|
+
if (mappings.length > 0) {
|
|
444
|
+
console.log(chalk.gray(' Features pre-selected based on detected models:\n'));
|
|
445
|
+
}
|
|
557
446
|
const { features } = await inquirer.prompt([
|
|
558
447
|
{
|
|
559
448
|
type: 'checkbox',
|
|
560
449
|
name: 'features',
|
|
561
450
|
message: 'Select features to enable:',
|
|
562
451
|
choices: [
|
|
563
|
-
{ name: 'CRM (contacts, organizations)', value: 'crm', checked:
|
|
564
|
-
{ name: 'Events (event management, registrations)', value: 'events', checked:
|
|
565
|
-
{ name: 'Forms (form builder, submissions)', value: 'forms', checked:
|
|
566
|
-
{ name: 'Products (product catalog, inventory)', value: 'products', checked:
|
|
567
|
-
{ name: 'Checkout (cart, payments)', value: 'checkout', checked:
|
|
568
|
-
{ name: 'Invoicing (B2B/B2C invoices)', value: 'invoicing', checked:
|
|
569
|
-
{ name: 'Benefits (claims, commissions)', value: 'benefits', checked:
|
|
570
|
-
{ name: 'Certificates (CME, attendance)', value: 'certificates', checked:
|
|
571
|
-
{ name: 'Projects (task management)', value: 'projects', checked:
|
|
452
|
+
{ name: 'CRM (contacts, organizations)', value: 'crm', checked: autoCheckedFeatures.has('crm') },
|
|
453
|
+
{ name: 'Events (event management, registrations)', value: 'events', checked: autoCheckedFeatures.has('events') },
|
|
454
|
+
{ name: 'Forms (form builder, submissions)', value: 'forms', checked: autoCheckedFeatures.has('forms') },
|
|
455
|
+
{ name: 'Products (product catalog, inventory)', value: 'products', checked: autoCheckedFeatures.has('products') },
|
|
456
|
+
{ name: 'Checkout (cart, payments)', value: 'checkout', checked: autoCheckedFeatures.has('checkout') },
|
|
457
|
+
{ name: 'Invoicing (B2B/B2C invoices)', value: 'invoicing', checked: autoCheckedFeatures.has('invoicing') },
|
|
458
|
+
{ name: 'Benefits (claims, commissions)', value: 'benefits', checked: autoCheckedFeatures.has('benefits') },
|
|
459
|
+
{ name: 'Certificates (CME, attendance)', value: 'certificates', checked: autoCheckedFeatures.has('certificates') },
|
|
460
|
+
{ name: 'Projects (task management)', value: 'projects', checked: autoCheckedFeatures.has('projects') },
|
|
572
461
|
{ name: 'OAuth Authentication', value: 'oauth', checked: false },
|
|
573
462
|
],
|
|
574
463
|
},
|
|
@@ -772,6 +661,16 @@ async function handleSpread() {
|
|
|
772
661
|
console.log(chalk.gray(` • ${path.relative(process.cwd(), generatedFiles.gitignore)} (updated)`));
|
|
773
662
|
}
|
|
774
663
|
|
|
664
|
+
// Generate .l4yercak3.json manifest
|
|
665
|
+
const manifestPath = manifestGenerator.generate({
|
|
666
|
+
projectPath: detection.projectPath,
|
|
667
|
+
detection,
|
|
668
|
+
models,
|
|
669
|
+
routes: detectedRoutes,
|
|
670
|
+
mappings,
|
|
671
|
+
});
|
|
672
|
+
console.log(chalk.gray(` • ${path.relative(process.cwd(), manifestPath)}`));
|
|
673
|
+
|
|
775
674
|
// Step 9: Register application with backend
|
|
776
675
|
console.log(chalk.cyan('\n 🔗 Registering with L4YERCAK3...\n'));
|
|
777
676
|
|
|
@@ -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
|
+
};
|