docks-kit 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -15
- package/README.md +12 -11
- package/SoT/.agents/skills.txt +3 -3
- package/SoT/.claude/settings.json +3 -26
- package/SoT/models.json +1 -1
- package/SoT/toolchain.json +3 -3
- package/cli/docs/install.md +39 -12
- package/cli/docs/overview.md +3 -4
- package/cli/docs/platforms.md +35 -22
- package/cli/src/commands/sync.ts +32 -4
- package/cli/src/commands/update.ts +116 -0
- package/cli/src/engine-native/DESIGN.md +89 -0
- package/cli/src/engine-native/claudeModel.ts +48 -0
- package/cli/src/engine-native/claudeSync.ts +771 -0
- package/cli/src/engine-native/codexSync.ts +439 -0
- package/cli/src/engine-native/codexToml.ts +67 -0
- package/cli/src/engine-native/exec.ts +54 -0
- package/cli/src/engine-native/index.ts +109 -0
- package/cli/src/engine-native/jq.ts +63 -0
- package/cli/src/engine-native/models.ts +73 -0
- package/cli/src/engine-native/modes.ts +133 -0
- package/cli/src/engine-native/output.ts +20 -0
- package/cli/src/engine-native/parseArgs.ts +223 -0
- package/cli/src/engine-native/settings.ts +41 -0
- package/cli/src/engine-native/skillsSync.ts +369 -0
- package/cli/src/engine-native/toolchain.ts +257 -0
- package/cli/src/engine.ts +35 -18
- package/cli/src/kitHome.ts +4 -4
- package/cli/src/main.ts +14 -1
- package/cli/src/manifests.ts +3 -2
- package/cli/tsconfig.json +1 -1
- package/docks-kit +6 -3
- package/package.json +8 -4
- package/lib/claude.sh +0 -846
- package/lib/codex.sh +0 -518
- package/lib/common.sh +0 -229
- package/lib/engine.sh +0 -153
- package/lib/skills.sh +0 -373
- package/lib/toolchain.sh +0 -222
package/lib/claude.sh
DELETED
|
@@ -1,846 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
# Source-order guards: this lib depends on common.sh's log/warn/err and on
|
|
5
|
-
# the entry script's REPO_DIR. Fail fast with a clear message if sourced standalone.
|
|
6
|
-
declare -F log >/dev/null 2>&1 || { printf '\033[1;31m[err]\033[0m %s\n' "lib/claude.sh must be sourced after lib/common.sh" >&2; exit 1; }
|
|
7
|
-
[[ -n "${REPO_DIR:-}" ]] || { printf '\033[1;31m[err]\033[0m %s\n' "REPO_DIR must be set before sourcing lib/claude.sh" >&2; exit 1; }
|
|
8
|
-
|
|
9
|
-
claude::sync() {
|
|
10
|
-
CLAUDE_DIR="$HOME/.claude"
|
|
11
|
-
CLAUDE_SYNCED=1
|
|
12
|
-
|
|
13
|
-
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
14
|
-
mkdir -p "$CLAUDE_DIR"
|
|
15
|
-
fi
|
|
16
|
-
|
|
17
|
-
# Toolchain before config: on a first-ever install `rtk init --global`
|
|
18
|
-
# rewrites ~/.claude/settings.json (clears hooks.PreToolUse), so it must run
|
|
19
|
-
# BEFORE the settings merge + deploy-time modifiers — the merge then
|
|
20
|
-
# normalizes whatever rtk wrote and the modifiers land last, unclobberable.
|
|
21
|
-
claude::sync_rtk
|
|
22
|
-
claude::sync_scripts
|
|
23
|
-
claude::sync_hooks
|
|
24
|
-
claude::sync_claude_md
|
|
25
|
-
claude::sync_settings
|
|
26
|
-
claude::sync_compact_window
|
|
27
|
-
claude::sync_permissive
|
|
28
|
-
claude::sync_model
|
|
29
|
-
claude::sync_claude_json
|
|
30
|
-
claude::sync_connector_env
|
|
31
|
-
claude::sync_removals
|
|
32
|
-
claude::sync_plugins
|
|
33
|
-
claude::sync_optional_plugins
|
|
34
|
-
claude::sync_lsp_servers
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
claude::sync_scripts() {
|
|
38
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
39
|
-
echo "[dry-run] cp statusline.sh, fetch-usage.sh, notification.mp3"
|
|
40
|
-
return
|
|
41
|
-
fi
|
|
42
|
-
|
|
43
|
-
[[ -f "$REPO_DIR/SoT/.claude/statusline.sh" ]] && { cp "$REPO_DIR/SoT/.claude/statusline.sh" "$CLAUDE_DIR/"; chmod +x "$CLAUDE_DIR/statusline.sh"; }
|
|
44
|
-
[[ -f "$REPO_DIR/SoT/.claude/fetch-usage.sh" ]] && { cp "$REPO_DIR/SoT/.claude/fetch-usage.sh" "$CLAUDE_DIR/"; chmod +x "$CLAUDE_DIR/fetch-usage.sh"; }
|
|
45
|
-
[[ -f "$REPO_DIR/notification.mp3" ]] && cp "$REPO_DIR/notification.mp3" "$CLAUDE_DIR/"
|
|
46
|
-
log "Scripts synced (statusline, fetch-usage, notification)"
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
claude::sync_hooks() {
|
|
50
|
-
local hook_count
|
|
51
|
-
|
|
52
|
-
[[ -d "$REPO_DIR/SoT/.claude/hooks" ]] || return
|
|
53
|
-
|
|
54
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
55
|
-
echo "[dry-run] cp -R $REPO_DIR/SoT/.claude/hooks/. $CLAUDE_DIR/hooks/"
|
|
56
|
-
return
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
mkdir -p "$CLAUDE_DIR/hooks"
|
|
60
|
-
# cp -R (not rsync — rsync is not coreutils and is absent on minimal images
|
|
61
|
-
# like the Claude-Code-on-the-web Ubuntu sandbox). `src/.` copies contents
|
|
62
|
-
# (incl. dotfiles) into dst; additive, like rsync without --delete.
|
|
63
|
-
cp -R "$REPO_DIR/SoT/.claude/hooks/." "$CLAUDE_DIR/hooks/"
|
|
64
|
-
find "$CLAUDE_DIR/hooks" -maxdepth 1 -name '*.sh' -exec chmod +x {} +
|
|
65
|
-
hook_count=$(find "$CLAUDE_DIR/hooks" -maxdepth 1 -name '*.sh' 2>/dev/null | wc -l)
|
|
66
|
-
log "Hooks synced ($hook_count scripts)"
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
claude::sync_claude_md() {
|
|
70
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
71
|
-
if [[ "${SKIP_RTK:-0}" -eq 1 ]]; then
|
|
72
|
-
echo "[dry-run] cp SoT/.claude/CLAUDE.md -> ~/.claude/CLAUDE.md (stripping @RTK.md import: --skip-rtk)"
|
|
73
|
-
else
|
|
74
|
-
echo "[dry-run] cp SoT/.claude/CLAUDE.md -> ~/.claude/CLAUDE.md"
|
|
75
|
-
fi
|
|
76
|
-
return
|
|
77
|
-
fi
|
|
78
|
-
|
|
79
|
-
# The strip must happen HERE, on the file being written: sync_rtk runs
|
|
80
|
-
# earlier (toolchain-before-config), so a strip there would be clobbered
|
|
81
|
-
# by this copy.
|
|
82
|
-
if [[ "${SKIP_RTK:-0}" -eq 1 ]]; then
|
|
83
|
-
grep -v '^@RTK\.md$' "$REPO_DIR/SoT/.claude/CLAUDE.md" > "$CLAUDE_DIR/CLAUDE.md"
|
|
84
|
-
log "CLAUDE.md synced (@RTK.md import stripped: --skip-rtk)"
|
|
85
|
-
else
|
|
86
|
-
cp "$REPO_DIR/SoT/.claude/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
|
|
87
|
-
log "CLAUDE.md synced"
|
|
88
|
-
fi
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# Validate that the existing user settings file is parseable. Returns 0 if
|
|
92
|
-
# the file is missing (caller will install) or valid; 1 if invalid (caller
|
|
93
|
-
# returns early without touching the file).
|
|
94
|
-
claude::_settings_validate() {
|
|
95
|
-
local user_settings="$1"
|
|
96
|
-
[[ -f "$user_settings" ]] || return 0
|
|
97
|
-
if ! jq empty "$user_settings" 2>/dev/null; then
|
|
98
|
-
err "Skipping settings sync: $user_settings is not valid JSON. Fix it manually or delete it to reinstall."
|
|
99
|
-
return 1
|
|
100
|
-
fi
|
|
101
|
-
return 0
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
# First-install path — no user settings exist yet, copy SoT verbatim.
|
|
105
|
-
claude::_settings_install() {
|
|
106
|
-
local repo_settings="$1" user_settings="$2"
|
|
107
|
-
cp "$repo_settings" "$user_settings"
|
|
108
|
-
log "Settings installed"
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
# Force-reconcile path: SoT keys win, permissions arrays REPLACED wholesale by
|
|
112
|
-
# SoT (user-added permissions are discarded), user-only top-level keys survive.
|
|
113
|
-
claude::_settings_reconcile() {
|
|
114
|
-
local repo_settings="$1" user_settings="$2"
|
|
115
|
-
cp "$user_settings" "$user_settings.bak"
|
|
116
|
-
if ! jq -s '.[0] as $repo | .[1] as $user | $user * $repo' \
|
|
117
|
-
"$repo_settings" "$user_settings" > "$user_settings.tmp"; then
|
|
118
|
-
rm -f "$user_settings.tmp"
|
|
119
|
-
err "jq reconcile failed — settings unchanged (backup at settings.json.bak)"
|
|
120
|
-
return
|
|
121
|
-
fi
|
|
122
|
-
mv "$user_settings.tmp" "$user_settings"
|
|
123
|
-
log "Settings reconciled (backup at settings.json.bak; user-only keys preserved, permissions arrays replaced by SoT)"
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
# Default-merge path: SoT keys win, permissions.{allow,deny,ask} arrays UNIONED
|
|
127
|
-
# (user + repo, deduplicated), user-only top-level keys survive.
|
|
128
|
-
claude::_settings_merge() {
|
|
129
|
-
local repo_settings="$1" user_settings="$2"
|
|
130
|
-
cp "$user_settings" "$user_settings.bak"
|
|
131
|
-
if ! jq -s '
|
|
132
|
-
.[0] as $repo | .[1] as $user |
|
|
133
|
-
($user * $repo) |
|
|
134
|
-
.permissions.allow = (($user.permissions.allow // []) + ($repo.permissions.allow // []) | unique) |
|
|
135
|
-
.permissions.deny = (($user.permissions.deny // []) + ($repo.permissions.deny // []) | unique) |
|
|
136
|
-
.permissions.ask = (($user.permissions.ask // []) + ($repo.permissions.ask // []) | unique)
|
|
137
|
-
' "$repo_settings" "$user_settings" > "$user_settings.tmp"; then
|
|
138
|
-
rm -f "$user_settings.tmp"
|
|
139
|
-
err "jq merge failed — settings unchanged (backup at settings.json.bak)"
|
|
140
|
-
return
|
|
141
|
-
fi
|
|
142
|
-
mv "$user_settings.tmp" "$user_settings"
|
|
143
|
-
log "Settings merged (backup at settings.json.bak)"
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
claude::sync_settings() {
|
|
147
|
-
local repo_settings="$REPO_DIR/SoT/.claude/settings.json"
|
|
148
|
-
local user_settings="$CLAUDE_DIR/settings.json"
|
|
149
|
-
|
|
150
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
151
|
-
if [[ ! -f "$user_settings" ]]; then
|
|
152
|
-
echo "[dry-run] install $repo_settings -> $user_settings"
|
|
153
|
-
elif [[ "$RECONCILE" -eq 1 ]]; then
|
|
154
|
-
echo "[dry-run] reconcile $repo_settings -> $user_settings (SoT keys win; permissions arrays replaced; user-only keys preserved)"
|
|
155
|
-
else
|
|
156
|
-
echo "[dry-run] merge $repo_settings -> $user_settings (SoT keys win; permissions arrays unioned; user-only keys preserved)"
|
|
157
|
-
fi
|
|
158
|
-
return
|
|
159
|
-
fi
|
|
160
|
-
|
|
161
|
-
claude::_settings_validate "$user_settings" || return
|
|
162
|
-
|
|
163
|
-
if [[ ! -f "$user_settings" ]]; then
|
|
164
|
-
claude::_settings_install "$repo_settings" "$user_settings"
|
|
165
|
-
elif [[ "$RECONCILE" -eq 1 ]]; then
|
|
166
|
-
claude::_settings_reconcile "$repo_settings" "$user_settings"
|
|
167
|
-
else
|
|
168
|
-
claude::_settings_merge "$repo_settings" "$user_settings"
|
|
169
|
-
fi
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
# Deploy-time modifier (--claude-compact-window=<tokens>): set the DEPLOYED
|
|
173
|
-
# autocompact window (e.g. 680k for disposable containers) — host machines stay
|
|
174
|
-
# on the SoT cap. Touches only ~/.claude/settings.json — the SoT stays at its
|
|
175
|
-
# cap and the model selection is never changed. A later flag-less sync restores
|
|
176
|
-
# the SoT value via the repo-wins merge, so re-pass the flag on machines that
|
|
177
|
-
# should keep the override.
|
|
178
|
-
claude::sync_compact_window() {
|
|
179
|
-
local user_settings="${CLAUDE_DIR:-$HOME/.claude}/settings.json"
|
|
180
|
-
|
|
181
|
-
[[ -n "$CLAUDE_COMPACT_WINDOW" ]] || return 0
|
|
182
|
-
|
|
183
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
184
|
-
echo "[dry-run] (--claude-compact-window) set env.CLAUDE_CODE_AUTO_COMPACT_WINDOW=$CLAUDE_COMPACT_WINDOW in $user_settings"
|
|
185
|
-
return
|
|
186
|
-
fi
|
|
187
|
-
|
|
188
|
-
[[ -f "$user_settings" ]] || { warn "(--claude-compact-window) $user_settings missing — skipped"; return; }
|
|
189
|
-
jq empty "$user_settings" 2>/dev/null || { err "(--claude-compact-window) $user_settings is not valid JSON — skipped"; return; }
|
|
190
|
-
if ! jq --arg w "$CLAUDE_COMPACT_WINDOW" '.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = $w' "$user_settings" > "$user_settings.tmp"; then
|
|
191
|
-
rm -f "$user_settings.tmp"
|
|
192
|
-
err "(--claude-compact-window) jq edit failed — settings unchanged"
|
|
193
|
-
return
|
|
194
|
-
fi
|
|
195
|
-
mv "$user_settings.tmp" "$user_settings"
|
|
196
|
-
log "Compact window: set to $CLAUDE_COMPACT_WINDOW tokens in deployed settings (SoT and model unchanged; flag-less sync reverts)"
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
# Deploy-time modifier (--claude-permissive): for disposable sandboxes/containers.
|
|
200
|
-
# Empties permissions.ask and permissions.deny in the DEPLOYED settings so no
|
|
201
|
-
# rule prompts or blocks — git push drops out of ask and is covered by the
|
|
202
|
-
# existing Bash(git *) allow rule, so commits and pushes run unattended. The SoT
|
|
203
|
-
# arrays are untouched; a later flag-less sync re-unions them back in.
|
|
204
|
-
claude::sync_permissive() {
|
|
205
|
-
local user_settings="${CLAUDE_DIR:-$HOME/.claude}/settings.json"
|
|
206
|
-
|
|
207
|
-
[[ "$CLAUDE_PERMISSIVE" -eq 1 ]] || return 0
|
|
208
|
-
|
|
209
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
210
|
-
echo "[dry-run] (--claude-permissive) empty permissions.ask and permissions.deny in $user_settings"
|
|
211
|
-
return
|
|
212
|
-
fi
|
|
213
|
-
|
|
214
|
-
[[ -f "$user_settings" ]] || { warn "(--claude-permissive) $user_settings missing — skipped"; return; }
|
|
215
|
-
jq empty "$user_settings" 2>/dev/null || { err "(--claude-permissive) $user_settings is not valid JSON — skipped"; return; }
|
|
216
|
-
if ! jq '.permissions.ask = [] | .permissions.deny = []' "$user_settings" > "$user_settings.tmp"; then
|
|
217
|
-
rm -f "$user_settings.tmp"
|
|
218
|
-
err "(--claude-permissive) jq edit failed — settings unchanged"
|
|
219
|
-
return
|
|
220
|
-
fi
|
|
221
|
-
mv "$user_settings.tmp" "$user_settings"
|
|
222
|
-
log "Permissive mode: permissions.ask/deny emptied in deployed settings (sandbox use; SoT unchanged)"
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
# Deploy-time modifier (--claude-model=<m>): set the DEPLOYED model selection.
|
|
226
|
-
# Same contract as the other modifiers: only ~/.claude/settings.json changes,
|
|
227
|
-
# the SoT keeps its own pin, and a later flag-less sync restores the SoT value
|
|
228
|
-
# via the repo-wins merge. 'default' deletes the key so the account default
|
|
229
|
-
# applies. Also callable standalone (docks-kit model claude <m>) — hence the
|
|
230
|
-
# ${CLAUDE_DIR:-} fallback, since claude::sync may not have run.
|
|
231
|
-
claude::sync_model() {
|
|
232
|
-
local user_settings="${CLAUDE_DIR:-$HOME/.claude}/settings.json"
|
|
233
|
-
|
|
234
|
-
[[ -n "$CLAUDE_MODEL" ]] || return 0
|
|
235
|
-
|
|
236
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
237
|
-
if [[ "$CLAUDE_MODEL" == "default" ]]; then
|
|
238
|
-
echo "[dry-run] (--claude-model) delete .model in $user_settings (account default applies)"
|
|
239
|
-
else
|
|
240
|
-
echo "[dry-run] (--claude-model) set .model=$CLAUDE_MODEL in $user_settings"
|
|
241
|
-
fi
|
|
242
|
-
return
|
|
243
|
-
fi
|
|
244
|
-
|
|
245
|
-
[[ -f "$user_settings" ]] || { warn "(--claude-model) $user_settings missing — skipped"; return; }
|
|
246
|
-
jq empty "$user_settings" 2>/dev/null || { err "(--claude-model) $user_settings is not valid JSON — skipped"; return; }
|
|
247
|
-
if [[ "$CLAUDE_MODEL" == "default" ]]; then
|
|
248
|
-
jq 'del(.model)' "$user_settings" > "$user_settings.tmp"
|
|
249
|
-
else
|
|
250
|
-
jq --arg m "$CLAUDE_MODEL" '.model = $m' "$user_settings" > "$user_settings.tmp"
|
|
251
|
-
fi || {
|
|
252
|
-
rm -f "$user_settings.tmp"
|
|
253
|
-
err "(--claude-model) jq edit failed — settings unchanged"
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
mv "$user_settings.tmp" "$user_settings"
|
|
257
|
-
log "Model: deployed settings model set to $CLAUDE_MODEL (SoT unchanged; flag-less sync reverts)"
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
claude::sync_claude_json() {
|
|
261
|
-
local claude_json="$HOME/.claude.json"
|
|
262
|
-
local mcp_sot="$REPO_DIR/SoT/.claude/mcp-servers.json"
|
|
263
|
-
local jq_args=() filter='.showTurnDuration = true' have_mcp=0
|
|
264
|
-
|
|
265
|
-
# settings.json cannot hold mcpServers (schema rejects it), so user-scoped MCP
|
|
266
|
-
# servers the kit manages live here in ~/.claude.json. Merge is additive:
|
|
267
|
-
# `(.mcpServers // {}) * SoT` — the user's own servers survive, SoT wins per
|
|
268
|
-
# server key. Dropping a server from the SoT file does NOT remove it (additive
|
|
269
|
-
# by default, like every other sync layer).
|
|
270
|
-
if [[ -f "$mcp_sot" ]] && jq empty "$mcp_sot" 2>/dev/null; then
|
|
271
|
-
have_mcp=1
|
|
272
|
-
jq_args=(--slurpfile mcp "$mcp_sot")
|
|
273
|
-
filter+=' | .mcpServers = ((.mcpServers // {}) * ($mcp[0].mcpServers // {}))'
|
|
274
|
-
fi
|
|
275
|
-
|
|
276
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
277
|
-
echo "[dry-run] set showTurnDuration=true in ~/.claude.json"
|
|
278
|
-
[[ "$have_mcp" -eq 1 ]] && echo "[dry-run] merge mcpServers from SoT/.claude/mcp-servers.json into ~/.claude.json"
|
|
279
|
-
return
|
|
280
|
-
fi
|
|
281
|
-
|
|
282
|
-
if [[ -f "$claude_json" ]]; then
|
|
283
|
-
if ! jq empty "$claude_json" 2>/dev/null; then
|
|
284
|
-
err "Skipping ~/.claude.json edit: not valid JSON. Fix or delete it."
|
|
285
|
-
return
|
|
286
|
-
fi
|
|
287
|
-
if ! jq "${jq_args[@]+"${jq_args[@]}"}" "$filter" "$claude_json" > "$claude_json.tmp"; then
|
|
288
|
-
rm -f "$claude_json.tmp"
|
|
289
|
-
err "jq edit of ~/.claude.json failed — file unchanged"
|
|
290
|
-
return
|
|
291
|
-
fi
|
|
292
|
-
mv "$claude_json.tmp" "$claude_json"
|
|
293
|
-
else
|
|
294
|
-
jq -n "${jq_args[@]+"${jq_args[@]}"}" "{} | $filter" > "$claude_json"
|
|
295
|
-
fi
|
|
296
|
-
log "~/.claude.json updated (showTurnDuration$([[ "$have_mcp" -eq 1 ]] && printf ', mcpServers'))"
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
# Ensure ENABLE_CLAUDEAI_MCP_SERVERS=false is exported as a REAL shell env var.
|
|
300
|
-
# This is the only working way to suppress claude.ai cloud connectors (Figma,
|
|
301
|
-
# Drive, Gmail, ...): they're fetched from the account at session start and
|
|
302
|
-
# IGNORE the settings.json `env` block (applied too late), so the var must be
|
|
303
|
-
# present in the process before `claude` launches — i.e. in the shell rc.
|
|
304
|
-
# Surgical: disables ONLY claude.ai connectors (MCP source #5 in Claude Code's
|
|
305
|
-
# scope hierarchy). Local/project/user/plugin servers (supabase, n8n, .mcp.json)
|
|
306
|
-
# are untouched. Idempotent + non-clobbering: if the var is already set in any
|
|
307
|
-
# common rc (any value), it's left as-is — set it to `true` yourself to keep
|
|
308
|
-
# connectors. Multi-platform: targets ~/.zshrc (zsh) or ~/.bashrc (bash),
|
|
309
|
-
# ~/.profile otherwise.
|
|
310
|
-
claude::sync_connector_env() {
|
|
311
|
-
local line="export ENABLE_CLAUDEAI_MCP_SERVERS=false"
|
|
312
|
-
local marker="# docks-kit: disable claude.ai cloud MCP connectors (set =true to keep them)"
|
|
313
|
-
local -a candidates=("$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile" "$HOME/.zshenv")
|
|
314
|
-
local f target shell_name
|
|
315
|
-
|
|
316
|
-
for f in "${candidates[@]}"; do
|
|
317
|
-
if [[ -f "$f" ]] && grep -q 'ENABLE_CLAUDEAI_MCP_SERVERS' "$f" 2>/dev/null; then
|
|
318
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
319
|
-
echo "[dry-run] ENABLE_CLAUDEAI_MCP_SERVERS already in $f — would skip"
|
|
320
|
-
else
|
|
321
|
-
log "claude.ai connectors: ENABLE_CLAUDEAI_MCP_SERVERS already set in $f (left as-is)"
|
|
322
|
-
fi
|
|
323
|
-
return
|
|
324
|
-
fi
|
|
325
|
-
done
|
|
326
|
-
|
|
327
|
-
shell_name="$(basename "${SHELL:-bash}")"
|
|
328
|
-
case "$shell_name" in
|
|
329
|
-
zsh) target="$HOME/.zshrc" ;;
|
|
330
|
-
bash) target="$HOME/.bashrc" ;;
|
|
331
|
-
*) target="$HOME/.profile" ;;
|
|
332
|
-
esac
|
|
333
|
-
|
|
334
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
335
|
-
echo "[dry-run] append 'export ENABLE_CLAUDEAI_MCP_SERVERS=false' to $target"
|
|
336
|
-
return
|
|
337
|
-
fi
|
|
338
|
-
|
|
339
|
-
printf '\n%s\n%s\n' "$marker" "$line" >> "$target"
|
|
340
|
-
log "claude.ai connectors disabled via $target (start a new shell to apply)"
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
# --- Removed-artifact pruning -------------------------------------------------
|
|
344
|
-
# Declarative manifest of kit-owned artifacts the kit no longer ships. The
|
|
345
|
-
# default sync is additive (the jq merge keeps user-only keys; cp -R never
|
|
346
|
-
# deletes), so config the kit dropped in an older version would otherwise
|
|
347
|
-
# linger forever on an already-synced machine. `claude::sync_removals` prunes
|
|
348
|
-
# the items below on every sync.
|
|
349
|
-
#
|
|
350
|
-
# NARROW exception to "additive by default": entries here are force-removed from
|
|
351
|
-
# EVERY synced ~/.claude. List kit-owned keys the kit used to set and has since
|
|
352
|
-
# dropped — they are pruned from the kit-managed settings.json. A deliberate
|
|
353
|
-
# per-machine override of any of these belongs in settings.local.json, which
|
|
354
|
-
# sync never touches. Do NOT list a key the kit never owned (a user's custom env
|
|
355
|
-
# vars, mcpServers, theme) — the additive merge already preserves those.
|
|
356
|
-
# hooks hook scripts under ~/.claude/hooks/ to delete (the matching
|
|
357
|
-
# settings.json hook entry is already dropped by the SoT
|
|
358
|
-
# settings merge, which replaces .hooks wholesale)
|
|
359
|
-
# files other paths under ~/.claude/ to delete
|
|
360
|
-
# settingsKeys dotted key paths to del() from ~/.claude/settings.json
|
|
361
|
-
# claudeJsonKeys dotted key paths to del() from ~/.claude.json
|
|
362
|
-
claude::_removed_manifest() {
|
|
363
|
-
cat <<'JSON'
|
|
364
|
-
{
|
|
365
|
-
"hooks": ["disable-claudeai-connectors.sh"],
|
|
366
|
-
"files": ["alert_bubble.mp3"],
|
|
367
|
-
"settingsKeys": [
|
|
368
|
-
"showTurnDuration",
|
|
369
|
-
"env.CLAUDE_CODE_SUBAGENT_MODEL",
|
|
370
|
-
"env.ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
371
|
-
"env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE",
|
|
372
|
-
"env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
|
|
373
|
-
"env.CLAUDE_CODE_FORK_SUBAGENT",
|
|
374
|
-
"env.CLAUDE_CODE_EFFORT_LEVEL"
|
|
375
|
-
],
|
|
376
|
-
"claudeJsonKeys": []
|
|
377
|
-
}
|
|
378
|
-
JSON
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
# Delete dotted key paths from a JSON file via jq delpaths. Echoes the count of
|
|
382
|
-
# keys that were actually present (and removed); echoes 0 when the file is
|
|
383
|
-
# missing/invalid or no listed key exists. Honors DRY_RUN (counts, no write).
|
|
384
|
-
# delpaths ignores absent paths, so this is idempotent.
|
|
385
|
-
claude::_prune_json_keys() {
|
|
386
|
-
local file="$1" keys="$2" present
|
|
387
|
-
{ [[ "$keys" == "[]" || ! -f "$file" ]] || ! jq empty "$file" 2>/dev/null; } && { echo 0; return; }
|
|
388
|
-
present=$(jq --argjson k "$keys" '. as $doc | [ $k[] | split(".") as $p | select($doc | getpath($p) != null) ] | length' "$file" 2>/dev/null || echo 0)
|
|
389
|
-
[[ "$present" -gt 0 ]] || { echo 0; return; }
|
|
390
|
-
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
391
|
-
if jq --argjson k "$keys" 'delpaths([ $k[] | split(".") ])' "$file" > "$file.tmp" 2>/dev/null; then
|
|
392
|
-
mv "$file.tmp" "$file"
|
|
393
|
-
else
|
|
394
|
-
rm -f "$file.tmp"; warn "Failed to prune stale keys from $file"; echo 0; return
|
|
395
|
-
fi
|
|
396
|
-
fi
|
|
397
|
-
echo "$present"
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
claude::sync_removals() {
|
|
401
|
-
local manifest name path hooks_removed=0 files_removed=0 skeys cjkeys
|
|
402
|
-
|
|
403
|
-
manifest="$(claude::_removed_manifest)"
|
|
404
|
-
|
|
405
|
-
while IFS= read -r name; do
|
|
406
|
-
[[ -z "$name" || ! -e "$CLAUDE_DIR/hooks/$name" ]] && continue
|
|
407
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
408
|
-
echo "[dry-run] rm $CLAUDE_DIR/hooks/$name"
|
|
409
|
-
else
|
|
410
|
-
rm -f "$CLAUDE_DIR/hooks/$name"; hooks_removed=$((hooks_removed + 1))
|
|
411
|
-
fi
|
|
412
|
-
done < <(jq -r '.hooks[]? // empty' <<<"$manifest")
|
|
413
|
-
|
|
414
|
-
while IFS= read -r path; do
|
|
415
|
-
[[ -z "$path" || ! -e "$CLAUDE_DIR/$path" ]] && continue
|
|
416
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
417
|
-
echo "[dry-run] rm $CLAUDE_DIR/$path"
|
|
418
|
-
else
|
|
419
|
-
rm -f "$CLAUDE_DIR/$path"; files_removed=$((files_removed + 1))
|
|
420
|
-
fi
|
|
421
|
-
done < <(jq -r '.files[]? // empty' <<<"$manifest")
|
|
422
|
-
|
|
423
|
-
skeys=$(claude::_prune_json_keys "$CLAUDE_DIR/settings.json" "$(jq -c '.settingsKeys // []' <<<"$manifest")")
|
|
424
|
-
cjkeys=$(claude::_prune_json_keys "$HOME/.claude.json" "$(jq -c '.claudeJsonKeys // []' <<<"$manifest")")
|
|
425
|
-
|
|
426
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
427
|
-
[[ "$skeys" -gt 0 ]] && echo "[dry-run] del $skeys stale key(s) from $CLAUDE_DIR/settings.json"
|
|
428
|
-
[[ "$cjkeys" -gt 0 ]] && echo "[dry-run] del $cjkeys stale key(s) from $HOME/.claude.json"
|
|
429
|
-
# Explicit 0: the trailing [[ ]] tests above leave $?=1 when their counts are
|
|
430
|
-
# 0, and a bare `return` would propagate that — aborting sync under `set -e`.
|
|
431
|
-
return 0
|
|
432
|
-
fi
|
|
433
|
-
|
|
434
|
-
if [[ $((hooks_removed + files_removed + skeys + cjkeys)) -gt 0 ]]; then
|
|
435
|
-
log "Pruned stale artifacts (hooks: $hooks_removed, files: $files_removed, settings keys: $skeys, claude.json keys: $cjkeys)"
|
|
436
|
-
fi
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
# Thin facade around the `claude` CLI plugin pipeline. Centralizes the kit's
|
|
440
|
-
# coupling so renaming the CLI or changing the binary lookup is a one-place
|
|
441
|
-
# edit instead of touching 6 call sites across 5 plugin-pipeline helpers.
|
|
442
|
-
# `command` prefix avoids potential alias/function interference.
|
|
443
|
-
claude::_cli() {
|
|
444
|
-
command claude "$@"
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
# Pass 1 — install any marketplace in SoT that is not yet known. Echoes
|
|
448
|
-
# "<added> <failed>" on stdout (bash 3.2-portable counter return; namerefs
|
|
449
|
-
# require bash 4.3+ and would break macOS /bin/bash).
|
|
450
|
-
claude::_plugins_add_marketplaces() {
|
|
451
|
-
local repo_settings="$1" known_marketplaces="$2"
|
|
452
|
-
local mp_name mp_repo added=0 failed=0
|
|
453
|
-
|
|
454
|
-
while IFS=$'\t' read -r mp_name mp_repo; do
|
|
455
|
-
[[ -z "$mp_name" ]] && continue
|
|
456
|
-
if [[ -f "$known_marketplaces" ]] && jq -e --arg n "$mp_name" '.[$n]' "$known_marketplaces" >/dev/null 2>&1; then
|
|
457
|
-
continue
|
|
458
|
-
fi
|
|
459
|
-
if claude::_cli plugin marketplace add "$mp_repo" >/dev/null 2>&1; then
|
|
460
|
-
added=$((added + 1))
|
|
461
|
-
else
|
|
462
|
-
warn "Failed to add marketplace: $mp_name ($mp_repo)"
|
|
463
|
-
failed=$((failed + 1))
|
|
464
|
-
fi
|
|
465
|
-
done < <(jq -r '.extraKnownMarketplaces // {} | to_entries[] | "\(.key)\t\(.value.source.repo)"' "$repo_settings")
|
|
466
|
-
|
|
467
|
-
echo "$added $failed"
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
# True when installed_plugins.json records a USER-scope install for the plugin.
|
|
471
|
-
# Registry values are per-scope record arrays (Claude Code >= 2.1.198); the
|
|
472
|
-
# type guard tolerates the older single-object form. A project/local-scope
|
|
473
|
-
# record does NOT count — the kit's tri-state contract requires user scope.
|
|
474
|
-
claude::_plugin_user_scope_installed() {
|
|
475
|
-
local installed_plugins="$1" plugin_id="$2"
|
|
476
|
-
[[ -f "$installed_plugins" ]] || return 1
|
|
477
|
-
jq -e --arg n "$plugin_id" \
|
|
478
|
-
'.plugins[$n] // empty | (if type == "array" then . else [.] end) | any(.scope? == "user")' \
|
|
479
|
-
"$installed_plugins" >/dev/null 2>&1
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
# Pass 2 — install any SoT-enabled plugin not yet installed at user scope.
|
|
483
|
-
claude::_plugins_install() {
|
|
484
|
-
local repo_settings="$1" installed_plugins="$2"
|
|
485
|
-
local plugin_id added=0 failed=0 refreshed=0
|
|
486
|
-
|
|
487
|
-
while IFS= read -r plugin_id; do
|
|
488
|
-
[[ -z "$plugin_id" ]] && continue
|
|
489
|
-
if claude::_plugin_user_scope_installed "$installed_plugins" "$plugin_id"; then
|
|
490
|
-
continue
|
|
491
|
-
fi
|
|
492
|
-
# Stale-manifest guard: an already-cloned marketplace may predate a plugin
|
|
493
|
-
# later added to it, and pass 3's manifest refresh runs after this pass —
|
|
494
|
-
# too late for the install. Refresh once before the first install attempt.
|
|
495
|
-
if [[ "$refreshed" -eq 0 ]]; then
|
|
496
|
-
claude::_cli plugin marketplace update >/dev/null 2>&1 || true
|
|
497
|
-
refreshed=1
|
|
498
|
-
fi
|
|
499
|
-
if claude::_cli plugin install "$plugin_id" >/dev/null 2>&1; then
|
|
500
|
-
added=$((added + 1))
|
|
501
|
-
else
|
|
502
|
-
warn "Failed to install plugin: $plugin_id"
|
|
503
|
-
failed=$((failed + 1))
|
|
504
|
-
fi
|
|
505
|
-
done < <(jq -r '.enabledPlugins // {} | keys[]' "$repo_settings")
|
|
506
|
-
|
|
507
|
-
echo "$added $failed"
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
# Pass 3 — refresh every installed plugin. Failures are not counted (best
|
|
511
|
-
# effort); only the "Successfully updated" classification bumps the counter.
|
|
512
|
-
claude::_plugins_update() {
|
|
513
|
-
local installed_plugins="$1"
|
|
514
|
-
local plugin_id out updated=0
|
|
515
|
-
|
|
516
|
-
claude::_cli plugin marketplace update >/dev/null 2>&1 || true
|
|
517
|
-
|
|
518
|
-
if [[ -f "$installed_plugins" ]]; then
|
|
519
|
-
while IFS= read -r plugin_id; do
|
|
520
|
-
[[ -z "$plugin_id" ]] && continue
|
|
521
|
-
out=$(claude::_cli plugin update "$plugin_id" 2>&1 || true)
|
|
522
|
-
if [[ "$out" == *"Successfully updated"* ]]; then
|
|
523
|
-
updated=$((updated + 1))
|
|
524
|
-
fi
|
|
525
|
-
done < <(jq -r '.plugins | keys[]' "$installed_plugins")
|
|
526
|
-
fi
|
|
527
|
-
|
|
528
|
-
echo "$updated 0"
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
# Pass 4 — uninstall any installed plugin no longer declared in SoT. Gated by
|
|
532
|
-
# PRUNE by the caller; helper assumes it should run.
|
|
533
|
-
claude::_plugins_uninstall() {
|
|
534
|
-
local repo_settings="$1" installed_plugins="$2"
|
|
535
|
-
local plugin_id removed=0 failed=0
|
|
536
|
-
|
|
537
|
-
if [[ -f "$installed_plugins" ]]; then
|
|
538
|
-
while IFS= read -r plugin_id; do
|
|
539
|
-
[[ -z "$plugin_id" ]] && continue
|
|
540
|
-
if ! jq -e --arg n "$plugin_id" '.enabledPlugins | has($n)' "$repo_settings" >/dev/null 2>&1; then
|
|
541
|
-
claude::_plugin_user_scope_installed "$installed_plugins" "$plugin_id" || continue
|
|
542
|
-
if claude::_cli plugin uninstall -y --scope user "$plugin_id" >/dev/null 2>&1; then
|
|
543
|
-
removed=$((removed + 1))
|
|
544
|
-
else
|
|
545
|
-
warn "Failed to uninstall plugin: $plugin_id"
|
|
546
|
-
failed=$((failed + 1))
|
|
547
|
-
fi
|
|
548
|
-
fi
|
|
549
|
-
done < <(jq -r '.plugins | keys[]' "$installed_plugins")
|
|
550
|
-
fi
|
|
551
|
-
|
|
552
|
-
echo "$removed $failed"
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
# Pass 5 — remove any extra marketplace not declared in SoT.
|
|
556
|
-
# claude-plugins-official is built-in and never removed (protection contract).
|
|
557
|
-
claude::_plugins_remove_marketplaces() {
|
|
558
|
-
local repo_settings="$1" known_marketplaces="$2"
|
|
559
|
-
local mp_name removed=0 failed=0
|
|
560
|
-
|
|
561
|
-
if [[ -f "$known_marketplaces" ]]; then
|
|
562
|
-
while IFS= read -r mp_name; do
|
|
563
|
-
[[ -z "$mp_name" ]] && continue
|
|
564
|
-
[[ "$mp_name" == "claude-plugins-official" ]] && continue
|
|
565
|
-
if ! jq -e --arg n "$mp_name" '.extraKnownMarketplaces[$n]' "$repo_settings" >/dev/null 2>&1; then
|
|
566
|
-
if claude::_cli plugin marketplace remove "$mp_name" >/dev/null 2>&1; then
|
|
567
|
-
removed=$((removed + 1))
|
|
568
|
-
else
|
|
569
|
-
warn "Failed to remove marketplace: $mp_name"
|
|
570
|
-
failed=$((failed + 1))
|
|
571
|
-
fi
|
|
572
|
-
fi
|
|
573
|
-
done < <(jq -r '. | keys[]' "$known_marketplaces")
|
|
574
|
-
fi
|
|
575
|
-
|
|
576
|
-
echo "$removed $failed"
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
# Pass 6 — enforce SoT enabled-state in ~/.claude/settings.json. `claude plugin
|
|
580
|
-
# install` (pass 2) enables at user scope as a side effect. A plain jq rewrite of
|
|
581
|
-
# that back to false loses a race: the CLI owns settings.json and reverts the
|
|
582
|
-
# external edit, so the false value only sticks on a SECOND sync (once the plugin
|
|
583
|
-
# is already installed and pass 2 skips it). Fix: disable SoT-false plugins
|
|
584
|
-
# through the CLI's own `plugin disable` verb — authoritative, sticks in one pass
|
|
585
|
-
# — THEN jq-normalize so every SoT-declared value wins and user-only
|
|
586
|
-
# enabledPlugins entries are preserved. The disable is guarded on the plugin
|
|
587
|
-
# being currently enabled (re-disabling an already-disabled plugin is a CLI
|
|
588
|
-
# error), which keeps steady-state syncs quiet. If `plugin disable` is missing or
|
|
589
|
-
# fails the jq-normalize still runs, degrading to the old two-sync behavior
|
|
590
|
-
# rather than breaking. Idempotent — safe to run every sync.
|
|
591
|
-
claude::_plugins_reassert_enabled_state() {
|
|
592
|
-
local repo_settings="$1" user_settings="$2" plugin_id
|
|
593
|
-
[[ -f "$user_settings" ]] || return 0
|
|
594
|
-
|
|
595
|
-
while IFS= read -r plugin_id; do
|
|
596
|
-
[[ -z "$plugin_id" ]] && continue
|
|
597
|
-
jq -e --arg n "$plugin_id" '.enabledPlugins[$n] == true' "$user_settings" >/dev/null 2>&1 || continue
|
|
598
|
-
claude::_cli plugin disable "$plugin_id" >/dev/null 2>&1 \
|
|
599
|
-
|| warn "Failed to disable SoT-false plugin: $plugin_id (will retry next sync)"
|
|
600
|
-
done < <(jq -r '.enabledPlugins // {} | to_entries[] | select(.value == false) | .key' "$repo_settings")
|
|
601
|
-
|
|
602
|
-
if jq -s '.[0].enabledPlugins as $sot | .[1]
|
|
603
|
-
| .enabledPlugins = ((.enabledPlugins // {}) * $sot)' \
|
|
604
|
-
"$repo_settings" "$user_settings" > "$user_settings.tmp"; then
|
|
605
|
-
mv "$user_settings.tmp" "$user_settings"
|
|
606
|
-
else
|
|
607
|
-
rm -f "$user_settings.tmp"
|
|
608
|
-
warn "enabledPlugins re-assert failed — false-keyed plugins may be left enabled"
|
|
609
|
-
fi
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
claude::sync_plugins() {
|
|
613
|
-
local repo_settings="$REPO_DIR/SoT/.claude/settings.json"
|
|
614
|
-
local known_marketplaces="$CLAUDE_DIR/plugins/known_marketplaces.json"
|
|
615
|
-
local installed_plugins="$CLAUDE_DIR/plugins/installed_plugins.json"
|
|
616
|
-
local added_mp added_pl updated_pl removed_pl=0 removed_mp=0
|
|
617
|
-
local f1 f2 f3 f4=0 f5=0 failed
|
|
618
|
-
|
|
619
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
620
|
-
echo "[dry-run] bootstrap + update plugin marketplaces + plugins from SoT"
|
|
621
|
-
if [[ "$PRUNE" -eq 1 ]]; then
|
|
622
|
-
echo "[dry-run] (--prune) would also uninstall plugins not in SoT and remove extra marketplaces"
|
|
623
|
-
fi
|
|
624
|
-
return
|
|
625
|
-
fi
|
|
626
|
-
|
|
627
|
-
if ! command -v claude >/dev/null 2>&1; then
|
|
628
|
-
warn "claude CLI not in PATH — skipping plugin reconcile (run /plugin marketplace add + /plugin install manually)"
|
|
629
|
-
return
|
|
630
|
-
fi
|
|
631
|
-
|
|
632
|
-
read -r added_mp f1 < <(claude::_plugins_add_marketplaces "$repo_settings" "$known_marketplaces")
|
|
633
|
-
read -r added_pl f2 < <(claude::_plugins_install "$repo_settings" "$installed_plugins")
|
|
634
|
-
read -r updated_pl f3 < <(claude::_plugins_update "$installed_plugins")
|
|
635
|
-
if [[ "$PRUNE" -eq 1 ]]; then
|
|
636
|
-
read -r removed_pl f4 < <(claude::_plugins_uninstall "$repo_settings" "$installed_plugins")
|
|
637
|
-
read -r removed_mp f5 < <(claude::_plugins_remove_marketplaces "$repo_settings" "$known_marketplaces")
|
|
638
|
-
fi
|
|
639
|
-
claude::_plugins_reassert_enabled_state "$repo_settings" "$CLAUDE_DIR/settings.json"
|
|
640
|
-
failed=$((f1 + f2 + f3 + f4 + f5))
|
|
641
|
-
|
|
642
|
-
if [[ "$added_mp" -gt 0 || "$added_pl" -gt 0 || "$updated_pl" -gt 0 || "$removed_pl" -gt 0 || "$removed_mp" -gt 0 ]]; then
|
|
643
|
-
log "Plugins synced (marketplaces: +$added_mp -$removed_mp, plugins: +$added_pl ~$updated_pl -$removed_pl)"
|
|
644
|
-
else
|
|
645
|
-
log "Plugins already in sync"
|
|
646
|
-
fi
|
|
647
|
-
if [[ "$failed" -gt 0 ]]; then
|
|
648
|
-
warn "$failed plugin operation(s) failed — re-run sync or install manually"
|
|
649
|
-
fi
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
# --- Optional (flag-gated) plugins --------------------------------------------
|
|
653
|
-
# supabase and n8n are kept OUT of the SoT entirely — neither key is in
|
|
654
|
-
# enabledPlugins, so a flag-less sync installs, loads, and enables neither (an
|
|
655
|
-
# absent plugin is simply not installed; only `--prune` uninstalls one already
|
|
656
|
-
# present). n8n's marketplace is dropped from the SoT too; supabase's is the
|
|
657
|
-
# built-in claude-plugins-official one, which stays. `--claude-plugin=<name>`
|
|
658
|
-
# opts one in on THIS machine — add its marketplace when third-party, install it,
|
|
659
|
-
# enable it — a sticky, install-once opt-in that survives later flag-less syncs
|
|
660
|
-
# (both keys are absent from the SoT, so the pass-6 reassert never touches them).
|
|
661
|
-
claude::_enable_optional_plugin() {
|
|
662
|
-
local plugin_id="$1" marketplace_repo="$2"
|
|
663
|
-
local installed_plugins="$CLAUDE_DIR/plugins/installed_plugins.json"
|
|
664
|
-
local known_marketplaces="$CLAUDE_DIR/plugins/known_marketplaces.json"
|
|
665
|
-
local mp_name="${plugin_id##*@}"
|
|
666
|
-
|
|
667
|
-
# Third-party plugins need their marketplace known first; official plugins
|
|
668
|
-
# (claude-plugins-official) are built in, so marketplace_repo is empty.
|
|
669
|
-
if [[ -n "$marketplace_repo" ]] \
|
|
670
|
-
&& ! { [[ -f "$known_marketplaces" ]] && jq -e --arg n "$mp_name" '.[$n]' "$known_marketplaces" >/dev/null 2>&1; }; then
|
|
671
|
-
claude::_cli plugin marketplace add "$marketplace_repo" >/dev/null 2>&1 \
|
|
672
|
-
|| { warn "Failed to add marketplace $marketplace_repo for $plugin_id"; return; }
|
|
673
|
-
fi
|
|
674
|
-
|
|
675
|
-
if ! claude::_plugin_user_scope_installed "$installed_plugins" "$plugin_id"; then
|
|
676
|
-
claude::_cli plugin install "$plugin_id" >/dev/null 2>&1 \
|
|
677
|
-
|| { warn "Failed to install optional plugin $plugin_id"; return; }
|
|
678
|
-
fi
|
|
679
|
-
|
|
680
|
-
# `plugin install` enables as a side effect, but call `plugin enable`
|
|
681
|
-
# explicitly so an already-installed-but-disabled plugin (e.g. supabase left
|
|
682
|
-
# `false` on a machine by an earlier kit version) also flips on. The CLI owns
|
|
683
|
-
# settings.json, so this persists where a plain jq edit would lose the race.
|
|
684
|
-
if claude::_cli plugin enable "$plugin_id" >/dev/null 2>&1; then
|
|
685
|
-
log "Optional plugin opted in: $plugin_id"
|
|
686
|
-
else
|
|
687
|
-
warn "Failed to enable optional plugin $plugin_id"
|
|
688
|
-
fi
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
claude::sync_optional_plugins() {
|
|
692
|
-
[[ -n "$CLAUDE_PLUGINS" ]] || return 0
|
|
693
|
-
|
|
694
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
695
|
-
common::claude_plugin_wanted supabase && echo "[dry-run] (--claude-plugin=supabase) install + enable supabase@claude-plugins-official in deployed settings"
|
|
696
|
-
common::claude_plugin_wanted n8n && echo "[dry-run] (--claude-plugin=n8n) add czlonkowski/n8n-skills marketplace + install + enable n8n-mcp-skills@n8n-mcp-skills"
|
|
697
|
-
return 0
|
|
698
|
-
fi
|
|
699
|
-
|
|
700
|
-
if ! command -v claude >/dev/null 2>&1; then
|
|
701
|
-
warn "claude CLI not in PATH — cannot opt in optional plugins (--claude-plugin)"
|
|
702
|
-
return
|
|
703
|
-
fi
|
|
704
|
-
|
|
705
|
-
if common::claude_plugin_wanted supabase; then
|
|
706
|
-
claude::_enable_optional_plugin "supabase@claude-plugins-official" ""
|
|
707
|
-
fi
|
|
708
|
-
if common::claude_plugin_wanted n8n; then
|
|
709
|
-
claude::_enable_optional_plugin "n8n-mcp-skills@n8n-mcp-skills" "czlonkowski/n8n-skills"
|
|
710
|
-
fi
|
|
711
|
-
return 0
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
# The php-lsp / typescript-lsp plugins only register lspServers config (it
|
|
715
|
-
# ships in the marketplace manifest) — the language-server binaries are
|
|
716
|
-
# separate npm globals, without which the plugins are silent no-ops. Gated on
|
|
717
|
-
# key presence in SoT enabledPlugins (true OR false: false-keyed plugins can
|
|
718
|
-
# be enabled per-project, so the binary must still exist).
|
|
719
|
-
# npm install spec for an LSP tool, pinned to its toolchain.json `verified`
|
|
720
|
-
# version when declared: <manifest tool key> <npm package name>.
|
|
721
|
-
claude::_lsp_pkg() {
|
|
722
|
-
local tool="$1" pkg="$2" v
|
|
723
|
-
v="$(toolchain::field "$tool" verified 2>/dev/null || true)"
|
|
724
|
-
printf '%s' "$pkg${v:+@$v}"
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
claude::sync_lsp_servers() {
|
|
728
|
-
local sot_settings="$REPO_DIR/SoT/.claude/settings.json"
|
|
729
|
-
local missing=()
|
|
730
|
-
|
|
731
|
-
jq -e '.enabledPlugins | has("php-lsp@claude-plugins-official") or has("typescript-lsp@claude-plugins-official")' \
|
|
732
|
-
"$sot_settings" >/dev/null 2>&1 || return 0
|
|
733
|
-
|
|
734
|
-
# Install specs are pinned to the manifest `verified` versions (supply-chain:
|
|
735
|
-
# no floating npm globals). Manifest tool key first, npm package name second.
|
|
736
|
-
if jq -e '.enabledPlugins | has("php-lsp@claude-plugins-official")' "$sot_settings" >/dev/null 2>&1; then
|
|
737
|
-
command -v intelephense >/dev/null 2>&1 || missing+=("$(claude::_lsp_pkg intelephense intelephense)")
|
|
738
|
-
fi
|
|
739
|
-
if jq -e '.enabledPlugins | has("typescript-lsp@claude-plugins-official")' "$sot_settings" >/dev/null 2>&1; then
|
|
740
|
-
command -v typescript-language-server >/dev/null 2>&1 || missing+=("$(claude::_lsp_pkg typescript-language-server typescript-language-server)")
|
|
741
|
-
command -v tsc >/dev/null 2>&1 || missing+=("$(claude::_lsp_pkg tsc typescript)")
|
|
742
|
-
fi
|
|
743
|
-
|
|
744
|
-
if [[ ${#missing[@]} -eq 0 ]]; then
|
|
745
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
746
|
-
echo "[dry-run] LSP server binaries present"
|
|
747
|
-
else
|
|
748
|
-
log "LSP server binaries present"
|
|
749
|
-
fi
|
|
750
|
-
return
|
|
751
|
-
fi
|
|
752
|
-
|
|
753
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
754
|
-
echo "[dry-run] would install: npm install -g ${missing[*]}"
|
|
755
|
-
return
|
|
756
|
-
fi
|
|
757
|
-
|
|
758
|
-
if ! command -v npm >/dev/null 2>&1; then
|
|
759
|
-
warn "npm not found — cannot install LSP servers (${missing[*]}); the php-lsp/typescript-lsp plugins stay no-ops. Install Node.js, then re-run sync."
|
|
760
|
-
return
|
|
761
|
-
fi
|
|
762
|
-
|
|
763
|
-
log "Installing LSP servers via npm: ${missing[*]}..."
|
|
764
|
-
if npm install -g "${missing[@]}" >/dev/null 2>&1; then
|
|
765
|
-
log "LSP servers installed (${missing[*]})"
|
|
766
|
-
else
|
|
767
|
-
warn "npm install -g ${missing[*]} failed. Try manually: npm install -g ${missing[*]}"
|
|
768
|
-
fi
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
# toolchain::ensure callback: download-then-run install/upgrade of RTK, with an
|
|
772
|
-
# optional exact-version pin via the installer's RTK_VERSION env (empty = latest).
|
|
773
|
-
# RTK runs as a PreToolUse bash hook, so unverified upgrades are gated by the
|
|
774
|
-
# toolchain verified pin (SoT/toolchain.json) — review releases before bumping it.
|
|
775
|
-
claude::_rtk_install() {
|
|
776
|
-
local mode="$1" version="${2:-}"
|
|
777
|
-
local tmp_rtk_installer
|
|
778
|
-
# Pinned installs fetch the installer from the version TAG, not master —
|
|
779
|
-
# otherwise the pinned binary is still bootstrapped by a mutable script.
|
|
780
|
-
local installer_ref="refs/heads/master"
|
|
781
|
-
[[ -n "$version" ]] && installer_ref="refs/tags/v$version"
|
|
782
|
-
|
|
783
|
-
[[ "$mode" == "upgrade" ]] && log "Upgrading RTK${version:+ to $version}..." || warn "RTK not found. Installing${version:+ $version}..."
|
|
784
|
-
tmp_rtk_installer=$(mktemp 2>/dev/null || echo "/tmp/rtk-install-$$.sh")
|
|
785
|
-
curl -fsSL "https://raw.githubusercontent.com/rtk-ai/rtk/$installer_ref/install.sh" -o "$tmp_rtk_installer" \
|
|
786
|
-
&& RTK_VERSION="${version:+v$version}" bash "$tmp_rtk_installer"
|
|
787
|
-
rm -f "$tmp_rtk_installer"
|
|
788
|
-
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
|
789
|
-
if command -v rtk >/dev/null 2>&1; then
|
|
790
|
-
log "RTK ready ($(rtk --version 2>/dev/null || echo 'version unknown'))"
|
|
791
|
-
else
|
|
792
|
-
err "RTK install failed. Install manually: https://github.com/rtk-ai/rtk"
|
|
793
|
-
return 1
|
|
794
|
-
fi
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
claude::sync_rtk() {
|
|
798
|
-
if [[ "${SKIP_RTK:-0}" -eq 1 ]]; then
|
|
799
|
-
# The @RTK.md import strip lives in claude::sync_claude_md (which runs
|
|
800
|
-
# after this and would clobber a strip done here).
|
|
801
|
-
warn "Skipping RTK (--skip-rtk)"
|
|
802
|
-
return
|
|
803
|
-
fi
|
|
804
|
-
|
|
805
|
-
# `|| warn` (not a plain call): under set -e a failed install would otherwise
|
|
806
|
-
# abort the whole sync — and rtk runs FIRST, so nothing would get deployed.
|
|
807
|
-
toolchain::ensure rtk claude::_rtk_install || warn "RTK bootstrap failed — continuing sync without it"
|
|
808
|
-
|
|
809
|
-
command -v rtk >/dev/null 2>&1 || return 0
|
|
810
|
-
if [[ ! -f "$CLAUDE_DIR/RTK.md" ]]; then
|
|
811
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
812
|
-
echo "[dry-run] rtk init --global (RTK.md missing; runs before the settings merge, which normalizes rtk's settings rewrite)"
|
|
813
|
-
return
|
|
814
|
-
fi
|
|
815
|
-
rtk init --global
|
|
816
|
-
log "RTK initialized (RTK.md generated; the following settings merge re-asserts the SoT hooks)"
|
|
817
|
-
elif [[ "$DRY_RUN" -eq 0 ]]; then
|
|
818
|
-
log "RTK already initialized"
|
|
819
|
-
fi
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
claude::summary() {
|
|
823
|
-
local hook_count plugin_count
|
|
824
|
-
|
|
825
|
-
[[ "${CLAUDE_SYNCED:-0}" -eq 1 ]] || return
|
|
826
|
-
echo "Claude: ${CLAUDE_DIR:-$HOME/.claude}"
|
|
827
|
-
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
828
|
-
hook_count=$(find "${CLAUDE_DIR:-$HOME/.claude}/hooks" -maxdepth 1 -name '*.sh' 2>/dev/null | wc -l)
|
|
829
|
-
echo "Hooks: $hook_count scripts"
|
|
830
|
-
if command -v rtk >/dev/null 2>&1; then
|
|
831
|
-
echo "RTK: $(rtk --version 2>/dev/null || echo 'installed')"
|
|
832
|
-
else
|
|
833
|
-
echo "RTK: not installed"
|
|
834
|
-
fi
|
|
835
|
-
if command -v claude >/dev/null 2>&1; then
|
|
836
|
-
plugin_count=$(jq -r '.plugins | keys | length' "${CLAUDE_DIR:-$HOME/.claude}/plugins/installed_plugins.json" 2>/dev/null || echo 0)
|
|
837
|
-
echo "Plugins: $plugin_count installed (from SoT enabledPlugins + Anthropic auto-installs)"
|
|
838
|
-
fi
|
|
839
|
-
fi
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
claude::next_steps() {
|
|
843
|
-
[[ "${CLAUDE_SYNCED:-0}" -eq 1 ]] || return
|
|
844
|
-
echo "In a Claude Code session, run /reload-plugins to pick up newly installed plugins."
|
|
845
|
-
echo "Restart Claude Code for hook/env-var changes to take effect."
|
|
846
|
-
}
|