offbyt 1.0.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/README.md +2 -0
- package/cli/index.js +2 -0
- package/cli.js +206 -0
- package/core/detector/detectAxios.js +107 -0
- package/core/detector/detectFetch.js +148 -0
- package/core/detector/detectForms.js +55 -0
- package/core/detector/detectSocket.js +341 -0
- package/core/generator/generateControllers.js +17 -0
- package/core/generator/generateModels.js +25 -0
- package/core/generator/generateRoutes.js +17 -0
- package/core/generator/generateServer.js +18 -0
- package/core/generator/generateSocket.js +160 -0
- package/core/index.js +14 -0
- package/core/ir/IRTypes.js +25 -0
- package/core/ir/buildIR.js +83 -0
- package/core/parser/parseJS.js +26 -0
- package/core/parser/parseTS.js +27 -0
- package/core/rules/relationRules.js +38 -0
- package/core/rules/resourceRules.js +32 -0
- package/core/rules/schemaInference.js +26 -0
- package/core/scanner/scanProject.js +58 -0
- package/deploy/cloudflare.js +41 -0
- package/deploy/cloudflareWorker.js +122 -0
- package/deploy/connect.js +198 -0
- package/deploy/flyio.js +51 -0
- package/deploy/index.js +322 -0
- package/deploy/netlify.js +29 -0
- package/deploy/railway.js +215 -0
- package/deploy/render.js +195 -0
- package/deploy/utils.js +383 -0
- package/deploy/vercel.js +29 -0
- package/index.js +18 -0
- package/lib/generator/advancedCrudGenerator.js +475 -0
- package/lib/generator/crudCodeGenerator.js +486 -0
- package/lib/generator/irBasedGenerator.js +360 -0
- package/lib/ir-builder/index.js +16 -0
- package/lib/ir-builder/irBuilder.js +330 -0
- package/lib/ir-builder/rulesEngine.js +353 -0
- package/lib/ir-builder/templateEngine.js +193 -0
- package/lib/ir-builder/templates/index.js +14 -0
- package/lib/ir-builder/templates/model.template.js +47 -0
- package/lib/ir-builder/templates/routes-generic.template.js +66 -0
- package/lib/ir-builder/templates/routes-user.template.js +105 -0
- package/lib/ir-builder/templates/routes.template.js +102 -0
- package/lib/ir-builder/templates/validation.template.js +15 -0
- package/lib/ir-integration.js +349 -0
- package/lib/modes/benchmark.js +162 -0
- package/lib/modes/configBasedGenerator.js +2258 -0
- package/lib/modes/connect.js +1125 -0
- package/lib/modes/doctorAi.js +172 -0
- package/lib/modes/generateApi.js +435 -0
- package/lib/modes/interactiveSetup.js +548 -0
- package/lib/modes/offline.clean.js +14 -0
- package/lib/modes/offline.enhanced.js +787 -0
- package/lib/modes/offline.js +295 -0
- package/lib/modes/offline.v2.js +13 -0
- package/lib/modes/sync.js +629 -0
- package/lib/scanner/apiEndpointExtractor.js +387 -0
- package/lib/scanner/authPatternDetector.js +54 -0
- package/lib/scanner/frontendScanner.js +642 -0
- package/lib/utils/apiClientGenerator.js +242 -0
- package/lib/utils/apiScanner.js +95 -0
- package/lib/utils/codeInjector.js +350 -0
- package/lib/utils/doctor.js +381 -0
- package/lib/utils/envGenerator.js +36 -0
- package/lib/utils/loadTester.js +61 -0
- package/lib/utils/performanceAnalyzer.js +298 -0
- package/lib/utils/resourceDetector.js +281 -0
- package/package.json +20 -0
- package/templates/.env.template +31 -0
- package/templates/advanced.model.template.js +201 -0
- package/templates/advanced.route.template.js +341 -0
- package/templates/auth.middleware.template.js +87 -0
- package/templates/auth.routes.template.js +238 -0
- package/templates/auth.user.model.template.js +78 -0
- package/templates/cache.middleware.js +34 -0
- package/templates/chat.models.template.js +260 -0
- package/templates/chat.routes.template.js +478 -0
- package/templates/compression.middleware.js +19 -0
- package/templates/database.config.js +74 -0
- package/templates/errorHandler.middleware.js +54 -0
- package/templates/express/controller.ejs +26 -0
- package/templates/express/model.ejs +9 -0
- package/templates/express/route.ejs +18 -0
- package/templates/express/server.ejs +16 -0
- package/templates/frontend.env.template +14 -0
- package/templates/model.template.js +86 -0
- package/templates/package.production.json +51 -0
- package/templates/package.template.json +41 -0
- package/templates/pagination.utility.js +110 -0
- package/templates/production.server.template.js +233 -0
- package/templates/rateLimiter.middleware.js +36 -0
- package/templates/requestLogger.middleware.js +19 -0
- package/templates/response.helper.js +179 -0
- package/templates/route.template.js +130 -0
- package/templates/security.middleware.js +78 -0
- package/templates/server.template.js +91 -0
- package/templates/socket.server.template.js +433 -0
- package/templates/utils.helper.js +157 -0
- package/templates/validation.middleware.js +63 -0
- package/templates/validation.schema.js +128 -0
- package/utils/fileWriter.js +15 -0
- package/utils/logger.js +18 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import Groq from 'groq-sdk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
export async function runDoctorAi(projectPath, options) {
|
|
9
|
+
// Fast custom .env loader without dotenv dependency
|
|
10
|
+
try {
|
|
11
|
+
const envContent = await fs.readFile(path.join(projectPath, '.env'), 'utf-8');
|
|
12
|
+
envContent.split('\n').forEach(line => {
|
|
13
|
+
const match = line.match(/^\s*([\w]+)\s*=\s*(.*)?\s*$/);
|
|
14
|
+
if (match) {
|
|
15
|
+
let [_, key, value = ''] = match;
|
|
16
|
+
value = value.replace(/^['"]|['"]$/g, ''); // Remove quotes
|
|
17
|
+
process.env[key] = process.env[key] || value; // Don't override existing env vars
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
} catch (err) { /* ignore if no .env */ }
|
|
21
|
+
|
|
22
|
+
const cmd = options.cmd || 'npm run dev';
|
|
23
|
+
|
|
24
|
+
const groq = process.env.GROQ_API_KEY ? new Groq({ apiKey: process.env.GROQ_API_KEY }) : null;
|
|
25
|
+
|
|
26
|
+
if (!groq) {
|
|
27
|
+
console.warn(chalk.yellow('⚠️ GROQ_API_KEY is not set in environment variables.'));
|
|
28
|
+
console.warn(chalk.yellow('AI analysis will not be available. Please set GROQ_API_KEY to enable doctor-ai.'));
|
|
29
|
+
console.info(chalk.cyan('You can get an API key from https://console.groq.com/keys'));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(chalk.cyan(`\n🩺 Starting Doctor AI... Monitoring command: ${chalk.bold(cmd)}`));
|
|
34
|
+
console.log(chalk.gray(`Watching directory: ${projectPath}\n`));
|
|
35
|
+
|
|
36
|
+
// Split command
|
|
37
|
+
const [command, ...args] = cmd.split(' ');
|
|
38
|
+
// Use shell true to handle npm run dev properly on windows/linux
|
|
39
|
+
const child = spawn(command, args, { cwd: projectPath, shell: true });
|
|
40
|
+
|
|
41
|
+
let errorBuffer = '';
|
|
42
|
+
let analysisInProgress = false;
|
|
43
|
+
|
|
44
|
+
child.stdout.on('data', (data) => {
|
|
45
|
+
process.stdout.write(data);
|
|
46
|
+
const str = data.toString();
|
|
47
|
+
if (str.toLowerCase().includes('error')) {
|
|
48
|
+
errorBuffer += str;
|
|
49
|
+
triggerAnalysis();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
child.stderr.on('data', (data) => {
|
|
54
|
+
process.stderr.write(data);
|
|
55
|
+
errorBuffer += data.toString();
|
|
56
|
+
triggerAnalysis();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
child.on('close', (code) => {
|
|
60
|
+
console.log(chalk.gray(`\nChild process exited with code ${code}`));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Debounce the analysis so we don't trigger it 100 times for one stack trace
|
|
64
|
+
let timeoutId = null;
|
|
65
|
+
function triggerAnalysis() {
|
|
66
|
+
if (analysisInProgress) return;
|
|
67
|
+
|
|
68
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
69
|
+
timeoutId = setTimeout(async () => {
|
|
70
|
+
if (errorBuffer.trim().length > 0 && !analysisInProgress) {
|
|
71
|
+
await analyzeError(errorBuffer);
|
|
72
|
+
errorBuffer = ''; // reset buffer after analysis
|
|
73
|
+
}
|
|
74
|
+
}, 2000); // Wait 2 seconds for stack trace to settle
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function analyzeError(errorText) {
|
|
78
|
+
analysisInProgress = true;
|
|
79
|
+
const spinner = ora('AI is analyzing the error...').start();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// 1. Extract potential files from error text
|
|
83
|
+
const filePathsToRead = extractFilesFromError(errorText, projectPath);
|
|
84
|
+
let fileContents = '';
|
|
85
|
+
|
|
86
|
+
for (const file of filePathsToRead) {
|
|
87
|
+
try {
|
|
88
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
89
|
+
fileContents += `\n--- File: ${path.relative(projectPath, file)} ---\n\`\`\`javascript\n${content}\n\`\`\`\n`;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// ignore files that couldn't be read
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Call Groq
|
|
96
|
+
const prompt = `
|
|
97
|
+
You are an expert Backend Developer and AI debugging assistant.
|
|
98
|
+
The user's application crashed or produced an error.
|
|
99
|
+
Please analyze the following error trace and the involved file codes to determine the root cause and provide a solution.
|
|
100
|
+
|
|
101
|
+
ERROR TRACE:
|
|
102
|
+
${errorText}
|
|
103
|
+
|
|
104
|
+
INVOLVED FILES:
|
|
105
|
+
${fileContents}
|
|
106
|
+
|
|
107
|
+
Please provide:
|
|
108
|
+
1. **Root Cause**: Why did the error happen?
|
|
109
|
+
2. **Solution**: What exactly needs to be changed? Show the code replacements clearly.
|
|
110
|
+
3. **Prevention**: How can this be prevented in the future?
|
|
111
|
+
|
|
112
|
+
Format the output cleanly in Markdown so it can be saved as a report.
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const completion = await groq.chat.completions.create({
|
|
116
|
+
messages: [
|
|
117
|
+
{
|
|
118
|
+
role: "system",
|
|
119
|
+
content: "You are a helpful, expert AI debugging assistant. Respond in clear markdown."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
role: "user",
|
|
123
|
+
content: prompt
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
model: "llama-3.3-70b-versatile",
|
|
127
|
+
temperature: 0.2,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const aiResponse = completion.choices[0]?.message?.content || 'No completion generated.';
|
|
131
|
+
|
|
132
|
+
// 3. Write to report
|
|
133
|
+
const reportPath = path.join(process.cwd(), 'AI_DEBUG_REPORT.md'); // Write to root where CLI was called
|
|
134
|
+
|
|
135
|
+
const reportContent = `# 🩺 Doctor AI Debug Report
|
|
136
|
+
_Generated on ${new Date().toLocaleString()}_
|
|
137
|
+
|
|
138
|
+
## Monitored Command
|
|
139
|
+
\`${cmd}\`
|
|
140
|
+
|
|
141
|
+
` + aiResponse;
|
|
142
|
+
|
|
143
|
+
await fs.writeFile(reportPath, reportContent);
|
|
144
|
+
spinner.succeed(chalk.green(`Analysis complete! Report saved to ${chalk.bold('AI_DEBUG_REPORT.md')}`));
|
|
145
|
+
|
|
146
|
+
} catch (err) {
|
|
147
|
+
spinner.fail(chalk.red('AI Analysis failed.'));
|
|
148
|
+
console.error(chalk.red(err.message));
|
|
149
|
+
} finally {
|
|
150
|
+
analysisInProgress = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function extractFilesFromError(errorText, basePath) {
|
|
155
|
+
// Look for file paths like /path/to/file.js:line:col or C:\path\to\file.js:line:col
|
|
156
|
+
// specifically we look for .js, .jsx, .ts, .tsx files
|
|
157
|
+
const regex = /(?:[a-zA-Z]:[\\/]|[\.\/\\]+)[a-zA-Z0-9_\-\/\\]+\.(js|jsx|ts|tsx)/g;
|
|
158
|
+
const matches = errorText.match(regex) || [];
|
|
159
|
+
|
|
160
|
+
// Deduplicate and resolve absolute paths
|
|
161
|
+
const uniqueFiles = new Set();
|
|
162
|
+
matches.forEach(match => {
|
|
163
|
+
// Ignore node internal files like internal/modules/... or node_modules
|
|
164
|
+
if (match.includes('node_modules') || match.startsWith('internal/')) return;
|
|
165
|
+
|
|
166
|
+
const absPath = path.resolve(basePath, match.trim());
|
|
167
|
+
uniqueFiles.add(absPath);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return Array.from(uniqueFiles);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart API Generation Mode
|
|
3
|
+
* Detects resources from frontend patterns and generates full-stack APIs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { detectResourcesFromFrontend } from '../utils/resourceDetector.js';
|
|
12
|
+
import { generateApiClients } from '../utils/apiClientGenerator.js';
|
|
13
|
+
import { injectApiCalls } from '../utils/codeInjector.js';
|
|
14
|
+
import { generateAdvancedCrudModel, generateAdvancedCrudRoutes } from '../generator/advancedCrudGenerator.js';
|
|
15
|
+
import { generateSQLFiles } from './configBasedGenerator.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Main Smart API Generation Function
|
|
19
|
+
*/
|
|
20
|
+
export async function generateSmartAPI(projectPath, options = {}) {
|
|
21
|
+
// Extract config from options (passed from cli.js)
|
|
22
|
+
const config = options.config || { database: 'mongodb' };
|
|
23
|
+
const isSQL = ['postgresql', 'mysql', 'sqlite'].includes(config.database);
|
|
24
|
+
|
|
25
|
+
console.log(chalk.cyan('\n🎯 offbyt Smart API Generation\n'));
|
|
26
|
+
console.log(chalk.gray('Detecting resources from your frontend...\n'));
|
|
27
|
+
if (isSQL) {
|
|
28
|
+
console.log(chalk.blue(`ðŸ"¦ Using ${config.database.toUpperCase()} database\n`));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const backendPath = path.join(projectPath, 'backend');
|
|
32
|
+
|
|
33
|
+
// ========================================================
|
|
34
|
+
// STEP 1: DETECT RESOURCES FROM FRONTEND
|
|
35
|
+
// ========================================================
|
|
36
|
+
const step1 = ora('Step 1/6: Scanning frontend for data patterns...').start();
|
|
37
|
+
const resources = detectResourcesFromFrontend(projectPath);
|
|
38
|
+
|
|
39
|
+
if (resources.length === 0) {
|
|
40
|
+
step1.fail(chalk.yellow('No resources detected in frontend'));
|
|
41
|
+
console.log(chalk.gray('\nMake sure your frontend has state variables like:'));
|
|
42
|
+
console.log(chalk.gray(' const [products, setProducts] = useState([])'));
|
|
43
|
+
console.log(chalk.gray(' const [users, setUsers] = useState([])'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
step1.succeed(chalk.green(`✅ Detected ${resources.length} resources`));
|
|
48
|
+
|
|
49
|
+
console.log(chalk.cyan('\n📦 Detected Resources:\n'));
|
|
50
|
+
for (const resource of resources) {
|
|
51
|
+
console.log(chalk.white(` • ${resource.name}`) + chalk.gray(` (${resource.singular})`));
|
|
52
|
+
if (resource.fields.length > 0) {
|
|
53
|
+
console.log(chalk.gray(` Fields: ${resource.fields.join(', ')}`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
// ========================================================
|
|
59
|
+
// STEP 2: CREATE BACKEND STRUCTURE
|
|
60
|
+
// ========================================================
|
|
61
|
+
const step2 = ora('Step 2/6: Creating backend structure...').start();
|
|
62
|
+
|
|
63
|
+
const backendExists = fs.existsSync(backendPath);
|
|
64
|
+
createBackendStructure(backendPath, projectPath);
|
|
65
|
+
ensureValidationMiddleware(backendPath);
|
|
66
|
+
if (backendExists) {
|
|
67
|
+
step2.succeed('✅ Backend structure exists and required utility files ensured');
|
|
68
|
+
} else {
|
|
69
|
+
step2.succeed('✅ Backend structure created');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ========================================================
|
|
73
|
+
// STEP 3: GENERATE BACKEND MODELS
|
|
74
|
+
// ========================================================
|
|
75
|
+
const step3 = ora('Step 3/6: Generating backend models...').start();
|
|
76
|
+
const modelsDir = path.join(backendPath, 'models');
|
|
77
|
+
let modelCount = 0;
|
|
78
|
+
|
|
79
|
+
for (const resource of resources) {
|
|
80
|
+
const modelName = capitalize(resource.singular);
|
|
81
|
+
const modelFile = path.join(modelsDir, `${modelName}.js`);
|
|
82
|
+
|
|
83
|
+
// Only create if doesn't exist
|
|
84
|
+
if (!fs.existsSync(modelFile)) {
|
|
85
|
+
const modelCode = generateAdvancedCrudModel(
|
|
86
|
+
resource.name,
|
|
87
|
+
resource.fields,
|
|
88
|
+
false,
|
|
89
|
+
[]
|
|
90
|
+
);
|
|
91
|
+
fs.writeFileSync(modelFile, modelCode, 'utf8');
|
|
92
|
+
modelCount++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
step3.succeed(`✅ Generated ${modelCount} models`);
|
|
97
|
+
|
|
98
|
+
// ========================================================
|
|
99
|
+
// STEP 4: GENERATE BACKEND ROUTES
|
|
100
|
+
// ========================================================
|
|
101
|
+
const step4 = ora('Step 4/6: Generating backend routes...').start();
|
|
102
|
+
const routesDir = path.join(backendPath, 'routes');
|
|
103
|
+
let routeCount = 0;
|
|
104
|
+
|
|
105
|
+
for (const resource of resources) {
|
|
106
|
+
const routeFile = path.join(routesDir, `${resource.name}.routes.js`);
|
|
107
|
+
|
|
108
|
+
// Only create if doesn't exist
|
|
109
|
+
if (!fs.existsSync(routeFile)) {
|
|
110
|
+
const routeCode = generateAdvancedCrudRoutes(
|
|
111
|
+
resource.name,
|
|
112
|
+
resource.fields,
|
|
113
|
+
false,
|
|
114
|
+
[],
|
|
115
|
+
[]
|
|
116
|
+
);
|
|
117
|
+
fs.writeFileSync(routeFile, routeCode, 'utf8');
|
|
118
|
+
routeCount++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
step4.succeed(`✅ Generated ${routeCount} route files`);
|
|
123
|
+
|
|
124
|
+
// ========================================================
|
|
125
|
+
// STEP 4.5: GENERATE SQL FILES (for SQL databases only)
|
|
126
|
+
// ========================================================
|
|
127
|
+
if (isSQL && resources.length > 0) {
|
|
128
|
+
const sqlStep = ora('Generating SQL schema and seed files...').start();
|
|
129
|
+
try {
|
|
130
|
+
// Convert resources array to Map format expected by generateSQLFiles
|
|
131
|
+
const resourcesMap = new Map();
|
|
132
|
+
for (const resource of resources) {
|
|
133
|
+
resourcesMap.set(resource.name, {
|
|
134
|
+
fields: resource.fields || [],
|
|
135
|
+
hasAuth: false
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
generateSQLFiles(backendPath, resourcesMap, config, { relationships: [] });
|
|
140
|
+
sqlStep.succeed(chalk.green('✅ SQL files generated in backend/sql/'));
|
|
141
|
+
console.log(chalk.gray(' • 01_schema.sql'));
|
|
142
|
+
console.log(chalk.gray(' • 02_crud_operations.sql'));
|
|
143
|
+
console.log(chalk.gray(' • 03_relationships_joins.sql'));
|
|
144
|
+
console.log(chalk.gray(' • 04_seed_data.sql\n'));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
sqlStep.fail(`Failed to generate SQL files: ${error.message}`);
|
|
147
|
+
console.error(chalk.red(error.stack));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ========================================================
|
|
152
|
+
// STEP 5: GENERATE FRONTEND API CLIENTS
|
|
153
|
+
// ========================================================
|
|
154
|
+
const step5 = ora('Step 5/6: Generating frontend API clients...').start();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const generatedClients = generateApiClients(projectPath, resources);
|
|
158
|
+
step5.succeed(`✅ Generated ${generatedClients.length} API client files`);
|
|
159
|
+
|
|
160
|
+
console.log(chalk.cyan('\n📠Generated API Clients:\n'));
|
|
161
|
+
for (const file of generatedClients) {
|
|
162
|
+
console.log(chalk.gray(` • ${file}`));
|
|
163
|
+
}
|
|
164
|
+
console.log('');
|
|
165
|
+
} catch (error) {
|
|
166
|
+
step5.fail(`Failed to generate API clients: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ========================================================
|
|
170
|
+
// STEP 6: INJECT API CALLS IN FRONTEND (Optional)
|
|
171
|
+
// ========================================================
|
|
172
|
+
if (options.inject !== false) {
|
|
173
|
+
const step6 = ora('Step 6/6: Injecting API calls in frontend...').start();
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const injectedFiles = injectApiCalls(projectPath, resources);
|
|
177
|
+
|
|
178
|
+
if (injectedFiles.length > 0) {
|
|
179
|
+
step6.succeed(`✅ Injected API calls in ${injectedFiles.length} files`);
|
|
180
|
+
|
|
181
|
+
console.log(chalk.cyan('\n💉 Modified Files:\n'));
|
|
182
|
+
for (const file of injectedFiles.slice(0, 10)) {
|
|
183
|
+
console.log(chalk.gray(` • ${file}`));
|
|
184
|
+
}
|
|
185
|
+
if (injectedFiles.length > 10) {
|
|
186
|
+
console.log(chalk.gray(` ... and ${injectedFiles.length - 10} more`));
|
|
187
|
+
}
|
|
188
|
+
console.log('');
|
|
189
|
+
} else {
|
|
190
|
+
step6.succeed('✅ No injection needed (files already up-to-date)');
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
step6.warn(`Skipped injection: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
console.log(chalk.gray(' âï¸ Skipped frontend injection (--no-inject)\n'));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ========================================================
|
|
200
|
+
// STEP 7: UPDATE SERVER.JS
|
|
201
|
+
// ========================================================
|
|
202
|
+
const serverFile = path.join(backendPath, 'server.js');
|
|
203
|
+
if (fs.existsSync(serverFile)) {
|
|
204
|
+
updateServerWithRoutes(serverFile, resources);
|
|
205
|
+
} else {
|
|
206
|
+
createServerFile(backendPath, resources);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ========================================================
|
|
210
|
+
// SUMMARY
|
|
211
|
+
// ========================================================
|
|
212
|
+
console.log(chalk.green('\n✅ Smart API Generation Complete!\n'));
|
|
213
|
+
|
|
214
|
+
console.log(chalk.cyan('📋 Generated APIs:\n'));
|
|
215
|
+
for (const resource of resources) {
|
|
216
|
+
console.log(chalk.white(` ${resource.name}:`));
|
|
217
|
+
console.log(chalk.gray(` GET /api/${resource.name}`));
|
|
218
|
+
console.log(chalk.gray(` GET /api/${resource.name}/:id`));
|
|
219
|
+
console.log(chalk.gray(` POST /api/${resource.name}`));
|
|
220
|
+
console.log(chalk.gray(` PUT /api/${resource.name}/:id`));
|
|
221
|
+
console.log(chalk.gray(` DELETE /api/${resource.name}/:id`));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(chalk.cyan('\n🚀 Next Steps:\n'));
|
|
225
|
+
console.log(chalk.yellow(' 1. Update backend with new APIs:'));
|
|
226
|
+
console.log(chalk.green(' backendify sync\n'));
|
|
227
|
+
console.log(chalk.gray(' This will generate backend routes & models for your new APIs\n'));
|
|
228
|
+
console.log(chalk.yellow(' 2. Start your servers:'));
|
|
229
|
+
console.log(chalk.gray(' • Start MongoDB: mongod'));
|
|
230
|
+
console.log(chalk.gray(' • Start backend: cd backend && npm run dev'));
|
|
231
|
+
console.log(chalk.gray(' • Start frontend: npm start'));
|
|
232
|
+
console.log(chalk.green('\n Your frontend and backend will be fully connected! 🎉\n'));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create backend structure
|
|
237
|
+
*/
|
|
238
|
+
function createBackendStructure(backendPath, projectPath) {
|
|
239
|
+
const dirs = ['routes', 'models', 'middleware', 'config', 'utils'];
|
|
240
|
+
for (const dir of dirs) {
|
|
241
|
+
const dirPath = path.join(backendPath, dir);
|
|
242
|
+
if (!fs.existsSync(dirPath)) {
|
|
243
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Get templates directory path
|
|
248
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
249
|
+
const __dirname = path.dirname(__filename);
|
|
250
|
+
const templatesDir = path.join(__dirname, '..', '..', 'templates');
|
|
251
|
+
|
|
252
|
+
// Copy pagination utility
|
|
253
|
+
const paginationTemplate = path.join(templatesDir, 'pagination.utility.js');
|
|
254
|
+
const paginationDest = path.join(backendPath, 'utils', 'pagination.js');
|
|
255
|
+
if (fs.existsSync(paginationTemplate) && !fs.existsSync(paginationDest)) {
|
|
256
|
+
fs.copyFileSync(paginationTemplate, paginationDest);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Copy response helper
|
|
260
|
+
const helperTemplate = path.join(templatesDir, 'utils.helper.js');
|
|
261
|
+
const helperDest = path.join(backendPath, 'utils', 'helper.js');
|
|
262
|
+
if (fs.existsSync(helperTemplate) && !fs.existsSync(helperDest)) {
|
|
263
|
+
fs.copyFileSync(helperTemplate, helperDest);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Copy validation schema
|
|
267
|
+
const validationTemplate = path.join(templatesDir, 'validation.schema.js');
|
|
268
|
+
const validationDest = path.join(backendPath, 'middleware', 'validation.js');
|
|
269
|
+
if (fs.existsSync(validationTemplate) && !fs.existsSync(validationDest)) {
|
|
270
|
+
fs.copyFileSync(validationTemplate, validationDest);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Create .env file from template
|
|
274
|
+
const envTemplate = path.join(templatesDir, '.env.template');
|
|
275
|
+
const envDest = path.join(backendPath, '.env');
|
|
276
|
+
if (fs.existsSync(envTemplate) && !fs.existsSync(envDest)) {
|
|
277
|
+
let envContent = fs.readFileSync(envTemplate, 'utf8');
|
|
278
|
+
// Replace DB_NAME placeholder with project name
|
|
279
|
+
const dbName = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
280
|
+
envContent = envContent.replace(/<DB_NAME>/g, dbName);
|
|
281
|
+
fs.writeFileSync(envDest, envContent, 'utf8');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Ensure validation middleware has validateErrors export expected by generated routes.
|
|
287
|
+
*/
|
|
288
|
+
function ensureValidationMiddleware(backendPath) {
|
|
289
|
+
const validationFile = path.join(backendPath, 'middleware', 'validation.js');
|
|
290
|
+
if (!fs.existsSync(validationFile)) return;
|
|
291
|
+
|
|
292
|
+
const content = fs.readFileSync(validationFile, 'utf8');
|
|
293
|
+
if (content.includes('export const validateErrors')) return;
|
|
294
|
+
|
|
295
|
+
let updated = content;
|
|
296
|
+
if (!updated.includes("from 'express-validator'") && !updated.includes('from "express-validator"')) {
|
|
297
|
+
updated = `import { validationResult } from 'express-validator';\n\n` + updated;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const validateErrorsBlock = `export const validateErrors = (req, res, next) => {\n const errors = validationResult(req);\n if (!errors.isEmpty()) {\n return res.status(400).json({\n success: false,\n errors: errors.array()\n });\n }\n next();\n};\n\n`;
|
|
301
|
+
|
|
302
|
+
fs.writeFileSync(validationFile, validateErrorsBlock + updated, 'utf8');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Update server.js with new routes
|
|
307
|
+
*/
|
|
308
|
+
function updateServerWithRoutes(serverFile, resources) {
|
|
309
|
+
let content = fs.readFileSync(serverFile, 'utf8');
|
|
310
|
+
let modified = false;
|
|
311
|
+
|
|
312
|
+
for (const resource of resources) {
|
|
313
|
+
const routeImport = `import ${resource.name}Routes from './routes/${resource.name}.routes.js';`;
|
|
314
|
+
const routeUse = `app.use('/api/${resource.name}', ${resource.name}Routes);`;
|
|
315
|
+
|
|
316
|
+
// Add import if not present
|
|
317
|
+
if (!content.includes(`from './routes/${resource.name}.routes.js'`)) {
|
|
318
|
+
// Find last import
|
|
319
|
+
const lastImportIndex = content.lastIndexOf('import ');
|
|
320
|
+
const endOfLine = content.indexOf('\n', lastImportIndex);
|
|
321
|
+
content = content.slice(0, endOfLine + 1) + routeImport + '\n' + content.slice(endOfLine + 1);
|
|
322
|
+
modified = true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Add route use if not present
|
|
326
|
+
if (!content.includes(`app.use('/api/${resource.name}'`)) {
|
|
327
|
+
// Find where routes are defined (after middleware, before server/app listen)
|
|
328
|
+
const appListenIndex = content.indexOf('app.listen');
|
|
329
|
+
const serverListenIndex = content.indexOf('server.listen');
|
|
330
|
+
const listenCandidates = [appListenIndex, serverListenIndex].filter(idx => idx !== -1);
|
|
331
|
+
const listenIndex = listenCandidates.length > 0 ? Math.min(...listenCandidates) : -1;
|
|
332
|
+
|
|
333
|
+
if (listenIndex !== -1) {
|
|
334
|
+
content = content.slice(0, listenIndex) + routeUse + '\n\n' + content.slice(listenIndex);
|
|
335
|
+
modified = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (modified) {
|
|
341
|
+
fs.writeFileSync(serverFile, content, 'utf8');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Create basic server.js
|
|
347
|
+
*/
|
|
348
|
+
function createServerFile(backendPath, resources) {
|
|
349
|
+
let serverCode = `import express from 'express';
|
|
350
|
+
import cors from 'cors';
|
|
351
|
+
import mongoose from 'mongoose';
|
|
352
|
+
import dotenv from 'dotenv';
|
|
353
|
+
|
|
354
|
+
dotenv.config();
|
|
355
|
+
|
|
356
|
+
const app = express();
|
|
357
|
+
|
|
358
|
+
// Middleware
|
|
359
|
+
app.use(cors());
|
|
360
|
+
app.use(express.json());
|
|
361
|
+
app.use(express.urlencoded({ extended: true }));
|
|
362
|
+
|
|
363
|
+
// MongoDB Connection
|
|
364
|
+
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/offbyt', {
|
|
365
|
+
useNewUrlParser: true,
|
|
366
|
+
useUnifiedTopology: true
|
|
367
|
+
}).then(() => {
|
|
368
|
+
console.log('✅ MongoDB Connected');
|
|
369
|
+
}).catch((err) => {
|
|
370
|
+
console.error('⌠MongoDB Connection Failed:', err.message);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
`;
|
|
374
|
+
|
|
375
|
+
// Add route imports
|
|
376
|
+
for (const resource of resources) {
|
|
377
|
+
serverCode += `import ${resource.name}Routes from './routes/${resource.name}.routes.js';\n`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
serverCode += '\n// Routes\n';
|
|
381
|
+
|
|
382
|
+
// Add route uses
|
|
383
|
+
for (const resource of resources) {
|
|
384
|
+
serverCode += `app.use('/api/${resource.name}', ${resource.name}Routes);\n`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
serverCode += `
|
|
388
|
+
// Health check
|
|
389
|
+
app.get('/health', (req, res) => {
|
|
390
|
+
res.json({ status: 'ok', message: 'Server is running' });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Start server
|
|
394
|
+
const PORT = process.env.PORT || 5000;
|
|
395
|
+
app.listen(PORT, () => {
|
|
396
|
+
console.log(\`🚀 Server running on http://localhost:\${PORT}\`);
|
|
397
|
+
});
|
|
398
|
+
`;
|
|
399
|
+
|
|
400
|
+
fs.writeFileSync(path.join(backendPath, 'server.js'), serverCode, 'utf8');
|
|
401
|
+
|
|
402
|
+
// Create package.json
|
|
403
|
+
const packageJson = {
|
|
404
|
+
name: 'backend',
|
|
405
|
+
version: '1.0.0',
|
|
406
|
+
type: 'module',
|
|
407
|
+
scripts: {
|
|
408
|
+
dev: 'node server.js',
|
|
409
|
+
start: 'node server.js'
|
|
410
|
+
},
|
|
411
|
+
dependencies: {
|
|
412
|
+
express: '^4.18.0',
|
|
413
|
+
mongoose: '^7.0.0',
|
|
414
|
+
cors: '^2.8.5',
|
|
415
|
+
dotenv: '^16.0.0',
|
|
416
|
+
'express-validator': '^7.0.0'
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
fs.writeFileSync(
|
|
421
|
+
path.join(backendPath, 'package.json'),
|
|
422
|
+
JSON.stringify(packageJson, null, 2),
|
|
423
|
+
'utf8'
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Capitalize first letter
|
|
429
|
+
*/
|
|
430
|
+
function capitalize(str) {
|
|
431
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export default { generateSmartAPI };
|
|
435
|
+
|