Skip to content

Commit a3e9db8

Browse files
authored
Merge branch 'master' into fix/powershell-completion
2 parents 92a122c + 4511ba9 commit a3e9db8

84 files changed

Lines changed: 7800 additions & 256 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/plonecli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../plonecli/skills/plonecli

.claude/skills/plonecli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../plonecli/skills/plonecli

.copier-answers.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2+
_commit: 98bd98e
3+
_src_path: /home/maik/develop/src/copier-claude-code-devcontainer/
4+
enable_docker_in_docker: true
5+
enable_plone: true
6+
enable_pnpm: false
7+
enable_python_uv: true
8+
enable_terminal_recording: true
9+
extra_mounts:
10+
- source=${localEnv:HOME}/develop/plone/src/copier-templates,target=/home/node/develop/plone/src/copier-templates,type=bind,consistency=delegated
11+
node_version: '20'
12+

.devcontainer/Dockerfile

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
2626
chromium \
2727
python3 \
2828
python3-venv \
29+
asciinema \
30+
ffmpeg \
2931
&& apt-get clean && rm -rf /var/lib/apt/lists/*
3032

3133
# Ensure default node user has access to /usr/local/share
@@ -55,12 +57,32 @@ RUN ARCH=$(dpkg --print-architecture) && \
5557
sudo dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
5658
rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"
5759

60+
# Terminal recording tools: vhs (scripted demos), agg (cast -> GIF), ttyd (vhs dep)
61+
ARG VHS_VERSION=0.8.0
62+
ARG AGG_VERSION=1.5.0
63+
ARG TTYD_VERSION=1.7.7
64+
RUN ARCH=$(dpkg --print-architecture) && \
65+
case "$ARCH" in \
66+
amd64) RUST_ARCH=x86_64-unknown-linux-gnu; TTYD_ARCH=x86_64 ;; \
67+
arm64) RUST_ARCH=aarch64-unknown-linux-gnu; TTYD_ARCH=aarch64 ;; \
68+
*) echo "Unsupported arch: $ARCH" && exit 1 ;; \
69+
esac && \
70+
wget "https://github.com/charmbracelet/vhs/releases/download/v${VHS_VERSION}/vhs_${VHS_VERSION}_${ARCH}.deb" && \
71+
sudo dpkg -i "vhs_${VHS_VERSION}_${ARCH}.deb" && \
72+
rm "vhs_${VHS_VERSION}_${ARCH}.deb" && \
73+
wget -O /tmp/agg "https://github.com/asciinema/agg/releases/download/v${AGG_VERSION}/agg-${RUST_ARCH}" && \
74+
sudo install -m 0755 /tmp/agg /usr/local/bin/agg && \
75+
rm /tmp/agg && \
76+
wget -O /tmp/ttyd "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${TTYD_ARCH}" && \
77+
sudo install -m 0755 /tmp/ttyd /usr/local/bin/ttyd && \
78+
rm /tmp/ttyd
79+
5880
# Set up non-root user
5981
USER node
6082

6183
# Install global packages
6284
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
63-
ENV PATH=$HOME/.local/bin:$PATH:/usr/local/share/npm-global/bin
85+
ENV PATH=/home/node/.local/bin:$PATH:/usr/local/share/npm-global/bin
6486

6587
# Set the default shell to zsh rather than sh
6688
ENV SHELL=/bin/zsh
@@ -97,3 +119,6 @@ USER node
97119

98120
# Install UV package manager
99121
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
122+
123+
# Install plonecli (Plone scaffolding CLI) as a global uv tool
124+
RUN $HOME/.local/bin/uv tool install plonecli

.devcontainer/devcontainer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
1414
},
1515

16+
"initializeCommand": "mkdir -p \"$HOME/.claude\" \"$HOME/develop/plone/src/copier-templates\"",
1617
"runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"],
1718
"customizations": {
1819
"vscode": {
@@ -46,8 +47,8 @@
4647
"mounts": [
4748
"source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
4849
"source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume",
49-
"source=${localEnv:HOME}/develop/plone/src/copier-templates,target=/home/node/develop/plone/src/copier-templates,type=bind,consistency=delegated",
50-
"source=${localEnv:HOME}/.copier-templates/plone-copier-templates,target=/home/node/.copier-templates/plone-copier-templates,type=bind,readonly"
50+
"source=${localEnv:HOME}/.claude,target=/host-claude-config,type=bind,readonly",
51+
"source=${localEnv:HOME}/develop/plone/src/copier-templates,target=/home/node/.copier-templates/plone-copier-templates,type=bind,consistency=delegated"
5152
],
5253
"containerEnv": {
5354
"CLAUDE_CONFIG_DIR": "/home/node/.claude",

.devcontainer/init-firewall.sh

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,17 @@ for domain in \
8686
"api.anthropic.com" \
8787
"claude.ai" \
8888
"sentry.io" \
89-
"statsig.anthropic.com" \
9089
"statsig.com" \
9190
"marketplace.visualstudio.com" \
9291
"vscode.blob.core.windows.net" \
93-
"update.code.visualstudio.com" \
94-
"opencode.ai"; do
92+
"update.code.visualstudio.com"; do
9593
echo "Resolving $domain..."
9694
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
9795
if [ -z "$ips" ]; then
9896
echo "ERROR: Failed to resolve $domain"
9997
exit 1
10098
fi
101-
99+
102100
while read -r ip; do
103101
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
104102
echo "ERROR: Invalid IP from DNS for $domain: $ip"
@@ -112,7 +110,26 @@ done
112110
# PyPI (for UV package installation)
113111
for domain in \
114112
"pypi.org" \
115-
"files.pythonhosted.org" \
113+
"files.pythonhosted.org"; do
114+
echo "Resolving $domain..."
115+
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
116+
if [ -z "$ips" ]; then
117+
echo "ERROR: Failed to resolve $domain"
118+
exit 1
119+
fi
120+
121+
while read -r ip; do
122+
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
123+
echo "ERROR: Invalid IP from DNS for $domain: $ip"
124+
exit 1
125+
fi
126+
echo "Adding $ip for $domain"
127+
ipset add allowed-domains "$ip" 2>/dev/null || true
128+
done < <(echo "$ips")
129+
done
130+
131+
# Plone (for version constraints)
132+
for domain in \
116133
"dist.plone.org"; do
117134
echo "Resolving $domain..."
118135
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')

.devcontainer/setup-claude.sh

Lines changed: 132 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,98 @@ else
1515
echo "WARNING: Failed to download Claude Code installer, skipping"
1616
fi
1717

18-
echo "Installing OpenCode..."
19-
if curl -fsSL -o "$TMPDIR/opencode-install.sh" https://opencode.ai/install; then
20-
bash "$TMPDIR/opencode-install.sh"
21-
else
22-
echo "WARNING: Failed to download OpenCode installer, skipping"
23-
fi
18+
# ---------------------------------------------------------------------------
19+
# Sync host's global Claude Code configuration (if available)
20+
# ---------------------------------------------------------------------------
21+
sync_host_claude_config() {
22+
local HOST_CONFIG="/host-claude-config"
23+
local USER_CONFIG="$HOME/.claude"
24+
25+
if [ ! -d "$HOST_CONFIG" ] || [ -z "$(ls -A "$HOST_CONFIG" 2>/dev/null)" ]; then
26+
echo "No host Claude config found, skipping sync"
27+
return 0
28+
fi
29+
30+
echo "Syncing host Claude Code configuration..."
31+
mkdir -p "$USER_CONFIG"
32+
33+
# Copy portable single files
34+
for file in CLAUDE.md keybindings.json; do
35+
if [ -f "$HOST_CONFIG/$file" ]; then
36+
echo " Copying $file"
37+
cp "$HOST_CONFIG/$file" "$USER_CONFIG/$file"
38+
fi
39+
done
40+
41+
# Copy portable directories
42+
for dir in agents commands skills; do
43+
if [ -d "$HOST_CONFIG/$dir" ] && [ -n "$(ls -A "$HOST_CONFIG/$dir" 2>/dev/null)" ]; then
44+
echo " Copying $dir/"
45+
cp -r "$HOST_CONFIG/$dir" "$USER_CONFIG/"
46+
fi
47+
done
48+
49+
# Copy plugins (selective — skip marketplaces which are ~300MB of git repos)
50+
if [ -d "$HOST_CONFIG/plugins" ]; then
51+
echo " Syncing plugins..."
52+
mkdir -p "$USER_CONFIG/plugins"
53+
54+
# Copy small metadata files
55+
for file in config.json blocklist.json; do
56+
if [ -f "$HOST_CONFIG/plugins/$file" ]; then
57+
cp "$HOST_CONFIG/plugins/$file" "$USER_CONFIG/plugins/$file"
58+
fi
59+
done
60+
61+
# Copy and rewrite installed_plugins.json (fix host-specific paths)
62+
if [ -f "$HOST_CONFIG/plugins/installed_plugins.json" ]; then
63+
node -e "
64+
const fs = require('fs');
65+
const data = JSON.parse(fs.readFileSync('$HOST_CONFIG/plugins/installed_plugins.json', 'utf8'));
66+
if (data.plugins) {
67+
for (const entries of Object.values(data.plugins)) {
68+
for (const entry of entries) {
69+
if (entry.installPath) {
70+
entry.installPath = entry.installPath.replace(/^\/home\/[^/]+\/.claude/, '$USER_CONFIG');
71+
}
72+
}
73+
}
74+
}
75+
fs.writeFileSync('$USER_CONFIG/plugins/installed_plugins.json', JSON.stringify(data, null, 2));
76+
"
77+
fi
78+
79+
# Copy and rewrite known_marketplaces.json (fix host-specific paths)
80+
if [ -f "$HOST_CONFIG/plugins/known_marketplaces.json" ]; then
81+
node -e "
82+
const fs = require('fs');
83+
const data = JSON.parse(fs.readFileSync('$HOST_CONFIG/plugins/known_marketplaces.json', 'utf8'));
84+
for (const marketplace of Object.values(data)) {
85+
if (marketplace.installLocation) {
86+
marketplace.installLocation = marketplace.installLocation.replace(/^\/home\/[^/]+\/.claude/, '$USER_CONFIG');
87+
}
88+
}
89+
fs.writeFileSync('$USER_CONFIG/plugins/known_marketplaces.json', JSON.stringify(data, null, 2));
90+
"
91+
fi
2492

93+
# Copy plugin subdirectories (cache and local plugins, skip large marketplaces)
94+
for dir in cache local data; do
95+
if [ -d "$HOST_CONFIG/plugins/$dir" ] && [ -n "$(ls -A "$HOST_CONFIG/plugins/$dir" 2>/dev/null)" ]; then
96+
echo " Copying plugins/$dir/"
97+
cp -r "$HOST_CONFIG/plugins/$dir" "$USER_CONFIG/plugins/"
98+
fi
99+
done
100+
fi
101+
102+
echo "Host config sync complete."
103+
}
104+
105+
sync_host_claude_config
106+
107+
# ---------------------------------------------------------------------------
108+
# Configure MCP servers
109+
# ---------------------------------------------------------------------------
25110
if command -v claude &>/dev/null; then
26111
echo "Configuring Chrome DevTools MCP server..."
27112
claude mcp remove chrome-devtools 2>/dev/null || true
@@ -30,19 +115,48 @@ else
30115
echo "WARNING: claude CLI not found, skipping MCP configuration"
31116
fi
32117

33-
echo "Enabling remote control for all sessions..."
118+
# ---------------------------------------------------------------------------
119+
# Merge settings.json (host settings + container overrides)
120+
# ---------------------------------------------------------------------------
121+
echo "Configuring settings.json..."
34122
mkdir -p ~/.claude
35123
SETTINGS_FILE="$HOME/.claude/settings.json"
36-
if [ -f "$SETTINGS_FILE" ]; then
37-
# Merge preferRemoteControl into existing settings using node (available in the container)
38-
node -e "
124+
HOST_SETTINGS="/host-claude-config/settings.json"
125+
126+
node -e "
39127
const fs = require('fs');
40-
const s = JSON.parse(fs.readFileSync('$SETTINGS_FILE', 'utf8'));
41-
s.preferRemoteControl = true;
42-
fs.writeFileSync('$SETTINGS_FILE', JSON.stringify(s, null, 2));
43-
"
44-
else
45-
echo '{"preferRemoteControl": true}' > "$SETTINGS_FILE"
46-
fi
128+
let settings = {};
129+
130+
// Layer 1: Host settings (portable subset)
131+
if (fs.existsSync('$HOST_SETTINGS')) {
132+
try {
133+
settings = JSON.parse(fs.readFileSync('$HOST_SETTINGS', 'utf8'));
134+
// Remove keys that reference host-specific file paths
135+
delete settings.hooks;
136+
delete settings.statusLine;
137+
console.log(' Merged host settings.json');
138+
} catch (e) {
139+
console.error(' WARNING: Failed to parse host settings.json:', e.message);
140+
settings = {};
141+
}
142+
}
143+
144+
// Layer 2: Existing container settings (from previous setup/rebuild)
145+
if (fs.existsSync('$SETTINGS_FILE')) {
146+
try {
147+
const existing = JSON.parse(fs.readFileSync('$SETTINGS_FILE', 'utf8'));
148+
settings = { ...settings, ...existing };
149+
console.log(' Merged existing container settings.json');
150+
} catch (e) {
151+
console.error(' WARNING: Failed to parse existing settings.json:', e.message);
152+
}
153+
}
154+
155+
// Layer 3: Container-required settings
156+
settings.preferRemoteControl = true;
157+
158+
fs.writeFileSync('$SETTINGS_FILE', JSON.stringify(settings, null, 2));
159+
console.log(' Wrote merged settings.json');
160+
"
47161

48-
echo "Claude Code and OpenCode setup complete."
162+
echo "Claude Code setup complete."

.devshell.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "plonecli",
3+
"dirs": [
4+
"."
5+
]
6+
}

.github/workflows/python-package.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@ jobs:
1919

2020
steps:
2121
- uses: actions/checkout@v2
22+
- name: Checkout copier-templates
23+
uses: actions/checkout@v2
24+
with:
25+
repository: plone/copier-templates
26+
path: copier-templates
2227
- name: Set up Python ${{ matrix.python-version }}
2328
uses: actions/setup-python@v2
2429
with:
2530
python-version: ${{ matrix.python-version }}
31+
- name: Set up uv
32+
uses: astral-sh/setup-uv@v5
2633
- name: Install dependencies
2734
run: |
2835
python -m pip install --upgrade pip
@@ -32,9 +39,11 @@ jobs:
3239
- name: Lint with flake8
3340
run: |
3441
# stop the build if there are Python syntax errors or undefined names
35-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
42+
flake8 . --exclude=copier-templates --count --select=E9,F63,F7,F82 --show-source --statistics
3643
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
37-
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
44+
flake8 . --exclude=copier-templates --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
3845
- name: Test with pytest
46+
env:
47+
PLONECLI_TEMPLATES_DIR: ${{ github.workspace }}/copier-templates
3948
run: |
4049
pytest

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
- use native uv, not uv pi or pip
2+
- if editing of copier-templates is need, do it in dev path: develop/plone/src/copier-templates, not in the local copy in .copier-templates dir. When in devcontainer, the directory should be in /home/node/.copier-templates/plone-copier-templates/
3+
- no claude claude mentioning in commit messages or readme/changelog
4+
- all test have to pass, don't skip tests!
5+
- if changes on the devcontainer setup are needed, do it in the copier template "/home/maik/develop/src/copier-claude-code-devcontainer/" and apply the template again.
6+
- keep commit and changelog messages, as well as doc strings short and lean, focus on facts
7+
- don't create a manual test plan in PR description, create real test which are run in the CI!
8+
- keep commit message and PR description lean
9+

0 commit comments

Comments
 (0)