diff --git a/changelog.d/unreleased/1693.fixed.md b/changelog.d/unreleased/1693.fixed.md new file mode 100644 index 0000000000..b294d7c3a7 --- /dev/null +++ b/changelog.d/unreleased/1693.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 1693 +affected: + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/ProgramRunner.cs + - tests/CodeIndex.Tests/ProgramCliTests.cs +--- + +## English + +- **Subcommand help now prints command-specific usage (#1693)** — `cdidx --help` now shows the matching command's synopsis instead of the full top-level help block. + +## 日本語 + +- **サブコマンド help がコマンド別の usage を表示するようになりました (#1693)** — `cdidx --help` はトップレベル help 全体ではなく、対象コマンドの synopsis を表示するようになりました。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 581d1748e5..db167b5220 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -54,7 +54,7 @@ internal static class CliFlagSchema [ "index", "backfill-fold", "optimize", "search", "definition", "references", "callers", "callees", "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status", - "validate", "deps", "impact", "unused", "hotspots", "languages", "batch", "mcp", "db", "vacuum", "report", "license", + "validate", "deps", "impact", "unused", "hotspots", "languages", "batch", "mcp", "completions", "db", "vacuum", "report", "license", ]; // Commands that accept the `--` end-of-options marker so a user can pass a literal diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 6be1e0d37d..3c72f7dbe3 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -93,6 +93,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("languages", "cdidx languages [--json]"), ("batch", "cdidx batch [--db ] # reads JSON string arrays from stdin, one query command per line"), ("mcp", "cdidx mcp [--db ]"), + ("completions", "cdidx completions "), ("license", "cdidx license"), ]; @@ -794,6 +795,35 @@ public static void PrintLicenseSummary() return null; } + public static bool PrintCommandUsage(string command) + { + var usages = GetCommandUsageLines(command); + if (usages.Count == 0) + return false; + + Console.WriteLine("Usage:"); + foreach (var usage in usages) + Console.WriteLine($" {usage}"); + Console.WriteLine(); + Console.WriteLine("Run `cdidx --help` to show all commands and shared options."); + return true; + } + + private static IReadOnlyList GetCommandUsageLines(string command) + { + var usages = new List(); + foreach (var (name, usage) in CommandUsageLines) + { + if (string.Equals(name, command, StringComparison.Ordinal) + || string.Equals(command, "index", StringComparison.Ordinal) && name.StartsWith("index-", StringComparison.Ordinal)) + { + usages.Add(usage); + } + } + + return usages; + } + // --- Did-you-mean / もしかして --- /// @@ -910,7 +940,7 @@ private static int DamerauLevenshteinDistance(string s, string t) [ "index", "backfill-fold", "optimize", "search", "definition", "references", "callers", "callees", "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status", - "validate", "deps", "impact", "unused", "hotspots", "languages", "batch", "mcp", "db", "vacuum", "report", "license", + "validate", "deps", "impact", "unused", "hotspots", "languages", "batch", "mcp", "completions", "db", "vacuum", "report", "license", ]; /// diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index 3678bcfb2f..bad92cfacb 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -87,15 +87,31 @@ internal static int Run( if (args[0] is "--license" or "license") { + if (args[0] == "license" && args.Length > 1 && ArgHelper.WantsHelp(args.AsSpan(1))) + { + ConsoleUi.PrintCommandUsage("license"); + GlobalToolLog.Info($"command_complete exit_code={CommandExitCodes.Success} subcommand_help=true"); + EmitCommandMetric("license", args, commandStartTimestamp, commandStopwatch, CommandExitCodes.Success); + return CommandExitCodes.Success; + } + ConsoleUi.PrintLicenseSummary(); GlobalToolLog.Info($"command_complete exit_code={CommandExitCodes.Success} license_only=true"); EmitCommandMetric("license", args, commandStartTimestamp, commandStopwatch, CommandExitCodes.Success); return CommandExitCodes.Success; } - if (args[0] == "--completions") + if (args[0] is "--completions" or "completions") { - var exitCode = RunCompletions(args[1..]); + if (args[0] == "completions" && args.Length > 1 && ArgHelper.WantsHelp(args.AsSpan(1))) + { + ConsoleUi.PrintCommandUsage("completions"); + GlobalToolLog.Info($"command_complete exit_code={CommandExitCodes.Success} subcommand_help=true"); + EmitCommandMetric("completions", args, commandStartTimestamp, commandStopwatch, CommandExitCodes.Success); + return CommandExitCodes.Success; + } + + var exitCode = RunCompletions(args[1..], args[0] == "completions" ? "completions" : "--completions"); GlobalToolLog.Info($"command_complete exit_code={exitCode} command=completions"); EmitCommandMetric("completions", args, commandStartTimestamp, commandStopwatch, exitCode); return exitCode; @@ -103,7 +119,8 @@ internal static int Run( if (args.Length > 1 && ArgHelper.WantsHelp(args.AsSpan(1))) { - ConsoleUi.PrintUsage(showBanner: true); + if (!ConsoleUi.PrintCommandUsage(args[0])) + ConsoleUi.PrintUsage(showBanner: true); GlobalToolLog.Info($"command_complete exit_code={CommandExitCodes.Success} subcommand_help=true"); EmitCommandMetric(args[0], args, commandStartTimestamp, commandStopwatch, CommandExitCodes.Success); return CommandExitCodes.Success; @@ -1078,28 +1095,29 @@ internal static string FormatVersionLine(ConsoleUi.BuildMetadata metadata, strin return $"cdidx v{metadata.Version} (commit {commit}, built {buildDate}, {dirty}){suffix}"; } - private static int RunCompletions(string[] cmdArgs) + private static int RunCompletions(string[] cmdArgs, string commandName = "--completions") { + var usage = $"cdidx {commandName} "; if (cmdArgs.Length == 0) return CommandErrorWriter.Write( - "--completions requires a shell value.", + $"{commandName} requires a shell value.", CommandExitCodes.UsageError, "rerun with one of `bash`, `zsh`, `fish`, or `powershell`.", - "cdidx --completions "); + usage); if (cmdArgs[0].StartsWith("-", StringComparison.Ordinal)) return CommandErrorWriter.Write( - $"--completions requires a shell value, got option-like token '{cmdArgs[0]}'.", + $"{commandName} requires a shell value, got option-like token '{cmdArgs[0]}'.", CommandExitCodes.UsageError, "rerun with one of `bash`, `zsh`, `fish`, or `powershell`.", - "cdidx --completions "); + usage); if (cmdArgs.Length > 1) return CommandErrorWriter.Write( - $"--completions accepts exactly one shell value, got extra {ConsoleUi.Counted(cmdArgs.Length - 1, "argument")}: {string.Join(", ", cmdArgs.Skip(1).Select(arg => $"`{arg}`"))}.", + $"{commandName} accepts exactly one shell value, got extra {ConsoleUi.Counted(cmdArgs.Length - 1, "argument")}: {string.Join(", ", cmdArgs.Skip(1).Select(arg => $"`{arg}`"))}.", CommandExitCodes.UsageError, "rerun with exactly one shell name: `bash`, `zsh`, `fish`, or `powershell`.", - "cdidx --completions "); + usage); if (ConsoleUi.PrintCompletions(cmdArgs[0])) return CommandExitCodes.Success; @@ -1108,7 +1126,7 @@ private static int RunCompletions(string[] cmdArgs) $"unsupported completion shell `{cmdArgs[0]}`.", CommandExitCodes.UsageError, "rerun with one of `bash`, `zsh`, `fish`, or `powershell`.", - "cdidx --completions "); + usage); } private static string StripErrorPrefix(string message) diff --git a/tests/CodeIndex.Tests/ProgramCliTests.cs b/tests/CodeIndex.Tests/ProgramCliTests.cs index a3e3517ec5..4d258685f1 100644 --- a/tests/CodeIndex.Tests/ProgramCliTests.cs +++ b/tests/CodeIndex.Tests/ProgramCliTests.cs @@ -128,6 +128,55 @@ public void Completions_OptionLikeShellTokenReturnsUsageError() Assert.DoesNotContain("Unknown shell", stderr); } + [Theory] + [InlineData("index", "cdidx index ")] + [InlineData("search", "cdidx search ")] + [InlineData("references", "cdidx references ")] + [InlineData("callers", "cdidx callers ")] + [InlineData("callees", "cdidx callees ")] + [InlineData("impact", "cdidx impact ")] + [InlineData("unused", "cdidx unused")] + [InlineData("validate", "cdidx validate")] + [InlineData("backfill-fold", "cdidx backfill-fold")] + [InlineData("outline", "cdidx outline ")] + [InlineData("inspect", "cdidx inspect ")] + [InlineData("definition", "cdidx definition ")] + [InlineData("find", "cdidx find ")] + [InlineData("excerpt", "cdidx excerpt ")] + [InlineData("hotspots", "cdidx hotspots")] + [InlineData("deps", "cdidx deps")] + [InlineData("map", "cdidx map")] + [InlineData("status", "cdidx status")] + [InlineData("completions", "cdidx completions ")] + [InlineData("license", "cdidx license")] + public void SubcommandHelp_PrintsCommandSpecificUsage(string command, string expectedUsage) + { + var (exitCode, stdout, stderr) = RunCliInSubprocess([command, "--help"]); + + Assert.Equal(0, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("Usage:", stdout); + Assert.Contains(expectedUsage, stdout); + Assert.Contains("Run `cdidx --help`", stdout); + Assert.DoesNotContain("Commands:", stdout); + Assert.DoesNotContain("Index and update options:", stdout); + Assert.DoesNotContain("██████╗", stdout); + } + + [Theory] + [InlineData("completions")] + [InlineData("completions", "--json")] + [InlineData("completions", "bash", "extra")] + public void CompletionsCommand_ErrorsUseCommandUsage(params string[] args) + { + var (exitCode, stdout, stderr) = RunCliInSubprocess(args); + + Assert.Equal(1, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("Usage: cdidx completions ", stderr); + Assert.DoesNotContain("Usage: cdidx --completions ", stderr); + } + [Fact] public void Completions_ExtraArgsReturnUsageError() {