@@ -34,7 +34,7 @@ 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
37+ exec <cmd> Execute a command in the running container
3838 upgrade Upgrade Claude Code to latest version
3939 mount <host> <cont> Add a mount to the devcontainer (recreates container)
4040 help Show this help message
@@ -46,7 +46,7 @@ Examples:
4646 devc shell # Open interactive shell
4747 devc self-install # Install devc to PATH
4848 devc update # Update to latest version
49- devc exec -- ls -la # Run command in container
49+ devc exec ls -la # Run command in container
5050 devc upgrade # Upgrade Claude Code to latest
5151 devc mount ~/data /data # Add mount to container
5252EOF
@@ -90,39 +90,23 @@ extract_mounts_to_file() {
9090
9191 temp_file=$( mktemp)
9292
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
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
126110}
127111
128112# Merge preserved mounts back into devcontainer.json
@@ -133,32 +117,15 @@ merge_mounts_from_file() {
133117 [[ -f " $mounts_file " ]] || return 0
134118 [[ -s " $mounts_file " ]] || return 0
135119
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)
120+ local custom_mounts
121+ custom_mounts=$( cat " $mounts_file " )
155122
156- config[" mounts" ] = existing_mounts
123+ local updated
124+ updated=$( jq --argjson custom " $custom_mounts " '
125+ .mounts = ((.mounts // []) + $custom | unique)
126+ ' " $devcontainer_json " )
157127
158- with open (devcontainer_json, " w" ) as f:
159- json.dump(config, f, indent = 2 )
160- f.write(" \n " )
161- PYTHON
128+ echo " $updated " > " $devcontainer_json "
162129}
163130
164131# Add or update a mount in devcontainer.json
@@ -168,35 +135,18 @@ update_devcontainer_mounts() {
168135 local container_path=" $3 "
169136 local readonly=" ${4:- false} "
170137
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} " )]
138+ local mount_str=" source=${host_path} ,target=${container_path} ,type=bind"
139+ [[ " $readonly " == " true" ]] && mount_str=" ${mount_str} ,readonly"
192140
193- mounts.append(mount_str)
194- config[" mounts" ] = mounts
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 " )
195148
196- with open (devcontainer_json, " w" ) as f:
197- json.dump(config, f, indent = 2 )
198- f.write(" \n " )
199- PYTHON
149+ echo " $updated " > " $devcontainer_json "
200150}
201151
202152cmd_template () {
@@ -310,8 +260,7 @@ cmd_upgrade() {
310260 check_devcontainer_cli
311261 log_info " Upgrading Claude Code..."
312262
313- devcontainer exec --workspace-folder " $workspace_folder " \
314- npm install -g @anthropic-ai/claude-code@latest
263+ devcontainer exec --workspace-folder " $workspace_folder " claude update
315264
316265 log_success " Claude Code upgraded"
317266}
0 commit comments