@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 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
@@ -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
+ }