AX Commander 비교본 런처 기능 대량 이식
변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
This commit is contained in:
238
src/AxCopilot/Handlers/PkgHandler.cs
Normal file
238
src/AxCopilot/Handlers/PkgHandler.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L28-1: winget 앱 검색·설치·목록 핸들러. "pkg" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: pkg → 사용법 안내
|
||||
/// pkg vscode → winget search vscode
|
||||
/// pkg install {id} → winget install {id}
|
||||
/// pkg list → 설치된 앱 목록
|
||||
/// pkg upgrade → 업그레이드 가능 목록
|
||||
/// winget 미설치 시 안내 메시지 표시.
|
||||
/// </summary>
|
||||
public partial class PkgHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "pkg";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"앱 패키지",
|
||||
"winget 앱 검색·설치·업그레이드",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
private static bool? _wingetAvailable;
|
||||
|
||||
public async Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
// 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<List<PkgResult>> 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<PkgResult> ParseWingetOutput(string output)
|
||||
{
|
||||
var results = new List<PkgResult>();
|
||||
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<bool> CheckWingetAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var output = await RunWingetAsync("--version", CancellationToken.None);
|
||||
return output.TrimStart().StartsWith('v');
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static async Task<string> 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user