#!/usr/bin/env bash # install.sh — Wire ~/dotfiles/.agents/ into global tool configs. # Idempotent: safe to re-run. Creates dirs, symlinks, and config entries. # Run once per machine after cloning dotfiles. set -euo pipefail DOTFILES_AGENTS="$(cd "$(dirname "$0")" && pwd)" log() { printf '\033[0;32m✓\033[0m %s\n' "$1"; } warn() { printf '\033[0;33m⚠\033[0m %s\n' "$1"; } skip() { printf '\033[0;34m–\033[0m %s\n' "$1"; } # ── 1. Copilot global hooks ────────────────────────────────────────────────── # Generate ~/.copilot/hooks/hooks.json with absolute paths so the hooks # work from any workspace — no per-project symlinks or stubs needed. COPILOT_HOOKS_DIR="$HOME/.copilot/hooks" COPILOT_HOOK_FILE="$COPILOT_HOOKS_DIR/hooks.json" mkdir -p "$COPILOT_HOOKS_DIR" # Migrate: remove old symlink if present if [[ -L "$COPILOT_HOOK_FILE" ]]; then rm "$COPILOT_HOOK_FILE" log "Removed old Copilot hook symlink (migrating to generated file)" fi EXPECTED_PRE="$DOTFILES_AGENTS/hooks/pre-tool-use.sh" if [[ -f "$COPILOT_HOOK_FILE" ]] && \ node -e "const c=JSON.parse(require('fs').readFileSync('$COPILOT_HOOK_FILE','utf8')); process.exit(c.hooks&&c.hooks.PreToolUse&&c.hooks.PreToolUse[0].command==='$EXPECTED_PRE'?0:1);" 2>/dev/null; then skip "Copilot global hooks already up-to-date: $COPILOT_HOOK_FILE" else node -e " const fs = require('fs'); const d = '$DOTFILES_AGENTS/hooks'; const hooks = { UserPromptSubmit: [{type:'command',command:d+'/user-prompt-submit.sh',timeout:5}], SessionStart: [{type:'command',command:d+'/session-start.sh',timeout:10}], PreToolUse: [{type:'command',command:d+'/pre-tool-use.sh',timeout:5}], PostToolUse: [{type:'command',command:d+'/post-tool-use.sh',timeout:5}], PreCompact: [{type:'command',command:d+'/pre-compact.sh',timeout:10}], Stop: [{type:'command',command:d+'/stop.sh',timeout:5}] }; fs.writeFileSync('$COPILOT_HOOK_FILE', JSON.stringify({hooks}, null, 2) + '\n'); " log "Copilot global hooks generated with absolute paths: $COPILOT_HOOK_FILE" fi # ── 2. OpenCode global plugin ──────────────────────────────────────────────── OC_PLUGINS_DIR="$HOME/.config/opencode/plugins" OC_PLUGIN_TARGET="$DOTFILES_AGENTS/frameworks/opencode/plugin.ts" OC_PLUGIN_LINK="$OC_PLUGINS_DIR/plugin.ts" mkdir -p "$OC_PLUGINS_DIR" if [[ -L "$OC_PLUGIN_LINK" && "$(readlink "$OC_PLUGIN_LINK")" == "$OC_PLUGIN_TARGET" ]]; then skip "OpenCode plugin symlink already set: $OC_PLUGIN_LINK" else ln -sf "$OC_PLUGIN_TARGET" "$OC_PLUGIN_LINK" log "OpenCode plugin symlink: $OC_PLUGIN_LINK → $OC_PLUGIN_TARGET" fi # ── 3. OpenCode global agents dir ─────────────────────────────────────────── OC_AGENTS_DIR="$HOME/.config/opencode/agents" OC_AGENTS_SOURCE="$DOTFILES_AGENTS/agents" mkdir -p "$OC_AGENTS_DIR" for src in "$OC_AGENTS_SOURCE"/*.md; do name="$(basename "$src")" link="$OC_AGENTS_DIR/$name" if [[ "$name" == "AGENTS.md" ]]; then continue; fi # not a slash-command agent if [[ -L "$link" && "$(readlink "$link")" == "$src" ]]; then skip "OpenCode agent symlink already set: $link" else ln -sf "$src" "$link" log "OpenCode agent symlink: $link → $src" fi done # ── 3a. OpenCode global AGENTS.md ─────────────────────────────────────────── OC_AGENTS_TARGET="$DOTFILES_AGENTS/AGENTS.md" OC_AGENTS_LINK="$HOME/.config/opencode/AGENTS.md" if [[ -L "$OC_AGENTS_LINK" && "$(readlink "$OC_AGENTS_LINK")" == "$OC_AGENTS_TARGET" ]]; then skip "OpenCode AGENTS.md symlink already set: $OC_AGENTS_LINK" else ln -sf "$OC_AGENTS_TARGET" "$OC_AGENTS_LINK" log "OpenCode AGENTS.md symlink: $OC_AGENTS_LINK → $OC_AGENTS_TARGET" fi # ── 4. OpenCode global MCP entry ──────────────────────────────────────────── OC_CONFIG="$HOME/.config/opencode/opencode.json" MCP_KEY="all-agents" MCP_CMD="[\"node\", \"--experimental-strip-types\", \"$DOTFILES_AGENTS/mcp/index.ts\"]" if [[ ! -f "$OC_CONFIG" ]]; then warn "No OpenCode config at $OC_CONFIG — creating minimal config with MCP entry." printf '{\n "$schema": "https://opencode.ai/config.json",\n "mcp": {\n "%s": {\n "type": "local",\n "command": %s\n }\n }\n}\n' "$MCP_KEY" "$MCP_CMD" > "$OC_CONFIG" log "Created $OC_CONFIG with all-agents MCP entry" elif node -e "const c=JSON.parse(require('fs').readFileSync('$OC_CONFIG','utf8')); process.exit(c.mcp && c.mcp['$MCP_KEY'] ? 0 : 1)" 2>/dev/null; then skip "OpenCode MCP entry '$MCP_KEY' already present in $OC_CONFIG" else # Merge the MCP entry using node — jq may not be available everywhere node -e " const fs = require('fs'); const path = '$OC_CONFIG'; const config = JSON.parse(fs.readFileSync(path, 'utf8')); config.mcp = config.mcp || {}; config.mcp['$MCP_KEY'] = { type: 'local', command: $MCP_CMD }; fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\n'); console.log('Merged all-agents MCP entry into ' + path); " log "OpenCode MCP entry merged: $OC_CONFIG" fi # ── 5. VS Code global MCP ──────────────────────────────────────────────────── # Primary remote/server path; falls back to local if running VS Code locally. VSCODE_MCP_PATHS=( "$HOME/.vscode-server/data/User/mcp.json" "$HOME/.vscode/data/User/mcp.json" "$HOME/Library/Application Support/Code/User/mcp.json" ) for VSCODE_MCP in "${VSCODE_MCP_PATHS[@]}"; do if [[ -d "$(dirname "$VSCODE_MCP")" ]]; then MCP_SERVER_CMD="node" MCP_SERVER_ARGS="[\"--experimental-strip-types\", \"$DOTFILES_AGENTS/mcp/index.ts\"]" node -e " const fs = require('fs'); const path = '$VSCODE_MCP'; const config = fs.existsSync(path) ? JSON.parse(fs.readFileSync(path, 'utf8')) : {}; config.servers = config.servers || {}; let changed = false; if (!config.servers['$MCP_KEY']) { config.servers['$MCP_KEY'] = { type: 'stdio', command: '$MCP_SERVER_CMD', args: $MCP_SERVER_ARGS }; changed = true; } if (!config.servers['exa']) { config.servers['exa'] = { type: 'http', url: 'https://mcp.exa.ai/mcp' }; changed = true; } if (changed) { fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\n'); console.log('VS Code MCP config updated: ' + path); } else { process.stdout.write(''); } " log "VS Code MCP entries ensured: $VSCODE_MCP" break fi done # ── 6. VS Code global prompts dir ─────────────────────────────────────────── for VSCODE_PROMPTS_DIR in \ "$HOME/.vscode-server/data/User/prompts" \ "$HOME/.vscode/data/User/prompts"; do if [[ -d "$(dirname "$(dirname "$VSCODE_PROMPTS_DIR")")" ]]; then mkdir -p "$VSCODE_PROMPTS_DIR" log "VS Code prompts dir ensured: $VSCODE_PROMPTS_DIR" break fi done # ── 7. MCP server dependencies ─────────────────────────────────────────────── MCP_DIR="$DOTFILES_AGENTS/mcp" if [[ ! -d "$MCP_DIR/node_modules/@modelcontextprotocol" ]]; then log "Installing MCP server dependencies (npm install in $MCP_DIR)..." npm install --prefix "$MCP_DIR" --silent log "MCP server dependencies installed" else skip "MCP server node_modules already present" fi # ── Done ───────────────────────────────────────────────────────────────────── printf '\n\033[0;32minstall.sh complete.\033[0m\n' printf 'Next steps:\n' printf ' 1. Restart OpenCode to pick up the new global plugin.\n' printf ' 2. Reload VS Code / reconnect to reload MCP servers.\n' printf ' 3. Smoke test: /research slash prompt fires; a denied terminal command is blocked.\n'