Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions changelog.d/unreleased/1693.fixed.md
Original file line number Diff line number Diff line change
@@ -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 <command> --help` now shows the matching command's synopsis instead of the full top-level help block.

## 日本語

- **サブコマンド help がコマンド別の usage を表示するようになりました (#1693)** — `cdidx <command> --help` はトップレベル help 全体ではなく、対象コマンドの synopsis を表示するようになりました。
2 changes: 1 addition & 1 deletion src/CodeIndex/Cli/CliFlagSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion src/CodeIndex/Cli/ConsoleUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
("languages", "cdidx languages [--json]"),
("batch", "cdidx batch [--db <path>] # reads JSON string arrays from stdin, one query command per line"),
("mcp", "cdidx mcp [--db <path>]"),
("completions", "cdidx completions <shell>"),
("license", "cdidx license"),
];

Expand Down Expand Up @@ -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<string> GetCommandUsageLines(string command)
{
var usages = new List<string>();
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 / もしかして ---

/// <summary>
Expand Down Expand Up @@ -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",
];

/// <summary>
Expand Down
40 changes: 29 additions & 11 deletions src/CodeIndex/Cli/ProgramRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,23 +87,40 @@ 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;
}

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;
Expand Down Expand Up @@ -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} <shell>";
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 <shell>");
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 <shell>");
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 <shell>");
usage);

if (ConsoleUi.PrintCompletions(cmdArgs[0]))
return CommandExitCodes.Success;
Expand All @@ -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 <shell>");
usage);
}

private static string StripErrorPrefix(string message)
Expand Down
49 changes: 49 additions & 0 deletions tests/CodeIndex.Tests/ProgramCliTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,55 @@ public void Completions_OptionLikeShellTokenReturnsUsageError()
Assert.DoesNotContain("Unknown shell", stderr);
}

[Theory]
[InlineData("index", "cdidx index <projectPath>")]
[InlineData("search", "cdidx search <query>")]
[InlineData("references", "cdidx references <query>")]
[InlineData("callers", "cdidx callers <query>")]
[InlineData("callees", "cdidx callees <query>")]
[InlineData("impact", "cdidx impact <query>")]
[InlineData("unused", "cdidx unused")]
[InlineData("validate", "cdidx validate")]
[InlineData("backfill-fold", "cdidx backfill-fold")]
[InlineData("outline", "cdidx outline <path>")]
[InlineData("inspect", "cdidx inspect <query>")]
[InlineData("definition", "cdidx definition <query>")]
[InlineData("find", "cdidx find <query>")]
[InlineData("excerpt", "cdidx excerpt <path>")]
[InlineData("hotspots", "cdidx hotspots")]
[InlineData("deps", "cdidx deps")]
[InlineData("map", "cdidx map")]
[InlineData("status", "cdidx status")]
[InlineData("completions", "cdidx completions <shell>")]
[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 <shell>", stderr);
Assert.DoesNotContain("Usage: cdidx --completions <shell>", stderr);
}

[Fact]
public void Completions_ExtraArgsReturnUsageError()
{
Expand Down
Loading