Skip to content

Commit d6955fd

Browse files
authored
feat(packages): add declarative npm package installation via chezmoi (#5)
1 parent 1c65576 commit d6955fd

File tree

5 files changed

+176
-2
lines changed

5 files changed

+176
-2
lines changed

.chezmoidata/packages.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
# Package declarations for declarative installation
3+
# Packages are installed via run_onchange scripts that execute when this file changes
4+
5+
npm:
6+
global:
7+
- "@anthropic-ai/claude-code"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
# Install npm packages declaratively based on .chezmoidata/packages.yaml
3+
# This script runs when the package list changes
4+
5+
{{ if .include_defaults -}}
6+
set -e
7+
8+
# Function to run npm commands via mise
9+
run_npm() {
10+
if command -v mise >/dev/null 2>&1; then
11+
mise exec -- npm "$@"
12+
else
13+
npm "$@"
14+
fi
15+
}
16+
17+
# Check if npm is available (via mise or directly)
18+
if command -v mise >/dev/null 2>&1; then
19+
if ! mise exec -- npm --version >/dev/null 2>&1; then
20+
echo "⚠️ Node.js/npm not available via mise. Skipping npm package installation."
21+
exit 0
22+
fi
23+
elif ! command -v npm >/dev/null 2>&1; then
24+
echo "⚠️ npm not found. Skipping npm package installation."
25+
exit 0
26+
fi
27+
28+
# Install global npm packages
29+
{{ if .npm.global -}}
30+
{{ range .npm.global -}}
31+
if ! run_npm list -g "{{ . }}" >/dev/null 2>&1; then
32+
echo "📦 Installing {{ . }}..."
33+
run_npm install -g "{{ . }}"
34+
echo "✓ Installed {{ . }}"
35+
else
36+
echo "✓ {{ . }} already installed"
37+
fi
38+
{{ end -}}
39+
{{ end -}}
40+
41+
{{ end -}}

.claude/skills/chezmoi-development/SKILL.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,80 @@ if ! command -v jq &> /dev/null; then
562562
fi
563563
```
564564
565+
## Declarative Package Installation
566+
567+
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.
568+
569+
### Pattern: npm Package Installation
570+
571+
**1. Declare packages in `.chezmoidata/packages.yaml`:**
572+
```yaml
573+
---
574+
# Package declarations for declarative installation
575+
# Top-level keys become template variables (e.g., .npm, .apt, .brew)
576+
npm:
577+
global:
578+
- "@anthropic-ai/claude-code"
579+
- "typescript"
580+
```
581+
582+
**2. Create installation script `.chezmoiscripts/run_onchange_after_install-npm-packages.sh.tmpl`:**
583+
```bash
584+
#!/bin/bash
585+
# Install npm packages declaratively based on .chezmoidata/packages.yaml
586+
# This script runs when the package list changes
587+
588+
{{ if .include_defaults -}}
589+
set -e
590+
591+
# Function to run npm commands via mise
592+
run_npm() {
593+
if command -v mise >/dev/null 2>&1; then
594+
mise exec -- npm "$@"
595+
else
596+
npm "$@"
597+
fi
598+
}
599+
600+
# Check if npm is available (via mise or directly)
601+
if command -v mise >/dev/null 2>&1; then
602+
if ! mise exec -- npm --version >/dev/null 2>&1; then
603+
echo "⚠️ Node.js/npm not available via mise. Skipping npm package installation."
604+
exit 0
605+
fi
606+
elif ! command -v npm >/dev/null 2>&1; then
607+
echo "⚠️ npm not found. Skipping npm package installation."
608+
exit 0
609+
fi
610+
611+
# Install global npm packages
612+
{{ if .npm.global -}}
613+
{{ range .npm.global -}}
614+
if ! run_npm list -g "{{ . }}" >/dev/null 2>&1; then
615+
echo "📦 Installing {{ . }}..."
616+
run_npm install -g "{{ . }}"
617+
echo "✓ Installed {{ . }}"
618+
else
619+
echo "✓ {{ . }} already installed"
620+
fi
621+
{{ end -}}
622+
{{ end -}}
623+
624+
{{ end -}}
625+
```
626+
627+
**How it works:**
628+
- The script template references `.npm.global` from `.chezmoidata/packages.yaml`
629+
- `run_onchange_` prefix means the script executes when its rendered content changes
630+
- When you add/remove packages in `packages.yaml`, the rendered script changes, triggering re-execution
631+
- Each package is checked before installation to avoid redundant installs
632+
- Uses `mise exec` to ensure npm is available from mise-managed Node.js
633+
634+
**Adaptable to other package managers:**
635+
- apt: Create `.chezmoidata/packages.yaml` with `apt: [...]` and use `{{ range .apt }}`
636+
- brew: Create with `brew: [...]` and use `{{ range .brew }}`
637+
- pip: Create with `pip: [...]` and use `{{ range .pip }}`
638+
565639
## Reference Documentation
566640
567641
For a complete, working example of this pattern, see:

test/test-coder-existing.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -e
44

55
# Create existing ~/.claude with user content
66
mkdir -p "$HOME/.claude"
7-
echo "user_settings" > "$HOME/.claude/settings.json"
7+
echo '{"alwaysThinkingEnabled":true,"userSetting":"value"}' > "$HOME/.claude/settings.json"
88
echo "user_custom" > "$HOME/.claude/custom.txt"
99

1010
# Run setup

test/verify-marketplace.sh

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
#!/bin/bash
2-
# Verify marketplace configuration
2+
# Verify marketplace configuration and npm packages
33
set -e
44

5+
# Ensure mise is activated if available
6+
if command -v mise >/dev/null 2>&1; then
7+
export PATH="$HOME/.local/bin:$PATH"
8+
eval "$(mise activate bash 2>/dev/null)" || true
9+
fi
10+
511
if [ ! -f "$HOME/.claude/settings.json" ]; then
612
echo "✗ settings.json not created"
713
exit 1
@@ -18,3 +24,49 @@ if ! grep -q "git@github.com:fx/cc.git" "$HOME/.claude/settings.json"; then
1824
fi
1925

2026
echo "✓ Claude marketplace configured correctly"
27+
28+
# Verify npm package installation
29+
# Helper function to run npm via mise
30+
run_npm() {
31+
if command -v mise >/dev/null 2>&1; then
32+
mise exec -- npm "$@"
33+
else
34+
npm "$@"
35+
fi
36+
}
37+
38+
# Check if npm is available
39+
if command -v mise >/dev/null 2>&1; then
40+
if ! mise exec -- npm --version >/dev/null 2>&1; then
41+
echo "⚠️ npm not available, skipping package verification"
42+
exit 0
43+
fi
44+
elif ! command -v npm >/dev/null 2>&1; then
45+
echo "⚠️ npm not available, skipping package verification"
46+
exit 0
47+
fi
48+
49+
# Parse packages.yaml and verify each npm global package
50+
if command -v yq >/dev/null 2>&1 && [ -f "$HOME/.local/share/chezmoi/.chezmoidata/packages.yaml" ]; then
51+
# Use yq to parse the YAML file
52+
npm_packages=$(yq eval '.npm.global[]' "$HOME/.local/share/chezmoi/.chezmoidata/packages.yaml" 2>/dev/null || echo "")
53+
if [ -n "$npm_packages" ]; then
54+
while IFS= read -r package; do
55+
if [ -n "$package" ]; then
56+
# npm list can exit non-zero due to warnings, so check the output instead
57+
if ! run_npm list -g "$package" 2>&1 | grep -qF "$package@"; then
58+
echo "✗ npm package $package not installed"
59+
exit 1
60+
fi
61+
echo "✓ npm package $package installed correctly"
62+
fi
63+
done <<< "$npm_packages"
64+
fi
65+
else
66+
# Fallback: Check known packages if yq is not available
67+
if ! run_npm list -g @anthropic-ai/claude-code 2>&1 | grep -qF "@anthropic-ai/claude-code@"; then
68+
echo "✗ @anthropic-ai/claude-code not installed"
69+
exit 1
70+
fi
71+
echo "✓ @anthropic-ai/claude-code installed correctly"
72+
fi

0 commit comments

Comments
 (0)