--- - name: Ensure host is macOS ansible.builtin.assert: that: - ansible_system == "Darwin" fail_msg: "Role macos_dev_station supports macOS hosts only" - name: Ensure launchd mode is valid ansible.builtin.assert: that: - macos_dev_station_launchd_mode == "daemon" fail_msg: "macos_dev_station currently supports daemon mode only. Set macos_dev_station_launchd_mode to 'daemon'." - name: Check passwordless sudo availability for daemon mode ansible.builtin.command: sudo -n true register: __macos_dev_station_sudo_check changed_when: false failed_when: false - name: Ensure sudo access is available for daemon mode ansible.builtin.assert: that: - __macos_dev_station_sudo_check.rc == 0 or (ansible_become_password | default('') | length > 0) fail_msg: "Daemon mode needs sudo. Configure passwordless sudo for ansible user or set ANSIBLE_BECOME_PASSWORD via sectool." - name: Ensure OpenCode server credentials are provided ansible.builtin.assert: that: - macos_dev_station_opencode_server_username | length > 0 - macos_dev_station_opencode_server_password | length > 0 fail_msg: "Set OPENCODE_SERVER_USERNAME and OPENCODE_SERVER_PASSWORD in environment (recommended via sectool) before running the playbook." when: macos_dev_station_opencode_launchd_enabled | bool - name: Ensure code-server password is provided ansible.builtin.assert: that: - macos_dev_station_code_server_password | length > 0 fail_msg: "Set CODE_SERVER_PASSWORD in environment (recommended via sectool) before running the playbook." when: - macos_dev_station_code_server_launchd_enabled | bool - macos_dev_station_code_server_auth == "password" - name: Install Homebrew packages become: false community.general.homebrew: name: "{{ macos_dev_station_brew_packages }}" state: present - name: Ensure dedicated service user exists become: true ansible.builtin.user: name: "{{ macos_dev_station_service_user }}" shell: "{{ (macos_dev_station_service_user_ssh_enabled | bool) | ternary(macos_dev_station_service_user_ssh_shell, macos_dev_station_service_shell) }}" home: "{{ macos_dev_station_service_home }}" create_home: true state: present when: macos_dev_station_service_user_enabled | bool - name: Discover npm executable path ansible.builtin.command: /bin/sh -lc "command -v npm" register: __macos_npm_path changed_when: false failed_when: __macos_npm_path.rc != 0 environment: PATH: "{{ macos_dev_station_path }}" when: macos_dev_station_npm_packages | length > 0 - name: Install global npm packages community.general.npm: name: "{{ item.name }}" version: "{{ item.version }}" executable: "{{ __macos_npm_path.stdout | trim }}" global: true environment: PATH: "{{ macos_dev_station_path }}" loop: "{{ macos_dev_station_npm_packages }}" when: macos_dev_station_npm_packages | length > 0 - name: Ensure required user directories exist become: true ansible.builtin.file: path: "{{ item }}" state: directory mode: "0750" owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" loop: - "{{ macos_dev_station_service_home }}" - "{{ macos_dev_station_workspace_dir }}" - "{{ macos_dev_station_scripts_dir }}" - "{{ macos_dev_station_logs_dir }}" - name: Ensure service home permissions are restrictive become: true ansible.builtin.file: path: "{{ macos_dev_station_service_home }}" state: directory owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" mode: "0700" - name: Ensure service user config directories exist become: true ansible.builtin.file: path: "{{ item }}" state: directory owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" mode: "0750" loop: - "{{ macos_dev_station_service_home }}/.cache" - "{{ macos_dev_station_service_home }}/.local" - "{{ macos_dev_station_service_home }}/.local/bin" - "{{ macos_dev_station_service_home }}/.local/lib" - "{{ macos_dev_station_service_home }}/.local/lib/node_modules" - "{{ macos_dev_station_service_home }}/.config" - "{{ macos_dev_station_service_home }}/.config/opencode" when: macos_dev_station_service_user_enabled | bool - name: Ensure OpenCode config object is valid when provided ansible.builtin.assert: that: - macos_dev_station_opencode_config is mapping fail_msg: "macos_dev_station_opencode_config must be a mapping (dictionary)." - name: Render OpenCode config from host vars become: true ansible.builtin.template: src: opencode.json.j2 dest: "{{ macos_dev_station_opencode_config_path }}" owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" mode: "0600" notify: Restart opencode launch agent when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_opencode_config | length > 0 - name: Download Oh My Zsh installer become: true ansible.builtin.get_url: url: "{{ macos_dev_station_oh_my_zsh_install_url }}" dest: "{{ macos_dev_station_service_home }}/.cache/ohmyzsh-install.sh" owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" mode: "0755" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_oh_my_zsh_enabled | bool - name: Install Oh My Zsh for service user become: true become_user: "{{ macos_dev_station_service_user }}" ansible.builtin.command: >- /bin/sh {{ macos_dev_station_service_home }}/.cache/ohmyzsh-install.sh --unattended args: creates: "{{ macos_dev_station_service_home }}/.oh-my-zsh" environment: HOME: "{{ macos_dev_station_service_home }}" USER: "{{ macos_dev_station_service_user }}" RUNZSH: "no" CHSH: "no" KEEP_ZSHRC: "yes" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_oh_my_zsh_enabled | bool - name: Ensure .zshrc has ssh-agent startup for devstation become: true ansible.builtin.blockinfile: path: "{{ macos_dev_station_service_home }}/.zshrc" block: | # ansible: macos_dev_station ssh-agent export SSH_AUTH_SOCK="{{ macos_dev_station_ssh_agent_socket }}" if [ -S "$SSH_AUTH_SOCK" ] && ssh-add -l &>/dev/null; then # Agent socket exists and has keys — nothing to do true else if [ -S "$SSH_AUTH_SOCK" ]; then # Socket exists but agent is dead or has no keys — restart rm -f "$SSH_AUTH_SOCK" eval "$(ssh-agent -a "$SSH_AUTH_SOCK" 2>/dev/null)" || true else # No socket — start fresh agent eval "$(ssh-agent -a "$SSH_AUTH_SOCK" 2>/dev/null)" || true fi if [ -f "{{ macos_dev_station_scripts_dir }}/ssh-agent-start.sh" ]; then source "{{ macos_dev_station_scripts_dir }}/ssh-agent-start.sh" fi fi marker: "# {mark} ansible: macos_dev_station ssh-agent" create: true mode: "0644" owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_ssh_agent_enabled | bool - name: Install managed MCP server runtimes ansible.builtin.include_tasks: mcp_installers.yml when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_mcp_installers_enabled | bool - macos_dev_station_mcp_installers | length > 0 - name: Run MCP bootstrap commands become: true become_user: "{{ macos_dev_station_service_user }}" ansible.builtin.shell: "{{ item }}" register: __mcp_bootstrap_result changed_when: __mcp_bootstrap_result.rc == 0 args: executable: /bin/sh environment: HOME: "{{ macos_dev_station_service_home }}" USER: "{{ macos_dev_station_service_user }}" PATH: "{{ macos_dev_station_path }}" loop: "{{ macos_dev_station_mcp_bootstrap_commands }}" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_mcp_bootstrap_commands | length > 0 - name: List installed code-server extensions for service user become: true become_user: "{{ macos_dev_station_service_user }}" ansible.builtin.command: code-server --list-extensions register: __code_server_extensions_installed changed_when: false environment: HOME: "{{ macos_dev_station_service_home }}" USER: "{{ macos_dev_station_service_user }}" PATH: "{{ macos_dev_station_path }}" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_code_server_extensions_enabled | bool - macos_dev_station_code_server_extensions | length > 0 - name: Install base code-server extensions for service user become: true become_user: "{{ macos_dev_station_service_user }}" ansible.builtin.command: >- code-server --install-extension {{ item }} register: __code_server_extension_install changed_when: __code_server_extension_install.rc == 0 failed_when: >- (macos_dev_station_code_server_extensions_fail_on_error | bool) and (__code_server_extension_install.rc != 0) environment: HOME: "{{ macos_dev_station_service_home }}" USER: "{{ macos_dev_station_service_user }}" PATH: "{{ macos_dev_station_path }}" notify: Restart code-server launch agent loop: "{{ macos_dev_station_code_server_extensions }}" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_code_server_extensions_enabled | bool - item not in (__code_server_extensions_installed.stdout_lines | default([])) - name: Report skipped code-server extensions that are unavailable ansible.builtin.debug: msg: >- Skipping unavailable code-server extension '{{ item.item }}': {{ (item.stderr_lines | default([])) | join(' ') }} loop: "{{ __code_server_extension_install.results | default([]) }}" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_code_server_extensions_enabled | bool - not (macos_dev_station_code_server_extensions_fail_on_error | bool) - item.rc is defined - item.rc != 0 - name: Ensure devstation SSH directory exists when SSH access is enabled become: true ansible.builtin.file: path: "{{ macos_dev_station_service_home }}/.ssh" state: directory owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" mode: "0700" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_service_user_ssh_enabled | bool - name: Ensure devstation authorized keys are present when SSH access is enabled become: true ansible.posix.authorized_key: user: "{{ macos_dev_station_service_user }}" key: "{{ item }}" state: present loop: "{{ macos_dev_station_service_user_ssh_authorized_keys }}" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_service_user_ssh_enabled | bool - name: Check whether macOS Remote Login allowlist group exists become: true ansible.builtin.command: dscl . -read /Groups/com.apple.access_ssh register: __macos_access_ssh_group changed_when: false failed_when: false when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_service_user_ssh_enabled | bool - name: Check whether service user is in macOS Remote Login allowlist become: true ansible.builtin.command: >- dseditgroup -o checkmember -m {{ macos_dev_station_service_user }} com.apple.access_ssh register: __macos_access_ssh_member changed_when: false failed_when: false when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_service_user_ssh_enabled | bool - __macos_access_ssh_group.rc == 0 - name: Ensure service user is allowed by macOS Remote Login allowlist become: true ansible.builtin.command: >- dseditgroup -o edit -a {{ macos_dev_station_service_user }} -t user com.apple.access_ssh changed_when: true when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_service_user_ssh_enabled | bool - __macos_access_ssh_group.rc == 0 - __macos_access_ssh_member.rc != 0 - name: Render ssh-agent launch daemon become: true ansible.builtin.template: src: ssh-agent.plist.j2 dest: "{{ macos_dev_station_launchd_plist_dir }}/com.a13labs.ssh-agent.plist" mode: "0644" owner: root group: wheel when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_ssh_agent_enabled | bool notify: Restart ssh-agent launch daemon - name: Render ssh-agent starter script become: true ansible.builtin.template: src: ssh-agent-start.sh.j2 dest: "{{ macos_dev_station_scripts_dir }}/ssh-agent-start.sh" mode: "0755" owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_ssh_agent_enabled | bool notify: Restart ssh-agent launch daemon - name: Ensure ssh_config has ssh-agent settings for devstation become: true ansible.builtin.blockinfile: path: "{{ macos_dev_station_service_home }}/.ssh/config" block: | Host * AddKeysToAgent yes ServerAliveInterval 60 ServerAliveCountMax 3 marker: "# {mark} ansible: macos_dev_station ssh-agent" create: true mode: "0600" owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" when: - macos_dev_station_service_user_enabled | bool - macos_dev_station_ssh_agent_enabled | bool - name: Check ssh-agent launch daemon status become: true ansible.builtin.command: launchctl print system/com.a13labs.ssh-agent register: __ssh_agent_status failed_when: false changed_when: false when: - macos_dev_station_manage_launchd | bool - macos_dev_station_ssh_agent_enabled | bool - macos_dev_station_service_user_enabled | bool - name: Bootstrap ssh-agent launch daemon if missing become: true ansible.builtin.command: >- launchctl bootstrap system {{ macos_dev_station_launchd_plist_dir }}/com.a13labs.ssh-agent.plist changed_when: true when: - macos_dev_station_manage_launchd | bool - macos_dev_station_ssh_agent_enabled | bool - macos_dev_station_service_user_enabled | bool - __ssh_agent_status.rc != 0 - name: Ensure ssh-agent launch daemon is enabled become: true ansible.builtin.command: launchctl enable system/com.a13labs.ssh-agent changed_when: false when: - macos_dev_station_manage_launchd | bool - macos_dev_station_ssh_agent_enabled | bool - name: Remove legacy user launch agent plists for hard mode ansible.builtin.file: path: "{{ ansible_facts['env']['HOME'] }}/Library/LaunchAgents/{{ item }}" state: absent loop: - com.a13labs.code-server.plist - com.a13labs.opencode.plist - name: Ensure SSH directory exists ansible.builtin.file: path: "{{ ansible_facts['env']['HOME'] }}/.ssh" state: directory mode: "0700" - name: Generate SSH key if missing ansible.builtin.command: >- ssh-keygen -t ed25519 -C {{ macos_dev_station_ssh_key_comment }} -f {{ macos_dev_station_ssh_key_path }} -N '' args: creates: "{{ macos_dev_station_ssh_key_path }}" - name: Configure Git init default branch community.general.git_config: name: init.defaultBranch scope: global value: "{{ macos_dev_station_git_init_default_branch }}" - name: Configure Git pull.rebase community.general.git_config: name: pull.rebase scope: global value: "{{ macos_dev_station_git_pull_rebase | ternary('true', 'false') }}" - name: Render code-server launcher script become: true ansible.builtin.template: src: start-ide.sh.j2 dest: "{{ macos_dev_station_scripts_dir }}/start-ide.sh" mode: "0755" owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" - name: Render opencode launcher script become: true ansible.builtin.template: src: start-opencode.sh.j2 dest: "{{ macos_dev_station_scripts_dir }}/start-opencode.sh" mode: "0755" owner: "{{ macos_dev_station_service_user }}" group: "{{ macos_dev_station_service_group }}" - name: Render tmux configuration ansible.builtin.copy: dest: "{{ ansible_facts['env']['HOME'] }}/.tmux.conf" mode: "0644" content: | set -g mouse on set -g history-limit 100000 - name: Ensure Colima runtime is started ansible.builtin.command: >- colima start --cpu {{ macos_dev_station_colima_cpu }} --memory {{ macos_dev_station_colima_memory }} register: __colima_start changed_when: "'already running' not in ((__colima_start.stdout | default('')) + (__colima_start.stderr | default('')))" when: macos_dev_station_colima_enabled | bool environment: PATH: "{{ macos_dev_station_path }}" - name: Render code-server launchd unit become: true ansible.builtin.template: src: code-server.plist.j2 dest: "{{ macos_dev_station_launchd_plist_dir }}/com.a13labs.code-server.plist" mode: "0644" owner: root group: wheel when: macos_dev_station_code_server_launchd_enabled | bool notify: Restart code-server launch agent - name: Render opencode launchd unit become: true ansible.builtin.template: src: opencode.plist.j2 dest: "{{ macos_dev_station_launchd_plist_dir }}/com.a13labs.opencode.plist" mode: "0644" owner: root group: wheel when: macos_dev_station_opencode_launchd_enabled | bool notify: Restart opencode launch agent - name: Check code-server daemon status become: true ansible.builtin.command: launchctl print system/com.a13labs.code-server register: __code_server_daemon_status failed_when: false changed_when: false when: - macos_dev_station_manage_launchd | bool - macos_dev_station_code_server_launchd_enabled | bool - name: Bootstrap code-server daemon if missing become: true ansible.builtin.command: >- launchctl bootstrap system {{ macos_dev_station_launchd_plist_dir }}/com.a13labs.code-server.plist changed_when: true when: - macos_dev_station_manage_launchd | bool - macos_dev_station_code_server_launchd_enabled | bool - __code_server_daemon_status.rc != 0 - name: Ensure code-server daemon is enabled become: true ansible.builtin.command: launchctl enable system/com.a13labs.code-server changed_when: false when: - macos_dev_station_manage_launchd | bool - macos_dev_station_code_server_launchd_enabled | bool - name: Check opencode daemon status become: true ansible.builtin.command: launchctl print system/com.a13labs.opencode register: __opencode_daemon_status failed_when: false changed_when: false when: - macos_dev_station_manage_launchd | bool - macos_dev_station_opencode_launchd_enabled | bool - name: Bootstrap opencode daemon if missing become: true ansible.builtin.command: >- launchctl bootstrap system {{ macos_dev_station_launchd_plist_dir }}/com.a13labs.opencode.plist changed_when: true when: - macos_dev_station_manage_launchd | bool - macos_dev_station_opencode_launchd_enabled | bool - __opencode_daemon_status.rc != 0 - name: Ensure opencode daemon is enabled become: true ansible.builtin.command: launchctl enable system/com.a13labs.opencode changed_when: false when: - macos_dev_station_manage_launchd | bool - macos_dev_station_opencode_launchd_enabled | bool - name: Ensure launch agents are loaded now ansible.builtin.meta: flush_handlers when: macos_dev_station_manage_launchd | bool