dotfiles/install.sh

419 lines
18 KiB
Bash
Executable File
Raw Permalink 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, VS Code, Docker, and extensions.
# 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"
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_AGENTS/frameworks/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. Build llama-server (requires --host) ──────────────────────────────────
if [[ "$INSTALL_HOST" != "true" ]]; then
skip "llama-server build skipped (use --host to install)"
else
if [[ -x /opt/llama-server/llama-server ]]; then
skip "llama-server already installed at /opt/llama-server/llama-server"
else
sudo apt-get install -y cmake build-essential nvidia-cuda-toolkit libgomp1 git
(
git clone --depth 1 --branch b9279 https://github.com/ggml-org/llama.cpp.git /tmp/llama-build
cd /tmp/llama-build
cmake -B build \
-DGGML_CUDA=ON \
-DCMAKE_BUILD_TYPE=Release \
-DLLAMA_BUILD_SERVER=ON \
-DLLAMA_BUILD_TESTS=OFF \
-DLLAMA_BUILD_EXAMPLES=OFF
cmake --build build --config Release -j$(nproc)
sudo mkdir -p /opt/llama-server
sudo cp build/bin/llama-server /opt/llama-server/
sudo cp -P build/bin/libggml*.so* /opt/llama-server/
sudo cp -P build/bin/libllama*.so* /opt/llama-server/
sudo cp -P build/bin/libmtmd*.so* /opt/llama-server/ 2>/dev/null || true
echo "/opt/llama-server" | sudo tee /etc/ld.so.conf.d/llama-server.conf
sudo ldconfig
rm -rf /tmp/llama-build
)
log "llama-server built and installed to /opt/llama-server/"
fi
fi
# ── 6. Llama-server host config (requires --host) ───────────────────────────
if [[ "$INSTALL_HOST" != "true" ]]; then
skip "Llama-server host config skipped (use --host to install)"
else
# ── 6a. 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_AGENTS/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_AGENTS/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_AGENTS/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_AGENTS/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_AGENTS/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
# ── 7. 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
# ── 8. 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
# ── 9. 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
# ── 10. VS Code, Docker, extensions (requires --host) ────────────────────────
if [[ "$INSTALL_HOST" != "true" ]]; then
skip "VS Code, Docker, extensions skipped (use --host to install)"
else
# ── 10a. VS Code ──────────────────────────────────────────────────────
if command -v code >/dev/null 2>&1; then
skip "VS Code already installed"
else
log "Installing VS Code..."
sudo apt-get update
sudo apt-get install -y wget gpg
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | \
gpg --dearmor -o /usr/share/keyrings/packages.microsoft.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | \
sudo tee /etc/apt/sources.list.d/vscode.list
sudo apt-get update
sudo apt-get install -y code
log "VS Code installed"
fi
# ── 10b. Docker ──────────────────────────────────────────────────────
if command -v docker >/dev/null 2>&1; then
skip "Docker already installed"
else
log "Installing Docker..."
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
log "Docker installed"
fi
# ── 10c. VS Code extensions ──────────────────────────────────────────
_install_ext() {
local ext_id="$1"
if code --list-extensions 2>/dev/null | grep -qi "^${ext_id}$"; then
skip "VS Code extension already installed: $ext_id"
else
code --install-extension "$ext_id" >/dev/null 2>&1
log "VS Code extension installed: $ext_id"
fi
}
_install_ext "ms-vscode-remote.vscode-remote-extensionpack"
_install_ext "ms-azuretools.vscode-docker"
_install_ext "streetsidesoftware.code-spell-checker"
_install_ext "EditorConfig.EditorConfig"
_install_ext "dbaeumer.vscode-eslint"
_install_ext "mhutchie.git-graph"
_install_ext "bierner.github-markdown-preview"
_install_ext "esbenp.prettier-vscode"
_install_ext "yoavbls.pretty-ts-errors"
fi
# ── 11. SSH keys, agent, forwarding & .bashrc (requires --host) ───────────────
if [[ "$INSTALL_HOST" != "true" ]]; then
skip "SSH & .bashrc setup skipped (use --host to install)"
else
# ── 11a. SSH directory & keys ──────────────────────────────────────────
mkdir -p "$HOME/.ssh"
if ! ls "$HOME/.ssh/"* >/dev/null 2>&1; then
warn "No SSH keys found in ~/.ssh — please generate one manually:"
warn " ssh-keygen -t ed25519 -C \"your@email.com\""
warn " ssh-add ~/.ssh/id_ed25519"
else
# Fix permissions on existing keys
for key in "$HOME/.ssh"/*; do
if [[ -f "$key" ]] && [[ ! -d "$key" ]]; then
basename_key="$(basename "$key")"
# Private keys (id_* without .pub/.cer) and config should be 600
if [[ "$basename_key" =~ ^id_ ]] && [[ ! "$basename_key" =~ \.(pub|cer)$ ]]; then
chmod 600 "$key"
log "Fixed SSH key permissions: $key (600)"
elif [[ "$basename_key" == "config" ]]; then
chmod 600 "$key"
log "Fixed SSH config permissions: $key (600)"
elif [[ "$basename_key" =~ \.(pub|cer)$ ]]; then
chmod 644 "$key"
log "Fixed SSH public key permissions: $key (644)"
fi
fi
done
chmod 700 "$HOME/.ssh"
log "SSH directory permissions set (700)"
fi
# ── 11b. SSH agent & forwarding & PATH in .bashrc ─────────────────────
BASHRC="$HOME/.bashrc"
SSH_AGENT_MARKER="# --- install.sh: ssh-agent ---"
if ! grep -qF "$SSH_AGENT_MARKER" "$BASHRC" 2>/dev/null; then
cat >> "$BASHRC" << 'BASHRC_EOF'
# --- install.sh: ssh-agent ---
# Start ssh-agent if not already running
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" >/dev/null 2>&1
fi
# --- install.sh: ssh forwarding ---
# SSH agent forwarding: use 'ssh -A host' to forward agent to remote host.
# To auto-forward on specific hosts, add to ~/.ssh/config:
# Host example.com
# ForwardAgent yes
# --- install.sh: PATH additions ---
# llama-server
export PATH="/opt/llama-server:$PATH"
# WSL lib (for CUDA/cuDNN libraries in WSL environments)
if [ -d "/usr/lib/wsl/lib" ]; then
export LD_LIBRARY_PATH="/usr/lib/wsl/lib:$LD_LIBRARY_PATH"
fi
BASHRC_EOF
log "Added ssh-agent, forwarding notes, and PATH to $BASHRC"
else
skip "ssh-agent snippet already in $BASHRC"
fi
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'