bandit-cli 1.0.1 → 1.0.3
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 +29 -8
- package/dist/cli/api.js +251 -40
- package/dist/index.js +9 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# Bandit CLI
|
|
1
|
+
# Bandit CLI
|
|
2
|
+
|
|
2
3
|
> **The Swiss Army Knife for Backend Developers**
|
|
3
4
|
|
|
4
5
|
`bandit` is an interactive, high-utility terminal workspace helper built for backend developers. Instead of relying on heavy GUI clients or complex shell scripts, `bandit` lets you test APIs, benchmark endpoints, terminate locked ports, validate environment setups, and audit server health directly from your command line.
|
|
@@ -7,23 +8,25 @@
|
|
|
7
8
|
|
|
8
9
|
## 🚀 Key Features
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
- **Interactive API Client**: Auto-scans your codebase (Express, Fastify, Hono, NestJS) to find route definitions, maps parameters, takes JSON payloads, prompts for Bearer Auth tokens, and displays pretty-printed HTTP responses.
|
|
12
|
+
- **High-Speed Load Benchmarker**: Benchmark any local or remote HTTP endpoint with configurable concurrency, calculating throughput (RPS), p50-p99 percentiles, and rendering a beautiful ASCII distribution chart.
|
|
13
|
+
- **Port Inspector & Process Killer**: Scans active ports, identifies the PID and process names (e.g. `node.exe`, `postgres.exe`, `ollama.exe`), and lets you terminate locked processes in one click.
|
|
14
|
+
- **Live Server Doctor**: Probes your running server for security headers, CORS wildcards, stack trace/error leakage, and payload crash handling.
|
|
15
|
+
- **Environment Variable Auditor**: Compares active `.env` files against `.env.example` to flag missing keys, empty values, and placeholder defaults before you boot.
|
|
16
|
+
- **Static Project Auditor**: Evaluates package setup, file organization, safety configs, and dependency scopes.
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
19
20
|
## 📦 Installation
|
|
20
21
|
|
|
21
22
|
Install globally via npm:
|
|
23
|
+
|
|
22
24
|
```bash
|
|
23
25
|
npm install -g bandit-cli
|
|
24
26
|
```
|
|
25
27
|
|
|
26
28
|
Or run instantly using `npx`:
|
|
29
|
+
|
|
27
30
|
```bash
|
|
28
31
|
npx bandit-cli <command>
|
|
29
32
|
```
|
|
@@ -33,37 +36,54 @@ npx bandit-cli <command>
|
|
|
33
36
|
## 🛠️ Usage & Commands
|
|
34
37
|
|
|
35
38
|
### 1. Interactive API Playfield
|
|
36
|
-
|
|
39
|
+
|
|
40
|
+
Find and test routes in your codebase interactively:
|
|
37
41
|
```bash
|
|
38
42
|
bandit api
|
|
39
43
|
```
|
|
40
44
|
|
|
45
|
+
Or execute a direct API request immediately (acts like a local curl/HTTPie):
|
|
46
|
+
```bash
|
|
47
|
+
bandit api GET /api/v1/products --url http://localhost:8080 --token "jwt-token"
|
|
48
|
+
bandit api POST /api/v1/users --body '{"name":"Raj"}'
|
|
49
|
+
```
|
|
50
|
+
|
|
41
51
|
### 2. Endpoint Benchmarking
|
|
52
|
+
|
|
42
53
|
Run a concurrent load test on an endpoint:
|
|
54
|
+
|
|
43
55
|
```bash
|
|
44
56
|
bandit bench http://localhost:3000/api/users --connections 10 --requests 200
|
|
45
57
|
```
|
|
46
58
|
|
|
47
59
|
### 3. Ports Inspector
|
|
60
|
+
|
|
48
61
|
List processes listening on local ports and terminate them:
|
|
62
|
+
|
|
49
63
|
```bash
|
|
50
64
|
bandit ports
|
|
51
65
|
```
|
|
52
66
|
|
|
53
67
|
### 4. Active Server Diagnostics
|
|
68
|
+
|
|
54
69
|
Audit a running server's security headers and vulnerabilities:
|
|
70
|
+
|
|
55
71
|
```bash
|
|
56
72
|
bandit doctor http://localhost:3000
|
|
57
73
|
```
|
|
58
74
|
|
|
59
75
|
### 5. Environment variable check
|
|
76
|
+
|
|
60
77
|
Validate local environment configurations:
|
|
78
|
+
|
|
61
79
|
```bash
|
|
62
80
|
bandit env
|
|
63
81
|
```
|
|
64
82
|
|
|
65
83
|
### 6. Static Project Audit
|
|
84
|
+
|
|
66
85
|
Audit files, directory structure, and dependencies:
|
|
86
|
+
|
|
67
87
|
```bash
|
|
68
88
|
bandit audit
|
|
69
89
|
```
|
|
@@ -71,4 +91,5 @@ bandit audit
|
|
|
71
91
|
---
|
|
72
92
|
|
|
73
93
|
## 🎨 Under the Hood
|
|
94
|
+
|
|
74
95
|
Built with TypeScript, [Commander](https://github.com/tj/commander.js), and [@clack/prompts](https://github.com/natemoo-re/clack) for a modern, fluid interactive terminal interface.
|
package/dist/cli/api.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import fg from "fast-glob";
|
|
4
4
|
import * as clack from "@clack/prompts";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
-
// Scans files for common route definitions
|
|
6
|
+
// Scans files for common route definitions and mounts
|
|
7
7
|
async function scanForRoutes(projectPath) {
|
|
8
8
|
const routes = [];
|
|
9
9
|
const files = await fg(["src/**/*.{ts,js}", "*.{ts,js}", "routes/**/*.{ts,js}", "controllers/**/*.{ts,js}"], {
|
|
@@ -11,18 +11,39 @@ async function scanForRoutes(projectPath) {
|
|
|
11
11
|
absolute: true,
|
|
12
12
|
ignore: ["**/node_modules/**", "**/dist/**", "**/*.test.{ts,js}", "**/*.spec.{ts,js}"],
|
|
13
13
|
});
|
|
14
|
+
const prefixMap = new Map();
|
|
15
|
+
// Regex to detect router mounts, e.g. app.use('/api/v1/auth', authRouter) or router.use('/auth', authRouter)
|
|
16
|
+
// Group 3 matches the path prefix, Group 4 matches the router variable name
|
|
17
|
+
const mountPattern = /\b([a-zA-Z0-9_]+)\.(use|route|group)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*([a-zA-Z0-9_]+)/gi;
|
|
18
|
+
// Read files first to construct prefixMap
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
try {
|
|
21
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
22
|
+
let match;
|
|
23
|
+
mountPattern.lastIndex = 0;
|
|
24
|
+
while ((match = mountPattern.exec(content)) !== null) {
|
|
25
|
+
const prefix = match[3];
|
|
26
|
+
const routerName = match[4];
|
|
27
|
+
if (prefix && routerName && routerName !== "express" && routerName !== "cors" && routerName !== "helmet") {
|
|
28
|
+
prefixMap.set(routerName, prefix);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
}
|
|
14
34
|
// Regex patterns
|
|
15
|
-
// 1.
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
// 1. Generic Router paths: authRouter.post('/login', ...)
|
|
36
|
+
// Group 1 matches the router variable, Group 2 matches method, Group 3 matches endpoint path
|
|
37
|
+
const expressPattern = /\b([a-zA-Z0-9_]+)\.(get|post|put|delete|patch|options|head)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
|
|
38
|
+
// 2. NestJS decorators: @Get('/path')
|
|
18
39
|
const nestPattern = /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
|
|
19
|
-
// 3. NestJS controllers
|
|
40
|
+
// 3. NestJS controllers: @Controller('/prefix')
|
|
20
41
|
const controllerPattern = /@Controller\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
|
|
21
42
|
for (const file of files) {
|
|
22
43
|
try {
|
|
23
44
|
const content = fs.readFileSync(file, "utf-8");
|
|
24
45
|
const relativePath = path.relative(projectPath, file);
|
|
25
|
-
//
|
|
46
|
+
// NestJS controller prefix
|
|
26
47
|
let controllerPrefix = "";
|
|
27
48
|
const cMatch = [...content.matchAll(controllerPattern)];
|
|
28
49
|
if (cMatch.length > 0) {
|
|
@@ -30,20 +51,30 @@ async function scanForRoutes(projectPath) {
|
|
|
30
51
|
if (controllerPrefix === "/")
|
|
31
52
|
controllerPrefix = "";
|
|
32
53
|
}
|
|
33
|
-
// 1.
|
|
54
|
+
// 1. Express-style
|
|
34
55
|
let match;
|
|
35
56
|
expressPattern.lastIndex = 0;
|
|
36
57
|
while ((match = expressPattern.exec(content)) !== null) {
|
|
58
|
+
const instanceName = match[1];
|
|
37
59
|
const method = match[2].toUpperCase();
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
const subPath = match[3];
|
|
61
|
+
let rPath = subPath;
|
|
62
|
+
// If the instanceName has a registered mount prefix, prepend it!
|
|
63
|
+
if (prefixMap.has(instanceName)) {
|
|
64
|
+
const prefix = prefixMap.get(instanceName).replace(/\/$/, "");
|
|
65
|
+
rPath = `${prefix}/${subPath.replace(/^\//, "")}`;
|
|
66
|
+
}
|
|
67
|
+
// Clean up double slashes
|
|
68
|
+
rPath = rPath.replace(/\/+/g, "/");
|
|
69
|
+
if (!rPath.startsWith("/"))
|
|
70
|
+
rPath = `/${rPath}`;
|
|
40
71
|
routes.push({
|
|
41
72
|
method,
|
|
42
73
|
routePath: rPath,
|
|
43
74
|
sourceFile: relativePath,
|
|
44
75
|
});
|
|
45
76
|
}
|
|
46
|
-
// 2.
|
|
77
|
+
// 2. NestJS
|
|
47
78
|
nestPattern.lastIndex = 0;
|
|
48
79
|
while ((match = nestPattern.exec(content)) !== null) {
|
|
49
80
|
const method = match[1].toUpperCase();
|
|
@@ -51,17 +82,16 @@ async function scanForRoutes(projectPath) {
|
|
|
51
82
|
if (subPath === "/")
|
|
52
83
|
subPath = "";
|
|
53
84
|
const fullPath = `${controllerPrefix.replace(/\/$/, "")}/${subPath.replace(/^\//, "")}`;
|
|
54
|
-
|
|
85
|
+
let rPath = fullPath.startsWith("/") ? fullPath : `/${fullPath}`;
|
|
86
|
+
rPath = rPath.replace(/\/+/g, "/");
|
|
55
87
|
routes.push({
|
|
56
88
|
method: method === "DELETE" ? "DELETE" : method,
|
|
57
|
-
routePath:
|
|
89
|
+
routePath: rPath,
|
|
58
90
|
sourceFile: relativePath,
|
|
59
91
|
});
|
|
60
92
|
}
|
|
61
93
|
}
|
|
62
|
-
catch
|
|
63
|
-
// Ignore reading errors
|
|
64
|
-
}
|
|
94
|
+
catch { }
|
|
65
95
|
}
|
|
66
96
|
// Deduplicate routes by method + routePath
|
|
67
97
|
const seen = new Set();
|
|
@@ -90,9 +120,90 @@ function detectLocalPort(projectPath) {
|
|
|
90
120
|
catch { }
|
|
91
121
|
return 3000;
|
|
92
122
|
}
|
|
93
|
-
export async function runApiCommand(
|
|
123
|
+
export async function runApiCommand(methodOrPath = ".", routePath, opts = {}) {
|
|
124
|
+
const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
125
|
+
const isDirectMode = routePath !== undefined && httpMethods.includes(methodOrPath.toUpperCase());
|
|
126
|
+
if (isDirectMode) {
|
|
127
|
+
const finalMethod = methodOrPath.toUpperCase();
|
|
128
|
+
const finalPath = routePath;
|
|
129
|
+
// Resolve base URL
|
|
130
|
+
const absoluteProjectPath = path.resolve(".");
|
|
131
|
+
const port = detectLocalPort(absoluteProjectPath);
|
|
132
|
+
let baseUrl = opts.url || `http://localhost:${port}`;
|
|
133
|
+
baseUrl = baseUrl.replace(/\/$/, "");
|
|
134
|
+
// Resolve Headers
|
|
135
|
+
const headers = {
|
|
136
|
+
Accept: "application/json, text/plain, */*",
|
|
137
|
+
};
|
|
138
|
+
if (opts.token) {
|
|
139
|
+
headers["Authorization"] = `Bearer ${opts.token}`;
|
|
140
|
+
}
|
|
141
|
+
if (opts.body) {
|
|
142
|
+
headers["Content-Type"] = "application/json";
|
|
143
|
+
}
|
|
144
|
+
// Parse custom headers -H "Key: Val"
|
|
145
|
+
if (opts.header) {
|
|
146
|
+
const hList = Array.isArray(opts.header) ? opts.header : [opts.header];
|
|
147
|
+
for (const h of hList) {
|
|
148
|
+
const colonIdx = h.indexOf(":");
|
|
149
|
+
if (colonIdx !== -1) {
|
|
150
|
+
const key = h.slice(0, colonIdx).trim();
|
|
151
|
+
const val = h.slice(colonIdx + 1).trim();
|
|
152
|
+
headers[key] = val;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const finalUrl = `${baseUrl}${finalPath}`;
|
|
157
|
+
clack.intro(chalk.bold.bgBlue.white(" Bandit Direct API Request "));
|
|
158
|
+
clack.log.info(`Sending ${chalk.bold.green(finalMethod)} request to ${chalk.cyan(finalUrl)}...`);
|
|
159
|
+
const s = clack.spinner();
|
|
160
|
+
s.start("Executing request...");
|
|
161
|
+
try {
|
|
162
|
+
const start = performance.now();
|
|
163
|
+
const res = await fetch(finalUrl, {
|
|
164
|
+
method: finalMethod,
|
|
165
|
+
headers,
|
|
166
|
+
...(opts.body ? { body: opts.body } : {}),
|
|
167
|
+
});
|
|
168
|
+
const end = performance.now();
|
|
169
|
+
const duration = end - start;
|
|
170
|
+
s.stop(`Request completed in ${duration.toFixed(1)}ms.`);
|
|
171
|
+
const contentType = res.headers.get("content-type") || "";
|
|
172
|
+
const resText = await res.text();
|
|
173
|
+
console.log("\n" + chalk.bold.cyan("📬 RESPONSE"));
|
|
174
|
+
console.log(chalk.gray("──────────────────────────────────────────────────"));
|
|
175
|
+
console.log(`Status: ${res.status >= 200 && res.status < 300 ? chalk.bold.green(res.status) : chalk.bold.red(res.status)} ${res.statusText}`);
|
|
176
|
+
console.log(`Time: ${duration.toFixed(1)} ms`);
|
|
177
|
+
console.log(chalk.gray("──────────────────────────────────────────────────"));
|
|
178
|
+
console.log(chalk.bold.cyan("Headers:"));
|
|
179
|
+
res.headers.forEach((val, key) => {
|
|
180
|
+
console.log(` ${chalk.gray(key)}: ${val}`);
|
|
181
|
+
});
|
|
182
|
+
console.log(chalk.gray("──────────────────────────────────────────────────"));
|
|
183
|
+
console.log(chalk.bold.cyan("Body:"));
|
|
184
|
+
if (contentType.includes("application/json")) {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(resText);
|
|
187
|
+
console.log(chalk.green(JSON.stringify(parsed, null, 2)));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
console.log(resText);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(resText || chalk.italic.gray("<empty body>"));
|
|
195
|
+
}
|
|
196
|
+
console.log(chalk.gray("──────────────────────────────────────────────────"));
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
s.stop("Request failed.");
|
|
200
|
+
clack.log.error(`Fetch execution error: ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
clack.outro(chalk.bold.green("Direct API call complete."));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
94
205
|
clack.intro(chalk.bold.bgBlue.white(" Bandit Interactive API Client "));
|
|
95
|
-
const absoluteProjectPath = path.resolve(
|
|
206
|
+
const absoluteProjectPath = path.resolve(methodOrPath);
|
|
96
207
|
const s = clack.spinner();
|
|
97
208
|
s.start("Scanning codebase for API endpoints...");
|
|
98
209
|
const discoveredRoutes = await scanForRoutes(absoluteProjectPath);
|
|
@@ -190,10 +301,20 @@ export async function runApiCommand(projectPath = ".") {
|
|
|
190
301
|
}
|
|
191
302
|
else {
|
|
192
303
|
finalMethod = selectedRoute.method;
|
|
193
|
-
|
|
304
|
+
// Let the developer confirm and edit the path (highly convenient for prefix additions!)
|
|
305
|
+
const editPath = await clack.text({
|
|
306
|
+
message: "Confirm or edit the route path:",
|
|
307
|
+
defaultValue: selectedRoute.routePath,
|
|
308
|
+
placeholder: selectedRoute.routePath,
|
|
309
|
+
validate: (v) => (!v || !v.startsWith("/") ? "Route path must start with a slash '/'" : undefined),
|
|
310
|
+
});
|
|
311
|
+
if (clack.isCancel(editPath)) {
|
|
312
|
+
clack.outro("Execution stopped.");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
finalPath = editPath;
|
|
194
316
|
}
|
|
195
317
|
// 1. Process Path Parameters if any (e.g. /users/:id or /users/{id})
|
|
196
|
-
// We look for :name or {name}
|
|
197
318
|
const pathParamPattern = /:([a-zA-Z0-9_]+)|\{([a-zA-Z0-9_]+)\}/g;
|
|
198
319
|
let matches;
|
|
199
320
|
const pathParams = [];
|
|
@@ -213,7 +334,6 @@ export async function runApiCommand(projectPath = ".") {
|
|
|
213
334
|
clack.outro("Execution stopped.");
|
|
214
335
|
return;
|
|
215
336
|
}
|
|
216
|
-
// Replace :param or {param}
|
|
217
337
|
resolvedPath = resolvedPath
|
|
218
338
|
.replace(`:${param}`, val)
|
|
219
339
|
.replace(`{${param}}`, val);
|
|
@@ -242,7 +362,7 @@ export async function runApiCommand(projectPath = ".") {
|
|
|
242
362
|
queryStr = trimmed.startsWith("?") ? trimmed : `?${trimmed}`;
|
|
243
363
|
}
|
|
244
364
|
}
|
|
245
|
-
// 3. Process Request Body if POST/PUT/PATCH
|
|
365
|
+
// 3. Process Request Body if POST/PUT/PATCH (Interactive Key-Value Editor!)
|
|
246
366
|
let requestBody;
|
|
247
367
|
if (["POST", "PUT", "PATCH"].includes(finalMethod)) {
|
|
248
368
|
const enterBody = await clack.confirm({
|
|
@@ -254,32 +374,119 @@ export async function runApiCommand(projectPath = ".") {
|
|
|
254
374
|
return;
|
|
255
375
|
}
|
|
256
376
|
if (enterBody) {
|
|
257
|
-
const
|
|
258
|
-
message: "
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
JSON.parse(v);
|
|
265
|
-
return undefined;
|
|
266
|
-
}
|
|
267
|
-
catch {
|
|
268
|
-
return "Invalid JSON syntax. Please enter valid JSON.";
|
|
269
|
-
}
|
|
270
|
-
},
|
|
377
|
+
const bodyInputType = await clack.select({
|
|
378
|
+
message: "How would you like to enter the JSON body?",
|
|
379
|
+
options: [
|
|
380
|
+
{ value: "kv", label: "Interactive Key-Value builder (Easy/Convenient)" },
|
|
381
|
+
{ value: "raw", label: "Paste raw JSON string" },
|
|
382
|
+
],
|
|
271
383
|
});
|
|
272
|
-
if (clack.isCancel(
|
|
384
|
+
if (clack.isCancel(bodyInputType)) {
|
|
273
385
|
clack.outro("Execution stopped.");
|
|
274
386
|
return;
|
|
275
387
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
388
|
+
if (bodyInputType === "kv") {
|
|
389
|
+
const bodyObj = {};
|
|
390
|
+
let addingFields = true;
|
|
391
|
+
while (addingFields) {
|
|
392
|
+
const fieldKey = await clack.text({
|
|
393
|
+
message: "Enter field key:",
|
|
394
|
+
placeholder: "e.g. email",
|
|
395
|
+
});
|
|
396
|
+
if (clack.isCancel(fieldKey) || !fieldKey || !fieldKey.trim())
|
|
397
|
+
break;
|
|
398
|
+
const fieldVal = await clack.text({
|
|
399
|
+
message: `Enter value for '${fieldKey.trim()}':`,
|
|
400
|
+
placeholder: "e.g. raj@example.com",
|
|
401
|
+
});
|
|
402
|
+
if (clack.isCancel(fieldVal))
|
|
403
|
+
break;
|
|
404
|
+
const trimmedVal = fieldVal.trim();
|
|
405
|
+
let parsedVal = trimmedVal;
|
|
406
|
+
// Smart casting
|
|
407
|
+
if (trimmedVal.toLowerCase() === "true")
|
|
408
|
+
parsedVal = true;
|
|
409
|
+
else if (trimmedVal.toLowerCase() === "false")
|
|
410
|
+
parsedVal = false;
|
|
411
|
+
else if (trimmedVal.toLowerCase() === "null")
|
|
412
|
+
parsedVal = null;
|
|
413
|
+
else if (!isNaN(Number(trimmedVal)) && trimmedVal !== "")
|
|
414
|
+
parsedVal = Number(trimmedVal);
|
|
415
|
+
bodyObj[fieldKey.trim()] = parsedVal;
|
|
416
|
+
const addMore = await clack.confirm({
|
|
417
|
+
message: "Add another field to the request body?",
|
|
418
|
+
initialValue: false,
|
|
419
|
+
});
|
|
420
|
+
if (clack.isCancel(addMore) || !addMore) {
|
|
421
|
+
addingFields = false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
requestBody = JSON.stringify(bodyObj);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// Raw JSON input
|
|
428
|
+
const bodyInput = await clack.text({
|
|
429
|
+
message: "Enter JSON payload:",
|
|
430
|
+
placeholder: '{"name": "Alice", "email": "alice@gmail.com"}',
|
|
431
|
+
validate: (v) => {
|
|
432
|
+
if (!v || !v.trim())
|
|
433
|
+
return undefined;
|
|
434
|
+
try {
|
|
435
|
+
JSON.parse(v);
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return "Invalid JSON syntax. Please enter valid JSON.";
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
if (clack.isCancel(bodyInput)) {
|
|
444
|
+
clack.outro("Execution stopped.");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const val = bodyInput.trim();
|
|
448
|
+
if (val) {
|
|
449
|
+
requestBody = val;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// 4. Custom Headers Editor
|
|
455
|
+
const customHeaders = {};
|
|
456
|
+
const addHeaders = await clack.confirm({
|
|
457
|
+
message: "Do you want to add custom request headers?",
|
|
458
|
+
initialValue: false,
|
|
459
|
+
});
|
|
460
|
+
if (clack.isCancel(addHeaders)) {
|
|
461
|
+
clack.outro("Execution stopped.");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (addHeaders) {
|
|
465
|
+
let addingHeaders = true;
|
|
466
|
+
while (addingHeaders) {
|
|
467
|
+
const headerKey = await clack.text({
|
|
468
|
+
message: "Enter Header Name:",
|
|
469
|
+
placeholder: "e.g. X-Custom-Key",
|
|
470
|
+
});
|
|
471
|
+
if (clack.isCancel(headerKey) || !headerKey || !headerKey.trim())
|
|
472
|
+
break;
|
|
473
|
+
const headerVal = await clack.text({
|
|
474
|
+
message: `Enter value for '${headerKey.trim()}':`,
|
|
475
|
+
placeholder: "e.g. test-value",
|
|
476
|
+
});
|
|
477
|
+
if (clack.isCancel(headerVal))
|
|
478
|
+
break;
|
|
479
|
+
customHeaders[headerKey.trim()] = headerVal.trim();
|
|
480
|
+
const addMoreHeaders = await clack.confirm({
|
|
481
|
+
message: "Add another custom header?",
|
|
482
|
+
initialValue: false,
|
|
483
|
+
});
|
|
484
|
+
if (clack.isCancel(addMoreHeaders) || !addMoreHeaders) {
|
|
485
|
+
addingHeaders = false;
|
|
279
486
|
}
|
|
280
487
|
}
|
|
281
488
|
}
|
|
282
|
-
//
|
|
489
|
+
// 5. Prompt for Authorization Header (Bearer token)
|
|
283
490
|
const addAuth = await clack.confirm({
|
|
284
491
|
message: "Do you want to add an Authorization Bearer token?",
|
|
285
492
|
initialValue: false,
|
|
@@ -304,6 +511,9 @@ export async function runApiCommand(projectPath = ".") {
|
|
|
304
511
|
// Confirm and Execute Request
|
|
305
512
|
const finalUrl = `${baseUrl}${resolvedPath}${queryStr}`;
|
|
306
513
|
clack.log.info(`Ready to request: ${chalk.bold.green(finalMethod)} ${chalk.cyan(finalUrl)}`);
|
|
514
|
+
if (Object.keys(customHeaders).length > 0) {
|
|
515
|
+
clack.log.info(`Headers:\n${chalk.gray(JSON.stringify(customHeaders, null, 2))}`);
|
|
516
|
+
}
|
|
307
517
|
if (bearerToken) {
|
|
308
518
|
const preview = bearerToken.length > 12 ? `${bearerToken.slice(0, 8)}...` : bearerToken;
|
|
309
519
|
clack.log.info(`Auth: Bearer ${preview} (masked)`);
|
|
@@ -325,6 +535,7 @@ export async function runApiCommand(projectPath = ".") {
|
|
|
325
535
|
const headers = {
|
|
326
536
|
Accept: "application/json, text/plain, */*",
|
|
327
537
|
...(requestBody ? { "Content-Type": "application/json" } : {}),
|
|
538
|
+
...customHeaders,
|
|
328
539
|
};
|
|
329
540
|
if (bearerToken) {
|
|
330
541
|
headers["Authorization"] = `Bearer ${bearerToken}`;
|
package/dist/index.js
CHANGED
|
@@ -57,11 +57,16 @@ program
|
|
|
57
57
|
// Subcommand: api
|
|
58
58
|
program
|
|
59
59
|
.command("api")
|
|
60
|
-
.description("
|
|
61
|
-
.argument("[
|
|
62
|
-
.
|
|
60
|
+
.description("API playfield: scan codebase routes (interactive) or send direct requests")
|
|
61
|
+
.argument("[method_or_path]", "HTTP Method or project path", ".")
|
|
62
|
+
.argument("[route_path]", "Route path (only when method is provided)")
|
|
63
|
+
.option("-u, --url <url>", "Server base URL")
|
|
64
|
+
.option("-b, --body <body>", "JSON request body")
|
|
65
|
+
.option("-t, --token <token>", "Bearer Auth token")
|
|
66
|
+
.option("-H, --header <headers...>", "Custom headers in Key:Value format")
|
|
67
|
+
.action(async (methodOrPath, routePath, opts) => {
|
|
63
68
|
const { runApiCommand } = await import("./cli/api.js");
|
|
64
|
-
await runApiCommand(
|
|
69
|
+
await runApiCommand(methodOrPath, routePath, opts);
|
|
65
70
|
});
|
|
66
71
|
// Subcommand: bench
|
|
67
72
|
program
|