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..dfca221 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 -qF "$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 -qF "@anthropic-ai/claude-code@"; then + echo "✗ @anthropic-ai/claude-code not installed" + exit 1 + fi + echo "✓ @anthropic-ai/claude-code installed correctly" +fi