@@ -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,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+
77152cmd_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+
158306cmd_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