변경 목적: 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개를 확인했습니다.
252 lines
9.3 KiB
C#
252 lines
9.3 KiB
C#
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}줄)";
|
|
}
|
|
}
|