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
4 changes: 4 additions & 0 deletions src/specify_cli/_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@

console = Console(highlight=False)

# Stderr-bound console for error/diagnostic output, so human-facing messages
# never contaminate stdout (which carries machine-readable ``--json`` payloads).
err_console = Console(stderr=True, highlight=False)

class StepTracker:
"""Track and render hierarchical steps without emojis, similar to Claude Code tree output.
Supports live auto-refresh via an attached refresh callback.
Expand Down
6 changes: 4 additions & 2 deletions src/specify_cli/commands/bundle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import typer

from ..._console import console
from ..._console import console, err_console
from ...bundler import BundlerError
from ...bundler.lib.project import (
active_integration,
Expand Down Expand Up @@ -41,7 +41,9 @@

def _fail(message: str) -> None:
"""Print an actionable error to stderr and exit non-zero."""
console.print(f"[red]Error:[/red] {message}", style=None)
# Use the stderr console so the error never lands on stdout, which under
# ``--json`` carries the machine-readable payload and must stay parseable.
err_console.print(f"[red]Error:[/red] {message}", style=None)
raise typer.Exit(code=1)


Expand Down
15 changes: 15 additions & 0 deletions tests/contract/test_bundle_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ def test_commands_outside_project_fail_with_guidance(tmp_path: Path, monkeypatch
assert "Spec Kit project" in result.output


def test_fail_writes_error_to_stderr_not_stdout(capsys):
"""_fail must write to stderr, not stdout: every bundle command routes errors
through it, and under --json the error would otherwise corrupt the JSON payload
that consumers read from stdout."""
import typer

from specify_cli.commands.bundle import _fail

with pytest.raises(typer.Exit):
_fail("something broke")
captured = capsys.readouterr()
assert "something broke" in captured.err
assert "something broke" not in captured.out


def test_search_works_without_a_project(tmp_path: Path, monkeypatch):
# Discovery commands fall back to the built-in/user catalog stack and must
# not require a Spec Kit project (matches README/quickstart examples).
Expand Down