diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ba5259..495b0b8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,8 @@ PyPI version — not the changelog header. `@cli` `success()/error()/warning()` helpers instead of raw `console.print` markup. Owners self-audited and self-fixed their own branches; devpulse verified each diff + re-ran each audit and committed per wave. Landed so far: spawn, - drone, flow, daemon, prax, ai_mail, backup, seedgo, memory, trigger, api, cli. - Remaining waves following. + drone, flow, daemon, prax, ai_mail, backup, seedgo, memory, trigger, api, cli, + aipass. Remaining: commons. ### Fixed diff --git a/src/aipass/aipass/apps/aipass.py b/src/aipass/aipass/apps/aipass.py index 4b451595..34eee1b2 100644 --- a/src/aipass/aipass/apps/aipass.py +++ b/src/aipass/aipass/apps/aipass.py @@ -121,6 +121,14 @@ def main(): command = args[0] remaining = args[1:] if len(args) > 1 else [] + # Subcommand --help guard: intercept before dispatch + if remaining and remaining[0] in ("--help", "-h"): + for module in modules: + if module.handle_command(command, ["--help"]): + return 0 + print(f"Unknown command: {command}") + return 1 + try: if route_command(command, remaining, modules): return 0 diff --git a/src/aipass/aipass/apps/handlers/provider_wire.py b/src/aipass/aipass/apps/handlers/provider_wire.py new file mode 100644 index 00000000..80e12820 --- /dev/null +++ b/src/aipass/aipass/apps/handlers/provider_wire.py @@ -0,0 +1,155 @@ +# =================== AIPass ==================== +# Name: provider_wire.py +# Description: Auto-wire provider settings from manifest into user config +# Version: 1.0.0 +# Created: 2026-07-11 +# Modified: 2026-07-11 +# ============================================= + +"""provider_wire — auto-wire provider settings. + +Implements the additive merge of manifest into ~/.claude/settings.json. +""" + +from __future__ import annotations + +import json +import shutil +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, List + +from aipass.aipass.apps.handlers.json import json_handler + +# ============================================================================= +# HOOK & ENV DESCRIPTIONS +# ============================================================================= + +HOOK_DESCRIPTIONS: Dict[str, str] = { + "pre_edit_gate.py": "blocks edits outside agent's branch", + "subagent_stop_gate.py": "validates agent output on exit", + "auto_fix_diagnostics.py": "auto-fixes lint issues after edits", + "global_prompt_loader.py": "injects branch context on each turn", + "identity_injector.py": "injects agent identity on each turn", + "email_notification.py": "notifies on incoming agent mail", + "branch_prompt_loader.py": "loads branch-specific prompts", + "pre_compact.py": "saves state before context compaction", +} + +ENV_DESCRIPTIONS: Dict[str, str] = { + "AIPASS_HOME": "tells agents where AIPass lives", + "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "prevents conflict with .trinity/ memory system", +} + + +# ============================================================================= +# AUTO-WIRE +# ============================================================================= + + +def auto_wire_provider(manifest_path: Path, interactive: bool = True) -> List[str]: + """Auto-wire provider settings from manifest into ~/.claude/settings.json. + + Additive merge only — never removes or overwrites existing keys/values. + Returns list of action descriptions (for logging/display). + """ + actions: List[str] = [] + + manifest = json_handler.load_path(manifest_path) + if manifest is None: + return actions + claude_section = manifest.get("cli", {}).get("claude", {}) + if not claude_section: + return actions + + settings_path = Path.home() / ".claude" / "settings.json" + if settings_path.exists(): + settings = json_handler.load_path(settings_path) or {} + else: + settings = {} + + if settings_path.exists(): + date_stamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d") + backup_path = settings_path.with_suffix(f".json.bak.{date_stamp}") + shutil.copy2(settings_path, backup_path) + actions.append(f"Backed up settings to {backup_path.name}") + + manifest_hooks = claude_section.get("hooks", []) + + for hook in manifest_hooks: + command = hook.get("command", "") + event = hook.get("event", "") + if not command or not event: + continue + + if "hooks" not in settings: + settings["hooks"] = {} + if event not in settings["hooks"]: + settings["hooks"][event] = [] + event_hooks = settings["hooks"][event] + if not isinstance(event_hooks, list): + event_hooks = [event_hooks] + settings["hooks"][event] = event_hooks + + hook_matcher = hook.get("matcher", "") + already_wired = any( + isinstance(h, dict) and command in json.dumps(h) and h.get("matcher", "") == hook_matcher + for h in event_hooks + ) + if not already_wired: + cmd_entry: Dict[str, object] = { + "type": "command", + "command": command, + } + if hook.get("timeout"): + cmd_entry["timeout"] = hook["timeout"] + wrapper: Dict[str, object] = {} + if hook.get("matcher"): + wrapper["matcher"] = hook["matcher"] + wrapper["hooks"] = [cmd_entry] + event_hooks.append(wrapper) + label = command.rsplit(" ", 1)[-1] if " " in command else command + actions.append(f"Wired hook {label} -> {event}") + + manifest_env = claude_section.get("env", {}) + if manifest_env: + if "env" not in settings: + settings["env"] = {} + repo_root = str(manifest_path.parent.parent) + project_root = str(Path.cwd()) + for key, value in manifest_env.items(): + if key not in settings["env"]: + resolved = value.replace("{{REPO_ROOT}}", repo_root) + resolved = resolved.replace("{{PROJECT_ROOT}}", project_root) + settings["env"][key] = resolved + actions.append(f"Set env {key}={resolved}") + + manifest_perms = claude_section.get("permissions", {}) + manifest_deny = manifest_perms.get("deny", []) + manifest_ask = manifest_perms.get("ask", []) + + if manifest_deny or manifest_ask: + if "permissions" not in settings: + settings["permissions"] = {} + if "deny" not in settings["permissions"]: + settings["permissions"]["deny"] = [] + if "ask" not in settings["permissions"]: + settings["permissions"]["ask"] = [] + + existing_deny = set(settings["permissions"]["deny"]) + for rule in manifest_deny: + if rule not in existing_deny: + settings["permissions"]["deny"].append(rule) + actions.append(f"Added deny rule: {rule}") + + existing_ask = set(settings["permissions"]["ask"]) + for rule in manifest_ask: + if rule not in existing_ask: + settings["permissions"]["ask"].append(rule) + actions.append(f"Added ask rule: {rule}") + + json_handler.save_path(settings_path, settings) + actions.append("Updated ~/.claude/settings.json") + + json_handler.log_operation("auto_wire_provider", {"actions": len(actions)}) + return actions diff --git a/src/aipass/aipass/apps/modules/doctor.py b/src/aipass/aipass/apps/modules/doctor.py index 908a5ce7..c2286db6 100644 --- a/src/aipass/aipass/apps/modules/doctor.py +++ b/src/aipass/aipass/apps/modules/doctor.py @@ -17,7 +17,7 @@ import sys from pathlib import Path from typing import Dict, List, NamedTuple -from aipass.cli.apps.modules import console +from aipass.cli.apps.modules import console, error as cli_error, success from aipass.prax import logger from aipass.aipass.shared.registry_discovery import find_registry as _discover_registry @@ -60,8 +60,8 @@ from aipass.aipass.apps.modules.doctor_fix import ( ) from aipass.aipass.apps.modules.doctor_wire import ( _auto_wire_provider, + _prompt_auto_wire as prompt_auto_wire, check_wire_verify, - prompt_auto_wire, reconcile_stale_deny, ) from aipass.aipass.apps.handlers.system_detect.system_detector import ( @@ -424,7 +424,7 @@ def _check_provider_manifest(interactive: bool = False, fix: bool = False) -> Li if fix: actions = _auto_wire_provider(manifest_path, interactive=False) for action in actions: - console.print(f"[green]✓[/green] {action}") + success(action) wired = bool(actions) else: wired = prompt_auto_wire(manifest_path, missing_hooks, missing_env, missing_deny, missing_ask) @@ -899,11 +899,11 @@ def run_cross_os_record(path: str | None = None, run_e2e: bool = False) -> int: try: written = generate_run_record(path, run_heavy_e2e=run_e2e) except RunRecordError as exc: - console.print(f"[red]✗[/red] {exc}") + cli_error(str(exc)) logger.error("[doctor] cross-os run record failed: %s", exc) return 1 - console.print(f"[green]✓[/green] Run Record written: [bold]{written}[/bold]") + success(f"Run Record written: {written}") console.print("[dim]Complete the '— human' rows and run the real Layer-3 acceptance pass before it counts.[/dim]") console.print() logger.info("[doctor] cross-os run record written to %s", written) @@ -1012,7 +1012,7 @@ def print_help() -> None: "[dim]# write a machine pre-flight Run Record draft (human completes it)[/dim]" ) console.print() - console.print("[yellow]OUTPUT:[/yellow] [green]✓[/green] pass [yellow]![/yellow] warn [red]✗[/red] error") + console.print("[yellow]OUTPUT:[/yellow] pass / warn / error (color-coded)") console.print("[yellow]EXIT:[/yellow] 0 = pass/warn | 1 = errors found") console.print() diff --git a/src/aipass/aipass/apps/modules/doctor_wire.py b/src/aipass/aipass/apps/modules/doctor_wire.py index 81084267..f430f6a0 100644 --- a/src/aipass/aipass/apps/modules/doctor_wire.py +++ b/src/aipass/aipass/apps/modules/doctor_wire.py @@ -18,38 +18,20 @@ Provides: from __future__ import annotations -import json -import shutil import subprocess import sys -from datetime import datetime, timezone from pathlib import Path -from typing import Dict, List, NamedTuple +from typing import List, NamedTuple from aipass.cli.apps.modules import console, success from aipass.prax import logger from aipass.aipass.apps.handlers.json import json_handler - -# ============================================================================= -# HOOK & ENV DESCRIPTIONS (for interactive "no" warning) -# ============================================================================= - -HOOK_DESCRIPTIONS: Dict[str, str] = { - "pre_edit_gate.py": "blocks edits outside agent's branch", - "subagent_stop_gate.py": "validates agent output on exit", - "auto_fix_diagnostics.py": "auto-fixes lint issues after edits", - "global_prompt_loader.py": "injects branch context on each turn", - "identity_injector.py": "injects agent identity on each turn", - "email_notification.py": "notifies on incoming agent mail", - "branch_prompt_loader.py": "loads branch-specific prompts", - "pre_compact.py": "saves state before context compaction", -} - -ENV_DESCRIPTIONS: Dict[str, str] = { - "AIPASS_HOME": "tells agents where AIPass lives", - "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "prevents conflict with .trinity/ memory system", -} +from aipass.aipass.apps.handlers.provider_wire import ( # noqa: F401 + HOOK_DESCRIPTIONS, + ENV_DESCRIPTIONS, + auto_wire_provider as _auto_wire_provider, +) # ============================================================================= @@ -59,130 +41,12 @@ ENV_DESCRIPTIONS: Dict[str, str] = { from aipass.aipass.apps.handlers.provider_reconcile import reconcile_stale_deny # noqa: E402, F401 -# ============================================================================= -# AUTO-WIRE -# ============================================================================= - - -def _auto_wire_provider(manifest_path: Path, interactive: bool = True) -> List[str]: - """Auto-wire provider settings from manifest into ~/.claude/settings.json. - - Additive merge only — never removes or overwrites existing keys/values. - Returns list of action descriptions (for logging/display). - """ - actions: List[str] = [] - - manifest = json_handler.load_path(manifest_path) - if manifest is None: - return actions - claude_section = manifest.get("cli", {}).get("claude", {}) - if not claude_section: - return actions - - # Read existing settings - settings_path = Path.home() / ".claude" / "settings.json" - if settings_path.exists(): - settings = json_handler.load_path(settings_path) or {} - else: - settings = {} - - # Backup - if settings_path.exists(): - date_stamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d") - backup_path = settings_path.with_suffix(f".json.bak.{date_stamp}") - shutil.copy2(settings_path, backup_path) - actions.append(f"Backed up settings to {backup_path.name}") - - # Hooks — add bridge entries to provider settings - manifest_hooks = claude_section.get("hooks", []) - - for hook in manifest_hooks: - command = hook.get("command", "") - event = hook.get("event", "") - if not command or not event: - continue - - if "hooks" not in settings: - settings["hooks"] = {} - if event not in settings["hooks"]: - settings["hooks"][event] = [] - event_hooks = settings["hooks"][event] - if not isinstance(event_hooks, list): - event_hooks = [event_hooks] - settings["hooks"][event] = event_hooks - - hook_matcher = hook.get("matcher", "") - already_wired = any( - isinstance(h, dict) and command in json.dumps(h) and h.get("matcher", "") == hook_matcher - for h in event_hooks - ) - if not already_wired: - cmd_entry: Dict[str, object] = { - "type": "command", - "command": command, - } - if hook.get("timeout"): - cmd_entry["timeout"] = hook["timeout"] - wrapper: Dict[str, object] = {} - if hook.get("matcher"): - wrapper["matcher"] = hook["matcher"] - wrapper["hooks"] = [cmd_entry] - event_hooks.append(wrapper) - label = command.rsplit(" ", 1)[-1] if " " in command else command - actions.append(f"Wired hook {label} -> {event}") - - # Env vars - manifest_env = claude_section.get("env", {}) - if manifest_env: - if "env" not in settings: - settings["env"] = {} - repo_root = str(manifest_path.parent.parent) - project_root = str(Path.cwd()) - for key, value in manifest_env.items(): - if key not in settings["env"]: - resolved = value.replace("{{REPO_ROOT}}", repo_root) - resolved = resolved.replace("{{PROJECT_ROOT}}", project_root) - settings["env"][key] = resolved - actions.append(f"Set env {key}={resolved}") - - # Permissions - manifest_perms = claude_section.get("permissions", {}) - manifest_deny = manifest_perms.get("deny", []) - manifest_ask = manifest_perms.get("ask", []) - - if manifest_deny or manifest_ask: - if "permissions" not in settings: - settings["permissions"] = {} - if "deny" not in settings["permissions"]: - settings["permissions"]["deny"] = [] - if "ask" not in settings["permissions"]: - settings["permissions"]["ask"] = [] - - existing_deny = set(settings["permissions"]["deny"]) - for rule in manifest_deny: - if rule not in existing_deny: - settings["permissions"]["deny"].append(rule) - actions.append(f"Added deny rule: {rule}") - - existing_ask = set(settings["permissions"]["ask"]) - for rule in manifest_ask: - if rule not in existing_ask: - settings["permissions"]["ask"].append(rule) - actions.append(f"Added ask rule: {rule}") - - # Write settings back - json_handler.save_path(settings_path, settings) - actions.append("Updated ~/.claude/settings.json") - - return actions - - # ============================================================================= # INTERACTIVE WIRE PROMPTS # ============================================================================= -def prompt_auto_wire( +def _prompt_auto_wire( manifest_path: Path, missing_hooks: List[str], missing_env: List[str], diff --git a/src/aipass/aipass/apps/modules/handoff.py b/src/aipass/aipass/apps/modules/handoff.py index 7056a287..00f831f5 100644 --- a/src/aipass/aipass/apps/modules/handoff.py +++ b/src/aipass/aipass/apps/modules/handoff.py @@ -21,7 +21,7 @@ Usage: from __future__ import annotations -from aipass.cli.apps.modules import console, warning +from aipass.cli.apps.modules import console, success, warning from aipass.prax import logger from aipass.aipass.apps.handlers.json import json_handler @@ -64,7 +64,7 @@ def do_handoff( if launched: console.print() - console.print(f"[green]✓[/green] Session started via tmux/wt — CLI: [cyan]{cli}[/cyan]") + success(f"Session started via tmux/wt — CLI: {cli}") console.print(f"[dim]Session name: aipass-handoff | cwd: {cwd}[/dim]") console.print() else: diff --git a/src/aipass/aipass/apps/modules/init_flow.py b/src/aipass/aipass/apps/modules/init_flow.py index 498faf05..06f4e36a 100644 --- a/src/aipass/aipass/apps/modules/init_flow.py +++ b/src/aipass/aipass/apps/modules/init_flow.py @@ -35,7 +35,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, List -from aipass.cli.apps.modules import console, warning +from aipass.cli.apps.modules import console, error as cli_error, success, warning from aipass.prax import logger from aipass.aipass.apps.handlers.json import json_handler @@ -210,7 +210,7 @@ def _choose(msg: str, choices: List[str], default: str | None = None) -> str: return choices[idx] except ValueError as exc: logger.info("[init_flow] invalid menu input %r: %s", raw, exc) - console.print("[red]Invalid choice.[/red]") + cli_error("Invalid choice.") # --- STAGE FUNCTIONS --- @@ -358,7 +358,7 @@ def stage_3_user_profile( else: profile_mod.save_profile(existing) - console.print(f"[green]✓[/green] Hello, {name}!") + success(f"Hello, {name}!") _save_stage(3, {"name": name}, dry_run=dry_run) return {"name": name} @@ -379,7 +379,7 @@ def stage_4_style_questions( else: style = _choose("What are you looking to do?", STYLE_CHOICES, default=STYLE_CHOICES[0]) - console.print(f"[green]✓[/green] Got it: {style}") + success(f"Got it: {style}") _save_stage(4, {"style": style}, dry_run=dry_run) return {"style": style} @@ -424,7 +424,7 @@ def _handle_missing_claude(non_interactive: bool) -> None: if raw.lower() in ("y", "yes", ""): console.print("[cyan]Installing Claude Code[/cyan] [dim](this can take a minute)…[/dim]") if _install_claude_code(): - console.print("[green]✓[/green] Claude Code installed successfully.") + success("Claude Code installed successfully.") else: warning("[bold yellow]Installation failed.[/bold yellow]") console.print(" Install manually: https://claude.ai/download") @@ -460,7 +460,7 @@ def stage_5_tool_choice( default="default", ) - console.print(f"[green]✓[/green] {cli_choice} ({flag_variant})") + success(f"{cli_choice} ({flag_variant})") _save_stage(5, {"cli": cli_choice, "flag_variant": flag_variant}, dry_run=dry_run) if dry_run: @@ -496,14 +496,14 @@ def stage_6_first_agent(non_interactive: bool = False, dry_run: bool = False) -> agent_path = f"src/{agent_name}" console.print(f"[cyan]Creating your first agent[/cyan] [dim](drone @spawn create {agent_path})…[/dim]") - success = False + spawned = False if dry_run: console.print(f"[yellow]\\[dry-run][/yellow] would run: drone @spawn create {agent_path}") - success = True + spawned = True else: try: proc = subprocess.run(["drone", "@spawn", "create", agent_path], timeout=60) - success = proc.returncode == 0 + spawned = proc.returncode == 0 except FileNotFoundError as exc: logger.warning("[init_flow] drone not found in stage 6: %s", exc) warning("drone not found — skipping agent creation.") @@ -511,10 +511,10 @@ def stage_6_first_agent(non_interactive: bool = False, dry_run: bool = False) -> logger.warning("[init_flow] spawn timed out in stage 6: %s", exc) warning("spawn timed out — agent may still be created.") - if success: - console.print(f"[green]✓[/green] Agent created at {agent_path}") + if spawned: + success(f"Agent created at {agent_path}") - _save_stage(6, {"agent_name": agent_name, "agent_path": agent_path, "success": success}, dry_run=dry_run) + _save_stage(6, {"agent_name": agent_name, "agent_path": agent_path, "success": spawned}, dry_run=dry_run) return {"agent_name": agent_name, "agent_path": agent_path} @@ -576,12 +576,12 @@ def stage_8_smoke_test(non_interactive: bool = False, dry_run: bool = False) -> aipass_bin = shutil.which("aipass") if drone_bin: - console.print(f"[green]✓[/green] drone: {drone_bin}") + success(f"drone: {drone_bin}") else: warning("drone not on PATH — clone the repo and run setup.sh") if aipass_bin: - console.print(f"[green]✓[/green] aipass: {aipass_bin}") + success(f"aipass: {aipass_bin}") else: warning("aipass not on PATH — clone the repo and run setup.sh") @@ -640,7 +640,7 @@ def stage_9_handoff( if accumulated: _write_init_report(accumulated.get("agent_path", agent_path), accumulated, dry_run=dry_run) console.print() - console.print("[bold green]✓ Setup complete![/bold green]") + success("Setup complete!") console.print() from aipass.aipass.apps.handlers.handoff_platform import launch_inline @@ -716,7 +716,7 @@ def stage_10_done(accumulated: Dict[str, Any] | None = None, dry_run: bool = Fal console.print() console.print(render_step_header(10, TOTAL_STAGES, "Done!")) console.print() - console.print("[bold green]✓ Setup complete![/bold green]") + success("Setup complete!") console.print() console.print(" [cyan]aipass help[/cyan] [dim]# Ask any question[/dim]") console.print(" [cyan]aipass doctor[/cyan] [dim]# Check system health[/dim]") @@ -777,7 +777,7 @@ def run_init( # Pre-flight: refuse to run inside existing projects or agent dirs err = _preflight_check() if err: - console.print(f"[red]✗[/red] {err}") + cli_error(str(err)) return 1 # Template selection — before scaffold @@ -799,7 +799,7 @@ def run_init( if not dry_run: with activity_spinner("Building project scaffold…"): init_project(cwd) - console.print("[green]✓[/green] Project scaffold ready") + success("Project scaffold ready") else: console.print("[yellow]\\[dry-run][/yellow] would create project scaffold") @@ -807,7 +807,7 @@ def run_init( last_done = 0 if dry_run else _get_last_completed_stage() if last_done >= TOTAL_STAGES: - console.print("[green]✓[/green] Setup already complete.") + success("Setup already complete.") console.print("[dim]Run 'aipass doctor' to check status.[/dim]") return 0 @@ -859,7 +859,7 @@ def run_init( if template != TEMPLATE_AIPASS: console.print() - console.print("[green]✓[/green] Project initialized.") + success("Project initialized.") console.print("[dim]Run 'aipass init agent ' to add an agent.[/dim]") return 0 @@ -877,7 +877,7 @@ def print_introspection() -> None: if last == 0: console.print("[dim]Setup not started. Run: aipass init run[/dim]") elif last >= TOTAL_STAGES: - console.print("[green]✓[/green] Setup complete.") + success("Setup complete.") else: console.print(f"[yellow]In progress:[/yellow] stage {last}/{TOTAL_STAGES} completed.") console.print(f"[dim]Run 'aipass init run' to resume from stage {last + 1}.[/dim]") @@ -911,7 +911,7 @@ def _handle_init_scaffold(args: list[str]) -> int: project_name = args[1] if len(args) > 1 else None try: result = init_project(target, project_name) - console.print(f"\n[green]✓[/green] Project initialized at [bold]{target}[/bold]") + success(f"Project initialized at {target}") console.print() # Find the package directory to show in guidance @@ -938,7 +938,7 @@ def _handle_init_scaffold(args: list[str]) -> int: return 0 except Exception as exc: logger.warning("[init_flow] scaffold failed: %s", exc) - console.print(f"[red]✗[/red] Init failed: {exc}") + cli_error(f"Init failed: {exc}") return 1 @@ -952,11 +952,11 @@ def _handle_init_update(args: list[str]) -> int: updated = result.get("updated_files", []) current = result.get("already_current", []) if updated: - console.print(f"[green]✓[/green] Updated {len(updated)} file(s):") + success(f"Updated {len(updated)} file(s):") for f in updated: - console.print(f" [green]+[/green] {f}") + console.print(f" + {f}") else: - console.print("[green]✓[/green] All files already current.") + success("All files already current.") if current: console.print(f" ({len(current)} already up to date)") # Heal registry: prune stale entries (e.g. cross-project ../paths) @@ -968,7 +968,7 @@ def _handle_init_update(args: list[str]) -> int: timeout=30, ) if sync_proc.returncode == 0: - console.print(" [green]Registry synced.[/green]") + success("Registry synced.") except Exception as sync_exc: logger.warning("[init_flow] registry sync during update skipped: %s", sync_exc) @@ -976,14 +976,14 @@ def _handle_init_update(args: list[str]) -> int: return 0 except Exception as exc: logger.warning("[init_flow] update failed: %s", exc) - console.print(f"[red]✗[/red] Update failed: {exc}") + cli_error(f"Update failed: {exc}") return 1 def _handle_init_agent(args: list[str]) -> int: """Handle `aipass init agent ` — create a new agent via spawn.""" if not args: - console.print("[red]✗[/red] Usage: aipass init agent ") + cli_error("Usage: aipass init agent ") return 1 agent_name = args[0] import subprocess as _sp @@ -1075,7 +1075,7 @@ def handle_command(command: str, args: list[str]) -> bool: # Positional args = target path and/or project name for scaffold err = _preflight_check() if err: - console.print(f"[red]✗[/red] {err}") + cli_error(str(err)) sys.exit(1) sys.exit(_handle_init_scaffold(args)) return True diff --git a/src/aipass/aipass/apps/modules/profile.py b/src/aipass/aipass/apps/modules/profile.py index f029d252..f701da29 100644 --- a/src/aipass/aipass/apps/modules/profile.py +++ b/src/aipass/aipass/apps/modules/profile.py @@ -24,7 +24,7 @@ import os import tempfile from pathlib import Path -from aipass.cli.apps.modules import console, error, warning +from aipass.cli.apps.modules import console, error, success, warning from aipass.prax import logger from aipass.aipass.apps.handlers.json import json_handler @@ -151,13 +151,13 @@ def handle_command(command: str, args: list[str]) -> bool: return True field, value = args[1], args[2] if field not in USER_FIELDS: - console.print(f"[red]Unknown field: {field}[/red]") + error(f"Unknown field: {field}") console.print("[dim]Valid fields: " + ", ".join(USER_FIELDS) + "[/dim]") return True profile = get_user_profile() profile[field] = value save_profile(profile) - console.print(f"[green]✓[/green] {field} = {value}") + success(f"{field} = {value}") return True if args[0] == "clear": @@ -166,7 +166,7 @@ def handle_command(command: str, args: list[str]) -> bool: skip_confirm = any(a in ("--yes", "-y") for a in args[1:]) if skip_confirm: save_profile({f: None for f in USER_FIELDS}) - console.print("[green]✓[/green] Profile cleared.") + success("Profile cleared.") return True warning("Type 'aipass' to confirm clearing your profile (ctrl-C to cancel):") try: @@ -177,7 +177,7 @@ def handle_command(command: str, args: list[str]) -> bool: return True if confirm == "aipass": save_profile({f: None for f in USER_FIELDS}) - console.print("[green]✓[/green] Profile cleared.") + success("Profile cleared.") else: console.print("[yellow]Cancelled.[/yellow]") return True diff --git a/src/aipass/aipass/tests/test_doctor.py b/src/aipass/aipass/tests/test_doctor.py index bd60b8f0..7213fb11 100644 --- a/src/aipass/aipass/tests/test_doctor.py +++ b/src/aipass/aipass/tests/test_doctor.py @@ -658,7 +658,7 @@ class TestReconcileStaleDeny: """Missing settings.json returns no results.""" from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny - with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): + with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path): results = reconcile_stale_deny(fix=False) assert results == [] @@ -672,7 +672,7 @@ class TestReconcileStaleDeny: json.dumps({"permissions": {"deny": ["Bash(git push --force*)", "Bash(git reset --hard*)"]}}), encoding="utf-8", ) - with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): + with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path): results = reconcile_stale_deny(fix=False) assert len(results) == 1 assert results[0][1] == GLYPH_PASS @@ -688,7 +688,7 @@ class TestReconcileStaleDeny: json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(git push --force*)", "Bash(rm -r *)"]}}), encoding="utf-8", ) - with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): + with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path): results = reconcile_stale_deny(fix=False) assert len(results) == 1 assert results[0][1] == GLYPH_WARN @@ -706,7 +706,7 @@ class TestReconcileStaleDeny: "env": {"AIPASS_HOME": "/test"}, } settings.write_text(json.dumps(original), encoding="utf-8") - with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): + with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path): results = reconcile_stale_deny(fix=True) assert len(results) == 1 assert results[0][1] == GLYPH_PASS @@ -727,7 +727,7 @@ class TestReconcileStaleDeny: json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(git reset --hard*)"]}}), encoding="utf-8", ) - with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): + with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path): results = reconcile_stale_deny(fix=True) assert len(results) == 1 assert results[0][1] == GLYPH_PASS @@ -744,7 +744,7 @@ class TestReconcileStaleDeny: json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(rm -r *)"]}}), encoding="utf-8", ) - with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): + with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path): reconcile_stale_deny(fix=True) results = reconcile_stale_deny(fix=True) assert len(results) == 1 @@ -758,7 +758,7 @@ class TestReconcileStaleDeny: settings = tmp_path / ".claude" / "settings.json" settings.parent.mkdir(parents=True) settings.write_text(json.dumps({"permissions": {"deny": []}}), encoding="utf-8") - with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): + with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path): results = reconcile_stale_deny(fix=False) assert len(results) == 1 assert results[0][1] == GLYPH_PASS @@ -770,7 +770,7 @@ class TestReconcileStaleDeny: settings = tmp_path / ".claude" / "settings.json" settings.parent.mkdir(parents=True) settings.write_text(json.dumps({"env": {"FOO": "bar"}}), encoding="utf-8") - with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): + with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path): results = reconcile_stale_deny(fix=False) assert len(results) == 1 assert results[0][1] == GLYPH_PASS @@ -857,7 +857,7 @@ class TestPromptAutoWireIsatty: patch.object(doctor_wire, "_print_manual_wire_warning") as mock_warn, ): mock_stdin.isatty.return_value = False - result = doctor_wire.prompt_auto_wire(**self._args()) + result = doctor_wire._prompt_auto_wire(**self._args()) assert result is False mock_input.assert_not_called() @@ -873,7 +873,7 @@ class TestPromptAutoWireIsatty: patch.object(doctor_wire, "_print_manual_wire_warning"), ): mock_stdin.isatty.return_value = True - result = doctor_wire.prompt_auto_wire(**self._args()) + result = doctor_wire._prompt_auto_wire(**self._args()) assert result is False mock_input.assert_called_once() @@ -888,7 +888,7 @@ class TestPromptAutoWireIsatty: patch.object(doctor_wire, "_auto_wire_provider", return_value=["wired hook"]) as mock_wire, ): mock_stdin.isatty.return_value = True - result = doctor_wire.prompt_auto_wire(**self._args()) + result = doctor_wire._prompt_auto_wire(**self._args()) assert result is True mock_wire.assert_called_once() diff --git a/src/aipass/aipass/tests/test_init_flow.py b/src/aipass/aipass/tests/test_init_flow.py index 5d3213b6..3c30149a 100644 --- a/src/aipass/aipass/tests/test_init_flow.py +++ b/src/aipass/aipass/tests/test_init_flow.py @@ -671,7 +671,8 @@ class TestInitUpdateRegistrySync: return_value={"updated_files": [], "already_current": []}, ), patch(f"{_MOD_UPDATE}.subprocess.run", return_value=mock_result) as mock_run, - patch(f"{_MOD_UPDATE}.console") as mock_console, + patch(f"{_MOD_UPDATE}.console"), + patch(f"{_MOD_UPDATE}.success") as mock_success, patch(f"{_MOD_UPDATE}.json_handler"), ): rc = _handle_init_update([str(tmp_path)]) @@ -682,7 +683,7 @@ class TestInitUpdateRegistrySync: text=True, timeout=30, ) - sync_calls = [c for c in mock_console.print.call_args_list if "Registry synced" in str(c)] + sync_calls = [c for c in mock_success.call_args_list if "Registry synced" in str(c)] assert len(sync_calls) == 1 def test_sync_failure_degrades_silently(self, tmp_path: Path) -> None: