Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 82 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,21 @@ tasks:
cmds:
- |
echo "APP_NAME: {{.APP_NAME}}"
echo "DEVCELL_DIR: {{.DEVCELL_DIR}}"

- vagrant destroy -f

macos:claude:
desc: Run Claude CLI in dev vagrant
silent: false
silent: true
var:
TMUX_PANE: ""
cmds:
- |
echo "APP_NAME: {{.APP_NAME}}"
echo "GUEST_DIR: {{.GUEST_DIR}}"
echo "CELL_HOME: {{.CELL_HOME}}"

# Copy Vagrantfile.local from example if it doesn't exist
if [ ! -f "{{.DEVCELL_DIR}}/Vagrantfile.local" ]; then
cp "{{.DEVCELL_DIR}}/Vagrantfile.local-example" "{{.DEVCELL_DIR}}/Vagrantfile.local"
Expand All @@ -140,6 +145,8 @@ tasks:
macos:shell:
desc: Run shell in dev vagrant
silent: true
var:
TMUX_PANE: ""
cmds:
- |
echo "APP_NAME: {{.APP_NAME}}"
Expand All @@ -149,6 +156,8 @@ tasks:
macos:stop:
desc: Stop dev vagrant VM
silent: true
var:
TMUX_PANE: ""
cmds:
- |
echo "APP_NAME: {{.APP_NAME}}"
Expand All @@ -161,3 +170,75 @@ tasks:
- |
echo "APP_NAME: {{.APP_NAME}}"
- vagrant {{.CLI_ARGS}}

vagrant:box:export:
desc: "Export UTM VM as Vagrant box (usage: task vagrant:box:export -- <boxname> <vm-name>)"
silent: false
cmds:
- |
set -e
if [ -z "{{.CLI_ARGS}}" ]; then
echo "Usage: task vagrant:box:export -- <boxname> <vm-name>"
echo "Example: task vagrant:box:export -- devcell-macOS26 callnut-60"
exit 1
fi

BOX_NAME=$(echo "{{.CLI_ARGS}}" | awk '{print $1}')
VM_NAME=$(echo "{{.CLI_ARGS}}" | awk '{print $2}')
UTM_DIR="$HOME/Library/Containers/com.utmapp.UTM/Data/Documents"

if [ -z "$BOX_NAME" ] || [ -z "$VM_NAME" ]; then
echo "Error: Both boxname and vm-name are required"
echo "Usage: task vagrant:box:export -- <boxname> <vm-name>"
exit 1
fi

