From 7b975cd55b5ec0bb353ba45a6a3bd2ab9d9002a4 Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 6 Nov 2025 04:58:48 +0000 Subject: [PATCH 1/2] feat(packages): add declarative npm package installation via chezmoi --- .chezmoidata/packages.yaml | 7 ++ ...nchange_after_install-npm-packages.sh.tmpl | 41 ++++++++++ .claude/skills/chezmoi-development/SKILL.md | 74 +++++++++++++++++++ test/test-coder-existing.sh | 2 +- test/verify-marketplace.sh | 54 +++++++++++++- 5 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 .chezmoidata/packages.yaml create mode 100755 .chezmoiscripts/run_onchange_after_install-npm-packages.sh.tmpl diff --git a/.chezmoidata/packages.yaml b/.chezmoidata/packages.yaml new file mode 100644 index 0000000..acca3fa --- /dev/null +++ b/.chezmoidata/packages.yaml @@ -0,0 +1,7 @@ +--- +# Package declarations for declarative installation +# Packages are installed via run_onchange scripts that execute when this file changes + +npm: + global: + - "@anthropic-ai/claude-code" diff --git a/.chezmoiscripts/run_onchange_after_install-npm-packages.sh.tmpl b/.chezmoiscripts/run_onchange_after_install-npm-packages.sh.tmpl new file mode 100755 index 0000000..ee3b45d --- /dev/null +++ b/.chezmoiscripts/run_onchange_after_install-npm-packages.sh.tmpl @@ -0,0 +1,41 @@ +#!/bin/bash +# Install npm packages declaratively based on .chezmoidata/packages.yaml +# This script runs when the package list changes + +{{ if .include_defaults -}} +set -e + +# Function to run npm commands via mise +run_npm() { + if command -v mise >/dev/null 2>&1; then + mise exec -- npm "$@" + else + npm "$@" + fi +} + +# Check if npm is available (via mise or directly) +if command -v mise >/dev/null 2>&1; then + if ! mise exec -- npm --version >/dev/null 2>&1; then + echo "⚠️ Node.js/npm not available via mise. Skipping npm package installation." + exit 0 + fi +elif ! command -v npm >/dev/null 2>&1; then + echo "⚠️ npm not found. Skipping npm package installation." + exit 0 +fi + +# Install global npm packages +{{ if .npm.global -}} +{{ range .npm.global -}} +if ! run_npm list -g "{{ . }}" >/dev/null 2>&1; then + echo "📦 Installing {{ . }}..." + run_npm install -g "{{ . }}" + echo "✓ Installed {{ . }}" +else + echo "✓ {{ . }} already installed" +fi +{{ end -}} +{{ end -}} + +{{ end -}} diff --git a/.claude/skills/chezmoi-development/SKILL.md b/.claude/skills/chezmoi-development/SKILL.md index 1519136..4dd0cf8 100644 --- a/.claude/skills/chezmoi-development/SKILL.md +++ b/.claude/skills/chezmoi-development/SKILL.md @@ -562,6 +562,80 @@ if ! command -v jq &> /dev/null; then fi ``` +## Declarative Package Installation + +Chezmoi can install packages declaratively using a combination of `.chezmoidata/packages.yaml` and `run_onchange_` scripts. This pattern ensures packages are installed when the package list changes. + +### Pattern: npm Package Installation + +**1. Declare packages in `.chezmoidata/packages.yaml`:** +```yaml +--- +# Package declarations for declarative installation +# Top-level keys become template variables (e.g., .npm, .apt, .brew) +npm: + global: + - "@anthropic-ai/claude-code" + - "typescript" +``` + +**2. Create installation script `.chezmoiscripts/run_onchange_after_install-npm-packages.sh.tmpl`:** +```bash +#!/bin/bash +# Install npm packages declaratively based on .chezmoidata/packages.yaml +# This script runs when the package list changes + +{{ if .include_defaults -}} +set -e + +# Function to run npm commands via mise +run_npm() { + if command -v mise >/dev/null 2>&1; then + mise exec -- npm "$@" + else + npm "$@" + fi +} + +# Check if npm is available (via mise or directly) +if command -v mise >/dev/null 2>&1; then + if ! mise exec -- npm --version >/dev/null 2>&1; then + echo "⚠️ Node.js/npm not available via mise. Skipping npm package installation." + exit 0 + fi +elif ! command -v npm >/dev/null 2>&1; then + echo "⚠️ npm not found. Skipping npm package installation." + exit 0 +fi + +# Install global npm packages +{{ if .npm.global -}} +{{ range .npm.global -}} +if ! run_npm list -g "{{ . }}" >/dev/null 2>&1; then + echo "📦 Installing {{ . }}..." + run_npm install -g "{{ . }}" + echo "✓ Installed {{ . }}" +else + echo "✓ {{ . }} already installed" +fi +{{ end -}} +{{ end -}} + +{{ end -}} +``` + +**How it works:** +- The script template references `.npm.global` from `.chezmoidata/packages.yaml` +- `run_onchange_` prefix means the script executes when its rendered content changes +- When you add/remove packages in `packages.yaml`, the rendered script changes, triggering re-execution +- Each package is checked before installation to avoid redundant installs +- Uses `mise exec` to ensure npm is available from mise-managed Node.js + +**Adaptable to other package managers:** +- apt: Create `.chezmoidata/packages.yaml` with `apt: [...]` and use `{{ range .apt }}` +- brew: Create with `brew: [...]` and use `{{ range .brew }}` +- pip: Create with `pip: [...]` and use `{{ range .pip }}` + ## Reference Documentation For a complete, working example of this pattern, see: diff --git a/test/test-coder-existing.sh b/test/test-coder-existing.sh index f72a9fe..cf04e32 100755 --- a/test/test-coder-existing.sh +++ b/test/test-coder-existing.sh @@ -4,7 +4,7 @@ set -e # Create existing ~/.claude with user content mkdir -p "$HOME/.claude" -echo "user_settings" > "$HOME/.claude/settings.json" +echo '{"alwaysThinkingEnabled":true,"userSetting":"value"}' > "$HOME/.claude/settings.json" echo "user_custom" > "$HOME/.claude/custom.txt" # Run setup diff --git a/test/verify-marketplace.sh b/test/verify-marketplace.sh index d2ce88e..4490c5d 100755 --- a/test/verify-marketplace.sh +++ b/test/verify-marketplace.sh @@ -1,7 +1,13 @@ #!/bin/bash -# Verify marketplace configuration +# Verify marketplace configuration and npm packages set -e +# Ensure mise is activated if available +if command -v mise >/dev/null 2>&1; then + export PATH="$HOME/.local/bin:$PATH" + eval "$(mise activate bash 2>/dev/null)" || true +fi + if [ ! -f "$HOME/.claude/settings.json" ]; then echo "✗ settings.json not created" exit 1 @@ -18,3 +24,49 @@ if ! grep -q "git@github.com:fx/cc.git" "$HOME/.claude/settings.json"; then fi echo "✓ Claude marketplace configured correctly" + +# Verify npm package installation +# Helper function to run npm via mise +run_npm() { + if command -v mise >/dev/null 2>&1; then + mise exec -- npm "$@" + else + npm "$@" + fi +} + +# Check if npm is available +if command -v mise >/dev/null 2>&1; then + if ! mise exec -- npm --version >/dev/null 2>&1; then + echo "⚠️ npm not available, skipping package verification" + exit 0 + fi +elif ! command -v npm >/dev/null 2>&1; then + echo "⚠️ npm not available, skipping package verification" + exit 0 +fi + +# Parse packages.yaml and verify each npm global package +if command -v yq >/dev/null 2>&1 && [ -f "$HOME/.local/share/chezmoi/.chezmoidata/packages.yaml" ]; then + # Use yq to parse the YAML file + npm_packages=$(yq eval '.npm.global[]' "$HOME/.local/share/chezmoi/.chezmoidata/packages.yaml" 2>/dev/null || echo "") + if [ -n "$npm_packages" ]; then + while IFS= read -r package; do + if [ -n "$package" ]; then + # npm list can exit non-zero due to warnings, so check the output instead + if ! run_npm list -g "$package" 2>&1 | grep -q "$package@"; then + echo "✗ npm package $package not installed" + exit 1 + fi + echo "✓ npm package $package installed correctly" + fi + done <<< "$npm_packages" + fi +else + # Fallback: Check known packages if yq is not available + if ! run_npm list -g @anthropic-ai/claude-code 2>&1 | grep -q "@anthropic-ai/claude-code@"; then + echo "✗ @anthropic-ai/claude-code not installed" + exit 1 + fi + echo "✓ @anthropic-ai/claude-code installed correctly" +fi From 6223104fd9ee9d8d748e06bf2c3db81221c1b0e9 Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 6 Nov 2025 05:18:47 +0000 Subject: [PATCH 2/2] fix(test): use grep -qF for scoped package verification to handle @ symbols correctly --- test/verify-marketplace.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/verify-marketplace.sh b/test/verify-marketplace.sh index 4490c5d..dfca221 100755 --- a/test/verify-marketplace.sh +++ b/test/verify-marketplace.sh @@ -54,7 +54,7 @@ if command -v yq >/dev/null 2>&1 && [ -f "$HOME/.local/share/chezmoi/.chezmoidat while IFS= read -r package; do if [ -n "$package" ]; then # npm list can exit non-zero due to warnings, so check the output instead - if ! run_npm list -g "$package" 2>&1 | grep -q "$package@"; then + if ! run_npm list -g "$package" 2>&1 | grep -qF "$package@"; then echo "✗ npm package $package not installed" exit 1 fi @@ -64,7 +64,7 @@ if command -v yq >/dev/null 2>&1 && [ -f "$HOME/.local/share/chezmoi/.chezmoidat fi else # Fallback: Check known packages if yq is not available - if ! run_npm list -g @anthropic-ai/claude-code 2>&1 | grep -q "@anthropic-ai/claude-code@"; then + if ! run_npm list -g @anthropic-ai/claude-code 2>&1 | grep -qF "@anthropic-ai/claude-code@"; then echo "✗ @anthropic-ai/claude-code not installed" exit 1 fi