diff --git a/standup/config.py b/standup/config.py index d375d93..f8a940f 100644 --- a/standup/config.py +++ b/standup/config.py @@ -58,7 +58,7 @@ def load_config() -> dict: if not config_path.exists(): console.print( - f"[yellow]⚠️ No config found at {CONFIG_PATH}. Using defaults. Run: standup --setup[/yellow]" + f"[yellow][!] No config found at {CONFIG_PATH}. Using defaults. Run: standup --setup[/yellow]" ) raw_config: dict[str, Any] = {} else: @@ -67,12 +67,12 @@ def load_config() -> dict: raw_config = json.loads(read_text_restricted(CONFIG_PATH, label="Config file")) except json.JSONDecodeError as exc: console.print( - f"[red]❌ Invalid JSON in {CONFIG_PATH}: {sanitize_error_message(exc)}[/red]" + f"[red][x] Invalid JSON in {CONFIG_PATH}: {sanitize_error_message(exc)}[/red]" ) sys.exit(1) except OSError as exc: console.print( - f"[red]❌ Could not read {CONFIG_PATH}: {sanitize_error_message(exc)}[/red]" + f"[red][x] Could not read {CONFIG_PATH}: {sanitize_error_message(exc)}[/red]" ) sys.exit(1) @@ -95,7 +95,7 @@ def load_config() -> dict: ok, errors = validate_full_config(config) if not ok: log_event("config_validation_failed", error_count=len(errors)) - console.print("[red]❌ Config validation failed:[/red]") + console.print("[red][x] Config validation failed:[/red]") for error in errors: console.print(f" [red]• {error}[/red]") console.print(f"\nFix your config at: {CONFIG_PATH}") @@ -108,7 +108,7 @@ def load_config() -> dict: if repo_ok: valid_repos.append(repo) else: - console.print(f"[yellow]⚠️ Skipping invalid repo: {message}[/yellow]") + console.print(f"[yellow][!] Skipping invalid repo: {message}[/yellow]") config["repos"] = valid_repos return config @@ -142,4 +142,4 @@ def save_config(config: dict) -> None: json.dumps(on_disk, indent=2), label="Config file", ) - console.print(f"[green]✅ Config saved to {CONFIG_PATH}[/green]") + console.print(f"[green][+] Config saved to {CONFIG_PATH}[/green]") diff --git a/standup/git_reader.py b/standup/git_reader.py index f0defeb..7127dd7 100644 --- a/standup/git_reader.py +++ b/standup/git_reader.py @@ -69,7 +69,7 @@ def get_recent_commits( try: import git # type: ignore[import] except ImportError: - console.print("[red]❌ GitPython is not installed. Run: pip install gitpython[/red]") + console.print("[red][x] GitPython is not installed. Run: pip install gitpython[/red]") return [] repo_name = Path(repo_path).name @@ -78,14 +78,14 @@ def get_recent_commits( try: repo = git.Repo(repo_path) except git.exc.InvalidGitRepositoryError: - console.print(f"[yellow]⚠️ Not a git repository: {repo_path}[/yellow]") + console.print(f"[yellow][!] Not a git repository: {repo_path}[/yellow]") return [] except git.exc.NoSuchPathError: - console.print(f"[yellow]⚠️ Repo path not found: {repo_path}[/yellow]") + console.print(f"[yellow][!] Repo path not found: {repo_path}[/yellow]") return [] except Exception as exc: console.print( - f"[yellow]⚠️ Could not open repo {repo_path}: {sanitize_error_message(exc)}[/yellow]" + f"[yellow][!] Could not open repo {repo_path}: {sanitize_error_message(exc)}[/yellow]" ) return [] @@ -137,7 +137,7 @@ def get_recent_commits( if len(commits) >= MAX_COMMITS_PER_RUN: console.print( - f"[yellow]⚠️ Commit count exceeded {MAX_COMMITS_PER_RUN}; truncating to the most recent entries.[/yellow]" + f"[yellow][!] Commit count exceeded {MAX_COMMITS_PER_RUN}; truncating to the most recent entries.[/yellow]" ) log_event( "commit_limit_truncated", @@ -147,7 +147,7 @@ def get_recent_commits( break except Exception as exc: console.print( - f"[yellow]⚠️ Error reading commits from {repo_path}: {sanitize_error_message(exc)}[/yellow]" + f"[yellow][!] Error reading commits from {repo_path}: {sanitize_error_message(exc)}[/yellow]" ) return commits[:MAX_COMMITS_PER_RUN] diff --git a/standup/history.py b/standup/history.py index 78c2f9c..10e9416 100644 --- a/standup/history.py +++ b/standup/history.py @@ -265,7 +265,7 @@ def init_db() -> None: except (sqlite3.Error, OSError) as exc: log_event("db_error", operation="init") console.print( - f"[yellow]⚠️ Could not initialize history database: {sanitize_error_message(exc)}[/yellow]" + f"[yellow][!] Could not initialize history database: {sanitize_error_message(exc)}[/yellow]" ) @@ -331,7 +331,7 @@ def find_cached_standup_entry( except sqlite3.Error as exc: log_event("db_error", operation="find_cached") console.print( - f"[yellow]⚠️ Could not read standup history: {sanitize_error_message(exc)}[/yellow]" + f"[yellow][!] Could not read standup history: {sanitize_error_message(exc)}[/yellow]" ) return None @@ -447,7 +447,7 @@ def save_standup( except (sqlite3.Error, OSError, TypeError, ValueError) as exc: log_event("db_error", operation="save") console.print( - f"[yellow]⚠️ Could not save standup history: {sanitize_error_message(exc)}[/yellow]" + f"[yellow][!] Could not save standup history: {sanitize_error_message(exc)}[/yellow]" ) @@ -480,7 +480,7 @@ def get_history(limit: int = 10) -> list[dict[str, object]]: except (sqlite3.Error, TypeError, ValueError) as exc: log_event("db_error", operation="history") console.print( - f"[yellow]⚠️ Could not fetch standup history: {sanitize_error_message(exc)}[/yellow]" + f"[yellow][!] Could not fetch standup history: {sanitize_error_message(exc)}[/yellow]" ) return [] @@ -512,7 +512,7 @@ def clear_history(older_than_days: int | None = None) -> int: except (sqlite3.Error, TypeError, ValueError) as exc: log_event("db_error", operation="clear") console.print( - f"[yellow]⚠️ Could not clear standup history: {sanitize_error_message(exc)}[/yellow]" + f"[yellow][!] Could not clear standup history: {sanitize_error_message(exc)}[/yellow]" ) return 0 diff --git a/standup/main.py b/standup/main.py index e81df85..2bafa47 100644 --- a/standup/main.py +++ b/standup/main.py @@ -42,7 +42,7 @@ def _prompt_bool(field_name: str, default: bool) -> bool: ok, message = validate_setup_input(field_name, raw_value) if ok: return parse_bool_text(raw_value) - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") def _post_to_slack(webhook_url: str, text: str) -> None: @@ -51,13 +51,13 @@ def _post_to_slack(webhook_url: str, text: str) -> None: try: response = requests.post(webhook_url, json={"text": text}, timeout=10) if response.status_code == 200: - console.print("[green]✅ Posted to Slack![/green]") + console.print("[green][+] Posted to Slack![/green]") else: console.print( - f"[red]❌ Slack post failed: {response.status_code} {response.text}[/red]" + f"[red][x] Slack post failed: {response.status_code} {response.text}[/red]" ) except Exception as exc: - console.print(f"[red]❌ Slack post error: {sanitize_error_message(exc)}[/red]") + console.print(f"[red][x] Slack post error: {sanitize_error_message(exc)}[/red]") def _get_provider_slug(provider: object) -> str: @@ -302,10 +302,10 @@ def _install_startup(config: dict) -> None: subprocess.run( ["systemctl", "--user", "enable", "--now", "standupbot-warmup.service"], check=True ) - console.print("[green]✅ Startup warm-up installed.[/green]") + console.print("[green][+] Startup warm-up installed.[/green]") except Exception as exc: console.print( - f"[yellow]⚠️ Startup files were written, but automatic registration failed: {sanitize_error_message(exc)}[/yellow]" + f"[yellow][!] Startup files were written, but automatic registration failed: {sanitize_error_message(exc)}[/yellow]" ) @@ -360,7 +360,7 @@ def _uninstall_startup() -> None: artifact = paths.get(key) if artifact and artifact.exists(): artifact.unlink() - console.print("[green]✅ Startup warm-up files removed.[/green]") + console.print("[green][+] Startup warm-up files removed.[/green]") def run_setup_wizard() -> None: @@ -401,7 +401,7 @@ def run_setup_wizard() -> None: choice = _prompt("Enter choice", "1") if choice in ("1", "2"): break - console.print("[red]❌ Invalid choice. Enter 1 or 2.[/red]") + console.print("[red][x] Invalid choice. Enter 1 or 2.[/red]") if choice == "2": config["provider"]["name"] = "groq" @@ -415,17 +415,17 @@ def run_setup_wizard() -> None: if ok: config["provider"]["groq"]["model"] = model_choice break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") api_key = _prompt("Groq API key (leave blank to use env var)", "") ok, message = validate_setup_input("groq_api_key", api_key) if ok and api_key: console.print( - "[yellow]⚠️ Storing API key in config file. Consider using GROQ_API_KEY env var instead.[/yellow]" + "[yellow][!] Storing API key in config file. Consider using GROQ_API_KEY env var instead.[/yellow]" ) config["provider"]["groq"]["api_key"] = api_key elif not ok: - console.print(f"[yellow]⚠️ {message} Leaving API key blank.[/yellow]") + console.print(f"[yellow][!] {message} Leaving API key blank.[/yellow]") else: config["provider"]["name"] = "ollama" while True: @@ -434,7 +434,7 @@ def run_setup_wizard() -> None: if ok: config["provider"]["ollama"]["base_url"] = base_url break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") while True: model = _prompt("Ollama model", "llama3") @@ -442,17 +442,17 @@ def run_setup_wizard() -> None: if ok: config["provider"]["ollama"]["model"] = model break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") from standup.llm.ollama_provider import OllamaProvider provider = OllamaProvider(config) if provider.is_available(): - console.print("[green]✅ Ollama is running and the model is available.[/green]") + console.print("[green][+] Ollama is running and the model is available.[/green]") else: model_name = config["provider"]["ollama"]["model"] console.print( - f"[yellow]⚠️ Ollama not detected. Install from https://ollama.com, " + f"[yellow][!] Ollama not detected. Install from https://ollama.com, " f"then run: ollama pull {model_name}[/yellow]" ) @@ -466,9 +466,9 @@ def run_setup_wizard() -> None: ok, message = validate_setup_input("repo_path", repo_path) if ok: repos.append(repo_path) - console.print(f" [green]✅ Added: {repo_path}[/green]") + console.print(f" [green][+] Added: {repo_path}[/green]") else: - console.print(f" [red]❌ {message}[/red]") + console.print(f" [red][x] {message}[/red]") config["repos"] = repos while True: @@ -477,7 +477,7 @@ def run_setup_wizard() -> None: if ok: config["author_email"] = email break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") while True: hours = _prompt("Hours to look back", "24") @@ -485,7 +485,7 @@ def run_setup_wizard() -> None: if ok: config["hours_lookback"] = int(hours) break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") while True: tone = _prompt("Tone (casual/formal)", "casual") @@ -493,14 +493,14 @@ def run_setup_wizard() -> None: if ok: config["tone"] = tone.lower().strip() break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") webhook = sanitize_string(_prompt("Slack webhook URL (optional)", "")) ok, message = validate_setup_input("slack_webhook_url", webhook) if ok: config["slack_webhook_url"] = webhook else: - console.print(f"[yellow]⚠️ {message} Leaving Slack webhook blank.[/yellow]") + console.print(f"[yellow][!] {message} Leaving Slack webhook blank.[/yellow]") while True: cooldown = _prompt("Cooldown minutes between calls", "30") @@ -508,7 +508,7 @@ def run_setup_wizard() -> None: if ok: config["rate_limit"]["cooldown_minutes"] = int(cooldown) break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") while True: max_calls = _prompt("Max calls per day", "10") @@ -516,7 +516,7 @@ def run_setup_wizard() -> None: if ok: config["rate_limit"]["max_calls_per_day"] = int(max_calls) break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") while True: template_name = _prompt("Template (default/slack/minimal/detailed/jira)", "default") @@ -524,7 +524,7 @@ def run_setup_wizard() -> None: if ok: config["template"] = template_name break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") config["noise_filter_enabled"] = _prompt_bool("noise_filter_enabled", True) config["quality"]["enabled"] = _prompt_bool("quality_enabled", True) @@ -535,7 +535,7 @@ def run_setup_wizard() -> None: if ok: config["quality"]["min_score"] = int(min_score) break - console.print(f"[red]❌ {message}[/red]") + console.print(f"[red][x] {message}[/red]") config["quality"]["show_breakdown"] = _prompt_bool("quality_show_breakdown", False) config["auto_warm_up"] = _prompt_bool("auto_warm_up", False) @@ -650,7 +650,7 @@ def main() -> None: # noqa: C901 errors = validate_cli_args(args, config) if errors: for error in errors: - console.print(f"[red]❌ {error}[/red]") + console.print(f"[red][x] {error}[/red]") sys.exit(1) if args.command == "doctor": @@ -671,9 +671,9 @@ def main() -> None: # noqa: C901 console.print("[yellow]Log clear cancelled.[/yellow]") return if clear_logs(): - console.print("[green]✅ Log file cleared.[/green]") + console.print("[green][+] Log file cleared.[/green]") else: - console.print("[yellow]⚠️ Could not clear log file.[/yellow]") + console.print("[yellow][!] Could not clear log file.[/yellow]") return entries = read_log_entries(args.tail) @@ -739,7 +739,7 @@ def main() -> None: # noqa: C901 return deleted = clear_history(args.days) noun = "entry" if deleted == 1 else "entries" - console.print(f"[green]✅ Deleted {deleted} history {noun}.[/green]") + console.print(f"[green][+] Deleted {deleted} history {noun}.[/green]") return entries = get_history(args.limit) @@ -805,7 +805,7 @@ def main() -> None: # noqa: C901 repos = config.get("repos", []) if not repos: - console.print("[yellow]⚠️ No repos configured. Run: standup --setup[/yellow]") + console.print("[yellow][!] No repos configured. Run: standup --setup[/yellow]") sys.exit(1) author_email = config.get("author_email", "") @@ -829,7 +829,7 @@ def main() -> None: # noqa: C901 ) if filtering_enabled and not processed_commits: console.print( - "[yellow]⚠️ All commits matched the default noise filter. Falling back to unfiltered commits for this run.[/yellow]" + "[yellow][!] All commits matched the default noise filter. Falling back to unfiltered commits for this run.[/yellow]" ) processed_commits = annotate_commits(all_commits) @@ -860,7 +860,7 @@ def main() -> None: # noqa: C901 quality["score"] = int(cached_entry.get("quality_score") or 0) # type: ignore[call-overload] used_cache = True cache_time = str(cached_entry.get("created_at", ""))[11:16] - console.print(f"[dim]⚡ Using cached standup from {cache_time}[/dim]") + console.print(f"[dim][!] Using cached standup from {cache_time}[/dim]") else: enforce_rate_limit(config, force=args.force) @@ -883,7 +883,7 @@ def main() -> None: # noqa: C901 raw_standup_text = provider.generate_standup(prompt, tone) quality = score_standup(raw_standup_text, provider) except LLMProviderError as exc: - console.print(f"[red]❌ {sanitize_error_message(exc)}[/red]") + console.print(f"[red][x] {sanitize_error_message(exc)}[/red]") sys.exit(1) usage = load_usage() @@ -921,10 +921,10 @@ def main() -> None: # noqa: C901 import pyperclip pyperclip.copy(final_output) - console.print("[green]✅ Copied to clipboard![/green]") + console.print("[green][+] Copied to clipboard![/green]") except Exception as exc: console.print( - f"[yellow]⚠️ Clipboard copy failed: {sanitize_error_message(exc)}[/yellow]" + f"[yellow][!] Clipboard copy failed: {sanitize_error_message(exc)}[/yellow]" ) if args.slack: @@ -932,7 +932,7 @@ def main() -> None: # noqa: C901 if webhook: _post_to_slack(webhook, final_output) else: - console.print("[red]❌ No Slack webhook configured.[/red]") + console.print("[red][x] No Slack webhook configured.[/red]") duration_ms = int((time.perf_counter() - started_at) * 1000) log_event( diff --git a/standup/rate_limiter.py b/standup/rate_limiter.py index faf42cd..76978d3 100644 --- a/standup/rate_limiter.py +++ b/standup/rate_limiter.py @@ -152,7 +152,7 @@ def enforce_rate_limit(config: dict, force: bool = False) -> None: if not isinstance(rate, dict) or not rate.get("enabled", True): return if force: - console.print("[dim]⚡ Rate limit bypassed with --force[/dim]") + console.print("[dim][!] Rate limit bypassed with --force[/dim]") return cooldown_minutes = int(rate.get("cooldown_minutes", 30)) @@ -174,7 +174,7 @@ def enforce_rate_limit(config: dict, force: bool = False) -> None: if not allowed: log_event("rate_limit_hit", limit_type="daily", seconds_remaining=0) console.print( - f"[yellow]🚫 Daily cap reached ({calls_today}/{max_calls} calls today).[/yellow]\n" + f"[yellow][x] Daily cap reached ({calls_today}/{max_calls} calls today).[/yellow]\n" "[dim]Use --force to bypass, or wait until tomorrow.[/dim]" ) sys.exit(1) diff --git a/standup/security.py b/standup/security.py index 661bd8e..759f909 100644 --- a/standup/security.py +++ b/standup/security.py @@ -148,7 +148,7 @@ def redact_sensitive_patterns(text: str) -> str: text = pattern.sub(f"[REDACTED:{tag}]", text) if text != original: console.print( - "[yellow]⚠️ Sensitive patterns detected and redacted from commit messages.[/yellow]" + "[yellow][!] Sensitive patterns detected and redacted from commit messages.[/yellow]" ) log_event("redaction_fired", count=match_count) return text @@ -224,7 +224,7 @@ def store_secret(key_name: str, value: str) -> bool: if key_name not in _KEYRING_WARNED_KEYS: _KEYRING_WARNED_KEYS.add(key_name) console.print( - f"[yellow]⚠️ No OS keychain available for {key_name}; " + f"[yellow][!] No OS keychain available for {key_name}; " "storing it in the config file instead (permissions " "still restricted to your user).[/yellow]" ) @@ -318,7 +318,7 @@ def _enforce_windows_acl(file_path: str, label: str = "File") -> None: if file_path not in _PERMISSION_WARNED_PATHS: _PERMISSION_WARNED_PATHS.add(file_path) console.print( - f"[yellow]⚠️ Could not restrict permissions on {label} via icacls. " + f"[yellow][!] Could not restrict permissions on {label} via icacls. " "Ensure only your user can read it.[/yellow]" ) @@ -674,6 +674,6 @@ def _record(name: str, status: str, detail: str) -> None: score = int(100 * (passed + 0.5 * warned) / total) if total else 0 color = "green" if score >= 80 else "yellow" if score >= 60 else "red" console.print( - f"\n[bold {color}]Health Score: {score}/100[/bold {color}] ✅ {passed} ⚠️ {warned} ❌ {failed}" + f"\n[bold {color}]Health Score: {score}/100[/bold {color}] [+] {passed} [!] {warned} [x] {failed}" ) log_event("doctor_run", health_score=score, passed=passed, warned=warned, failed=failed) diff --git a/tests/test_git_reader.py b/tests/test_git_reader.py index 7d78083..64b5302 100644 --- a/tests/test_git_reader.py +++ b/tests/test_git_reader.py @@ -1,7 +1,6 @@ """Tests for standup/git_reader.py.""" import builtins -import subprocess import sys import types @@ -72,28 +71,16 @@ def test_infer_modules_mixed_depths(): @pytest.fixture def git_repo(tmp_path): - subprocess.run(["git", "init", str(tmp_path)], check=True, capture_output=True) - subprocess.run( - ["git", "config", "user.email", "test@example.com"], - cwd=str(tmp_path), - check=True, - capture_output=True, - ) - subprocess.run( - ["git", "config", "user.name", "Test User"], - cwd=str(tmp_path), - check=True, - capture_output=True, - ) + import git as git_module + + repo = git_module.Repo.init(str(tmp_path)) + with repo.config_writer() as cw: + cw.set_value("user", "email", "test@example.com") + cw.set_value("user", "name", "Test User") file_path = tmp_path / "hello.py" file_path.write_text("print('hello')\n", encoding="utf-8") - subprocess.run(["git", "add", "."], cwd=str(tmp_path), check=True, capture_output=True) - subprocess.run( - ["git", "commit", "-m", "initial commit"], - cwd=str(tmp_path), - check=True, - capture_output=True, - ) + repo.index.add(["hello.py"]) + repo.index.commit("initial commit") return tmp_path diff --git a/tests/test_main_startup.py b/tests/test_main_startup.py index fa6b3eb..0160cb2 100644 --- a/tests/test_main_startup.py +++ b/tests/test_main_startup.py @@ -71,15 +71,12 @@ def _make_test_repo(tmp_path): repo = tmp_path / "repo" repo.mkdir() - env = {"GIT_AUTHOR_NAME": "Test", "GIT_AUTHOR_EMAIL": "a@b.com"} subprocess.run(["git", "init", "-q"], cwd=repo, check=True) subprocess.run(["git", "config", "user.email", "a@b.com"], cwd=repo, check=True) subprocess.run(["git", "config", "user.name", "Test"], cwd=repo, check=True) (repo / "f.txt").write_text("hi") subprocess.run(["git", "add", "."], cwd=repo, check=True) - subprocess.run( - ["git", "commit", "-q", "-m", "feat: add file"], cwd=repo, check=True, env=None or env - ) + subprocess.run(["git", "commit", "-q", "-m", "feat: add file"], cwd=repo, check=True) return str(repo)