Files
Aperant/apps/backend/cli/utils.py
T
StillKnotKnown 31519c2a10 fix: add helpful error message when Python dependencies are missing (ACS-145) (#755)
* fix: add helpful error message when Python dependencies are missing

When running runner scripts (spec_runner, insights_runner, etc.) without
the virtual environment activated, users would get a cryptic
ModuleNotFoundError for 'dotenv' or other dependencies.

This fix adds a try-except around the dotenv import that provides a clear
error message explaining:
- The issue is likely due to not using the virtual environment
- How to activate the venv (Linux/macOS/Windows)
- How to install dependencies directly
- Shows the current Python executable being used

Also fixes CLI-USAGE.md which had incorrect paths for spec_runner.py
(the file is in runners/, not the backend root).

Related to: ACS-145

Signed-off-by: StillKnotKnown <stillknotknown@users.noreply.github.com>

* fix: improve error messages with explicit package name and requirements path

- cli/utils.py: Explicitly mention 'python-dotenv' and add 'pip install python-dotenv' option
- insights_runner.py: Use full path 'apps/backend/requirements.txt' for clarity

Signed-off-by: StillKnotKnown <stillknotknown@users.noreply.github.com>

* refactor: centralize dotenv import error handling

- Create shared import_dotenv() function in cli/utils.py
- Update all runner scripts to use centralized function
- Removes ~73 lines of duplicate code across 6 files
- Ensures consistent error messaging (mentions python-dotenv explicitly)
- Fixes path inconsistency in insights_runner.py

Addresses CodeRabbit feedback about DRY principle violations.

Signed-off-by: StillKnotKnown <stillknotknown@users.noreply.github.com>

* style: fix import ordering to satisfy ruff I001 rule

Add blank lines to separate local imports and function calls from
third-party imports, properly delineating import groups.

Signed-off-by: StillKnotKnown <stillknotknown@users.noreply.github.com>

* style: auto-fix ruff I001 import ordering

Ruff auto-fixed by adding blank line after 'from cli.utils import import_dotenv'
to properly separate the import from the function call.

Signed-off-by: StillKnotKnown <stillknotknown@users.noreply.github.com>

* style: apply ruff formatting to cli/utils.py

- Add blank line after import statement
- Use double quotes instead of single quotes

Signed-off-by: StillKnotKnown <stillknotknown@users.noreply.github.com>

* refactor: return load_dotenv instead of mutating sys.modules

- Change import_dotenv() to return load_dotenv callable
- Remove sys.modules mutation for cleaner approach
- Update callers to do: load_dotenv = import_dotenv()
- Fixes ruff I001 import ordering violations
- Preserves same error message on ImportError

Addresses CodeRabbit feedback about import-order complexity.

Signed-off-by: StillKnotKnown <stillknotknown@users.noreply.github.com>

---------

Signed-off-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
Co-authored-by: Alex <63423455+AlexMadera@users.noreply.github.com>
2026-01-07 07:19:24 +01:00

254 lines
8.0 KiB
Python

"""
CLI Utilities
==============
Shared utility functions for the Auto Claude CLI.
"""
import os
import sys
from pathlib import Path
# Ensure parent directory is in path for imports (before other imports)
_PARENT_DIR = Path(__file__).parent.parent
if str(_PARENT_DIR) not in sys.path:
sys.path.insert(0, str(_PARENT_DIR))
from core.auth import get_auth_token, get_auth_token_source
def import_dotenv():
"""
Import and return load_dotenv with helpful error message if not installed.
This centralized function ensures consistent error messaging across all
runner scripts when python-dotenv is not available.
Returns:
The load_dotenv function
Raises:
SystemExit: If dotenv cannot be imported, with helpful installation instructions.
"""
try:
from dotenv import load_dotenv as _load_dotenv
return _load_dotenv
except ImportError:
sys.exit(
"Error: Required Python package 'python-dotenv' is not installed.\n"
"\n"
"This usually means you're not using the virtual environment.\n"
"\n"
"To fix this:\n"
"1. From the 'apps/backend/' directory, activate the venv:\n"
" source .venv/bin/activate # Linux/macOS\n"
" .venv\\Scripts\\activate # Windows\n"
"\n"
"2. Or install dependencies directly:\n"
" pip install python-dotenv\n"
" pip install -r requirements.txt\n"
"\n"
f"Current Python: {sys.executable}\n"
)
# Load .env with helpful error if dependencies not installed
load_dotenv = import_dotenv()
from graphiti_config import get_graphiti_status
from linear_integration import LinearManager
from linear_updater import is_linear_enabled
from spec.pipeline import get_specs_dir
from ui import (
Icons,
bold,
box,
icon,
muted,
)
# Configuration - uses shorthand that resolves via API Profile if configured
DEFAULT_MODEL = "sonnet" # Changed from "opus" (fix #433)
def setup_environment() -> Path:
"""
Set up the environment and return the script directory.
Returns:
Path to the auto-claude directory
"""
# Add auto-claude directory to path for imports
script_dir = Path(__file__).parent.parent.resolve()
sys.path.insert(0, str(script_dir))
# Load .env file - check both auto-claude/ and dev/auto-claude/ locations
env_file = script_dir / ".env"
dev_env_file = script_dir.parent / "dev" / "auto-claude" / ".env"
if env_file.exists():
load_dotenv(env_file)
elif dev_env_file.exists():
load_dotenv(dev_env_file)
return script_dir
def find_spec(project_dir: Path, spec_identifier: str) -> Path | None:
"""
Find a spec by number or full name.
Args:
project_dir: Project root directory
spec_identifier: Either "001" or "001-feature-name"
Returns:
Path to spec folder, or None if not found
"""
specs_dir = get_specs_dir(project_dir)
if specs_dir.exists():
# Try exact match first
exact_path = specs_dir / spec_identifier
if exact_path.exists() and (exact_path / "spec.md").exists():
return exact_path
# Try matching by number prefix
for spec_folder in specs_dir.iterdir():
if spec_folder.is_dir() and spec_folder.name.startswith(
spec_identifier + "-"
):
if (spec_folder / "spec.md").exists():
return spec_folder
# Check worktree specs (for merge-preview, merge, review, discard operations)
worktree_base = project_dir / ".auto-claude" / "worktrees" / "tasks"
if worktree_base.exists():
# Try exact match in worktree
worktree_spec = (
worktree_base / spec_identifier / ".auto-claude" / "specs" / spec_identifier
)
if worktree_spec.exists() and (worktree_spec / "spec.md").exists():
return worktree_spec
# Try matching by prefix in worktrees
for worktree_dir in worktree_base.iterdir():
if worktree_dir.is_dir() and worktree_dir.name.startswith(
spec_identifier + "-"
):
spec_in_worktree = (
worktree_dir / ".auto-claude" / "specs" / worktree_dir.name
)
if (
spec_in_worktree.exists()
and (spec_in_worktree / "spec.md").exists()
):
return spec_in_worktree
return None
def validate_environment(spec_dir: Path) -> bool:
"""
Validate that the environment is set up correctly.
Returns:
True if valid, False otherwise (with error messages printed)
"""
valid = True
# Check for OAuth token (API keys are not supported)
if not get_auth_token():
print("Error: No OAuth token found")
print("\nAuto Claude requires Claude Code OAuth authentication.")
print("Direct API keys (ANTHROPIC_API_KEY) are not supported.")
print("\nTo authenticate, run:")
print(" claude setup-token")
valid = False
else:
# Show which auth source is being used
source = get_auth_token_source()
if source:
print(f"Auth: {source}")
# Show custom base URL if set
base_url = os.environ.get("ANTHROPIC_BASE_URL")
if base_url:
print(f"API Endpoint: {base_url}")
# Check for spec.md in spec directory
spec_file = spec_dir / "spec.md"
if not spec_file.exists():
print(f"\nError: spec.md not found in {spec_dir}")
valid = False
# Check Linear integration (optional but show status)
if is_linear_enabled():
print("Linear integration: ENABLED")
# Show Linear project status if initialized
project_dir = (
spec_dir.parent.parent
) # auto-claude/specs/001-name -> project root
linear_manager = LinearManager(spec_dir, project_dir)
if linear_manager.is_initialized:
summary = linear_manager.get_progress_summary()
print(f" Project: {summary.get('project_name', 'Unknown')}")
print(
f" Issues: {summary.get('mapped_subtasks', 0)}/{summary.get('total_subtasks', 0)} mapped"
)
else:
print(" Status: Will be initialized during planner session")
else:
print("Linear integration: DISABLED (set LINEAR_API_KEY to enable)")
# Check Graphiti integration (optional but show status)
graphiti_status = get_graphiti_status()
if graphiti_status["available"]:
print("Graphiti memory: ENABLED")
print(f" Database: {graphiti_status['database']}")
if graphiti_status.get("db_path"):
print(f" Path: {graphiti_status['db_path']}")
elif graphiti_status["enabled"]:
print(
f"Graphiti memory: CONFIGURED but unavailable ({graphiti_status['reason']})"
)
else:
print("Graphiti memory: DISABLED (set GRAPHITI_ENABLED=true to enable)")
print()
return valid
def print_banner() -> None:
"""Print the Auto-Build banner."""
content = [
bold(f"{icon(Icons.LIGHTNING)} AUTO-BUILD FRAMEWORK"),
"",
"Autonomous Multi-Session Coding Agent",
muted("Subtask-Based Implementation with Phase Dependencies"),
]
print()
print(box(content, width=70, style="heavy"))
def get_project_dir(provided_dir: Path | None) -> Path:
"""
Determine the project directory.
Args:
provided_dir: User-provided project directory (or None)
Returns:
Resolved project directory path
"""
if provided_dir:
return provided_dir.resolve()
project_dir = Path.cwd()
# Auto-detect if running from within apps/backend directory (the source code)
if project_dir.name == "backend" and (project_dir / "run.py").exists():
# Running from within apps/backend/ source directory, go up 2 levels
project_dir = project_dir.parent.parent
return project_dir