react-genx-cli
Version:
343 lines (293 loc) • 11.6 kB
JavaScript
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;