using System.Diagnostics; using System.Text.RegularExpressions; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L28-1: winget 앱 검색·설치·목록 핸들러. "pkg" 프리픽스로 사용합니다. /// /// 예: pkg → 사용법 안내 /// pkg vscode → winget search vscode /// pkg install {id} → winget install {id} /// pkg list → 설치된 앱 목록 /// pkg upgrade → 업그레이드 가능 목록 /// winget 미설치 시 안내 메시지 표시. /// public partial class PkgHandler : IActionHandler { public string? Prefix => "pkg"; public PluginMetadata Metadata => new( "앱 패키지", "winget 앱 검색·설치·업그레이드", "1.0", "AX"); private static bool? _wingetAvailable; public async Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); // winget 설치 여부 체크 (캐시) _wingetAvailable ??= await CheckWingetAsync(); if (_wingetAvailable == false) { items.Add(new LauncherItem( "winget이 설치되어 있지 않습니다", "Windows Package Manager는 Windows 10 1709+ 에서 사용 가능합니다", null, null, Symbol: Symbols.Warning)); return items; } if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("winget 앱 패키지 관리", "pkg {검색어} · pkg install {id} · pkg list · pkg upgrade", null, null, Symbol: "\uECAA")); return items; } // ── list 명령 ───────────────────────────────────────────────────────── if (q.Equals("list", StringComparison.OrdinalIgnoreCase)) { items.Add(new LauncherItem("설치된 앱 목록 조회 중...", "winget list 실행", null, ("list", ""), Symbol: "\uECAA")); // 실행 시 터미널에서 보여주기 return items; } // ── upgrade 명령 ────────────────────────────────────────────────────── if (q.Equals("upgrade", StringComparison.OrdinalIgnoreCase)) { items.Add(new LauncherItem("업그레이드 가능 앱 확인", "Enter: winget upgrade 실행", null, ("upgrade", ""), Symbol: "\uE777")); return items; } // ── install 명령 ────────────────────────────────────────────────────── if (q.StartsWith("install ", StringComparison.OrdinalIgnoreCase)) { var id = q[8..].Trim(); if (!string.IsNullOrWhiteSpace(id)) { items.Add(new LauncherItem( $"앱 설치: {id}", $"Enter: winget install --id {id}", null, ("install", id), Symbol: "\uE896")); } else { items.Add(new LauncherItem("사용법: pkg install {앱ID}", "예: pkg install Microsoft.VisualStudioCode", null, null, Symbol: Symbols.Info)); } return items; } // ── 검색 ────────────────────────────────────────────────────────────── try { var results = await SearchAsync(q, ct); if (results.Count == 0) { items.Add(new LauncherItem($"'{q}' 검색 결과 없음", "다른 검색어를 시도하세요", null, null, Symbol: Symbols.Search)); } else { items.Add(new LauncherItem($"검색 결과: {results.Count}개", "Enter: winget install --id {ID}", null, null, Symbol: Symbols.Search)); foreach (var r in results.Take(10)) { items.Add(new LauncherItem( $"{r.Name} [{r.Version}]", $"{r.Id} · {r.Source}", null, ("install", r.Id), Symbol: "\uECAA")); } } } catch (OperationCanceledException) { } catch (Exception ex) { items.Add(new LauncherItem("검색 오류", ex.Message, null, null, Symbol: Symbols.Error)); } return items; } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is ("install", string id) && !string.IsNullOrWhiteSpace(id)) { RunWingetInTerminal($"install --id \"{id}\" --accept-source-agreements --accept-package-agreements"); NotificationService.Notify("pkg", $"설치 시작: {id}"); } else if (item.Data is ("list", _)) { RunWingetInTerminal("list"); } else if (item.Data is ("upgrade", _)) { RunWingetInTerminal("upgrade --include-unknown"); } return Task.CompletedTask; } // ─── winget 검색 ────────────────────────────────────────────────────────── private record PkgResult(string Name, string Id, string Version, string Source); private static async Task> SearchAsync(string query, CancellationToken ct) { var output = await RunWingetAsync($"search \"{query}\" --accept-source-agreements", ct); return ParseWingetOutput(output); } [GeneratedRegex(@"^(.+?)\s{2,}(\S+)\s{2,}(\S+)\s{2,}(\S+)\s*$")] private static partial Regex WingetLineRegex(); private static List ParseWingetOutput(string output) { var results = new List(); var lines = output.Split('\n'); bool pastHeader = false; foreach (var rawLine in lines) { var line = rawLine.TrimEnd(); // 헤더 구분선 (---) 이후부터 데이터 if (line.StartsWith("---") || line.StartsWith("───")) { pastHeader = true; continue; } if (!pastHeader || string.IsNullOrWhiteSpace(line)) continue; var match = WingetLineRegex().Match(line); if (match.Success) { results.Add(new PkgResult( match.Groups[1].Value.Trim(), match.Groups[2].Value.Trim(), match.Groups[3].Value.Trim(), match.Groups[4].Value.Trim())); } } return results; } // ─── winget 실행 ────────────────────────────────────────────────────────── private static async Task CheckWingetAsync() { try { var output = await RunWingetAsync("--version", CancellationToken.None); return output.TrimStart().StartsWith('v'); } catch { return false; } } private static async Task RunWingetAsync(string args, CancellationToken ct) { using var proc = new Process { StartInfo = new ProcessStartInfo { FileName = "winget", Arguments = args, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = System.Text.Encoding.UTF8 } }; proc.Start(); var output = await proc.StandardOutput.ReadToEndAsync(ct); await proc.WaitForExitAsync(ct); return output; } private static void RunWingetInTerminal(string args) { try { // 사용자에게 진행 상황이 보이도록 터미널 창으로 실행 Process.Start(new ProcessStartInfo { FileName = "cmd.exe", Arguments = $"/k winget {args}", UseShellExecute = true }); } catch (Exception ex) { LogService.Warn($"winget 실행 실패: {ex.Message}"); } } }