diff --git a/Taskfile.yml b/Taskfile.yml index 6f7df418..6b5b06e5 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -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" @@ -140,6 +145,8 @@ tasks: macos:shell: desc: Run shell in dev vagrant silent: true + var: + TMUX_PANE: "" cmds: - | echo "APP_NAME: {{.APP_NAME}}" @@ -149,6 +156,8 @@ tasks: macos:stop: desc: Stop dev vagrant VM silent: true + var: + TMUX_PANE: "" cmds: - | echo "APP_NAME: {{.APP_NAME}}" @@ -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 -- )" + silent: false + cmds: + - | + set -e + if [ -z "{{.CLI_ARGS}}" ]; then + echo "Usage: task vagrant:box:export -- " + 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 -- " + 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 diff --git a/Vagrantfile b/Vagrantfile index 78838c00..3362b083 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -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) @@ -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) @@ -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 @@ -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) @@ -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 @@ -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 @@ -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 diff --git a/Vagrantfile.local-example b/Vagrantfile.local-example index 78357aaa..b6c75d48 100644 --- a/Vagrantfile.local-example +++ b/Vagrantfile.local-example @@ -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