-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
171 lines (164 loc) · 8.4 KB
/
install.sh
File metadata and controls
171 lines (164 loc) · 8.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
#!/usr/bin/env bash
# install.sh — install the engine ONCE per host (#54, PRD #52). The engine (all code/assets) lives
# at a single host location, $HARNESS_HOME/engine, and `harness` is symlinked onto PATH. Projects
# never get their own engine copy; per-project config + state live in <project>/.harness/ (created
# by `harness init`). `harness update` ff-pulls only this one shared install.
set -uo pipefail
HARNESS_REPO_URL="${HARNESS_REPO_URL:-https://github.com/VocanicZ/Harness.git}"
HARNESS_HOME="${HARNESS_HOME:-$HOME/.harness}" # host root for the shared engine (+ PRD-B subdirs)
HARNESS_BIN_DIR="${HARNESS_BIN_DIR:-$HOME/.local/bin}" # where the `harness` PATH symlink is placed
HARNESS_MARKETPLACE="${HARNESS_MARKETPLACE:-anthropics/claude-plugins-official}"
HARNESS_MARKETPLACE_NAME="${HARNESS_MARKETPLACE_NAME:-claude-plugins-official}"
MATTPOCOCK_SKILLS_URL="${MATTPOCOCK_SKILLS_URL:-https://github.com/mattpocock/skills.git}"
need(){ command -v "$1" >/dev/null 2>&1; }
check_prereqs(){
local ok=1
for b in git tmux python3; do need "$b" || { echo "MISSING: $b" >&2; ok=0; }; done
if ! need gh; then echo "MISSING: gh (install: https://cli.github.com)" >&2; ok=0
elif ! gh auth status >/dev/null 2>&1; then echo "gh not authenticated — run: gh auth login" >&2; ok=0; fi
if ! need claude; then echo "MISSING: claude (Claude Code CLI)" >&2; ok=0
elif ! claude --version >/dev/null 2>&1; then echo "claude present but not runnable (model configured?)" >&2; ok=0; fi
[[ "$ok" == 1 ]]
}
_plugin_installed(){ # $1 = name@marketplace
local f="$HOME/.claude/plugins/installed_plugins.json"
[[ -f "$f" ]] && python3 -c "import json,sys
d=json.load(open(sys.argv[1]))
sys.exit(0 if sys.argv[2] in d.get('plugins',{}) else 1)" "$f" "$1" 2>/dev/null
}
_ensure_plugin(){ # $1 = plugin name
local ref="$1@$HARNESS_MARKETPLACE_NAME"
if _plugin_installed "$ref"; then echo " ✓ plugin $ref already installed"; return 0; fi
echo " installing plugin $ref ..."
claude plugin install "$ref" --scope user >/dev/null 2>&1 \
&& echo " ✓ installed $ref" \
|| echo " ! could not install $ref — install manually: claude plugin install $ref"
}
ensure_skills(){
echo "ensuring required Claude plugins + skills (best-effort) ..."
if need claude; then
claude plugin marketplace add "$HARNESS_MARKETPLACE" >/dev/null 2>&1 || true
_ensure_plugin superpowers
_ensure_plugin ralph-loop
find "$HOME/.claude/plugins/cache" -path '*/ralph-loop/*/hooks/*.sh' -exec chmod +x {} \; 2>/dev/null || true
else
echo " ! 'claude' CLI not found — install superpowers + ralph-loop plugins manually"
fi
local sk="$HOME/.claude/skills"
if [[ -d "$sk/to-prd" && -d "$sk/to-issues" ]]; then
echo " ✓ matt-pocock skills (to-prd/to-issues) already present"
else
local tmp; tmp="$(mktemp -d)"
if git clone --depth 1 "$MATTPOCOCK_SKILLS_URL" "$tmp" >/dev/null 2>&1; then
mkdir -p "$sk"; local want src
# Copy ONLY the intended skills (the ones the guard above checks) — NOT every SKILL.md parent
# dir in the clone. Copying everything would clobber a user's pre-existing same-named skill and
# pull in skills never requested. Locate each wanted skill's dir by its SKILL.md (any depth).
for want in to-prd to-issues; do
[[ -d "$sk/$want" ]] && continue # don't overwrite an existing same-named user skill
src="$(find "$tmp" -type f -name SKILL.md -path "*/$want/SKILL.md" 2>/dev/null | head -n1)"
[[ -n "$src" ]] && cp -r "$(dirname "$src")" "$sk/" 2>/dev/null || true
done
if [[ -d "$sk/to-prd" || -d "$sk/to-issues" ]]; then
echo " ✓ installed matt-pocock skills into $sk"
else
echo " ! cloned $MATTPOCOCK_SKILLS_URL but found no skills to copy — install to-prd/to-issues manually"
fi
else
echo " ! could not clone $MATTPOCOCK_SKILLS_URL — install to-prd/to-issues manually"
fi
rm -rf "$tmp"
fi
return 0
}
# place_engine — clone/place the engine at the SINGLE host location $HARNESS_HOME/engine (never into
# a project's ./.harness/). Idempotent: an existing install is fast-forwarded, not re-cloned.
place_engine(){
local home="${HARNESS_HOME:-$HOME/.harness}" dest
dest="$home/engine"
mkdir -p "$home"
if [[ -d "$dest/.git" ]]; then
echo " engine already present at $dest — fast-forwarding"
# Surface a failed ff-pull as a NON-zero return (consistent with update.sh, which exit 1s on the
# same failure) instead of swallowing it — otherwise a diverged/dirty shared engine silently
# stays stale for ALL fleets while install still reports success.
if ! git -C "$dest" pull --ff-only; then
echo " ! could not ff-pull $dest (diverged or local engine edits) — resolve there manually." >&2
return 1
fi
else
echo " installing engine to $dest ..."
git clone "$HARNESS_REPO_URL" "$dest"
fi
}
# create_host_root — establish the ~/.harness host ROOT with the poller/ and snapshots/ subdirs
# (#57, PRD #52). PRD-B (#69) populates them: the host poller writes its registry + pidfile under
# poller/ and per-repo snapshot JSON under snapshots/ (opt-in per fleet via HARNESS_USE_POLLER).
# Install just creates the dirs so the host layout is stable. Idempotent (mkdir -p is a no-op).
create_host_root(){
local home="${HARNESS_HOME:-$HOME/.harness}"
mkdir -p "$home/poller" "$home/snapshots"
}
# install_harness_skills — deploy the engine's OWN /harness operator skills to USER scope
# (~/.claude/skills) ONCE per host, not vendored per project (#57, PRD #52). Source is the shared
# engine's skill/ dir (place_engine runs first in main); the umbrella skill/SKILL.md → harness/, and
# each per-command skill/<name>/SKILL.md → <name>/. Best-effort: a missing source warns, never fails.
install_harness_skills(){
local src="${HARNESS_SKILL_SRC:-${HARNESS_HOME:-$HOME/.harness}/engine/skill}"
local dst="${HARNESS_USER_SKILLS:-$HOME/.claude/skills}"
if [[ ! -d "$src" ]]; then
echo " ! engine skill/ not found at $src — install /harness skills manually" >&2; return 0
fi
mkdir -p "$dst"
local n=0 d
if [[ -f "$src/SKILL.md" ]]; then
mkdir -p "$dst/harness"; cp "$src/SKILL.md" "$dst/harness/SKILL.md" && n=$((n+1))
fi
for d in "$src"/*/; do
[[ -f "$d/SKILL.md" ]] || continue
mkdir -p "$dst/$(basename "$d")"; cp "$d/SKILL.md" "$dst/$(basename "$d")/SKILL.md" && n=$((n+1))
done
echo " ✓ installed $n /harness skill(s) into $dst (user scope)"
}
# print_path_instructions — portability fallback when the PATH symlink can't be written: tell the
# user exactly how to put the engine's bin/ on PATH themselves.
print_path_instructions(){
local target="$1" dir; dir="$(dirname "$target")"
cat >&2 <<EOF
! could not create the 'harness' symlink (${HARNESS_BIN_DIR:-$HOME/.local/bin} not writable).
Put the engine on your PATH manually instead:
export PATH="$dir:\$PATH"
(add that line to ~/.bashrc or ~/.zshrc, then re-open your shell)
EOF
}
# link_path — symlink `harness` onto PATH ($HARNESS_BIN_DIR/harness → engine/bin/harness). ENGINE_DIR
# resolves via realpath of the entrypoint's OWN path (bin/harness), so the symlink runs the real
# engine. If the link can't be written, fall back to printing explicit PATH instructions.
link_path(){
local home="${HARNESS_HOME:-$HOME/.harness}" bindir="${HARNESS_BIN_DIR:-$HOME/.local/bin}"
local target="$home/engine/bin/harness" link="$bindir/harness"
mkdir -p "$bindir" 2>/dev/null
if ln -sfn "$target" "$link" 2>/dev/null; then
echo " linked $link -> $target"
case ":$PATH:" in
*":$bindir:"*) ;;
*) echo " NOTE: $bindir is not on your PATH yet. Add it:"
echo " export PATH=\"$bindir:\$PATH\" # add to ~/.bashrc or ~/.zshrc" ;;
esac
else
print_path_instructions "$target"
fi
}
main(){
check_prereqs || { echo "Prerequisites unmet — fix the above and re-run." >&2; exit 1; }
ensure_skills
place_engine || { echo "Engine install/update failed — see above; not finalizing." >&2; exit 1; }
create_host_root # ~/.harness/{poller,snapshots}/ — host-poller dirs (PRD-B, #69)
install_harness_skills # /harness operator skills → ~/.claude/skills (user scope, once)
link_path
cat <<EOF
Done. The engine is installed once at $HARNESS_HOME/engine and linked as 'harness'.
Next: cd into a project and run harness init (creates that project's .harness/ config + state).
EOF
}
[[ "${HARNESS_INSTALL_NOMAIN:-0}" == 1 ]] || main "$@"