Files
trailofbits-claude-code-dev…/install.sh
T
2026-01-29 20:13:16 -05:00

256 lines
6.0 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Claude Code Devcontainer CLI Helper
# Provides the `devc` command for managing devcontainers
# Resolve symlinks to get actual script location
SOURCE="${BASH_SOURCE[0]}"
while [[ -L "$SOURCE" ]]; do
DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
done
SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
SCRIPT_NAME="$(basename "$0")"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_usage() {
cat <<EOF
Usage: devc <command> [options]
Commands:
. Install devcontainer template to current directory and start
up Start the devcontainer in current directory
rebuild Rebuild the devcontainer (preserves auth volumes)
down Stop the devcontainer
shell Open a shell in the running container
self-install Install 'devc' command to ~/.local/bin
update Update devc to the latest version
template [dir] Copy devcontainer template to directory (default: current)
help Show this help message
Examples:
devc . # Install template and start container
devc up # Start container in current directory
devc rebuild # Clean rebuild
devc shell # Open interactive shell
devc self-install # Install devc to PATH
devc update # Update to latest version
EOF
}
log_info() {
echo -e "${BLUE}[devc]${NC} $1"
}
log_success() {
echo -e "${GREEN}[devc]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[devc]${NC} $1"
}
log_error() {
echo -e "${RED}[devc]${NC} $1" >&2
}
check_devcontainer_cli() {
if ! command -v devcontainer &>/dev/null; then
log_error "devcontainer CLI not found."
log_info "Install it with: npm install -g @devcontainers/cli"
exit 1
fi
}
get_workspace_folder() {
echo "${1:-$(pwd)}"
}
cmd_template() {
local target_dir="${1:-.}"
target_dir="$(cd "$target_dir" 2>/dev/null && pwd)" || {
log_error "Directory does not exist: $1"
exit 1
}
local devcontainer_dir="$target_dir/.devcontainer"
if [[ -d "$devcontainer_dir" ]]; then
log_warn "Devcontainer already exists at $devcontainer_dir"
read -p "Overwrite? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "Aborted."
exit 0
fi
fi
mkdir -p "$devcontainer_dir"
# Copy template files
cp "$SCRIPT_DIR/Dockerfile" "$devcontainer_dir/"
cp "$SCRIPT_DIR/devcontainer.json" "$devcontainer_dir/"
cp "$SCRIPT_DIR/post_install.py" "$devcontainer_dir/"
cp "$SCRIPT_DIR/.zshrc" "$devcontainer_dir/"
log_success "Template installed to $devcontainer_dir"
}
cmd_up() {
local workspace_folder
workspace_folder="$(get_workspace_folder "${1:-}")"
check_devcontainer_cli
log_info "Starting devcontainer in $workspace_folder..."
devcontainer up --workspace-folder "$workspace_folder"
log_success "Devcontainer started"
}
cmd_rebuild() {
local workspace_folder
workspace_folder="$(get_workspace_folder "${1:-}")"
check_devcontainer_cli
log_info "Rebuilding devcontainer in $workspace_folder..."
devcontainer up --workspace-folder "$workspace_folder" --remove-existing-container
log_success "Devcontainer rebuilt"
}
cmd_down() {
local workspace_folder
workspace_folder="$(get_workspace_folder "${1:-}")"
check_devcontainer_cli
log_info "Stopping devcontainer..."
# Get container ID and stop it
local container_id
container_id=$(docker ps -q --filter "label=devcontainer.local_folder=$workspace_folder" 2>/dev/null || true)
if [[ -n "$container_id" ]]; then
docker stop "$container_id"
log_success "Devcontainer stopped"
else
log_warn "No running devcontainer found for $workspace_folder"
fi
}
cmd_shell() {
local workspace_folder
workspace_folder="$(get_workspace_folder)"
check_devcontainer_cli
log_info "Opening shell in devcontainer..."
devcontainer exec --workspace-folder "$workspace_folder" zsh
}
cmd_self_install() {
local install_dir="$HOME/.local/bin"
local install_path="$install_dir/devc"
mkdir -p "$install_dir"
# Create a symlink to the original script
ln -sf "$SCRIPT_DIR/$SCRIPT_NAME" "$install_path"
log_success "Installed 'devc' to $install_path"
# Check if in PATH
if [[ ":$PATH:" != *":$install_dir:"* ]]; then
log_warn "$install_dir is not in your PATH"
log_info "Add this to your shell profile:"
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
fi
}
cmd_update() {
log_info "Updating devc..."
if ! git -C "$SCRIPT_DIR" rev-parse --is-inside-work-tree &>/dev/null; then
log_error "Not a git repository: $SCRIPT_DIR"
log_info "Re-clone with: rm -rf ~/.claude-devcontainer && git clone https://github.com/trailofbits/claude-code-devcontainer ~/.claude-devcontainer"
exit 1
fi
local before_sha after_sha
before_sha=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
if ! git -C "$SCRIPT_DIR" pull --ff-only; then
log_error "Update failed. Try: cd $SCRIPT_DIR && git pull"
exit 1
fi
after_sha=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
if [[ "$before_sha" == "$after_sha" ]]; then
log_success "Already up to date"
else
log_success "Updated from ${before_sha:0:7} to ${after_sha:0:7}"
fi
}
cmd_dot() {
# Install template and start container in one command
cmd_template "."
cmd_up "."
}
# Main command dispatcher
main() {
if [[ $# -eq 0 ]]; then
print_usage
exit 1
fi
local command="$1"
shift
case "$command" in
.)
cmd_dot
;;
up)
cmd_up "$@"
;;
rebuild)
cmd_rebuild "$@"
;;
down)
cmd_down "$@"
;;
shell)
cmd_shell
;;
self-install)
cmd_self_install
;;
update)
cmd_update
;;
template)
cmd_template "$@"
;;
help | --help | -h)
print_usage
;;
*)
log_error "Unknown command: $command"
print_usage
exit 1
;;
esac
}
main "$@"