@l4yercak3/cli 1.2.16 → 1.2.19
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 +3 -1
- package/docs/CRM-PIPELINES-SEQUENCES-SPEC.md +429 -0
- package/docs/INTEGRATION_PATHS_ARCHITECTURE.md +1543 -0
- package/package.json +1 -1
- package/src/commands/login.js +26 -7
- package/src/commands/spread.js +251 -10
- package/src/detectors/database-detector.js +245 -0
- package/src/detectors/expo-detector.js +4 -4
- package/src/detectors/index.js +17 -4
- package/src/generators/api-only/client.js +683 -0
- package/src/generators/api-only/index.js +96 -0
- package/src/generators/api-only/types.js +618 -0
- package/src/generators/api-only/webhooks.js +377 -0
- package/src/generators/env-generator.js +23 -8
- package/src/generators/expo-auth-generator.js +1009 -0
- package/src/generators/index.js +88 -2
- package/src/generators/mcp-guide-generator.js +256 -0
- package/src/generators/quickstart/components/index.js +1699 -0
- package/src/generators/quickstart/components-mobile/index.js +1440 -0
- package/src/generators/quickstart/database/convex.js +1257 -0
- package/src/generators/quickstart/database/index.js +34 -0
- package/src/generators/quickstart/database/supabase.js +1132 -0
- package/src/generators/quickstart/hooks/index.js +1065 -0
- package/src/generators/quickstart/index.js +177 -0
- package/src/generators/quickstart/pages/index.js +1466 -0
- package/src/generators/quickstart/screens/index.js +1498 -0
- package/src/mcp/registry/domains/benefits.js +798 -0
- package/src/mcp/registry/index.js +2 -0
- package/tests/database-detector.test.js +221 -0
- package/tests/expo-detector.test.js +3 -4
- package/tests/generators-index.test.js +215 -3
package/package.json
CHANGED
package/src/commands/login.js
CHANGED
|
@@ -232,6 +232,18 @@ async function handleLogin() {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Get friendly framework name for display
|
|
237
|
+
*/
|
|
238
|
+
function getFrameworkDisplayName(frameworkType) {
|
|
239
|
+
const names = {
|
|
240
|
+
'nextjs': 'Next.js',
|
|
241
|
+
'expo': 'Expo',
|
|
242
|
+
'react-native': 'React Native',
|
|
243
|
+
};
|
|
244
|
+
return names[frameworkType] || frameworkType;
|
|
245
|
+
}
|
|
246
|
+
|
|
235
247
|
/**
|
|
236
248
|
* Prompt user to run the setup wizard after login
|
|
237
249
|
*/
|
|
@@ -243,7 +255,7 @@ async function promptSetupWizard() {
|
|
|
243
255
|
const isInProject = detection.framework.type !== null;
|
|
244
256
|
|
|
245
257
|
if (isInProject) {
|
|
246
|
-
const frameworkName = detection.framework.type
|
|
258
|
+
const frameworkName = getFrameworkDisplayName(detection.framework.type);
|
|
247
259
|
console.log(chalk.cyan(` 🔍 Detected ${frameworkName} project in current directory\n`));
|
|
248
260
|
|
|
249
261
|
// Check if project is already configured
|
|
@@ -280,6 +292,10 @@ async function promptSetupWizard() {
|
|
|
280
292
|
|
|
281
293
|
if (!runWizard) {
|
|
282
294
|
console.log('');
|
|
295
|
+
console.log(chalk.yellow(` ℹ️ To generate ${frameworkName}-specific code later, run:\n`));
|
|
296
|
+
console.log(chalk.cyan(' l4yercak3 spread\n'));
|
|
297
|
+
console.log(chalk.gray(' This will create components, hooks, and screens'));
|
|
298
|
+
console.log(chalk.gray(` optimized for ${frameworkName}.\n`));
|
|
283
299
|
const action = await showPostLoginMenu({ isInProject: true, hasExistingConfig: false });
|
|
284
300
|
await executeMenuAction(action);
|
|
285
301
|
return;
|
|
@@ -294,14 +310,17 @@ async function promptSetupWizard() {
|
|
|
294
310
|
} else {
|
|
295
311
|
// Not in a project directory
|
|
296
312
|
console.log(chalk.cyan(' 📋 What\'s Next?\n'));
|
|
297
|
-
console.log(chalk.gray(' To integrate L4YERCAK3 with your
|
|
313
|
+
console.log(chalk.gray(' To integrate L4YERCAK3 with your project:\n'));
|
|
298
314
|
console.log(chalk.gray(' 1. Navigate to your project directory'));
|
|
299
315
|
console.log(chalk.gray(' 2. Run: l4yercak3 spread\n'));
|
|
300
|
-
console.log(chalk.gray('
|
|
301
|
-
console.log(chalk.gray(' •
|
|
302
|
-
console.log(chalk.gray(' •
|
|
303
|
-
console.log(chalk.gray('
|
|
304
|
-
console.log(chalk.gray(' •
|
|
316
|
+
console.log(chalk.gray(' Supported frameworks:'));
|
|
317
|
+
console.log(chalk.gray(' • Next.js (App Router & Pages Router)'));
|
|
318
|
+
console.log(chalk.gray(' • Expo / React Native\n'));
|
|
319
|
+
console.log(chalk.gray(' The spread command will:'));
|
|
320
|
+
console.log(chalk.gray(' • Detect your framework automatically'));
|
|
321
|
+
console.log(chalk.gray(' • Generate platform-specific components'));
|
|
322
|
+
console.log(chalk.gray(' • Set up API client & environment variables'));
|
|
323
|
+
console.log(chalk.gray(' • Configure OAuth authentication (optional)\n'));
|
|
305
324
|
}
|
|
306
325
|
}
|
|
307
326
|
|
package/src/commands/spread.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
8
9
|
const configManager = require('../config/config-manager');
|
|
9
10
|
const backendClient = require('../api/backend-client');
|
|
10
11
|
const projectDetector = require('../detectors');
|
|
@@ -54,6 +55,124 @@ async function generateNewApiKey(organizationId) {
|
|
|
54
55
|
return apiKey;
|
|
55
56
|
}
|
|
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
|
+
// Skip if not a git repo
|
|
94
|
+
if (!isGitRepo(projectPath)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const status = getGitStatus(projectPath);
|
|
99
|
+
|
|
100
|
+
// No uncommitted changes - proceed
|
|
101
|
+
if (!status) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Count changes
|
|
106
|
+
const changes = status.split('\n').filter(line => line.trim());
|
|
107
|
+
const modifiedCount = changes.filter(line => line.startsWith(' M') || line.startsWith('M ')).length;
|
|
108
|
+
const untrackedCount = changes.filter(line => line.startsWith('??')).length;
|
|
109
|
+
const stagedCount = changes.filter(line => /^[MADRC]/.test(line)).length;
|
|
110
|
+
|
|
111
|
+
console.log(chalk.yellow(' ⚠️ Uncommitted changes detected\n'));
|
|
112
|
+
|
|
113
|
+
if (modifiedCount > 0) {
|
|
114
|
+
console.log(chalk.gray(` ${modifiedCount} modified file(s)`));
|
|
115
|
+
}
|
|
116
|
+
if (untrackedCount > 0) {
|
|
117
|
+
console.log(chalk.gray(` ${untrackedCount} untracked file(s)`));
|
|
118
|
+
}
|
|
119
|
+
if (stagedCount > 0) {
|
|
120
|
+
console.log(chalk.gray(` ${stagedCount} staged file(s)`));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log(chalk.gray(' We recommend committing your changes before generating'));
|
|
125
|
+
console.log(chalk.gray(' new files, so you can easily revert if needed.\n'));
|
|
126
|
+
|
|
127
|
+
const { action } = await inquirer.prompt([
|
|
128
|
+
{
|
|
129
|
+
type: 'list',
|
|
130
|
+
name: 'action',
|
|
131
|
+
message: 'How would you like to proceed?',
|
|
132
|
+
choices: [
|
|
133
|
+
{
|
|
134
|
+
name: 'Continue anyway - I\'ll handle it later',
|
|
135
|
+
value: 'continue',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'Commit changes now - Create a checkpoint commit',
|
|
139
|
+
value: 'commit',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'Abort - I\'ll commit manually first',
|
|
143
|
+
value: 'abort',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
if (action === 'abort') {
|
|
150
|
+
console.log(chalk.gray('\n No worries! Run "l4yercak3 spread" again after committing.\n'));
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (action === 'commit') {
|
|
155
|
+
try {
|
|
156
|
+
// Stage all changes
|
|
157
|
+
execSync('git add -A', { cwd: projectPath, stdio: 'pipe' });
|
|
158
|
+
|
|
159
|
+
// Create commit
|
|
160
|
+
const commitMessage = 'chore: checkpoint before L4YERCAK3 integration';
|
|
161
|
+
execSync(`git commit -m "${commitMessage}"`, { cwd: projectPath, stdio: 'pipe' });
|
|
162
|
+
|
|
163
|
+
console.log(chalk.green('\n ✅ Changes committed successfully'));
|
|
164
|
+
console.log(chalk.gray(` Message: "${commitMessage}"`));
|
|
165
|
+
console.log(chalk.gray(' You can revert with: git reset --soft HEAD~1\n'));
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.log(chalk.yellow('\n ⚠️ Could not create commit automatically'));
|
|
168
|
+
console.log(chalk.gray(` ${error.message}`));
|
|
169
|
+
console.log(chalk.gray(' Proceeding with file generation anyway...\n'));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
57
176
|
async function handleSpread() {
|
|
58
177
|
// Check if logged in
|
|
59
178
|
if (!configManager.isLoggedIn()) {
|
|
@@ -71,11 +190,17 @@ async function handleSpread() {
|
|
|
71
190
|
|
|
72
191
|
// Display framework detection results
|
|
73
192
|
if (detection.framework.type) {
|
|
74
|
-
const
|
|
193
|
+
const frameworkNames = {
|
|
194
|
+
'nextjs': 'Next.js',
|
|
195
|
+
'expo': 'Expo',
|
|
196
|
+
'react-native': 'React Native',
|
|
197
|
+
};
|
|
198
|
+
const frameworkName = frameworkNames[detection.framework.type] || detection.framework.type;
|
|
75
199
|
console.log(chalk.green(` ✅ Detected ${frameworkName} project`));
|
|
76
|
-
|
|
200
|
+
|
|
201
|
+
const meta = detection.framework.metadata || {};
|
|
202
|
+
|
|
77
203
|
if (detection.framework.type === 'nextjs') {
|
|
78
|
-
const meta = detection.framework.metadata;
|
|
79
204
|
if (meta.version) {
|
|
80
205
|
console.log(chalk.gray(` Version: ${meta.version}`));
|
|
81
206
|
}
|
|
@@ -85,6 +210,21 @@ async function handleSpread() {
|
|
|
85
210
|
if (meta.hasTypeScript) {
|
|
86
211
|
console.log(chalk.gray(' TypeScript: Yes'));
|
|
87
212
|
}
|
|
213
|
+
} else if (detection.framework.type === 'expo' || detection.framework.type === 'react-native') {
|
|
214
|
+
if (meta.expoVersion) {
|
|
215
|
+
console.log(chalk.gray(` Expo SDK: ${meta.expoVersion}`));
|
|
216
|
+
}
|
|
217
|
+
if (meta.reactNativeVersion) {
|
|
218
|
+
console.log(chalk.gray(` React Native: ${meta.reactNativeVersion}`));
|
|
219
|
+
}
|
|
220
|
+
if (meta.routerType) {
|
|
221
|
+
const routerName = meta.routerType === 'expo-router' ? 'Expo Router' :
|
|
222
|
+
meta.routerType === 'react-navigation' ? 'React Navigation' : 'Native';
|
|
223
|
+
console.log(chalk.gray(` Navigation: ${routerName}`));
|
|
224
|
+
}
|
|
225
|
+
if (meta.hasTypeScript) {
|
|
226
|
+
console.log(chalk.gray(' TypeScript: Yes'));
|
|
227
|
+
}
|
|
88
228
|
}
|
|
89
229
|
|
|
90
230
|
// Show supported features
|
|
@@ -405,17 +545,83 @@ async function handleSpread() {
|
|
|
405
545
|
choices: [
|
|
406
546
|
{ name: 'CRM (contacts, organizations)', value: 'crm', checked: true },
|
|
407
547
|
{ name: 'Events (event management, registrations)', value: 'events', checked: false },
|
|
408
|
-
{ name: '
|
|
409
|
-
{ name: '
|
|
410
|
-
{ name: '
|
|
411
|
-
{ name: 'Invoicing (
|
|
412
|
-
{ name: '
|
|
413
|
-
{ name: '
|
|
548
|
+
{ name: 'Forms (form builder, submissions)', value: 'forms', checked: false },
|
|
549
|
+
{ name: 'Products (product catalog, inventory)', value: 'products', checked: false },
|
|
550
|
+
{ name: 'Checkout (cart, payments)', value: 'checkout', checked: false },
|
|
551
|
+
{ name: 'Invoicing (B2B/B2C invoices)', value: 'invoicing', checked: false },
|
|
552
|
+
{ name: 'Benefits (claims, commissions)', value: 'benefits', checked: false },
|
|
553
|
+
{ name: 'Certificates (CME, attendance)', value: 'certificates', checked: false },
|
|
554
|
+
{ name: 'Projects (task management)', value: 'projects', checked: false },
|
|
414
555
|
{ name: 'OAuth Authentication', value: 'oauth', checked: false },
|
|
415
556
|
],
|
|
416
557
|
},
|
|
417
558
|
]);
|
|
418
559
|
|
|
560
|
+
// Step 4.5: Integration path selection
|
|
561
|
+
console.log(chalk.cyan('\n 🛤️ Integration Path\n'));
|
|
562
|
+
const { integrationPath } = await inquirer.prompt([
|
|
563
|
+
{
|
|
564
|
+
type: 'list',
|
|
565
|
+
name: 'integrationPath',
|
|
566
|
+
message: 'Choose your integration approach:',
|
|
567
|
+
choices: [
|
|
568
|
+
{
|
|
569
|
+
name: 'Quick Start (Recommended) - Full-stack with UI components & database',
|
|
570
|
+
value: 'quickstart',
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: 'API Only - Just the typed API client, you build the UI',
|
|
574
|
+
value: 'api-only',
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
name: 'MCP-Assisted - AI-powered custom generation with Claude Code',
|
|
578
|
+
value: 'mcp-assisted',
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
},
|
|
582
|
+
]);
|
|
583
|
+
console.log(chalk.green(` ✅ Path: ${integrationPath === 'quickstart' ? 'Quick Start' : integrationPath === 'api-only' ? 'API Only' : 'MCP-Assisted'}\n`));
|
|
584
|
+
|
|
585
|
+
// Step 4.6: Database selection (for Quick Start path when no DB detected)
|
|
586
|
+
let selectedDatabase = null;
|
|
587
|
+
if (integrationPath === 'quickstart') {
|
|
588
|
+
const dbDetection = detection.database || { hasDatabase: false };
|
|
589
|
+
|
|
590
|
+
if (!dbDetection.hasDatabase) {
|
|
591
|
+
console.log(chalk.yellow(' ℹ️ No database detected in your project.\n'));
|
|
592
|
+
|
|
593
|
+
const { database } = await inquirer.prompt([
|
|
594
|
+
{
|
|
595
|
+
type: 'list',
|
|
596
|
+
name: 'database',
|
|
597
|
+
message: 'Which database would you like to use?',
|
|
598
|
+
choices: [
|
|
599
|
+
{
|
|
600
|
+
name: 'Convex (Recommended) - Real-time, serverless, TypeScript-first',
|
|
601
|
+
value: 'convex',
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: 'Supabase - PostgreSQL with Auth, Storage, and Edge Functions',
|
|
605
|
+
value: 'supabase',
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: 'None - I\'ll set up my own database later',
|
|
609
|
+
value: 'none',
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
},
|
|
613
|
+
]);
|
|
614
|
+
|
|
615
|
+
selectedDatabase = database !== 'none' ? database : null;
|
|
616
|
+
if (selectedDatabase) {
|
|
617
|
+
console.log(chalk.green(` ✅ Database: ${selectedDatabase}\n`));
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
console.log(chalk.green(` ✅ Detected ${dbDetection.primary?.type || 'existing'} database\n`));
|
|
621
|
+
selectedDatabase = dbDetection.primary?.type || 'existing';
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
419
625
|
// Step 5: OAuth provider selection (if OAuth enabled)
|
|
420
626
|
let oauthProviders = [];
|
|
421
627
|
if (features.includes('oauth')) {
|
|
@@ -468,7 +674,13 @@ async function handleSpread() {
|
|
|
468
674
|
}
|
|
469
675
|
}
|
|
470
676
|
|
|
471
|
-
// Step 8:
|
|
677
|
+
// Step 8: Check for uncommitted changes before generating files
|
|
678
|
+
const shouldProceed = await checkGitStatusBeforeGeneration(detection.projectPath);
|
|
679
|
+
if (!shouldProceed) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Step 9: Generate files
|
|
472
684
|
console.log(chalk.cyan('\n 📝 Generating files...\n'));
|
|
473
685
|
|
|
474
686
|
// Extract framework metadata for generation
|
|
@@ -489,15 +701,38 @@ async function handleSpread() {
|
|
|
489
701
|
isTypeScript,
|
|
490
702
|
routerType,
|
|
491
703
|
frameworkType: detection.framework.type || 'unknown',
|
|
704
|
+
integrationPath,
|
|
705
|
+
selectedDatabase,
|
|
492
706
|
};
|
|
493
707
|
|
|
494
708
|
const generatedFiles = await fileGenerator.generate(generationOptions);
|
|
495
709
|
|
|
496
710
|
// Display results
|
|
497
711
|
console.log(chalk.green(' ✅ Files generated:\n'));
|
|
712
|
+
|
|
713
|
+
// API client files (api-only and quickstart paths)
|
|
498
714
|
if (generatedFiles.apiClient) {
|
|
499
715
|
console.log(chalk.gray(` • ${path.relative(process.cwd(), generatedFiles.apiClient)}`));
|
|
500
716
|
}
|
|
717
|
+
if (generatedFiles.types) {
|
|
718
|
+
console.log(chalk.gray(` • ${path.relative(process.cwd(), generatedFiles.types)}`));
|
|
719
|
+
}
|
|
720
|
+
if (generatedFiles.webhooks) {
|
|
721
|
+
console.log(chalk.gray(` • ${path.relative(process.cwd(), generatedFiles.webhooks)}`));
|
|
722
|
+
}
|
|
723
|
+
if (generatedFiles.index) {
|
|
724
|
+
console.log(chalk.gray(` • ${path.relative(process.cwd(), generatedFiles.index)}`));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// MCP files (mcp-assisted path)
|
|
728
|
+
if (generatedFiles.mcpConfig) {
|
|
729
|
+
console.log(chalk.gray(` • ${path.relative(process.cwd(), generatedFiles.mcpConfig)}`));
|
|
730
|
+
}
|
|
731
|
+
if (generatedFiles.mcpGuide) {
|
|
732
|
+
console.log(chalk.gray(` • ${path.relative(process.cwd(), generatedFiles.mcpGuide)}`));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Common files
|
|
501
736
|
if (generatedFiles.envFile) {
|
|
502
737
|
console.log(chalk.gray(` • ${path.relative(process.cwd(), generatedFiles.envFile)}`));
|
|
503
738
|
}
|
|
@@ -641,6 +876,8 @@ async function handleSpread() {
|
|
|
641
876
|
oauthProviders,
|
|
642
877
|
productionDomain,
|
|
643
878
|
frameworkType: detection.framework.type,
|
|
879
|
+
integrationPath,
|
|
880
|
+
selectedDatabase,
|
|
644
881
|
createdAt: Date.now(),
|
|
645
882
|
updatedAt: isUpdate ? Date.now() : undefined,
|
|
646
883
|
};
|
|
@@ -678,6 +915,10 @@ async function handleSpread() {
|
|
|
678
915
|
|
|
679
916
|
console.log(chalk.gray(' Your project is now connected to L4YERCAK3! 🍰\n'));
|
|
680
917
|
|
|
918
|
+
// Show menu for next actions
|
|
919
|
+
const action = await showMainMenu({ isLoggedIn: true, isInProject: true, hasExistingConfig: true });
|
|
920
|
+
await executeMenuAction(action);
|
|
921
|
+
|
|
681
922
|
} catch (error) {
|
|
682
923
|
console.error(chalk.red(`\n ❌ Error: ${error.message}\n`));
|
|
683
924
|
if (error.stack) {
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Detector
|
|
3
|
+
* Detects existing database configurations in a project
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if a file exists
|
|
11
|
+
* @param {string} filePath - Path to check
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
function fileExists(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
return fs.existsSync(filePath);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read and parse package.json
|
|
24
|
+
* @param {string} projectPath - Project root path
|
|
25
|
+
* @returns {object|null}
|
|
26
|
+
*/
|
|
27
|
+
function readPackageJson(projectPath) {
|
|
28
|
+
try {
|
|
29
|
+
const packagePath = path.join(projectPath, 'package.json');
|
|
30
|
+
if (fileExists(packagePath)) {
|
|
31
|
+
const content = fs.readFileSync(packagePath, 'utf8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Ignore parse errors
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detect database configuration in a project
|
|
42
|
+
* @param {string} projectPath - Project root path
|
|
43
|
+
* @returns {object} Detection result
|
|
44
|
+
*/
|
|
45
|
+
function detectDatabase(projectPath = process.cwd()) {
|
|
46
|
+
const detections = [];
|
|
47
|
+
|
|
48
|
+
// Check for Convex
|
|
49
|
+
const convexDir = path.join(projectPath, 'convex');
|
|
50
|
+
if (fileExists(convexDir)) {
|
|
51
|
+
const hasSchema = fileExists(path.join(convexDir, 'schema.ts')) ||
|
|
52
|
+
fileExists(path.join(convexDir, 'schema.js'));
|
|
53
|
+
detections.push({
|
|
54
|
+
type: 'convex',
|
|
55
|
+
confidence: 'high',
|
|
56
|
+
configPath: 'convex/',
|
|
57
|
+
hasSchema,
|
|
58
|
+
details: {
|
|
59
|
+
schemaFile: hasSchema ? 'convex/schema.ts' : null,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for Supabase
|
|
65
|
+
const supabaseDir = path.join(projectPath, 'supabase');
|
|
66
|
+
if (fileExists(supabaseDir)) {
|
|
67
|
+
const hasMigrations = fileExists(path.join(supabaseDir, 'migrations'));
|
|
68
|
+
detections.push({
|
|
69
|
+
type: 'supabase',
|
|
70
|
+
confidence: 'high',
|
|
71
|
+
configPath: 'supabase/',
|
|
72
|
+
hasMigrations,
|
|
73
|
+
details: {
|
|
74
|
+
migrationsDir: hasMigrations ? 'supabase/migrations/' : null,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for Prisma
|
|
80
|
+
const prismaDir = path.join(projectPath, 'prisma');
|
|
81
|
+
if (fileExists(prismaDir)) {
|
|
82
|
+
const hasSchema = fileExists(path.join(prismaDir, 'schema.prisma'));
|
|
83
|
+
detections.push({
|
|
84
|
+
type: 'prisma',
|
|
85
|
+
confidence: 'high',
|
|
86
|
+
configPath: 'prisma/',
|
|
87
|
+
hasSchema,
|
|
88
|
+
details: {
|
|
89
|
+
schemaFile: hasSchema ? 'prisma/schema.prisma' : null,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check for Drizzle config
|
|
95
|
+
const drizzleConfig = [
|
|
96
|
+
'drizzle.config.ts',
|
|
97
|
+
'drizzle.config.js',
|
|
98
|
+
'drizzle.config.mjs',
|
|
99
|
+
].find(f => fileExists(path.join(projectPath, f)));
|
|
100
|
+
if (drizzleConfig) {
|
|
101
|
+
detections.push({
|
|
102
|
+
type: 'drizzle',
|
|
103
|
+
confidence: 'high',
|
|
104
|
+
configPath: drizzleConfig,
|
|
105
|
+
details: {
|
|
106
|
+
configFile: drizzleConfig,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check package.json for database dependencies
|
|
112
|
+
const packageJson = readPackageJson(projectPath);
|
|
113
|
+
if (packageJson) {
|
|
114
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
115
|
+
|
|
116
|
+
// Convex from package.json
|
|
117
|
+
if (deps['convex'] && !detections.find(d => d.type === 'convex')) {
|
|
118
|
+
detections.push({
|
|
119
|
+
type: 'convex',
|
|
120
|
+
confidence: 'medium',
|
|
121
|
+
source: 'package.json',
|
|
122
|
+
details: {
|
|
123
|
+
version: deps['convex'],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Supabase from package.json
|
|
129
|
+
if (deps['@supabase/supabase-js'] && !detections.find(d => d.type === 'supabase')) {
|
|
130
|
+
detections.push({
|
|
131
|
+
type: 'supabase',
|
|
132
|
+
confidence: 'medium',
|
|
133
|
+
source: 'package.json',
|
|
134
|
+
details: {
|
|
135
|
+
version: deps['@supabase/supabase-js'],
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Prisma from package.json
|
|
141
|
+
if ((deps['prisma'] || deps['@prisma/client']) && !detections.find(d => d.type === 'prisma')) {
|
|
142
|
+
detections.push({
|
|
143
|
+
type: 'prisma',
|
|
144
|
+
confidence: 'medium',
|
|
145
|
+
source: 'package.json',
|
|
146
|
+
details: {
|
|
147
|
+
version: deps['prisma'] || deps['@prisma/client'],
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Drizzle from package.json
|
|
153
|
+
if (deps['drizzle-orm'] && !detections.find(d => d.type === 'drizzle')) {
|
|
154
|
+
detections.push({
|
|
155
|
+
type: 'drizzle',
|
|
156
|
+
confidence: 'medium',
|
|
157
|
+
source: 'package.json',
|
|
158
|
+
details: {
|
|
159
|
+
version: deps['drizzle-orm'],
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// MongoDB/Mongoose
|
|
165
|
+
if (deps['mongoose']) {
|
|
166
|
+
detections.push({
|
|
167
|
+
type: 'mongodb',
|
|
168
|
+
confidence: 'medium',
|
|
169
|
+
source: 'package.json',
|
|
170
|
+
details: {
|
|
171
|
+
client: 'mongoose',
|
|
172
|
+
version: deps['mongoose'],
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (deps['mongodb'] && !detections.find(d => d.type === 'mongodb')) {
|
|
178
|
+
detections.push({
|
|
179
|
+
type: 'mongodb',
|
|
180
|
+
confidence: 'medium',
|
|
181
|
+
source: 'package.json',
|
|
182
|
+
details: {
|
|
183
|
+
client: 'mongodb',
|
|
184
|
+
version: deps['mongodb'],
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Firebase/Firestore
|
|
190
|
+
if (deps['firebase'] || deps['firebase-admin']) {
|
|
191
|
+
detections.push({
|
|
192
|
+
type: 'firebase',
|
|
193
|
+
confidence: 'medium',
|
|
194
|
+
source: 'package.json',
|
|
195
|
+
details: {
|
|
196
|
+
client: deps['firebase'] ? 'firebase' : 'firebase-admin',
|
|
197
|
+
version: deps['firebase'] || deps['firebase-admin'],
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// PostgreSQL (pg)
|
|
203
|
+
if (deps['pg'] && !detections.find(d => ['prisma', 'drizzle', 'supabase'].includes(d.type))) {
|
|
204
|
+
detections.push({
|
|
205
|
+
type: 'postgresql',
|
|
206
|
+
confidence: 'low',
|
|
207
|
+
source: 'package.json',
|
|
208
|
+
details: {
|
|
209
|
+
client: 'pg',
|
|
210
|
+
version: deps['pg'],
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// MySQL
|
|
216
|
+
if (deps['mysql2'] || deps['mysql']) {
|
|
217
|
+
detections.push({
|
|
218
|
+
type: 'mysql',
|
|
219
|
+
confidence: 'low',
|
|
220
|
+
source: 'package.json',
|
|
221
|
+
details: {
|
|
222
|
+
client: deps['mysql2'] ? 'mysql2' : 'mysql',
|
|
223
|
+
version: deps['mysql2'] || deps['mysql'],
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Sort by confidence (high first)
|
|
230
|
+
const sortedDetections = detections.sort((a, b) => {
|
|
231
|
+
const confidenceOrder = { high: 2, medium: 1, low: 0 };
|
|
232
|
+
return (confidenceOrder[b.confidence] || 0) - (confidenceOrder[a.confidence] || 0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
hasDatabase: sortedDetections.length > 0,
|
|
237
|
+
detections: sortedDetections,
|
|
238
|
+
primary: sortedDetections[0] || null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
detect: detectDatabase,
|
|
244
|
+
detectDatabase,
|
|
245
|
+
};
|
|
@@ -13,7 +13,7 @@ class ExpoDetector extends BaseDetector {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
get priority() {
|
|
16
|
-
return 95; //
|
|
16
|
+
return 95; // Slightly lower than Next.js (100), but high enough for framework detection
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -106,11 +106,11 @@ class ExpoDetector extends BaseDetector {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// Determine confidence based on what we found
|
|
109
|
-
let confidence = 0.
|
|
109
|
+
let confidence = 0.85; // Base confidence for React Native
|
|
110
110
|
if (results.isExpo) {
|
|
111
|
-
confidence = 0.9; // Higher for Expo
|
|
111
|
+
confidence = 0.9; // Higher confidence for Expo projects
|
|
112
112
|
if (results.config) {
|
|
113
|
-
confidence = 0.95; // Even higher with config file
|
|
113
|
+
confidence = 0.95; // Even higher with config file (app.json)
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|