@arvoretech/runtime-lens-mcp 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.
Files changed (42) hide show
  1. package/.vscodeignore +21 -0
  2. package/README.md +136 -0
  3. package/agent/index.ts +263 -0
  4. package/agent/tsconfig.json +17 -0
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +17 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/log-collector.d.ts +73 -0
  10. package/dist/log-collector.d.ts.map +1 -0
  11. package/dist/log-collector.js +349 -0
  12. package/dist/log-collector.js.map +1 -0
  13. package/dist/process-inspector.d.ts +44 -0
  14. package/dist/process-inspector.d.ts.map +1 -0
  15. package/dist/process-inspector.js +190 -0
  16. package/dist/process-inspector.js.map +1 -0
  17. package/dist/runtime-interceptor.d.ts +18 -0
  18. package/dist/runtime-interceptor.d.ts.map +1 -0
  19. package/dist/runtime-interceptor.js +133 -0
  20. package/dist/runtime-interceptor.js.map +1 -0
  21. package/dist/server.d.ts +26 -0
  22. package/dist/server.d.ts.map +1 -0
  23. package/dist/server.js +301 -0
  24. package/dist/server.js.map +1 -0
  25. package/dist/types.d.ts +280 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/dist/types.js +102 -0
  28. package/dist/types.js.map +1 -0
  29. package/eslint.config.js +41 -0
  30. package/extension/decorator.ts +144 -0
  31. package/extension/extension.ts +98 -0
  32. package/extension/runtime-bridge.ts +206 -0
  33. package/extension/tsconfig.json +17 -0
  34. package/package.json +134 -0
  35. package/src/index.ts +18 -0
  36. package/src/log-collector.ts +441 -0
  37. package/src/process-inspector.ts +235 -0
  38. package/src/runtime-interceptor.ts +152 -0
  39. package/src/server.ts +387 -0
  40. package/src/types.ts +128 -0
  41. package/tsconfig.json +20 -0
  42. package/vitest.config.ts +13 -0
