internal: workmux config (#8065)
* config * nit * add wmdev config * remove playwright mcp * add asciicinema
This commit is contained in:
@@ -1,30 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Resolve _ee.rs symlinks to actual files so Claude can read them
|
|
||||||
# This script runs before each user prompt is processed
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/home/farhad/windmill}"
|
|
||||||
MANIFEST_FILE="$PROJECT_DIR/.claude/hooks/.symlink-manifest"
|
|
||||||
|
|
||||||
# Find all _ee.rs symlinks and store their targets
|
|
||||||
find "$PROJECT_DIR" -name "*_ee.rs" -type l 2>/dev/null | while read -r symlink; do
|
|
||||||
target=$(readlink -f "$symlink" 2>/dev/null) || continue
|
|
||||||
|
|
||||||
# Only process if target file exists
|
|
||||||
if [[ -f "$target" ]]; then
|
|
||||||
# Store symlink path and target in manifest
|
|
||||||
echo "$symlink|$target" >> "$MANIFEST_FILE.tmp"
|
|
||||||
|
|
||||||
# Replace symlink with actual file content
|
|
||||||
rm "$symlink"
|
|
||||||
cp "$target" "$symlink"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Atomically replace manifest
|
|
||||||
if [[ -f "$MANIFEST_FILE.tmp" ]]; then
|
|
||||||
mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Restore _ee.rs symlinks after Claude finishes processing
|
|
||||||
# This script runs when Claude stops
|
|
||||||
# IMPORTANT: Copies any modifications back to the target before restoring symlinks
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/home/farhad/windmill}"
|
|
||||||
MANIFEST_FILE="$PROJECT_DIR/.claude/hooks/.symlink-manifest"
|
|
||||||
|
|
||||||
# Check if manifest exists
|
|
||||||
if [[ ! -f "$MANIFEST_FILE" ]]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Read manifest and restore symlinks
|
|
||||||
while IFS='|' read -r symlink target; do
|
|
||||||
if [[ -n "$symlink" && -n "$target" ]]; then
|
|
||||||
# If the file exists (not a symlink) and target exists, copy changes back
|
|
||||||
if [[ -f "$symlink" && ! -L "$symlink" && -e "$target" ]]; then
|
|
||||||
# Copy the potentially modified file back to the target
|
|
||||||
cp "$symlink" "$target"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove the regular file (which was a copy)
|
|
||||||
rm -f "$symlink" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Recreate the symlink
|
|
||||||
ln -s "$target" "$symlink" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done < "$MANIFEST_FILE"
|
|
||||||
|
|
||||||
# Clean up manifest
|
|
||||||
rm -f "$MANIFEST_FILE"
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
|
"additionalDirectories": [
|
||||||
|
"../windmill-ee-private"
|
||||||
|
],
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(grep:*)",
|
"Bash(grep:*)",
|
||||||
@@ -63,39 +66,6 @@
|
|||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": true,
|
"enableAllProjectMcpServers": true,
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"UserPromptSubmit": [
|
|
||||||
{
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/resolve-symlinks.sh",
|
|
||||||
"timeout": 30
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Stop": [
|
|
||||||
{
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/restore-symlinks.sh",
|
|
||||||
"timeout": 30
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"SessionEnd": [
|
|
||||||
{
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/restore-symlinks.sh",
|
|
||||||
"timeout": 30
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"PostToolUse": [
|
"PostToolUse": [
|
||||||
{
|
{
|
||||||
"matcher": "Edit|Write",
|
"matcher": "Edit|Write",
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,6 +17,9 @@ rust-client/Cargo.toml
|
|||||||
# Worktree-generated port isolation
|
# Worktree-generated port isolation
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# Worktree-specific Claude Code settings (generated by scripts/worktree-env)
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
# Symlinked cache directories (for git worktrees)
|
# Symlinked cache directories (for git worktrees)
|
||||||
backend/target
|
backend/target
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
"svelte": {
|
"svelte": {
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"url": "https://mcp.svelte.dev/mcp"
|
"url": "https://mcp.svelte.dev/mcp"
|
||||||
},
|
|
||||||
"playwright": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["@playwright/mcp@latest"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
66
.wmdev.yaml
Normal file
66
.wmdev.yaml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
services:
|
||||||
|
- name: BE
|
||||||
|
portEnv: BACKEND_PORT
|
||||||
|
- name: FE
|
||||||
|
portEnv: FRONTEND_PORT
|
||||||
|
|
||||||
|
profiles:
|
||||||
|
default:
|
||||||
|
name: default
|
||||||
|
|
||||||
|
sandbox:
|
||||||
|
name: sandbox
|
||||||
|
systemPrompt: >
|
||||||
|
You are running inside a sandboxed container with full permissions.
|
||||||
|
This worktree is configured with the following ports:
|
||||||
|
|
||||||
|
- Backend: port ${BACKEND_PORT}.
|
||||||
|
Start with: cd backend && PORT=${BACKEND_PORT}
|
||||||
|
DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill
|
||||||
|
cargo watch -x run
|
||||||
|
|
||||||
|
- Frontend: port ${FRONTEND_PORT}.
|
||||||
|
Start with: cd frontend && REMOTE=http://localhost:${BACKEND_PORT}
|
||||||
|
npm run dev -- --port ${FRONTEND_PORT} --host 0.0.0.0
|
||||||
|
|
||||||
|
--- Screenshots ---
|
||||||
|
You can take screenshots of the frontend UI and upload them to R2
|
||||||
|
for use in PR descriptions.
|
||||||
|
1) Take a screenshot:
|
||||||
|
bunx playwright screenshot --browser chromium
|
||||||
|
http://localhost:${FRONTEND_PORT}/path/to/page /tmp/screenshot.png
|
||||||
|
2) Upload to R2:
|
||||||
|
aws s3 cp /tmp/screenshot.png
|
||||||
|
"s3://$(printenv R2_BUCKET)/$(git rev-parse --abbrev-ref HEAD)/screenshot.png"
|
||||||
|
--endpoint-url "$(printenv R2_ENDPOINT)"
|
||||||
|
3) The public URL will be:
|
||||||
|
$(printenv R2_PUBLIC_URL)/<branch>/screenshot.png
|
||||||
|
4) Include screenshots in PR descriptions as markdown images:
|
||||||
|
/<branch>/screenshot.png)
|
||||||
|
|
||||||
|
--- Terminal Recordings (asciinema) ---
|
||||||
|
You can record terminal sessions and upload them for sharing.
|
||||||
|
asciinema is pre-installed at /usr/local/bin/asciinema.
|
||||||
|
|
||||||
|
1) Write a shell script with the commands to demo. Add sleep
|
||||||
|
delays for readable pacing:
|
||||||
|
- 0.5s after printing a "$ command" line (lets viewer read it)
|
||||||
|
- 1.5-2s after command output (lets viewer absorb the result)
|
||||||
|
- Set GIT_PAGER=cat and PAGER=cat to prevent pager hangs
|
||||||
|
|
||||||
|
2) Record headlessly:
|
||||||
|
asciinema rec --headless --overwrite \
|
||||||
|
-c "bash /tmp/demo.sh" \
|
||||||
|
--window-size 120x50 \
|
||||||
|
--title "Description of demo" \
|
||||||
|
/tmp/demo.cast
|
||||||
|
|
||||||
|
3) Upload to asciinema.org:
|
||||||
|
XDG_DATA_HOME=/tmp/.local/share \
|
||||||
|
asciinema upload --server-url https://asciinema.org /tmp/demo.cast
|
||||||
|
envPassthrough:
|
||||||
|
- AWS_ACCESS_KEY_ID
|
||||||
|
- AWS_SECRET_ACCESS_KEY
|
||||||
|
- R2_ENDPOINT
|
||||||
|
- R2_BUCKET
|
||||||
|
- R2_PUBLIC_URL
|
||||||
@@ -46,11 +46,20 @@ pre_remove:
|
|||||||
- ./scripts/worktree-cleanup
|
- ./scripts/worktree-cleanup
|
||||||
|
|
||||||
panes:
|
panes:
|
||||||
- command: <agent>
|
- command: >-
|
||||||
|
claude --append-system-prompt
|
||||||
|
"You are running inside a tmux session with other panes running services.\n
|
||||||
|
Pane layout (current window):\n
|
||||||
|
- Pane 0: this pane (claude agent)\n
|
||||||
|
- Pane 1: backend (cargo watch -x run)\n
|
||||||
|
- Pane 2: frontend (npm run dev)\n\n
|
||||||
|
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (backend) or \`tmux capture-pane -t .2 -p -S -50\` (frontend).\n
|
||||||
|
When restarting backend or frontend, make sure to use the ports listed in .env.local.\n
|
||||||
|
Because we are running backend with cargo watch, to verify your changes, just check the logs in the backend pane. No need for cargo check."
|
||||||
focus: true
|
focus: true
|
||||||
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x run'
|
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x run'
|
||||||
split: horizontal
|
split: horizontal
|
||||||
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm install && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000}'
|
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm install && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0'
|
||||||
split: vertical
|
split: vertical
|
||||||
|
|
||||||
files:
|
files:
|
||||||
@@ -61,3 +70,6 @@ files:
|
|||||||
sandbox:
|
sandbox:
|
||||||
enabled: false
|
enabled: false
|
||||||
toolchain: off
|
toolchain: off
|
||||||
|
# image, host_commands, and extra_mounts configured in global
|
||||||
|
# ~/.config/workmux/config.yaml — see README_WORKMUX_DEV.md for required
|
||||||
|
# extra_mounts (windmill-ee-private access in sandbox)
|
||||||
238
Dockerfile.sandbox
Normal file
238
Dockerfile.sandbox
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
git \
|
||||||
|
iptables \
|
||||||
|
gosu \
|
||||||
|
sudo \
|
||||||
|
unzip \
|
||||||
|
# Rust native build deps (for cargo check)
|
||||||
|
pkg-config \
|
||||||
|
cmake \
|
||||||
|
clang \
|
||||||
|
mold \
|
||||||
|
libtool \
|
||||||
|
libssl-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libxmlsec1-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
libffi-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libclang-dev \
|
||||||
|
libkrb5-dev \
|
||||||
|
libsasl2-dev \
|
||||||
|
# PostgreSQL (for local DB during development)
|
||||||
|
postgresql \
|
||||||
|
postgresql-client \
|
||||||
|
# Node.js 22 (for npm run check / frontend dev)
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
# Container runs as arbitrary UIDs (--user uid:gid). These three lines make
|
||||||
|
# sudo work for any UID:
|
||||||
|
# 1) NOPASSWD rule so sudo never prompts for a password
|
||||||
|
# 2) Writable passwd/group so the entrypoint can register the dynamic UID
|
||||||
|
# 3) Writable shadow so unix_chkpwd can validate the account (without this,
|
||||||
|
# sudo fails with "account validation failure, is your account locked?")
|
||||||
|
&& echo "ALL ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/sandbox \
|
||||||
|
&& chmod 0440 /etc/sudoers.d/sandbox \
|
||||||
|
&& chmod 666 /etc/passwd /etc/group /etc/shadow
|
||||||
|
|
||||||
|
# ── GitHub CLI (for PR creation) ──────────────────────────────────────────────
|
||||||
|
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||||
|
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
||||||
|
> /etc/apt/sources.list.d/github-cli.list \
|
||||||
|
&& apt-get update && apt-get install -y --no-install-recommends gh \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ── Rust toolchain ────────────────────────────────────────────────────────────
|
||||||
|
# Install under /usr/local/lib/ so bins are world-readable with default umask.
|
||||||
|
# CARGO_HOME is overridden to /tmp/.cargo at the end for mutable runtime state.
|
||||||
|
ENV RUSTUP_HOME=/usr/local/lib/rustup CARGO_HOME=/usr/local/lib/cargo
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
||||||
|
sh -s -- -y --default-toolchain stable --profile minimal && \
|
||||||
|
ln -s /usr/local/lib/cargo/bin/* /usr/local/bin/
|
||||||
|
RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres && \
|
||||||
|
cargo install cargo-watch && \
|
||||||
|
cargo install --locked --git https://github.com/asciinema/asciinema && \
|
||||||
|
ln -sf /usr/local/lib/cargo/bin/sqlx /usr/local/bin/sqlx && \
|
||||||
|
ln -sf /usr/local/lib/cargo/bin/cargo-watch /usr/local/bin/cargo-watch && \
|
||||||
|
ln -sf /usr/local/lib/cargo/bin/asciinema /usr/local/bin/asciinema
|
||||||
|
|
||||||
|
# ── Register dynamic runtime users ───────────────────────────────────────────
|
||||||
|
RUN cat <<'SCRIPT' > /usr/local/bin/register-dynamic-user.sh
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
uid="${1:-}"
|
||||||
|
gid="${2:-}"
|
||||||
|
|
||||||
|
if [ -z "$uid" ] || [ -z "$gid" ]; then
|
||||||
|
echo "register-dynamic-user: usage: register-dynamic-user <uid> <gid>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! getent group "$gid" >/dev/null 2>&1; then
|
||||||
|
echo "sandbox:x:${gid}:" >> /etc/group
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! getent passwd "$uid" >/dev/null 2>&1; then
|
||||||
|
echo "sandbox:x:${uid}:${gid}:sandbox:/tmp:/bin/sh" >> /etc/passwd
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add a shadow entry ("*" = no password) so unix_chkpwd doesn't reject sudo.
|
||||||
|
if ! grep -q "^sandbox:" /etc/shadow 2>/dev/null; then
|
||||||
|
echo "sandbox:*:19000:0:99999:7:::" >> /etc/shadow
|
||||||
|
fi
|
||||||
|
SCRIPT
|
||||||
|
RUN chmod +x /usr/local/bin/register-dynamic-user.sh
|
||||||
|
|
||||||
|
# ── Network init script (iptables firewall + privilege drop) ──────────────────
|
||||||
|
RUN cat <<'SCRIPT' > /usr/local/bin/network-init.sh
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -n "${WM_PROXY_HOST:-}" ] && [ -n "${WM_PROXY_PORT:-}" ]; then
|
||||||
|
# Resolve hostnames to ALL IPs (multi-A records, round-robin DNS)
|
||||||
|
PROXY_IPS=$(getent ahostsv4 "$WM_PROXY_HOST" | awk '{print $1}' | sort -u)
|
||||||
|
RPC_HOST="${WM_RPC_HOST:-$WM_PROXY_HOST}"
|
||||||
|
RPC_IPS=$(getent ahostsv4 "$RPC_HOST" | awk '{print $1}' | sort -u)
|
||||||
|
|
||||||
|
if [ -z "$PROXY_IPS" ] || [ -z "$RPC_IPS" ]; then
|
||||||
|
echo "network-init: failed to resolve proxy/RPC host" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# IPv4: default deny outbound
|
||||||
|
iptables -P OUTPUT DROP
|
||||||
|
iptables -A OUTPUT -o lo -j ACCEPT
|
||||||
|
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
|
||||||
|
# Allow DNS (UDP/TCP 53) to configured nameservers.
|
||||||
|
if [ -f /etc/resolv.conf ]; then
|
||||||
|
grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | while read -r ns; do
|
||||||
|
iptables -A OUTPUT -d "$ns" -p udp --dport 53 -j ACCEPT
|
||||||
|
iptables -A OUTPUT -d "$ns" -p tcp --dport 53 -j ACCEPT
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Allow ALL resolved proxy IPs (handles multi-A DNS)
|
||||||
|
for ip in $PROXY_IPS; do
|
||||||
|
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_PROXY_PORT" -j ACCEPT
|
||||||
|
done
|
||||||
|
|
||||||
|
# Allow ALL resolved RPC IPs
|
||||||
|
if [ -n "${WM_RPC_PORT:-}" ]; then
|
||||||
|
for ip in $RPC_IPS; do
|
||||||
|
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_RPC_PORT" -j ACCEPT
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reject (not drop) everything else to fail fast instead of hanging
|
||||||
|
iptables -A OUTPUT -j REJECT
|
||||||
|
|
||||||
|
# IPv6: block entirely to prevent leaks (fail closed)
|
||||||
|
if ip6tables -L -n >/dev/null 2>&1; then
|
||||||
|
ip6tables -P OUTPUT DROP
|
||||||
|
ip6tables -A OUTPUT -o lo -j ACCEPT
|
||||||
|
ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
ip6tables -A OUTPUT -j REJECT
|
||||||
|
else
|
||||||
|
if ! sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null; then
|
||||||
|
echo "network-init: failed to block IPv6 (neither ip6tables nor sysctl available)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add sandbox user/group so sudo works after dropping privileges.
|
||||||
|
if [ -z "${WM_TARGET_UID:-}" ] || [ -z "${WM_TARGET_GID:-}" ]; then
|
||||||
|
echo "network-init: WM_TARGET_UID and WM_TARGET_GID are required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
/usr/local/bin/register-dynamic-user.sh "${WM_TARGET_UID}" "${WM_TARGET_GID}"
|
||||||
|
|
||||||
|
# Fix PTY ownership so the unprivileged user can read/write the terminal.
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
chown "${WM_TARGET_UID}:${WM_TARGET_GID}" "$(tty)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Drop privileges and exec the user command.
|
||||||
|
exec gosu "${WM_TARGET_UID}:${WM_TARGET_GID}" env HOME=/tmp "$@"
|
||||||
|
SCRIPT
|
||||||
|
RUN chmod +x /usr/local/bin/network-init.sh
|
||||||
|
|
||||||
|
# ── workmux (sandbox RPC) ────────────────────────────────────────────────────
|
||||||
|
RUN curl -fsSL https://raw.githubusercontent.com/raine/workmux/main/scripts/install.sh | bash
|
||||||
|
|
||||||
|
# ── Claude Code ───────────────────────────────────────────────────────────────
|
||||||
|
RUN curl -fsSL https://claude.ai/install.sh | bash && \
|
||||||
|
target="$(readlink -f /root/.local/bin/claude)" && \
|
||||||
|
mv /root/.local/share/claude /usr/local/lib/claude && \
|
||||||
|
ln -s "/usr/local/lib/claude/versions/$(basename "$target")" /usr/local/bin/claude && \
|
||||||
|
mkdir -p /tmp/.local/bin && \
|
||||||
|
ln -s /usr/local/bin/claude /tmp/.local/bin/claude && \
|
||||||
|
chmod -R a+rwX /tmp/.local
|
||||||
|
|
||||||
|
# ── Codex ─────────────────────────────────────────────────────────────────────
|
||||||
|
RUN npm i -g @openai/codex
|
||||||
|
|
||||||
|
# ── Bun ───────────────────────────────────────────────────────────────────────
|
||||||
|
ENV BUN_INSTALL=/usr/local/lib/bun
|
||||||
|
RUN curl -fsSL https://bun.sh/install | bash && \
|
||||||
|
ln -s /usr/local/lib/bun/bin/bun /usr/local/bin/bun && \
|
||||||
|
ln -s /usr/local/lib/bun/bin/bunx /usr/local/bin/bunx
|
||||||
|
|
||||||
|
# ── Playwright + Chromium (for screenshots) ──────────────────────────────────
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/lib/playwright-browsers
|
||||||
|
RUN bun add -g @playwright/test \
|
||||||
|
&& bunx playwright install chromium --with-deps \
|
||||||
|
&& chmod -R a+rwX /usr/local/lib/playwright-browsers \
|
||||||
|
&& chmod -R a+rwX /usr/local/lib/bun/install \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/bunx-*
|
||||||
|
|
||||||
|
# ── AWS CLI (for S3-compatible uploads to R2) ─────────────────────────────────
|
||||||
|
RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \
|
||||||
|
&& unzip -q /tmp/awscliv2.zip -d /tmp \
|
||||||
|
&& /tmp/aws/install \
|
||||||
|
&& rm -rf /tmp/aws /tmp/awscliv2.zip
|
||||||
|
|
||||||
|
ENV AWS_DEFAULT_REGION=auto
|
||||||
|
|
||||||
|
# ── Runtime env for arbitrary UID ─────────────────────────────────────────────
|
||||||
|
# Mutable state goes to /tmp (writable by any UID). Toolchains stay read-only.
|
||||||
|
ENV CARGO_HOME=/tmp/.cargo BUN_TMPDIR=/tmp
|
||||||
|
|
||||||
|
# ── Entrypoint ────────────────────────────────────────────────────────────────
|
||||||
|
RUN cat <<'ENTRY' > /usr/local/bin/entrypoint.sh
|
||||||
|
#!/bin/sh
|
||||||
|
/usr/local/bin/register-dynamic-user.sh "$(id -u)" "$(id -g)"
|
||||||
|
|
||||||
|
# Start PostgreSQL (unix socket in /tmp, owned by postgres user)
|
||||||
|
mkdir -p /tmp/pgdata && sudo chown postgres:postgres /tmp/pgdata
|
||||||
|
if [ ! -f /tmp/pgdata/PG_VERSION ]; then
|
||||||
|
sudo -u postgres /usr/lib/postgresql/15/bin/initdb -D /tmp/pgdata --auth=trust
|
||||||
|
fi
|
||||||
|
sudo -u postgres /usr/lib/postgresql/15/bin/pg_ctl -D /tmp/pgdata -l /tmp/pg.log start -o "-k /tmp"
|
||||||
|
sudo -u postgres psql -h /tmp -c "CREATE ROLE sandbox SUPERUSER LOGIN" 2>/dev/null || true
|
||||||
|
sudo -u postgres createdb -h /tmp windmill 2>/dev/null || true
|
||||||
|
|
||||||
|
# Run database migrations so sqlx compile-time checks work
|
||||||
|
if [ -d "$PWD/backend/migrations" ]; then
|
||||||
|
DATABASE_URL="postgres://sandbox@localhost/windmill?host=/tmp" \
|
||||||
|
sqlx migrate run --source "$PWD/backend/migrations" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install frontend dependencies and generate backend client
|
||||||
|
if [ -d "$PWD/frontend" ]; then
|
||||||
|
(cd "$PWD/frontend" && npm install && npm run generate-backend-client) 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
|
ENTRY
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
@@ -172,6 +172,26 @@ The setup is defined in `.workmux.yaml` at the repo root. Key sections:
|
|||||||
- **`files.copy`**: Copies `backend/.env` and `scripts/` into each worktree
|
- **`files.copy`**: Copies `backend/.env` and `scripts/` into each worktree
|
||||||
- **`files.symlink`**: Symlinks `node_modules` and `.svelte-kit` to avoid reinstalling per worktree
|
- **`files.symlink`**: Symlinks `node_modules` and `.svelte-kit` to avoid reinstalling per worktree
|
||||||
|
|
||||||
|
## Enterprise (EE) Code Access
|
||||||
|
|
||||||
|
The enterprise source code lives in the `windmill-ee-private` repository (sibling to this repo). When you create a worktree, `scripts/worktree-env` automatically creates a matching EE worktree on the same branch and configures Claude Code's `additionalDirectories` to grant access.
|
||||||
|
|
||||||
|
### Sandbox setup
|
||||||
|
|
||||||
|
When using sandbox mode, the container needs explicit mounts to access the EE repo. Add the following to your global workmux config (`~/.config/workmux/config.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sandbox:
|
||||||
|
extra_mounts:
|
||||||
|
- host_path: ~/windmill-ee-private
|
||||||
|
writable: true
|
||||||
|
- host_path: ~/windmill-ee-private__worktrees
|
||||||
|
writable: true
|
||||||
|
```
|
||||||
|
|
||||||
|
This mounts both the main EE repo (used by the main worktree) and the EE worktrees directory (used by feature worktrees) into every sandbox container.
|
||||||
|
|
||||||
|
|
||||||
## Cursor SSH Integration (`wmc`)
|
## Cursor SSH Integration (`wmc`)
|
||||||
|
|
||||||
`wm-cursor` (aliased as `wmc`) gives each worktree its own Cursor SSH remote window with an independently-focused tmux session. All windows are visible in the status bar across all Cursor terminals, but each one is focused on its own worktree.
|
`wm-cursor` (aliased as `wmc`) gives each worktree its own Cursor SSH remote window with an independently-focused tmux session. All windows are visible in the status bar across all Cursor terminals, but each one is focused on its own worktree.
|
||||||
|
|||||||
@@ -1,11 +1,34 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Use WM_WORKTREE_PATH (set by workmux) so this works regardless of cwd
|
||||||
|
wt_dir="${WM_WORKTREE_PATH:-.}"
|
||||||
|
echo "[cleanup] cwd=$(pwd) WM_WORKTREE_PATH=${WM_WORKTREE_PATH:-<unset>} wt_dir=$wt_dir"
|
||||||
|
|
||||||
|
# Kill backend/frontend processes using this worktree's ports
|
||||||
|
if [ -f "$wt_dir/.env.local" ]; then
|
||||||
|
source "$wt_dir/.env.local"
|
||||||
|
echo "[cleanup] .env.local found: BACKEND_PORT=${BACKEND_PORT:-<unset>} FRONTEND_PORT=${FRONTEND_PORT:-<unset>}"
|
||||||
|
for port in "${BACKEND_PORT:-}" "${FRONTEND_PORT:-}"; do
|
||||||
|
[ -z "$port" ] && continue
|
||||||
|
pid=$(lsof -ti "TCP:${port}" -sTCP:LISTEN 2>/dev/null || true)
|
||||||
|
if [ -n "$pid" ]; then
|
||||||
|
kill "$pid" 2>/dev/null && echo "[cleanup] Killed process $pid on port $port" \
|
||||||
|
|| echo "[cleanup] Warning: Could not kill process $pid on port $port"
|
||||||
|
else
|
||||||
|
echo "[cleanup] No process listening on port $port"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "[cleanup] No .env.local at $wt_dir/.env.local"
|
||||||
|
fi
|
||||||
|
|
||||||
# Remove the matching windmill-ee-private worktree if one exists
|
# Remove the matching windmill-ee-private worktree if one exists
|
||||||
wt_basename=$(basename "$(pwd)")
|
wt_basename=$(basename "$wt_dir")
|
||||||
|
|
||||||
# Check parent directory first (sibling to worktree root), then fall back to home
|
# Check parent directory first (sibling to worktree root), then fall back to home
|
||||||
parent_dir="$(cd "$(pwd)/.." && pwd)"
|
parent_dir="$(cd "$wt_dir/.." && pwd)"
|
||||||
|
echo "[cleanup] wt_basename=$wt_basename parent_dir=$parent_dir"
|
||||||
if [ -d "${parent_dir}/windmill-ee-private" ]; then
|
if [ -d "${parent_dir}/windmill-ee-private" ]; then
|
||||||
ee_repo="${parent_dir}/windmill-ee-private"
|
ee_repo="${parent_dir}/windmill-ee-private"
|
||||||
else
|
else
|
||||||
@@ -13,11 +36,9 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
ee_worktree_dir="${ee_repo}__worktrees/${wt_basename}"
|
ee_worktree_dir="${ee_repo}__worktrees/${wt_basename}"
|
||||||
|
echo "[cleanup] ee_repo=$ee_repo ee_worktree_dir=$ee_worktree_dir exists=$([ -d "$ee_worktree_dir" ] && echo yes || echo no)"
|
||||||
if [ -d "$ee_worktree_dir" ]; then
|
if [ -d "$ee_worktree_dir" ]; then
|
||||||
git -C "$ee_repo" worktree remove "$ee_worktree_dir" --force 2>/dev/null \
|
git -C "$ee_repo" worktree remove "$ee_worktree_dir" --force 2>/dev/null \
|
||||||
&& echo "Removed EE worktree at $ee_worktree_dir" \
|
&& echo "[cleanup] Removed EE worktree at $ee_worktree_dir" \
|
||||||
|| echo "Warning: Could not remove EE worktree at $ee_worktree_dir"
|
|| echo "[cleanup] Warning: Could not remove EE worktree at $ee_worktree_dir"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up Cursor grouped tmux session
|
|
||||||
tmux kill-session -t "cursor-${wt_basename}" 2>/dev/null || true
|
|
||||||
|
|||||||
@@ -5,30 +5,29 @@ port_in_use() {
|
|||||||
lsof -nP -iTCP:"$1" -sTCP:LISTEN &>/dev/null
|
lsof -nP -iTCP:"$1" -sTCP:LISTEN &>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
find_port() {
|
|
||||||
local port=$1
|
|
||||||
while port_in_use "$port"; do
|
|
||||||
((port++))
|
|
||||||
done
|
|
||||||
echo "$port"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ -z "${WM_SLOT:-}" ]]; then
|
if [[ -z "${WM_SLOT:-}" ]]; then
|
||||||
# Auto-assign: find the first slot (1-99) where both ports are free
|
# Scan .env.local files of existing worktrees to find which slots are claimed,
|
||||||
# Slot 0 (8000/3000) is reserved for the main worktree
|
# then pick the lowest free slot. This avoids collisions when worktrees are
|
||||||
for slot in $(seq 1 99); do
|
# removed and new ones created (position-based indexing would re-use slots
|
||||||
bp=$((8000 + slot * 10))
|
# still held by surviving worktrees).
|
||||||
fp=$((3000 + slot * 10))
|
used_slots=()
|
||||||
if ! port_in_use "$bp" && ! port_in_use "$fp"; then
|
current_dir="$(pwd)"
|
||||||
WM_SLOT=$slot
|
while IFS= read -r wt_path; do
|
||||||
break
|
[[ "$wt_path" == "$current_dir" ]] && continue
|
||||||
|
if [[ -f "$wt_path/.env.local" ]]; then
|
||||||
|
bp=$(grep '^BACKEND_PORT=' "$wt_path/.env.local" | cut -d= -f2 || true)
|
||||||
|
if [[ -n "$bp" && "$bp" -gt 8000 ]]; then
|
||||||
|
used_slots+=("$(( (bp - 8000) / 10 ))")
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
done < <(git worktree list --porcelain | sed -n 's/^worktree //p')
|
||||||
|
|
||||||
|
# Find lowest available slot (slot 0 = 8000/3000 is reserved for main)
|
||||||
|
WM_SLOT=1
|
||||||
|
while [[ " ${used_slots[*]:-} " == *" $WM_SLOT "* ]]; do
|
||||||
|
((WM_SLOT++))
|
||||||
done
|
done
|
||||||
if [[ -z "${WM_SLOT:-}" ]]; then
|
echo "Auto-assigned slot $WM_SLOT (used: ${used_slots[*]:-none})"
|
||||||
echo "ERROR: No available slot found (tried 1-99)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Auto-assigned slot $WM_SLOT"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Slot-based: predictable ports for SSH forwarding
|
# Slot-based: predictable ports for SSH forwarding
|
||||||
@@ -37,8 +36,7 @@ backend_port=$((8000 + WM_SLOT * 10))
|
|||||||
frontend_port=$((3000 + WM_SLOT * 10))
|
frontend_port=$((3000 + WM_SLOT * 10))
|
||||||
|
|
||||||
if port_in_use "$backend_port" || port_in_use "$frontend_port"; then
|
if port_in_use "$backend_port" || port_in_use "$frontend_port"; then
|
||||||
echo "ERROR: Slot $WM_SLOT ports ($backend_port/$frontend_port) already in use" >&2
|
echo "WARNING: Slot $WM_SLOT ports ($backend_port/$frontend_port) already in use" >&2
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate .env.local with port overrides
|
# Generate .env.local with port overrides
|
||||||
@@ -50,6 +48,12 @@ EOF
|
|||||||
|
|
||||||
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
|
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
|
||||||
|
|
||||||
|
# --- Allow direnv so the nix devshell activates in pane commands ---
|
||||||
|
if command -v direnv &>/dev/null && [ -f .envrc ]; then
|
||||||
|
direnv allow
|
||||||
|
echo "direnv allowed"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Create matching windmill-ee-private worktree ---
|
# --- Create matching windmill-ee-private worktree ---
|
||||||
# Check parent directory first (sibling to worktree root), then fall back to home
|
# Check parent directory first (sibling to worktree root), then fall back to home
|
||||||
parent_dir="$(cd "$(pwd)/.." && pwd)"
|
parent_dir="$(cd "$(pwd)/.." && pwd)"
|
||||||
@@ -81,8 +85,24 @@ if [ -d "$ee_repo" ]; then
|
|||||||
echo "EE worktree already exists at $ee_worktree_dir"
|
echo "EE worktree already exists at $ee_worktree_dir"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create symlinks from backend crates to the EE worktree
|
# Point Claude Code additionalDirectories at the EE worktree
|
||||||
if [ -d "$ee_worktree_dir" ] && [ -x "./backend/substitute_ee_code.sh" ]; then
|
if [ -d "$ee_worktree_dir" ]; then
|
||||||
./backend/substitute_ee_code.sh -d "$ee_worktree_dir"
|
ee_rel=$(python3 -c "import os; print(os.path.relpath('$ee_worktree_dir', '$(pwd)'))" 2>/dev/null || echo "$ee_worktree_dir")
|
||||||
|
mkdir -p .claude
|
||||||
|
cat > .claude/settings.local.json <<EOFCLAUDE
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"additionalDirectories": [
|
||||||
|
"$ee_rel"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOFCLAUDE
|
||||||
|
echo "Created .claude/settings.local.json with EE path: $ee_rel"
|
||||||
|
|
||||||
|
# Create symlinks from backend crates to the EE worktree
|
||||||
|
if [ -x "./backend/substitute_ee_code.sh" ]; then
|
||||||
|
./backend/substitute_ee_code.sh -d "$ee_worktree_dir"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
Reference in New Issue
Block a user