feat(cli): 100% seedgo compliance — all 24 standards (#66)

* fix(ai_mail): resolve relative mailbox_path bug + archive dead handlers

Fix get_user_by_email() and get_all_users() returning relative mailbox_path
values from registry instead of absolute paths (causing doubled paths in reply).
Added path resolution matching get_current_user() pattern.

Archive 7 unused handler files to apps/.archive/ (gitignored):
- pending_work.py — planned feature, never wired
- lock_utils.py — superseded by inbox_lock.py
- data_ops.py — consumer (error_monitor) was archived
- config_generator.py, users/load.py, registry/validate.py — scaffolded, never used
- trigger/error_handler.py — superseded by trigger branch's event system

Added test_user_paths.py (13 tests) covering absolute path resolution.

Co-Authored-By: @ai_mail <ai_mail@aipass>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(cli): achieve 100% seedgo compliance across all 24 standards

Clean up json_handler (remove prax dependency, use portable Path resolution),
wire json_handler into display.py, templates.py, and init_project.py using
module import pattern. Restructure handle_command() with seedgo-compliant
no-args introspection gate. Update README with init commands and architecture.

Co-Authored-By: @cli <cli@aipass>

---------

Co-authored-by: @ai_mail <ai_mail@aipass>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: @cli <cli@aipass>
This commit is contained in:
AIPass
2026-03-17 16:44:37 -07:00
committed by GitHub
co-authored by @cli @ai_mail Claude Opus 4.6
parent eb527fe0b2
commit d7f2587465
16 changed files with 349 additions and 1293 deletions
@@ -1,151 +0,0 @@
# =================== AIPass ====================
# Name: pending_work.py
# Description: Pending Work Handler
# Version: 1.0.0
# Created: 2026-02-17
# Modified: 2026-02-17
# =============================================
"""
Pending Work Handler
Read/write utils for .pending_work.json per branch.
Tracks dispatch workflows, waiting_for states, and next_action queues.
"""
import json
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, Optional
PENDING_WORK_FILENAME = ".pending_work.json"
def _get_pending_path(branch_path: Path) -> Path:
"""Get the pending work file path for a branch."""
if branch_path == Path("/"):
return Path.cwd() / ".ai_mail.local" / PENDING_WORK_FILENAME
return branch_path / ".ai_mail.local" / PENDING_WORK_FILENAME
def load_pending_work(branch_path: Path) -> Dict[str, Any]:
"""
Load pending work for a branch.
Args:
branch_path: Path to the branch directory
Returns:
Pending work dict with 'workflows' array
"""
pending_path = _get_pending_path(branch_path)
if not pending_path.exists():
return {"workflows": []}
try:
with open(pending_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if "workflows" not in data:
data["workflows"] = []
return data
except (json.JSONDecodeError, OSError):
return {"workflows": []}
def save_pending_work(branch_path: Path, data: Dict[str, Any]) -> bool:
"""
Save pending work for a branch.
Args:
branch_path: Path to the branch directory
data: Pending work dict to save
Returns:
True if saved successfully
"""
pending_path = _get_pending_path(branch_path)
pending_path.parent.mkdir(parents=True, exist_ok=True)
try:
data["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(pending_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return True
except OSError:
return False
def add_workflow(
branch_path: Path,
dispatch_id: str,
dispatched_to: str,
subject: str,
waiting_for: Optional[str] = None,
next_action: Optional[str] = None
) -> bool:
"""
Add a workflow entry to a branch's pending work.
Adds a new dispatch workflow to track pending communications and actions.
Args:
branch_path: Path to the branch directory
dispatch_id: Message ID of the dispatched email
dispatched_to: Target branch email (e.g., @flow)
subject: Subject of the dispatched email
waiting_for: What the branch is waiting for (e.g., reply from @flow)
next_action: What to do when the response arrives
Returns:
True if added successfully
"""
data = load_pending_work(branch_path)
entry = {
"dispatch_id": dispatch_id,
"dispatched_to": dispatched_to,
"subject": subject,
"status": "waiting",
"created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
if waiting_for:
entry["waiting_for"] = waiting_for
if next_action:
entry["next_action"] = next_action
data["workflows"].append(entry)
return save_pending_work(branch_path, data)
def clear_workflow(branch_path: Path, dispatch_id: str) -> bool:
"""
Remove a completed workflow entry.
Args:
branch_path: Path to the branch directory
dispatch_id: Message ID to remove
Returns:
True if removed successfully
"""
data = load_pending_work(branch_path)
data["workflows"] = [
w for w in data["workflows"]
if w.get("dispatch_id") != dispatch_id
]
return save_pending_work(branch_path, data)
def has_pending_work(branch_path: Path) -> bool:
"""
Check if a branch has any pending workflows.
Args:
branch_path: Path to the branch directory
Returns:
True if there are active workflows
"""
data = load_pending_work(branch_path)
return len(data.get("workflows", [])) > 0
@@ -1,202 +0,0 @@
# =================== AIPass ====================
# Name: lock_utils.py
# Description: Dispatch Lock Handler
# Version: 1.1.0
# Created: 2026-02-09
# Modified: 2026-02-09
# =============================================
"""
Dispatch Lock Handler
PID-based single instance lock per branch.
Prevents multiple dispatch agents from spawning concurrently at the same branch.
Uses atomic file creation (O_CREAT|O_EXCL) to avoid race conditions.
"""
import os
import json
from pathlib import Path
from datetime import datetime
# Standard logging
# Lock file name - placed in branch's .ai_mail.local/ directory
LOCK_FILENAME = ".dispatch.lock"
# Stale lock timeout in seconds (10 minutes)
STALE_LOCK_TIMEOUT = 600
def _get_lock_path(branch_path: Path) -> Path:
"""Get the lock file path for a branch."""
if branch_path == Path("/"):
return Path.cwd() / ".ai_mail.local" / LOCK_FILENAME
return branch_path / ".ai_mail.local" / LOCK_FILENAME
def _is_pid_running(pid: int) -> bool:
"""Check if a process with the given PID is still running."""
try:
os.kill(pid, 0)
return True
except PermissionError:
# Process exists but we don't have permission to signal it
return True
except ProcessLookupError:
return False
def _is_lock_stale(lock_data: dict) -> bool:
"""
Check if a lock is stale (process dead or timeout exceeded).
Returns True if the lock should be considered stale and can be removed.
"""
pid = lock_data.get("pid")
timestamp = lock_data.get("timestamp")
# No PID = stale
if pid is None:
return True
# Process no longer running = stale
if not _is_pid_running(pid):
return True # Process no longer running - stale
# Timeout check - if lock is older than STALE_LOCK_TIMEOUT, consider stale
if timestamp:
try:
lock_time = datetime.fromisoformat(timestamp)
elapsed = (datetime.now() - lock_time).total_seconds()
if elapsed > STALE_LOCK_TIMEOUT:
return True # Timeout exceeded - stale
except (ValueError, TypeError) as e:
pass # Unparseable timestamp - ignore
return False
def acquire_lock(branch_path: Path, pid: int) -> tuple[bool, str]:
"""
Attempt to acquire a dispatch lock for a branch.
Uses atomic file creation to prevent race conditions.
Args:
branch_path: Path to the target branch
pid: PID of the agent being spawned
Returns:
Tuple of (acquired: bool, message: str)
If not acquired, message contains reason (e.g., existing PID info)
"""
lock_path = _get_lock_path(branch_path)
# Ensure parent directory exists
lock_path.parent.mkdir(parents=True, exist_ok=True)
# Check for existing lock first
if lock_path.exists():
try:
with open(lock_path, 'r', encoding='utf-8') as f:
existing_lock = json.load(f)
if _is_lock_stale(existing_lock):
# Remove stale lock and try again
lock_path.unlink(missing_ok=True) # Remove stale lock
else:
# Active lock exists - bounce
existing_pid = existing_lock.get("pid", "unknown")
existing_sender = existing_lock.get("sender", "unknown")
existing_time = existing_lock.get("timestamp", "unknown")
msg = f"Branch already has active dispatch agent (PID: {existing_pid}, sender: {existing_sender}, since: {existing_time})"
return False, msg
except (json.JSONDecodeError, OSError) as e:
# Corrupted lock file - remove it
lock_path.unlink(missing_ok=True) # Corrupted lock - remove it
# Create lock file atomically using O_CREAT|O_EXCL
lock_data = {
"pid": pid,
"timestamp": datetime.now().isoformat(),
"branch": str(branch_path)
}
try:
fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
try:
os.write(fd, json.dumps(lock_data, indent=2).encode('utf-8'))
finally:
os.close(fd)
return True, "Lock acquired"
except FileExistsError:
# Race condition - another process created the lock between our check and create
msg = "Lock acquisition failed - another dispatch just started"
return False, msg
def release_lock(branch_path: Path, pid: int | None = None) -> bool:
"""
Release a dispatch lock for a branch.
Args:
branch_path: Path to the target branch
pid: If provided, only release if lock belongs to this PID (safety check)
Returns:
True if lock was released, False if not found or owned by different PID
"""
lock_path = _get_lock_path(branch_path)
if not lock_path.exists():
return True # No lock = already released
# If PID specified, verify ownership before releasing
if pid is not None:
try:
with open(lock_path, 'r', encoding='utf-8') as f:
lock_data = json.load(f)
if lock_data.get("pid") != pid:
return False # Lock owned by different PID
except (json.JSONDecodeError, OSError) as e:
pass # Corrupted lock file during release - continue anyway
try:
lock_path.unlink(missing_ok=True)
return True
except OSError as e:
return False # Failed to release lock
def check_lock(branch_path: Path) -> dict | None:
"""
Check if a branch has an active dispatch lock.
Returns:
Lock data dict if active lock exists, None otherwise.
Automatically cleans up stale locks.
"""
lock_path = _get_lock_path(branch_path)
if not lock_path.exists():
return None
try:
with open(lock_path, 'r', encoding='utf-8') as f:
lock_data = json.load(f)
if _is_lock_stale(lock_data):
# Auto-cleanup stale lock
lock_path.unlink(missing_ok=True) # Auto-cleaned stale lock
return None
return lock_data
except (json.JSONDecodeError, OSError):
# Corrupted - clean up
lock_path.unlink(missing_ok=True)
return None
@@ -1,66 +0,0 @@
# =================== AIPass ====================
# Name: data_ops.py
# Description: Error Monitor Data Operations Handler
# Version: 1.0.0
# Created: 2025-11-15
# Modified: 2025-11-15
# =============================================
"""
Error Monitor Data Operations Handler
Independent handler for error tracking data persistence.
Provides functions for loading and saving error monitor data.
Architecture:
- No cross-domain imports (independent handler)
- Provides: data loading, data saving
- Used by: error_monitor module
"""
# =============================================
# IMPORTS
# =============================================
import json
from pathlib import Path
from typing import Dict
# =============================================
# BUSINESS LOGIC
# =============================================
def load_data(data_file: Path) -> Dict:
"""
Load error tracking data from file.
Args:
data_file: Path to the error tracking data file
Returns:
Dictionary containing error tracking data, or empty dict if file doesn't exist
"""
if not data_file.exists():
return {}
try:
# Direct file read for error tracking data (non-standard structure)
with open(data_file, 'r', encoding='utf-8') as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except Exception:
return {}
def save_data(data: Dict, data_file: Path) -> None:
"""
Save error tracking data to file.
Args:
data: Dictionary containing error tracking data
data_file: Path to the error tracking data file
"""
# Direct file write for error tracking data (non-standard structure)
data_file.parent.mkdir(parents=True, exist_ok=True)
with open(data_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
@@ -1,240 +0,0 @@
# =================== AIPass ====================
# Name: validate.py
# Description: Registry Validation Handler
# Version: 1.0.0
# Created: 2025-11-15
# Modified: 2025-11-15
# =============================================
"""
Registry Validation Handler
Handles validation of branch registry data including:
- Checking for email address collisions
- Validating email derivation rules
- Detecting unreachable branches
Handler Independence:
- No module imports from ai_mail
- Only uses Prax logger and standard library
- Fully transportable and self-contained
"""
from pathlib import Path
from typing import List, Dict, Tuple
# Constants
MODULE_NAME = "registry.validate"
def check_email_collisions(branches: List[Dict]) -> Tuple[bool, List[Dict]]:
"""
Check for email address collisions in branch list.
Args:
branches: List of branch dicts with keys: name, path, email
Returns:
Tuple of (has_collisions: bool, collisions: List[Dict])
Collision dict format:
{
"email": "@email",
"branch1": "First Branch Name",
"branch2": "Second Branch Name"
}
"""
email_map = {}
collisions = []
for branch in branches:
email = branch["email"]
if email in email_map:
# Collision detected
collisions.append({
"email": email,
"branch1": email_map[email],
"branch2": branch["name"]
})
else:
email_map[email] = branch["name"]
has_collisions = len(collisions) > 0
return has_collisions, collisions
def get_collision_report(collisions: List[Dict]) -> str:
"""
Generate human-readable collision report.
Args:
collisions: List of collision dicts from check_email_collisions()
Returns:
Formatted report string
"""
if not collisions:
return "No collisions detected."
report_lines = [
f"EMAIL ADDRESS COLLISIONS DETECTED: {len(collisions)}",
"",
"One or more branches are unreachable via AI_Mail!",
"",
"Collisions:"
]
for collision in collisions:
report_lines.extend([
"",
f" {collision['email']}",
f" - {collision['branch1']}",
f" - {collision['branch2']}",
])
report_lines.extend([
"",
"Fix: Rename branches in BRANCH_REGISTRY.json to ensure unique email derivation",
"See email derivation rules in registry/read.py"
])
return "\n".join(report_lines)
def validate_branch_data(branch: Dict) -> Tuple[bool, str]:
"""
Validate a single branch entry.
Args:
branch: Branch dict with keys: name, path, email
Returns:
Tuple of (is_valid: bool, error_message: str)
error_message is empty string if valid
"""
# Check required fields
required_fields = ["name", "path", "email"]
for field in required_fields:
if field not in branch:
return False, f"Missing required field: {field}"
if not branch[field]:
return False, f"Empty value for field: {field}"
# Validate email format
if not branch["email"].startswith("@"):
return False, f"Email must start with @: {branch['email']}"
# Validate path format
path = branch["path"]
if not path.startswith("/"):
return False, f"Path must be absolute: {path}"
return True, ""
def validate_all_branches(branches: List[Dict]) -> Tuple[bool, List[str]]:
"""
Validate all branch entries.
Args:
branches: List of branch dicts
Returns:
Tuple of (all_valid: bool, error_messages: List[str])
"""
errors = []
for i, branch in enumerate(branches):
is_valid, error_msg = validate_branch_data(branch)
if not is_valid:
errors.append(f"Branch {i} ({branch.get('name', 'UNKNOWN')}): {error_msg}")
all_valid = len(errors) == 0
return all_valid, errors
def get_duplicate_names(branches: List[Dict]) -> List[str]:
"""
Find duplicate branch names.
Args:
branches: List of branch dicts
Returns:
List of duplicate branch names
"""
name_counts = {}
duplicates = []
for branch in branches:
name = branch.get("name", "")
if name:
name_counts[name] = name_counts.get(name, 0) + 1
for name, count in name_counts.items():
if count > 1:
duplicates.append(name)
return duplicates
def get_duplicate_paths(branches: List[Dict]) -> List[str]:
"""
Find duplicate branch paths.
Args:
branches: List of branch dicts
Returns:
List of duplicate branch paths
"""
path_counts = {}
duplicates = []
for branch in branches:
path = branch.get("path", "")
if path:
path_counts[path] = path_counts.get(path, 0) + 1
for path, count in path_counts.items():
if count > 1:
duplicates.append(path)
return duplicates
if __name__ == "__main__":
from aipass.cli.apps.modules import console
console.print("\n" + "="*70)
console.print("AI_MAIL HANDLER: registry/validate.py")
console.print("="*70)
console.print("\nRegistry Validation Handler")
console.print()
console.print("FUNCTIONS PROVIDED:")
console.print(" - check_email_collisions(branches) -> Tuple[bool, List[Dict]]")
console.print(" - get_collision_report(collisions) -> str")
console.print(" - validate_branch_data(branch) -> Tuple[bool, str]")
console.print(" - validate_all_branches(branches) -> Tuple[bool, List[str]]")
console.print(" - get_duplicate_names(branches) -> List[str]")
console.print(" - get_duplicate_paths(branches) -> List[str]")
console.print()
console.print("TESTING:")
# Sample test data
test_branches = [
{"name": "AIPASS.admin", "path": "/", "email": "@admin"},
{"name": "FLOW", "path": "src/aipass/flow", "email": "@flow"},
{"name": "DRONE", "path": "src/aipass/drone", "email": "@drone"},
]
has_collisions, collisions = check_email_collisions(test_branches)
console.print(f"\nCollisions detected: {has_collisions}")
console.print(f"Number of collisions: {len(collisions)}")
all_valid, errors = validate_all_branches(test_branches)
console.print(f"\nAll branches valid: {all_valid}")
console.print(f"Validation errors: {len(errors)}")
console.print("\n" + "="*70 + "\n")
@@ -21,6 +21,6 @@ Contains handlers that respond to Trigger events.
These handlers are registered by Trigger's event registry.
"""
from .error_handler import handle_error_detected
__all__ = ['handle_error_detected']
# error_handler.py moved to apps/.archive/trigger_error_handler(disabled).py
# Trigger branch owns event handling — ai_mail provides delivery only.
__all__ = []
@@ -1,172 +0,0 @@
# =================== AIPass ====================
# Name: error_handler.py
# Description: Error Detected Event Consumer
# Version: 1.0.0
# Created: 2026-02-02
# Modified: 2026-02-02
# =============================================
"""
Error Detected Event Consumer
Handles error_detected events fired by Trigger's log_watcher.
Delivers notifications to affected branches via AI_MAIL.
Event data from log_watcher.py:
- branch: Target branch name (e.g., 'FLOW')
- module: Module that logged the error
- message: Error message text
- log_path: Path to log file
- error_hash: 8-char hash for deduplication
- timestamp: When error occurred
Architecture:
1. Trigger's log_watcher detects ERROR in branch logs
2. log_watcher fires error_detected event
3. This handler receives event, calls deliver_email_to_branch()
4. Email delivered to affected branch inbox (auto_execute=True)
5. Branch agent spawns and investigates
"""
from datetime import datetime
from pathlib import Path
from typing import Any
from aipass.prax.apps.modules.logger import system_logger as logger
def _build_notification_message(
error_hash: str,
module: str,
message: str,
timestamp: str,
log_path: str
) -> str:
"""
Build error notification message with investigation instructions.
Args:
error_hash: Unique error identifier (8-char)
module: Module that logged the error
message: Error message text
timestamp: When error occurred
log_path: Path to source log file
Returns:
Formatted message string with investigation instructions
"""
return f"""Error detected - investigate and respond.
Error ID: {error_hash}
Module: {module}
Timestamp: {timestamp}
Log file: {log_path}
Error message:
{message}
---
Investigation steps:
1. Check the log file for context around this error
2. Identify root cause
Decision tree:
- Simple fix (typo, missing import, config issue):
-> Fix it yourself, then report what you did to @devpulse
- Complex/unclear (needs research, affects multiple files):
-> Report findings only to @devpulse, recommend action, don't fix
- Critical (data loss risk, security, system stability):
-> Stop immediately, escalate to @devpulse with full context
Report to @devpulse:
ai_mail send @devpulse "ERROR {error_hash} - [STATUS]" "Findings..."
Include: Error ID, severity (low/medium/high/critical), what you found, action taken or recommended.
"""
def handle_error_detected(
branch: str | None = None,
module: str | None = None,
message: str | None = None,
log_path: str | None = None,
error_hash: str | None = None,
timestamp: str | None = None,
**kwargs: Any
) -> None:
"""
Handle error_detected event - deliver notification to affected branch.
Called by Trigger when log_watcher detects an ERROR in branch logs.
Sends email to affected branch with auto_execute=True so an
investigation agent spawns automatically.
Args:
branch: Target branch name (e.g., 'FLOW') - REQUIRED
module: Module that logged the error - REQUIRED
message: Error message text - REQUIRED
log_path: Path to source log file
error_hash: 8-char unique error identifier - REQUIRED
timestamp: When error occurred (defaults to now)
**kwargs: Additional event data (ignored)
Returns:
None - handlers must not return values
Note:
Handler follows silent failure pattern - all exceptions caught.
NO logger imports (causes infinite recursion with trigger events).
NO console.print() (handlers must be silent).
"""
try:
# Validate required fields
if not branch or not module or not message or not error_hash:
return
# Import delivery handler here to avoid import-time failures
try:
from aipass.ai_mail.apps.handlers.email.delivery import deliver_email_to_branch
except ImportError:
return
# Default timestamp to now if not provided
if not timestamp:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Default log_path if not provided
if not log_path:
log_path = "unknown"
# Convert branch name to email format (FLOW -> @flow)
branch_email = f"@{branch.lower()}"
# Build subject line
subject = f"[ERROR] {module} - detected in logs"
# Build notification message
notification_message = _build_notification_message(
error_hash=error_hash,
module=module,
message=message,
timestamp=timestamp,
log_path=log_path
)
# Build email data for delivery
email_data = {
'from': '@error_monitor',
'from_name': 'Error Monitor',
'to': branch_email,
'subject': subject,
'message': notification_message,
'timestamp': timestamp,
'auto_execute': True,
'priority': 'normal',
'reply_to': '@devpulse'
}
# Deliver via inbox.json
deliver_email_to_branch(branch_email, email_data)
except Exception as e:
logger.warning("[error_handler] Failed to deliver error notification to %s: %s", branch_email, e)
@@ -1,15 +1,11 @@
"""
User Handlers - User Configuration and Management
Handles loading of user configuration files and user information for AI_Mail system.
Handles user information retrieval for AI_Mail system.
"""
from .load import (
load_user_config,
load_config,
create_default_config,
load_or_create_config
)
# load.py moved to apps/.archive/users_load(disabled).py
# config_generator.py moved to apps/.archive/config_generator(disabled).py
from .user import (
get_current_user,
@@ -18,13 +14,6 @@ from .user import (
)
__all__ = [
# Config loading
'load_user_config',
'load_config',
'create_default_config',
'load_or_create_config',
# User info
'get_current_user',
'get_user_by_email',
'get_all_users',
@@ -1,260 +0,0 @@
# =================== AIPass ====================
# Name: config_generator.py
# Description: Local Config Auto-Generation Handler
# Version: 1.0.0
# Created: 2025-11-18
# Modified: 2025-11-18
# =============================================
"""
Local Config Auto-Generation Handler
Auto-generates user_config.json files for branches that use AI_MAIL.
Creates ai_mail_config/ directory and populates with branch-specific config.
"""
# =============================================
# IMPORTS
# =============================================
import json
from pathlib import Path
from typing import Dict
# =============================================
# CONFIG GENERATION FUNCTIONS
# =============================================
def generate_local_config(branch_info: Dict) -> Dict:
"""
Generate local user_config.json content for a branch.
Args:
branch_info: Branch info dict from registry with keys:
- name: Branch name (e.g., "SEED")
- path: Branch path
- email: Branch email address (e.g., "@seed")
- description: Branch description
Returns:
Dict with user_config.json structure
"""
branch_name = branch_info.get("name", "").lower()
branch_email = branch_info.get("email", f"@{branch_name}")
branch_path = Path(branch_info.get("path", ""))
# Generate display name from branch info
display_name = generate_display_name(branch_info)
# Generate mailbox path (.ai_mail.local/ in branch directory)
mailbox_path = str(branch_path / ".ai_mail.local")
config = {
"version": "1.0.0",
"current_user": branch_name,
"users": {
branch_name: {
"name": branch_info.get("name", "").title(),
"email_address": branch_email,
"display_name": display_name,
"mailbox_path": mailbox_path
}
},
"settings": {
"timestamp_format": "%Y-%m-%d %H:%M:%S",
"max_inbox_display": 20,
"max_sent_display": 20
}
}
return config
def generate_display_name(branch_info: Dict) -> str:
"""
Generate user-friendly display name for branch.
Args:
branch_info: Branch info dict from registry
Returns:
Display name string (e.g., "Seed (Standards Branch)")
"""
name = branch_info.get("name", "Unknown").title()
description = branch_info.get("description", "")
# If description is meaningful (not default), use it
if description and description != "New branch - purpose TBD":
return f"{name} ({description})"
else:
# Use profile as context if available
profile = branch_info.get("profile", "")
if profile and profile != "AIPass Workshop":
return f"{name} ({profile})"
else:
# Just branch name
return name
def create_local_config_file(branch_info: Dict, force: bool = False) -> Path | None:
"""
Create local user_config.json file for a branch.
Saves to branch's [branch_name]_json/ directory (e.g., seed_json/user_config.json).
Follows the pattern: all JSON files for a branch go in their [branch]_json/ folder.
Args:
branch_info: Branch info dict from registry
force: If True, overwrite existing config file
Returns:
Path to created config file, or None if failed
"""
try:
branch_path = Path(branch_info.get("path", ""))
if not branch_path.exists():
return None
# Get branch name for directory pattern
branch_name = branch_info.get("name", "").lower()
# Config directory: [branch_name]_json/
config_dir = branch_path / f"{branch_name}_json"
# Create directory if it doesn't exist
if not config_dir.exists():
config_dir.mkdir(parents=True, exist_ok=True)
# Config file path
config_file = config_dir / "user_config.json"
# Check if already exists
if config_file.exists() and not force:
return config_file
# Generate config content
config = generate_local_config(branch_info)
# Write config file
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2)
return config_file
except Exception:
return None
def create_mailbox_directory(branch_path: Path) -> Path | None:
"""
Create .ai_mail.local/ mailbox directory for a branch.
Creates subdirectories: inbox/, sent/, deleted/
Args:
branch_path: Path to branch directory
Returns:
Path to mailbox directory, or None if failed
"""
try:
mailbox_dir = branch_path / ".ai_mail.local"
mailbox_dir.mkdir(parents=True, exist_ok=True)
# Create subdirectories
(mailbox_dir / "sent").mkdir(exist_ok=True)
# Create empty inbox.json if doesn't exist
inbox_file = mailbox_dir / "inbox.json"
if not inbox_file.exists():
inbox_data = {
"mailbox": "inbox",
"total_messages": 0,
"unread_count": 0,
"messages": []
}
with open(inbox_file, 'w', encoding='utf-8') as f:
json.dump(inbox_data, f, indent=2)
# Create empty sent.json if doesn't exist
sent_file = mailbox_dir / "sent.json"
if not sent_file.exists():
sent_data = {
"mailbox": "sent",
"total_messages": 0,
"messages": []
}
with open(sent_file, 'w', encoding='utf-8') as f:
json.dump(sent_data, f, indent=2)
return mailbox_dir
except Exception:
return None
def setup_branch_for_aimail(branch_info: Dict, force: bool = False) -> bool:
"""
Complete AI_MAIL setup for a branch.
Creates:
- ai_mail_config/user_config.json
- .ai_mail.local/ mailbox directory
- .ai_mail.local/inbox.json
- .ai_mail.local/sent.json
Args:
branch_info: Branch info dict from registry
force: If True, overwrite existing files
Returns:
True if setup successful, False otherwise
"""
try:
# Create config file
config_file = create_local_config_file(branch_info, force=force)
if not config_file:
return False
# Create mailbox directory
branch_path = Path(branch_info.get("path", ""))
mailbox_dir = create_mailbox_directory(branch_path)
if not mailbox_dir:
return False
return True
except Exception:
return False
if __name__ == "__main__":
from aipass.cli.apps.modules import console
console.print("\n" + "="*70)
console.print("LOCAL CONFIG AUTO-GENERATION HANDLER")
console.print("="*70)
console.print("\nPURPOSE:")
console.print(" Auto-generates user_config.json files for branches using AI_MAIL")
console.print(" Creates ai_mail_config/ directory and mailbox structure")
console.print()
console.print("FUNCTIONS PROVIDED:")
console.print(" - generate_local_config(branch_info) -> Dict")
console.print(" - generate_display_name(branch_info) -> str")
console.print(" - create_local_config_file(branch_info, force) -> Path | None")
console.print(" - create_mailbox_directory(branch_path) -> Path | None")
console.print(" - setup_branch_for_aimail(branch_info, force) -> bool")
console.print()
console.print("HANDLER CHARACTERISTICS:")
console.print(" ✓ Independent - no module dependencies")
console.print(" ✓ Can import Prax (service provider)")
console.print(" ✓ Pure business logic")
console.print(" ✗ CANNOT import parent modules")
console.print()
console.print("SETUP WORKFLOW:")
console.print(" 1. Generate config from branch registry info")
console.print(" 2. Create ai_mail_config/ directory")
console.print(" 3. Write user_config.json with branch-specific settings")
console.print(" 4. Create .ai_mail.local/ mailbox directory")
console.print(" 5. Initialize inbox.json and sent.json")
console.print()
console.print("="*70 + "\n")
@@ -1,144 +0,0 @@
# =================== AIPass ====================
# Name: load.py
# Description: User Config Loading Handler
# Version: 1.2.0
# Created: 2025-11-15
# Modified: 2025-11-15
# =============================================
"""
User Config Loading Handler
Handles loading of user configuration files for AI_Mail system.
Provides unified config loading for user_config.json and module configs.
NEW in v1.2.0: Per-branch config support
- Detects calling branch from PWD/CWD
- Checks for local config at branch_path/ai_mail_config/user_config.json
- Falls back to AI_MAIL's global config if no local config found
"""
# =============================================
# IMPORTS
# =============================================
import json
from pathlib import Path
from typing import Dict
# =============================================
# CONSTANTS
# =============================================
_AI_MAIL_ROOT = Path(__file__).resolve().parents[3] # ai_mail/
AI_MAIL_JSON = _AI_MAIL_ROOT / ".ai_mail.local"
USER_CONFIG_FILE = AI_MAIL_JSON / "user_config.json"
# Import branch detection (after constants defined)
from .branch_detection import detect_branch_from_pwd, get_local_config_path
# =============================================
# CONFIG LOADING FUNCTIONS
# =============================================
def load_user_config() -> Dict:
"""
Load user configuration from user_config.json
NEW in v1.2.0: Per-branch config support
- First checks if calling from a branch (detects via PWD)
- Looks for local config at branch_path/ai_mail_config/user_config.json
- Falls back to AI_MAIL's global config if no local config
- Auto-generates local config if branch detected but config missing
Returns:
Dict containing user configuration
Raises:
FileNotFoundError: If no config found (neither local nor global)
"""
# Try to detect calling branch from PWD
branch_info = detect_branch_from_pwd()
if branch_info:
# Branch detected - check for local config
branch_path = Path(branch_info["path"])
branch_name = branch_info["name"]
local_config_path = get_local_config_path(branch_path, branch_name)
if local_config_path.exists():
# Local config exists - use it
with open(local_config_path, 'r', encoding='utf-8') as f:
return json.load(f)
else:
# No local config - offer to auto-generate
# For now, fall through to global config
pass
# No branch detected or no local config - use AI_MAIL's global config
if not USER_CONFIG_FILE.exists():
raise FileNotFoundError(f"User config not found: {USER_CONFIG_FILE}")
with open(USER_CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
def load_config(config_file: Path) -> Dict:
"""
Load generic configuration file with auto-healing
Creates default config if missing (for module configs).
Args:
config_file: Path to configuration file
Returns:
Dict containing configuration data
Raises:
Exception: If config cannot be loaded or created
"""
if not config_file.exists():
raise FileNotFoundError(f"Config file not found: {config_file}")
try:
with open(config_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
raise
def create_default_config(config_file: Path, default_config: Dict) -> None:
"""
Create default configuration file
Args:
config_file: Path where config should be created
default_config: Default configuration dictionary
Raises:
Exception: If config cannot be created
"""
# Ensure parent directory exists
config_file.parent.mkdir(parents=True, exist_ok=True)
try:
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(default_config, f, indent=2)
except Exception as e:
raise
def load_or_create_config(config_file: Path, default_config: Dict) -> Dict:
"""
Load config file, creating with defaults if missing
Args:
config_file: Path to configuration file
default_config: Default configuration to use if file doesn't exist
Returns:
Dict containing configuration data
"""
if not config_file.exists():
create_default_config(config_file, default_config)
return load_config(config_file)
@@ -112,12 +112,16 @@ def get_user_by_email(email: str) -> Dict | None:
try:
import json
_repo_root = registry_path.parent
with open(registry_path, 'r', encoding='utf-8') as f:
registry = json.load(f)
for branch in _get_branches_list(registry):
if branch.get("email") == email:
branch_path = Path(branch.get("path", ""))
if branch_path and not branch_path.is_absolute():
branch_path = (_repo_root / branch_path).resolve()
return {
"email_address": branch.get("email"),
"display_name": branch.get("name"),
@@ -147,11 +151,14 @@ def get_all_users() -> Dict[str, Dict]:
with open(registry_path, 'r', encoding='utf-8') as f:
registry = json.load(f)
_repo_root = registry_path.parent
users = {}
for branch in _get_branches_list(registry):
email = branch.get("email", "")
if email:
branch_path = Path(branch.get("path", ""))
if branch_path and not branch_path.is_absolute():
branch_path = (_repo_root / branch_path).resolve()
users[email] = {
"email_address": email,
"display_name": branch.get("name"),
+274
View File
@@ -0,0 +1,274 @@
# =================== AIPass ====================
# Name: test_user_paths.py
# Description: Tests for absolute mailbox_path resolution in user functions
# Version: 1.0.0
# Created: 2026-03-17
# Modified: 2026-03-17
# =============================================
"""
Tests for mailbox_path Absolute Resolution
Bug: get_user_by_email() and get_all_users() returned relative paths like
"src/aipass/ai_mail/.ai_mail.local" instead of absolute paths.
get_current_user() was already correct (resolved against _repo_root).
Fix: Both functions now resolve relative registry paths against _repo_root
(the parent of BRANCH_REGISTRY_PATH), matching get_current_user()'s pattern.
These tests verify:
1. get_user_by_email() returns an absolute mailbox_path
2. get_all_users() returns absolute mailbox_path for every entry
3. Paths are never doubled (no src/aipass/.../src/aipass/...)
4. Absolute paths in the registry are preserved as-is
"""
import json
import pytest
from pathlib import Path
from unittest.mock import patch
from aipass.ai_mail.apps.handlers.users.user import get_user_by_email, get_all_users
# ─── Fixtures ────────────────────────────────────────────
@pytest.fixture
def relative_path_registry(tmp_path):
"""Create a registry with relative paths (production format).
Production AIPASS_REGISTRY.json uses list format with relative paths:
"path": "src/aipass/ai_mail"
The registry sits at tmp_path/AIPASS_REGISTRY.json, so _repo_root
is tmp_path. Resolved paths should be tmp_path / "src/aipass/..." .
Returns (registry_path, expected_repo_root).
"""
registry = {"branches": [
{
"name": "AI_MAIL",
"path": "src/aipass/ai_mail",
"email": "@ai_mail",
"status": "active",
"description": "Agent-to-agent messaging system",
},
{
"name": "SPAWN",
"path": "src/aipass/spawn",
"email": "@spawn",
"status": "active",
"description": "Branch spawner",
},
{
"name": "TRIGGER",
"path": "src/aipass/trigger",
"email": "@trigger",
"status": "active",
"description": "Event trigger system",
},
]}
registry_path = tmp_path / "AIPASS_REGISTRY.json"
registry_path.write_text(json.dumps(registry, indent=2), encoding="utf-8")
return registry_path, tmp_path
@pytest.fixture
def absolute_path_registry(tmp_path):
"""Create a registry where paths are already absolute.
Ensures absolute paths pass through without double-resolution.
Returns (registry_path, branch_dir).
"""
branch_dir = tmp_path / "src" / "aipass" / "solo_branch"
registry = {"branches": [
{
"name": "SOLO",
"path": str(branch_dir),
"email": "@solo",
"status": "active",
"description": "Branch with absolute path",
},
]}
registry_path = tmp_path / "AIPASS_REGISTRY.json"
registry_path.write_text(json.dumps(registry, indent=2), encoding="utf-8")
return registry_path, branch_dir
@pytest.fixture
def dict_format_registry(tmp_path):
"""Create a registry using dict format (legacy).
Tests that the dict->list normalization via _get_branches_list still
produces absolute paths.
"""
registry = {"branches": {
"devpulse": {
"name": "DEVPULSE",
"path": "src/aipass/devpulse",
"email": "@devpulse",
"status": "active",
"description": "DevPulse branch",
},
"backup": {
"name": "BACKUP",
"path": "src/aipass/backup",
"email": "@backup",
"status": "active",
"description": "Backup branch",
},
}}
registry_path = tmp_path / "AIPASS_REGISTRY.json"
registry_path.write_text(json.dumps(registry, indent=2), encoding="utf-8")
return registry_path, tmp_path
# ─── get_user_by_email() tests ───────────────────────────
class TestGetUserByEmailPaths:
"""Verify get_user_by_email() returns absolute mailbox_path values."""
def test_returns_absolute_mailbox_path(self, relative_path_registry):
"""Relative registry paths must be resolved to absolute mailbox_path."""
registry_path, _ = relative_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
result = get_user_by_email("@ai_mail")
assert result is not None
mailbox = Path(result["mailbox_path"])
assert mailbox.is_absolute(), (
f"mailbox_path must be absolute, got: {result['mailbox_path']}"
)
def test_path_rooted_at_repo_root(self, relative_path_registry):
"""Resolved path should start from the repo root (registry parent)."""
registry_path, repo_root = relative_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
result = get_user_by_email("@spawn")
assert result is not None
expected = str((repo_root / "src" / "aipass" / "spawn" / ".ai_mail.local").resolve())
assert result["mailbox_path"] == expected
def test_no_doubled_relative_path(self, relative_path_registry):
"""Path must not contain the relative prefix twice (the old bug)."""
registry_path, _ = relative_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
result = get_user_by_email("@trigger")
assert result is not None
path = result["mailbox_path"]
# Count occurrences of the relative segment
assert path.count("src/aipass/trigger") == 1, (
f"Path contains doubled segment: {path}"
)
def test_absolute_path_preserved(self, absolute_path_registry):
"""Registry entries with absolute paths should not be re-rooted."""
registry_path, branch_dir = absolute_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
result = get_user_by_email("@solo")
assert result is not None
expected = str((branch_dir / ".ai_mail.local").resolve())
assert result["mailbox_path"] == expected
def test_returns_none_for_unknown_email(self, relative_path_registry):
"""Unknown email should return None, not crash."""
registry_path, _ = relative_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
result = get_user_by_email("@nonexistent_branch_xyz")
assert result is None
def test_dict_format_returns_absolute_path(self, dict_format_registry):
"""Dict-format registry should also produce absolute paths."""
registry_path, repo_root = dict_format_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
result = get_user_by_email("@devpulse")
assert result is not None
mailbox = Path(result["mailbox_path"])
assert mailbox.is_absolute(), (
f"mailbox_path must be absolute (dict format), got: {result['mailbox_path']}"
)
expected = str((repo_root / "src" / "aipass" / "devpulse" / ".ai_mail.local").resolve())
assert result["mailbox_path"] == expected
# ─── get_all_users() tests ───────────────────────────────
class TestGetAllUsersPaths:
"""Verify get_all_users() returns absolute mailbox_path for every entry."""
def test_all_paths_are_absolute(self, relative_path_registry):
"""Every user returned must have an absolute mailbox_path."""
registry_path, _ = relative_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
users = get_all_users()
assert len(users) == 3, f"Expected 3 users, got {len(users)}"
for email, info in users.items():
mailbox = Path(info["mailbox_path"])
assert mailbox.is_absolute(), (
f"mailbox_path for {email} must be absolute, got: {info['mailbox_path']}"
)
def test_all_paths_end_with_ai_mail_local(self, relative_path_registry):
"""Every mailbox_path should end with .ai_mail.local."""
registry_path, _ = relative_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
users = get_all_users()
for email, info in users.items():
assert info["mailbox_path"].endswith(".ai_mail.local"), (
f"mailbox_path for {email} should end with .ai_mail.local, "
f"got: {info['mailbox_path']}"
)
def test_no_doubled_paths_in_any_entry(self, relative_path_registry):
"""No entry should have a doubled relative segment."""
registry_path, _ = relative_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
users = get_all_users()
for email, info in users.items():
path = info["mailbox_path"]
# The relative prefix "src/aipass" should appear exactly once
assert path.count("src/aipass") == 1, (
f"Path for {email} contains doubled 'src/aipass': {path}"
)
def test_paths_resolve_against_repo_root(self, relative_path_registry):
"""Resolved paths should be rooted at the registry's parent dir."""
registry_path, repo_root = relative_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
users = get_all_users()
for email, info in users.items():
assert info["mailbox_path"].startswith(str(repo_root)), (
f"Path for {email} should start with repo root {repo_root}, "
f"got: {info['mailbox_path']}"
)
def test_absolute_paths_preserved(self, absolute_path_registry):
"""Entries with absolute paths should pass through unchanged."""
registry_path, branch_dir = absolute_path_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
users = get_all_users()
assert "@solo" in users
expected = str((branch_dir / ".ai_mail.local").resolve())
assert users["@solo"]["mailbox_path"] == expected
def test_dict_format_all_absolute(self, dict_format_registry):
"""Dict-format registry should produce absolute paths for all entries."""
registry_path, _ = dict_format_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
users = get_all_users()
assert len(users) == 2
for email, info in users.items():
mailbox = Path(info["mailbox_path"])
assert mailbox.is_absolute(), (
f"mailbox_path for {email} must be absolute (dict format), "
f"got: {info['mailbox_path']}"
)
def test_empty_registry_returns_empty_dict(self, tmp_path):
"""Missing registry file should return empty dict, not crash."""
fake_path = tmp_path / "nonexistent_registry.json"
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", fake_path):
users = get_all_users()
assert users == {}
+19 -6
View File
@@ -2,7 +2,7 @@
**Purpose:** Display and output formatting service for AIPass modules. Provides consistent terminal output — headers, success/error/warning messages, section breaks, and operation templates — so every module looks the same without duplicating Rich formatting code.
**Module:** `aipass.cli`
**Last Updated:** 2026-03-08
**Last Updated:** 2026-03-17
## Usage
@@ -42,9 +42,16 @@ cli/
│ ├── cli.py # Entry point
│ ├── modules/
│ │ ├── display.py # header, success, error, warning, section
│ │ └── templates.py # operation_start, operation_complete
│ │ ├── templates.py # operation_start, operation_complete
│ │ └── init_project.py # aipass init command routing
│ └── handlers/
│ ├── init/ # Project bootstrap logic
│ │ └── bootstrap.py
│ └── json/ # JSON file management
│ └── json_handler.py
├── cli_json/ # Auto-created JSON output (three-file pattern)
├── dropbox/ # Inbound file drop
├── logs/ # Branch-level logs
└── tests/
```
@@ -54,9 +61,14 @@ cli/
## Commands / Usage
```bash
drone @cli --help # Show services and Rich formatting showcase
drone @cli --version # Show version
drone @cli help # Same as --help
drone @cli --help # Show services and Rich formatting showcase
drone @cli --version # Show version
drone @cli help # Same as --help
drone @cli aipass # Show aipass subcommands
drone @cli aipass init # Bootstrap AIPass project in current dir
drone @cli aipass init /path # Bootstrap in target directory
drone @cli aipass init /path MyProject # Bootstrap with custom name
drone @cli aipass init --help # Detailed init usage
```
---
@@ -72,7 +84,8 @@ drone @cli help # Same as --help
- All modules — display formatting (headers, success/error/warning, section breaks)
- All modules — operation templates (`operation_start`, `operation_complete`)
- All modules — Rich console access
- All users — `aipass init` project bootstrap command
---
*Last Updated: 2026-03-08*
*Last Updated: 2026-03-17*
@@ -13,22 +13,12 @@ from pathlib import Path
from datetime import datetime
from typing import Dict, Any, Optional
import inspect
from aipass.prax.apps.modules.logger import system_logger as logger
def _find_repo_root() -> Path:
"""Walk up from this file to find the repo root (contains pyproject.toml or AIPASS_REGISTRY.json)."""
current = Path(__file__).resolve().parent
for parent in [current] + list(current.parents):
if (parent / "pyproject.toml").exists() or (parent / "AIPASS_REGISTRY.json").exists():
return parent
return Path.cwd()
# Constants — resolved via repo root walk-up (portable across any machine)
_REPO_ROOT = _find_repo_root()
CLI_ROOT = _REPO_ROOT / "src" / "aipass" / "cli"
CLI_JSON_DIR = CLI_ROOT / "cli_json"
JSON_TEMPLATES_DIR = CLI_ROOT / "apps" / "json_templates"
# Constants — resolved via __file__ (portable across any machine)
_BRANCH_ROOT = Path(__file__).resolve().parents[3] # json/ -> handlers/ -> apps/ -> cli/
_BRANCH_NAME = _BRANCH_ROOT.name
JSON_DIR = _BRANCH_ROOT / f"{_BRANCH_NAME}_json"
JSON_TEMPLATES_DIR = _BRANCH_ROOT / "apps" / "json_templates"
def _get_caller_module_name() -> str:
@@ -94,12 +84,12 @@ def validate_json_structure(data: Any, json_type: str) -> bool:
def get_json_path(module_name: str, json_type: str) -> Path:
"""Get path for module JSON file"""
filename = f"{module_name}_{json_type}.json"
return CLI_JSON_DIR / filename
return JSON_DIR / filename
def ensure_json_exists(module_name: str, json_type: str) -> bool:
"""Ensure JSON file exists, create from template if missing"""
CLI_JSON_DIR.mkdir(parents=True, exist_ok=True)
JSON_DIR.mkdir(parents=True, exist_ok=True)
json_path = get_json_path(module_name, json_type)
@@ -111,8 +101,9 @@ def ensure_json_exists(module_name: str, json_type: str) -> bool:
if validate_json_structure(data, json_type):
return True
# If corrupted, fall through to regenerate
except Exception as exc:
logger.warning("JSON unreadable at %s, regenerating: %s", json_path, exc)
except Exception:
# If unreadable, fall through to regenerate
pass
template = load_template(json_type, module_name)
@@ -257,7 +248,7 @@ if __name__ == "__main__":
update_data_metrics("cli", test_metric="working")
console.print()
console.print(f"[green]Check {CLI_JSON_DIR}/ for created files:[/green]")
console.print(f"[green]Check {JSON_DIR}/ for created files:[/green]")
console.print(" [dim]•[/dim] cli_config.json")
console.print(" [dim]•[/dim] cli_data.json")
console.print(" [dim]•[/dim] cli_log.json")
+12 -3
View File
@@ -29,6 +29,8 @@ from rich.panel import Panel
from rich.table import Table
from rich.columns import Columns
from aipass.cli.apps.handlers.json import json_handler
# NOTE: Cannot import prax here — circular import (prax depends on cli)
# from aipass.prax import logger
@@ -226,11 +228,15 @@ def handle_command(command: str, args: List[str]) -> bool:
if command == "demo":
run_demo()
return True
elif command in ["display", "show"]:
if command not in ("display", "show"):
return False
if not args:
print_introspection()
return True
else:
return False
if args[0] == "demo":
run_demo()
return True
return False
def run_demo():
@@ -262,6 +268,9 @@ def run_demo():
CONSOLE.print("[bold green]✨ Rich library integration complete![/bold green]")
CONSOLE.print("[dim]All display functions now use Rich for beautiful terminal output[/dim]")
json_handler.log_operation("display_demo")
CONSOLE.print()
@@ -22,6 +22,7 @@ from typing import List
from aipass.cli.apps.handlers.init.bootstrap import init_project
from aipass.cli.apps.modules.display import console, success, error, header
from aipass.cli.apps.handlers.json import json_handler
# =============================================================================
@@ -217,6 +218,13 @@ def _handle_init(args: List[str]) -> bool:
console.print()
success(f"Created {len(result['created_files'])} files")
json_handler.log_operation("aipass_init", {
"project_name": result["project_name"],
"target": result["target"],
"files_created": len(result["created_files"]),
})
console.print()
return True
+12 -12
View File
@@ -18,10 +18,11 @@ Uses Rich library for beautiful terminal output.
import sys
from pathlib import Path
from typing import Dict, Any, Optional, List
from typing import List
# Import console from CLI display module (using our own service!)
from aipass.cli.apps.modules.display import console as CONSOLE
from aipass.cli.apps.handlers.json import json_handler
# NOTE: Cannot import prax here — circular import (prax depends on cli)
# from aipass.prax import logger
@@ -116,21 +117,17 @@ def print_help():
def handle_command(command: str, args: List[str]) -> bool:
"""Handle 'templates' and 'demo' commands"""
# Handle demo as direct command
if command == "demo":
run_demo()
return True
# Handle templates command
if command == "templates":
# Check for subcommand in args
if args and args[0] == "demo":
run_demo()
else:
# templates just shows introspection
print_introspection()
if command != "templates":
return False
if not args:
print_introspection()
return True
if args[0] == "demo":
run_demo()
return True
return False
@@ -161,6 +158,9 @@ def run_demo():
CONSOLE.print("[bold green]✨ Rich library integration complete![/bold green]")
CONSOLE.print("[dim]Templates provide consistent operation patterns across all branches[/dim]")
json_handler.log_operation("templates_demo")
CONSOLE.print()