diff --git a/Documentation/arc/commands.md b/Documentation/arc/commands.md new file mode 100644 index 0000000..b4caa89 --- /dev/null +++ b/Documentation/arc/commands.md @@ -0,0 +1,44 @@ +# arc commands list + +Lists all registered command endpoints in the connected Arc application. + +## Usage + +```bash +cratis arc commands list [--url ] [-o ] +``` + +## Options + +| Option | Description | +|---|---| +| `--url ` | Base URL of the Arc application (default: `https://localhost:5001`). | +| `-o, --output ` | Output format: `table`, `plain`, `json`, `json-compact`. | + +## Output + +Each row contains: + +| Column | Description | +|---|---| +| Name | The simple type name of the command (e.g. `OpenDebitAccount`). | +| Namespace | The dot-separated namespace path of the command. | +| Route | The HTTP POST route registered for this command (e.g. `/api/accounts/open-debit-account`). | +| Summary | The XML documentation summary of the command type, if available. | + +## Examples + +```bash +# List commands using the default URL +cratis arc commands list + +# List commands from a specific URL +cratis arc commands list --url https://myapp.local:5001 + +# Output as compact JSON +cratis arc commands list -o json-compact +``` + +## Notes + +The introspection endpoint `GET /.cratis/commands` must be registered in the Arc application. This happens automatically when the application calls `MapIntrospectionEndpoints()` during startup. diff --git a/Documentation/arc/index.md b/Documentation/arc/index.md new file mode 100644 index 0000000..1f44fa1 --- /dev/null +++ b/Documentation/arc/index.md @@ -0,0 +1,36 @@ +# Arc + +The `cratis arc` command group provides commands for introspecting a running Cratis Arc application. It connects to the Arc application's built-in introspection HTTP endpoints to discover registered commands and queries. + +## Connection Flags + +All `cratis arc` commands accept the following connection flag: + +| Flag | Description | +|---|---| +| `--url ` | Base URL of the Arc application. Overrides the `ARC_URL` environment variable. Defaults to `https://localhost:5001`. | + +## Connection Resolution Order + +The CLI resolves the Arc application URL in this order: + +1. `--url` flag +2. `ARC_URL` environment variable +3. Default: `https://localhost:5001` + +## Sub-Commands + +| Sub-command | Description | +|---|---| +| `commands` | List registered command endpoints in the Arc application. | +| `queries` | List registered query endpoints in the Arc application. | + +## Global Flags + +All `cratis arc` commands also inherit the top-level global flags: + +| Flag | Description | +|---|---| +| `-o, --output ` | Output format: `table`, `plain`, `json`, `json-compact`. | +| `-q, --quiet` | Output only key identifiers, one per line. | +| `--debug` | Print debug information to stderr. | diff --git a/Documentation/arc/queries.md b/Documentation/arc/queries.md new file mode 100644 index 0000000..f22b5d5 --- /dev/null +++ b/Documentation/arc/queries.md @@ -0,0 +1,45 @@ +# arc queries list + +Lists all registered query endpoints in the connected Arc application. + +## Usage + +```bash +cratis arc queries list [--url ] [-o ] +``` + +## Options + +| Option | Description | +|---|---| +| `--url ` | Base URL of the Arc application (default: `https://localhost:5001`). | +| `-o, --output ` | Output format: `table`, `plain`, `json`, `json-compact`. | + +## Output + +Each row contains: + +| Column | Description | +|---|---| +| Name | The simple name of the query method (e.g. `AllAccounts`). | +| Namespace | The dot-separated namespace path of the query. | +| Route | The HTTP GET route registered for this query (e.g. `/api/accounts/all-accounts`). | +| Type | The runtime type name of the query (e.g. `ClientObservable`, `IEnumerable`). | +| Summary | The XML documentation summary of the query type, if available. | + +## Examples + +```bash +# List queries using the default URL +cratis arc queries list + +# List queries from a specific URL +cratis arc queries list --url https://myapp.local:5001 + +# Output as compact JSON +cratis arc queries list -o json-compact +``` + +## Notes + +The introspection endpoint `GET /.cratis/queries` must be registered in the Arc application. This happens automatically when the application calls `MapIntrospectionEndpoints()` during startup. diff --git a/Documentation/arc/toc.yml b/Documentation/arc/toc.yml new file mode 100644 index 0000000..0c50516 --- /dev/null +++ b/Documentation/arc/toc.yml @@ -0,0 +1,6 @@ +- name: Arc + href: index.md +- name: Commands + href: commands.md +- name: Queries + href: queries.md diff --git a/Documentation/toc.yml b/Documentation/toc.yml index 5bd7fc7..5fa1911 100644 --- a/Documentation/toc.yml +++ b/Documentation/toc.yml @@ -4,5 +4,7 @@ href: context/toc.yml - name: Chronicle href: chronicle/toc.yml +- name: Arc + href: arc/toc.yml - name: Reference href: reference/toc.yml diff --git a/Source/Cli/Commands/Arc/ArcCommand.cs b/Source/Cli/Commands/Arc/ArcCommand.cs new file mode 100644 index 0000000..229613a --- /dev/null +++ b/Source/Cli/Commands/Arc/ArcCommand.cs @@ -0,0 +1,61 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Net.Sockets; + +namespace Cratis.Cli.Commands.Arc; + +/// +/// Base class for all CLI commands that interact with a Cratis Arc application over HTTP. +/// +/// The settings type for this command. +public abstract class ArcCommand : AsyncCommand + where TSettings : ArcSettings +{ + /// + protected sealed override async Task ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken) + { + var format = settings.ResolveOutputFormat(); + + try + { + using var handler = CreateHttpHandler(); + using var httpClient = new HttpClient(handler); + return await ExecuteCommandAsync(httpClient, settings, format, cancellationToken); + } + catch (HttpRequestException ex) when (ex.InnerException is SocketException || ex.StatusCode is null) + { + OutputFormatter.WriteError(format, ArcDefaults.CannotConnectMessage, BuildConnectionHint(settings), ExitCodes.ConnectionErrorCode); + return ExitCodes.ConnectionError; + } + catch (HttpRequestException ex) + { + OutputFormatter.WriteError(format, $"HTTP error: {ex.Message}", BuildConnectionHint(settings), ExitCodes.ServerErrorCode); + return ExitCodes.ServerError; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + OutputFormatter.WriteError(format, ex.Message, errorCode: ExitCodes.ServerErrorCode); + return ExitCodes.ServerError; + } + } + + /// + /// Executes the command logic using the provided . + /// + /// The HTTP client configured to reach the Arc application. + /// The command settings. + /// The resolved output format. + /// Cancellation token. + /// The exit code. + protected abstract Task ExecuteCommandAsync(HttpClient httpClient, TSettings settings, string format, CancellationToken cancellationToken); + +#pragma warning disable MA0039 // TLS validation is intentionally skipped for local development Arc applications + static HttpClientHandler CreateHttpHandler() => + new() { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }; +#pragma warning restore MA0039 + + static string BuildConnectionHint(ArcSettings settings) => + $"Verify the Arc application is running at {settings.ResolveUrl()}\n" + + "Use --url to specify a different URL, or set the ARC_URL environment variable."; +} diff --git a/Source/Cli/Commands/Arc/ArcDefaults.cs b/Source/Cli/Commands/Arc/ArcDefaults.cs new file mode 100644 index 0000000..b577b63 --- /dev/null +++ b/Source/Cli/Commands/Arc/ArcDefaults.cs @@ -0,0 +1,25 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.Commands.Arc; + +/// +/// Default values for Arc CLI commands. +/// +public static class ArcDefaults +{ + /// + /// The default URL for an Arc application. + /// + public const string DefaultUrl = "https://localhost:5001"; + + /// + /// Environment variable name for the Arc application URL. + /// + public const string UrlEnvVar = "ARC_URL"; + + /// + /// Standard error message when the CLI cannot reach the Arc application. + /// + public const string CannotConnectMessage = "Cannot connect to Arc application"; +} diff --git a/Source/Cli/Commands/Arc/ArcSettings.cs b/Source/Cli/Commands/Arc/ArcSettings.cs new file mode 100644 index 0000000..5d0f108 --- /dev/null +++ b/Source/Cli/Commands/Arc/ArcSettings.cs @@ -0,0 +1,39 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.Commands.Arc; + +/// +/// Settings shared by all commands that connect to a Cratis Arc application. +/// +public class ArcSettings : GlobalSettings +{ + /// + /// Gets or sets the base URL of the Arc application. + /// + [CommandOption("--url ")] + [Description("Base URL of the Arc application (e.g. https://localhost:5001)")] + public string? Url { get; set; } + + /// + /// Resolves the effective base URL by checking the flag, environment variable, then default. + /// + /// The resolved base URL string. +#pragma warning disable CA1055 // URI string return type — string is intentional here for consistency with connection helpers + public string ResolveUrl() +#pragma warning restore CA1055 + { + if (!string.IsNullOrWhiteSpace(Url)) + { + return Url.TrimEnd('/'); + } + + var envVar = Environment.GetEnvironmentVariable(ArcDefaults.UrlEnvVar); + if (!string.IsNullOrWhiteSpace(envVar)) + { + return envVar.TrimEnd('/'); + } + + return ArcDefaults.DefaultUrl; + } +} diff --git a/Source/Cli/Commands/Arc/CommandIntrospectionMetadata.cs b/Source/Cli/Commands/Arc/CommandIntrospectionMetadata.cs new file mode 100644 index 0000000..f5a9205 --- /dev/null +++ b/Source/Cli/Commands/Arc/CommandIntrospectionMetadata.cs @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.Commands.Arc; + +/// +/// Introspection metadata for a registered command endpoint in the Arc application. +/// +/// The simple name of the command type. +/// The namespace path of the command. +/// The HTTP route registered for this command. +/// The fully qualified type name of the command. +/// The XML documentation summary of the command type. +public record CommandIntrospectionMetadata(string Name, string Namespace, string Route, string Type, string DocumentationSummary); diff --git a/Source/Cli/Commands/Arc/Commands/ListCommandsCommand.cs b/Source/Cli/Commands/Arc/Commands/ListCommandsCommand.cs new file mode 100644 index 0000000..0353ef3 --- /dev/null +++ b/Source/Cli/Commands/Arc/Commands/ListCommandsCommand.cs @@ -0,0 +1,46 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.Commands.Arc.Commands; + +/// +/// Lists all registered command endpoints in the connected Arc application. +/// +[LlmDescription("Lists all registered command endpoints in the Arc application. Returns name, namespace, HTTP route, type, and documentation summary for each command.")] +[CliCommand("list", "List registered command endpoints", Branch = typeof(ArcBranch.Commands))] +[CliExample("arc", "commands", "list")] +[CliExample("arc", "commands", "list", "--url", "https://localhost:5001")] +[CliExample("arc", "commands", "list", "-o", "json")] +[LlmOutputAdvice("plain", "plain is significantly smaller than JSON for large command lists.")] +public class ListCommandsCommand : ArcCommand +{ + /// + protected override async Task ExecuteCommandAsync(HttpClient httpClient, ArcSettings settings, string format, CancellationToken cancellationToken) + { + var url = settings.ResolveUrl(); + var response = await httpClient.GetAsync($"{url}/.cratis/commands", cancellationToken); + + if (!response.IsSuccessStatusCode) + { + OutputFormatter.WriteError(format, $"Arc application returned {(int)response.StatusCode}", errorCode: ExitCodes.ServerErrorCode); + return ExitCodes.ServerError; + } + + var json = await response.Content.ReadAsStringAsync(cancellationToken); + var commands = JsonSerializer.Deserialize>(json, OutputFormatter.JsonSerializerOptions) ?? []; + + OutputFormatter.Write( + format, + commands, + ["Name", "Namespace", "Route", "Summary"], + cmd => + [ + cmd.Name, + cmd.Namespace, + cmd.Route, + cmd.DocumentationSummary + ]); + + return ExitCodes.Success; + } +} diff --git a/Source/Cli/Commands/Arc/Queries/ListQueriesCommand.cs b/Source/Cli/Commands/Arc/Queries/ListQueriesCommand.cs new file mode 100644 index 0000000..fac7747 --- /dev/null +++ b/Source/Cli/Commands/Arc/Queries/ListQueriesCommand.cs @@ -0,0 +1,47 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.Commands.Arc.Queries; + +/// +/// Lists all registered query endpoints in the connected Arc application. +/// +[LlmDescription("Lists all registered query endpoints in the Arc application. Returns name, namespace, HTTP route, fully qualified type, and documentation summary for each query.")] +[CliCommand("list", "List registered query endpoints", Branch = typeof(ArcBranch.Queries))] +[CliExample("arc", "queries", "list")] +[CliExample("arc", "queries", "list", "--url", "https://localhost:5001")] +[CliExample("arc", "queries", "list", "-o", "json")] +[LlmOutputAdvice("plain", "plain is significantly smaller than JSON for large query lists.")] +public class ListQueriesCommand : ArcCommand +{ + /// + protected override async Task ExecuteCommandAsync(HttpClient httpClient, ArcSettings settings, string format, CancellationToken cancellationToken) + { + var url = settings.ResolveUrl(); + var response = await httpClient.GetAsync($"{url}/.cratis/queries", cancellationToken); + + if (!response.IsSuccessStatusCode) + { + OutputFormatter.WriteError(format, $"Arc application returned {(int)response.StatusCode}", errorCode: ExitCodes.ServerErrorCode); + return ExitCodes.ServerError; + } + + var json = await response.Content.ReadAsStringAsync(cancellationToken); + var queries = JsonSerializer.Deserialize>(json, OutputFormatter.JsonSerializerOptions) ?? []; + + OutputFormatter.Write( + format, + queries, + ["Name", "Namespace", "Route", "Type", "Summary"], + q => + [ + q.Name, + q.Namespace, + q.Route, + q.Type, + q.DocumentationSummary + ]); + + return ExitCodes.Success; + } +} diff --git a/Source/Cli/Commands/Arc/QueryIntrospectionMetadata.cs b/Source/Cli/Commands/Arc/QueryIntrospectionMetadata.cs new file mode 100644 index 0000000..35b5d04 --- /dev/null +++ b/Source/Cli/Commands/Arc/QueryIntrospectionMetadata.cs @@ -0,0 +1,15 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.Commands.Arc; + +/// +/// Introspection metadata for a registered query endpoint in the Arc application. +/// +/// The simple name of the query. +/// The namespace path of the query. +/// The HTTP route registered for this query. +/// The fully qualified type name of the query. +/// The runtime type name of the query. +/// The XML documentation summary of the query type. +public record QueryIntrospectionMetadata(string Name, string Namespace, string Route, string FullyQualifiedName, string Type, string DocumentationSummary); diff --git a/Source/Cli/Registration/ArcBranch.cs b/Source/Cli/Registration/ArcBranch.cs new file mode 100644 index 0000000..9038721 --- /dev/null +++ b/Source/Cli/Registration/ArcBranch.cs @@ -0,0 +1,21 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable RCS1251, SA1502, CA1034 // Marker types: intentionally empty and nested for branch hierarchy + +namespace Cratis.Cli.Registration; + +/// +/// Arc application commands branch. Contains sub-branches for commands and queries. +/// +[CliBranch("arc", "Commands for interacting with a Cratis Arc application. Contains sub-branches for introspecting registered commands and queries.")] +public static class ArcBranch +{ + /// Command introspection. + [CliBranch("commands", "Introspect registered command endpoints in the Arc application. List all commands with their routes, namespaces, and documentation.")] + public static class Commands { } + + /// Query introspection. + [CliBranch("queries", "Introspect registered query endpoints in the Arc application. List all queries with their routes, namespaces, and documentation.")] + public static class Queries { } +}