diff --git a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs
index 2a76b5f..c073ee4 100644
--- a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs
+++ b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs
@@ -123,6 +123,31 @@ public async Task HelpOption_PrintsHelpAndExitsZero()
Assert.Contains("--version", result.StdOut, StringComparison.OrdinalIgnoreCase);
}
+ [Fact]
+ public async Task UnknownRootArgument_ReturnsUnknownArgumentError()
+ {
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: ["not-a-root-option"],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotEqual(0, result.ExitCode);
+ Assert.Contains("Unrecognized argument 'not-a-root-option'", result.StdOut);
+ Assert.DoesNotContain("Option '--connect-mode' is defined with a bad format", result.StdOut);
+ }
+
+ [Fact]
+ public async Task MissingConnectValue_ReportsUserFacingOptionAlias()
+ {
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: ["--connect"],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotEqual(0, result.ExitCode);
+ Assert.Contains("Required option '--connect' is missing", result.StdOut);
+ }
+
[Fact]
[Trait("Category", "Emulator")]
public async Task ConnectOption_WithExecuteAndQuit_RunsCommandAgainstEmulator()
diff --git a/CosmosDBShell.Tests/UtilTest/NormalizeArgumentsTests.cs b/CosmosDBShell.Tests/UtilTest/NormalizeArgumentsTests.cs
new file mode 100644
index 0000000..a69228d
--- /dev/null
+++ b/CosmosDBShell.Tests/UtilTest/NormalizeArgumentsTests.cs
@@ -0,0 +1,113 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace CosmosShell.Tests.UtilTest;
+
+public class NormalizeArgumentsTests
+{
+ [Fact]
+ public void EmptyArgs_ReturnsEmpty()
+ {
+ Assert.Empty(Program.NormalizeArguments([]));
+ }
+
+ [Fact]
+ public void NonCommandArgs_PassThroughUnchanged()
+ {
+ var input = new[] { "--connect", "endpoint", "--verbose" };
+ Assert.Equal(input, Program.NormalizeArguments(input));
+ }
+
+ [Fact]
+ public void DashC_ConsumesRemainingTokensAsSingleString()
+ {
+ var result = Program.NormalizeArguments(["-c", "help", "mkitem"]);
+ Assert.Equal(["-c", "help mkitem"], result);
+ }
+
+ [Fact]
+ public void DashK_ConsumesRemainingTokensAsSingleString()
+ {
+ var result = Program.NormalizeArguments(["-k", "help", "mkitem"]);
+ Assert.Equal(["-k", "help mkitem"], result);
+ }
+
+ [Theory]
+ [InlineData("/c")]
+ [InlineData("/C")]
+ public void SlashC_IsTranslatedToDashC(string token)
+ {
+ var result = Program.NormalizeArguments([token, "help", "mkitem"]);
+ Assert.Equal(["-c", "help mkitem"], result);
+ }
+
+ [Theory]
+ [InlineData("/k")]
+ [InlineData("/K")]
+ public void SlashK_IsTranslatedToDashK(string token)
+ {
+ var result = Program.NormalizeArguments([token, "help", "mkitem"]);
+ Assert.Equal(["-k", "help mkitem"], result);
+ }
+
+ [Fact]
+ public void AppOptionsBeforeDashC_ArePreserved()
+ {
+ var result = Program.NormalizeArguments(
+ ["--connect", "endpoint", "-c", "help", "mkitem"]);
+ Assert.Equal(["--connect", "endpoint", "-c", "help mkitem"], result);
+ }
+
+ [Fact]
+ public void DashCWithoutTail_LeavesDashCAlone()
+ {
+ var result = Program.NormalizeArguments(["-c"]);
+ Assert.Equal(["-c"], result);
+ }
+
+ [Fact]
+ public void DashCWithQuotedSingleToken_StaysSingleToken()
+ {
+ var result = Program.NormalizeArguments(["-c", "help mkitem"]);
+ Assert.Equal(["-c", "help mkitem"], result);
+ }
+
+ [Fact]
+ public void TokensThatLookLikeOptions_AfterDashC_AreAbsorbed()
+ {
+ var result = Program.NormalizeArguments(
+ ["-c", "seed.csh", "--connect", "xyz"]);
+ Assert.Equal(["-c", "seed.csh --connect xyz"], result);
+ }
+
+ [Fact]
+ public void TakePreCommandArgs_ReturnsEverythingBeforeDashC()
+ {
+ var result = Program.TakePreCommandArgs(
+ ["--verbose", "-c", "help"]);
+ Assert.Equal(["--verbose"], result);
+ }
+
+ [Fact]
+ public void TakePreCommandArgs_ReturnsEverythingBeforeDashK()
+ {
+ var result = Program.TakePreCommandArgs(
+ ["--connect", "ep", "-k", "help"]);
+ Assert.Equal(["--connect", "ep"], result);
+ }
+
+ [Fact]
+ public void TakePreCommandArgs_NoCommandMarker_ReturnsAll()
+ {
+ var input = new[] { "--connect", "endpoint", "--verbose" };
+ Assert.Equal(input, Program.TakePreCommandArgs(input));
+ }
+
+ [Fact]
+ public void TakePreCommandArgs_DashCFirst_ReturnsEmpty()
+ {
+ var result = Program.TakePreCommandArgs(["-c", "--help"]);
+ Assert.Empty(result);
+ }
+}
diff --git a/CosmosDBShell.Tests/UtilTest/ParseDocDBConnectionTests.cs b/CosmosDBShell.Tests/UtilTest/ParseDocDBConnectionTests.cs
index 6b47104..61ad77c 100644
--- a/CosmosDBShell.Tests/UtilTest/ParseDocDBConnectionTests.cs
+++ b/CosmosDBShell.Tests/UtilTest/ParseDocDBConnectionTests.cs
@@ -2,13 +2,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------
-using CommandLine;
using Azure.Data.Cosmos.Shell.Util;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace CosmosShell.Tests.UtilTest;
diff --git a/CosmosDBShell.Tests/UtilTest/SentenceBuilderTests.cs b/CosmosDBShell.Tests/UtilTest/SentenceBuilderTests.cs
index 109d9f4..16cf39b 100644
--- a/CosmosDBShell.Tests/UtilTest/SentenceBuilderTests.cs
+++ b/CosmosDBShell.Tests/UtilTest/SentenceBuilderTests.cs
@@ -2,13 +2,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------
-using CommandLine;
using Azure.Data.Cosmos.Shell.Util;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace CosmosShell.Tests.UtilTest;
@@ -24,18 +18,4 @@ public void TestCommandDescriptions()
Assert.NotNull(LocalizableSentenceBuilder.ConnectionString);
Assert.NotNull(LocalizableSentenceBuilder.Command);
}
-
- [Fact]
- public void TestLocalizableSentenceBuilder()
- {
- var builder = new LocalizableSentenceBuilder();
- Assert.NotNull(builder.RequiredWord());
- Assert.NotNull(builder.ErrorsHeadingText());
- Assert.NotNull(builder.UsageHeadingText());
- Assert.NotNull(builder.OptionGroupWord());
- Assert.NotNull(builder.HelpCommandText(true));
- Assert.NotNull(builder.HelpCommandText(false));
- Assert.NotNull(builder.VersionCommandText(true));
- Assert.NotNull(builder.VersionCommandText(false));
- }
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs
index 4dd6d59..a1a563b 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs
@@ -10,7 +10,6 @@ namespace Azure.Data.Cosmos.Shell.Commands;
using Azure.Data.Cosmos.Shell.Core;
using Azure.Data.Cosmos.Shell.Parser;
using Azure.Data.Cosmos.Shell.Util;
-using CommandLine.Text;
using RadLine;
using Spectre.Console;
using static System.Net.Mime.MediaTypeNames;
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Util/LocalizableSentenceBuilder.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Util/LocalizableSentenceBuilder.cs
index cdccd53..cae8be3 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Util/LocalizableSentenceBuilder.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Util/LocalizableSentenceBuilder.cs
@@ -4,12 +4,13 @@
namespace Azure.Data.Cosmos.Shell.Util;
-using global::CommandLine;
-using global::CommandLine.Text;
-
-using Spectre.Console;
-
-public class LocalizableSentenceBuilder : SentenceBuilder
+///
+/// Static accessors for localized CLI option descriptions and usage strings.
+/// Previously implemented CommandLine.Text.SentenceBuilder; after the
+/// migration to System.CommandLine these strings are pulled directly
+/// by the option builder in Program.cs.
+///
+public static class LocalizableSentenceBuilder
{
public static string ExecuteAndContinue => MessageService.GetString("help-ExecuteAndContinue");
@@ -43,111 +44,5 @@ public class LocalizableSentenceBuilder : SentenceBuilder
public static string Verbose => MessageService.GetString("help-Verbose");
- public override Func RequiredWord => () => MessageService.GetString("help-RequiredWord");
-
- public override Func ErrorsHeadingText => () => MessageService.GetString("help-ErrorsHeadingText");
-
- public override Func UsageHeadingText => () => MessageService.GetString("help-UsageHeadingText");
-
- public override Func OptionGroupWord => () => MessageService.GetString("help-OptionGroupWord");
-
- public override Func HelpCommandText
- {
- get
- {
- return isOption => isOption
- ? MessageService.GetString("help-HelpCommandScreenText")
- : MessageService.GetString("help-HelpCommandMoreText");
- }
- }
-
- public override Func VersionCommandText => (b) => MessageService.GetString("help-VersionCommandText");
-
- public override Func FormatError
- {
- get
- {
- return error =>
- {
- switch (error.Tag)
- {
- case ErrorType.BadFormatTokenError:
- return MessageService.GetString("help-error-BadFormatTokenError", new Dictionary { { "token", ((BadFormatTokenError)error).Token } });
- case ErrorType.MissingValueOptionError:
- return MessageService.GetString("help-error-MissingValueOptionError", new Dictionary { { "option", ((MissingValueOptionError)error).NameInfo.NameText } });
- case ErrorType.UnknownOptionError:
- return MessageService.GetString("help-error-UnknownOptionError", new Dictionary { { "option", ((UnknownOptionError)error).Token } });
- case ErrorType.MissingRequiredOptionError:
- var errMisssing = (MissingRequiredOptionError)error;
- return errMisssing.NameInfo.Equals(NameInfo.EmptyName)
- ? MessageService.GetString("help-error-MissingRequiredOptionError1")
- : MessageService.GetString("help-error-MissingRequiredOptionError2", new Dictionary { { "option", errMisssing.NameInfo.NameText } });
- case ErrorType.BadFormatConversionError:
- var badFormat = (BadFormatConversionError)error;
- return badFormat.NameInfo.Equals(NameInfo.EmptyName)
- ? MessageService.GetString("help-error-BadFormatConversionError1")
- : MessageService.GetString("help-error-BadFormatConversionError2", new Dictionary { { "option", badFormat.NameInfo.NameText } });
- case ErrorType.SequenceOutOfRangeError:
- var seqOutRange = (SequenceOutOfRangeError)error;
- return seqOutRange.NameInfo.Equals(NameInfo.EmptyName)
- ? MessageService.GetString("help-error-SequenceOutOfRangeError1")
- : MessageService.GetString("help-error-SequenceOutOfRangeError2", new Dictionary { { "option", seqOutRange.NameInfo.NameText } });
- case ErrorType.BadVerbSelectedError:
- return MessageService.GetString("help-error-BadVerbSelectedError", new Dictionary { { "token", ((BadVerbSelectedError)error).Token } });
- case ErrorType.NoVerbSelectedError:
- return MessageService.GetString("help-error-NoVerbSelectedError");
- case ErrorType.RepeatedOptionError:
- return MessageService.GetString("help-error-RepeatedOptionError", new Dictionary { { "option", ((RepeatedOptionError)error).NameInfo.NameText } });
- case ErrorType.SetValueExceptionError:
- var setValueError = (SetValueExceptionError)error;
- return MessageService.GetString("help-error-SetValueExceptionError", new Dictionary
- {
- { "option", setValueError.NameInfo.NameText },
- { "message", setValueError.Exception.Message },
- });
- case ErrorType.MissingGroupOptionError:
- var missingGroupOptionError = (MissingGroupOptionError)error;
- return MessageService.GetString("help-error-MissingGroupOptionError", new Dictionary
- {
- { "option", missingGroupOptionError.Group },
- { "req_options", string.Join(", ", missingGroupOptionError.Names.Select(n => n.NameText)) },
- });
- case ErrorType.GroupOptionAmbiguityError:
- var groupOptionAmbiguityError = (GroupOptionAmbiguityError)error;
- return MessageService.GetString("help-error-GroupOptionAmbiguityError", new Dictionary { { "option", groupOptionAmbiguityError.Option.NameText } });
- case ErrorType.MultipleDefaultVerbsError:
- return MultipleDefaultVerbsError.ErrorMessage;
- }
-
- throw new InvalidOperationException();
- };
- }
- }
-
- public override Func, string> FormatMutuallyExclusiveSetErrors => errors =>
- {
- var bySet = from e in errors
- group e by e.SetName into g
- select new { SetName = g.Key, Errors = g.ToList() };
-
- var msgs = bySet.Select(
- set =>
- {
- var names = string.Join(string.Empty, from e in set.Errors select "'" + e.NameInfo.NameText + "', ");
- var namesCount = set.Errors.Count;
-
- var incompat = string.Join(
- string.Empty,
- (from x in (from s in bySet where !s.SetName.Equals(set.SetName) from e in s.Errors select e).Distinct()
- select "'" + x.NameInfo.NameText + "', ").ToArray());
-
- return MessageService.GetString("help-error-FormatMutuallyExclusiveSetErrors", new Dictionary
- {
- { "count", namesCount },
- { "option", names[..^2] },
- { "incompat", incompat[..^2] },
- });
- }).ToArray();
- return string.Join(Environment.NewLine, msgs);
- };
-}
\ No newline at end of file
+ public static string UsageHeadingText => MessageService.GetString("help-UsageHeadingText");
+}
diff --git a/CosmosDBShell/CosmosDBShell.csproj b/CosmosDBShell/CosmosDBShell.csproj
index a4e4e21..a874d2c 100644
--- a/CosmosDBShell/CosmosDBShell.csproj
+++ b/CosmosDBShell/CosmosDBShell.csproj
@@ -86,7 +86,7 @@
-
+
diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs
index e2d2132..2ae8324 100644
--- a/CosmosDBShell/Program.cs
+++ b/CosmosDBShell/Program.cs
@@ -2,22 +2,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------
+using System.CommandLine;
+using System.CommandLine.Parsing;
using System.Reflection;
using Azure.Data.Cosmos.Shell.Commands;
using Azure.Data.Cosmos.Shell.Core;
using Azure.Data.Cosmos.Shell.Lsp;
using Azure.Data.Cosmos.Shell.Mcp;
using Azure.Data.Cosmos.Shell.Util;
-using CommandLine;
-using CommandLine.Text;
using Microsoft.Azure.Cosmos;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
-using OmniSharp.Extensions.LanguageServer.Protocol.Document;
-using OmniSharp.Extensions.LanguageServer.Protocol.Window;
-using OmniSharp.Extensions.LanguageServer.Server;
using Spectre.Console;
internal class Program
@@ -26,9 +20,18 @@ internal class Program
public static async Task Main(string[] args)
{
+ // Normalize argv first so that any --lsp/--stdio token that is part
+ // of a -c/-k command tail is absorbed as command text and does not
+ // accidentally trigger LSP mode.
+ args = NormalizeArguments(args);
+
// Handle LSP mode early, before any other code can write to stdout.
- // The LSP protocol requires exclusive access to stdin/stdout.
- if (args.Contains("--lsp") || args.Contains("--stdio"))
+ // The LSP protocol requires exclusive access to stdin/stdout. Only
+ // inspect the prefix before -c / -k so that a command tail of literally
+ // "--lsp" or "--stdio" is forwarded to the shell command rather than
+ // accidentally starting the LSP server.
+ var preCommandLsp = TakePreCommandArgs(args);
+ if (preCommandLsp.Any(a => a is "--lsp" or "--stdio"))
{
var server = await LspServer.CreateLanguageServerAsync();
await server.WaitForExit;
@@ -38,234 +41,238 @@ public static async Task Main(string[] args)
IHost? host = null;
try
{
- args = NormalizeArguments(args);
- SentenceBuilder.Factory = () => new LocalizableSentenceBuilder();
-
- // Use a custom parser so we can render our own heading for --version
- // (CommandLineParser's default output is " ",
- // which prints the build-metadata SHA inline and duplicated when both
- // /p:InformationalVersion and SourceLink's SourceRevisionId contribute
- // metadata). HelpWriter=null disables the built-in help/version writer;
- // we delegate back to HelpText.AutoBuild for --help and parse errors.
- using var parser = new Parser(settings =>
+ // --help / --version handled manually so we can render our own
+ // localized usage and version heading. Only inspect the prefix
+ // before -c / -k so that a command tail of literally "--help" or
+ // "--version" (e.g. `-c --help`) is forwarded to the shell command
+ // instead of intercepted here.
+ var preCommandArgs = TakePreCommandArgs(args);
+ if (preCommandArgs.Any(a => a is "--help" or "-h" or "-?" or "/?" or "/h"))
+ {
+ ShellInterpreter.WriteLine(BuildHelpText());
+ return;
+ }
+
+ if (preCommandArgs.Any(a => a is "--version"))
{
- settings.HelpWriter = null;
- });
- var parseResult = parser.ParseArguments(args);
+ WriteVersionHeading();
+ return;
+ }
+
+ var (rootCommand, optionMap) = BuildRootCommand();
+ var configuration = new System.CommandLine.CommandLineConfiguration(
+ rootCommand,
+ resources: new LocalizedCliResources());
+ var parser = new System.CommandLine.Parsing.Parser(configuration);
+ var parseResult = parser.Parse(args);
- // Handle parse errors
- parseResult.WithNotParsed(errors =>
+ if (parseResult.Errors.Count > 0)
{
- if (errors.IsVersion())
+ foreach (var error in parseResult.Errors)
{
- WriteVersionHeading();
- return;
+ ShellInterpreter.WriteLine(error.Message);
}
- var helpText = errors.IsHelp()
- ? HelpText.AutoBuild(parseResult)
- : HelpText.AutoBuild(parseResult, h => HelpText.DefaultParsingErrorsHandler(parseResult, h), e => e);
- ShellInterpreter.WriteLine(helpText.ToString());
+ ShellInterpreter.WriteLine(BuildHelpText());
+ Environment.ExitCode = 1;
+ return;
+ }
- if (!errors.IsHelp())
+ var o = new CosmosShellOptions
+ {
+ ColorSystem = parseResult.GetValueForOption(optionMap.ColorSystem),
+ ExecuteAndQuit = parseResult.GetValueForOption(optionMap.ExecuteAndQuit),
+ ExecuteAndContinue = parseResult.GetValueForOption(optionMap.ExecuteAndContinue),
+ ClearHistory = parseResult.GetValueForOption(optionMap.ClearHistory),
+ ConnectionString = parseResult.GetValueForOption(optionMap.ConnectionString),
+ ConnectionMode = parseResult.GetValueForOption(optionMap.ConnectionMode),
+ ConnectTenant = parseResult.GetValueForOption(optionMap.ConnectTenant),
+ ConnectHint = parseResult.GetValueForOption(optionMap.ConnectHint),
+ ConnectAuthorityHost = parseResult.GetValueForOption(optionMap.ConnectAuthorityHost),
+ ConnectManagedIdentity = parseResult.GetValueForOption(optionMap.ConnectManagedIdentity),
+ ConnectVSCodeCredential = parseResult.GetValueForOption(optionMap.ConnectVSCodeCredential),
+ StartLspServer = parseResult.GetValueForOption(optionMap.StartLspServer),
+ LspStdio = parseResult.GetValueForOption(optionMap.LspStdio),
+ Verbose = parseResult.GetValueForOption(optionMap.Verbose),
+ };
+
+ // --mcp supports an optional value: when the option is present without
+ // an integer, fall back to the default port.
+ var mcpResult = parseResult.FindResultFor(optionMap.McpPort);
+ if (mcpResult is not null)
+ {
+ var mcpValue = parseResult.GetValueForOption(optionMap.McpPort);
+ o.McpPort = mcpValue ?? DefaultMcpPort;
+ }
+
+ if (o.StartLspServer)
+ {
+ // Already handled above, but keep for completeness
+ var server = await LspServer.CreateLanguageServerAsync();
+ await server.WaitForExit;
+ return;
+ }
+
+ if (!string.IsNullOrWhiteSpace(o.ExecuteAndQuit) && !string.IsNullOrWhiteSpace(o.ExecuteAndContinue))
+ {
+ Environment.ExitCode = 1;
+ ShellInterpreter.WriteLine(MessageService.GetString("error-mutually-exclusive-options"));
+ return;
+ }
+
+ var executeAndQuitCommand = string.IsNullOrWhiteSpace(o.ExecuteAndQuit) ? null : o.ExecuteAndQuit;
+ var executeAndContinueCommand = string.IsNullOrWhiteSpace(o.ExecuteAndContinue) ? null : o.ExecuteAndContinue;
+ var explicitCommand = executeAndContinueCommand ?? executeAndQuitCommand;
+
+ if (o.ClearHistory)
+ {
+ if (File.Exists(ShellInterpreter.Instance.HistoryFile))
{
- Environment.ExitCode = 1;
+ File.Delete(ShellInterpreter.Instance.HistoryFile);
}
- });
- _ = await parseResult.WithParsedAsync(async o =>
+ ShellInterpreter.WriteLine(MessageService.GetString("shell-hisory_file_deleted"));
+ return;
+ }
+
+ AnsiConsole.Profile.Capabilities.ColorSystem = o.ColorSystem switch
+ {
+ 1 => ColorSystem.Standard,
+ 2 => ColorSystem.TrueColor,
+ _ => ColorSystem.NoColors,
+ };
+ ShellInterpreter.Instance.Options = o;
+
+ if (o.ConnectionString != null)
{
- if (o.StartLspServer)
+ using var connectTokenSource = ShellInterpreter.UserCancellationTokenSource;
+ var connectToken = connectTokenSource.Token;
+ try
{
- // Already handled above, but keep for completeness
- var server = await LspServer.CreateLanguageServerAsync();
- await server.WaitForExit;
- return;
+ await ShellInterpreter.Instance.ConnectAsync(
+ o.ConnectionString,
+ o.ConnectHint,
+ o.ConnectionMode,
+ tenantId: o.ConnectTenant,
+ authorityHost: o.ConnectAuthorityHost,
+ managedIdentityClientId: o.ConnectManagedIdentity,
+ useVSCodeCredential: o.ConnectVSCodeCredential,
+ token: connectToken);
}
-
- if (!string.IsNullOrWhiteSpace(o.ExecuteAndQuit) && !string.IsNullOrWhiteSpace(o.ExecuteAndContinue))
+ catch (OperationCanceledException) when (connectToken.IsCancellationRequested)
{
- Environment.ExitCode = 1;
- ShellInterpreter.WriteLine(MessageService.GetString("error-mutually-exclusive-options"));
return;
}
-
- var executeAndQuitCommand = string.IsNullOrWhiteSpace(o.ExecuteAndQuit) ? null : o.ExecuteAndQuit;
- var executeAndContinueCommand = string.IsNullOrWhiteSpace(o.ExecuteAndContinue) ? null : o.ExecuteAndContinue;
- var explicitCommand = executeAndContinueCommand ?? executeAndQuitCommand;
-
- if (o.ClearHistory)
+ catch (Exception ex)
{
- if (File.Exists(ShellInterpreter.Instance.HistoryFile))
+ Environment.ExitCode = 1;
+ if (ConnectCommand.TryGetPrincipalIdFromRbacException(ex, out var id, out var permission))
{
- File.Delete(ShellInterpreter.Instance.HistoryFile);
+ ConnectCommand.AskForRBacPermissions(id ?? string.Empty, permission ?? string.Empty);
+ return;
}
- ShellInterpreter.WriteLine(MessageService.GetString("shell-hisory_file_deleted"));
+ ShellInterpreter.WriteLine(ex.Message);
return;
}
+ }
- AnsiConsole.Profile.Capabilities.ColorSystem = o.ColorSystem switch
- {
- 1 => ColorSystem.Standard,
- 2 => ColorSystem.TrueColor,
- _ => ColorSystem.NoColors,
- };
- ShellInterpreter.Instance.Options = o;
+ // Start MCP server if requested
+ Task? hostTask = null;
- if (o.ConnectionString != null)
+ if (o.McpPort is int mcpPort)
+ {
+ if (mcpPort <= 0)
{
- using var connectTokenSource = ShellInterpreter.UserCancellationTokenSource;
- var connectToken = connectTokenSource.Token;
- try
- {
- await ShellInterpreter.Instance.ConnectAsync(
- o.ConnectionString,
- o.ConnectHint,
- o.ConnectionMode,
- tenantId: o.ConnectTenant,
- authorityHost: o.ConnectAuthorityHost,
- managedIdentityClientId: o.ConnectManagedIdentity,
- useVSCodeCredential: o.ConnectVSCodeCredential,
- token: connectToken);
- }
- catch (OperationCanceledException) when (connectToken.IsCancellationRequested)
- {
- return;
- }
- catch (Exception ex)
- {
- Environment.ExitCode = 1;
- if (ConnectCommand.TryGetPrincipalIdFromRbacException(ex, out var id, out var permission))
- {
- ConnectCommand.AskForRBacPermissions(id ?? string.Empty, permission ?? string.Empty);
- return;
- }
-
- ShellInterpreter.WriteLine(ex.Message);
- return;
- }
+ AnsiConsole.WriteLine(MessageService.GetString("mcp-error-invalid-port"));
+ Environment.ExitCode = 1;
+ return;
}
- // Start MCP server if requested
- Task? hostTask = null;
-
- if (o.McpPort is int mcpPort)
+ try
{
- if (mcpPort <= 0)
- {
- AnsiConsole.WriteLine(MessageService.GetString("mcp-error-invalid-port"));
- Environment.ExitCode = 1;
- return;
- }
-
- try
- {
- host = McpServer.CreateHost(o);
- }
- catch (Exception ex)
- {
- AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-creating-server", "message", Markup.Escape(ex.Message)));
- Environment.ExitCode = 1;
- return;
- }
-
- if (host != null)
- {
- ShellInterpreter.Instance.McpPort = mcpPort;
- hostTask = Task.Run(async () =>
- {
- try
- {
- var token = CancellationToken.None;
- await host.StartAsync(token);
- await host.WaitForShutdownAsync(token);
- }
- catch (Exception ex)
- {
- AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-server-failed-start", "message", Markup.Escape(ex.Message)));
- Environment.ExitCode = 1;
- }
- });
- }
+ host = McpServer.CreateHost(o);
+ }
+ catch (Exception ex)
+ {
+ AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-creating-server", "message", Markup.Escape(ex.Message)));
+ Environment.ExitCode = 1;
+ return;
}
- if (Console.IsInputRedirected)
+ if (host != null)
{
- // Read entire stdin (script)
- var script = await Console.In.ReadToEndAsync() + Environment.NewLine;
- if (!string.IsNullOrWhiteSpace(script))
+ ShellInterpreter.Instance.McpPort = mcpPort;
+ hostTask = Task.Run(async () =>
{
- var state = await ShellInterpreter.Instance.ExecuteCommandAsync(script, default);
- if (state.IsError)
+ try
{
- Environment.ExitCode = 1;
- if (executeAndContinueCommand is null)
- {
- return;
- }
+ var token = CancellationToken.None;
+ await host.StartAsync(token);
+ await host.WaitForShutdownAsync(token);
}
-
- // If user only wants to execute piped script then quit (unless -k / ExecuteAndContinue)
- if (executeAndContinueCommand is null && executeAndQuitCommand is null)
+ catch (Exception ex)
{
- // Stop host gracefully before returning
- if (host != null)
- {
- await host.StopAsync();
- }
-
- return;
+ AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-server-failed-start", "message", Markup.Escape(ex.Message)));
+ Environment.ExitCode = 1;
}
- }
+ });
}
+ }
- if (explicitCommand is not null)
+ if (Console.IsInputRedirected)
+ {
+ // Read entire stdin (script)
+ var script = await Console.In.ReadToEndAsync() + Environment.NewLine;
+ if (!string.IsNullOrWhiteSpace(script))
{
- var state = await ShellInterpreter.Instance.ExecuteCommandAsync(explicitCommand, default);
+ var state = await ShellInterpreter.Instance.ExecuteCommandAsync(script, default);
if (state.IsError)
{
Environment.ExitCode = 1;
if (executeAndContinueCommand is null)
{
- // Stop host gracefully before returning
- if (host != null)
- {
- await host.StopAsync();
- }
-
- // Wait for the host task to complete
- if (hostTask != null)
- {
- await hostTask;
- }
+ await StopHostAsync(host, hostTask);
return;
}
}
- if (executeAndContinueCommand is not null)
+ // If user only wants to execute piped script then quit (unless -k / ExecuteAndContinue)
+ if (executeAndContinueCommand is null && executeAndQuitCommand is null)
{
- await ShellInterpreter.Instance.RunAsync();
+ await StopHostAsync(host, hostTask);
+
+ return;
}
}
- else
- {
- await ShellInterpreter.Instance.RunAsync();
- }
+ }
- // Stop the host gracefully before the task completes
- if (host != null)
+ if (explicitCommand is not null)
+ {
+ var state = await ShellInterpreter.Instance.ExecuteCommandAsync(explicitCommand, default);
+ if (state.IsError)
{
- await host.StopAsync();
+ Environment.ExitCode = 1;
+ if (executeAndContinueCommand is null)
+ {
+ await StopHostAsync(host, hostTask);
+
+ return;
+ }
}
- // Wait for the host task to complete
- if (hostTask != null)
+ if (executeAndContinueCommand is not null)
{
- await hostTask;
+ await ShellInterpreter.Instance.RunAsync();
}
- });
+ }
+ else
+ {
+ await ShellInterpreter.Instance.RunAsync();
+ }
+
+ await StopHostAsync(host, hostTask);
}
finally
{
@@ -286,82 +293,340 @@ private static void WriteVersionHeading()
var version = ShellInterpreter.GetDisplayVersion(assembly);
var commit = ShellInterpreter.GetDisplayCommit(assembly);
var heading = string.IsNullOrEmpty(commit)
- ? new HeadingInfo(product, version)
- : new HeadingInfo(product, $"{version} ({commit})");
- ShellInterpreter.WriteLine(heading.ToString());
+ ? $"{product} {version}"
+ : $"{product} {version} ({commit})";
+ ShellInterpreter.WriteLine(heading);
+ }
+
+ private static async Task StopHostAsync(IHost? host, Task? hostTask)
+ {
+ if (host != null)
+ {
+ await host.StopAsync();
+ }
+
+ if (hostTask != null)
+ {
+#pragma warning disable VSTHRD003 // hostTask is created by this process via Task.Run for the MCP host loop.
+ await hostTask;
+#pragma warning restore VSTHRD003
+ }
}
- private static string[] NormalizeArguments(string[] args)
+ ///
+ /// Pre-processes argv before sees it:
+ /// 1. Translates Windows-style /c//k switches into their
+ /// POSIX equivalents (-c/-k).
+ /// 2. Once -c or -k is encountered, the rest of the
+ /// command line is collapsed into a single string value, so users
+ /// don't have to quote multi-word commands:
+ /// CosmosDBShell -c help mkitem
+ /// becomes
+ /// CosmosDBShell -c "help mkitem"
+ /// App-level options must therefore come before -c/-k.
+ ///
+ internal static string[] NormalizeArguments(string[] args)
{
- var normalizedArguments = new List(args.Length);
+ var result = new List(args.Length);
for (int index = 0; index < args.Length; index++)
{
var argument = args[index];
- if (argument == "--mcp")
+
+ // Translate /c, /k, /C, /K to -c / -k early so all later checks
+ // can treat them uniformly.
+ if (argument is "/c" or "/C")
+ {
+ argument = "-c";
+ }
+ else if (argument is "/k" or "/K")
+ {
+ argument = "-k";
+ }
+
+ if (argument is "-c" or "-k")
+ {
+ result.Add(argument);
+ if (index + 1 < args.Length)
+ {
+ result.Add(string.Join(' ', args.Skip(index + 1)));
+ }
+
+ return [.. result];
+ }
+
+ result.Add(argument);
+ }
+
+ return [.. result];
+ }
+
+ ///
+ /// Returns the argv prefix that precedes the first -c / -k
+ /// (already normalized from /c, /k). Anything from the
+ /// command marker onward is the consume-rest command tail and must not
+ /// be inspected for app-level flags such as --help, --version,
+ /// --lsp, or --stdio.
+ ///
+ internal static string[] TakePreCommandArgs(string[] args)
+ {
+ for (int index = 0; index < args.Length; index++)
+ {
+ if (args[index] is "-c" or "-k")
+ {
+ return args.Take(index).ToArray();
+ }
+ }
+
+ return args;
+ }
+
+ private static (RootCommand Command, OptionMap Map) BuildRootCommand()
+ {
+ var colorSystem = new Option(
+ aliases: ["--color-system", "--cs"],
+ getDefaultValue: () => 2,
+ description: MessageService.GetString("help-ColorSystem"));
+
+ var executeAndQuit = new Option("-c", MessageService.GetString("help-ExecuteAndQuit"));
+ var executeAndContinue = new Option("-k", MessageService.GetString("help-ExecuteAndContinue"));
+
+ var clearHistory = new Option(
+ aliases: ["--clear-history", "--clearhistory"],
+ description: MessageService.GetString("help-ClearHistory"));
+ var connectionString = new Option("--connect", MessageService.GetString("help-ConnectionString"));
+
+ var connectionMode = new Option(
+ "--connect-mode",
+ parseArgument: argResult =>
{
- if (index + 1 < args.Length && !args[index + 1].StartsWith("-", StringComparison.Ordinal))
+ if (argResult.Tokens.Count == 0)
{
- normalizedArguments.Add(argument);
- normalizedArguments.Add(args[++index]);
- continue;
+ return null;
}
- normalizedArguments.Add($"--mcp={DefaultMcpPort}");
+ var token = argResult.Tokens[0].Value;
+ if (Enum.TryParse(token, ignoreCase: true, out var mode))
+ {
+ return mode;
+ }
+
+ argResult.ErrorMessage = MessageService.GetArgsString(
+ "help-error-BadFormatConversionError2",
+ "option",
+ "--connect-mode");
+ return null;
+ },
+ description: MessageService.GetString("help-ConnectionMode"));
+
+ var connectTenant = new Option("--connect-tenant", MessageService.GetString("help-ConnectTenant"));
+ var connectHint = new Option("--connect-hint", MessageService.GetString("help-ConnectHint"));
+ var connectAuthorityHost = new Option("--connect-authority-host", MessageService.GetString("help-ConnectAuthorityHost"));
+ var connectManagedIdentity = new Option("--connect-managed-identity", MessageService.GetString("help-ConnectManagedIdentity"));
+ var connectVSCodeCredential = new Option("--connect-vscode-credential", MessageService.GetString("help-ConnectVSCodeCredential"))
+ {
+ IsHidden = true,
+ };
+
+ var mcpPort = new Option("--mcp", MessageService.GetString("help-McpPort"))
+ {
+ Arity = ArgumentArity.ZeroOrOne,
+ };
+
+ var startLspServer = new Option("--lsp", MessageService.GetString("help-EnableLspServer"));
+ var lspStdio = new Option("--stdio", MessageService.GetString("help-EnableLspServer"))
+ {
+ IsHidden = true,
+ };
+ var verbose = new Option("--verbose", MessageService.GetString("help-Verbose"));
+
+ var root = new RootCommand("Cosmos DB Shell")
+ {
+ colorSystem,
+ executeAndQuit,
+ executeAndContinue,
+ clearHistory,
+ connectionString,
+ connectionMode,
+ connectTenant,
+ connectHint,
+ connectAuthorityHost,
+ connectManagedIdentity,
+ connectVSCodeCredential,
+ mcpPort,
+ startLspServer,
+ lspStdio,
+ verbose,
+ };
+
+ var map = new OptionMap(
+ colorSystem,
+ executeAndQuit,
+ executeAndContinue,
+ clearHistory,
+ connectionString,
+ connectionMode,
+ connectTenant,
+ connectHint,
+ connectAuthorityHost,
+ connectManagedIdentity,
+ connectVSCodeCredential,
+ mcpPort,
+ startLspServer,
+ lspStdio,
+ verbose);
+
+ return (root, map);
+ }
+
+ private static string BuildHelpText()
+ {
+ var (rootCommand, map) = BuildRootCommand();
+ var product = (typeof(Program).Assembly.GetName().Name ?? "CosmosDBShell").ToLowerInvariant();
+
+ // Per-option value placeholder shown after the alias list.
+ var placeholders = new Dictionary
+ {
+ [map.ExecuteAndQuit] = "...",
+ [map.ExecuteAndContinue] = "...",
+ [map.ColorSystem] = "",
+ [map.ConnectionString] = "",
+ [map.ConnectionMode] = "",
+ [map.ConnectTenant] = "",
+ [map.ConnectHint] = "",
+ [map.ConnectAuthorityHost] = "",
+ [map.ConnectManagedIdentity] = "",
+ [map.McpPort] = "[]",
+ };
+
+ var rows = new List<(string Label, string? Description)>();
+ foreach (var option in rootCommand.Options)
+ {
+ if (option.IsHidden)
+ {
continue;
}
- normalizedArguments.Add(argument);
+ var label = string.Join(", ", option.Aliases);
+ if (placeholders.TryGetValue((System.CommandLine.Option)option, out var placeholder))
+ {
+ label = $"{label} {placeholder}";
+ }
+
+ rows.Add((label, option.Description));
}
- return [.. normalizedArguments];
+ // --help / --version are intercepted before parsing, so they are not
+ // declared as Option. Surface them in the rendered help so users
+ // can discover them.
+ rows.Add(("--help, -h, -?", MessageService.GetString("help-HelpOptionDescription")));
+ rows.Add(("--version", MessageService.GetString("help-VersionOptionDescription")));
+
+ var width = rows.Max(r => r.Label.Length);
+
+ var builder = new System.Text.StringBuilder();
+ builder.AppendLine(MessageService.GetString("help-UsageHeadingText"));
+ builder.AppendLine(" " + MessageService.GetArgsString("help-UsageSynopsis", "command", product));
+ builder.AppendLine();
+ builder.AppendLine(MessageService.GetString("help-OptionsHeadingText"));
+ foreach (var (label, description) in rows)
+ {
+ builder.AppendLine($" {label.PadRight(width)} {description}");
+ }
+
+ builder.AppendLine();
+ builder.AppendLine(MessageService.GetString("help-NotesHeadingText"));
+ builder.AppendLine(" " + MessageService.GetString("help-CommandTailNote"));
+
+ return builder.ToString();
+ }
+
+ private sealed record OptionMap(
+ Option ColorSystem,
+ Option ExecuteAndQuit,
+ Option ExecuteAndContinue,
+ Option ClearHistory,
+ Option ConnectionString,
+ Option ConnectionMode,
+ Option ConnectTenant,
+ Option ConnectHint,
+ Option ConnectAuthorityHost,
+ Option ConnectManagedIdentity,
+ Option ConnectVSCodeCredential,
+ Option McpPort,
+ Option StartLspServer,
+ Option LspStdio,
+ Option Verbose);
+
+ ///
+ /// Maps the most common System.CommandLine parse error messages
+ /// to the existing help-error-* entries in en.ftl so the
+ /// localized help/error strings authored for the previous parser are not
+ /// silently lost. Anything not overridden falls back to the default
+ /// English text from .
+ ///
+ private sealed class LocalizedCliResources : System.CommandLine.LocalizationResources
+ {
+ public override string UnrecognizedCommandOrArgument(string arg) =>
+ arg.StartsWith('-')
+ ? MessageService.GetArgsString("help-error-UnknownOptionError", "option", arg)
+ : MessageService.GetArgsString("help-error-UnknownArgumentError", "argument", arg);
+
+ public override string UnrecognizedArgument(string unrecognizedArg, IReadOnlyCollection allowedValues) =>
+ unrecognizedArg.StartsWith('-')
+ ? MessageService.GetArgsString("help-error-UnknownOptionError", "option", unrecognizedArg)
+ : MessageService.GetArgsString("help-error-UnknownArgumentError", "argument", unrecognizedArg);
+
+ public override string ExpectsOneArgument(System.CommandLine.Parsing.SymbolResult symbolResult) =>
+ MessageService.GetArgsString("help-error-MissingValueOptionError", "option", GetDisplayName(symbolResult));
+
+ public override string NoArgumentProvided(System.CommandLine.Parsing.SymbolResult symbolResult) =>
+ MessageService.GetArgsString("help-error-MissingValueOptionError", "option", GetDisplayName(symbolResult));
+
+ public override string RequiredArgumentMissing(System.CommandLine.Parsing.SymbolResult symbolResult) =>
+ MessageService.GetArgsString("help-error-MissingRequiredOptionError2", "option", GetDisplayName(symbolResult));
+
+ public override string ArgumentConversionCannotParseForOption(string value, string optionName, Type expectedType) =>
+ MessageService.GetArgsString("help-error-BadFormatConversionError2", "option", optionName);
+
+ private static string GetDisplayName(System.CommandLine.Parsing.SymbolResult symbolResult)
+ {
+ return symbolResult.Symbol is System.CommandLine.Option option && option.Aliases.Count > 0
+ ? option.Aliases.First()
+ : symbolResult.Symbol.Name;
+ }
}
public class CosmosShellOptions
{
- [Option("cs", Required = false, HelpText = "ColorSystem", ResourceType = typeof(LocalizableSentenceBuilder))]
public int ColorSystem { get; set; } = 2;
- [Option('c', Required = false, HelpText = "ExecuteAndQuit", ResourceType = typeof(LocalizableSentenceBuilder))]
public string? ExecuteAndQuit { get; set; }
- [Option('k', Required = false, HelpText = "ExecuteAndContinue", ResourceType = typeof(LocalizableSentenceBuilder))]
public string? ExecuteAndContinue { get; set; }
- [Option("clearhistory", Required = false, HelpText = "ClearHistory", ResourceType = typeof(LocalizableSentenceBuilder))]
public bool ClearHistory { get; set; }
- [Option("connect", Required = false, HelpText = "ConnectionString", ResourceType = typeof(LocalizableSentenceBuilder))]
public string? ConnectionString { get; set; }
- [Option("connect-mode", Required = false, HelpText = "ConnectionMode", ResourceType = typeof(LocalizableSentenceBuilder))]
public ConnectionMode? ConnectionMode { get; set; }
- [Option("connect-tenant", Required = false, HelpText = "ConnectTenant", ResourceType = typeof(LocalizableSentenceBuilder))]
public string? ConnectTenant { get; set; }
- [Option("connect-hint", Required = false, HelpText = "ConnectHint", ResourceType = typeof(LocalizableSentenceBuilder))]
public string? ConnectHint { get; set; }
- [Option("connect-authority-host", Required = false, HelpText = "ConnectAuthorityHost", ResourceType = typeof(LocalizableSentenceBuilder))]
public string? ConnectAuthorityHost { get; set; }
- [Option("connect-managed-identity", Required = false, HelpText = "ConnectManagedIdentity", ResourceType = typeof(LocalizableSentenceBuilder))]
public string? ConnectManagedIdentity { get; set; }
- [Option("connect-vscode-credential", Required = false, HelpText = "ConnectVSCodeCredential", ResourceType = typeof(LocalizableSentenceBuilder), Hidden = true)]
public bool ConnectVSCodeCredential { get; set; }
- [Option("mcp", Required = false, HelpText = "McpPort", ResourceType = typeof(LocalizableSentenceBuilder))]
public int? McpPort { get; set; }
- [Option("lsp", Required = false, HelpText = "EnableLspServer", ResourceType = typeof(LocalizableSentenceBuilder))]
public bool StartLspServer { get; set; }
- [Option("stdio", Required = false, HelpText = "EnableLspServer", ResourceType = typeof(LocalizableSentenceBuilder), Hidden = true)]
public bool LspStdio { get; set; }
- [Option("verbose", Required = false, HelpText = "Verbose", ResourceType = typeof(LocalizableSentenceBuilder))]
public bool Verbose { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/CosmosDBShell/lang/en.ftl b/CosmosDBShell/lang/en.ftl
index 50c60b8..3277e8a 100644
--- a/CosmosDBShell/lang/en.ftl
+++ b/CosmosDBShell/lang/en.ftl
@@ -436,6 +436,12 @@ command-version-repo = Report issues at [link={ $url }]{ $url }[/]
help-RequiredWord = Required.
help-ErrorsHeadingText = ERROR(S):
help-UsageHeadingText = USAGE:
+help-UsageSynopsis = { $command } [options] [-c|-k ...]
+help-CommandTailNote = Everything after -c / -k (or /c, /k) is taken as the command (no quoting needed). App-level options must come before -c / -k.
+help-OptionsHeadingText = OPTIONS:
+help-NotesHeadingText = NOTES:
+help-HelpOptionDescription = Show this help text and exit.
+help-VersionOptionDescription = Show product version and exit.
help-OptionGroupWord = Group
help-HelpCommandScreenText = Display this help screen.
help-HelpCommandMoreText = Display more information on a specific command.
@@ -450,6 +456,7 @@ help-SentenceMutuallyExclusiveSetErrors =
help-error-BadFormatTokenError = Token '{ $token }' is not recognized.
help-error-MissingValueOptionError = Option '{ $option }' has no value.
help-error-UnknownOptionError = Option '{ $option }' is unknown.
+help-error-UnknownArgumentError = Unrecognized argument '{ $argument }'.
help-error-MissingRequiredOptionError1 = A required value not bound to option name is missing.
help-error-MissingRequiredOptionError2 = Required option '{ $option }' is missing.
help-error-BadFormatConversionError1 = A value not bound to option name is defined with a bad format.
@@ -463,9 +470,9 @@ help-error-SetValueExceptionError = Error setting value to option '{ $option }':
help-error-MissingGroupOptionError = At least one option from group '{ $option }"' ({ $req_options }) is required.
help-error-GroupOptionAmbiguityError= Both SetName and Group are not allowed in option: ({ $option })
-help-ExecuteAndContinue = Executes the specified command and keeps the shell running (for example: /k "help").
-help-ExecuteAndQuit = Executes the specified command and exits the shell (for example: /c "help").
-help-ColorSystem = ColorSystem to use.(0=off, 1=standard, 2=true color)
+help-ExecuteAndContinue = Execute the specified command, then keep the shell running.
+help-ExecuteAndQuit = Execute the specified command, then exit.
+help-ColorSystem = Color system: 0=off, 1=standard, 2=true color (default: 2).
help-ClearHistory = Clears command history and exits.
help-ConnectionString = The endpoint URL or connection string to connect to.
help-ConnectionMode = Connection mode: 'direct' (default) or 'gateway'
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 38f24b1..20c4575 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,7 +6,7 @@
-
+
diff --git a/README.md b/README.md
index 2aeccfe..31ed833 100644
--- a/README.md
+++ b/README.md
@@ -123,14 +123,18 @@ Packaging runs produce preview versions in the form `1.0.-preview.`
| `--connect-managed-identity ` | Use a user-assigned managed identity |
| `--mcp [port]` | Enable MCP server on the given port, or `6128` by default |
| `--verbose` | Print full exception details |
-| `--cs ` | Colors: 0=off, 1=standard, 2=truecolor |
+| `--color-system ` | Colors: 0=off, 1=standard, 2=truecolor (alias: `--cs`) |
| `--help` | Show help |
Examples:
```bash
# Run a script and exit. Script arguments become $1, $2, ... inside the script.
-cosmosdbshell -c "seed.csh mydb mycontainer" --connect "AccountEndpoint=...;AccountKey=..."
+cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." -c "seed.csh mydb mycontainer"
+
+# -c also accepts an unquoted command tail; everything after -c becomes the
+# command, so app-level options (like --connect) must come BEFORE -c.
+cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." -c seed.csh mydb mycontainer
# Run a script from piped command text.
echo "seed.csh mydb mycontainer" | cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..."
diff --git a/docs/navigation.md b/docs/navigation.md
index 59c4976..e3d2126 100644
--- a/docs/navigation.md
+++ b/docs/navigation.md
@@ -207,8 +207,8 @@ Start the shell with options to customize behavior:
| Option | Description |
| ------ | ----------- |
-| `-c ` | Execute command and exit |
-| `-k ` | Execute command and stay in shell |
+| `-c ` | Execute command and exit. Everything after `-c` is taken as the command, so app-level options must come before `-c`. Windows-style `/c` is also accepted. |
+| `-k ` | Execute command and stay in shell. Everything after `-k` is taken as the command, so app-level options must come before `-k`. Windows-style `/k` is also accepted. |
| `--connect ` | Connect with this connection string or endpoint on startup |
| `--connect-mode ` | Connection mode at startup: 'direct' or 'gateway' |
| `--connect-tenant ` | Entra ID tenant ID at startup |
@@ -216,8 +216,8 @@ Start the shell with options to customize behavior:
| `--connect-authority-host ` | Authority host URL at startup |
| `--connect-managed-identity ` | User-assigned managed identity client ID at startup |
| `--mcp [port]` | Enable MCP (Model Context Protocol) server on the given port, or `6128` by default |
-| `--cs ` | Color scheme: 0=off, 1=standard, 2=truecolor |
-| `--clearhistory` | Clear command history on start |
+| `--color-system ` | Color scheme: 0=off, 1=standard, 2=truecolor (alias: `--cs`) |
+| `--clear-history` | Clear command history on start |
| `--help` | Show usage information |
| `--version` | Show version |
diff --git a/docs/programming.md b/docs/programming.md
index 59e88c8..a18da87 100644
--- a/docs/programming.md
+++ b/docs/programming.md
@@ -115,13 +115,19 @@ Use `-k` to run a command or script and then stay in the interactive shell:
cosmosdbshell -k "seed.csh \"AccountEndpoint=...;AccountKey=...\" mydb mycontainer"
```
-Startup connection options still belong to the shell process, not to the script:
+Startup connection options still belong to the shell process, not to the script. Because everything after `-c` / `-k` is captured as the command, place app-level options before `-c` / `-k`:
```bash
-cosmosdbshell -c "seed.csh mydb mycontainer" --connect "AccountEndpoint=...;AccountKey=..."
+cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." -c "seed.csh mydb mycontainer"
```
-If you want a value such as `--connect` to be passed to the script, put it inside the `-c` or `-k` command text:
+Quotes around the command are optional — the shell joins all remaining tokens after `-c` / `-k` into a single command string:
+
+```bash
+cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." -c seed.csh mydb mycontainer
+```
+
+If you want a value such as `--connect` to be passed to the script, put it inside the `-c` or `-k` command text (after `-c` everything goes to the script anyway):
```bash
cosmosdbshell -c "seed.csh --connect xyz"