bps-kit 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/package.json +1 -1
  2. package/templates/.agents/agents/backend-specialist.md +263 -0
  3. package/templates/.agents/agents/code-archaeologist.md +106 -0
  4. package/templates/.agents/agents/database-architect.md +226 -0
  5. package/templates/.agents/agents/debugger.md +225 -0
  6. package/templates/.agents/agents/devops-engineer.md +242 -0
  7. package/templates/.agents/agents/documentation-writer.md +104 -0
  8. package/templates/.agents/agents/explorer-agent.md +73 -0
  9. package/templates/.agents/agents/frontend-specialist.md +593 -0
  10. package/templates/.agents/agents/game-developer.md +162 -0
  11. package/templates/.agents/agents/mobile-developer.md +377 -0
  12. package/templates/.agents/agents/orchestrator.md +416 -0
  13. package/templates/.agents/agents/penetration-tester.md +188 -0
  14. package/templates/.agents/agents/performance-optimizer.md +187 -0
  15. package/templates/.agents/agents/product-manager.md +112 -0
  16. package/templates/.agents/agents/product-owner.md +95 -0
  17. package/templates/.agents/agents/project-planner.md +406 -0
  18. package/templates/.agents/agents/qa-automation-engineer.md +103 -0
  19. package/templates/.agents/agents/security-auditor.md +170 -0
  20. package/templates/.agents/agents/seo-specialist.md +111 -0
  21. package/templates/.agents/agents/test-engineer.md +158 -0
  22. package/templates/.agents/rules/GEMINI.md +219 -0
  23. package/templates/.agents/scripts/auto_preview.py +148 -0
  24. package/templates/.agents/scripts/checklist.py +217 -0
  25. package/templates/.agents/scripts/session_manager.py +120 -0
  26. package/templates/.agents/scripts/verify_all.py +327 -0
  27. package/templates/.agents/workflows/brainstorm.md +113 -0
  28. package/templates/.agents/workflows/create.md +59 -0
  29. package/templates/.agents/workflows/debug.md +103 -0
  30. package/templates/.agents/workflows/deploy.md +176 -0
  31. package/templates/.agents/workflows/enhance.md +63 -0
  32. package/templates/.agents/workflows/orchestrate.md +237 -0
  33. package/templates/.agents/workflows/plan.md +89 -0
  34. package/templates/.agents/workflows/preview.md +81 -0
  35. package/templates/.agents/workflows/setup-brain.md +39 -0
  36. package/templates/.agents/workflows/status.md +86 -0
  37. package/templates/.agents/workflows/test.md +144 -0
  38. package/templates/.agents/workflows/ui-ux-pro-max.md +296 -0
  39. package/templates/skills_normal/api-patterns/scripts/api_validator.py +211 -0
  40. package/templates/skills_normal/database-design/scripts/schema_validator.py +172 -0
  41. package/templates/skills_normal/frontend-design/scripts/accessibility_checker.py +183 -0
  42. package/templates/skills_normal/frontend-design/scripts/ux_audit.py +722 -0
  43. package/templates/skills_normal/git-pushing/scripts/smart_commit.sh +19 -0
  44. package/templates/skills_normal/lint-and-validate/scripts/lint_runner.py +184 -0
  45. package/templates/skills_normal/lint-and-validate/scripts/type_coverage.py +173 -0
  46. package/templates/skills_normal/performance-profiling/scripts/lighthouse_audit.py +76 -0
  47. package/templates/skills_normal/senior-fullstack/scripts/code_quality_analyzer.py +114 -0
  48. package/templates/skills_normal/senior-fullstack/scripts/fullstack_scaffolder.py +114 -0
  49. package/templates/skills_normal/senior-fullstack/scripts/project_scaffolder.py +114 -0
  50. package/templates/skills_normal/seo-fundamentals/scripts/seo_checker.py +219 -0
  51. package/templates/skills_normal/testing-patterns/scripts/test_runner.py +219 -0
  52. package/templates/skills_normal/vulnerability-scanner/scripts/security_scan.py +458 -0
  53. package/templates/vault/007/scripts/config.py +472 -0
  54. package/templates/vault/007/scripts/full_audit.py +1306 -0
  55. package/templates/vault/007/scripts/quick_scan.py +481 -0
  56. package/templates/vault/007/scripts/requirements.txt +26 -0
  57. package/templates/vault/007/scripts/scanners/__init__.py +0 -0
  58. package/templates/vault/007/scripts/scanners/dependency_scanner.py +1305 -0
  59. package/templates/vault/007/scripts/scanners/injection_scanner.py +1104 -0
  60. package/templates/vault/007/scripts/scanners/secrets_scanner.py +1008 -0
  61. package/templates/vault/007/scripts/score_calculator.py +693 -0
  62. package/templates/vault/agent-orchestrator/scripts/match_skills.py +329 -0
  63. package/templates/vault/agent-orchestrator/scripts/orchestrate.py +304 -0
  64. package/templates/vault/agent-orchestrator/scripts/requirements.txt +1 -0
  65. package/templates/vault/agent-orchestrator/scripts/scan_registry.py +508 -0
  66. package/templates/vault/ai-studio-image/scripts/config.py +613 -0
  67. package/templates/vault/ai-studio-image/scripts/generate.py +630 -0
  68. package/templates/vault/ai-studio-image/scripts/prompt_engine.py +424 -0
  69. package/templates/vault/ai-studio-image/scripts/requirements.txt +4 -0
  70. package/templates/vault/ai-studio-image/scripts/templates.py +349 -0
  71. package/templates/vault/android_ui_verification/scripts/verify_ui.sh +32 -0
  72. package/templates/vault/apify-audience-analysis/reference/scripts/run_actor.js +363 -0
  73. package/templates/vault/apify-brand-reputation-monitoring/reference/scripts/run_actor.js +363 -0
  74. package/templates/vault/apify-competitor-intelligence/reference/scripts/run_actor.js +363 -0
  75. package/templates/vault/apify-content-analytics/reference/scripts/run_actor.js +363 -0
  76. package/templates/vault/apify-ecommerce/reference/scripts/package.json +3 -0
  77. package/templates/vault/apify-ecommerce/reference/scripts/run_actor.js +369 -0
  78. package/templates/vault/apify-influencer-discovery/reference/scripts/run_actor.js +363 -0
  79. package/templates/vault/apify-lead-generation/reference/scripts/run_actor.js +363 -0
  80. package/templates/vault/apify-market-research/reference/scripts/run_actor.js +363 -0
  81. package/templates/vault/apify-trend-analysis/reference/scripts/run_actor.js +363 -0
  82. package/templates/vault/apify-ultimate-scraper/reference/scripts/run_actor.js +363 -0
  83. package/templates/vault/audio-transcriber/scripts/install-requirements.sh +190 -0
  84. package/templates/vault/audio-transcriber/scripts/transcribe.py +486 -0
  85. package/templates/vault/claude-monitor/scripts/api_bench.py +240 -0
  86. package/templates/vault/claude-monitor/scripts/config.py +69 -0
  87. package/templates/vault/claude-monitor/scripts/health_check.py +362 -0
  88. package/templates/vault/claude-monitor/scripts/monitor.py +296 -0
  89. package/templates/vault/content-creator/scripts/brand_voice_analyzer.py +185 -0
  90. package/templates/vault/content-creator/scripts/seo_optimizer.py +419 -0
  91. package/templates/vault/context-agent/scripts/active_context.py +227 -0
  92. package/templates/vault/context-agent/scripts/compressor.py +149 -0
  93. package/templates/vault/context-agent/scripts/config.py +69 -0
  94. package/templates/vault/context-agent/scripts/context_loader.py +155 -0
  95. package/templates/vault/context-agent/scripts/context_manager.py +302 -0
  96. package/templates/vault/context-agent/scripts/models.py +103 -0
  97. package/templates/vault/context-agent/scripts/project_registry.py +132 -0
  98. package/templates/vault/context-agent/scripts/requirements.txt +6 -0
  99. package/templates/vault/context-agent/scripts/search.py +115 -0
  100. package/templates/vault/context-agent/scripts/session_parser.py +206 -0
  101. package/templates/vault/context-agent/scripts/session_summary.py +319 -0
  102. package/templates/vault/context-guardian/scripts/context_snapshot.py +229 -0
  103. package/templates/vault/docx/ooxml/scripts/pack.py +159 -0
  104. package/templates/vault/docx/ooxml/scripts/unpack.py +29 -0
  105. package/templates/vault/docx/ooxml/scripts/validate.py +69 -0
  106. package/templates/vault/docx/ooxml/scripts/validation/__init__.py +15 -0
  107. package/templates/vault/docx/ooxml/scripts/validation/base.py +951 -0
  108. package/templates/vault/docx/ooxml/scripts/validation/docx.py +274 -0
  109. package/templates/vault/docx/ooxml/scripts/validation/pptx.py +315 -0
  110. package/templates/vault/docx/ooxml/scripts/validation/redlining.py +279 -0
  111. package/templates/vault/docx/scripts/__init__.py +1 -0
  112. package/templates/vault/docx/scripts/document.py +1276 -0
  113. package/templates/vault/docx/scripts/templates/comments.xml +3 -0
  114. package/templates/vault/docx/scripts/templates/commentsExtended.xml +3 -0
  115. package/templates/vault/docx/scripts/templates/commentsExtensible.xml +3 -0
  116. package/templates/vault/docx/scripts/templates/commentsIds.xml +3 -0
  117. package/templates/vault/docx/scripts/templates/people.xml +3 -0
  118. package/templates/vault/docx/scripts/utilities.py +374 -0
  119. package/templates/vault/docx-official/ooxml/scripts/pack.py +159 -0
  120. package/templates/vault/docx-official/ooxml/scripts/unpack.py +29 -0
  121. package/templates/vault/docx-official/ooxml/scripts/validate.py +69 -0
  122. package/templates/vault/docx-official/ooxml/scripts/validation/__init__.py +15 -0
  123. package/templates/vault/docx-official/ooxml/scripts/validation/base.py +951 -0
  124. package/templates/vault/docx-official/ooxml/scripts/validation/docx.py +274 -0
  125. package/templates/vault/docx-official/ooxml/scripts/validation/pptx.py +315 -0
  126. package/templates/vault/docx-official/ooxml/scripts/validation/redlining.py +279 -0
  127. package/templates/vault/docx-official/scripts/__init__.py +1 -0
  128. package/templates/vault/docx-official/scripts/document.py +1276 -0
  129. package/templates/vault/docx-official/scripts/templates/comments.xml +3 -0
  130. package/templates/vault/docx-official/scripts/templates/commentsExtended.xml +3 -0
  131. package/templates/vault/docx-official/scripts/templates/commentsExtensible.xml +3 -0
  132. package/templates/vault/docx-official/scripts/templates/commentsIds.xml +3 -0
  133. package/templates/vault/docx-official/scripts/templates/people.xml +3 -0
  134. package/templates/vault/docx-official/scripts/utilities.py +374 -0
  135. package/templates/vault/geo-fundamentals/scripts/geo_checker.py +289 -0
  136. package/templates/vault/helm-chart-scaffolding/scripts/validate-chart.sh +244 -0
  137. package/templates/vault/i18n-localization/scripts/i18n_checker.py +241 -0
  138. package/templates/vault/instagram/scripts/account_setup.py +233 -0
  139. package/templates/vault/instagram/scripts/analyze.py +221 -0
  140. package/templates/vault/instagram/scripts/api_client.py +444 -0
  141. package/templates/vault/instagram/scripts/auth.py +411 -0
  142. package/templates/vault/instagram/scripts/comments.py +160 -0
  143. package/templates/vault/instagram/scripts/config.py +111 -0
  144. package/templates/vault/instagram/scripts/db.py +467 -0
  145. package/templates/vault/instagram/scripts/export.py +138 -0
  146. package/templates/vault/instagram/scripts/governance.py +233 -0
  147. package/templates/vault/instagram/scripts/hashtags.py +114 -0
  148. package/templates/vault/instagram/scripts/insights.py +170 -0
  149. package/templates/vault/instagram/scripts/media.py +65 -0
  150. package/templates/vault/instagram/scripts/messages.py +103 -0
  151. package/templates/vault/instagram/scripts/profile.py +58 -0
  152. package/templates/vault/instagram/scripts/publish.py +449 -0
  153. package/templates/vault/instagram/scripts/requirements.txt +5 -0
  154. package/templates/vault/instagram/scripts/run_all.py +189 -0
  155. package/templates/vault/instagram/scripts/schedule.py +189 -0
  156. package/templates/vault/instagram/scripts/serve_api.py +234 -0
  157. package/templates/vault/instagram/scripts/templates.py +155 -0
  158. package/templates/vault/junta-leiloeiros/scripts/db.py +216 -0
  159. package/templates/vault/junta-leiloeiros/scripts/export.py +137 -0
  160. package/templates/vault/junta-leiloeiros/scripts/requirements.txt +15 -0
  161. package/templates/vault/junta-leiloeiros/scripts/run_all.py +190 -0
  162. package/templates/vault/junta-leiloeiros/scripts/scraper/__init__.py +4 -0
  163. package/templates/vault/junta-leiloeiros/scripts/scraper/base_scraper.py +209 -0
  164. package/templates/vault/junta-leiloeiros/scripts/scraper/generic_scraper.py +110 -0
  165. package/templates/vault/junta-leiloeiros/scripts/scraper/jucap.py +110 -0
  166. package/templates/vault/junta-leiloeiros/scripts/scraper/juceac.py +72 -0
  167. package/templates/vault/junta-leiloeiros/scripts/scraper/juceal.py +72 -0
  168. package/templates/vault/junta-leiloeiros/scripts/scraper/juceb.py +68 -0
  169. package/templates/vault/junta-leiloeiros/scripts/scraper/jucec.py +63 -0
  170. package/templates/vault/junta-leiloeiros/scripts/scraper/jucema.py +211 -0
  171. package/templates/vault/junta-leiloeiros/scripts/scraper/jucemg.py +218 -0
  172. package/templates/vault/junta-leiloeiros/scripts/scraper/jucep.py +70 -0
  173. package/templates/vault/junta-leiloeiros/scripts/scraper/jucepa.py +74 -0
  174. package/templates/vault/junta-leiloeiros/scripts/scraper/jucepar.py +80 -0
  175. package/templates/vault/junta-leiloeiros/scripts/scraper/jucepe.py +78 -0
  176. package/templates/vault/junta-leiloeiros/scripts/scraper/jucepi.py +69 -0
  177. package/templates/vault/junta-leiloeiros/scripts/scraper/jucer.py +256 -0
  178. package/templates/vault/junta-leiloeiros/scripts/scraper/jucerja.py +170 -0
  179. package/templates/vault/junta-leiloeiros/scripts/scraper/jucern.py +71 -0
  180. package/templates/vault/junta-leiloeiros/scripts/scraper/jucesc.py +89 -0
  181. package/templates/vault/junta-leiloeiros/scripts/scraper/jucesp.py +233 -0
  182. package/templates/vault/junta-leiloeiros/scripts/scraper/jucetins.py +134 -0
  183. package/templates/vault/junta-leiloeiros/scripts/scraper/jucis_df.py +63 -0
  184. package/templates/vault/junta-leiloeiros/scripts/scraper/jucisrs.py +299 -0
  185. package/templates/vault/junta-leiloeiros/scripts/scraper/states.py +99 -0
  186. package/templates/vault/junta-leiloeiros/scripts/serve_api.py +164 -0
  187. package/templates/vault/junta-leiloeiros/scripts/web_scraper_fallback.py +233 -0
  188. package/templates/vault/last30days/scripts/last30days.py +521 -0
  189. package/templates/vault/last30days/scripts/lib/__init__.py +1 -0
  190. package/templates/vault/last30days/scripts/lib/cache.py +152 -0
  191. package/templates/vault/last30days/scripts/lib/dates.py +124 -0
  192. package/templates/vault/last30days/scripts/lib/dedupe.py +120 -0
  193. package/templates/vault/last30days/scripts/lib/env.py +149 -0
  194. package/templates/vault/last30days/scripts/lib/http.py +152 -0
  195. package/templates/vault/last30days/scripts/lib/models.py +175 -0
  196. package/templates/vault/last30days/scripts/lib/normalize.py +160 -0
  197. package/templates/vault/last30days/scripts/lib/openai_reddit.py +230 -0
  198. package/templates/vault/last30days/scripts/lib/reddit_enrich.py +232 -0
  199. package/templates/vault/last30days/scripts/lib/render.py +383 -0
  200. package/templates/vault/last30days/scripts/lib/schema.py +336 -0
  201. package/templates/vault/last30days/scripts/lib/score.py +311 -0
  202. package/templates/vault/last30days/scripts/lib/ui.py +324 -0
  203. package/templates/vault/last30days/scripts/lib/websearch.py +401 -0
  204. package/templates/vault/last30days/scripts/lib/xai_x.py +217 -0
  205. package/templates/vault/leiloeiro-avaliacao/scripts/governance.py +106 -0
  206. package/templates/vault/leiloeiro-avaliacao/scripts/requirements.txt +1 -0
  207. package/templates/vault/leiloeiro-edital/scripts/governance.py +106 -0
  208. package/templates/vault/leiloeiro-edital/scripts/requirements.txt +1 -0
  209. package/templates/vault/leiloeiro-ia/scripts/governance.py +106 -0
  210. package/templates/vault/leiloeiro-ia/scripts/requirements.txt +1 -0
  211. package/templates/vault/leiloeiro-juridico/scripts/governance.py +106 -0
  212. package/templates/vault/leiloeiro-juridico/scripts/requirements.txt +1 -0
  213. package/templates/vault/leiloeiro-mercado/scripts/governance.py +106 -0
  214. package/templates/vault/leiloeiro-mercado/scripts/requirements.txt +1 -0
  215. package/templates/vault/leiloeiro-risco/scripts/governance.py +106 -0
  216. package/templates/vault/leiloeiro-risco/scripts/requirements.txt +1 -0
  217. package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/database.ts +24 -0
  218. package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/db.ts +35 -0
  219. package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/index.ts +2 -0
  220. package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/migrations.ts +31 -0
  221. package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/schema.sql +8 -0
  222. package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/index.ts +44 -0
  223. package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/routes/todos.ts +155 -0
  224. package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/types/index.ts +35 -0
  225. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/App.css +384 -0
  226. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/App.tsx +81 -0
  227. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/api/todos.ts +57 -0
  228. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/ConfirmDialog.tsx +26 -0
  229. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/EmptyState.tsx +8 -0
  230. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/TodoForm.tsx +43 -0
  231. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/TodoItem.tsx +36 -0
  232. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/TodoList.tsx +27 -0
  233. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/hooks/useTodos.ts +81 -0
  234. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/index.css +48 -0
  235. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/main.tsx +10 -0
  236. package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/vite-env.d.ts +1 -0
  237. package/templates/vault/loki-mode/scripts/export-to-vibe-kanban.sh +178 -0
  238. package/templates/vault/loki-mode/scripts/loki-wrapper.sh +281 -0
  239. package/templates/vault/loki-mode/scripts/take-screenshots.js +55 -0
  240. package/templates/vault/matematico-tao/scripts/complexity_analyzer.py +544 -0
  241. package/templates/vault/matematico-tao/scripts/dependency_graph.py +538 -0
  242. package/templates/vault/mcp-builder/scripts/connections.py +151 -0
  243. package/templates/vault/mcp-builder/scripts/evaluation.py +373 -0
  244. package/templates/vault/mcp-builder/scripts/example_evaluation.xml +22 -0
  245. package/templates/vault/mcp-builder/scripts/requirements.txt +2 -0
  246. package/templates/vault/mobile-design/scripts/mobile_audit.py +670 -0
  247. package/templates/vault/notebooklm/scripts/__init__.py +81 -0
  248. package/templates/vault/notebooklm/scripts/ask_question.py +256 -0
  249. package/templates/vault/notebooklm/scripts/auth_manager.py +358 -0
  250. package/templates/vault/notebooklm/scripts/browser_session.py +255 -0
  251. package/templates/vault/notebooklm/scripts/browser_utils.py +107 -0
  252. package/templates/vault/notebooklm/scripts/cleanup_manager.py +302 -0
  253. package/templates/vault/notebooklm/scripts/config.py +44 -0
  254. package/templates/vault/notebooklm/scripts/notebook_manager.py +410 -0
  255. package/templates/vault/notebooklm/scripts/run.py +102 -0
  256. package/templates/vault/notebooklm/scripts/setup_environment.py +204 -0
  257. package/templates/vault/pdf/scripts/check_bounding_boxes.py +70 -0
  258. package/templates/vault/pdf/scripts/check_bounding_boxes_test.py +226 -0
  259. package/templates/vault/pdf/scripts/check_fillable_fields.py +12 -0
  260. package/templates/vault/pdf/scripts/convert_pdf_to_images.py +35 -0
  261. package/templates/vault/pdf/scripts/create_validation_image.py +41 -0
  262. package/templates/vault/pdf/scripts/extract_form_field_info.py +152 -0
  263. package/templates/vault/pdf/scripts/fill_fillable_fields.py +114 -0
  264. package/templates/vault/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
  265. package/templates/vault/pdf-official/scripts/check_bounding_boxes.py +70 -0
  266. package/templates/vault/pdf-official/scripts/check_bounding_boxes_test.py +226 -0
  267. package/templates/vault/pdf-official/scripts/check_fillable_fields.py +12 -0
  268. package/templates/vault/pdf-official/scripts/convert_pdf_to_images.py +35 -0
  269. package/templates/vault/pdf-official/scripts/create_validation_image.py +41 -0
  270. package/templates/vault/pdf-official/scripts/extract_form_field_info.py +152 -0
  271. package/templates/vault/pdf-official/scripts/fill_fillable_fields.py +114 -0
  272. package/templates/vault/pdf-official/scripts/fill_pdf_form_with_annotations.py +108 -0
  273. package/templates/vault/planning-with-files/scripts/check-complete.sh +44 -0
  274. package/templates/vault/planning-with-files/scripts/init-session.sh +120 -0
  275. package/templates/vault/pptx/ooxml/scripts/pack.py +159 -0
  276. package/templates/vault/pptx/ooxml/scripts/unpack.py +29 -0
  277. package/templates/vault/pptx/ooxml/scripts/validate.py +69 -0
  278. package/templates/vault/pptx/ooxml/scripts/validation/__init__.py +15 -0
  279. package/templates/vault/pptx/ooxml/scripts/validation/base.py +951 -0
  280. package/templates/vault/pptx/ooxml/scripts/validation/docx.py +274 -0
  281. package/templates/vault/pptx/ooxml/scripts/validation/pptx.py +315 -0
  282. package/templates/vault/pptx/ooxml/scripts/validation/redlining.py +279 -0
  283. package/templates/vault/pptx/scripts/html2pptx.js +979 -0
  284. package/templates/vault/pptx/scripts/inventory.py +1020 -0
  285. package/templates/vault/pptx/scripts/rearrange.py +231 -0
  286. package/templates/vault/pptx/scripts/replace.py +385 -0
  287. package/templates/vault/pptx/scripts/thumbnail.py +450 -0
  288. package/templates/vault/pptx-official/ooxml/scripts/pack.py +159 -0
  289. package/templates/vault/pptx-official/ooxml/scripts/unpack.py +29 -0
  290. package/templates/vault/pptx-official/ooxml/scripts/validate.py +69 -0
  291. package/templates/vault/pptx-official/ooxml/scripts/validation/__init__.py +15 -0
  292. package/templates/vault/pptx-official/ooxml/scripts/validation/base.py +951 -0
  293. package/templates/vault/pptx-official/ooxml/scripts/validation/docx.py +274 -0
  294. package/templates/vault/pptx-official/ooxml/scripts/validation/pptx.py +315 -0
  295. package/templates/vault/pptx-official/ooxml/scripts/validation/redlining.py +279 -0
  296. package/templates/vault/pptx-official/scripts/html2pptx.js +979 -0
  297. package/templates/vault/pptx-official/scripts/inventory.py +1020 -0
  298. package/templates/vault/pptx-official/scripts/rearrange.py +231 -0
  299. package/templates/vault/pptx-official/scripts/replace.py +385 -0
  300. package/templates/vault/pptx-official/scripts/thumbnail.py +450 -0
  301. package/templates/vault/product-manager-toolkit/scripts/customer_interview_analyzer.py +441 -0
  302. package/templates/vault/product-manager-toolkit/scripts/rice_prioritizer.py +296 -0
  303. package/templates/vault/prompt-engineering-patterns/scripts/optimize-prompt.py +279 -0
  304. package/templates/vault/scripts/.skill_cache.json +7538 -0
  305. package/templates/vault/scripts/skill_search.py +228 -0
  306. package/templates/vault/senior-architect/scripts/architecture_diagram_generator.py +114 -0
  307. package/templates/vault/senior-architect/scripts/dependency_analyzer.py +114 -0
  308. package/templates/vault/senior-architect/scripts/project_architect.py +114 -0
  309. package/templates/vault/shopify-development/scripts/requirements.txt +19 -0
  310. package/templates/vault/shopify-development/scripts/shopify_graphql.py +428 -0
  311. package/templates/vault/shopify-development/scripts/shopify_init.py +441 -0
  312. package/templates/vault/shopify-development/scripts/tests/test_shopify_init.py +379 -0
  313. package/templates/vault/skill-creator/scripts/init_skill.py +303 -0
  314. package/templates/vault/skill-creator/scripts/package_skill.py +110 -0
  315. package/templates/vault/skill-creator/scripts/quick_validate.py +95 -0
  316. package/templates/vault/skill-installer/scripts/detect_skills.py +318 -0
  317. package/templates/vault/skill-installer/scripts/install_skill.py +1708 -0
  318. package/templates/vault/skill-installer/scripts/package_skill.py +417 -0
  319. package/templates/vault/skill-installer/scripts/requirements.txt +1 -0
  320. package/templates/vault/skill-installer/scripts/validate_skill.py +430 -0
  321. package/templates/vault/skill-sentinel/scripts/analyzers/__init__.py +13 -0
  322. package/templates/vault/skill-sentinel/scripts/analyzers/code_quality.py +247 -0
  323. package/templates/vault/skill-sentinel/scripts/analyzers/cross_skill.py +134 -0
  324. package/templates/vault/skill-sentinel/scripts/analyzers/dependencies.py +121 -0
  325. package/templates/vault/skill-sentinel/scripts/analyzers/documentation.py +189 -0
  326. package/templates/vault/skill-sentinel/scripts/analyzers/governance_audit.py +153 -0
  327. package/templates/vault/skill-sentinel/scripts/analyzers/performance.py +164 -0
  328. package/templates/vault/skill-sentinel/scripts/analyzers/security.py +189 -0
  329. package/templates/vault/skill-sentinel/scripts/config.py +158 -0
  330. package/templates/vault/skill-sentinel/scripts/cost_optimizer.py +146 -0
  331. package/templates/vault/skill-sentinel/scripts/db.py +354 -0
  332. package/templates/vault/skill-sentinel/scripts/governance.py +58 -0
  333. package/templates/vault/skill-sentinel/scripts/recommender.py +228 -0
  334. package/templates/vault/skill-sentinel/scripts/report_generator.py +224 -0
  335. package/templates/vault/skill-sentinel/scripts/requirements.txt +1 -0
  336. package/templates/vault/skill-sentinel/scripts/run_audit.py +290 -0
  337. package/templates/vault/skill-sentinel/scripts/scanner.py +271 -0
  338. package/templates/vault/stability-ai/scripts/config.py +266 -0
  339. package/templates/vault/stability-ai/scripts/generate.py +687 -0
  340. package/templates/vault/stability-ai/scripts/requirements.txt +4 -0
  341. package/templates/vault/stability-ai/scripts/styles.py +174 -0
  342. package/templates/vault/telegram/assets/boilerplate/nodejs/src/bot-client.ts +86 -0
  343. package/templates/vault/telegram/assets/boilerplate/nodejs/src/handlers.ts +79 -0
  344. package/templates/vault/telegram/assets/boilerplate/nodejs/src/index.ts +32 -0
  345. package/templates/vault/telegram/scripts/send_message.py +143 -0
  346. package/templates/vault/telegram/scripts/setup_project.py +103 -0
  347. package/templates/vault/telegram/scripts/test_bot.py +144 -0
  348. package/templates/vault/typescript-expert/scripts/ts_diagnostic.py +203 -0
  349. package/templates/vault/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc +0 -0
  350. package/templates/vault/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
  351. package/templates/vault/ui-ux-pro-max/scripts/core.py +257 -0
  352. package/templates/vault/ui-ux-pro-max/scripts/design_system.py +487 -0
  353. package/templates/vault/ui-ux-pro-max/scripts/search.py +76 -0
  354. package/templates/vault/videodb/scripts/ws_listener.py +204 -0
  355. package/templates/vault/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
  356. package/templates/vault/web-artifacts-builder/scripts/init-artifact.sh +322 -0
  357. package/templates/vault/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  358. package/templates/vault/webapp-testing/scripts/with_server.py +106 -0
  359. package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/index.ts +125 -0
  360. package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/template-manager.ts +67 -0
  361. package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/types.ts +216 -0
  362. package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts +173 -0
  363. package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/whatsapp-client.ts +193 -0
  364. package/templates/vault/whatsapp-cloud-api/scripts/send_test_message.py +137 -0
  365. package/templates/vault/whatsapp-cloud-api/scripts/setup_project.py +118 -0
  366. package/templates/vault/whatsapp-cloud-api/scripts/validate_config.py +190 -0
  367. package/templates/vault/youtube-summarizer/scripts/extract-transcript.py +65 -0
  368. package/templates/vault/youtube-summarizer/scripts/install-dependencies.sh +28 -0
