31519c2a10
* 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>
254 lines
8.0 KiB
Python
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
|