Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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,29 +37,25 @@ 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)
// Determine the repo root - use the provided workspace root, or infer from .claude folder location
DirectoryInfo? repoRoot = context.RepositoryRoot;
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)");
// .claude folder's parent is the repo root (override the provided workspace root)
repoRoot = claudeCodeFolder.Parent ?? context.RepositoryRoot;
_logger.LogDebug("Inferred workspace root from .claude folder parent: {RepoRoot}", repoRoot.FullName);
}

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);
_logger.LogDebug("Found .claude folder or workspace root at: {RepoRoot}", targetRepoRoot.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...");
Expand Down Expand Up @@ -110,12 +101,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 +121,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