diff --git a/README.md b/README.md index 3366c44..f7ae3b9 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ claude # Ready to work devc . Install template + start container in current directory devc up Start the devcontainer devc rebuild Rebuild container (preserves persistent volumes) +devc destroy [-f] Remove container, volumes, and image for current project devc down Stop the container devc shell Open zsh shell in container devc exec CMD Execute command inside the container @@ -130,6 +131,8 @@ devc template DIR Copy devcontainer files to directory devc self-install Install devc to ~/.local/bin ``` +> **Note:** Use `devc destroy` to clean up a project's Docker resources. Removing containers manually (e.g., `docker rm`) will leave orphaned volumes and images behind that `devc destroy` won't be able to find. + ## Session Sync for `/insights` Claude Code's `/insights` command analyzes your session history, but it only reads from `~/.claude/projects/` on the host. Sessions inside devcontainer volumes are invisible to it. diff --git a/devcontainer.json b/devcontainer.json index d3cb0b2..a6b66ea 100644 --- a/devcontainer.json +++ b/devcontainer.json @@ -42,9 +42,9 @@ }, "remoteUser": "vscode", "mounts": [ - "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", - "source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume", - "source=claude-code-gh-${devcontainerId},target=/home/vscode/.config/gh,type=volume", + "source=devc-${localWorkspaceFolderBasename}-bashhistory-${devcontainerId},target=/commandhistory,type=volume", + "source=devc-${localWorkspaceFolderBasename}-config-${devcontainerId},target=/home/vscode/.claude,type=volume", + "source=devc-${localWorkspaceFolderBasename}-gh-${devcontainerId},target=/home/vscode/.config/gh,type=volume", "source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly", "source=${localWorkspaceFolder}/.devcontainer,target=/workspace/.devcontainer,type=bind,readonly" ], diff --git a/install.sh b/install.sh index ce9f050..09863b1 100755 --- a/install.sh +++ b/install.sh @@ -39,6 +39,7 @@ Commands: mount Add a mount to the devcontainer (recreates container) sync [project] [--trusted] Sync sessions from devcontainers to host cp Copy files/directories from container to host + destroy [-f] Remove container, volumes, and image for current project help Show this help message Examples: @@ -54,6 +55,8 @@ Examples: devc sync # Sync sessions from all devcontainers devc sync crypto # Sync only matching devcontainer devc cp /some/file ./out # Copy a path from container to host + devc destroy # Remove all project Docker resources + devc destroy -f # Skip confirmation prompt EOF } @@ -639,6 +642,155 @@ cmd_dot() { cmd_up "." } +# Discovers all Docker resources associated with the current workspace. +# Sets global variables: CONTAINER_ID, CONTAINER_STATUS, VOLUMES (array), IMAGE, IMAGE_UID +discover_resources() { + local workspace_folder="$1" + local label="devcontainer.local_folder=$workspace_folder" + + CONTAINER_ID="" + CONTAINER_STATUS="" + VOLUMES=() + IMAGE="" + IMAGE_UID="" + + # Find container (any state: running, stopped, created, etc.) + CONTAINER_ID=$(docker ps -aq --filter "label=$label" 2>/dev/null | head -1) + + if [[ -z "$CONTAINER_ID" ]]; then + return 0 + fi + + # Get container status + CONTAINER_STATUS=$(docker inspect "$CONTAINER_ID" --format '{{.State.Status}}' 2>/dev/null || true) + + # Get volumes (docker volumes only, not bind mounts) + while IFS= read -r vol; do + [[ -n "$vol" ]] && VOLUMES+=("$vol") + done < <(docker inspect "$CONTAINER_ID" --format '{{json .Mounts}}' 2>/dev/null \ + | jq -r '.[] | select(.Type == "volume") | .Name' 2>/dev/null) + + # Get image and its -uid variant + IMAGE=$(docker inspect "$CONTAINER_ID" --format '{{.Config.Image}}' 2>/dev/null || true) + if [[ -n "$IMAGE" ]]; then + if [[ "$IMAGE" == *-uid ]]; then + IMAGE_UID="$IMAGE" + IMAGE="${IMAGE%-uid}" + else + IMAGE_UID="${IMAGE}-uid" + fi + fi +} + +print_destroy_summary() { + echo "" + log_warn "The following resources will be permanently removed:" + echo "" + + if [[ -n "$CONTAINER_ID" ]]; then + local container_name + container_name=$(docker inspect "$CONTAINER_ID" --format '{{.Name}}' 2>/dev/null | sed 's|^/||') + echo " Container: ${container_name:-$CONTAINER_ID}" + if [[ "$CONTAINER_STATUS" == "running" ]]; then + echo " (currently running -- will be force-stopped)" + fi + fi + + if [[ ${#VOLUMES[@]} -gt 0 ]]; then + echo " Volumes:" + for vol in "${VOLUMES[@]}"; do + echo " $vol" + done + fi + + if [[ -n "$IMAGE" ]]; then + echo " Image: $IMAGE" + if docker image inspect "$IMAGE_UID" &>/dev/null; then + echo " $IMAGE_UID" + fi + fi + + echo "" +} + +cmd_destroy() { + local force=false + + # Parse flags + while [[ $# -gt 0 ]]; do + case "$1" in + -f|--force) + force=true + shift + ;; + *) + break + ;; + esac + done + + local workspace_folder + workspace_folder="$(get_workspace_folder "${1:-}")" + + discover_resources "$workspace_folder" + + # No resources found (idempotent behavior) + if [[ -z "$CONTAINER_ID" ]]; then + log_info "No devcontainer found for $workspace_folder" + return 0 + fi + + print_destroy_summary + + # Running container warning + if [[ "$CONTAINER_STATUS" == "running" && "$force" != true ]]; then + log_warn "Container is currently running!" + read -p "Force-stop the running container? [y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_info "Aborted." + return 0 + fi + fi + + # Main confirmation prompt + if [[ "$force" != true ]]; then + read -p "Destroy these resources? [y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_info "Aborted." + return 0 + fi + fi + + # Deletion, in order: stop, remove container, volumes, images + if [[ -n "$CONTAINER_ID" && "$CONTAINER_STATUS" == "running" ]]; then + log_info "Stopping container..." + docker stop "$CONTAINER_ID" >/dev/null 2>&1 || true + fi + + if [[ -n "$CONTAINER_ID" ]]; then + log_info "Removing container..." + docker rm -f "$CONTAINER_ID" >/dev/null 2>&1 || true + fi + + for vol in "${VOLUMES[@]}"; do + log_info "Removing volume: $vol" + docker volume rm -f "$vol" >/dev/null 2>&1 || true + done + + if [[ -n "$IMAGE" ]]; then + log_info "Removing image: $IMAGE" + docker rmi -f "$IMAGE" >/dev/null 2>&1 || true + if docker image inspect "$IMAGE_UID" &>/dev/null 2>&1; then + log_info "Removing image: $IMAGE_UID" + docker rmi -f "$IMAGE_UID" >/dev/null 2>&1 || true + fi + fi + + log_success "All resources destroyed for $workspace_folder" +} + # Main command dispatcher main() { if [[ $# -eq 0 ]]; then @@ -662,6 +814,9 @@ main() { down) cmd_down "$@" ;; + destroy) + cmd_destroy "$@" + ;; shell) cmd_shell ;;