Skip to content

Commit 6dc4b4f

Browse files
rpgdevdisconnect3d
andauthored
feat: add devc destroy command and standardize volume naming (#31)
Add `devc destroy [-f]` to cleanly remove all Docker resources (container, volumes, images) for a project. Includes resource discovery via container inspection, itemized pre-deletion summary, y/N confirmation prompt with --force bypass, running container warning, and idempotent no-op when no resources exist. Rename volume prefix from claude-code-* to devc-* and include workspace folder name for self-identifying volumes (e.g., devc-myproject-config-{devcontainerId}). Co-authored-by: Disconnect3d <dominik.b.czarnota@gmail.com>
1 parent 8256ebb commit 6dc4b4f

File tree

3 files changed

+161
-3
lines changed

3 files changed

+161
-3
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ claude # Ready to work
120120
devc . Install template + start container in current directory
121121
devc up Start the devcontainer
122122
devc rebuild Rebuild container (preserves persistent volumes)
123+
devc destroy [-f] Remove container, volumes, and image for current project
123124
devc down Stop the container
124125
devc shell Open zsh shell in container
125126
devc exec CMD Execute command inside the container
@@ -130,6 +131,8 @@ devc template DIR Copy devcontainer files to directory
130131
devc self-install Install devc to ~/.local/bin
131132
```
132133

134+
> **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.
135+
133136
## Session Sync for `/insights`
134137

135138
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.

devcontainer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242
},
4343
"remoteUser": "vscode",
4444
"mounts": [
45-
"source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
46-
"source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume",
47-
"source=claude-code-gh-${devcontainerId},target=/home/vscode/.config/gh,type=volume",
45+
"source=devc-${localWorkspaceFolderBasename}-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
46+
"source=devc-${localWorkspaceFolderBasename}-config-${devcontainerId},target=/home/vscode/.claude,type=volume",
47+
"source=devc-${localWorkspaceFolderBasename}-gh-${devcontainerId},target=/home/vscode/.config/gh,type=volume",
4848
"source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly",
4949
"source=${localWorkspaceFolder}/.devcontainer,target=/workspace/.devcontainer,type=bind,readonly"
5050
],

install.sh

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Commands:
3939
mount <host> <cont> Add a mount to the devcontainer (recreates container)
4040
sync [project] [--trusted] Sync sessions from devcontainers to host
4141
cp <cont> <host> Copy files/directories from container to host
42+
destroy [-f] Remove container, volumes, and image for current project
4243
help Show this help message
4344
4445
Examples:
@@ -54,6 +55,8 @@ Examples:
5455
devc sync # Sync sessions from all devcontainers
5556
devc sync crypto # Sync only matching devcontainer
5657
devc cp /some/file ./out # Copy a path from container to host
58+
devc destroy # Remove all project Docker resources
59+
devc destroy -f # Skip confirmation prompt
5760
EOF
5861
}
5962

@@ -639,6 +642,155 @@ cmd_dot() {
639642
cmd_up "."
640643
}
641644

645+
# Discovers all Docker resources associated with the current workspace.
646+
# Sets global variables: CONTAINER_ID, CONTAINER_STATUS, VOLUMES (array), IMAGE, IMAGE_UID
647+
discover_resources() {
648+
local workspace_folder="$1"
649+
local label="devcontainer.local_folder=$workspace_folder"
650+
651+
CONTAINER_ID=""
652+
CONTAINER_STATUS=""
653+
VOLUMES=()
654+
IMAGE=""
655+
IMAGE_UID=""
656+
657+
# Find container (any state: running, stopped, created, etc.)
658+
CONTAINER_ID=$(docker ps -aq --filter "label=$label" 2>/dev/null | head -1)
659+
660+
if [[ -z "$CONTAINER_ID" ]]; then
661+
return 0
662+
fi
663+
664+
# Get container status
665+
CONTAINER_STATUS=$(docker inspect "$CONTAINER_ID" --format '{{.State.Status}}' 2>/dev/null || true)
666+
667+
# Get volumes (docker volumes only, not bind mounts)
668+
while IFS= read -r vol; do
669+
[[ -n "$vol" ]] && VOLUMES+=("$vol")
670+
done < <(docker inspect "$CONTAINER_ID" --format '{{json .Mounts}}' 2>/dev/null \
671+
| jq -r '.[] | select(.Type == "volume") | .Name' 2>/dev/null)
672+
673+
# Get image and its -uid variant
674+
IMAGE=$(docker inspect "$CONTAINER_ID" --format '{{.Config.Image}}' 2>/dev/null || true)
675+
if [[ -n "$IMAGE" ]]; then
676+
if [[ "$IMAGE" == *-uid ]]; then
677+
IMAGE_UID="$IMAGE"
678+
IMAGE="${IMAGE%-uid}"
679+
else
680+
IMAGE_UID="${IMAGE}-uid"
681+
fi
682+
fi
683+
}
684+
685+
print_destroy_summary() {
686+
echo ""
687+
log_warn "The following resources will be permanently removed:"
688+
echo ""
689+
690+
if [[ -n "$CONTAINER_ID" ]]; then
691+
local container_name
692+
container_name=$(docker inspect "$CONTAINER_ID" --format '{{.Name}}' 2>/dev/null | sed 's|^/||')
693+
echo " Container: ${container_name:-$CONTAINER_ID}"
694+
if [[ "$CONTAINER_STATUS" == "running" ]]; then
695+
echo " (currently running -- will be force-stopped)"
696+
fi
697+
fi
698+
699+
if [[ ${#VOLUMES[@]} -gt 0 ]]; then
700+
echo " Volumes:"
701+
for vol in "${VOLUMES[@]}"; do
702+
echo " $vol"
703+
done
704+
fi
705+
706+
if [[ -n "$IMAGE" ]]; then
707+
echo " Image: $IMAGE"
708+
if docker image inspect "$IMAGE_UID" &>/dev/null; then
709+
echo " $IMAGE_UID"
710+
fi
711+
fi
712+
713+
echo ""
714+
}
715+
716+
cmd_destroy() {
717+
local force=false
718+
719+
# Parse flags
720+
while [[ $# -gt 0 ]]; do
721+
case "$1" in
722+
-f|--force)
723+
force=true
724+
shift
725+
;;
726+
*)
727+
break
728+
;;
729+
esac
730+
done
731+
732+
local workspace_folder
733+
workspace_folder="$(get_workspace_folder "${1:-}")"
734+
735+
discover_resources "$workspace_folder"
736+
737+
# No resources found (idempotent behavior)
738+
if [[ -z "$CONTAINER_ID" ]]; then
739+
log_info "No devcontainer found for $workspace_folder"
740+
return 0
741+
fi
742+
743+
print_destroy_summary
744+
745+
# Running container warning
746+
if [[ "$CONTAINER_STATUS" == "running" && "$force" != true ]]; then
747+
log_warn "Container is currently running!"
748+
read -p "Force-stop the running container? [y/N] " -n 1 -r
749+
echo
750+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
751+
log_info "Aborted."
752+
return 0
753+
fi
754+
fi
755+
756+
# Main confirmation prompt
757+
if [[ "$force" != true ]]; then
758+
read -p "Destroy these resources? [y/N] " -n 1 -r
759+
echo
760+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
761+
log_info "Aborted."
762+
return 0
763+
fi
764+
fi
765+
766+
# Deletion, in order: stop, remove container, volumes, images
767+
if [[ -n "$CONTAINER_ID" && "$CONTAINER_STATUS" == "running" ]]; then
768+
log_info "Stopping container..."
769+
docker stop "$CONTAINER_ID" >/dev/null 2>&1 || true
770+
fi
771+
772+
if [[ -n "$CONTAINER_ID" ]]; then
773+
log_info "Removing container..."
774+
docker rm -f "$CONTAINER_ID" >/dev/null 2>&1 || true
775+
fi
776+
777+
for vol in "${VOLUMES[@]}"; do
778+
log_info "Removing volume: $vol"
779+
docker volume rm -f "$vol" >/dev/null 2>&1 || true
780+
done
781+
782+
if [[ -n "$IMAGE" ]]; then
783+
log_info "Removing image: $IMAGE"
784+
docker rmi -f "$IMAGE" >/dev/null 2>&1 || true
785+
if docker image inspect "$IMAGE_UID" &>/dev/null 2>&1; then
786+
log_info "Removing image: $IMAGE_UID"
787+
docker rmi -f "$IMAGE_UID" >/dev/null 2>&1 || true
788+
fi
789+
fi
790+
791+
log_success "All resources destroyed for $workspace_folder"
792+
}
793+
642794
# Main command dispatcher
643795
main() {
644796
if [[ $# -eq 0 ]]; then
@@ -662,6 +814,9 @@ main() {
662814
down)
663815
cmd_down "$@"
664816
;;
817+
destroy)
818+
cmd_destroy "$@"
819+
;;
665820
shell)
666821
cmd_shell
667822
;;

0 commit comments

Comments
 (0)