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:
251
src/AxCopilot/Handlers/GitHandler.cs
Normal file
251
src/AxCopilot/Handlers/GitHandler.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L7-1: Git 빠른 조회 핸들러. "git" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: git → 최근 작업 폴더(또는 현재 앱 폴더)의 git 상태 요약
|
||||
/// git status → git status --short 출력
|
||||
/// git log → 최근 커밋 10개
|
||||
/// git branch → 브랜치 목록 (현재 브랜치 강조)
|
||||
/// git stash → stash 목록
|
||||
/// git diff → git diff --stat 요약
|
||||
/// git pull → git pull 실행
|
||||
/// Enter → 결과를 클립보드에 복사.
|
||||
/// </summary>
|
||||
public class GitHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "git";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Git",
|
||||
"Git 빠른 조회 — git status · log · branch · stash",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
// ── 서브커맨드 정의 ──────────────────────────────────────────────────────
|
||||
private static readonly (string Sub, string Args, string Label, string Icon)[] SubCommands =
|
||||
[
|
||||
("status", "status --short", "변경 파일 목록", "\uE9F5"),
|
||||
("log", "log --oneline -10", "최근 커밋 10개", "\uE81C"),
|
||||
("branch", "branch -a", "브랜치 목록", "\uE8FB"),
|
||||
("stash", "stash list", "Stash 목록", "\uE7C4"),
|
||||
("diff", "diff --stat", "변경 통계", "\uE8A1"),
|
||||
("pull", "pull", "git pull 실행", "\uE8AF"),
|
||||
];
|
||||
|
||||
public async Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim().ToLowerInvariant();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
// 작업 디렉토리 결정
|
||||
var workDir = FindGitRoot();
|
||||
|
||||
if (string.IsNullOrEmpty(q))
|
||||
{
|
||||
// 빠른 상태 요약
|
||||
if (!string.IsNullOrEmpty(workDir))
|
||||
{
|
||||
var branch = await RunGitAsync("branch --show-current", workDir, ct);
|
||||
var statusOut = await RunGitAsync("status --short", workDir, ct);
|
||||
var changed = statusOut?.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length ?? 0;
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"Git: {branch?.Trim() ?? "unknown"}",
|
||||
changed == 0 ? "변경 없음" : $"{changed}개 파일 변경됨 · {System.IO.Path.GetFileName(workDir)}",
|
||||
null,
|
||||
("status_summary", workDir),
|
||||
Symbol: "\uE9F5"));
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem("Git 저장소 없음",
|
||||
"현재 작업 폴더에 .git 디렉토리가 없습니다", null, null,
|
||||
Symbol: "\uE783"));
|
||||
}
|
||||
|
||||
// 서브커맨드 목록
|
||||
foreach (var (sub, args, label, icon) in SubCommands)
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
$"git {sub}",
|
||||
label,
|
||||
null,
|
||||
(sub, workDir ?? ""),
|
||||
Symbol: icon));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// 서브커맨드 매칭
|
||||
var matched = SubCommands
|
||||
.Where(sc => sc.Sub.StartsWith(q, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
if (matched.Count > 0)
|
||||
{
|
||||
foreach (var (sub, args, label, icon) in matched)
|
||||
{
|
||||
string? preview = null;
|
||||
if (!string.IsNullOrEmpty(workDir))
|
||||
preview = await RunGitAsync(args, workDir, ct);
|
||||
|
||||
var subtitle = preview != null
|
||||
? TruncateLines(preview, 3)
|
||||
: label;
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"git {sub}",
|
||||
subtitle,
|
||||
null,
|
||||
(sub, workDir ?? "", args),
|
||||
Symbol: icon));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 자유 명령 실행 (git <query>)
|
||||
string? output = null;
|
||||
if (!string.IsNullOrEmpty(workDir))
|
||||
output = await RunGitAsync(q, workDir, ct);
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"git {query.Trim()}",
|
||||
output != null ? TruncateLines(output, 3) : "실행 후 결과 클립보드 복사",
|
||||
null,
|
||||
("custom", workDir ?? "", query.Trim()),
|
||||
Symbol: "\uE9F5"));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
string? result = null;
|
||||
|
||||
switch (item.Data)
|
||||
{
|
||||
// 상태 요약 항목
|
||||
case ("status_summary", string workDir):
|
||||
result = await RunGitAsync("status", workDir, ct);
|
||||
break;
|
||||
|
||||
// 서브커맨드 항목 (sub, workDir, args)
|
||||
case (string sub, string workDir, string args):
|
||||
if (sub == "pull")
|
||||
{
|
||||
// pull은 별도 터미널 창으로 실행
|
||||
if (!string.IsNullOrEmpty(workDir))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell.exe",
|
||||
Arguments = $"-NoProfile -Command \"cd '{workDir}'; git pull; Read-Host 'Enter 키를 누르면 닫힙니다'\"",
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
result = await RunGitAsync(args, workDir, ct);
|
||||
break;
|
||||
|
||||
// 서브커맨드 항목 (sub, workDir) — args 없는 경우
|
||||
case (string sub2, string workDir2):
|
||||
var found = SubCommands.FirstOrDefault(sc => sc.Sub == sub2);
|
||||
if (found != default)
|
||||
result = await RunGitAsync(found.Args, workDir2, ct);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result))
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(
|
||||
() => Clipboard.SetText(result));
|
||||
NotificationService.Notify("Git", "결과를 클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>현재 앱 설정의 작업 폴더에서 .git root를 찾습니다.</summary>
|
||||
private static string? FindGitRoot()
|
||||
{
|
||||
var app = System.Windows.Application.Current as App;
|
||||
var workDir = app?.SettingsService?.Settings.Llm.WorkFolder ?? "";
|
||||
|
||||
if (string.IsNullOrEmpty(workDir) || !System.IO.Directory.Exists(workDir))
|
||||
workDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
// .git 폴더를 찾아 상위로 이동
|
||||
var dir = new System.IO.DirectoryInfo(workDir);
|
||||
while (dir != null)
|
||||
{
|
||||
if (System.IO.Directory.Exists(System.IO.Path.Combine(dir.FullName, ".git")))
|
||||
return dir.FullName;
|
||||
dir = dir.Parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>git 명령을 비동기로 실행하고 출력을 반환합니다.</summary>
|
||||
private static async Task<string?> RunGitAsync(string args, string workDir, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrEmpty(workDir)) return null;
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("git", args)
|
||||
{
|
||||
WorkingDirectory = workDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
};
|
||||
|
||||
using var proc = Process.Start(psi);
|
||||
if (proc == null) return null;
|
||||
|
||||
var output = await proc.StandardOutput.ReadToEndAsync(ct);
|
||||
var error = await proc.StandardError.ReadToEndAsync(ct);
|
||||
await proc.WaitForExitAsync(ct);
|
||||
|
||||
var text = output.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(error))
|
||||
text = error.Trim();
|
||||
|
||||
return string.IsNullOrWhiteSpace(text) ? "(출력 없음)" : text;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"오류: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>긴 출력을 maxLines줄로 자릅니다.</summary>
|
||||
private static string TruncateLines(string text, int maxLines)
|
||||
{
|
||||
var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lines.Length <= maxLines) return string.Join(" · ", lines.Take(maxLines)).Trim();
|
||||
return string.Join(" · ", lines.Take(maxLines)) + $" … (+{lines.Length - maxLines}줄)";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user