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 .claude/rules/devcontainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Common categories: `**Claude Code**`, `**Bun**`, `**Code Quality**` (OXC), `**Gi

```jsonc
"settings": {
"terminal.integrated.defaultProfile.linux": "fish",
"terminal.integrated.defaultProfile.linux": "zsh",
"extensions.ignoreRecommendations": true
}
```
93 changes: 68 additions & 25 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@
# ═════════════════════════════════════════════════════════════════════════════

# ── rtk (token-optimized CLI proxy) ──────────────────────────────────────
# ── oh-my-zsh + powerlevel10k ─────────────────────────────────
# Pinned to commit SHAs and fetched as tarballs. Neither project tags usefully --
# oh-my-zsh has no tags at all, and powerlevel10k's last release predates its
# current master by years -- so a SHA is the only real pin. Bump deliberately:
# auto-adopting upstream shell-framework changes is what repeatedly broke this
# image, and a prompt theme does not need a 3-day release cadence.
FROM alpine:3.21 AS ohmyzsh-download
RUN apk add --no-cache curl tar
ARG OH_MY_ZSH_REF=0ee67f042872d1dfab74270c31867771ca35aef4
ARG POWERLEVEL10K_REF=d05a1b00f9a61f9578bf9dc19b8451942dde8734
RUN set -eux; \
mkdir -p /omz/custom/themes/powerlevel10k; \
curl -fsSL "https://github.com/ohmyzsh/ohmyzsh/archive/${OH_MY_ZSH_REF}.tar.gz" \
| tar -xz -C /omz --strip-components=1; \
curl -fsSL "https://github.com/romkatv/powerlevel10k/archive/${POWERLEVEL10K_REF}.tar.gz" \
| tar -xz -C /omz/custom/themes/powerlevel10k --strip-components=1

FROM alpine:3.21 AS rtk-download
RUN apk add --no-cache curl jq
RUN set -eux; \
Expand Down Expand Up @@ -68,30 +85,29 @@ RUN set -eux; \
# ─── BASE ─────────────────────────────────────────────────────────────────────
FROM node:24-trixie-slim AS base

ARG GIT_DELTA_VERSION=0.18.2
# renovate: datasource=github-releases depName=cli/cli
ARG GH_VERSION=2.100.0

# System packages (each justified — see claude-code Dockerfile for rationale)
# - ca-certificates: SSL/TLS for HTTPS connections
# - curl: downloading tools and installers
# - fish: interactive shell (built-in syntax highlighting, autosuggestions, completions)
# - fzf: fuzzy finder (fish integration)
# - gh: GitHub CLI
# - fzf: fuzzy finder; zsh integration comes from the oh-my-zsh fzf plugin
# - git: version control
# - jq: JSON processing (onboarding patch, firewall script)
# - less: pager for git delta output
# - less: pager for git and other CLI output
# - sudo: privilege escalation (chown for named volumes, firewall setup)
# - zsh: interactive shell; oh-my-zsh + powerlevel10k configured below
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
fish \
fzf \
gh \
git \
jq \
less \
sudo
sudo \
zsh

# npm global directory with proper permissions for node user
# Pre-create /lib to prevent "ENOENT" errors during npx commands
Expand All @@ -101,37 +117,64 @@ RUN mkdir -p /usr/local/share/npm-global/lib \
ENV DEVCONTAINER=true

# Create workspace and config directories with proper ownership
RUN mkdir -p /workspace /home/node/.claude /home/node/.local/share/fish \
&& chown -R node:node /workspace /home/node/.claude /home/node/.local
RUN mkdir -p /workspace /home/node/.claude /home/node/.local /commandhistory \
&& chown -R node:node /workspace /home/node/.claude /home/node/.local /commandhistory

WORKDIR /workspace

# Install git-delta (pinned — v0.19.0 dropped arm64 .deb)
# gh (GitHub CLI) — installed from the upstream .deb, not apt: Debian trixie
# freezes gh at 2.46.0 (Apr 2024). Version kept current by Renovate.
# Must stay AFTER the apt block: the .deb depends on git, and dpkg -i does not
# resolve dependencies — it fails loudly ("dependency problems") if git is absent.
RUN ARCH=$(dpkg --print-architecture) \
&& curl -fsSL -o "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \
"https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \
&& dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \
&& rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"
&& curl -fsSL -o "gh_${GH_VERSION}_linux_${ARCH}.deb" \
"https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.deb" \
&& dpkg -i "gh_${GH_VERSION}_linux_${ARCH}.deb" \
&& rm "gh_${GH_VERSION}_linux_${ARCH}.deb"

# ── Non-root user setup ──────────────────────────────────────────────────────
USER node

ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin
ENV SHELL=/usr/bin/fish
ENV SHELL=/usr/bin/zsh
# Only LANG, and only C.UTF-8 — what the official images do (ruby sets exactly
# this; node/debian/python set nothing). LC_ALL would override every LC_* category
# and silently defeat a consuming project's containerEnv. TERM is deliberately
# unset: no official image sets it, an image ENV beats the tty value `docker exec -t`
# supplies, and tput/clear failing without a terminal is correct behaviour.
ENV LANG=C.UTF-8
ENV EDITOR="code --wait"
ENV VISUAL="code --wait"

# ── Starship prompt ──────────────────────────────────────────────────────────
USER root
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes
SHELL ["/bin/sh", "-c"]

# ── zsh + oh-my-zsh + powerlevel10k ──────────────────────────────────────────
# Written as node: $HOME during RUN follows USER, so this must not run as root.
USER node
RUN mkdir -p /home/node/.config/fish \
&& starship preset no-runtime-versions -o /home/node/.config/starship.toml \
&& printf '%s\n' 'set -g fish_greeting' 'starship init fish | source' > /home/node/.config/fish/config.fish
# oh-my-zsh tree is COPYed from the parallel download stage above, so the final
# image needs no git or curl for it and nothing is fetched at build time here.
COPY --from=ohmyzsh-download --chown=node:node /omz /home/node/.oh-my-zsh

# .zshrc is written here rather than by an installer, so nothing upstream can
# inject env overrides (locale/TERM) that then have to be patched back out.
# The wizard flag matters: without a POWERLEVEL9K_* config powerlevel10k starts
# its interactive configurator on first shell, which blocks a container.
# $HOME and $ZSH are single-quoted on purpose: they must reach .zshrc
# unexpanded so zsh resolves them at runtime, not at build time.
# hadolint ignore=SC2016
RUN printf '%s\n' \
'export ZSH="$HOME/.oh-my-zsh"' \
'ZSH_THEME="powerlevel10k/powerlevel10k"' \
'plugins=(git fzf)' \
"zstyle ':omz:update' mode disabled" \
'POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true' \
'export HISTFILE=/commandhistory/.zsh_history' \
'source $ZSH/oh-my-zsh.sh' \
'POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_last' \
'POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(user dir vcs status)' \
'POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=()' \
'POWERLEVEL9K_STATUS_OK=false' \
'POWERLEVEL9K_STATUS_CROSS=true' \
> "$HOME/.zshrc"

# ── Dev tools (copied from parallel download stages) ─────────────────────────
# rtk (token-optimized CLI proxy) and ralphex (autonomous plan execution).
Expand Down
8 changes: 4 additions & 4 deletions .devcontainer/claude-sandbox/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
"ms-azuretools.vscode-docker"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "fish",
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"fish": { "path": "fish" },
"zsh": { "path": "zsh" },
"bash": { "path": "bash", "icon": "terminal-bash" }
},
"extensions.ignoreRecommendations": true,
Expand Down Expand Up @@ -57,7 +57,7 @@
},
"mounts": [
"source=devcontainers-sandbox-config-${devcontainerId},target=/home/node/.claude,type=volume",
"source=devcontainers-sandbox-fish-${devcontainerId},target=/home/node/.local/share/fish,type=volume",
"source=devcontainers-sandbox-zsh-history-${devcontainerId},target=/commandhistory,type=volume",
// Mount firewall script into the expected path
"source=${localWorkspaceFolder}/.devcontainer/claude-sandbox/init-firewall.sh,target=/usr/local/bin/init-firewall.sh,type=bind"
],
Expand All @@ -69,7 +69,7 @@
"CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}"
},
// Plugins before firewall (network still open during postCreateCommand)
"postCreateCommand": "sudo chown -R node /home/node/.claude && bash /workspace/.devcontainer/init-plugins.sh",
"postCreateCommand": "sudo chown -R node /home/node/.claude /commandhistory && bash /workspace/.devcontainer/init-plugins.sh",
// Firewall locks down the network
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh",
"waitFor": "postStartCommand"
Expand Down
10 changes: 5 additions & 5 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
"ms-azuretools.vscode-docker"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "fish",
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"fish": { "path": "fish" },
"zsh": { "path": "zsh" },
"bash": { "path": "bash", "icon": "terminal-bash" }
},
// Suppress extension recommendation prompts
Expand Down Expand Up @@ -58,8 +58,8 @@
"mounts": [
// Persist Claude Code configuration between container rebuilds
"source=devcontainers-claude-config-${devcontainerId},target=/home/node/.claude,type=volume",
// Persist fish shell history between container rebuilds
"source=devcontainers-fish-data-${devcontainerId},target=/home/node/.local/share/fish,type=volume"
// Persist zsh history between container rebuilds
"source=devcontainers-zsh-history-${devcontainerId},target=/commandhistory,type=volume"
],
"containerEnv": {
"TZ": "${localEnv:TZ:America/Edmonton}",
Expand All @@ -71,6 +71,6 @@
// VS Code installs extensions, so claude commands race with the
// Claude Code extension's OAuth flow and can corrupt auth state.
// Run .devcontainer/init-plugins.sh manually after first login.
"postCreateCommand": "sudo chown -R node /home/node/.claude /home/node/.local/share/fish",
"postCreateCommand": "sudo chown -R node /home/node/.claude /commandhistory",
"waitFor": "postCreateCommand"
}
25 changes: 20 additions & 5 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@

packageRules: [
{
// rtk / ralphex release tags look like "v0.43.0"; strip the leading "v"
// so the datasource version matches the bare ARG value ("0.43.0").
matchDatasources: ['github-releases'],
// Most GitHub tags carry a leading "v" ("v0.48.0", "v29.8.1"); strip it so the
// datasource version matches the bare ARG value ("0.48.0"). A tag with no "v"
// passes through unchanged, so this is safe to apply datasource-wide.
matchDatasources: ['github-releases', 'github-tags'],
extractVersion: '^v?(?<version>.+)$',
},
{
// Group the four tools into one PR and auto-merge once CI passes.
// Group the tracked tools into one PR and auto-merge once CI passes.
//
// Relies on Renovate's default platformAutomerge:true — GitHub's native
// auto-merge merges on green with no second Renovate run. The previous
Expand All @@ -45,8 +46,15 @@
'umputun/ralphex',
'@anthropic-ai/claude-code',
'agent-browser',
'cli/cli', // gh — upstream .deb; apt's trixie build is frozen at 2.46.0
'docker/cli', // docker CLI static binary (download.docker.com).
// github-tags, NOT github-releases: moby/moby tags its
// releases 'docker-v29.8.0', which extractVersion cannot
// parse, so every candidate is silently discarded.
'go', // ralphex-fe Go toolchain (go.dev)
'node', // hugo-bun-node musl build; base-image node is NOT managed here
],
groupName: 'devcontainer agent tools',
groupName: 'devcontainer tools',
automerge: true,

// These bumps merge unreviewed and publish straight to ghcr.io, so let a
Expand All @@ -73,5 +81,12 @@
automerge: true,
minimumReleaseAge: '3 days',
},
{
// hugo-bun-node installs Node from the unofficial musl builds — its base
// image (oven/bun:*-alpine) ships no node at all. The image is Node 24 LTS
// by design, so major bumps stay a deliberate call, not a Renovate PR.
matchDatasources: ['node-version'],
allowedVersions: '^24',
},
],
}
2 changes: 1 addition & 1 deletion .github/workflows/build-claude-bun.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ jobs:
context: claude-bun/.devcontainer
dockerfile: claude-bun/.devcontainer/Dockerfile
version-tag: ${{ needs.prepare.outputs.version-tag }}
verify-command: 'bun --version'
verify-command: "bun --version && gh --version && zsh --version && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
secrets: inherit
8 changes: 4 additions & 4 deletions .github/workflows/build-claude-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,19 @@ jobs:
matrix:
include:
- image-suffix: claude-code
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium"
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
runner: ubuntu-24.04
arch: amd64
- image-suffix: claude-code
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium"
verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
runner: ubuntu-24.04-arm
arch: arm64
- image-suffix: claude-code-sandbox
verify-command: "claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp"
verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
runner: ubuntu-24.04
arch: amd64
- image-suffix: claude-code-sandbox
verify-command: "claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp"
verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k"
runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build-ralphex-fe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ jobs:
hugo version &&
python3 --version &&
go version &&
docker --version &&
node --version &&
/srv/ralphex --version &&
claude --version &&
Expand Down
Loading