using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows; namespace AxCopilot.Services.Agent; public class BuildRunTool : IAgentTool { private record ProjectInfo(string Type, string Marker, string BuildCommand, string TestCommand, string RunCommand, string LintCommand, string FormatCommand); private static readonly string[] DangerousPatterns = new string[16] { "format ", "del /s", "rd /s", "rmdir /s", "rm -rf", "Remove-Item -Recurse -Force", "Stop-Computer", "Restart-Computer", "shutdown", "taskkill /f", "reg delete", "reg add", "net user", "net localgroup", "schtasks /create", "schtasks /delete" }; public string Name => "build_run"; public string Description => "Detect project type and run build/test commands. Supports: .NET (dotnet), Maven (mvn), Gradle, Node.js (npm), Python (pytest), CMake, Make. Actions: detect (show project type), build, test, run, custom (run arbitrary command with longer timeout)."; public ToolParameterSchema Parameters { get { ToolParameterSchema toolParameterSchema = new ToolParameterSchema(); Dictionary dictionary = new Dictionary(); ToolProperty obj = new ToolProperty { Type = "string", Description = "Action to perform: detect, build, test, run, lint, format, custom" }; int num = 7; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "detect"; span[1] = "build"; span[2] = "test"; span[3] = "run"; span[4] = "lint"; span[5] = "format"; span[6] = "custom"; obj.Enum = list; dictionary["action"] = obj; dictionary["command"] = new ToolProperty { Type = "string", Description = "Custom command to execute (required for action='custom')" }; dictionary["project_path"] = new ToolProperty { Type = "string", Description = "Subdirectory within work folder (optional, defaults to work folder root)" }; toolParameterSchema.Properties = dictionary; num = 1; List list2 = new List(num); CollectionsMarshal.SetCount(list2, num); CollectionsMarshal.AsSpan(list2)[0] = "action"; toolParameterSchema.Required = list2; return toolParameterSchema; } } public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { string action = args.GetProperty("action").GetString() ?? "detect"; JsonElement cmd; string customCmd = (args.TryGetProperty("command", out cmd) ? (cmd.GetString() ?? "") : ""); JsonElement pp; string subPath = (args.TryGetProperty("project_path", out pp) ? (pp.GetString() ?? "") : ""); string workDir = context.WorkFolder; if (!string.IsNullOrEmpty(subPath)) { workDir = Path.Combine(workDir, subPath); } if (string.IsNullOrEmpty(workDir) || !Directory.Exists(workDir)) { return ToolResult.Fail("작업 폴더가 유효하지 않습니다: " + workDir); } ProjectInfo project = DetectProjectType(workDir); if (action == "detect") { if (project == null) { return ToolResult.Ok("프로젝트 감지 실패: " + workDir + "\n알려진 프로젝트 마커 파일이 없습니다."); } return ToolResult.Ok($"프로젝트 감지 완료:\n 타입: {project.Type}\n 마커: {project.Marker}\n 빌드: {project.BuildCommand}\n 테스트: {project.TestCommand}\n 실행: {project.RunCommand}\n 린트: {(string.IsNullOrEmpty(project.LintCommand) ? "(미지원)" : project.LintCommand)}\n 포맷: {(string.IsNullOrEmpty(project.FormatCommand) ? "(미지원)" : project.FormatCommand)}\n 경로: {workDir}"); } string command; if (action == "custom") { if (string.IsNullOrWhiteSpace(customCmd)) { return ToolResult.Fail("custom 액션에는 command 파라미터가 필요합니다."); } command = customCmd; } else { if (project == null) { return ToolResult.Fail("프로젝트 타입을 감지할 수 없습니다. action='custom'으로 직접 명령을 지정하세요."); } if (1 == 0) { } string text = action switch { "build" => project.BuildCommand, "test" => project.TestCommand, "run" => project.RunCommand, "lint" => string.IsNullOrEmpty(project.LintCommand) ? null : project.LintCommand, "format" => string.IsNullOrEmpty(project.FormatCommand) ? null : project.FormatCommand, _ => project.BuildCommand, }; if (1 == 0) { } command = text; if (command == null) { return ToolResult.Fail($"이 프로젝트 타입({project.Type})에서 '{action}' 작업은 지원되지 않습니다."); } } string[] dangerousPatterns = DangerousPatterns; foreach (string pattern in dangerousPatterns) { if (command.Contains(pattern, StringComparison.OrdinalIgnoreCase)) { return ToolResult.Fail("차단된 명령 패턴: " + pattern); } } if (!(await context.CheckWritePermissionAsync(Name, workDir))) { return ToolResult.Fail("빌드 실행 권한이 거부되었습니다."); } int timeout = (Application.Current as App)?.SettingsService?.Settings.Llm.Code.BuildTimeout ?? 120; try { ProcessStartInfo psi = new ProcessStartInfo("cmd.exe", "/C " + command) { WorkingDirectory = workDir, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(timeout)); using Process proc = Process.Start(psi); if (proc == null) { return ToolResult.Fail("프로세스 시작 실패"); } Task stdoutTask = proc.StandardOutput.ReadToEndAsync(cts.Token); Task stderrTask = proc.StandardError.ReadToEndAsync(cts.Token); await proc.WaitForExitAsync(cts.Token); string stdout = await stdoutTask; string stderr = await stderrTask; if (stdout.Length > 8000) { stdout = stdout.Substring(0, 8000) + "\n... (출력 잘림)"; } if (stderr.Length > 4000) { stderr = stderr.Substring(0, 4000) + "\n... (출력 잘림)"; } StringBuilder sb = new StringBuilder(); StringBuilder stringBuilder = sb; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 2, stringBuilder); handler.AppendLiteral("["); handler.AppendFormatted(action); handler.AppendLiteral("] "); handler.AppendFormatted(command); stringBuilder2.AppendLine(ref handler); stringBuilder = sb; StringBuilder stringBuilder3 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder); handler.AppendLiteral("[Exit code: "); handler.AppendFormatted(proc.ExitCode); handler.AppendLiteral("]"); stringBuilder3.AppendLine(ref handler); if (!string.IsNullOrWhiteSpace(stdout)) { sb.AppendLine(stdout); } if (!string.IsNullOrWhiteSpace(stderr)) { stringBuilder = sb; StringBuilder stringBuilder4 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder); handler.AppendLiteral("[stderr]\n"); handler.AppendFormatted(stderr); stringBuilder4.AppendLine(ref handler); } return (proc.ExitCode == 0) ? ToolResult.Ok(sb.ToString()) : ToolResult.Fail(sb.ToString()); } catch (OperationCanceledException) { return ToolResult.Fail($"빌드 타임아웃 ({timeout}초 초과): {command}"); } catch (Exception ex2) { return ToolResult.Fail("실행 오류: " + ex2.Message); } } private static ProjectInfo? DetectProjectType(string dir) { if (Directory.GetFiles(dir, "*.sln").Length != 0) { return new ProjectInfo(".NET Solution", "*.sln", "dotnet build", "dotnet test", "dotnet run", "dotnet format --verify-no-changes", "dotnet format"); } if (Directory.GetFiles(dir, "*.csproj").Length != 0) { return new ProjectInfo(".NET Project", "*.csproj", "dotnet build", "dotnet test", "dotnet run", "dotnet format --verify-no-changes", "dotnet format"); } if (File.Exists(Path.Combine(dir, "pom.xml"))) { return new ProjectInfo("Maven", "pom.xml", "mvn compile", "mvn test", "mvn exec:java", "mvn checkstyle:check", ""); } if (Directory.GetFiles(dir, "build.gradle*").Length != 0) { return new ProjectInfo("Gradle", "build.gradle", "gradle build", "gradle test", "gradle run", "gradle check", ""); } if (File.Exists(Path.Combine(dir, "package.json"))) { return new ProjectInfo("Node.js", "package.json", "npm run build", "npm test", "npm start", "npx eslint .", "npx prettier --write ."); } if (File.Exists(Path.Combine(dir, "CMakeLists.txt"))) { return new ProjectInfo("CMake", "CMakeLists.txt", "cmake --build build", "ctest --test-dir build", "", "", ""); } if (File.Exists(Path.Combine(dir, "pyproject.toml"))) { return new ProjectInfo("Python (pyproject)", "pyproject.toml", "python -m build", "python -m pytest", "python -m", "python -m ruff check .", "python -m black ."); } if (File.Exists(Path.Combine(dir, "setup.py"))) { return new ProjectInfo("Python (setup.py)", "setup.py", "python setup.py build", "python -m pytest", "python", "python -m ruff check .", "python -m black ."); } if (File.Exists(Path.Combine(dir, "Makefile"))) { return new ProjectInfo("Make", "Makefile", "make", "make test", "make run", "make lint", "make format"); } if (Directory.GetFiles(dir, "*.py").Length != 0) { return new ProjectInfo("Python Scripts", "*.py", "", "python -m pytest", "python", "python -m ruff check .", "python -m black ."); } return null; } }