diff --git a/changelog.d/unreleased/1471.fixed.md b/changelog.d/unreleased/1471.fixed.md new file mode 100644 index 0000000000..2ea8a290b6 --- /dev/null +++ b/changelog.d/unreleased/1471.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 1471 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +--- + +## English + +- **`index --verbose --json` now keeps stdout parseable (#1471)** — verbose index status lines are written to stderr when JSON output is requested, so stdout remains a clean JSON stream for machine consumers. + +## 日本語 + +- **`index --verbose --json` の stdout が parse 可能なままになりました (#1471)** — JSON 出力が指定された場合、verbose な index status 行は stderr に出力されるため、機械処理向けの stdout は純粋な JSON stream として保たれます。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index 39316b7931..db2d1415c7 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -1315,6 +1315,22 @@ void ResumeUpdateSpinnerAfterConsoleWrite() StartUpdateSpinnerIfNeeded(); } + void WriteUpdateVerboseStatus(string message) + { + if (!options.Verbose || options.Quiet) + return; + + if (options.Json) + { + Console.Error.WriteLine(message); + return; + } + + PauseUpdateSpinnerForConsoleWrite(); + Console.WriteLine(message); + ResumeUpdateSpinnerAfterConsoleWrite(); + } + void ThrowIfUpdateCancelled() { if (!cancellationToken.IsCancellationRequested) @@ -1412,12 +1428,7 @@ void ThrowIfUpdateCancelled() if (!writer.HasFileAtPath(relPath)) { skipped++; - if (options.Verbose && !options.Json && !options.Quiet) - { - PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [SKIP] {relPath} (not in DB)"); - ResumeUpdateSpinnerAfterConsoleWrite(); - } + WriteUpdateVerboseStatus($" [SKIP] {relPath} (not in DB)"); continue; } @@ -1429,22 +1440,12 @@ void ThrowIfUpdateCancelled() deleteTxn.Commit(); removed++; ftsMutated = true; - if (options.Verbose && !options.Json && !options.Quiet) - { - PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [DEL ] {relPath}"); - ResumeUpdateSpinnerAfterConsoleWrite(); - } + WriteUpdateVerboseStatus($" [DEL ] {relPath}"); } else { skipped++; - if (options.Verbose && !options.Json && !options.Quiet) - { - PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [SKIP] {relPath} (not in DB)"); - ResumeUpdateSpinnerAfterConsoleWrite(); - } + WriteUpdateVerboseStatus($" [SKIP] {relPath} (not in DB)"); } continue; } @@ -1658,12 +1659,7 @@ void ThrowIfUpdateCancelled() updated++; ftsMutated = true; ThrowIfUpdateCancelled(); - if (options.Verbose && !options.Json && !options.Quiet) - { - PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [OK ] {relPath} ({chunks.Count} chunks, {symbols.Count} symbols, {references.Count} refs)"); - ResumeUpdateSpinnerAfterConsoleWrite(); - } + WriteUpdateVerboseStatus($" [OK ] {relPath} ({chunks.Count} chunks, {symbols.Count} symbols, {references.Count} refs)"); } catch (Exception ex) { @@ -2702,6 +2698,23 @@ void ResumeIndexSpinnerAfterConsoleWrite() StartIndexSpinnerIfNeeded(); } + void WriteIndexVerboseStatus(string message) + { + if (!options.Verbose || options.Quiet) + return; + + if (options.Json) + { + Console.Error.WriteLine(message); + return; + } + + PauseIndexSpinnerForConsoleWrite(); + ConsoleUi.ClearProgressLine(); + Console.WriteLine(message); + ResumeIndexSpinnerAfterConsoleWrite(); + } + void EnsureIndexingActivityVisible() { if (options.Json || options.Quiet) @@ -2975,13 +2988,7 @@ void StopJsonHeartbeat() WriteProjectRootOnce(); txn.Commit(); - if (options.Verbose && !options.Json && !options.Quiet) - { - PauseIndexSpinnerForConsoleWrite(); - ConsoleUi.ClearProgressLine(); - Console.WriteLine($" [OK ] {record.Path} ({chunks.Count} chunks, {symbols.Count} symbols, {references.Count} refs)"); - ResumeIndexSpinnerAfterConsoleWrite(); - } + WriteIndexVerboseStatus($" [OK ] {record.Path} ({chunks.Count} chunks, {symbols.Count} symbols, {references.Count} refs)"); } catch (Exception ex) { diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index 3559fd1813..f3f6daa869 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -2871,6 +2871,35 @@ public void Run_UpdateMode_VerboseRedirectedOutput_DoesNotRepeatUpdatingBanner() } } + [Fact] + public void Run_UpdateMode_VerboseJson_WritesStatusToStderrAndKeepsStdoutJson() + { + var projectRoot = CreateTempProject(); + try + { + var sourcePath = Path.Combine(projectRoot, "app.cs"); + File.WriteAllText(sourcePath, "public class App { public void Run() { } }\n"); + + var initialExitCode = IndexCommandRunner.Run([projectRoot, "--json"], _jsonOptions); + Assert.Equal(CommandExitCodes.Success, initialExitCode); + + File.WriteAllText(sourcePath, "public class App { public void Run() { } public void Extra() { } }\n"); + File.SetLastWriteTimeUtc(sourcePath, DateTime.UtcNow.AddSeconds(2)); + + var (exitCode, stdout, stderr) = RunAndCaptureStreams([projectRoot, "--files", "app.cs", "--verbose", "--json"]); + + Assert.Equal(CommandExitCodes.Success, exitCode); + using var json = JsonDocument.Parse(stdout); + Assert.Equal("success", json.RootElement.GetProperty("Status").GetString()); + Assert.DoesNotContain("[OK ]", stdout); + Assert.Contains("[OK ] app.cs", stderr); + } + finally + { + DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_UpdateMode_JsonKeepsGraphAndIssuesReadyAfterHealthyScopedRefresh() {