@@ -0,0 +1,1708 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Skill Installer v3.0 - Enterprise-grade installer with 11-step redundant workflow.
4
+
5
+ Detects, validates, copies, registers, and verifies skills in the ecosystem
6
+ with maximum redundancy, safety, auto-repair, rollback, and rich diagnostics.
7
+
8
+ Usage:
9
+ python install_skill.py --source "C:\\path\\to\\skill"
10
+ python install_skill.py --source "C:\\path" --name "my-skill"
11
+ python install_skill.py --source "C:\\path" --force
12
+ python install_skill.py --source "C:\\path" --dry-run
13
+ python install_skill.py --detect
14
+ python install_skill.py --detect --auto
15
+ python install_skill.py --uninstall "skill-name"
16
+ python install_skill.py --health
17
+ python install_skill.py --health --repair
18
+ python install_skill.py --rollback "skill-name"
19
+ python install_skill.py --reinstall-all
20
+ python install_skill.py --status
21
+ python install_skill.py --log [N]
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import os
27
+ import sys
28
+ import json
29
+ import shutil
30
+ import hashlib
31
+ import subprocess
32
+ import re
33
+ from pathlib import Path
34
+ from datetime import datetime
35
+
36
+ # Add scripts directory to path for imports
37
+ SCRIPT_DIR = Path(__file__).parent.resolve()
38
+ sys.path.insert(0, str(SCRIPT_DIR))
39
+
40
+ from validate_skill import validate, parse_yaml_frontmatter
41
+ from detect_skills import detect
42
+
43
+ # ── Configuration ──────────────────────────────────────────────────────────
44
+
45
+ SKILLS_ROOT = Path(r"C:\Users\renat\skills")
46
+ CLAUDE_SKILLS = SKILLS_ROOT / ".claude" / "skills"
47
+ INSTALLER_DIR = SKILLS_ROOT / "skill-installer"
48
+ DATA_DIR = INSTALLER_DIR / "data"
49
+ BACKUPS_DIR = DATA_DIR / "backups"
50
+ STAGING_DIR = DATA_DIR / "staging"
51
+ LOG_PATH = DATA_DIR / "install_log.json"
52
+ SCAN_SCRIPT = SKILLS_ROOT / "agent-orchestrator" / "scripts" / "scan_registry.py"
53
+ REGISTRY_PATH = SKILLS_ROOT / "agent-orchestrator" / "data" / "registry.json"
54
+
55
+ MAX_BACKUPS_PER_SKILL = 5
56
+ MAX_LOG_ENTRIES = 500 # Log rotation threshold
57
+ VERSION = "3.0.0"
58
+
59
+
60
+ # ── Console Colors ─────────────────────────────────────────────────────────
61
+
62
+ class _C:
63
+ """ANSI color codes for terminal output. Degrades gracefully on Windows."""
64
+ _enabled = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
65
+ # Check if stdout can handle UTF-8 symbols
66
+ _utf8 = False
67
+ try:
68
+ _utf8 = sys.stdout.encoding and sys.stdout.encoding.lower().replace("-", "") in ("utf8", "utf16")
69
+ except Exception:
70
+ pass
71
+
72
+ @staticmethod
73
+ def _wrap(code: str, text: str) -> str:
74
+ if _C._enabled:
75
+ return f"\033[{code}m{text}\033[0m"
76
+ return text
77
+
78
+ @staticmethod
79
+ def green(t: str) -> str: return _C._wrap("32", t)
80
+ @staticmethod
81
+ def red(t: str) -> str: return _C._wrap("31", t)
82
+ @staticmethod
83
+ def yellow(t: str) -> str: return _C._wrap("33", t)
84
+ @staticmethod
85
+ def cyan(t: str) -> str: return _C._wrap("36", t)
86
+ @staticmethod
87
+ def bold(t: str) -> str: return _C._wrap("1", t)
88
+ @staticmethod
89
+ def dim(t: str) -> str: return _C._wrap("2", t)
90
+
91
+ # ASCII-safe symbols for Windows cp1252 compatibility
92
+ OK = "[OK]"
93
+ FAIL = "[FAIL]"
94
+ WARN = "[WARN]"
95
+
96
+
97
+ def _step(n: int, total: int, msg: str):
98
+ """Print a step progress indicator."""
99
+ print(f" {_C.cyan(f'[{n}/{total}]')} {msg}")
100
+
101
+
102
+ def _ok(msg: str):
103
+ print(f" {_C.green(_C.OK)} {msg}")
104
+
105
+
106
+ def _warn(msg: str):
107
+ print(f" {_C.yellow(_C.WARN)} {msg}")
108
+
109
+
110
+ def _fail(msg: str):
111
+ print(f" {_C.red(_C.FAIL)} {msg}")
112
+
113
+
114
+ # ── Utility Functions ──────────────────────────────────────────────────────
115
+
116
+ def sanitize_name(name: str) -> str:
117
+ """Sanitize skill name: lowercase, hyphens, no spaces."""
118
+ name = name.strip().lower()
119
+ name = name.replace(" ", "-")
120
+ name = name.replace("_", "-")
121
+ # Remove any chars that aren't alphanumeric or hyphens
122
+ name = "".join(c for c in name if c.isalnum() or c == "-")
123
+ # Remove leading/trailing hyphens and collapse multiples
124
+ while "--" in name:
125
+ name = name.replace("--", "-")
126
+ return name.strip("-")
127
+
128
+
129
+ def md5_dir(path: Path, exclude_dirs: set = None) -> str:
130
+ """Compute combined MD5 hash of all files in a directory.
131
+
132
+ Excludes backup/staging dirs and normalizes paths to forward slashes
133
+ for cross-platform consistency.
134
+ """
135
+ if exclude_dirs is None:
136
+ exclude_dirs = {"backups", "staging", ".git", "__pycache__", "node_modules", ".venv"}
137
+
138
+ h = hashlib.md5()
139
+ for root, dirs, files in os.walk(path):
140
+ # Filter out excluded directories
141
+ dirs[:] = [d for d in dirs if d not in exclude_dirs]
142
+ for f in sorted(files):
143
+ fp = Path(root) / f
144
+ try:
145
+ # Normalize to forward slashes for consistent hashing
146
+ rel = fp.relative_to(path).as_posix()
147
+ h.update(rel.encode("utf-8"))
148
+ with open(fp, "rb") as fh:
149
+ for chunk in iter(lambda: fh.read(8192), b""):
150
+ h.update(chunk)
151
+ except Exception:
152
+ pass
153
+ return h.hexdigest()
154
+
155
+
156
+ def parse_version(ver: str) -> tuple:
157
+ """Parse a semver string into a comparable tuple.
158
+
159
+ Examples: '1.0.0' -> (1,0,0), '2.1' -> (2,1,0), '' -> (0,0,0)
160
+ """
161
+ if not ver:
162
+ return (0, 0, 0)
163
+ parts = re.findall(r'\d+', str(ver))
164
+ while len(parts) < 3:
165
+ parts.append("0")
166
+ try:
167
+ return tuple(int(p) for p in parts[:3])
168
+ except (ValueError, TypeError):
169
+ return (0, 0, 0)
170
+
171
+
172
+ def compare_versions(installed: str, source: str) -> str:
173
+ """Compare two version strings.
174
+
175
+ Returns: 'same', 'upgrade', 'downgrade', or 'unknown'.
176
+ """
177
+ inst = parse_version(installed)
178
+ src = parse_version(source)
179
+
180
+ if inst == (0, 0, 0) or src == (0, 0, 0):
181
+ return "unknown"
182
+ if inst == src:
183
+ return "same"
184
+ if src > inst:
185
+ return "upgrade"
186
+ return "downgrade"
187
+
188
+
189
+ def load_log() -> list:
190
+ """Load install log."""
191
+ if LOG_PATH.exists():
192
+ try:
193
+ data = json.loads(LOG_PATH.read_text(encoding="utf-8"))
194
+ return data.get("operations", [])
195
+ except Exception:
196
+ pass
197
+ return []
198
+
199
+
200
+ def save_log(operations: list):
201
+ """Save install log with rotation (keeps last MAX_LOG_ENTRIES)."""
202
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
203
+ # Rotate: keep only the last N entries
204
+ if len(operations) > MAX_LOG_ENTRIES:
205
+ operations = operations[-MAX_LOG_ENTRIES:]
206
+ data = {
207
+ "version": VERSION,
208
+ "operations": operations,
209
+ "total_operations": len(operations),
210
+ "last_updated": datetime.now().isoformat(),
211
+ }
212
+ LOG_PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
213
+
214
+
215
+ def append_log(entry: dict):
216
+ """Append entry to install log."""
217
+ ops = load_log()
218
+ ops.append(entry)
219
+ save_log(ops)
220
+
221
+
222
+ def cleanup_old_backups(skill_name: str):
223
+ """Keep only the last N backups for a skill."""
224
+ if not BACKUPS_DIR.exists():
225
+ return
226
+
227
+ prefix = f"{skill_name}_"
228
+ backups = sorted(
229
+ [d for d in BACKUPS_DIR.iterdir() if d.is_dir() and d.name.startswith(prefix)],
230
+ key=lambda d: d.stat().st_mtime,
231
+ )
232
+
233
+ while len(backups) > MAX_BACKUPS_PER_SKILL:
234
+ old = backups.pop(0)
235
+ try:
236
+ shutil.rmtree(old)
237
+ except Exception:
238
+ pass
239
+
240
+
241
+ def get_all_skill_dirs() -> list:
242
+ """Get all skill directories in the ecosystem (top-level + nested)."""
243
+ dirs = []
244
+ for item in sorted(SKILLS_ROOT.iterdir()):
245
+ if not item.is_dir() or item.name.startswith("."):
246
+ continue
247
+ if item.name == "agent-orchestrator":
248
+ continue
249
+ skill_md = item / "SKILL.md"
250
+ if skill_md.exists():
251
+ dirs.append(item)
252
+ # Check nested (e.g., juntas-comerciais/junta-leiloeiros)
253
+ for child in item.iterdir():
254
+ if child.is_dir() and (child / "SKILL.md").exists():
255
+ if child not in dirs:
256
+ dirs.append(child)
257
+ return dirs
258
+
259
+
260
+ # ── Installation Steps ─────────────────────────────────────────────────────
261
+
262
+ def step1_resolve_source(source: str = None, do_detect: bool = False, auto: bool = False) -> dict:
263
+ """STEP 1: Resolve source directory."""
264
+ if source:
265
+ source_path = Path(source).resolve()
266
+ if not source_path.exists():
267
+ return {"success": False, "error": f"Source does not exist: {source_path}"}
268
+ if not (source_path / "SKILL.md").exists():
269
+ return {"success": False, "error": f"No SKILL.md found in {source_path}"}
270
+ return {"success": True, "sources": [str(source_path)]}
271
+
272
+ if do_detect:
273
+ result = detect()
274
+ candidates = [c for c in result["candidates"] if not c["already_installed"]]
275
+
276
+ if not candidates:
277
+ return {
278
+ "success": False,
279
+ "error": "No uninstalled skills detected",
280
+ "scanned_locations": result.get("scanned_locations", []),
281
+ }
282
+
283
+ if auto:
284
+ return {
285
+ "success": True,
286
+ "sources": [c["source_path"] for c in candidates],
287
+ "candidates": candidates,
288
+ }
289
+
290
+ # Return candidates for user to choose
291
+ return {
292
+ "success": True,
293
+ "sources": [c["source_path"] for c in candidates],
294
+ "candidates": candidates,
295
+ "interactive": True,
296
+ }
297
+
298
+ return {"success": False, "error": "No --source or --detect provided"}
299
+
300
+
301
+ def step2_validate(source_path: Path) -> dict:
302
+ """STEP 2: Validate the skill."""
303
+ result = validate(source_path)
304
+ return result
305
+
306
+
307
+ def step3_determine_name(source_path: Path, name_override: str = None) -> str:
308
+ """STEP 3: Determine skill name."""
309
+ if name_override:
310
+ return sanitize_name(name_override)
311
+
312
+ meta = parse_yaml_frontmatter(source_path / "SKILL.md")
313
+ name = meta.get("name", source_path.name)
314
+ return sanitize_name(name)
315
+
316
+
317
+ def step4_check_conflicts(skill_name: str) -> dict:
318
+ """STEP 4: Check for existing skill with same name."""
319
+ dest = SKILLS_ROOT / skill_name
320
+ claude_dest = CLAUDE_SKILLS / skill_name
321
+
322
+ conflicts = []
323
+ if dest.exists():
324
+ conflicts.append(str(dest))
325
+ if claude_dest.exists():
326
+ conflicts.append(str(claude_dest))
327
+
328
+ return {
329
+ "has_conflicts": len(conflicts) > 0,
330
+ "conflicts": conflicts,
331
+ "destination": str(dest),
332
+ "claude_destination": str(claude_dest),
333
+ }
334
+
335
+
336
+ def _backup_ignore(directory, contents):
337
+ """Ignore function for shutil.copytree to skip backup/staging dirs."""
338
+ ignored = set()
339
+ dir_path = Path(directory)
340
+ for item in contents:
341
+ item_path = dir_path / item
342
+ # Skip backup and staging directories to prevent recursion
343
+ if item in ("backups", "staging") and dir_path.name == "data":
344
+ ignored.add(item)
345
+ # Skip .git and __pycache__
346
+ if item in (".git", "__pycache__", "node_modules", ".venv"):
347
+ ignored.add(item)
348
+ return ignored
349
+
350
+
351
+ def step5_backup(skill_name: str) -> dict:
352
+ """STEP 5: Backup existing skill before overwrite."""
353
+ dest = SKILLS_ROOT / skill_name
354
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
355
+ backup_name = f"{skill_name}_{timestamp}"
356
+ backup_path = BACKUPS_DIR / backup_name
357
+
358
+ BACKUPS_DIR.mkdir(parents=True, exist_ok=True)
359
+
360
+ backed_up = []
361
+
362
+ if dest.exists():
363
+ try:
364
+ shutil.copytree(dest, backup_path, ignore=_backup_ignore, dirs_exist_ok=True)
365
+ backed_up.append(str(dest))
366
+ except Exception as e:
367
+ return {"success": False, "error": f"Backup failed for {dest}: {e}"}
368
+
369
+ claude_dest = CLAUDE_SKILLS / skill_name
370
+ if claude_dest.exists():
371
+ claude_backup = backup_path / ".claude-registration"
372
+ claude_backup.mkdir(parents=True, exist_ok=True)
373
+ try:
374
+ shutil.copytree(claude_dest, claude_backup / skill_name, dirs_exist_ok=True)
375
+ backed_up.append(str(claude_dest))
376
+ except Exception as e:
377
+ return {"success": False, "error": f"Backup failed for {claude_dest}: {e}"}
378
+
379
+ # Cleanup old backups
380
+ cleanup_old_backups(skill_name)
381
+
382
+ return {
383
+ "success": True,
384
+ "backup_path": str(backup_path),
385
+ "backed_up": backed_up,
386
+ }
387
+
388
+
389
+ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict:
390
+ """STEP 6: Copy to skills root via staging area."""
391
+ dest = SKILLS_ROOT / skill_name
392
+ staging = STAGING_DIR / skill_name
393
+
394
+ STAGING_DIR.mkdir(parents=True, exist_ok=True)
395
+
396
+ # Clean staging
397
+ if staging.exists():
398
+ shutil.rmtree(staging)
399
+
400
+ # Copy to staging first (skip backups/staging to prevent recursion)
401
+ try:
402
+ shutil.copytree(source_path, staging, ignore=_backup_ignore, dirs_exist_ok=True)
403
+ except Exception as e:
404
+ return {"success": False, "error": f"Copy to staging failed: {e}"}
405
+
406
+ # Validate staging copy
407
+ staging_skill_md = staging / "SKILL.md"
408
+ if not staging_skill_md.exists():
409
+ shutil.rmtree(staging, ignore_errors=True)
410
+ return {"success": False, "error": "SKILL.md missing after copy to staging"}
411
+
412
+ # Verify hash matches
413
+ source_hash = md5_dir(source_path)
414
+ staging_hash = md5_dir(staging)
415
+ if source_hash != staging_hash:
416
+ shutil.rmtree(staging, ignore_errors=True)
417
+ return {
418
+ "success": False,
419
+ "error": f"Hash mismatch: source={source_hash} staging={staging_hash}",
420
+ }
421
+
422
+ # Remove existing destination if exists
423
+ if dest.exists():
424
+ try:
425
+ shutil.rmtree(dest)
426
+ except Exception as e:
427
+ shutil.rmtree(staging, ignore_errors=True)
428
+ return {"success": False, "error": f"Cannot remove existing destination: {e}"}
429
+
430
+ # Move staging to final destination
431
+ try:
432
+ shutil.move(str(staging), str(dest))
433
+ except Exception as e:
434
+ # Try copy + delete as fallback (cross-device moves)
435
+ try:
436
+ shutil.copytree(staging, dest, dirs_exist_ok=True)
437
+ shutil.rmtree(staging, ignore_errors=True)
438
+ except Exception as e2:
439
+ shutil.rmtree(staging, ignore_errors=True)
440
+ return {"success": False, "error": f"Move failed: {e}, copy fallback failed: {e2}"}
441
+
442
+ return {
443
+ "success": True,
444
+ "installed_to": str(dest),
445
+ "hash": source_hash,
446
+ }
447
+
448
+
449
+ def step7_register_claude(skill_name: str) -> dict:
450
+ """STEP 7: Register in .claude/skills/ for native Claude Code discovery."""
451
+ source_skill_md = SKILLS_ROOT / skill_name / "SKILL.md"
452
+ claude_dest_dir = CLAUDE_SKILLS / skill_name
453
+
454
+ if not source_skill_md.exists():
455
+ return {"success": False, "error": f"SKILL.md not found at {source_skill_md}"}
456
+
457
+ claude_dest_dir.mkdir(parents=True, exist_ok=True)
458
+
459
+ # Copy SKILL.md
460
+ try:
461
+ shutil.copy2(source_skill_md, claude_dest_dir / "SKILL.md")
462
+ except Exception as e:
463
+ return {"success": False, "error": f"Failed to copy SKILL.md to Claude skills: {e}"}
464
+
465
+ # Also copy references/ if it exists (useful for Claude to read)
466
+ refs_dir = SKILLS_ROOT / skill_name / "references"
467
+ if refs_dir.exists():
468
+ claude_refs = claude_dest_dir / "references"
469
+ try:
470
+ if claude_refs.exists():
471
+ shutil.rmtree(claude_refs)
472
+ shutil.copytree(refs_dir, claude_refs)
473
+ except Exception:
474
+ pass # Non-critical
475
+
476
+ return {
477
+ "success": True,
478
+ "registered_at": str(claude_dest_dir),
479
+ "files_registered": ["SKILL.md"] + (
480
+ ["references/"] if refs_dir.exists() else []
481
+ ),
482
+ }
483
+
484
+
485
+ def step8_update_registry() -> dict:
486
+ """STEP 8: Run scan_registry.py to update orchestrator registry."""
487
+ if not SCAN_SCRIPT.exists():
488
+ return {
489
+ "success": False,
490
+ "error": f"scan_registry.py not found at {SCAN_SCRIPT}",
491
+ }
492
+
493
+ try:
494
+ result = subprocess.run(
495
+ ["python", str(SCAN_SCRIPT), "--force"],
496
+ capture_output=True,
497
+ text=True,
498
+ timeout=30,
499
+ cwd=str(SKILLS_ROOT),
500
+ )
501
+ if result.returncode == 0:
502
+ try:
503
+ scan_output = json.loads(result.stdout)
504
+ except json.JSONDecodeError:
505
+ scan_output = {"raw": result.stdout[:500]}
506
+ return {"success": True, "scan_output": scan_output}
507
+ else:
508
+ return {
509
+ "success": False,
510
+ "error": f"scan_registry.py failed: {result.stderr[:500]}",
511
+ }
512
+ except subprocess.TimeoutExpired:
513
+ return {"success": False, "error": "scan_registry.py timed out (30s)"}
514
+ except Exception as e:
515
+ return {"success": False, "error": f"Failed to run scan_registry.py: {e}"}
516
+
517
+
518
+ def step9_verify(skill_name: str) -> dict:
519
+ """STEP 9: Verify installation is complete and correct."""
520
+ checks = []
521
+
522
+ # Check 1: Skill directory exists
523
+ dest = SKILLS_ROOT / skill_name
524
+ checks.append({
525
+ "check": "skill_dir_exists",
526
+ "pass": dest.exists(),
527
+ "path": str(dest),
528
+ })
529
+
530
+ # Check 2: SKILL.md exists and is readable
531
+ skill_md = dest / "SKILL.md"
532
+ skill_md_ok = False
533
+ if skill_md.exists():
534
+ try:
535
+ text = skill_md.read_text(encoding="utf-8")
536
+ skill_md_ok = len(text) > 10
537
+ except Exception:
538
+ pass
539
+ checks.append({
540
+ "check": "skill_md_readable",
541
+ "pass": skill_md_ok,
542
+ "path": str(skill_md),
543
+ })
544
+
545
+ # Check 3: Frontmatter parseable
546
+ meta = parse_yaml_frontmatter(skill_md) if skill_md.exists() else {}
547
+ checks.append({
548
+ "check": "frontmatter_parseable",
549
+ "pass": bool(meta.get("name")),
550
+ "name": meta.get("name", ""),
551
+ })
552
+
553
+ # Check 4: Claude Code registration
554
+ claude_skill_md = CLAUDE_SKILLS / skill_name / "SKILL.md"
555
+ checks.append({
556
+ "check": "claude_registered",
557
+ "pass": claude_skill_md.exists(),
558
+ "path": str(claude_skill_md),
559
+ })
560
+
561
+ # Check 5: Appears in registry
562
+ in_registry = False
563
+ if REGISTRY_PATH.exists():
564
+ try:
565
+ registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
566
+ skill_names = [s.get("name", "").lower() for s in registry.get("skills", [])]
567
+ in_registry = skill_name.lower() in skill_names
568
+ except Exception:
569
+ pass
570
+ checks.append({
571
+ "check": "in_registry",
572
+ "pass": in_registry,
573
+ })
574
+
575
+ all_passed = all(c["pass"] for c in checks)
576
+
577
+ return {
578
+ "success": all_passed,
579
+ "checks": checks,
580
+ "total": len(checks),
581
+ "passed": sum(1 for c in checks if c["pass"]),
582
+ "failed": sum(1 for c in checks if not c["pass"]),
583
+ }
584
+
585
+
586
+ def step10_log(skill_name: str, source: str, result: dict):
587
+ """STEP 10: Log the operation."""
588
+ entry = {
589
+ "timestamp": datetime.now().isoformat(),
590
+ "action": "install",
591
+ "skill_name": skill_name,
592
+ "source": source,
593
+ "destination": str(SKILLS_ROOT / skill_name),
594
+ "registered": result.get("registered", False),
595
+ "registry_updated": result.get("registry_updated", False),
596
+ "backup_path": result.get("backup_path"),
597
+ "success": result.get("success", False),
598
+ "verification": result.get("verification", {}),
599
+ "warnings": result.get("warnings", []),
600
+ }
601
+
602
+ try:
603
+ append_log(entry)
604
+ except Exception:
605
+ pass # Logging failure is non-critical
606
+
607
+ return entry
608
+
609
+
610
+ # ── Main Install Workflow ──────────────────────────────────────────────────
611
+
612
+ def install_single(
613
+ source_path: str,
614
+ name_override: str = None,
615
+ force: bool = False,
616
+ dry_run: bool = False,
617
+ verbose: bool = True,
618
+ ) -> dict:
619
+ """Install a single skill through the 11-step workflow.
620
+
621
+ Args:
622
+ source_path: Path to skill directory containing SKILL.md.
623
+ name_override: Optional name to use instead of frontmatter name.
624
+ force: If True, overwrite existing skill (backup first).
625
+ dry_run: If True, simulate all steps without writing anything.
626
+ verbose: If True, print step-by-step progress to stdout.
627
+ """
628
+ source = Path(source_path).resolve()
629
+ total_steps = 11
630
+ result = {
631
+ "success": False,
632
+ "skill_name": "",
633
+ "installed_to": "",
634
+ "registered": False,
635
+ "registry_updated": False,
636
+ "backup_path": None,
637
+ "warnings": [],
638
+ "steps": {},
639
+ "dry_run": dry_run,
640
+ "installer_version": VERSION,
641
+ }
642
+
643
+ if dry_run and verbose:
644
+ print(f"\n{_C.bold(_C.yellow('=== DRY RUN MODE === No changes will be made'))}\n")
645
+
646
+ # STEP 1: Already resolved (source is provided)
647
+ if verbose:
648
+ _step(1, total_steps, "Resolving source...")
649
+ if not source.exists() or not (source / "SKILL.md").exists():
650
+ result["error"] = f"Invalid source: {source}"
651
+ if verbose:
652
+ _fail(f"Source invalid: {source}")
653
+ return result
654
+
655
+ result["steps"]["1_resolve"] = {"success": True, "source": str(source)}
656
+ if verbose:
657
+ _ok(f"Source: {source}")
658
+
659
+ # STEP 2: Validate
660
+ if verbose:
661
+ _step(2, total_steps, "Validating skill...")
662
+ validation = step2_validate(source)
663
+ result["steps"]["2_validate"] = validation
664
+
665
+ if not validation["valid"]:
666
+ result["error"] = f"Validation failed: {'; '.join(validation['errors'])}"
667
+ result["warnings"] = validation.get("warnings", [])
668
+ if verbose:
669
+ _fail(f"Validation failed: {len(validation['errors'])} error(s)")
670
+ for e in validation["errors"]:
671
+ _fail(f" {e}")
672
+ return result
673
+
674
+ if verbose:
675
+ _ok(f"Validation passed ({validation['passed']}/{validation['total_checks']} checks)")
676
+ for w in validation.get("warnings", []):
677
+ _warn(f" {w}")
678
+
679
+ result["warnings"].extend(validation.get("warnings", []))
680
+
681
+ # STEP 3: Determine name
682
+ if verbose:
683
+ _step(3, total_steps, "Determining skill name...")
684
+ skill_name = step3_determine_name(source, name_override)
685
+ result["skill_name"] = skill_name
686
+ result["steps"]["3_name"] = {"name": skill_name}
687
+
688
+ if not skill_name:
689
+ result["error"] = "Could not determine skill name"
690
+ if verbose:
691
+ _fail("Could not determine skill name")
692
+ return result
693
+ if verbose:
694
+ _ok(f"Name: {_C.bold(skill_name)}")
695
+
696
+ # Version comparison with installed
697
+ source_meta = parse_yaml_frontmatter(source / "SKILL.md")
698
+ source_version = source_meta.get("version", "")
699
+ dest = SKILLS_ROOT / skill_name
700
+ if dest.exists() and (dest / "SKILL.md").exists():
701
+ installed_meta = parse_yaml_frontmatter(dest / "SKILL.md")
702
+ installed_version = installed_meta.get("version", "")
703
+ ver_cmp = compare_versions(installed_version, source_version)
704
+ result["version_comparison"] = {
705
+ "installed": installed_version,
706
+ "source": source_version,
707
+ "result": ver_cmp,
708
+ }
709
+ if verbose and ver_cmp != "unknown":
710
+ if ver_cmp == "upgrade":
711
+ _ok(f"Version: {installed_version} -> {_C.green(source_version)} (upgrade)")
712
+ elif ver_cmp == "downgrade":
713
+ _warn(f"Version: {installed_version} -> {_C.yellow(source_version)} (downgrade)")
714
+ elif ver_cmp == "same":
715
+ _ok(f"Version: {source_version} (same)")
716
+
717
+ # STEP 4: Check conflicts
718
+ if verbose:
719
+ _step(4, total_steps, "Checking conflicts...")
720
+ conflicts = step4_check_conflicts(skill_name)
721
+ result["steps"]["4_conflicts"] = conflicts
722
+
723
+ if conflicts["has_conflicts"] and not force:
724
+ result["error"] = (
725
+ f"Skill '{skill_name}' already exists at: {', '.join(conflicts['conflicts'])}. "
726
+ f"Use --force to overwrite."
727
+ )
728
+ if verbose:
729
+ _fail(f"Conflict: skill already exists. Use --force to overwrite.")
730
+ return result
731
+ if verbose:
732
+ if conflicts["has_conflicts"]:
733
+ _warn(f"Conflict detected -- will overwrite (--force)")
734
+ else:
735
+ _ok("No conflicts")
736
+
737
+ # STEP 5: Backup (if overwriting)
738
+ if verbose:
739
+ _step(5, total_steps, "Creating backup...")
740
+ backup_result = {"success": True, "backup_path": None}
741
+ if conflicts["has_conflicts"] and force:
742
+ if dry_run:
743
+ backup_result = {"success": True, "backup_path": "(dry-run)", "dry_run": True}
744
+ if verbose:
745
+ _ok("Backup would be created (dry-run)")
746
+ else:
747
+ backup_result = step5_backup(skill_name)
748
+ if not backup_result["success"]:
749
+ result["error"] = f"Backup failed: {backup_result.get('error')}"
750
+ if verbose:
751
+ _fail(f"Backup failed: {backup_result.get('error')}")
752
+ return result
753
+ result["backup_path"] = backup_result.get("backup_path")
754
+ if verbose:
755
+ _ok(f"Backup saved: {backup_result.get('backup_path', '?')}")
756
+ else:
757
+ if verbose:
758
+ _ok("No backup needed (new install)")
759
+
760
+ result["steps"]["5_backup"] = backup_result
761
+
762
+ # Check idempotency: same content?
763
+ idempotent = False
764
+ if dest.exists():
765
+ source_hash = md5_dir(source)
766
+ dest_hash = md5_dir(dest)
767
+ if source_hash == dest_hash:
768
+ idempotent = True
769
+ result["idempotent"] = True
770
+ result["installed_to"] = str(dest)
771
+ result["steps"]["6_copy"] = {
772
+ "success": True,
773
+ "installed_to": str(dest),
774
+ "skipped": "identical content already at destination",
775
+ "hash": source_hash,
776
+ }
777
+ if verbose:
778
+ _ok("Content identical -- skipping copy")
779
+
780
+ # STEP 6: Copy to skills root (skip if idempotent)
781
+ if not idempotent:
782
+ if verbose:
783
+ _step(6, total_steps, "Copying to skills root via staging...")
784
+ if dry_run:
785
+ result["steps"]["6_copy"] = {
786
+ "success": True,
787
+ "installed_to": str(dest),
788
+ "dry_run": True,
789
+ }
790
+ result["installed_to"] = str(dest)
791
+ if verbose:
792
+ _ok(f"Would copy to: {dest} (dry-run)")
793
+ else:
794
+ copy_result = step6_copy_to_skills_root(source, skill_name)
795
+ result["steps"]["6_copy"] = copy_result
796
+
797
+ if not copy_result["success"]:
798
+ result["error"] = f"Copy failed: {copy_result.get('error')}"
799
+ if verbose:
800
+ _fail(f"Copy failed: {copy_result.get('error')}")
801
+ step10_log(skill_name, str(source), result)
802
+ return result
803
+
804
+ result["installed_to"] = copy_result["installed_to"]
805
+ if verbose:
806
+ _ok(f"Copied to: {copy_result['installed_to']}")
807
+ elif verbose and not idempotent:
808
+ _step(6, total_steps, "Copying to skills root...")
809
+
810
+ # STEP 7: Register in Claude Code (ALWAYS runs, even if idempotent)
811
+ if verbose:
812
+ _step(7, total_steps, "Registering in Claude Code CLI...")
813
+ if dry_run:
814
+ result["steps"]["7_register"] = {"success": True, "dry_run": True}
815
+ result["registered"] = True
816
+ if verbose:
817
+ _ok("Would register in .claude/skills/ (dry-run)")
818
+ else:
819
+ register_result = step7_register_claude(skill_name)
820
+ result["steps"]["7_register"] = register_result
821
+ result["registered"] = register_result["success"]
822
+
823
+ if not register_result["success"]:
824
+ result["warnings"].append(f"Registration warning: {register_result.get('error')}")
825
+ if verbose:
826
+ _warn(f"Registration: {register_result.get('error')}")
827
+ elif verbose:
828
+ _ok(f"Registered at: {register_result.get('registered_at')}")
829
+
830
+ # STEP 8: Update orchestrator registry
831
+ if verbose:
832
+ _step(8, total_steps, "Updating orchestrator registry...")
833
+ if dry_run:
834
+ result["steps"]["8_registry"] = {"success": True, "dry_run": True}
835
+ result["registry_updated"] = True
836
+ if verbose:
837
+ _ok("Would update registry (dry-run)")
838
+ else:
839
+ registry_result = step8_update_registry()
840
+ result["steps"]["8_registry"] = registry_result
841
+ result["registry_updated"] = registry_result["success"]
842
+
843
+ if not registry_result["success"]:
844
+ result["warnings"].append(f"Registry update warning: {registry_result.get('error')}")
845
+ if verbose:
846
+ _warn(f"Registry: {registry_result.get('error')}")
847
+ elif verbose:
848
+ _ok("Registry updated")
849
+
850
+ # STEP 9: Verify installation
851
+ if verbose:
852
+ _step(9, total_steps, "Verifying installation...")
853
+ if dry_run:
854
+ result["steps"]["9_verify"] = {"success": True, "dry_run": True}
855
+ result["verification"] = {"success": True, "dry_run": True}
856
+ if verbose:
857
+ _ok("Verification skipped (dry-run)")
858
+ else:
859
+ verify_result = step9_verify(skill_name)
860
+ result["steps"]["9_verify"] = verify_result
861
+ result["verification"] = verify_result
862
+ if verbose:
863
+ if verify_result["success"]:
864
+ _ok(f"All {verify_result['total']} verification checks passed")
865
+ else:
866
+ failed_checks = [c for c in verify_result["checks"] if not c["pass"]]
867
+ _warn(f"{verify_result['failed']}/{verify_result['total']} checks failed")
868
+ for c in failed_checks:
869
+ _fail(f" {c['check']}")
870
+
871
+ # STEP 10: Package ZIP for Claude.ai web upload
872
+ if verbose:
873
+ _step(10, total_steps, "Packaging ZIP for Claude.ai...")
874
+ if dry_run:
875
+ result["steps"]["10_package"] = {"success": True, "dry_run": True}
876
+ if verbose:
877
+ _ok("Would create ZIP (dry-run)")
878
+ else:
879
+ zip_result = {"success": False, "skipped": True}
880
+ try:
881
+ from package_skill import package_skill as pkg_skill
882
+ zip_result = pkg_skill(SKILLS_ROOT / skill_name)
883
+ result["steps"]["10_package"] = zip_result
884
+ result["zip_path"] = zip_result.get("zip_path") if zip_result["success"] else None
885
+ if verbose:
886
+ if zip_result["success"]:
887
+ _ok(f"ZIP: {zip_result.get('zip_path')} ({zip_result.get('zip_size_kb', '?')} KB)")
888
+ else:
889
+ _warn(f"ZIP: {zip_result.get('error', 'failed')}")
890
+ except Exception as e:
891
+ zip_result = {"success": False, "error": str(e)}
892
+ result["steps"]["10_package"] = zip_result
893
+ result["warnings"].append(f"ZIP packaging warning: {e}")
894
+ if verbose:
895
+ _warn(f"ZIP packaging: {e}")
896
+
897
+ # STEP 11: Log
898
+ if verbose:
899
+ _step(11, total_steps, "Logging operation...")
900
+ if dry_run:
901
+ result["success"] = True
902
+ result["steps"]["11_log"] = {"logged": False, "dry_run": True}
903
+ if verbose:
904
+ _ok("Would log operation (dry-run)")
905
+ print(f"\n{_C.bold(_C.green('DRY RUN COMPLETE'))} -- no changes were made.\n")
906
+ else:
907
+ result["success"] = result.get("verification", {}).get("success", False)
908
+ if not result.get("verification", {}).get("success", True):
909
+ failed_checks = [c for c in result.get("verification", {}).get("checks", []) if not c.get("pass")]
910
+ result["warnings"].append(
911
+ f"Verification: {result['verification'].get('failed', 0)} check(s) failed: "
912
+ + ", ".join(c["check"] for c in failed_checks)
913
+ )
914
+
915
+ log_entry = step10_log(skill_name, str(source), result)
916
+ result["steps"]["11_log"] = {"logged": True}
917
+ if verbose:
918
+ _ok("Operation logged")
919
+ if result["success"]:
920
+ print(f"\n{_C.bold(_C.green('SUCCESS'))} -- {_C.bold(skill_name)} installed.\n")
921
+ else:
922
+ print(f"\n{_C.bold(_C.red('FAILED'))} -- see warnings above.\n")
923
+
924
+ return result
925
+
926
+
927
+ # ── Uninstall ─────────────────────────────────────────────────────────────
928
+
929
+ def uninstall_skill(skill_name: str, keep_backup: bool = True) -> dict:
930
+ """Uninstall a skill: remove from skills root, .claude/skills/, and registry."""
931
+ skill_name = sanitize_name(skill_name)
932
+ result = {
933
+ "success": False,
934
+ "skill_name": skill_name,
935
+ "removed": [],
936
+ "backup_path": None,
937
+ }
938
+
939
+ dest = SKILLS_ROOT / skill_name
940
+ claude_dest = CLAUDE_SKILLS / skill_name
941
+
942
+ if not dest.exists() and not claude_dest.exists():
943
+ result["error"] = f"Skill '{skill_name}' not found in any location"
944
+ return result
945
+
946
+ # Backup before removing
947
+ if keep_backup and dest.exists():
948
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
949
+ backup_path = BACKUPS_DIR / f"{skill_name}_{timestamp}"
950
+ BACKUPS_DIR.mkdir(parents=True, exist_ok=True)
951
+ try:
952
+ shutil.copytree(dest, backup_path, dirs_exist_ok=True)
953
+ result["backup_path"] = str(backup_path)
954
+ except Exception as e:
955
+ result["error"] = f"Backup failed: {e}"
956
+ return result
957
+
958
+ # Remove from skills root
959
+ if dest.exists():
960
+ try:
961
+ shutil.rmtree(dest)
962
+ result["removed"].append(str(dest))
963
+ except Exception as e:
964
+ result["error"] = f"Failed to remove {dest}: {e}"
965
+ return result
966
+
967
+ # Remove from .claude/skills/
968
+ if claude_dest.exists():
969
+ try:
970
+ shutil.rmtree(claude_dest)
971
+ result["removed"].append(str(claude_dest))
972
+ except Exception as e:
973
+ result["warnings"] = [f"Failed to remove Claude registration: {e}"]
974
+
975
+ # Update registry
976
+ registry_result = step8_update_registry()
977
+
978
+ # Remove ZIP from Desktop if exists
979
+ zip_path = Path(os.path.expanduser("~")) / "Desktop" / f"{skill_name}.zip"
980
+ if zip_path.exists():
981
+ try:
982
+ zip_path.unlink()
983
+ result["removed"].append(str(zip_path))
984
+ except Exception:
985
+ pass
986
+
987
+ # Log operation
988
+ entry = {
989
+ "timestamp": datetime.now().isoformat(),
990
+ "action": "uninstall",
991
+ "skill_name": skill_name,
992
+ "removed": result["removed"],
993
+ "backup_path": result.get("backup_path"),
994
+ "success": True,
995
+ }
996
+ try:
997
+ append_log(entry)
998
+ except Exception:
999
+ pass
1000
+
1001
+ result["success"] = True
1002
+ result["registry_updated"] = registry_result.get("success", False)
1003
+ return result
1004
+
1005
+
1006
+ # ── Health Check ──────────────────────────────────────────────────────────
1007
+
1008
+ def health_check() -> dict:
1009
+ """Run a global health check on all installed skills."""
1010
+ results = []
1011
+
1012
+ # Load registry
1013
+ registry_skills = []
1014
+ if REGISTRY_PATH.exists():
1015
+ try:
1016
+ registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
1017
+ registry_skills = registry.get("skills", [])
1018
+ except Exception:
1019
+ pass
1020
+
1021
+ registry_names = {s.get("name", "").lower() for s in registry_skills}
1022
+
1023
+ # Check all skill directories in skills root
1024
+ for item in sorted(SKILLS_ROOT.iterdir()):
1025
+ if not item.is_dir():
1026
+ continue
1027
+ if item.name.startswith("."):
1028
+ continue
1029
+ if item.name in ("agent-orchestrator", "skill-installer"):
1030
+ continue
1031
+
1032
+ skill_md = item / "SKILL.md"
1033
+ if not skill_md.exists():
1034
+ continue
1035
+
1036
+ meta = parse_yaml_frontmatter(skill_md)
1037
+ name = meta.get("name", item.name).lower()
1038
+
1039
+ checks = {
1040
+ "name": name,
1041
+ "dir": str(item),
1042
+ "skill_md_exists": skill_md.exists(),
1043
+ "frontmatter_ok": bool(meta.get("name") and meta.get("description")),
1044
+ "claude_registered": (CLAUDE_SKILLS / name / "SKILL.md").exists(),
1045
+ "in_registry": name in registry_names,
1046
+ "has_scripts": (item / "scripts").exists(),
1047
+ "has_references": (item / "references").exists(),
1048
+ }
1049
+
1050
+ # Count issues
1051
+ issues = []
1052
+ if not checks["frontmatter_ok"]:
1053
+ issues.append("invalid frontmatter (missing name or description)")
1054
+ if not checks["claude_registered"]:
1055
+ issues.append("not registered in .claude/skills/")
1056
+ if not checks["in_registry"]:
1057
+ issues.append("not in orchestrator registry")
1058
+
1059
+ checks["healthy"] = len(issues) == 0
1060
+ checks["issues"] = issues
1061
+ results.append(checks)
1062
+
1063
+ # Also check nested skills (e.g., juntas-comerciais/junta-leiloeiros)
1064
+ for parent in SKILLS_ROOT.iterdir():
1065
+ if not parent.is_dir() or parent.name.startswith("."):
1066
+ continue
1067
+ if parent.name in ("agent-orchestrator", "skill-installer"):
1068
+ continue
1069
+ for child in parent.iterdir():
1070
+ if child.is_dir() and (child / "SKILL.md").exists():
1071
+ # Skip if already checked at top level
1072
+ if any(r["dir"] == str(child) for r in results):
1073
+ continue
1074
+ meta = parse_yaml_frontmatter(child / "SKILL.md")
1075
+ name = meta.get("name", child.name).lower()
1076
+ checks = {
1077
+ "name": name,
1078
+ "dir": str(child),
1079
+ "skill_md_exists": True,
1080
+ "frontmatter_ok": bool(meta.get("name") and meta.get("description")),
1081
+ "claude_registered": (CLAUDE_SKILLS / name / "SKILL.md").exists(),
1082
+ "in_registry": name in registry_names,
1083
+ "has_scripts": (child / "scripts").exists(),
1084
+ "has_references": (child / "references").exists(),
1085
+ }
1086
+ issues = []
1087
+ if not checks["frontmatter_ok"]:
1088
+ issues.append("invalid frontmatter")
1089
+ if not checks["claude_registered"]:
1090
+ issues.append("not registered in .claude/skills/")
1091
+ if not checks["in_registry"]:
1092
+ issues.append("not in orchestrator registry")
1093
+ checks["healthy"] = len(issues) == 0
1094
+ checks["issues"] = issues
1095
+ results.append(checks)
1096
+
1097
+ healthy = sum(1 for r in results if r["healthy"])
1098
+ unhealthy = sum(1 for r in results if not r["healthy"])
1099
+
1100
+ # Check for registry duplicates
1101
+ from collections import Counter
1102
+ reg_name_counts = Counter(s.get("name", "").lower() for s in registry_skills)
1103
+ duplicates = {name: count for name, count in reg_name_counts.items() if count > 1}
1104
+
1105
+ return {
1106
+ "total_skills": len(results),
1107
+ "healthy": healthy,
1108
+ "unhealthy": unhealthy,
1109
+ "registry_duplicates": duplicates,
1110
+ "skills": results,
1111
+ }
1112
+
1113
+
1114
+ # ── Auto-Repair ──────────────────────────────────────────────────────────
1115
+
1116
+ def repair_health(verbose: bool = True) -> dict:
1117
+ """Run health check and automatically fix all issues found.
1118
+
1119
+ Fixes:
1120
+ - Skills not registered in .claude/skills/ -> registers them
1121
+ - Skills not in orchestrator registry -> triggers registry scan
1122
+ - Registry duplicates -> triggers re-scan with deduplication
1123
+ """
1124
+ if verbose:
1125
+ print(f"\n{_C.bold('=== HEALTH CHECK + AUTO-REPAIR ===')}\n")
1126
+
1127
+ health = health_check()
1128
+ repairs = []
1129
+ errors = []
1130
+
1131
+ unhealthy_skills = [s for s in health["skills"] if not s["healthy"]]
1132
+
1133
+ if not unhealthy_skills and not health["registry_duplicates"]:
1134
+ if verbose:
1135
+ _ok(f"All {health['total_skills']} skills are healthy. Nothing to repair.")
1136
+ health["repairs"] = []
1137
+ return health
1138
+
1139
+ # Fix: register missing skills in .claude/skills/
1140
+ for skill in unhealthy_skills:
1141
+ if "not registered in .claude/skills/" in "; ".join(skill["issues"]):
1142
+ name = skill["name"]
1143
+ skill_dir = Path(skill["dir"])
1144
+ skill_md = skill_dir / "SKILL.md"
1145
+ if skill_md.exists():
1146
+ claude_dest = CLAUDE_SKILLS / name
1147
+ if verbose:
1148
+ _step(1, 2, f"Registering '{name}' in .claude/skills/...")
1149
+ try:
1150
+ claude_dest.mkdir(parents=True, exist_ok=True)
1151
+ shutil.copy2(skill_md, claude_dest / "SKILL.md")
1152
+ # Also copy references/ if present
1153
+ refs = skill_dir / "references"
1154
+ if refs.exists():
1155
+ claude_refs = claude_dest / "references"
1156
+ if claude_refs.exists():
1157
+ shutil.rmtree(claude_refs)
1158
+ shutil.copytree(refs, claude_refs)
1159
+ repairs.append({"skill": name, "action": "registered", "success": True})
1160
+ if verbose:
1161
+ _ok(f"Registered: {name}")
1162
+ except Exception as e:
1163
+ errors.append({"skill": name, "action": "register", "error": str(e)})
1164
+ if verbose:
1165
+ _fail(f"Failed to register {name}: {e}")
1166
+
1167
+ # Fix: update registry to pick up missing skills and remove duplicates
1168
+ needs_registry_update = (
1169
+ any("not in orchestrator registry" in "; ".join(s["issues"]) for s in unhealthy_skills)
1170
+ or health["registry_duplicates"]
1171
+ )
1172
+ if needs_registry_update:
1173
+ if verbose:
1174
+ _step(2, 2, "Updating orchestrator registry...")
1175
+ reg_result = step8_update_registry()
1176
+ if reg_result["success"]:
1177
+ repairs.append({"action": "registry_update", "success": True})
1178
+ if verbose:
1179
+ _ok("Registry updated")
1180
+ else:
1181
+ errors.append({"action": "registry_update", "error": reg_result.get("error")})
1182
+ if verbose:
1183
+ _fail(f"Registry update failed: {reg_result.get('error')}")
1184
+
1185
+ # Re-run health check to confirm
1186
+ health_after = health_check()
1187
+
1188
+ result = {
1189
+ "before": {
1190
+ "healthy": health["healthy"],
1191
+ "unhealthy": health["unhealthy"],
1192
+ "duplicates": len(health["registry_duplicates"]),
1193
+ },
1194
+ "after": {
1195
+ "healthy": health_after["healthy"],
1196
+ "unhealthy": health_after["unhealthy"],
1197
+ "duplicates": len(health_after["registry_duplicates"]),
1198
+ },
1199
+ "repairs": repairs,
1200
+ "errors": errors,
1201
+ "skills": health_after["skills"],
1202
+ }
1203
+
1204
+ if verbose:
1205
+ fixed = health["unhealthy"] - health_after["unhealthy"]
1206
+ print(f"\n{_C.bold('Result:')} Fixed {_C.green(str(fixed))} of {health['unhealthy']} issues.")
1207
+ if health_after["unhealthy"] > 0:
1208
+ _warn(f"{health_after['unhealthy']} issues remaining")
1209
+ else:
1210
+ _ok("All skills healthy!")
1211
+ print()
1212
+
1213
+ return result
1214
+
1215
+
1216
+ # ── Rollback ─────────────────────────────────────────────────────────────
1217
+
1218
+ def rollback_skill(skill_name: str, verbose: bool = True) -> dict:
1219
+ """Restore a skill from its latest backup.
1220
+
1221
+ Finds the most recent backup for the given skill and restores it
1222
+ to the skills root, re-registers, and updates the registry.
1223
+ """
1224
+ skill_name = sanitize_name(skill_name)
1225
+ result = {
1226
+ "success": False,
1227
+ "skill_name": skill_name,
1228
+ "restored_from": None,
1229
+ }
1230
+
1231
+ if not BACKUPS_DIR.exists():
1232
+ result["error"] = "No backups directory found"
1233
+ if verbose:
1234
+ _fail("No backups directory found")
1235
+ return result
1236
+
1237
+ # Find backups for this skill
1238
+ prefix = f"{skill_name}_"
1239
+ backups = sorted(
1240
+ [d for d in BACKUPS_DIR.iterdir() if d.is_dir() and d.name.startswith(prefix)],
1241
+ key=lambda d: d.stat().st_mtime,
1242
+ reverse=True,
1243
+ )
1244
+
1245
+ if not backups:
1246
+ result["error"] = f"No backups found for skill '{skill_name}'"
1247
+ if verbose:
1248
+ _fail(f"No backups found for '{skill_name}'")
1249
+ # Show available backups
1250
+ all_backups = [d.name for d in BACKUPS_DIR.iterdir() if d.is_dir()]
1251
+ if all_backups:
1252
+ print(f" Available backups: {', '.join(sorted(set(b.rsplit('_', 2)[0] for b in all_backups)))}")
1253
+ return result
1254
+
1255
+ latest_backup = backups[0]
1256
+ backup_skill_md = latest_backup / "SKILL.md"
1257
+
1258
+ if not backup_skill_md.exists():
1259
+ result["error"] = f"Backup is invalid (no SKILL.md): {latest_backup}"
1260
+ if verbose:
1261
+ _fail(f"Backup invalid: {latest_backup}")
1262
+ return result
1263
+
1264
+ if verbose:
1265
+ timestamp = latest_backup.name.replace(f"{skill_name}_", "")
1266
+ print(f"\n{_C.bold(f'=== ROLLBACK: {skill_name} ===')}")
1267
+ print(f" Backup: {latest_backup.name} ({timestamp})")
1268
+
1269
+ # Restore to skills root
1270
+ dest = SKILLS_ROOT / skill_name
1271
+ if verbose:
1272
+ _step(1, 3, "Restoring from backup...")
1273
+
1274
+ try:
1275
+ if dest.exists():
1276
+ shutil.rmtree(dest)
1277
+ shutil.copytree(latest_backup, dest, ignore=_backup_ignore, dirs_exist_ok=True)
1278
+ result["restored_from"] = str(latest_backup)
1279
+ if verbose:
1280
+ _ok(f"Restored to: {dest}")
1281
+ except Exception as e:
1282
+ result["error"] = f"Restore failed: {e}"
1283
+ if verbose:
1284
+ _fail(f"Restore failed: {e}")
1285
+ return result
1286
+
1287
+ # Re-register in Claude Code
1288
+ if verbose:
1289
+ _step(2, 3, "Re-registering...")
1290
+ reg = step7_register_claude(skill_name)
1291
+ if verbose:
1292
+ if reg["success"]:
1293
+ _ok("Registered")
1294
+ else:
1295
+ _warn(f"Registration: {reg.get('error')}")
1296
+
1297
+ # Update registry
1298
+ if verbose:
1299
+ _step(3, 3, "Updating registry...")
1300
+ step8_update_registry()
1301
+ if verbose:
1302
+ _ok("Registry updated")
1303
+
1304
+ # Log operation
1305
+ append_log({
1306
+ "timestamp": datetime.now().isoformat(),
1307
+ "action": "rollback",
1308
+ "skill_name": skill_name,
1309
+ "backup_used": str(latest_backup),
1310
+ "success": True,
1311
+ })
1312
+
1313
+ result["success"] = True
1314
+ if verbose:
1315
+ print(f"\n{_C.bold(_C.green('ROLLBACK COMPLETE'))}\n")
1316
+ return result
1317
+
1318
+
1319
+ # ── Reinstall All ────────────────────────────────────────────────────────
1320
+
1321
+ def reinstall_all(force: bool = True, verbose: bool = True) -> dict:
1322
+ """Re-register every installed skill in one pass.
1323
+
1324
+ Iterates all skill directories, re-copies SKILL.md to .claude/skills/,
1325
+ re-packages ZIPs, and updates the registry.
1326
+ """
1327
+ if verbose:
1328
+ print(f"\n{_C.bold('=== REINSTALL ALL SKILLS ===')}\n")
1329
+
1330
+ skill_dirs = get_all_skill_dirs()
1331
+ results_list = []
1332
+
1333
+ for i, skill_dir in enumerate(skill_dirs, 1):
1334
+ meta = parse_yaml_frontmatter(skill_dir / "SKILL.md")
1335
+ name = meta.get("name", skill_dir.name)
1336
+ name = sanitize_name(name)
1337
+
1338
+ if verbose:
1339
+ print(f" [{i}/{len(skill_dirs)}] {_C.bold(name)}...")
1340
+
1341
+ # Re-register in .claude/skills/
1342
+ reg = step7_register_claude(name)
1343
+
1344
+ # Re-package ZIP
1345
+ zip_result = {"success": False}
1346
+ try:
1347
+ from package_skill import package_skill as pkg_skill
1348
+ zip_result = pkg_skill(skill_dir)
1349
+ except Exception:
1350
+ pass
1351
+
1352
+ r = {
1353
+ "skill": name,
1354
+ "registered": reg["success"],
1355
+ "zipped": zip_result.get("success", False),
1356
+ }
1357
+ results_list.append(r)
1358
+
1359
+ if verbose:
1360
+ status = _C.green(_C.OK) if reg["success"] else _C.red(_C.FAIL)
1361
+ zip_status = _C.green("ZIP-OK") if zip_result.get("success") else _C.yellow("ZIP-WARN")
1362
+ print(f" {status} registered {zip_status}")
1363
+
1364
+ # Final registry update
1365
+ if verbose:
1366
+ print(f"\n Updating registry...")
1367
+ step8_update_registry()
1368
+
1369
+ registered_ok = sum(1 for r in results_list if r["registered"])
1370
+ zipped_ok = sum(1 for r in results_list if r["zipped"])
1371
+
1372
+ result = {
1373
+ "total": len(results_list),
1374
+ "registered": registered_ok,
1375
+ "zipped": zipped_ok,
1376
+ "results": results_list,
1377
+ }
1378
+
1379
+ if verbose:
1380
+ print(f"\n{_C.bold('Result:')} {registered_ok}/{len(results_list)} registered, {zipped_ok}/{len(results_list)} zipped.")
1381
+ print()
1382
+
1383
+ # Log
1384
+ append_log({
1385
+ "timestamp": datetime.now().isoformat(),
1386
+ "action": "reinstall_all",
1387
+ "total": len(results_list),
1388
+ "registered": registered_ok,
1389
+ "zipped": zipped_ok,
1390
+ "success": True,
1391
+ })
1392
+
1393
+ return result
1394
+
1395
+
1396
+ # ── Status Dashboard ─────────────────────────────────────────────────────
1397
+
1398
+ def show_status(verbose: bool = True) -> dict:
1399
+ """Rich status dashboard showing all skills, versions, and health."""
1400
+ health = health_check()
1401
+
1402
+ # Load registry for version info
1403
+ registry_skills = {}
1404
+ if REGISTRY_PATH.exists():
1405
+ try:
1406
+ reg = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
1407
+ for s in reg.get("skills", []):
1408
+ registry_skills[s.get("name", "").lower()] = s
1409
+ except Exception:
1410
+ pass
1411
+
1412
+ # Count backups per skill
1413
+ backup_counts = {}
1414
+ if BACKUPS_DIR.exists():
1415
+ for d in BACKUPS_DIR.iterdir():
1416
+ if d.is_dir():
1417
+ # Extract skill name (everything before last _TIMESTAMP)
1418
+ parts = d.name.rsplit("_", 2)
1419
+ if len(parts) >= 3:
1420
+ bname = parts[0]
1421
+ else:
1422
+ bname = d.name
1423
+ backup_counts[bname] = backup_counts.get(bname, 0) + 1
1424
+
1425
+ # Log stats
1426
+ log_ops = load_log()
1427
+ install_count = sum(1 for o in log_ops if o.get("action") == "install")
1428
+ uninstall_count = sum(1 for o in log_ops if o.get("action") == "uninstall")
1429
+ rollback_count = sum(1 for o in log_ops if o.get("action") == "rollback")
1430
+
1431
+ if verbose:
1432
+ print(f"\n{_C.bold('+' + '='*62 + '+')}")
1433
+ print(f"{_C.bold('|')} {_C.bold(_C.cyan('Skill Installer v' + VERSION + ' -- Status Dashboard'))} {_C.bold('|')}")
1434
+ print(f"{_C.bold('+' + '='*62 + '+')}\n")
1435
+
1436
+ # Skills table header
1437
+ print(f" {'Name':<24} {'Version':<10} {'Health':<10} {'Registered':<12} {'Backups':<8}")
1438
+ print(f" {'-'*24} {'-'*10} {'-'*10} {'-'*12} {'-'*8}")
1439
+
1440
+ for skill in health["skills"]:
1441
+ name = skill["name"][:22]
1442
+ reg_entry = registry_skills.get(skill["name"], {})
1443
+ version = reg_entry.get("version", "-") or "-"
1444
+ status = _C.green("OK") if skill["healthy"] else _C.red("ISSUE")
1445
+ registered = _C.green("Yes") if skill["claude_registered"] else _C.red("No")
1446
+ backups = str(backup_counts.get(skill["name"], 0))
1447
+ print(f" {name:<24} {version:<10} {status:<19} {registered:<21} {backups:<8}")
1448
+
1449
+ if not skill["healthy"]:
1450
+ for issue in skill["issues"]:
1451
+ print(f" {_C.dim(f' -> {issue}')}")
1452
+
1453
+ print(f"\n {_C.bold('Summary:')}")
1454
+ print(f" Skills: {_C.bold(str(health['total_skills']))} total, "
1455
+ f"{_C.green(str(health['healthy']))} healthy, "
1456
+ f"{_C.red(str(health['unhealthy'])) if health['unhealthy'] else '0'} unhealthy")
1457
+ if health["registry_duplicates"]:
1458
+ print(f" {_C.yellow('Duplicates:')} {health['registry_duplicates']}")
1459
+
1460
+ print(f"\n {_C.bold('Operations Log:')}")
1461
+ print(f" Installs: {install_count} | Uninstalls: {uninstall_count} | Rollbacks: {rollback_count}")
1462
+ print(f" Total logged: {len(log_ops)}")
1463
+ print()
1464
+
1465
+ return {
1466
+ "health": health,
1467
+ "backup_counts": backup_counts,
1468
+ "log_stats": {
1469
+ "total": len(log_ops),
1470
+ "installs": install_count,
1471
+ "uninstalls": uninstall_count,
1472
+ "rollbacks": rollback_count,
1473
+ },
1474
+ }
1475
+
1476
+
1477
+ # ── Log Viewer ───────────────────────────────────────────────────────────
1478
+
1479
+ def show_log(n: int = 20, verbose: bool = True) -> list:
1480
+ """Show the last N log entries."""
1481
+ ops = load_log()
1482
+ recent = ops[-n:] if len(ops) > n else ops
1483
+
1484
+ if verbose:
1485
+ print(f"\n{_C.bold(f'=== Last {len(recent)} Operations ===')}\n")
1486
+ for op in reversed(recent):
1487
+ ts = op.get("timestamp", "?")[:19]
1488
+ action = op.get("action", "?")
1489
+ name = op.get("skill_name", "?")
1490
+ success = op.get("success", False)
1491
+
1492
+ # Color the action
1493
+ if action == "install":
1494
+ action_str = _C.green("INSTALL")
1495
+ elif action == "uninstall":
1496
+ action_str = _C.red("UNINSTALL")
1497
+ elif action == "rollback":
1498
+ action_str = _C.yellow("ROLLBACK")
1499
+ elif action == "reinstall_all":
1500
+ action_str = _C.cyan("REINSTALL-ALL")
1501
+ else:
1502
+ action_str = action.upper()
1503
+
1504
+ status = _C.green(_C.OK) if success else _C.red(_C.FAIL)
1505
+ print(f" {_C.dim(ts)} {action_str:<22} {name:<24} {status}")
1506
+
1507
+ print()
1508
+
1509
+ return recent
1510
+
1511
+
1512
+ # ── CLI Entry Point ───────────────────────────────────────────────────────
1513
+
1514
+ def main():
1515
+ args = sys.argv[1:]
1516
+
1517
+ source = None
1518
+ name_override = None
1519
+ force = "--force" in args
1520
+ dry_run = "--dry-run" in args
1521
+ do_detect = "--detect" in args
1522
+ auto = "--auto" in args
1523
+ do_uninstall = "--uninstall" in args
1524
+ do_health = "--health" in args
1525
+ do_repair = "--repair" in args
1526
+ do_rollback = "--rollback" in args
1527
+ do_reinstall_all = "--reinstall-all" in args
1528
+ do_status = "--status" in args
1529
+ do_log = "--log" in args
1530
+ json_output = "--json" in args
1531
+
1532
+ if "--source" in args:
1533
+ idx = args.index("--source")
1534
+ if idx + 1 < len(args):
1535
+ source = args[idx + 1]
1536
+
1537
+ if "--name" in args:
1538
+ idx = args.index("--name")
1539
+ if idx + 1 < len(args):
1540
+ name_override = args[idx + 1]
1541
+
1542
+ # ── Status dashboard ──
1543
+ if do_status:
1544
+ result = show_status(verbose=not json_output)
1545
+ if json_output:
1546
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1547
+ sys.exit(0)
1548
+
1549
+ # ── Log viewer ──
1550
+ if do_log:
1551
+ n = 20
1552
+ idx = args.index("--log")
1553
+ if idx + 1 < len(args):
1554
+ try:
1555
+ n = int(args[idx + 1])
1556
+ except ValueError:
1557
+ pass
1558
+ result = show_log(n=n, verbose=not json_output)
1559
+ if json_output:
1560
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1561
+ sys.exit(0)
1562
+
1563
+ # ── Health check (with optional auto-repair) ──
1564
+ if do_health:
1565
+ if do_repair:
1566
+ result = repair_health(verbose=not json_output)
1567
+ if json_output:
1568
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1569
+ remaining = result.get("after", {}).get("unhealthy", 0)
1570
+ sys.exit(0 if remaining == 0 else 1)
1571
+ else:
1572
+ result = health_check()
1573
+ if json_output:
1574
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1575
+ else:
1576
+ # Pretty print health
1577
+ print(f"\n{_C.bold('=== HEALTH CHECK ===')}\n")
1578
+ for s in result["skills"]:
1579
+ if s["healthy"]:
1580
+ _ok(s["name"])
1581
+ else:
1582
+ _fail(f"{s['name']}: {'; '.join(s['issues'])}")
1583
+ print(f"\n {_C.bold(str(result['healthy']))}/{result['total_skills']} healthy")
1584
+ if result["unhealthy"] > 0:
1585
+ print(f" {_C.yellow('Tip:')} run with --repair to auto-fix issues")
1586
+ if result["registry_duplicates"]:
1587
+ print(f" {_C.yellow('Duplicates:')} {result['registry_duplicates']}")
1588
+ print()
1589
+ sys.exit(0 if result["unhealthy"] == 0 else 1)
1590
+
1591
+ # ── Rollback ──
1592
+ if do_rollback:
1593
+ idx = args.index("--rollback")
1594
+ if idx + 1 >= len(args):
1595
+ print(json.dumps({"error": "Usage: --rollback <skill-name>"}, indent=2))
1596
+ sys.exit(1)
1597
+ skill_name = args[idx + 1]
1598
+ result = rollback_skill(skill_name, verbose=not json_output)
1599
+ if json_output:
1600
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1601
+ sys.exit(0 if result["success"] else 1)
1602
+
1603
+ # ── Reinstall all ──
1604
+ if do_reinstall_all:
1605
+ result = reinstall_all(force=True, verbose=not json_output)
1606
+ if json_output:
1607
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1608
+ sys.exit(0)
1609
+
1610
+ # ── Uninstall ──
1611
+ if do_uninstall:
1612
+ idx = args.index("--uninstall")
1613
+ if idx + 1 >= len(args):
1614
+ print(json.dumps({"error": "Usage: --uninstall <skill-name>"}, indent=2))
1615
+ sys.exit(1)
1616
+ skill_name = args[idx + 1]
1617
+ result = uninstall_skill(skill_name)
1618
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1619
+ sys.exit(0 if result["success"] else 1)
1620
+
1621
+ # ── No arguments: show usage ──
1622
+ if not source and not do_detect:
1623
+ print(f"\n{_C.bold(_C.cyan('Skill Installer v' + VERSION))}\n")
1624
+ print(f" {_C.bold('Install:')}")
1625
+ print(f" --source <path> Install skill from path")
1626
+ print(f" --source <path> --force Overwrite if exists")
1627
+ print(f" --source <path> --name <name> Custom name override")
1628
+ print(f" --source <path> --dry-run Simulate without changes")
1629
+ print(f" --detect Auto-detect uninstalled skills")
1630
+ print(f" --detect --auto Detect and install all")
1631
+ print(f"")
1632
+ print(f" {_C.bold('Manage:')}")
1633
+ print(f" --uninstall <name> Uninstall (with backup)")
1634
+ print(f" --rollback <name> Restore from latest backup")
1635
+ print(f" --reinstall-all Re-register + re-package all skills")
1636
+ print(f"")
1637
+ print(f" {_C.bold('Monitor:')}")
1638
+ print(f" --health Health check all skills")
1639
+ print(f" --health --repair Health check + auto-fix issues")
1640
+ print(f" --status Rich status dashboard")
1641
+ print(f" --log [N] Show last N operations (default: 20)")
1642
+ print(f"")
1643
+ print(f" {_C.bold('Flags:')}")
1644
+ print(f" --json Output JSON instead of pretty text")
1645
+ print(f" --force Force overwrite")
1646
+ print(f" --dry-run Simulate without changes")
1647
+ print()
1648
+ sys.exit(1)
1649
+
1650
+ # ── Install from source ──
1651
+ if source:
1652
+ result = install_single(source, name_override, force, dry_run=dry_run, verbose=not json_output)
1653
+ if json_output:
1654
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1655
+ sys.exit(0 if result["success"] else 1)
1656
+
1657
+ # ── Detection mode ──
1658
+ elif do_detect:
1659
+ resolve = step1_resolve_source(do_detect=True, auto=auto)
1660
+
1661
+ if not resolve["success"]:
1662
+ print(json.dumps(resolve, indent=2, ensure_ascii=False))
1663
+ sys.exit(1)
1664
+
1665
+ if resolve.get("interactive") and not auto:
1666
+ if json_output:
1667
+ print(json.dumps({
1668
+ "mode": "interactive",
1669
+ "message": "Skills detected but not installed.",
1670
+ "candidates": resolve["candidates"],
1671
+ }, indent=2, ensure_ascii=False))
1672
+ else:
1673
+ print(f"\n{_C.bold('=== Detected Uninstalled Skills ===')}\n")
1674
+ for i, c in enumerate(resolve["candidates"], 1):
1675
+ name = c.get("name", "?")
1676
+ src = c.get("source_path", "?")
1677
+ loc = c.get("location_type", "?")
1678
+ valid = _C.green(_C.OK) if c.get("valid_frontmatter") else _C.red(_C.FAIL)
1679
+ print(f" {i}. {_C.bold(name)} {valid}")
1680
+ print(f" {_C.dim(src)} ({loc})")
1681
+ print(f"\n Run with --auto to install all, or --source <path> to install one.\n")
1682
+ sys.exit(0)
1683
+
1684
+ # Auto mode: install all candidates
1685
+ results = []
1686
+ for src in resolve["sources"]:
1687
+ r = install_single(src, force=force, dry_run=dry_run, verbose=not json_output)
1688
+ results.append(r)
1689
+
1690
+ total = len(results)
1691
+ success = sum(1 for r in results if r["success"])
1692
+ failed = total - success
1693
+
1694
+ summary = {
1695
+ "mode": "auto",
1696
+ "total": total,
1697
+ "success": success,
1698
+ "failed": failed,
1699
+ "results": results,
1700
+ }
1701
+
1702
+ if json_output:
1703
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
1704
+ sys.exit(0 if failed == 0 else 1)
1705
+
1706
+
1707
+ if __name__ == "__main__":
1708
+ main()