-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup
More file actions
executable file
Β·638 lines (577 loc) Β· 25.4 KB
/
Copy pathsetup
File metadata and controls
executable file
Β·638 lines (577 loc) Β· 25.4 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
#!/usr/bin/env bash
# Perry setup β install perry/ into the host's skills directory (Claude Code
# at ~/.claude/skills/, Codex CLI at ~/.agents/skills/, OpenCode at
# ~/.config/opencode/skills/) so it surfaces /perry.
#
# ONE skill is registered. goals, work and decide are lanes inside it, reached as
# `/perry goals β¦`, and loaded on demand by the router. Earlier versions also
# symlinked them as sibling skills; this script now removes those links,
# because `design` and `okr` are common words in a shared namespace and Perry
# should not claim them (see install_at for the full reasoning).
#
# All hosts read SKILL.md frontmatter for skill discovery; the
# install is just a symlink in the host's canonical skills directory.
#
# Host selection:
# No flag β auto-detect; install for whichever host(s) are present
# in PATH (claude / codex / opencode). Fails if none is detected
# and no flag forces.
# --claude β force install for Claude Code (~/.claude/skills/)
# --codex β force install for Codex CLI (~/.agents/skills/)
# --opencode β force install for OpenCode (~/.config/opencode/skills/)
#
# Before the symlink phase, runs a dependency check (Phase 0). On a fresh
# macOS, several deps may be missing β git (Xcode CLT), Homebrew, gtimeout
# (coreutils), Node.js (for codex), codex CLI. The script reports each, and
# with --yes-deps will auto-install the ones that can be installed
# non-interactively.
set -e
SETUP_DIR="$(cd "$(dirname "$0")" && pwd)"
SOURCE_DIR="$(cd "$(dirname "$0")" && pwd -P)" # resolves symlinks
# βββ Parse flags ββββββββββββββββββββββββββββββββββββββββββββββ
LOCAL=0
QUIET=0
EXPLICIT_CLAUDE=0
EXPLICIT_CODEX=0
EXPLICIT_OPENCODE=0
ANY_EXPLICIT=0
DEPS_MODE="prompt" # prompt | yes | no | check-only
while [ $# -gt 0 ]; do
case "$1" in
--local) LOCAL=1; shift ;;
--claude) EXPLICIT_CLAUDE=1; ANY_EXPLICIT=1; shift ;;
--codex) EXPLICIT_CODEX=1; ANY_EXPLICIT=1; shift ;;
--opencode) EXPLICIT_OPENCODE=1; ANY_EXPLICIT=1; shift ;;
--quiet) QUIET=1; shift ;;
--yes-deps) DEPS_MODE="yes"; shift ;;
--no-deps) DEPS_MODE="no"; shift ;;
--check-deps-only) DEPS_MODE="check-only"; shift ;;
-h|--help)
cat <<'USAGE'
Perry setup β register the one Perry skill with Claude Code, Codex CLI, and/or OpenCode.
Usage: setup [--claude] [--codex] [--opencode] [--local] [--yes-deps|--no-deps|--check-deps-only] [--quiet]
Host selection:
(no flag) Auto-detect: install for whichever of claude / codex / opencode
is in PATH. Multiple found β install all. None β fail.
--claude Force install for Claude Code (~/.claude/skills/).
--codex Force install for Codex CLI (~/.agents/skills/).
--opencode Force install for OpenCode (~/.config/opencode/skills/).
Combine host flags to force multiple targets.
--local Per-project install for Claude Code (./.claude/skills/) and
OpenCode (./.opencode/skills/). Codex remains global.
Dependency handling (Phase 0, runs before symlink phase):
(default) Check deps; prompt for each missing one.
--yes-deps Auto-install everything that can be installed
non-interactively (Homebrew, coreutils, Node, codex).
Items needing GUI / sudo / external download still
require user action and are flagged.
--no-deps Skip the dep check entirely; just do the symlink phase.
--check-deps-only Report dep status and exit; do not install or symlink.
--quiet Suppress non-error output.
What setup does:
Phase 0 β dependency check (skippable with --no-deps):
git, curl, bash, perl β always required (pre-installed on Mac;
Xcode CLT prompts for git on first run)
claude β required only if installing for Claude Code
codex β required only if installing for Codex CLI
opencode β required only if installing for OpenCode
Homebrew β optional gate for other Mac installs
coreutils (gtimeout) β soft; Perry's codex preflight uses timeout
if available, else runs unprotected (warning)
Node.js + npm β required for `npm install -g @openai/codex`
Phase 1 β symlink (per selected host):
For each target skills directory:
1. Symlinks perry/ β this script's source.
2. REMOVES any legacy sibling links (okr, pmo, design) left by older
installs. They are lanes inside /perry now, not host commands.
Re-running is safe and idempotent.
USAGE
exit 0 ;;
*) echo "Unknown flag: $1 (try --help)" >&2; exit 1 ;;
esac
done
log() { [ "$QUIET" -eq 1 ] || echo "$@"; }
warn() { echo "$@" >&2; }
fail() { echo "$@" >&2; exit 1; }
# βββ OS detection βββββββββββββββββββββββββββββββββββββββββββββ
OS="$(uname -s)"
IS_MAC=0; IS_LINUX=0
case "$OS" in
Darwin*) IS_MAC=1 ;;
Linux*) IS_LINUX=1 ;;
esac
dep_present() { command -v "$1" >/dev/null 2>&1; }
# βββ TTY / agent-context detection ββββββββββββββββββββββββββββ
# When invoked by Claude Code / Codex CLI / any agent's Bash tool, stdin
# is typically NOT a TTY. Interactive Y/N prompts (`read -r`) would hang
# or return EOF, leaving the user with no idea what was skipped. Detect
# this and switch DEPS_MODE from "prompt" to a non-blocking variant
# ("auto-skip") that still runs the dep CHECK but never tries to install
# or read from stdin.
IS_INTERACTIVE=1
if [ ! -t 0 ]; then IS_INTERACTIVE=0; fi
if [ "$IS_INTERACTIVE" -eq 0 ] && [ "$DEPS_MODE" = "prompt" ]; then
DEPS_MODE="auto-skip" # internal: check + report, skip all installs
fi
# Tracks items skipped in auto-skip mode so we can surface a clear "next
# steps" summary at the end of Phase 0 (the agent can relay these to the
# user as TODOs).
DEPS_SKIPPED_INSTALL=()
# βββ Host selection: which hosts are we installing for? βββββββ
INSTALL_CLAUDE=0
INSTALL_CODEX=0
INSTALL_OPENCODE=0
if [ "$ANY_EXPLICIT" -eq 1 ]; then
# User specified at least one host flag β honor that exact set.
INSTALL_CLAUDE=$EXPLICIT_CLAUDE
INSTALL_CODEX=$EXPLICIT_CODEX
INSTALL_OPENCODE=$EXPLICIT_OPENCODE
else
# Auto-detect from PATH
dep_present claude && INSTALL_CLAUDE=1 || true
dep_present codex && INSTALL_CODEX=1 || true
dep_present opencode && INSTALL_OPENCODE=1 || true
fi
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then
warn ""
warn "β No host detected and no --claude/--codex/--opencode flag given."
warn ""
warn "Perry installs as one skill for Claude Code, Codex CLI, or OpenCode."
warn "Detection looks for the 'claude', 'codex', or 'opencode' binaries in PATH."
warn ""
warn "Options:"
warn " 1. Install Claude Code from https://claude.com/download and re-run setup."
warn " 2. Install Codex CLI: npm install -g @openai/codex (needs Node.js)"
warn " 3. Run with explicit flag if a host is installed but not in PATH:"
warn " ~/perry/setup --claude"
warn " ~/perry/setup --codex"
warn " ~/perry/setup --claude --codex"
warn " ~/perry/setup --opencode"
exit 1
fi
# βββ Phase 0: dependency check ββββββββββββββββββββββββββββββββ
DEPS_MISSING_REQUIRED=()
DEPS_MISSING_SOFT=()
DEPS_MISSING_OPTIONAL=()
confirm() {
case "$DEPS_MODE" in
yes) return 0 ;;
no|check-only|auto-skip) return 1 ;;
esac
# Interactive prompt (TTY only β auto-skip handles non-TTY already)
local prompt="$1"
local reply
printf "%s [Y/n] " "$prompt"
read -r reply || return 1
case "$reply" in
y|Y|yes|YES|"") return 0 ;;
*) return 1 ;;
esac
}
check_deps() {
log ""
log "ββ Phase 0: dependency check βββββββββββββββββββββββββββββ"
log " Installing for: $(
[ "$INSTALL_CLAUDE" -eq 1 ] && printf "claude-code "
[ "$INSTALL_CODEX" -eq 1 ] && printf "codex-cli"
[ "$INSTALL_OPENCODE" -eq 1 ] && printf " opencode"
)"
if [ "$IS_INTERACTIVE" -eq 0 ]; then
log " βΉοΈ Non-interactive stdin (likely run by an agent's Bash tool)."
log " Mode: $DEPS_MODE β will check + report, but skip any install that would prompt for input or sudo."
log " To install missing deps automatically, re-run with --yes-deps (note: Homebrew install still needs sudo)."
fi
# --- Always required ---
for c in bash curl perl; do
if dep_present "$c"; then
log " β
$c ($(command -v "$c"))"
else
log " β $c (REQUIRED, missing β unexpected on macOS / common Linux)"
DEPS_MISSING_REQUIRED+=("$c")
fi
done
# --- git ---
if dep_present git && git --version >/dev/null 2>&1; then
log " β
git ($(command -v git))"
else
if [ "$IS_MAC" -eq 1 ]; then
log " β git (REQUIRED. macOS: install Xcode Command Line Tools)"
log " β run: xcode-select --install (GUI prompt, ~5β10 min)"
else
log " β git (REQUIRED. install via your package manager)"
fi
DEPS_MISSING_REQUIRED+=("git")
fi
# --- claude (only required if installing Claude host) ---
if [ "$INSTALL_CLAUDE" -eq 1 ]; then
if dep_present claude; then
log " β
claude ($(command -v claude))"
else
log " β claude (REQUIRED for Claude Code install target)"
log " β install from https://claude.com/download"
log " (Or drop --claude / pass --codex only if you don't use Claude Code.)"
DEPS_MISSING_REQUIRED+=("claude")
fi
else
if dep_present claude; then
log " βΉοΈ claude (in PATH, but install target is Codex-only β skipping Claude install)"
fi
fi
# --- codex (required if installing Codex host; otherwise optional executor) ---
if [ "$INSTALL_CODEX" -eq 1 ]; then
if dep_present codex; then
log " β
codex ($(command -v codex) β $(codex --version 2>/dev/null | head -1))"
else
log " β codex (REQUIRED for Codex CLI install target)"
log " β install: npm install -g @openai/codex (needs Node.js)"
DEPS_MISSING_REQUIRED+=("codex")
fi
else
if dep_present codex; then
log " β
codex ($(command -v codex) β $(codex --version 2>/dev/null | head -1)) β available as executor"
else
log " βΉοΈ codex (optional β only if you use codex executor in /perry work dispatch)"
DEPS_MISSING_OPTIONAL+=("codex")
fi
fi
# --- opencode (only required if installing OpenCode host) ---
if [ "$INSTALL_OPENCODE" -eq 1 ]; then
if dep_present opencode; then
log " β
opencode ($(command -v opencode))"
else
log " β opencode (REQUIRED for OpenCode install target)"
DEPS_MISSING_REQUIRED+=("opencode")
fi
fi
# --- Homebrew (Mac only) ---
if [ "$IS_MAC" -eq 1 ]; then
if dep_present brew; then
log " β
brew ($(command -v brew))"
else
log " β οΈ brew (Homebrew not installed β gate for coreutils / node)"
DEPS_MISSING_OPTIONAL+=("brew")
fi
fi
# --- timeout / gtimeout (soft) ---
if dep_present timeout || dep_present gtimeout; then
local which_timeout
which_timeout="$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null)"
log " β
timeout ($which_timeout)"
else
log " β οΈ timeout (soft β perry-codex-preflight degrades gracefully without it)"
log " (Mac: brew install coreutils β installs gtimeout)"
DEPS_MISSING_SOFT+=("coreutils")
fi
# --- Node.js (optional, for codex install path) ---
if dep_present node; then
log " β
node ($(command -v node) β $(node --version 2>/dev/null))"
else
log " βΉοΈ node (optional β needed only for codex install / executor)"
DEPS_MISSING_OPTIONAL+=("node")
fi
log ""
# --- Decision summary ---
if [ ${#DEPS_MISSING_REQUIRED[@]} -gt 0 ]; then
log "Required missing: ${DEPS_MISSING_REQUIRED[*]}"
fi
if [ ${#DEPS_MISSING_SOFT[@]} -gt 0 ]; then
log "Soft missing : ${DEPS_MISSING_SOFT[*]} (Perry still works)"
fi
if [ ${#DEPS_MISSING_OPTIONAL[@]} -gt 0 ]; then
log "Optional missing: ${DEPS_MISSING_OPTIONAL[*]}"
fi
if [ "$DEPS_MODE" = "check-only" ]; then
log ""
log "β --check-deps-only: not installing anything; not symlinking. Exit."
exit 0
fi
}
# βββ Installer helpers ββββββββββββββββββββββββββββββββββββββββ
install_homebrew() {
[ "$IS_MAC" -eq 0 ] && return 0
dep_present brew && return 0
log ""
log "π¦ Homebrew installer (needs sudo password during install):"
log " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
if confirm " Install Homebrew now?"; then
# NONINTERACTIVE=1 skips "press RETURN to continue" but does NOT skip
# sudo. On a fresh Mac, the sudo prompt will still appear (interactive
# only). When this script is invoked by an agent's Bash tool (no TTY),
# confirm() returns 1 in auto-skip mode and we never reach this line.
if [ "$DEPS_MODE" = "yes" ]; then
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
else
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
if [ -x /opt/homebrew/bin/brew ]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [ -x /usr/local/bin/brew ]; then
eval "$(/usr/local/bin/brew shellenv)"
fi
if ! dep_present brew; then
warn " Homebrew installed but not on PATH. Re-open the terminal or re-run setup."
return 1
fi
else
log " Skipped Homebrew install."
DEPS_SKIPPED_INSTALL+=("Homebrew: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"")
return 1
fi
}
install_coreutils() {
dep_present timeout && return 0
dep_present gtimeout && return 0
if ! dep_present brew; then
log " (coreutils needs Homebrew β skipping; you can install later: brew install coreutils)"
DEPS_SKIPPED_INSTALL+=("coreutils (needs Homebrew first): brew install coreutils")
return 0
fi
log ""
log "π¦ coreutils (provides gtimeout):"
if confirm " brew install coreutils now?"; then
brew install coreutils
else
DEPS_SKIPPED_INSTALL+=("coreutils: brew install coreutils")
fi
}
install_node() {
dep_present node && return 0
if ! dep_present brew; then
log " (Node.js needs Homebrew or a separate install β skipping)"
DEPS_SKIPPED_INSTALL+=("Node.js (needs Homebrew first): brew install node")
return 0
fi
log ""
log "π¦ Node.js (needed only for codex CLI):"
if confirm " brew install node now?"; then
brew install node
else
DEPS_SKIPPED_INSTALL+=("Node.js: brew install node")
fi
}
install_codex_cli() {
dep_present codex && return 0
if ! dep_present npm; then
log " (codex needs npm β skipping; install Node.js first)"
DEPS_SKIPPED_INSTALL+=("codex CLI (needs Node.js first): npm install -g @openai/codex")
return 0
fi
log ""
log "π¦ codex CLI (only needed for codex executor in /perry work dispatch):"
if confirm " npm install -g @openai/codex now?"; then
npm install -g @openai/codex
else
DEPS_SKIPPED_INSTALL+=("codex CLI: npm install -g @openai/codex")
fi
}
# βββ Run Phase 0 ββββββββββββββββββββββββββββββββββββββββββββββ
if [ "$DEPS_MODE" != "no" ]; then
check_deps
if [ ${#DEPS_MISSING_REQUIRED[@]} -gt 0 ]; then
warn ""
warn "β Required dependencies missing: ${DEPS_MISSING_REQUIRED[*]}"
for d in "${DEPS_MISSING_REQUIRED[@]}"; do
case "$d" in
git)
[ "$IS_MAC" -eq 1 ] \
&& warn " Run: xcode-select --install (then re-run this setup)" \
|| warn " Install git via your distro's package manager."
;;
claude)
warn " Download Claude Code: https://claude.com/download"
warn " (Or drop --claude / pass --codex only if you don't use Claude Code.)"
;;
codex)
warn " Install codex: npm install -g @openai/codex (needs Node.js)"
warn " (Or drop --codex / pass --claude only if you don't use Codex CLI.)"
;;
opencode)
warn " Install OpenCode, or drop --opencode and select another host."
;;
*)
warn " $d is missing β install before re-running setup."
;;
esac
done
warn ""
warn "Re-run setup after installing, or pass --no-deps to skip the dep check (not recommended)."
exit 1
fi
# Soft + optional installs (Mac auto-installs via brew)
if [ "$IS_MAC" -eq 1 ]; then
install_homebrew || true
install_coreutils || true
# Only offer Node + codex CLI if user is installing Codex host OR explicitly
# told us about codex via --yes-deps. Don't push it on Claude-only users.
if [ "$INSTALL_CODEX" -eq 1 ] || [ "$DEPS_MODE" = "yes" ]; then
install_node || true
install_codex_cli || true
fi
else
for d in "${DEPS_MISSING_SOFT[@]}" "${DEPS_MISSING_OPTIONAL[@]}"; do
log " (Linux: install $d via your package manager if you need it)"
done
fi
# Surface any skipped install actions as a clear "next steps" block.
# The agent calling setup can relay this to the user as TODOs.
if [ ${#DEPS_SKIPPED_INSTALL[@]} -gt 0 ]; then
log ""
log "π Skipped installs β run these manually if you want the soft/optional deps:"
for item in "${DEPS_SKIPPED_INSTALL[@]}"; do
log " β’ $item"
done
if [ "$IS_INTERACTIVE" -eq 0 ]; then
log ""
log " (Setup is in auto-skip mode because stdin isn't a TTY. To auto-install"
log " everything possible, ask the user to re-run setup interactively, or run with --yes-deps.)"
fi
fi
log ""
log "ββ Phase 0 done ββββββββββββββββββββββββββββββββββββββββββ"
fi
# βββ Phase 1: install / symlink βββββββββββββββββββββββββββββββ
install_at() {
local host_dir="$1"
local label="$2"
mkdir -p "$host_dir"
local perry_target="$host_dir/perry"
if [ "$SETUP_DIR" = "$perry_target" ]; then
log " [$label] perry/ already lives at $perry_target (running from there)"
elif [ -L "$perry_target" ]; then
local resolved="$(cd "$perry_target" && pwd -P)"
if [ "$resolved" = "$SOURCE_DIR" ]; then
log " [$label] perry/ symlink already points at $SOURCE_DIR"
else
warn " [$label] perry/ symlink points at $resolved"
warn " [$label] this script lives at $SOURCE_DIR β leaving existing link in place."
warn " [$label] rm \"$perry_target\" and re-run if you want to repoint."
return 1
fi
elif [ -d "$perry_target" ]; then
warn " [$label] perry/ exists at $perry_target (not a symlink) β leaving in place."
warn " [$label] Sync new content with: rsync -a --delete \"$SOURCE_DIR/\" \"$perry_target/\""
return 1
elif [ -e "$perry_target" ]; then
warn " [$label] $perry_target exists and is not a directory β refusing to touch."
return 1
else
log " [$label] linking perry/ -> $SOURCE_DIR"
ln -snf "$SOURCE_DIR" "$perry_target"
fi
if [ ! -d "$perry_target" ]; then
warn " [$label] perry directory not found at $perry_target β aborting child link"
return 1
fi
# Perry registers ONE skill: perry. The children (okr, pmo, design) are
# lanes inside it, loaded on demand by the router β not host commands.
#
# They used to be symlinked as siblings so `/okr`, `/pmo` and `/design`
# worked directly. That was withdrawn because the names are generic and the
# host's skill namespace is shared: `design` collides with design-review,
# design-consultation, design-html, design-shotgun and a whole `design:`
# plugin family; `okr` collides with lark-okr. Claiming a common English
# word in a shared namespace is the tool taking something it was not given β
# the same rule the state root already follows. Users reached Perry through
# `/perry` and let it route anyway, so the siblings cost a namespace and
# bought nothing.
return 0
}
log ""
log "ββ Phase 1: skill symlink ββββββββββββββββββββββββββββββββ"
INSTALL_FAILURES=0
# Claude Code install
CC_DIR=""
# Legacy sibling links, swept once from every directory Perry could have
# installed into β including the --local one, which the per-host cleanup never
# reached.
#
# The list is HISTORICAL and therefore literal. Deriving it from the current
# `$PERRY_HOME/*/` would make the loop look for links named after the lanes as
# they are named TODAY, which no installer ever created, while the real
# leftovers on an upgrading machine (`~/.claude/skills/okr` and friends) became
# invisible. A cleanup routine that reads current state cannot remove what
# current state forgot. `goals work decide` are in the list not because any
# installer made them, but because a future rename would otherwise inherit the
# same blind spot β the list is the record of every name Perry has ever put in
# a host's namespace.
#
# One function, one list, called once per directory: an earlier version had a
# second copy inside install_at, byte-identical and unreachable on any
# non---local run, so its user-facing message was dead code and the two arrays
# were a silent two-copy invariant.
PERRY_LEGACY_CHILDREN=(okr pmo design goals work decide)
sweep_legacy_links() {
local dirs=("$HOME/.claude/skills" "$HOME/.agents/skills" "$HOME/.config/opencode/skills")
[ "$LOCAL" -eq 1 ] && dirs+=("$(pwd)/.claude/skills" "$(pwd)/.opencode/skills")
local removed=() d name
for d in "${dirs[@]}"; do
[ -d "$d" ] || continue
for name in "${PERRY_LEGACY_CHILDREN[@]}"; do
# Only ever remove a link this installer created. Anything else at that
# path belongs to someone else.
if [ -L "$d/$name" ] && [ "$(readlink "$d/$name")" = "perry/$name" ]; then
rm -f "$d/$name"
removed+=("$d/$name")
fi
done
done
if [ ${#removed[@]} -gt 0 ]; then
log " removed legacy child skill links: ${removed[*]}"
log " these are lanes inside /perry now β use '/perry <lane> <subcommand>'"
fi
}
sweep_legacy_links
if [ "$INSTALL_CLAUDE" -eq 1 ]; then
if [ "$LOCAL" -eq 1 ]; then
CC_DIR="$(pwd)/.claude/skills"
else
CC_DIR="$HOME/.claude/skills"
fi
install_at "$CC_DIR" "claude-code" || { warn " [claude-code] install step failed; continuing with other hosts."; INSTALL_FAILURES=$((INSTALL_FAILURES + 1)); }
fi
# Codex install
CODEX_DIR=""
if [ "$INSTALL_CODEX" -eq 1 ]; then
if [ "$LOCAL" -eq 1 ]; then
log ""
log " Note: --local does not apply to Codex; its install stays global at ~/.agents/skills/"
fi
CODEX_DIR="$HOME/.agents/skills"
install_at "$CODEX_DIR" "codex" || { warn " [codex] install step failed; continuing with other hosts."; INSTALL_FAILURES=$((INSTALL_FAILURES + 1)); }
if [ -z "${PERRY_HOME:-}" ] || [ -z "${PERRY_HOST:-}" ]; then
log ""
log " [codex] Recommended shell exports (so SKILL.md \$PERRY_HOME refs are unambiguous):"
[ -z "${PERRY_HOME:-}" ] && log " export PERRY_HOME=\"$CODEX_DIR/perry\"" || true
[ -z "${PERRY_HOST:-}" ] && log " export PERRY_HOST=codex-cli" || true
fi
fi
# OpenCode install
OPENCODE_DIR=""
if [ "$INSTALL_OPENCODE" -eq 1 ]; then
if [ "$LOCAL" -eq 1 ]; then
OPENCODE_DIR="$(pwd)/.opencode/skills"
else
OPENCODE_DIR="$HOME/.config/opencode/skills"
fi
install_at "$OPENCODE_DIR" "opencode" || { warn " [opencode] install step failed; continuing with other hosts."; INSTALL_FAILURES=$((INSTALL_FAILURES + 1)); }
fi
# βββ Summary ββββββββββββββββββββββββββββββββββββββββββββββββββ
log ""
if [ "$INSTALL_FAILURES" -gt 0 ]; then
warn ""
warn "Perry setup incomplete: $INSTALL_FAILURES selected host install(s) failed."
else
log "Perry ready."
fi
log " source : $SOURCE_DIR"
[ "$INSTALL_CLAUDE" -eq 1 ] && log " claude-code : $CC_DIR" || true
[ "$INSTALL_CODEX" -eq 1 ] && log " codex : $CODEX_DIR" || true
[ "$INSTALL_OPENCODE" -eq 1 ] && log " opencode : $OPENCODE_DIR" || true
log " skill : /perry (one entrance; goals / work / decide are lanes inside it)"
log " lanes : /perry goals β¦ Β· /perry work β¦ Β· /perry decide β¦ Β· /perry diagnose"
log ""
[ "$INSTALL_CLAUDE" -eq 1 ] && log " Claude Code: invoke /perry inside a project for the combined dashboard." || true
[ "$INSTALL_CODEX" -eq 1 ] && log " Codex CLI : invoke /skills, then pick perry β or type \$perry to mention." || true
[ "$INSTALL_OPENCODE" -eq 1 ] && log " OpenCode : invoke the perry skill; lanes remain arguments to that one skill." || true
if [ "$INSTALL_FAILURES" -gt 0 ]; then exit 1; fi
exit 0