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
2 changes: 1 addition & 1 deletion Brewfile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ cask "font-zed-mono-nerd-font" # Zed editor font with icons

# macOS only (no Linux formula/cask available)
if OS.mac?
cask "codex" # OpenAI's coding assistant (use npm on Linux)
cask "codex" # OpenAI's coding assistant (npm on Linux, from the dev_user role)
cask "gcloud-cli" # Google Cloud CLI (use apt/dnf on Linux)
end

Expand Down
10 changes: 10 additions & 0 deletions ansible/roles/dev_user/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,13 @@ dev_user_brewfile: true
# keeps the tool inside the home, like everything else this role puts there.
dev_user_claude_code: true
dev_user_claude_code_channel: ""

# Codex CLI. The Brewfile covers macOS with a cask, but Homebrew publishes no formula for it, and
# casks do not exist on Linux -- hence npm here, which is the install OpenAI document alongside the
# cask and which ships the same version. So this needs the Brewfile: npm arrives with node, out of
# the prefix. It installs globally, which for that node means into the prefix rather than the home,
# and the account owns the prefix outright -- so it is that npm the role installs with, and a box
# whose prefix has none is one this leaves alone. Empty takes the latest release; set an exact
# version to pin one, for the reason the Claude Code channel gives.
dev_user_codex: true
dev_user_codex_version: ""
57 changes: 56 additions & 1 deletion ansible/roles/dev_user/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@
# It went unnoticed because a GitHub runner ships apt's jq on the default PATH; on a stock
# cloud image, which is what this role is for, there is no jq anywhere but the prefix.
environment:
PATH: "{{ dev_user_brew_prefix }}/bin:{{ ansible_env.PATH }}"
PATH: "{{ dev_user_brew_prefix }}/bin:{{ ansible_facts.env.PATH }}"
register: dev_user_dotfiles_install
# It marks work with a tick and no-ops with an equals sign, which is the whole reason the dry run
# and the already-linked check were worth porting.
Expand Down Expand Up @@ -373,6 +373,61 @@
changed_when: false
when: dev_user_claude_code | bool

# The Brewfile installs this on macOS and cannot here: Homebrew has only a cask for Codex, and
# casks are macOS-only. npm is what OpenAI document beside the cask and it publishes the same
# version, so this is the same tool by another route rather than a substitute for it.
#
# Global, which with the account's own Homebrew node means the Homebrew prefix -- the account owns
# it, so no escalation is involved. npm is not on the PATH `command` gets, for the reason the
# installer above needs the same line: no login shell has run, so `brew shellenv` has not either.
#
# That whole sentence is about the account's own npm, so it is the one named, and the pair is
# skipped where it is absent rather than falling through to whatever npm the PATH turns up. A
# system npm installs into a prefix this account may not own and leaves the binary somewhere the
# check below does not look, which is what the executed-role job saw: it runs with the Brewfile
# off, the install went to the runner's own npm, and `{{ dev_user_brew_prefix }}/bin/codex` was
# never written. A box without the Brewfile has no Codex until it has one.
- name: Whether the account has an npm of its own to install with
ansible.builtin.stat:
path: "{{ dev_user_brew_prefix }}/bin/npm"
register: dev_user_brew_npm

- name: Codex CLI, from npm
community.general.npm:
name: "@openai/codex"
version: "{{ dev_user_codex_version or omit }}"
executable: "{{ dev_user_brew_prefix }}/bin/npm"
global: true
state: present
become: true
become_user: "{{ dev_user_name }}"
environment:
PATH: "{{ dev_user_brew_prefix }}/bin:{{ ansible_facts.env.PATH }}"
when:
- dev_user_codex | bool
- dev_user_brew_npm.stat.exists

# Stated rather than assumed, as with Claude Code above. npm reporting a package installed says
# the tarball unpacked; it does not say the launcher resolves or that the platform-specific binary
# it shells out to exists for this architecture.
#
# The PATH again, and not for the reason the install task has it: what npm links into the prefix is
# a script whose shebang is `/usr/bin/env node`, so running it needs node found the same way. Claude
# Code above gets away without this because what its installer leaves behind is a native binary that
# asks nothing of the PATH -- which is why the task said `node: No such file or directory` when it
# was written to that shape, on a box where the install itself had just succeeded.
- name: Codex answers
ansible.builtin.command: "{{ dev_user_brew_prefix }}/bin/codex --version"
become: true
become_user: "{{ dev_user_name }}"
register: dev_user_codex_reported_version
environment:
PATH: "{{ dev_user_brew_prefix }}/bin:{{ ansible_facts.env.PATH }}"
changed_when: false
when:
- dev_user_codex | bool
- dev_user_brew_npm.stat.exists

# The private overlay. This repo is public and deliberately knows nothing about any
# employer, so the default is empty and the whole block is skipped. Supply the repo in an
# untracked vars file to turn it on: ansible-playbook ... -e @private.yml
Expand Down
Loading