diff --git a/.agents/skills/plonecli b/.agents/skills/plonecli new file mode 120000 index 0000000..1a2c952 --- /dev/null +++ b/.agents/skills/plonecli @@ -0,0 +1 @@ +../../plonecli/skills/plonecli \ No newline at end of file diff --git a/.claude/skills/plonecli b/.claude/skills/plonecli new file mode 120000 index 0000000..1a2c952 --- /dev/null +++ b/.claude/skills/plonecli @@ -0,0 +1 @@ +../../plonecli/skills/plonecli \ No newline at end of file diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..441e9ce --- /dev/null +++ b/.copier-answers.yml @@ -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' + diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 480b8d5..6890019 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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 @@ -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 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 22f60c5..a221e51 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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": { @@ -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", diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh index e74b6e3..5944dea 100644 --- a/.devcontainer/init-firewall.sh +++ b/.devcontainer/init-firewall.sh @@ -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" @@ -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}') diff --git a/.devcontainer/setup-claude.sh b/.devcontainer/setup-claude.sh index 444966a..ae231ee 100644 --- a/.devcontainer/setup-claude.sh +++ b/.devcontainer/setup-claude.sh @@ -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 @@ -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." diff --git a/CHANGES.md b/CHANGES.md index 0400cd5..a8b98c8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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/` 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) diff --git a/CLAUDE.md b/CLAUDE.md index ff3d135..5ff2c14 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index f971aaf..1c77ccc 100644 --- a/README.md +++ b/README.md @@ -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: + + + +> ▶ 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) @@ -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. diff --git a/devcontainer.sh b/devcontainer.sh index 0f8fdcc..0f1f2a3 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -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 @@ -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 diff --git a/docs/demo-backend.tape b/docs/demo-backend.tape new file mode 100644 index 0000000..d809d88 --- /dev/null +++ b/docs/demo-backend.tape @@ -0,0 +1,398 @@ +# vhs tape: plonecli demo — BACKEND category +# +# Video 1 of 3. Creates `collective.backenddemo`, runs every backend +# subtemplate (content_type is demonstrated twice: Container then Item), +# then zope-setup with FileStorage (instance). +# +# This tape DRIVES every copier prompt — arrow keys for choices, y/n +# for booleans, text for inputs. Nothing waits on a human. +# +# Requires copier-templates >= the revision that adds `parent_content_type` +# (question fires when global_allow == false). Run `plonecli update` if +# the second content_type step skips that prompt. +# +# Render: vhs docs/demo-backend.tape → docs/demo-backend.webm + +Output docs/demo-backend.webm + +Set Shell "bash" +Set FontSize 16 +Set FontFamily "JetBrains Mono" +Set Width 1440 +Set Height 900 +Set Theme "Dracula" +Set TypingSpeed 80ms +Set PlaybackSpeed 1.0 + +# ---- Pre-demo (hidden) ---- +Hide +Type `eval "$(_PLONECLI_COMPLETE=bash_source plonecli)"` Enter +Type `mkdir -p /tmp/plonecli-demo && cd /tmp/plonecli-demo && rm -rf collective.backenddemo` Enter +Type "clear" Enter +Show +Sleep 1s + +# ---- Intro banner ---- +Type `echo -e "\n\033[1;45m ━━━ plonecli Demo 1 of 3 — BACKEND templates ━━━ \033[0m\n"` Enter +Sleep 3s + +# ---- Common intro: version / list / help / completion ---- +Type `echo -e "\n\033[1;46m ▶ STEP 1 — plonecli version \033[0m\n"` Enter +Sleep 2s +Type "plonecli -V" Enter +Sleep 3s +Sleep 5s + +Type `echo -e "\n\033[1;46m ▶ STEP 2 — list available templates \033[0m\n"` Enter +Sleep 2s +Type "plonecli -l" Enter +Sleep 5s + +Type `echo -e "\n\033[1;46m ▶ STEP 3 — top-level help \033[0m\n"` Enter +Sleep 2s +Type "plonecli --help" Enter +Sleep 5s + +Type `echo -e "\n\033[1;46m ▶ STEP 4 — tab-complete top-level commands \033[0m\n"` Enter +Sleep 2s +Type "plonecli " +Sleep 400ms +Tab Tab +Sleep 3s +Ctrl+C +Sleep 1s + +Type `echo -e "\n\033[1;46m ▶ STEP 5 — tab-complete main templates for 'create' \033[0m\n"` Enter +Sleep 2s +Type "plonecli create " +Sleep 400ms +Tab Tab +Sleep 3s +Type "add" +Tab +Sleep 1500ms +Ctrl+C +Sleep 1s + +# ---- Create the addon (drive 7 backend_addon prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 6 — create collective.backenddemo addon \033[0m\n"` Enter +Sleep 2s +Type "plonecli create addon collective.backenddemo" Enter +Sleep 3s +# package_name — default "collective.backenddemo" +Enter +Sleep 1500ms +# package_title +Enter +Sleep 1500ms +# package_description +Enter +Sleep 1500ms +# plone_version (choice — demonstrate arrow nav even for default) +Down +Sleep 800ms +Up +Sleep 800ms +Enter +Sleep 1500ms +# is_headless (bool, default no) +Enter +Sleep 1500ms +# author_name +Enter +Sleep 1500ms +# author_email +Enter +Sleep 20s + +Type "cd collective.backenddemo" Enter +Sleep 2s + +# ---- behavior (4 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 7 — add behavior \033[0m\n"` Enter +Sleep 2s +Type "plonecli add be" +Tab +Sleep 600ms +Enter +Sleep 3s +# behavior_name (required) +Type "IFeatured" +Sleep 500ms +Enter +Sleep 1500ms +# behavior_description +Enter +Sleep 1500ms +# behavior_marker +Enter +Sleep 1500ms +# behavior_factory +Enter +Sleep 8s +Sleep 5s + +# ---- content_type #1 — Container, globally addable ---- +Type `echo -e "\n\033[1;46m ▶ STEP 8 — add content_type (Container 'Project', globally addable) \033[0m\n"` Enter +Sleep 2s +Type "plonecli add conte" +Tab +Sleep 600ms +Enter +Sleep 3s +# content_type_name +Type "Project" +Sleep 500ms +Enter +Sleep 1500ms +# content_type_description +Type "A project folder" +Sleep 500ms +Enter +Sleep 1500ms +# content_type_base (choice — Container default, demo arrow navigation) +Down +Sleep 800ms +Up +Sleep 800ms +Enter +Sleep 1500ms +# content_type_icon +Type "folder" +Sleep 500ms +Enter +Sleep 1500ms +# global_allow (bool, default yes) +Enter +Sleep 1500ms +# filter_content_types (bool, default yes — only when Container) +Enter +Sleep 1500ms +# activate_default_behaviors (bool) — answer NO so we can demo dublin/nav prompts +Type "n" +Sleep 500ms +Enter +Sleep 1500ms +# enable_dublin_core (bool, default yes) +Enter +Sleep 1500ms +# enable_navigation (bool, default yes) +Enter +Sleep 10s +Sleep 5s + +# ---- content_type #2 — Item, not globally addable ---- +Type `echo -e "\n\033[1;46m ▶ STEP 9 — add content_type (Item 'Task', not globally addable) \033[0m\n"` Enter +Sleep 2s +Type "plonecli add conte" +Tab +Sleep 600ms +Enter +Sleep 3s +# content_type_name +Type "Task" +Sleep 500ms +Enter +Sleep 1500ms +# content_type_description +Type "A task inside a Project" +Sleep 500ms +Enter +Sleep 1500ms +# content_type_base (choice — navigate Down to pick Item) +Down +Sleep 800ms +Enter +Sleep 1500ms +# content_type_icon +Type "check2-square" +Sleep 500ms +Enter +Sleep 1500ms +# global_allow — answer NO (opposite of first run) +Type "n" +Sleep 500ms +Enter +Sleep 1500ms +# parent_content_type (choice — default Folder). Choice order: +# Document, File, Image, News Item, Event, Link, Folder, Collection, +# Project, . Navigate 2 x Down from Folder → Project. +Down +Sleep 800ms +Down +Sleep 800ms +Enter +Sleep 1500ms +# (filter_content_types is skipped — content_type_base != Container) +# activate_default_behaviors (bool, default yes) +Enter +Sleep 1500ms +Sleep 10s +Sleep 5s + +# ---- controlpanel (3 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 10 — add controlpanel \033[0m\n"` Enter +Sleep 2s +Type "plonecli add contr" +Tab +Sleep 600ms +Enter +Sleep 3s +# controlpanel_name +Enter +Sleep 1500ms +# controlpanel_title +Enter +Sleep 1500ms +# controlpanel_description +Enter +Sleep 8s +Sleep 5s + +# ---- indexer (2 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 11 — add indexer \033[0m\n"` Enter +Sleep 2s +Type "plonecli add in" +Tab +Sleep 600ms +Enter +Sleep 3s +# indexer_name +Enter +Sleep 1500ms +# indexer_description +Enter +Sleep 8s +Sleep 5s + +# ---- site_initialization (2 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 12 — add site_initialization \033[0m\n"` Enter +Sleep 2s +Type "plonecli add si" +Tab +Sleep 600ms +Enter +Sleep 3s +# site_name +Enter +Sleep 1500ms +# language +Enter +Sleep 8s +Sleep 5s + +# ---- subscriber (4 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 13 — add subscriber \033[0m\n"` Enter +Sleep 2s +Type "plonecli add su" +Tab +Sleep 600ms +Enter +Sleep 3s +# subscriber_handler_name +Enter +Sleep 1500ms +# subscriber_event +Enter +Sleep 1500ms +# subscriber_for +Enter +Sleep 1500ms +# subscriber_description +Enter +Sleep 8s +Sleep 5s + +# ---- upgrade_step (4 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 14 — add upgrade_step \033[0m\n"` Enter +Sleep 2s +Type "plonecli add up" +Tab +Sleep 600ms +Enter +Sleep 3s +# upgrade_step_title (required) +Type "Add catalog index" +Sleep 500ms +Enter +Sleep 1500ms +# upgrade_step_description +Enter +Sleep 1500ms +# source_version +Enter +Sleep 1500ms +# destination_version +Enter +Sleep 8s +Sleep 5s + +# ---- vocabulary (3 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 15 — add vocabulary \033[0m\n"` Enter +Sleep 2s +Type "plonecli add vo" +Tab +Sleep 600ms +Enter +Sleep 3s +# vocabulary_name +Enter +Sleep 1500ms +# vocabulary_description +Enter +Sleep 1500ms +# vocabulary_type (choice simple/catalog — accept simple default) +Enter +Sleep 8s +Sleep 5s + +# ---- zope-setup: FileStorage (instance) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 16 — plonecli setup with FileStorage (instance) \033[0m\n"` Enter +Sleep 2s +Type "plonecli setup" Enter +Sleep 3s +# plone_version (choice) +Enter +Sleep 1500ms +# distribution (choice, default plone.volto) +Enter +Sleep 1500ms +# base_path +Enter +Sleep 1500ms +# db_storage (choice — 'instance' is default; demo arrow-nav and pick instance) +Down +Sleep 800ms +Down +Sleep 800ms +Up +Sleep 800ms +Up +Sleep 800ms +Enter +Sleep 1500ms +# author_name +Enter +Sleep 1500ms +# author_email +Enter +Sleep 1500ms +# initial_zope_username +Enter +Sleep 1500ms +# initial_user_password (secret) +Enter +Sleep 20s + +# ---- Outro ---- +Type `echo -e "\n\033[1;46m ▶ STEP 17 — resulting project layout \033[0m\n"` Enter +Sleep 2s +Type "ls -la" Enter +Sleep 4s +Type "ls .copier-answers*.yml" Enter +Sleep 4s +Type "ls src var" Enter +Sleep 4s + +Type `echo -e "\n\033[1;42m ✔ Backend demo complete — 7 backend subtemplates + content_type (x2) + FileStorage instance \033[0m\n"` Enter +Sleep 5s diff --git a/docs/demo-backend.webm b/docs/demo-backend.webm new file mode 100644 index 0000000..ae602c3 Binary files /dev/null and b/docs/demo-backend.webm differ diff --git a/docs/demo-classicui.tape b/docs/demo-classicui.tape new file mode 100644 index 0000000..3491080 --- /dev/null +++ b/docs/demo-classicui.tape @@ -0,0 +1,330 @@ +# vhs tape: plonecli demo — CLASSIC UI category +# +# Video 3 of 3. Creates `collective.classicuidemo`, runs all 8 classic-ui +# subtemplates, then zope-setup with RelStorage (PostgreSQL). +# +# This tape DRIVES every copier prompt — arrow keys for choices, y/n +# for booleans, text for inputs. Nothing waits on a human. +# +# Render: vhs docs/demo-classicui.tape → docs/demo-classicui.gif + +Output docs/demo-classicui.gif + +Set Shell "bash" +Set FontSize 16 +Set FontFamily "JetBrains Mono" +Set Width 1440 +Set Height 900 +Set Theme "Dracula" +Set TypingSpeed 80ms +Set PlaybackSpeed 1.0 + +# ---- Pre-demo (hidden) ---- +Hide +Type `eval "$(_PLONECLI_COMPLETE=bash_source plonecli)"` Enter +Type `mkdir -p /tmp/plonecli-demo && cd /tmp/plonecli-demo && rm -rf collective.classicuidemo` Enter +Type "clear" Enter +Show +Sleep 1s + +# ---- Intro banner ---- +Type `echo -e "\n\033[1;45m ━━━ plonecli Demo 3 of 3 — CLASSIC UI templates ━━━ \033[0m\n"` Enter +Sleep 3s + +# ---- Common intro: version / list / help / completion ---- +Type `echo -e "\n\033[1;46m ▶ STEP 1 — plonecli version \033[0m\n"` Enter +Sleep 2s +Type "plonecli -V" Enter +Sleep 5s + +Type `echo -e "\n\033[1;46m ▶ STEP 2 — list available templates \033[0m\n"` Enter +Sleep 2s +Type "plonecli -l" Enter +Sleep 5s + +Type `echo -e "\n\033[1;46m ▶ STEP 3 — top-level help \033[0m\n"` Enter +Sleep 2s +Type "plonecli --help" Enter +Sleep 5s + +Type `echo -e "\n\033[1;46m ▶ STEP 4 — tab-complete top-level commands \033[0m\n"` Enter +Sleep 2s +Type "plonecli " +Sleep 400ms +Tab Tab +Sleep 3s +Ctrl+C +Sleep 1s + +Type `echo -e "\n\033[1;46m ▶ STEP 5 — tab-complete main templates for 'create' \033[0m\n"` Enter +Sleep 2s +Type "plonecli create " +Sleep 400ms +Tab Tab +Sleep 3s +Type "add" +Tab +Sleep 1500ms +Ctrl+C +Sleep 1s + +# ---- Create the addon (drive 7 backend_addon prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 6 — create collective.classicuidemo addon \033[0m\n"` Enter +Sleep 2s +Type "plonecli create addon collective.classicuidemo" Enter +Sleep 3s +# package_name +Enter +Sleep 1500ms +# package_title +Enter +Sleep 1500ms +# package_description +Enter +Sleep 1500ms +# plone_version (choice — demo arrow nav) +Down +Sleep 800ms +Up +Sleep 800ms +Enter +Sleep 1500ms +# is_headless (bool, default no) +Enter +Sleep 1500ms +# author_name +Enter +Sleep 1500ms +# author_email +Enter +Sleep 20s + +Type "cd collective.classicuidemo" Enter +Sleep 2s + +# ---- form (4 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 7 — add form \033[0m\n"` Enter +Sleep 2s +Type "plonecli add fo" +Tab +Sleep 600ms +Enter +Sleep 3s +# form_name +Enter +Sleep 1500ms +# form_class_name +Enter +Sleep 1500ms +# form_for +Enter +Sleep 1500ms +# form_description +Enter +Sleep 8s +Sleep 5s + +# ---- mockup_pattern (2 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 8 — add mockup_pattern \033[0m\n"` Enter +Sleep 2s +Type "plonecli add mo" +Tab +Sleep 600ms +Enter +Sleep 3s +# pattern_name +Enter +Sleep 1500ms +# pattern_description +Enter +Sleep 8s +Sleep 5s + +# ---- portlet (2 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 9 — add portlet \033[0m\n"` Enter +Sleep 2s +Type "plonecli add po" +Tab +Sleep 600ms +Enter +Sleep 3s +# portlet_name +Enter +Sleep 1500ms +# portlet_description +Enter +Sleep 8s +Sleep 5s + +# ---- theme (2 prompts) ---- +# 'theme' alone is ambiguous with theme_barceloneta/theme_basic — Tab Tab +# lists all three, then we disambiguate by backspacing extras and pressing Enter. +Type `echo -e "\n\033[1;46m ▶ STEP 10 — add theme \033[0m\n"` Enter +Sleep 2s +Type "plonecli add theme" +Sleep 400ms +Tab Tab +Sleep 2s +Type " " +Sleep 400ms +Enter +Sleep 3s +# theme_name +Enter +Sleep 1500ms +# theme_description +Enter +Sleep 8s +Sleep 5s + +# ---- theme_barceloneta (2 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 11 — add theme_barceloneta \033[0m\n"` Enter +Sleep 2s +Type "plonecli add theme_bar" +Tab +Sleep 600ms +Enter +Sleep 3s +# theme_name +Enter +Sleep 1500ms +# theme_description +Enter +Sleep 8s +Sleep 5s + +# ---- theme_basic (2 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 12 — add theme_basic \033[0m\n"` Enter +Sleep 2s +Type "plonecli add theme_bas" +Tab +Sleep 600ms +Enter +Sleep 3s +# theme_name +Enter +Sleep 1500ms +# theme_description +Enter +Sleep 8s +Sleep 5s + +# ---- view (6 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 13 — add view \033[0m\n"` Enter +Sleep 2s +Type "plonecli add vie" +Tab +Sleep 600ms +Enter +Sleep 3s +# view_name +Enter +Sleep 1500ms +# view_class_name +Enter +Sleep 1500ms +# view_base_class (choice) +Enter +Sleep 1500ms +# view_template (bool, default yes) +Enter +Sleep 1500ms +# view_for (choice) +Enter +Sleep 1500ms +# view_description +Enter +Sleep 8s +Sleep 5s + +# ---- viewlet (6 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 14 — add viewlet \033[0m\n"` Enter +Sleep 2s +Type "plonecli add viewl" +Tab +Sleep 600ms +Enter +Sleep 3s +# viewlet_name +Enter +Sleep 1500ms +# viewlet_class_name +Enter +Sleep 1500ms +# viewlet_manager (choice — accept plone.portalheader default) +Enter +Sleep 1500ms +# viewlet_for +Enter +Sleep 1500ms +# viewlet_template (bool, default yes) +Enter +Sleep 1500ms +# viewlet_description +Enter +Sleep 8s +Sleep 5s + +# ---- zope-setup: RelStorage (PostgreSQL) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 15 — plonecli setup with RelStorage (PostgreSQL) \033[0m\n"` Enter +Sleep 2s +Type "plonecli setup" Enter +Sleep 3s +# plone_version +Enter +Sleep 1500ms +# distribution — pick plone.classicui (Down) +Down +Sleep 800ms +Enter +Sleep 1500ms +# base_path +Enter +Sleep 1500ms +# db_storage — choices are [instance, relstorage, zeo]; pick 'relstorage' (1 x Down) +Down +Sleep 800ms +Enter +Sleep 1500ms +# pg_host +Enter +Sleep 1500ms +# pg_port +Enter +Sleep 1500ms +# pg_dbname (default plone_collective_classicuidemo) +Enter +Sleep 1500ms +# pg_user +Enter +Sleep 1500ms +# pg_password (secret) +Type "secret" +Sleep 500ms +Enter +Sleep 1500ms +# author_name +Enter +Sleep 1500ms +# author_email +Enter +Sleep 1500ms +# initial_zope_username +Enter +Sleep 1500ms +# initial_user_password +Enter +Sleep 20s + +# ---- Outro ---- +Type `echo -e "\n\033[1;46m ▶ STEP 16 — resulting project layout \033[0m\n"` Enter +Sleep 2s +Type "ls -la" Enter +Sleep 4s +Type "ls .copier-answers*.yml" Enter +Sleep 4s +Type "ls src var" Enter +Sleep 4s + +Type `echo -e "\n\033[1;42m ✔ Classic UI demo complete — 8 classic-ui subtemplates + RelStorage (PostgreSQL) \033[0m\n"` Enter +Sleep 5s diff --git a/docs/demo-recording-prompt.md b/docs/demo-recording-prompt.md new file mode 100644 index 0000000..2cfa758 --- /dev/null +++ b/docs/demo-recording-prompt.md @@ -0,0 +1,232 @@ +# Record plonecli screencasts — instructions for the demo driver + +Three short, focused screencasts — one per template category — instead of one +long monolithic video. Each video is **a one-shot scripted sequence**: the tape +driver types every command *and* answers every copier prompt (Enter, arrow +keys, y/n, typed strings). Nothing waits for a human to confirm anything, but +the visual experience is fully interactive: the viewer sees copier's prompts +render, the cursor move in choice lists, and answers get entered at reading +speed. + +## Pacing + +- **Between plonecli commands:** `Sleep 5s`. This is the only idle gap — gives + the viewer time to read the finished output before the next headline. +- **Inside a copier prompt sequence:** ~1.5 s between keypresses. Fast enough + to keep the video moving, slow enough that a viewer can read each prompt + and see the answer land. +- **After sending a command (`Enter`) and before the first prompt answer:** + `Sleep 2s` so the first copier prompt has time to render before we start + driving it. +- **Typing speed:** ~80 ms/char. + +## Tooling & setup (all three videos) + +- Recorder: [`vhs`](https://github.com/charmbracelet/vhs) tape file. +- Terminal: 120 × 36, dark theme, JetBrains Mono 16 pt. +- Shell: `bash` with plonecli completion active: + `eval "$(_PLONECLI_COMPLETE=bash_source plonecli)"` +- Scratch cwd: `/tmp/plonecli-demo` (each tape wipes its own addon dir). +- Headline banner between each step: + + ```bash + echo -e "\n\033[1;46m ▶ STEP $N — $DESCRIPTION \033[0m\n"; sleep 2 + ``` + +- For every `create` / `add`: type a **partial token**, press `` (or + `` to list), pause ~1 s, then continue. + +## Template categories + +### Backend (8) — pure server-side building blocks + +`behavior`, `content_type` (shown twice — see scenario below), `controlpanel`, +`indexer`, `site_initialization`, `subscriber`, `upgrade_step`, `vocabulary` + +### REST API (2) — headless API + a frontend that consumes it + +`restapi_service`, `svelte_app` + +### Classic UI (8) — views, themes, portlets, forms + +`form`, `mockup_pattern`, `portlet`, `theme`, `theme_barceloneta`, +`theme_basic`, `view`, `viewlet` + +## Video matrix + +| # | Video | Addon package | `zope-setup` storage | +|---|--------------|------------------------------|-------------------------------| +| 1 | Backend | `collective.backenddemo` | FileStorage (`instance`) | +| 2 | REST API | `collective.restapidemo` | ZEO | +| 3 | Classic UI | `collective.classicuidemo` | RelStorage (PostgreSQL) | + +## Common intro (every video) + +1. `plonecli -V` +2. `plonecli -l` +3. `plonecli --help` +4. `plonecli ` — list top-level commands, abort with `` +5. `plonecli create ` — list main templates, then `add` + completes to `addon`, abort with `` +6. `plonecli create addon ` — create the video's addon + (answer the ~8 `backend_addon` prompts with defaults) + +## Common outro (every video) + +- `ls -la` +- `ls .copier-answers*.yml` +- `ls src var` +- Closing banner naming the category and the storage backend used. + +## Content-type scenario (Backend video only) + +`content_type` is demonstrated **twice in a row** to showcase copier's +choice UI and the interplay between `content_type_base`, `global_allow` +and `parent_content_type`. + +### First `plonecli add content_type` — a Container, globally addable + +Answers: + +| Prompt | UI kind | Answer | +|--------------------------------|-------------|-----------------------------| +| `content_type_name` | text | `Project` | +| `content_type_description` | text | `A project folder` | +| `content_type_base` | **choice** | **`Container`** (default — demonstrate arrow-key navigation even when accepting default: `Up` / `Down` / `Up` / `Enter`) | +| `content_type_icon` | text | `folder` | +| `global_allow` | **bool** | **`yes`** (default) | +| `filter_content_types` | bool | `yes` | +| `activate_default_behaviors` | bool | `yes` | +| `enable_dublin_core` | bool | `yes` | +| `enable_navigation` | bool | `yes` | + +Note: `parent_content_type` is **skipped** here (it's gated on +`global_allow == false`). + +### Second `plonecli add content_type` — an Item, not globally addable, parented on the first CT + +Answers: + +| Prompt | UI kind | Answer | +|--------------------------------|-------------|------------------------------------------------------------| +| `content_type_name` | text | `Task` | +| `content_type_description` | text | `A task inside a Project` | +| `content_type_base` | **choice** | **`Item`** (arrow `Down` then `Enter`) | +| `content_type_icon` | text | `check2-square` | +| `global_allow` | **bool** | **`no`** — opposite of the first CT | +| `parent_content_type` | **choice** | **`Project`** — the CT we just created must appear in the list; navigate to it and `Enter` | +| `activate_default_behaviors` | bool | `yes` | +| `enable_dublin_core` | bool | `yes` | +| `enable_navigation` | bool | `no` — demonstrate a `n`/`Enter` answer | + +This second run is what the user explicitly asked to see: **Item vs Container, +both `globally_addable` values, and picking the parent CT from copier's choice +list.** + +## Copier UI widgets demonstrated + +- **Text input** — almost every prompt. +- **Choice (single-select)** — copier renders with arrow navigation: + - `content_type_base` (Container / Item) — shown twice, different answers + - `parent_content_type` (Folder / Document / … / Project) — shown once + - `plone_version` — shown during every `create addon` and `setup` + - `distribution` (plone.volto / plone.classicui) — shown during `setup` + - `db_storage` (instance / zeo / relstorage) — shown during `setup`, + answered differently per video +- **Bool (yes/no)** — many prompts. Demonstrate both `yes` and `no` answers + across the content_type scenario and `enable_navigation`. + +**Multi-select / multichoice is *not* available** in the current +`copier-templates` — none of the 20 template `copier.yml` files define a +`multiselect: true` question. If showing copier's multichoice widget is a +must-have, we need to add such a prompt to a template first (e.g., a +`behaviors:` multi-select on `content_type`); otherwise, drop that goal from +the script. + +## Video 1 — Backend (FileStorage) + +Steps: + +1. Common intro → create `collective.backenddemo`. +2. `cd collective.backenddemo` +3. `plonecli add behavior` (defaults) +4. `plonecli add content_type` — **Container scenario** above +5. `plonecli add content_type` — **Item + parent scenario** above +6. `plonecli add controlpanel` (defaults) +7. `plonecli add indexer` (defaults) +8. `plonecli add site_initialization` (defaults) +9. `plonecli add subscriber` (defaults) +10. `plonecli add upgrade_step` (defaults) +11. `plonecli add vocabulary` (defaults) +12. `plonecli setup` — choose **`instance`** at `db_storage`, defaults for the rest. +13. Common outro. + +## Video 2 — REST API (ZEO) + +Steps: + +1. Common intro → create `collective.restapidemo`. +2. `cd collective.restapidemo` +3. `plonecli add restapi_service` (defaults — many prompts, shows the + service name / route / view class questions). +4. `plonecli add svelte_app` (defaults — demonstrate a front-end that will + consume the REST API). +5. `plonecli setup` — choose **`zeo`** at `db_storage`, accept + `zeo_address=localhost:8100`, defaults elsewhere. +6. Common outro. + +## Video 3 — Classic UI (RelStorage / PostgreSQL) + +Steps: + +1. Common intro → create `collective.classicuidemo`. +2. `cd collective.classicuidemo` +3. `plonecli add form` (defaults) +4. `plonecli add mockup_pattern` (defaults) +5. `plonecli add portlet` (defaults) +6. `plonecli add theme` (defaults) +7. `plonecli add theme_barceloneta` (defaults) +8. `plonecli add theme_basic` (defaults) +9. `plonecli add view` (defaults) +10. `plonecli add viewlet` (defaults) +11. `plonecli setup` — choose **`relstorage`** at `db_storage`, answer + PostgreSQL prompts (`pg_host=localhost`, `pg_port=5432`, + `pg_dbname=plone_classicuidemo`, `pg_user=plone`, `pg_password=secret`). +12. Common outro. + +## Prompt counts (for sizing Enter sequences) + +Visible prompts per template (measured from the current copier-templates — +re-check with `awk` on `copier.yml` if templates change): + +| Template | Visible prompts | +|-----------------------|-----------------| +| backend_addon | 8 | +| behavior | 7 | +| content_type | 14 (fewer shown at runtime because of conditional `when:` gates — Container run skips `parent_content_type*`, Item run skips `filter_content_types`) | +| controlpanel | 6 | +| indexer | 5 | +| site_initialization | 5 | +| subscriber | 7 | +| upgrade_step | 7 | +| vocabulary | 6 | +| restapi_service | 11 | +| svelte_app | 6 | +| form | 7 | +| mockup_pattern | 5 | +| portlet | 5 | +| theme | 5 | +| theme_barceloneta | 5 | +| theme_basic | 5 | +| view | 11 | +| viewlet | 9 | +| zope-setup | up to 18 (conditional on `db_storage`; FileStorage ≈ 10, ZEO ≈ 11, RelStorage ≈ 15) | + +Use these as the baseline for how many answer keystrokes each command block +sends. Plus one or two buffer Enters — extra Enters at a shell prompt are +harmless newlines. + +## Failure policy + +If any step errors, **stop recording** and surface the error. Do not paper +over failures with retries — the viewer must see only successful output. diff --git a/docs/demo-restapi.tape b/docs/demo-restapi.tape new file mode 100644 index 0000000..3d046df --- /dev/null +++ b/docs/demo-restapi.tape @@ -0,0 +1,212 @@ +# vhs tape: plonecli demo — REST API category +# +# Video 2 of 3. Creates `collective.restapidemo`, runs the two REST API +# subtemplates (restapi_service, svelte_app), then zope-setup with ZEO. +# +# This tape DRIVES every copier prompt — arrow keys for choices, y/n +# for booleans, text for inputs. Nothing waits on a human. +# +# Render: vhs docs/demo-restapi.tape → docs/demo-restapi.gif + +Output docs/demo-restapi.gif + +Set Shell "bash" +Set FontSize 16 +Set FontFamily "JetBrains Mono" +Set Width 1440 +Set Height 900 +Set Theme "Dracula" +Set TypingSpeed 80ms +Set PlaybackSpeed 1.0 + +# ---- Pre-demo (hidden) ---- +Hide +Type `eval "$(_PLONECLI_COMPLETE=bash_source plonecli)"` Enter +Type `mkdir -p /tmp/plonecli-demo && cd /tmp/plonecli-demo && rm -rf collective.restapidemo` Enter +Type "clear" Enter +Show +Sleep 1s + +# ---- Intro banner ---- +Type `echo -e "\n\033[1;45m ━━━ plonecli Demo 2 of 3 — REST API templates ━━━ \033[0m\n"` Enter +Sleep 3s + +# ---- Common intro: version / list / help / completion ---- +Type `echo -e "\n\033[1;46m ▶ STEP 1 — plonecli version \033[0m\n"` Enter +Sleep 2s +Type "plonecli -V" Enter +Sleep 5s + +Type `echo -e "\n\033[1;46m ▶ STEP 2 — list available templates \033[0m\n"` Enter +Sleep 2s +Type "plonecli -l" Enter +Sleep 5s + +Type `echo -e "\n\033[1;46m ▶ STEP 3 — top-level help \033[0m\n"` Enter +Sleep 2s +Type "plonecli --help" Enter +Sleep 5s + +Type `echo -e "\n\033[1;46m ▶ STEP 4 — tab-complete top-level commands \033[0m\n"` Enter +Sleep 2s +Type "plonecli " +Sleep 400ms +Tab Tab +Sleep 3s +Ctrl+C +Sleep 1s + +Type `echo -e "\n\033[1;46m ▶ STEP 5 — tab-complete main templates for 'create' \033[0m\n"` Enter +Sleep 2s +Type "plonecli create " +Sleep 400ms +Tab Tab +Sleep 3s +Type "add" +Tab +Sleep 1500ms +Ctrl+C +Sleep 1s + +# ---- Create the addon (drive 7 backend_addon prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 6 — create collective.restapidemo addon \033[0m\n"` Enter +Sleep 2s +Type "plonecli create addon collective.restapidemo" Enter +Sleep 3s +# package_name +Enter +Sleep 1500ms +# package_title +Enter +Sleep 1500ms +# package_description +Enter +Sleep 1500ms +# plone_version (choice — demo arrow nav) +Down +Sleep 800ms +Up +Sleep 800ms +Enter +Sleep 1500ms +# is_headless (bool, default no) +Enter +Sleep 1500ms +# author_name +Enter +Sleep 1500ms +# author_email +Enter +Sleep 20s + +Type "cd collective.restapidemo" Enter +Sleep 2s + +# ---- restapi_service (8 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 7 — add restapi_service \033[0m\n"` Enter +Sleep 2s +Type "plonecli add re" +Tab +Sleep 600ms +Enter +Sleep 3s +# service_name (required) +Type "stats" +Sleep 500ms +Enter +Sleep 1500ms +# service_description +Enter +Sleep 1500ms +# expandable (bool, default no) +Enter +Sleep 1500ms +# http_get (bool, default yes) +Enter +Sleep 1500ms +# http_post (bool, default no) +Enter +Sleep 1500ms +# http_patch (bool, default no) +Enter +Sleep 1500ms +# http_delete (bool, default no) +Enter +Sleep 1500ms +# service_for (choice — demo arrow nav, accept default) +Down +Sleep 800ms +Up +Sleep 800ms +Enter +Sleep 10s +Sleep 5s + +# ---- svelte_app (3 prompts) ---- +Type `echo -e "\n\033[1;46m ▶ STEP 8 — add svelte_app (frontend that consumes the REST API) \033[0m\n"` Enter +Sleep 2s +Type "plonecli add sv" +Tab +Sleep 600ms +Enter +Sleep 3s +# svelte_app_name +Enter +Sleep 1500ms +# svelte_app_description +Enter +Sleep 1500ms +# svelte_app_custom_element (bool, default no) +Enter +Sleep 15s +Sleep 5s + +# ---- zope-setup: ZEO ---- +Type `echo -e "\n\033[1;46m ▶ STEP 9 — plonecli setup with ZEO \033[0m\n"` Enter +Sleep 2s +Type "plonecli setup" Enter +Sleep 3s +# plone_version +Enter +Sleep 1500ms +# distribution (default plone.volto — REST API / headless path) +Enter +Sleep 1500ms +# base_path +Enter +Sleep 1500ms +# db_storage — choices are [instance, relstorage, zeo]; pick 'zeo' (2 x Down) +Down +Sleep 800ms +Down +Sleep 800ms +Enter +Sleep 1500ms +# zeo_address (default localhost:8100) +Enter +Sleep 1500ms +# author_name +Enter +Sleep 1500ms +# author_email +Enter +Sleep 1500ms +# initial_zope_username +Enter +Sleep 1500ms +# initial_user_password +Enter +Sleep 20s + +# ---- Outro ---- +Type `echo -e "\n\033[1;46m ▶ STEP 10 — resulting project layout \033[0m\n"` Enter +Sleep 2s +Type "ls -la" Enter +Sleep 4s +Type "ls .copier-answers*.yml" Enter +Sleep 4s +Type "ls src var" Enter +Sleep 4s + +Type `echo -e "\n\033[1;42m ✔ REST API demo complete — restapi_service + svelte_app + ZEO instance \033[0m\n"` Enter +Sleep 5s diff --git a/plonecli/cli.py b/plonecli/cli.py index b4a0752..2437422 100644 --- a/plonecli/cli.py +++ b/plonecli/cli.py @@ -44,7 +44,7 @@ class ClickFilteredAliasedGroup(ClickAliasedGroup): def list_commands(self, ctx): existing_cmds = super().list_commands(ctx) project = find_project_root() - global_cmds = ["completion", "create", "config", "update"] + global_cmds = ["completion", "create", "config", "update", "skill"] global_only_cmds = ["create"] if not project: cmds = [cmd for cmd in existing_cmds if cmd in global_cmds] @@ -315,6 +315,67 @@ def update(context): echo(f"\nTemplates: {get_templates_info(config)}", fg="green") +@cli.command("skill") +@click.argument("action", type=click.Choice(["install", "update", "status"])) +@click.option( + "--scope", + type=click.Choice(["project", "user"]), + default="project", + help="Install for this project (default) or globally for the current user.", +) +@click.option( + "--copy", + "copy_only", + is_flag=True, + help="Copy files for the .claude alias instead of symlinking.", +) +@click.option("--force", is_flag=True, help="Overwrite an existing installation.") +@click.pass_context +def skill(context, action, scope, copy_only, force): + """Install/update the plonecli Agent Skill for AI coding agents. + + Drops the bundled SKILL.md (Agent Skills open standard) into + `.agents/skills/plonecli` and links it from `.claude/skills/plonecli`, so + Claude Code, Codex, Gemini CLI, Cursor and other compatible agents pick it + up. Use --scope user to install into your home directory instead. + """ + from plonecli import skill_installer + + project = context.obj.get("project") + project_root = project.root_folder if project else None + + if action == "status": + info = skill_installer.skill_status(scope, project_root) + echo(f"\nplonecli skill ({scope} scope)", fg="green", reverse=True) + echo(f" base: {info['base']}") + echo(f" source: {info['source']}") + for key in ("agents", "claude"): + path, state = info[key] + echo(f" {path}: {state}") + return + + try: + base, actions = skill_installer.install_skill( + scope=scope, + project_root=project_root, + copy_only=copy_only, + force=force, + update=(action == "update"), + ) + except FileExistsError as e: + raise click.UsageError(str(e)) from e + except FileNotFoundError as e: # pragma: no cover - packaging guard + raise click.ClickException(str(e)) from e + + verb = "Updated" if action == "update" else "Installed" + echo(f"\n{verb} plonecli skill under {base}", fg="green", reverse=True) + for act in actions: + if act.kind == "symlink": + echo(f" symlink {act.target} -> {act.points_to}") + else: + echo(f" copied {act.target}") + + @cli.command() @click.argument( "shell", diff --git a/plonecli/skill_installer.py b/plonecli/skill_installer.py new file mode 100644 index 0000000..ddf11c8 --- /dev/null +++ b/plonecli/skill_installer.py @@ -0,0 +1,122 @@ +"""Install the bundled plonecli Agent Skill into a project or user config. + +The skill ships inside this package at ``plonecli/skills/plonecli`` and follows +the Agent Skills open standard, so the exact same ``SKILL.md`` is loaded by +Claude Code, Codex, Gemini CLI, Cursor and other compatible agents. + +Installation places one real copy at ``/.agents/skills/plonecli`` (the +open-standard discovery path) and exposes it to Claude Code via a relative +symlink at ``/.claude/skills/plonecli``. ``base`` is the project root for +``project`` scope or the user's home for ``user`` scope. +""" + +from __future__ import annotations + +import os +import shutil +from dataclasses import dataclass +from pathlib import Path + +SKILL_NAME = "plonecli" +AGENTS_REL = Path(".agents") / "skills" / SKILL_NAME +CLAUDE_REL = Path(".claude") / "skills" / SKILL_NAME + + +@dataclass +class Action: + kind: str # "copy" | "symlink" + target: Path + points_to: str | None = None + + +def get_source_skill_dir() -> Path: + """Path to the skill bundled inside the installed plonecli package.""" + return Path(__file__).resolve().parent / "skills" / SKILL_NAME + + +def resolve_base(scope: str, project_root: Path | None) -> Path: + """Resolve the base directory the skill is installed under.""" + if scope == "user": + return Path.home() + if project_root is not None: + return Path(project_root) + return Path.cwd() + + +def _remove_existing(path: Path) -> None: + if path.is_symlink() or path.is_file(): + path.unlink() + elif path.is_dir(): + shutil.rmtree(path) + + +def _copy_tree(source: Path, target: Path) -> Action: + _remove_existing(target) + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(source, target) + return Action("copy", target) + + +def _link_or_copy(source: Path, target: Path, copy_only: bool) -> Action: + """Make ``target`` a relative symlink to ``source``, falling back to a copy.""" + _remove_existing(target) + target.parent.mkdir(parents=True, exist_ok=True) + if copy_only or os.name == "nt": + return _copy_tree(source, target) + rel = os.path.relpath(source, target.parent) + try: + target.symlink_to(rel, target_is_directory=True) + return Action("symlink", target, rel) + except OSError: + return _copy_tree(source, target) + + +def install_skill( + scope: str = "project", + project_root: Path | None = None, + copy_only: bool = False, + force: bool = False, + update: bool = False, +) -> tuple[Path, list[Action]]: + """Install or refresh the skill under the resolved base directory. + + Returns the base dir and the list of filesystem actions performed. + """ + source = get_source_skill_dir() + if not source.is_dir(): + raise FileNotFoundError(f"Bundled skill not found at {source}") + + base = resolve_base(scope, project_root) + agents_target = base / AGENTS_REL + claude_target = base / CLAUDE_REL + + if agents_target.exists() and not (force or update): + raise FileExistsError( + f"Skill already installed at {agents_target}. " + "Run 'plonecli skill update' or pass --force to overwrite." + ) + + actions = [_copy_tree(source, agents_target)] + actions.append(_link_or_copy(agents_target, claude_target, copy_only)) + return base, actions + + +def skill_status(scope: str, project_root: Path | None) -> dict: + """Report where the skill is installed under the resolved base.""" + base = resolve_base(scope, project_root) + agents_target = base / AGENTS_REL + claude_target = base / CLAUDE_REL + + def describe(path: Path) -> str: + if path.is_symlink(): + return f"symlink -> {os.readlink(path)}" + if path.is_dir(): + return "copy" + return "not installed" + + return { + "base": base, + "source": get_source_skill_dir(), + "agents": (agents_target, describe(agents_target)), + "claude": (claude_target, describe(claude_target)), + } diff --git a/plonecli/skills/plonecli/SKILL.md b/plonecli/skills/plonecli/SKILL.md new file mode 100644 index 0000000..2918e44 --- /dev/null +++ b/plonecli/skills/plonecli/SKILL.md @@ -0,0 +1,74 @@ +--- +name: plonecli +description: Scaffold and develop Plone packages with plonecli (copier-template based). Use when creating a Plone backend add-on or Zope project, adding features like content types, behaviors, or REST API services, running/testing a Plone instance, or updating/reconfiguring a plonecli-generated project. Triggers on "plonecli", "create a Plone addon", "add a content type/behavior/restapi service", "plone scaffold", "zope-setup". +--- + +# plonecli + +`plonecli` scaffolds and develops Plone packages using [copier](https://copier.readthedocs.io/) templates. It creates backend add-ons and Zope project setups, adds features (content types, behaviors, REST API services) via subtemplates, and wraps the project's `invoke` tasks for serving and testing. + +## How to invoke it + +- **End users:** `uvx plonecli ` (no install needed). +- **Inside this repo (plonecli's own source):** it is not on `PATH` — use `uv run plonecli `. + +On first run, plonecli clones the copier-templates to `~/.copier-templates/plone-copier-templates`. If a command complains about missing templates, run `plonecli update` first. + +## Command map + +| Command | Scope | What it does | +|---|---|---| +| `create