@@ -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
3942Examples:
@@ -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
4652EOF
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+
77202cmd_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+
158357cmd_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