Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <claude|codex>` 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

Expand Down
34 changes: 20 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,21 @@ 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

### AI-Assisted Setup (Recommended)

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:**
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
56 changes: 34 additions & 22 deletions src/MauiDevFlow.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(
"--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 =====
Expand All @@ -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 =====
Expand Down Expand Up @@ -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"));
Expand All @@ -1656,15 +1662,16 @@ 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:");
foreach (var file in files)
{
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();

Expand All @@ -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
Expand All @@ -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,
Expand All @@ -1728,20 +1736,23 @@ await File.WriteAllTextAsync(versionPath,
catch { /* non-fatal — version tracking is best-effort */ }
}

private static async Task<string?> GetRemoteSkillCommitShaAsync(HttpClient http, string branch)
private static async Task<string?> 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<JsonElement>(json);
foreach (var commit in commits.EnumerateArray())
return commit.GetProperty("sha").GetString();
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
Expand All @@ -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");
Expand All @@ -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.");
Expand All @@ -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)
{
Expand All @@ -1802,7 +1814,7 @@ private static async Task SkillVersionAsync(string? outputDir, string branch)
private static async Task<List<string>> GetSkillFilesFromGitHubAsync(HttpClient http, string branch)
{
var files = new List<string>();
await ListGitHubDirectoryAsync(http, SkillBasePath, "", files, branch);
await ListGitHubDirectoryAsync(http, SkillHostPaths.CanonicalSourceBasePath, "", files, branch);
return files;
}

Expand Down
30 changes: 30 additions & 0 deletions src/MauiDevFlow.CLI/SkillHostPaths.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
3 changes: 2 additions & 1 deletion tests/MauiDevFlow.Tests/MauiDevFlow.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\MauiDevFlow.Agent.Core\MauiDevFlow.Agent.Core.csproj" />
<ProjectReference Include="..\..\src\MauiDevFlow.CLI\MauiDevFlow.CLI.csproj" />
<ProjectReference Include="..\..\src\MauiDevFlow.Driver\MauiDevFlow.Driver.csproj" />
<ProjectReference Include="..\..\src\MauiDevFlow.Logging\MauiDevFlow.Logging.csproj" />
</ItemGroup>
Expand All @@ -22,4 +23,4 @@
<Using Include="Xunit" />
</ItemGroup>

</Project>
</Project>
37 changes: 37 additions & 0 deletions tests/MauiDevFlow.Tests/SkillHostPathsTests.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentOutOfRangeException>(() => SkillHostPaths.GetInstallBasePath("copilot"));

Assert.Contains("claude", ex.Message);
Assert.Contains("codex", ex.Message);
}
}