bandit-cli 1.0.6 → 1.0.7

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/dist/cli/env.js CHANGED
@@ -69,7 +69,10 @@ export async function runEnvCommand(projectPath = ".") {
69
69
  }
70
70
  else {
71
71
  const val = currentKeysMap.get(expectedKey);
72
- if (val === "" || val === "your_key_here" || val === "placeholder" || val.includes("TODO")) {
72
+ if (val === "" ||
73
+ val === "your_key_here" ||
74
+ val === "placeholder" ||
75
+ val.includes("TODO")) {
73
76
  emptyKeys.push(expectedKey);
74
77
  }
75
78
  }
@@ -81,7 +84,9 @@ export async function runEnvCommand(projectPath = ".") {
81
84
  }
82
85
  }
83
86
  // Output report for this file
84
- if (missingKeys.length === 0 && emptyKeys.length === 0 && extraKeys.length === 0) {
87
+ if (missingKeys.length === 0 &&
88
+ emptyKeys.length === 0 &&
89
+ extraKeys.length === 0) {
85
90
  clack.log.success(chalk.green(` ✔ ${envFile} is perfectly in sync with .env.example!`));
86
91
  }
87
92
  else {
package/dist/index.js CHANGED
@@ -38,17 +38,6 @@ program
38
38
  const { runPortsCommand } = await import("./cli/ports.js");
39
39
  await runPortsCommand();
40
40
  });
41
- // Subcommand: env
42
- program
43
- .command("env")
44
- .description("Audit local .env configurations against .env.example")
45
- .argument("[path]", "Path to the project", ".")
46
- .action(async (projectPath) => {
47
- const { resolveProjectPath } = await import("./utils/monorepo.js");
48
- const resolvedPath = await resolveProjectPath(projectPath || ".");
49
- const { runEnvCommand } = await import("./cli/env.js");
50
- await runEnvCommand(resolvedPath);
51
- });
52
41
  // Subcommand: doctor
53
42
  program
54
43
  .command("doctor")
@@ -4,6 +4,7 @@ import { ruleEnvExampleExists } from "./mvp.rules.js";
4
4
  import { ruleEnvInGitignore } from "./mvp.rules.js";
5
5
  import { ruleDockerfileExists } from "./mvp.rules.js";
6
6
  import { ruleHasTestScript } from "./mvp.rules.js";
7
+ import { ruleEnvKeysMatch } from "./mvp.rules.js";
7
8
  import { ruleSrcFolderExists, ruleDetectFramework, ruleSecurityDeps, ruleGlobalErrorHandler, } from "./phase2.rules.js";
8
9
  import { ruleLoggingSetup, ruleEnvValidation, ruleDatabaseSetup, ruleTypeScriptStrict, ruleProductionDeps, } from "./phase3.rules.js";
9
10
  export const rules = [
@@ -12,6 +13,7 @@ export const rules = [
12
13
  ruleEnvExists,
13
14
  ruleEnvExampleExists,
14
15
  ruleEnvInGitignore,
16
+ ruleEnvKeysMatch,
15
17
  ruleDockerfileExists,
16
18
  ruleHasTestScript,
17
19
  // Phase 2 - Structure + detection + security
@@ -136,3 +136,106 @@ export const ruleHasTestScript = {
136
136
  };
137
137
  },
138
138
  };
139
+ function parseEnvContent(content) {
140
+ const map = new Map();
141
+ const lines = content.split("\n");
142
+ for (const line of lines) {
143
+ const trimmed = line.trim();
144
+ if (!trimmed || trimmed.startsWith("#"))
145
+ continue;
146
+ const firstEqual = trimmed.indexOf("=");
147
+ if (firstEqual === -1)
148
+ continue;
149
+ const key = trimmed.slice(0, firstEqual).trim();
150
+ let val = trimmed.slice(firstEqual + 1).trim();
151
+ if ((val.startsWith('"') && val.endsWith('"')) ||
152
+ (val.startsWith("'") && val.endsWith("'"))) {
153
+ val = val.slice(1, -1);
154
+ }
155
+ map.set(key, val);
156
+ }
157
+ return map;
158
+ }
159
+ export const ruleEnvKeysMatch = {
160
+ id: "env-keys-match",
161
+ title: ".env aligns with .env.example",
162
+ severity: "warn",
163
+ async run(ctx) {
164
+ const envPath = path.join(ctx.projectPath, ".env");
165
+ const envExamplePath = path.join(ctx.projectPath, ".env.example");
166
+ if (!exists(envPath) || !exists(envExamplePath)) {
167
+ return {
168
+ id: this.id,
169
+ title: this.title,
170
+ severity: this.severity,
171
+ status: "skip",
172
+ details: "Requires both .env and .env.example files to run comparison.",
173
+ };
174
+ }
175
+ try {
176
+ const envContent = readText(envPath) || "";
177
+ const exampleContent = readText(envExamplePath) || "";
178
+ const envKeys = parseEnvContent(envContent);
179
+ const exampleKeys = parseEnvContent(exampleContent);
180
+ const missing = [];
181
+ const extra = [];
182
+ const placeholders = [];
183
+ for (const key of exampleKeys.keys()) {
184
+ if (!envKeys.has(key)) {
185
+ missing.push(key);
186
+ }
187
+ else {
188
+ const val = envKeys.get(key);
189
+ if (val === "" ||
190
+ val === "your_key_here" ||
191
+ val === "placeholder" ||
192
+ val.includes("TODO")) {
193
+ placeholders.push(key);
194
+ }
195
+ }
196
+ }
197
+ for (const key of envKeys.keys()) {
198
+ if (!exampleKeys.has(key)) {
199
+ extra.push(key);
200
+ }
201
+ }
202
+ if (missing.length === 0 && extra.length === 0 && placeholders.length === 0) {
203
+ return {
204
+ id: this.id,
205
+ title: this.title,
206
+ severity: this.severity,
207
+ status: "pass",
208
+ details: ".env has all keys defined in .env.example with active values.",
209
+ };
210
+ }
211
+ const issues = [];
212
+ if (missing.length > 0) {
213
+ issues.push(`Missing keys: ${missing.join(", ")}`);
214
+ }
215
+ if (placeholders.length > 0) {
216
+ issues.push(`Placeholder/Empty values: ${placeholders.join(", ")}`);
217
+ }
218
+ if (extra.length > 0) {
219
+ issues.push(`Extra keys: ${extra.join(", ")}`);
220
+ }
221
+ return {
222
+ id: this.id,
223
+ title: this.title,
224
+ severity: this.severity,
225
+ status: "fail",
226
+ details: issues.join(" | "),
227
+ suggestion: "Update your .env or .env.example to keep them in sync and replace any placeholders.",
228
+ };
229
+ }
230
+ catch (err) {
231
+ return {
232
+ id: this.id,
233
+ title: this.title,
234
+ severity: this.severity,
235
+ status: "fail",
236
+ details: `Failed to compare env files: ${err.message}`,
237
+ suggestion: "Ensure env files are valid text files.",
238
+ };
239
+ }
240
+ },
241
+ };
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { scanForRoutes } from "../cli/api.js";
2
4
  import { StudioDB } from "./db.js";
