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..2839034 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs @@ -135,11 +135,53 @@ 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(Markup.Escape(MessageService.GetString("command-connect-not_connected-usage-header"))); + + 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; + } + + var highlighted = shell.BuildHighlightedMarkup(example); + AnsiConsole.MarkupLine($" {highlighted}"); + if (!string.IsNullOrWhiteSpace(description)) + { + AnsiConsole.MarkupLine($" [silver]{Markup.Escape(description)}[/]"); + } + + shown++; + } + } + + AnsiConsole.MarkupLine(Markup.Escape(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