Files
AIPass/src/aipass/drone/cli.py
T
AIOSAIandClaude Opus 4.6 b3067ff45a feat(drone): add introspective pattern — no-args discovery vs --help
Implements the internal AIPass navigation pattern:
- `drone @module` (no args) → introspective: shows what's connected
- `drone @module --help` → help: shows usage documentation

Seedgo introspective lists all discovered plugins with descriptions.
Modules that don't implement get_introspective() fall back to help.

442 tests pass. 5 new introspective tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:06:28 -08:00

212 lines
5.9 KiB
Python

"""
Drone CLI — command-line interface for aipass.drone.
Entry point: `drone` (wired via pyproject.toml console_scripts to cli.main).
Usage:
drone Show available commands
drone --help Show help
drone --version Show version
drone systems List registered branches and modules
drone @branch command [args] Route command to branch
drone @module command [args] Route command to internal module
drone @branch --help Show help for branch or module
Zero external dependencies — stdlib only (sys).
"""
from __future__ import annotations
import sys
import aipass
from aipass.drone import (
CommandExecutionError,
BranchNotFoundError,
get_help,
list_branches,
route_command,
)
from aipass.drone.modules import (
is_module,
list_modules,
get_module_info,
get_module_introspective,
route_module_command,
get_module_help,
)
_HELP_TEXT = """\
drone — AIPass command router
Usage:
drone Show this help
drone --help Show this help
drone --version Show version
drone systems List registered branches and modules
drone @target command [args] Route command to branch or module
drone @target --help Show help for branch or module
Examples:
drone systems
drone @seedgo audit src/
drone @seedgo check src/myfile.py
drone @flow status
drone @flow --help
"""
def main() -> None:
"""Entry point for the `drone` CLI command.
Parses sys.argv manually and dispatches to the appropriate handler.
Exits with the command's exit code on branch commands, 0 on success,
1 on error.
"""
args = sys.argv[1:]
# No args or explicit --help
if not args or args == ["--help"]:
print(_HELP_TEXT, end="")
sys.exit(0)
# --version
if args == ["--version"]:
print(aipass.__version__)
sys.exit(0)
# systems — list registered branches
if args[0] == "systems":
_cmd_systems()
return
# @branch ... — route to a branch
if args[0].startswith("@"):
_cmd_branch(args)
return
# Unknown command
print(f"drone: unknown command '{args[0]}'", file=sys.stderr)
print("Run 'drone --help' for usage.", file=sys.stderr)
sys.exit(1)
def _cmd_systems() -> None:
"""Handle `drone systems` — list registered branches and modules."""
branches = list_branches()
modules = list_modules()
if not branches and not modules:
print("No branches or modules registered.")
sys.exit(0)
if modules:
print(f"Modules ({len(modules)}):")
for name in modules:
info = get_module_info(name)
if info:
print(f" @{name} {info.description}")
else:
print(f" @{name} (not available)")
if branches:
print()
if branches:
print(f"Branches ({len(branches)}):")
for name in sorted(branches):
print(f" {name}")
sys.exit(0)
def _cmd_branch(args: list[str]) -> None:
"""Handle `drone @target command [args]` or `drone @target --help`.
Checks internal modules first (e.g. @seedgo), then falls back
to branch registry for external branches.
"""
target = args[0]
rest = args[1:]
module_name = target.lstrip("@")
# Check if this is a registered internal module
if is_module(module_name):
_cmd_module(module_name, rest)
return
# Fall through to branch routing
# drone @branch --help
if not rest or rest == ["--help"]:
try:
result = get_help(target)
if result.text:
print(result.text, end="")
else:
print(f"No help available for {target}.")
except BranchNotFoundError as exc:
print(f"drone: {exc}", file=sys.stderr)
sys.exit(1)
except CommandExecutionError as exc:
print(f"drone: {exc}", file=sys.stderr)
sys.exit(1)
sys.exit(0)
# drone @branch command [args...]
command = rest[0]
cmd_args = rest[1:]
try:
result = route_command(target, command, args=cmd_args if cmd_args else None)
except BranchNotFoundError as exc:
print(f"drone: {exc}", file=sys.stderr)
sys.exit(1)
except CommandExecutionError as exc:
print(f"drone: {exc}", file=sys.stderr)
sys.exit(1)
if result.stdout:
print(result.stdout, end="")
if result.stderr:
print(result.stderr, end="", file=sys.stderr)
sys.exit(result.exit_code)
def _cmd_module(name: str, args: list[str]) -> None:
"""Handle routing to an internal module (e.g. @seedgo).
No args → introspective (discovery: what's connected).
--help → help text (usage documentation).
"""
# drone @module (no args) → introspective / discovery
if not args:
intro_text = get_module_introspective(name)
if intro_text:
print(intro_text, end="")
else:
print(f"No information available for @{name}.")
sys.exit(0)
# drone @module --help → usage documentation
if args == ["--help"]:
help_text = get_module_help(name)
if help_text:
print(help_text, end="")
else:
print(f"No help available for @{name}.")
sys.exit(0)
command = args[0]
cmd_args = args[1:] if len(args) > 1 else None
try:
result = route_module_command(name, command, cmd_args)
except (ImportError, AttributeError) as exc:
print(f"drone: module @{name} is registered but not available: {exc}", file=sys.stderr)
sys.exit(1)
if result.get("stdout"):
print(result["stdout"], end="")
if result.get("stderr"):
print(result["stderr"], end="", file=sys.stderr)
sys.exit(result.get("exit_code", 0))