install.ps1 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. <#
  2. .SYNOPSIS
  3. Install claude-mods extensions to ~/.claude/
  4. .DESCRIPTION
  5. Copies commands, skills, agents, and rules to the global Claude Code config.
  6. Handles cleanup of deprecated items and command-to-skill migrations.
  7. .PARAMETER Statusline
  8. Opt in to installing the context-usage statusline. Off by default so a
  9. shared install never changes the user's prompt UI without being asked.
  10. .NOTES
  11. Run from the claude-mods directory:
  12. .\scripts\install.ps1
  13. .\scripts\install.ps1 -Statusline
  14. #>
  15. param(
  16. [switch]$Statusline
  17. )
  18. $ErrorActionPreference = "Stop"
  19. Write-Host "================================================================" -ForegroundColor Cyan
  20. Write-Host " claude-mods Installer (Windows) " -ForegroundColor Cyan
  21. Write-Host "================================================================" -ForegroundColor Cyan
  22. Write-Host ""
  23. $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
  24. $projectRoot = Split-Path -Parent $scriptDir
  25. if ($env:CLAUDE_DIR) {
  26. $claudeDir = $env:CLAUDE_DIR
  27. } else {
  28. $claudeDir = "$env:USERPROFILE\.claude"
  29. }
  30. # Ensure ~/.claude directories exist
  31. $dirs = @("commands", "skills", "agents", "rules", "output-styles", "hooks")
  32. foreach ($dir in $dirs) {
  33. $path = Join-Path $claudeDir $dir
  34. if (-not (Test-Path $path)) {
  35. New-Item -ItemType Directory -Path $path -Force | Out-Null
  36. Write-Host " Created $path" -ForegroundColor Green
  37. }
  38. }
  39. # =============================================================================
  40. # DEPRECATED ITEMS - Remove these from user config
  41. # =============================================================================
  42. $deprecated = @(
  43. "$claudeDir\commands\review.md",
  44. "$claudeDir\commands\testgen.md",
  45. "$claudeDir\commands\conclave.md",
  46. "$claudeDir\commands\pulse.md",
  47. "$claudeDir\skills\conclave",
  48. "$claudeDir\skills\claude-code-templates", # Replaced by skill-creator
  49. "$claudeDir\skills\agentmail", # Renamed to pigeon (v2.3.0)
  50. "$claudeDir\skills\claude-code-debug", # Merged into claude-code-ops (v3.0)
  51. "$claudeDir\skills\claude-code-headless", # Merged into claude-code-ops (v3.0)
  52. "$claudeDir\skills\claude-code-hooks", # Merged into claude-code-ops (v3.0)
  53. "$claudeDir\skills\dsp-launch", # Superseded by fleet-worker + native background agents (2026-07)
  54. # Deprecated agents (v3.0): folded into their -ops skill twins
  55. "$claudeDir\agents\python-expert.md",
  56. "$claudeDir\agents\typescript-expert.md",
  57. "$claudeDir\agents\javascript-expert.md",
  58. "$claudeDir\agents\go-expert.md",
  59. "$claudeDir\agents\rust-expert.md",
  60. "$claudeDir\agents\react-expert.md",
  61. "$claudeDir\agents\vue-expert.md",
  62. "$claudeDir\agents\astro-expert.md",
  63. "$claudeDir\agents\laravel-expert.md",
  64. "$claudeDir\agents\sql-expert.md",
  65. "$claudeDir\agents\postgres-expert.md",
  66. "$claudeDir\agents\cypress-expert.md", # -> skills/cypress-ops
  67. "$claudeDir\agents\cloudflare-expert.md", # -> skills/cloudflare-ops
  68. "$claudeDir\agents\wrangler-expert.md", # -> skills/cloudflare-ops
  69. "$claudeDir\agents\bash-expert.md", # -> skills/bash-ops
  70. "$claudeDir\agents\claude-architect.md", # -> skills/claude-code-ops
  71. "$claudeDir\agents\aws-fargate-ecs-expert.md", # -> skills/container-orchestration
  72. "$claudeDir\agents\craftcms-expert.md", # -> skills/craftcms-ops
  73. "$claudeDir\agents\payloadcms-expert.md", # -> skills/payloadcms-ops
  74. "$claudeDir\agents\asus-router-expert.md" # -> skills/asus-router-ops
  75. )
  76. # Renamed skills: -patterns -> -ops (March 2026)
  77. $renamedSkills = @(
  78. "cli-patterns",
  79. "mcp-patterns",
  80. "python-async-patterns",
  81. "python-cli-patterns",
  82. "python-database-patterns",
  83. "python-fastapi-patterns",
  84. "python-observability-patterns",
  85. "python-pytest-patterns",
  86. "python-typing-patterns",
  87. "rest-patterns",
  88. "security-patterns",
  89. "sql-patterns",
  90. "tailwind-patterns",
  91. "testing-patterns"
  92. )
  93. foreach ($oldSkill in $renamedSkills) {
  94. $oldPath = "$claudeDir\skills\$oldSkill"
  95. if (Test-Path $oldPath) {
  96. try {
  97. Remove-Item -Path $oldPath -Recurse -Force -ErrorAction Stop
  98. $newName = $oldSkill -replace '-patterns$', '-ops'
  99. Write-Host " Removed renamed: $oldSkill (now $newName)" -ForegroundColor Red
  100. } catch {
  101. Write-Host " WARNING: could not remove $oldPath ($($_.Exception.Message)) - continuing" -ForegroundColor Yellow
  102. }
  103. }
  104. }
  105. Write-Host "Cleaning up deprecated items..." -ForegroundColor Yellow
  106. foreach ($item in $deprecated) {
  107. if (Test-Path $item) {
  108. try {
  109. Remove-Item -Path $item -Recurse -Force -ErrorAction Stop
  110. Write-Host " Removed: $item" -ForegroundColor Red
  111. } catch {
  112. Write-Host " WARNING: could not remove $item ($($_.Exception.Message)) - continuing" -ForegroundColor Yellow
  113. }
  114. }
  115. }
  116. Write-Host ""
  117. # =============================================================================
  118. # COMMANDS - Only copy commands that have not been migrated to skills
  119. # =============================================================================
  120. Write-Host "Installing commands..." -ForegroundColor Cyan
  121. $skipCommands = @("review.md", "testgen.md")
  122. $commandsDir = Join-Path $projectRoot "commands"
  123. Get-ChildItem -Path $commandsDir -Filter "*.md" | ForEach-Object {
  124. if ($_.Name -notin $skipCommands -and $_.Name -notlike "archive*") {
  125. Copy-Item $_.FullName -Destination "$claudeDir\commands\" -Force
  126. Write-Host " $($_.Name)" -ForegroundColor Green
  127. }
  128. }
  129. Write-Host ""
  130. # =============================================================================
  131. # SKILLS - Merge-sync each skill directory (NEVER delete-then-copy)
  132. #
  133. # A registered service can hold a skill dir as its CWD (e.g. Process Compose
  134. # "fleetflow" runs python ff-serve.py with CWD ~/.claude/skills/fleetflow),
  135. # which locks the directory handle on Windows. On 2026-08-01 the old
  136. # Remove-Item pass half-deleted that dir (destroying machine-local unversioned
  137. # files) before failing, then aborted the run, leaving every skill after it
  138. # unsynced. So: file-level overwrite copy (works while the dir is locked),
  139. # dest-only files are reported but never deleted, and a failure on one skill
  140. # continues to the next.
  141. # =============================================================================
  142. Write-Host "Installing skills..." -ForegroundColor Cyan
  143. $skillsDir = Join-Path $projectRoot "skills"
  144. $failedSkills = @()
  145. foreach ($skill in (Get-ChildItem -Path $skillsDir -Directory)) {
  146. $src = $skill.FullName
  147. $dest = "$claudeDir\skills\$($skill.Name)"
  148. try {
  149. if (-not (Test-Path $dest)) {
  150. Copy-Item -Path $src -Destination $dest -Recurse -Force -ErrorAction Stop
  151. Write-Host " $($skill.Name)/" -ForegroundColor Green
  152. continue
  153. }
  154. # Merge copy: overwrite file-by-file so a locked destination still syncs.
  155. $srcRel = @{}
  156. $fileErrors = @()
  157. foreach ($f in (Get-ChildItem -Path $src -Recurse -File)) {
  158. $rel = $f.FullName.Substring($src.Length + 1)
  159. $srcRel[$rel] = $true
  160. try {
  161. $destFile = Join-Path $dest $rel
  162. $destFileDir = Split-Path -Parent $destFile
  163. if (-not (Test-Path $destFileDir)) {
  164. New-Item -ItemType Directory -Path $destFileDir -Force -ErrorAction Stop | Out-Null
  165. }
  166. Copy-Item -Path $f.FullName -Destination $destFile -Force -ErrorAction Stop
  167. } catch {
  168. $fileErrors += "$rel ($($_.Exception.Message))"
  169. }
  170. }
  171. # Dest-only files are machine-local (unversioned) or stale. Surface
  172. # them, never delete them - deleting is the 2026-08-01 data loss.
  173. $destOnly = @(Get-ChildItem -Path $dest -Recurse -File | Where-Object {
  174. -not $srcRel.ContainsKey($_.FullName.Substring($dest.Length + 1))
  175. })
  176. if ($fileErrors.Count -gt 0) {
  177. $failedSkills += $skill.Name
  178. Write-Host " $($skill.Name)/ - $($fileErrors.Count) file(s) failed to sync:" -ForegroundColor Red
  179. foreach ($e in $fileErrors) { Write-Host " $e" -ForegroundColor Red }
  180. } elseif ($destOnly.Count -gt 0) {
  181. Write-Host " $($skill.Name)/ (kept $($destOnly.Count) dest-only file(s) not in repo)" -ForegroundColor Yellow
  182. foreach ($f in $destOnly) {
  183. Write-Host " $($f.FullName.Substring($dest.Length + 1))" -ForegroundColor Yellow
  184. }
  185. } else {
  186. Write-Host " $($skill.Name)/" -ForegroundColor Green
  187. }
  188. } catch {
  189. $failedSkills += $skill.Name
  190. Write-Host " WARNING: $($skill.Name)/ failed to sync ($($_.Exception.Message)) - continuing" -ForegroundColor Red
  191. }
  192. }
  193. if ($failedSkills.Count -gt 0) {
  194. Write-Host ""
  195. Write-Host " $($failedSkills.Count) skill(s) had sync failures: $($failedSkills -join ', ')" -ForegroundColor Red
  196. }
  197. Write-Host ""
  198. # =============================================================================
  199. # AGENTS - Copy all agent files
  200. # =============================================================================
  201. Write-Host "Installing agents..." -ForegroundColor Cyan
  202. $agentsDir = Join-Path $projectRoot "agents"
  203. Get-ChildItem -Path $agentsDir -Filter "*.md" | ForEach-Object {
  204. Copy-Item $_.FullName -Destination "$claudeDir\agents\" -Force
  205. Write-Host " $($_.Name)" -ForegroundColor Green
  206. }
  207. Write-Host ""
  208. # =============================================================================
  209. # RULES - Copy all rule files
  210. # =============================================================================
  211. Write-Host "Installing rules..." -ForegroundColor Cyan
  212. $rulesDir = Join-Path $projectRoot "rules"
  213. Get-ChildItem -Path $rulesDir -Filter "*.md" | ForEach-Object {
  214. Copy-Item $_.FullName -Destination "$claudeDir\rules\" -Force
  215. Write-Host " $($_.Name)" -ForegroundColor Green
  216. }
  217. Write-Host ""
  218. # =============================================================================
  219. # OUTPUT STYLES - Copy all output style files
  220. # =============================================================================
  221. Write-Host "Installing output styles..." -ForegroundColor Cyan
  222. $stylesDir = Join-Path $projectRoot "output-styles"
  223. if (Test-Path $stylesDir) {
  224. Get-ChildItem -Path $stylesDir -Filter "*.md" | ForEach-Object {
  225. Copy-Item $_.FullName -Destination "$claudeDir\output-styles\" -Force
  226. Write-Host " $($_.Name)" -ForegroundColor Green
  227. }
  228. }
  229. Write-Host ""
  230. # =============================================================================
  231. # HOOKS - Copy scripts and merge plugin-equivalent wiring into settings.json
  232. # =============================================================================
  233. Write-Host "Installing hooks..." -ForegroundColor Cyan
  234. $hooksDir = Join-Path $projectRoot "hooks"
  235. Get-ChildItem -Path $hooksDir -Filter "*.sh" | ForEach-Object {
  236. Copy-Item $_.FullName -Destination "$claudeDir\hooks\" -Force
  237. }
  238. $settingsPath = Join-Path $claudeDir "settings.json"
  239. if (Test-Path $settingsPath) {
  240. $settings = Get-Content $settingsPath -Raw | ConvertFrom-Json
  241. } else {
  242. $settings = [PSCustomObject]@{}
  243. }
  244. if (-not $settings.PSObject.Properties["hooks"]) {
  245. $settings | Add-Member -MemberType NoteProperty -Name hooks -Value ([PSCustomObject]@{})
  246. }
  247. $desired = Get-Content (Join-Path $hooksDir "hooks.json") -Raw | ConvertFrom-Json
  248. foreach ($eventProperty in $desired.hooks.PSObject.Properties) {
  249. $eventName = $eventProperty.Name
  250. if (-not $settings.hooks.PSObject.Properties[$eventName]) {
  251. $settings.hooks | Add-Member -MemberType NoteProperty -Name $eventName -Value @()
  252. }
  253. $eventGroups = @($settings.hooks.$eventName)
  254. foreach ($group in @($eventProperty.Value)) {
  255. $missingHooks = @()
  256. foreach ($hook in @($group.hooks)) {
  257. $command = $hook.command.Replace('${CLAUDE_PLUGIN_ROOT}/hooks', (Join-Path $claudeDir "hooks"))
  258. # Dedup on the script NAME under hooks/, not the resolved path: a
  259. # hook wired by a plugin install carries the
  260. # ${CLAUDE_PLUGIN_ROOT}/hooks/ form and must still count as
  261. # already-wired (mixed-method double-fire). Separator-tolerant:
  262. # Join-Path yields \hooks while plugin form uses /hooks.
  263. $hookName = ($command -replace '^.*hooks[/\\]', '') -replace '"$', ''
  264. $alreadyWired = $false
  265. foreach ($existingGroup in $eventGroups) {
  266. foreach ($existingHook in @($existingGroup.hooks)) {
  267. if ($existingHook.command -and ($existingHook.command.Contains("hooks/$hookName") -or $existingHook.command.Contains("hooks\$hookName"))) {
  268. $alreadyWired = $true
  269. }
  270. }
  271. }
  272. if (-not $alreadyWired) {
  273. $copy = $hook.PSObject.Copy()
  274. $copy.command = $command
  275. $missingHooks += $copy
  276. }
  277. }
  278. if ($missingHooks.Count -gt 0) {
  279. $newGroup = $group.PSObject.Copy()
  280. $newGroup.hooks = $missingHooks
  281. $eventGroups += $newGroup
  282. }
  283. }
  284. $settings.hooks.$eventName = $eventGroups
  285. }
  286. # STATUSLINE - opt-in only (-Statusline). Even when opted in we add it ONLY if
  287. # the user has none: a statusline is a whole-config key, so we never clobber
  288. # one the user already set, and we touch nothing else.
  289. if ($Statusline) {
  290. if (-not $settings.PSObject.Properties["statusLine"]) {
  291. $statuslineTemplate = Join-Path $projectRoot "templates\settings.json"
  292. if (Test-Path $statuslineTemplate) {
  293. $tpl = Get-Content $statuslineTemplate -Raw | ConvertFrom-Json
  294. if ($tpl.PSObject.Properties["statusLine"]) {
  295. $settings | Add-Member -MemberType NoteProperty -Name statusLine -Value $tpl.statusLine
  296. Write-Host " Context-usage statusline added to settings.json" -ForegroundColor Green
  297. }
  298. }
  299. } else {
  300. Write-Host " Existing statusline preserved (remove it first to use the claude-mods one)" -ForegroundColor Green
  301. }
  302. } else {
  303. Write-Host " Statusline skipped (re-run with -Statusline to install it)" -ForegroundColor DarkGray
  304. }
  305. $settings | ConvertTo-Json -Depth 20 | Set-Content $settingsPath -Encoding UTF8
  306. Write-Host " Security and peer-guard hooks wired in settings.json" -ForegroundColor Green
  307. Write-Host ""
  308. # =============================================================================
  309. # PIGEON - Global install (scripts + hook config hint)
  310. # =============================================================================
  311. Write-Host "Installing pigeon (pmail)..." -ForegroundColor Cyan
  312. # Clean up old agentmail install if present
  313. $oldAgentmailDir = Join-Path $claudeDir "agentmail"
  314. if (Test-Path $oldAgentmailDir) {
  315. Remove-Item -Path $oldAgentmailDir -Recurse -Force
  316. Write-Host " Removed old agentmail/ (renamed to pigeon/)" -ForegroundColor Red
  317. }
  318. $pigeonDir = Join-Path $claudeDir "pigeon"
  319. New-Item -ItemType Directory -Force -Path $pigeonDir | Out-Null
  320. $mailDbSrc = Join-Path $projectRoot "skills\pigeon\scripts\mail-db.sh"
  321. $checkMailSrc = Join-Path $projectRoot "hooks\check-mail.sh"
  322. if (Test-Path $mailDbSrc) {
  323. Copy-Item $mailDbSrc -Destination "$pigeonDir\" -Force
  324. Write-Host " mail-db.sh" -ForegroundColor Green
  325. }
  326. if (Test-Path $checkMailSrc) {
  327. Copy-Item $checkMailSrc -Destination "$pigeonDir\" -Force
  328. Write-Host " check-mail.sh" -ForegroundColor Green
  329. }
  330. $settingsPath = Join-Path $claudeDir "settings.json"
  331. # Migrate stale agentmail hook path -> pigeon
  332. if ((Test-Path $settingsPath) -and (Select-String -Path $settingsPath -Pattern "agentmail/check-mail\.sh" -Quiet)) {
  333. $content = Get-Content $settingsPath -Raw
  334. $content = $content -replace 'agentmail/check-mail\.sh', 'pigeon/check-mail.sh'
  335. Set-Content $settingsPath -Value $content -NoNewline
  336. Write-Host " Migrated agentmail hook -> pigeon in settings.json" -ForegroundColor Green
  337. }
  338. # Check if hook is already configured (pigeon path)
  339. if ((Test-Path $settingsPath) -and (Select-String -Path $settingsPath -Pattern "pigeon/check-mail\.sh" -Quiet)) {
  340. Write-Host " Hook already configured in settings.json" -ForegroundColor Green
  341. } else {
  342. Write-Host ""
  343. Write-Host ' To enable automatic pmail notifications, add this to ~/.claude/settings.json:' -ForegroundColor Yellow
  344. Write-Host ""
  345. Write-Host ' "hooks": {'
  346. Write-Host ' "PreToolUse": [{'
  347. Write-Host ' "matcher": "*",'
  348. Write-Host ' "hooks": [{'
  349. Write-Host ' "type": "command",'
  350. Write-Host ' "command": "bash \"$HOME/.claude/pigeon/check-mail.sh\"",'
  351. Write-Host ' "timeout": 5'
  352. Write-Host ' }]'
  353. Write-Host ' }]'
  354. Write-Host ' }'
  355. Write-Host ""
  356. Write-Host " Without this, pigeon works but you must check manually (pigeon read)." -ForegroundColor Yellow
  357. }
  358. Write-Host ""
  359. # =============================================================================
  360. # AUTO-SKILL - Global install (tracking + evaluation hooks)
  361. # =============================================================================
  362. Write-Host "Installing auto-skill..." -ForegroundColor Cyan
  363. $autoSkillDir = Join-Path $claudeDir "auto-skill"
  364. New-Item -ItemType Directory -Force -Path $autoSkillDir | Out-Null
  365. $scripts = @("track-tools.sh", "evaluate.sh")
  366. foreach ($script in $scripts) {
  367. $src = Join-Path $projectRoot "skills\auto-skill\scripts\$script"
  368. if (Test-Path $src) {
  369. Copy-Item $src -Destination "$autoSkillDir\" -Force
  370. Write-Host " $script" -ForegroundColor Green
  371. }
  372. }
  373. $settingsPath = Join-Path $claudeDir "settings.json"
  374. if ((Test-Path $settingsPath) -and (Select-String -Path $settingsPath -Pattern "auto-skill" -Quiet)) {
  375. Write-Host " Hooks already configured in settings.json" -ForegroundColor Green
  376. } else {
  377. Write-Host ""
  378. Write-Host ' To enable automatic skill suggestions, add these hooks to ~/.claude/settings.json:' -ForegroundColor Yellow
  379. Write-Host ""
  380. Write-Host ' "PostToolUse": [{ "matcher": "*", "hooks": [{'
  381. Write-Host ' "type": "command",'
  382. Write-Host ' "command": "bash \"$HOME/.claude/auto-skill/track-tools.sh\"", "timeout": 2'
  383. Write-Host ' }] }],'
  384. Write-Host ' "Stop": [{ "hooks": [{'
  385. Write-Host ' "type": "command",'
  386. Write-Host ' "command": "bash \"$HOME/.claude/auto-skill/evaluate.sh\"", "timeout": 5'
  387. Write-Host ' }] }]'
  388. Write-Host ""
  389. Write-Host " Without this, /auto-skill still works but won't suggest automatically." -ForegroundColor Yellow
  390. }
  391. Write-Host ""
  392. # =============================================================================
  393. # SUMMARY
  394. # =============================================================================
  395. Write-Host "================================================================" -ForegroundColor Cyan
  396. Write-Host " Installation complete!" -ForegroundColor Green
  397. Write-Host "================================================================" -ForegroundColor Cyan
  398. Write-Host ""
  399. Write-Host "Restart Claude Code to load the new extensions." -ForegroundColor Yellow
  400. Write-Host ""