mirror of
https://github.com/trailofbits/claude-code-devcontainer
synced 2026-06-21 14:11:48 +00:00
Add exec, upgrade, and mount commands to devc CLI (#8)
* Add exec, upgrade, and mount commands to devc CLI - exec: Run arbitrary commands in the container - upgrade: Update Claude Code to latest version - mount: Add bind mounts to devcontainer.json (recreates container) Also preserves custom mounts when template command overwrites existing devcontainer configuration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Address PR review comments - Replace `npm install -g @anthropic-ai/claude-code@latest` with `claude update` since Claude Code now uses native installer - Remove `[--]` from exec command help/examples (not required) - Replace embedded Python with jq for devcontainer.json manipulation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+158
@@ -34,6 +34,9 @@ Commands:
|
||||
self-install Install 'devc' command to ~/.local/bin
|
||||
update Update devc to the latest version
|
||||
template [dir] Copy devcontainer template to directory (default: current)
|
||||
exec <cmd> Execute a command in the running container
|
||||
upgrade Upgrade Claude Code to latest version
|
||||
mount <host> <cont> Add a mount to the devcontainer (recreates container)
|
||||
help Show this help message
|
||||
|
||||
Examples:
|
||||
@@ -43,6 +46,9 @@ Examples:
|
||||
devc shell # Open interactive shell
|
||||
devc self-install # Install devc to PATH
|
||||
devc update # Update to latest version
|
||||
devc exec ls -la # Run command in container
|
||||
devc upgrade # Upgrade Claude Code to latest
|
||||
devc mount ~/data /data # Add mount to container
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -74,6 +80,75 @@ get_workspace_folder() {
|
||||
echo "${1:-$(pwd)}"
|
||||
}
|
||||
|
||||
# Extract custom mounts from devcontainer.json to a temp file
|
||||
# Returns the temp file path, or empty string if no custom mounts
|
||||
extract_mounts_to_file() {
|
||||
local devcontainer_json="$1"
|
||||
local temp_file
|
||||
|
||||
[[ -f "$devcontainer_json" ]] || return 0
|
||||
|
||||
temp_file=$(mktemp)
|
||||
|
||||
# Filter out default mounts (template mounts we don't want to preserve)
|
||||
local custom_mounts
|
||||
custom_mounts=$(jq -c '
|
||||
.mounts // [] | map(
|
||||
select(
|
||||
(startswith("source=claude-code-bashhistory-") | not) and
|
||||
(startswith("source=claude-code-config-") | not) and
|
||||
(startswith("source=claude-code-gh-") | not) and
|
||||
(startswith("source=${localEnv:HOME}/.gitconfig,") | not)
|
||||
)
|
||||
) | if length > 0 then . else empty end
|
||||
' "$devcontainer_json" 2>/dev/null) || true
|
||||
|
||||
if [[ -n "$custom_mounts" ]]; then
|
||||
echo "$custom_mounts" >"$temp_file"
|
||||
echo "$temp_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Merge preserved mounts back into devcontainer.json
|
||||
merge_mounts_from_file() {
|
||||
local devcontainer_json="$1"
|
||||
local mounts_file="$2"
|
||||
|
||||
[[ -f "$mounts_file" ]] || return 0
|
||||
[[ -s "$mounts_file" ]] || return 0
|
||||
|
||||
local custom_mounts
|
||||
custom_mounts=$(cat "$mounts_file")
|
||||
|
||||
local updated
|
||||
updated=$(jq --argjson custom "$custom_mounts" '
|
||||
.mounts = ((.mounts // []) + $custom | unique)
|
||||
' "$devcontainer_json")
|
||||
|
||||
echo "$updated" >"$devcontainer_json"
|
||||
}
|
||||
|
||||
# Add or update a mount in devcontainer.json
|
||||
update_devcontainer_mounts() {
|
||||
local devcontainer_json="$1"
|
||||
local host_path="$2"
|
||||
local container_path="$3"
|
||||
local readonly="${4:-false}"
|
||||
|
||||
local mount_str="source=${host_path},target=${container_path},type=bind"
|
||||
[[ "$readonly" == "true" ]] && mount_str="${mount_str},readonly"
|
||||
|
||||
local updated
|
||||
updated=$(jq --arg target "$container_path" --arg mount "$mount_str" '
|
||||
.mounts = (
|
||||
((.mounts // []) | map(select(contains("target=" + $target + ",") or endswith("target=" + $target) | not)))
|
||||
+ [$mount]
|
||||
)
|
||||
' "$devcontainer_json")
|
||||
|
||||
echo "$updated" >"$devcontainer_json"
|
||||
}
|
||||
|
||||
cmd_template() {
|
||||
local target_dir="${1:-.}"
|
||||
target_dir="$(cd "$target_dir" 2>/dev/null && pwd)" || {
|
||||
@@ -82,6 +157,8 @@ cmd_template() {
|
||||
}
|
||||
|
||||
local devcontainer_dir="$target_dir/.devcontainer"
|
||||
local devcontainer_json="$devcontainer_dir/devcontainer.json"
|
||||
local preserved_mounts=""
|
||||
|
||||
if [[ -d "$devcontainer_dir" ]]; then
|
||||
log_warn "Devcontainer already exists at $devcontainer_dir"
|
||||
@@ -91,6 +168,12 @@ cmd_template() {
|
||||
log_info "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Preserve custom mounts before overwriting
|
||||
preserved_mounts=$(extract_mounts_to_file "$devcontainer_json")
|
||||
if [[ -n "$preserved_mounts" ]]; then
|
||||
log_info "Preserving custom mounts..."
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$devcontainer_dir"
|
||||
@@ -101,6 +184,13 @@ cmd_template() {
|
||||
cp "$SCRIPT_DIR/post_install.py" "$devcontainer_dir/"
|
||||
cp "$SCRIPT_DIR/.zshrc" "$devcontainer_dir/"
|
||||
|
||||
# Restore preserved mounts
|
||||
if [[ -n "$preserved_mounts" ]]; then
|
||||
merge_mounts_from_file "$devcontainer_json" "$preserved_mounts"
|
||||
rm -f "$preserved_mounts"
|
||||
log_info "Custom mounts restored"
|
||||
fi
|
||||
|
||||
log_success "Template installed to $devcontainer_dir"
|
||||
}
|
||||
|
||||
@@ -155,6 +245,64 @@ cmd_shell() {
|
||||
devcontainer exec --workspace-folder "$workspace_folder" zsh
|
||||
}
|
||||
|
||||
cmd_exec() {
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder)"
|
||||
|
||||
check_devcontainer_cli
|
||||
devcontainer exec --workspace-folder "$workspace_folder" "$@"
|
||||
}
|
||||
|
||||
cmd_upgrade() {
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder)"
|
||||
|
||||
check_devcontainer_cli
|
||||
log_info "Upgrading Claude Code..."
|
||||
|
||||
devcontainer exec --workspace-folder "$workspace_folder" claude update
|
||||
|
||||
log_success "Claude Code upgraded"
|
||||
}
|
||||
|
||||
cmd_mount() {
|
||||
local host_path="${1:-}"
|
||||
local container_path="${2:-}"
|
||||
local readonly="false"
|
||||
|
||||
if [[ -z "$host_path" ]] || [[ -z "$container_path" ]]; then
|
||||
log_error "Usage: devc mount <host_path> <container_path> [--readonly]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ "${3:-}" == "--readonly" ]] && readonly="true"
|
||||
|
||||
# Expand and validate host path
|
||||
host_path="$(cd "$host_path" 2>/dev/null && pwd)" || {
|
||||
log_error "Host path does not exist: $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder)"
|
||||
local devcontainer_json="$workspace_folder/.devcontainer/devcontainer.json"
|
||||
|
||||
if [[ ! -f "$devcontainer_json" ]]; then
|
||||
log_error "No devcontainer.json found. Run 'devc template' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_devcontainer_cli
|
||||
|
||||
log_info "Adding mount: $host_path → $container_path"
|
||||
update_devcontainer_mounts "$devcontainer_json" "$host_path" "$container_path" "$readonly"
|
||||
|
||||
log_info "Recreating container with new mount..."
|
||||
devcontainer up --workspace-folder "$workspace_folder" --remove-existing-container
|
||||
|
||||
log_success "Mount added: $host_path → $container_path"
|
||||
}
|
||||
|
||||
cmd_self_install() {
|
||||
local install_dir="$HOME/.local/bin"
|
||||
local install_path="$install_dir/devc"
|
||||
@@ -232,6 +380,16 @@ main() {
|
||||
shell)
|
||||
cmd_shell
|
||||
;;
|
||||
exec)
|
||||
[[ "${1:-}" == "--" ]] && shift
|
||||
cmd_exec "$@"
|
||||
;;
|
||||
upgrade)
|
||||
cmd_upgrade
|
||||
;;
|
||||
mount)
|
||||
cmd_mount "$@"
|
||||
;;
|
||||
self-install)
|
||||
cmd_self_install
|
||||
;;
|
||||
|
||||
Reference in New Issue
Block a user