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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Built for speed and simplicity, this tool streamlines common **messaging**, **te
- 🧩 **Supports custom message types** – Define and send serialized payloads that match your message contracts.
- 🌐 **Supported transports** – Works with **Azure Service Bus**, **RabbitMQ**, and **AWS SQS**.
- ⚙️ **Transport configuration file** – Configure and easily switch between multiple transports. Transports can run on your **local machine** or be pointed to **development**, **staging**, or **production** environments.
- 🔍 **ServiceControl integration** – Interact with [Particular ServiceControl](https://docs.particular.net/servicecontrol/) instances to search for messages, view license status, and delete/decommission endpoints — all from the terminal.
- 💻 **Cross-platform support** – Works seamlessly on Windows, macOS, and Linux.
- 📦 **Multiple installation options** – Install via **Chocolatey**, **WinGet**, **Docker**, **.NET tool (via NuGet)**, or by **downloading the binary directly from GitHub Releases**.

Expand Down
1 change: 1 addition & 0 deletions src/BuslyCLI.Console/BuslyCLI.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.7" />
<PackageReference Include="NServiceBus" Version="10.1.4" />
<PackageReference Include="NServiceBus.AmazonSQS" Version="9.0.1" />
<PackageReference Include="NServiceBus.RabbitMQ" Version="11.2.1" />
Expand Down
2 changes: 1 addition & 1 deletion src/BuslyCLI.Console/Commands/Demo/StartCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class StartDemoCommand(IAnsiConsole console, IRawEndpointFactory rawEndpo
protected override async Task<int> ExecuteAsync(CommandContext context, CurrentTransportSettings settings, CancellationToken cancellationToken)
{
console.WriteLine($"Starting demo endpoint named {Constants.DemoDefaultOriginatingEndpoint} for quick start guide...");
var config = await nServiceBusConfiguration.GetValidatedConfigurationAsync(settings.Config.Path);
var config = await nServiceBusConfiguration.GetTransportValidatedConfigurationAsync(settings.Config.Path);
var rawEndpoint = await rawEndpointFactory.CreateRawEndpoint(Constants.DemoDefaultOriginatingEndpoint, config.CurrentTransportConfig);

await rawEndpoint.StartEndpoint();
Expand Down
2 changes: 1 addition & 1 deletion src/BuslyCLI.Console/Commands/NsbCommand/SendCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class SendCommand(IAnsiConsole console, IRawEndpointFactory rawEndpointFa
{
protected override async Task<int> ExecuteAsync(CommandContext context, SendCommandSettings settings, CancellationToken cancellationToken)
{
var config = await nServiceBusConfiguration.GetValidatedConfigurationAsync(settings.Config.Path);
var config = await nServiceBusConfiguration.GetTransportValidatedConfigurationAsync(settings.Config.Path);

// TODO: Validate body is valid json/xml
var headers = new Dictionary<string, string>
Expand Down
2 changes: 1 addition & 1 deletion src/BuslyCLI.Console/Commands/NsbEvent/PublishCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class PublishCommand(IAnsiConsole console, IRawEndpointFactory rawEndpoin
{
protected override async Task<int> ExecuteAsync(CommandContext context, PublishCommandSettings settings, CancellationToken cancellationToken)
{
var config = await nServiceBusConfiguration.GetValidatedConfigurationAsync(settings.Config.Path);
var config = await nServiceBusConfiguration.GetTransportValidatedConfigurationAsync(settings.Config.Path);

// TODO: Validate body is valid json/xml
var headers = new Dictionary<string, string>
Expand Down
2 changes: 1 addition & 1 deletion src/BuslyCLI.Console/Commands/NsbTimeout/SendTimeout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class SendTimeout(IAnsiConsole console, IRawEndpointFactory rawEndpointFa

protected override async Task<int> ExecuteAsync(CommandContext context, SendTimeoutCommandSettings settings, CancellationToken cancellationToken)
{
var config = await nServiceBusConfiguration.GetValidatedConfigurationAsync(settings.Config.Path);
var config = await nServiceBusConfiguration.GetTransportValidatedConfigurationAsync(settings.Config.Path);

if (UnsupportedTransportTypes.Contains(config.CurrentTransportConfig.Config.GetType()))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using BuslyCLI.Config;
using BuslyCLI.Infrastructure;
using Spectre.Console;
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Endpoint;

public class DeleteEndpointCommand(IAnsiConsole console, INServiceBusConfiguration nservicebusConfiguration, ServiceControlClient serviceControlClient)
: AsyncCommand<DeleteEndpointSettings>
{
protected override async Task<int> ExecuteAsync(CommandContext context, DeleteEndpointSettings settings, CancellationToken cancellationToken)
{
var config = await nservicebusConfiguration.GetServiceControlValidatedConfigurationAsync(settings.Config.Path);

if (AnsiConsole.Confirm("Are you sure you want to delete this endpoint? This action cannot be undone and all data associated with this endpoint will be lost."))
{
var wasDeleted = await serviceControlClient.DeleteEndpointAsync(config.CurrentServiceControlInstanceConfig.Url, settings.Id, cancellationToken);
if (!wasDeleted)
{
console.WriteLine($"Endpoint with id {settings.Id} did not exist at {config.CurrentServiceControlInstanceConfig.Name}.");
return 1;
}
console.WriteLine($"Deleted endpoint with id {settings.Id} from {config.CurrentServiceControlInstanceConfig.Name}.");
}

return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Endpoint;

public class DeleteEndpointSettings : GlobalCommandSettings
{
[CommandArgument(0, "<id>")]
public Guid Id { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using BuslyCLI.Config;
using BuslyCLI.Infrastructure;
using Spectre.Console;
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Endpoint;

public class ListEndpointsCommand(IAnsiConsole console, INServiceBusConfiguration nservicebusConfiguration, ServiceControlClient serviceControlClient)
: AsyncCommand<ListEndpointsSettings>
{
protected override async Task<int> ExecuteAsync(CommandContext context, ListEndpointsSettings settings, CancellationToken cancellationToken)
{
var config = await nservicebusConfiguration.GetServiceControlValidatedConfigurationAsync(settings.Config.Path);

var endpoints = await serviceControlClient.GetEndpointsAsync(config.CurrentServiceControlInstanceConfig.Url, cancellationToken);

var table = new Table();
table.AddColumn("Id");
table.AddColumn("Name");
table.AddColumn("Host Display Name");
table.AddColumn("Sending Heartbeats");
table.AddColumn("Heartbeat Status");
table.AddColumn("Last Reported At");

foreach (var endpoint in endpoints.OrderByDescending(x => x.Name))
{
table.AddRow(
endpoint.Id,
endpoint.Name,
endpoint.HostDisplayName,
endpoint.IsSendingHeartbeats ? "Yes" : "No",
endpoint.HeartbeatInformation.ReportedStatus == "dead" ? "[red]Dead[/]" : "[green]Alive[/]",
endpoint.HeartbeatInformation.LastReportAt.ToString("u"));
}

console.Write(table);

return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace BuslyCLI.Commands.ServiceControl.Endpoint;

public class ListEndpointsSettings : GlobalCommandSettings
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using BuslyCLI.Config;
using BuslyCLI.Infrastructure;
using Spectre.Console;
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Event;

public class ListEventsCommand(IAnsiConsole console, INServiceBusConfiguration nservicebusConfiguration, ServiceControlClient serviceControlClient)
: AsyncCommand<ListEventsSettings>
{
protected override async Task<int> ExecuteAsync(CommandContext context, ListEventsSettings settings, CancellationToken cancellationToken)
{
var config = await nservicebusConfiguration.GetServiceControlValidatedConfigurationAsync(settings.Config.Path);

var eventLogItems = await serviceControlClient.GetEventLogItemsAsync(config.CurrentServiceControlInstanceConfig.Url, page: settings.PageNumber, perPage: settings.PageSize, cancellationToken);

var table = new Table();
table.AddColumn("Occured At");
table.AddColumn("Severity");
table.AddColumn("Description");

foreach (var eventLogItem in eventLogItems.OrderByDescending(x => x.RaisedAt))
{
table.AddRow(
eventLogItem.RaisedAt.ToString("u"),
eventLogItem.Severity == "error" ? "[red]Error[/]" : "Info",
eventLogItem.Description);
}

console.Write(table);

return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.ComponentModel;
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Event;

public class ListEventsSettings : GlobalCommandSettings
{
[CommandOption("--page-size <page-size>")]
[DefaultValue(50)]
[Description("The page size to use when searching events")]
public int PageSize { get; set; }

[CommandOption("--page-number <page-number>")]
[DefaultValue(1)]
[Description("The page number to use when searching events")]
public int PageNumber { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using BuslyCLI.Config;
using Spectre.Console;
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Instance;

public class CurrentServiceControlInstanceCommand(IAnsiConsole console, INServiceBusConfiguration nservicebusConfiguration)
: AsyncCommand<CurrentServiceControlInstanceSettings>
{
protected override async Task<int> ExecuteAsync(CommandContext context, CurrentServiceControlInstanceSettings settings, CancellationToken cancellationToken)
{
var config = await nservicebusConfiguration.GetUnValidatedConfigurationAsync(settings.Config.Path);

console.WriteLine(config?.CurrentServiceControlInstance is not null
? config.CurrentServiceControlInstance
: "Current service control instance is not set.");

return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace BuslyCLI.Commands.ServiceControl.Instance;

public class CurrentServiceControlInstanceSettings : GlobalCommandSettings
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using BuslyCLI.Config;
using Spectre.Console;
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Instance;

public class DeleteServiceControlInstanceCommand(IAnsiConsole console, INServiceBusConfiguration nservicebusConfiguration)
: AsyncCommand<DeleteServiceControlInstanceSettings>
{
protected override async Task<int> ExecuteAsync(CommandContext context, DeleteServiceControlInstanceSettings settings, CancellationToken cancellationToken)
{
var config = await nservicebusConfiguration.GetUnValidatedConfigurationAsync(settings.Config.Path);
var targetInstance = settings.InstanceName.ToLower();

if (config?.ServiceControlInstances != null &&
config.ServiceControlInstances.Select(x => x.Name.ToLower()).Contains(targetInstance))
{
if (config.CurrentServiceControlInstance?.ToLower() == targetInstance)
{
await nservicebusConfiguration.UpdateCurrentServiceControlInstanceAsync(settings.Config.Path, "");
console.WriteLine("This removed your active service control instance, use \"busly servicecontrol instance set\" to select a different one.");
}

await nservicebusConfiguration.RemoveServiceControlInstanceAsync(settings.Config.Path, targetInstance);
console.WriteLine($"Deleted service control instance named {targetInstance} from {settings.Config.Path}");
}
else
{
console.WriteLine($"Cannot delete service control instance {settings.InstanceName} since it doesn't exist in the config file.");
}

return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Instance;

public class DeleteServiceControlInstanceSettings : GlobalCommandSettings
{
[CommandArgument(0, "<name>")]
public string InstanceName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using BuslyCLI.Config;
using Spectre.Console;
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Instance;

public class ListServiceControlInstancesCommand(IAnsiConsole console, INServiceBusConfiguration nservicebusConfiguration)
: AsyncCommand<ListServiceControlInstancesSettings>
{
protected override async Task<int> ExecuteAsync(CommandContext context, ListServiceControlInstancesSettings settings, CancellationToken cancellationToken)
{
var grid = new Grid();
// Add columns
grid.AddColumn();
grid.AddColumn();
grid.AddColumn();
// Add header row
grid.AddRow("CURRENT", "NAME", "URL");

var config = await nservicebusConfiguration.GetUnValidatedConfigurationAsync(settings.Config.Path);

if (config is { ServiceControlInstances: not null })
{
foreach (var instance in config.ServiceControlInstances)
{
grid.AddRow(
config.CurrentServiceControlInstance == instance.Name ? "*" : "",
instance.Name,
instance.Url);
}
}

console.Write(grid);
return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace BuslyCLI.Commands.ServiceControl.Instance;

public class ListServiceControlInstancesSettings : GlobalCommandSettings
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using BuslyCLI.Config;
using Spectre.Console;
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Instance;

public class SetServiceControlInstanceCommand(IAnsiConsole console, INServiceBusConfiguration nservicebusConfiguration)
: AsyncCommand<SetServiceControlInstanceSettings>
{
protected override async Task<int> ExecuteAsync(CommandContext context, SetServiceControlInstanceSettings settings, CancellationToken cancellationToken)
{
var config = await nservicebusConfiguration.GetUnValidatedConfigurationAsync(settings.Config.Path);
var targetInstance = settings.InstanceName.ToLower();

if (config?.ServiceControlInstances != null &&
config.ServiceControlInstances.Select(x => x.Name.ToLower()).Contains(targetInstance))
{
await nservicebusConfiguration.UpdateCurrentServiceControlInstanceAsync(settings.Config.Path, targetInstance);
console.WriteLine($"Switched to service control instance \"{targetInstance}\".");
}
else
{
console.WriteLine($"No service control instance exists with the name {targetInstance}.");
}

return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Spectre.Console.Cli;

namespace BuslyCLI.Commands.ServiceControl.Instance;

public class SetServiceControlInstanceSettings : GlobalCommandSettings
{
[CommandArgument(0, "<name>")]
public string InstanceName { get; set; }
}
Loading
Loading