Skip to content

Commit 1e21261

Browse files
sjnimsclaude
andcommitted
feat(command-development): add scripts/ directory with validation utilities
Add two validation scripts to command-development skill, matching the pattern established by hook-development: - validate-command.sh: Validates command file structure including existence, .md extension, YAML frontmatter syntax, and filename conventions. Supports multiple files and provides colored output with error/warning counts. - check-frontmatter.sh: Validates YAML frontmatter fields including model (shorthand or full ID), description length, allowed-tools format, argument-hint convention, and disable-model-invocation boolean. Warns about unknown fields. Both scripts follow established patterns: - set -euo pipefail for safety - Colored output with ✅/❌/⚠️ indicators - Error and warning counting - Multi-file support - Clear, actionable messages Also updates SKILL.md to document the new scripts in the "Validation Scripts" section. Fixes #59 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent da12c7e commit 1e21261

3 files changed

Lines changed: 410 additions & 0 deletions

File tree

plugins/plugin-dev/skills/command-development/SKILL.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,3 +713,19 @@ For self-documenting command patterns and maintenance docs, see `references/docu
713713
For testing approaches from syntax validation to user acceptance, see `references/testing-strategies.md`.
714714
For distribution guidelines and quality standards, see `references/marketplace-considerations.md`.
715715
For command pattern examples, see `examples/` directory.
716+
717+
## Validation Scripts
718+
719+
Utility scripts for validating commands (execute without loading into context):
720+
721+
```bash
722+
# Validate command file structure
723+
./scripts/validate-command.sh .claude/commands/my-command.md
724+
725+
# Validate YAML frontmatter fields
726+
./scripts/check-frontmatter.sh .claude/commands/my-command.md
727+
728+
# Validate multiple files
729+
./scripts/validate-command.sh commands/*.md
730+
./scripts/check-frontmatter.sh commands/*.md
731+
```
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#!/bin/bash
2+
# Command Frontmatter Validator
3+
# Validates YAML frontmatter fields in command files
4+
5+
set -euo pipefail
6+
7+
# Usage
8+
if [ $# -eq 0 ]; then
9+
echo "Usage: $0 <path/to/command.md> [command2.md ...]"
10+
echo ""
11+
echo "Validates frontmatter fields for:"
12+
echo " - 'model' field (sonnet, opus, haiku, or full model ID)"
13+
echo " - 'description' length (warns if > 60 chars)"
14+
echo " - 'allowed-tools' format"
15+
echo " - 'argument-hint' format"
16+
echo " - 'disable-model-invocation' boolean"
17+
echo " - Unknown fields (warning)"
18+
echo ""
19+
echo "Examples:"
20+
echo " $0 .claude/commands/review.md"
21+
echo " $0 commands/*.md"
22+
exit 1
23+
fi
24+
25+
# Known frontmatter fields for commands
26+
KNOWN_FIELDS="description model allowed-tools argument-hint disable-model-invocation"
27+
28+
total_errors=0
29+
total_warnings=0
30+
31+
check_frontmatter() {
32+
local COMMAND_FILE="$1"
33+
local error_count=0
34+
local warning_count=0
35+
36+
echo "🔍 Checking frontmatter: $COMMAND_FILE"
37+
echo ""
38+
39+
# Check file exists
40+
if [ ! -f "$COMMAND_FILE" ]; then
41+
echo "❌ Error: File not found: $COMMAND_FILE"
42+
return 1
43+
fi
44+
45+
# Check for frontmatter
46+
if ! head -n 1 "$COMMAND_FILE" | grep -q "^---"; then
47+
echo "ℹ️ No frontmatter found (frontmatter is optional)"
48+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
49+
echo "$COMMAND_FILE: No frontmatter to validate"
50+
echo ""
51+
return 0
52+
fi
53+
54+
# Extract frontmatter - only the first block between lines 1 and the second ---
55+
# Use awk to get content between first and second --- markers only
56+
local frontmatter
57+
frontmatter=$(awk '
58+
/^---$/ { count++; if (count == 2) exit; next }
59+
count == 1 { print }
60+
' "$COMMAND_FILE")
61+
62+
if [ -z "$frontmatter" ]; then
63+
echo "⚠️ Warning: Empty frontmatter block"
64+
((warning_count++))
65+
total_warnings=$((total_warnings + warning_count))
66+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
67+
echo "⚠️ $COMMAND_FILE: Passed with $warning_count warning(s)"
68+
echo ""
69+
return 0
70+
fi
71+
72+
echo "Frontmatter found. Validating fields..."
73+
echo ""
74+
75+
# Check 'model' field
76+
if echo "$frontmatter" | grep -q "^model:"; then
77+
local model
78+
model=$(echo "$frontmatter" | grep "^model:" | cut -d: -f2 | tr -d ' ')
79+
80+
# Valid values: sonnet, opus, haiku, or full model ID (claude-*)
81+
if [[ "$model" =~ ^(sonnet|opus|haiku)$ ]]; then
82+
echo "✅ model: $model (shorthand)"
83+
elif [[ "$model" =~ ^claude- ]]; then
84+
echo "✅ model: $model (full model ID)"
85+
else
86+
echo "❌ Error: Invalid model '$model'"
87+
echo " Valid: sonnet, opus, haiku, or full model ID (e.g., claude-sonnet-4-5-20250929)"
88+
((error_count++))
89+
fi
90+
fi
91+
92+
# Check 'description' field
93+
if echo "$frontmatter" | grep -q "^description:"; then
94+
local desc
95+
desc=$(echo "$frontmatter" | grep "^description:" | cut -d: -f2- | sed 's/^ *//')
96+
local length=${#desc}
97+
98+
if [ "$length" -eq 0 ]; then
99+
echo "⚠️ Warning: Empty description"
100+
((warning_count++))
101+
elif [ "$length" -gt 80 ]; then
102+
echo "⚠️ Warning: Description too long ($length chars, recommend < 60)"
103+
((warning_count++))
104+
elif [ "$length" -gt 60 ]; then
105+
echo "⚠️ Warning: Description length $length (recommend < 60 chars)"
106+
((warning_count++))
107+
else
108+
echo "✅ description: $length chars"
109+
fi
110+
fi
111+
112+
# Check 'allowed-tools' field
113+
if echo "$frontmatter" | grep -q "^allowed-tools:"; then
114+
local tools
115+
tools=$(echo "$frontmatter" | grep "^allowed-tools:" | cut -d: -f2- | sed 's/^ *//')
116+
117+
if [ -z "$tools" ]; then
118+
echo "⚠️ Warning: Empty allowed-tools field"
119+
((warning_count++))
120+
else
121+
# Check for common patterns
122+
if [[ "$tools" == "*" ]]; then
123+
echo "⚠️ Warning: allowed-tools: * grants all tools (consider restricting)"
124+
((warning_count++))
125+
elif [[ "$tools" =~ Bash\(\*\) ]]; then
126+
echo "⚠️ Warning: Bash(*) is very permissive (consider Bash(git:*) or similar)"
127+
((warning_count++))
128+
else
129+
echo "✅ allowed-tools: $tools"
130+
fi
131+
fi
132+
fi
133+
134+
# Check 'argument-hint' field
135+
if echo "$frontmatter" | grep -q "^argument-hint:"; then
136+
local hint
137+
hint=$(echo "$frontmatter" | grep "^argument-hint:" | cut -d: -f2- | sed 's/^ *//')
138+
139+
if [ -z "$hint" ]; then
140+
echo "⚠️ Warning: Empty argument-hint field"
141+
((warning_count++))
142+
else
143+
# Check for bracket convention
144+
if [[ ! "$hint" =~ \[.*\] ]]; then
145+
echo "⚠️ Warning: argument-hint missing bracket convention (e.g., [arg-name])"
146+
((warning_count++))
147+
else
148+
echo "✅ argument-hint: $hint"
149+
fi
150+
fi
151+
fi
152+
153+
# Check 'disable-model-invocation' field
154+
if echo "$frontmatter" | grep -q "^disable-model-invocation:"; then
155+
local value
156+
value=$(echo "$frontmatter" | grep "^disable-model-invocation:" | cut -d: -f2 | tr -d ' ')
157+
158+
if [[ "$value" =~ ^(true|false)$ ]]; then
159+
echo "✅ disable-model-invocation: $value"
160+
else
161+
echo "❌ Error: disable-model-invocation must be true or false (got '$value')"
162+
((error_count++))
163+
fi
164+
fi
165+
166+
# Check for unknown fields
167+
echo ""
168+
echo "Checking for unknown fields..."
169+
local unknown_found=false
170+
171+
while IFS= read -r line; do
172+
# Skip empty lines
173+
[ -z "$line" ] && continue
174+
175+
# Extract field name (everything before the colon)
176+
local field
177+
field=$(echo "$line" | grep -oE "^[a-z-]+" || true)
178+
179+
if [ -n "$field" ]; then
180+
local known=false
181+
for known_field in $KNOWN_FIELDS; do
182+
if [ "$field" = "$known_field" ]; then
183+
known=true
184+
break
185+
fi
186+
done
187+
188+
if [ "$known" = false ]; then
189+
echo "⚠️ Warning: Unknown field '$field'"
190+
((warning_count++))
191+
unknown_found=true
192+
fi
193+
fi
194+
done <<< "$frontmatter"
195+
196+
if [ "$unknown_found" = false ]; then
197+
echo "✅ No unknown fields"
198+
fi
199+
200+
# Summary
201+
echo ""
202+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
203+
if [ $error_count -eq 0 ] && [ $warning_count -eq 0 ]; then
204+
echo "$COMMAND_FILE: All frontmatter checks passed!"
205+
elif [ $error_count -eq 0 ]; then
206+
echo "⚠️ $COMMAND_FILE: Passed with $warning_count warning(s)"
207+
else
208+
echo "$COMMAND_FILE: Failed with $error_count error(s) and $warning_count warning(s)"
209+
fi
210+
echo ""
211+
212+
total_errors=$((total_errors + error_count))
213+
total_warnings=$((total_warnings + warning_count))
214+
215+
return $error_count
216+
}
217+
218+
# Process all provided files
219+
for file in "$@"; do
220+
check_frontmatter "$file" || true
221+
done
222+
223+
# Final summary for multiple files
224+
if [ $# -gt 1 ]; then
225+
echo "═══════════════════════════════════════"
226+
echo "Total: $# files checked"
227+
echo "Errors: $total_errors"
228+
echo "Warnings: $total_warnings"
229+
fi
230+
231+
if [ $total_errors -gt 0 ]; then
232+
exit 1
233+
fi
234+
exit 0

0 commit comments

Comments
 (0)