feat(cli,drone): CLI front door — seedgo-compliant discovery + drone internal module

CLI entry point rewritten to follow seedgo auto-discovery pattern:
- discover_modules() scans for handle_command() instead of hardcoded list
- Introspection shows routable modules vs import-only services
- init_project.py now owns the 'aipass' command with subcommand routing
- Rich colors fixed (force_terminal=True) for drone subprocess pipes

CLI registered as drone internal module (drone_adapter.py):
- drone @cli accessible from any directory, not just inside AIPass repo
- drone @cli aipass init works from external projects
- Drone introspection now shows 3 internal modules: cli, drone, seedgo

Drone: CLI added to interactive allow list for terminal passthrough.

Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
AIOSAI
2026-03-15 14:05:35 -07:00
co-authored by @devpulse
parent bfeb6c0ec7
commit 091c1b710c
7 changed files with 542 additions and 356 deletions
+163 -268
View File
@@ -1,16 +1,18 @@
# =================== AIPass ====================
# Name: cli.py
# Description: Entry point CLI for drone @cli — display service and Rich formatting
# Version: 1.0.0
# Description: Entry point for drone @cli — seedgo-compliant module discovery and routing
# Version: 2.0.0
# Created: 2026-03-08
# Modified: 2026-03-08
# Modified: 2026-03-15
# =============================================
"""
CLI Branch - Universal Display/Output Service Provider
PURPOSE: Provides consistent CLI display across all AIPass branches.
Similar to Prax (logging service), CLI is a service provider for output.
Routes commands to auto-discovered modules via seedgo pattern.
- 'aipass init /path' -> init_project module (handle_command)
- Modules auto-discovered from modules/ directory
- Service modules (display, templates) also discoverable via handle_command()
SERVICES PROVIDED:
- Display: headers, success/error/warning messages, sections
@@ -23,9 +25,9 @@ ARCHITECTURE:
"""
import sys
import importlib
import importlib.util
from pathlib import Path
from typing import List
from typing import List, Any
# Prax logger
from aipass.prax.apps.modules.logger import system_logger as logger
@@ -34,96 +36,161 @@ from aipass.prax.apps.modules.logger import system_logger as logger
from rich.table import Table
from rich.columns import Columns
from rich.panel import Panel
from rich.text import Text
from rich.rule import Rule
from rich import box
# CLI modules (showcasing our own services!)
from aipass.cli.apps.modules.display import console as CONSOLE, header, success, error, warning, section
from aipass.cli.apps.modules.templates import operation_start, operation_complete
VERSION = "2.0.0"
CLI_ROOT = Path(__file__).parent
MODULES_DIR = CLI_ROOT / "modules"
# Service modules — import-only, listed separately from command modules
SERVICE_MODULES = {"display", "templates"}
# =============================================================================
# INTROSPECTION DISPLAY
# MODULE DISCOVERY
# =============================================================================
def get_handler_exports(handler_package_name: str) -> List[str]:
"""Get exported functions from handler package"""
try:
handler_module = importlib.import_module(f"aipass.cli.apps.handlers.{handler_package_name}")
return getattr(handler_module, '__all__', [])
except Exception:
return []
def discover_modules() -> List[Any]:
"""Auto-discover CLI modules in modules/ directory.
def print_introspection():
Scans modules/*.py for files with handle_command().
Excludes __init__.py and private files.
Follows seedgo discovery pattern exactly.
"""
Display discovered modules and handlers - Quick reference
modules = []
if not MODULES_DIR.exists():
return modules
for file_path in sorted(MODULES_DIR.glob("*.py")):
if file_path.name.startswith("_"):
continue
try:
spec = importlib.util.spec_from_file_location(file_path.stem, file_path)
if spec is None or spec.loader is None:
continue
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if hasattr(module, "handle_command"):
modules.append(module)
except Exception as e:
logger.error(f"[CLI] Failed to load module {file_path.stem}: {e}")
return modules
def route_command(command: str, args: List[str], modules: List[Any]) -> bool:
"""Route command to appropriate module."""
for module in modules:
try:
if module.handle_command(command, args):
return True
except Exception as e:
logger.error(f"[CLI] Module error: {e}")
return False
# =============================================================================
# DISPLAY
# =============================================================================
def print_introspection() -> None:
"""Display auto-discovered modules — seedgo-compliant introspection.
Called when cli.py runs with no arguments.
Shows what's connected to CLI entry point (modules and handler domains).
Shows command modules (with handle_command) and service modules separately.
"""
modules = discover_modules()
# Separate command modules from service modules
command_modules = [m for m in modules
if getattr(m, "__name__", "").split(".")[-1] not in SERVICE_MODULES]
service_modules = [m for m in modules
if getattr(m, "__name__", "").split(".")[-1] in SERVICE_MODULES]
CONSOLE.print()
CONSOLE.print("[bold cyan]CLI - Command Line Interface Branch[/bold cyan]")
CONSOLE.print(f" Version: {VERSION}")
CONSOLE.print()
CONSOLE.print("[dim]Universal Display & Output Service Provider[/dim]")
CONSOLE.print()
# Discover modules
CONSOLE.print("[yellow]Discovered Modules:[/yellow] 3")
# Discovered command modules
CONSOLE.print(f"[yellow]Discovered Modules:[/yellow] {len(command_modules)}")
for module in command_modules:
name = getattr(module, "__name__", "unknown").split(".")[-1]
desc = (module.__doc__ or "").strip().split("\n")[0] if module.__doc__ else "No description"
CONSOLE.print(f" [cyan]\u2022[/cyan] {name} \u2014 {desc}")
if not command_modules:
CONSOLE.print(" [dim]No command modules discovered[/dim]")
CONSOLE.print()
CONSOLE.print(" [cyan]•[/cyan] display")
CONSOLE.print(" [cyan]•[/cyan] templates")
CONSOLE.print(" [cyan]•[/cyan] console (Rich library wrapper)")
CONSOLE.print()
CONSOLE.print("[dim]Run 'python3 cli.py --help' for usage information[/dim]")
# Service modules (import-only, but with utility commands)
if service_modules:
CONSOLE.print(f"[yellow]Services:[/yellow] {len(service_modules)}")
for module in service_modules:
name = getattr(module, "__name__", "unknown").split(".")[-1]
desc = (module.__doc__ or "").strip().split("\n")[0] if module.__doc__ else "No description"
CONSOLE.print(f" [cyan]\u2022[/cyan] {name} \u2014 {desc}")
CONSOLE.print()
CONSOLE.print("[yellow]Next:[/yellow] Explore a module")
CONSOLE.print(" [green]drone @cli aipass[/green] [dim]# Project commands[/dim]")
CONSOLE.print(" [green]drone @cli aipass init --help[/green] [dim]# Bootstrap a project[/dim]")
CONSOLE.print(" [green]drone @cli --help[/green] [dim]# Full usage guide[/dim]")
CONSOLE.print()
def print_help():
def print_help() -> None:
"""Display Rich-formatted help - CLI services showcase!
Demonstrates:
- How to import and use CLI modules
- Rich formatting patterns (headers, tables, panels, columns)
- Drone compliance with Commands line
- Beautiful terminal presentation
Shows COMMANDS, EXAMPLES, full reference.
Follows seedgo help pattern.
"""
CONSOLE.print()
# =============================================================================
# SECTION 1: MAIN HEADER
# Demonstrates: header() function from cli.apps.modules.display
# =============================================================================
header("CLI - Display & Templates Service Provider")
CONSOLE.print("[dim]Universal display and output formatting for all AIPass branches[/dim]")
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 2: WHAT IS CLI?
# Demonstrates: Rich text styling with [bold], [cyan], [green], etc.
# =============================================================================
# What is CLI
CONSOLE.print("[bold cyan]WHAT IS CLI?[/bold cyan]")
CONSOLE.print()
CONSOLE.print("CLI is the [bold]Display & Templates Service[/bold] - like Prax for logging:")
CONSOLE.print(" [green]✓[/green] Centralized display formatting (headers, tables, panels)")
CONSOLE.print(" [green]✓[/green] Reusable templates for common operations")
CONSOLE.print(" [green]✓[/green] Rich library integration for beautiful output")
CONSOLE.print(" [green]✓[/green] Consistent styling across all AIPass branches")
CONSOLE.print(" [green]\u2713[/green] Centralized display formatting (headers, tables, panels)")
CONSOLE.print(" [green]\u2713[/green] Reusable templates for common operations")
CONSOLE.print(" [green]\u2713[/green] Rich library integration for beautiful output")
CONSOLE.print(" [green]\u2713[/green] Consistent styling across all AIPass branches")
CONSOLE.print()
CONSOLE.print("Update CLI once → All branches instantly benefit from improvements")
CONSOLE.print("Update CLI once \u2192 All branches instantly benefit from improvements")
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 3: PUBLIC SERVICES
# Demonstrates: Table() from rich.table with columns, styling
# =============================================================================
# Commands
CONSOLE.print("[bold cyan]COMMANDS:[/bold cyan]")
CONSOLE.print()
CONSOLE.print(" [green]drone @cli[/green] [dim]# Show discovered modules[/dim]")
CONSOLE.print(" [green]drone @cli aipass[/green] [dim]# Project commands[/dim]")
CONSOLE.print(" [green]drone @cli aipass init[/green] [dim]# Bootstrap a project[/dim]")
CONSOLE.print(" [green]drone @cli aipass init /path MyProj[/green] [dim]# Bootstrap with name[/dim]")
CONSOLE.print(" [green]drone @cli display[/green] [dim]# Display module info[/dim]")
CONSOLE.print(" [green]drone @cli display demo[/green] [dim]# Run display demo[/dim]")
CONSOLE.print(" [green]drone @cli templates[/green] [dim]# Templates module info[/dim]")
CONSOLE.print(" [green]drone @cli templates demo[/green] [dim]# Run templates demo[/dim]")
CONSOLE.print(" [green]drone @cli --help[/green] [dim]# This help message[/dim]")
CONSOLE.print()
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
# Public services
CONSOLE.print("[bold cyan]PUBLIC SERVICES (apps/modules/):[/bold cyan]")
CONSOLE.print()
@@ -145,13 +212,10 @@ def print_help():
CONSOLE.print(services_table)
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 4: IMPORT EXAMPLES
# Demonstrates: Code examples with [dim] styling and [yellow] for labels
# =============================================================================
# Import examples
CONSOLE.print("[bold cyan]HOW TO IMPORT CLI SERVICES:[/bold cyan]")
CONSOLE.print()
@@ -164,257 +228,88 @@ def print_help():
CONSOLE.print()
CONSOLE.print("[yellow]Rich console:[/yellow]")
CONSOLE.print("[dim] from rich.console import Console[/dim]")
CONSOLE.print("[dim] CONSOLE = Console() # Access Rich library directly[/dim]")
CONSOLE.print("[dim] from aipass.cli.apps.modules.display import console[/dim]")
CONSOLE.print("[dim] console.print('[bold]Hello[/bold]') # Rich formatted output[/dim]")
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 5: USAGE EXAMPLES
# Demonstrates: Columns() for side-by-side layout
# =============================================================================
CONSOLE.print("[bold cyan]USAGE EXAMPLES:[/bold cyan]")
CONSOLE.print()
example_cols = [
"[yellow]Display header:[/yellow]\n[dim]header('Create Branch',\n {'Name': 'feature',\n 'Type': 'module'})[/dim]",
"[yellow]Show success:[/yellow]\n[dim]success('Files created',\n items=12,\n time='2.3s')[/dim]",
"[yellow]Show error:[/yellow]\n[dim]error('Path not found',\n suggestion='Check spelling')[/dim]"
]
CONSOLE.print(Columns(example_cols, equal=True, expand=True))
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 6: ARCHITECTURE
# Demonstrates: Panel() for highlighted content blocks
# =============================================================================
# Architecture
CONSOLE.print("[bold cyan]ARCHITECTURE:[/bold cyan]")
CONSOLE.print()
arch_text = """[bold]CLI Branch Structure:[/bold]
[green]✓[/green] apps/modules/ = PUBLIC API (what branches import)
[green]\u2713[/green] apps/modules/ = PUBLIC API (what branches import)
- display.py Display functions (header, success, error, etc.)
- templates.py Standard operation patterns
- init_project.py Project bootstrap (aipass init)
[green]✓[/green] apps/handlers/ = PRIVATE (internal implementation)
[green]\u2713[/green] apps/handlers/ = PRIVATE (internal implementation)
- display/ Header, message formatters
- templates/ Operation patterns
- init/ Bootstrap logic
[green]✓[/green] Rich library = Underlying formatting engine
[green]\u2713[/green] Rich library = Underlying formatting engine
- Console, Table, Panel, Columns, Text styling"""
CONSOLE.print(Panel(arch_text, border_style="green", padding=(1, 2), box=box.ROUNDED))
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 8: FOR BRANCHES USING CLI
# Demonstrates: Service provider pattern explanation
# =============================================================================
CONSOLE.print("[bold cyan]FOR BRANCHES USING CLI:[/bold cyan]")
CONSOLE.print()
workflow_text = """[bold]Replace custom display code with CLI imports:[/bold]
[green]✓[/green] Displaying headers? → [dim]from aipass.cli.apps.modules.display import header[/dim]
[green]✓[/green] Showing success/errors? → [dim]from aipass.cli.apps.modules.display import success, error[/dim]
[green]✓[/green] Operation templates? → [dim]from aipass.cli.apps.modules.templates import operation_*[/dim]
[green]✓[/green] Rich formatting? → [dim]from rich.console import Console[/dim]
[bold]Benefits:[/bold]
• No code duplication across branches
• Consistent formatting system-wide
• Update CLI once → affects all branches instantly
• Rich library integration done once, used everywhere"""
CONSOLE.print(Panel(workflow_text, border_style="green", padding=(1, 2), box=box.ROUNDED))
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 9: QUICK REFERENCE
# Demonstrates: Using both CONSOLE.print() and module functions
# =============================================================================
CONSOLE.print("[bold cyan]QUICK REFERENCE:[/bold cyan]")
CONSOLE.print()
quick_ref = Table(show_header=True, header_style="bold cyan", border_style="dim")
quick_ref.add_column("Task", style="green")
quick_ref.add_column("CLI Function", style="yellow")
quick_ref.add_column("Example", style="dim")
quick_ref.add_row(
"Display title",
"header()",
"header('Task Complete')"
)
quick_ref.add_row(
"Success message",
"success()",
"success('Created', items=5)"
)
quick_ref.add_row(
"Error message",
"error()",
"error('Failed', suggestion='Retry')"
)
quick_ref.add_row(
"Warning message",
"warning()",
"warning('Be careful')"
)
quick_ref.add_row(
"Section break",
"section()",
"section('Results')"
)
quick_ref.add_row(
"Operation start",
"operation_start()",
"operation_start('Process', count=10)"
)
quick_ref.add_row(
"Operation end",
"operation_complete()",
"operation_complete(created=5, failed=0)"
)
CONSOLE.print(quick_ref)
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 10: FULL DOCUMENTATION
# =============================================================================
CONSOLE.print("[bold cyan]FULL DOCUMENTATION:[/bold cyan]")
CONSOLE.print()
_cli_root = Path(__file__).resolve().parents[1] # cli.py -> apps -> cli
CONSOLE.print(f" [yellow]Source code:[/yellow] [dim]{_cli_root / 'apps'}[/dim]")
CONSOLE.print(f" [yellow]Public API:[/yellow] [dim]{_cli_root / 'apps' / 'modules'}[/dim]")
CONSOLE.print(f" [yellow]Implementation:[/yellow] [dim]{_cli_root / 'apps' / 'handlers'}[/dim]")
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 11: RICH FORMATTING TIPS
# This section itself demonstrates Rich formatting!
# =============================================================================
CONSOLE.print("[bold cyan]RICH FORMATTING TIPS:[/bold cyan]")
CONSOLE.print()
CONSOLE.print(" [yellow]Text styles:[/yellow] [bold]bold[/bold], [dim]dim[/dim], [italic]italic[/italic], [underline]underline[/underline]")
CONSOLE.print(" [yellow]Colors:[/yellow] [red]red[/red], [green]green[/green], [yellow]yellow[/yellow], [blue]blue[/blue], [cyan]cyan[/cyan]")
CONSOLE.print(" [yellow]Icons:[/yellow] ✓ ✅ ❌ ⚠️ ⚙️ → • — ─")
CONSOLE.print(" [yellow]Structures:[/yellow] Table, Panel, Columns, Text, Progress")
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print()
# =============================================================================
# SECTION 12: DRONE COMPLIANCE
# CRITICAL: Commands line must be present for drone discovery
# =============================================================================
CONSOLE.print("[dim]Commands: help, --help, -h[/dim]")
# Drone compliance — commands line
CONSOLE.print("[dim]Commands: aipass, display, templates, demo, --help[/dim]")
CONSOLE.print()
def show_version():
"""Print version from META DATA HEADER."""
CONSOLE.print("CLI v0.2.0")
"""Print version."""
CONSOLE.print(f"CLI v{VERSION}")
def _handle_aipass(args):
"""Route aipass subcommands."""
if not args or (args[0] in ("--help", "-h", "help")):
_print_aipass_introspection()
return
# =============================================================================
# MAIN ENTRY POINT
# =============================================================================
subcmd = args[0]
sub_args = args[1:]
def main() -> int:
"""Main entry point - routes to modules."""
modules = discover_modules()
args = sys.argv[1:]
from aipass.cli.apps.modules.init_project import handle_command
if handle_command(subcmd, sub_args):
return
error(f"Unknown aipass subcommand: {subcmd}", suggestion="drone @cli aipass --help")
sys.exit(1)
def _print_aipass_introspection():
"""Show available aipass subcommands — discovery for new users."""
from rich.table import Table
CONSOLE.print()
header("aipass — Project Commands")
CONSOLE.print("[dim]Manage AIPass projects from the command line[/dim]")
CONSOLE.print()
table = Table(show_header=True, header_style="bold cyan", border_style="dim")
table.add_column("Command", style="green")
table.add_column("Description", style="white")
table.add_column("Example", style="dim")
table.add_row(
"init",
"Bootstrap a new AIPass project",
"drone @cli aipass init /path MyProject",
)
CONSOLE.print(table)
CONSOLE.print()
CONSOLE.print("[dim]Run [bold]drone @cli aipass init --help[/bold] for detailed usage[/dim]")
CONSOLE.print()
CONSOLE.print("[dim]Commands: init, --help[/dim]")
CONSOLE.print()
def main():
"""CLI branch entry point - shows available services"""
# Show introspection when run without arguments
if len(sys.argv) == 1:
# No args -> introspection (discovery mode)
if not args:
print_introspection()
return
return 0
# Handle version flag
if len(sys.argv) > 1 and sys.argv[1] in ['--version', '-V']:
show_version()
return
# Handle help flags
if len(sys.argv) > 1 and sys.argv[1] in ['--help', '-h', 'help']:
# Help flag -> full help with usage
if args[0] in ["--help", "-h", "help"]:
print_help()
return
return 0
# Route subcommands
command = sys.argv[1]
cmd_args = sys.argv[2:] if len(sys.argv) > 2 else []
if args[0] in ["--version", "-V"]:
show_version()
return 0
if command == "aipass":
_handle_aipass(cmd_args)
return
command = args[0]
remaining = args[1:] if len(args) > 1 else []
# Route to modules
if route_command(command, remaining, modules):
return 0
# Unknown command
error(f"Unknown command: {command}", suggestion="Run 'drone @cli --help' for usage")
sys.exit(1)
return 1
if __name__ == "__main__":
try:
main()
sys.exit(main())
except KeyboardInterrupt:
CONSOLE.print("\n[yellow]Operation cancelled[/yellow]")
sys.exit(0)
except Exception as e:
logger.error(f"CLI error: {e}", exc_info=True)
CONSOLE.print(f"\n[red]❌ Error: {e}[/red]")
CONSOLE.print(f"\n[red]Error: {e}[/red]")
sys.exit(1)