diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 335a9d32a9..32d7349611 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -282,7 +282,7 @@ sections below show examples and option details for the most common workflows. | Analysis | `hotspots` | Rank high-impact symbols or statements by reference volume | `symbol_hotspots` | | Analysis | `validate` | Report encoding and line-ending issues in indexed files; U+FFFD rows include origin/severity metadata | `validate` | | Status | `status` | Show DB statistics, freshness, and readiness metadata | `status` | -| Status | `languages` | List language extensions and symbol/graph capabilities | `languages` | +| Status | `languages` | List language extensions and symbol/graph capabilities; add `--indexed-only` and `--capability graph|symbols|references` for workspace audits | `languages` | | Diagnostics | `db --integrity-check` | Run SQLite `PRAGMA integrity_check` against the DB | -- | | Diagnostics | `report --output ` | Build a redacted bug-report bundle | -- | | Feedback | `suggestions` | List, inspect, and export local suggestion history | -- | @@ -1531,7 +1531,7 @@ The database reflects the working tree at the time of the last index. After swit ## Supported languages -All indexed languages are searchable through FTS5. Rows with **Symbols = yes** also support structured queries by function, class, import, or language-specific symbol name. +All indexed languages are searchable through FTS5. Rows with **Symbols = yes** also support structured queries by function, class, import, or language-specific symbol name. Use `cdidx languages --indexed-only --json` to list only languages present in the current DB, and add `--capability graph|symbols|references` to narrow the table to languages that support a specific structured capability. | Language | Extensions | Symbols | |---|---|:---:| @@ -1627,9 +1627,11 @@ All indexed languages are searchable through FTS5. Rows with **Symbols = yes** a ### Language extraction matrix -Use `cdidx languages --json` as the live capability probe. This matrix explains -the common extraction behavior so users know when to trust structured commands -and when to fall back to `search`. +Use `cdidx languages --json` as the live capability probe. Add `--indexed-only` +when you only want languages present in the current DB, and add +`--capability graph|symbols|references` when auditing a specific structured +capability. This matrix explains the common extraction behavior so users know +when to trust structured commands and when to fall back to `search`. | Language family | Symbols | References / graph | Notes and example query | |---|---|---|---| @@ -3820,7 +3822,9 @@ indexing はファイル単位の SQLite transaction を commit します。長 ### 言語別 extraction matrix 現在の capability は `cdidx languages --json` を live probe として確認してください。 -この matrix は、構造化 command を信頼できる場面と `search` に戻るべき場面を判断するための概要です。 +現在の DB に存在する言語だけを見たい場合は `--indexed-only`、特定の構造化 capability を監査する場合は +`--capability graph|symbols|references` を追加します。この matrix は、構造化 command を信頼できる場面と +`search` に戻るべき場面を判断するための概要です。 | 言語ファミリ | Symbols | References / graph | メモと例 | |---|---|---|---| @@ -4198,7 +4202,7 @@ OpenAI Codex CLI (`codex.json` または `~/.codex/config.json`): | `symbol_hotspots` | 影響の大きい hotspot を検索。`groupBy` は `symbol` / `file` / `statement` を指定でき、SQL scope は statement grouping、非 SQL scope は symbol grouping が既定。 | | `batch_query` | 複数クエリを1回で実行(MCP専用、最大10件)。レスポンスにはトップレベル `metadata`(`submitted` / `executed` / `errors` / `total_elapsed_ms` / `success_count` / `failure_count`)と各 `results` エントリの `request_index` / `ok` / `elapsed_ms` / `args_summary` が含まれ、位置だけに依存せず部分失敗や遅い内部クエリを把握できます。 | | `validate` | エンコーディング問題(origin/severity 付き U+FFFD、BOM、null バイト、改行混在 / CR-only 行末、UTF-16 BOM 検出、UTF-8 以外と推定されるエンコーディング)を報告 | -| `languages` | 対応言語一覧を拡張子・機能付きで表示 | +| `languages` | 対応言語一覧を拡張子・機能付きで表示。`--indexed-only` と `--capability graph|symbols|references` で現在の DB や機能別に絞り込み可能 | | `ping` | 軽量な接続確認 | | `index` | プロジェクトのインデックス作成・更新 | | `backfill_fold` | 既存 DB の folded-name key をソース再解析なしで更新 | diff --git a/changelog.d/unreleased/3000.added.md b/changelog.d/unreleased/3000.added.md new file mode 100644 index 0000000000..f8b81debff --- /dev/null +++ b/changelog.d/unreleased/3000.added.md @@ -0,0 +1,18 @@ +--- +category: added +issues: + - 3000 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - USER_GUIDE.md +--- + +## English + +- **`languages` can now filter by indexed languages and capability (#3000)** — `cdidx languages` accepts `--indexed-only` to show only languages present in the current database and `--capability graph|symbols|references` to narrow language audits to a specific structured capability. + +## 日本語 + +- **`languages` が indexed language と capability で絞り込めるようになりました (#3000)** — `cdidx languages` は現在のデータベースに存在する言語だけを表示する `--indexed-only` と、特定の構造化 capability に絞る `--capability graph|symbols|references` を受け付けるようになりました。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index d2b58a0674..cd32f0c8bb 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -121,6 +121,7 @@ internal static class CliFlagSchema private static readonly string[] EntrypointConfidenceCommands = ["map"]; private static readonly string[] MapSectionCommands = ["map"]; private static readonly string[] DependencyCycleCommands = ["deps"]; + private static readonly string[] LanguagesFilterCommands = ["languages"]; // `--exact` is the legacy shorthand that every name-resolution command accepts. // `--exact` は名前解決系の全コマンドで受け付けるレガシー shorthand。 @@ -154,7 +155,7 @@ internal static class CliFlagSchema [ "index", "backfill-fold", "optimize", "search", "definition", "goto", "references", "callers", "callees", "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status", - "validate", "deps", "impact", "unused", "hotspots", "db", "vacuum", "report", "batch", "mcp", + "validate", "deps", "impact", "unused", "hotspots", "languages", "db", "vacuum", "report", "batch", "mcp", ]; private static readonly string[] WorkspaceDbCommands = ["deps"]; @@ -163,14 +164,14 @@ internal static class CliFlagSchema [ "index", "search", "definition", "goto", "references", "callers", "callees", "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status", - "validate", "deps", "impact", "unused", "hotspots", "batch", + "validate", "deps", "impact", "unused", "hotspots", "languages", "batch", ]; private static readonly string[] ReadOnlyDbCommands = [ "search", "definition", "goto", "references", "callers", "callees", "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status", - "validate", "deps", "impact", "unused", "hotspots", + "validate", "deps", "impact", "unused", "hotspots", "languages", ]; private static readonly string[] JsonCommands = @@ -239,6 +240,8 @@ private static IReadOnlyList BuildAll() new() { Name = "--min-entrypoint-confidence", ValuePlaceholder = "<0.0..1.0>", Description = "Map: omit entrypoint candidates below this confidence", Commands = Set(EntrypointConfidenceCommands) }, new() { Name = "--sections", ValuePlaceholder = "", Description = "Map: comma-separated response sections to include", Commands = Set(MapSectionCommands) }, new() { Name = "--cycles", Description = "Deps: return dependency cycles instead of edge rows", Commands = Set(DependencyCycleCommands) }, + new() { Name = "--indexed-only", Description = "Languages: list only languages present in the current index", Commands = Set(LanguagesFilterCommands) }, + new() { Name = "--capability", ValuePlaceholder = "", Description = "Languages: filter by language capability", Commands = Set(LanguagesFilterCommands) }, new() { Name = "--query", ValuePlaceholder = "", Description = "Literal query", Commands = Set(QueryCommands) }, new() { Name = "--body", Description = "Include body", Commands = Set(BodyCommands) }, new() { Name = "--exact", Description = "Backward-compatible exact shorthand", Commands = Set(ExactCommands) }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 4d37b06056..a4802a25c2 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -108,7 +108,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("export", "cdidx export [--db ] [--json]"), ("export", "cdidx export ctags [--output ] [--db ]"), ("import", "cdidx import [--db ] [--prune-paths] [--json]"), - ("languages", "cdidx languages [--json]"), + ("languages", "cdidx languages [--db ] [--json] [--indexed-only] [--capability ]"), ("batch", "cdidx batch [--db ] # reads JSON string arrays from stdin, one query command per line; max 1,048,576 chars/line and 256 arguments"), ("mcp", "cdidx mcp [--db ] [--transport stdio|http] [--http-listen ] [--audit-log ] [--audit-log-include-values] [--audit-log-max-bytes ] [--suggestion-dedup-threshold <0..1>]"), ("lsp", "cdidx lsp [--db ]"), diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 5b43fc59eb..cb86def1ca 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -28,6 +28,9 @@ public static class QueryCommandRunner internal const string DefaultSnippetLinesEnvironmentVariable = "CDIDX_DEFAULT_SNIPPET_LINES"; internal const string DefaultMaxLineWidthEnvironmentVariable = "CDIDX_DEFAULT_MAX_LINE_WIDTH"; internal const string StaleAfterEnvironmentVariable = "CDIDX_STALE_AFTER"; + private const string LanguageCapabilityGraph = "graph"; + private const string LanguageCapabilityReferences = "references"; + private const string LanguageCapabilitySymbols = "symbols"; internal static readonly TimeSpan DefaultStaleAfter = TimeSpan.FromHours(24); internal static TimeProvider TimeProvider { get; set; } = TimeProvider.System; [ThreadStatic] @@ -5019,13 +5022,13 @@ public static int RunLanguages(string[] cmdArgs, JsonSerializerOptions jsonOptio // Build a consolidated view: language -> (extensions, hasSymbols, hasGraph) // 統合ビュー: 言語 -> (拡張子, シンボル対応, グラフ対応) - var allLangs = new Dictionary Extensions, List Aliases, bool Symbols, bool Graph)>(StringComparer.Ordinal); + var allLangs = new Dictionary(StringComparer.Ordinal); foreach (var (ext, lang) in langExtensions) { if (!allLangs.TryGetValue(lang, out var info)) { - info = (new List(), GetLanguageAliases(lang).ToList(), symbolLangs.Contains(lang), graphLangs.Contains(lang)); + info = new LanguageSupportInfo([], GetLanguageAliases(lang).ToList(), symbolLangs.Contains(lang), graphLangs.Contains(lang)); allLangs[lang] = info; } info.Extensions.Add(ext); @@ -5034,48 +5037,84 @@ public static int RunLanguages(string[] cmdArgs, JsonSerializerOptions jsonOptio // Sort by language name / 言語名でソート var sorted = allLangs.OrderBy(kv => kv.Key).ToList(); - if (json) + if (options.LanguagesIndexedOnly) { - var entries = sorted.Select(kv => new LanguageEntryJsonResult( - kv.Key, - kv.Value.Extensions.OrderBy(e => e).ToList(), - kv.Value.Aliases.OrderBy(a => a).ToList(), - kv.Value.Symbols, - kv.Value.Graph)).ToList(); - Console.WriteLine(JsonSerializer.Serialize(new LanguagesJsonResult(entries), CliJsonSerializerContextFactory.Create(jsonOptions).LanguagesJsonResult)); + return WithDb(options, jsonOptions, reader => + { + var indexedLanguages = new HashSet(reader.GetStatus().Languages.Keys, StringComparer.Ordinal); + return WriteLanguages(sorted.Where(kv => indexedLanguages.Contains(kv.Key))); + }); } - else + + return WriteLanguages(sorted); + + int WriteLanguages(IEnumerable> languages) { - // Fixed-width Extensions column for short lists; spill long lists onto a continuation - // line so the Symbols / Graph columns are never swallowed by a wide extension string. - // 拡張子が短い場合は固定幅テーブル、長い場合は継続行に退避させることで、 - // Symbols / Graph 列が拡張子文字列に埋もれないようにする。 - const int ExtensionColumnWidth = 36; - const int AliasColumnWidth = 12; - Console.WriteLine($"{"Language",-14} {"Extensions",-36} {"Aliases",-12} {"Symbols",-9} {"Graph",-7}"); - Console.WriteLine(new string('-', 79)); - foreach (var (lang, info) in sorted) - { - var exts = string.Join(" ", info.Extensions.OrderBy(e => e)); - var aliases = string.Join(" ", info.Aliases.OrderBy(a => a)); - var aliasCell = string.IsNullOrWhiteSpace(aliases) ? "-" : aliases; - var sym = info.Symbols ? "yes" : "-"; - var graph = info.Graph ? "yes" : "-"; - if (exts.Length <= ExtensionColumnWidth && aliases.Length <= AliasColumnWidth) - { - Console.WriteLine($"{lang,-14} {exts,-36} {aliasCell,-12} {sym,-9} {graph,-7}"); - } - else - { - Console.WriteLine($"{lang,-14} {"",-36} {"",-12} {sym,-9} {graph,-7}"); - Console.WriteLine($" Extensions: {exts}"); - if (!string.IsNullOrWhiteSpace(aliases)) - Console.WriteLine($" Aliases: {aliases}"); + var filtered = languages + .Where(kv => options.LanguageCapabilities.All(capability => LanguageMatchesCapability(kv.Value, capability))) + .OrderBy(kv => kv.Key, StringComparer.Ordinal) + .ToList(); + + if (json) + { + var entries = filtered.Select(kv => new LanguageEntryJsonResult( + kv.Key, + kv.Value.Extensions.OrderBy(e => e).ToList(), + kv.Value.Aliases.OrderBy(a => a).ToList(), + kv.Value.Symbols, + kv.Value.Graph)).ToList(); + Console.WriteLine(JsonSerializer.Serialize(new LanguagesJsonResult(entries), CliJsonSerializerContextFactory.Create(jsonOptions).LanguagesJsonResult)); + } + else + { + // Fixed-width Extensions column for short lists; spill long lists onto a continuation + // line so the Symbols / Graph columns are never swallowed by a wide extension string. + // 拡張子が短い場合は固定幅テーブル、長い場合は継続行に退避させることで、 + // Symbols / Graph 列が拡張子文字列に埋もれないようにする。 + const int ExtensionColumnWidth = 36; + const int AliasColumnWidth = 12; + Console.WriteLine($"{"Language",-14} {"Extensions",-36} {"Aliases",-12} {"Symbols",-9} {"Graph",-7}"); + Console.WriteLine(new string('-', 79)); + foreach (var (lang, info) in filtered) + { + var exts = string.Join(" ", info.Extensions.OrderBy(e => e)); + var aliases = string.Join(" ", info.Aliases.OrderBy(a => a)); + var aliasCell = string.IsNullOrWhiteSpace(aliases) ? "-" : aliases; + var sym = info.Symbols ? "yes" : "-"; + var graph = info.Graph ? "yes" : "-"; + if (exts.Length <= ExtensionColumnWidth && aliases.Length <= AliasColumnWidth) + { + Console.WriteLine($"{lang,-14} {exts,-36} {aliasCell,-12} {sym,-9} {graph,-7}"); + } + else + { + Console.WriteLine($"{lang,-14} {"",-36} {"",-12} {sym,-9} {graph,-7}"); + Console.WriteLine($" Extensions: {exts}"); + if (!string.IsNullOrWhiteSpace(aliases)) + Console.WriteLine($" Aliases: {aliases}"); + } } + Console.Error.WriteLine($"\n({filtered.Count} languages)"); } - Console.Error.WriteLine($"\n({sorted.Count} languages)"); + + return CommandExitCodes.Success; } - return CommandExitCodes.Success; + } + + private sealed record LanguageSupportInfo(List Extensions, List Aliases, bool Symbols, bool Graph); + + private static bool LanguageMatchesCapability(LanguageSupportInfo language, string capability) + => capability switch + { + LanguageCapabilitySymbols => language.Symbols, + LanguageCapabilityGraph or LanguageCapabilityReferences => language.Graph, + _ => false, + }; + + private static bool TryNormalizeLanguageCapability(string value, out string capability) + { + capability = value.Trim().ToLowerInvariant(); + return capability is LanguageCapabilityGraph or LanguageCapabilityReferences or LanguageCapabilitySymbols; } public static QueryCommandOptions ParseArgs( @@ -5157,6 +5196,8 @@ public static QueryCommandOptions ParseArgs( bool impactDeprecatedDepthUsed = false; List? mapSections = null; bool dependencyCycles = false; + bool languagesIndexedOnly = false; + var languageCapabilities = new List(); void AddParseError(string error) { @@ -5307,6 +5348,23 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) AddParseError($"Error: --json format must be one of ndjson or array, got '{inlineValue}'. Hint: use `--json` or `--json=ndjson` for newline-delimited JSON, or `--json=array` for a single JSON array."); } break; + case "--indexed-only": + languagesIndexedOnly = true; + break; + case "--capability": + if (!TryReadStringOptionValue(args, ref i, "--capability", inlineValue, allowSeparatedDashPrefixedLiteralValue: false, out var capabilityValue, out var capabilityError)) + { + AddParseError(capabilityError!); + } + else if (TryNormalizeLanguageCapability(capabilityValue!, out var capability)) + { + languageCapabilities.Add(capability); + } + else + { + AddParseError($"Error: unsupported --capability value '{capabilityValue}'. Use graph, symbols, or references."); + } + break; case "--format": if (TryReadStringOptionValue(args, ref i, "--format", inlineValue, allowSeparatedDashPrefixedLiteralValue: false, out var formatValue, out var formatError)) { @@ -5945,6 +6003,8 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) ExtraNames = extraNames, MapSections = mapSections, DependencyCycles = dependencyCycles, + LanguagesIndexedOnly = languagesIndexedOnly, + LanguageCapabilities = languageCapabilities, ParseError = parseErrors == null ? null : string.Join(Environment.NewLine, parseErrors), }; } @@ -8703,5 +8763,7 @@ public sealed class QueryCommandOptions public List ExtraNames { get; init; } = []; public List? MapSections { get; init; } public bool DependencyCycles { get; init; } + public bool LanguagesIndexedOnly { get; init; } + public List LanguageCapabilities { get; init; } = []; public string? ParseError { get; init; } } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index c0b38b30f0..940a471005 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -1566,8 +1566,7 @@ public void QueryEntrypoints_UnexpectedPositionalsReturnUsageError(string scenar [Theory] - [InlineData("--db", "/tmp/does-not-matter.db")] - [InlineData("--db=")] + [InlineData("--path", "src")] [InlineData("--mystery")] public void RunLanguages_UnsupportedOptionsReturnUsageError(string flag, string? value = null) { @@ -1584,6 +1583,123 @@ public void RunLanguages_UnsupportedOptionsReturnUsageError(string flag, string? Assert.DoesNotContain("Warning: unknown option", stderr); } + [Fact] + public void RunLanguages_JsonIndexedOnlyListsLanguagesPresentInDatabase() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_languages_indexed_only"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", "class App { }\n"); + TestProjectHelper.InsertIndexedFile(dbPath, "README.md", "markdown", "# App\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => + QueryCommandRunner.RunLanguages(["--json", "--indexed-only", "--db", dbPath], _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + + using var document = ParseJsonOutput(stdout); + var names = document.RootElement.GetProperty("languages").EnumerateArray() + .Select(lang => lang.GetProperty("lang").GetString()) + .ToList(); + + Assert.Equal(["csharp", "markdown"], names); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunLanguages_JsonIndexedOnlyCombinesWithCapabilityFilter() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_languages_indexed_capability"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", "class App { }\n"); + TestProjectHelper.InsertIndexedFile(dbPath, "README.md", "markdown", "# App\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => + QueryCommandRunner.RunLanguages(["--json", "--indexed-only", "--capability", "graph", "--db", dbPath], _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + + using var document = ParseJsonOutput(stdout); + var names = document.RootElement.GetProperty("languages").EnumerateArray() + .Select(lang => lang.GetProperty("lang").GetString()) + .ToList(); + + Assert.Equal(["csharp"], names); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("graph")] + [InlineData("references")] + public void RunLanguages_JsonCapabilityGraphFiltersGraphSupport(string capability) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => + QueryCommandRunner.RunLanguages(["--json", "--capability", capability], _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + + using var document = ParseJsonOutput(stdout); + var languages = document.RootElement.GetProperty("languages").EnumerateArray().ToList(); + + Assert.NotEmpty(languages); + Assert.Contains(languages, lang => lang.GetProperty("lang").GetString() == "csharp"); + Assert.All(languages, lang => Assert.True(lang.GetProperty("graph_queries").GetBoolean())); + } + + [Fact] + public void RunLanguages_JsonCapabilitySymbolsFiltersSymbolSupport() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => + QueryCommandRunner.RunLanguages(["--json", "--capability", "symbols"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + + using var document = ParseJsonOutput(stdout); + var languages = document.RootElement.GetProperty("languages").EnumerateArray().ToList(); + + Assert.NotEmpty(languages); + Assert.Contains(languages, lang => lang.GetProperty("lang").GetString() == "html"); + Assert.DoesNotContain(languages, lang => lang.GetProperty("lang").GetString() == "msbuild"); + Assert.All(languages, lang => Assert.True(lang.GetProperty("symbol_extraction").GetBoolean())); + } + + [Fact] + public void RunLanguages_InvalidCapabilityReturnsUsageError() + { + var (exitCode, _, stderr) = CaptureConsole(() => + QueryCommandRunner.RunLanguages(["--capability", "lint"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("unsupported --capability value 'lint'", stderr); + Assert.Contains("graph, symbols, or references", stderr); + } + + [Fact] + public void RunLanguages_MissingCapabilityReturnsUsageError() + { + var (exitCode, _, stderr) = CaptureConsole(() => + QueryCommandRunner.RunLanguages(["--capability"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--capability", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("languages")}", stderr); + } + [Fact] public void RunLanguages_JsonListsModernNodeModuleExtensions() {