diff --git a/packages/cli/src/repowise/cli/commands/init_cmd/command.py b/packages/cli/src/repowise/cli/commands/init_cmd/command.py index b6d623eb9..8209ae871 100644 --- a/packages/cli/src/repowise/cli/commands/init_cmd/command.py +++ b/packages/cli/src/repowise/cli/commands/init_cmd/command.py @@ -45,6 +45,7 @@ run_async, save_config_partial, save_state, + warn, ) from repowise.cli.platform import telemetry from repowise.cli.providers import resolve_embedder @@ -336,8 +337,8 @@ def _run_generation_phase( # Warn when a local provider runs with default concurrency if provider.provider_name in ("ollama", "codex_cli", "opencode") and concurrency > 4: - console.print( - f" [{WARN}]Warning:[/] {provider.provider_name} is a local provider " + warn( + f" {provider.provider_name} is a local provider " f"running with concurrency={concurrency}. " f"If you see timeout errors, try [bold]--concurrency 1[/bold]." ) diff --git a/packages/cli/src/repowise/cli/commands/mcp_cmd.py b/packages/cli/src/repowise/cli/commands/mcp_cmd.py index d14de36a2..778ee9b40 100644 --- a/packages/cli/src/repowise/cli/commands/mcp_cmd.py +++ b/packages/cli/src/repowise/cli/commands/mcp_cmd.py @@ -7,7 +7,7 @@ import click -from repowise.cli.helpers import console, find_repowise_repo_root, resolve_repo_path +from repowise.cli.helpers import console, find_repowise_repo_root, resolve_repo_path, warn from repowise.cli.ui import load_dotenv from repowise.core.workspace.config import WorkspaceConfig, find_workspace_root @@ -160,9 +160,8 @@ def mcp_command( workspace = _workspace_summary(repo_path) repowise_dir = repo_path / ".repowise" if workspace is None and not repowise_dir.exists(): - console.print( - f"[yellow]Warning: No .repowise directory found at {repo_path}.[/yellow]\n" - "Run 'repowise init' first to generate documentation." + warn( + f"No .repowise directory found at {repo_path}.\nRun 'repowise init' first to generate documentation." ) resolved_host = host or os.environ.get("REPOWISE_HOST", "127.0.0.1") diff --git a/packages/cli/src/repowise/cli/commands/reindex_cmd.py b/packages/cli/src/repowise/cli/commands/reindex_cmd.py index 3a81ef134..4d6242aa4 100644 --- a/packages/cli/src/repowise/cli/commands/reindex_cmd.py +++ b/packages/cli/src/repowise/cli/commands/reindex_cmd.py @@ -11,6 +11,7 @@ get_db_url_for_repo, resolve_repo_path, run_async, + warn, ) from repowise.cli.ui import BRAND_STYLE, OWL_SPINNER @@ -155,9 +156,7 @@ async def _embed_slice(items: list[tuple[str, str, dict]]) -> None: failed += 1 warned += 1 if warned <= 3: - console.print( - f"[yellow] Warning: failed to embed {page_id}: {exc}[/yellow]" - ) + warn(f" failed to embed {page_id}: {exc}") # Pages — one batched embed per slice instead of one embedder # round-trip per page (a large wiki paid thousands of serial calls). @@ -173,10 +172,7 @@ async def _embed_slice(items: list[tuple[str, str, dict]]) -> None: failed += 1 warned += 1 if warned <= 3: - console.print( - f"[yellow] Warning: skipped {page.id}: no title to index it by" - "[/yellow]" - ) + warn(f" skipped {page.id}: no title to index it by") continue item = embed_item( page.id, diff --git a/packages/cli/src/repowise/cli/commands/workspace_cmd.py b/packages/cli/src/repowise/cli/commands/workspace_cmd.py index 5b40b20a9..8649b03c8 100644 --- a/packages/cli/src/repowise/cli/commands/workspace_cmd.py +++ b/packages/cli/src/repowise/cli/commands/workspace_cmd.py @@ -17,6 +17,7 @@ resolve_reasoning, resolve_repo_path, run_async, + warn, ) from repowise.cli.output import emit_json, format_option, json_option, resolve_format from repowise.core.docs_mode import docs_mode_state_fields, resolve_docs_mode @@ -624,7 +625,7 @@ async def _do_index() -> tuple[int, int, int]: file_count, symbol_count, _ = run_async(_do_index()) console.print(f" [green]✓[/green] {file_count} files, {symbol_count:,} symbols") except Exception as exc: - console.print(f"[yellow]Warning:[/yellow] Indexing failed for '{alias}': {exc}") + warn(f"Indexing failed for '{alias}': {exc}") return # Run LLM doc generation through the existing single-repo init pathway diff --git a/packages/cli/src/repowise/cli/helpers.py b/packages/cli/src/repowise/cli/helpers.py index 8af5627e4..8b475603e 100644 --- a/packages/cli/src/repowise/cli/helpers.py +++ b/packages/cli/src/repowise/cli/helpers.py @@ -51,6 +51,17 @@ console = Console(width=resolve_console_width(sys.stdout)) err_console = Console(stderr=True, width=resolve_console_width(sys.stderr)) + +def warn(text: str) -> None: + """Print a warning to stderr with the shared yellow ``Warning:`` prefix. + + Every CLI warning funnels through this helper so warnings render on the + same ``err_console`` stream (never stdout) and use one ``[yellow]Warning:[/yellow]`` + spelling instead of hand-synced copies scattered across commands. + """ + err_console.print(f"[yellow]Warning:[/yellow] {text}") + + STATE_FILENAME = "state.json" REPOWISE_DIR = ".repowise" @@ -693,8 +704,8 @@ def _persist_provider_key(repo_path: Path, provider: str) -> None: try: save_repo_env_key(repo_path, env_var, value) except (OSError, ValueError) as exc: - err_console.print( - f"[yellow]Warning:[/yellow] could not save {env_var} to " + warn( + f"could not save {env_var} to " f".repowise/.env ({exc}). The index is complete, but " f"`repowise mcp` will need {env_var} in its environment." ) @@ -961,7 +972,7 @@ def _build(name: str) -> Any: warnings = validate_provider_config(provider_name) if warnings: for warning in warnings: - err_console.print(f"[yellow]Warning:[/yellow] {warning}") + warn(warning) # For explicit provider requests, we still try to create it # The provider constructor will fail if the API key is actually required diff --git a/packages/cli/src/repowise/cli/worktree.py b/packages/cli/src/repowise/cli/worktree.py index df5e90877..2b692ce9a 100644 --- a/packages/cli/src/repowise/cli/worktree.py +++ b/packages/cli/src/repowise/cli/worktree.py @@ -15,7 +15,7 @@ import uuid from pathlib import Path -from repowise.cli.helpers import console +from repowise.cli.helpers import console, warn def _git_output(args: list[str], cwd: Path) -> str: @@ -200,10 +200,10 @@ def seed_index_from_base( if include_submodules is not None: state_include = st_data.get("include_submodules", False) if include_submodules != state_include: - console.print( - f"[yellow]Warning: --include-submodules={include_submodules} " + warn( + f"--include-submodules={include_submodules} " f"conflicts with copied state ({state_include}). Seeded state " - f"will take precedence.[/yellow]" + f"will take precedence." ) (temp_dir / "state.json").write_text(json.dumps(st_data, indent=2), encoding="utf-8") diff --git a/tests/unit/cli/test_helpers.py b/tests/unit/cli/test_helpers.py index 61e639d9c..345947eae 100644 --- a/tests/unit/cli/test_helpers.py +++ b/tests/unit/cli/test_helpers.py @@ -21,6 +21,7 @@ run_async, save_state, validate_provider_config, + warn, ) # --------------------------------------------------------------------------- @@ -28,6 +29,24 @@ # --------------------------------------------------------------------------- +class TestWarn: + def test_warn_prints_warning_prefix_to_stderr(self, capsys): + warn("something went wrong") + captured = capsys.readouterr() + assert "Warning:" in captured.err + assert "something went wrong" in captured.err + # Nothing leaks to stdout. + assert captured.out == "" + + def test_warn_prefix_rendered(self, capsys): + warn("boom") + captured = capsys.readouterr() + # Rich renders the [yellow] markup away when stderr isn't a tty, but the + # human-facing "Warning:" prefix must survive on the stderr stream. + assert captured.err.startswith("Warning: boom\n") + assert captured.out == "" + + class TestRunAsync: def test_returns_coroutine_result(self): async def _add(a, b):