validate.ps1 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. # claude-mods validation script (PowerShell)
  2. # Validates YAML frontmatter, required fields, and naming conventions
  3. param(
  4. [switch]$YamlOnly,
  5. [switch]$NamesOnly
  6. )
  7. $ErrorActionPreference = "Stop"
  8. # Counters
  9. $script:Pass = 0
  10. $script:Fail = 0
  11. $script:Warn = 0
  12. # Get project directory
  13. $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
  14. $ProjectDir = Split-Path -Parent $ScriptDir
  15. function Write-Pass {
  16. param([string]$Message)
  17. Write-Host "PASS" -ForegroundColor Green -NoNewline
  18. Write-Host ": $Message"
  19. $script:Pass++
  20. }
  21. function Write-Fail {
  22. param([string]$Message)
  23. Write-Host "FAIL" -ForegroundColor Red -NoNewline
  24. Write-Host ": $Message"
  25. $script:Fail++
  26. }
  27. function Write-Warn {
  28. param([string]$Message)
  29. Write-Host "WARN" -ForegroundColor Yellow -NoNewline
  30. Write-Host ": $Message"
  31. $script:Warn++
  32. }
  33. function Test-YamlFrontmatter {
  34. param([string]$FilePath)
  35. $content = Get-Content -Path $FilePath -Raw
  36. # Check for opening ---
  37. if (-not $content.StartsWith("---")) {
  38. Write-Fail "$FilePath - Missing YAML frontmatter (no opening ---)"
  39. return $false
  40. }
  41. # Check for closing ---
  42. $lines = $content -split "`n"
  43. $foundClosing = $false
  44. for ($i = 1; $i -lt $lines.Count; $i++) {
  45. if ($lines[$i].Trim() -eq "---") {
  46. $foundClosing = $true
  47. break
  48. }
  49. }
  50. if (-not $foundClosing) {
  51. Write-Fail "$FilePath - Invalid YAML frontmatter (no closing ---)"
  52. return $false
  53. }
  54. return $true
  55. }
  56. function Get-YamlField {
  57. param(
  58. [string]$FilePath,
  59. [string]$Field
  60. )
  61. $content = Get-Content -Path $FilePath -Raw
  62. $lines = $content -split "`n"
  63. $inFrontmatter = $false
  64. foreach ($line in $lines) {
  65. if ($line.Trim() -eq "---") {
  66. if ($inFrontmatter) { break }
  67. $inFrontmatter = $true
  68. continue
  69. }
  70. if ($inFrontmatter -and $line -match "^${Field}:\s*(.+)$") {
  71. $value = $matches[1].Trim()
  72. # Remove quotes
  73. $value = $value -replace '^["'']|["'']$', ''
  74. return $value
  75. }
  76. }
  77. return $null
  78. }
  79. function Test-RequiredFields {
  80. param(
  81. [string]$FilePath,
  82. [string]$Type
  83. )
  84. $name = Get-YamlField -FilePath $FilePath -Field "name"
  85. $description = Get-YamlField -FilePath $FilePath -Field "description"
  86. if (-not $name) {
  87. Write-Fail "$FilePath - Missing required field: name"
  88. return $false
  89. }
  90. if (-not $description) {
  91. Write-Fail "$FilePath - Missing required field: description"
  92. return $false
  93. }
  94. return $true
  95. }
  96. function Test-Naming {
  97. param([string]$FilePath)
  98. $basename = [System.IO.Path]::GetFileNameWithoutExtension($FilePath)
  99. # Check if filename is kebab-case
  100. if ($basename -notmatch "^[a-z][a-z0-9]*(-[a-z0-9]+)*$") {
  101. Write-Warn "$FilePath - Filename not kebab-case: $basename"
  102. return $false
  103. }
  104. # Check if name field matches filename
  105. $name = Get-YamlField -FilePath $FilePath -Field "name"
  106. if ($name -and $name -ne $basename) {
  107. Write-Warn "$FilePath - Name field '$name' doesn't match filename '$basename'"
  108. return $false
  109. }
  110. return $true
  111. }
  112. function Test-Agents {
  113. Write-Host ""
  114. Write-Host "=== Validating Agents ===" -ForegroundColor Cyan
  115. $agentDir = Join-Path $ProjectDir "agents"
  116. if (-not (Test-Path $agentDir)) {
  117. Write-Warn "agents/ directory not found"
  118. return
  119. }
  120. $files = Get-ChildItem -Path $agentDir -Filter "*.md" -File
  121. foreach ($file in $files) {
  122. if (-not $NamesOnly) {
  123. if (Test-YamlFrontmatter -FilePath $file.FullName) {
  124. if (Test-RequiredFields -FilePath $file.FullName -Type "agent") {
  125. Write-Pass "$($file.FullName) - Valid agent"
  126. }
  127. }
  128. }
  129. if (-not $YamlOnly) {
  130. Test-Naming -FilePath $file.FullName | Out-Null
  131. }
  132. }
  133. }
  134. function Test-Commands {
  135. Write-Host ""
  136. Write-Host "=== Validating Commands ===" -ForegroundColor Cyan
  137. $cmdDir = Join-Path $ProjectDir "commands"
  138. if (-not (Test-Path $cmdDir)) {
  139. Write-Warn "commands/ directory not found"
  140. return
  141. }
  142. # Check .md files directly in commands/
  143. $files = Get-ChildItem -Path $cmdDir -Filter "*.md" -File
  144. foreach ($file in $files) {
  145. if (-not $NamesOnly) {
  146. if (Test-YamlFrontmatter -FilePath $file.FullName) {
  147. if (Test-RequiredFields -FilePath $file.FullName -Type "command") {
  148. Write-Pass "$($file.FullName) - Valid command"
  149. }
  150. }
  151. }
  152. if (-not $YamlOnly) {
  153. Test-Naming -FilePath $file.FullName | Out-Null
  154. }
  155. }
  156. # Check subdirectories
  157. $subdirs = Get-ChildItem -Path $cmdDir -Directory
  158. foreach ($subdir in $subdirs) {
  159. $subFiles = Get-ChildItem -Path $subdir.FullName -Filter "*.md" -File
  160. foreach ($file in $subFiles) {
  161. if (-not $NamesOnly) {
  162. if (Test-YamlFrontmatter -FilePath $file.FullName) {
  163. $desc = Get-YamlField -FilePath $file.FullName -Field "description"
  164. if ($desc) {
  165. Write-Pass "$($file.FullName) - Valid subcommand"
  166. } else {
  167. Write-Warn "$($file.FullName) - Missing description"
  168. }
  169. }
  170. }
  171. }
  172. }
  173. }
  174. function Test-Skills {
  175. Write-Host ""
  176. Write-Host "=== Validating Skills ===" -ForegroundColor Cyan
  177. $skillsDir = Join-Path $ProjectDir "skills"
  178. if (-not (Test-Path $skillsDir)) {
  179. Write-Warn "skills/ directory not found"
  180. return
  181. }
  182. $subdirs = Get-ChildItem -Path $skillsDir -Directory
  183. foreach ($subdir in $subdirs) {
  184. $skillFile = Join-Path $subdir.FullName "SKILL.md"
  185. if (-not (Test-Path $skillFile)) {
  186. Write-Fail "$($subdir.FullName) - Missing SKILL.md"
  187. continue
  188. }
  189. if (-not $NamesOnly) {
  190. if (Test-YamlFrontmatter -FilePath $skillFile) {
  191. $name = Get-YamlField -FilePath $skillFile -Field "name"
  192. $desc = Get-YamlField -FilePath $skillFile -Field "description"
  193. if ($name -and $desc) {
  194. Write-Pass "$skillFile - Valid skill"
  195. } else {
  196. if (-not $name) { Write-Fail "$skillFile - Missing name" }
  197. if (-not $desc) { Write-Fail "$skillFile - Missing description" }
  198. }
  199. }
  200. }
  201. }
  202. }
  203. # Main
  204. Write-Host "claude-mods Validation"
  205. Write-Host "======================"
  206. Write-Host "Project: $ProjectDir"
  207. Test-Agents
  208. Test-Commands
  209. Test-Skills
  210. Write-Host ""
  211. Write-Host "======================"
  212. Write-Host "Results: " -NoNewline
  213. Write-Host "$script:Pass passed" -ForegroundColor Green -NoNewline
  214. Write-Host ", " -NoNewline
  215. Write-Host "$script:Fail failed" -ForegroundColor Red -NoNewline
  216. Write-Host ", " -NoNewline
  217. Write-Host "$script:Warn warnings" -ForegroundColor Yellow
  218. if ($script:Fail -gt 0) {
  219. exit 1
  220. }
  221. exit 0