-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·391 lines (357 loc) · 13.7 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·391 lines (357 loc) · 13.7 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
#!/usr/bin/env bash
# /onboard universal installer — works without Claude Code's /plugin command.
#
# Usage:
# curl -sSL https://raw.githubusercontent.com/sdsrss/onboard/main/install.sh | bash
# curl -sSL .../install.sh | bash -s -- update
# curl -sSL .../install.sh | bash -s -- uninstall
# curl -sSL .../install.sh | bash -s -- doctor
#
# Local invocation:
# ./install.sh [install|update|uninstall|doctor]
#
# Environment:
# ONBOARD_REPO git URL (default: https://github.com/sdsrss/onboard.git)
# ONBOARD_BRANCH branch/tag (default: main)
# ONBOARD_TARGET user | project (default: user)
# ONBOARD_ALLOW_DIRTY 1 to skip dirty-tree check on update
# ONBOARD_NO_OVERWRITE 1 to refuse install if INSTALL_DIR exists (v2.11+: default overwrite)
# ONBOARD_CONFIRM_UNINSTALL set to 'yes' for non-interactive uninstall
# ONBOARD_UNINSTALL_MODE 'skill' | 'all' (default 'all'; documents caller intent, v2.11+)
#
# v2.10 layout note:
# The repo organizes the skill at skills/onboard/ (Claude Code plugin convention).
# install.sh copies skills/onboard/<contents> into the install target so the
# target ends up shaped like a standalone skill: <target>/SKILL.md, <target>/hooks/.
# Source is kept at ~/.claude/.cache/onboard-source/ for fast updates.
set -euo pipefail
REPO="${ONBOARD_REPO:-https://github.com/sdsrss/onboard.git}"
BRANCH="${ONBOARD_BRANCH:-main}"
TARGET="${ONBOARD_TARGET:-user}"
ACTION="${1:-install}"
case "$TARGET" in
user)
INSTALL_DIR="$HOME/.claude/skills/onboard"
STAGE_DIR="$HOME/.claude/.cache/onboard-source"
;;
project)
: "${CLAUDE_PROJECT_DIR:=$(pwd)}"
INSTALL_DIR="$CLAUDE_PROJECT_DIR/.claude/skills/onboard"
STAGE_DIR="$CLAUDE_PROJECT_DIR/.claude/.cache/onboard-source"
;;
*)
echo "ERROR: ONBOARD_TARGET must be 'user' or 'project' (got '$TARGET')" >&2
exit 2
;;
esac
c() { printf '\033[%sm%s\033[0m' "$1" "$2"; }
info() { echo "$(c '1;34' '[onboard]') $*"; }
warn() { echo "$(c '1;33' '[onboard]') $*" >&2; }
err() { echo "$(c '1;31' '[onboard]') $*" >&2; }
ok() { echo "$(c '1;32' '[onboard]') $*"; }
require_cmd() {
command -v "$1" >/dev/null 2>&1 || { err "missing required command: $1"; exit 1; }
}
# When invoked via `curl ... | bash -s -- <action>`, $0 expands to "bash" and
# hints like "$0 update" become literally "bash update" — unrunnable. self_cmd
# returns "$0" when running from a real file, else the canonical curl recipe so
# the user can copy-paste the hint verbatim.
self_cmd() {
if [ -f "$0" ]; then
printf '%s' "$0"
else
printf 'curl -sSL https://raw.githubusercontent.com/sdsrss/onboard/main/install.sh | bash -s --'
fi
}
check_deps() {
require_cmd git
require_cmd bash
if ! command -v jq >/dev/null 2>&1; then
err "jq not found on PATH — all 4 guard hooks shell out to jq for input parsing"
err " and JSON deny-output. Without jq they exit 127 and PreToolUse silently"
err " allows everything (no permissionDecision payload), defeating guard-bash"
err " + guard-edit. Refusing install to fail-loud instead of installing-broken."
err ""
err "install with one of:"
err " brew install jq # macOS"
err " apt install jq # Debian / Ubuntu"
err " dnf install jq # Fedora / RHEL"
err " pacman -S jq # Arch"
exit 1
fi
}
current_version() {
local skill="$INSTALL_DIR/SKILL.md"
[ -f "$skill" ] || { echo "none"; return; }
grep -E "^# /onboard.*\(v[0-9]" "$skill" 2>/dev/null \
| head -1 \
| sed -E 's/.*\(v([0-9.]+).*/\1/' \
|| echo "unknown"
}
deploy_skill_from_stage() {
if [ ! -d "$STAGE_DIR/skills/onboard" ]; then
err "stage missing skills/onboard/ — repo layout invalid"
exit 1
fi
rm -rf "$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
cp -a "$STAGE_DIR/skills/onboard/." "$INSTALL_DIR/"
chmod +x "$INSTALL_DIR/hooks/"*.sh 2>/dev/null || true
chmod +x "$INSTALL_DIR/scripts/"*.sh 2>/dev/null || true
for f in README.md CHANGELOG.md LICENSE; do
[ -f "$STAGE_DIR/$f" ] && cp "$STAGE_DIR/$f" "$INSTALL_DIR/" 2>/dev/null || true
done
}
do_install() {
check_deps
info "target: $TARGET → $INSTALL_DIR"
info "stage: $STAGE_DIR"
if [ -d "$INSTALL_DIR" ]; then
local v
v=$(current_version)
if [ "${ONBOARD_NO_OVERWRITE:-0}" = "1" ]; then
warn "/onboard already installed (v$v) at $INSTALL_DIR — refusing to overwrite (ONBOARD_NO_OVERWRITE=1)"
local self
self=$(self_cmd)
warn "to refresh: $self update"
warn "to reinstall clean: $self uninstall && $self install"
exit 0
fi
info "/onboard v$v already at $INSTALL_DIR — overwriting (set ONBOARD_NO_OVERWRITE=1 to refuse)"
case "$INSTALL_DIR" in
""|/|/.|/..) err "refusing rm -rf on suspect path: '$INSTALL_DIR'"; exit 2 ;;
esac
rm -rf "$INSTALL_DIR"
fi
rm -rf "$STAGE_DIR"
mkdir -p "$(dirname "$STAGE_DIR")"
info "cloning $REPO @ $BRANCH into stage"
if ! git clone --depth 1 --branch "$BRANCH" "$REPO" "$STAGE_DIR" >/dev/null 2>&1; then
err "git clone failed. Check ONBOARD_REPO / ONBOARD_BRANCH and network."
exit 1
fi
deploy_skill_from_stage
local v
v=$(current_version)
ok "installed /onboard v$v"
echo ""
echo "Next:"
echo " • In any git project, run: /onboard"
echo " • Default mode is --local-only (zero team pollution)"
echo " • Full docs: $INSTALL_DIR/README.md"
}
do_update() {
check_deps
if [ ! -d "$INSTALL_DIR" ]; then
err "not installed at $INSTALL_DIR"
err "run first: $(self_cmd) install"
exit 1
fi
local old_v
old_v=$(current_version)
info "updating from v$old_v"
if [ ! -d "$STAGE_DIR/.git" ]; then
warn "stage cache missing at $STAGE_DIR — re-cloning"
rm -rf "$STAGE_DIR"
mkdir -p "$(dirname "$STAGE_DIR")"
if ! git clone --depth 1 --branch "$BRANCH" "$REPO" "$STAGE_DIR" >/dev/null 2>&1; then
err "git clone failed"
exit 1
fi
else
pushd "$STAGE_DIR" >/dev/null
if [ "${ONBOARD_ALLOW_DIRTY:-0}" != "1" ]; then
if ! git diff --quiet HEAD 2>/dev/null || ! git diff --cached --quiet HEAD 2>/dev/null; then
popd >/dev/null
err "uncommitted local changes in stage $STAGE_DIR"
err "either commit/stash them, or rerun with ONBOARD_ALLOW_DIRTY=1 to overwrite"
exit 1
fi
fi
# Iron Law 14 (SKILL.md:58) forbids `git reset --hard` on PROJECT files.
# STAGE_DIR is a throwaway clone under ~/.claude/.cache/, NOT a PROJECT —
# the user-global cache is by design owned by this script. Hard reset is
# the correct operation here; the rule doesn't reach into installer state.
git fetch --depth 1 origin "$BRANCH" >/dev/null 2>&1
git reset --hard "origin/$BRANCH" >/dev/null 2>&1
# reset --hard ignores untracked files; without this clean a stray file at
# $STAGE_DIR/skills/onboard/<name> leaks into INSTALL_DIR via the cp -a in
# deploy_skill_from_stage and persists across updates.
git clean -fdx >/dev/null 2>&1
popd >/dev/null
fi
deploy_skill_from_stage
local new_v
new_v=$(current_version)
ok "updated /onboard ($old_v → $new_v)"
echo ""
echo "Per-project state files in projects you've onboarded are unchanged."
echo "To align a project with the new spec version:"
echo " cd <project> && /onboard --update"
}
do_uninstall() {
local mode="${ONBOARD_UNINSTALL_MODE:-all}"
case "$mode" in
skill) info "uninstall mode: skill (per-project config preserved by caller)" ;;
all) info "uninstall mode: all (default; per-project cleanup is caller's job — see /onboard --uninstall=all)" ;;
*) err "ONBOARD_UNINSTALL_MODE must be 'skill' or 'all' (got '$mode')"; exit 2 ;;
esac
local mirror_dir="$HOME/.claude/onboard-runtime/hooks"
if [ ! -d "$INSTALL_DIR" ] && [ ! -d "$STAGE_DIR" ] && [ ! -d "$mirror_dir" ]; then
warn "/onboard not installed at $INSTALL_DIR — nothing to remove"
exit 0
fi
info "uninstall plan"
[ -d "$INSTALL_DIR" ] && echo " • Will remove: $INSTALL_DIR (skill files)"
[ -d "$STAGE_DIR" ] && echo " • Will remove: $STAGE_DIR (source cache)"
[ -d "$mirror_dir" ] && echo " • Will remove: $mirror_dir (plugin-mode hook mirror)"
echo ""
warn "What this global uninstall does NOT do:"
warn " • Per-project state (.claude/onboarding-state.json or .claude/local-only/) is NOT removed"
warn " • Per-project hook entries in settings.json / settings.local.json are NOT removed"
warn " • CLAUDE.md / CLAUDE.local.md content written by onboard is NOT removed"
warn " • .gitignore / .git/info/exclude lines written by onboard are NOT removed"
echo ""
warn "For per-project cleanup, run inside each project BEFORE this uninstall:"
warn " cd <project> && /onboard --uninstall=all # full cleanup"
warn " cd <project> && /onboard --uninstall=skill # keep project config, drop user-global only"
echo ""
if [ -t 0 ]; then
read -r -p "Proceed with global uninstall? [y/N] " ans
else
ans="${ONBOARD_CONFIRM_UNINSTALL:-n}"
case "$ans" in
y|Y|yes|YES) ;;
*)
warn "non-interactive stdin (no TTY) — re-run with ONBOARD_CONFIRM_UNINSTALL=yes to proceed"
;;
esac
fi
case "$ans" in
y|Y|yes|YES)
# §8 SAFETY: refuse to rm on empty or root-ish paths even though set -u +
# the case-construct above already make these unreachable in practice.
# Explicit guard makes the dangerous call auditable on its own line.
for victim in "$INSTALL_DIR" "$STAGE_DIR" "$mirror_dir"; do
case "$victim" in
""|/|/.|/..) err "refusing rm -rf on suspect path: '$victim'"; exit 2 ;;
esac
done
local removed_skill=0
if [ -d "$INSTALL_DIR" ]; then
rm -rf "$INSTALL_DIR"
removed_skill=1
fi
[ -d "$STAGE_DIR" ] && rm -rf "$STAGE_DIR"
if [ -d "$mirror_dir" ]; then
rm -rf "$mirror_dir"
rmdir "$HOME/.claude/onboard-runtime" 2>/dev/null || true
ok "mirror directory removed: $mirror_dir"
fi
if [ "$removed_skill" -eq 1 ]; then
ok "/onboard global skill files removed"
else
ok "/onboard cleanup complete (no skill install dir was present)"
fi
echo ""
echo "If you forgot per-project cleanup, manually clean each project:"
echo ""
echo " # local-only mode projects"
echo " rm -rf .claude/local-only"
echo " # remove between '# >>> /onboard' and '# <<< /onboard' markers in:"
echo " # .git/info/exclude"
echo ""
echo " # share mode projects"
echo " # remove between markers in:"
echo " # .gitignore"
echo " # CLAUDE.md"
echo " # remove entries with \"_onboard_managed\": true from:"
echo " # .claude/settings.json"
;;
*)
info "aborted"
exit 0
;;
esac
}
do_doctor() {
echo "── /onboard installer doctor ──"
echo ""
echo "Global skill install:"
if [ -d "$INSTALL_DIR" ]; then
local v
v=$(current_version)
echo " ✓ installed v$v"
echo " $INSTALL_DIR"
local exec_count=0
if [ -d "$INSTALL_DIR/hooks" ]; then
exec_count=$(find "$INSTALL_DIR"/hooks -name "*.sh" -perm -u+x 2>/dev/null | wc -l | tr -d ' ')
fi
echo " hooks exec $exec_count/4"
local script_count=0
if [ -d "$INSTALL_DIR/scripts" ]; then
script_count=$(find "$INSTALL_DIR"/scripts -name "*.sh" -perm -u+x 2>/dev/null | wc -l | tr -d ' ')
fi
if [ "$script_count" -ge 1 ]; then
echo " scripts exec $script_count/1 (mirror-hooks.sh, v2.10.1+)"
else
echo " scripts exec 0/1 — mirror-hooks.sh missing or not +x (re-run $(self_cmd) update)"
fi
else
echo " ✗ not installed at $INSTALL_DIR"
echo " install with: $(self_cmd) install"
fi
echo " stage cache $([ -d "$STAGE_DIR" ] && echo "✓ $STAGE_DIR" || echo "✗ missing (will be re-cloned on update)")"
echo ""
echo "Required commands on PATH:"
for cmd in git bash jq; do
if command -v "$cmd" >/dev/null 2>&1; then
echo " ✓ $cmd ($(command -v "$cmd"))"
else
echo " ✗ $cmd missing"
fi
done
echo ""
echo "Optional commands (Phase 0 detection):"
for cmd in gh glab tea make gtimeout mise asdf; do
if command -v "$cmd" >/dev/null 2>&1; then
echo " ✓ $cmd"
else
echo " - $cmd (not installed)"
fi
done
echo ""
echo "For per-project diagnosis: cd <project> && /onboard --doctor"
}
case "$ACTION" in
install) do_install ;;
update) do_update ;;
uninstall) do_uninstall ;;
doctor) do_doctor ;;
-h|--help|help)
cat <<'HELP'
Usage: install.sh [install|update|uninstall|doctor]
Actions:
install Clone the skill into ~/.claude/skills/onboard (default)
update Pull latest changes from origin (refuses on dirty stage tree)
uninstall Remove the global skill install + source cache (asks for confirmation)
doctor Diagnose installer state + dependency availability
help Show this message
Environment overrides:
ONBOARD_REPO git URL of the skill repository
ONBOARD_BRANCH branch or tag to install/update (default: main)
ONBOARD_TARGET 'user' (default, ~/.claude/skills) or 'project' (./.claude/skills)
ONBOARD_ALLOW_DIRTY 1 to skip dirty-tree check on update
ONBOARD_NO_OVERWRITE 1 to refuse install if INSTALL_DIR exists (v2.11+: default overwrite)
ONBOARD_CONFIRM_UNINSTALL set to 'yes' for non-interactive uninstall
ONBOARD_UNINSTALL_MODE 'skill' or 'all' (default 'all'; documents caller intent, v2.11+)
Per-project cleanup is a separate operation (do BEFORE global uninstall):
cd <project> && /onboard --uninstall=all
HELP
;;
*)
err "unknown action: $ACTION"
err "valid: install | update | uninstall | doctor | help"
exit 2
;;
esac