Skip to content

Commit 3830746

Browse files
authored
Merge pull request #98 from plone/add-plonecli-skill
Add plonecli Agent Skill and 'skill install/update' command
2 parents 4511a64 + ddc78af commit 3830746

25 files changed

Lines changed: 2036 additions & 46 deletions

.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: 978185b
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: 22 additions & 0 deletions
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,6 +57,26 @@ 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

.devcontainer/devcontainer.json

Lines changed: 5 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/.copier-templates\" \"$HOME/.plonecli\"",
1617
"runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"],
1718
"customizations": {
1819
"vscode": {
@@ -46,8 +47,10 @@
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}/.copier-templates,target=/home/node/.copier-templates,type=bind,readonly",
52+
"source=${localEnv:HOME}/.plonecli,target=/home/node/.plonecli,type=bind,readonly",
53+
"source=${localEnv:HOME}/develop/plone/src/copier-templates,target=/home/node/develop/plone/src/copier-templates,type=bind,consistency=delegated"
5154
],
5255
"containerEnv": {
5356
"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."

CHANGES.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,28 @@
33
## 7.0.0b3 (unreleased)
44

55

6-
- Nothing changed yet.
6+
- Add `plonecli skill install|update|status` to install the bundled AI coding
7+
agent skill. The skill follows the Agent Skills open standard and is written
8+
to `.agents/skills/plonecli` (project or `--scope user`) and linked from
9+
`.claude/skills/plonecli`, so Claude Code, Codex, Gemini CLI, Cursor and other
10+
compatible agents load the same `SKILL.md`.
11+
[MrTango]
12+
13+
- Fix `plonecli update` failing with "Not possible to fast-forward" when the
14+
upstream copier-templates branch was rebased or force-pushed. The local
15+
clone is now refreshed by fetching and hard-resetting to
16+
`origin/<branch>` instead of attempting a fast-forward merge.
17+
[MrTango]
18+
19+
- Show available templates in `plonecli create --help`. The help output
20+
now lists each main template with its description and aliases
21+
(composite templates render as `composite: a + b`).
22+
[MrTango]
23+
24+
- Document the `invoke reconfigure` task in the README, covering the
25+
`addon`, `zope-setup`, and `instance` targets and the `--name` flag
26+
for named instances.
27+
[MrTango]
728

829

930
## 7.0.0b2 (2026-04-16)

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
- 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/
33
- no claude claude mentioning in commit messages or readme/changelog
44
- 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 lean, focus on facts

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
The Plone CLI is meant for developing Plone packages. It uses [copier](https://copier.readthedocs.io/) templates to scaffold Plone backend addons, Zope project setups, and add features like content types, behaviors, and REST API services.
1111

1212

13+
## Demo
14+
15+
Scaffolding a backend add-on, adding a content type and behavior, and wiring up a REST API service:
16+
17+
<video src="https://github.com/plone/plonecli/raw/master/docs/demo-backend.webm" controls muted width="100%"></video>
18+
19+
> ▶ If the video does not play inline, [watch `docs/demo-backend.webm`](https://github.com/plone/plonecli/raw/master/docs/demo-backend.webm).
20+
21+
1322
## Installation
1423

1524
### UV Tool (Recommended)
@@ -204,6 +213,27 @@ plonecli update
204213
This pulls the latest copier-templates and checks PyPI for plonecli updates.
205214

206215

216+
### AI Coding Agent Skill
217+
218+
plonecli ships an [Agent Skill](https://www.anthropic.com/news/skills) that teaches AI coding agents how to use it. Because the skill follows the Agent Skills open standard, the same `SKILL.md` is loaded by Claude Code, Codex, Gemini CLI, Cursor and other compatible agents.
219+
220+
```shell
221+
# install into the current project (.agents/skills + .claude/skills)
222+
plonecli skill install
223+
224+
# install globally for your user (~/.agents/skills + ~/.claude/skills)
225+
plonecli skill install --scope user
226+
227+
# refresh after upgrading plonecli
228+
plonecli skill update
229+
230+
# show where it is installed
231+
plonecli skill status
232+
```
233+
234+
The skill is written to `.agents/skills/plonecli` (the open-standard discovery path) and linked from `.claude/skills/plonecli` for Claude Code. Pass `--copy` if your environment cannot create symlinks, and `--force` to overwrite an existing install.
235+
236+
207237
### Reconfiguring an Existing Project
208238

209239
After initial creation, you can re-run a template's questions to change settings without recreating the project. The zope-setup template provides an `invoke reconfigure` task that wraps `copier recopy --trust --overwrite` and points at the right answers file for each target.

0 commit comments

Comments
 (0)