feat(cli): migrate aipass init from devpulse prototype (#48)

Restructure init_project.py into CLI's 3-layer architecture:
- Handler: apps/handlers/init/bootstrap.py (pure Python business logic)
- Module: apps/modules/init_project.py (thin orchestration)
- Wiring: cli.py routes `aipass init` via _handle_aipass()

Command: drone @cli aipass init [target_dir] [project_name]

FPLAN-0041

Co-authored-by: @cli <cli@aipass>
This commit is contained in:
AIPass
2026-03-14 23:29:50 -07:00
committed by GitHub
co-authored by @cli
parent f9cff284b3
commit 05af9e5cd5
4 changed files with 367 additions and 2 deletions
+28 -2
View File
@@ -332,6 +332,23 @@ def show_version():
CONSOLE.print("CLI v0.2.0")
def _handle_aipass(args):
"""Route aipass subcommands."""
if not args:
error("Missing subcommand", suggestion="Try: drone @cli aipass init")
sys.exit(1)
subcmd = args[0]
sub_args = args[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="Try: drone @cli aipass init")
sys.exit(1)
def main():
"""CLI branch entry point - shows available services"""
@@ -350,8 +367,17 @@ def main():
print_help()
return
# Default behavior for other commands
print_help()
# Route subcommands
command = sys.argv[1]
cmd_args = sys.argv[2:] if len(sys.argv) > 2 else []
if command == "aipass":
_handle_aipass(cmd_args)
return
# Unknown command
error(f"Unknown command: {command}", suggestion="Run 'drone @cli --help' for usage")
sys.exit(1)
if __name__ == "__main__":
@@ -0,0 +1,174 @@
# =================== AIPass ====================
# Name: bootstrap.py
# Description: Init handler — bootstrap an AIPass project in any directory
# Version: 1.0.0
# Created: 2026-03-14
# Modified: 2026-03-14
# =============================================
"""
Init Bootstrap Handler - PRIVATE implementation
Business logic for `aipass init`. Creates the project scaffold:
1. {NAME}_REGISTRY.json — project registry with UUID
2. .trinity/passport.json — project identity
3. .trinity/local.json — local context (empty)
4. .trinity/observations.json — observations (empty)
5. .aipass/aipass_local_prompt.md — local prompt
6. AIPASS.md — project prompt
RULES:
- Pure Python only (no module/prax/cli imports)
- Returns dict, raises exceptions on errors
- No hardcoded paths
"""
import json
import re
import uuid
from datetime import date
from pathlib import Path
def _sanitize_name(raw: str) -> str:
"""Sanitize a project name for use in filenames.
Replaces non-alphanumeric characters (except underscore/hyphen) with
underscores and strips leading/trailing underscores.
"""
return re.sub(r"[^A-Z0-9_-]", "_", raw.upper()).strip("_")
def init_project(target: Path, project_name: str | None = None) -> dict:
"""Initialize an AIPass project in the target directory.
Args:
target: Directory to initialize
project_name: Name for the registry (defaults to directory name)
Returns:
dict with registry_id, registry_file, project_name, target, created_files
Raises:
ValueError: If project name is empty after sanitization
FileExistsError: If passport or registry already exists
"""
target = target.resolve()
if not target.exists():
target.mkdir(parents=True)
raw_name = project_name or target.name
name = _sanitize_name(raw_name)
if not name:
raise ValueError(
f"Cannot derive project name from '{raw_name}'. "
"Pass a project name explicitly."
)
registry_id = str(uuid.uuid4())
today = date.today().isoformat()
created = []
# 1. Registry
registry_filename = f"{name}_REGISTRY.json"
registry_path = target / registry_filename
if registry_path.exists():
raise FileExistsError(f"Registry already exists: {registry_path}")
registry_data = {
"metadata": {
"id": registry_id,
"name": name,
"version": "1.0.0",
"created": today,
"last_updated": today,
"total_branches": 0,
},
"branches": [],
}
registry_path.write_text(
json.dumps(registry_data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
created.append(str(registry_path))
# 2. .trinity/
trinity_dir = target / ".trinity"
trinity_dir.mkdir(exist_ok=True)
passport = {
"document_metadata": {
"document_type": "project_identity",
"document_name": f"{name}.PASSPORT",
"version": "1.0.0",
"created": today,
"last_updated": today,
},
"identity": {
"project_name": name,
"role": "project_root",
"purpose": "",
},
"citizenship": {
"registered": True,
"registry_id": registry_id,
"registry_name": name,
},
}
passport_path = trinity_dir / "passport.json"
if passport_path.exists():
raise FileExistsError(
f"Passport already exists: {passport_path}. "
"Remove .trinity/passport.json to re-initialize."
)
passport_path.write_text(
json.dumps(passport, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
created.append(str(passport_path))
# local.json + observations.json
for filename in ("local.json", "observations.json"):
p = trinity_dir / filename
if not p.exists():
p.write_text("{}\n", encoding="utf-8")
created.append(str(p))
# 3. .aipass/
aipass_dir = target / ".aipass"
aipass_dir.mkdir(exist_ok=True)
prompt_path = aipass_dir / "aipass_local_prompt.md"
if not prompt_path.exists():
prompt_path.write_text(
f"# {name} — Local Prompt\n\nInjected every turn. Add project-specific context here.\n",
encoding="utf-8",
)
created.append(str(prompt_path))
# 4. AIPASS.md
aipass_md = target / "AIPASS.md"
if not aipass_md.exists():
aipass_md.write_text(
"# AIPass — Project Prompt\n\n"
"Project-level instructions. Persists in context for the entire conversation.\n\n"
"## Startup\n\n"
"On any greeting, silently read these files from CWD and run the commands "
"— no narration, no announcing steps. Just do it and respond with the status.\n\n"
"**Read:** `.trinity/passport.json`, `.trinity/local.json`, "
"`.trinity/observations.json`, `README.md`\n"
"**Run:** `git status`\n\n"
"## Memories\n\n"
"Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. "
"If compaction hits before you save, it's gone.\n",
encoding="utf-8",
)
created.append(str(aipass_md))
return {
"registry_id": registry_id,
"registry_file": registry_filename,
"project_name": name,
"target": str(target),
"created_files": created,
}
+165
View File
@@ -0,0 +1,165 @@
# =================== AIPass ====================
# Name: init_project.py
# Description: Init Project Module — orchestration layer for aipass init
# Version: 1.0.0
# Created: 2026-03-14
# Modified: 2026-03-14
# =============================================
"""
Init Project Module - PUBLIC API
Thin orchestration module for the `aipass init` command.
Routes to the init bootstrap handler for business logic.
Usage:
drone @cli aipass init [target_dir] [project_name]
"""
import os
import sys
from pathlib import Path
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
# =============================================================================
# MODULE PATTERN FUNCTIONS (SEED compliant)
# =============================================================================
def print_introspection():
"""Display module info and connected handlers."""
console.print()
console.print("[bold cyan]Init Project Module[/bold cyan]")
console.print()
console.print("[dim]Bootstrap an AIPass project in any directory[/dim]")
console.print()
console.print("[yellow]Connected Handlers:[/yellow]")
console.print()
console.print(" [cyan]handlers/init/[/cyan]")
console.print(" [dim]- bootstrap.py[/dim]")
console.print()
console.print("[dim]Run 'drone @cli aipass init --help' for usage[/dim]")
console.print()
def print_help():
"""Display help for the init command."""
console.print()
header("aipass init - Project Bootstrap")
console.print("[dim]Initialize an AIPass project in any directory[/dim]")
console.print()
console.print("[bold cyan]USAGE:[/bold cyan]")
console.print()
console.print(" [dim]drone @cli aipass init[/dim] Initialize in current directory")
console.print(" [dim]drone @cli aipass init /path/to/dir[/dim] Initialize in target directory")
console.print(" [dim]drone @cli aipass init /path/to/dir MyProj[/dim] Initialize with custom name")
console.print()
console.print("[bold cyan]WHAT IT CREATES:[/bold cyan]")
console.print()
console.print(" [green]1.[/green] {NAME}_REGISTRY.json Project registry with UUID")
console.print(" [green]2.[/green] .trinity/passport.json Project identity")
console.print(" [green]3.[/green] .trinity/local.json Local context (empty)")
console.print(" [green]4.[/green] .trinity/observations.json Observations (empty)")
console.print(" [green]5.[/green] .aipass/aipass_local_prompt.md Local prompt")
console.print(" [green]6.[/green] AIPASS.md Project prompt")
console.print()
console.print("[dim]Commands: init, init --help[/dim]")
console.print()
def handle_command(command: str, args: List[str]) -> bool:
"""Handle 'init' command.
Args:
command: The subcommand string (e.g. "init")
args: Remaining positional arguments after the subcommand
Returns:
True if the command was handled, False otherwise
"""
if command != "init":
return False
# Handle help flag
if args and args[0] in ("--help", "-h", "help"):
print_help()
return True
# Parse positional args: [target_dir] [project_name]
caller_cwd = os.environ.get("AIPASS_CALLER_CWD", os.getcwd())
target = Path(args[0]) if args else Path(caller_cwd)
project_name = args[1] if len(args) > 1 else None
try:
result = init_project(target, project_name)
except ValueError as exc:
error(str(exc), suggestion="Pass a project name explicitly")
sys.exit(1)
except FileExistsError as exc:
error(str(exc), suggestion="Remove the existing file to re-initialize")
sys.exit(1)
except OSError as exc:
error(f"Filesystem error: {exc}")
sys.exit(1)
# Display results
header("Project Initialized")
console.print(f" [bold]{result['project_name']}[/bold]")
console.print()
success(
f"Created {len(result['created_files'])} files",
registry=f"{result['registry_file']} (id: {result['registry_id'][:8]}...)",
target=result["target"],
)
console.print()
console.print("[dim]Files created:[/dim]")
for f in result["created_files"]:
console.print(f" [dim]{f}[/dim]")
console.print()
return True
# =============================================================================
# MODULE EXPORTS
# =============================================================================
__all__ = [
"handle_command",
]
# =============================================================================
# ENTRY POINT (SEED pattern)
# =============================================================================
if __name__ == "__main__":
try:
if len(sys.argv) == 1:
print_introspection()
sys.exit(0)
if sys.argv[1] in ("--help", "-h", "help"):
print_help()
sys.exit(0)
command = sys.argv[1]
cmd_args = sys.argv[2:] if len(sys.argv) > 2 else []
if handle_command(command, cmd_args):
sys.exit(0)
else:
console.print(f"[red]Unknown command: {command}[/red]")
console.print("[dim]Run 'python3 init_project.py --help' for usage[/dim]")
sys.exit(1)
except Exception as e:
console.print(f"[red]Error: {e}[/red]")
sys.exit(1)