From ddb536ad441335e5c5b5f2d5884422c11ebc196a Mon Sep 17 00:00:00 2001 From: MrTango Date: Fri, 17 Apr 2026 15:29:43 +0200 Subject: [PATCH 01/10] Fix plonecli update failing on diverging upstream branch git pull --ff-only fails when origin has been rebased or force-pushed. Since the copier-templates clone is plonecli-managed, refresh it via fetch + hard-reset to origin/ instead. --- CHANGES.md | 16 ++++++++++++++- plonecli/templates.py | 43 ++++++++++++++++++++++++++++++++--------- tests/test_templates.py | 41 ++++++++++++++++++++++++++++++++------- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0400cd5..6468949 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,21 @@ ## 7.0.0b3 (unreleased) -- Nothing changed yet. +- 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/plonecli/templates.py b/plonecli/templates.py index ec68615..54bb998 100644 --- a/plonecli/templates.py +++ b/plonecli/templates.py @@ -38,28 +38,53 @@ def ensure_templates_cloned(config: PlonecliConfig) -> Path: def update_templates_clone(config: PlonecliConfig) -> str: - """Update the local copier-templates clone via git pull. + """Update the local copier-templates clone. - Returns a status message. + Fetches from origin and hard-resets to the configured branch. The clone is + plonecli-managed, so divergence (e.g. from an upstream rebase or force-push) + is resolved by resetting to ``origin/`` rather than attempting a + merge. """ templates_dir = Path(config.templates_dir) if not templates_dir.exists(): ensure_templates_cloned(config) return "Templates cloned successfully." - result = subprocess.run( - ["git", "pull", "--ff-only"], + branch = config.repo_branch + subprocess.run( + ["git", "fetch", "--depth", "1", "origin", branch], cwd=str(templates_dir), + check=True, capture_output=True, text=True, ) - if result.returncode != 0: - return f"Failed to update templates: {result.stderr.strip()}" - output = result.stdout.strip() - if "Already up to date" in output: + before = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=str(templates_dir), + check=True, + capture_output=True, + text=True, + ).stdout.strip() + after = subprocess.run( + ["git", "rev-parse", f"origin/{branch}"], + cwd=str(templates_dir), + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + if before == after: return "Templates already up to date." - return f"Templates updated: {output}" + + subprocess.run( + ["git", "reset", "--hard", f"origin/{branch}"], + cwd=str(templates_dir), + check=True, + capture_output=True, + text=True, + ) + return f"Templates updated: {before[:7]} → {after[:7]}" def get_template_path(template_name: str, config: PlonecliConfig) -> Path: diff --git a/tests/test_templates.py b/tests/test_templates.py index c550c24..2846620 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -52,22 +52,49 @@ def side_effect(*args, **kwargs): @patch("plonecli.templates.subprocess.run") -def test_update_templates_clone(mock_run, tmp_path): +def test_update_templates_clone_up_to_date(mock_run, tmp_path): templates_dir = tmp_path / "templates" templates_dir.mkdir() (templates_dir / ".git").mkdir() - mock_run.return_value = MagicMock( - returncode=0, - stdout="Already up to date.", - stderr="", - ) + sha = "abc1234abc1234abc1234abc1234abc1234abc1" + mock_run.side_effect = [ + MagicMock(returncode=0, stdout="", stderr=""), # fetch + MagicMock(returncode=0, stdout=sha + "\n", stderr=""), # rev-parse HEAD + MagicMock(returncode=0, stdout=sha + "\n", stderr=""), # rev-parse origin + ] config = PlonecliConfig(templates_dir=str(templates_dir)) msg = update_templates_clone(config) assert "up to date" in msg.lower() - mock_run.assert_called_once() + assert mock_run.call_count == 3 + + +@patch("plonecli.templates.subprocess.run") +def test_update_templates_clone_resets_on_divergence(mock_run, tmp_path): + """When local and origin diverge, hard-reset to origin instead of failing.""" + templates_dir = tmp_path / "templates" + templates_dir.mkdir() + (templates_dir / ".git").mkdir() + + before = "951b15c951b15c951b15c951b15c951b15c951b1" + after = "9e89e5b9e89e5b9e89e5b9e89e5b9e89e5b9e89e" + mock_run.side_effect = [ + MagicMock(returncode=0, stdout="", stderr=""), # fetch + MagicMock(returncode=0, stdout=before + "\n", stderr=""), # HEAD + MagicMock(returncode=0, stdout=after + "\n", stderr=""), # origin/branch + MagicMock(returncode=0, stdout="", stderr=""), # reset --hard + ] + + config = PlonecliConfig(templates_dir=str(templates_dir), repo_branch="main") + msg = update_templates_clone(config) + + assert "updated" in msg.lower() + assert "951b15c" in msg + assert "9e89e5b" in msg + reset_call = mock_run.call_args_list[3][0][0] + assert reset_call == ["git", "reset", "--hard", "origin/main"] def test_get_template_path(tmp_path): From cf6ee2b41409770198ae79f73b0f9669f58c6745 Mon Sep 17 00:00:00 2001 From: MrTango Date: Fri, 17 Apr 2026 15:30:41 +0200 Subject: [PATCH 02/10] Add terminal recording toolchain and plonecli demo tapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Install vhs, agg, ttyd (plus asciinema and ffmpeg) in the devcontainer so demo screencasts can be rendered from scripted tapes. Add three vhs tapes covering the main plonecli template categories — backend, REST API, classic UI — and the recording-driver prompt that documents pacing and prompt-driving conventions. --- .devcontainer/Dockerfile | 22 ++ docs/demo-backend.tape | 398 ++++++++++++++++++++++++++++++++++ docs/demo-classicui.tape | 330 ++++++++++++++++++++++++++++ docs/demo-recording-prompt.md | 232 ++++++++++++++++++++ docs/demo-restapi.tape | 212 ++++++++++++++++++ 5 files changed, 1194 insertions(+) create mode 100644 docs/demo-backend.tape create mode 100644 docs/demo-classicui.tape create mode 100644 docs/demo-recording-prompt.md create mode 100644 docs/demo-restapi.tape 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/docs/demo-backend.tape b/docs/demo-backend.tape new file mode 100644 index 0000000..3b2ead6 --- /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.gif + +Output docs/demo-backend.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.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-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 From 7159af4fd61b9477f0d8da913c58e6f0d57a8b03 Mon Sep 17 00:00:00 2001 From: MrTango Date: Thu, 21 May 2026 12:41:40 +0300 Subject: [PATCH 03/10] Sync devcontainer with updated copier template Re-applies the claude-code-devcontainer template: host Claude config sync in setup-claude.sh, drop OpenCode, split firewall rules for Plone/PyPI and remove defunct statsig/opencode hosts, preserve container on stop for fast restart, and track copier answers (incl. extra_mounts for the live copier-templates bind and terminal-recording toolchain). --- .copier-answers.yml | 12 +++ .devcontainer/devcontainer.json | 7 +- .devcontainer/init-firewall.sh | 27 ++++-- .devcontainer/setup-claude.sh | 150 ++++++++++++++++++++++++++++---- CLAUDE.md | 1 + devcontainer.sh | 6 +- 6 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 .copier-answers.yml diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..1f5878b --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,12 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +_commit: 34bd81d +_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/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/CLAUDE.md b/CLAUDE.md index ff3d135..d50b2e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,3 +2,4 @@ - 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. 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 From bff7c956237b4d3e8373a3d781828ce6915d1c09 Mon Sep 17 00:00:00 2001 From: MrTango Date: Thu, 21 May 2026 12:47:18 +0300 Subject: [PATCH 04/10] Bump copier template ref to 978185b --- .copier-answers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 1f5878b..441e9ce 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -_commit: 34bd81d +_commit: 978185b _src_path: /home/maik/develop/src/copier-claude-code-devcontainer/ enable_docker_in_docker: true enable_plone: true From a2835f3a28f08624fe8c4ec93f698bbbc55741ba Mon Sep 17 00:00:00 2001 From: MrTango Date: Thu, 21 May 2026 03:15:38 -0700 Subject: [PATCH 05/10] Add plonecli skill for scaffolding and developing Plone packages Documents create/add/setup/serve/test/debug/update/config commands, with template/subtemplate gating and reconfigure flows. Reference docs cover project creation, feature subtemplates, and maintenance. Corrects the addon vs backend_addon distinction (addon is a composite of backend_addon + zope-setup, not an alias), notes that the invoke harness (tasks.py) comes from the zope-setup layer, and documents the PLONECLI_TEMPLATES_DIR override for templates discovery. Co-Authored-By: Claude Opus 4.7 --- .claude/skills/plonecli/SKILL.md | 72 +++++++++++++++++++ .claude/skills/plonecli/reference/add.md | 39 ++++++++++ .claude/skills/plonecli/reference/create.md | 46 ++++++++++++ .claude/skills/plonecli/reference/maintain.md | 67 +++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 .claude/skills/plonecli/SKILL.md create mode 100644 .claude/skills/plonecli/reference/add.md create mode 100644 .claude/skills/plonecli/reference/create.md create mode 100644 .claude/skills/plonecli/reference/maintain.md diff --git a/.claude/skills/plonecli/SKILL.md b/.claude/skills/plonecli/SKILL.md new file mode 100644 index 0000000..1ee5ab1 --- /dev/null +++ b/.claude/skills/plonecli/SKILL.md @@ -0,0 +1,72 @@ +--- +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