@reddb-io/red-skills 3.18.12 → 3.19.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.
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/install-opencode.sh — install the RedSkills opencode-host
3
+ # adapter into a target directory or a user-scoped OpenCode-compatible config.
4
+ #
5
+ # The generated OpenCode surface includes provider/model config, MCP servers,
6
+ # flat skills, plugin event modules, and TUI attention config. The generator
7
+ # emits plugin-local trees under ./dist/opencode/<plugin>/; this script merges
8
+ # them into either <target>/.opencode/ or ~/.config/opencode/.
9
+ #
10
+ # Usage:
11
+ # scripts/install-opencode.sh [TARGET_DIR]
12
+ # scripts/install-opencode.sh --copy [TARGET_DIR]
13
+ # scripts/install-opencode.sh --dry-run [TARGET_DIR]
14
+ # scripts/install-opencode.sh --global [--host opencode|redcode]
15
+ # scripts/install-opencode.sh --uninstall [--global] [TARGET_DIR]
16
+ #
17
+ # Positional TARGET_DIR (default $PWD):
18
+ # The directory to install into. The script writes:
19
+ # <TARGET_DIR>/.opencode/plugin/ (plugin modules)
20
+ # <TARGET_DIR>/.opencode/skills/ (skill symlinks/copies)
21
+ # <TARGET_DIR>/opencode.json (provider + model + MCP servers)
22
+ # <TARGET_DIR>/tui.json (attention sounds/notifications)
23
+ # The user then runs `opencode <TARGET_DIR>` (or `cd <TARGET_DIR>
24
+ # && opencode .`) and opencode auto-loads the .opencode subtree.
25
+ #
26
+ # --global: install into ~/.config/opencode/plugins/ instead. opencode
27
+ # auto-loads .ts files from that directory but does NOT recurse into
28
+ # sub-skills/. The global mode therefore flattens the .opencode/
29
+ # subtree to a single plugin directory. Provider/MCP config is written to
30
+ # ~/.config/opencode/opencode.json(c), and attention config is written to
31
+ # ~/.config/opencode/tui.json(c). Existing config files are backed up first.
32
+ #
33
+ # --copy: copy SKILL.md into the target instead of symlinking. Use
34
+ # this when the source tree is on a different filesystem than
35
+ # $HOME (cross-fs symlinks are not portable).
36
+ #
37
+ # Preconditions:
38
+ # - This script must be run from a clone of the red-skills repo
39
+ # (or with a --config pointing at a .red/config.yaml that
40
+ # already has plugins.dev.enabled: true). The opt-in gate is
41
+ # fail-closed (ADR 0067); a missing enabled: true aborts the
42
+ # install.
43
+ # - pnpm install has been run at least once (the script runs it
44
+ # automatically on the first invocation).
45
+ #
46
+ # The script is idempotent. Re-running replaces the install in place. Uninstall
47
+ # removes only files that are either recorded in the RedSkills manifest or still
48
+ # match RedSkills-generated/source content.
49
+
50
+ set -euo pipefail
51
+
52
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
53
+ TARGET_DIR=""
54
+ MODE="local"
55
+ ACTION="install"
56
+ COPY="false"
57
+ DRY_RUN="false"
58
+ CONFIG_PATH="$REPO_ROOT/.red/config.yaml"
59
+ PLUGINS_ROOT="$REPO_ROOT/plugins"
60
+ OUT_DIR="$REPO_ROOT/dist/opencode"
61
+ PREGENERATED_TREE_MARKER="$OUT_DIR/.release-generated"
62
+ MANIFEST_FILE=""
63
+ MANIFEST_TMP=""
64
+ HOST="opencode"
65
+ HOST_LABEL="OpenCode"
66
+ HOST_COMMAND="opencode"
67
+ HOST_CONFIG_DIR="opencode"
68
+
69
+ usage() {
70
+ cat <<'EOF'
71
+ Usage: scripts/install-opencode.sh [TARGET_DIR]
72
+ [--global]
73
+ [--host opencode|redcode]
74
+ [--uninstall]
75
+ [--copy] [--dry-run]
76
+
77
+ Positional:
78
+ TARGET_DIR install into this directory (default: $PWD)
79
+
80
+ Options:
81
+ --global install into the selected host's user config
82
+ --host <host> OpenCode-compatible host (default: opencode)
83
+ --uninstall remove RedSkills files/config for the selected host
84
+ --copy copy SKILL.md instead of symlinking
85
+ --dry-run print the steps, do not write
86
+
87
+ Examples:
88
+ scripts/install-opencode.sh # install into $PWD
89
+ scripts/install-opencode.sh /path/to/my-project
90
+ scripts/install-opencode.sh --global # user-scoped install
91
+ scripts/install-opencode.sh --global --host redcode # ~/.config/redcode
92
+ scripts/install-opencode.sh --uninstall --global # remove user-scoped install
93
+ scripts/install-opencode.sh --copy /path/to/project # cross-fs safety
94
+ EOF
95
+ }
96
+
97
+ while [ $# -gt 0 ]; do
98
+ case "$1" in
99
+ --global) MODE="global" ;;
100
+ --host)
101
+ [ $# -ge 2 ] || { echo "error: --host requires a value" >&2; exit 2; }
102
+ HOST="$2"
103
+ shift
104
+ ;;
105
+ --uninstall) ACTION="uninstall" ;;
106
+ --copy) COPY="true" ;;
107
+ --dry-run) DRY_RUN="true" ;;
108
+ -h|--help) usage; exit 0 ;;
109
+ -*) echo "error: unknown arg $1" >&2; usage; exit 2 ;;
110
+ *) TARGET_DIR="$1" ;;
111
+ esac
112
+ shift
113
+ done
114
+
115
+ case "$HOST" in
116
+ opencode)
117
+ HOST_LABEL="OpenCode"
118
+ HOST_COMMAND="opencode"
119
+ HOST_CONFIG_DIR="opencode"
120
+ ;;
121
+ redcode)
122
+ HOST_LABEL="RedCode"
123
+ HOST_COMMAND="redcode"
124
+ HOST_CONFIG_DIR="redcode"
125
+ ;;
126
+ *) printf 'install-opencode: unsupported host %s (expected opencode or redcode)\n' "$HOST" >&2; exit 1 ;;
127
+ esac
128
+
129
+ if [ "$MODE" = "local" ] && [ -z "$TARGET_DIR" ]; then
130
+ TARGET_DIR="$PWD"
131
+ fi
132
+
133
+ log() { printf 'install-%s: %s\n' "$HOST" "$*"; }
134
+ die() { printf 'install-%s: %s\n' "$HOST" "$*" >&2; exit 1; }
135
+
136
+ run_rm() {
137
+ path="$1"
138
+ if [ "$DRY_RUN" = "true" ]; then
139
+ log "(dry-run) remove $path"
140
+ return 0
141
+ fi
142
+ rm -rf "$path"
143
+ }
144
+
145
+ begin_manifest() {
146
+ MANIFEST_FILE="$1"
147
+ [ "$DRY_RUN" = "false" ] || return 0
148
+ mkdir -p "$(dirname "$MANIFEST_FILE")"
149
+ MANIFEST_TMP="$MANIFEST_FILE.tmp-$$"
150
+ {
151
+ printf '# RedSkills %s install manifest\n' "$HOST_LABEL"
152
+ printf '# One absolute path per line. Used by scripts/install-opencode.sh --uninstall.\n'
153
+ } > "$MANIFEST_TMP"
154
+ }
155
+
156
+ record_manifest() {
157
+ path="$1"
158
+ [ -n "$MANIFEST_TMP" ] || return 0
159
+ printf '%s\n' "$path" >> "$MANIFEST_TMP"
160
+ }
161
+
162
+ finish_manifest() {
163
+ [ -n "$MANIFEST_TMP" ] || return 0
164
+ mv "$MANIFEST_TMP" "$MANIFEST_FILE"
165
+ log "wrote uninstall manifest $MANIFEST_FILE"
166
+ }
167
+
168
+ remove_manifest_paths() {
169
+ manifest="$1"
170
+ allowed_root="$2"
171
+ [ -f "$manifest" ] || return 1
172
+
173
+ while IFS= read -r path || [ -n "$path" ]; do
174
+ case "$path" in
175
+ ""|\#*) continue ;;
176
+ "$allowed_root"/*)
177
+ if [ -e "$path" ] || [ -L "$path" ]; then
178
+ run_rm "$path"
179
+ fi
180
+ ;;
181
+ *) log "(note) skipped manifest path outside $allowed_root: $path" ;;
182
+ esac
183
+ done < "$manifest"
184
+
185
+ run_rm "$manifest"
186
+ return 0
187
+ }
188
+
189
+ remove_generated_config() {
190
+ file="$1"
191
+ [ -f "$file" ] || return 0
192
+ if head -n 1 "$file" | grep -Fq 'generated by @reddb-io/red-skills'; then
193
+ run_rm "$file"
194
+ else
195
+ log "(note) kept $file because it is not a RedSkills-generated config"
196
+ fi
197
+ }
198
+
199
+ write_expected_tui() {
200
+ out="$1"
201
+ cat > "$out" <<'TUI'
202
+ {
203
+ "$schema": "https://opencode.ai/tui.json",
204
+ "attention": {
205
+ "enabled": true,
206
+ "notifications": true,
207
+ "sound": true,
208
+ "volume": 0.4
209
+ }
210
+ }
211
+ TUI
212
+ }
213
+
214
+ remove_redskills_tui() {
215
+ file="$1"
216
+ [ -f "$file" ] || return 0
217
+ tmp="$(mktemp)"
218
+ write_expected_tui "$tmp"
219
+ if cmp -s "$file" "$tmp"; then
220
+ rm -f "$tmp"
221
+ run_rm "$file"
222
+ else
223
+ rm -f "$tmp"
224
+ log "(note) kept $file because it no longer exactly matches the RedSkills TUI template"
225
+ fi
226
+ }
227
+
228
+ remove_matching_source_skills() {
229
+ target_skills_dir="$1"
230
+ [ -d "$target_skills_dir" ] || return 0
231
+ [ -d "$PLUGINS_ROOT" ] || {
232
+ log "(note) no source plugins tree available; only manifest-recorded skills can be removed"
233
+ return 0
234
+ }
235
+
236
+ ordered_plugins=""
237
+ for preferred in dev memory brain; do
238
+ [ -d "$PLUGINS_ROOT/$preferred" ] && ordered_plugins="$ordered_plugins $preferred"
239
+ done
240
+ for plugin_dir in "$PLUGINS_ROOT"/*; do
241
+ [ -d "$plugin_dir" ] || continue
242
+ plugin="$(basename "$plugin_dir")"
243
+ case " $ordered_plugins " in
244
+ *" $plugin "*) ;;
245
+ *) ordered_plugins="$ordered_plugins $plugin" ;;
246
+ esac
247
+ done
248
+
249
+ for plugin in $ordered_plugins; do
250
+ [ -d "$PLUGINS_ROOT/$plugin/skills" ] || continue
251
+ while IFS= read -r source_skill; do
252
+ skill_name="$(basename "$(dirname "$source_skill")")"
253
+ target_skill="$target_skills_dir/$skill_name/SKILL.md"
254
+ if [ -e "$target_skill" ] && cmp -s "$target_skill" "$source_skill"; then
255
+ run_rm "$target_skills_dir/$skill_name"
256
+ fi
257
+ done < <(find "$PLUGINS_ROOT/$plugin/skills" -name SKILL.md -type f | sort)
258
+ done
259
+ }
260
+
261
+ remove_local_install() {
262
+ TARGET_OC="$TARGET_DIR/.opencode"
263
+ manifest="$TARGET_OC/redskills-install-manifest.txt"
264
+ remove_manifest_paths "$manifest" "$TARGET_DIR" || true
265
+
266
+ if [ -d "$TARGET_OC/plugin" ]; then
267
+ for file in "$TARGET_OC/plugin"/*.ts; do
268
+ [ -f "$file" ] || continue
269
+ if grep -Fq 'generated by @reddb-io/red-skills' "$file"; then
270
+ run_rm "$file"
271
+ fi
272
+ done
273
+ fi
274
+
275
+ remove_matching_source_skills "$TARGET_OC/skills"
276
+ remove_generated_config "$TARGET_DIR/opencode.json"
277
+ remove_generated_config "$TARGET_DIR/opencode.jsonc"
278
+ remove_redskills_tui "$TARGET_DIR/tui.json"
279
+ remove_redskills_tui "$TARGET_DIR/tui.jsonc"
280
+ log "done. removed RedSkills $HOST_LABEL files from $TARGET_DIR"
281
+ }
282
+
283
+ remove_global_install() {
284
+ XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
285
+ OPENCODE_ROOT="$XDG_CONFIG_HOME/$HOST_CONFIG_DIR"
286
+ GLOBAL_PLUGINS_DIR="$OPENCODE_ROOT/plugins"
287
+ GLOBAL_SKILLS_DIR="$OPENCODE_ROOT/skills"
288
+ manifest="$OPENCODE_ROOT/redskills-install-manifest.txt"
289
+
290
+ remove_manifest_paths "$manifest" "$OPENCODE_ROOT" || true
291
+
292
+ for file in "$GLOBAL_PLUGINS_DIR"/redskills-*.ts; do
293
+ [ -e "$file" ] || continue
294
+ run_rm "$file"
295
+ done
296
+
297
+ remove_matching_source_skills "$GLOBAL_SKILLS_DIR"
298
+ remove_generated_config "$OPENCODE_ROOT/opencode.json"
299
+ remove_generated_config "$OPENCODE_ROOT/opencode.jsonc"
300
+ remove_redskills_tui "$OPENCODE_ROOT/tui.json"
301
+ remove_redskills_tui "$OPENCODE_ROOT/tui.jsonc"
302
+ log "done. removed RedSkills $HOST_LABEL files from $OPENCODE_ROOT"
303
+ }
304
+
305
+ if [ "$ACTION" = "uninstall" ]; then
306
+ if [ "$MODE" = "local" ]; then
307
+ remove_local_install
308
+ else
309
+ remove_global_install
310
+ fi
311
+ exit 0
312
+ fi
313
+
314
+ # 0. Preconditions
315
+ [ -f "$CONFIG_PATH" ] || die "config not found at $CONFIG_PATH — run from a red-skills clone"
316
+ [ -d "$PLUGINS_ROOT" ] || die "plugins tree not found at $PLUGINS_ROOT"
317
+
318
+ # 1. resolve the generator (release bundle preferred, tsx fallback)
319
+ BUNDLE="$REPO_ROOT/dist/opencode-host.bundle.min.mjs"
320
+ if [ "$DRY_RUN" = "true" ]; then
321
+ GENERATOR="(tsx $REPO_ROOT/apps/opencode-host/src/generate.ts)"
322
+ elif [ -f "$BUNDLE" ]; then
323
+ GENERATOR="node $BUNDLE"
324
+ else
325
+ # Build-from-source fallback for checkout installs. The universal curl
326
+ # installer downloads the release asset above, so normal user installs do not
327
+ # need pnpm unless the asset is unavailable.
328
+ if [ ! -d "$REPO_ROOT/node_modules" ]; then
329
+ log "running pnpm install (first run)"
330
+ (cd "$REPO_ROOT" && pnpm install --frozen-lockfile 2>/dev/null || pnpm install) >/dev/null
331
+ fi
332
+ log "bundle missing — building"
333
+ (cd "$REPO_ROOT" && pnpm --filter @reddb-io/red-skills bundle) >/dev/null
334
+ GENERATOR="node $BUNDLE"
335
+ fi
336
+
337
+ # 2. prepare the dist tree. Release installs prefer the pre-generated tree
338
+ # extracted by scripts/install.sh; source checkouts regenerate from the current
339
+ # tree so local development stays fresh.
340
+ if [ -f "$PREGENERATED_TREE_MARKER" ]; then
341
+ log "using release-generated dist tree under $OUT_DIR"
342
+ else
343
+ GEN_ARGS=(--config "$CONFIG_PATH" --plugins-root "$PLUGINS_ROOT" --out-dir "$OUT_DIR")
344
+ [ "$COPY" = "true" ] && GEN_ARGS+=(--copy)
345
+
346
+ log "generating dist tree under $OUT_DIR"
347
+ if [ "$DRY_RUN" = "true" ]; then
348
+ log "(dry-run) $GENERATOR ${GEN_ARGS[*]}"
349
+ else
350
+ (cd "$REPO_ROOT" && $GENERATOR "${GEN_ARGS[@]}")
351
+ fi
352
+ fi
353
+
354
+ # 3. discover the plugins that have a dist subtree
355
+ PLUGINS=""
356
+ for d in "$OUT_DIR"/*/; do
357
+ [ -d "$d" ] || continue
358
+ name=$(basename "$d")
359
+ [ -d "$d/.opencode" ] && PLUGINS="$PLUGINS $name"
360
+ done
361
+ PLUGINS="${PLUGINS# }"
362
+ [ -n "$PLUGINS" ] || die "no plugin dist trees under $OUT_DIR — the generator emitted nothing"
363
+
364
+ ORDERED_PLUGINS=""
365
+ for preferred in dev memory brain; do
366
+ case " $PLUGINS " in
367
+ *" $preferred "*) ORDERED_PLUGINS="$ORDERED_PLUGINS $preferred" ;;
368
+ esac
369
+ done
370
+ for plugin in $PLUGINS; do
371
+ case " $ORDERED_PLUGINS " in
372
+ *" $plugin "*) ;;
373
+ *) ORDERED_PLUGINS="$ORDERED_PLUGINS $plugin" ;;
374
+ esac
375
+ done
376
+ PLUGINS="${ORDERED_PLUGINS# }"
377
+
378
+ # 5. install into the target
379
+ if [ "$MODE" = "local" ]; then
380
+ TARGET_OC="$TARGET_DIR/.opencode"
381
+ TARGET_CFG="$TARGET_DIR/opencode.json"
382
+ if [ "$DRY_RUN" = "false" ]; then
383
+ mkdir -p "$TARGET_OC/plugin" "$TARGET_OC/skills"
384
+ begin_manifest "$TARGET_OC/redskills-install-manifest.txt"
385
+ fi
386
+
387
+ for plugin in $PLUGINS; do
388
+ SRC="$OUT_DIR/$plugin/.opencode"
389
+ if [ "$DRY_RUN" = "true" ]; then
390
+ log "(dry-run) merge $SRC into $TARGET_OC/"
391
+ continue
392
+ fi
393
+ # Merge: skill symlinks/copies and plugin modules go into the
394
+ # shared .opencode/ subtree. Multiple RedSkills plugins share
395
+ # one .opencode/ root; a skill with the same name from two
396
+ # plugins is a build error and would have been caught by the
397
+ # Slice 2 name validation.
398
+ [ -d "$SRC/skills" ] && for skill_dir in "$SRC/skills"/*/; do
399
+ [ -d "$skill_dir" ] || continue
400
+ skill_name="$(basename "$skill_dir")"
401
+ dst="$TARGET_OC/skills/$skill_name"
402
+ if [ -e "$dst" ]; then
403
+ log "(note) skill $skill_name already existed in $TARGET_OC/skills/ — left in place"
404
+ else
405
+ cp -R "$skill_dir" "$dst"
406
+ fi
407
+ [ -e "$dst/SKILL.md" ] && cmp -s "$dst/SKILL.md" "$skill_dir/SKILL.md" && record_manifest "$dst"
408
+ done
409
+ [ -d "$SRC/plugin" ] && for src_ts in "$SRC/plugin"/*.ts; do
410
+ [ -f "$src_ts" ] || continue
411
+ dst="$TARGET_OC/plugin/$(basename "$src_ts")"
412
+ cp "$src_ts" "$dst"
413
+ record_manifest "$dst"
414
+ done
415
+ log "merged $plugin into $TARGET_OC/"
416
+ done
417
+
418
+ # Slice 1+3: write the standalone opencode.json (provider block +
419
+ # MCP servers from every installed plugin, deduplicated). Slice 1
420
+ # is now part of Slice 1+3 — the generator emits a single file
421
+ # with `provider> + mcp:`, and the install script writes that
422
+ # to the target root.
423
+ if [ "$DRY_RUN" = "true" ]; then
424
+ log "(dry-run) write $TARGET_CFG (Slice 1+3 provider block + MCP servers)"
425
+ else
426
+ STANDALONE_OUT="$TARGET_CFG.tmp-$$"
427
+ (cd "$REPO_ROOT" && $GENERATOR --config "$CONFIG_PATH" --plugins-root "$PLUGINS_ROOT" --out "$STANDALONE_OUT") >/dev/null
428
+ if [ -f "$STANDALONE_OUT" ]; then
429
+ mv "$STANDALONE_OUT" "$TARGET_CFG"
430
+ log "wrote $TARGET_CFG (provider block + MCP servers from $(echo $PLUGINS | wc -w) plugin(s))"
431
+ else
432
+ cp "$OUT_DIR/dev/opencode.json" "$TARGET_CFG"
433
+ log "(warning) standalone emit failed; fell back to per-plugin opencode.json"
434
+ fi
435
+ fi
436
+
437
+ # Slice 4: write tui.json to the project root with attention
438
+ # enabled (built-in done/error/permission/question sounds +
439
+ # notifications). The user can rename it to tui.jsonc and move
440
+ # it to ~/.config/opencode/ to make it user-scoped.
441
+ TARGET_TUI="$TARGET_DIR/tui.json"
442
+ if [ "$DRY_RUN" = "true" ]; then
443
+ log "(dry-run) write $TARGET_TUI (Slice 4: tui.json with attention.enabled)"
444
+ else
445
+ cat > "$TARGET_TUI" <<'TUI'
446
+ {
447
+ "$schema": "https://opencode.ai/tui.json",
448
+ "attention": {
449
+ "enabled": true,
450
+ "notifications": true,
451
+ "sound": true,
452
+ "volume": 0.4
453
+ }
454
+ }
455
+ TUI
456
+ log "wrote $TARGET_TUI (Slice 4: tui.json with attention.enabled; move to ~/.config/opencode/ to make it user-scoped)"
457
+ fi
458
+
459
+ finish_manifest
460
+ log "done. run: cd $TARGET_DIR && $HOST_COMMAND ."
461
+ else
462
+ # global mode
463
+ XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
464
+ HOST_ROOT="$XDG_CONFIG_HOME/$HOST_CONFIG_DIR"
465
+ GLOBAL_PLUGINS_DIR="$HOST_ROOT/plugins"
466
+ GLOBAL_SKILLS_DIR="$HOST_ROOT/skills"
467
+ GLOBAL_CFG="$HOST_ROOT/opencode.json"
468
+ [ -f "$HOST_ROOT/opencode.jsonc" ] && GLOBAL_CFG="$HOST_ROOT/opencode.jsonc"
469
+ declare -A INSTALLED_SKILLS=()
470
+
471
+ if [ "$DRY_RUN" = "true" ]; then
472
+ log "(dry-run) flatten each plugin module into $GLOBAL_PLUGINS_DIR/redskills-<plugin>-<event>.ts"
473
+ log "(dry-run) flatten each skill into $GLOBAL_SKILLS_DIR/<name>/SKILL.md"
474
+ log "(dry-run) merge provider block into $GLOBAL_CFG"
475
+ else
476
+ mkdir -p "$GLOBAL_PLUGINS_DIR" "$GLOBAL_SKILLS_DIR"
477
+ begin_manifest "$HOST_ROOT/redskills-install-manifest.txt"
478
+ for plugin in $PLUGINS; do
479
+ SRC="$OUT_DIR/$plugin/.opencode"
480
+ [ -d "$SRC/plugin" ] && for src_ts in "$SRC/plugin"/*.ts; do
481
+ [ -f "$src_ts" ] || continue
482
+ event=$(basename "$src_ts" .ts)
483
+ dst="$GLOBAL_PLUGINS_DIR/redskills-$plugin-$event.ts"
484
+ cp "$src_ts" "$dst"
485
+ record_manifest "$dst"
486
+ log "installed $plugin/$event -> $dst"
487
+ done
488
+ [ -d "$SRC/skills" ] && for skill_dir in "$SRC/skills"/*/; do
489
+ [ -d "$skill_dir" ] || continue
490
+ skill_name=$(basename "$skill_dir")
491
+ if [ -n "${INSTALLED_SKILLS[$skill_name]:-}" ]; then
492
+ log "(note) duplicate skill $skill_name from $plugin skipped; already installed from ${INSTALLED_SKILLS[$skill_name]}"
493
+ continue
494
+ fi
495
+ dst="$GLOBAL_SKILLS_DIR/$skill_name"
496
+ rm -rf "$dst"
497
+ cp -R "$skill_dir" "$dst"
498
+ INSTALLED_SKILLS[$skill_name]="$plugin"
499
+ record_manifest "$dst"
500
+ log "installed skill $skill_name -> $dst"
501
+ done
502
+ done
503
+
504
+ # Merge provider block + MCP servers into the user's
505
+ # opencode.json(c). The generator emits a single standalone
506
+ # file with `provider> + mcp:` (Slice 1+3) for the global
507
+ # install; the per-plugin dist files are not used here because
508
+ # the global config is a single file, not a per-plugin tree.
509
+ if [ -f "$GLOBAL_CFG" ] && [ -s "$GLOBAL_CFG" ] && [ "$(cat "$GLOBAL_CFG" 2>/dev/null)" != "{}" ]; then
510
+ backup="$GLOBAL_CFG.backup-$(date +%Y%m%d%H%M%S)"
511
+ cp "$GLOBAL_CFG" "$backup"
512
+ log "backed up existing $GLOBAL_CFG -> $backup"
513
+ fi
514
+ STANDALONE_OUT="$GLOBAL_CFG.tmp-$$"
515
+ (cd "$REPO_ROOT" && $GENERATOR --config "$CONFIG_PATH" --plugins-root "$PLUGINS_ROOT" --out "$STANDALONE_OUT") >/dev/null
516
+ if [ -f "$STANDALONE_OUT" ]; then
517
+ mv "$STANDALONE_OUT" "$GLOBAL_CFG"
518
+ log "wrote $GLOBAL_CFG (provider block + MCP servers)"
519
+ else
520
+ cp "$OUT_DIR/dev/opencode.json" "$GLOBAL_CFG"
521
+ log "(warning) standalone emit failed; fell back to per-plugin opencode.json"
522
+ fi
523
+
524
+ # Slice 4: merge the tui.json with attention.enabled. opencode
525
+ # reads tui.json (or tui.jsonc) at the selected host config root
526
+ # on every session start, so this enables the built-in
527
+ # done / error / permission / question / subagent_done
528
+ # sounds + notifications globally.
529
+ GLOBAL_TUI="$HOST_ROOT/tui.json"
530
+ [ -f "$HOST_ROOT/tui.jsonc" ] && GLOBAL_TUI="$HOST_ROOT/tui.jsonc"
531
+ if [ "$DRY_RUN" = "true" ]; then
532
+ log "(dry-run) write $GLOBAL_TUI (Slice 4: tui.json with attention.enabled)"
533
+ else
534
+ if [ -f "$GLOBAL_TUI" ] && [ -s "$GLOBAL_TUI" ] && [ "$(cat "$GLOBAL_TUI" 2>/dev/null)" != "{}" ]; then
535
+ backup="$GLOBAL_TUI.backup-$(date +%Y%m%d%H%M%S)"
536
+ cp "$GLOBAL_TUI" "$backup"
537
+ log "backed up existing $GLOBAL_TUI -> $backup"
538
+ fi
539
+ cat > "$GLOBAL_TUI" <<'TUI'
540
+ {
541
+ "$schema": "https://opencode.ai/tui.json",
542
+ "attention": {
543
+ "enabled": true,
544
+ "notifications": true,
545
+ "sound": true,
546
+ "volume": 0.4
547
+ }
548
+ }
549
+ TUI
550
+ log "wrote $GLOBAL_TUI (Slice 4: tui.json with attention.enabled)"
551
+ fi
552
+ finish_manifest
553
+ fi
554
+
555
+ log "done. $HOST_COMMAND in any directory now picks up the same model + plugins."
556
+ fi