UNPKG

react-genx-cli

Version:

343 lines (293 loc) 11.6 kB
#!/usr/bin/env node const fs = require("fs"); const path = require("path"); const axios = require("axios"); const crypto = require("crypto"); class ReactBuilder { constructor() { this.apiKey = null; this.testFileDir = null; this.outputRoot = null; this.model = null; this.encryptedKey = { data: "fcff299d4934e11819aea82a6c22faaf5644af63d5e5c1f7620d8dff705c8b353b4007bfde5c3761596df6db131d1352525ee44f2c11d6ba8bfd0dc497c52a28995f5b2aa4ec9178c3730505b26378846f4066ee788f5331b6780b52485e06520323959fcd6a9668f8c17bdff91592527edcc4a24edeb4fb2d31db0073a9344ac88d4994d22ab2fa3abae62d6c4375049ebbdbe1d33693c354c57074aac77be0941480de", salt: "82cf1877aa62a67390b539a2f1f8d6b3", iv: "1812e4c990c2a229e2eb0ecf", authTag: "8a4b18ee334c4041c1ec72e07eafd895", }; } decryptKey(password) { if (!this.encryptedKey) { throw new Error("No encrypted key available"); } const { data, salt, iv, authTag } = this.encryptedKey; const key = crypto.pbkdf2Sync( password, Buffer.from(salt, "hex"), 100000, 32, "sha256" ); const decipher = crypto.createDecipheriv( "aes-256-gcm", key, Buffer.from(iv, "hex") ); decipher.setAuthTag(Buffer.from(authTag, "hex")); let decrypted = decipher.update(data, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } getDirectoryTree(dirPath = ".", prefix = "") { let result = ""; try { const items = fs .readdirSync(dirPath) .filter( (item) => ![ "node_modules", ".git", "dist", "tests", "logo.svg", "reportWebVitals.js", "setupTests.js", ].includes(item) ); items.forEach((item, index) => { const itemPath = path.join(dirPath, item); const isLast = index === items.length - 1; const connector = isLast ? "└── " : "├── "; result += prefix + connector + item + "\n"; // Check if it's a directory and recurse if (fs.statSync(itemPath).isDirectory()) { const newPrefix = prefix + (isLast ? " " : "│ "); result += this.getDirectoryTree(itemPath, newPrefix); } }); } catch (error) { result += `Error reading directory: ${error.message}\n`; } return result; } configure(password, options = {}) { this.apiKey = this.decryptKey(password); this.testFileDir = options.testFileDir || path.resolve("../react/tests", "App.test.js"); this.outputRoot = options.outputRoot || process.cwd(); this.model = options.model || "gpt-4"; } async render(appName, options = {}) { if (!this.apiKey) throw new Error("API key not set"); const { noTest = false, customMessage = "", testFileData = "" } = options; let prompt; if (noTest) { // No test case mode prompt = ` CRITICAL REQUIREMENTS: - ONLY code - Generate **React.js code** based on the following requirements. - Follow the EXACT folder structure: - Each file must contain working React component code. - Use functional components + hooks. - all components file format must be .jsx not .js - ensure correct paths of files imports in the components - reactapp/src has: App.js, App.css, index.css, index.js, components/, data/ - Do NOT include explanations or comments. - Ensure imports/exports are correct. - Implement proper form validation if required with appropriate error messages. REQUIREMENTS: ${customMessage} TASK: - Generate ALL React source files required to fulfill the requirements. - Return ONLY valid JSON mapping: { "filePath": "fileContent" } - Provide each and every component correctly as per the requirements DIRECTORY STRUCTURE - Current Directory Structure - Follow the current directory structure and create only essential files and folders other than the given structure - I'm inside reactapp hence I need the code inside src reactapp ${this.getDirectoryTree()} `; } else { // Test case mode (existing functionality) if (!this.testFileDir) throw new Error("Test file not set"); const testData = testFileData || fs.readFileSync(this.testFileDir, "utf8"); prompt = ` CRITICAL REQUIREMENTS: - ONLY code - Generate **React.js code** that passes ALL test cases. - Each file must contain working React component code. - Use functional components + hooks. - all components file format must be .jsx not .js - ensure correct paths of files imports in the components - Do NOT include explanations or comments. - Match exact text as required in test cases (headers, button labels, etc.). - Ensure imports/exports are correct. - If form validation are required then implement them properly with error messages same as given in test cases TEST CASES TO MUST PASS: ${testData} ${ customMessage ? `\nADDITIONAL REQUIREMENTS:\n${customMessage}\n` : "" } TASK: - Generate ALL React source files required to make ALL tests pass. - Return ONLY valid JSON mapping: { "filePath": "fileContent" } - Provide each and every component correctly as per the test cases DIRECTORY STRUCTURE - Follow the EXACT folder structure: - Current Directory Structure - Strictly Follow the current directory structure and create only essential files and folders other than the given structure ${this.getDirectoryTree()} `; } try { const response = await axios.post( "https://api.openai.com/v1/chat/completions", { model: this.model, messages: [ { role: "system", content: "You are an expert React.js developer. Always return pure executable code in JSON format.", }, { role: "user", content: prompt }, ], temperature: 0.2, }, { headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`, }, } ); let raw = response.data.choices[0].message.content.trim(); if (raw.startsWith("```")) { raw = raw.replace(/```(json)?/g, "").trim(); } const files = JSON.parse(raw); for (const [filePath, content] of Object.entries(files)) { const absPath = path.join(this.outputRoot, filePath); fs.mkdirSync(path.dirname(absPath), { recursive: true }); fs.writeFileSync(absPath, content, "utf8"); } return Object.keys(files); } catch (err) { if (err.response) { console.error("API Error:", err.response.data); } else { console.error("Failure:", err.message); } throw err; } } } // CLI Logic function parseArgs() { const args = process.argv.slice(2); const obj = new ReactBuilder(); const options = { password: null, testFile: "../react/tests/App.test.js", outputDir: ".", noTest: false, message: "", }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "-p" && i + 1 < args.length) { options.password = args[i + 1]; i++; // skip next arg } else if (arg === "-t" && i + 1 < args.length) { options.testFile = args[i + 1]; i++; // skip next arg } else if (arg === "-o" && i + 1 < args.length) { options.outputDir = args[i + 1]; i++; // skip next arg } else if (arg === "-msg" && i + 1 < args.length) { options.message = args[i + 1]; i++; // skip next arg } else if (arg === "-nt") { options.noTest = true; } else if (arg === "--help" || arg === "-h") { console.log(` Usage: npx react-genx-cli -p <password> [OPTIONS] with FS Mode 1 - With Test Cases (default): npx react-genx-cli -p <password> [-t <test-file>] [-o <output-dir>] [-msg <additional-prompt>] Mode 2 - No Test Cases: npx react-genx-cli -p <password> -nt -msg <prompt> [-o <output-dir>] Options: -p <password> Password to decrypt API key (required) -t <test-file> Path to test file (optional, default: ../react/tests/App.test.js) -o <output-dir> Output directory (optional, default: ./src) -msg <message> Custom prompt/requirements (required when using -nt, optional otherwise) -nt No test mode - generate based on prompt only -h, --help Show this help message Examples: # With test cases (existing functionality) npx react-genx-cli -p 'iamneo' npx react-genx-cli -p 'iamneo' -t '../react/tests/custom.test.js' npx react-genx-cli -p 'iamneo' -t '../react/tests/app.test.js' -msg 'Add dark theme support' # Without test cases (new functionality) npx react-genx-cli -p 'iamneo' -nt -msg 'Create a todo app with add, delete, and mark complete features' npx react-genx-cli -p 'iamneo' -nt -msg 'Build a calculator with basic arithmetic operations' -o './calculator-src' `); process.exit(0); } } if (!options.password) { console.error("❌ Error: Password (-p) is required"); console.log("Use --help for usage information"); process.exit(1); } if (options.noTest && !options.message) { console.error( "❌ Error: Message (-msg) is required when using no test mode (-nt)" ); console.log("Use --help for usage information"); process.exit(1); } return options; } async function main() { try { const options = parseArgs(); console.log("🚀 Starting React app generation..."); if (options.noTest) { console.log("📝 Mode: No test cases"); console.log(`💬 Prompt: ${options.message}`); } else { console.log("🧪 Mode: Test-driven"); console.log(`📁 Test file: ${options.testFile}`); if (options.message) { console.log(`💬 Additional requirements: ${options.message}`); } } console.log(`📂 Output directory: ${options.outputDir}`); const app = new ReactBuilder(); app.configure(options.password, { testFileDir: options.noTest ? null : options.testFile, outputRoot: options.outputDir, }); const generatedFiles = await app.render("", { noTest: options.noTest, customMessage: options.message, }); console.log("React app built successfully!"); console.log("Generated files:"); generatedFiles.forEach((file) => console.log(` - ${file}`)); } catch (error) { console.error("❌ Build failed:", error.message); process.exit(1); } } if (require.main === module) { main(); } module.exports = ReactBuilder;