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
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,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,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,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"]}
|