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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chijindu Azubuike
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ <h1 align="center">AI Test CLI ๐Ÿงช๐Ÿค–</h1>
2
+
3
+ <p align="center">
4
+ <strong>An autonomous AI software testing engineer that dynamically generates, evaluates, and auto-fixes test suites for complex codebases.</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://badge.fury.io/js/ai-test-cli"><img src="https://badge.fury.io/js/ai-test-cli.svg" alt="npm version" /></a>
9
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
10
+ </p>
11
+
12
+ ---
13
+
14
+ ## ๐ŸŒŸ Why AI Test CLI?
15
+
16
+ Most AI testing tools just write boilerplate code and give up when tests fail. **AI Test CLI is different.** It operates like a real Senior QA engineer using a fully autonomous "Agentic Loop":
17
+
18
+ 1. **Intelligent Generation**: It reads your file, understands its imports, scans your workspace structure, and writes comprehensive tests using your existing framework (Jest, Vitest, Mocha, etc.).
19
+ 2. **Massive File Support**: Got an 11,000+ line controller? No problem. The **Agentic Chunking Generator** explores the file function-by-function, incrementally building out coverage without blowing up LLM token limits.
20
+ 3. **Smart Context for Local Models**: Running Ollama on a MacBook? The CLI automatically detects Local LLMs and engages "Extreme Chunking Mode" to guarantee your computer never crashes from context-window overflow.
21
+ 4. **Autonomous Auto-Fixing**: It actually *runs* the tests it writes. If they fail, it reads the error stack trace, uses search tools to inspect the broken dependencies, and dynamically applies patches until the test passes.
22
+ 5. **Bring Your Own Key (BYOK)**: Supports DeepSeek, OpenAI, Anthropic, Gemini, and Local Models (Ollama/LMStudio) via the Vercel AI SDK.
23
+
24
+ ---
25
+
26
+ ## ๐Ÿš€ Quickstart
27
+
28
+ Install globally via npm:
29
+
30
+ ```bash
31
+ npm install -g ai-test-cli
32
+ ```
33
+
34
+ Initialize the configuration in your project root. This creates an `.aitestrc.json` file.
35
+
36
+ ```bash
37
+ aitest init
38
+ ```
39
+
40
+ Set your API key in your `.env` file, or set it in your environment:
41
+
42
+ ```bash
43
+ # Depending on your chosen provider:
44
+ export DEEPSEEK_API_KEY="sk-..."
45
+ export OPENAI_API_KEY="sk-..."
46
+ export ANTHROPIC_API_KEY="sk-..."
47
+ export GEMINI_API_KEY="AIza..."
48
+ export CUSTOM_API_KEY="sk-..."
49
+ ```
50
+
51
+ ---
52
+
53
+ ## ๐Ÿ› ๏ธ Usage
54
+
55
+ ### Generate Tests
56
+
57
+ Generate a test suite for a single file:
58
+ ```bash
59
+ aitest generate --file path/to/your/file.ts
60
+ ```
61
+
62
+ Generate tests for your entire project (this will skip files that already have tests):
63
+ ```bash
64
+ aitest generate --all
65
+ ```
66
+
67
+ **Iterative Coverage**: If a test file already exists, running `aitest generate` again will automatically evaluate the existing file for missing coverage and append tests for untested functions!
68
+
69
+ ### Auto-Fix Broken Tests
70
+
71
+ Got a test suite that's failing because of broken mocks or outdated logic? Unleash the agentic fixer:
72
+ ```bash
73
+ aitest fix
74
+ ```
75
+ The AI will run your entire test suite, isolate the failures, analyze the error stack traces, and incrementally patch your test files until they turn green.
76
+
77
+ ### Explain Failures
78
+ Don't want the AI to fix it for you? Just ask it to explain why a test is failing:
79
+ ```bash
80
+ aitest explain
81
+ ```
82
+
83
+ ---
84
+
85
+ ## ๐Ÿง  Supported Providers
86
+
87
+ The CLI supports the following providers out of the box via your `.aitestrc.json`:
88
+ - `deepseek` (Highly Recommended: 128k context, extremely cheap)
89
+ - `openai`
90
+ - `anthropic`
91
+ - `gemini`
92
+ - `ollama` (Local models, automatically triggers Smart Context chunking)
93
+
94
+ ---
95
+
96
+ ## ๐Ÿ›ก๏ธ Safety Mechanisms
97
+
98
+ AI Test CLI is built with strict API billing safeguards:
99
+ - **Stagnation Protection**: If the AI gets confused and fails to write a test chunk after 10 attempts, it safely aborts.
100
+ - **Duplicate Action Protection**: If the AI falls into a hallucinatory repetition loop, the CLI detects it and breaks the loop instantly to save your API credits.
101
+ - **Smart Context**: Drastically reduces token consumption for massive files by chunking out irrelevant lines during the repair loop.
102
+
103
+ ---
104
+
105
+ ## โ˜• Support the Project
106
+
107
+ Did AI Test CLI just save you 20 hours of grueling unit testing? Support the creator and buy them a coffee!
108
+
109
+ <a href="https://buymeacoffee.com/cijaytechnh" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
110
+
111
+ ---
112
+
113
+ ## ๐Ÿ“„ License
114
+
115
+ MIT License. See [LICENSE](LICENSE) for more information.
package/bin/aitest.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/index.js';
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+
3
+ declare const configCommand: Command;
4
+
5
+ export { configCommand };
@@ -0,0 +1,80 @@
1
+ // src/commands/config.ts
2
+ import { Command } from "commander";
3
+
4
+ // src/core/config.ts
5
+ import { cosmiconfig } from "cosmiconfig";
6
+ import { writeFile } from "fs/promises";
7
+ import { resolve } from "path";
8
+
9
+ // src/core/logger.ts
10
+ import chalk from "chalk";
11
+ import ora from "ora";
12
+ var logger = {
13
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
14
+ success: (msg) => console.log(chalk.green("\u2714"), msg),
15
+ error: (msg) => console.error(chalk.red("\u2716"), msg),
16
+ warn: (msg) => console.warn(chalk.yellow("\u26A0"), msg),
17
+ log: (msg) => console.log(msg),
18
+ spinner: (text) => ora(text)
19
+ };
20
+
21
+ // src/core/config.ts
22
+ var moduleName = "aitest";
23
+ var explorer = cosmiconfig(moduleName);
24
+ async function loadConfig() {
25
+ try {
26
+ const result = await explorer.search();
27
+ if (result) {
28
+ return result.config;
29
+ }
30
+ return null;
31
+ } catch (error) {
32
+ logger.error(`Failed to load config: ${error}`);
33
+ return null;
34
+ }
35
+ }
36
+ async function saveConfig(config, targetDir = process.cwd()) {
37
+ const configPath = resolve(targetDir, ".aitestrc.json");
38
+ try {
39
+ await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
40
+ logger.success(`Saved config to ${configPath}`);
41
+ } catch (error) {
42
+ logger.error(`Failed to save config: ${error}`);
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ // src/commands/config.ts
48
+ var configCommand = new Command("config").description("Manage settings (e.g., provider, model, apiKey)");
49
+ configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
50
+ let config = await loadConfig();
51
+ if (!config) {
52
+ config = { provider: "openai", model: "gpt-4o" };
53
+ }
54
+ let parsedValue = value;
55
+ if (value === "true") parsedValue = true;
56
+ if (value === "false") parsedValue = false;
57
+ if (!isNaN(Number(value))) parsedValue = Number(value);
58
+ config[key] = parsedValue;
59
+ await saveConfig(config);
60
+ });
61
+ configCommand.command("list").description("List current configuration").action(async () => {
62
+ const config = await loadConfig();
63
+ if (!config) {
64
+ logger.warn("No configuration found. Run `aitest init`.");
65
+ return;
66
+ }
67
+ console.log("\n--- Current Configuration ---");
68
+ for (const [key, value] of Object.entries(config)) {
69
+ if (key === "apiKey") {
70
+ console.log(`${key}: ********* (Hidden)`);
71
+ } else {
72
+ console.log(`${key}: ${value}`);
73
+ }
74
+ }
75
+ console.log("---------------------------\n");
76
+ });
77
+ export {
78
+ configCommand
79
+ };
80
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/commands/config.ts","../../src/core/config.ts","../../src/core/logger.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { loadConfig, saveConfig } from '../core/config.js';\nimport { logger } from '../core/logger.js';\n\nexport const configCommand = new Command('config')\n .description('Manage settings (e.g., provider, model, apiKey)');\n\nconfigCommand.command('set <key> <value>')\n .description('Set a configuration value')\n .action(async (key, value) => {\n let config = await loadConfig();\n if (!config) {\n config = { provider: 'openai', model: 'gpt-4o' }; // fallback default\n }\n \n // basic type coercion\n let parsedValue: any = value;\n if (value === 'true') parsedValue = true;\n if (value === 'false') parsedValue = false;\n if (!isNaN(Number(value))) parsedValue = Number(value);\n\n (config as any)[key] = parsedValue;\n \n await saveConfig(config);\n });\n\nconfigCommand.command('list')\n .description('List current configuration')\n .action(async () => {\n const config = await loadConfig();\n if (!config) {\n logger.warn('No configuration found. Run `aitest init`.');\n return;\n }\n \n console.log('\\n--- Current Configuration ---');\n for (const [key, value] of Object.entries(config)) {\n if (key === 'apiKey') {\n console.log(`${key}: ********* (Hidden)`);\n } else {\n console.log(`${key}: ${value}`);\n }\n }\n console.log('---------------------------\\n');\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 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"],"mappings":";AAAA,SAAS,eAAe;;;ACAxB,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB;AAC1B,SAAS,eAAe;;;ACFxB,OAAO,WAAW;AAClB,OAAO,SAAS;AAET,IAAM,SAAS;AAAA,EACpB,MAAM,CAAC,QAAgB,QAAQ,IAAI,MAAM,KAAK,QAAG,GAAG,GAAG;AAAA,EACvD,SAAS,CAAC,QAAgB,QAAQ,IAAI,MAAM,MAAM,QAAG,GAAG,GAAG;AAAA,EAC3D,OAAO,CAAC,QAAgB,QAAQ,MAAM,MAAM,IAAI,QAAG,GAAG,GAAG;AAAA,EACzD,MAAM,CAAC,QAAgB,QAAQ,KAAK,MAAM,OAAO,QAAG,GAAG,GAAG;AAAA,EAC1D,KAAK,CAAC,QAAgB,QAAQ,IAAI,GAAG;AAAA,EACrC,SAAS,CAAC,SAAiB,IAAI,IAAI;AACrC;;;ADMA,IAAM,aAAa;AACnB,IAAM,WAAW,YAAY,UAAU;AAEvC,eAAsB,aAA2C;AAC/D,MAAI;AACF,UAAM,SAAS,MAAM,SAAS,OAAO;AACrC,QAAI,QAAQ;AACV,aAAO,OAAO;AAAA,IAChB;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,MAAM,0BAA0B,KAAK,EAAE;AAC9C,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,WAAW,QAAsB,YAAoB,QAAQ,IAAI,GAAkB;AACvG,QAAM,aAAa,QAAQ,WAAW,gBAAgB;AACtD,MAAI;AACF,UAAM,UAAU,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,OAAO;AACpE,WAAO,QAAQ,mBAAmB,UAAU,EAAE;AAAA,EAChD,SAAS,OAAO;AACd,WAAO,MAAM,0BAA0B,KAAK,EAAE;AAC9C,UAAM;AAAA,EACR;AACF;;;ADrCO,IAAM,gBAAgB,IAAI,QAAQ,QAAQ,EAC9C,YAAY,iDAAiD;AAEhE,cAAc,QAAQ,mBAAmB,EACtC,YAAY,2BAA2B,EACvC,OAAO,OAAO,KAAK,UAAU;AAC5B,MAAI,SAAS,MAAM,WAAW;AAC9B,MAAI,CAAC,QAAQ;AACX,aAAS,EAAE,UAAU,UAAU,OAAO,SAAS;AAAA,EACjD;AAGA,MAAI,cAAmB;AACvB,MAAI,UAAU,OAAQ,eAAc;AACpC,MAAI,UAAU,QAAS,eAAc;AACrC,MAAI,CAAC,MAAM,OAAO,KAAK,CAAC,EAAG,eAAc,OAAO,KAAK;AAErD,EAAC,OAAe,GAAG,IAAI;AAEvB,QAAM,WAAW,MAAM;AACzB,CAAC;AAEH,cAAc,QAAQ,MAAM,EACzB,YAAY,4BAA4B,EACxC,OAAO,YAAY;AAClB,QAAM,SAAS,MAAM,WAAW;AAChC,MAAI,CAAC,QAAQ;AACX,WAAO,KAAK,4CAA4C;AACxD;AAAA,EACF;AAEA,UAAQ,IAAI,iCAAiC;AAC7C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,QAAQ,UAAU;AACnB,cAAQ,IAAI,GAAG,GAAG,sBAAsB;AAAA,IAC3C,OAAO;AACJ,cAAQ,IAAI,GAAG,GAAG,KAAK,KAAK,EAAE;AAAA,IACjC;AAAA,EACF;AACA,UAAQ,IAAI,+BAA+B;AAC7C,CAAC;","names":[]}
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+
3
+ declare const doctorCommand: Command;
4
+
5
+ export { doctorCommand };
@@ -0,0 +1,185 @@
1
+ // src/commands/doctor.ts
2
+ import { Command } from "commander";
3
+
4
+ // src/core/config.ts
5
+ import { cosmiconfig } from "cosmiconfig";
6
+
7
+ // src/core/logger.ts
8
+ import chalk from "chalk";
9
+ import ora from "ora";
10
+ var logger = {
11
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
12
+ success: (msg) => console.log(chalk.green("\u2714"), msg),
13
+ error: (msg) => console.error(chalk.red("\u2716"), msg),
14
+ warn: (msg) => console.warn(chalk.yellow("\u26A0"), msg),
15
+ log: (msg) => console.log(msg),
16
+ spinner: (text) => ora(text)
17
+ };
18
+
19
+ // src/core/config.ts
20
+ var moduleName = "aitest";
21
+ var explorer = cosmiconfig(moduleName);
22
+ async function loadConfig() {
23
+ try {
24
+ const result = await explorer.search();
25
+ if (result) {
26
+ return result.config;
27
+ }
28
+ return null;
29
+ } catch (error) {
30
+ logger.error(`Failed to load config: ${error}`);
31
+ return null;
32
+ }
33
+ }
34
+
35
+ // src/core/detector.ts
36
+ import { readFileSync, existsSync } from "fs";
37
+ import { resolve } from "path";
38
+ function detectProjectInfo(targetDir = process.cwd()) {
39
+ const packageJsonPath = resolve(targetDir, "package.json");
40
+ const tsconfigPath = resolve(targetDir, "tsconfig.json");
41
+ let packageJson = {};
42
+ if (existsSync(packageJsonPath)) {
43
+ try {
44
+ packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
45
+ } catch (e) {
46
+ }
47
+ }
48
+ const allDeps = {
49
+ ...packageJson.dependencies || {},
50
+ ...packageJson.devDependencies || {}
51
+ };
52
+ const language = existsSync(tsconfigPath) || allDeps["typescript"] ? "typescript" : "javascript";
53
+ let testRunner = "unknown";
54
+ if (allDeps["jest"]) testRunner = "jest";
55
+ else if (allDeps["vitest"]) testRunner = "vitest";
56
+ else if (allDeps["mocha"]) testRunner = "mocha";
57
+ let framework = "unknown";
58
+ if (allDeps["react"]) framework = "react";
59
+ else if (allDeps["vue"]) framework = "vue";
60
+ else if (allDeps["@angular/core"]) framework = "angular";
61
+ else if (allDeps["express"] || allDeps["@nestjs/core"] || allDeps["fastify"]) framework = "node";
62
+ let packageManager = "npm";
63
+ if (existsSync(resolve(targetDir, "pnpm-lock.yaml"))) packageManager = "pnpm";
64
+ else if (existsSync(resolve(targetDir, "yarn.lock"))) packageManager = "yarn";
65
+ else if (existsSync(resolve(targetDir, "bun.lockb"))) packageManager = "bun";
66
+ return { language, testRunner, framework, packageManager };
67
+ }
68
+
69
+ // src/core/ai.ts
70
+ import { generateText } from "ai";
71
+ import { createOpenAI } from "@ai-sdk/openai";
72
+ import { createAnthropic } from "@ai-sdk/anthropic";
73
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
74
+ import { createDeepSeek } from "@ai-sdk/deepseek";
75
+ import * as dotenv from "dotenv";
76
+ dotenv.config();
77
+ function getAIModel(config2) {
78
+ const providerName = config2.provider.toLowerCase();
79
+ if (providerName === "openai") {
80
+ const openai = createOpenAI({
81
+ apiKey: config2.apiKey || process.env.OPENAI_API_KEY
82
+ });
83
+ return openai(config2.model || "gpt-4o");
84
+ }
85
+ if (providerName === "anthropic") {
86
+ const anthropic = createAnthropic({
87
+ apiKey: config2.apiKey || process.env.ANTHROPIC_API_KEY
88
+ });
89
+ return anthropic(config2.model || "claude-3-5-sonnet-20240620");
90
+ }
91
+ if (providerName === "gemini" || providerName === "google") {
92
+ const google = createGoogleGenerativeAI({
93
+ apiKey: config2.apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY
94
+ });
95
+ return google(config2.model || "gemini-2.5-flash");
96
+ }
97
+ if (providerName === "deepseek") {
98
+ const deepseek = createDeepSeek({
99
+ apiKey: config2.apiKey || process.env.DEEPSEEK_API_KEY
100
+ });
101
+ return deepseek(config2.model || "deepseek-coder");
102
+ }
103
+ if (providerName === "ollama") {
104
+ const ollama = createOpenAI({
105
+ baseURL: "http://localhost:11434/v1",
106
+ apiKey: "ollama"
107
+ // API key isn't strictly needed for local Ollama
108
+ });
109
+ return ollama(config2.model || "llama3.1");
110
+ }
111
+ if (providerName === "custom") {
112
+ const custom = createOpenAI({
113
+ baseURL: config2.baseURL,
114
+ apiKey: config2.apiKey || process.env.CUSTOM_API_KEY || "custom",
115
+ headers: config2.customHeaders
116
+ });
117
+ return custom(config2.model || "local-model");
118
+ }
119
+ throw new Error(`Unsupported AI provider: ${config2.provider}`);
120
+ }
121
+ async function askAI(config2, system, prompt) {
122
+ const model = getAIModel(config2);
123
+ const { text } = await generateText({
124
+ model,
125
+ system,
126
+ prompt,
127
+ maxRetries: 5,
128
+ temperature: config2.temperature || 0.1
129
+ });
130
+ return text;
131
+ }
132
+
133
+ // src/commands/doctor.ts
134
+ var doctorCommand = new Command("doctor").description("Verify installation, configuration, and environment").action(async () => {
135
+ logger.info("Running AI Test CLI Diagnostics...\n");
136
+ let allChecksPassed = true;
137
+ const configSpinner = logger.spinner("Checking configuration...").start();
138
+ const config2 = await loadConfig();
139
+ if (config2) {
140
+ configSpinner.succeed("Configuration found");
141
+ } else {
142
+ configSpinner.fail("Configuration missing. Run `aitest init`");
143
+ allChecksPassed = false;
144
+ }
145
+ const apiSpinner = logger.spinner("Checking API key...").start();
146
+ if (config2?.apiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY) {
147
+ apiSpinner.succeed("API key configured");
148
+ } else {
149
+ apiSpinner.warn("No explicit API key found in config or environment variables (might be okay for local models)");
150
+ }
151
+ if (config2) {
152
+ const modelSpinner = logger.spinner(`Testing access to ${config2.provider} (${config2.model})...`).start();
153
+ try {
154
+ const response = await askAI(config2, 'Reply with exactly "OK"', "Test connection");
155
+ if (response.includes("OK") || response.trim() !== "") {
156
+ modelSpinner.succeed("Model connection successful");
157
+ } else {
158
+ modelSpinner.warn("Model responded, but unexpectedly");
159
+ }
160
+ } catch (error) {
161
+ modelSpinner.fail(`Model connection failed: ${error.message}`);
162
+ allChecksPassed = false;
163
+ }
164
+ }
165
+ const envSpinner = logger.spinner("Checking project environment...").start();
166
+ const projectInfo = detectProjectInfo();
167
+ if (projectInfo.packageManager !== "npm" && projectInfo.packageManager !== "yarn" && projectInfo.packageManager !== "pnpm" && projectInfo.packageManager !== "bun") {
168
+ envSpinner.fail("Supported package manager not found");
169
+ allChecksPassed = false;
170
+ } else if (projectInfo.testRunner === "unknown") {
171
+ envSpinner.warn(`Test runner not explicitly detected. Fallback may be used.`);
172
+ } else {
173
+ envSpinner.succeed(`Project environment valid (${projectInfo.language}, ${projectInfo.packageManager}, ${projectInfo.testRunner})`);
174
+ }
175
+ console.log("\n--- Diagnostic Summary ---");
176
+ if (allChecksPassed) {
177
+ logger.success("\u2705 All critical checks passed. You are ready to generate tests!");
178
+ } else {
179
+ logger.warn("\u26A0\uFE0F Some checks failed. Please resolve the issues above before proceeding.");
180
+ }
181
+ });
182
+ export {
183
+ doctorCommand
184
+ };
185
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/commands/doctor.ts","../../src/core/config.ts","../../src/core/logger.ts","../../src/core/detector.ts","../../src/core/ai.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { loadConfig } from '../core/config.js';\nimport { logger } from '../core/logger.js';\nimport { detectProjectInfo } from '../core/detector.js';\nimport { askAI } from '../core/ai.js';\n\nexport const doctorCommand = new Command('doctor')\n .description('Verify installation, configuration, and environment')\n .action(async () => {\n logger.info('Running AI Test CLI Diagnostics...\\n');\n\n let allChecksPassed = true;\n\n // 1. Check Configuration\n const configSpinner = logger.spinner('Checking configuration...').start();\n const config = await loadConfig();\n if (config) {\n configSpinner.succeed('Configuration found');\n } else {\n configSpinner.fail('Configuration missing. Run `aitest init`');\n allChecksPassed = false;\n }\n\n // 2. Check API Key\n const apiSpinner = logger.spinner('Checking API key...').start();\n if (config?.apiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY) {\n apiSpinner.succeed('API key configured');\n } else {\n apiSpinner.warn('No explicit API key found in config or environment variables (might be okay for local models)');\n }\n\n // 3. Test Model Access\n if (config) {\n const modelSpinner = logger.spinner(`Testing access to ${config.provider} (${config.model})...`).start();\n try {\n const response = await askAI(config, 'Reply with exactly \"OK\"', 'Test connection');\n if (response.includes('OK') || response.trim() !== '') {\n modelSpinner.succeed('Model connection successful');\n } else {\n modelSpinner.warn('Model responded, but unexpectedly');\n }\n } catch (error: any) {\n modelSpinner.fail(`Model connection failed: ${error.message}`);\n allChecksPassed = false;\n }\n }\n\n // 4. Check Project Environment\n const envSpinner = logger.spinner('Checking project environment...').start();\n const projectInfo = detectProjectInfo();\n \n if (projectInfo.packageManager !== 'npm' && projectInfo.packageManager !== 'yarn' && projectInfo.packageManager !== 'pnpm' && projectInfo.packageManager !== 'bun') {\n envSpinner.fail('Supported package manager not found');\n allChecksPassed = false;\n } else if (projectInfo.testRunner === 'unknown') {\n envSpinner.warn(`Test runner not explicitly detected. Fallback may be used.`);\n } else {\n envSpinner.succeed(`Project environment valid (${projectInfo.language}, ${projectInfo.packageManager}, ${projectInfo.testRunner})`);\n }\n\n console.log('\\n--- Diagnostic Summary ---');\n if (allChecksPassed) {\n logger.success('โœ… All critical checks passed. You are ready to generate tests!');\n } else {\n logger.warn('โš ๏ธ Some checks failed. Please resolve the issues above before proceeding.');\n }\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 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 { 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"],"mappings":"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","names":["config","config"]}
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+
3
+ declare const explainCommand: Command;
4
+
5
+ export { explainCommand };
@@ -0,0 +1,146 @@
1
+ // src/commands/explain.ts
2
+ import { Command } from "commander";
3
+
4
+ // src/core/config.ts
5
+ import { cosmiconfig } from "cosmiconfig";
6
+
7
+ // src/core/logger.ts
8
+ import chalk from "chalk";
9
+ import ora from "ora";
10
+ var logger = {
11
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
12
+ success: (msg) => console.log(chalk.green("\u2714"), msg),
13
+ error: (msg) => console.error(chalk.red("\u2716"), msg),
14
+ warn: (msg) => console.warn(chalk.yellow("\u26A0"), msg),
15
+ log: (msg) => console.log(msg),
16
+ spinner: (text) => ora(text)
17
+ };
18
+
19
+ // src/core/config.ts
20
+ var moduleName = "aitest";
21
+ var explorer = cosmiconfig(moduleName);
22
+ async function loadConfig() {
23
+ try {
24
+ const result = await explorer.search();
25
+ if (result) {
26
+ return result.config;
27
+ }
28
+ return null;
29
+ } catch (error) {
30
+ logger.error(`Failed to load config: ${error}`);
31
+ return null;
32
+ }
33
+ }
34
+
35
+ // src/core/ai.ts
36
+ import { generateText } from "ai";
37
+ import { createOpenAI } from "@ai-sdk/openai";
38
+ import { createAnthropic } from "@ai-sdk/anthropic";
39
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
40
+ import { createDeepSeek } from "@ai-sdk/deepseek";
41
+ import * as dotenv from "dotenv";
42
+ dotenv.config();
43
+ function getAIModel(config2) {
44
+ const providerName = config2.provider.toLowerCase();
45
+ if (providerName === "openai") {
46
+ const openai = createOpenAI({
47
+ apiKey: config2.apiKey || process.env.OPENAI_API_KEY
48
+ });
49
+ return openai(config2.model || "gpt-4o");
50
+ }
51
+ if (providerName === "anthropic") {
52
+ const anthropic = createAnthropic({
53
+ apiKey: config2.apiKey || process.env.ANTHROPIC_API_KEY
54
+ });
55
+ return anthropic(config2.model || "claude-3-5-sonnet-20240620");
56
+ }
57
+ if (providerName === "gemini" || providerName === "google") {
58
+ const google = createGoogleGenerativeAI({
59
+ apiKey: config2.apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY
60
+ });
61
+ return google(config2.model || "gemini-2.5-flash");
62
+ }
63
+ if (providerName === "deepseek") {
64
+ const deepseek = createDeepSeek({
65
+ apiKey: config2.apiKey || process.env.DEEPSEEK_API_KEY
66
+ });
67
+ return deepseek(config2.model || "deepseek-coder");
68
+ }
69
+ if (providerName === "ollama") {
70
+ const ollama = createOpenAI({
71
+ baseURL: "http://localhost:11434/v1",
72
+ apiKey: "ollama"
73
+ // API key isn't strictly needed for local Ollama
74
+ });
75
+ return ollama(config2.model || "llama3.1");
76
+ }
77
+ if (providerName === "custom") {
78
+ const custom = createOpenAI({
79
+ baseURL: config2.baseURL,
80
+ apiKey: config2.apiKey || process.env.CUSTOM_API_KEY || "custom",
81
+ headers: config2.customHeaders
82
+ });
83
+ return custom(config2.model || "local-model");
84
+ }
85
+ throw new Error(`Unsupported AI provider: ${config2.provider}`);
86
+ }
87
+ async function askAI(config2, system, prompt) {
88
+ const model = getAIModel(config2);
89
+ const { text } = await generateText({
90
+ model,
91
+ system,
92
+ prompt,
93
+ maxRetries: 5,
94
+ temperature: config2.temperature || 0.1
95
+ });
96
+ return text;
97
+ }
98
+
99
+ // src/commands/explain.ts
100
+ import { exec } from "child_process";
101
+ import { promisify } from "util";
102
+ var execAsync = promisify(exec);
103
+ var explainCommand = new Command("explain").description("Explains test failures in plain English").action(async () => {
104
+ const config2 = await loadConfig();
105
+ if (!config2) {
106
+ logger.error("Configuration not found. Run `aitest init` first.");
107
+ return;
108
+ }
109
+ const spinner = logger.spinner("Running tests to gather failure data...").start();
110
+ let stdoutStr = "";
111
+ let stderrStr = "";
112
+ try {
113
+ await execAsync("npm test");
114
+ spinner.succeed("Tests passed! Nothing to explain.");
115
+ return;
116
+ } catch (error) {
117
+ stdoutStr = error.stdout || "";
118
+ stderrStr = error.stderr || error.message;
119
+ spinner.info("Test failure detected.");
120
+ }
121
+ const outputToAnalyze = `
122
+ STDOUT:
123
+ ${stdoutStr.slice(-2e3)}
124
+
125
+ STDERR:
126
+ ${stderrStr.slice(-2e3)}
127
+ `.trim();
128
+ logger.info("Analyzing root cause with AI...");
129
+ const analyzeSpinner = logger.spinner("Thinking...").start();
130
+ try {
131
+ const explanation = await askAI(
132
+ config2,
133
+ "You are an expert QA and software testing engineer. The user has failing tests. Explain the failure in plain English. Include:\n- Root cause\n- Stack analysis\n- Suggested fixes",
134
+ outputToAnalyze
135
+ );
136
+ analyzeSpinner.succeed("Explanation ready:");
137
+ console.log("\n" + explanation + "\n");
138
+ } catch (error) {
139
+ analyzeSpinner.fail("AI explanation failed.");
140
+ logger.error(error.message);
141
+ }
142
+ });
143
+ export {
144
+ explainCommand
145
+ };
146
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/commands/explain.ts","../../src/core/config.ts","../../src/core/logger.ts","../../src/core/ai.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { loadConfig } from '../core/config.js';\nimport { logger } from '../core/logger.js';\nimport { askAI } from '../core/ai.js';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\n\nconst execAsync = promisify(exec);\n\nexport const explainCommand = new Command('explain')\n .description('Explains test failures in plain English')\n .action(async () => {\n const config = await loadConfig();\n if (!config) {\n logger.error('Configuration not found. Run `aitest init` first.');\n return;\n }\n\n const spinner = logger.spinner('Running tests to gather failure data...').start();\n \n let stdoutStr = '';\n let stderrStr = '';\n\n try {\n await execAsync('npm test');\n spinner.succeed('Tests passed! Nothing to explain.');\n return;\n } catch (error: any) {\n stdoutStr = error.stdout || '';\n stderrStr = error.stderr || error.message;\n spinner.info('Test failure detected.');\n }\n\n const outputToAnalyze = `\nSTDOUT:\n${stdoutStr.slice(-2000)}\n\nSTDERR:\n${stderrStr.slice(-2000)}\n `.trim();\n\n logger.info('Analyzing root cause with AI...');\n const analyzeSpinner = logger.spinner('Thinking...').start();\n \n try {\n const explanation = await askAI(\n config,\n 'You are an expert QA and software testing engineer. The user has failing tests. Explain the failure in plain English. Include:\\n- Root cause\\n- Stack analysis\\n- Suggested fixes',\n outputToAnalyze\n );\n \n analyzeSpinner.succeed('Explanation ready:');\n console.log('\\n' + explanation + '\\n');\n \n } catch (error: any) {\n analyzeSpinner.fail('AI explanation failed.');\n logger.error(error.message);\n }\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 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 { 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"],"mappings":"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","names":["config","config"]}
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+
3
+ declare const fixCommand: Command;
4
+
5
+ export { fixCommand };