-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·679 lines (598 loc) · 23 KB
/
setup.sh
File metadata and controls
executable file
·679 lines (598 loc) · 23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
#!/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 || true
if ! "$VENV_PYTHON" -m pip --version &>/dev/null 2>&1; then
echo "ensurepip did not install pip — falling back to get-pip.py"
"$VENV_PYTHON" -c "import urllib.request; urllib.request.urlretrieve('https://bootstrap.pypa.io/get-pip.py', 'get-pip.py')"
"$VENV_PYTHON" get-pip.py
rm -f get-pip.py
fi
"$VENV_PYTHON" -m pip --version || { echo "ERROR: pip still missing after bootstrap" >&2; exit 1; }
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
# --- Detect shadowing drone installs (Windows) ---
# Issues #317 + #321: system-Python pip or legacy npm aipass-drone can shadow venv drone.exe.
# Warn the user with precise uninstall commands; don't touch anything automatically.
if [ "$IS_WINDOWS" -eq 1 ]; then
echo ""
echo "Checking for shadowing drone installs ..."
# System Python check (#317)
for sys_py in "python" "py -3" "python3"; do
if command -v $sys_py &>/dev/null; then
if $sys_py -m pip show aipass &>/dev/null 2>&1; then
# Don't match our own venv python
SYS_PY_PATH=$($sys_py -c "import sys; print(sys.executable)" 2>/dev/null || echo "")
if [ -n "$SYS_PY_PATH" ] && [[ "$SYS_PY_PATH" != *".venv"* ]]; then
echo " WARN: aipass is installed in system Python at $SYS_PY_PATH"
echo " This shadows the venv drone.exe on Windows PATH. To fix:"
echo " \"$SYS_PY_PATH\" -m pip uninstall aipass -y"
break
fi
fi
fi
done
# Legacy npm aipass-drone check (#321)
NPM_BIN="$APPDATA/npm"
if [ -d "$NPM_BIN" ] && { [ -f "$NPM_BIN/drone" ] || [ -f "$NPM_BIN/drone.cmd" ] || [ -f "$NPM_BIN/drone.ps1" ]; }; then
echo " WARN: Legacy npm drone scripts found in $NPM_BIN — these shadow venv drone.exe."
echo " To fix:"
echo " npm uninstall -g aipass-drone"
echo " rm -f \"$NPM_BIN/drone\" \"$NPM_BIN/drone.cmd\" \"$NPM_BIN/drone.ps1\""
fi
fi
# --- 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"
# --- Seed branch config files from .example defaults ---
# Some branches need a config file that's gitignored (contains local state).
# Ship `*.example.json` in git; seed the real file from it on fresh install.
MEMORY_CONFIG_DIR="$SCRIPT_DIR/src/aipass/memory/config"
MEMORY_CONFIG_FILE="$MEMORY_CONFIG_DIR/memory_bank.config.json"
MEMORY_CONFIG_EXAMPLE="$MEMORY_CONFIG_DIR/memory_bank.config.example.json"
if [ -f "$MEMORY_CONFIG_EXAMPLE" ] && [ ! -f "$MEMORY_CONFIG_FILE" ]; then
cp "$MEMORY_CONFIG_EXAMPLE" "$MEMORY_CONFIG_FILE"
echo " memory_bank.config.json seeded from example"
fi
# --- 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}]},
],
}
# Inject AIPASS_HOME into env block so dispatched agents find AIPass
import os
env_block = settings.get("env", {})
env_block["AIPASS_HOME"] = repo_root
# Windows: force UTF-8 for Rich output in hook processes
msys = os.environ.get("MSYSTEM", "") + os.environ.get("OSTYPE", "")
if "MSYS" in msys or "msys" in msys or "MINGW" in msys:
env_block["PYTHONUTF8"] = "1"
settings["env"] = env_block
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
print(f" hooks -> {settings_path}")
print(f" AIPASS_HOME -> {repo_root} (in settings.json env)")
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"
# HOOK_PYTHON was set earlier in the Claude hooks block; reuse it.
# Fall back to python3 if this block runs without that setup (defensive).
GEMINI_HOOK_PYTHON="${HOOK_PYTHON:-python3}"
python3 - "$SCRIPT_DIR" "$GEMINI_SETTINGS" "$GEMINI_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}/.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
# --- Set AIPASS_HOME + PATH so all services work from any project ---
echo ""
echo "Configuring cross-project access ..."
if [ "$IS_WINDOWS" -eq 1 ]; then
# Windows (Git Bash): write to ~/.bash_profile
PROFILE="$HOME/.bash_profile"
touch "$PROFILE"
# AIPASS_HOME
if ! grep -q "AIPASS_HOME" "$PROFILE" 2>/dev/null; then
echo "" >> "$PROFILE"
echo "# AIPass — cross-project access" >> "$PROFILE"
echo "export AIPASS_HOME=\"$SCRIPT_DIR\"" >> "$PROFILE"
echo " AIPASS_HOME added to $PROFILE"
else
echo " AIPASS_HOME already in $PROFILE"
fi
# PATH (venv Scripts for Windows)
VENV_SCRIPTS="$SCRIPT_DIR/.venv/Scripts"
if ! grep -q ".venv/Scripts" "$PROFILE" 2>/dev/null; then
echo "export PATH=\"$VENV_SCRIPTS:\$PATH\"" >> "$PROFILE"
echo " PATH updated in $PROFILE (drone available globally)"
else
echo " PATH already includes venv in $PROFILE"
fi
# PYTHONUTF8
if ! grep -q "PYTHONUTF8" "$PROFILE" 2>/dev/null; then
echo "export PYTHONUTF8=1" >> "$PROFILE"
echo " PYTHONUTF8=1 added to $PROFILE"
fi
# Export for current session too
export AIPASS_HOME="$SCRIPT_DIR"
export PATH="$VENV_SCRIPTS:$PATH"
export PYTHONUTF8=1
# PowerShell profile wrapper — makes `drone @branch cmd` work from PowerShell
# without @ being consumed by PS splatting operator. See issue #340.
PS_PROFILE_DIR="$HOME/Documents/WindowsPowerShell"
PS_PROFILE="$PS_PROFILE_DIR/Microsoft.PowerShell_profile.ps1"
mkdir -p "$PS_PROFILE_DIR"
if [ ! -f "$PS_PROFILE" ] || ! grep -q "AIPass drone wrapper" "$PS_PROFILE" 2>/dev/null; then
cat >> "$PS_PROFILE" <<'PSWRAP'
# AIPass drone wrapper — preserves @branch args that PowerShell would otherwise splat
function drone {
$exe = Join-Path $env:AIPASS_HOME '.venv\Scripts\drone.exe'
if (-not (Test-Path $exe)) { Write-Error "drone.exe not found at $exe"; return }
$raw = $MyInvocation.Line.Trim()
if ($raw -match '^drone\s+(.+)$') {
$argsPart = $Matches[1]
$argsPart = ($argsPart -split '\s*\|\s*')[0].TrimEnd()
cmd /c "`"$exe`" $argsPart"
} else { & $exe }
}
PSWRAP
echo " PowerShell drone wrapper written to $PS_PROFILE"
else
echo " PowerShell drone wrapper already in $PS_PROFILE"
fi
else
# Linux/macOS: write to ~/.bashrc
PROFILE="$HOME/.bashrc"
# AIPASS_HOME
if ! grep -q "AIPASS_HOME" "$PROFILE" 2>/dev/null; then
echo "" >> "$PROFILE"
echo "# AIPass — cross-project access" >> "$PROFILE"
echo "export AIPASS_HOME=\"$SCRIPT_DIR\"" >> "$PROFILE"
echo " AIPASS_HOME added to $PROFILE"
else
echo " AIPASS_HOME already in $PROFILE"
fi
export AIPASS_HOME="$SCRIPT_DIR"
fi
# --- Create global symlinks for CLI tools (Linux/macOS only) ---
echo ""
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Windows: drone available via PATH (set above)"
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