Skip to content

Commit ae00301

Browse files
dguidoclaude
andcommitted
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>
1 parent fefdf3f commit ae00301

File tree

1 file changed

+209
-0
lines changed

1 file changed

+209
-0
lines changed

install.sh

Lines changed: 209 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,125 @@ 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+
python3 - "$devcontainer_json" "$temp_file" <<'PYTHON'
94+
import json
95+
import sys
96+
97+
devcontainer_json = sys.argv[1]
98+
temp_file = sys.argv[2]
99+
100+
# Default mounts from the template (these should not be preserved as "custom")
101+
DEFAULT_MOUNT_PREFIXES = [
102+
"source=claude-code-bashhistory-",
103+
"source=claude-code-config-",
104+
"source=claude-code-gh-",
105+
"source=${localEnv:HOME}/.gitconfig,",
106+
]
107+
108+
with open(devcontainer_json) as f:
109+
config = json.load(f)
110+
111+
mounts = config.get("mounts", [])
112+
custom_mounts = []
113+
114+
for mount in mounts:
115+
is_default = any(mount.startswith(prefix) for prefix in DEFAULT_MOUNT_PREFIXES)
116+
if not is_default:
117+
custom_mounts.append(mount)
118+
119+
if custom_mounts:
120+
with open(temp_file, "w") as f:
121+
json.dump(custom_mounts, f)
122+
print(temp_file)
123+
else:
124+
print("")
125+
PYTHON
126+
}
127+
128+
# Merge preserved mounts back into devcontainer.json
129+
merge_mounts_from_file() {
130+
local devcontainer_json="$1"
131+
local mounts_file="$2"
132+
133+
[[ -f "$mounts_file" ]] || return 0
134+
[[ -s "$mounts_file" ]] || return 0
135+
136+
python3 - "$devcontainer_json" "$mounts_file" <<'PYTHON'
137+
import json
138+
import sys
139+
140+
devcontainer_json = sys.argv[1]
141+
mounts_file = sys.argv[2]
142+
143+
with open(devcontainer_json) as f:
144+
config = json.load(f)
145+
146+
with open(mounts_file) as f:
147+
custom_mounts = json.load(f)
148+
149+
existing_mounts = config.get("mounts", [])
150+
151+
# Add custom mounts that aren't already present
152+
for mount in custom_mounts:
153+
if mount not in existing_mounts:
154+
existing_mounts.append(mount)
155+
156+
config["mounts"] = existing_mounts
157+
158+
with open(devcontainer_json, "w") as f:
159+
json.dump(config, f, indent=2)
160+
f.write("\n")
161+
PYTHON
162+
}
163+
164+
# Add or update a mount in devcontainer.json
165+
update_devcontainer_mounts() {
166+
local devcontainer_json="$1"
167+
local host_path="$2"
168+
local container_path="$3"
169+
local readonly="${4:-false}"
170+
171+
python3 - "$devcontainer_json" "$host_path" "$container_path" "$readonly" <<'PYTHON'
172+
import json
173+
import sys
174+
175+
devcontainer_json = sys.argv[1]
176+
host_path = sys.argv[2]
177+
container_path = sys.argv[3]
178+
readonly = sys.argv[4] == "true"
179+
180+
with open(devcontainer_json) as f:
181+
config = json.load(f)
182+
183+
mounts = config.get("mounts", [])
184+
185+
# Build the new mount string
186+
mount_str = f"source={host_path},target={container_path},type=bind"
187+
if readonly:
188+
mount_str += ",readonly"
189+
190+
# Remove any existing mount with the same target
191+
mounts = [m for m in mounts if f"target={container_path}," not in m and not m.endswith(f"target={container_path}")]
192+
193+
mounts.append(mount_str)
194+
config["mounts"] = mounts
195+
196+
with open(devcontainer_json, "w") as f:
197+
json.dump(config, f, indent=2)
198+
f.write("\n")
199+
PYTHON
200+
}
201+
77202
cmd_template() {
78203
local target_dir="${1:-.}"
79204
target_dir="$(cd "$target_dir" 2>/dev/null && pwd)" || {
@@ -82,6 +207,8 @@ cmd_template() {
82207
}
83208

84209
local devcontainer_dir="$target_dir/.devcontainer"
210+
local devcontainer_json="$devcontainer_dir/devcontainer.json"
211+
local preserved_mounts=""
85212

86213
if [[ -d "$devcontainer_dir" ]]; then
87214
log_warn "Devcontainer already exists at $devcontainer_dir"
@@ -91,6 +218,12 @@ cmd_template() {
91218
log_info "Aborted."
92219
exit 0
93220
fi
221+
222+
# Preserve custom mounts before overwriting
223+
preserved_mounts=$(extract_mounts_to_file "$devcontainer_json")
224+
if [[ -n "$preserved_mounts" ]]; then
225+
log_info "Preserving custom mounts..."
226+
fi
94227
fi
95228

96229
mkdir -p "$devcontainer_dir"
@@ -101,6 +234,13 @@ cmd_template() {
101234
cp "$SCRIPT_DIR/post_install.py" "$devcontainer_dir/"
102235
cp "$SCRIPT_DIR/.zshrc" "$devcontainer_dir/"
103236

237+
# Restore preserved mounts
238+
if [[ -n "$preserved_mounts" ]]; then
239+
merge_mounts_from_file "$devcontainer_json" "$preserved_mounts"
240+
rm -f "$preserved_mounts"
241+
log_info "Custom mounts restored"
242+
fi
243+
104244
log_success "Template installed to $devcontainer_dir"
105245
}
106246

@@ -155,6 +295,65 @@ cmd_shell() {
155295
devcontainer exec --workspace-folder "$workspace_folder" zsh
156296
}
157297

298+
cmd_exec() {
299+
local workspace_folder
300+
workspace_folder="$(get_workspace_folder)"
301+
302+
check_devcontainer_cli
303+
devcontainer exec --workspace-folder "$workspace_folder" "$@"
304+
}
305+
306+
cmd_upgrade() {
307+
local workspace_folder
308+
workspace_folder="$(get_workspace_folder)"
309+
310+
check_devcontainer_cli
311+
log_info "Upgrading Claude Code..."
312+
313+
devcontainer exec --workspace-folder "$workspace_folder" \
314+
npm install -g @anthropic-ai/claude-code@latest
315+
316+
log_success "Claude Code upgraded"
317+
}
318+
319+
cmd_mount() {
320+
local host_path="${1:-}"
321+
local container_path="${2:-}"
322+
local readonly="false"
323+
324+
if [[ -z "$host_path" ]] || [[ -z "$container_path" ]]; then
325+
log_error "Usage: devc mount <host_path> <container_path> [--readonly]"
326+
exit 1
327+
fi
328+
329+
[[ "${3:-}" == "--readonly" ]] && readonly="true"
330+
331+
# Expand and validate host path
332+
host_path="$(cd "$host_path" 2>/dev/null && pwd)" || {
333+
log_error "Host path does not exist: $1"
334+
exit 1
335+
}
336+
337+
local workspace_folder
338+
workspace_folder="$(get_workspace_folder)"
339+
local devcontainer_json="$workspace_folder/.devcontainer/devcontainer.json"
340+
341+
if [[ ! -f "$devcontainer_json" ]]; then
342+
log_error "No devcontainer.json found. Run 'devc template' first."
343+
exit 1
344+
fi
345+
346+
check_devcontainer_cli
347+
348+
log_info "Adding mount: $host_path$container_path"
349+
update_devcontainer_mounts "$devcontainer_json" "$host_path" "$container_path" "$readonly"
350+
351+
log_info "Recreating container with new mount..."
352+
devcontainer up --workspace-folder "$workspace_folder" --remove-existing-container
353+
354+
log_success "Mount added: $host_path$container_path"
355+
}
356+
158357
cmd_self_install() {
159358
local install_dir="$HOME/.local/bin"
160359
local install_path="$install_dir/devc"
@@ -232,6 +431,16 @@ main() {
232431
shell)
233432
cmd_shell
234433
;;
434+
exec)
435+
[[ "${1:-}" == "--" ]] && shift
436+
cmd_exec "$@"
437+
;;
438+
upgrade)
439+
cmd_upgrade
440+
;;
441+
mount)
442+
cmd_mount "$@"
443+
;;
235444
self-install)
236445
cmd_self_install
237446
;;

0 commit comments

Comments
 (0)