Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
05de5b2
Add `maui ai` command group for AI-assisted development bootstrapping
jfversluis Apr 17, 2026
81732cf
fix: Address review security, UX, and spec compliancefindings
jfversluis Apr 17, 2026
070fd24
fix: Address round 2 cancellation, converter, error reportingreview
jfversluis Apr 17, 2026
e26d415
fix: Address PR review tree caching, dedup, error handlingfeedback
jfversluis Apr 17, 2026
c5d0549
fix: Final indentation, return codes, test coveragepolish
jfversluis Apr 17, 2026
63cd080
fix: Dry-run before confirmation, remove dead code, fix comment
jfversluis Apr 17, 2026
5cdd66b
fix: Path normalization and YAML block scalar parsing
jfversluis Apr 17, 2026
c9f496f
fix: Windows path use OrdinalIgnoreCase for filesystem pathscompatib…
jfversluis Apr 17, 2026
fc21b52
test: Add 12 filesystem integration tests for maui ai
jfversluis Apr 17, 2026
b3f158d
fix: Address second PR review error propagation, status accuracyround
jfversluis Apr 17, 2026
64a50b9
Merge main and update MAUI AI bootstrap
jfversluis May 20, 2026
522f581
Extend MAUI AI status and update coverage
jfversluis May 21, 2026
25bb579
Address MAUI AI bootstrap review feedback
jfversluis May 21, 2026
d6144c3
Address follow-up MAUI AI review feedback
jfversluis May 21, 2026
97c66a9
Address additional MAUI AI review feedback
jfversluis May 21, 2026
44d193a
Harden MAUI AI bootstrap review fixes
jfversluis May 21, 2026
124ffb6
Address MAUI AI expert review findings
jfversluis May 21, 2026
fe2e0f4
Fix MAUI AI review regressions
jfversluis May 21, 2026
7a1936a
Harden MAUI AI asset writes
jfversluis May 21, 2026
921a3a3
Tighten MAUI AI path and agent matching
jfversluis May 21, 2026
e8d38a3
Resolve MAUI AI review edge cases
jfversluis May 21, 2026
2c68c43
Preserve MAUI AI agent asset paths
jfversluis May 21, 2026
87d1e40
Harden MAUI AI update review fixes
jfversluis May 21, 2026
a5d425e
Tighten MAUI AI filesystem writes
jfversluis May 21, 2026
e76be62
Stabilize MAUI AI asset update edge cases
jfversluis May 21, 2026
cb471ba
Harden MAUI AI local asset handling
jfversluis May 21, 2026
4046de7
Harden MAUI AI status and metadata writes
jfversluis May 21, 2026
90b1943
Address MAUI AI review hardening
jfversluis May 21, 2026
bcaaed3
Address MAUI AI status review feedback
jfversluis May 21, 2026
5e6ffed
Tighten MAUI AI skill path scanning
jfversluis May 21, 2026
8dfdbc7
Harden MAUI AI parsing and temp writes
jfversluis May 21, 2026
9ef22de
Track MAUI AI bootstrap failures
jfversluis May 21, 2026
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
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ A command-line tool for .NET MAUI development environment setup, device manageme
- **Apple platform management** (`maui apple`) — Xcode, simulator, and runtime management (macOS)
- **Device listing** (`maui device list`) across all connected platforms
- **DevFlow app automation** (`maui devflow`) — visual tree inspection, element interaction, screenshots, WebView/CDP automation, network monitoring, profiling, storage access, real-time log/sensor streaming, and MCP server for AI agents
- **AI-powered development bootstrap** (`maui ai init`) — install MAUI Copilot skills, DevFlow skills, Copilot agents, and MCP configuration for the current project
- **MAUI Go** (`maui go`) — create, serve, and upgrade single-file Comet Go projects for rapid prototyping
- **Version info** (`maui version`)
- **Global options** — `--json` for CI pipelines, `--verbose`, `--dry-run`, `--ci`
Expand Down Expand Up @@ -142,14 +143,27 @@ Built artifacts are exposed as `@(MauiAppArtifact)` items with `ArtifactType`, `

## Agent Skills

This repository is also a marketplace for distributable agent skills for .NET MAUI development. Skills are organized as plugins compatible with Copilot CLI, Claude Code, and VS Code.
This repository is also a marketplace for distributable agent skills for .NET MAUI development. The recommended one-stop setup is `maui ai init`, which installs the relevant MAUI skills, bundled DevFlow skills, Copilot agent definitions, and MCP configuration for detected agent environments.

| Plugin | Description |
|--------|-------------|
| [`dotnet-maui`](plugins/dotnet-maui/) | MAUI development: DevFlow automation, profiling, accessibility, platform bindings, diagnostics, session review |

```bash
# Install via Copilot CLI
# Preview what will be installed and configured
maui ai init --dry-run

# Bootstrap this project for AI-powered MAUI development
maui ai init

# Check and refresh installed AI development assets
maui ai status --check-updates
maui ai update
```

Direct plugin installation remains available for agent runtimes that support plugin marketplaces:

```bash
/plugin marketplace add dotnet/maui-labs
/plugin install dotnet-maui@dotnet-maui-labs
```
Expand Down
249 changes: 249 additions & 0 deletions src/Cli/Microsoft.Maui.Cli.UnitTests/AgentEnvironmentDetectorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Maui.Cli.Ai;
using Microsoft.Maui.Cli.Ai.Models;
using Xunit;

namespace Microsoft.Maui.Cli.UnitTests;

public class AgentEnvironmentDetectorTests : IDisposable
{
private readonly string _tempDir;

public AgentEnvironmentDetectorTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(_tempDir);
}

public void Dispose()
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, recursive: true);
}

[Fact]
public void Detect_ClaudeDirectoryExists_DetectsClaudeEnvironment()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".claude"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);

Assert.Contains(environments, e => e.Kind == AgentEnvironmentKind.Claude);
}

[Fact]
public void Detect_VsCodeDirectoryExists_DetectsVsCodeEnvironment()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".vscode"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);

Assert.Contains(environments, e => e.Kind == AgentEnvironmentKind.VsCode);
}

[Fact]
public void Detect_OpenCodeDirectoryExists_DetectsOpenCodeEnvironment()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".opencode"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);

Assert.Contains(environments, e => e.Kind == AgentEnvironmentKind.OpenCode);
}

[Fact]
public void Detect_NoAgentDirectories_ReturnsEmptyOrOnlyCopilotCli()
{
// No .claude, .vscode, or .opencode directories
var environments = AgentEnvironmentDetector.Detect(_tempDir);

// Only CopilotCli might be detected (if ~/.copilot exists on the machine)
Assert.DoesNotContain(environments, e => e.Kind == AgentEnvironmentKind.Claude);
Assert.DoesNotContain(environments, e => e.Kind == AgentEnvironmentKind.VsCode);
Assert.DoesNotContain(environments, e => e.Kind == AgentEnvironmentKind.OpenCode);
}

[Fact]
public void Detect_MultipleEnvironments_DetectsAll()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".claude"));
Directory.CreateDirectory(Path.Combine(_tempDir, ".vscode"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);

Assert.Contains(environments, e => e.Kind == AgentEnvironmentKind.Claude);
Assert.Contains(environments, e => e.Kind == AgentEnvironmentKind.VsCode);
}

[Fact]
public void Detect_Claude_SkillsDirectoryIsCorrect()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".claude"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);
var claude = Assert.Single(environments, e => e.Kind == AgentEnvironmentKind.Claude);

var expected = Path.Combine(_tempDir, ".claude", "skills");
Assert.Equal(expected, claude.SkillsDirectory);
}

[Fact]
public void Detect_Claude_McpConfigPathIsCorrect()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".claude"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);
var claude = Assert.Single(environments, e => e.Kind == AgentEnvironmentKind.Claude);

var expected = Path.Combine(_tempDir, ".claude", "mcp.json");
Assert.Equal(expected, claude.McpConfigPath);
}

[Fact]
public void Detect_VsCode_SkillsDirectoryUsesGitHubPath()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".vscode"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);
var vscode = Assert.Single(environments, e => e.Kind == AgentEnvironmentKind.VsCode);

var expected = Path.Combine(_tempDir, ".github", "skills");
Assert.Equal(expected, vscode.SkillsDirectory);
}

[Fact]
public void Detect_VsCode_McpConfigPathIsCorrect()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".vscode"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);
var vscode = Assert.Single(environments, e => e.Kind == AgentEnvironmentKind.VsCode);

var expected = Path.Combine(_tempDir, ".vscode", "mcp.json");
Assert.Equal(expected, vscode.McpConfigPath);
}

[Fact]
public void Detect_OpenCode_SkillsDirectoryIsCorrect()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".opencode"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);
var opencode = Assert.Single(environments, e => e.Kind == AgentEnvironmentKind.OpenCode);

var expected = Path.Combine(_tempDir, ".opencode", "skills");
Assert.Equal(expected, opencode.SkillsDirectory);
}

[Fact]
public void Detect_OpenCode_McpConfigPathIsCorrect()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".opencode"));

var environments = AgentEnvironmentDetector.Detect(_tempDir);
var opencode = Assert.Single(environments, e => e.Kind == AgentEnvironmentKind.OpenCode);

var expected = Path.Combine(_tempDir, ".opencode", "config.json");
Assert.Equal(expected, opencode.McpConfigPath);
}

[Fact]
public void Detect_McpConfigExists_ReflectsRealFilePresence()
{
var claudeDir = Path.Combine(_tempDir, ".claude");
Directory.CreateDirectory(claudeDir);

// Before creating mcp.json
var envBefore = AgentEnvironmentDetector.Detect(_tempDir);
var claudeBefore = Assert.Single(envBefore, e => e.Kind == AgentEnvironmentKind.Claude);
Assert.False(claudeBefore.McpConfigExists);

// After creating mcp.json
File.WriteAllText(Path.Combine(claudeDir, "mcp.json"), "{}");
var envAfter = AgentEnvironmentDetector.Detect(_tempDir);
var claudeAfter = Assert.Single(envAfter, e => e.Kind == AgentEnvironmentKind.Claude);
Assert.True(claudeAfter.McpConfigExists);
}

[Fact]
public void Detect_StopsAtGitRoot()
{
// Create a git root with .claude at the root level
Directory.CreateDirectory(Path.Combine(_tempDir, ".git"));
Directory.CreateDirectory(Path.Combine(_tempDir, ".claude"));

// Create a subdirectory to scan from
var subDir = Path.Combine(_tempDir, "src", "project");
Directory.CreateDirectory(subDir);

var environments = AgentEnvironmentDetector.Detect(subDir);

// Should still find .claude at the git root
Assert.Contains(environments, e => e.Kind == AgentEnvironmentKind.Claude);
}

[Fact]
public void Detect_StopsAtGitRootFileForWorktree()
{
File.WriteAllText(Path.Combine(_tempDir, ".git"), "gitdir: ../repo/.git/worktrees/project");
Directory.CreateDirectory(Path.Combine(_tempDir, ".vscode"));

var subDir = Path.Combine(_tempDir, "src", "project");
Directory.CreateDirectory(subDir);

var environments = AgentEnvironmentDetector.Detect(subDir);
var vscode = Assert.Single(environments, e => e.Kind == AgentEnvironmentKind.VsCode);

Assert.Equal(Path.Combine(_tempDir, ".github", "skills"), vscode.SkillsDirectory);
}

[Fact]
public void ResolveProjectRoot_ReturnsGitRootFromSubdirectory()
{
File.WriteAllText(Path.Combine(_tempDir, ".git"), "gitdir: ../repo/.git/worktrees/project");
var subDir = Path.Combine(_tempDir, "src", "project");
Directory.CreateDirectory(subDir);

var projectRoot = AgentEnvironmentDetector.ResolveProjectRoot(subDir);

Assert.Equal(_tempDir, projectRoot);
}

[Fact]
public void Detect_NoGitRoot_DoesNotScanAncestorDirectories()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".vscode"));
var subDir = Path.Combine(_tempDir, "src", "project");
Directory.CreateDirectory(subDir);

var environments = AgentEnvironmentDetector.Detect(subDir);

Assert.DoesNotContain(environments, e => e.Kind == AgentEnvironmentKind.VsCode);
}

[Fact]
public void GetCopilotCliEnvironment_EmptyUserHome_ReturnsNull()
{
Directory.CreateDirectory(Path.Combine(_tempDir, ".copilot"));

var environment = AgentEnvironmentDetector.GetCopilotCliEnvironment(string.Empty, _tempDir);

Assert.Null(environment);
}

[Fact]
public void GetCopilotCliEnvironment_ValidUserHome_ReturnsCopilotCliEnvironment()
{
var userHome = Path.Combine(_tempDir, "home");
Directory.CreateDirectory(Path.Combine(userHome, ".copilot"));

var environment = AgentEnvironmentDetector.GetCopilotCliEnvironment(userHome, _tempDir);

Assert.NotNull(environment);
Assert.Equal(AgentEnvironmentKind.CopilotCli, environment.Kind);
Assert.Equal(Path.Combine(_tempDir, ".github", "skills"), environment.SkillsDirectory);
Assert.Equal(Path.Combine(userHome, ".copilot", "mcp.json"), environment.McpConfigPath);
}
}
Loading
Loading