From 082be69ae984b4c62618734a15fb5c914e447604 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Thu, 29 Jan 2026 08:48:17 +0000 Subject: [PATCH 1/2] Initialize home directory and improve environment setup - chore(Dockerfile): add /opt/home and related directories with correct ownership - chore(Dockerfile): move asdf and nix config templates to /opt/home for runtime copying - chore(entrypoint.sh): copy template dotfiles and config from /opt/home to home on container start - chore(Taskfile.yml): add backup and restore tasks for .claude.json; update CELL_HOME path - chore(Taskfile.yml): add backup dependency for claude CLI tasks - chore(.github/workflows/release.yml): fix tag iteration to skip empty lines in release manifest steps - chore(compose.yml): simplify mounts by mounting entire CELL_HOME instead of separate files/dirs - chore(Vagrantfile): add comment about mDNS-based SSH host for macOS VM using Bonjour --- .github/workflows/release.yml | 6 ++++-- Dockerfile | 31 ++++++++++++++++------------- Taskfile.yml | 37 ++++++++++++++++++++++++++++++----- Vagrantfile | 1 + compose.yml | 13 +----------- entrypoint.sh | 34 +++++++++++++++++++++++++++++++- 6 files changed, 88 insertions(+), 34 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e401974..3e040695 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -107,7 +107,8 @@ jobs: run: | IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" - for TAG in ${{ steps.meta.outputs.tags }}; do + echo '${{ steps.meta.outputs.tags }}' | while read -r TAG; do + [ -z "$TAG" ] && continue TAG_NAME="${TAG##*:}" echo "Creating manifest for ${TAG}" @@ -118,7 +119,8 @@ jobs: - name: Inspect manifests run: | - for TAG in ${{ steps.meta.outputs.tags }}; do + echo '${{ steps.meta.outputs.tags }}' | while read -r TAG; do + [ -z "$TAG" ] && continue echo "=== Inspecting ${TAG} ===" docker buildx imagetools inspect "${TAG}" done diff --git a/Dockerfile b/Dockerfile index 1dd04fbf..57f4e340 100644 --- a/Dockerfile +++ b/Dockerfile @@ -117,16 +117,19 @@ RUN \ echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers # Create config and data directories with proper ownership -RUN mkdir -p /config /data && \ - chown -R ${USER_UID}:${USER_GID} /config /data +RUN mkdir -p /config /data /opt/asdf /opt/home/.config/nix /opt/home/.local/bin && \ + chown -R ${USER_UID}:${USER_GID} /config /data /opt/asdf /opt/home # Switch to non-root user USER ${USER_UID}:${USER_GID} -# Configure shell for user -RUN echo '. ${ASDF_DIR}/asdf.sh' >> /home/${USER_NAME}/.bashrc && \ - echo '. ${ASDF_DIR}/asdf.sh' >> /home/${USER_NAME}/.profile && \ - echo '. ${ASDF_DIR}/asdf.sh' >> /home/${USER_NAME}/.zshrc +# Copy .tool-versions to /opt/home (copied to ~ at runtime by entrypoint) +COPY --chown=${USER_UID}:${USER_GID} .tool-versions /opt/home/.tool-versions + +# Create template shell RC files in /opt/home (copied to ~ at runtime by entrypoint) +RUN echo '. ${ASDF_DIR}/asdf.sh' >> /opt/home/.bashrc && \ + echo '. ${ASDF_DIR}/asdf.sh' >> /opt/home/.profile && \ + echo '. ${ASDF_DIR}/asdf.sh' >> /opt/home/.zshrc WORKDIR /home/${USER_NAME} @@ -138,24 +141,24 @@ ENV CELL_HOME=/home/${USER_NAME} ENV HOME=/home/${USER_NAME} # Install Nix package manager with flakes support (conditional) +# Note: Nix installs to /home/${USER_NAME}/.nix-profile, templates go to /opt/home RUN if [ "$DEVCELL_NIX_ENABLED" = "true" ]; then \ curl -L https://nixos.org/nix/install | sh -s -- --no-daemon && \ - mkdir -p /home/${USER_NAME}/.config/nix && \ - echo "experimental-features = nix-command flakes" >> /home/${USER_NAME}/.config/nix/nix.conf && \ - echo '. ${HOME}/.nix-profile/etc/profile.d/nix.sh' >> /home/${USER_NAME}/.bashrc && \ - echo '. ${HOME}/.nix-profile/etc/profile.d/nix.sh' >> /home/${USER_NAME}/.profile && \ - echo '. ${HOME}/.nix-profile/etc/profile.d/nix.sh' >> /home/${USER_NAME}/.zshrc; \ + echo "experimental-features = nix-command flakes" >> /opt/home/.config/nix/nix.conf && \ + echo '. ${HOME}/.nix-profile/etc/profile.d/nix.sh' >> /opt/home/.bashrc && \ + echo '. ${HOME}/.nix-profile/etc/profile.d/nix.sh' >> /opt/home/.profile && \ + echo '. ${HOME}/.nix-profile/etc/profile.d/nix.sh' >> /opt/home/.zshrc; \ fi # Install asdf version manager ENV ASDF_VERSION=v0.14.1 -ENV ASDF_DIR=/home/${USER_NAME}/.asdf -ENV ASDF_DATA_DIR=/home/${USER_NAME}/.asdf +ENV ASDF_DIR=/opt/asdf +ENV ASDF_DATA_DIR=/opt/asdf ENV PATH="${ASDF_DIR}/bin:${ASDF_DIR}/shims:${PATH}" # Install and configure asdf to read legacy version files (.python-version, .ruby-version, etc.) RUN git clone https://github.com/asdf-vm/asdf.git ${ASDF_DIR} --branch ${ASDF_VERSION} && \ - echo "legacy_version_file = yes" > /home/${USER_NAME}/.asdfrc + echo "legacy_version_file = yes" > /opt/home/.asdfrc # Install asdf plugins diff --git a/Taskfile.yml b/Taskfile.yml index 6b5b06e5..6b353e11 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -21,7 +21,7 @@ vars: CODEX: "{{.RUN}} codex --dangerously-bypass-approvals-and-sandbox" SHELL: "{{.RUN}} bash" - CELL_HOME: '{{.HOME}}/.claude-{{ env "TMUX_SESSION_NAME" | default "sandbox"}}' + CELL_HOME: '{{.HOME}}/.devcell/{{ env "TMUX_SESSION_NAME" | default "sandbox"}}' PORT_PREFIX: '{{env "TMUX_PANE" | replace "%" "" | default ""}}' BASE_DIR: "{{ default .USER_WORKING_DIR }}" @@ -29,6 +29,7 @@ vars: GUEST_DIR: '{{regexReplaceAll "^/Users/[^/]+" .USER_WORKING_DIR "/Users/vagrant"}}' VAGRANT_CLAUDE: 'vagrant ssh -c "cd {{.GUEST_DIR}} && claude --dangerously-skip-permissions {{.CLI_ARGS}}"' VAGRANT_SHELL: 'vagrant ssh -- -t "cd {{.GUEST_DIR}} && exec zsh -l"' + # MACOS_SSH_HOST: "{{.APP_NAME}}.local" # Use Vagrantfile default env: APP_NAME: "{{.APP_NAME}}" @@ -44,6 +45,7 @@ env: PORT: "{{.PORT}}" BUILDKIT_PROGRESS: plain CELL_HOME: "{{.CELL_HOME}}" + # MACOS_SSH_HOST: "{{.MACOS_SSH_HOST}}" # Use Vagrantfile default ### # Include this via: @@ -53,17 +55,41 @@ env: # flatten: true tasks: + devcell:claude-json:backup: + desc: Backup .claude.json with timestamp, keep last 5 + internal: true + cmds: + - mkdir -p "{{.CELL_HOME}}" + - touch "{{.CELL_HOME}}/.claude.json" + - | + BACKUP_FILE="{{.CELL_HOME}}/.claude.json-devcell-$(date +%Y-%m-%dT%H-%M-%S)-backup" + cp "{{.CELL_HOME}}/.claude.json" "$BACKUP_FILE" 2>/dev/null || true + ls -t "{{.CELL_HOME}}"/.claude.json-devcell-*-backup 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true + + devcell:claude-json:restore: + desc: Restore .claude.json from latest backup + cmds: + - | + LATEST=$(ls -t "{{.CELL_HOME}}"/.claude.json-devcell-*-backup 2>/dev/null | head -1) + if [ -n "$LATEST" ]; then + cp "$LATEST" "{{.CELL_HOME}}/.claude.json" + echo "Restored from: $LATEST" + else + echo "No backup found in {{.CELL_HOME}}" + exit 1 + fi + claude: desc: Run Claude CLI in the dev container silent: true + deps: [devcell:claude-json:backup] cmds: - | echo "APP_NAME: {{.APP_NAME}}" echo "PORT_PREFIX: {{.PORT_PREFIX}}" - - echo "CELL_HOME: {{.CELL_HOME}} / $CELL_HOME" + echo "CELL_HOME: {{.CELL_HOME}}" echo "DEVCELL_DIR: {{.DEVCELL_DIR}}" - mkdir -p "{{.CELL_HOME}}" + - "{{.CLAUDE}} {{.CLI_ARGS}}" codex: @@ -127,6 +153,7 @@ tasks: macos:claude: desc: Run Claude CLI in dev vagrant silent: true + deps: [devcell:claude-json:backup] var: TMUX_PANE: "" cmds: @@ -134,7 +161,7 @@ tasks: 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" diff --git a/Vagrantfile b/Vagrantfile index 3362b083..6c9b2499 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -32,6 +32,7 @@ Vagrant.configure("2") do |config| config.vm.box_url = ENV["MACOS_BOX_URL"] if ENV["MACOS_BOX_URL"] # Direct SSH to VM (no port forwarding for Apple Virtualization) + # Uses mDNS/Bonjour: base VM hostname is "vagrant-macos", accessible at vagrant-macos.local config.ssh.host = ENV["MACOS_SSH_HOST"] || "vagrant-macos.local" config.ssh.port = 22 config.ssh.username = "vagrant" diff --git a/compose.yml b/compose.yml index 38a7d1bf..4a70ccef 100644 --- a/compose.yml +++ b/compose.yml @@ -36,21 +36,10 @@ services: # - ${HOME:-}/.config/git/:/home/${USER}/.config/git/:ro # XDG config git dir - disabled since host'ss config is managed by nix # Mount Claude and host agents - - ${CELL_HOME}/.claude.json:/home/${USER}/.claude.json # claude json - - ${CELL_HOME}/.claude/:/home/${USER}/.claude/ + - ${CELL_HOME}/:/home/${USER}/ - ${HOME:-}/.claude/commands:/home/${USER}/.claude/commands:ro # Host-based commands - ${HOME:-}/.claude/agents:/home/${USER}/.claude/agents:ro # Host-based agents - - # Mount Codex and host agents - - ${CELL_HOME}/.codex/:/home/${USER}/.codex/ - - ${HOME}/.codex/config.toml:/home/${USER}/.codex/config.toml - - # TODO: Replace with a dynamic path to a taskfile - - ${HOME:-}/dev/Taskfile.yml:/home/${USER}/dev/Taskfile.yml - - # Additional mounts can be added via compose.local.yml - environment: APP_NAME: ${APP_NAME:-app} IS_SANDBOX: 1 diff --git a/entrypoint.sh b/entrypoint.sh index 5350ede7..c6c0ba42 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,37 @@ #!/bin/bash -# Entrypoint script - optionally starts VNC server and runs command +# Entrypoint script - initializes home directory and optionally starts VNC server + +# ============================================================ +# Initialize home directory files (needed when ~ is mounted) +# Copy templates from /opt/home to $HOME if they don't exist +# ============================================================ + +OPT_HOME="/opt/home" + +# Copy individual dotfiles from /opt/home if they don't exist in $HOME +for file in .asdfrc .bashrc .profile .zshrc .tool-versions; do + if [ -f "$OPT_HOME/$file" ] && [ ! -f "$HOME/$file" ]; then + echo "Copying $file to ~/" + cp "$OPT_HOME/$file" "$HOME/$file" + fi +done + +# Copy .config directory structure (nix config, etc.) +if [ -d "$OPT_HOME/.config" ]; then + mkdir -p "$HOME/.config" + # Copy nix config if it exists and target doesn't + if [ -d "$OPT_HOME/.config/nix" ] && [ ! -d "$HOME/.config/nix" ]; then + echo "Copying .config/nix/ to ~/" + cp -r "$OPT_HOME/.config/nix" "$HOME/.config/" + fi +fi + +# Create .local/bin if it doesn't exist +mkdir -p "$HOME/.local/bin" + +# ============================================================ +# GUI Setup (optional) +# ============================================================ # Only start GUI if DEVCELL_GUI_ENABLED is set to "true" if [ "$DEVCELL_GUI_ENABLED" = "true" ]; then From 42f593cac4ac91b7e14e41f95d1354fc1416be97 Mon Sep 17 00:00:00 2001 From: Dmitry Kireev Date: Sat, 31 Jan 2026 08:24:56 +0000 Subject: [PATCH 2/2] Update .tool-versions-example to remove ruby version - chore(.tool-versions-example): remove ruby 3.4.7 version entry from tool versions list --- .tool-versions-example | 1 - 1 file changed, 1 deletion(-) diff --git a/.tool-versions-example b/.tool-versions-example index 48ba5631..4e3a8408 100644 --- a/.tool-versions-example +++ b/.tool-versions-example @@ -1,5 +1,4 @@ nodejs 24.12.0 -ruby 3.4.7 python 3.14.2 opentofu 1.11.0 golang 1.25.5