From 9f96e71d7c1b077303ae6b556f134c730f416ee0 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 15 Jun 2026 06:47:39 +0900 Subject: [PATCH 1/2] Split query batch runner for #3380 --- changelog.d/unreleased/3380.internal.md | 16 ++ src/CodeIndex/Cli/QueryCommandRunner.Batch.cs | 214 ++++++++++++++++++ src/CodeIndex/Cli/QueryCommandRunner.cs | 208 +---------------- 3 files changed, 231 insertions(+), 207 deletions(-) create mode 100644 changelog.d/unreleased/3380.internal.md create mode 100644 src/CodeIndex/Cli/QueryCommandRunner.Batch.cs diff --git a/changelog.d/unreleased/3380.internal.md b/changelog.d/unreleased/3380.internal.md new file mode 100644 index 0000000000..3d0f4b5540 --- /dev/null +++ b/changelog.d/unreleased/3380.internal.md @@ -0,0 +1,16 @@ +--- +category: internal +issues: + - 3380 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/QueryCommandRunner.Batch.cs +--- + +## English + +- **Split query batch orchestration into a focused partial (#3380)** — `QueryCommandRunner` now keeps batch-mode parsing, DB setup, and query dispatch in a dedicated partial without changing CLI behavior. + +## 日本語 + +- **query batch orchestration を focused partial に分割しました (#3380)** — `QueryCommandRunner` の batch mode parsing、DB setup、query dispatch を専用 partial に移し、CLI 挙動は変更していません。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Batch.cs b/src/CodeIndex/Cli/QueryCommandRunner.Batch.cs new file mode 100644 index 0000000000..a0e4ecb435 --- /dev/null +++ b/src/CodeIndex/Cli/QueryCommandRunner.Batch.cs @@ -0,0 +1,214 @@ +using System.Text; +using System.Text.Json; +using CodeIndex.Database; + +namespace CodeIndex.Cli; + +public static partial class QueryCommandRunner +{ + public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions) + { + var dbPath = Path.Combine(".cdidx", "codeindex.db"); + var dbPathExplicit = false; + for (var i = 0; i < cmdArgs.Length; i++) + { + var arg = cmdArgs[i]; + if (arg == "--db") + { + if (i + 1 >= cmdArgs.Length || string.IsNullOrWhiteSpace(cmdArgs[i + 1])) + { + Console.Error.WriteLine(BuildMissingOptionValueError("--db")); + return CommandExitCodes.UsageError; + } + dbPath = cmdArgs[++i]; + dbPathExplicit = true; + continue; + } + + if (arg.StartsWith("--db=", StringComparison.Ordinal)) + { + dbPath = arg["--db=".Length..]; + if (string.IsNullOrWhiteSpace(dbPath)) + { + Console.Error.WriteLine(BuildMissingOptionValueError("--db")); + return CommandExitCodes.UsageError; + } + dbPathExplicit = true; + continue; + } + + Console.Error.WriteLine($"Error: {ConsoleUi.FormatBoundedValue(arg)} is not supported for batch."); + Console.Error.WriteLine($"Usage: {ConsoleUi.GetUsageLine("batch")}"); + return CommandExitCodes.UsageError; + } + + var isUri = dbPath.StartsWith("file:", StringComparison.OrdinalIgnoreCase); + if (!isUri && !File.Exists(dbPath)) + { + Console.Error.WriteLine($"Error [{CommandErrorCodes.DbNotFound}]: database not found at {FormatDbDiagnosticValue(Path.GetFullPath(dbPath))}"); + Console.Error.WriteLine("Hint: create or refresh the index with `cdidx index ` (or `cdidx .`) and then rerun this command."); + return CommandExitCodes.DatabaseError; + } + + try + { + using var db = new DbContext(dbPath); + if (!db.TryValidateIsCodeIndexDb(out var validationReason)) + return WriteInvalidCodeIndexDbError(dbPath, validationReason); + + db.TryMigrateForRead(); + s_batchReader = new DbReader(db); + s_batchDbPath = dbPath; + s_batchDbPathExplicit = dbPathExplicit; + var firstFailure = CommandExitCodes.Success; + var lineNumber = 0; + while (TryReadBatchLine(Console.In, out var line, out var lineExceededLimit)) + { + lineNumber++; + if (lineExceededLimit) + { + Console.Error.WriteLine($"Error: batch line {lineNumber} exceeds the {BatchMaxLineChars} character limit."); + if (firstFailure == CommandExitCodes.Success) + firstFailure = CommandExitCodes.UsageError; + continue; + } + + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (!TryParseBatchLine(line, lineNumber, out var commandName, out var subArgs, out var parseExitCode)) + { + if (firstFailure == CommandExitCodes.Success) + firstFailure = parseExitCode; + continue; + } + + var exitCode = RunBatchQueryCommand(commandName, subArgs, jsonOptions); + if (exitCode != CommandExitCodes.Success && firstFailure == CommandExitCodes.Success) + firstFailure = exitCode; + } + + return firstFailure; + } + finally + { + s_batchReader = null; + s_batchDbPath = null; + s_batchDbPathExplicit = false; + } + } + + private static bool TryReadBatchLine(TextReader reader, out string? line, out bool exceededLimit) + { + line = null; + exceededLimit = false; + var builder = new StringBuilder(); + while (true) + { + var next = reader.Read(); + if (next < 0) + { + if (builder.Length == 0 && !exceededLimit) + return false; + line = exceededLimit ? string.Empty : builder.ToString(); + return true; + } + + var ch = (char)next; + if (ch == '\n') + { + line = exceededLimit ? string.Empty : builder.ToString(); + return true; + } + + if (exceededLimit) + continue; + if (builder.Length >= BatchMaxLineChars) + { + exceededLimit = true; + continue; + } + + builder.Append(ch); + } + } + + private static bool TryParseBatchLine(string line, int lineNumber, out string commandName, out string[] subArgs, out int exitCode) + { + commandName = string.Empty; + subArgs = []; + exitCode = CommandExitCodes.UsageError; + + try + { + using var document = JsonDocument.Parse(line, BatchJsonDocumentOptions); + if (document.RootElement.ValueKind != JsonValueKind.Array || document.RootElement.GetArrayLength() == 0) + { + Console.Error.WriteLine($"Error: batch line {lineNumber} must be a non-empty JSON string array."); + return false; + } + if (document.RootElement.GetArrayLength() > BatchMaxArgumentCount + 1) + { + Console.Error.WriteLine($"Error: batch line {lineNumber} must contain at most {BatchMaxArgumentCount} command arguments."); + return false; + } + + var values = new List(); + foreach (var element in document.RootElement.EnumerateArray()) + { + if (element.ValueKind != JsonValueKind.String) + { + Console.Error.WriteLine($"Error: batch line {lineNumber} must contain only strings."); + return false; + } + var value = element.GetString() ?? string.Empty; + if (value.Length > BatchMaxArgumentChars) + { + Console.Error.WriteLine($"Error: batch line {lineNumber} argument {values.Count + 1} exceeds the {BatchMaxArgumentChars} character limit."); + return false; + } + values.Add(value); + } + + commandName = values[0]; + subArgs = values.Skip(1).ToArray(); + return true; + } + catch (JsonException) + { + Console.Error.WriteLine($"Error [{CommandErrorCodes.UsageError}]: batch line {lineNumber} {SafeDiagnosticFormatter.FormatCategoryType("invalid_batch_json", nameof(JsonException))}."); + return false; + } + } + + private static int RunBatchQueryCommand(string commandName, string[] subArgs, JsonSerializerOptions jsonOptions) + => commandName switch + { + "search" => RunSearch(subArgs, jsonOptions), + "definition" => RunDefinition(subArgs, jsonOptions), + "references" => RunReferences(subArgs, jsonOptions), + "callers" => RunCallers(subArgs, jsonOptions), + "callees" => RunCallees(subArgs, jsonOptions), + "symbols" => RunSymbols(subArgs, jsonOptions), + "files" => RunFiles(subArgs, jsonOptions), + "find" => RunFind(subArgs, jsonOptions), + "excerpt" => RunExcerpt(subArgs, jsonOptions), + "map" => RunMap(subArgs, jsonOptions), + "inspect" => RunInspect(subArgs, jsonOptions), + "outline" => RunOutline(subArgs, jsonOptions), + "status" => RunStatus(subArgs, jsonOptions), + "validate" => RunValidate(subArgs, jsonOptions), + "impact" => RunImpact(subArgs, jsonOptions), + "deps" => RunDeps(subArgs, jsonOptions), + "unused" => RunUnused(subArgs, jsonOptions), + "hotspots" => RunHotspots(subArgs, jsonOptions), + _ => WriteBatchUnsupportedCommand(commandName), + }; + + private static int WriteBatchUnsupportedCommand(string commandName) + { + Console.Error.WriteLine($"Error: batch only supports query commands; '{commandName}' is not supported."); + Console.Error.WriteLine("Hint: use one of search, definition, references, callers, callees, symbols, files, find, excerpt, map, inspect, outline, status, validate, impact, deps, unused, or hotspots."); + return CommandExitCodes.UsageError; + } +} diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 8888b54688..e7a09183bc 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -17,7 +17,7 @@ namespace CodeIndex.Cli; /// Runs query-style CLI commands. /// クエリ系CLIコマンドを実行する。 /// -public static class QueryCommandRunner +public static partial class QueryCommandRunner { internal const int DefaultQueryLimit = 20; internal const int DefaultMapLimit = 10; @@ -343,212 +343,6 @@ private sealed record StatusReadinessField( StringComparer.Ordinal); private const string FindUsage = "Usage: cdidx find (--path |--all) [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--snippet-lines ] [--focus-line ] [--focus-column ] [--max-line-width ] [--exact] [--regex] [--count]\n cdidx find --query (--path |--all) [...]\n cdidx find [options] -- "; - public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions) - { - var dbPath = Path.Combine(".cdidx", "codeindex.db"); - var dbPathExplicit = false; - for (var i = 0; i < cmdArgs.Length; i++) - { - var arg = cmdArgs[i]; - if (arg == "--db") - { - if (i + 1 >= cmdArgs.Length || string.IsNullOrWhiteSpace(cmdArgs[i + 1])) - { - Console.Error.WriteLine(BuildMissingOptionValueError("--db")); - return CommandExitCodes.UsageError; - } - dbPath = cmdArgs[++i]; - dbPathExplicit = true; - continue; - } - - if (arg.StartsWith("--db=", StringComparison.Ordinal)) - { - dbPath = arg["--db=".Length..]; - if (string.IsNullOrWhiteSpace(dbPath)) - { - Console.Error.WriteLine(BuildMissingOptionValueError("--db")); - return CommandExitCodes.UsageError; - } - dbPathExplicit = true; - continue; - } - - Console.Error.WriteLine($"Error: {ConsoleUi.FormatBoundedValue(arg)} is not supported for batch."); - Console.Error.WriteLine($"Usage: {ConsoleUi.GetUsageLine("batch")}"); - return CommandExitCodes.UsageError; - } - - var isUri = dbPath.StartsWith("file:", StringComparison.OrdinalIgnoreCase); - if (!isUri && !File.Exists(dbPath)) - { - Console.Error.WriteLine($"Error [{CommandErrorCodes.DbNotFound}]: database not found at {FormatDbDiagnosticValue(Path.GetFullPath(dbPath))}"); - Console.Error.WriteLine("Hint: create or refresh the index with `cdidx index ` (or `cdidx .`) and then rerun this command."); - return CommandExitCodes.DatabaseError; - } - - try - { - using var db = new DbContext(dbPath); - if (!db.TryValidateIsCodeIndexDb(out var validationReason)) - return WriteInvalidCodeIndexDbError(dbPath, validationReason); - - db.TryMigrateForRead(); - s_batchReader = new DbReader(db); - s_batchDbPath = dbPath; - s_batchDbPathExplicit = dbPathExplicit; - var firstFailure = CommandExitCodes.Success; - var lineNumber = 0; - while (TryReadBatchLine(Console.In, out var line, out var lineExceededLimit)) - { - lineNumber++; - if (lineExceededLimit) - { - Console.Error.WriteLine($"Error: batch line {lineNumber} exceeds the {BatchMaxLineChars} character limit."); - if (firstFailure == CommandExitCodes.Success) - firstFailure = CommandExitCodes.UsageError; - continue; - } - - if (string.IsNullOrWhiteSpace(line)) - continue; - - if (!TryParseBatchLine(line, lineNumber, out var commandName, out var subArgs, out var parseExitCode)) - { - if (firstFailure == CommandExitCodes.Success) - firstFailure = parseExitCode; - continue; - } - - var exitCode = RunBatchQueryCommand(commandName, subArgs, jsonOptions); - if (exitCode != CommandExitCodes.Success && firstFailure == CommandExitCodes.Success) - firstFailure = exitCode; - } - - return firstFailure; - } - finally - { - s_batchReader = null; - s_batchDbPath = null; - s_batchDbPathExplicit = false; - } - } - - private static bool TryReadBatchLine(TextReader reader, out string? line, out bool exceededLimit) - { - line = null; - exceededLimit = false; - var builder = new StringBuilder(); - while (true) - { - var next = reader.Read(); - if (next < 0) - { - if (builder.Length == 0 && !exceededLimit) - return false; - line = exceededLimit ? string.Empty : builder.ToString(); - return true; - } - - var ch = (char)next; - if (ch == '\n') - { - line = exceededLimit ? string.Empty : builder.ToString(); - return true; - } - - if (exceededLimit) - continue; - if (builder.Length >= BatchMaxLineChars) - { - exceededLimit = true; - continue; - } - - builder.Append(ch); - } - } - - private static bool TryParseBatchLine(string line, int lineNumber, out string commandName, out string[] subArgs, out int exitCode) - { - commandName = string.Empty; - subArgs = []; - exitCode = CommandExitCodes.UsageError; - - try - { - using var document = JsonDocument.Parse(line, BatchJsonDocumentOptions); - if (document.RootElement.ValueKind != JsonValueKind.Array || document.RootElement.GetArrayLength() == 0) - { - Console.Error.WriteLine($"Error: batch line {lineNumber} must be a non-empty JSON string array."); - return false; - } - if (document.RootElement.GetArrayLength() > BatchMaxArgumentCount + 1) - { - Console.Error.WriteLine($"Error: batch line {lineNumber} must contain at most {BatchMaxArgumentCount} command arguments."); - return false; - } - - var values = new List(); - foreach (var element in document.RootElement.EnumerateArray()) - { - if (element.ValueKind != JsonValueKind.String) - { - Console.Error.WriteLine($"Error: batch line {lineNumber} must contain only strings."); - return false; - } - var value = element.GetString() ?? string.Empty; - if (value.Length > BatchMaxArgumentChars) - { - Console.Error.WriteLine($"Error: batch line {lineNumber} argument {values.Count + 1} exceeds the {BatchMaxArgumentChars} character limit."); - return false; - } - values.Add(value); - } - - commandName = values[0]; - subArgs = values.Skip(1).ToArray(); - return true; - } - catch (JsonException) - { - Console.Error.WriteLine($"Error [{CommandErrorCodes.UsageError}]: batch line {lineNumber} {SafeDiagnosticFormatter.FormatCategoryType("invalid_batch_json", nameof(JsonException))}."); - return false; - } - } - - private static int RunBatchQueryCommand(string commandName, string[] subArgs, JsonSerializerOptions jsonOptions) - => commandName switch - { - "search" => RunSearch(subArgs, jsonOptions), - "definition" => RunDefinition(subArgs, jsonOptions), - "references" => RunReferences(subArgs, jsonOptions), - "callers" => RunCallers(subArgs, jsonOptions), - "callees" => RunCallees(subArgs, jsonOptions), - "symbols" => RunSymbols(subArgs, jsonOptions), - "files" => RunFiles(subArgs, jsonOptions), - "find" => RunFind(subArgs, jsonOptions), - "excerpt" => RunExcerpt(subArgs, jsonOptions), - "map" => RunMap(subArgs, jsonOptions), - "inspect" => RunInspect(subArgs, jsonOptions), - "outline" => RunOutline(subArgs, jsonOptions), - "status" => RunStatus(subArgs, jsonOptions), - "validate" => RunValidate(subArgs, jsonOptions), - "impact" => RunImpact(subArgs, jsonOptions), - "deps" => RunDeps(subArgs, jsonOptions), - "unused" => RunUnused(subArgs, jsonOptions), - "hotspots" => RunHotspots(subArgs, jsonOptions), - _ => WriteBatchUnsupportedCommand(commandName), - }; - - private static int WriteBatchUnsupportedCommand(string commandName) - { - Console.Error.WriteLine($"Error: batch only supports query commands; '{commandName}' is not supported."); - Console.Error.WriteLine("Hint: use one of search, definition, references, callers, callees, symbols, files, find, excerpt, map, inspect, outline, status, validate, impact, deps, unused, or hotspots."); - return CommandExitCodes.UsageError; - } - public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) { var previewOptionError = ValidatePreviewOptions("search", cmdArgs, allowMaxLineWidth: true, allowFocusOptions: false); From a677391bb2be4e3431d9f98c1c48ba942c158b76 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 15 Jun 2026 06:54:05 +0900 Subject: [PATCH 2/2] Split program dispatch runner for #3418 --- changelog.d/unreleased/3418.internal.md | 16 ++ src/CodeIndex/Cli/ProgramRunner.Dispatch.cs | 300 ++++++++++++++++++++ src/CodeIndex/Cli/ProgramRunner.cs | 298 +------------------ 3 files changed, 317 insertions(+), 297 deletions(-) create mode 100644 changelog.d/unreleased/3418.internal.md create mode 100644 src/CodeIndex/Cli/ProgramRunner.Dispatch.cs diff --git a/changelog.d/unreleased/3418.internal.md b/changelog.d/unreleased/3418.internal.md new file mode 100644 index 0000000000..ef12ceb541 --- /dev/null +++ b/changelog.d/unreleased/3418.internal.md @@ -0,0 +1,16 @@ +--- +category: internal +issues: + - 3418 +affected: + - src/CodeIndex/Cli/ProgramRunner.cs + - src/CodeIndex/Cli/ProgramRunner.Dispatch.cs +--- + +## English + +- **Split top-level command dispatch into a focused partial (#3418)** — `ProgramRunner` now keeps immediate command handling, query dispatch, and non-query dispatch in a dedicated partial without changing CLI behavior. + +## 日本語 + +- **top-level command dispatch を focused partial に分割しました (#3418)** — `ProgramRunner` の immediate command handling、query dispatch、non-query dispatch を専用 partial に移し、CLI 挙動は変更していません。 diff --git a/src/CodeIndex/Cli/ProgramRunner.Dispatch.cs b/src/CodeIndex/Cli/ProgramRunner.Dispatch.cs new file mode 100644 index 0000000000..df90f7a5f1 --- /dev/null +++ b/src/CodeIndex/Cli/ProgramRunner.Dispatch.cs @@ -0,0 +1,300 @@ +namespace CodeIndex.Cli; + +internal static partial class ProgramRunner +{ + private static bool TryRunImmediateCommand(string[] args, CommandRunContext context, out int exitCode) + { + if (TryRunHelpVersionOrUpdateCommand(args, context, out exitCode)) + return true; + if (TryRunStandaloneUtilityCommand(args, context, out exitCode)) + return true; + if (TryRunSubcommandHelp(args, context, out exitCode)) + return true; + if (TryRunDoctorCommand(args, context, out exitCode)) + return true; + if (TryRunEasterEggCommand(args, context, out exitCode)) + return true; + + exitCode = CommandExitCodes.Success; + return false; + } + + private static bool TryRunHelpVersionOrUpdateCommand(string[] args, CommandRunContext context, out int exitCode) + { + if (args.Length == 0 || args[0] is "--help" or "-h") + { + ConsoleUi.PrintUsageBrief(showBanner: args.Length > 0); + exitCode = args.Length == 0 ? CommandExitCodes.UsageError : CommandExitCodes.Success; + GlobalToolLog.Info($"command_complete exit_code={exitCode} help_or_usage=true"); + EmitCommandMetric("help", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + if (args[0] is "--help-all" or "--help-extended") + { + ConsoleUi.PrintUsageFull(showBanner: true); + exitCode = CommandExitCodes.Success; + GlobalToolLog.Info($"command_complete exit_code={exitCode} help_all=true"); + EmitCommandMetric("help-all", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + if (args[0] == "--help-flags") + { + ConsoleUi.PrintFlagUsage(showBanner: true); + exitCode = CommandExitCodes.Success; + GlobalToolLog.Info($"command_complete exit_code={exitCode} help_flags=true"); + EmitCommandMetric("help-flags", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + if (args[0] is "--version" or "-V") + { + exitCode = RunVersion(args[1..], context.JsonOptions, context.AppVersion, context.CancellationToken); + GlobalToolLog.Info($"command_complete exit_code={exitCode} version_only=true"); + EmitCommandMetric("version", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + if (args[0] == "--check-updates") + { + exitCode = RunCheckUpdates(args[1..], context.JsonOptions, context.AppVersion, context.CancellationToken); + GlobalToolLog.Info($"command_complete exit_code={exitCode} check_updates=true"); + EmitCommandMetric("check-updates", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + exitCode = CommandExitCodes.Success; + return false; + } + + private static bool TryRunStandaloneUtilityCommand(string[] args, CommandRunContext context, out int exitCode) + { + if (args[0] is "--license" or "license") + { + if (args[0] == "license" && args.Length > 1 && ArgHelper.WantsHelp(args.AsSpan(1))) + { + ConsoleUi.PrintCommandUsage("license"); + exitCode = CommandExitCodes.Success; + GlobalToolLog.Info($"command_complete exit_code={exitCode} subcommand_help=true"); + EmitCommandMetric("license", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + ConsoleUi.PrintLicenseSummary(); + exitCode = CommandExitCodes.Success; + GlobalToolLog.Info($"command_complete exit_code={exitCode} license_only=true"); + EmitCommandMetric("license", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + if (args[0] is "--completions" or "completions") + { + if (args[0] == "completions" && args.Length > 1 && ArgHelper.WantsHelp(args.AsSpan(1))) + { + ConsoleUi.PrintCommandUsage("completions"); + exitCode = CommandExitCodes.Success; + GlobalToolLog.Info($"command_complete exit_code={exitCode} subcommand_help=true"); + EmitCommandMetric("completions", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + exitCode = RunCompletions(args[1..], args[0] == "completions" ? "completions" : "--completions"); + GlobalToolLog.Info($"command_complete exit_code={exitCode} command=completions"); + EmitCommandMetric("completions", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + exitCode = CommandExitCodes.Success; + return false; + } + + private static bool TryRunSubcommandHelp(string[] args, CommandRunContext context, out int exitCode) + { + if (args.Length > 1 && ArgHelper.WantsHelp(args.AsSpan(1))) + { + if (!ConsoleUi.PrintCommandUsage(args[0])) + ConsoleUi.PrintUsage(showBanner: true); + exitCode = CommandExitCodes.Success; + GlobalToolLog.Info($"command_complete exit_code={exitCode} subcommand_help=true"); + EmitCommandMetric(args[0], args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + exitCode = CommandExitCodes.Success; + return false; + } + + private static bool TryRunDoctorCommand(string[] args, CommandRunContext context, out int exitCode) + { + if (args[0] == "doctor") + { + exitCode = RunDoctor(args[1..], context.AppVersion); + GlobalToolLog.Info($"command_complete exit_code={exitCode} command=doctor"); + EmitCommandMetric("doctor", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + exitCode = CommandExitCodes.Success; + return false; + } + + private static bool TryRunEasterEggCommand(string[] args, CommandRunContext context, out int exitCode) + { + var easterEgg = args.FirstOrDefault(a => a is "--sushi" or "--coffee" or "--ramen" or "--wine" or "--beer" or "--matcha" or "--whisky"); + if (easterEgg != null && !args.Any(a => !a.StartsWith('-'))) + { + ConsoleUi.PrintEasterEggMessage(easterEgg); + exitCode = CommandExitCodes.Success; + GlobalToolLog.Info($"command_complete exit_code={exitCode} easter_egg={easterEgg}"); + EmitCommandMetric("easter_egg", args, context.StartTimestamp, context.Stopwatch, exitCode); + return true; + } + + exitCode = CommandExitCodes.Success; + return false; + } + + private static int RunDispatchedCommand( + string[] args, + CommandRunContext context, + Action? beforeDispatchForTesting) + { + beforeDispatchForTesting?.Invoke(); + + if (args[0] is "mcp" or "mcp-server") + { + var mcpExitCode = RunMcp(args[1..], context.AppVersion); + GlobalToolLog.Info($"command_complete exit_code={mcpExitCode} command=mcp"); + EmitCommandMetric("mcp", args, context.StartTimestamp, context.Stopwatch, mcpExitCode); + return mcpExitCode; + } + + if (args[0] is "lsp" or "--lsp") + { + var lspExitCode = RunLsp(args[1..], context.AppVersion, context.JsonOptions, context.CancellationToken); + GlobalToolLog.Info($"command_complete exit_code={lspExitCode} command=lsp"); + EmitCommandMetric("lsp", args, context.StartTimestamp, context.Stopwatch, lspExitCode); + return lspExitCode; + } + + var commandName = args[0]; + var subArgs = args[1..]; + var queryRunner = ResolveQueryRunner(commandName, context); + + int exitCode; + if (queryRunner is not null) + { + subArgs = InsertQueryLiteralSentinelForNonLogGlobalOption(commandName, subArgs); + + if (!TryConsumeQueryTraceFlag(ref subArgs, out var traceMode, out var traceError)) + { + CommandErrorWriter.Write(StripErrorPrefix(traceError), "use one of `none`, `stderr`, or `file`."); + GlobalToolLog.Info($"command_complete exit_code={CommandExitCodes.InvalidArgument} command={commandName} trace_flag_invalid=true"); + EmitCommandMetric(commandName, args, context.StartTimestamp, context.Stopwatch, CommandExitCodes.InvalidArgument); + return CommandExitCodes.InvalidArgument; + } + + using var traceCapture = QueryTraceOutputCapture.TryStart(traceMode, subArgs); + exitCode = JsonEnvelopeWrapper.ShouldWrap(commandName, subArgs) + ? JsonEnvelopeWrapper.RunWrapped(commandName, subArgs, context.AppVersion, context.JsonOptions, queryRunner) + : queryRunner(subArgs); + EmitQueryTrace(traceMode, commandName, subArgs, context.StartTimestamp, context.Stopwatch, exitCode, traceCapture?.ResultCount); + } + else + { + exitCode = RunNonQueryCommand(commandName, subArgs, args, context); + } + + GlobalToolLog.Info($"command_complete exit_code={exitCode} command={commandName}"); + EmitCommandMetric(commandName, args, context.StartTimestamp, context.Stopwatch, exitCode); + return exitCode; + } + + private static Func? ResolveQueryRunner(string commandName, CommandRunContext context) => + commandName switch + { + "search" => a => QueryCommandRunner.RunSearch(a, context.JsonOptions), + "definition" => a => QueryCommandRunner.RunDefinition(a, context.JsonOptions), + "goto" => a => QueryCommandRunner.RunGoto(a, context.JsonOptions), + "references" => a => QueryCommandRunner.RunReferences(a, context.JsonOptions), + "callers" => a => QueryCommandRunner.RunCallers(a, context.JsonOptions), + "callees" => a => QueryCommandRunner.RunCallees(a, context.JsonOptions), + "symbols" => a => QueryCommandRunner.RunSymbols(a, context.JsonOptions), + "files" => a => QueryCommandRunner.RunFiles(a, context.JsonOptions), + "find" => a => QueryCommandRunner.RunFind(a, context.JsonOptions), + "excerpt" => a => QueryCommandRunner.RunExcerpt(a, context.JsonOptions), + "map" => a => QueryCommandRunner.RunMap(a, context.JsonOptions), + "inspect" => a => QueryCommandRunner.RunInspect(a, context.JsonOptions), + "outline" => a => QueryCommandRunner.RunOutline(a, context.JsonOptions), + "status" => a => QueryCommandRunner.RunStatus(a, context.JsonOptions, context.AppVersion, context.CancellationToken), + "validate" => a => QueryCommandRunner.RunValidate(a, context.JsonOptions), + "languages" => a => QueryCommandRunner.RunLanguages(a, context.JsonOptions), + "impact" => a => QueryCommandRunner.RunImpact(a, context.JsonOptions), + "deps" => a => QueryCommandRunner.RunDeps(a, context.JsonOptions), + "unused" => a => QueryCommandRunner.RunUnused(a, context.JsonOptions), + "hotspots" => a => QueryCommandRunner.RunHotspots(a, context.JsonOptions), + "batch" => a => QueryCommandRunner.RunBatch(a, context.JsonOptions), + "suggestions" => a => SuggestionsCommandRunner.Run(a, context.JsonOptions), + _ => null, + }; + + private static int RunNonQueryCommand( + string commandName, + string[] subArgs, + string[] originalArgs, + CommandRunContext context) => + commandName switch + { + "upgrade" => RunUpgrade(subArgs, context.JsonOptions, context.AppVersion, context.CancellationToken), + "index" => IndexCommandRunner.Run(subArgs, context.JsonOptions), + "export" => ExportImportCommandRunner.RunExport(subArgs, context.JsonOptions, context.AppVersion), + "import" => ExportImportCommandRunner.RunImport(subArgs, context.JsonOptions), + "diff" => DiffCommandRunner.Run(subArgs, context.JsonOptions), + "hooks" => HookCommandRunner.Run(subArgs, context.JsonOptions), + "backfill-fold" => IndexCommandRunner.RunBackfillFold(subArgs, context.JsonOptions), + "optimize" => IndexCommandRunner.RunOptimizeFts(subArgs, context.JsonOptions), + "vacuum" => QueryCommandRunner.RunVacuum(subArgs, context.JsonOptions), + "validate-config" => CdidxConfigFile.RunValidate(subArgs, context.JsonOptions), + "config" => subArgs.Length > 0 && subArgs[0] == "show" + ? CdidxConfigFile.RunShow(subArgs[1..], context.JsonOptions) + : CommandErrorWriter.WriteJsonOrHuman( + ContainsJsonOutputFlag(subArgs), + context.JsonOptions, + "Unknown config command: use `cdidx config show`.", + CommandExitCodes.UsageError, + "use `cdidx config show`."), + "workspace" => WorkspaceCommandRunner.Run(subArgs, context.JsonOptions), + "db" => DbCommandRunner.Run(subArgs, context.JsonOptions), + "report" => ReportCommandRunner.Run(subArgs, context.JsonOptions, context.AppVersion), + "test-extractor" => RunTestExtractor(subArgs, context.JsonOptions), + _ when IsProjectPathArg(commandName) + => IndexCommandRunner.Run(originalArgs, context.JsonOptions), + _ => ShowError(originalArgs, $"Unknown command: {commandName}") + }; + + internal static bool IsProjectPathArg(string arg) + { + if (arg.StartsWith('-')) + return false; + + if (arg == "." || Directory.Exists(arg) || Path.IsPathRooted(arg) || Path.IsPathFullyQualified(arg)) + return true; + + if (arg.Contains(Path.DirectorySeparatorChar)) + return true; + + if (Path.AltDirectorySeparatorChar != '\0' + && Path.AltDirectorySeparatorChar != Path.DirectorySeparatorChar + && arg.Contains(Path.AltDirectorySeparatorChar)) + return true; + + return OperatingSystem.IsWindows() + && (IsWindowsDrivePath(arg) || arg.StartsWith(@"\\", StringComparison.Ordinal)); + } + + private static bool IsWindowsDrivePath(string arg) => + arg.Length >= 2 + && arg[1] == ':' + && ((arg[0] >= 'A' && arg[0] <= 'Z') || (arg[0] >= 'a' && arg[0] <= 'z')); +} diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index 65ae116667..8bcd7cc35f 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -17,7 +17,7 @@ namespace CodeIndex.Cli; -internal static class ProgramRunner +internal static partial class ProgramRunner { private const int RetainedQueryTraceFileCount = 30; internal const int QueryTraceValueMaxChars = 128; @@ -229,302 +229,6 @@ internal static int Run( } } - private static bool TryRunImmediateCommand(string[] args, CommandRunContext context, out int exitCode) - { - if (TryRunHelpVersionOrUpdateCommand(args, context, out exitCode)) - return true; - if (TryRunStandaloneUtilityCommand(args, context, out exitCode)) - return true; - if (TryRunSubcommandHelp(args, context, out exitCode)) - return true; - if (TryRunDoctorCommand(args, context, out exitCode)) - return true; - if (TryRunEasterEggCommand(args, context, out exitCode)) - return true; - - exitCode = CommandExitCodes.Success; - return false; - } - - private static bool TryRunHelpVersionOrUpdateCommand(string[] args, CommandRunContext context, out int exitCode) - { - if (args.Length == 0 || args[0] is "--help" or "-h") - { - ConsoleUi.PrintUsageBrief(showBanner: args.Length > 0); - exitCode = args.Length == 0 ? CommandExitCodes.UsageError : CommandExitCodes.Success; - GlobalToolLog.Info($"command_complete exit_code={exitCode} help_or_usage=true"); - EmitCommandMetric("help", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - if (args[0] is "--help-all" or "--help-extended") - { - ConsoleUi.PrintUsageFull(showBanner: true); - exitCode = CommandExitCodes.Success; - GlobalToolLog.Info($"command_complete exit_code={exitCode} help_all=true"); - EmitCommandMetric("help-all", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - if (args[0] == "--help-flags") - { - ConsoleUi.PrintFlagUsage(showBanner: true); - exitCode = CommandExitCodes.Success; - GlobalToolLog.Info($"command_complete exit_code={exitCode} help_flags=true"); - EmitCommandMetric("help-flags", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - if (args[0] is "--version" or "-V") - { - exitCode = RunVersion(args[1..], context.JsonOptions, context.AppVersion, context.CancellationToken); - GlobalToolLog.Info($"command_complete exit_code={exitCode} version_only=true"); - EmitCommandMetric("version", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - if (args[0] == "--check-updates") - { - exitCode = RunCheckUpdates(args[1..], context.JsonOptions, context.AppVersion, context.CancellationToken); - GlobalToolLog.Info($"command_complete exit_code={exitCode} check_updates=true"); - EmitCommandMetric("check-updates", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - exitCode = CommandExitCodes.Success; - return false; - } - - private static bool TryRunStandaloneUtilityCommand(string[] args, CommandRunContext context, out int exitCode) - { - if (args[0] is "--license" or "license") - { - if (args[0] == "license" && args.Length > 1 && ArgHelper.WantsHelp(args.AsSpan(1))) - { - ConsoleUi.PrintCommandUsage("license"); - exitCode = CommandExitCodes.Success; - GlobalToolLog.Info($"command_complete exit_code={exitCode} subcommand_help=true"); - EmitCommandMetric("license", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - ConsoleUi.PrintLicenseSummary(); - exitCode = CommandExitCodes.Success; - GlobalToolLog.Info($"command_complete exit_code={exitCode} license_only=true"); - EmitCommandMetric("license", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - if (args[0] is "--completions" or "completions") - { - if (args[0] == "completions" && args.Length > 1 && ArgHelper.WantsHelp(args.AsSpan(1))) - { - ConsoleUi.PrintCommandUsage("completions"); - exitCode = CommandExitCodes.Success; - GlobalToolLog.Info($"command_complete exit_code={exitCode} subcommand_help=true"); - EmitCommandMetric("completions", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - exitCode = RunCompletions(args[1..], args[0] == "completions" ? "completions" : "--completions"); - GlobalToolLog.Info($"command_complete exit_code={exitCode} command=completions"); - EmitCommandMetric("completions", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - exitCode = CommandExitCodes.Success; - return false; - } - - private static bool TryRunSubcommandHelp(string[] args, CommandRunContext context, out int exitCode) - { - if (args.Length > 1 && ArgHelper.WantsHelp(args.AsSpan(1))) - { - if (!ConsoleUi.PrintCommandUsage(args[0])) - ConsoleUi.PrintUsage(showBanner: true); - exitCode = CommandExitCodes.Success; - GlobalToolLog.Info($"command_complete exit_code={exitCode} subcommand_help=true"); - EmitCommandMetric(args[0], args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - exitCode = CommandExitCodes.Success; - return false; - } - - private static bool TryRunDoctorCommand(string[] args, CommandRunContext context, out int exitCode) - { - if (args[0] == "doctor") - { - exitCode = RunDoctor(args[1..], context.AppVersion); - GlobalToolLog.Info($"command_complete exit_code={exitCode} command=doctor"); - EmitCommandMetric("doctor", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - exitCode = CommandExitCodes.Success; - return false; - } - - private static bool TryRunEasterEggCommand(string[] args, CommandRunContext context, out int exitCode) - { - var easterEgg = args.FirstOrDefault(a => a is "--sushi" or "--coffee" or "--ramen" or "--wine" or "--beer" or "--matcha" or "--whisky"); - if (easterEgg != null && !args.Any(a => !a.StartsWith('-'))) - { - ConsoleUi.PrintEasterEggMessage(easterEgg); - exitCode = CommandExitCodes.Success; - GlobalToolLog.Info($"command_complete exit_code={exitCode} easter_egg={easterEgg}"); - EmitCommandMetric("easter_egg", args, context.StartTimestamp, context.Stopwatch, exitCode); - return true; - } - - exitCode = CommandExitCodes.Success; - return false; - } - - private static int RunDispatchedCommand( - string[] args, - CommandRunContext context, - Action? beforeDispatchForTesting) - { - beforeDispatchForTesting?.Invoke(); - - if (args[0] is "mcp" or "mcp-server") - { - var mcpExitCode = RunMcp(args[1..], context.AppVersion); - GlobalToolLog.Info($"command_complete exit_code={mcpExitCode} command=mcp"); - EmitCommandMetric("mcp", args, context.StartTimestamp, context.Stopwatch, mcpExitCode); - return mcpExitCode; - } - - if (args[0] is "lsp" or "--lsp") - { - var lspExitCode = RunLsp(args[1..], context.AppVersion, context.JsonOptions, context.CancellationToken); - GlobalToolLog.Info($"command_complete exit_code={lspExitCode} command=lsp"); - EmitCommandMetric("lsp", args, context.StartTimestamp, context.Stopwatch, lspExitCode); - return lspExitCode; - } - - var commandName = args[0]; - var subArgs = args[1..]; - var queryRunner = ResolveQueryRunner(commandName, context); - - int exitCode; - if (queryRunner is not null) - { - subArgs = InsertQueryLiteralSentinelForNonLogGlobalOption(commandName, subArgs); - - if (!TryConsumeQueryTraceFlag(ref subArgs, out var traceMode, out var traceError)) - { - CommandErrorWriter.Write(StripErrorPrefix(traceError), "use one of `none`, `stderr`, or `file`."); - GlobalToolLog.Info($"command_complete exit_code={CommandExitCodes.InvalidArgument} command={commandName} trace_flag_invalid=true"); - EmitCommandMetric(commandName, args, context.StartTimestamp, context.Stopwatch, CommandExitCodes.InvalidArgument); - return CommandExitCodes.InvalidArgument; - } - - using var traceCapture = QueryTraceOutputCapture.TryStart(traceMode, subArgs); - exitCode = JsonEnvelopeWrapper.ShouldWrap(commandName, subArgs) - ? JsonEnvelopeWrapper.RunWrapped(commandName, subArgs, context.AppVersion, context.JsonOptions, queryRunner) - : queryRunner(subArgs); - EmitQueryTrace(traceMode, commandName, subArgs, context.StartTimestamp, context.Stopwatch, exitCode, traceCapture?.ResultCount); - } - else - { - exitCode = RunNonQueryCommand(commandName, subArgs, args, context); - } - - GlobalToolLog.Info($"command_complete exit_code={exitCode} command={commandName}"); - EmitCommandMetric(commandName, args, context.StartTimestamp, context.Stopwatch, exitCode); - return exitCode; - } - - private static Func? ResolveQueryRunner(string commandName, CommandRunContext context) => - commandName switch - { - "search" => a => QueryCommandRunner.RunSearch(a, context.JsonOptions), - "definition" => a => QueryCommandRunner.RunDefinition(a, context.JsonOptions), - "goto" => a => QueryCommandRunner.RunGoto(a, context.JsonOptions), - "references" => a => QueryCommandRunner.RunReferences(a, context.JsonOptions), - "callers" => a => QueryCommandRunner.RunCallers(a, context.JsonOptions), - "callees" => a => QueryCommandRunner.RunCallees(a, context.JsonOptions), - "symbols" => a => QueryCommandRunner.RunSymbols(a, context.JsonOptions), - "files" => a => QueryCommandRunner.RunFiles(a, context.JsonOptions), - "find" => a => QueryCommandRunner.RunFind(a, context.JsonOptions), - "excerpt" => a => QueryCommandRunner.RunExcerpt(a, context.JsonOptions), - "map" => a => QueryCommandRunner.RunMap(a, context.JsonOptions), - "inspect" => a => QueryCommandRunner.RunInspect(a, context.JsonOptions), - "outline" => a => QueryCommandRunner.RunOutline(a, context.JsonOptions), - "status" => a => QueryCommandRunner.RunStatus(a, context.JsonOptions, context.AppVersion, context.CancellationToken), - "validate" => a => QueryCommandRunner.RunValidate(a, context.JsonOptions), - "languages" => a => QueryCommandRunner.RunLanguages(a, context.JsonOptions), - "impact" => a => QueryCommandRunner.RunImpact(a, context.JsonOptions), - "deps" => a => QueryCommandRunner.RunDeps(a, context.JsonOptions), - "unused" => a => QueryCommandRunner.RunUnused(a, context.JsonOptions), - "hotspots" => a => QueryCommandRunner.RunHotspots(a, context.JsonOptions), - "batch" => a => QueryCommandRunner.RunBatch(a, context.JsonOptions), - "suggestions" => a => SuggestionsCommandRunner.Run(a, context.JsonOptions), - _ => null, - }; - - private static int RunNonQueryCommand( - string commandName, - string[] subArgs, - string[] originalArgs, - CommandRunContext context) => - commandName switch - { - "upgrade" => RunUpgrade(subArgs, context.JsonOptions, context.AppVersion, context.CancellationToken), - "index" => IndexCommandRunner.Run(subArgs, context.JsonOptions), - "export" => ExportImportCommandRunner.RunExport(subArgs, context.JsonOptions, context.AppVersion), - "import" => ExportImportCommandRunner.RunImport(subArgs, context.JsonOptions), - "diff" => DiffCommandRunner.Run(subArgs, context.JsonOptions), - "hooks" => HookCommandRunner.Run(subArgs, context.JsonOptions), - "backfill-fold" => IndexCommandRunner.RunBackfillFold(subArgs, context.JsonOptions), - "optimize" => IndexCommandRunner.RunOptimizeFts(subArgs, context.JsonOptions), - "vacuum" => QueryCommandRunner.RunVacuum(subArgs, context.JsonOptions), - "validate-config" => CdidxConfigFile.RunValidate(subArgs, context.JsonOptions), - "config" => subArgs.Length > 0 && subArgs[0] == "show" - ? CdidxConfigFile.RunShow(subArgs[1..], context.JsonOptions) - : CommandErrorWriter.WriteJsonOrHuman( - ContainsJsonOutputFlag(subArgs), - context.JsonOptions, - "Unknown config command: use `cdidx config show`.", - CommandExitCodes.UsageError, - "use `cdidx config show`."), - "workspace" => WorkspaceCommandRunner.Run(subArgs, context.JsonOptions), - "db" => DbCommandRunner.Run(subArgs, context.JsonOptions), - "report" => ReportCommandRunner.Run(subArgs, context.JsonOptions, context.AppVersion), - "test-extractor" => RunTestExtractor(subArgs, context.JsonOptions), - _ when IsProjectPathArg(commandName) - => IndexCommandRunner.Run(originalArgs, context.JsonOptions), - _ => ShowError(originalArgs, $"Unknown command: {commandName}") - }; - - internal static bool IsProjectPathArg(string arg) - { - if (arg.StartsWith('-')) - return false; - - if (arg == "." || Directory.Exists(arg) || Path.IsPathRooted(arg) || Path.IsPathFullyQualified(arg)) - return true; - - if (arg.Contains(Path.DirectorySeparatorChar)) - return true; - - if (Path.AltDirectorySeparatorChar != '\0' - && Path.AltDirectorySeparatorChar != Path.DirectorySeparatorChar - && arg.Contains(Path.AltDirectorySeparatorChar)) - return true; - - return OperatingSystem.IsWindows() - && (IsWindowsDrivePath(arg) || arg.StartsWith(@"\\", StringComparison.Ordinal)); - } - - private static bool IsWindowsDrivePath(string arg) => - arg.Length >= 2 - && arg[1] == ':' - && ((arg[0] >= 'A' && arg[0] <= 'Z') || (arg[0] >= 'a' && arg[0] <= 'z')); - private static int RunTestExtractor(string[] args, JsonSerializerOptions jsonOptions) { string? language = null;