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 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
- * **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.
11
- * **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.
12
- * **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.
13
- * **Live Server Doctor**: Probes your running server for security headers, CORS wildcards, stack trace/error leakage, and payload crash handling.
14
- * **Environment Variable Auditor**: Compares active `.env` files against `.env.example` to flag missing keys, empty values, and placeholder defaults before you boot.
15
- * **Static Project Auditor**: Evaluates package setup, file organization, safety configs, and dependency scopes.
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
- Find and test routes in your codebase:
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. Express/Fastify/Hono: router.get('/path', ...) or app.post('/path', ...)
16
- const expressPattern = /\b(app|router|route|fastify|hono|api)\.(get|post|put|delete|patch|options|head)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
17
- // 2. NestJS decorators: @Get('/path') or @Post('/path')
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 to prefix: @Controller('/prefix')
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
- // Check NestJS Controller prefix if any
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. Check Express-style matches
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
- let rPath = match[3];
39
- // Sanitize path variable names if necessary
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. Check NestJS matches
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
- const sanitizedFullPath = fullPath.startsWith("/") ? fullPath : `/${fullPath}`;
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: sanitizedFullPath,
89
+ routePath: rPath,
58
90
  sourceFile: relativePath,
59
91
  });
60
92
  }
61
93
  }
62
- catch (err) {
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(projectPath = ".") {
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(projectPath);
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
- finalPath = selectedRoute.routePath;
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 bodyInput = await clack.text({
258
- message: "Enter JSON payload:",
259
- placeholder: '{"name": "Alice", "email": "alice@gmail.com"}',
260
- validate: (v) => {
261
- if (!v || !v.trim())
262
- return undefined;
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(bodyInput)) {
384
+ if (clack.isCancel(bodyInputType)) {
273
385
  clack.outro("Execution stopped.");
274
386
  return;
275
387
  }
276
- const val = bodyInput.trim();
277
- if (val) {
278
- requestBody = val;
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
- // 4. Prompt for Authorization Header (Bearer token)
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("Interactive API playfield: scan codebase routes and send test requests")
61
- .argument("[path]", "Path to the project", ".")
62
- .action(async (projectPath) => {
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(projectPath);
69
+ await runApiCommand(methodOrPath, routePath, opts);
65
70
  });
66
71
  // Subcommand: bench
67
72
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bandit-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {