Skip to content
Merged
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
5 changes: 3 additions & 2 deletions src/Aspire.Cli/Agents/AgentEnvironmentDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ namespace Aspire.Cli.Agents;
internal sealed class AgentEnvironmentDetector(IEnumerable<IAgentEnvironmentScanner> scanners) : IAgentEnvironmentDetector
{
/// <inheritdoc />
public async Task<AgentEnvironmentApplicator[]> DetectAsync(DirectoryInfo workingDirectory, CancellationToken cancellationToken)
public async Task<AgentEnvironmentApplicator[]> DetectAsync(DirectoryInfo workingDirectory, DirectoryInfo repositoryRoot, CancellationToken cancellationToken)
{
var context = new AgentEnvironmentScanContext
{
WorkingDirectory = workingDirectory
WorkingDirectory = workingDirectory,
RepositoryRoot = repositoryRoot
};

foreach (var scanner in scanners)
Expand Down
7 changes: 7 additions & 0 deletions src/Aspire.Cli/Agents/AgentEnvironmentScanContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ internal sealed class AgentEnvironmentScanContext
/// </summary>
public required DirectoryInfo WorkingDirectory { get; init; }

/// <summary>
/// Gets the root directory of the repository/workspace.
/// This is typically the git repository root if available, otherwise the working directory.
/// Scanners should use this as the boundary for searches instead of searching up the directory tree.
/// </summary>
public required DirectoryInfo RepositoryRoot { get; init; }

/// <summary>
/// Adds an applicator to the collection of detected agent environments.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Cli.Git;
using Aspire.Cli.Resources;
using Microsoft.Extensions.Logging;

Expand All @@ -18,22 +17,18 @@ internal sealed class ClaudeCodeAgentEnvironmentScanner : IAgentEnvironmentScann
private const string McpConfigFileName = ".mcp.json";
private const string AspireServerName = "aspire";

private readonly IGitRepository _gitRepository;
private readonly IClaudeCodeCliRunner _claudeCodeCliRunner;
private readonly ILogger<ClaudeCodeAgentEnvironmentScanner> _logger;

/// <summary>
/// Initializes a new instance of <see cref="ClaudeCodeAgentEnvironmentScanner"/>.
/// </summary>
/// <param name="gitRepository">The Git repository service for finding repository boundaries.</param>
/// <param name="claudeCodeCliRunner">The Claude Code CLI runner for checking if Claude Code is installed.</param>
/// <param name="logger">The logger for diagnostic output.</param>
public ClaudeCodeAgentEnvironmentScanner(IGitRepository gitRepository, IClaudeCodeCliRunner claudeCodeCliRunner, ILogger<ClaudeCodeAgentEnvironmentScanner> logger)
public ClaudeCodeAgentEnvironmentScanner(IClaudeCodeCliRunner claudeCodeCliRunner, ILogger<ClaudeCodeAgentEnvironmentScanner> logger)
{
ArgumentNullException.ThrowIfNull(gitRepository);
ArgumentNullException.ThrowIfNull(claudeCodeCliRunner);
ArgumentNullException.ThrowIfNull(logger);
_gitRepository = gitRepository;
_claudeCodeCliRunner = claudeCodeCliRunner;
_logger = logger;
}
Expand All @@ -42,64 +37,52 @@ public ClaudeCodeAgentEnvironmentScanner(IGitRepository gitRepository, IClaudeCo
public async Task ScanAsync(AgentEnvironmentScanContext context, CancellationToken cancellationToken)
{
_logger.LogDebug("Starting Claude Code environment scan in directory: {WorkingDirectory}", context.WorkingDirectory.FullName);

// Get the git root to use as a boundary for searching
_logger.LogDebug("Finding git repository root...");
var gitRoot = await _gitRepository.GetRootAsync(cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Git root: {GitRoot}", gitRoot?.FullName ?? "(none)");
_logger.LogDebug("Workspace root: {RepositoryRoot}", context.RepositoryRoot.FullName);

// Find the .claude folder to determine if Claude Code is being used in this project
_logger.LogDebug("Searching for .claude folder...");
var claudeCodeFolder = FindClaudeCodeFolder(context.WorkingDirectory, gitRoot);
var claudeCodeFolder = FindClaudeCodeFolder(context.WorkingDirectory, context.RepositoryRoot);

// Determine the repo root - use git root, or infer from .claude folder location, or fall back to working directory
DirectoryInfo? repoRoot = gitRoot;
if (repoRoot is null && claudeCodeFolder is not null)
if (claudeCodeFolder is not null)
{
// .claude folder's parent is the repo root
repoRoot = claudeCodeFolder.Parent;
_logger.LogDebug("Inferred repo root from .claude folder parent: {RepoRoot}", repoRoot?.FullName ?? "(none)");
}

if (claudeCodeFolder is not null || repoRoot is not null)
{
var targetRepoRoot = repoRoot ?? context.WorkingDirectory;
_logger.LogDebug("Found .claude folder or repo root at: {RepoRoot}", targetRepoRoot.FullName);
// If .claude folder is found, override the workspace root with its parent directory
var workspaceRoot = claudeCodeFolder.Parent ?? context.RepositoryRoot;
_logger.LogDebug("Inferred workspace root from .claude folder parent: {WorkspaceRoot}", workspaceRoot.FullName);

// Check if the aspire server is already configured in .mcp.json
_logger.LogDebug("Checking if Aspire MCP server is already configured in .mcp.json...");
if (HasAspireServerConfigured(targetRepoRoot))
if (HasAspireServerConfigured(workspaceRoot))
{
_logger.LogDebug("Aspire MCP server is already configured - skipping");
// Already configured, no need to offer an applicator
return;
}

// Found a .claude folder or git repo - add an applicator to configure MCP
_logger.LogDebug("Adding Claude Code applicator for .mcp.json at: {RepoRoot}", targetRepoRoot.FullName);
context.AddApplicator(CreateApplicator(targetRepoRoot));
// Found a .claude folder - add an applicator to configure MCP
_logger.LogDebug("Adding Claude Code applicator for .mcp.json at: {WorkspaceRoot}", workspaceRoot.FullName);
context.AddApplicator(CreateApplicator(workspaceRoot));
}
else
{
// No .claude folder or git repo found - check if Claude Code CLI is installed
_logger.LogDebug("No .claude folder or git repo found, checking for Claude Code CLI installation...");
// No .claude folder found - check if Claude Code CLI is installed
_logger.LogDebug("No .claude folder found, checking for Claude Code CLI installation...");
var claudeCodeVersion = await _claudeCodeCliRunner.GetVersionAsync(cancellationToken).ConfigureAwait(false);

if (claudeCodeVersion is not null)
{
_logger.LogDebug("Found Claude Code CLI version: {Version}", claudeCodeVersion);

// Check if the aspire server is already configured in .mcp.json
if (HasAspireServerConfigured(context.WorkingDirectory))
if (HasAspireServerConfigured(context.RepositoryRoot))
{
_logger.LogDebug("Aspire MCP server is already configured - skipping");
// Already configured, no need to offer an applicator
return;
}

// Claude Code is installed - offer to create config at working directory
_logger.LogDebug("Adding Claude Code applicator for .mcp.json at working directory: {WorkingDirectory}", context.WorkingDirectory.FullName);
context.AddApplicator(CreateApplicator(context.WorkingDirectory));
// Claude Code is installed - offer to create config at workspace root
_logger.LogDebug("Adding Claude Code applicator for .mcp.json at workspace root: {WorkspaceRoot}", context.RepositoryRoot.FullName);
context.AddApplicator(CreateApplicator(context.RepositoryRoot));
}
else
{
Expand All @@ -110,12 +93,12 @@ public async Task ScanAsync(AgentEnvironmentScanContext context, CancellationTok

/// <summary>
/// Walks up the directory tree to find a .claude folder.
/// Stops if we go above the git root (if provided).
/// Stops if we go above the workspace root.
/// Ignores the .claude folder in the user's home directory.
/// </summary>
/// <param name="startDirectory">The directory to start searching from.</param>
/// <param name="gitRoot">The git repository root, or null if not in a git repository.</param>
private static DirectoryInfo? FindClaudeCodeFolder(DirectoryInfo startDirectory, DirectoryInfo? gitRoot)
/// <param name="repositoryRoot">The workspace root to use as the boundary for searches.</param>
private static DirectoryInfo? FindClaudeCodeFolder(DirectoryInfo startDirectory, DirectoryInfo repositoryRoot)
{
var currentDirectory = startDirectory;
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
Expand All @@ -130,9 +113,9 @@ public async Task ScanAsync(AgentEnvironmentScanContext context, CancellationTok
return new DirectoryInfo(claudeCodePath);
}

// Stop if we've reached the git root without finding .claude
// (don't search above the repository boundary)
if (gitRoot is not null && string.Equals(currentDirectory.FullName, gitRoot.FullName, StringComparison.OrdinalIgnoreCase))
// Stop if we've reached the workspace root without finding .claude
// (don't search above the workspace boundary)
if (string.Equals(currentDirectory.FullName, repositoryRoot.FullName, StringComparison.OrdinalIgnoreCase))
{
return null;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Cli/Agents/IAgentEnvironmentDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ internal interface IAgentEnvironmentDetector
/// Detects available agent environments by running all registered scanners.
/// </summary>
/// <param name="workingDirectory">The working directory to scan.</param>
/// <param name="repositoryRoot">The root directory of the repository/workspace. Scanners use this as the boundary for searches.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An array of applicators for detected agent environments.</returns>
Task<AgentEnvironmentApplicator[]> DetectAsync(DirectoryInfo workingDirectory, CancellationToken cancellationToken);
Task<AgentEnvironmentApplicator[]> DetectAsync(DirectoryInfo workingDirectory, DirectoryInfo repositoryRoot, CancellationToken cancellationToken);
}
19 changes: 5 additions & 14 deletions src/Aspire.Cli/Agents/OpenCode/OpenCodeAgentEnvironmentScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Cli.Git;
using Aspire.Cli.Resources;
using Microsoft.Extensions.Logging;

Expand All @@ -17,22 +16,18 @@ internal sealed class OpenCodeAgentEnvironmentScanner : IAgentEnvironmentScanner
private const string OpenCodeConfigFileName = "opencode.jsonc";
private const string AspireServerName = "aspire";

private readonly IGitRepository _gitRepository;
private readonly IOpenCodeCliRunner _openCodeCliRunner;
private readonly ILogger<OpenCodeAgentEnvironmentScanner> _logger;

/// <summary>
/// Initializes a new instance of <see cref="OpenCodeAgentEnvironmentScanner"/>.
/// </summary>
/// <param name="gitRepository">The Git repository service for finding repository boundaries.</param>
/// <param name="openCodeCliRunner">The OpenCode CLI runner for checking if OpenCode is installed.</param>
/// <param name="logger">The logger for diagnostic output.</param>
public OpenCodeAgentEnvironmentScanner(IGitRepository gitRepository, IOpenCodeCliRunner openCodeCliRunner, ILogger<OpenCodeAgentEnvironmentScanner> logger)
public OpenCodeAgentEnvironmentScanner(IOpenCodeCliRunner openCodeCliRunner, ILogger<OpenCodeAgentEnvironmentScanner> logger)
{
ArgumentNullException.ThrowIfNull(gitRepository);
ArgumentNullException.ThrowIfNull(openCodeCliRunner);
ArgumentNullException.ThrowIfNull(logger);
_gitRepository = gitRepository;
_openCodeCliRunner = openCodeCliRunner;
_logger = logger;
}
Expand All @@ -41,14 +36,10 @@ public OpenCodeAgentEnvironmentScanner(IGitRepository gitRepository, IOpenCodeCl
public async Task ScanAsync(AgentEnvironmentScanContext context, CancellationToken cancellationToken)
{
_logger.LogDebug("Starting OpenCode environment scan in directory: {WorkingDirectory}", context.WorkingDirectory.FullName);

// Get the git root - OpenCode config should be at the repo root
_logger.LogDebug("Finding git repository root...");
var gitRoot = await _gitRepository.GetRootAsync(cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Git root: {GitRoot}", gitRoot?.FullName ?? "(none)");

// Look for existing opencode.jsonc file at git root or working directory
var configDirectory = gitRoot ?? context.WorkingDirectory;
_logger.LogDebug("Workspace root: {RepositoryRoot}", context.RepositoryRoot.FullName);

// Look for existing opencode.jsonc file at workspace root
var configDirectory = context.RepositoryRoot;
var configFilePath = Path.Combine(configDirectory.FullName, OpenCodeConfigFileName);
var configFileExists = File.Exists(configFilePath);

Expand Down
32 changes: 11 additions & 21 deletions src/Aspire.Cli/Agents/VsCode/VsCodeAgentEnvironmentScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Cli.Git;
using Aspire.Cli.Resources;
using Microsoft.Extensions.Logging;

Expand All @@ -19,22 +18,18 @@ internal sealed class VsCodeAgentEnvironmentScanner : IAgentEnvironmentScanner
private const string VsCodeEnvironmentVariablePrefix = "VSCODE_";
private const string AspireServerName = "aspire";

private readonly IGitRepository _gitRepository;
private readonly IVsCodeCliRunner _vsCodeCliRunner;
private readonly ILogger<VsCodeAgentEnvironmentScanner> _logger;

/// <summary>
/// Initializes a new instance of <see cref="VsCodeAgentEnvironmentScanner"/>.
/// </summary>
/// <param name="gitRepository">The Git repository service for finding repository boundaries.</param>
/// <param name="vsCodeCliRunner">The VS Code CLI runner for checking if VS Code is installed.</param>
/// <param name="logger">The logger for diagnostic output.</param>
public VsCodeAgentEnvironmentScanner(IGitRepository gitRepository, IVsCodeCliRunner vsCodeCliRunner, ILogger<VsCodeAgentEnvironmentScanner> logger)
public VsCodeAgentEnvironmentScanner(IVsCodeCliRunner vsCodeCliRunner, ILogger<VsCodeAgentEnvironmentScanner> logger)
{
ArgumentNullException.ThrowIfNull(gitRepository);
ArgumentNullException.ThrowIfNull(vsCodeCliRunner);
ArgumentNullException.ThrowIfNull(logger);
_gitRepository = gitRepository;
_vsCodeCliRunner = vsCodeCliRunner;
_logger = logger;
}
Expand All @@ -43,14 +38,10 @@ public VsCodeAgentEnvironmentScanner(IGitRepository gitRepository, IVsCodeCliRun
public async Task ScanAsync(AgentEnvironmentScanContext context, CancellationToken cancellationToken)
{
_logger.LogDebug("Starting VS Code environment scan in directory: {WorkingDirectory}", context.WorkingDirectory.FullName);

// Get the git root to use as a boundary for searching
_logger.LogDebug("Finding git repository root...");
var gitRoot = await _gitRepository.GetRootAsync(cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Git root: {GitRoot}", gitRoot?.FullName ?? "(none)");
_logger.LogDebug("Workspace root: {RepositoryRoot}", context.RepositoryRoot.FullName);

_logger.LogDebug("Searching for .vscode folder...");
var vsCodeFolder = FindVsCodeFolder(context.WorkingDirectory, gitRoot);
var vsCodeFolder = FindVsCodeFolder(context.WorkingDirectory, context.RepositoryRoot);

if (vsCodeFolder is not null)
{
Expand All @@ -72,9 +63,8 @@ public async Task ScanAsync(AgentEnvironmentScanContext context, CancellationTok
{
_logger.LogDebug("No .vscode folder found, but VS Code is available on the system");
// No .vscode folder found, but VS Code is available
// Use git root if available, otherwise fall back to current working directory
var targetDirectory = gitRoot ?? context.WorkingDirectory;
var targetVsCodeFolder = new DirectoryInfo(Path.Combine(targetDirectory.FullName, VsCodeFolderName));
// Use workspace root for new .vscode folder
var targetVsCodeFolder = new DirectoryInfo(Path.Combine(context.RepositoryRoot.FullName, VsCodeFolderName));
_logger.LogDebug("Adding VS Code applicator for new .vscode folder at: {VsCodeFolder}", targetVsCodeFolder.FullName);
context.AddApplicator(CreateApplicator(targetVsCodeFolder));
}
Expand Down Expand Up @@ -125,12 +115,12 @@ private async Task<bool> IsVsCodeAvailableAsync(CancellationToken cancellationTo

/// <summary>
/// Walks up the directory tree to find a .vscode folder.
/// Stops if we go above the git root (if provided).
/// Stops if we go above the workspace root.
/// Ignores the .vscode folder in the user's home directory (used for user settings, not workspace config).
/// </summary>
/// <param name="startDirectory">The directory to start searching from.</param>
/// <param name="gitRoot">The git repository root, or null if not in a git repository.</param>
private static DirectoryInfo? FindVsCodeFolder(DirectoryInfo startDirectory, DirectoryInfo? gitRoot)
/// <param name="repositoryRoot">The workspace root to use as the boundary for searches.</param>
private static DirectoryInfo? FindVsCodeFolder(DirectoryInfo startDirectory, DirectoryInfo repositoryRoot)
{
var currentDirectory = startDirectory;
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
Expand All @@ -145,9 +135,9 @@ private async Task<bool> IsVsCodeAvailableAsync(CancellationToken cancellationTo
return new DirectoryInfo(vsCodePath);
}

// Stop if we've reached the git root without finding .vscode
// (don't search above the repository boundary)
if (gitRoot is not null && string.Equals(currentDirectory.FullName, gitRoot.FullName, StringComparison.OrdinalIgnoreCase))
// Stop if we've reached the workspace root without finding .vscode
// (don't search above the workspace boundary)
if (string.Equals(currentDirectory.FullName, repositoryRoot.FullName, StringComparison.OrdinalIgnoreCase))
{
return null;
}
Expand Down
6 changes: 4 additions & 2 deletions src/Aspire.Cli/Commands/McpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Aspire.Cli.Agents;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Configuration;
using Aspire.Cli.Git;
using Aspire.Cli.Interaction;
using Aspire.Cli.Resources;
using Aspire.Cli.Utils;
Expand All @@ -23,15 +24,16 @@ public McpCommand(
IAuxiliaryBackchannelMonitor auxiliaryBackchannelMonitor,
ILoggerFactory loggerFactory,
ILogger<McpStartCommand> logger,
IAgentEnvironmentDetector agentEnvironmentDetector)
IAgentEnvironmentDetector agentEnvironmentDetector,
IGitRepository gitRepository)
: base("mcp", McpCommandStrings.Description, features, updateNotifier, executionContext, interactionService)
{
ArgumentNullException.ThrowIfNull(interactionService);

var startCommand = new McpStartCommand(interactionService, features, updateNotifier, executionContext, auxiliaryBackchannelMonitor, loggerFactory, logger);
Subcommands.Add(startCommand);

var initCommand = new McpInitCommand(interactionService, features, updateNotifier, executionContext, agentEnvironmentDetector);
var initCommand = new McpInitCommand(interactionService, features, updateNotifier, executionContext, agentEnvironmentDetector, gitRepository);
Subcommands.Add(initCommand);
}

Expand Down
Loading
Loading