| name | installer-workmanship |
| description | Write production-grade curl|bash installers. Use when creating install.sh, curl-pipe-bash installer, or one-liner install for CLI tools. |
Installer Workmanship
Core Principle: Study /dp/destructive_command_guard/install.sh and /dp/remote_compilation_helper/install.sh and 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.
cat /dp/destructive_command_guard/install.sh
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)
HAS_GUM=0
if command -v gum &> /dev/null && [ -t 1 ]; then
HAS_GUM=1
fi
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" ... }
warn() { ... --foreground 214 ... "\033[1;33m" ... }
err() { ... --foreground 196 ... "\033[0;31m" ... }
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" -- ""
info
}
Box Drawing (Required)
draw_box() {
local color="$1"; shift
local lines=("$@")
}
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"
fi
Preflight Checks
preflight_checks() {
info "Running preflight checks"
check_disk_space
check_write_permissions
check_existing_install
check_network
}
Version Resolution
resolve_version() {
curl -fsSL "https://api.github.com/repos/OWNER/REPO/releases/latest" \
| grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
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
}
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
sha256sum or shasum -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"
exit 0
fi
Agent Auto-Configuration
Detection
detect_agents() {
[[ -d "$HOME/.claude" ]] || command -v claude &>/dev/null
[[ -d "$HOME/.codex" ]] || command -v codex &>/dev/null
[[ -d "$HOME/.gemini" ]] || command -v gemini &>/dev/null
[[ -d "$HOME/.cursor" ]] || command -v cursor &>/dev/null
command -v aider &>/dev/null
command -v copilot &>/dev/null
[[ -d "$HOME/.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"
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
info "Creating minimal skill (download failed)..."
cat > "$claude_dest/SKILL.md" << 'SKILL_EOF'
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 ;;
*)
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
Status Tracking Pattern
CLAUDE_STATUS=""
GEMINI_STATUS=""
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
References