if [ ! -d "$UTM_DIR/$VM_NAME.utm" ]; then
echo "Error: VM not found at $UTM_DIR/$VM_NAME.utm"
echo "Available VMs:"
ls -1 "$UTM_DIR"/*.utm 2>/dev/null | xargs -I {} basename {} .utm
exit 1
fi

echo "Exporting VM '$VM_NAME' as box '$BOX_NAME'..."

# Create temp directory with correct structure
TEMP_DIR=$(mktemp -d)

# Create metadata.json
echo '{"provider":"utm"}' > "$TEMP_DIR/metadata.json"

# Create embedded Vagrantfile with box defaults
cat > "$TEMP_DIR/Vagrantfile" <<'VAGRANTFILE'
Vagrant.configure("2") do |config|
config.ssh.username = "vagrant"
config.ssh.host = "vagrant-macos.local"
config.ssh.port = 22
config.ssh.insert_key = false

config.vm.provider :utm do |utm|
utm.check_guest_additions = false
utm.skip_directory_share_mode = true
end
end
VAGRANTFILE

# Copy VM as box.utm (required name for vagrant_utm)
echo "Copying VM (this may take a while)..."
cp -R "$UTM_DIR/$VM_NAME.utm" "$TEMP_DIR/box.utm"

# Create box archive (no utm/ wrapper - vagrant creates the provider folder)
echo "Creating box archive..."
tar cvzf ~/$BOX_NAME.box -C "$TEMP_DIR" metadata.json Vagrantfile box.utm

# Cleanup temp dir
rm -rf "$TEMP_DIR"

echo "Adding box to Vagrant..."
vagrant box add ~/$BOX_NAME.box --name $BOX_NAME --force

echo ""
echo "Box exported successfully!"
echo "Use with: MACOS_BOX=$BOX_NAME task macos:claude"
echo ""
vagrant box list
37 changes: 22 additions & 15 deletions Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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

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

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

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

config.vm.provider :utm do |utm|
utm.name = VM_NAME
utm.memory = 4096
utm.cpus = 2
utm.memory = 8192
utm.cpus = 4
utm.check_guest_additions = false
# Apple Virtualization: skip QEMU directory_share_mode (uses VirtioFS instead)
utm.skip_directory_share_mode = true
Expand All @@ -74,9 +73,13 @@ Vagrant.configure("2") do |config|
ln -sfn "#{DEVCELL_VIRTFS_MOUNT}" "#{DEVCELL_GUEST_PATH}"
fi

# Claude/Codex config symlinks (VirtioFS mounts to ~/.claude and ~/.codex)
[ -d "/Volumes/My Shared Files/.claude" ] && ln -sfn "/Volumes/My Shared Files/.claude" ~/.claude
[ -d "/Volumes/My Shared Files/.codex" ] && ln -sfn "/Volumes/My Shared Files/.codex" ~/.codex
# Claude/Codex config symlinks from CELL_HOME mount
CELL_MOUNT="/Volumes/My Shared Files/#{File.basename(CELL_HOME)}"
if [ -d "$CELL_MOUNT" ]; then
[ -d "$CELL_MOUNT/.claude" ] && ln -sfn "$CELL_MOUNT/.claude" ~/.claude
[ -f "$CELL_MOUNT/.claude.json" ] && ln -sfn "$CELL_MOUNT/.claude.json" ~/.claude.json
[ -d "$CELL_MOUNT/.codex" ] && ln -sfn "$CELL_MOUNT/.codex" ~/.codex
fi
SHELL

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

# =============================================================================
# BASE PROVISIONERS (already done in devcell-macOS26 box, kept for reference)
# To re-run: vagrant provision --provision-with asdf
# To rebuild base box: MACOS_BOX=macOS26 vagrant up --provision
# =============================================================================

# Install asdf via Homebrew (run as vagrant user, not root)
config.vm.provision "shell", name: "asdf", privileged: false, inline: <<~ASDF_BREW
config.vm.provision "shell", name: "asdf", run: "never", privileged: false, inline: <<~ASDF_BREW
if ! command -v brew >/dev/null 2>&1; then
echo "Homebrew not found. Installing..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" < /dev/null
Expand All @@ -108,7 +117,7 @@ Vagrant.configure("2") do |config|
ASDF_BREW

# Install asdf plugins (mirrors Dockerfile)
config.vm.provision "shell", name: "asdf-plugins", privileged: false, inline: <<~ASDF_PLUGINS
config.vm.provision "shell", name: "asdf-plugins", run: "never", privileged: false, inline: <<~ASDF_PLUGINS
export PATH="${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$(brew --prefix asdf)/libexec:$PATH"

asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git || true
Expand All @@ -126,10 +135,8 @@ end
# Load secondary Vagrantfile from HOST_PATH (user's project directory) if it exists
# This allows projects to extend the base configuration (similar to Dockerfile-local)
# Must be outside the configure block so both configure blocks are at the same level
user_vagrantfile = File.join(HOST_PATH, "Vagrantfile.local")
user_vagrantfile = File.join(DEVCELL_DIR, "Vagrantfile.local")
if File.exist?(user_vagrantfile)
puts "Loading Vagrantfile.local from: #{user_vagrantfile}"
load user_vagrantfile
else
puts "Vagrantfile.local not found at: #{user_vagrantfile}"
end
23 changes: 12 additions & 11 deletions Vagrantfile.local-example
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,24 @@

Vagrant.configure("2") do |config|
# Install runtimes from .tool-versions via asdf
# Check project first, fallback to devcell
# Always install devcell's tools first (nodejs, uv, etc.), then project's
config.vm.provision "shell", name: "asdf-install", privileged: false, inline: <<~ASDF_INSTALL
export PATH="${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$(brew --prefix asdf)/libexec:$PATH"

TOOL_VERSIONS=""
if [ -f "#{GUEST_PATH}/.tool-versions" ]; then
TOOL_VERSIONS="#{GUEST_PATH}/.tool-versions"
elif [ -f "#{DEVCELL_GUEST_PATH}/.tool-versions" ]; then
TOOL_VERSIONS="#{DEVCELL_GUEST_PATH}/.tool-versions"
# First install devcell's .tool-versions (base tooling: nodejs, uv, etc.)
if [ -f "#{DEVCELL_GUEST_PATH}/.tool-versions" ]; then
echo "Installing base tools from #{DEVCELL_GUEST_PATH}/.tool-versions"
cp "#{DEVCELL_GUEST_PATH}/.tool-versions" ~/.tool-versions
asdf install
fi

if [ -n "$TOOL_VERSIONS" ]; then
echo "Using $TOOL_VERSIONS"
cp "$TOOL_VERSIONS" ~/.tool-versions
# Then install project's .tool-versions (may override or add versions)
if [ -f "#{GUEST_PATH}/.tool-versions" ] && [ "#{GUEST_PATH}" != "#{DEVCELL_GUEST_PATH}" ]; then
echo "Installing project tools from #{GUEST_PATH}/.tool-versions"
# Merge: project versions take precedence
cat "#{GUEST_PATH}/.tool-versions" >> ~/.tool-versions
sort -u -k1,1 ~/.tool-versions -o ~/.tool-versions
asdf install
else
echo ".tool-versions not found; skipping asdf install."
fi
ASDF_INSTALL

Expand Down
Loading