@@ -0,0 +1,441 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile, readdir, stat } from "node:fs/promises";
3
+ import { join, basename } from "node:path";
4
+ import {
5
+ LogEntry,
6
+ LogLevel,
7
+ Framework,
8
+ HttpRequest,
9
+ PerformanceMetric,
10
+ } from "./types.js";
11
+
12
+ const MAX_BUFFER_SIZE = 10000;
13
+
14
+ interface LogBuffer {
15
+ logs: LogEntry[];
16
+ requests: HttpRequest[];
17
+ metrics: PerformanceMetric[];
18
+ }
19
+
20
+ export class LogCollector {
21
+ private buffer: LogBuffer = {
22
+ logs: [],
23
+ requests: [],
24
+ metrics: [],
25
+ };
26
+
27
+ private logPaths: string[];
28
+ private projectRoot: string;
29
+
30
+ constructor(projectRoot?: string, logPaths?: string[]) {
31
+ this.projectRoot = projectRoot || process.cwd();
32
+ this.logPaths = logPaths || [];
33
+ }
34
+
35
+ async collectFromFiles(): Promise<void> {
36
+ const paths = this.logPaths.length > 0
37
+ ? this.logPaths
38
+ : await this.discoverLogFiles();
39
+
40
+ for (const filePath of paths) {
41
+ try {
42
+ const content = await readFile(filePath, "utf-8");
43
+ const entries = this.parseLogFile(content, filePath);
44
+ this.addLogs(entries);
45
+ } catch {
46
+ // skip unreadable files
47
+ }
48
+ }
49
+ }
50
+
51
+ async collectFromProcess(): Promise<void> {
52
+ const mem = process.memoryUsage();
53
+ const cpu = process.cpuUsage();
54
+
55
+ this.addMetric({
56
+ name: "memory.rss",
57
+ value: mem.rss,
58
+ unit: "bytes",
59
+ timestamp: new Date().toISOString(),
60
+ tags: { source: "process" },
61
+ });
62
+
63
+ this.addMetric({
64
+ name: "memory.heap_used",
65
+ value: mem.heapUsed,
66
+ unit: "bytes",
67
+ timestamp: new Date().toISOString(),
68
+ tags: { source: "process" },
69
+ });
70
+
71
+ this.addMetric({
72
+ name: "cpu.user",
73
+ value: cpu.user,
74
+ unit: "microseconds",
75
+ timestamp: new Date().toISOString(),
76
+ tags: { source: "process" },
77
+ });
78
+
79
+ this.addMetric({
80
+ name: "cpu.system",
81
+ value: cpu.system,
82
+ unit: "microseconds",
83
+ timestamp: new Date().toISOString(),
84
+ tags: { source: "process" },
85
+ });
86
+ }
87
+
88
+ async scanProjectStructure(): Promise<{
89
+ framework: Framework;
90
+ logFiles: string[];
91
+ configFiles: string[];
92
+ }> {
93
+ const framework = await this.detectFramework();
94
+ const logFiles = await this.discoverLogFiles();
95
+ const configFiles = await this.discoverConfigFiles();
96
+
97
+ return { framework, logFiles, configFiles };
98
+ }
99
+
100
+ addLog(entry: Omit<LogEntry, "id" | "timestamp">): void {
101
+ this.buffer.logs.push({
102
+ ...entry,
103
+ id: randomUUID(),
104
+ timestamp: new Date().toISOString(),
105
+ });
106
+ this.trimBuffer("logs");
107
+ }
108
+
109
+ addLogs(entries: LogEntry[]): void {
110
+ this.buffer.logs.push(...entries);
111
+ this.trimBuffer("logs");
112
+ }
113
+
114
+ addRequest(request: Omit<HttpRequest, "id" | "timestamp">): void {
115
+ this.buffer.requests.push({
116
+ ...request,
117
+ id: randomUUID(),
118
+ timestamp: new Date().toISOString(),
119
+ });
120
+ this.trimBuffer("requests");
121
+ }
122
+
123
+ addMetric(metric: PerformanceMetric): void {
124
+ this.buffer.metrics.push(metric);
125
+ this.trimBuffer("metrics");
126
+ }
127
+
128
+ getLogs(options?: {
129
+ lines?: number;
130
+ level?: LogLevel;
131
+ framework?: Framework;
132
+ source?: string;
133
+ }): LogEntry[] {
134
+ let logs = [...this.buffer.logs];
135
+
136
+ if (options?.level) {
137
+ logs = logs.filter((l) => l.level === options.level);
138
+ }
139
+ if (options?.framework) {
140
+ logs = logs.filter((l) => l.framework === options.framework);
141
+ }
142
+ if (options?.source) {
143
+ logs = logs.filter((l) => l.source?.includes(options.source!));
144
+ }
145
+
146
+ const limit = options?.lines || 50;
147
+ return logs.slice(-limit);
148
+ }
149
+
150
+ searchLogs(options: {
151
+ query: string;
152
+ level?: LogLevel;
153
+ framework?: Framework;
154
+ limit?: number;
155
+ since?: string;
156
+ }): LogEntry[] {
157
+ let logs = [...this.buffer.logs];
158
+ const regex = new RegExp(options.query, "i");
159
+
160
+ if (options.since) {
161
+ const sinceDate = new Date(options.since);
162
+ logs = logs.filter((l) => new Date(l.timestamp) >= sinceDate);
163
+ }
164
+ if (options.level) {
165
+ logs = logs.filter((l) => l.level === options.level);
166
+ }
167
+ if (options.framework) {
168
+ logs = logs.filter((l) => l.framework === options.framework);
169
+ }
170
+
171
+ logs = logs.filter(
172
+ (l) =>
173
+ regex.test(l.message) ||
174
+ (l.stackTrace && regex.test(l.stackTrace)) ||
175
+ (l.source && regex.test(l.source))
176
+ );
177
+
178
+ return logs.slice(-(options.limit || 50));
179
+ }
180
+
181
+ getErrors(options?: {
182
+ limit?: number;
183
+ framework?: Framework;
184
+ grouped?: boolean;
185
+ }): LogEntry[] | { message: string; count: number; lastSeen: string; sample: LogEntry }[] {
186
+ let errors = this.buffer.logs.filter(
187
+ (l) => l.level === "error" || l.level === "fatal"
188
+ );
189
+
190
+ if (options?.framework) {
191
+ errors = errors.filter((l) => l.framework === options.framework);
192
+ }
193
+
194
+ if (options?.grouped) {
195
+ const groups = new Map<string, { count: number; lastSeen: string; sample: LogEntry }>();
196
+ for (const error of errors) {
197
+ const key = error.message.slice(0, 100);
198
+ const existing = groups.get(key);
199
+ if (existing) {
200
+ existing.count++;
201
+ if (error.timestamp > existing.lastSeen) {
202
+ existing.lastSeen = error.timestamp;
203
+ existing.sample = error;
204
+ }
205
+ } else {
206
+ groups.set(key, { count: 1, lastSeen: error.timestamp, sample: error });
207
+ }
208
+ }
209
+ return Array.from(groups.entries())
210
+ .map(([message, data]) => ({ message, ...data }))
211
+ .sort((a, b) => b.count - a.count)
212
+ .slice(0, options?.limit || 20);
213
+ }
214
+
215
+ return errors.slice(-(options?.limit || 20));
216
+ }
217
+
218
+ getRequests(options?: {
219
+ id?: string;
220
+ method?: string;
221
+ urlPattern?: string;
222
+ statusCode?: number;
223
+ limit?: number;
224
+ }): HttpRequest[] {
225
+ let requests = [...this.buffer.requests];
226
+
227
+ if (options?.id) {
228
+ return requests.filter((r) => r.id === options.id);
229
+ }
230
+ if (options?.method) {
231
+ requests = requests.filter((r) => r.method.toUpperCase() === options.method!.toUpperCase());
232
+ }
233
+ if (options?.urlPattern) {
234
+ const regex = new RegExp(options.urlPattern, "i");
235
+ requests = requests.filter((r) => regex.test(r.url));
236
+ }
237
+ if (options?.statusCode) {
238
+ requests = requests.filter((r) => r.statusCode === options.statusCode);
239
+ }
240
+
241
+ return requests.slice(-(options?.limit || 20));
242
+ }
243
+
244
+ getMetrics(options?: {
245
+ metric?: string;
246
+ since?: string;
247
+ limit?: number;
248
+ }): PerformanceMetric[] {
249
+ let metrics = [...this.buffer.metrics];
250
+
251
+ if (options?.metric) {
252
+ metrics = metrics.filter((m) => m.name.includes(options.metric!));
253
+ }
254
+ if (options?.since) {
255
+ const sinceDate = new Date(options.since);
256
+ metrics = metrics.filter((m) => new Date(m.timestamp) >= sinceDate);
257
+ }
258
+
259
+ return metrics.slice(-(options?.limit || 20));
260
+ }
261
+
262
+ clearLogs(): { cleared: number } {
263
+ const count = this.buffer.logs.length + this.buffer.requests.length + this.buffer.metrics.length;
264
+ this.buffer = { logs: [], requests: [], metrics: [] };
265
+ return { cleared: count };
266
+ }
267
+
268
+ getStats(): {
269
+ totalLogs: number;
270
+ totalRequests: number;
271
+ totalMetrics: number;
272
+ byLevel: Record<string, number>;
273
+ byFramework: Record<string, number>;
274
+ } {
275
+ const byLevel: Record<string, number> = {};
276
+ const byFramework: Record<string, number> = {};
277
+
278
+ for (const log of this.buffer.logs) {
279
+ byLevel[log.level] = (byLevel[log.level] || 0) + 1;
280
+ const fw = log.framework || "unknown";
281
+ byFramework[fw] = (byFramework[fw] || 0) + 1;
282
+ }
283
+
284
+ return {
285
+ totalLogs: this.buffer.logs.length,
286
+ totalRequests: this.buffer.requests.length,
287
+ totalMetrics: this.buffer.metrics.length,
288
+ byLevel,
289
+ byFramework,
290
+ };
291
+ }
292
+
293
+ private parseLogFile(content: string, filePath: string): LogEntry[] {
294
+ const lines = content.split("\n").filter(Boolean);
295
+ const entries: LogEntry[] = [];
296
+ const framework = this.inferFramework(filePath);
297
+
298
+ for (const line of lines) {
299
+ const entry = this.parseLogLine(line, filePath, framework);
300
+ if (entry) entries.push(entry);
301
+ }
302
+
303
+ return entries;
304
+ }
305
+
306
+ private parseLogLine(line: string, source: string, framework: Framework): LogEntry | null {
307
+ const jsonMatch = this.tryParseJson(line);
308
+ if (jsonMatch) {
309
+ return {
310
+ id: randomUUID(),
311
+ timestamp: jsonMatch.timestamp || jsonMatch.time || new Date().toISOString(),
312
+ level: this.normalizeLevel(jsonMatch.level || jsonMatch.severity || "info"),
313
+ message: jsonMatch.message || jsonMatch.msg || JSON.stringify(jsonMatch),
314
+ source: basename(source),
315
+ framework,
316
+ metadata: jsonMatch,
317
+ };
318
+ }
319
+
320
+ const timestampRegex = /^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[.\d]*Z?)\s*/;
321
+ const levelRegex = /\b(DEBUG|INFO|WARN|ERROR|FATAL|debug|info|warn|error|fatal)\b/;
322
+
323
+ const tsMatch = line.match(timestampRegex);
324
+ const lvlMatch = line.match(levelRegex);
325
+
326
+ return {
327
+ id: randomUUID(),
328
+ timestamp: tsMatch?.[1] || new Date().toISOString(),
329
+ level: this.normalizeLevel(lvlMatch?.[1] || "info"),
330
+ message: line,
331
+ source: basename(source),
332
+ framework,
333
+ };
334
+ }
335
+
336
+ private tryParseJson(line: string): Record<string, any> | null {
337
+ try {
338
+ const parsed = JSON.parse(line.trim());
339
+ if (typeof parsed === "object" && parsed !== null) return parsed;
340
+ } catch {
341
+ // not JSON
342
+ }
343
+ return null;
344
+ }
345
+
346
+ private normalizeLevel(level: string): LogLevel {
347
+ const normalized = level.toLowerCase();
348
+ if (["debug", "info", "warn", "error", "fatal"].includes(normalized)) {
349
+ return normalized as LogLevel;
350
+ }
351
+ if (normalized === "warning") return "warn";
352
+ if (normalized === "critical" || normalized === "emergency") return "fatal";
353
+ if (normalized === "trace" || normalized === "verbose") return "debug";
354
+ return "info";
355
+ }
356
+
357
+ private inferFramework(filePath: string): Framework {
358
+ if (filePath.includes(".next") || filePath.includes("next")) return "nextjs";
359
+ if (filePath.includes("nest") || filePath.includes("dist/main")) return "nestjs";
360
+ if (filePath.includes("react") || filePath.includes("src/App")) return "react";
361
+ return "unknown";
362
+ }
363
+
364
+ private async detectFramework(): Promise<Framework> {
365
+ try {
366
+ const pkgPath = join(this.projectRoot, "package.json");
367
+ const content = await readFile(pkgPath, "utf-8");
368
+ const pkg = JSON.parse(content);
369
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
370
+
371
+ if (deps["@nestjs/core"]) return "nestjs";
372
+ if (deps["next"]) return "nextjs";
373
+ if (deps["react"]) return "react";
374
+ } catch {
375
+ // no package.json
376
+ }
377
+ return "unknown";
378
+ }
379
+
380
+ private async discoverLogFiles(): Promise<string[]> {
381
+ const candidates = [
382
+ "logs",
383
+ ".next/server",
384
+ "dist",
385
+ "tmp",
386
+ ];
387
+
388
+ const logFiles: string[] = [];
389
+
390
+ for (const dir of candidates) {
391
+ const fullPath = join(this.projectRoot, dir);
392
+ try {
393
+ const dirStat = await stat(fullPath);
394
+ if (!dirStat.isDirectory()) continue;
395
+
396
+ const files = await readdir(fullPath);
397
+ for (const file of files) {
398
+ if (file.endsWith(".log") || file.endsWith(".json")) {
399
+ logFiles.push(join(fullPath, file));
400
+ }
401
+ }
402
+ } catch {
403
+ // directory doesn't exist
404
+ }
405
+ }
406
+
407
+ return logFiles;
408
+ }
409
+
410
+ private async discoverConfigFiles(): Promise<string[]> {
411
+ const candidates = [
412
+ "package.json",
413
+ "tsconfig.json",
414
+ "next.config.js",
415
+ "next.config.mjs",
416
+ "nest-cli.json",
417
+ ".env",
418
+ ".env.local",
419
+ "vite.config.ts",
420
+ "webpack.config.js",
421
+ ];
422
+
423
+ const found: string[] = [];
424
+ for (const file of candidates) {
425
+ try {
426
+ await stat(join(this.projectRoot, file));
427
+ found.push(file);
428
+ } catch {
429
+ // doesn't exist
430
+ }
431
+ }
432
+ return found;
433
+ }
434
+
435
+ private trimBuffer(key: keyof LogBuffer): void {
436
+ const arr = this.buffer[key] as unknown[];
437
+ if (arr.length > MAX_BUFFER_SIZE) {
438
+ (this.buffer[key] as unknown[]) = arr.slice(-MAX_BUFFER_SIZE);
439
+ }
440
+ }
441
+ }
@@ -0,0 +1,235 @@
1
+ import { exec } from "node:child_process";
2
+ import { readFile, readdir, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { ProcessInfo, Framework, RuntimeLensError } from "./types.js";
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ interface RunningProcess {
10
+ pid: number;
11
+ command: string;
12
+ port?: number;
13
+ framework?: Framework;
14
+ }
15
+
16
+ export class ProcessInspector {
17
+ private readonly projectRoot: string;
18
+
19
+ constructor(projectRoot?: string) {
20
+ this.projectRoot = projectRoot || process.cwd();
21
+ }
22
+
23
+ getProcessInfo(): ProcessInfo {
24
+ const mem = process.memoryUsage();
25
+ const cpu = process.cpuUsage();
26
+
27
+ return {
28
+ pid: process.pid,
29
+ uptime: process.uptime(),
30
+ memoryUsage: {
31
+ rss: mem.rss,
32
+ heapTotal: mem.heapTotal,
33
+ heapUsed: mem.heapUsed,
34
+ external: mem.external,
35
+ },
36
+ cpuUsage: {
37
+ user: cpu.user,
38
+ system: cpu.system,
39
+ },
40
+ nodeVersion: process.version,
41
+ platform: process.platform,
42
+ arch: process.arch,
43
+ cwd: process.cwd(),
44
+ };
45
+ }
46
+
47
+ async findRunningProcesses(): Promise<RunningProcess[]> {
48
+ const processes: RunningProcess[] = [];
49
+
50
+ try {
51
+ const { stdout } = await execAsync(
52
+ "ps aux | grep -E '(node|next|nest|react-scripts)' | grep -v grep"
53
+ );
54
+
55
+ for (const line of stdout.split("\n").filter(Boolean)) {
56
+ const parts = line.trim().split(/\s+/);
57
+ const pid = parseInt(parts[1], 10);
58
+ const command = parts.slice(10).join(" ");
59
+
60
+ if (isNaN(pid)) continue;
61
+
62
+ const framework = this.detectFrameworkFromCommand(command);
63
+ const port = await this.findPortForPid(pid);
64
+
65
+ processes.push({ pid, command, port: port || undefined, framework });
66
+ }
67
+ } catch {
68
+ // ps command failed
69
+ }
70
+
71
+ return processes;
72
+ }
73
+
74
+ async getPortListeners(): Promise<{ port: number; pid: number; process: string }[]> {
75
+ const listeners: { port: number; pid: number; process: string }[] = [];
76
+
77
+ try {
78
+ const { stdout } = await execAsync(
79
+ "lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep node || true"
80
+ );
81
+
82
+ for (const line of stdout.split("\n").filter(Boolean)) {
83
+ const parts = line.trim().split(/\s+/);
84
+ const processName = parts[0];
85
+ const pid = parseInt(parts[1], 10);
86
+ const portRegex = /:(\d+)$/;
87
+ const portMatch = parts[8] ? portRegex.exec(parts[8]) : null;
88
+
89
+ if (!isNaN(pid) && portMatch) {
90
+ listeners.push({
91
+ port: parseInt(portMatch[1], 10),
92
+ pid,
93
+ process: processName,
94
+ });
95
+ }
96
+ }
97
+ } catch {
98
+ // lsof failed
99
+ }
100
+
101
+ return listeners;
102
+ }
103
+
104
+ async getEnvironmentInfo(): Promise<{
105
+ process: ProcessInfo;
106
+ runningProcesses: RunningProcess[];
107
+ ports: { port: number; pid: number; process: string }[];
108
+ projectStructure: {
109
+ framework: Framework;
110
+ logFiles: string[];
111
+ configFiles: string[];
112
+ };
113
+ }> {
114
+ const [runningProcesses, ports, projectStructure] = await Promise.all([
115
+ this.findRunningProcesses(),
116
+ this.getPortListeners(),
117
+ this.scanProject(),
118
+ ]);
119
+
120
+ return {
121
+ process: this.getProcessInfo(),
122
+ runningProcesses,
123
+ ports,
124
+ projectStructure,
125
+ };
126
+ }
127
+
128
+ async executeExpression(expression: string): Promise<{ result: string; type: string }> {
129
+ try {
130
+ const fn = new Function("require", `return (${expression})`);
131
+ const result = fn(require);
132
+ const type = typeof result;
133
+ return {
134
+ result: typeof result === "object" ? JSON.stringify(result, null, 2) : String(result),
135
+ type,
136
+ };
137
+ } catch (error) {
138
+ throw new RuntimeLensError(
139
+ `Expression evaluation failed: ${error instanceof Error ? error.message : String(error)}`,
140
+ "EVAL_FAILED"
141
+ );
142
+ }
143
+ }
144
+
145
+ private async scanProject(): Promise<{
146
+ framework: Framework;
147
+ logFiles: string[];
148
+ configFiles: string[];
149
+ }> {
150
+ const framework = await this.detectProjectFramework();
151
+ const configFiles = await this.findConfigFiles();
152
+ const logFiles = await this.findLogFiles();
153
+
154
+ return { framework, logFiles, configFiles };
155
+ }
156
+
157
+ private async detectProjectFramework(): Promise<Framework> {
158
+ try {
159
+ const content = await readFile(join(this.projectRoot, "package.json"), "utf-8");
160
+ const pkg = JSON.parse(content);
161
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
162
+
163
+ if (deps["@nestjs/core"]) return "nestjs";
164
+ if (deps["next"]) return "nextjs";
165
+ if (deps["react"]) return "react";
166
+ } catch {
167
+ // no package.json
168
+ }
169
+ return "unknown";
170
+ }
171
+
172
+ private async findConfigFiles(): Promise<string[]> {
173
+ const candidates = [
174
+ "package.json", "tsconfig.json", "next.config.js", "next.config.mjs",
175
+ "next.config.ts", "nest-cli.json", ".env", ".env.local",
176
+ "vite.config.ts", "webpack.config.js", "tailwind.config.js",
177
+ "tailwind.config.ts", "postcss.config.js",
178
+ ];
179
+
180
+ const found: string[] = [];
181
+ for (const file of candidates) {
182
+ try {
183
+ await stat(join(this.projectRoot, file));
184
+ found.push(file);
185
+ } catch {
186
+ // doesn't exist
187
+ }
188
+ }
189
+ return found;
190
+ }
191
+
192
+ private async findLogFiles(): Promise<string[]> {
193
+ const dirs = ["logs", ".next/server", "dist", "tmp"];
194
+ const logFiles: string[] = [];
195
+
196
+ for (const dir of dirs) {
197
+ try {
198
+ const fullPath = join(this.projectRoot, dir);
199
+ const dirStat = await stat(fullPath);
200
+ if (!dirStat.isDirectory()) continue;
201
+
202
+ const files = await readdir(fullPath);
203
+ for (const file of files) {
204
+ if (file.endsWith(".log")) {
205
+ logFiles.push(join(dir, file));
206
+ }
207
+ }
208
+ } catch {
209
+ // skip
210
+ }
211
+ }
212
+
213
+ return logFiles;
214
+ }
215
+
216
+ private detectFrameworkFromCommand(command: string): Framework {
217
+ if (command.includes("next")) return "nextjs";
218
+ if (command.includes("nest")) return "nestjs";
219
+ if (command.includes("react-scripts") || command.includes("vite")) return "react";
220
+ return "unknown";
221
+ }
222
+
223
+ private async findPortForPid(pid: number): Promise<number | null> {
224
+ try {
225
+ const { stdout } = await execAsync(
226
+ `lsof -iTCP -sTCP:LISTEN -P -n -p ${pid} 2>/dev/null | tail -1`
227
+ );
228
+ const portRegex = /:(\d+)\s/;
229
+ const portMatch = portRegex.exec(stdout);
230
+ return portMatch ? parseInt(portMatch[1], 10) : null;
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ }