Skip to content

Commit 9dfbcf7

Browse files
authored
Merge pull request #3 from DimmKirr/feature/wip
Add UTM VM export task and improve macOS Vagrant setup
2 parents e824b5e + 14a03c2 commit 9dfbcf7

3 files changed

Lines changed: 116 additions & 27 deletions

File tree

‎Taskfile.yml‎

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,21 @@ tasks:
120120
cmds:
121121
- |
122122
echo "APP_NAME: {{.APP_NAME}}"
123+
echo "DEVCELL_DIR: {{.DEVCELL_DIR}}"
123124
124125
- vagrant destroy -f
125126

126127
macos:claude:
127128
desc: Run Claude CLI in dev vagrant
128-
silent: false
129+
silent: true
130+
var:
131+
TMUX_PANE: ""
129132
cmds:
130133
- |
131134
echo "APP_NAME: {{.APP_NAME}}"
132135
echo "GUEST_DIR: {{.GUEST_DIR}}"
136+
echo "CELL_HOME: {{.CELL_HOME}}"
137+
133138
# Copy Vagrantfile.local from example if it doesn't exist
134139
if [ ! -f "{{.DEVCELL_DIR}}/Vagrantfile.local" ]; then
135140
cp "{{.DEVCELL_DIR}}/Vagrantfile.local-example" "{{.DEVCELL_DIR}}/Vagrantfile.local"
@@ -140,6 +145,8 @@ tasks:
140145
macos:shell:
141146
desc: Run shell in dev vagrant
142147
silent: true
148+
var:
149+
TMUX_PANE: ""
143150
cmds:
144151
- |
145152
echo "APP_NAME: {{.APP_NAME}}"
@@ -149,6 +156,8 @@ tasks:
149156
macos:stop:
150157
desc: Stop dev vagrant VM
151158
silent: true
159+
var:
160+
TMUX_PANE: ""
152161
cmds:
153162
- |
154163
echo "APP_NAME: {{.APP_NAME}}"
@@ -161,3 +170,75 @@ tasks:
161170
- |
162171
echo "APP_NAME: {{.APP_NAME}}"
163172
- vagrant {{.CLI_ARGS}}
173+
174+
vagrant:box:export:
175+
desc: "Export UTM VM as Vagrant box (usage: task vagrant:box:export -- <boxname> <vm-name>)"
176+
silent: false
177+
cmds:
178+
- |
179+
set -e
180+
if [ -z "{{.CLI_ARGS}}" ]; then
181+
echo "Usage: task vagrant:box:export -- <boxname> <vm-name>"
182+
echo "Example: task vagrant:box:export -- devcell-macOS26 callnut-60"
183+
exit 1
184+
fi
185+
186+
BOX_NAME=$(echo "{{.CLI_ARGS}}" | awk '{print $1}')
187+
VM_NAME=$(echo "{{.CLI_ARGS}}" | awk '{print $2}')
188+
UTM_DIR="$HOME/Library/Containers/com.utmapp.UTM/Data/Documents"
189+
190+
if [ -z "$BOX_NAME" ] || [ -z "$VM_NAME" ]; then
191+
echo "Error: Both boxname and vm-name are required"
192+
echo "Usage: task vagrant:box:export -- <boxname> <vm-name>"
193+
exit 1
194+
fi
195+
196+
if [ ! -d "$UTM_DIR/$VM_NAME.utm" ]; then
197+
echo "Error: VM not found at $UTM_DIR/$VM_NAME.utm"
198+
echo "Available VMs:"
199+
ls -1 "$UTM_DIR"/*.utm 2>/dev/null | xargs -I {} basename {} .utm
200+
exit 1
201+
fi
202+
203+
echo "Exporting VM '$VM_NAME' as box '$BOX_NAME'..."
204+
205+
# Create temp directory with correct structure
206+
TEMP_DIR=$(mktemp -d)
207+
208+
# Create metadata.json
209+
echo '{"provider":"utm"}' > "$TEMP_DIR/metadata.json"
210+
211+
# Create embedded Vagrantfile with box defaults
212+
cat > "$TEMP_DIR/Vagrantfile" <<'VAGRANTFILE'
213+
Vagrant.configure("2") do |config|
214+
config.ssh.username = "vagrant"
215+
config.ssh.host = "vagrant-macos.local"
216+
config.ssh.port = 22
217+
config.ssh.insert_key = false
218+
219+
config.vm.provider :utm do |utm|
220+
utm.check_guest_additions = false
221+
utm.skip_directory_share_mode = true
222+
end
223+
end
224+
VAGRANTFILE
225+
226+
# Copy VM as box.utm (required name for vagrant_utm)
227+
echo "Copying VM (this may take a while)..."
228+
cp -R "$UTM_DIR/$VM_NAME.utm" "$TEMP_DIR/box.utm"
229+
230+
# Create box archive (no utm/ wrapper - vagrant creates the provider folder)
231+
echo "Creating box archive..."
232+
tar cvzf ~/$BOX_NAME.box -C "$TEMP_DIR" metadata.json Vagrantfile box.utm
233+
234+
# Cleanup temp dir
235+
rm -rf "$TEMP_DIR"
236+
237+
echo "Adding box to Vagrant..."
238+
vagrant box add ~/$BOX_NAME.box --name $BOX_NAME --force
239+
240+
echo ""
241+
echo "Box exported successfully!"
242+
echo "Use with: MACOS_BOX=$BOX_NAME task macos:claude"
243+
echo ""
244+
vagrant box list

‎Vagrantfile‎

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# Derive paths from synced folder source (BASE_DIR from Taskfile, or current dir)
88
HOST_PATH = ENV["BASE_DIR"] || File.expand_path(".")
9-
DEVCELL_DIR = File.expand_path(".") # Where this Vagrantfile lives
9+
DEVCELL_DIR = File.dirname(File.expand_path(__FILE__)) # Where this Vagrantfile lives
1010
FOLDER_NAME = File.basename(HOST_PATH)
1111
DEVCELL_FOLDER = File.basename(DEVCELL_DIR)
1212

@@ -28,7 +28,7 @@ Vagrant.configure("2") do |config|
2828
config.vm.define VM_NAME
2929
config.vm.hostname = VM_NAME
3030

31-
config.vm.box = ENV["MACOS_BOX"] || "macOS26"
31+
config.vm.box = ENV["MACOS_BOX"] || "devcell-macOS26"
3232
config.vm.box_url = ENV["MACOS_BOX_URL"] if ENV["MACOS_BOX_URL"]
3333

3434
# Direct SSH to VM (no port forwarding for Apple Virtualization)
@@ -47,14 +47,13 @@ Vagrant.configure("2") do |config|
4747
if HOST_PATH != DEVCELL_DIR
4848
config.vm.synced_folder DEVCELL_DIR, "/devcell", type: :utm
4949
end
50-
# Claude/Codex config directories (shared between host and guest)
51-
config.vm.synced_folder "#{CELL_HOME}/.claude", "/Users/vagrant/.claude", type: :utm, create: true
52-
config.vm.synced_folder "#{CELL_HOME}/.codex", "/Users/vagrant/.codex", type: :utm, create: true
50+
# Claude/Codex config home (CELL_HOME contains .claude/, .claude.json, .codex/)
51+
config.vm.synced_folder CELL_HOME, "/cell-home", type: :utm, create: true
5352

5453
config.vm.provider :utm do |utm|
5554
utm.name = VM_NAME
56-
utm.memory = 4096
57-
utm.cpus = 2
55+
utm.memory = 8192
56+
utm.cpus = 4
5857
utm.check_guest_additions = false
5958
# Apple Virtualization: skip QEMU directory_share_mode (uses VirtioFS instead)
6059
utm.skip_directory_share_mode = true
@@ -74,9 +73,13 @@ Vagrant.configure("2") do |config|
7473
ln -sfn "#{DEVCELL_VIRTFS_MOUNT}" "#{DEVCELL_GUEST_PATH}"
7574
fi
7675
77-
# Claude/Codex config symlinks (VirtioFS mounts to ~/.claude and ~/.codex)
78-
[ -d "/Volumes/My Shared Files/.claude" ] && ln -sfn "/Volumes/My Shared Files/.claude" ~/.claude
79-
[ -d "/Volumes/My Shared Files/.codex" ] && ln -sfn "/Volumes/My Shared Files/.codex" ~/.codex
76+
# Claude/Codex config symlinks from CELL_HOME mount
77+
CELL_MOUNT="/Volumes/My Shared Files/#{File.basename(CELL_HOME)}"
78+
if [ -d "$CELL_MOUNT" ]; then
79+
[ -d "$CELL_MOUNT/.claude" ] && ln -sfn "$CELL_MOUNT/.claude" ~/.claude
80+
[ -f "$CELL_MOUNT/.claude.json" ] && ln -sfn "$CELL_MOUNT/.claude.json" ~/.claude.json
81+
[ -d "$CELL_MOUNT/.codex" ] && ln -sfn "$CELL_MOUNT/.codex" ~/.codex
82+
fi
8083
SHELL
8184

8285
# # Fix home directory permissions (in case previous provisioning created files as root)
@@ -87,8 +90,14 @@ Vagrant.configure("2") do |config|
8790
# done
8891
# FIX_PERMS
8992

93+
# =============================================================================
94+
# BASE PROVISIONERS (already done in devcell-macOS26 box, kept for reference)
95+
# To re-run: vagrant provision --provision-with asdf
96+
# To rebuild base box: MACOS_BOX=macOS26 vagrant up --provision
97+
# =============================================================================
98+
9099
# Install asdf via Homebrew (run as vagrant user, not root)
91-
config.vm.provision "shell", name: "asdf", privileged: false, inline: <<~ASDF_BREW
100+
config.vm.provision "shell", name: "asdf", run: "never", privileged: false, inline: <<~ASDF_BREW
92101
if ! command -v brew >/dev/null 2>&1; then
93102
echo "Homebrew not found. Installing..."
94103
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" < /dev/null
@@ -108,7 +117,7 @@ Vagrant.configure("2") do |config|
108117
ASDF_BREW
109118

110119
# Install asdf plugins (mirrors Dockerfile)
111-
config.vm.provision "shell", name: "asdf-plugins", privileged: false, inline: <<~ASDF_PLUGINS
120+
config.vm.provision "shell", name: "asdf-plugins", run: "never", privileged: false, inline: <<~ASDF_PLUGINS
112121
export PATH="${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$(brew --prefix asdf)/libexec:$PATH"
113122
114123
asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git || true
@@ -126,10 +135,8 @@ end
126135
# Load secondary Vagrantfile from HOST_PATH (user's project directory) if it exists
127136
# This allows projects to extend the base configuration (similar to Dockerfile-local)
128137
# Must be outside the configure block so both configure blocks are at the same level
129-
user_vagrantfile = File.join(HOST_PATH, "Vagrantfile.local")
138+
user_vagrantfile = File.join(DEVCELL_DIR, "Vagrantfile.local")
130139
if File.exist?(user_vagrantfile)
131140
puts "Loading Vagrantfile.local from: #{user_vagrantfile}"
132141
load user_vagrantfile
133-
else
134-
puts "Vagrantfile.local not found at: #{user_vagrantfile}"
135142
end

‎Vagrantfile.local-example‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,24 @@
99

1010
Vagrant.configure("2") do |config|
1111
# Install runtimes from .tool-versions via asdf
12-
# Check project first, fallback to devcell
12+
# Always install devcell's tools first (nodejs, uv, etc.), then project's
1313
config.vm.provision "shell", name: "asdf-install", privileged: false, inline: <<~ASDF_INSTALL
1414
export PATH="${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$(brew --prefix asdf)/libexec:$PATH"
1515
16-
TOOL_VERSIONS=""
17-
if [ -f "#{GUEST_PATH}/.tool-versions" ]; then
18-
TOOL_VERSIONS="#{GUEST_PATH}/.tool-versions"
19-
elif [ -f "#{DEVCELL_GUEST_PATH}/.tool-versions" ]; then
20-
TOOL_VERSIONS="#{DEVCELL_GUEST_PATH}/.tool-versions"
16+
# First install devcell's .tool-versions (base tooling: nodejs, uv, etc.)
17+
if [ -f "#{DEVCELL_GUEST_PATH}/.tool-versions" ]; then
18+
echo "Installing base tools from #{DEVCELL_GUEST_PATH}/.tool-versions"
19+
cp "#{DEVCELL_GUEST_PATH}/.tool-versions" ~/.tool-versions
20+
asdf install
2121
fi
2222
23-
if [ -n "$TOOL_VERSIONS" ]; then
24-
echo "Using $TOOL_VERSIONS"
25-
cp "$TOOL_VERSIONS" ~/.tool-versions
23+
# Then install project's .tool-versions (may override or add versions)
24+
if [ -f "#{GUEST_PATH}/.tool-versions" ] && [ "#{GUEST_PATH}" != "#{DEVCELL_GUEST_PATH}" ]; then
25+
echo "Installing project tools from #{GUEST_PATH}/.tool-versions"
26+
# Merge: project versions take precedence
27+
cat "#{GUEST_PATH}/.tool-versions" >> ~/.tool-versions
28+
sort -u -k1,1 ~/.tool-versions -o ~/.tool-versions
2629
asdf install
27-
else
28-
echo ".tool-versions not found; skipping asdf install."
2930
fi
3031
ASDF_INSTALL
3132

0 commit comments

Comments
 (0)