From c4d3f287ac28a550d5e7dc30f36767f8730d0db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Mon, 11 May 2026 13:35:58 +0200 Subject: [PATCH 1/3] Show usage hint when the shell starts disconnected or `connect` is run with no args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #81: a first-time user launching `cosmosdbshell` lands at the prompt with no indication that authentication is required or how to perform it. The legacy MS Learn doc says "it prompts you for authentication" but in fact it does not — the user has to type `connect `, which is not discoverable from the welcome banner. Two surgical changes: 1. ShellInterpreter.RunAsync now prints a one-line yellow hint after the ready banner when the shell starts in DisconnectedState, telling the user to run `connect ` (or `help connect`). 2. ConnectCommand.PrintConnectionInfoAsync now follows the existing "Not connected" line with a small usage block listing the connect examples taken from the command's CosmosExample metadata, plus a pointer to `help connect` for the full option list. Reusing ExamplesWithDescriptions keeps a single source of truth shared with the help system. Adds three tests under ConnectCommandTests: - The new ftl strings resolve to non-empty values. - The connect command exposes example variants beyond the bare `connect` no-arg form so the hint helper has something to print. - The hint helper runs against a fresh ShellInterpreter without throwing. No command-surface changes; file-redirected and MCP output are unaffected since the hint is written via AnsiConsole only when stdin is interactive. --- .../CommandTests/ConnectCommandTests.cs | 38 +++++++++++++++++++ .../ConnectCommand.cs | 32 ++++++++++++++++ .../ShellInterpreter.cs | 9 +++++ CosmosDBShell/lang/en.ftl | 3 ++ 4 files changed, 82 insertions(+) diff --git a/CosmosDBShell.Tests/CommandTests/ConnectCommandTests.cs b/CosmosDBShell.Tests/CommandTests/ConnectCommandTests.cs index db40cef..5a113ca 100644 --- a/CosmosDBShell.Tests/CommandTests/ConnectCommandTests.cs +++ b/CosmosDBShell.Tests/CommandTests/ConnectCommandTests.cs @@ -8,6 +8,7 @@ namespace CosmosShell.Tests.CommandTests; using Azure.Data.Cosmos.Shell.Core; using Azure.Data.Cosmos.Shell.Lsp.Semantics; using Azure.Data.Cosmos.Shell.Parser; +using Azure.Data.Cosmos.Shell.Util; using Microsoft.Azure.Cosmos; public class ConnectCommandTests @@ -78,4 +79,41 @@ private static async Task BindConnectCommandAsync(string command var command = await statement.CreateCommandAsync(factory, shell, new CommandState(), CancellationToken.None); return Assert.IsType(command); } + + [Fact] + public void ConnectCommand_NotConnectedUsageHint_LocalizationKeysAreDefined() + { + // Issue #81: running `connect` while disconnected used to print only + // "Not connected" with no hint about how to authenticate. The hint + // strings must resolve to non-empty values. + Assert.False(string.IsNullOrWhiteSpace(MessageService.GetString("command-connect-not_connected-usage-header"))); + Assert.False(string.IsNullOrWhiteSpace(MessageService.GetString("command-connect-not_connected-usage-footer"))); + Assert.False(string.IsNullOrWhiteSpace(MessageService.GetString("shell-not_connected_hint"))); + } + + [Fact] + public void ConnectCommand_PrintConnectUsageHint_HasExamplesToPrint() + { + // The hint helper iterates the connect command's CosmosExample metadata and + // skips the bare `connect` no-arg form. Confirm there is at least one other + // example to display so the helper output is meaningful. + Assert.True(CommandFactory.TryCreateFactory(typeof(ConnectCommand), out var factory)); + + var examples = factory.ExamplesWithDescriptions + .Where(e => !string.IsNullOrWhiteSpace(e.Example) && e.Example != "connect") + .ToList(); + + Assert.NotEmpty(examples); + } + + [Fact] + public void ConnectCommand_PrintConnectUsageHint_RunsWithoutThrowing() + { + using var shell = ShellInterpreter.CreateInstance(); + + // Smoke test: must not throw even when the shell's command map exposes the + // factory through ShellInterpreter.App.Commands. + var ex = Record.Exception(() => ConnectCommand.PrintConnectUsageHint(shell)); + Assert.Null(ex); + } } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs index 692eb62..cd4287c 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs @@ -135,11 +135,43 @@ internal static void AskForRBacPermissions(string principalId, string permission ShellInterpreter.WriteLine(MessageService.GetArgsString("command-connect-rbac-error", "id", principalId, "permission", permission)); } + /// + /// Prints a short usage block with examples taken from the + /// metadata on this command. Shown when the user runs connect without arguments while + /// disconnected so the available authentication options are discoverable without having to know + /// about help connect (see issue #81). + /// + internal static void PrintConnectUsageHint(ShellInterpreter shell) + { + AnsiConsole.MarkupLine(MessageService.GetString("command-connect-not_connected-usage-header")); + + if (shell.App.Commands.TryGetValue("connect", out var factory)) + { + foreach (var (example, description) in factory.ExamplesWithDescriptions) + { + if (string.IsNullOrWhiteSpace(example) || example == "connect") + { + // Skip the no-arg example — that's the one the user just ran. + continue; + } + + AnsiConsole.MarkupLine($" [lightyellow3]{Markup.Escape(example)}[/]"); + if (!string.IsNullOrWhiteSpace(description)) + { + AnsiConsole.MarkupLine($" [dim]{Markup.Escape(description)}[/]"); + } + } + } + + AnsiConsole.MarkupLine(MessageService.GetString("command-connect-not_connected-usage-footer")); + } + private static async Task PrintConnectionInfoAsync(ShellInterpreter shell, CommandState commandState, CancellationToken token) { if (shell.State is not ConnectedState connectedState) { AnsiConsole.MarkupLine(MessageService.GetString("command-connect-not_connected")); + PrintConnectUsageHint(shell); commandState.IsPrinted = true; var notConnectedJson = new Dictionary { diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index 7460313..cb480e9 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -464,6 +464,15 @@ internal async Task RunAsync() var result = 0; this.PrintVersion(null); WriteLine(MessageService.GetString("shell-ready")); + + // First-run hint: if the shell starts without a connection, point users at + // the `connect` command. Otherwise users can land at the prompt with no + // obvious next step (see issue #81). + if (this.State is DisconnectedState) + { + AnsiConsole.MarkupLine("[yellow]" + Markup.Escape(MessageService.GetString("shell-not_connected_hint")) + "[/]"); + } + while (this.IsRunning) { this.StdOutRedirect = null; diff --git a/CosmosDBShell/lang/en.ftl b/CosmosDBShell/lang/en.ftl index 50c60b8..9b40837 100644 --- a/CosmosDBShell/lang/en.ftl +++ b/CosmosDBShell/lang/en.ftl @@ -1,4 +1,5 @@ shell-ready = Cosmos DB shell ready. +shell-not_connected_hint = Not connected. Run 'connect ' to authenticate, or 'help connect' for more options. shell-hisory_file_deleted = History deleted. shell-connect-browser-auth = Authenticating via browser. Please complete the login in the browser window that opens. shell-connect-devicecode-auth = Browser authentication failed. Falling back to device code authentication. @@ -324,6 +325,8 @@ command-connect-connected = Connected to account '{ $account }' command-connect-emulator-detected = Emulator endpoint detected, using well-known account key and gateway mode. command-connect-switching = Disconnecting from '{ $endpoint }'... command-connect-not_connected = Not connected to any Cosmos DB account. +command-connect-not_connected-usage-header = Use 'connect ' to authenticate. Common forms: +command-connect-not_connected-usage-footer = Run 'help connect' for the full list of options. command-connect-info-title = Connection Information command-connect-info-account = Account command-connect-info-endpoint = Endpoint From c5779941daf4f4dcc959ae02fe0f099c4a6da1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Mon, 11 May 2026 13:46:15 +0200 Subject: [PATCH 2/3] Escape localized header/footer in PrintConnectUsageHint The localized strings were passed directly to AnsiConsole.MarkupLine, so a future translation containing '[' or ']' would be parsed as Spectre markup. The example/description lines in the same helper already use Markup.Escape; apply the same to the header and footer for consistency. Addresses Copilot review feedback on PR #82. --- .../Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs index cd4287c..ed0bd3d 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs @@ -143,7 +143,7 @@ internal static void AskForRBacPermissions(string principalId, string permission /// internal static void PrintConnectUsageHint(ShellInterpreter shell) { - AnsiConsole.MarkupLine(MessageService.GetString("command-connect-not_connected-usage-header")); + AnsiConsole.MarkupLine(Markup.Escape(MessageService.GetString("command-connect-not_connected-usage-header"))); if (shell.App.Commands.TryGetValue("connect", out var factory)) { @@ -163,7 +163,7 @@ internal static void PrintConnectUsageHint(ShellInterpreter shell) } } - AnsiConsole.MarkupLine(MessageService.GetString("command-connect-not_connected-usage-footer")); + AnsiConsole.MarkupLine(Markup.Escape(MessageService.GetString("command-connect-not_connected-usage-footer"))); } private static async Task PrintConnectionInfoAsync(ShellInterpreter shell, CommandState commandState, CancellationToken token) From 5ad6d8da3b22152e1fbc6fca91bbf62d3cf243ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Tue, 12 May 2026 10:50:20 +0200 Subject: [PATCH 3/3] Highlight and trim disconnected connect hint --- .../ConnectCommand.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs index ed0bd3d..2839034 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs @@ -147,19 +147,29 @@ internal static void PrintConnectUsageHint(ShellInterpreter shell) if (shell.App.Commands.TryGetValue("connect", out var factory)) { + const int MaxExamples = 2; + var shown = 0; foreach (var (example, description) in factory.ExamplesWithDescriptions) { + if (shown >= MaxExamples) + { + break; + } + if (string.IsNullOrWhiteSpace(example) || example == "connect") { // Skip the no-arg example — that's the one the user just ran. continue; } - AnsiConsole.MarkupLine($" [lightyellow3]{Markup.Escape(example)}[/]"); + var highlighted = shell.BuildHighlightedMarkup(example); + AnsiConsole.MarkupLine($" {highlighted}"); if (!string.IsNullOrWhiteSpace(description)) { - AnsiConsole.MarkupLine($" [dim]{Markup.Escape(description)}[/]"); + AnsiConsole.MarkupLine($" [silver]{Markup.Escape(description)}[/]"); } + + shown++; } }