Instead of symlinking ~/.copilot/hooks/agent-support.json to dotfiles hooks.json (which uses relative paths resolved from workspace root), generate the file at install time with absolute paths to dotfiles hooks. This means projects no longer need per-project hook stubs or symlinks. A project only needs .agents/hooks/post-tool-use-remnant.sh (or similar) for its own overlay, wired as a second PostToolUse in .github/hooks/.
162 lines
7.7 KiB
Bash
Executable File
162 lines
7.7 KiB
Bash
Executable File
#!/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/agent-support.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/agent-support.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/agent-support.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.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\"]"
|
||
|
||
if [[ ! -f "$VSCODE_MCP" ]]; then
|
||
printf '{\n "servers": {\n "%s": {\n "type": "stdio",\n "command": "%s",\n "args": %s\n }\n }\n}\n' \
|
||
"$MCP_KEY" "$MCP_SERVER_CMD" "$MCP_SERVER_ARGS" > "$VSCODE_MCP"
|
||
log "Created VS Code global MCP config: $VSCODE_MCP"
|
||
elif node -e "const c=JSON.parse(require('fs').readFileSync('$VSCODE_MCP','utf8')); process.exit(c.servers && c.servers['$MCP_KEY'] ? 0 : 1)" 2>/dev/null; then
|
||
skip "VS Code MCP entry '$MCP_KEY' already present in $VSCODE_MCP"
|
||
else
|
||
node -e "
|
||
const fs = require('fs');
|
||
const path = '$VSCODE_MCP';
|
||
const config = JSON.parse(fs.readFileSync(path, 'utf8'));
|
||
config.servers = config.servers || {};
|
||
config.servers['$MCP_KEY'] = {
|
||
type: 'stdio',
|
||
command: '$MCP_SERVER_CMD',
|
||
args: $MCP_SERVER_ARGS
|
||
};
|
||
fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\n');
|
||
"
|
||
log "VS Code MCP entry merged: $VSCODE_MCP"
|
||
fi
|
||
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'
|