Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·153 lines (129 loc) · 4.93 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·153 lines (129 loc) · 4.93 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
#!/usr/bin/env bash
#
# install.sh — link this package into place for Claude Code.
#
# Usage:
# ./install.sh
#
# Set CLAUDE_CONFIG_DIR to install somewhere other than ~/.claude.
#
set -euo pipefail
SYMPHONY_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
usage() {
cat <<'USAGE'
install.sh — put this package where Claude Code will find it.
There are two ways to install it, and you want one of them, not both.
usage: install.sh [--link | --plugin | --help]
(no argument) same as --link
--link symlink the rules, agents, scripts and commands into place,
and merge the hooks into your settings. Commands are run as
/review. Editing a file in this clone takes effect at once.
--plugin register this clone as a marketplace and install it as a
plugin. Commands are run as /symphony:review, and updates
come through claude plugin update.
--help print this and change nothing
USAGE
}
MODE="link"
case "${1:-}" in
"") MODE="link" ;;
--link) MODE="link" ;;
--plugin) MODE="plugin" ;;
-h|--help) usage; exit 0 ;;
*) printf 'unrecognised argument: %s\n' "$1" >&2; usage >&2; exit 64 ;;
esac
link_file() {
local src="$1" dest="$2"
if [ -L "$dest" ] && [ "$(readlink "$dest")" = "$src" ]; then
echo " skip $dest (already linked)"
return
fi
if [ -e "$dest" ] || [ -L "$dest" ]; then
echo " backup $dest → ${dest}.backup"
mv "$dest" "${dest}.backup"
fi
ln -s "$src" "$dest"
echo " link $dest → $src"
}
linked_here() {
[ "$(readlink "$CONFIG_DIR/rules" 2>/dev/null)" = "$SYMPHONY_DIR/rules" ]
}
plugin_installed() {
[ -f "$CONFIG_DIR/settings.json" ] &&
jq -e '(.enabledPlugins // {}) | keys[] | select(startswith("symphony@"))' \
"$CONFIG_DIR/settings.json" >/dev/null 2>&1
}
if [ "$MODE" = plugin ]; then
if linked_here; then
echo "symphony is already linked into $CONFIG_DIR." >&2
echo "Remove the rules, agents and bin links there, and its entries from" >&2
echo "settings.json, before installing the plugin — installing both" >&2
echo "registers every hook twice." >&2
exit 75
fi
command -v claude >/dev/null || { echo "the claude CLI is required to install as a plugin" >&2; exit 1; }
echo "Plugin:"
claude plugin marketplace add "$SYMPHONY_DIR"
claude plugin install symphony@symphony
echo " installed — its commands are namespaced, so a review is /symphony:review"
exit 0
fi
# Both installs at once would register the hooks twice, so each refuses while
# the other is in place rather than layering on it. This reads the settings file
# rather than asking the claude CLI, which rewrites that file as it runs.
if plugin_installed; then
echo "symphony is already installed as a plugin." >&2
echo "Remove it with 'claude plugin uninstall symphony@symphony' before linking," >&2
echo "or keep the plugin — installing both registers every hook twice." >&2
exit 75
fi
mkdir -p "$CONFIG_DIR"
echo "Rules, agents and scripts:"
for dir in rules agents bin; do
link_file "$SYMPHONY_DIR/$dir" "$CONFIG_DIR/$dir"
done
echo ""
echo "Shell commands:"
mkdir -p "$HOME/.local/bin"
link_file "$SYMPHONY_DIR/bin/up.sh" "$HOME/.local/bin/up"
echo ""
echo "Commands:"
mkdir -p "$CONFIG_DIR/commands"
for cmd in "$SYMPHONY_DIR"/commands/*.md; do
[ -e "$cmd" ] || continue
link_file "$cmd" "$CONFIG_DIR/commands/$(basename "$cmd")"
done
echo ""
echo "Hooks:"
command -v jq >/dev/null || { echo " jq is required to register the hooks" >&2; exit 1; }
SETTINGS="$CONFIG_DIR/settings.json"
[ -f "$SETTINGS" ] || echo '{}' > "$SETTINGS"
# One hooks file serves both installs: the plugin runtime substitutes
# CLAUDE_PLUGIN_ROOT itself, and a linked install substitutes it here.
ours="$(sed "s|\${CLAUDE_PLUGIN_ROOT}|$SYMPHONY_DIR|g" "$SYMPHONY_DIR/hooks/hooks.json" | jq '.hooks')"
# Every entry of ours is dropped before ours are added back, so a hook that
# moved upstream replaces its predecessor instead of running alongside it.
# An entry pointing anywhere else is left alone.
merged="$(jq --argjson ours "$ours" --arg mine "$SYMPHONY_DIR/bin/" '
.hooks = reduce ($ours | to_entries[]) as $event ((.hooks // {});
.[$event.key] = (
((.[$event.key] // [])
| map(select(any(.hooks[]?; (.command // "") | startswith($mine)) | not)))
+ $event.value))
' "$SETTINGS")"
printf '%s\n' "$merged" > "$SETTINGS"
echo " merged into $SETTINGS"
echo ""
echo "Status line:"
existing="$(jq -r '.statusLine.command // empty' "$SETTINGS")"
ours_line="$SYMPHONY_DIR/bin/status-line.sh"
if [ -n "$existing" ] && [ "$existing" != "$ours_line" ]; then
echo " already set to $existing, left unchanged"
else
with_status_line="$(jq --arg command "$ours_line" '
.statusLine = (.statusLine // {}) + {type: "command", command: $command, refreshInterval: 2}
' "$SETTINGS")"
printf '%s\n' "$with_status_line" > "$SETTINGS"
echo " set in $SETTINGS"
fi