Files
AIPass/setup.sh
T

538 lines
17 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# AIPass setup script
# Creates a venv, installs the package in editable mode, and verifies CLI entry points.
#
set -euo pipefail
# cd to repo root (where this script lives) so it works from anywhere
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# --- OS detection ---
# Detect Windows (Git Bash / MSYS2 / Cygwin) — used throughout the script
IS_WINDOWS=0
case "${OSTYPE:-}" in
msys*|cygwin*|mingw*) IS_WINDOWS=1 ;;
*)
# Fallback: check uname if OSTYPE is unset
if uname -s 2>/dev/null | grep -qi "mingw\|msys\|cygwin"; then
IS_WINDOWS=1
fi
;;
esac
echo "=== AIPass Setup ==="
echo "Repo root: $SCRIPT_DIR"
echo ""
# --- Strip broken venv from PATH (Windows: stale .venv/Scripts shadows real Python) ---
if [ "$IS_WINDOWS" -eq 1 ]; then
export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '\.venv' | tr '\n' ':' | sed 's/:$//')
export PYTHONUTF8=1
fi
# --- Find working Python (#292: Windows python3 → MS Store alias) ---
PYTHON=""
# Try python3 first — verify it actually runs (not just exists on PATH)
if command -v python3 &>/dev/null && python3 -c "import sys" &>/dev/null 2>&1; then
PYTHON="python3"
# Fall back to python (Windows installs as 'python' not 'python3')
elif command -v python &>/dev/null && python -c "import sys" &>/dev/null 2>&1; then
PYTHON="python"
else
echo "FAIL: No working Python found. Install Python 3.10+ and try again."
echo " Windows: install from python.org, NOT the Microsoft Store."
echo " Then disable the Store alias: Settings > Apps > Advanced app settings > App execution aliases"
exit 1
fi
PY_VERSION=$($PYTHON -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
echo "Found $PYTHON $PY_VERSION"
# --- Check minimum version ---
PY_OK=$($PYTHON -c 'import sys; print(int(sys.version_info >= (3, 10)))')
if [ "$PY_OK" != "1" ]; then
echo "FAIL: Python 3.10+ required, found $PY_VERSION"
exit 1
fi
# --- Create venv ---
if [ "$IS_WINDOWS" -eq 1 ] && [ -f ".venv/Scripts/python.exe" ]; then
# Windows: skip venv recreation if python.exe exists (rm -rf unreliable due to file locking)
echo "Existing .venv found — reusing (Windows file locking prevents clean removal)"
elif [ -d ".venv" ]; then
echo "Existing .venv found — removing it for a clean install."
rm -rf .venv
fi
if [ ! -d ".venv" ]; then
echo "Creating virtual environment at .venv ..."
if [ "$IS_WINDOWS" -eq 1 ]; then
# Windows: create without pip, bootstrap manually to avoid subprocess path issues
$PYTHON -m venv --without-pip .venv
else
$PYTHON -m venv .venv
fi
fi
# --- Activate and install ---
# Determine venv python path for explicit invocation
if [ "$IS_WINDOWS" -eq 1 ] && [ -f ".venv/Scripts/python.exe" ]; then
source .venv/Scripts/activate
VENV_PYTHON=".venv/Scripts/python.exe"
# Bootstrap pip if missing (--without-pip on Windows)
if ! "$VENV_PYTHON" -m pip --version &>/dev/null 2>&1; then
echo "Bootstrapping pip in venv ..."
"$VENV_PYTHON" -m ensurepip --default-pip --quiet 2>/dev/null || true
fi
else
source .venv/bin/activate
VENV_PYTHON="python3"
fi
echo "Upgrading pip ..."
"$VENV_PYTHON" -m pip install --upgrade pip --quiet
echo "Installing aipass in editable mode (with dev extras) ..."
"$VENV_PYTHON" -m pip install -e ".[dev]" --quiet
# --- Verify CLI entry points ---
FAIL=0
echo ""
echo "Verifying entry points ..."
if drone --help &>/dev/null; then
echo " drone ... ok"
else
echo " drone ... FAILED"
FAIL=1
fi
# seedgo is accessed via drone @seedgo, not as a standalone CLI
if drone @seedgo --help &>/dev/null; then
echo " seedgo ... ok (via drone @seedgo)"
else
echo " seedgo ... FAILED"
FAIL=1
fi
# --- Create secrets directory ---
SECRETS_DIR="$HOME/.secrets/aipass"
if [ ! -d "$SECRETS_DIR" ]; then
echo "Creating secrets directory at $SECRETS_DIR ..."
mkdir -p "$SECRETS_DIR"
chmod 700 "$HOME/.secrets"
echo " ~/.secrets/aipass/ ... created"
else
echo "Secrets directory already exists — skipping"
fi
# --- Seed .env template into secrets ---
if [ ! -f "$SECRETS_DIR/.env" ] && [ -f ".env.example" ]; then
cp .env.example "$SECRETS_DIR/.env"
echo " Copied .env.example → ~/.secrets/aipass/.env (add your API keys there)"
fi
# --- Generate branch registry ---
if [ ! -f "AIPASS_REGISTRY.json" ]; then
echo "Generating AIPASS_REGISTRY.json ..."
python3 - "$SCRIPT_DIR" << 'PYEOF'
import json, sys, os
from pathlib import Path
from datetime import date
repo_root = sys.argv[1]
src_dir = Path(repo_root) / "src" / "aipass"
today = date.today().isoformat()
branches = []
# Discover modules under src/aipass/
for d in sorted(src_dir.iterdir()):
if d.is_dir() and not d.name.startswith(("_", ".")):
branches.append({
"name": d.name,
"path": str(d),
"profile": "library",
"description": "",
"email": f"@{d.name}",
"status": "active",
"created": today,
"last_active": today,
})
# NOTE: commons and skills were external branches, now removed from public repo.
# Registry only includes branches discovered under src/aipass/.
registry = {
"metadata": {
"version": "1.0.0",
"last_updated": today,
"total_branches": len(branches),
},
"branches": branches,
}
out = Path(repo_root) / "AIPASS_REGISTRY.json"
out.write_text(json.dumps(registry, indent=2) + "\n")
print(f" {len(branches)} branches registered")
PYEOF
else
echo "AIPASS_REGISTRY.json already exists — skipping"
fi
# --- Bootstrap branch identity and memory files ---
echo ""
echo "Bootstrapping branch identity files ..."
DATE_TODAY=$(date +%Y-%m-%d)
bootstrap_branch() {
local name="$1"
local path="$2"
local citizen_class="$3"
local role="$4"
local created=0
# .trinity/passport.json
mkdir -p "$path/.trinity"
if [ ! -f "$path/.trinity/passport.json" ]; then
cat > "$path/.trinity/passport.json" << JSONEOF
{
"document_metadata": {
"document_type": "identity",
"document_name": "${name}.PASSPORT",
"version": "1.0.0",
"schema_version": "1.0.0",
"created": "${DATE_TODAY}",
"last_updated": "${DATE_TODAY}",
"managed_by": "${name}"
},
"identity": {
"name": "${name}",
"citizen_class": "${citizen_class}",
"role": "${role}",
"status": "active"
}
}
JSONEOF
created=1
fi
# .trinity/local.json
if [ ! -f "$path/.trinity/local.json" ]; then
cat > "$path/.trinity/local.json" << JSONEOF
{
"document_metadata": {
"document_type": "session_history",
"document_name": "${name}.LOCAL",
"version": "1.0.0",
"schema_version": "1.0.0",
"created": "${DATE_TODAY}",
"last_updated": "${DATE_TODAY}",
"managed_by": "${name}",
"tags": ["session_tracking", "work_log", "${name}"],
"limits": {"max_lines": 600, "note": "Auto-rollover when max_lines exceeded"},
"status": {"health": "healthy", "current_lines": 0, "last_health_check": "${DATE_TODAY}"}
},
"active_tasks": {
"today_focus": "First session — explore codebase and capabilities",
"recently_completed": []
},
"key_learnings": {},
"sessions": []
}
JSONEOF
created=1
fi
# .trinity/observations.json
if [ ! -f "$path/.trinity/observations.json" ]; then
cat > "$path/.trinity/observations.json" << JSONEOF
{
"document_metadata": {
"document_type": "collaboration_patterns",
"document_name": "${name}.OBSERVATIONS",
"version": "1.0.0",
"schema_version": "1.0.0",
"created": "${DATE_TODAY}",
"last_updated": "${DATE_TODAY}",
"managed_by": "${name}",
"tags": ["collaboration", "patterns", "${name}"],
"limits": {"max_lines": 600, "note": "Auto-rollover when max_lines exceeded"},
"status": {"health": "healthy", "current_lines": 0, "last_health_check": "${DATE_TODAY}"}
},
"guidelines": {
"purpose": "Capture collaboration patterns and experiential insights over time",
"chronological_order": "Newest entries at TOP, oldest at BOTTOM - NEVER reorder"
},
"observations": [
{
"date": "${DATE_TODAY}",
"session": 1,
"entries": [
{"title": "First Contact", "detail": "Branch initialized. Ready to begin capturing collaboration patterns."}
]
}
]
}
JSONEOF
created=1
fi
# .seedgo/bypass.json
mkdir -p "$path/.seedgo"
if [ ! -f "$path/.seedgo/bypass.json" ]; then
echo '{}' > "$path/.seedgo/bypass.json"
created=1
fi
# .ai_mail.local/inbox.json
mkdir -p "$path/.ai_mail.local"
if [ ! -f "$path/.ai_mail.local/inbox.json" ]; then
echo '{"inbox": []}' > "$path/.ai_mail.local/inbox.json"
created=1
fi
if [ "$created" -eq 1 ]; then
echo " @${name} ... bootstrapped"
else
echo " @${name} ... exists (skipped)"
fi
}
# Branches inside src/aipass/
bootstrap_branch "drone" "$SCRIPT_DIR/src/aipass/drone" "builder" "Command routing and module discovery"
bootstrap_branch "seedgo" "$SCRIPT_DIR/src/aipass/seedgo" "builder" "Standards enforcement and code auditing"
bootstrap_branch "prax" "$SCRIPT_DIR/src/aipass/prax" "builder" "Logging and monitoring system"
bootstrap_branch "cli" "$SCRIPT_DIR/src/aipass/cli" "builder" "Display formatting service"
bootstrap_branch "flow" "$SCRIPT_DIR/src/aipass/flow" "builder" "Workflow and plan management"
bootstrap_branch "ai_mail" "$SCRIPT_DIR/src/aipass/ai_mail" "builder" "Inter-agent messaging and dispatch"
bootstrap_branch "api" "$SCRIPT_DIR/src/aipass/api" "builder" "LLM access and model routing"
bootstrap_branch "trigger" "$SCRIPT_DIR/src/aipass/trigger" "builder" "Event-driven automation"
bootstrap_branch "spawn" "$SCRIPT_DIR/src/aipass/spawn" "builder" "Branch lifecycle management"
bootstrap_branch "devpulse" "$SCRIPT_DIR/src/aipass/devpulse" "manager" "Orchestration hub and coordination"
bootstrap_branch "memory" "$SCRIPT_DIR/src/aipass/memory" "builder" "Vector memory bank"
# External branches
# NOTE: backup, daemon removed S82/S87. commons, skills moved to external repos.
# Only the 11 core branches above should be bootstrapped.
echo " 15 branches bootstrapped"
# --- Install Claude Code hooks ---
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
if [ -d "$SCRIPT_DIR/.claude/hooks" ]; then
echo "Installing Claude Code hooks ..."
mkdir -p "$HOME/.claude"
# Determine python command for hooks — venv python on Windows, python3 on Unix
if [ "$IS_WINDOWS" -eq 1 ]; then
HOOK_PYTHON="$SCRIPT_DIR/.venv/Scripts/python.exe"
else
HOOK_PYTHON="python3"
fi
"$PYTHON" - "$SCRIPT_DIR" "$CLAUDE_SETTINGS" "$HOOK_PYTHON" << 'PYEOF'
import json
import sys
from pathlib import Path
repo_root = sys.argv[1]
settings_path = Path(sys.argv[2])
hook_python = sys.argv[3]
hooks_dir = f"{repo_root}/.claude/hooks"
# Load existing settings or start fresh
if settings_path.exists():
settings = json.loads(settings_path.read_text())
else:
settings = {}
# Build hooks config with absolute paths
settings["hooks"] = {
"UserPromptSubmit": [
{"hooks": [{"type": "command", "command": f"cat {repo_root}/.aipass/aipass_global_prompt.md 2>/dev/null || true"}]},
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/branch_prompt_loader.py"}]},
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/identity_injector.py"}]},
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/email_notification.py"}]},
],
"PreToolUse": [
{"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task",
"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/tool_use_sound.py"}]},
],
"PostToolUse": [
{"matcher": "Edit|MultiEdit|Write|NotebookEdit",
"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/auto_fix_diagnostics.py"}]},
],
"Stop": [
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/stop_sound.py"}]},
],
"Notification": [
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/notification_sound.py"}]},
],
"PreCompact": [
{"matcher": "manual", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact.py", "timeout": 60}]},
{"matcher": "auto", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact.py", "timeout": 60}]},
],
}
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
print(f" hooks -> {settings_path}")
PYEOF
else
echo "Skipping hooks (no .claude/hooks/ directory found)"
fi
# --- Install Codex CLI hooks ---
if command -v codex &>/dev/null; then
if [ -f "$SCRIPT_DIR/.codex/hooks.json" ]; then
echo "Installing Codex CLI hooks ..."
mkdir -p "$HOME/.codex"
python3 - "$SCRIPT_DIR" "$HOME/.codex/config.toml" << 'PYEOF'
import sys
from pathlib import Path
repo_root = sys.argv[1]
config_path = Path(sys.argv[2])
# Read existing config or start fresh
existing = {}
if config_path.exists():
for line in config_path.read_text().splitlines():
line = line.strip()
if line and not line.startswith("[") and not line.startswith("#") and "=" in line:
key, val = line.split("=", 1)
existing[key.strip()] = val.strip()
# Preserve model if set
model = existing.get("model", '"o4-mini"')
config = f'''model = {model}
check_for_update_on_startup = false
[features]
codex_hooks = true
[experimental_features]
multi_agent = true
multi_agent_v2 = true
[projects."{repo_root}"]
trust_level = "trusted"
'''
config_path.write_text(config)
print(f" config.toml -> {config_path}")
print(f" hooks.json -> {repo_root}/.codex/hooks.json (project-level, travels with repo)")
PYEOF
else
echo "Skipping Codex hooks (no .codex/hooks.json found in repo)"
fi
else
echo "Skipping Codex CLI (not installed)"
fi
# --- Install Gemini CLI hooks ---
if command -v gemini &>/dev/null; then
if [ -d "$SCRIPT_DIR/.gemini/hooks" ]; then
echo "Installing Gemini CLI hooks ..."
GEMINI_SETTINGS="$HOME/.gemini/settings.json"
mkdir -p "$HOME/.gemini"
python3 - "$SCRIPT_DIR" "$GEMINI_SETTINGS" << 'PYEOF'
import json
import sys
from pathlib import Path
repo_root = sys.argv[1]
settings_path = Path(sys.argv[2])
hooks_dir = f"{repo_root}/.gemini/hooks"
# Load existing settings or start fresh
if settings_path.exists():
settings = json.loads(settings_path.read_text())
else:
settings = {}
# Build hooks config with absolute paths (Gemini uses different event names)
settings["hooks"] = {
"SessionStart": [
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/session_start_identity.py", "timeout": 10}]}
],
"BeforeModel": [
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/prompt_inject.py", "timeout": 10}]}
],
"BeforeTool": [
{"matcher": "Edit|Write",
"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_edit_gate.py", "timeout": 5}]}
],
}
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
print(f" hooks -> {settings_path}")
PYEOF
else
echo "Skipping Gemini hooks (no .gemini/hooks/ directory found in repo)"
fi
else
echo "Skipping Gemini CLI (not installed)"
fi
# --- Create global symlinks for CLI tools (Linux/macOS only) ---
echo ""
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Windows detected — skipping symlink (ln/sudo not available on Windows)"
echo ""
echo "To use drone from any directory, add the venv to your PATH:"
echo " PowerShell: \$env:PATH = \"$SCRIPT_DIR\\.venv\\Scripts;\" + \$env:PATH"
echo " Git Bash: export PATH=\"$SCRIPT_DIR/.venv/bin:\$PATH\""
echo " (Add to ~/.bash_profile for Git Bash persistence)"
else
echo "Creating global symlinks ..."
VENV_BIN="$SCRIPT_DIR/.venv/bin"
LOCAL_BIN="/usr/local/bin"
for cmd in drone; do
if [ -f "$VENV_BIN/$cmd" ]; then
if sudo ln -sf "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd" 2>/dev/null; then
echo " $LOCAL_BIN/$cmd -> $VENV_BIN/$cmd"
else
echo " WARN: Could not create symlink for $cmd (try running with sudo)"
echo " Manual fix: sudo ln -sf $VENV_BIN/$cmd $LOCAL_BIN/$cmd"
fi
fi
done
fi
# --- Result ---
echo ""
if [ "$FAIL" -eq 0 ]; then
echo "=== Setup complete ==="
echo ""
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "drone is available in .venv/Scripts/ (or .venv/bin/ for Git Bash)."
echo "Add the appropriate directory to your PATH (see above)."
else
echo "drone is available globally via /usr/local/bin symlink."
fi
echo "seedgo is accessed via: drone @seedgo"
echo "No venv activation needed for CLI commands."
echo ""
echo "CLI integrations:"
echo " Claude Code: hooks installed to ~/.claude/settings.json"
command -v codex &>/dev/null && echo " Codex CLI: hooks at .codex/hooks.json + config at ~/.codex/config.toml"
command -v gemini &>/dev/null && echo " Gemini CLI: hooks installed to ~/.gemini/settings.json"
echo ""
else
echo "=== Setup finished with errors ==="
echo "The venv was created and the package was installed, but one or more"
echo "CLI entry points failed verification. Check the output above."
exit 1
fi