From 3dc1a9589c1367c596deea9926d969c5758dd28d Mon Sep 17 00:00:00 2001 From: Daniel Hufnagl Date: Thu, 2 Apr 2026 14:15:28 +0200 Subject: [PATCH] add codex skill host support --- AGENTS.md | 9 ++- README.md | 34 ++++++----- src/MauiDevFlow.CLI/Program.cs | 56 +++++++++++-------- src/MauiDevFlow.CLI/SkillHostPaths.cs | 30 ++++++++++ .../MauiDevFlow.Tests.csproj | 3 +- .../MauiDevFlow.Tests/SkillHostPathsTests.cs | 37 ++++++++++++ 6 files changed, 129 insertions(+), 40 deletions(-) create mode 100644 src/MauiDevFlow.CLI/SkillHostPaths.cs create mode 100644 tests/MauiDevFlow.Tests/SkillHostPathsTests.cs diff --git a/AGENTS.md b/AGENTS.md index 4f56dcb..1ea47d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ MauiDevFlow is a toolkit for AI-assisted .NET MAUI app development. It provides: - **Blazor CDP GTK** (`MauiDevFlow.Blazor.Gtk`) — Blazor CDP bridge for WebKitGTK on Linux - **CLI Tool** (`MauiDevFlow.CLI`) — Terminal commands for both native MAUI and Blazor automation - **Driver Library** (`MauiDevFlow.Driver`) — Platform-aware orchestration (Mac Catalyst, Android, iOS, Windows, Linux) -- **AI Skill** (`.claude/skills/maui-ai-debugging/`) — Skill files teaching AI agents the full build→deploy→inspect→fix workflow +- **AI Skill** (`maui-ai-debugging`) — Host-installable skill files teaching AI agents the full build→deploy→inspect→fix workflow ## Architecture @@ -87,12 +87,15 @@ The Driver library (`Redth.MauiDevFlow.Driver`) conditionally references `Intero ## Skill System -The `.claude/skills/maui-ai-debugging/` directory contains AI skill files: +The canonical skill source lives in `.claude/skills/maui-ai-debugging/` and is installable for +multiple hosts: - `SKILL.md` — Main skill document with full command reference and workflows - `references/setup.md` — Detailed setup guide - `references/*.md` — Platform-specific guides -The CLI command `maui-devflow update-skill` downloads the latest skill files from GitHub. When updating skill docs, keep `SKILL.md` as the authoritative command reference. +The CLI command `maui-devflow update-skill --host ` downloads the latest skill files +from GitHub into the host-specific install path (`.claude/...` for Claude, `.agents/...` for +Codex). When updating skill docs, keep `SKILL.md` as the authoritative command reference. ## Logging Architecture diff --git a/README.md b/README.md index 87df7e2..458c453 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ manually check the simulator. - **Broker Daemon** — Automatic port assignment and agent discovery for simultaneous multi-app debugging - **CLI Tool** (`maui-devflow`) — Scriptable commands for both native and Blazor automation - **Driver Library** — Platform-aware (Mac Catalyst, Android, iOS Simulator, Linux/GTK) orchestration -- **AI Skill** — Claude Code skill (`.claude/skills/maui-ai-debugging`) for AI-driven development workflows +- **AI Skill** — Installable skill for Claude Code or Codex, plus MCP support for other compatible hosts ## Quick Start @@ -35,11 +35,13 @@ manually check the simulator. The fastest way to get started is to let your AI agent set everything up using the included skill: -**1. Install the CLI tool and download the skill:** +**1. Install the CLI tool and download the skill for your host:** ```bash dotnet tool install --global Redth.MauiDevFlow.CLI -maui-devflow update-skill +maui-devflow update-skill --host claude +# or +maui-devflow update-skill --host codex ``` **2. Ask your AI agent to set up MauiDevFlow:** @@ -393,8 +395,8 @@ Hybrid page, connected via Shell navigation (`//native` and `//blazor` routes). ## AI Agent Integration -This project includes a Claude Code skill (`.claude/skills/maui-ai-debugging`) that teaches AI -agents the complete build → deploy → inspect → fix feedback loop. The skill covers: +This project includes an installable AI skill for Claude Code and Codex that teaches AI agents +the complete build → deploy → inspect → fix feedback loop. The skill covers: - Installing and configuring all required tools - Building and deploying to iOS simulators, Android emulators, Mac Catalyst, and Linux/GTK @@ -409,26 +411,30 @@ The CLI can download the latest skill files directly from GitHub into your proje ```bash # Interactive — shows files and asks for confirmation -maui-devflow update-skill +maui-devflow update-skill --host claude # Skip confirmation -maui-devflow update-skill -y +maui-devflow update-skill --host codex -y # Download to a specific directory -maui-devflow update-skill -o /path/to/my-project +maui-devflow update-skill --host codex -o /path/to/my-project # Use a different branch -maui-devflow update-skill -b dev +maui-devflow update-skill --host claude -b dev ``` -This downloads the skill files into `.claude/skills/maui-ai-debugging/` relative to the output -directory (or current directory if `--output` is not specified). Existing files are overwritten. -The file list is discovered dynamically from the repository, so new reference docs are picked up -automatically. +This downloads the skill files into a host-specific path relative to the output directory +(or current directory if `--output` is not specified): + +- Claude: `.claude/skills/maui-ai-debugging/` +- Codex: `.agents/skills/maui-ai-debugging/` + +Existing files are overwritten. The file list is discovered dynamically from the repository, so +new reference docs are picked up automatically. ## MCP Server -MauiDevFlow includes an MCP (Model Context Protocol) server for integration with AI coding agents in VS Code Copilot Chat, Claude Desktop, and other MCP-compatible hosts. The MCP server returns structured JSON and inline images — enabling AI agents to see screenshots directly and query the visual tree without text parsing. +MauiDevFlow includes an MCP (Model Context Protocol) server for integration with AI coding agents in VS Code Copilot Chat, Claude Desktop, Codex hosts that support MCP, and other compatible clients. The MCP server returns structured JSON and inline images — enabling AI agents to see screenshots directly and query the visual tree without text parsing. ### Configuration diff --git a/src/MauiDevFlow.CLI/Program.cs b/src/MauiDevFlow.CLI/Program.cs index 01c44cd..651647a 100644 --- a/src/MauiDevFlow.CLI/Program.cs +++ b/src/MauiDevFlow.CLI/Program.cs @@ -785,11 +785,15 @@ await SimpleGetAsync(host, port, "/api/sensors", OutputWriter.ResolveJsonMode(js ["--branch", "-b"], () => "main", "GitHub branch to download from"); + var hostOption = new Option( + "--host", + () => "claude", + "Skill host to install for (claude or codex)"); var updateSkillCmd = new Command("update-skill", "Download the latest maui-ai-debugging skill from GitHub") { - forceOption, outputDirOption, branchOption + forceOption, outputDirOption, branchOption, hostOption }; - updateSkillCmd.SetHandler(async (force, output, branch) => await UpdateSkillAsync(force, output, branch), forceOption, outputDirOption, branchOption); + updateSkillCmd.SetHandler(async (force, output, branch, host) => await UpdateSkillAsync(force, output, branch, host), forceOption, outputDirOption, branchOption, hostOption); rootCommand.Add(updateSkillCmd); // ===== skill-version command ===== @@ -802,9 +806,9 @@ await SimpleGetAsync(host, port, "/api/sensors", OutputWriter.ResolveJsonMode(js "GitHub branch to check against"); var skillVersionCmd = new Command("skill-version", "Check the installed skill version and compare with remote") { - skillVersionOutputOption, skillVersionBranchOption + skillVersionOutputOption, skillVersionBranchOption, hostOption }; - skillVersionCmd.SetHandler(async (output, branch) => await SkillVersionAsync(output, branch), skillVersionOutputOption, skillVersionBranchOption); + skillVersionCmd.SetHandler(async (output, branch, host) => await SkillVersionAsync(output, branch, host), skillVersionOutputOption, skillVersionBranchOption, hostOption); rootCommand.Add(skillVersionCmd); // ===== broker commands ===== @@ -1624,12 +1628,14 @@ private record CommandDescription(string Command, string Description, bool Mutat // ===== Update Skill Command ===== private const string SkillRepo = "Redth/MauiDevFlow"; - private const string SkillBasePath = ".claude/skills/maui-ai-debugging"; - private static async Task UpdateSkillAsync(bool force, string? outputDir, string branch) + private static async Task UpdateSkillAsync(bool force, string? outputDir, string branch, string host) { + var normalizedHost = SkillHostPaths.NormalizeHost(host); + var sourceBasePath = SkillHostPaths.GetSourceBasePath(normalizedHost); var root = outputDir ?? Directory.GetCurrentDirectory(); - var destBase = Path.Combine(root, SkillBasePath); + var installBasePath = SkillHostPaths.GetInstallBasePath(normalizedHost); + var destBase = Path.Combine(root, installBasePath); using var http = new HttpClient(); http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MauiDevFlow-CLI", "1.0")); @@ -1656,7 +1662,8 @@ private static async Task UpdateSkillAsync(bool force, string? outputDir, string Console.WriteLine(); Console.WriteLine("maui-devflow update-skill"); - Console.WriteLine($" Source: https://github.com/{SkillRepo}/tree/{branch}/{SkillBasePath}"); + Console.WriteLine($" Host: {normalizedHost}"); + Console.WriteLine($" Source: https://github.com/{SkillRepo}/tree/{branch}/{sourceBasePath}"); Console.WriteLine($" Destination: {destBase}"); Console.WriteLine(); Console.WriteLine("Files to download:"); @@ -1664,7 +1671,7 @@ private static async Task UpdateSkillAsync(bool force, string? outputDir, string { var destPath = Path.Combine(destBase, file); var exists = File.Exists(destPath); - Console.WriteLine($" {SkillBasePath}/{file}{(exists ? " (overwrite)" : " (new)")}"); + Console.WriteLine($" {installBasePath}/{file}{(exists ? " (overwrite)" : " (new)")}"); } Console.WriteLine(); @@ -1682,7 +1689,7 @@ private static async Task UpdateSkillAsync(bool force, string? outputDir, string var success = 0; foreach (var file in files) { - var url = $"https://raw.githubusercontent.com/{SkillRepo}/{branch}/{SkillBasePath}/{file}"; + var url = $"https://raw.githubusercontent.com/{SkillRepo}/{branch}/{sourceBasePath}/{file}"; var destPath = Path.Combine(destBase, file); try @@ -1705,21 +1712,22 @@ private static async Task UpdateSkillAsync(bool force, string? outputDir, string : $"Done. {success}/{files.Count} files updated."); // Write .skill-version with the latest commit SHA - await WriteSkillVersionAsync(http, destBase, branch); + await WriteSkillVersionAsync(http, destBase, branch, sourceBasePath, normalizedHost); } - private static async Task WriteSkillVersionAsync(HttpClient http, string destBase, string branch) + private static async Task WriteSkillVersionAsync(HttpClient http, string destBase, string branch, string sourceBasePath, string host) { try { - var sha = await GetRemoteSkillCommitShaAsync(http, branch); + var sha = await GetRemoteSkillCommitShaAsync(http, branch, sourceBasePath); if (sha == null) return; var versionInfo = new { commit = sha, updatedAt = DateTime.UtcNow.ToString("o"), - branch + branch, + host }; var versionPath = Path.Combine(destBase, ".skill-version"); await File.WriteAllTextAsync(versionPath, @@ -1728,9 +1736,9 @@ await File.WriteAllTextAsync(versionPath, catch { /* non-fatal — version tracking is best-effort */ } } - private static async Task GetRemoteSkillCommitShaAsync(HttpClient http, string branch) + private static async Task GetRemoteSkillCommitShaAsync(HttpClient http, string branch, string sourceBasePath) { - var url = $"https://api.github.com/repos/{SkillRepo}/commits?path={SkillBasePath}&sha={branch}&per_page=1"; + var url = $"https://api.github.com/repos/{SkillRepo}/commits?path={sourceBasePath}&sha={branch}&per_page=1"; var json = await http.GetStringAsync(url); var commits = JsonSerializer.Deserialize(json); foreach (var commit in commits.EnumerateArray()) @@ -1738,10 +1746,13 @@ await File.WriteAllTextAsync(versionPath, return null; } - private static async Task SkillVersionAsync(string? outputDir, string branch) + private static async Task SkillVersionAsync(string? outputDir, string branch, string host) { + var normalizedHost = SkillHostPaths.NormalizeHost(host); var root = outputDir ?? Directory.GetCurrentDirectory(); - var destBase = Path.Combine(root, SkillBasePath); + var installBasePath = SkillHostPaths.GetInstallBasePath(normalizedHost); + var sourceBasePath = SkillHostPaths.GetSourceBasePath(normalizedHost); + var destBase = Path.Combine(root, installBasePath); var versionPath = Path.Combine(destBase, ".skill-version"); // Read local version @@ -1764,10 +1775,11 @@ private static async Task SkillVersionAsync(string? outputDir, string branch) if (localSha == null) { Console.WriteLine("No local skill version found."); - Console.WriteLine("Run 'maui-devflow update-skill' to install the skill and track its version."); + Console.WriteLine($"Run 'maui-devflow update-skill --host {normalizedHost}' to install the skill and track its version."); return; } + Console.WriteLine($"Host: {normalizedHost}"); Console.WriteLine($"Installed: {localSha[..12]} (branch: {localBranch ?? "unknown"})"); if (localDate != null && DateTime.TryParse(localDate, out var dt)) Console.WriteLine($"Updated: {dt:yyyy-MM-dd HH:mm:ss} UTC"); @@ -1779,7 +1791,7 @@ private static async Task SkillVersionAsync(string? outputDir, string branch) try { - var remoteSha = await GetRemoteSkillCommitShaAsync(http, branch); + var remoteSha = await GetRemoteSkillCommitShaAsync(http, branch, sourceBasePath); if (remoteSha == null) { Console.WriteLine("Could not fetch remote version."); @@ -1791,7 +1803,7 @@ private static async Task SkillVersionAsync(string? outputDir, string branch) if (string.Equals(localSha, remoteSha, StringComparison.OrdinalIgnoreCase)) Console.WriteLine("\n✓ Skill is up to date."); else - Console.WriteLine("\n⚠ Update available! Run 'maui-devflow update-skill' to get the latest version."); + Console.WriteLine($"\n⚠ Update available! Run 'maui-devflow update-skill --host {normalizedHost}' to get the latest version."); } catch (Exception ex) { @@ -1802,7 +1814,7 @@ private static async Task SkillVersionAsync(string? outputDir, string branch) private static async Task> GetSkillFilesFromGitHubAsync(HttpClient http, string branch) { var files = new List(); - await ListGitHubDirectoryAsync(http, SkillBasePath, "", files, branch); + await ListGitHubDirectoryAsync(http, SkillHostPaths.CanonicalSourceBasePath, "", files, branch); return files; } diff --git a/src/MauiDevFlow.CLI/SkillHostPaths.cs b/src/MauiDevFlow.CLI/SkillHostPaths.cs new file mode 100644 index 0000000..0b4dc4a --- /dev/null +++ b/src/MauiDevFlow.CLI/SkillHostPaths.cs @@ -0,0 +1,30 @@ +namespace MauiDevFlow.CLI; + +public static class SkillHostPaths +{ + public const string CanonicalSourceBasePath = ".claude/skills/maui-ai-debugging"; + + public static string GetSourceBasePath(string? host) + { + _ = NormalizeHost(host); + return CanonicalSourceBasePath; + } + + public static string GetInstallBasePath(string? host) => NormalizeHost(host) switch + { + "claude" => ".claude/skills/maui-ai-debugging", + "codex" => ".agents/skills/maui-ai-debugging", + _ => throw new ArgumentOutOfRangeException(nameof(host), "Supported hosts: claude, codex") + }; + + public static string NormalizeHost(string? host) + { + var normalized = string.IsNullOrWhiteSpace(host) + ? "claude" + : host.Trim().ToLowerInvariant(); + + return normalized is "claude" or "codex" + ? normalized + : throw new ArgumentOutOfRangeException(nameof(host), "Supported hosts: claude, codex"); + } +} diff --git a/tests/MauiDevFlow.Tests/MauiDevFlow.Tests.csproj b/tests/MauiDevFlow.Tests/MauiDevFlow.Tests.csproj index e68aba5..276bfa8 100644 --- a/tests/MauiDevFlow.Tests/MauiDevFlow.Tests.csproj +++ b/tests/MauiDevFlow.Tests/MauiDevFlow.Tests.csproj @@ -14,6 +14,7 @@ + @@ -22,4 +23,4 @@ - \ No newline at end of file + diff --git a/tests/MauiDevFlow.Tests/SkillHostPathsTests.cs b/tests/MauiDevFlow.Tests/SkillHostPathsTests.cs new file mode 100644 index 0000000..cf9e1df --- /dev/null +++ b/tests/MauiDevFlow.Tests/SkillHostPathsTests.cs @@ -0,0 +1,37 @@ +using MauiDevFlow.CLI; + +namespace MauiDevFlow.Tests; + +public class SkillHostPathsTests +{ + [Theory] + [InlineData(null, ".claude/skills/maui-ai-debugging")] + [InlineData("", ".claude/skills/maui-ai-debugging")] + [InlineData("claude", ".claude/skills/maui-ai-debugging")] + [InlineData("CLAUDE", ".claude/skills/maui-ai-debugging")] + [InlineData("codex", ".agents/skills/maui-ai-debugging")] + [InlineData("CODEX", ".agents/skills/maui-ai-debugging")] + public void GetInstallBasePath_ReturnsExpectedPath(string? host, string expectedPath) + { + var path = SkillHostPaths.GetInstallBasePath(host); + + Assert.Equal(expectedPath, path); + } + + [Fact] + public void GetSourceBasePath_UsesClaudeSkillAsCanonicalSource() + { + var path = SkillHostPaths.GetSourceBasePath("codex"); + + Assert.Equal(".claude/skills/maui-ai-debugging", path); + } + + [Fact] + public void GetInstallBasePath_RejectsUnknownHost() + { + var ex = Assert.Throws(() => SkillHostPaths.GetInstallBasePath("copilot")); + + Assert.Contains("claude", ex.Message); + Assert.Contains("codex", ex.Message); + } +}