@archznn/xavva 1.6.5

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.
@@ -0,0 +1,148 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import type { TomcatConfig } from "../types/config";
4
+ import { Logger } from "../utils/ui";
5
+
6
+ export interface Vulnerability {
7
+ id: string;
8
+ summary: string;
9
+ details: string;
10
+ severity: string;
11
+ fixedIn?: string;
12
+ }
13
+
14
+ export interface JarAuditResult {
15
+ jarName: string;
16
+ groupId?: string;
17
+ artifactId?: string;
18
+ version?: string;
19
+ vulnerabilities: Vulnerability[];
20
+ }
21
+
22
+ export class AuditService {
23
+ constructor(private tomcatConfig: TomcatConfig) {}
24
+
25
+ async runAudit(appName: string): Promise<JarAuditResult[]> {
26
+ const libPath = path.join(this.tomcatConfig.path, "webapps", appName, "WEB-INF", "lib");
27
+
28
+ if (!fs.existsSync(libPath)) {
29
+ throw new Error(`Pasta lib não encontrada em: ${libPath}. Faça o deploy da aplicação primeiro.`);
30
+ }
31
+
32
+ const jars = fs.readdirSync(libPath).filter(f => f.endsWith(".jar"));
33
+ const results: JarAuditResult[] = [];
34
+
35
+ const stopSpinner = Logger.spinner(`Auditando ${jars.length} dependências`);
36
+
37
+ // Process in chunks to avoid overwhelming the API
38
+ const chunkSize = 10;
39
+ for (let i = 0; i < jars.length; i += chunkSize) {
40
+ const chunk = jars.slice(i, i + chunkSize);
41
+ const chunkPromises = chunk.map(jar => this.auditJar(path.join(libPath, jar)));
42
+ const chunkResults = await Promise.all(chunkPromises);
43
+ results.push(...chunkResults);
44
+ }
45
+
46
+ stopSpinner();
47
+ return results;
48
+ }
49
+
50
+ private async auditJar(jarPath: string): Promise<JarAuditResult> {
51
+ const jarName = path.basename(jarPath);
52
+ const info = await this.extractJarInfo(jarPath);
53
+
54
+ if (!info.artifactId || !info.version) {
55
+ // Fallback to filename parsing if pom.properties is missing
56
+ const match = jarName.match(/(.+)-([\d\.]+.*)\.jar/);
57
+ if (match) {
58
+ info.artifactId = info.artifactId || match[1];
59
+ info.version = info.version || match[2];
60
+ }
61
+ }
62
+
63
+ const vulnerabilities = await this.checkVulnerabilities(info.groupId, info.artifactId, info.version);
64
+
65
+ return {
66
+ jarName,
67
+ ...info,
68
+ vulnerabilities
69
+ };
70
+ }
71
+
72
+ private async extractJarInfo(jarPath: string): Promise<{ groupId?: string, artifactId?: string, version?: string }> {
73
+ // We use PowerShell to quickly peek inside the JAR for pom.properties
74
+ // This is faster than extracting the whole JAR
75
+ const normalizedPath = jarPath.split(path.sep).join("/");
76
+ const psCommand = `
77
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
78
+ $zip = [System.IO.Compression.ZipFile]::OpenRead("${normalizedPath}")
79
+ $entry = $zip.Entries | Where-Object { $_.FullName -match "pom.properties$" } | Select-Object -First 1
80
+ if ($entry) {
81
+ $stream = $entry.Open()
82
+ $reader = New-Object System.IO.StreamReader($stream)
83
+ $content = $reader.ReadToEnd()
84
+ $reader.Close()
85
+ $stream.Close()
86
+ $content
87
+ }
88
+ $zip.Dispose()
89
+ `;
90
+
91
+ try {
92
+ const proc = Bun.spawn(["powershell", "-command", psCommand]);
93
+ const output = await new Response(proc.stdout).text();
94
+
95
+ const groupId = output.match(/groupId=(.*)/)?.[1]?.trim();
96
+ const artifactId = output.match(/artifactId=(.*)/)?.[1]?.trim();
97
+ const version = output.match(/version=(.*)/)?.[1]?.trim();
98
+
99
+ return { groupId, artifactId, version };
100
+ } catch (e) {
101
+ return {};
102
+ }
103
+ }
104
+
105
+ private async checkVulnerabilities(groupId?: string, artifactId?: string, version?: string): Promise<Vulnerability[]> {
106
+ if (!artifactId || !version) return [];
107
+
108
+ const name = groupId ? `${groupId}:${artifactId}` : artifactId;
109
+
110
+ try {
111
+ const response = await fetch("https://api.osv.dev/v1/query", {
112
+ method: "POST",
113
+ body: JSON.stringify({
114
+ version: version,
115
+ package: {
116
+ name: name,
117
+ ecosystem: "Maven"
118
+ }
119
+ })
120
+ });
121
+
122
+ const data = await response.json();
123
+ if (!data.vulns) return [];
124
+
125
+ return data.vulns.map((v: any) => ({
126
+ id: v.id,
127
+ summary: v.summary || v.details?.substring(0, 100) + "...",
128
+ details: v.details,
129
+ severity: this.extractSeverity(v),
130
+ fixedIn: v.affected?.[0]?.ranges?.[0]?.events?.find((e: any) => e.fixed)?.fixed
131
+ }));
132
+ } catch (e) {
133
+ return [];
134
+ }
135
+ }
136
+
137
+ private extractSeverity(vuln: any): string {
138
+ if (vuln.database_specific?.severity) return vuln.database_specific.severity;
139
+ if (vuln.advisories?.[0]?.url?.includes("github.com/advisories")) {
140
+ // Try to infer from details if common keywords exist
141
+ const d = (vuln.details || "").toLowerCase();
142
+ if (d.includes("critical")) return "CRITICAL";
143
+ if (d.includes("high")) return "HIGH";
144
+ if (d.includes("moderate") || d.includes("medium")) return "MEDIUM";
145
+ }
146
+ return "UNKNOWN";
147
+ }
148
+ }
@@ -0,0 +1,198 @@
1
+ import { readdirSync, copyFileSync, existsSync, statSync, mkdirSync } from "fs";
2
+ import path from "path";
3
+ import type { ProjectConfig, TomcatConfig } from "../types/config";
4
+ import { Logger } from "../utils/ui";
5
+
6
+ export class BuildService {
7
+ private inferredAppName: string | null = null;
8
+
9
+ constructor(private projectConfig: ProjectConfig, private tomcatConfig: TomcatConfig) { }
10
+
11
+ async runBuild(incremental = false) {
12
+ const command = [];
13
+
14
+ if (this.projectConfig.buildTool === 'maven') {
15
+ command.push("mvn");
16
+ if (incremental) {
17
+ command.push("compile");
18
+ } else {
19
+ command.push("clean", "package");
20
+ }
21
+ command.push("-DskipTests");
22
+ if (this.projectConfig.profile) command.push(`-P${this.projectConfig.profile}`);
23
+ } else {
24
+ command.push("gradle");
25
+ if (incremental) {
26
+ command.push("classes");
27
+ } else {
28
+ command.push("clean", "build");
29
+ }
30
+ command.push("-x", "test");
31
+ if (this.projectConfig.profile) command.push(`-Pprofile=${this.projectConfig.profile}`);
32
+ }
33
+
34
+ const stopSpinner = (this.projectConfig.verbose) ? () => {} : Logger.spinner(incremental ? "Incremental compilation" : "Full project build");
35
+
36
+ const proc = Bun.spawn(command, {
37
+ stdout: "pipe",
38
+ stderr: "pipe"
39
+ });
40
+
41
+ if (this.projectConfig.verbose) {
42
+ await Promise.all([
43
+ this.processBuildLogs(proc.stdout, false),
44
+ this.processBuildLogs(proc.stderr, false)
45
+ ]);
46
+ }
47
+
48
+ await proc.exited;
49
+ stopSpinner();
50
+
51
+ if (proc.exitCode !== 0) {
52
+ if (!this.projectConfig.verbose) {
53
+ const err = await new Response(proc.stderr).text();
54
+ console.log(err);
55
+ }
56
+ Logger.error(`${this.projectConfig.buildTool.toUpperCase()} build failed!`);
57
+ throw new Error("Falha no build do Java!");
58
+ }
59
+ }
60
+
61
+ private async processBuildLogs(stream: ReadableStream, quiet: boolean) {
62
+ const reader = stream.getReader();
63
+ const decoder = new TextDecoder();
64
+ let errorCount = 0;
65
+ const maxErrors = 15;
66
+
67
+ while (true) {
68
+ const { done, value } = await reader.read();
69
+ if (done) break;
70
+
71
+ const chunk = decoder.decode(value);
72
+ const lines = chunk.split(/[\r\n]+/);
73
+
74
+ for (const line of lines) {
75
+ const cleanLine = line.trim();
76
+ if (!cleanLine) continue;
77
+
78
+ if (cleanLine.includes("[ERROR]")) {
79
+ errorCount++;
80
+ if (errorCount > maxErrors && !this.projectConfig.verbose) {
81
+ if (errorCount === maxErrors + 1) {
82
+ console.log(`\n ${"\x1b[31m"}... e mais erros ocultos. Use -V para ver todos.${"\x1b[0m"}`);
83
+ }
84
+ continue;
85
+ }
86
+ }
87
+
88
+ if (quiet) {
89
+ if (!Logger.isEssential(cleanLine)) continue;
90
+ } else if (Logger.isSystemNoise(cleanLine)) {
91
+ continue;
92
+ }
93
+
94
+ const summarized = Logger.summarize(cleanLine);
95
+ if (summarized) console.log(summarized);
96
+ }
97
+ }
98
+ }
99
+
100
+ async syncClasses(): Promise<string | null> {
101
+ const fs = require("fs");
102
+ let appFolder = this.projectConfig.appName || this.inferredAppName || "";
103
+
104
+ const webappsPath = path.join(this.tomcatConfig.path, this.tomcatConfig.webapps);
105
+
106
+ if (!appFolder && fs.existsSync(webappsPath)) {
107
+ const folders = fs.readdirSync(webappsPath, { withFileTypes: true })
108
+ .filter((dirent: any) => dirent.isDirectory() && !["ROOT", "manager", "host-manager", "docs"].includes(dirent.name));
109
+
110
+ if (folders.length === 1) {
111
+ appFolder = folders[0].name;
112
+ } else if (folders.length > 1) {
113
+ const sorted = folders.map((f: any) => ({
114
+ name: f.name,
115
+ time: fs.statSync(path.join(webappsPath, f.name)).mtimeMs
116
+ })).sort((a: any, b: any) => b.time - a.time);
117
+ appFolder = sorted[0].name;
118
+ }
119
+ }
120
+
121
+ const sourceDir = this.projectConfig.buildTool === 'maven' ? 'target/classes' : 'build/classes/java/main';
122
+ const destDir = path.join(webappsPath, appFolder, "WEB-INF", "classes");
123
+
124
+ if (!fs.existsSync(sourceDir)) return null;
125
+
126
+ if (!appFolder || !fs.existsSync(destDir)) {
127
+ Logger.warn("Pasta descompactada no Tomcat não encontrada. Hot Swap impossível.");
128
+ return null;
129
+ }
130
+
131
+ const copyDir = (src: string, dest: string) => {
132
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
133
+ const list = fs.readdirSync(src, { withFileTypes: true });
134
+ for (const item of list) {
135
+ const s = path.join(src, item.name);
136
+ const d = path.join(dest, item.name);
137
+ if (item.isDirectory()) {
138
+ copyDir(s, d);
139
+ } else {
140
+ if (!fs.existsSync(d) || fs.statSync(s).mtimeMs > fs.statSync(d).mtimeMs) {
141
+ fs.copyFileSync(s, d);
142
+ }
143
+ }
144
+ }
145
+ };
146
+
147
+ copyDir(sourceDir, destDir);
148
+ Logger.success("Classes swapped in running Tomcat");
149
+ return appFolder;
150
+ }
151
+
152
+ async deployToWebapps(): Promise<string> {
153
+ const destDir = path.join(this.tomcatConfig.path, this.tomcatConfig.webapps);
154
+
155
+ Logger.step("Searching for generated artifacts");
156
+
157
+ const findWars = (dir: string): string[] => {
158
+ let results: string[] = [];
159
+ const list = readdirSync(dir, { withFileTypes: true });
160
+ for (const item of list) {
161
+ const res = path.resolve(dir, item.name);
162
+ if (item.isDirectory()) {
163
+ if (item.name === 'target' || item.name === 'build') {
164
+ results = results.concat(findWars(res));
165
+ } else if (!['node_modules', '.git', 'src', 'webapps', 'bin', 'conf', 'lib', 'logs', 'temp', 'work'].includes(item.name)) {
166
+ results = results.concat(findWars(res));
167
+ }
168
+ } else if (item.name.endsWith('.war')) {
169
+ results.push(res);
170
+ }
171
+ }
172
+ return results;
173
+ };
174
+
175
+ const allWars = findWars(process.cwd())
176
+ .map(f => ({ path: f, name: path.basename(f), time: statSync(f).mtime.getTime() }))
177
+ .sort((a, b) => b.time - a.time);
178
+
179
+ if (allWars.length === 0) {
180
+ throw new Error('Nenhum arquivo .war encontrado! Verifique se o build realmente gerou um artefato.');
181
+ }
182
+
183
+ const warFile = allWars[0];
184
+ const finalName = this.projectConfig.appName ? `${this.projectConfig.appName}.war` : warFile.name;
185
+
186
+ if (!this.projectConfig.quiet) {
187
+ Logger.info("Artifact", warFile.name);
188
+ if (this.projectConfig.appName) Logger.info("Deploy as", finalName);
189
+ } else {
190
+ const displayName = this.projectConfig.appName ? `${this.projectConfig.appName}` : warFile.name.replace(".war", "");
191
+ process.stdout.write(` ${"\x1b[90m"}➜${"\x1b[0m"} Deploying ${"\x1b[1m"}${displayName}${"\x1b[0m"}...\n`);
192
+ }
193
+
194
+ copyFileSync(warFile.path, path.join(destDir, finalName));
195
+ this.inferredAppName = finalName.replace(".war", "");
196
+ return finalName;
197
+ }
198
+ }
@@ -0,0 +1,124 @@
1
+ import { readdirSync, readFileSync } from "fs";
2
+ import path from "path";
3
+ import type { ApiEndpoint, ApiParam } from "../types/endpoint";
4
+
5
+ export class EndpointService {
6
+ static scan(srcPath: string, contextPath: string = ""): ApiEndpoint[] {
7
+ const endpoints: ApiEndpoint[] = [];
8
+
9
+ const scanDir = (dir: string) => {
10
+ const list = readdirSync(dir, { withFileTypes: true });
11
+ for (const item of list) {
12
+ const res = path.resolve(dir, item.name);
13
+ if (item.isDirectory()) {
14
+ if (!['node_modules', '.git', 'target', 'build'].includes(item.name)) {
15
+ scanDir(res);
16
+ }
17
+ } else if (item.name.endsWith('.java')) {
18
+ const content = readFileSync(res, 'utf8');
19
+ const fileEndpoints = this.parseJavaFile(content, item.name, contextPath);
20
+ endpoints.push(...fileEndpoints);
21
+ }
22
+ }
23
+ };
24
+
25
+ try {
26
+ scanDir(srcPath);
27
+ } catch (e) {}
28
+
29
+ return endpoints.sort((a, b) => a.fullPath.localeCompare(b.fullPath));
30
+ }
31
+
32
+ private static parseJavaFile(content: string, fileName: string, contextPath: string): ApiEndpoint[] {
33
+ const endpoints: ApiEndpoint[] = [];
34
+ const className = fileName.replace(".java", "");
35
+
36
+ // Find class-level mapping
37
+ const classPathMatch = content.match(/@(Path|RequestMapping)\s*\(\s*["'](.*?)["']\s*\)/);
38
+ const basePath = classPathMatch ? this.normalizePath(classPathMatch[2]) : "";
39
+
40
+ // Common mapping annotations
41
+ const methodRegex = /@(GET|POST|PUT|DELETE|PATCH|Path|RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*(\(\s*["'](.*?)["']\s*\))?\s*([\s\S]*?)\s+([a-zA-Z0-9_]+)\s*\(([\s\S]*?)\)/g;
42
+
43
+ let match;
44
+ while ((match = methodRegex.exec(content)) !== null) {
45
+ const annotation = match[1];
46
+ const pathArg = match[3] || "";
47
+ const methodName = match[5];
48
+ const paramsRaw = match[6];
49
+
50
+ const method = this.inferHttpMethod(annotation);
51
+ const methodPath = this.normalizePath(pathArg);
52
+ const fullPath = this.combinePaths(contextPath, basePath, methodPath);
53
+
54
+ const parameters = this.parseParameters(paramsRaw);
55
+
56
+ endpoints.push({
57
+ method,
58
+ path: methodPath,
59
+ fullPath,
60
+ className,
61
+ methodName,
62
+ parameters
63
+ });
64
+ }
65
+
66
+ return endpoints;
67
+ }
68
+
69
+ private static inferHttpMethod(annotation: string): ApiEndpoint["method"] {
70
+ if (annotation.includes("GET")) return "GET";
71
+ if (annotation.includes("POST")) return "POST";
72
+ if (annotation.includes("PUT")) return "PUT";
73
+ if (annotation.includes("DELETE")) return "DELETE";
74
+ if (annotation.includes("PATCH")) return "PATCH";
75
+ return "ALL";
76
+ }
77
+
78
+ private static normalizePath(p: string): string {
79
+ if (!p) return "";
80
+ let path = p.trim();
81
+ if (!path.startsWith("/")) path = "/" + path;
82
+ if (path.endsWith("/")) path = path.slice(0, -1);
83
+ return path;
84
+ }
85
+
86
+ private static combinePaths(...parts: string[]): string {
87
+ return parts
88
+ .map(p => this.normalizePath(p))
89
+ .filter(p => p && p !== "/")
90
+ .join("") || "/";
91
+ }
92
+
93
+ private static parseParameters(paramsRaw: string): ApiParam[] {
94
+ const params: ApiParam[] = [];
95
+ if (!paramsRaw.trim()) return params;
96
+
97
+ const individualParams = paramsRaw.split(",");
98
+ for (const p of individualParams) {
99
+ const trimmed = p.trim();
100
+
101
+ // Check for annotations
102
+ const pathParam = trimmed.match(/@PathParam\s*\(\s*["'](.*?)["']\s*\)\s*(\w+)\s+(\w+)/);
103
+ const pathVariable = trimmed.match(/@PathVariable\s*\(\s*["'](.*?)["']\s*\)\s*(\w+)\s+(\w+)/);
104
+ const queryParam = trimmed.match(/@QueryParam\s*\(\s*["'](.*?)["']\s*\)\s*(\w+)\s+(\w+)/);
105
+ const requestParam = trimmed.match(/@RequestParam\s*\(\s*["'](.*?)["']\s*\)\s*(\w+)\s+(\w+)/);
106
+ const headerParam = trimmed.match(/@HeaderParam\s*\(\s*["'](.*?)["']\s*\)\s*(\w+)\s+(\w+)/);
107
+ const requestBody = trimmed.match(/@RequestBody\s*(\w+)\s+(\w+)/);
108
+
109
+ if (pathParam || pathVariable) {
110
+ const m = pathParam || pathVariable!;
111
+ params.push({ name: m[1], type: m[2], source: "PATH", required: true });
112
+ } else if (queryParam || requestParam) {
113
+ const m = queryParam || requestParam!;
114
+ params.push({ name: m[1], type: m[2], source: "QUERY", required: !trimmed.includes("required = false") });
115
+ } else if (headerParam) {
116
+ params.push({ name: headerParam[1], type: headerParam[2], source: "HEADER", required: true });
117
+ } else if (requestBody) {
118
+ params.push({ name: "body", type: requestBody[1], source: "BODY", required: true });
119
+ }
120
+ }
121
+
122
+ return params;
123
+ }
124
+ }
@@ -0,0 +1,164 @@
1
+ import type { TomcatConfig } from "../types/config";
2
+ import { Logger } from "../utils/ui";
3
+
4
+ export class TomcatService {
5
+ private activeConfig: TomcatConfig;
6
+ private currentProcess: any = null;
7
+ private stopStartupSpinner?: (success?: boolean) => void;
8
+ public onReady?: () => void;
9
+ private pid: number | null = null;
10
+
11
+ constructor(customConfig: TomcatConfig) {
12
+ this.activeConfig = customConfig;
13
+ }
14
+
15
+ async getMemoryUsage(): Promise<string> {
16
+ if (!this.pid) return "0 MB";
17
+ try {
18
+ const { stdout } = Bun.spawnSync(["powershell", "-command", `(Get-Process -Id ${this.pid}).WorkingSet64 / 1MB`]);
19
+ const mem = await new Response(stdout).text();
20
+ return `${Math.round(parseFloat(mem))} MB`;
21
+ } catch (e) {
22
+ return "N/A";
23
+ }
24
+ }
25
+
26
+ async killConflict() {
27
+ const { stdout } = Bun.spawnSync(["cmd", "/c", `netstat -ano | findstr :${this.activeConfig.port}`]);
28
+ const output = await new Response(stdout).text();
29
+
30
+ if (output) {
31
+ const lines = output.trim().split('\n');
32
+ const pid = lines[0].trim().split(/\s+/).pop();
33
+ Logger.step(`Freeing port ${this.activeConfig.port}`);
34
+ Bun.spawnSync(["taskkill", "/F", "/PID", pid]);
35
+ }
36
+ }
37
+
38
+ clearWebapps(appName?: string) {
39
+ const fs = require("fs");
40
+ const path = require("path");
41
+ const webappsPath = path.join(this.activeConfig.path, "webapps");
42
+ const workPath = path.join(this.activeConfig.path, "work");
43
+ const tempPath = path.join(this.activeConfig.path, "temp");
44
+
45
+ try {
46
+ [workPath, tempPath].forEach(p => {
47
+ if (fs.existsSync(p)) {
48
+ fs.rmSync(p, { recursive: true, force: true });
49
+ fs.mkdirSync(p);
50
+ }
51
+ });
52
+
53
+ const files = fs.readdirSync(webappsPath);
54
+ for (const file of files) {
55
+ const fullPath = path.join(webappsPath, file);
56
+ if (file === "ROOT" || file === "manager" || file === "host-manager") continue;
57
+
58
+ fs.rmSync(fullPath, { recursive: true, force: true });
59
+ }
60
+ } catch (e) {
61
+ Logger.warn("Não foi possível limpar totalmente a pasta webapps ou cache.");
62
+ }
63
+ }
64
+
65
+ stop() {
66
+ if (this.currentProcess) {
67
+ Logger.warn("Stopping active server...");
68
+ this.currentProcess.kill();
69
+ this.currentProcess = null;
70
+ }
71
+ }
72
+
73
+ start(cleanLogs: boolean = false, debug: boolean = false, skipScan: boolean = false, quiet: boolean = false) {
74
+ const binPath = `${this.activeConfig.path}\\bin\\catalina.bat`;
75
+ const args = debug ? ["jpda", "run"] : ["run"];
76
+
77
+ const catalinaOpts = [process.env.CATALINA_OPTS || ""];
78
+
79
+ if (skipScan) {
80
+ catalinaOpts.push(
81
+ "-Dtomcat.util.scan.StandardJarScanFilter.jarsToSkip=*.jar",
82
+ "-Dtomcat.util.scan.StandardJarScanFilter.jarsToScan=",
83
+ "-Dorg.apache.catalina.startup.ContextConfig.jarsToSkip=*.jar",
84
+ "-Dorg.apache.catalina.startup.TldConfig.jarsToSkip=*.jar",
85
+ "-Dorg.apache.tomcat.util.scan.StandardJarScanFilter.jarsToSkip=*.jar",
86
+ "-Dorg.apache.catalina.startup.ContextConfig.jarsToScan="
87
+ );
88
+ }
89
+
90
+ const env: any = {
91
+ ...process.env,
92
+ CATALINA_HOME: this.activeConfig.path,
93
+ CATALINA_OPTS: catalinaOpts.join(" ").trim()
94
+ };
95
+
96
+ if (debug) {
97
+ Logger.warn("🐞 Java Debugger habilitado na porta 5005");
98
+ env.JPDA_ADDRESS = "5005";
99
+ env.JPDA_TRANSPORT = "dt_socket";
100
+ }
101
+
102
+ if (cleanLogs || quiet) {
103
+ this.stopStartupSpinner = Logger.spinner("Starting Tomcat server");
104
+ }
105
+
106
+ this.currentProcess = Bun.spawn([binPath, ...args], {
107
+ stdout: "pipe",
108
+ stderr: "pipe",
109
+ env: env
110
+ });
111
+
112
+ this.pid = this.currentProcess.pid;
113
+
114
+ this.processLogStream(this.currentProcess.stdout, cleanLogs, quiet);
115
+ this.processLogStream(this.currentProcess.stderr, cleanLogs, quiet);
116
+ }
117
+
118
+ private async processLogStream(stream: ReadableStream, clean: boolean, quiet: boolean) {
119
+ const reader = stream.getReader();
120
+ const decoder = new TextDecoder();
121
+
122
+ while (true) {
123
+ const { done, value } = await reader.read();
124
+ if (done) break;
125
+
126
+ const chunk = decoder.decode(value);
127
+ const lines = chunk.split(/[\r\n]+/);
128
+
129
+ for (const line of lines) {
130
+ const cleanLine = line.trim();
131
+ if (!cleanLine || cleanLine.startsWith("Listening for transport")) continue;
132
+
133
+ if (cleanLine.includes("Server startup in") || cleanLine.includes("SEVERE") || cleanLine.includes("Exception")) {
134
+ const isSuccess = cleanLine.includes("Server startup in");
135
+ if (this.stopStartupSpinner) {
136
+ this.stopStartupSpinner(isSuccess);
137
+ this.stopStartupSpinner = undefined;
138
+ }
139
+ if (isSuccess && this.onReady) {
140
+ this.onReady();
141
+ }
142
+ }
143
+
144
+ if (clean) {
145
+ if (quiet && !Logger.isEssential(cleanLine)) {
146
+ if (Logger.isSystemNoise(cleanLine)) continue;
147
+ if (cleanLine.includes("INFO")) continue;
148
+ } else if (Logger.isSystemNoise(cleanLine)) {
149
+ continue;
150
+ }
151
+
152
+ if (this.activeConfig.grep && !cleanLine.toLowerCase().includes(this.activeConfig.grep.toLowerCase())) {
153
+ if (!Logger.isEssential(cleanLine)) continue;
154
+ }
155
+
156
+ const summarized = Logger.summarize(cleanLine);
157
+ console.log(summarized);
158
+ } else {
159
+ console.log(cleanLine);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,24 @@
1
+ export interface TomcatConfig {
2
+ path: string;
3
+ port: number;
4
+ webapps: string;
5
+ grep?: string;
6
+ }
7
+
8
+ export interface ProjectConfig {
9
+ appName: string;
10
+ buildTool: "maven" | "gradle";
11
+ profile: string;
12
+ skipBuild: boolean;
13
+ skipScan: boolean;
14
+ cleanLogs: boolean;
15
+ quiet: boolean;
16
+ verbose: boolean;
17
+ debug: boolean;
18
+ grep?: string;
19
+ }
20
+
21
+ export interface AppConfig {
22
+ tomcat: TomcatConfig;
23
+ project: ProjectConfig;
24
+ }
@@ -0,0 +1,15 @@
1
+ export interface ApiParam {
2
+ name: string;
3
+ type: string;
4
+ source: "PATH" | "QUERY" | "BODY" | "HEADER" | "FORM";
5
+ required: boolean;
6
+ }
7
+
8
+ export interface ApiEndpoint {
9
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ALL";
10
+ path: string;
11
+ fullPath: string;
12
+ className: string;
13
+ methodName: string;
14
+ parameters: ApiParam[];
15
+ }