dotfiles/install.sh
Brydon DeWitt 2a00366204 fix: move more into install.sh
model config, downloads, llama-server service and a README.md to explain --host
2026-06-08 20:38:24 -04:00

252 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# install.sh — Wire .agents/ into global tool configs.
# Run with --host to also install llama-server config and systemd services.
# Idempotent: safe to re-run. Creates dirs, symlinks, and config entries.
# Run once per machine after cloning dotfiles.
set -euo pipefail
INSTALL_HOST=false
for arg in "$@"; do case "$arg" in --host) INSTALL_HOST=true ;; esac; done
DOTFILES_AGENTS="$(cd "$(dirname "$0")" && pwd)/.agents"
DOTFILES_CONFIG="$(cd "$(dirname "$0")" && pwd)/config"
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 config (opencode.json) ────────────────────────────────
OC_CONFIG_SOURCE="$DOTFILES_CONFIG/opencode/opencode.json"
OC_CONFIG_LINK="$HOME/.config/opencode/opencode.json"
mkdir -p "$(dirname "$OC_CONFIG_LINK")"
if [[ -L "$OC_CONFIG_LINK" && "$(readlink "$OC_CONFIG_LINK")" == "$OC_CONFIG_SOURCE" ]]; then
skip "OpenCode config symlink already set: $OC_CONFIG_LINK"
else
ln -sf "$OC_CONFIG_SOURCE" "$OC_CONFIG_LINK"
log "OpenCode config symlink: $OC_CONFIG_LINK$OC_CONFIG_SOURCE"
fi
# ── 5. Llama-server host config (requires --host) ───────────────────────────
if [[ "$INSTALL_HOST" != "true" ]]; then
skip "Llama-server host config skipped (use --host to install)"
else
# ── 5a. Model downloads (requires --host) ──────────────────────────────────
if ! command -v huggingface-cli >/dev/null 2>&1; then
warn "huggingface-cli not found — skipping model downloads (install via 'pip install huggingface_hub')"
else
_hf_download() {
local repo="$1" file="$2" dir="$3"
local dest="$dir/$file"
if [[ -f "$dest" ]]; then
skip "Model already present: $dest"
else
mkdir -p "$dir"
huggingface-cli download "$repo" "$file" --local-dir "$dir" >/dev/null
log "Downloaded model: $repo/$file$dest"
fi
}
_hf_download "Jackrong/Qwopus3.6-27B-v2-MTP-GGUF" "Qwopus3.6-27B-v2-MTP-Q4_K_M.gguf" "$HOME/models"
_hf_download "Jackrong/Qwopus3.5-9B-Coder-MTP-GGUF" "Qwopus3.5-9B-Coder-MTP-Q8_0.gguf" "$HOME/models"
_hf_download "bartowski/agentica-org_DeepCoder-14B-Preview-GGUF" "agentica-org_DeepCoder-14B-Preview-Q5_K_M.gguf" "$HOME/models"
_hf_download "byteshape/Qwen3.6-35B-A3B-MTP-GGUF" "Qwen3.6-35B-A3B-IQ3_S-3.06bpw.gguf" "$HOME/models"
_hf_download "Jackrong/Qwopus3.6-35B-A3B-v1-MTP-GGUF" "Qwopus3.6-35B-A3B-v1-MTP-Q4_K_M.gguf" "$HOME/models"
_hf_download "mradermacher/OmniCoder-2-9B-GGUF" "OmniCoder-2-9B.Q8_0.gguf" "$HOME/models/OmniCoder-2-9B.Q8_0"
_hf_download "mradermacher/OmniCoder-2-9B-GGUF" "mmproj-Q8_0.gguf" "$HOME/models/OmniCoder-2-9B.Q8_0"
_hf_download "bartowski/Qwen_Qwen3-14B-GGUF" "Qwen_Qwen3-14B-Q4_K_M.gguf" "$HOME/models"
_hf_download "bartowski/Qwen_Qwen3.6-27B-GGUF" "Qwen_Qwen3.6-27B-Q4_K_M.gguf" "$HOME/models"
fi
PRESETS_SRC="$DOTFILES_CONFIG/llama-server/presets.ini"
PRESETS_DST="$HOME/models/presets.ini"
mkdir -p "$HOME/models"
if diff -q "$PRESETS_SRC" "$PRESETS_DST" >/dev/null 2>&1; then
skip "presets.ini already up-to-date: $PRESETS_DST"
else
cp "$PRESETS_SRC" "$PRESETS_DST"
log "Installed presets.ini → $PRESETS_DST"
fi
SVC_SRC="$DOTFILES_CONFIG/llama-server/llama-server.service"
SVC_DST="/etc/systemd/system/llama-server.service"
if diff -q "$SVC_SRC" "$SVC_DST" >/dev/null 2>&1; then
skip "llama-server.service already up-to-date: $SVC_DST"
else
cp "$SVC_SRC" "$SVC_DST"
log "Installed llama-server.service → $SVC_DST"
fi
PATH_SRC="$DOTFILES_CONFIG/llama-server/llama-server-presets.path"
PATH_DST="/etc/systemd/system/llama-server-presets.path"
if diff -q "$PATH_SRC" "$PATH_DST" >/dev/null 2>&1; then
skip "llama-server-presets.path already up-to-date: $PATH_DST"
else
cp "$PATH_SRC" "$PATH_DST"
log "Installed llama-server-presets.path → $PATH_DST"
fi
PSVC_SRC="$DOTFILES_CONFIG/llama-server/llama-server-presets.service"
PSVC_DST="/etc/systemd/system/llama-server-presets.service"
if diff -q "$PSVC_SRC" "$PSVC_DST" >/dev/null 2>&1; then
skip "llama-server-presets.service already up-to-date: $PSVC_DST"
else
cp "$PSVC_SRC" "$PSVC_DST"
log "Installed llama-server-presets.service → $PSVC_DST"
fi
START_SRC="$DOTFILES_CONFIG/llama-server/start.sh"
START_DST="/opt/llama-server/start.sh"
mkdir -p "$(dirname "$START_DST")"
if diff -q "$START_SRC" "$START_DST" >/dev/null 2>&1; then
skip "start.sh already up-to-date: $START_DST"
else
cp "$START_SRC" "$START_DST"
log "Installed start.sh → $START_DST"
fi
fi
# ── 6. 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_KEY="all-agents"
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
# ── 7. 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
# ── 8. 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'