arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
233 lines (224 loc) ⢠7.99 kB
JavaScript
// src/generate/flows.ts - AI-powered test flow generator
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import fs from 'fs-extra';
import pc from 'picocolors';
const execAsync = promisify(exec);
/**
* Generate test flows by having AI read code and create comprehensive YAML flows
*/
export async function generateFlows(options) {
const { goal, cwd, files, outputDir = '.arela/flows', model } = options;
console.log(pc.bold(pc.cyan('\nš¤ AI Flow Generator\n')));
console.log(pc.gray(`Goal: ${goal}`));
console.log(pc.gray(`Working Directory: ${cwd}\n`));
// Select AI model
const selectedModel = model || await selectBestModel();
console.log(pc.green(`ā
Using ${selectedModel} for flow generation\n`));
// Discover relevant files if not provided
const relevantFiles = files || await discoverRelevantFiles(cwd, goal);
console.log(pc.cyan(`š Reading ${relevantFiles.length} files...\n`));
// Read file contents
const fileContents = await readFiles(cwd, relevantFiles);
// Generate flows using AI
const flows = await generateFlowsWithAI(selectedModel, goal, fileContents);
// Save flows to disk
const outputPath = path.join(cwd, outputDir);
await fs.ensureDir(outputPath);
const savedFlows = [];
for (const flow of flows) {
const flowPath = path.join(outputPath, `${flow.name}.yml`);
await fs.writeFile(flowPath, flow.content);
savedFlows.push({
name: flow.name,
path: flowPath,
content: flow.content,
});
console.log(pc.green(`ā
Generated: ${flow.name}.yml`));
}
console.log(pc.bold(pc.green(`\nš Generated ${savedFlows.length} test flows!\n`)));
console.log(pc.gray('Run them with:'));
savedFlows.forEach(f => {
console.log(pc.cyan(` arela run web --flow ${f.name} --analyze`));
});
return savedFlows;
}
/**
* Select best available AI model
*/
async function selectBestModel() {
// Check for Claude CLI (preferred)
try {
await execAsync('which claude');
return 'claude';
}
catch {
// Claude not available
}
// Check for Codex CLI
try {
await execAsync('which codex');
return 'codex';
}
catch {
throw new Error('No AI CLI found. Install Claude or Codex CLI.');
}
}
/**
* Discover files relevant to the goal
*/
async function discoverRelevantFiles(cwd, goal) {
// Simple heuristic: look for common patterns based on goal
const patterns = [];
// Extract keywords from goal
const keywords = goal.toLowerCase().split(/\s+/);
// Common file patterns
if (keywords.some(k => ['signup', 'register', 'sign'].includes(k))) {
patterns.push('**/signup*', '**/register*', '**/auth*');
}
if (keywords.some(k => ['login', 'signin'].includes(k))) {
patterns.push('**/login*', '**/signin*', '**/auth*');
}
if (keywords.some(k => ['checkout', 'cart', 'payment'].includes(k))) {
patterns.push('**/checkout*', '**/cart*', '**/payment*');
}
// Default: look for pages and components
if (patterns.length === 0) {
patterns.push('**/pages/**/*', '**/components/**/*');
}
const files = [];
// Try multiple common directory structures
const dirsToCheck = ['src', 'app', 'components', 'pages', 'screens'];
for (const dir of dirsToCheck) {
try {
const { stdout } = await execAsync(`find ${cwd}/${dir} -type f \\( -name "*.tsx" -o -name "*.ts" -o -name "*.jsx" -o -name "*.js" \\) 2>/dev/null | head -20`);
const foundFiles = stdout.trim().split('\n').filter(Boolean);
files.push(...foundFiles);
if (files.length >= 20)
break; // Limit to 20 files total
}
catch {
// Directory doesn't exist, continue
}
}
return files;
}
/**
* Read file contents
*/
async function readFiles(cwd, files) {
const contents = new Map();
for (const file of files) {
try {
const fullPath = file.startsWith('/') ? file : path.join(cwd, file);
const content = await fs.readFile(fullPath, 'utf-8');
const relativePath = path.relative(cwd, fullPath);
contents.set(relativePath, content);
}
catch (error) {
console.log(pc.yellow(`ā ļø Could not read ${file}`));
}
}
return contents;
}
/**
* Generate flows using AI
*/
async function generateFlowsWithAI(model, goal, fileContents) {
const prompt = buildPrompt(goal, fileContents);
// Write prompt to temp file
const tempPromptPath = path.join('/tmp', `arela-flow-gen-${Date.now()}.txt`);
await fs.writeFile(tempPromptPath, prompt);
try {
let stdout;
if (model === 'claude') {
const { stdout: result } = await execAsync(`cat "${tempPromptPath}" | claude --print`);
stdout = result;
}
else {
const { stdout: result } = await execAsync(`cat "${tempPromptPath}" | codex --print`);
stdout = result;
}
await fs.remove(tempPromptPath);
// Parse AI response to extract flows
return parseFlowsFromResponse(stdout);
}
catch (error) {
await fs.remove(tempPromptPath);
throw new Error(`AI flow generation failed: ${error.message}`);
}
}
/**
* Build prompt for AI
*/
function buildPrompt(goal, fileContents) {
let prompt = `You are a QA engineer creating comprehensive test flows.
GOAL: ${goal}
CODE FILES:
`;
for (const [file, content] of fileContents) {
prompt += `\n--- ${file} ---\n${content}\n`;
}
prompt += `
TASK: Generate 3 test flows in YAML format for the goal: "${goal}"
IMPORTANT: You MUST respond with EXACTLY 3 YAML code blocks, each wrapped in \`\`\`yaml and \`\`\`.
FLOW TYPES TO GENERATE:
1. Happy path - Everything works perfectly
2. Validation errors - Test form validation
3. Edge cases - Unusual but valid scenarios
EXACT FORMAT FOR EACH FLOW:
\`\`\`yaml
name: Descriptive Flow Name
steps:
- action: navigate
target: /
- action: click
selector: button[data-testid="signup"]
- action: type
selector: input[name="email"]
value: test@example.com
- action: waitFor
selector: .success-message
- action: screenshot
name: success-screen
\`\`\`
AVAILABLE ACTIONS:
- navigate: Go to URL (target: string)
- click: Click element (selector: string)
- type: Fill input (selector: string, value: string)
- waitFor: Wait for element (selector: string, timeout?: number)
- screenshot: Capture screenshot (name: string)
REQUIREMENTS:
- Use specific selectors (data-testid, name, class, or element type)
- Add screenshots at key points
- Include waitFor before checking results
- Keep flows realistic and executable
NOW GENERATE EXACTLY 3 YAML FLOWS (each in its own \`\`\`yaml block):`;
return prompt;
}
/**
* Parse flows from AI response
*/
function parseFlowsFromResponse(response) {
const flows = [];
// Extract YAML blocks from response
const yamlBlocks = response.match(/```yaml\n([\s\S]*?)\n```/g);
if (!yamlBlocks || yamlBlocks.length === 0) {
// Debug: show what we got
console.log(pc.yellow('\nā ļø AI Response (first 500 chars):'));
console.log(pc.gray(response.substring(0, 500)));
throw new Error('No YAML flows found in AI response. Check format above.');
}
yamlBlocks.forEach((block, index) => {
const content = block.replace(/```yaml\n/, '').replace(/\n```/, '');
// Extract name from YAML
const nameMatch = content.match(/name:\s*(.+)/);
const name = nameMatch
? nameMatch[1].trim().toLowerCase().replace(/\s+/g, '-')
: `generated-flow-${index + 1}`;
flows.push({ name, content });
});
return flows;
}
//# sourceMappingURL=flow-generator.js.map