Skip to content

Add aspire config commands for managing configuration settings #9676

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* Do not emit "Act", "Arrange" or "Assert" comments.
* We do not use any mocking framework at the moment.
* Copy existing style in nearby files for test method names and capitalization.
* Do not use Directory.SetCurrentDirectory in tests as it can cause side effects when tests execute concurrently.

## Running tests

Expand Down
239 changes: 239 additions & 0 deletions src/Aspire.Cli/Commands/ConfigCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using Aspire.Cli.Configuration;
using Aspire.Cli.Interaction;
using Microsoft.Extensions.Configuration;

namespace Aspire.Cli.Commands;

internal sealed class ConfigCommand : BaseCommand
{
private readonly IConfiguration _configuration;
private readonly IConfigurationService _configurationService;
private readonly IInteractionService _interactionService;

public ConfigCommand(IConfiguration configuration, IConfigurationService configurationService, IInteractionService interactionService)
: base("config", "Manage configuration settings.")
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(configurationService);
ArgumentNullException.ThrowIfNull(interactionService);

_configuration = configuration;
_configurationService = configurationService;
_interactionService = interactionService;

var getCommand = new GetCommand(_configuration, _interactionService);
var setCommand = new SetCommand(configurationService, _interactionService);
var listCommand = new ListCommand(configurationService, _interactionService);
var deleteCommand = new DeleteCommand(configurationService, _interactionService);

Subcommands.Add(getCommand);
Subcommands.Add(setCommand);
Subcommands.Add(listCommand);
Subcommands.Add(deleteCommand);
}

protected override Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
// When no subcommand is provided, the system will automatically show help and return 0
return Task.FromResult(0);
}

private sealed class GetCommand : BaseCommand
{
private readonly IConfiguration _configuration;
private readonly IInteractionService _interactionService;

public GetCommand(IConfiguration configuration, IInteractionService interactionService)
: base("get", "Get a configuration value.")
{
_configuration = configuration;
_interactionService = interactionService;

var keyArgument = new Argument<string>("key")
{
Description = "The configuration key to retrieve."
};
Arguments.Add(keyArgument);
}

protected override Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var key = parseResult.GetValue<string>("key");
if (key is null)
{
_interactionService.DisplayError("Configuration key is required.");
return Task.FromResult(1);
}

var value = _configuration[key];

if (value is not null)
{
Console.WriteLine(value);
return Task.FromResult(0);
}
else
{
_interactionService.DisplayError($"Configuration key '{key}' not found.");
return Task.FromResult(1);
}
}
}

private sealed class SetCommand : BaseCommand
{
private readonly IConfigurationService _configurationService;
private readonly IInteractionService _interactionService;

public SetCommand(IConfigurationService configurationService, IInteractionService interactionService)
: base("set", "Set a configuration value.")
{
_configurationService = configurationService;
_interactionService = interactionService;

var keyArgument = new Argument<string>("key")
{
Description = "The configuration key to set."
};
Arguments.Add(keyArgument);

var valueArgument = new Argument<string>("value")
{
Description = "The configuration value to set."
};
Arguments.Add(valueArgument);

var globalOption = new Option<bool>("--global", "-g")
{
Description = "Set the configuration value globally in $HOME/.aspire/settings.json instead of the local settings file."
};
Options.Add(globalOption);
}

protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var key = parseResult.GetValue<string>("key");
var value = parseResult.GetValue<string>("value");
var isGlobal = parseResult.GetValue<bool>("--global");

if (key is null)
{
_interactionService.DisplayError("Configuration key is required.");
return 1;
}

if (value is null)
{
_interactionService.DisplayError("Configuration value is required.");
return 1;
}

try
{
await _configurationService.SetConfigurationAsync(key, value, isGlobal, cancellationToken);
var scope = isGlobal ? "globally" : "locally";
_interactionService.DisplaySuccess($"Configuration '{key}' set to '{value}' {scope}.");
return 0;
}
catch (Exception ex)
{
_interactionService.DisplayError($"Error setting configuration: {ex.Message}");
return 1;
}
}
}

private sealed class ListCommand : BaseCommand
{
private readonly IConfigurationService _configurationService;
private readonly IInteractionService _interactionService;

public ListCommand(IConfigurationService configurationService, IInteractionService interactionService)
: base("list", "List all configuration values.")
{
_configurationService = configurationService;
_interactionService = interactionService;
}

protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var allConfig = await _configurationService.GetAllConfigurationAsync(cancellationToken);

if (allConfig.Count == 0)
{
_interactionService.DisplayMessage("ℹ️", "No configuration values found.");
return ExitCodeConstants.Success;
}

foreach (var kvp in allConfig)
{
Console.WriteLine($"{kvp.Key}={kvp.Value}");
}

return ExitCodeConstants.Success;
}
}

private sealed class DeleteCommand : BaseCommand
{
private readonly IConfigurationService _configurationService;
private readonly IInteractionService _interactionService;

public DeleteCommand(IConfigurationService configurationService, IInteractionService interactionService)
: base("delete", "Delete a configuration value.")
{
_configurationService = configurationService;
_interactionService = interactionService;

var keyArgument = new Argument<string>("key")
{
Description = "The configuration key to delete."
};
Arguments.Add(keyArgument);

var globalOption = new Option<bool>("--global", "-g")
{
Description = "Delete the configuration value from the global $HOME/.aspire/settings.json instead of the local settings file."
};
Options.Add(globalOption);
}

protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var key = parseResult.GetValue<string>("key");
var isGlobal = parseResult.GetValue<bool>("--global");

if (key is null)
{
_interactionService.DisplayError("Configuration key is required.");
return 1;
}

try
{
var deleted = await _configurationService.DeleteConfigurationAsync(key, isGlobal, cancellationToken);

if (deleted)
{
var scope = isGlobal ? "globally" : "locally";
_interactionService.DisplaySuccess($"Configuration '{key}' deleted {scope}.");
return 0;
}
else
{
_interactionService.DisplayError($"Configuration key '{key}' not found.");
return 1;
}
}
catch (Exception ex)
{
_interactionService.DisplayError($"Error deleting configuration: {ex.Message}");
return 1;
}
}
}
}
4 changes: 3 additions & 1 deletion src/Aspire.Cli/Commands/RootCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ internal sealed class RootCommand : BaseRootCommand
{
private readonly IInteractionService _interactionService;

public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addCommand, PublishCommand publishCommand, IInteractionService interactionService)
public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addCommand, PublishCommand publishCommand, ConfigCommand configCommand, IInteractionService interactionService)
: base("The Aspire CLI can be used to create, run, and publish Aspire-based applications.")
{
ArgumentNullException.ThrowIfNull(newCommand);
ArgumentNullException.ThrowIfNull(runCommand);
ArgumentNullException.ThrowIfNull(addCommand);
ArgumentNullException.ThrowIfNull(publishCommand);
ArgumentNullException.ThrowIfNull(configCommand);
ArgumentNullException.ThrowIfNull(interactionService);

_interactionService = interactionService;
Expand Down Expand Up @@ -70,5 +71,6 @@ public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addC
Subcommands.Add(runCommand);
Subcommands.Add(addCommand);
Subcommands.Add(publishCommand);
Subcommands.Add(configCommand);
}
}
Loading
Loading