@esha_susan/mockingbird-cli 1.0.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/README.md +293 -0
- package/dist/cli/cli.js +60 -0
- package/dist/extractor/extractor.js +92 -0
- package/dist/generator/generator.js +121 -0
- package/dist/index.js +10 -0
- package/dist/reporter/reporter.js +42 -0
- package/dist/scanner/scanner.js +103 -0
- package/dist/utils/logger.js +26 -0
- package/dist/writer/writer.js +116 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# 🐦 Mockingbird CLI
|
|
2
|
+
|
|
3
|
+
> AI-powered Jest test generator for Express.js backends.
|
|
4
|
+
|
|
5
|
+
Mockingbird scans your Express.js project, extracts API routes from controller files, and uses Google Gemini AI to automatically generate Jest integration tests — saving hours of manual test writing.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Demo
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
$ mockingbird run ./my-express-project
|
|
13
|
+
|
|
14
|
+
[INFO] Scanning project at: ./my-express-project
|
|
15
|
+
[INFO] Found: userController.ts
|
|
16
|
+
[INFO] Found: authController.ts
|
|
17
|
+
[INFO] Found: productController.ts
|
|
18
|
+
[SUCCESS] Found 3 controller file(s)
|
|
19
|
+
|
|
20
|
+
Generating tests with Gemini AI...
|
|
21
|
+
[SUCCESS] Generated 224 lines of test code
|
|
22
|
+
[SUCCESS] Generated 189 lines of test code
|
|
23
|
+
[SUCCESS] Generated 300 lines of test code
|
|
24
|
+
|
|
25
|
+
Writing test files...
|
|
26
|
+
[SUCCESS] Written: tests/generated/userController.test.ts
|
|
27
|
+
[SUCCESS] Written: tests/generated/authController.test.ts
|
|
28
|
+
[SUCCESS] Written: tests/generated/productController.test.ts
|
|
29
|
+
|
|
30
|
+
═══════════════════════════════════════════════
|
|
31
|
+
MOCKINGBIRD GENERATION REPORT
|
|
32
|
+
═══════════════════════════════════════════════
|
|
33
|
+
|
|
34
|
+
Controllers scanned 3
|
|
35
|
+
Routes found 11
|
|
36
|
+
Tests generated 3
|
|
37
|
+
Tests written 3
|
|
38
|
+
Total lines generated 713
|
|
39
|
+
Time taken 18.45s
|
|
40
|
+
|
|
41
|
+
Output files:
|
|
42
|
+
✓ tests/generated/userController.test.ts
|
|
43
|
+
✓ tests/generated/authController.test.ts
|
|
44
|
+
✓ tests/generated/productController.test.ts
|
|
45
|
+
|
|
46
|
+
═══════════════════════════════════════════════
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- 🔍 **Recursive project scanning** — walks your entire project tree to find controller files
|
|
54
|
+
- 🧠 **AI-powered test generation** — uses Google Gemini to write realistic, meaningful Jest tests
|
|
55
|
+
- 📁 **Automatic file writing** — saves generated tests directly to disk, formatted with Prettier
|
|
56
|
+
- 📊 **Generation report** — prints a clean summary of everything generated
|
|
57
|
+
- ⚡ **Zero configuration** — works out of the box with standard Express.js project structures
|
|
58
|
+
- 🎨 **Colored terminal output** — clear, readable logs at every step
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Prerequisites
|
|
63
|
+
|
|
64
|
+
Before using Mockingbird, ensure your machine has:
|
|
65
|
+
|
|
66
|
+
- **Node.js** v18 or higher
|
|
67
|
+
- **npm** v9 or higher
|
|
68
|
+
- A **Google Gemini API key** — get one free at [aistudio.google.com](https://aistudio.google.com)
|
|
69
|
+
|
|
70
|
+
Your target Express.js project should have:
|
|
71
|
+
|
|
72
|
+
- Controller files named with `Controller.ts`, `controller.ts`, `Router.ts`, or `routes.ts`
|
|
73
|
+
- Routes defined using `router.get/post/put/delete/patch(...)` or `app.get/post/...` syntax
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
### Option 1 — Install globally from npm (recommended)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install -g mockingbird-cli
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Option 2 — Clone and run locally
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/yourusername/mockingbird-cli.git
|
|
89
|
+
cd mockingbird-cli
|
|
90
|
+
npm install
|
|
91
|
+
npm run build
|
|
92
|
+
npm link
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
Create a `.env` file in your **Mockingbird** directory (not your target project):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
GEMINI_API_KEY=your_api_key_here
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
> ⚠️ Never commit your `.env` file. It is already listed in `.gitignore`.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Usage
|
|
110
|
+
|
|
111
|
+
### Basic usage
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
mockingbird run <path-to-your-express-project>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### With custom output directory
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
mockingbird run <path-to-your-express-project> --output ./custom-tests
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Examples
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Scan a project in the current directory
|
|
127
|
+
mockingbird run .
|
|
128
|
+
|
|
129
|
+
# Scan a project at a specific path
|
|
130
|
+
mockingbird run ./my-express-api
|
|
131
|
+
|
|
132
|
+
# Save tests to a custom folder
|
|
133
|
+
mockingbird run ./my-express-api --output ./tests/ai-generated
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Help
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
mockingbird --help
|
|
140
|
+
mockingbird run --help
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## How It Works
|
|
146
|
+
|
|
147
|
+
Mockingbird runs a 5-step pipeline:
|
|
148
|
+
|
|
149
|
+
SCAN Recursively walks the project directory
|
|
150
|
+
|
|
151
|
+
1. Finds files matching controller naming patterns
|
|
152
|
+
|
|
153
|
+
↓
|
|
154
|
+
2. EXTRACT Reads each controller file
|
|
155
|
+
|
|
156
|
+
Uses regex to find route definitions
|
|
157
|
+
|
|
158
|
+
Extracts: HTTP method, path, handler name
|
|
159
|
+
|
|
160
|
+
↓
|
|
161
|
+
3. GENERATE Groups routes by controller
|
|
162
|
+
|
|
163
|
+
Sends route data to Google Gemini AI
|
|
164
|
+
|
|
165
|
+
Receives Jest + Supertest test code
|
|
166
|
+
|
|
167
|
+
↓
|
|
168
|
+
4. WRITE Formats code with Prettier
|
|
169
|
+
|
|
170
|
+
Creates output directory if needed
|
|
171
|
+
|
|
172
|
+
Saves .test.ts files to disk
|
|
173
|
+
|
|
174
|
+
↓
|
|
175
|
+
5. REPORT Prints a summary of everything generated
|
|
176
|
+
|
|
177
|
+
Shows file paths, line counts, timing
|
|
178
|
+
|
|
179
|
+
Each step is a separate module with a single responsibility. If any step fails, the error is caught and reported cleanly — the pipeline continues for other controllers.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Project Structure
|
|
184
|
+
mockingbird-cli/
|
|
185
|
+
|
|
186
|
+
├── src/
|
|
187
|
+
|
|
188
|
+
│ ├── index.ts ← Entry point, loads environment variables
|
|
189
|
+
|
|
190
|
+
│ ├── cli/
|
|
191
|
+
|
|
192
|
+
│ │ └── cli.ts ← Commander.js CLI definition and orchestration
|
|
193
|
+
|
|
194
|
+
│ ├── scanner/
|
|
195
|
+
|
|
196
|
+
│ │ └── scanner.ts ← Recursive directory traversal
|
|
197
|
+
|
|
198
|
+
│ ├── extractor/
|
|
199
|
+
|
|
200
|
+
│ │ └── extractor.ts ← Regex-based route extraction
|
|
201
|
+
|
|
202
|
+
│ ├── generator/
|
|
203
|
+
|
|
204
|
+
│ │ └── generator.ts ← Gemini AI integration
|
|
205
|
+
|
|
206
|
+
│ ├── writer/
|
|
207
|
+
|
|
208
|
+
│ │ └── writer.ts ← File creation and Prettier formatting
|
|
209
|
+
|
|
210
|
+
│ ├── reporter/
|
|
211
|
+
|
|
212
|
+
│ │ └── reporter.ts ← Terminal report generation
|
|
213
|
+
|
|
214
|
+
│ └── utils/
|
|
215
|
+
|
|
216
|
+
│ └── logger.ts ← Colored terminal logging
|
|
217
|
+
|
|
218
|
+
├── package.json
|
|
219
|
+
|
|
220
|
+
├── tsconfig.json
|
|
221
|
+
|
|
222
|
+
└── README.md
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Running Tests
|
|
227
|
+
|
|
228
|
+
Mockingbird has a unit test suite covering its core logic:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npm test
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Test coverage includes:
|
|
235
|
+
- **Scanner** — controller file detection patterns
|
|
236
|
+
- **Extractor** — regex route parsing across different Express patterns
|
|
237
|
+
- **Writer** — output path generation
|
|
238
|
+
- **Generator** — AI response cleaning and markdown fence removal
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Tech Stack
|
|
243
|
+
|
|
244
|
+
| Technology | Purpose | Why chosen |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| **TypeScript** | Primary language | Type safety catches bugs at compile time, not runtime |
|
|
247
|
+
| **Node.js** | Runtime | Native file system access, ideal for CLI tooling |
|
|
248
|
+
| **Commander.js** | CLI framework | Industry standard for Node.js CLIs, automatic help generation |
|
|
249
|
+
| **Google Gemini AI** | Test generation | Fast, free tier available, strong code generation capability |
|
|
250
|
+
| **Prettier** | Code formatting | Ensures generated tests are consistently formatted |
|
|
251
|
+
| **Chalk** | Terminal colors | Makes CLI output readable and professional |
|
|
252
|
+
| **Jest** | Test framework | Industry standard for Node.js/TypeScript testing |
|
|
253
|
+
| **dotenv** | Environment config | Keeps API keys out of source code |
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Limitations
|
|
258
|
+
|
|
259
|
+
Mockingbird works best with standard Express.js patterns. The following are known limitations:
|
|
260
|
+
|
|
261
|
+
- **Regex-based extraction** — routes defined dynamically (inside loops, conditionals, or factory functions) may not be detected
|
|
262
|
+
- **Express.js only** — currently supports Express route syntax (`router.get`, `app.post`, etc.)
|
|
263
|
+
- **Generated tests need a real app** — the generated test files assume your project exports an Express `app` object and has `jest`, `supertest`, and `@types/jest` installed
|
|
264
|
+
- **AI non-determinism** — Gemini may generate slightly different tests on repeated runs for the same routes
|
|
265
|
+
|
|
266
|
+
### Required dependencies in target project
|
|
267
|
+
|
|
268
|
+
Before running the generated tests in your Express project:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npm install --save-dev jest @types/jest supertest @types/supertest ts-jest
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Future Improvements
|
|
277
|
+
|
|
278
|
+
- [ ] AST-based route extraction for more complex patterns
|
|
279
|
+
- [ ] Support for Fastify, Koa, and Hapi frameworks
|
|
280
|
+
- [ ] Configurable test templates
|
|
281
|
+
- [ ] Watch mode — regenerate tests when controllers change
|
|
282
|
+
- [ ] CI/CD integration guide
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
## Author
|
|
288
|
+
|
|
289
|
+
Built by ESHA SUSAN SHAJI(https://github.com/esha-susan) as a portfolio project demonstrating:
|
|
290
|
+
- CLI tool development with Node.js and TypeScript
|
|
291
|
+
- AI API integration (Google Gemini)
|
|
292
|
+
- Modular software architecture
|
|
293
|
+
- Automated code generation
|
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCLI = runCLI;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const scanner_1 = require("../scanner/scanner");
|
|
6
|
+
const extractor_1 = require("../extractor/extractor");
|
|
7
|
+
const generator_1 = require("../generator/generator");
|
|
8
|
+
const writer_1 = require("../writer/writer");
|
|
9
|
+
const reporter_1 = require("../reporter/reporter");
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
function runCLI() {
|
|
12
|
+
const program = new commander_1.Command();
|
|
13
|
+
program
|
|
14
|
+
.name("mockingbird")
|
|
15
|
+
.description("AI-powered Jest test generator for Express.js APIs")
|
|
16
|
+
.version("1.0.0");
|
|
17
|
+
program
|
|
18
|
+
.command("run")
|
|
19
|
+
.description("Scan a project and generate Jest tests for its API routes")
|
|
20
|
+
.argument("<projectPath>", "path to the Express.js project to scan")
|
|
21
|
+
.option("-o, --output <dir>", "output directory for generated tests", "tests/generated")
|
|
22
|
+
.action(async (projectPath, options) => {
|
|
23
|
+
await handleRunCommand(projectPath, options.output);
|
|
24
|
+
});
|
|
25
|
+
program.parse(process.argv);
|
|
26
|
+
}
|
|
27
|
+
async function handleRunCommand(projectPath, outputDir) {
|
|
28
|
+
const startTime = Date.now();
|
|
29
|
+
try {
|
|
30
|
+
(0, logger_1.logHeader)("MOCKINGBIRD — AI Test Generator");
|
|
31
|
+
const scanResult = (0, scanner_1.scanProject)(projectPath);
|
|
32
|
+
if (scanResult.totalFound === 0) {
|
|
33
|
+
(0, logger_1.logError)("No controller files found. Nothing to generate.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const routes = (0, extractor_1.extractAllRoutes)(scanResult.controllerFiles);
|
|
37
|
+
if (routes.length === 0) {
|
|
38
|
+
(0, logger_1.logError)("No routes found in any controller file.");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
console.log("\nGenerating tests with Gemini AI...");
|
|
42
|
+
const generationResults = await (0, generator_1.generateAllTests)(routes);
|
|
43
|
+
console.log("\nWriting test files...");
|
|
44
|
+
const writeResults = await (0, writer_1.writeAllTestFiles)(generationResults, outputDir);
|
|
45
|
+
const endTime = Date.now();
|
|
46
|
+
(0, reporter_1.printReport)({
|
|
47
|
+
scanResult,
|
|
48
|
+
routes,
|
|
49
|
+
generationResults,
|
|
50
|
+
writeResults,
|
|
51
|
+
startTime,
|
|
52
|
+
endTime
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
57
|
+
(0, logger_1.logError)(`Fatal error: ${message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseRoutesFromCode = parseRoutesFromCode;
|
|
37
|
+
exports.extractRoutes = extractRoutes;
|
|
38
|
+
exports.extractAllRoutes = extractAllRoutes;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const logger_1 = require("../utils/logger");
|
|
41
|
+
const ROUTE_PATTERN = /(router|app)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*(?:\w+\s*,\s*)*(\w+)\s*\)/gi;
|
|
42
|
+
// Pure function — takes code as a string, returns routes
|
|
43
|
+
// No file system access — easy to test
|
|
44
|
+
function parseRoutesFromCode(code, controllerFile = "unknown") {
|
|
45
|
+
const routes = [];
|
|
46
|
+
const matches = code.matchAll(ROUTE_PATTERN);
|
|
47
|
+
for (const match of matches) {
|
|
48
|
+
routes.push({
|
|
49
|
+
method: match[2].toUpperCase(),
|
|
50
|
+
path: match[3],
|
|
51
|
+
handler: match[4],
|
|
52
|
+
controllerFile
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return routes;
|
|
56
|
+
}
|
|
57
|
+
function extractRoutes(controllerFilePath) {
|
|
58
|
+
let fileContent;
|
|
59
|
+
try {
|
|
60
|
+
fileContent = fs.readFileSync(controllerFilePath, "utf-8");
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
(0, logger_1.logError)(`Couldn't read file" ${controllerFilePath}`);
|
|
64
|
+
return {
|
|
65
|
+
routes: [],
|
|
66
|
+
totalFound: 0,
|
|
67
|
+
controllerFile: controllerFilePath
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
(0, logger_1.logInfo)(`Extracting routes from ${controllerFilePath}`);
|
|
71
|
+
// Use the pure function for the actual parsing
|
|
72
|
+
const routes = parseRoutesFromCode(fileContent, controllerFilePath);
|
|
73
|
+
routes.forEach(route => {
|
|
74
|
+
(0, logger_1.logInfo)(` ${route.method} ${route.path} → ${route.handler}`);
|
|
75
|
+
});
|
|
76
|
+
if (routes.length == 0) {
|
|
77
|
+
(0, logger_1.logWarning)(` No routes found in: ${controllerFilePath}`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
routes,
|
|
81
|
+
totalFound: routes.length,
|
|
82
|
+
controllerFile: controllerFilePath
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function extractAllRoutes(controllerFiles) {
|
|
86
|
+
const allRoutes = [];
|
|
87
|
+
for (const filePath of controllerFiles) {
|
|
88
|
+
const result = extractRoutes(filePath);
|
|
89
|
+
allRoutes.push(...result.routes);
|
|
90
|
+
}
|
|
91
|
+
return allRoutes;
|
|
92
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateTests = generateTests;
|
|
4
|
+
exports.generateAllTests = generateAllTests;
|
|
5
|
+
exports.cleanGeneratedCode = cleanGeneratedCode;
|
|
6
|
+
const generative_ai_1 = require("@google/generative-ai");
|
|
7
|
+
const logger_1 = require("../utils/logger");
|
|
8
|
+
// Initialize the Gemini client once at module level
|
|
9
|
+
function createGeminiClient() {
|
|
10
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
throw new Error("GEMINI_API_KEY is not set. Please add it to your .env file.");
|
|
13
|
+
}
|
|
14
|
+
return new generative_ai_1.GoogleGenerativeAI(apiKey);
|
|
15
|
+
}
|
|
16
|
+
function buildPrompt(routes, controllerName) {
|
|
17
|
+
const routeDescriptions = routes
|
|
18
|
+
.map(r => ` - ${r.method} ${r.path} (handler: ${r.handler})`)
|
|
19
|
+
.join("\n");
|
|
20
|
+
return `You are an expert Node.js testing engineer.
|
|
21
|
+
|
|
22
|
+
Generate Jest integration tests for the following Express.js API routes from the "${controllerName}" controller:
|
|
23
|
+
|
|
24
|
+
${routeDescriptions}
|
|
25
|
+
|
|
26
|
+
Requirements:
|
|
27
|
+
1. Use Jest as the testing framework
|
|
28
|
+
2. Use supertest for HTTP assertions
|
|
29
|
+
3. Import the Express app as: import app from "../app"
|
|
30
|
+
4. Each route must have at least:
|
|
31
|
+
- One test for successful response (2xx status)
|
|
32
|
+
- One test for error/edge case
|
|
33
|
+
5. Use describe blocks to group tests by route
|
|
34
|
+
6. Use clear, descriptive test names
|
|
35
|
+
7. Include realistic request bodies for POST/PUT routes
|
|
36
|
+
8. Test for correct status codes and response structure
|
|
37
|
+
|
|
38
|
+
Return ONLY the TypeScript test code. No explanations. No markdown code blocks. Just the raw TypeScript code starting with import statements.`;
|
|
39
|
+
}
|
|
40
|
+
function getControllerName(filePath) {
|
|
41
|
+
const fileName = filePath.split("/").pop() || filePath;
|
|
42
|
+
return fileName.replace(".ts", "").replace(".js", "");
|
|
43
|
+
}
|
|
44
|
+
async function generateTests(routes, controllerFile) {
|
|
45
|
+
if (routes.length === 0) {
|
|
46
|
+
(0, logger_1.logWarning)(`No routes to generate tests for: ${controllerFile}`);
|
|
47
|
+
return {
|
|
48
|
+
testCode: "",
|
|
49
|
+
controllerFile,
|
|
50
|
+
routeCount: 0,
|
|
51
|
+
success: false
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const controllerName = getControllerName(controllerFile);
|
|
55
|
+
(0, logger_1.logInfo)(`Generating tests for: ${controllerName} (${routes.length} routes)`);
|
|
56
|
+
try {
|
|
57
|
+
const genAI = createGeminiClient();
|
|
58
|
+
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
|
|
59
|
+
const prompt = buildPrompt(routes, controllerName);
|
|
60
|
+
// Step 3 — call Gemini
|
|
61
|
+
(0, logger_1.logInfo)(" Calling Gemini AI...");
|
|
62
|
+
const result = await model.generateContent(prompt);
|
|
63
|
+
const response = result.response;
|
|
64
|
+
let testCode = response.text();
|
|
65
|
+
// Step 4 — clean the response
|
|
66
|
+
// Sometimes AI wraps code in markdown blocks despite instructions
|
|
67
|
+
testCode = cleanGeneratedCode(testCode);
|
|
68
|
+
if (!testCode || testCode.trim().length === 0) {
|
|
69
|
+
throw new Error("Gemini returned empty response");
|
|
70
|
+
}
|
|
71
|
+
(0, logger_1.logSuccess)(` Generated ${testCode.split("\n").length} lines of test code`);
|
|
72
|
+
return {
|
|
73
|
+
testCode,
|
|
74
|
+
controllerFile,
|
|
75
|
+
routeCount: routes.length,
|
|
76
|
+
success: true
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
81
|
+
(0, logger_1.logError)(` Failed to generate tests: ${message}`);
|
|
82
|
+
return {
|
|
83
|
+
testCode: "",
|
|
84
|
+
controllerFile,
|
|
85
|
+
routeCount: routes.length,
|
|
86
|
+
success: false
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function generateAllTests(routes) {
|
|
91
|
+
const routesByController = groupRoutesByController(routes);
|
|
92
|
+
const results = [];
|
|
93
|
+
for (const [controllerFile, controllerRoutes] of routesByController) {
|
|
94
|
+
const result = await generateTests(controllerRoutes, controllerFile);
|
|
95
|
+
results.push(result);
|
|
96
|
+
// Small delay between API calls to avoid rate limiting
|
|
97
|
+
await delay(500);
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
function groupRoutesByController(routes) {
|
|
102
|
+
const grouped = new Map();
|
|
103
|
+
for (const route of routes) {
|
|
104
|
+
const existing = grouped.get(route.controllerFile) || [];
|
|
105
|
+
existing.push(route);
|
|
106
|
+
grouped.set(route.controllerFile, existing);
|
|
107
|
+
}
|
|
108
|
+
return grouped;
|
|
109
|
+
}
|
|
110
|
+
// Removes markdown code fences AI sometimes adds despite instructions
|
|
111
|
+
function cleanGeneratedCode(code) {
|
|
112
|
+
return code
|
|
113
|
+
.replace(/```typescript/gi, "")
|
|
114
|
+
.replace(/```ts/gi, "")
|
|
115
|
+
.replace(/```/g, "")
|
|
116
|
+
.trim();
|
|
117
|
+
}
|
|
118
|
+
// Simple delay utility
|
|
119
|
+
function delay(ms) {
|
|
120
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
121
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
|
+
dotenv_1.default.config();
|
|
9
|
+
const cli_1 = require("./cli/cli");
|
|
10
|
+
(0, cli_1.runCLI)();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.printReport = printReport;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
function printReport(data) {
|
|
9
|
+
const { scanResult, routes, generationResults, writeResults, startTime, endTime } = data;
|
|
10
|
+
const durationSeconds = ((endTime - startTime) / 1000).toFixed(2);
|
|
11
|
+
const successfulGenerations = generationResults.filter(r => r.success);
|
|
12
|
+
const successfulWrites = writeResults.filter(w => w.success);
|
|
13
|
+
const totalLines = writeResults.reduce((sum, w) => sum + w.linesWritten, 0);
|
|
14
|
+
const divider = "═".repeat(45);
|
|
15
|
+
console.log("\n" + chalk_1.default.bold.magenta(divider));
|
|
16
|
+
console.log(chalk_1.default.bold.magenta(" MOCKINGBIRD GENERATION REPORT"));
|
|
17
|
+
console.log(chalk_1.default.bold.magenta(divider) + "\n");
|
|
18
|
+
printStat("Controllers scanned", scanResult.totalFound);
|
|
19
|
+
printStat("Routes found", routes.length);
|
|
20
|
+
printStat("Tests generated", successfulGenerations.length);
|
|
21
|
+
printStat("Tests written", successfulWrites.length);
|
|
22
|
+
printStat("Total lines generated", totalLines);
|
|
23
|
+
printStat("Time taken", `${durationSeconds}s`);
|
|
24
|
+
console.log("\n " + chalk_1.default.bold("Output files:"));
|
|
25
|
+
writeResults.forEach(result => {
|
|
26
|
+
if (result.success) {
|
|
27
|
+
console.log(` ${chalk_1.default.green("✓")} ${result.outputPath}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log(` ${chalk_1.default.red("✗")} ${result.controllerFile} (failed)`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
console.log("\n" + chalk_1.default.bold.magenta(divider) + "\n");
|
|
34
|
+
const failedCount = generationResults.length - successfulGenerations.length;
|
|
35
|
+
if (failedCount > 0) {
|
|
36
|
+
console.log(chalk_1.default.yellow(`⚠ ${failedCount} controller(s) failed to generate tests. Check logs above for details.\n`));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function printStat(label, value) {
|
|
40
|
+
const paddedLabel = label.padEnd(24);
|
|
41
|
+
console.log(` ${paddedLabel} ${chalk_1.default.cyan(String(value))}`);
|
|
42
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.scanProject = scanProject;
|
|
37
|
+
exports.isControllerFile = isControllerFile;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const logger_1 = require("../utils/logger");
|
|
41
|
+
const CONTROLLER_PATTERNS = [
|
|
42
|
+
"controller.ts",
|
|
43
|
+
"Controller.ts",
|
|
44
|
+
"router.ts",
|
|
45
|
+
"Router.ts",
|
|
46
|
+
"routes.ts",
|
|
47
|
+
"Routes.ts"
|
|
48
|
+
];
|
|
49
|
+
const IGNORED_DIRECTORIES = [
|
|
50
|
+
"node_modules",
|
|
51
|
+
"dist",
|
|
52
|
+
".git",
|
|
53
|
+
"coverage",
|
|
54
|
+
"build"
|
|
55
|
+
];
|
|
56
|
+
function scanProject(projectPath) {
|
|
57
|
+
const absolutePath = path.resolve(projectPath);
|
|
58
|
+
if (!fs.existsSync(absolutePath)) {
|
|
59
|
+
(0, logger_1.logWarning)(`Directory not found: ${absolutePath}`);
|
|
60
|
+
return {
|
|
61
|
+
controllerFiles: [],
|
|
62
|
+
totalFound: 0,
|
|
63
|
+
scannedDirectory: absolutePath
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
(0, logger_1.logInfo)(`Scanning project at ${absolutePath}`);
|
|
67
|
+
const controllerFiles = [];
|
|
68
|
+
walkDirectory(absolutePath, controllerFiles);
|
|
69
|
+
(0, logger_1.logSuccess)(`Found ${controllerFiles.length} controller file(s)`);
|
|
70
|
+
return {
|
|
71
|
+
controllerFiles,
|
|
72
|
+
totalFound: controllerFiles.length,
|
|
73
|
+
scannedDirectory: absolutePath
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function walkDirectory(currentPath, results) {
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
(0, logger_1.logError)(`Couldn't read directory: ${currentPath}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
if (IGNORED_DIRECTORIES.includes(entry.name)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
walkDirectory(fullPath, results);
|
|
92
|
+
}
|
|
93
|
+
else if (entry.isFile()) {
|
|
94
|
+
if (isControllerFile(entry.name)) {
|
|
95
|
+
results.push(fullPath);
|
|
96
|
+
(0, logger_1.logInfo)(` Found:${entry.name}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function isControllerFile(fileName) {
|
|
102
|
+
return CONTROLLER_PATTERNS.some(pattern => fileName.toLowerCase().endsWith(pattern.toLocaleLowerCase()));
|
|
103
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logInfo = logInfo;
|
|
7
|
+
exports.logError = logError;
|
|
8
|
+
exports.logSuccess = logSuccess;
|
|
9
|
+
exports.logWarning = logWarning;
|
|
10
|
+
exports.logHeader = logHeader;
|
|
11
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
+
function logInfo(message) {
|
|
13
|
+
console.log(chalk_1.default.cyan("[INFO]"), message);
|
|
14
|
+
}
|
|
15
|
+
function logError(message) {
|
|
16
|
+
console.log(chalk_1.default.red("[ERROR]"), message);
|
|
17
|
+
}
|
|
18
|
+
function logSuccess(message) {
|
|
19
|
+
console.log(chalk_1.default.green("[SUCCESS]"), message);
|
|
20
|
+
}
|
|
21
|
+
function logWarning(message) {
|
|
22
|
+
console.log(chalk_1.default.yellow("[WARNING]"), message);
|
|
23
|
+
}
|
|
24
|
+
function logHeader(message) {
|
|
25
|
+
console.log(chalk_1.default.magenta("\n" + message));
|
|
26
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.writeTestFile = writeTestFile;
|
|
37
|
+
exports.writeAllTestFiles = writeAllTestFiles;
|
|
38
|
+
exports.buildOutputPath = buildOutputPath;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const prettier = __importStar(require("prettier"));
|
|
42
|
+
const logger_1 = require("../utils/logger");
|
|
43
|
+
const DEFAULT_OUTPUT_DIR = "tests/generated";
|
|
44
|
+
async function writeTestFile(generationResult, outputDir = DEFAULT_OUTPUT_DIR) {
|
|
45
|
+
if (!generationResult.success || !generationResult.testCode) {
|
|
46
|
+
(0, logger_1.logWarning)(`Skipping write — no test code for: ${generationResult.controllerFile}`);
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
outputPath: "",
|
|
50
|
+
controllerFile: generationResult.controllerFile,
|
|
51
|
+
linesWritten: 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const outputPath = buildOutputPath(generationResult.controllerFile, outputDir);
|
|
55
|
+
try {
|
|
56
|
+
ensureDirectoryExists(path.dirname(outputPath));
|
|
57
|
+
const formattedCode = await formatCode(generationResult.testCode);
|
|
58
|
+
fs.writeFileSync(outputPath, formattedCode, "utf-8");
|
|
59
|
+
const linesWritten = formattedCode.split("\n").length;
|
|
60
|
+
(0, logger_1.logSuccess)(`Written: ${outputPath} (${linesWritten} lines)`);
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
outputPath,
|
|
64
|
+
controllerFile: generationResult.controllerFile,
|
|
65
|
+
linesWritten
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
70
|
+
(0, logger_1.logError)(`Failed to write file: ${message}`);
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
outputPath,
|
|
74
|
+
controllerFile: generationResult.controllerFile,
|
|
75
|
+
linesWritten: 0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function writeAllTestFiles(generationResults, outputDir = DEFAULT_OUTPUT_DIR) {
|
|
80
|
+
(0, logger_1.logInfo)(`Writing test files to: ${outputDir}`);
|
|
81
|
+
const writeResults = [];
|
|
82
|
+
for (const result of generationResults) {
|
|
83
|
+
const writeResult = await writeTestFile(result, outputDir);
|
|
84
|
+
writeResults.push(writeResult);
|
|
85
|
+
}
|
|
86
|
+
return writeResults;
|
|
87
|
+
}
|
|
88
|
+
function buildOutputPath(controllerFilePath, outputDir) {
|
|
89
|
+
const fileName = path.basename(controllerFilePath);
|
|
90
|
+
const baseName = fileName.replace(".ts", "").replace(".js", "");
|
|
91
|
+
const testFileName = `${baseName}.test.ts`;
|
|
92
|
+
return path.join(outputDir, testFileName);
|
|
93
|
+
}
|
|
94
|
+
function ensureDirectoryExists(dirPath) {
|
|
95
|
+
if (!fs.existsSync(dirPath)) {
|
|
96
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
97
|
+
(0, logger_1.logInfo)(`Created directory: ${dirPath}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function formatCode(code) {
|
|
101
|
+
try {
|
|
102
|
+
const formatted = await prettier.format(code, {
|
|
103
|
+
parser: "typescript",
|
|
104
|
+
semi: true,
|
|
105
|
+
singleQuote: true,
|
|
106
|
+
tabWidth: 2,
|
|
107
|
+
trailingComma: "es5",
|
|
108
|
+
printWidth: 80
|
|
109
|
+
});
|
|
110
|
+
return formatted;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
(0, logger_1.logWarning)("Prettier formatting failed — saving unformatted code");
|
|
114
|
+
return code;
|
|
115
|
+
}
|
|
116
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@esha_susan/mockingbird-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered test generation CLI for Express.js backends",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mockingbird": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"dev": "ts-node src/index.ts",
|
|
17
|
+
"watch": "nodemon --exec ts-node src/index.ts",
|
|
18
|
+
"test": "jest"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"cli",
|
|
22
|
+
"testing",
|
|
23
|
+
"ai",
|
|
24
|
+
"express",
|
|
25
|
+
"jest"
|
|
26
|
+
],
|
|
27
|
+
"author": "Esha Susan",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@google/generative-ai": "^0.24.1",
|
|
31
|
+
"chalk": "^4.1.2",
|
|
32
|
+
"commander": "^15.0.0",
|
|
33
|
+
"dotenv": "^17.4.2",
|
|
34
|
+
"prettier": "^3.8.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/jest": "^30.0.0",
|
|
38
|
+
"@types/node": "^25.9.1",
|
|
39
|
+
"jest": "^30.4.2",
|
|
40
|
+
"nodemon": "^3.1.14",
|
|
41
|
+
"ts-jest": "^29.4.11",
|
|
42
|
+
"ts-node": "^10.9.2",
|
|
43
|
+
"typescript": "^6.0.3"
|
|
44
|
+
}
|
|
45
|
+
}
|