3
5
  export async function generateApiBlueprint(projectPath) {
@@ -24,11 +26,31 @@ export async function generateApiBlueprint(projectPath) {
24
26
  // Find matching telemetry if benchmarked
25
27
  const matchBench = benchmarks.find(b => b.targetUrl.endsWith(r.routePath));
26
28
  const metrics = matchBench ? { rps: matchBench.rps, p99: matchBench.latency.p99, avg: matchBench.latency.avg } : undefined;
29
+ // Static Auth Status Detection
30
+ let authStatus = "Public";
31
+ try {
32
+ const fullPath = path.resolve(projectPath, r.sourceFile);
33
+ if (fs.existsSync(fullPath)) {
34
+ const fileContent = fs.readFileSync(fullPath, "utf-8");
35
+ const lines = fileContent.split("\n");
36
+ // Check if any line defining this path contains auth keywords
37
+ const matchingLine = lines.find(line => line.includes(r.routePath) ||
38
+ (line.includes(r.method.toLowerCase()) && line.includes(r.routePath.split("/").pop() || "___")));
39
+ if (matchingLine) {
40
+ const authKeywords = ["auth", "protect", "require", "jwt", "session", "admin", "vendor", "guard", "cookie"];
41
+ const hasAuth = authKeywords.some(kw => matchingLine.toLowerCase().includes(kw));
42
+ if (hasAuth)
43
+ authStatus = "Protected";
44
+ }
45
+ }
46
+ }
47
+ catch { }
27
48
  nodes.push({
28
49
  id: routeId,
29
50
  label: routeLabel,
30
51
  type: "route",
31
52
  details: r.sourceFile,
53
+ authStatus,
32
54
  metrics,
33
55
  });
34
56
  const controllerName = r.sourceFile.split(/[/\\]/).pop() || "Controller";
@@ -1,4 +1,6 @@
1
1
  import http from "node:http";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
2
4
  import { StudioDB } from "./db.js";
3
5
  import { generateApiBlueprint } from "./scanner.js";
4
6
  export async function startStudioServer(projectPath, port = 4000) {
@@ -11,109 +13,234 @@ export async function startStudioServer(projectPath, port = 4000) {
11
13
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
14
  <title>Bandit Studio - Backend Developer Platform</title>
13
15
  <style>
16
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
17
+
14
18
  :root {
15
- --bg: #09090b;
16
- --card-bg: #141417;
17
- --card-hover: #1c1c21;
18
- --border: #27272a;
19
- --border-bright: #3f3f46;
19
+ --bg: #050507;
20
+ --card-bg: #0c0c0e;
21
+ --card-hover: #121215;
22
+ --border: #1f1f23;
23
+ --border-bright: #2f2f37;
20
24
  --text: #f4f4f5;
21
- --text-dim: #a1a1aa;
25
+ --text-dim: #71717a;
22
26
  --primary: #eab308;
23
27
  --primary-hover: #facc15;
24
- --primary-dim: rgba(234, 179, 8, 0.1);
28
+ --primary-dim: rgba(234, 179, 8, 0.08);
25
29
  --success: #22c55e;
30
+ --success-dim: rgba(34, 197, 94, 0.08);
26
31
  --warning: #f59e0b;
32
+ --warning-dim: rgba(245, 158, 11, 0.08);
27
33
  --danger: #ef4444;
28
- --accent-blue: #38bdf8;
29
- --accent-purple: #c084fc;
34
+ --danger-dim: rgba(239, 68, 68, 0.08);
35
+ --accent: #a855f7;
30
36
  }
31
37
  * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Inter', -apple-system, sans-serif; }
32
38
  body { background-color: var(--bg); color: var(--text); display: flex; flex-direction: column; min-height: 100vh; }
33
- header { background: #000000; border-bottom: 1px solid var(--border); padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; }
34
- .logo { font-size: 1.25rem; font-weight: 800; color: var(--primary); display: flex; align-items: center; gap: 0.6rem; letter-spacing: -0.02em; }
35
- .logo-badge { background: var(--primary); color: #000; font-size: 0.7rem; font-weight: 900; padding: 0.15rem 0.4rem; border-radius: 0.2rem; text-transform: uppercase; }
36
- nav { display: flex; gap: 0.5rem; background: #141417; padding: 0.3rem; border-radius: 0.5rem; border: 1px solid var(--border); }
39
+ header { background: #000000; border-bottom: 1px solid var(--border); padding: 0.8rem 2rem; display: flex; justify-content: space-between; align-items: center; }
40
+
41
+ .logo-container { display: flex; align-items: center; gap: 0.65rem; }
42
+ .logo-container img { height: 32px; width: auto; object-fit: contain; }
43
+ .logo-text { font-size: 1.45rem; font-weight: 800; color: #ffffff; letter-spacing: -0.03em; }
44
+ .logo-beta { font-size: 0.65rem; font-weight: 700; background: #131316; border: 1px solid var(--border); color: var(--text-dim); padding: 0.15rem 0.5rem; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.05em; margin-left: 0.1rem; }
45
+
46
+ nav { display: flex; gap: 0.5rem; background: #0c0c0e; padding: 0.3rem; border-radius: 0.5rem; border: 1px solid var(--border); }
37
47
  nav button { background: none; border: none; color: var(--text-dim); font-size: 0.875rem; font-weight: 600; padding: 0.5rem 1rem; cursor: pointer; border-radius: 0.375rem; transition: all 0.15s ease; }
38
- nav button.active { color: #000; background: var(--primary); font-weight: 700; }
48
+ nav button.active { color: #000000; background: var(--primary); font-weight: 700; }
39
49
  nav button:hover:not(.active) { color: var(--text); background: var(--card-hover); }
50
+
40
51
  main { flex: 1; padding: 2rem; max-width: 1280px; margin: 0 auto; width: 100%; }
41
52
  .tab-content { display: none; }
42
53
  .tab-content.active { display: block; }
43
- .section-header { margin-bottom: 1.5rem; }
44
- .section-header h2 { font-size: 1.5rem; font-weight: 700; color: var(--text); letter-spacing: -0.02em; }
54
+
55
+ .section-header { margin-bottom: 2rem; }
56
+ .section-header h2 { font-size: 1.6rem; font-weight: 800; color: var(--text); letter-spacing: -0.02em; }
45
57
  .section-header p { color: var(--text-dim); font-size: 0.9rem; margin-top: 0.25rem; }
46
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.5rem; }
47
- .card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; position: relative; overflow: hidden; }
48
- .card h3 { font-size: 1.05rem; font-weight: 700; margin-bottom: 1rem; color: var(--primary); display: flex; align-items: center; justify-content: space-between; }
49
- table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; text-align: left; }
50
- th, td { padding: 0.85rem 1rem; border-bottom: 1px solid var(--border); font-size: 0.875rem; }
51
- th { color: var(--text-dim); font-weight: 600; background: #09090b; }
58
+
59
+ .analytics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
60
+ .card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; transition: border-color 0.2s; }
61
+ .card:hover { border-color: var(--border-bright); }
62
+ .card h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 1.25rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
63
+
64
+ /* Progress Rings style */
65
+ .rings-row { display: flex; justify-content: space-around; align-items: center; height: 100px; }
66
+ .ring-container { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
67
+ .ring-svg { transform: rotate(-90deg); }
68
+ .ring-bg { fill: none; stroke: var(--border); stroke-width: 8; }
69
+ .ring-bar { fill: none; stroke: var(--primary); stroke-width: 8; stroke-dasharray: 220; stroke-dashoffset: 220; transition: stroke-dashoffset 1s ease-out; stroke-linecap: round; }
70
+ .ring-text { font-size: 0.85rem; font-weight: 800; color: var(--text); font-family: 'JetBrains Mono', monospace; }
71
+
72
+ /* Exposure Bar */
73
+ .exposure-box { display: flex; flex-direction: column; gap: 0.75rem; margin-top: 0.5rem; }
74
+ .progress-bar-bg { height: 10px; background: var(--border); border-radius: 999px; overflow: hidden; display: flex; }
75
+ .progress-bar-val { height: 100%; background: var(--primary); transition: width 0.8s ease-out; }
76
+
77
+ /* Latency Radar (Slowest Endpoints Chart) */
78
+ .radar-list { display: flex; flex-direction: column; gap: 0.75rem; }
79
+ .radar-item { display: flex; flex-direction: column; gap: 0.25rem; }
80
+ .radar-labels { display: flex; justify-content: space-between; font-size: 0.8rem; }
81
+ .radar-labels span { font-family: 'JetBrains Mono', monospace; }
82
+ .radar-bar-bg { height: 8px; background: var(--border); border-radius: 999px; overflow: hidden; }
83
+ .radar-bar-val { height: 100%; background: var(--danger); border-radius: 999px; transition: width 0.8s ease-out; }
84
+
85
+ /* Search Bar & Matrix Layout */
86
+ .controls-row { display: flex; gap: 1rem; align-items: center; margin-bottom: 1.25rem; }
87
+ .search-input { flex: 1; background: #000; border: 1px solid var(--border); padding: 0.75rem 1rem; border-radius: 0.5rem; color: var(--text); font-size: 0.9rem; transition: border-color 0.15s; }
88
+ .search-input:focus { outline: none; border-color: var(--primary); }
89
+
90
+ .matrix-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; }
91
+
92
+ table { width: 100%; border-collapse: collapse; text-align: left; }
93
+ th, td { padding: 1rem; border-bottom: 1px solid var(--border); font-size: 0.875rem; }
94
+ th { color: var(--text-dim); font-weight: 600; background: #000; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
52
95
  tr:hover td { background: var(--card-hover); }
53
- .badge { padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; display: inline-block; }
54
- .badge-success { background: rgba(34, 197, 94, 0.15); color: var(--success); border: 1px solid rgba(34, 197, 94, 0.3); }
55
- .badge-warning { background: rgba(245, 158, 11, 0.15); color: var(--warning); border: 1px solid rgba(245, 158, 11, 0.3); }
56
- .badge-danger { background: rgba(239, 68, 68, 0.15); color: var(--danger); border: 1px solid rgba(239, 68, 68, 0.3); }
57
- .badge-yellow { background: var(--primary-dim); color: var(--primary); border: 1px solid rgba(234, 179, 8, 0.3); }
58
96
 
59
- /* Architecture Blueprint Flow Layout */
60
- .flow-container { display: flex; flex-direction: column; gap: 1.5rem; margin-top: 1rem; }
61
- .flow-row { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; background: #09090b; padding: 1.25rem; border-radius: 0.75rem; border: 1px solid var(--border); }
62
- .flow-step { background: var(--card-bg); border: 1px solid var(--border); padding: 0.85rem 1.1rem; border-radius: 0.5rem; font-size: 0.875rem; display: flex; flex-direction: column; gap: 0.25rem; min-width: 200px; }
63
- .flow-step.route { border-color: var(--primary); }
64
- .flow-step.controller { border-color: var(--accent-blue); }
65
- .flow-step.service { border-color: var(--accent-purple); }
66
- .flow-step.infra { border-color: var(--success); }
67
- .flow-arrow { color: var(--primary); font-weight: bold; font-size: 1.2rem; }
68
- .metric-tag { font-size: 0.75rem; background: var(--primary-dim); color: var(--primary); padding: 0.15rem 0.4rem; border-radius: 0.2rem; font-weight: 700; width: fit-content; margin-top: 0.2rem; }
69
- code { font-family: monospace; background: #000; padding: 0.15rem 0.35rem; border-radius: 0.2rem; font-size: 0.8rem; color: var(--primary); }
97
+ .badge { padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; display: inline-block; border: 1px solid transparent; }
98
+ .badge-success { background: var(--success-dim); color: var(--success); border-color: rgba(34, 197, 94, 0.2); }
99
+ .badge-warning { background: var(--warning-dim); color: var(--warning); border-color: rgba(245, 158, 11, 0.2); }
100
+ .badge-danger { background: var(--danger-dim); color: var(--danger); border-color: rgba(239, 68, 68, 0.2); }
101
+ .badge-yellow { background: var(--primary-dim); color: var(--primary); border-color: rgba(234, 179, 8, 0.2); }
102
+
103
+ .actions-cell { display: flex; gap: 0.5rem; }
104
+ .btn-action { background: var(--border); border: 1px solid var(--border); color: var(--text-dim); padding: 0.4rem 0.75rem; border-radius: 0.375rem; cursor: pointer; font-size: 0.75rem; font-weight: 600; display: inline-flex; align-items: center; gap: 0.35rem; transition: all 0.15s; }
105
+ .btn-action:hover { color: var(--text); background: var(--border-bright); }
106
+
107
+ code, td strong, .radar-labels span, .ring-text, td code { font-family: 'JetBrains Mono', monospace; }
108
+ code { background: #000; padding: 0.15rem 0.35rem; border-radius: 0.2rem; font-size: 0.8rem; color: var(--primary); }
70
109
  </style>
71
110
  </head>
72
111
  <body>
73
112
  <header>
74
- <div class="logo">⚡ BANDIT <span class="logo-badge">STUDIO</span></div>
113
+ <div class="logo-container">
114
+ <img src="https://res.cloudinary.com/dv7iah7yv/image/upload/v1783707247/Gemini_Generated_Image_6xf1oi6xf1oi6xf1-removebg-preview_znn27s.png" alt="Bandit Logo" />
115
+ <span class="logo-text">Bandit CLI <span style="color: var(--primary);">Studio</span></span>
116
+ <span class="logo-beta">BETA</span>
117
+ </div>
75
118
  <nav>
76
- <button class="active" onclick="showTab('performance', this)">📈 Performance Regressions</button>
77
- <button onclick="showTab('blueprint', this)">🗺️ Architecture Blueprint</button>
78
- <button onclick="showTab('audits', this)">🛡️ Security Inspector</button>
119
+ <button onclick="showTab('blueprint', this)" class="active" style="display: inline-flex; align-items: center; gap: 0.45rem;">
120
+ <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg>
121
+ API Health Matrix
122
+ </button>
123
+ <button onclick="showTab('performance', this)" style="display: inline-flex; align-items: center; gap: 0.45rem;">
124
+ <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
125
+ Performance History
126
+ </button>
127
+ <button onclick="showTab('audits', this)" style="display: inline-flex; align-items: center; gap: 0.45rem;">
128
+ <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
129
+ Security Inspector
130
+ </button>
79
131
  </nav>
80
132
  </header>
81
133
  <main>
82
- <div id="performance" class="tab-content active">
134
+ <!-- API Health Matrix (Tab 1) -->
135
+ <div id="blueprint" class="tab-content active">
83
136
  <div class="section-header">
84
- <h2>Performance & Regression History</h2>
85
- <p>Tracks benchmark latency and throughput linked with Git commits.</p>
137
+ <h2>API Health & Performance Matrix</h2>
138
+ <p>Real-time endpoint registry tracking authentication scopes, load test latencies, and active vulnerability states.</p>
86
139
  </div>
87
- <div class="card">
88
- <h3>Recorded Load Test Runs</h3>
89
- <div id="benchmarks-table-container">Loading metrics...</div>
140
+
141
+ <!-- Analytics Row -->
142
+ <div class="analytics-grid">
143
+ <div class="card">
144
+ <h3>Testing Coverage</h3>
145
+ <div class="rings-row">
146
+ <div class="ring-container">
147
+ <svg width="70" height="70" class="ring-svg">
148
+ <circle cx="35" cy="35" r="30" class="ring-bg"></circle>
149
+ <circle cx="35" cy="35" r="30" class="ring-bar" id="ring-bench"></circle>
150
+ </svg>
151
+ <span class="ring-text" id="ring-bench-text">0%</span>
152
+ <span style="font-size: 0.75rem; color: var(--text-dim);">Load Tested</span>
153
+ </div>
154
+ <div class="ring-container">
155
+ <svg width="70" height="70" class="ring-svg">
156
+ <circle cx="35" cy="35" r="30" class="ring-bg"></circle>
157
+ <circle cx="35" cy="35" r="30" class="ring-bar" id="ring-security" style="stroke: var(--success);"></circle>
158
+ </svg>
159
+ <span class="ring-text" id="ring-security-text">0%</span>
160
+ <span style="font-size: 0.75rem; color: var(--text-dim);">Secured</span>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <div class="card">
166
+ <h3>Public Route Exposure</h3>
167
+ <div class="exposure-box">
168
+ <div style="display: flex; justify-content: space-between; font-size: 0.85rem;">
169
+ <span>🌐 Public: <strong id="exposure-public">0</strong></span>
170
+ <span>🔑 Protected: <strong id="exposure-private">0</strong></span>
171
+ </div>
172
+ <div class="progress-bar-bg">
173
+ <div class="progress-bar-val" id="exposure-bar-val" style="width: 0%;"></div>
174
+ </div>
175
+ <span style="font-size: 0.75rem; color: var(--text-dim);" id="exposure-ratio-desc">Analyzing exposure...</span>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="card">
180
+ <h3>Latency Hotspots (Avg ms)</h3>
181
+ <div class="radar-list" id="radar-list">
182
+ <p style="color: var(--text-dim); font-size: 0.85rem; padding-top: 0.5rem;">No benchmark data available.</p>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <!-- Matrix Table -->
188
+ <div class="controls-row">
189
+ <input type="text" class="search-input" id="route-search" placeholder="Filter endpoints by path, method, or controller file..." oninput="filterRoutes()" />
190
+ </div>
191
+
192
+ <div class="matrix-card">
193
+ <h3>API Endpoint Registry</h3>
194
+ <div style="overflow-x: auto; margin-top: 1rem;">
195
+ <table id="matrix-table">
196
+ <thead>
197
+ <tr>
198
+ <th>Endpoint</th>
199
+ <th>Auth Status</th>
200
+ <th>Performance (RPS / Latency)</th>
201
+ <th>Security Scan</th>
202
+ <th>Quick Actions</th>
203
+ </tr>
204
+ </thead>
205
+ <tbody id="matrix-table-body">
206
+ <tr>
207
+ <td colspan="5" style="color: var(--text-dim); text-align: center; padding: 2rem;">Loading API blueprint...</td>
208
+ </tr>
209
+ </tbody>
210
+ </table>
211
+ </div>
90
212
  </div>
91
213
  </div>
92
214
 
93
- <div id="blueprint" class="tab-content">
215
+ <!-- Performance History (Tab 2) -->
216
+ <div id="performance" class="tab-content">
94
217
  <div class="section-header">
95
- <h2>Backend Architecture Blueprint</h2>
96
- <p>Visual component dependency map tracing HTTP requests down to Controllers, Services, and Database layers.</p>
218
+ <h2>Performance & Regression History</h2>
219
+ <p>Tracks benchmark latency and throughput trends linked with Git commits.</p>
97
220
  </div>
98
221
  <div class="card">
99
- <h3>Request Execution Flow</h3>
100
- <div id="blueprint-container" class="flow-container">Loading architecture graph...</div>
222
+ <h3>Recorded Load Test Runs</h3>
223
+ <div id="benchmarks-table-container">Loading metrics...</div>
101
224
  </div>
102
225
  </div>
103
226
 
227
+ <!-- Security Inspector (Tab 3) -->
104
228
  <div id="audits" class="tab-content">
105
229
  <div class="section-header">
106
230
  <h2>Security & Codebase Inspector</h2>
107
- <p>Active penetration test findings (SQLi, XSS, Header Security) and framework diagnostic reports.</p>
231
+ <p>Active penetration test findings (SQLi, XSS, Header Security) and diagnostic logs.</p>
108
232
  </div>
109
233
  <div class="card">
110
234
  <h3>Active Penetration Test Results</h3>
111
- <div id="audits-container">Loading diagnostic findings...</div>
235
+ <div id="audits-container">Loading diagnostics...</div>
112
236
  </div>
113
237
  </div>
114
238
  </main>
115
239
 
116
240
  <script>
241
+ let globalRoutes = [];
242
+ let globalAudits = [];
243
+
117
244
  function showTab(tabId, btn) {
118
245
  document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
119
246
  document.querySelectorAll('nav button').forEach(el => el.classList.remove('active'));
@@ -121,25 +248,107 @@ export async function startStudioServer(projectPath, port = 4000) {
121
248
  btn.classList.add('active');
122
249
  }
123
250
 
251
+ function copyToClipboard(text) {
252
+ navigator.clipboard.writeText(text).then(() => {
253
+ alert("Command copied to clipboard: " + text);
254
+ }).catch(err => {
255
+ console.error("Could not copy text: ", err);
256
+ });
257
+ }
258
+
259
+ function filterRoutes() {
260
+ const q = document.getElementById('route-search').value.toLowerCase();
261
+ const filtered = globalRoutes.filter(r =>
262
+ r.label.toLowerCase().includes(q) ||
263
+ (r.details && r.details.toLowerCase().includes(q))
264
+ );
265
+ renderMatrixTable(filtered);
266
+ }
267
+
268
+ function renderMatrixTable(routes) {
269
+ const tbody = document.getElementById('matrix-table-body');
270
+ if (routes.length === 0) {
271
+ tbody.innerHTML = '<tr><td colspan="5" style="color: var(--text-dim); text-align: center; padding: 2rem;">No matching endpoints found.</td></tr>';
272
+ return;
273
+ }
274
+
275
+ let html = '';
276
+ routes.forEach(r => {
277
+ const method = r.label.match(/\\[([A-Z]+)\\]/)[1];
278
+ const path = r.label.replace(/\\[([A-Z]+)\\]\\s*/, '');
279
+
280
+ // Performance
281
+ let perfHtml = '<span style="color: var(--text-dim); display: inline-flex; align-items: center;"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3px;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>Untested</span>';
282
+ if (r.metrics) {
283
+ const speedIcon = r.metrics.avg > 200
284
+ ? '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3.5px; color: var(--warning);"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>'
285
+ : '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" style="vertical-align: middle; margin-right: 3.5px; color: var(--primary);"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>';
286
+ perfHtml = \`<div style="font-weight: 600; color: var(--primary); display: inline-flex; align-items: center;">\${speedIcon}\${r.metrics.rps.toFixed(0)} req/s</div>
287
+ <div style="font-size: 0.75rem; color: var(--text-dim);">Avg: \${r.metrics.avg.toFixed(1)}ms | p99: \${r.metrics.p99.toFixed(1)}ms</div>\`;
288
+ }
289
+
290
+ // Security
291
+ let secHtml = '<span class="badge badge-warning" style="display: inline-flex; align-items: center;"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3px;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>Unscanned</span>';
292
+ if (globalAudits.length > 0 && globalAudits[0].items) {
293
+ const failures = globalAudits[0].items.filter(item => item.status === 'fail');
294
+ if (failures.length > 0) {
295
+ secHtml = '<span class="badge badge-danger" style="display: inline-flex; align-items: center;"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>Vulnerable</span>';
296
+ } else {
297
+ secHtml = '<span class="badge badge-success" style="display: inline-flex; align-items: center;"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><polyline points="9 11 11 13 15 9"></polyline></svg>Secure</span>';
298
+ }
299
+ }
300
+
301
+ // Auth
302
+ const authBadgeClass = r.authStatus === 'Protected' ? 'badge-success' : 'badge-yellow';
303
+ const authIcon = r.authStatus === 'Protected'
304
+ ? '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3.5px;"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>'
305
+ : '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3.5px;"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>';
306
+
307
+ // Copy command helpers
308
+ const host = window.location.origin;
309
+ const benchCmd = \`bandit bench \${host}\${path} -c 50 -r 2000\`;
310
+ const docCmd = \`bandit doctor \${host}\`;
311
+
312
+ html += \`<tr>
313
+ <td>
314
+ <strong style="color: var(--text);">\${r.label}</strong>
315
+ <div style="font-size: 0.75rem; color: var(--text-dim);">\${r.details || '-'}</div>
316
+ </td>
317
+ <td><span class="badge \${authBadgeClass}" style="display: inline-flex; align-items: center;">\${authIcon}\${r.authStatus || 'Public'}</span></td>
318
+ <td>\${perfHtml}</td>
319
+ <td>\${secHtml}</td>
320
+ <td class="actions-cell">
321
+ <button class="btn-action" onclick="copyToClipboard('\${benchCmd}')" title="Copy bench command">
322
+ <svg viewBox="0 0 24 24" width="11" height="11" fill="currentColor"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
323
+ Bench
324
+ </button>
325
+ <button class="btn-action" onclick="copyToClipboard('\${docCmd}')" title="Copy doctor command">
326
+ <svg viewBox="0 0 24 24" width="11" height="11" stroke="currentColor" stroke-width="2.5" fill="none"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
327
+ Doctor
328
+ </button>
329
+ </td>
330
+ </tr>\`;
331
+ });
332
+ tbody.innerHTML = html;
333
+ }
334
+
124
335
  async function loadData() {
125
- // Load Benchmarks
336
+ // Load Audits
126
337
  try {
127
- const res = await fetch('/api/benchmarks');
128
- const data = await res.json();
129
- const container = document.getElementById('benchmarks-table-container');
130
- if (!data || data.length === 0) {
131
- container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No benchmark history recorded yet. Run <code>bandit bench &lt;url&gt;</code> in your terminal to log performance metrics.</p>';
338
+ const res = await fetch('/api/audits');
339
+ globalAudits = await res.json();
340
+ const container = document.getElementById('audits-container');
341
+ if (!globalAudits || globalAudits.length === 0 || !globalAudits[0].items) {
342
+ container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No active audits logged yet. Run <code>bandit doctor</code> in terminal to execute security probes.</p>';
132
343
  } else {
133
- let html = '<table><thead><tr><th>Timestamp</th><th>Commit</th><th>Target URL</th><th>Reqs / Conn</th><th>Throughput (RPS)</th><th>Avg Latency</th><th>p99 Tail Latency</th></tr></thead><tbody>';
134
- data.forEach(b => {
344
+ let html = '<table><thead><tr><th>Status</th><th>Probe Title</th><th>Diagnostic Details</th><th>Remediation Suggestion</th></tr></thead><tbody>';
345
+ globalAudits[0].items.forEach(item => {
346
+ const badgeClass = item.status === 'pass' ? 'badge-success' : item.status === 'warn' ? 'badge-warning' : 'badge-danger';
135
347
  html += \`<tr>
136
- <td>\${new Date(b.timestamp).toLocaleString()}</td>
137
- <td><code>\${b.gitCommit || 'HEAD'}</code></td>
138
- <td><strong>\${b.targetUrl}</strong></td>
139
- <td>\${b.requests} / \${b.connections}</td>
140
- <td style="color: var(--primary); font-weight: 700;">\${b.rps.toFixed(1)} req/s</td>
141
- <td>\${b.latency.avg.toFixed(1)} ms</td>
142
- <td style="color: \${b.latency.p99 > 200 ? 'var(--warning)' : 'var(--success)'}; font-weight: 600;">\${b.latency.p99.toFixed(1)} ms</td>
348
+ <td><span class="badge \${badgeClass}">\${item.status}</span></td>
349
+ <td><strong>\${item.title}</strong></td>
350
+ <td>\${item.details || '-'}</td>
351
+ <td style="color: var(--primary);">\${item.suggestion || 'No action needed'}</td>
143
352
  </tr>\`;
144
353
  });
145
354
  html += '</tbody></table>';
@@ -151,70 +360,80 @@ export async function startStudioServer(projectPath, port = 4000) {
151
360
  try {
152
361
  const res = await fetch('/api/blueprint');
153
362
  const graph = await res.json();
154
- const container = document.getElementById('blueprint-container');
155
- if (!graph.nodes || graph.nodes.length === 0) {
156
- container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No routes discovered in project.</p>';
157
- } else {
158
- const routeNodes = graph.nodes.filter(n => n.type === 'route');
159
- let html = '';
160
-
161
- if (routeNodes.length === 0) {
162
- html = '<p style="color: var(--text-dim);">No API endpoints found in source directory.</p>';
163
- } else {
164
- routeNodes.forEach(r => {
165
- const controllerEdge = graph.edges.find(e => e.from === r.id);
166
- const controllerNode = controllerEdge ? graph.nodes.find(n => n.id === controllerEdge.to) : null;
167
- const serviceEdge = controllerNode ? graph.edges.find(e => e.from === controllerNode.id) : null;
168
- const serviceNode = serviceEdge ? graph.nodes.find(n => n.id === serviceEdge.to) : null;
363
+ globalRoutes = graph.nodes.filter(n => n.type === 'route');
364
+
365
+ // Renders Table
366
+ renderMatrixTable(globalRoutes);
169
367
 
170
- html += \`<div class="flow-row">
171
- <div class="flow-step route">
172
- <span class="badge badge-yellow">HTTP ENDPOINT</span>
173
- <strong>\${r.label}</strong>
174
- <span style="font-size:0.75rem; color:var(--text-dim)">\${r.details || ''}</span>
175
- \${r.metrics ? \`<div class="metric-tag">\${r.metrics.rps.toFixed(0)} RPS | p99: \${r.metrics.p99.toFixed(0)}ms</div>\` : ''}
176
- </div>
177
- <div class="flow-arrow">➔</div>
178
- <div class="flow-step controller">
179
- <span class="badge badge-yellow" style="color:var(--accent-blue); background:rgba(56,189,248,0.1); border-color:rgba(56,189,248,0.3)">CONTROLLER LAYER</span>
180
- <strong>\${controllerNode ? controllerNode.label : 'Route Handler'}</strong>
181
- <span style="font-size:0.75rem; color:var(--text-dim)">Delegates request validation</span>
182
- </div>
183
- <div class="flow-arrow">➔</div>
184
- <div class="flow-step service">
185
- <span class="badge badge-yellow" style="color:var(--accent-purple); background:rgba(192,132,252,0.1); border-color:rgba(192,132,252,0.3)">SERVICE LAYER</span>
186
- <strong>\${serviceNode ? serviceNode.label : 'Business Service'}</strong>
187
- <span style="font-size:0.75rem; color:var(--text-dim)">ORM & Business logic</span>
368
+ // Process Analytics
369
+ const total = globalRoutes.length;
370
+ if (total > 0) {
371
+ // 1. Bench Ring
372
+ const benched = globalRoutes.filter(r => r.metrics).length;
373
+ const benchPct = Math.round((benched / total) * 100);
374
+ document.getElementById('ring-bench-text').innerText = benchPct + '%';
375
+ const benchOffset = 220 - (220 * benchPct) / 100;
376
+ document.getElementById('ring-bench').style.strokeDashoffset = benchOffset;
377
+
378
+ // 2. Security Ring
379
+ const securityPct = globalAudits.length > 0 ? 100 : 0;
380
+ document.getElementById('ring-security-text').innerText = securityPct + '%';
381
+ const secOffset = 220 - (220 * securityPct) / 100;
382
+ document.getElementById('ring-security').style.strokeDashoffset = secOffset;
383
+
384
+ // 3. Exposure Progress Bar
385
+ const privCount = globalRoutes.filter(r => r.authStatus === 'Protected').length;
386
+ const pubCount = total - privCount;
387
+ document.getElementById('exposure-public').innerText = pubCount;
388
+ document.getElementById('exposure-private').innerText = privCount;
389
+ const privatePct = Math.round((privCount / total) * 100);
390
+ document.getElementById('exposure-bar-val').style.width = privatePct + '%';
391
+ document.getElementById('exposure-ratio-desc').innerText = \`\${privatePct}% of your API routes are protected by middleware.\`;
392
+
393
+ // 4. Latency Hotspots (Slowest top 3)
394
+ const benchedRoutes = globalRoutes.filter(r => r.metrics);
395
+ const radarList = document.getElementById('radar-list');
396
+ if (benchedRoutes.length === 0) {
397
+ radarList.innerHTML = '<p style="color: var(--text-dim); font-size: 0.85rem; padding-top: 0.5rem;">No benchmark data available.</p>';
398
+ } else {
399
+ benchedRoutes.sort((a, b) => b.metrics.avg - a.metrics.avg);
400
+ const maxAvg = benchedRoutes[0].metrics.avg || 1;
401
+ let radarHtml = '';
402
+ benchedRoutes.slice(0, 3).forEach(br => {
403
+ const widthPct = Math.round((br.metrics.avg / maxAvg) * 100);
404
+ radarHtml += \`<div class="radar-item">
405
+ <div class="radar-labels">
406
+ <span style="font-weight: 500;">\${br.label}</span>
407
+ <span style="color: var(--danger); font-weight: 600;">\${br.metrics.avg.toFixed(0)} ms</span>
188
408
  </div>
189
- <div class="flow-arrow">➔</div>
190
- <div class="flow-step infra">
191
- <span class="badge badge-success">PERSISTENCE</span>
192
- <strong>PostgreSQL / Redis</strong>
193
- <span style="font-size:0.75rem; color:var(--text-dim)">Connection Pool</span>
409
+ <div class="radar-bar-bg">
410
+ <div class="radar-bar-val" style="width: \${widthPct}%;"></div>
194
411
  </div>
195
412
  </div>\`;
196
413
  });
414
+ radarList.innerHTML = radarHtml;
197
415
  }
198
- container.innerHTML = html;
199
416
  }
200
417
  } catch (err) { console.error(err); }
201
418
 
202
- // Load Audits
419
+ // Load Benchmarks
203
420
  try {
204
- const res = await fetch('/api/audits');
205
- const audits = await res.json();
206
- const container = document.getElementById('audits-container');
207
- if (!audits || audits.length === 0 || !audits[0].items) {
208
- container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No active audits logged yet. Run <code>bandit doctor</code> in terminal to execute security probes.</p>';
421
+ const res = await fetch('/api/benchmarks');
422
+ const data = await res.json();
423
+ const container = document.getElementById('benchmarks-table-container');
424
+ if (!data || data.length === 0) {
425
+ container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No benchmark history recorded yet. Run <code>bandit bench &lt;url&gt;</code> in your terminal.</p>';
209
426
  } else {
210
- let html = '<table><thead><tr><th>Status</th><th>Probe Title</th><th>Diagnostic Details</th><th>Remediation Suggestion</th></tr></thead><tbody>';
211
- audits[0].items.forEach(item => {
212
- const badgeClass = item.status === 'pass' ? 'badge-success' : item.status === 'warn' ? 'badge-warning' : 'badge-danger';
427
+ let html = '<table><thead><tr><th>Timestamp</th><th>Commit</th><th>Target URL</th><th>Reqs / Conn</th><th>Throughput (RPS)</th><th>Avg Latency</th><th>p99 Tail Latency</th></tr></thead><tbody>';
428
+ data.forEach(b => {
213
429
  html += \`<tr>
214
- <td><span class="badge \${badgeClass}">\${item.status}</span></td>
215
- <td><strong>\${item.title}</strong></td>
216
- <td>\${item.details || '-'}</td>
217
- <td style="color: var(--primary);">\${item.suggestion || 'No action needed'}</td>
430
+ <td>\${new Date(b.timestamp).toLocaleString()}</td>
431
+ <td><code>\${b.gitCommit || 'HEAD'}</code></td>
432
+ <td><strong>\${b.targetUrl}</strong></td>
433
+ <td>\${b.requests} / \${b.connections}</td>
434
+ <td style="color: var(--primary); font-weight: 700;">\${b.rps.toFixed(1)} req/s</td>
435
+ <td>\${b.latency.avg.toFixed(1)} ms</td>
436
+ <td style="color: \${b.latency.p99 > 200 ? 'var(--warning)' : 'var(--success)'}; font-weight: 600;">\${b.latency.p99.toFixed(1)} ms</td>
218
437
  </tr>\`;
219
438
  });
220
439
  html += '</tbody></table>';
@@ -230,6 +449,19 @@ export async function startStudioServer(projectPath, port = 4000) {
230
449
  };
231
450
  const server = http.createServer(async (req, res) => {
232
451
  const url = req.url || "/";
452
+ // Serve custom logo if saved in project root
453
+ if (url === "/api/logo" && req.method === "GET") {
454
+ const logoPath = path.join(projectPath, "logo.png");
455
+ if (fs.existsSync(logoPath)) {
456
+ res.writeHead(200, { "Content-Type": "image/png" });
457
+ res.end(fs.readFileSync(logoPath));
458
+ }
459
+ else {
460
+ res.writeHead(404);
461
+ res.end();
462
+ }
463
+ return;
464
+ }
233
465
  if (url === "/api/benchmarks" && req.method === "GET") {
234
466
  res.writeHead(200, { "Content-Type": "application/json" });
235
467
  res.end(JSON.stringify(db.getBenchmarks()));
@@ -3,6 +3,22 @@ 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
+ function detectFramework(pkg) {
7
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
8
+ const frameworks = {
9
+ express: "Express",
10
+ "@nestjs/core": "NestJS",
11
+ fastify: "Fastify",
12
+ koa: "Koa",
13
+ hapi: "Hapi",
14
+ };
15
+ for (const [dep, label] of Object.entries(frameworks)) {
16
+ if (deps[dep]) {
17
+ return label;
18
+ }
19
+ }
20
+ return "gRPC / Library";
21
+ }
6
22
  export async function resolveProjectPath(inputPath = ".") {
7
23
  const absoluteRoot = path.resolve(inputPath);
8
24
  // Find all package.json files, ignoring dependency and output directories
@@ -24,6 +40,7 @@ export async function resolveProjectPath(inputPath = ".") {
24
40
  services.push({
25
41
  name: pkg.name || path.basename(dir),
26
42
  path: dir,
43
+ framework: detectFramework(pkg),
27
44
  });
28
45
  }
29
46
  catch { }
@@ -33,9 +50,12 @@ export async function resolveProjectPath(inputPath = ".") {
33
50
  clack.log.info(`${chalk.bold.yellow("Monorepo detected!")} Found ${chalk.bold.cyan(services.length)} nested packages.`);
34
51
  const options = services.map((s) => {
35
52
  const relPath = path.relative(absoluteRoot, s.path) || ".";
53
+ const fwLabel = s.framework === "gRPC / Library"
54
+ ? chalk.dim(s.framework)
55
+ : chalk.cyan(s.framework);
36
56
  return {
37
57
  value: s.path,
38
- label: `${chalk.bold.green(s.name)} ${chalk.gray(`(${relPath})`)}`,
58
+ label: `${chalk.bold.green(s.name)} ${chalk.gray(`(${relPath})`)} - ${fwLabel}`,
39
59
  };
40
60
  });
41
61
  const selectedPath = await clack.select({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bandit-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {