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
1 change: 1 addition & 0 deletions .agents/skills/plonecli
1 change: 1 addition & 0 deletions .claude/skills/plonecli
12 changes: 12 additions & 0 deletions .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: 978185b
_src_path: /home/maik/develop/src/copier-claude-code-devcontainer/
enable_docker_in_docker: true
enable_plone: true
enable_pnpm: false
enable_python_uv: true
enable_terminal_recording: true
extra_mounts:
- source=${localEnv:HOME}/develop/plone/src/copier-templates,target=/home/node/develop/plone/src/copier-templates,type=bind,consistency=delegated
node_version: '20'

22 changes: 22 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
chromium \
python3 \
python3-venv \
asciinema \
ffmpeg \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

# Ensure default node user has access to /usr/local/share
Expand Down Expand Up @@ -55,6 +57,26 @@ RUN ARCH=$(dpkg --print-architecture) && \
sudo dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"

# Terminal recording tools: vhs (scripted demos), agg (cast -> GIF), ttyd (vhs dep)
ARG VHS_VERSION=0.8.0
ARG AGG_VERSION=1.5.0
ARG TTYD_VERSION=1.7.7
RUN ARCH=$(dpkg --print-architecture) && \
case "$ARCH" in \
amd64) RUST_ARCH=x86_64-unknown-linux-gnu; TTYD_ARCH=x86_64 ;; \
arm64) RUST_ARCH=aarch64-unknown-linux-gnu; TTYD_ARCH=aarch64 ;; \
*) echo "Unsupported arch: $ARCH" && exit 1 ;; \
esac && \
wget "https://github.com/charmbracelet/vhs/releases/download/v${VHS_VERSION}/vhs_${VHS_VERSION}_${ARCH}.deb" && \
sudo dpkg -i "vhs_${VHS_VERSION}_${ARCH}.deb" && \
rm "vhs_${VHS_VERSION}_${ARCH}.deb" && \
wget -O /tmp/agg "https://github.com/asciinema/agg/releases/download/v${AGG_VERSION}/agg-${RUST_ARCH}" && \
sudo install -m 0755 /tmp/agg /usr/local/bin/agg && \
rm /tmp/agg && \
wget -O /tmp/ttyd "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${TTYD_ARCH}" && \
sudo install -m 0755 /tmp/ttyd /usr/local/bin/ttyd && \
rm /tmp/ttyd

# Set up non-root user
USER node

Expand Down
7 changes: 5 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},

"initializeCommand": "mkdir -p \"$HOME/.claude\" \"$HOME/.copier-templates\" \"$HOME/.plonecli\"",
"runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"],
"customizations": {
"vscode": {
Expand Down Expand Up @@ -46,8 +47,10 @@
"mounts": [
"source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
"source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume",
"source=${localEnv:HOME}/develop/plone/src/copier-templates,target=/home/node/develop/plone/src/copier-templates,type=bind,consistency=delegated",
"source=${localEnv:HOME}/.copier-templates/plone-copier-templates,target=/home/node/.copier-templates/plone-copier-templates,type=bind,readonly"
"source=${localEnv:HOME}/.claude,target=/host-claude-config,type=bind,readonly",
"source=${localEnv:HOME}/.copier-templates,target=/home/node/.copier-templates,type=bind,readonly",
"source=${localEnv:HOME}/.plonecli,target=/home/node/.plonecli,type=bind,readonly",
"source=${localEnv:HOME}/develop/plone/src/copier-templates,target=/home/node/develop/plone/src/copier-templates,type=bind,consistency=delegated"
],
"containerEnv": {
"CLAUDE_CONFIG_DIR": "/home/node/.claude",
Expand Down
27 changes: 22 additions & 5 deletions .devcontainer/init-firewall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,17 @@ for domain in \
"api.anthropic.com" \
"claude.ai" \
"sentry.io" \
"statsig.anthropic.com" \
"statsig.com" \
"marketplace.visualstudio.com" \
"vscode.blob.core.windows.net" \
"update.code.visualstudio.com" \
"opencode.ai"; do
"update.code.visualstudio.com"; do
echo "Resolving $domain..."
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
if [ -z "$ips" ]; then
echo "ERROR: Failed to resolve $domain"
exit 1
fi

while read -r ip; do
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "ERROR: Invalid IP from DNS for $domain: $ip"
Expand All @@ -112,7 +110,26 @@ done
# PyPI (for UV package installation)
for domain in \
"pypi.org" \
"files.pythonhosted.org" \
"files.pythonhosted.org"; do
echo "Resolving $domain..."
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
if [ -z "$ips" ]; then
echo "ERROR: Failed to resolve $domain"
exit 1
fi

while read -r ip; do
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "ERROR: Invalid IP from DNS for $domain: $ip"
exit 1
fi
echo "Adding $ip for $domain"
ipset add allowed-domains "$ip" 2>/dev/null || true
done < <(echo "$ips")
done

# Plone (for version constraints)
for domain in \
"dist.plone.org"; do
echo "Resolving $domain..."
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
Expand Down
150 changes: 132 additions & 18 deletions .devcontainer/setup-claude.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,98 @@ else
echo "WARNING: Failed to download Claude Code installer, skipping"
fi

echo "Installing OpenCode..."
if curl -fsSL -o "$TMPDIR/opencode-install.sh" https://opencode.ai/install; then
bash "$TMPDIR/opencode-install.sh"
else
echo "WARNING: Failed to download OpenCode installer, skipping"
fi
# ---------------------------------------------------------------------------
# Sync host's global Claude Code configuration (if available)
# ---------------------------------------------------------------------------
sync_host_claude_config() {
local HOST_CONFIG="/host-claude-config"
local USER_CONFIG="$HOME/.claude"

if [ ! -d "$HOST_CONFIG" ] || [ -z "$(ls -A "$HOST_CONFIG" 2>/dev/null)" ]; then
echo "No host Claude config found, skipping sync"
return 0
fi

echo "Syncing host Claude Code configuration..."
mkdir -p "$USER_CONFIG"

# Copy portable single files
for file in CLAUDE.md keybindings.json; do
if [ -f "$HOST_CONFIG/$file" ]; then
echo " Copying $file"
cp "$HOST_CONFIG/$file" "$USER_CONFIG/$file"
fi
done

# Copy portable directories
for dir in agents commands skills; do
if [ -d "$HOST_CONFIG/$dir" ] && [ -n "$(ls -A "$HOST_CONFIG/$dir" 2>/dev/null)" ]; then
echo " Copying $dir/"
cp -r "$HOST_CONFIG/$dir" "$USER_CONFIG/"
fi
done

# Copy plugins (selective — skip marketplaces which are ~300MB of git repos)
if [ -d "$HOST_CONFIG/plugins" ]; then
echo " Syncing plugins..."
mkdir -p "$USER_CONFIG/plugins"

# Copy small metadata files
for file in config.json blocklist.json; do
if [ -f "$HOST_CONFIG/plugins/$file" ]; then
cp "$HOST_CONFIG/plugins/$file" "$USER_CONFIG/plugins/$file"
fi
done

# Copy and rewrite installed_plugins.json (fix host-specific paths)
if [ -f "$HOST_CONFIG/plugins/installed_plugins.json" ]; then
node -e "
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('$HOST_CONFIG/plugins/installed_plugins.json', 'utf8'));
if (data.plugins) {
for (const entries of Object.values(data.plugins)) {
for (const entry of entries) {
if (entry.installPath) {
entry.installPath = entry.installPath.replace(/^\/home\/[^/]+\/.claude/, '$USER_CONFIG');
}
}
}
}
fs.writeFileSync('$USER_CONFIG/plugins/installed_plugins.json', JSON.stringify(data, null, 2));
"
fi

# Copy and rewrite known_marketplaces.json (fix host-specific paths)
if [ -f "$HOST_CONFIG/plugins/known_marketplaces.json" ]; then
node -e "
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('$HOST_CONFIG/plugins/known_marketplaces.json', 'utf8'));
for (const marketplace of Object.values(data)) {
if (marketplace.installLocation) {
marketplace.installLocation = marketplace.installLocation.replace(/^\/home\/[^/]+\/.claude/, '$USER_CONFIG');
}
}
fs.writeFileSync('$USER_CONFIG/plugins/known_marketplaces.json', JSON.stringify(data, null, 2));
"
fi

# Copy plugin subdirectories (cache and local plugins, skip large marketplaces)
for dir in cache local data; do
if [ -d "$HOST_CONFIG/plugins/$dir" ] && [ -n "$(ls -A "$HOST_CONFIG/plugins/$dir" 2>/dev/null)" ]; then
echo " Copying plugins/$dir/"
cp -r "$HOST_CONFIG/plugins/$dir" "$USER_CONFIG/plugins/"
fi
done
fi

echo "Host config sync complete."
}

sync_host_claude_config

# ---------------------------------------------------------------------------
# Configure MCP servers
# ---------------------------------------------------------------------------
if command -v claude &>/dev/null; then
echo "Configuring Chrome DevTools MCP server..."
claude mcp remove chrome-devtools 2>/dev/null || true
Expand All @@ -30,19 +115,48 @@ else
echo "WARNING: claude CLI not found, skipping MCP configuration"
fi

echo "Enabling remote control for all sessions..."
# ---------------------------------------------------------------------------
# Merge settings.json (host settings + container overrides)
# ---------------------------------------------------------------------------
echo "Configuring settings.json..."
mkdir -p ~/.claude
SETTINGS_FILE="$HOME/.claude/settings.json"
if [ -f "$SETTINGS_FILE" ]; then
# Merge preferRemoteControl into existing settings using node (available in the container)
node -e "
HOST_SETTINGS="/host-claude-config/settings.json"

node -e "
const fs = require('fs');
const s = JSON.parse(fs.readFileSync('$SETTINGS_FILE', 'utf8'));
s.preferRemoteControl = true;
fs.writeFileSync('$SETTINGS_FILE', JSON.stringify(s, null, 2));
"
else
echo '{"preferRemoteControl": true}' > "$SETTINGS_FILE"
fi
let settings = {};

// Layer 1: Host settings (portable subset)
if (fs.existsSync('$HOST_SETTINGS')) {
try {
settings = JSON.parse(fs.readFileSync('$HOST_SETTINGS', 'utf8'));
// Remove keys that reference host-specific file paths
delete settings.hooks;
delete settings.statusLine;
console.log(' Merged host settings.json');
} catch (e) {
console.error(' WARNING: Failed to parse host settings.json:', e.message);
settings = {};
}
}

// Layer 2: Existing container settings (from previous setup/rebuild)
if (fs.existsSync('$SETTINGS_FILE')) {
try {
const existing = JSON.parse(fs.readFileSync('$SETTINGS_FILE', 'utf8'));
settings = { ...settings, ...existing };
console.log(' Merged existing container settings.json');
} catch (e) {
console.error(' WARNING: Failed to parse existing settings.json:', e.message);
}
}

// Layer 3: Container-required settings
settings.preferRemoteControl = true;

fs.writeFileSync('$SETTINGS_FILE', JSON.stringify(settings, null, 2));
console.log(' Wrote merged settings.json');
"

echo "Claude Code and OpenCode setup complete."
echo "Claude Code setup complete."
23 changes: 22 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,28 @@
## 7.0.0b3 (unreleased)


- Nothing changed yet.
- Add `plonecli skill install|update|status` to install the bundled AI coding
agent skill. The skill follows the Agent Skills open standard and is written
to `.agents/skills/plonecli` (project or `--scope user`) and linked from
`.claude/skills/plonecli`, so Claude Code, Codex, Gemini CLI, Cursor and other
compatible agents load the same `SKILL.md`.
[MrTango]

- Fix `plonecli update` failing with "Not possible to fast-forward" when the
upstream copier-templates branch was rebased or force-pushed. The local
clone is now refreshed by fetching and hard-resetting to
`origin/<branch>` instead of attempting a fast-forward merge.
[MrTango]

- Show available templates in `plonecli create --help`. The help output
now lists each main template with its description and aliases
(composite templates render as `composite: a + b`).
[MrTango]

- Document the `invoke reconfigure` task in the README, covering the
`addon`, `zope-setup`, and `instance` targets and the `--name` flag
for named instances.
[MrTango]


## 7.0.0b2 (2026-04-16)
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
- 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/
- no claude claude mentioning in commit messages or readme/changelog
- all test have to pass, don't skip tests!
- 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.
- keep commit and changelog messages lean, focus on facts
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
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.


## Demo

Scaffolding a backend add-on, adding a content type and behavior, and wiring up a REST API service:

<video src="https://github.com/plone/plonecli/raw/master/docs/demo-backend.webm" controls muted width="100%"></video>

> ▶ If the video does not play inline, [watch `docs/demo-backend.webm`](https://github.com/plone/plonecli/raw/master/docs/demo-backend.webm).


## Installation

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


### AI Coding Agent Skill

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.

```shell
# install into the current project (.agents/skills + .claude/skills)
plonecli skill install

# install globally for your user (~/.agents/skills + ~/.claude/skills)
plonecli skill install --scope user

# refresh after upgrading plonecli
plonecli skill update

# show where it is installed
plonecli skill status
```

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.


### Reconfiguring an Existing Project

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.
Expand Down
6 changes: 3 additions & 3 deletions devcontainer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ usage() {
echo "Usage: $0 {start|stop|rebuild|bash}"
echo ""
echo " start Start the devcontainer"
echo " stop Stop and remove the devcontainer"
echo " stop Stop the devcontainer (container preserved for fast restart)"
echo " rebuild Rebuild (no cache) and start the devcontainer"
echo " bash Open a bash shell inside the running devcontainer"
exit 1
Expand All @@ -22,8 +22,8 @@ cmd_stop() {
echo "Stopping devcontainer..."
CONTAINER_IDS=$(docker ps -q --filter "label=devcontainer.local_folder=$WORKSPACE_FOLDER")
if [ -n "$CONTAINER_IDS" ]; then
docker rm -f $CONTAINER_IDS
echo "Devcontainer stopped."
docker stop $CONTAINER_IDS
echo "Devcontainer stopped (container preserved; use 'rebuild' for a clean image)."
else
echo "No running devcontainer found."
fi
Expand Down
Loading
Loading