Repository navigation
Expand file tree
/
Copy pathbootstrap.sh
More file actions
executable file
·194 lines (180 loc) · 8.77 KB
/
Copy pathbootstrap.sh
File metadata and controls
executable file
·194 lines (180 loc) · 8.77 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
#!/usr/bin/env bash
# Merges the kit's user-settings fragment into ~/.claude/settings.json, adds
# the zsh options block to ~/.zshenv, writes the names of your zsh aliases for
# dk-guard, installs the kit's plugins from this checkout and adds --isolated
# to an existing user-scope playwright MCP server. A plugin cannot ship
# permission rules, autoMode settings, shell options or MCP arguments, so
# those travel this way.
#
# ./bootstrap.sh show the settings, .zshenv, alias list and MCP diffs, write nothing
# ./bootstrap.sh --apply back up both files, write them and the alias list, install plugins,
# re-register the playwright server
#
# Objects merge key by key, arrays gain the entries they lack (nothing is
# removed or reordered), scalars from the fragment win. A private overlay at
# ~/.config/dk-agent-kit/private.json (client-specific autoMode.environment,
# for example) is merged last and never lives in this public repo.
set -euo pipefail
repo=$(cd "$(dirname "$0")" && pwd)
settings=${CLAUDE_SETTINGS:-$HOME/.claude/settings.json}
private=${DK_PRIVATE_SETTINGS:-$HOME/.config/dk-agent-kit/private.json}
plugins=(dk-guard dk)
command -v jq >/dev/null || { echo "bootstrap: jq is required" >&2; exit 1; }
# a missing settings file merges as {}; only --apply writes it
current='{}'
[[ -f "$settings" ]] && current=$(cat "$settings")
layers=("$repo/settings/user.fragment.json")
[[ -f "$private" ]] && layers+=("$private")
merged=$(jq -s '
def merge($a; $b):
if ($a | type) == "object" and ($b | type) == "object" then
reduce ($b | keys_unsorted[]) as $k ($a; .[$k] = merge($a[$k]; $b[$k]))
elif ($a | type) == "array" and ($b | type) == "array" then
$a + [$b[] as $x | select($a | any(. == $x) | not) | $x]
elif $b == null then $a
else $b end;
reduce .[1:][] as $layer (.[0]; merge(.; $layer))
' <(printf '%s\n' "$current") "${layers[@]}")
if diff -u --label "$settings" --label "$settings (merged)" <(printf '%s\n' "$current") <(printf '%s\n' "$merged"); then
echo "bootstrap: settings already up to date"
changed=0
else
changed=1
fi
# The Bash tool runs zsh, and agents write bash: this block in the zsh
# environment file stops the glob and =word errors that mismatch causes.
zshenv=${ZDOTDIR:-$HOME}/.zshenv
zsh_block='# >>> dk-agent-kit zsh options >>>
# An unmatched glob (--include=*.go) is passed through literally, and a word
# starting with = (echo ====) is not expanded to a command path.
setopt NO_NOMATCH
unsetopt EQUALS
# <<< dk-agent-kit zsh options <<<'
zsh_current=$(cat "$zshenv" 2>/dev/null || true)
# A block written by hand (no marker) counts when it sets both options.
zsh_opt() { grep -Eiq "^[[:space:]]*($1)[[:space:]]*(#.*)?\$" <<<"$zsh_current"; }
if grep -qxF '# >>> dk-agent-kit zsh options >>>' <<<"$zsh_current" ||
{ zsh_opt 'setopt[[:space:]]+no_?nomatch|unsetopt[[:space:]]+nomatch' &&
zsh_opt 'unsetopt[[:space:]]+equals|setopt[[:space:]]+no_?equals'; }; then
echo "bootstrap: zsh options already in $zshenv"
zsh_changed=0
else
zsh_merged=$zsh_block
[[ -n "$zsh_current" ]] && zsh_merged=$(printf '%s\n\n%s' "$zsh_current" "$zsh_block")
diff -u --label "$zshenv" --label "$zshenv (merged)" \
<(printf '%s\n' "$zsh_current") <(printf '%s\n' "$zsh_merged") || true
zsh_changed=1
fi
# zsh refuses a function named like an alias ("defining function based on
# alias"), and agents write helpers such as g(){ ... }. dk-guard denies those
# names, read from this list beside the private overlay, where the hook finds
# it from HOME alone. Names only: the alias bodies never leave the shell.
aliases_file=$HOME/.config/dk-agent-kit/zsh-aliases
alias_changed=0
if ! command -v zsh >/dev/null; then
echo "bootstrap: no zsh on PATH, alias list not written"
else
# the marker keeps anything the interactive startup files print out of the list
alias_out=$(zsh -ic 'print -rl -- dk-alias:${^${(k)aliases}}' </dev/null 2>/dev/null) && zsh_ok=1 || zsh_ok=0
alias_names=$(sed -n 's/^dk-alias://p' <<<"$alias_out" | LC_ALL=C sort -u)
alias_new=$(printf '# zsh alias names, written by dk-agent-kit bootstrap.sh for dk-guard\n%s' "$alias_names")
alias_current=$(cat "$aliases_file" 2>/dev/null || true)
if ((!zsh_ok)); then
# an empty list would turn the guard's alias rule off without a word
echo "bootstrap: zsh failed, alias list kept as it is in $aliases_file"
elif [[ "$alias_current" == "$alias_new" ]]; then
echo "bootstrap: zsh alias list already up to date"
else
diff -u --label "$aliases_file" --label "$aliases_file (new)" \
<(printf '%s\n' "$alias_current") <(printf '%s\n' "$alias_new") || true
alias_changed=1
fi
fi
# Without --isolated, two sessions started from one directory share the
# Playwright MCP browser profile and the second gets "Browser is already in
# use". `claude mcp get` shows the server that wins in the current directory,
# so it runs from / where no project or local server shadows the user one; it
# also health-checks the server, the dry run's one side effect. It joins the
# args with spaces, so an arg containing a space would split; the playwright
# args have none.
mcp_changed=0
mcp_field() { sed -n "s/^ $1: //p" <<<"$mcp"; }
mcp_json() {
jq -cn --arg cmd "$(mcp_field Command)" --arg args "$1" \
--arg env "$(sed -n '/^ Environment:$/,/^$/s/^ //p' <<<"$mcp")" '
{type: "stdio", command: $cmd, args: ($args | split(" ") | map(select(. != ""))),
env: ([$env | split("\n")[] | select(. != "") | capture("^(?<k>[^=]+)=(?<v>.*)$") | {(.k): .v}] | add // {})}'
}
shell_quote() { local q="'\\''"; printf "'%s'" "${1//\'/$q}"; }
if ! command -v claude >/dev/null; then
echo "bootstrap: no claude CLI on PATH, playwright MCP server not checked"
elif ! mcp=$(cd / && claude mcp get playwright 2>&1); then
if grep -q '^No MCP server named "playwright"' <<<"$mcp"; then
echo "bootstrap: no user-scope playwright MCP server, none added"
else
printf 'bootstrap: claude mcp get playwright failed, MCP server not checked:\n%s\n' "$mcp"
fi
elif ! grep -q '^ Scope: User config' <<<"$mcp" || [[ "$(mcp_field Type)" != stdio ]]; then
echo "bootstrap: the playwright MCP server is not a user-scope stdio server, left as is"
elif [[ -z "$(mcp_field Command)" ]] || ! grep -q '^ Args:' <<<"$mcp"; then
printf 'bootstrap: no command or args in claude mcp get playwright, left as is:\n%s\n' "$mcp"
elif mcp_args=$(mcp_field Args); [[ " $mcp_args " =~ \ --isolated[\ =] ]]; then
echo "bootstrap: playwright already runs with --isolated"
else
diff -u --label "claude mcp get playwright" --label "claude mcp get playwright (merged)" \
<(printf ' Args: %s\n' "$mcp_args") <(printf ' Args: %s\n' "$mcp_args --isolated") || true
mcp_old=$(mcp_json "$mcp_args")
mcp_new=$(mcp_json "$mcp_args --isolated")
mcp_changed=1
fi
if [[ "${1:-}" != "--apply" ]]; then
echo
echo "bootstrap: dry run. --apply writes this and runs:"
echo " claude plugin marketplace add $repo"
for p in "${plugins[@]}"; do echo " claude plugin install $p@dk-agent-kit"; done
if ((mcp_changed)); then
echo " claude mcp remove playwright -s user"
echo " claude mcp add-json -s user playwright $(shell_quote "$(jq -c '.env |= map_values("<hidden>")' <<<"$mcp_new")")"
fi
exit 0
fi
if ((changed)); then
backup=
if [[ -f "$settings" ]]; then
backup="$settings.bak-$(date +%Y%m%d-%H%M%S)"
cp "$settings" "$backup"
fi
printf '%s\n' "$merged" > "$settings"
echo "bootstrap: wrote $settings${backup:+ (backup: $backup)}"
fi
if ((zsh_changed)); then
if [[ -f "$zshenv" ]]; then
zsh_backup="$zshenv.bak-$(date +%Y%m%d-%H%M%S)"
cp "$zshenv" "$zsh_backup"
echo "bootstrap: backed up $zshenv to $zsh_backup"
fi
printf '%s\n' "$zsh_merged" > "$zshenv"
echo "bootstrap: wrote the zsh options block to $zshenv"
fi
if ((alias_changed)); then
mkdir -p "$(dirname "$aliases_file")"
printf '%s\n' "$alias_new" > "$aliases_file"
echo "bootstrap: wrote $(grep -vc '^#' "$aliases_file" || true) zsh alias names to $aliases_file"
fi
if ! claude plugin marketplace list 2>/dev/null | grep -q 'dk-agent-kit'; then
claude plugin marketplace add "$repo"
fi
for p in "${plugins[@]}"; do
claude plugin install "$p@dk-agent-kit"
done
if ((mcp_changed)); then
# the env may hold credentials: kept in a private file, never printed
mcp_backup="$(dirname "$settings")/playwright-mcp.json.bak-$(date +%Y%m%d-%H%M%S)"
(umask 077 && printf '%s\n' "$mcp_old" > "$mcp_backup")
echo "bootstrap: re-registering playwright with --isolated; if the add fails, restore it with"
echo " claude mcp add-json -s user playwright \"\$(cat $(shell_quote "$mcp_backup"))\""
claude mcp remove playwright -s user
claude mcp add-json -s user playwright "$mcp_new"
echo "bootstrap: playwright now runs with --isolated in sessions started from now on"
fi
echo "bootstrap: done. Run /reload-plugins in open sessions."