Skip to content

Commit 96e3ca2

Browse files
dguidoclaude
andauthored
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>
1 parent 369b395 commit 96e3ca2

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

install.sh

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ Commands:
3434
self-install Install 'devc' command to ~/.local/bin
3535
update Update devc to the latest version
3636
template [dir] Copy devcontainer template to directory (default: current)
37+
exec <cmd> Execute a command in the running container
38+
upgrade Upgrade Claude Code to latest version
39+
mount <host> <cont> Add a mount to the devcontainer (recreates container)
3740
help Show this help message
3841
3942
Examples:
@@ -43,6 +46,9 @@ Examples:
4346
devc shell # Open interactive shell
4447
devc self-install # Install devc to PATH
4548
devc update # Update to latest version
49+
devc exec ls -la # Run command in container
50+
devc upgrade # Upgrade Claude Code to latest
51+
devc mount ~/data /data # Add mount to container
4652
EOF
4753
}
4854

@@ -74,6 +80,75 @@ get_workspace_folder() {
7480
echo "${1:-$(pwd)}"
7581
}
7682

83+
# Extract custom mounts from devcontainer.json to a temp file
84+
# Returns the temp file path, or empty string if no custom mounts
85+
extract_mounts_to_file() {
86+
local devcontainer_json="$1"
87+
local temp_file
88+
89+
[[ -f "$devcontainer_json" ]] || return 0
90+
91+
temp_file=$(mktemp)
92+
93+
# Filter out default mounts (template mounts we don't want to preserve)
94+
local custom_mounts
95+
custom_mounts=$(jq -c '
96+
.mounts // [] | map(
97+
select(
98+
(startswith("source=claude-code-bashhistory-") | not) and
99+
(startswith("source=claude-code-config-") | not) and
100+
(startswith("source=claude-code-gh-") | not) and
101+
(startswith("source=${localEnv:HOME}/.gitconfig,") | not)
102+
)
103+
) | if length > 0 then . else empty end
104+
' "$devcontainer_json" 2>/dev/null) || true
105+
106+
if [[ -n "$custom_mounts" ]]; then
107+
echo "$custom_mounts" >"$temp_file"
108+
echo "$temp_file"
109+
fi
110+
}
111+
112+
# Merge preserved mounts back into devcontainer.json
113+
merge_mounts_from_file() {
114+
local devcontainer_json="$1"
115+
local mounts_file="$2"
116+
117+
[[ -f "$mounts_file" ]] || return 0
118+
[[ -s "$mounts_file" ]] || return 0
119+
120+
local custom_mounts
121+
custom_mounts=$(cat "$mounts_file")
122+
123+
local updated
124+
updated=$(jq --argjson custom "$custom_mounts" '
125+
.mounts = ((.mounts // []) + $custom | unique)
126+
' "$devcontainer_json")
127+
128+
echo "$updated" >"$devcontainer_json"
129+
}
130+
131+
# Add or update a mount in devcontainer.json
132+
update_devcontainer_mounts() {
133+
local devcontainer_json="$1"
134+
local host_path="$2"
135+
local container_path="$3"
136+
local readonly="${4:-false}"
137+
138+
local mount_str="source=${host_path},target=${container_path},type=bind"
139+
[[ "$readonly" == "true" ]] && mount_str="${mount_str},readonly"
140+
141+
local updated
142+
updated=$(jq --arg target "$container_path" --arg mount "$mount_str" '
143+
.mounts = (
144+
((.mounts // []) | map(select(contains("target=" + $target + ",") or endswith("target=" + $target) | not)))
145+
+ [$mount]
146+
)
147+
' "$devcontainer_json")
148+
149+
echo "$updated" >"$devcontainer_json"
150+
}
151+
77152
cmd_template() {
78153
local target_dir="${1:-.}"
79154
target_dir="$(cd "$target_dir" 2>/dev/null && pwd)" || {
@@ -82,6 +157,8 @@ cmd_template() {
82157
}
83158

84159
local devcontainer_dir="$target_dir/.devcontainer"
160+
local devcontainer_json="$devcontainer_dir/devcontainer.json"
161+
local preserved_mounts=""
85162

86163
if [[ -d "$devcontainer_dir" ]]; then
87164
log_warn "Devcontainer already exists at $devcontainer_dir"
@@ -91,6 +168,12 @@ cmd_template() {
91168
log_info "Aborted."
92169
exit 0
93170
fi
171+
172+
# Preserve custom mounts before overwriting
173+
preserved_mounts=$(extract_mounts_to_file "$devcontainer_json")
174+
if [[ -n "$preserved_mounts" ]]; then
175+
log_info "Preserving custom mounts..."
176+
fi
94177
fi
95178

96179
mkdir -p "$devcontainer_dir"
@@ -101,6 +184,13 @@ cmd_template() {
101184
cp "$SCRIPT_DIR/post_install.py" "$devcontainer_dir/"
102185
cp "$SCRIPT_DIR/.zshrc" "$devcontainer_dir/"
103186

187+
# Restore preserved mounts
188+
if [[ -n "$preserved_mounts" ]]; then
189+
merge_mounts_from_file "$devcontainer_json" "$preserved_mounts"
190+
rm -f "$preserved_mounts"
191+
log_info "Custom mounts restored"
192+
fi
193+
104194
log_success "Template installed to $devcontainer_dir"
105195
}
106196

@@ -155,6 +245,64 @@ cmd_shell() {
155245
devcontainer exec --workspace-folder "$workspace_folder" zsh
156246
}
157247

248+
cmd_exec() {
249+
local workspace_folder
250+
workspace_folder="$(get_workspace_folder)"
251+
252+
check_devcontainer_cli
253+
devcontainer exec --workspace-folder "$workspace_folder" "$@"
254+
}
255+
256+
cmd_upgrade() {
257+
local workspace_folder
258+
workspace_folder="$(get_workspace_folder)"
259+
260+
check_devcontainer_cli
261+
log_info "Upgrading Claude Code..."
262+
263+
devcontainer exec --workspace-folder "$workspace_folder" claude update
264+
265+
log_success "Claude Code upgraded"
266+
}
267+
268+
cmd_mount() {
269+
local host_path="${1:-}"
270+
local container_path="${2:-}"
271+
local readonly="false"
272+
273+
if [[ -z "$host_path" ]] || [[ -z "$container_path" ]]; then
274+
log_error "Usage: devc mount <host_path> <container_path> [--readonly]"
275+
exit 1
276+
fi
277+
278+
[[ "${3:-}" == "--readonly" ]] && readonly="true"
279+
280+
# Expand and validate host path
281+
host_path="$(cd "$host_path" 2>/dev/null && pwd)" || {
282+
log_error "Host path does not exist: $1"
283+
exit 1
284+
}
285+
286+
local workspace_folder
287+
workspace_folder="$(get_workspace_folder)"
288+
local devcontainer_json="$workspace_folder/.devcontainer/devcontainer.json"
289+
290+
if [[ ! -f "$devcontainer_json" ]]; then
291+
log_error "No devcontainer.json found. Run 'devc template' first."
292+
exit 1
293+
fi
294+
295+
check_devcontainer_cli
296+
297+
log_info "Adding mount: $host_path$container_path"
298+
update_devcontainer_mounts "$devcontainer_json" "$host_path" "$container_path" "$readonly"
299+
300+
log_info "Recreating container with new mount..."
301+
devcontainer up --workspace-folder "$workspace_folder" --remove-existing-container
302+
303+
log_success "Mount added: $host_path$container_path"
304+
}
305+
158306
cmd_self_install() {
159307
local install_dir="$HOME/.local/bin"
160308
local install_path="$install_dir/devc"
@@ -232,6 +380,16 @@ main() {
232380
shell)
233381
cmd_shell
234382
;;
383+
exec)
384+
[[ "${1:-}" == "--" ]] && shift
385+
cmd_exec "$@"
386+
;;
387+
upgrade)
388+
cmd_upgrade
389+
;;
390+
mount)
391+
cmd_mount "$@"
392+
;;
235393
self-install)
236394
cmd_self_install
237395
;;

0 commit comments

Comments
 (0)