ai-test-cli 0.1.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/LICENSE +21 -0
- package/README.md +115 -0
- package/bin/aitest.js +2 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +80 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.js +185 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/explain.d.ts +5 -0
- package/dist/commands/explain.js +146 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/fix.d.ts +5 -0
- package/dist/commands/fix.js +1207 -0
- package/dist/commands/fix.js.map +1 -0
- package/dist/commands/generate.d.ts +28 -0
- package/dist/commands/generate.js +1089 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +386 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/run.d.ts +5 -0
- package/dist/commands/run.js +190 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/scan.d.ts +5 -0
- package/dist/commands/scan.js +140 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1789 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/logger.ts","../../src/core/config.ts","../../src/core/detector.ts","../../src/core/ai.ts","../../src/core/scanner.ts","../../src/core/runner.ts","../../src/core/agentic.ts","../../src/core/agentic.js","../../src/commands/generate.ts"],"sourcesContent":["import chalk from 'chalk';\nimport ora from 'ora';\n\nexport const logger = {\n info: (msg: string) => console.log(chalk.blue('ℹ'), msg),\n success: (msg: string) => console.log(chalk.green('✔'), msg),\n error: (msg: string) => console.error(chalk.red('✖'), msg),\n warn: (msg: string) => console.warn(chalk.yellow('⚠'), msg),\n log: (msg: string) => console.log(msg),\n spinner: (text: string) => ora(text),\n};\n","import { cosmiconfig } from 'cosmiconfig';\nimport { writeFile } from 'fs/promises';\nimport { resolve } from 'path';\nimport { logger } from './logger.js';\n\nexport interface AITestConfig {\n provider: string;\n model: string;\n apiKey?: string;\n baseURL?: string;\n customHeaders?: Record<string, string>;\n temperature?: number;\n autoFix?: boolean;\n maxRetries?: number;\n}\n\nconst moduleName = 'aitest';\nconst explorer = cosmiconfig(moduleName);\n\nexport async function loadConfig(): Promise<AITestConfig | null> {\n try {\n const result = await explorer.search();\n if (result) {\n return result.config as AITestConfig;\n }\n return null;\n } catch (error) {\n logger.error(`Failed to load config: ${error}`);\n return null;\n }\n}\n\nexport async function saveConfig(config: AITestConfig, targetDir: string = process.cwd()): Promise<void> {\n const configPath = resolve(targetDir, '.aitestrc.json');\n try {\n await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');\n logger.success(`Saved config to ${configPath}`);\n } catch (error) {\n logger.error(`Failed to save config: ${error}`);\n throw error;\n }\n}\n","import { readFileSync, existsSync } from 'fs';\nimport { resolve } from 'path';\n\nexport interface ProjectInfo {\n language: 'javascript' | 'typescript';\n testRunner: 'jest' | 'vitest' | 'mocha' | 'unknown';\n framework: 'react' | 'vue' | 'angular' | 'node' | 'unknown';\n packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun';\n}\n\nexport function detectProjectInfo(targetDir: string = process.cwd()): ProjectInfo {\n const packageJsonPath = resolve(targetDir, 'package.json');\n const tsconfigPath = resolve(targetDir, 'tsconfig.json');\n \n let packageJson: any = {};\n if (existsSync(packageJsonPath)) {\n try {\n packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\n } catch (e) {\n // ignore\n }\n }\n\n const allDeps = {\n ...(packageJson.dependencies || {}),\n ...(packageJson.devDependencies || {})\n };\n\n const language = existsSync(tsconfigPath) || allDeps['typescript'] ? 'typescript' : 'javascript';\n \n let testRunner: ProjectInfo['testRunner'] = 'unknown';\n if (allDeps['jest']) testRunner = 'jest';\n else if (allDeps['vitest']) testRunner = 'vitest';\n else if (allDeps['mocha']) testRunner = 'mocha';\n\n let framework: ProjectInfo['framework'] = 'unknown';\n if (allDeps['react']) framework = 'react';\n else if (allDeps['vue']) framework = 'vue';\n else if (allDeps['@angular/core']) framework = 'angular';\n else if (allDeps['express'] || allDeps['@nestjs/core'] || allDeps['fastify']) framework = 'node';\n\n let packageManager: ProjectInfo['packageManager'] = 'npm';\n if (existsSync(resolve(targetDir, 'pnpm-lock.yaml'))) packageManager = 'pnpm';\n else if (existsSync(resolve(targetDir, 'yarn.lock'))) packageManager = 'yarn';\n else if (existsSync(resolve(targetDir, 'bun.lockb'))) packageManager = 'bun';\n\n return { language, testRunner, framework, packageManager };\n}\n","import { generateText } from 'ai';\nimport { createOpenAI } from '@ai-sdk/openai';\nimport { createAnthropic } from '@ai-sdk/anthropic';\nimport { createGoogleGenerativeAI } from '@ai-sdk/google';\nimport { createDeepSeek } from '@ai-sdk/deepseek';\nimport { AITestConfig } from './config.js';\nimport * as dotenv from 'dotenv';\n\ndotenv.config();\n\nexport function getAIModel(config: AITestConfig) {\n const providerName = config.provider.toLowerCase();\n \n if (providerName === 'openai') {\n const openai = createOpenAI({\n apiKey: config.apiKey || process.env.OPENAI_API_KEY,\n });\n return openai(config.model || 'gpt-4o');\n } \n \n if (providerName === 'anthropic') {\n const anthropic = createAnthropic({\n apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,\n });\n return anthropic(config.model || 'claude-3-5-sonnet-20240620');\n }\n\n if (providerName === 'gemini' || providerName === 'google') {\n const google = createGoogleGenerativeAI({\n apiKey: config.apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY,\n });\n return google(config.model || 'gemini-2.5-flash');\n }\n\n if (providerName === 'deepseek') {\n const deepseek = createDeepSeek({\n apiKey: config.apiKey || process.env.DEEPSEEK_API_KEY,\n });\n return deepseek(config.model || 'deepseek-coder');\n }\n\n if (providerName === 'ollama') {\n const ollama = createOpenAI({\n baseURL: 'http://localhost:11434/v1',\n apiKey: 'ollama', // API key isn't strictly needed for local Ollama\n });\n return ollama(config.model || 'llama3.1');\n }\n\n if (providerName === 'custom') {\n const custom = createOpenAI({\n baseURL: config.baseURL,\n apiKey: config.apiKey || process.env.CUSTOM_API_KEY || 'custom',\n headers: config.customHeaders,\n });\n return custom(config.model || 'local-model');\n }\n\n throw new Error(`Unsupported AI provider: ${config.provider}`);\n}\n\nexport async function askAI(config: AITestConfig, system: string, prompt: string) {\n const model = getAIModel(config);\n \n const { text } = await generateText({\n model,\n system,\n prompt,\n maxRetries: 5,\n temperature: config.temperature || 0.1,\n });\n\n return text;\n}\n","import { Project } from 'ts-morph';\nimport { existsSync, readFileSync, statSync } from 'fs';\nimport { resolve, dirname } from 'path';\nimport { logger } from './logger.js';\n\nfunction resolveLocalPath(baseDir: string, moduleSpecifier: string): string | null {\n let cleanedSpecifier = moduleSpecifier;\n if (cleanedSpecifier.endsWith('.js')) {\n cleanedSpecifier = cleanedSpecifier.replace(/\\.js$/, '');\n }\n\n const exts = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.js', '/index.tsx', '/index.jsx'];\n const fullPath = resolve(baseDir, cleanedSpecifier);\n \n try {\n const exactPath = resolve(baseDir, moduleSpecifier);\n if (existsSync(exactPath) && statSync(exactPath).isFile()) {\n return exactPath;\n }\n } catch(e) {}\n \n for (const ext of exts) {\n const withExt = fullPath + ext;\n try {\n if (existsSync(withExt) && statSync(withExt).isFile()) {\n return withExt;\n }\n } catch(e) {}\n }\n return null;\n}\n\nexport function scanDependencies(filePath: string, maxTokens = 2000): string {\n try {\n const project = new Project();\n const sourceFile = project.addSourceFileAtPath(filePath);\n \n let context = '';\n const imports = sourceFile.getImportDeclarations();\n const baseDir = dirname(filePath);\n \n for (const imp of imports) {\n const moduleSpecifier = imp.getModuleSpecifierValue();\n \n // Skip node_modules or absolute built-in modules\n if (!moduleSpecifier.startsWith('.') && !moduleSpecifier.startsWith('/')) {\n continue;\n }\n \n const resolvedPath = resolveLocalPath(baseDir, moduleSpecifier);\n if (resolvedPath) {\n const content = readFileSync(resolvedPath, 'utf-8');\n context += `\\n--- Related Context from ${moduleSpecifier} ---\\n${content}\\n`;\n \n if (context.length > maxTokens * 4) {\n context = context.substring(0, maxTokens * 4) + '\\n... [Context Truncated due to size limits]';\n break;\n }\n }\n }\n \n // Look for prisma schema to provide type context\n const possiblePrismaPaths = [\n resolve(process.cwd(), 'prisma/schema.prisma'),\n resolve(process.cwd(), 'schema.prisma')\n ];\n for (const p of possiblePrismaPaths) {\n if (existsSync(p)) {\n const content = readFileSync(p, 'utf-8');\n context += `\\n--- Prisma Schema Context ---\\n${content}\\n`;\n break; // Only include it once\n }\n }\n \n return context.trim();\n } catch (error: any) {\n logger.error(`Architecture Scanner failed for ${filePath}: ${error.message}`);\n return '';\n }\n}\n\nimport fg from 'fast-glob';\n\nexport async function generateWorkspaceMap(): Promise<string> {\n try {\n const files = await fg([\n '**/*.{ts,js,tsx,jsx,json,prisma}',\n '!**/node_modules/**',\n '!**/dist/**',\n '!**/build/**',\n '!**/coverage/**'\n ], { cwd: process.cwd(), dot: true });\n \n let mapStr = files.join('\\n');\n \n // Hard limit the string length to prevent OpenAI API TPM / Token limit crashes.\n // 5,000 characters is roughly ~1,250 tokens.\n if (mapStr.length > 5000) {\n mapStr = mapStr.substring(0, 5000) + '\\n\\n... [WORKSPACE MAP TRUNCATED DUE TO SIZE LIMIT].\\nWARNING: The repository is too large to fit in this prompt. Use your `list_dir` tool to explore directories to find the exact file paths you need.';\n }\n \n return mapStr;\n } catch (error) {\n return '';\n }\n}\n","import { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { ProjectInfo } from './detector.js';\n\nconst execAsync = promisify(exec);\n\nexport interface TestResult {\n passed: boolean;\n output: string;\n}\n\nexport async function runSingleTest(testFilePath: string, projectInfo: ProjectInfo): Promise<TestResult> {\n // Default fallback for unknown runners\n let command = `npm test -- \"${testFilePath}\"`;\n \n if (projectInfo.testRunner === 'jest') {\n command = `npx jest \"${testFilePath}\" --forceExit`;\n } else if (projectInfo.testRunner === 'vitest') {\n command = `npx vitest run \"${testFilePath}\"`;\n } else if (projectInfo.testRunner === 'mocha') {\n command = `npx mocha \"${testFilePath}\" --exit`;\n }\n\n try {\n const { stdout, stderr } = await execAsync(command, { cwd: process.cwd() });\n return {\n passed: true,\n output: `${stdout}\\n${stderr}`.trim()\n };\n } catch (error: any) {\n return {\n passed: false,\n output: `${error.stdout || ''}\\n${error.stderr || ''}\\n${error.message || ''}`.trim()\n };\n }\n}\n","import { AITestConfig } from '../core/config.js';\nimport { ProjectInfo } from '../core/detector.js';\nimport { askAI } from '../core/ai.js';\nimport { runSingleTest } from '../core/runner.js';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { writeFileSync, readFileSync, existsSync, readdirSync } from 'fs';\nimport { resolve, dirname } from 'path';\nimport { generateTestForFile } from '../commands/generate.js';\nimport chalk from 'chalk';\n\nconst execAsync = promisify(exec);\n\ntype AgenticAction =\n | { action: 'apply_patch'; file: string; code: string }\n | { action: 'install_deps'; deps: string[] }\n | { action: 'abort'; reason: string }\n | { action: 'read_file'; path: string }\n | { action: 'list_dir'; path: string };\n\ninterface AgenticPlan {\n reasoning: string;\n steps: AgenticAction[];\n}\n\n/**\n * AgenticPlanner – lightweight LLM‑driven planning wrapper.\n *\n * Workflow:\n * 1️⃣ Generate an initial test (deterministic).\n * 2️⃣ Run the test. If it passes we are done.\n * 3️⃣ If it fails, ask the LLM for a **single‑step JSON plan** describing the next action.\n * 4️⃣ Parse, validate, and execute the plan.\n * 5️⃣ Loop up to a configurable max attempts (default 3).\n */\nexport class AgenticPlanner {\n constructor(public config: AITestConfig, public projectInfo: ProjectInfo, public workspaceMap: string = '') {}\n\n /** Detect if the AI provider is a cloud provider with a massive context window */\n private _isCloudProvider(): boolean {\n const p = (this.config.provider || '').toLowerCase();\n return p.includes('deepseek') || p.includes('openai') || p.includes('anthropic') || p.includes('gemini') || p.includes('google');\n }\n\n /** Generate tests for a file using the LLM planning loop. */\n async generateFile(\n sourceFilePath: string,\n options: { forceUpdate?: boolean; evaluateExisting?: boolean; workspaceMap?: string } = {}\n ): Promise<'generated' | 'skipped' | 'failed'> {\n const { forceUpdate = false, evaluateExisting = false } = options;\n\n let isMassive = false;\n let fileLength = 0;\n try {\n const content = readFileSync(sourceFilePath, 'utf-8');\n fileLength = content.length;\n isMassive = fileLength > 20000;\n } catch(e) {}\n\n let initialResult: 'generated' | 'skipped' | 'failed' = 'failed';\n\n if (isMassive) {\n console.log(chalk.yellow(`\\n⚠ File ${sourceFilePath.split('/').pop()} is massive (${fileLength} chars). Engaging Agentic Chunking Generator...`));\n initialResult = await this._runAgenticGeneratorLoop(sourceFilePath, this._deriveTestPath(sourceFilePath));\n } else {\n // 1️⃣ Create an initial test file using the existing generator.\n initialResult = await generateTestForFile(\n sourceFilePath,\n this.config,\n this.projectInfo,\n forceUpdate,\n evaluateExisting,\n this.workspaceMap\n );\n }\n\n if (initialResult === 'skipped') return 'skipped';\n if (initialResult === 'failed') return 'failed';\n\n // 2️⃣ Enter the repair loop for generation\n return this._runAgenticLoop(this._deriveTestPath(sourceFilePath), sourceFilePath);\n }\n\n /** Run the agentic repair loop on an existing test file. */\n async fixTestFile(testFilePath: string, sourceFilePath?: string): Promise<'generated' | 'skipped' | 'failed'> {\n return this._runAgenticLoop(testFilePath, sourceFilePath);\n }\n\n private async _runAgenticGeneratorLoop(sourceFilePath: string, testFilePath: string): Promise<'generated' | 'skipped' | 'failed'> {\n let attempts = 0;\n const maxAttempts = 50; // Hard limit fallback to protect API billing\n let stagnationCounter = 0;\n let previousStepsHash = '';\n let duplicateCount = 0;\n let testCode = '';\n const history: string[] = [];\n let fileLines: string[] = [];\n try {\n fileLines = readFileSync(sourceFilePath, 'utf-8').split('\\n');\n } catch(e) {\n return 'failed';\n }\n \n // Seed test code with an empty string or existing file if it exists\n if (existsSync(testFilePath)) {\n testCode = readFileSync(testFilePath, 'utf-8');\n } else {\n writeFileSync(testFilePath, testCode, 'utf-8');\n }\n\n console.log(chalk.blue(`ℹ Starting Agentic Chunking Generator for ${sourceFilePath.split('/').pop()} (${fileLines.length} lines)...`));\n\n while (attempts < maxAttempts) {\n attempts++;\n const plan = await this._requestGenPlan(testCode, testFilePath, sourceFilePath, fileLines.length, history);\n if (!plan) {\n console.log(chalk.red(`✖ AI failed to generate a valid generation plan.`));\n return 'failed';\n }\n\n const currentStepsHash = JSON.stringify(plan.steps);\n if (currentStepsHash === previousStepsHash) {\n duplicateCount++;\n if (duplicateCount >= 3) {\n console.log(chalk.red(`✖ AI generated the exact same action 3 times in a row. Forcibly breaking loop.`));\n break;\n }\n } else {\n duplicateCount = 0;\n previousStepsHash = currentStepsHash;\n }\n\n console.log(chalk.cyan(`🤖 AI Generator returned a plan:\\n 🤔 Reasoning: ${plan.reasoning}`));\n \n let isFinished = false;\n let madeProgress = false;\n \n for (const step of plan.steps) {\n if (step.action === 'read_lines') {\n console.log(chalk.cyan(` - 📖 Reading lines ${step.start} to ${step.end}`));\n const startIdx = Math.max(0, step.start - 1);\n const endIdx = Math.min(fileLines.length, step.end);\n const chunk = fileLines.slice(startIdx, endIdx).map((l, i) => `${startIdx + i + 1}: ${l}`).join('\\n');\n history.push(`Attempt ${attempts}: Read lines ${step.start}-${step.end}:\\n${chunk}`);\n } else if (step.action === 'search_file') {\n console.log(chalk.cyan(` - 🔍 Searching for \"${step.query}\"`));\n const matches = fileLines\n .map((line, idx) => ({ line, idx: idx + 1 }))\n .filter(({ line }) => line.includes(step.query))\n .slice(0, 30); // limit to 30 matches\n const matchStr = matches.length > 0 ? matches.map(m => `Line ${m.idx}: ${m.line}`).join('\\n') : 'No matches found.';\n history.push(`Attempt ${attempts}: Searched for \"${step.query}\". Results:\\n${matchStr}`);\n } else if (step.action === 'append_test') {\n console.log(chalk.cyan(` - 📝 Appending test code chunk`));\n testCode += '\\n' + step.code;\n writeFileSync(testFilePath, testCode, 'utf-8');\n history.push(`Attempt ${attempts}: Appended test code.`);\n madeProgress = true;\n } else if (step.action === 'finish') {\n console.log(chalk.green(` - ✅ AI finished generating the test file.`));\n history.push(`Attempt ${attempts}: Finished. Reason: ${step.reason}`);\n isFinished = true;\n }\n }\n\n if (isFinished) {\n return 'generated';\n }\n\n if (madeProgress) {\n stagnationCounter = 0;\n } else {\n stagnationCounter++;\n }\n\n if (stagnationCounter >= 10) {\n console.log(chalk.red(`\\n✖ AI exhausted 10 exploration attempts without making progress. Forcibly breaking loop.`));\n break;\n }\n \n // Wait a brief moment to avoid API spam\n await new Promise(r => setTimeout(r, 500));\n }\n \n if (attempts >= maxAttempts) {\n console.log(chalk.yellow(`\\n⚠ Reached maximum limit of ${maxAttempts} attempts. Safe breaking. To continue generating coverage, run the command again.`));\n }\n \n return testCode.trim().length > 0 ? 'generated' : 'failed';\n }\n\n private async _requestGenPlan(testCode: string, testFilePath: string, sourceFilePath: string, totalLines: number, history: string[]): Promise<any | null> {\n const historyContext = history.length > 0 ? `\\n--- PREVIOUS ACTIONS & RESULTS ---\\n${history.join('\\n\\n')}\\n` : '';\n const testFramework = this.projectInfo.testRunner === 'unknown' ? 'Jest' : this.projectInfo.testRunner;\n \n const isCloud = this._isCloudProvider();\n const testLines = testCode.split('\\n');\n let testContext = '';\n if (!isCloud && testLines.length > 500) {\n const topLines = testLines.slice(0, 50).map((l, i) => `${i + 1}: ${l}`).join('\\n');\n const bottomLines = testLines.slice(-200).map((l, i) => `${testLines.length - 200 + i + 1}: ${l}`).join('\\n');\n testContext = `\\n--- CURRENT TEST FILE PROGRESS (${testFilePath}) ---\\n${topLines}\\n... [${testLines.length - 250} lines omitted for Local LLM context support] ...\\n${bottomLines}`;\n } else {\n const testCodeWithLines = testLines.map((l, i) => `${i + 1}: ${l}`).join('\\n');\n testContext = `\\n--- CURRENT TEST FILE PROGRESS (${testFilePath}) ---\\n${testCodeWithLines || '(Empty)'}`;\n }\n\n const prompt = `You are an expert QA engineer building a test suite for a massive file using an interactive chunking agent.\n--- TARGET FILE INFO ---\nFile: ${sourceFilePath}\nTotal Lines: ${totalLines}\nTest Framework: ${testFramework}\n${testContext}\n${historyContext}\nYour goal is to explore the target file chunk-by-chunk and incrementally build the test suite by appending test blocks.\nBased on the current progress, suggest ONE JSON plan describing your next steps. You can combine multiple actions in one plan.\nThe JSON must follow this exact schema:\n{\n \"reasoning\": \"<explain what you are looking for or writing>\",\n \"steps\": [\n { \"action\": \"search_file\", \"query\": \"<string to search for, e.g. 'export function'>\" }\n | { \"action\": \"read_lines\", \"start\": <line number>, \"end\": <line number> }\n | { \"action\": \"append_test\", \"code\": \"<valid javascript/typescript test code block to append>\" }\n | { \"action\": \"finish\", \"reason\": \"<explanation of completion>\" }\n ]\n}\n\nCRITICAL RULES:\n1. Do NOT wrap the JSON in markdown blocks. Output ONLY raw JSON.\n2. If you don't know where the functions are, use \\`search_file\\` with queries like \"class \", \"function \", or \"module.exports\" to find line numbers.\n3. Once you know the line numbers, use \\`read_lines\\` to read the implementation of a specific function (max 300 lines at a time).\n4. After reading the implementation, use \\`append_test\\` to write the test case(s) for that specific function.\n5. If using \\`append_test\\`, ensure the code is a complete block (e.g. \\`describe('...', () => { ... })\\`).\n6. When you have tested all major functions, use \\`finish\\`.`;\n\n try {\n const raw = await askAI(this.config, 'Generate a JSON plan for incremental test generation.', prompt);\n const cleaned = raw.replace(/```json/g, '').replace(/```/g, '').trim();\n const match = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (!match) return null;\n return JSON.parse(match[0]);\n } catch (error: any) {\n console.log(chalk.red(`\\n✖ AI generator request failed: ${error.message || error}`));\n return null;\n }\n }\n\n /** Core agentic reasoning loop for generating and fixing tests. */\n private async _runAgenticLoop(testFilePath: string, sourceFilePath?: string): Promise<'generated' | 'skipped' | 'failed'> {\n let attempts = 0;\n const maxAttempts = this.config.maxRetries !== undefined ? this.config.maxRetries : 3;\n let lastError = '';\n let testCode = readFileSync(testFilePath, 'utf-8');\n const history: string[] = [];\n const recentPatches: string[] = []; // To detect loops\n const fileLabel = sourceFilePath ? sourceFilePath.split('/').pop() : testFilePath.split('/').pop();\n\n while (maxAttempts === -1 || attempts < maxAttempts) {\n attempts++;\n const result = await runSingleTest(testFilePath, this.projectInfo);\n if (result.passed) {\n if (attempts > 1) {\n console.log(chalk.green(`\\n✔ AI successfully fixed the test for ${fileLabel} after ${attempts - 1} attempt(s)!`));\n } else {\n console.log(chalk.green(`\\n✔ Test ${fileLabel} passed successfully in isolation. No AI fixes were needed!`));\n }\n return 'generated';\n }\n \n lastError = result.output;\n const maxText = maxAttempts === -1 ? '∞' : maxAttempts;\n console.log(chalk.yellow(`\\n⚠ Test failed for ${fileLabel}. Requesting fix from AI (Attempt ${attempts} of ${maxText})...`));\n \n const snippet = lastError.split('\\n').slice(0, 15).join('\\n');\n console.log(chalk.dim(` Error Snippet:\\n ${snippet.replace(/\\n/g, '\\n ')}`));\n\n let explorationContext = '';\n let explorationAttempts = 0;\n let planExecuted = false;\n let previousStepsHash = '';\n let duplicateCount = 0;\n\n while (explorationAttempts < 5 && !planExecuted) {\n explorationAttempts++;\n const plan = await this._requestPlan(lastError, testCode, testFilePath, history, sourceFilePath, explorationContext);\n if (!plan) {\n console.log(chalk.red(`✖ AI failed to generate a valid plan. Giving up on this file.`));\n return 'failed';\n }\n \n const currentStepsHash = JSON.stringify(plan.steps);\n if (currentStepsHash === previousStepsHash) {\n duplicateCount++;\n if (duplicateCount >= 2) {\n console.log(chalk.red(`✖ AI generated the exact same exploration action consecutively. Forcibly breaking loop.`));\n break;\n }\n } else {\n duplicateCount = 0;\n previousStepsHash = currentStepsHash;\n }\n\n console.log(chalk.cyan(`🤖 AI returned a plan:`));\n if (plan.reasoning) {\n console.log(chalk.gray(` 🤔 Reasoning: ${plan.reasoning}`));\n }\n \n let needsToBreakAndRunTests = false;\n \n for (const step of plan.steps) {\n if (step.action === 'read_file') {\n console.log(chalk.cyan(` - 📖 Reading file: ${step.path}`));\n try {\n const content = readFileSync(resolve(process.cwd(), step.path), 'utf-8');\n explorationContext += `\\n--- READ FILE: ${step.path} ---\\n${content}\\n`;\n history.push(`Attempt ${attempts} (Exploration ${explorationAttempts}): Read file ${step.path}`);\n } catch (err: any) {\n explorationContext += `\\n--- FAILED TO READ FILE: ${step.path} ---\\nError: ${err.message}\\n`;\n history.push(`Attempt ${attempts} (Exploration ${explorationAttempts}): Failed to read file ${step.path}`);\n }\n } else if (step.action === 'search_file') {\n console.log(chalk.cyan(` - 🔍 Searching for \"${step.query}\" in ${step.file}`));\n try {\n const content = readFileSync(resolve(process.cwd(), step.file), 'utf-8');\n const matches = content.split('\\n')\n .map((line, idx) => ({ line, idx: idx + 1 }))\n .filter(({ line }) => line.includes(step.query))\n .slice(0, 30);\n const matchStr = matches.length > 0 ? matches.map(m => `Line ${m.idx}: ${m.line}`).join('\\n') : 'No matches found.';\n explorationContext += `\\n--- SEARCH RESULTS FOR \"${step.query}\" IN ${step.file} ---\\n${matchStr}\\n`;\n history.push(`Attempt ${attempts} (Exploration ${explorationAttempts}): Searched for \"${step.query}\" in ${step.file}`);\n } catch (err: any) {\n explorationContext += `\\n--- FAILED TO SEARCH FILE: ${step.file} ---\\nError: ${err.message}\\n`;\n history.push(`Attempt ${attempts} (Exploration ${explorationAttempts}): Failed to search file ${step.file}`);\n }\n } else if (step.action === 'read_lines') {\n console.log(chalk.cyan(` - 📖 Reading lines ${step.startLine}-${step.endLine} from ${step.file}`));\n try {\n const content = readFileSync(resolve(process.cwd(), step.file), 'utf-8');\n const lines = content.split('\\n');\n const startIdx = Math.max(0, step.startLine - 1);\n const endIdx = Math.min(lines.length, step.endLine);\n const chunk = lines.slice(startIdx, endIdx).map((l, i) => `${startIdx + i + 1}: ${l}`).join('\\n');\n explorationContext += `\\n--- READ LINES ${step.startLine}-${step.endLine} FROM ${step.file} ---\\n${chunk}\\n`;\n history.push(`Attempt ${attempts} (Exploration ${explorationAttempts}): Read lines ${step.startLine}-${step.endLine} from ${step.file}`);\n } catch (err: any) {\n explorationContext += `\\n--- FAILED TO READ LINES: ${step.file} ---\\nError: ${err.message}\\n`;\n history.push(`Attempt ${attempts} (Exploration ${explorationAttempts}): Failed to read lines from ${step.file}`);\n }\n } else if (step.action === 'list_dir') {\n console.log(chalk.cyan(` - 📂 Listing directory: ${step.path}`));\n try {\n const files = readdirSync(resolve(process.cwd(), step.path));\n explorationContext += `\\n--- DIRECTORY: ${step.path} ---\\n${files.join('\\n')}\\n`;\n history.push(`Attempt ${attempts} (Exploration ${explorationAttempts}): Listed directory ${step.path}`);\n } catch (err: any) {\n explorationContext += `\\n--- FAILED TO LIST DIRECTORY: ${step.path} ---\\nError: ${err.message}\\n`;\n history.push(`Attempt ${attempts} (Exploration ${explorationAttempts}): Failed to list directory ${step.path}`);\n }\n } else if (step.action === 'replace_lines') {\n needsToBreakAndRunTests = true;\n console.log(chalk.cyan(` - 📝 Replace lines ${step.startLine}-${step.endLine} in ${step.file.split('/').pop()}`));\n history.push(`Attempt ${attempts}: Replaced lines ${step.startLine}-${step.endLine} in ${step.file.split('/').pop()}`);\n \n // Anti-loop safeguard: If AI applies the exact same code 3 times, hard abort.\n recentPatches.push(step.replacementCode);\n if (recentPatches.length > 3) recentPatches.shift();\n if (recentPatches.length === 3 && recentPatches.every(code => code === step.replacementCode)) {\n console.log(chalk.red(`✖ AI generated the exact same patch 3 times in a row. Forcibly breaking infinite loop.`));\n return 'failed';\n }\n } else if (step.action === 'install_deps') {\n needsToBreakAndRunTests = true;\n console.log(chalk.cyan(` - 📦 Install dependencies: ${step.deps.join(', ')}`));\n history.push(`Attempt ${attempts}: Installed dependencies ${step.deps.join(', ')}`);\n } else if (step.action === 'abort') {\n needsToBreakAndRunTests = true;\n console.log(chalk.cyan(` - 🛑 Abort: ${step.reason}`));\n history.push(`Attempt ${attempts}: Aborted with reason: ${step.reason}`);\n \n if (sourceFilePath) {\n const bugFile = resolve(process.cwd(), 'aitest-bugs.md');\n const fs = await import('fs');\n fs.appendFileSync(bugFile, `\\n## Source Code Issue Detected in ${sourceFilePath}\\n**AI Reasoning**: ${plan.reasoning || 'No reasoning provided.'}\\n**Abort Reason**: ${step.reason}\\n`, 'utf-8');\n console.log(chalk.yellow(` ⚠ Issue logged to aitest-bugs.md for later fixing.`));\n }\n }\n }\n\n if (needsToBreakAndRunTests) {\n // 4️⃣ Execute the plan.\n const execResult = await this._executePlan(plan);\n if (execResult === 'abort') {\n console.log(chalk.red(`✖ AI plan execution aborted.`));\n return 'skipped';\n }\n planExecuted = true;\n } else {\n // It just explored! Wait a brief moment before looping to avoid slamming the API too fast\n await new Promise(r => setTimeout(r, 500));\n }\n }\n \n if (!planExecuted) {\n console.log(chalk.red(`✖ AI exhausted exploration limit without applying a patch. Giving up on this file.`));\n return 'failed';\n }\n\n // Reload test code after a possible patch.\n if (existsSync(testFilePath)) {\n testCode = readFileSync(testFilePath, 'utf-8');\n }\n }\n \n console.log(chalk.red(`\\n✖ Exhausted ${maxAttempts} AI attempts without passing.`));\n return 'failed'; // exhausted attempts\n }\n\n /** Derive the .test.* filename from a source file path. */\n private _deriveTestPath(sourceFilePath: string): string {\n const ext = sourceFilePath.substring(sourceFilePath.lastIndexOf('.'));\n const base = sourceFilePath.substring(\n sourceFilePath.lastIndexOf('/') + 1,\n sourceFilePath.length - ext.length\n );\n const dir = dirname(sourceFilePath);\n return resolve(dir, `${base}.test${ext}`);\n }\n\n /** Prompt the LLM for a JSON plan based on a failed test run. */\n private async _requestPlan(errorOutput: string, testCode: string, testFilePath: string, history: string[], sourceFilePath?: string, explorationContext: string = ''): Promise<AgenticPlan | null> {\n const historyContext = history.length > 0 ? `\\n--- PREVIOUS ACTIONS TAKEN ---\\n${history.join('\\n')}\\nWARNING: The test is still failing. Do NOT suggest the exact same action again.` : '';\n let sourceContext = '';\n if (sourceFilePath && existsSync(sourceFilePath)) {\n const sourceContent = readFileSync(sourceFilePath, 'utf-8');\n const sourceLines = sourceContent.split('\\n');\n if (sourceLines.length > 1000) {\n sourceContext = `\\n--- SOURCE FILE INFO ---\\nThe source file (${sourceFilePath}) is massive (${sourceLines.length} lines) and has been omitted from this prompt to save context and improve accuracy. You MUST use the \"search_file\" and \"read_lines\" actions to dynamically read the specific functions you need to fix the test.`;\n } else {\n sourceContext = `\\n--- SOURCE FILE (${sourceFilePath}) ---\\n${sourceContent}`;\n }\n }\n const workspaceContext = this.workspaceMap ? `\\n--- WORKSPACE FILE STRUCTURE ---\\n${this.workspaceMap}` : '';\n const explorationContextString = explorationContext ? `\\n--- EXPLORATION CONTEXT ---\\n${explorationContext}` : '';\n \n const isCloud = this._isCloudProvider();\n const testLines = testCode.split('\\n');\n let testContext = '';\n \n if (!isCloud && testLines.length > 500) {\n testContext = `\\n--- TEST FILE INFO ---\\nThe test file (${testFilePath}) is massive (${testLines.length} lines) and has been omitted to support Local LLMs. Look at the ERROR OUTPUT to find the line number of the failing test (e.g., file.test.js:6540). You MUST use the \"read_lines\" action to read the test file around that line number before applying a \"replace_lines\" patch.`;\n } else {\n const testCodeWithLines = testLines.map((line, idx) => `${idx + 1}: ${line}`).join('\\n');\n testContext = `\\n--- TEST FILE (${testFilePath}) ---\\n${testCodeWithLines}`;\n }\n\n const prompt = `You are an expert QA engineer. The following test has failed.${sourceContext}${workspaceContext}${explorationContextString}${testContext}\n--- ERROR OUTPUT ---\n${errorOutput}${historyContext}\nBased on this information, suggest ONE JSON plan describing the next actionable step. The JSON must follow this schema:\n{ \"reasoning\": \"<explain the failure and your fix or exploration strategy>\", \"steps\": [ { \"action\": \"replace_lines\", \"file\": \"${testFilePath}\", \"startLine\": <number>, \"endLine\": <number>, \"replacementCode\": \"<new test code to replace the specified lines>\" } | { \"action\": \"search_file\", \"file\": \"<path>\", \"query\": \"<string to search for>\" } | { \"action\": \"read_lines\", \"file\": \"<path>\", \"startLine\": <number>, \"endLine\": <number> } | { \"action\": \"read_file\", \"path\": \"<relative path to file>\" } | { \"action\": \"list_dir\", \"path\": \"<relative path to dir>\" } | { \"action\": \"install_deps\", \"deps\": [\"<package>\"] } | { \"action\": \"abort\", \"reason\": \"<text>\" } ] }\nCRITICAL RULES:\n1. To explore massive files safely, use \"search_file\" to find function signatures, then \"read_lines\" to read the implementation block. For small files, use \"read_file\". If you need to see what files exist in a directory, use \"list_dir\".\n2. You may ONLY patch the test file (${testFilePath}) using the \"replace_lines\" action.\n3. For \"replace_lines\", provide the exact startLine and endLine numbers based on the line numbers shown in the TEST FILE block. To insert code without removing any lines, set startLine and endLine to the line number where you want to insert.\n4. Do NOT wrap the JSON in markdown code blocks. Output ONLY the raw JSON object.\n5. If you cannot fix the issue, return an \"abort\" action.`;\n try {\n const raw = await askAI(this.config, 'Generate a JSON plan to fix the failing test.', prompt);\n const plan = this._parsePlan(raw);\n if (plan && this._validatePlan(plan, testFilePath)) return plan;\n return null;\n } catch (error: any) {\n console.log(chalk.red(`\\n✖ AI request failed: ${error.message || error}`));\n return null;\n }\n }\n\n /** Extract JSON from LLM response and parse it. */\n private _parsePlan(text: string): AgenticPlan | null {\n try {\n const cleaned = text.replace(/```json/g, '').replace(/```/g, '').trim();\n const match = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (!match) return null;\n const obj = JSON.parse(match[0]);\n if (obj && Array.isArray(obj.steps)) return obj as AgenticPlan;\n } catch (e) {\n // malformed JSON\n }\n return null;\n }\n\n /** Validate that the plan only contains allowed actions and safe file paths. */\n private _validatePlan(plan: AgenticPlan, testFilePath: string): boolean {\n const allowed = new Set(['replace_lines', 'install_deps', 'abort', 'read_file', 'list_dir', 'search_file', 'read_lines']);\n for (const step of plan.steps) {\n if (!allowed.has(step.action)) return false;\n if (step.action === 'replace_lines') {\n const abs = resolve(step.file);\n if (abs !== testFilePath) return false; // Strictly enforce only patching the test file\n if (typeof (step as any).startLine !== 'number') return false;\n if (typeof (step as any).endLine !== 'number') return false;\n if (typeof (step as any).replacementCode !== 'string') return false;\n }\n if (step.action === 'install_deps') {\n if (!Array.isArray((step as any).deps)) return false;\n }\n if (step.action === 'read_file' || step.action === 'list_dir') {\n if (typeof (step as any).path !== 'string') return false;\n }\n if (step.action === 'search_file') {\n if (typeof (step as any).file !== 'string') return false;\n if (typeof (step as any).query !== 'string') return false;\n }\n if (step.action === 'read_lines') {\n if (typeof (step as any).file !== 'string') return false;\n if (typeof (step as any).startLine !== 'number') return false;\n if (typeof (step as any).endLine !== 'number') return false;\n }\n }\n return true;\n }\n\n /** Execute a validated plan step‑by‑step. */\n private async _executePlan(plan: AgenticPlan): Promise<'continue' | 'abort'> {\n for (const step of plan.steps) {\n switch (step.action) {\n case 'replace_lines': {\n const fileContent = existsSync(step.file) ? readFileSync(step.file, 'utf-8') : '';\n const lines = fileContent.split('\\n');\n const startIdx = Math.max(0, step.startLine - 1);\n const endIdx = Math.min(lines.length, step.endLine);\n \n const newLines = (step as any).replacementCode.split('\\n');\n lines.splice(startIdx, endIdx - startIdx, ...newLines);\n \n writeFileSync(step.file, lines.join('\\n'), 'utf-8');\n break;\n }\n case 'install_deps': {\n const deps = (step as any).deps.join(' ');\n try {\n await execAsync(`npm install --save-dev ${deps}`, { cwd: process.cwd() });\n } catch (e) {\n return 'abort';\n }\n break;\n }\n case 'abort': {\n return 'abort';\n }\n }\n }\n return 'continue';\n }\n}\n","// Shim to allow importing the TypeScript implementation as JavaScript.\nexport * from './agentic.ts';\n","import { Command } from 'commander';\nimport { loadConfig, AITestConfig } from '../core/config.js';\nimport { logger } from '../core/logger.js';\nimport { detectProjectInfo, ProjectInfo } from '../core/detector.js';\nimport { runSingleTest } from '../core/runner.js';\nimport { askAI } from '../core/ai.js';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { resolve, relative, dirname, basename, extname } from 'path';\nimport fg from 'fast-glob';\nimport { scanDependencies, generateWorkspaceMap } from '../core/scanner.js';\nimport chalk from 'chalk';\n\nconst execAsync = promisify(exec);\nconst delay = (ms: number) => new Promise(res => setTimeout(res, ms));\n\n/**\n * Generate tests for a single file.\n * Returns \"generated\", \"skipped\" or \"failed\".\n */\nexport async function generateTestForFile(\n filePath: string,\n config: AITestConfig,\n projectInfo: ProjectInfo,\n forceUpdate = false,\n evaluateExisting = false,\n workspaceMap = ''\n): Promise<'generated' | 'skipped' | 'failed'> {\n const relPath = relative(process.cwd(), filePath);\n const dir = dirname(filePath);\n const ext = extname(filePath);\n const name = basename(filePath, ext);\n const testFilePath = resolve(dir, `${name}.test${ext}`);\n\n const spinner = logger.spinner(`Analyzing ${relPath}...`).start();\n\n if (existsSync(testFilePath) && !forceUpdate) {\n spinner.info(`Evaluating existing test file for missing coverage: ${relPath}`);\n evaluateExisting = true;\n }\n\n let fileContent = '';\n try {\n fileContent = readFileSync(filePath, 'utf-8');\n } catch (e: any) {\n spinner.fail(`Could not read file ${filePath}`);\n return 'failed';\n }\n\n const depsContext = await scanDependencies(filePath);\n let additionalContext = depsContext\n ? `\\n\\n--- ARCHITECTURAL CONTEXT (DEPENDENCIES) ---\\n${depsContext}`\n : '';\n\n if (workspaceMap) {\n additionalContext += `\\n\\n--- WORKSPACE FILE STRUCTURE ---\\n${workspaceMap}`;\n }\n\n let existingTestContext = '';\n if (existsSync(testFilePath)) {\n const existingCode = readFileSync(testFilePath, 'utf-8');\n existingTestContext = `\\n\\n--- EXISTING TEST SUITE (NEEDS IMPROVEMENT) ---\\n${existingCode}\\n`;\n }\n\n try {\n const promptContext = `Original file name: ${relPath}\\n\\nCode:\\n${fileContent}\\n\\n${additionalContext}${existingTestContext}`;\n\n let promptInstructions = '';\n if (existingTestContext) {\n if (forceUpdate) {\n promptInstructions =\n 'You are an expert QA and software testing engineer. The provided existing test suite does not have 100% code coverage. Analyze the source code and the existing test suite, and REWRITE the test suite to include new test cases that cover the missing branches and logic. Output ONLY valid executable code (with markdown code blocks) and nothing else.';\n } else if (evaluateExisting) {\n promptInstructions =\n 'You are an expert QA and software testing engineer. Analyze the source code and the existing test suite to evaluate its code coverage. If the existing test suite is missing test cases for certain branches or logic, REWRITE the test suite to include new test cases that cover the missing parts. Output ONLY valid executable code (with markdown code blocks) and nothing else. If the existing test suite already fully covers the source code, reply ONLY with the exact string \"SKIP_FILE\".';\n }\n } else {\n const testFramework = projectInfo.testRunner === 'unknown' ? 'Jest' : projectInfo.testRunner;\n promptInstructions = `You are an expert QA and software testing engineer. Generate a comprehensive unit test suite for the provided code. Output ONLY valid executable code (with markdown code blocks) and nothing else. The target test framework is ${testFramework}. If the file is purely type definitions, interfaces, simple exports, or does not contain testable logic, reply ONLY with the exact string \"SKIP_FILE\" and do not generate any code. You will be provided with the target code and its imported dependencies to ensure you have the full architectural context.\\nCRITICAL: If testing Prisma or databases, use standard mock techniques (e.g. jest-mock-extended or vi.mock). If using Vitest, use vi.mocked() to mock imported functions to avoid TypeScript errors like 'Property does not exist on type'.`;\n }\n\n const generatedCode = await askAI(config, promptInstructions, promptContext);\n\n if (generatedCode.includes('SKIP_FILE')) {\n spinner.info(`Skipped ${relPath}: No testable logic found.`);\n return 'skipped';\n }\n\n let testCode = generatedCode;\n const match =\n generatedCode.match(/```(?:javascript|typescript|ts|js)?\\n([\\s\\S]*?)```/) ||\n generatedCode.match(/```[\\w]*\\n([\\s\\S]*?)```/);\n if (match && match[1]) {\n testCode = match[1].trim();\n }\n\n if (!testCode || testCode.trim() === '') {\n spinner.warn(`AI returned empty code for ${relPath}. Skipping to prevent writing an empty file.`);\n return 'skipped';\n }\n\n writeFileSync(testFilePath, testCode, 'utf-8');\n spinner.stop();\n return 'generated';\n } catch (error: any) {\n spinner.fail(`Error generating test for ${relPath}: ${error.message}`);\n return 'failed';\n }\n}\n\nexport const generateCommand = new Command('generate')\n .description('Generate missing unit tests for the current project using Agentic AI')\n .option('-a, --all', 'Generate tests for all missing files')\n .option('-f, --file <path>', 'Generate tests for a specific file')\n .option('-u, --force-update', 'Force rewrite existing test files (ignores code coverage logic)')\n .option('-e, --evaluate-existing', 'Evaluate existing tests and ONLY rewrite if missing code coverage')\n .action(async (options) => {\n const config = await loadConfig();\n if (!config) {\n logger.error('Configuration not found. Run `aitest init` first.');\n return;\n }\n if (!options.file && !options.all && !options.coverage) {\n logger.error('Please specify --file <path>, --all, or --coverage');\n return;\n }\n const projectInfo = detectProjectInfo();\n const workspaceMap = await generateWorkspaceMap();\n\n if (options.coverage) {\n logger.info('Running coverage-driven test generation...');\n let testCommand = 'npm test -- --coverage';\n if (projectInfo.testRunner === 'mocha' || projectInfo.testRunner === 'unknown') {\n testCommand = 'npm test --coverage';\n }\n const spinner = logger.spinner('Running test suite to collect coverage metrics...').start();\n try {\n const { stdout } = await execAsync(testCommand, { cwd: process.cwd() });\n spinner.succeed('Coverage data collected.');\n const analyzeSpinner = logger.spinner('Analyzing coverage gaps with AI...').start();\n const aiResponse = await askAI(\n config,\n 'Extract all file paths from this coverage report that have less than 100% coverage (statement, branch, or function). Return ONLY a JSON array of string paths (e.g. [\"src/math.js\"]). Do not output anything else. If no files are listed, return [].',\n `STDOUT:\\n${stdout.slice(-5000)}`\n );\n analyzeSpinner.succeed('Coverage gaps identified.');\n \n const match = aiResponse.match(/\\[[\\s\\S]*\\]/);\n if (match) {\n const filesToFix: string[] = JSON.parse(match[0]);\n if (filesToFix.length === 0) {\n logger.success('All files have 100% coverage!');\n return;\n }\n logger.info(`Found ${filesToFix.length} files missing coverage. Starting targeted generation...`);\n for (const file of filesToFix) {\n const fullPath = resolve(process.cwd(), file);\n if (existsSync(fullPath)) {\n await generateTestForFile(fullPath, config, projectInfo, true, false, workspaceMap);\n }\n }\n logger.success(`\\nFinished coverage-driven generation for ${filesToFix.length} files.`);\n return;\n } else {\n logger.error('Failed to parse AI response for coverage files.');\n return;\n }\n } catch (e: any) {\n spinner.fail(`Failed to collect coverage data: ${e.message}`);\n return;\n }\n }\n\n if (options.all) {\n logger.info('Scanning project for source files...');\n const files = await fg(['**/*.{js,ts,jsx,tsx}'], {\n ignore: [\n '**/node_modules/**',\n '**/dist/**',\n '**/build/**',\n '**/coverage/**',\n '**/*.test.*',\n '**/*.spec.*',\n '**/vite.config.*',\n '**/tsup.config.*',\n '**/.*'\n ],\n cwd: process.cwd()\n });\n if (files.length === 0) {\n logger.info('No source files found.');\n return;\n }\n logger.info(`Found ${files.length} files. Starting test generation using Agentic AI...`);\n let successCount = 0;\n let skippedCount = 0;\n \n const { AgenticPlanner } = await import('../core/agentic.js');\n const planner = new AgenticPlanner(config, projectInfo, workspaceMap);\n \n const startTime = Date.now();\n for (let i = 0; i < files.length; i++) {\n const file = files[i];\n const fullPath = resolve(process.cwd(), file);\n const result = await planner.generateFile(fullPath, { forceUpdate: options.forceUpdate, evaluateExisting: options.evaluateExisting, workspaceMap });\n if (result === 'generated') successCount++; else if (result === 'skipped') skippedCount++;\n }\n \n logger.success(`\\nFinished! Generated tests for ${successCount} files (Skipped ${skippedCount} files). Total processed: ${files.length}.`);\n if (successCount > 0) {\n const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);\n console.log(chalk.magentaBright(`\\n✨ AI successfully generated tests for ${successCount} files in ${elapsedSeconds}s! Saved you ~${successCount * 2} hours of typing.`));\n \n // Dynamically import terminal-link\n const terminalLink = (await import('terminal-link')).default;\n const tweetUrl = `https://twitter.com/intent/tweet?text=I%20just%20used%20%40aitestcli%20to%20autonomously%20generate%20${successCount}%20test%20suites%20in%20${elapsedSeconds}s!%20%F0%9F%A4%AF%0A%0A%23ai%20%23testing%20%23javascript&url=https://www.npmjs.com/package/ai-test-cli`;\n console.log(chalk.cyan('🚀 ' + terminalLink('Share your win on Twitter!', tweetUrl)));\n console.log(chalk.yellow('☕ ' + terminalLink('Buy the creator a coffee!', 'https://buymeacoffee.com/cijaytechnh')) + '\\n');\n }\n process.exit(0);\n } else if (options.file) {\n if (options.file.includes('.test.') || options.file.includes('.spec.')) {\n logger.error(`\\n✖ Oops! You passed a test file (${options.file}) to the generate command.`);\n logger.info(`💡 The 'generate' command expects the SOURCE file (e.g. app/controllers/AdminController.js).`);\n logger.info(`💡 If you want to evaluate/extend this existing test, point the generate command to the SOURCE file.`);\n logger.info(`💡 If you want to fix this broken test file, run: aitest fix`);\n process.exit(1);\n }\n\n const filePath = resolve(process.cwd(), options.file);\n const { AgenticPlanner } = await import('../core/agentic.js');\n const planner = new AgenticPlanner(config, projectInfo, workspaceMap);\n \n logger.info(`Starting Agentic test generation for ${options.file}...`);\n const startTime = Date.now();\n const result = await planner.generateFile(filePath, { forceUpdate: options.forceUpdate, evaluateExisting: options.evaluateExisting, workspaceMap });\n if (result === 'generated') {\n const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);\n logger.success(`\\nFinished! Successfully generated test for ${options.file}.`);\n console.log(chalk.magentaBright(`\\n✨ AI successfully generated the test suite in ${elapsedSeconds}s! Saved you ~2 hours of typing.`));\n \n // Dynamically import terminal-link\n const terminalLink = (await import('terminal-link')).default;\n const tweetUrl = `https://twitter.com/intent/tweet?text=I%20just%20used%20%40aitestcli%20to%20autonomously%20generate%20a%20test%20suite%20in%20${elapsedSeconds}s!%20%F0%9F%A4%AF%0A%0A%23ai%20%23testing%20%23javascript&url=https://www.npmjs.com/package/ai-test-cli`;\n console.log(chalk.cyan('🚀 ' + terminalLink('Share your win on Twitter!', tweetUrl)));\n console.log(chalk.yellow('☕ ' + terminalLink('Buy the creator a coffee!', 'https://buymeacoffee.com/cijaytechnh')) + '\\n');\n } else if (result === 'skipped') {\n logger.info(`\\nSkipped test generation for ${options.file}.`);\n } else {\n logger.error(`\\nFailed to generate test for ${options.file}.`);\n }\n process.exit(0);\n }\n });\n"],"mappings":"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","names":["config","existsSync","readFileSync","resolve","exec","promisify","readFileSync","existsSync","resolve","dirname","chalk","execAsync","config","init_agentic","exec","promisify","readFileSync","writeFileSync","existsSync","resolve","dirname","fg","chalk","config","execAsync","AgenticPlanner"]}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// src/commands/init.ts
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import { existsSync as existsSync2, appendFileSync, readFileSync as readFileSync4 } from "fs";
|
|
5
|
+
import { resolve as resolve5 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/core/config.ts
|
|
8
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
9
|
+
import { writeFile } from "fs/promises";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
|
|
12
|
+
// src/core/logger.ts
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import ora from "ora";
|
|
15
|
+
var logger = {
|
|
16
|
+
info: (msg) => console.log(chalk.blue("\u2139"), msg),
|
|
17
|
+
success: (msg) => console.log(chalk.green("\u2714"), msg),
|
|
18
|
+
error: (msg) => console.error(chalk.red("\u2716"), msg),
|
|
19
|
+
warn: (msg) => console.warn(chalk.yellow("\u26A0"), msg),
|
|
20
|
+
log: (msg) => console.log(msg),
|
|
21
|
+
spinner: (text) => ora(text)
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/core/config.ts
|
|
25
|
+
var moduleName = "aitest";
|
|
26
|
+
var explorer = cosmiconfig(moduleName);
|
|
27
|
+
async function saveConfig(config2, targetDir = process.cwd()) {
|
|
28
|
+
const configPath = resolve(targetDir, ".aitestrc.json");
|
|
29
|
+
try {
|
|
30
|
+
await writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
|
|
31
|
+
logger.success(`Saved config to ${configPath}`);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.error(`Failed to save config: ${error}`);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/core/detector.ts
|
|
39
|
+
import { readFileSync, existsSync } from "fs";
|
|
40
|
+
import { resolve as resolve2 } from "path";
|
|
41
|
+
function detectProjectInfo(targetDir = process.cwd()) {
|
|
42
|
+
const packageJsonPath = resolve2(targetDir, "package.json");
|
|
43
|
+
const tsconfigPath = resolve2(targetDir, "tsconfig.json");
|
|
44
|
+
let packageJson = {};
|
|
45
|
+
if (existsSync(packageJsonPath)) {
|
|
46
|
+
try {
|
|
47
|
+
packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const allDeps = {
|
|
52
|
+
...packageJson.dependencies || {},
|
|
53
|
+
...packageJson.devDependencies || {}
|
|
54
|
+
};
|
|
55
|
+
const language = existsSync(tsconfigPath) || allDeps["typescript"] ? "typescript" : "javascript";
|
|
56
|
+
let testRunner = "unknown";
|
|
57
|
+
if (allDeps["jest"]) testRunner = "jest";
|
|
58
|
+
else if (allDeps["vitest"]) testRunner = "vitest";
|
|
59
|
+
else if (allDeps["mocha"]) testRunner = "mocha";
|
|
60
|
+
let framework = "unknown";
|
|
61
|
+
if (allDeps["react"]) framework = "react";
|
|
62
|
+
else if (allDeps["vue"]) framework = "vue";
|
|
63
|
+
else if (allDeps["@angular/core"]) framework = "angular";
|
|
64
|
+
else if (allDeps["express"] || allDeps["@nestjs/core"] || allDeps["fastify"]) framework = "node";
|
|
65
|
+
let packageManager = "npm";
|
|
66
|
+
if (existsSync(resolve2(targetDir, "pnpm-lock.yaml"))) packageManager = "pnpm";
|
|
67
|
+
else if (existsSync(resolve2(targetDir, "yarn.lock"))) packageManager = "yarn";
|
|
68
|
+
else if (existsSync(resolve2(targetDir, "bun.lockb"))) packageManager = "bun";
|
|
69
|
+
return { language, testRunner, framework, packageManager };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/core/setup.ts
|
|
73
|
+
import { exec } from "child_process";
|
|
74
|
+
import { promisify } from "util";
|
|
75
|
+
import { resolve as resolve3 } from "path";
|
|
76
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
77
|
+
var execAsync = promisify(exec);
|
|
78
|
+
async function installTestRunner(projectInfo, targetDir = process.cwd()) {
|
|
79
|
+
const spinner = logger.spinner("Setting up test runner...").start();
|
|
80
|
+
const isTs = projectInfo.language === "typescript";
|
|
81
|
+
const runner = isTs ? "vitest" : "jest";
|
|
82
|
+
const packages = isTs ? "vitest" : "jest";
|
|
83
|
+
let installCmd = "";
|
|
84
|
+
switch (projectInfo.packageManager) {
|
|
85
|
+
case "npm":
|
|
86
|
+
installCmd = `npm install -D ${packages}`;
|
|
87
|
+
break;
|
|
88
|
+
case "yarn":
|
|
89
|
+
installCmd = `yarn add -D ${packages}`;
|
|
90
|
+
break;
|
|
91
|
+
case "pnpm":
|
|
92
|
+
installCmd = `pnpm add -D ${packages}`;
|
|
93
|
+
break;
|
|
94
|
+
case "bun":
|
|
95
|
+
installCmd = `bun add -D ${packages}`;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
spinner.text = `Installing ${runner} using ${projectInfo.packageManager}...`;
|
|
100
|
+
await execAsync(installCmd, { cwd: targetDir });
|
|
101
|
+
spinner.text = "Updating package.json...";
|
|
102
|
+
const packageJsonPath = resolve3(targetDir, "package.json");
|
|
103
|
+
let pkg = {};
|
|
104
|
+
try {
|
|
105
|
+
pkg = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
pkg = {};
|
|
108
|
+
}
|
|
109
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
110
|
+
if (runner === "vitest") {
|
|
111
|
+
pkg.scripts.test = "vitest run";
|
|
112
|
+
} else {
|
|
113
|
+
pkg.scripts.test = "jest";
|
|
114
|
+
}
|
|
115
|
+
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), "utf-8");
|
|
116
|
+
spinner.succeed(`Successfully installed and configured ${runner}!`);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
spinner.fail(`Failed to set up test runner: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/core/ai.ts
|
|
123
|
+
import { generateText } from "ai";
|
|
124
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
125
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
126
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
127
|
+
import { createDeepSeek } from "@ai-sdk/deepseek";
|
|
128
|
+
import * as dotenv from "dotenv";
|
|
129
|
+
dotenv.config();
|
|
130
|
+
function getAIModel(config2) {
|
|
131
|
+
const providerName = config2.provider.toLowerCase();
|
|
132
|
+
if (providerName === "openai") {
|
|
133
|
+
const openai = createOpenAI({
|
|
134
|
+
apiKey: config2.apiKey || process.env.OPENAI_API_KEY
|
|
135
|
+
});
|
|
136
|
+
return openai(config2.model || "gpt-4o");
|
|
137
|
+
}
|
|
138
|
+
if (providerName === "anthropic") {
|
|
139
|
+
const anthropic = createAnthropic({
|
|
140
|
+
apiKey: config2.apiKey || process.env.ANTHROPIC_API_KEY
|
|
141
|
+
});
|
|
142
|
+
return anthropic(config2.model || "claude-3-5-sonnet-20240620");
|
|
143
|
+
}
|
|
144
|
+
if (providerName === "gemini" || providerName === "google") {
|
|
145
|
+
const google = createGoogleGenerativeAI({
|
|
146
|
+
apiKey: config2.apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY
|
|
147
|
+
});
|
|
148
|
+
return google(config2.model || "gemini-2.5-flash");
|
|
149
|
+
}
|
|
150
|
+
if (providerName === "deepseek") {
|
|
151
|
+
const deepseek = createDeepSeek({
|
|
152
|
+
apiKey: config2.apiKey || process.env.DEEPSEEK_API_KEY
|
|
153
|
+
});
|
|
154
|
+
return deepseek(config2.model || "deepseek-coder");
|
|
155
|
+
}
|
|
156
|
+
if (providerName === "ollama") {
|
|
157
|
+
const ollama = createOpenAI({
|
|
158
|
+
baseURL: "http://localhost:11434/v1",
|
|
159
|
+
apiKey: "ollama"
|
|
160
|
+
// API key isn't strictly needed for local Ollama
|
|
161
|
+
});
|
|
162
|
+
return ollama(config2.model || "llama3.1");
|
|
163
|
+
}
|
|
164
|
+
if (providerName === "custom") {
|
|
165
|
+
const custom = createOpenAI({
|
|
166
|
+
baseURL: config2.baseURL,
|
|
167
|
+
apiKey: config2.apiKey || process.env.CUSTOM_API_KEY || "custom",
|
|
168
|
+
headers: config2.customHeaders
|
|
169
|
+
});
|
|
170
|
+
return custom(config2.model || "local-model");
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`Unsupported AI provider: ${config2.provider}`);
|
|
173
|
+
}
|
|
174
|
+
async function askAI(config2, system, prompt) {
|
|
175
|
+
const model = getAIModel(config2);
|
|
176
|
+
const { text } = await generateText({
|
|
177
|
+
model,
|
|
178
|
+
system,
|
|
179
|
+
prompt,
|
|
180
|
+
maxRetries: 5,
|
|
181
|
+
temperature: config2.temperature || 0.1
|
|
182
|
+
});
|
|
183
|
+
return text;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/core/config-fixer.ts
|
|
187
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "fs";
|
|
188
|
+
import { resolve as resolve4 } from "path";
|
|
189
|
+
import { exec as exec2 } from "child_process";
|
|
190
|
+
import { promisify as promisify2 } from "util";
|
|
191
|
+
var execAsync2 = promisify2(exec2);
|
|
192
|
+
async function applyConfigFix(configFix, projectInfo, spinner = logger.spinner("Applying fix...")) {
|
|
193
|
+
spinner.info(`Autonomous Config Fix initiated: ${configFix.reason}`);
|
|
194
|
+
if (configFix.dependencies && configFix.dependencies.length > 0) {
|
|
195
|
+
spinner.text = `Installing missing dependencies: ${configFix.dependencies.join(", ")}...`;
|
|
196
|
+
const installCmd = projectInfo.packageManager === "yarn" ? "yarn add -D" : projectInfo.packageManager === "pnpm" ? "pnpm add -D" : projectInfo.packageManager === "bun" ? "bun add -d" : "npm install -D --legacy-peer-deps";
|
|
197
|
+
await execAsync2(`${installCmd} ${configFix.dependencies.join(" ")}`, { cwd: process.cwd() });
|
|
198
|
+
}
|
|
199
|
+
if (configFix.files && configFix.files.length > 0) {
|
|
200
|
+
for (const file of configFix.files) {
|
|
201
|
+
const filePath = resolve4(process.cwd(), file.path);
|
|
202
|
+
spinner.text = `Writing config file: ${file.path}...`;
|
|
203
|
+
if (file.path.endsWith("package.json")) {
|
|
204
|
+
try {
|
|
205
|
+
const existingPkg = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
206
|
+
const newPkg = JSON.parse(file.content);
|
|
207
|
+
if (newPkg.scripts) {
|
|
208
|
+
existingPkg.scripts = { ...existingPkg.scripts || {}, ...newPkg.scripts };
|
|
209
|
+
}
|
|
210
|
+
if (newPkg.jest) {
|
|
211
|
+
existingPkg.jest = { ...existingPkg.jest || {}, ...newPkg.jest };
|
|
212
|
+
}
|
|
213
|
+
writeFileSync2(filePath, JSON.stringify(existingPkg, null, 2), "utf-8");
|
|
214
|
+
} catch (e) {
|
|
215
|
+
spinner.warn(`Failed to safely merge package.json: ${e.message}`);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
writeFileSync2(filePath, file.content, "utf-8");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/commands/init.ts
|
|
225
|
+
import fg from "fast-glob";
|
|
226
|
+
var initCommand = new Command("init").description("Initialize the project").action(async () => {
|
|
227
|
+
logger.info("Initializing AI Test CLI...");
|
|
228
|
+
const projectInfo = detectProjectInfo();
|
|
229
|
+
logger.info(`Detected Language: ${projectInfo.language}`);
|
|
230
|
+
logger.info(`Detected Framework: ${projectInfo.framework}`);
|
|
231
|
+
logger.info(`Detected Test Runner: ${projectInfo.testRunner}`);
|
|
232
|
+
logger.info(`Detected Package Manager: ${projectInfo.packageManager}`);
|
|
233
|
+
if (projectInfo.testRunner === "unknown") {
|
|
234
|
+
const setupRes = await prompts({
|
|
235
|
+
type: "confirm",
|
|
236
|
+
name: "installRunner",
|
|
237
|
+
message: "No test runner detected. Would you like AI Test CLI to automatically install and configure one?",
|
|
238
|
+
initial: true
|
|
239
|
+
});
|
|
240
|
+
if (setupRes.installRunner) {
|
|
241
|
+
await installTestRunner(projectInfo);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const response = await prompts([
|
|
245
|
+
{
|
|
246
|
+
type: "select",
|
|
247
|
+
name: "provider",
|
|
248
|
+
message: "Select AI Provider",
|
|
249
|
+
choices: [
|
|
250
|
+
{ title: "OpenAI", value: "openai" },
|
|
251
|
+
{ title: "Anthropic", value: "anthropic" },
|
|
252
|
+
{ title: "DeepSeek", value: "deepseek" },
|
|
253
|
+
{ title: "Gemini", value: "gemini" },
|
|
254
|
+
{ title: "Ollama (Local)", value: "ollama" },
|
|
255
|
+
{ title: "Custom (OpenAI Compatible)", value: "custom" }
|
|
256
|
+
],
|
|
257
|
+
initial: 0
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
name: "model",
|
|
262
|
+
message: "Enter model name",
|
|
263
|
+
initial: (prev) => {
|
|
264
|
+
if (prev === "openai") return "gpt-4o";
|
|
265
|
+
if (prev === "anthropic") return "claude-3-5-sonnet-latest";
|
|
266
|
+
if (prev === "deepseek") return "deepseek-coder";
|
|
267
|
+
if (prev === "gemini") return "gemini-2.5-flash";
|
|
268
|
+
if (prev === "ollama") return "llama3.1";
|
|
269
|
+
if (prev === "custom") return "local-model";
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
type: (prev, values) => values.provider === "custom" ? "text" : null,
|
|
275
|
+
name: "baseURL",
|
|
276
|
+
message: "Enter API Base URL (e.g., http://localhost:1234/v1)"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
type: (prev, values) => values.provider === "custom" ? "text" : null,
|
|
280
|
+
name: "customHeaders",
|
|
281
|
+
message: 'Enter custom headers as "Key: Value, Key2: Value2" (or leave empty)'
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
type: (prev, values) => values.provider === "ollama" ? null : "password",
|
|
285
|
+
name: "apiKey",
|
|
286
|
+
message: "Enter API Key"
|
|
287
|
+
}
|
|
288
|
+
]);
|
|
289
|
+
if (!response.provider || !response.model) {
|
|
290
|
+
logger.error("Initialization aborted.");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (response.apiKey) {
|
|
294
|
+
const envKeyMap = {
|
|
295
|
+
openai: "OPENAI_API_KEY",
|
|
296
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
297
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
298
|
+
gemini: "GEMINI_API_KEY",
|
|
299
|
+
custom: "CUSTOM_API_KEY"
|
|
300
|
+
};
|
|
301
|
+
const envKey = envKeyMap[response.provider] || "CUSTOM_API_KEY";
|
|
302
|
+
const envPath = resolve5(process.cwd(), ".env");
|
|
303
|
+
appendFileSync(envPath, `
|
|
304
|
+
${envKey}=${response.apiKey}
|
|
305
|
+
`, "utf-8");
|
|
306
|
+
process.env[envKey] = response.apiKey;
|
|
307
|
+
logger.success(`Saved API key to .env securely.`);
|
|
308
|
+
const gitignorePath = resolve5(process.cwd(), ".gitignore");
|
|
309
|
+
let gitignoreContent = "";
|
|
310
|
+
if (existsSync2(gitignorePath)) {
|
|
311
|
+
gitignoreContent = readFileSync4(gitignorePath, "utf-8");
|
|
312
|
+
}
|
|
313
|
+
if (!gitignoreContent.includes(".env")) {
|
|
314
|
+
appendFileSync(gitignorePath, "\n.env\n", "utf-8");
|
|
315
|
+
logger.info("Added .env to .gitignore");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
let parsedHeaders = void 0;
|
|
319
|
+
if (response.customHeaders && response.customHeaders.trim() !== "") {
|
|
320
|
+
parsedHeaders = {};
|
|
321
|
+
const parts = response.customHeaders.split(",");
|
|
322
|
+
for (const part of parts) {
|
|
323
|
+
const [key, ...valParts] = part.split(":");
|
|
324
|
+
if (key && valParts.length > 0) {
|
|
325
|
+
const cleanKey = key.trim().replace(/^["']|["']$/g, "");
|
|
326
|
+
const cleanVal = valParts.join(":").trim().replace(/^["']|["']$/g, "");
|
|
327
|
+
parsedHeaders[cleanKey] = cleanVal;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const config2 = {
|
|
332
|
+
provider: response.provider,
|
|
333
|
+
model: response.model,
|
|
334
|
+
baseURL: response.baseURL,
|
|
335
|
+
customHeaders: parsedHeaders,
|
|
336
|
+
temperature: 0.1,
|
|
337
|
+
autoFix: true,
|
|
338
|
+
maxRetries: -1
|
|
339
|
+
// Infinite by default
|
|
340
|
+
};
|
|
341
|
+
await saveConfig(config2);
|
|
342
|
+
logger.success("Basic initialization complete.");
|
|
343
|
+
const setupVerify = await prompts({
|
|
344
|
+
type: "confirm",
|
|
345
|
+
name: "verify",
|
|
346
|
+
message: "Would you like the AI to verify and configure your testing environment right now? (Recommended)",
|
|
347
|
+
initial: true
|
|
348
|
+
});
|
|
349
|
+
if (setupVerify.verify) {
|
|
350
|
+
const spinner = logger.spinner("Analyzing project setup...").start();
|
|
351
|
+
try {
|
|
352
|
+
const pkgPath = resolve5(process.cwd(), "package.json");
|
|
353
|
+
let pkgContent = "{}";
|
|
354
|
+
if (existsSync2(pkgPath)) {
|
|
355
|
+
pkgContent = readFileSync4(pkgPath, "utf-8");
|
|
356
|
+
}
|
|
357
|
+
const existingConfigs = await fg(["jest.config.*", "vite.config.*", ".babelrc*", "tsconfig.*", "vitest.config.*"], { cwd: process.cwd(), deep: 1 });
|
|
358
|
+
const setupContext = `Language: ${projectInfo.language}
|
|
359
|
+
Framework: ${projectInfo.framework}
|
|
360
|
+
Test Runner: ${projectInfo.testRunner}
|
|
361
|
+
Existing Config Files: ${existingConfigs.join(", ") || "None"}
|
|
362
|
+
Package.json:
|
|
363
|
+
${pkgContent}`;
|
|
364
|
+
const responseCode = await askAI(
|
|
365
|
+
config2,
|
|
366
|
+
'You are a testing architecture expert. Analyze this project setup. If the test environment is perfect and ready to run tests, reply exactly with "READY". If it is missing dependencies (e.g., Jest, Vitest, Testing Library, jsdom) or needs config files (e.g., .babelrc, jest.config.js) to support the detected framework, reply ONLY with a JSON block wrapped in ```json starting with {"type": "CONFIG_FIX", "dependencies": ["pkg"], "files": [{"path": ".babelrc", "content": "..."}], "reason": "..."}. CRITICAL: You must escape all newlines within the JSON "content" strings as \\n. DO NOT create duplicate configuration files for the same tool (e.g., if jest.config.js exists, do not create jest.config.ts). If an existing file needs updating, provide the file path of the existing file. Do not output raw unescaped newlines inside the JSON string. Do not output anything else.',
|
|
367
|
+
setupContext
|
|
368
|
+
);
|
|
369
|
+
const configFixMatch = responseCode.match(/```json\s*(\{[\s\S]*?"type":\s*"CONFIG_FIX"[\s\S]*?\})\s*```/);
|
|
370
|
+
if (configFixMatch && configFixMatch[1]) {
|
|
371
|
+
const configFix = JSON.parse(configFixMatch[1]);
|
|
372
|
+
await applyConfigFix(configFix, projectInfo, spinner);
|
|
373
|
+
spinner.succeed("Test environment successfully scaffolded!");
|
|
374
|
+
} else {
|
|
375
|
+
spinner.succeed("Test environment looks perfectly configured!");
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
spinner.fail(`Setup verification failed: ${e.message}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
logger.success("You can now use `aitest run` or `aitest generate`.");
|
|
382
|
+
});
|
|
383
|
+
export {
|
|
384
|
+
initCommand
|
|
385
|
+
};
|
|
386
|
+
//# sourceMappingURL=init.js.map
|