Installer Workmanship
Core Principle: Study
/dp/destructive_command_guard/install.shand/dp/remote_compilation_helper/install.shand emulate them in every respect. These are the gold standard. Every installer you write must match their quality.
The Non-Negotiables
Every installer MUST have ALL of these. No exceptions. No "we'll add it later."
| Feature | Why | Reference |
|---|---|---|
set -euo pipefail |
Fail fast, catch errors | DCG line 24 |
shopt -s lastpipe 2>/dev/null |
read in pipelines works |
RCH line 7 |
| Curl one-liner header with cache buster | Install docs in script | DOWNLOAD-PATTERNS.md |
Proxy support (PROXY_ARGS array) |
Corporate networks | DOWNLOAD-PATTERNS.md |
| Gum detection + ANSI fallback | Beautiful everywhere | DCG lines 53-107 |
draw_box() with box-drawing chars |
Professional headers | DCG lines 109-156 |
info/ok/warn/err log functions |
Consistent formatting | DCG lines 59-95 |
run_with_spinner() |
Show progress on slow ops | DCG lines 97-107 |
| Branded header banner | First impression | DCG lines 783-798 |
| Platform detection (OS + arch) | Cross-platform | DCG lines 439-461 |
| Preflight checks (disk, perms, net) | Fail early, fail clearly | DCG lines 544-550 |
| Atomic locking (mkdir-based) | No concurrent installs | DCG lines 870-892 |
| Checksum verification (SHA256) | Supply chain security | DCG lines 665-700 |
| Sigstore/cosign verification | Authenticity | DCG lines 703-737 |
| Build-from-source fallback | Works when binaries fail | DCG lines 910-927 |
| Shell completions install | Polish | DCG lines 578-644 |
| AI agent auto-configuration | Zero-friction setup | DCG lines 1069-1865 |
| Skill installation (tarball + inline fallback) | Agent knowledge | RCH lines 1070-1236 |
| Final summary with status per-agent | User knows what happened | DCG lines 1941-2116 |
| Uninstall instructions | Reversibility | DCG lines 2103-2115 |
--quiet, --no-gum, --force flags |
Flexibility | DCG lines 28-51 |
--offline TARBALL airgap mode |
Works without internet | DOWNLOAD-PATTERNS.md |
trap cleanup EXIT |
Never leave temp files | DCG lines 894-900 |
umask 022 |
Sane file permissions | DCG line 25 |
THE PLAN
Follow this exact sequence when building an installer. Do NOT skip steps. Do NOT reorder.
Phase 1: Scaffold
1. Copy the DCG installer structure (header, flags, logging, draw_box)
2. Adapt project name, repo URL, binary names
3. Wire up --help/usage with ALL flags documented
Phase 2: Core
4. Platform detection (OS + arch → Rust triple, WSL detection)
5. Proxy detection (HTTPS_PROXY → HTTP_PROXY → PROXY_ARGS array)
6. Version resolution (CLI flag → Cargo.toml → GitHub API → redirect → hardcoded)
7. Artifact URL construction (4-tier fallback chain)
8. Preflight checks (disk space, write perms, network, existing install)
9. Atomic locking (mkdir-based, stale PID detection)
10. Download + extract (with build-from-source fallback)
11. Checksum verification (dual tool: sha256sum/shasum + Sigstore)
12. Install binary with `install -m 0755`
Phase 3: Integration
13. Shell completions (bash/zsh/fish, XDG paths)
14. PATH setup (--easy-mode auto-updates rc files)
15. Service management if daemon (systemd user + launchd plist)
16. AI agent detection (Claude Code, Codex, Gemini, Cursor, etc.)
17. Agent hook auto-configuration (PreToolUse/BeforeTool hooks)
18. Skill installation (tarball from releases, inline heredoc fallback)
Phase 4: Polish
19. Self-test / --verify flag (post-install diagnostics)
20. Predecessor detection and migration
21. Final summary box (per-agent status, backup locations)
22. Uninstall/revert instructions
Reference Installers
ALWAYS read these before writing ANY installer. They are the canonical examples.
# THE gold standard (2116 lines)
cat /dp/destructive_command_guard/install.sh
# The companion reference (2600+ lines)
cat /dp/remote_compilation_helper/install.sh
What to steal from DCG
- Gum + ANSI dual-path output system
draw_box()with automatic width calculation and ANSI stripping- Agent detection that scans for 7 agents with version reporting
- JSON settings merging via embedded Python3 (with backup + rollback)
- Version-already-installed short-circuit (skip download, still configure)
- Predecessor detection with upgrade banner showing feature comparison
What to steal from RCH
- ASCII art banner with project name
- Multi-mode installer (
--local/--worker/--from-source) install_skill()with tarball primary + inline heredoc fallback- Service management (systemd user service + macOS launchd plist)
- Post-install diagnostics (
rch doctor) - Fleet harmonization (deploy to remote workers after local install)
- Proxy support (
HTTP_PROXY/HTTPS_PROXY)
Output & Formatting
The Output Stack (Required)
# Detect gum (https://github.com/charmbracelet/gum)
HAS_GUM=0
if command -v gum &> /dev/null && [ -t 1 ]; then
HAS_GUM=1
fi
# Every function: gum path + ANSI fallback
info() {
[ "$QUIET" -eq 1 ] && return 0
if [ "$HAS_GUM" -eq 1 ] && [ "$NO_GUM" -eq 0 ]; then
gum style --foreground 39 "-> $*"
else
echo -e "\033[0;34m->\033[0m $*"
fi
}
ok() { ... --foreground 42 ... "\033[0;32m" ... } # green checkmark
warn() { ... --foreground 214 ... "\033[1;33m" ... } # yellow warning
err() { ... --foreground 196 ... "\033[0;31m" ... } # red X (NO quiet gate)
run_with_spinner() {
local title="$1"; shift
if [ "$HAS_GUM" -eq 1 ] && [ "$NO_GUM" -eq 0 ] && [ "$QUIET" -eq 0 ]; then
gum spin --spinner dot --title "$title" -- "$@"
else
info "$title"
"$@"
fi
}
Box Drawing (Required)
draw_box() {
local color="$1"; shift
local lines=("$@")
# Strip ANSI for width calculation
# Use double-line box chars: ══ ╔ ╗ ║ ╚ ╝
# See DCG lines 109-156 for full implementation
}
Header Banner (Required)
if [ "$HAS_GUM" -eq 1 ] && [ "$NO_GUM" -eq 0 ]; then
gum style \
--border normal --border-foreground 39 \
--padding "0 1" --margin "1 0" \
"$(gum style --foreground 42 --bold 'project-name installer')" \
"$(gum style --foreground 245 'One-line description')"
else
echo -e "\033[1;32mproject-name installer\033[0m"
echo -e "\033[0;90mOne-line description\033[0m"
fi
Platform & Preflight
Platform Detection
detect_platform() {
OS=$(uname -s | tr 'A-Z' 'a-z')
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64) ARCH="x86_64" ;;
arm64|aarch64) ARCH="aarch64" ;;
esac
case "${OS}-${ARCH}" in
linux-x86_64) TARGET="x86_64-unknown-linux-musl" ;;
linux-aarch64) TARGET="aarch64-unknown-linux-musl" ;;
darwin-x86_64) TARGET="x86_64-apple-darwin" ;;
darwin-aarch64) TARGET="aarch64-apple-darwin" ;;
*) warn "No prebuilt for ${OS}/${ARCH}; falling back to source"; FROM_SOURCE=1 ;;
esac
}
Always use musl for Linux (static linking, portable binaries).
WSL Detection
if [[ "$OS" == "linux" ]] && grep -qi microsoft /proc/version 2>/dev/null; then
warn "WSL detected. Some features may need additional configuration"
# Continue with linux platform — don't block
fi
Preflight Checks
preflight_checks() {
info "Running preflight checks"
check_disk_space # df -Pk, minimum 10MB
check_write_permissions # mkdir -p + writable test
check_existing_install # Report current version if present
check_network # curl --connect-timeout 3 to artifact URL
}
Version Resolution
resolve_version() {
# Primary: GitHub API
curl -fsSL "https://api.github.com/repos/OWNER/REPO/releases/latest" \
| grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
# Fallback: redirect URL parsing
curl -fsSL -o /dev/null -w '%{url_effective}' \
"https://github.com/OWNER/REPO/releases/latest" | sed -E 's|.*/tag/||'
}
Binary Acquisition
Proxy Setup (Before Any Downloads)
setup_proxy() {
PROXY_ARGS=()
if [[ -n "${HTTPS_PROXY:-}" ]]; then
PROXY_ARGS=(--proxy "$HTTPS_PROXY")
elif [[ -n "${HTTP_PROXY:-}" ]]; then
PROXY_ARGS=(--proxy "$HTTP_PROXY")
fi
}
# Pass to EVERY curl call: curl -fsSL "${PROXY_ARGS[@]}" "$URL"
Why a bash array? "${PROXY_ARGS[@]}" expands to nothing when empty — no conditional curl construction.
Download Strategy (4-Tier Fallback)
1. Versioned artifact: project-v{VERSION}-{TARGET}.tar.gz
2. Unversioned latest: /releases/latest/download/project-{TARGET}.tar.gz
3. Simple naming: project-{OS}-{ARCH}.tar.gz
4. Build from source: cargo build --release
After download:
- Verify checksum (SHA256 via
sha256sumorshasum -a 256) — NEVER skip unless--no-verify - Verify Sigstore bundle if cosign available (soft-skip if no cosign, hard-fail if cosign present + bad sig)
- Extract with tar -xf, find binary,
install -m 0755
Full pattern with code: DOWNLOAD-PATTERNS.md
Already-Installed Short-Circuit
if [ "$FORCE_INSTALL" -eq 0 ] && check_installed_version "$VERSION"; then
ok "project $VERSION is already installed"
info "Use --force to reinstall"
# STILL run agent configuration (idempotent)
exit 0
fi
Agent Auto-Configuration
Detection
detect_agents() {
# Check for directories AND commands
[[ -d "$HOME/.claude" ]] || command -v claude &>/dev/null # Claude Code
[[ -d "$HOME/.codex" ]] || command -v codex &>/dev/null # Codex CLI
[[ -d "$HOME/.gemini" ]] || command -v gemini &>/dev/null # Gemini CLI
[[ -d "$HOME/.cursor" ]] || command -v cursor &>/dev/null # Cursor IDE
command -v aider &>/dev/null # Aider
command -v copilot &>/dev/null # GitHub Copilot
[[ -d "$HOME/.continue" ]] # Continue
}
Hook Configuration Pattern
For each agent that has hooks:
1. Check if already configured (grep for binary name) → skip if yes
2. Create timestamped backup: settings.bak.YYYYMMDDHHMMSS
3. If settings file exists → merge using embedded Python3 script
4. If settings file missing → create from heredoc template
5. Track status: created|merged|already|failed|skipped
Claude Code hook (PreToolUse, Bash matcher):
{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"/path/to/binary"}]}]}}
Gemini CLI hook (BeforeTool, run_shell_command matcher):
{"hooks":{"BeforeTool":[{"matcher":"run_shell_command","hooks":[{"name":"tool","type":"command","command":"/path/to/binary","timeout":5000}]}]}}
Skill Installation
install_skill() {
local claude_dest="$HOME/.claude/skills/project-name"
local codex_dest="$HOME/.codex/skills/project-name"
# Primary: download skill tarball from GitHub releases
local skill_url="https://github.com/OWNER/REPO/releases/latest/download/skill.tar.gz"
if curl -fsSL "$skill_url" -o "$TEMP/skill.tar.gz" 2>/dev/null; then
tar -xzf "$TEMP/skill.tar.gz" -C "$HOME/.claude/skills"
tar -xzf "$TEMP/skill.tar.gz" -C "$HOME/.codex/skills"
return 0
fi
# Fallback: create minimal skill inline
info "Creating minimal skill (download failed)..."
cat > "$claude_dest/SKILL.md" << 'SKILL_EOF'
# ... inline SKILL.md content ...
SKILL_EOF
}
Shell Integration
Completions
install_completions_for_shell() {
local shell="$1"
case "$shell" in
bash) target="${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions/project" ;;
zsh) target="${XDG_DATA_HOME:-$HOME/.local/share}/zsh/site-functions/_project" ;;
fish) target="${XDG_CONFIG_HOME:-$HOME/.config}/fish/completions/project.fish" ;;
esac
mkdir -p "$(dirname "$target")"
"$BINARY" completions "$shell" > "$target"
}
PATH Setup
maybe_add_path() {
case ":$PATH:" in
*:"$DEST":*) return 0 ;; # already in PATH
*)
if [ "$EASY" -eq 1 ]; then
for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do
[ -e "$rc" ] && [ -w "$rc" ] && echo "export PATH=\"$DEST:\$PATH\"" >> "$rc"
done
else
warn "Add $DEST to PATH to use project-name"
fi
;;
esac
}
Final Summary
Required Summary Box
# Build summary_lines array with per-agent status
# Show with gum style --border or draw_box fallback
# Include:
# - Per-agent: Created|Merged|Already configured|Skipped|Failed
# - Backup file locations
# - What the tool does now
# - Uninstall/revert instructions
Status Tracking Pattern
CLAUDE_STATUS="" # created|merged|already|failed
GEMINI_STATUS="" # created|merged|already|failed|skipped
# ... per agent
# In summary:
case "$CLAUDE_STATUS" in
created) summary_lines+=("Claude Code: Created settings with hook") ;;
merged) summary_lines+=("Claude Code: Added hook to existing settings") ;;
already) summary_lines+=("Claude Code: Already configured") ;;
failed) summary_lines+=("Claude Code: Configuration failed") ;;
esac
Anti-Patterns
| Don't | Why | Do Instead |
|---|---|---|
| Skip checksum verification | Supply chain attacks | Always verify SHA256 |
Use gnu target for Linux |
Not portable | Use musl (static linking) |
| Modify settings without backup | Can't revert | cp file file.bak.$(date +%s) |
Assume PATH includes ~/.local/bin |
Often doesn't | Check and offer --easy-mode |
| Hard-fail on optional features | Bad UX | Warn and continue |
Use flock for locking |
Not on macOS | Use mkdir (atomic everywhere) |
| Print raw text without formatting | Looks broken | Always use info/ok/warn/err |
| Skip the scan notice for slow ops | Users think it hung | Print notice BEFORE slow scan |
Run --version without timeout |
Some CLIs hang | Use timeout 1 or gtimeout 1 |
| Write JSON by hand in bash | Breaks on special chars | Use embedded Python3 for merges |
Use sha256sum only |
Not on macOS | Check sha256sum then shasum -a 256 |
| Ignore proxy environment | Breaks in corp networks | PROXY_ARGS array on every curl |
Skip shopt -s lastpipe |
read in pipes fails |
Add after set -euo pipefail |
Checklist Before Shipping
-
set -euo pipefailat top -
umask 022at top -
trap cleanup EXITafter temp dir creation - Branded header with gum + ANSI fallback
-
--helpdocuments ALL flags - curl one-liner with cache buster documented in header comment
- Platform detection covers linux/darwin x86_64/aarch64
- Preflight: disk space, write perms, network, existing install
- Atomic locking with stale PID detection
- SHA256 checksum verification
- Sigstore verification (best-effort, skip if no cosign)
- Build-from-source fallback
- Shell completions for detected shell
- AI agent detection and auto-configuration
- Skill installation with tarball + inline fallback
- Final summary with per-agent status
- Uninstall instructions shown
- All slow operations wrapped in spinner
- Agent scan notice printed before scan starts
- Version-already-installed short-circuit (still configures agents)
- Proxy support (
PROXY_ARGSon every curl call) - WSL detection with warning
- Dual checksum tool support (
sha256sum/shasum -a 256) - Tested with
bash install.sh --quiet(no output except errors) - Tested with
bash install.sh --no-gum(ANSI fallback works) - Tested with
bash install.sh --offline /path/to/tarball(if applicable)
References
| Need | Reference |
|---|---|
| Full DCG installer source | /dp/destructive_command_guard/install.sh |
| Full RCH installer source | /dp/remote_compilation_helper/install.sh |
| Agent hook JSON formats | AGENT-HOOKS.md |
| Gum style recipes | GUM-RECIPES.md |
| Download, checksum, proxy, offline | DOWNLOAD-PATTERNS.md |
| JSON merge (Python3 + jq) | PYTHON3-JSON-MERGE.md |
| systemd + launchd services | SERVICE-MANAGEMENT.md |