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:
274
src/AxCopilot/Handlers/WslHandler.cs
Normal file
274
src/AxCopilot/Handlers/WslHandler.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L15-1: WSL(Windows Subsystem for Linux) 관리 핸들러. "wsl" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: wsl → 설치된 distro 목록 + 상태
|
||||
/// wsl ubuntu → Ubuntu 실행 (새 터미널)
|
||||
/// wsl stop ubuntu → 특정 distro 종료
|
||||
/// wsl stop all → 전체 WSL 종료
|
||||
/// wsl default ubuntu → 기본 distro 변경
|
||||
/// Enter → distro 실행 또는 명령 실행.
|
||||
/// </summary>
|
||||
public class WslHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "wsl";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"WSL",
|
||||
"WSL 관리 — distro 목록 · 실행 · 종료",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
var distros = GetDistros();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
if (distros.Count == 0)
|
||||
{
|
||||
items.Add(new LauncherItem("WSL 없음",
|
||||
"WSL이 설치되지 않았거나 distro가 없습니다", null, null, Symbol: "\uE756"));
|
||||
items.Add(new LauncherItem("WSL 설치",
|
||||
"Microsoft Store에서 Ubuntu 등 설치", null,
|
||||
("open_url", "ms-windows-store://search/?query=linux"), Symbol: "\uE756"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"WSL distro {distros.Count}개",
|
||||
"Enter → 실행 / wsl stop all → 전체 종료",
|
||||
null, null, Symbol: "\uE756"));
|
||||
|
||||
foreach (var d in distros)
|
||||
items.Add(MakeDistroItem(d));
|
||||
|
||||
items.Add(new LauncherItem("wsl stop all", "전체 WSL 종료 (wsl --shutdown)", null,
|
||||
("shutdown", ""), Symbol: "\uE756"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
|
||||
switch (sub)
|
||||
{
|
||||
case "stop":
|
||||
case "shutdown":
|
||||
case "kill":
|
||||
{
|
||||
var target = parts.Length > 1 ? parts[1].ToLowerInvariant() : "all";
|
||||
if (target == "all" || target == "--all")
|
||||
{
|
||||
items.Add(new LauncherItem("WSL 전체 종료", "wsl --shutdown · Enter 실행",
|
||||
null, ("shutdown", ""), Symbol: "\uE756"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var found = distros.FirstOrDefault(d =>
|
||||
d.Name.Contains(target, StringComparison.OrdinalIgnoreCase));
|
||||
if (found == null)
|
||||
items.Add(new LauncherItem("없는 distro", $"'{target}'를 찾을 수 없습니다", null, null, Symbol: "\uE783"));
|
||||
else
|
||||
items.Add(new LauncherItem($"{found.Name} 종료", $"wsl --terminate {found.Name}",
|
||||
null, ("terminate", found.Name), Symbol: "\uE756"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "default":
|
||||
case "set-default":
|
||||
{
|
||||
var target = parts.Length > 1 ? parts[1] : "";
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
items.Add(new LauncherItem("distro 이름 입력", "예: wsl default Ubuntu", null, null, Symbol: "\uE783"));
|
||||
break;
|
||||
}
|
||||
items.Add(new LauncherItem($"기본 distro: {target}",
|
||||
$"wsl --set-default {target} · Enter 실행",
|
||||
null, ("set_default", target), Symbol: "\uE756"));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
// distro 이름 검색 → 실행
|
||||
var found = distros.Where(d =>
|
||||
d.Name.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
if (found.Count > 0)
|
||||
foreach (var d in found)
|
||||
items.Add(MakeDistroItem(d));
|
||||
else
|
||||
items.Add(new LauncherItem($"'{q}' distro 없음",
|
||||
"wsl 입력으로 전체 목록 확인", null, null, Symbol: "\uE946"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
switch (item.Data)
|
||||
{
|
||||
case ("launch", string distro):
|
||||
RunWsl($"-d \"{distro}\"");
|
||||
break;
|
||||
|
||||
case ("shutdown", _):
|
||||
RunWslSilent("--shutdown");
|
||||
NotificationService.Notify("WSL", "WSL 전체 종료 요청됨");
|
||||
break;
|
||||
|
||||
case ("terminate", string distro):
|
||||
RunWslSilent($"--terminate \"{distro}\"");
|
||||
NotificationService.Notify("WSL", $"{distro} 종료됨");
|
||||
break;
|
||||
|
||||
case ("set_default", string distro):
|
||||
RunWslSilent($"--set-default \"{distro}\"");
|
||||
NotificationService.Notify("WSL", $"기본 distro → {distro}");
|
||||
break;
|
||||
|
||||
case ("open_url", string url):
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = url, UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
break;
|
||||
|
||||
case ("copy", string text):
|
||||
try
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
|
||||
NotificationService.Notify("WSL", "복사됨");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
break;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── WSL 조회 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private record WslDistro(string Name, string State, string Version, bool IsDefault);
|
||||
|
||||
private static List<WslDistro> GetDistros()
|
||||
{
|
||||
var result = new List<WslDistro>();
|
||||
try
|
||||
{
|
||||
var psi = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "wsl",
|
||||
Arguments = "--list --verbose",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.Unicode, // WSL outputs UTF-16
|
||||
};
|
||||
using var proc = System.Diagnostics.Process.Start(psi);
|
||||
if (proc == null) return result;
|
||||
|
||||
var output = proc.StandardOutput.ReadToEnd();
|
||||
proc.WaitForExit(3000);
|
||||
|
||||
foreach (var line in output.Split('\n').Skip(1)) // 첫 줄은 헤더
|
||||
{
|
||||
var trimmed = line.Trim().TrimEnd('\r');
|
||||
if (string.IsNullOrWhiteSpace(trimmed)) continue;
|
||||
|
||||
var isDefault = trimmed.StartsWith('*');
|
||||
trimmed = trimmed.TrimStart('*').Trim();
|
||||
|
||||
var parts = trimmed.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 2) continue;
|
||||
|
||||
result.Add(new WslDistro(
|
||||
Name: parts[0],
|
||||
State: parts.Length > 1 ? parts[1] : "Unknown",
|
||||
Version: parts.Length > 2 ? parts[2] : "?",
|
||||
IsDefault: isDefault));
|
||||
}
|
||||
}
|
||||
catch { /* WSL 없음 */ }
|
||||
return result;
|
||||
}
|
||||
|
||||
private static LauncherItem MakeDistroItem(WslDistro d)
|
||||
{
|
||||
var icon = d.State.Equals("Running", StringComparison.OrdinalIgnoreCase) ? "\uE768" : "\uE756";
|
||||
var label = d.IsDefault ? $"★ {d.Name}" : d.Name;
|
||||
var subtitle = $"{d.State} · WSL {d.Version}" + (d.IsDefault ? " (기본)" : "");
|
||||
return new LauncherItem(label, subtitle, null, ("launch", d.Name), Symbol: icon);
|
||||
}
|
||||
|
||||
private static void RunWsl(string args)
|
||||
{
|
||||
// 터미널에서 실행 (wt 또는 powershell 폴백)
|
||||
var wtPath = FindExe("wt.exe");
|
||||
if (wtPath != null)
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = wtPath,
|
||||
Arguments = $"wsl {args}",
|
||||
UseShellExecute = false,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "wsl",
|
||||
Arguments = args,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunWslSilent(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "wsl",
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
using var proc = System.Diagnostics.Process.Start(psi);
|
||||
proc?.WaitForExit(5000);
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
|
||||
private static string? FindExe(string name)
|
||||
{
|
||||
var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
foreach (var dir in pathEnv.Split(';'))
|
||||
{
|
||||
var full = System.IO.Path.Combine(dir.Trim(), name);
|
||||
if (System.IO.File.Exists(full)) return full;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user