diff --git a/src/aipass/drone/__init__.py b/src/aipass/drone/__init__.py index 18df0abd..1de28f9d 100644 --- a/src/aipass/drone/__init__.py +++ b/src/aipass/drone/__init__.py @@ -41,7 +41,7 @@ from aipass.drone.apps.modules import ( from aipass.drone.apps.modules.resolver import branch_exists, get_branch_info, list_branches, resolve_branch from aipass.drone.apps.modules.router import route_all, route_command -__version__ = "1.0.0" +__version__ = "1.1.0" __all__ = [ # Core API diff --git a/src/aipass/drone/apps/drone.py b/src/aipass/drone/apps/drone.py index 7c7deeaf..f6da6e73 100644 --- a/src/aipass/drone/apps/drone.py +++ b/src/aipass/drone/apps/drone.py @@ -38,6 +38,10 @@ from aipass.drone.apps.modules.module_registry import ( VERSION = "1.1.0" MODULES_DIR = Path(__file__).parent / "modules" +# Interactive mode — commands/branches that bypass capture + timeout for live terminal output. +INTERACTIVE_COMMANDS = ("monitor", "audit") +INTERACTIVE_BRANCHES = ("cli",) + # ============================================================================= # AUTO-DISCOVERY @@ -287,10 +291,7 @@ def _handle_custom_command(args: list[str]) -> int: cmd_args = list(cmd_data.get("args", [])) + remaining_args module_name = target.lstrip("@").lower() - # Interactive detection -- same logic as _handle_target - interactive_commands = ("monitor", "audit") - interactive_branches = ("cli",) - interactive = command in interactive_commands or module_name in interactive_branches + interactive = command in INTERACTIVE_COMMANDS or module_name in INTERACTIVE_BRANCHES try: result = route_command( @@ -319,14 +320,9 @@ def _handle_target(args: List[str]) -> int: rest = args[1:] module_name = target.lstrip("@").lower() - # Interactive mode bypasses capture + timeout for human-facing output. - # Per-command: specific commands that need live terminal (progress bars, TUI). - # Per-branch: all commands from that branch get interactive mode (Rich CLI). - interactive_commands = ("monitor", "audit") - interactive_branches = ("cli",) first_cmd = rest[0] if rest and rest[0] != "--help" else None needs_interactive = ( - first_cmd in interactive_commands or module_name in interactive_branches + first_cmd in INTERACTIVE_COMMANDS or module_name in INTERACTIVE_BRANCHES ) # Route to internal module — unless command needs interactive terminal, diff --git a/src/aipass/drone/apps/handlers/discovery_handler.py b/src/aipass/drone/apps/handlers/discovery_handler.py index 051a8c70..61c65999 100644 --- a/src/aipass/drone/apps/handlers/discovery_handler.py +++ b/src/aipass/drone/apps/handlers/discovery_handler.py @@ -19,7 +19,6 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional -from aipass.prax import logger from aipass.prax.apps.modules.logger import system_logger from .exceptions import CommandExecutionError from aipass.drone.apps.handlers.json import json_handler diff --git a/src/aipass/drone/apps/handlers/router_handler.py b/src/aipass/drone/apps/handlers/router_handler.py index ba3f81ea..ee1c77ab 100644 --- a/src/aipass/drone/apps/handlers/router_handler.py +++ b/src/aipass/drone/apps/handlers/router_handler.py @@ -19,7 +19,6 @@ import sys from pathlib import Path from typing import Dict, List, Optional -from aipass.prax import logger from aipass.prax.apps.modules.logger import system_logger from .exceptions import CommandExecutionError from .executor import CommandResult, execute_command diff --git a/src/aipass/drone/apps/modules/router.py b/src/aipass/drone/apps/modules/router.py index 69793b19..57ece6b6 100644 --- a/src/aipass/drone/apps/modules/router.py +++ b/src/aipass/drone/apps/modules/router.py @@ -17,7 +17,6 @@ import os from pathlib import Path from typing import Dict, List, Optional -from aipass.prax import logger from aipass.prax.apps.modules.logger import system_logger from aipass.cli.apps.modules import console from aipass.drone.apps.handlers.executor import CommandResult diff --git a/src/aipass/spawn/apps/handlers/file_ops.py b/src/aipass/spawn/apps/handlers/file_ops.py index 6659456f..d48d6baf 100644 --- a/src/aipass/spawn/apps/handlers/file_ops.py +++ b/src/aipass/spawn/apps/handlers/file_ops.py @@ -186,7 +186,7 @@ def regenerate_template_registry(target_dir): file_id = f"f{file_idx:03d}" try: content = item.read_bytes() - content_hash = hashlib.sha256(content).hexdigest()[:16] + content_hash = hashlib.sha256(content).hexdigest()[:12] except (IOError, PermissionError) as e: logger.error(f"[spawn] Failed to read file for hashing {item}: {e}") content_hash = "unreadable" diff --git a/src/aipass/spawn/apps/modules/passport.py b/src/aipass/spawn/apps/modules/passport.py index 88e7d954..69f01afd 100644 --- a/src/aipass/spawn/apps/modules/passport.py +++ b/src/aipass/spawn/apps/modules/passport.py @@ -14,10 +14,13 @@ All implementation logic lives in apps/handlers/passport_ops.py. import argparse +from pathlib import Path + from aipass.cli.apps.modules import console, error, warning from aipass.prax import logger from aipass.spawn.apps.handlers.passport_ops import grant_passport +from aipass.spawn.apps.handlers.registry import find_registry from aipass.spawn.apps.handlers.json import json_handler @@ -58,6 +61,46 @@ def handle_command(command: str, args: list) -> bool: return handle_passport(args) == 0 +def _resolve_target(dirname: str) -> Path: + """Resolve a @dirname to a filesystem path. + + Searches CWD, project root (via registry), and src/ subdirectories. + For new directories, defaults to CWD-relative. + """ + # Absolute or home-relative path — use directly + if dirname.startswith("/") or dirname.startswith("~"): + return Path(dirname).expanduser() + + # 1. CWD-relative + cwd_path = Path.cwd() / dirname + if cwd_path.exists(): + return cwd_path + + # 2. Project-relative via registry + try: + reg_path = find_registry() + project_root = reg_path.parent + for candidate in [ + project_root / dirname, + project_root / "src" / dirname, + ]: + if candidate.exists(): + return candidate + # Search src/*/dirname (e.g., src/aipass/target) + src_dir = project_root / "src" + if src_dir.is_dir(): + for sub in src_dir.iterdir(): + if sub.is_dir(): + candidate = sub / dirname + if candidate.exists(): + return candidate + except Exception as exc: + logger.warning("[passport] Registry lookup failed during target resolution: %s", exc) + + # 3. Default: CWD-relative (passport will create it if needed) + return cwd_path + + def handle_passport(args: list[str]) -> int: """Parse args and execute passport grant. @@ -93,10 +136,8 @@ def handle_passport(args: list[str]) -> int: # Strip @ prefix if present target = parsed.target.lstrip("@") - # Resolve relative to src/aipass/ (standard branch location) - from pathlib import Path - aipass_root = Path(__file__).parents[3] # modules -> apps -> spawn -> aipass - target_path = aipass_root / target + # Resolve target path — CWD-aware for external project support + target_path = _resolve_target(target) result = grant_passport( target_path=str(target_path), diff --git a/src/aipass/spawn/tests/test_file_ops.py b/src/aipass/spawn/tests/test_file_ops.py index 9a48ca7c..330a8070 100644 --- a/src/aipass/spawn/tests/test_file_ops.py +++ b/src/aipass/spawn/tests/test_file_ops.py @@ -381,7 +381,7 @@ class TestRegenerateTemplateRegistry: expected_hash = hashlib.sha256( content.encode("utf-8") - ).hexdigest()[:16] + ).hexdigest()[:12] # Find the file entry files = data["files"]