@openhands/extensions 0.1.0 → 0.2.0

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 (347) hide show
  1. package/.agents/skills/custom-codereview-guide.md +25 -0
  2. package/.github/pull_request_template.md +38 -0
  3. package/.github/release.yml +14 -0
  4. package/.github/workflows/check-extensions.yml +72 -0
  5. package/.github/workflows/npm-publish.yml +89 -0
  6. package/.github/workflows/pr.yml +30 -0
  7. package/.github/workflows/release.yml +24 -0
  8. package/.github/workflows/tests.yml +25 -0
  9. package/.github/workflows/vulnerability-scan.yml +87 -0
  10. package/.release-please-manifest.json +3 -0
  11. package/AGENTS.md +132 -0
  12. package/README.md +10 -0
  13. package/analysis_results.md +162 -0
  14. package/marketplaces/large-codebase.json +66 -0
  15. package/marketplaces/openhands-extensions.json +682 -0
  16. package/package.json +4 -10
  17. package/plugins/README.md +30 -0
  18. package/plugins/city-weather/.plugin/plugin.json +13 -0
  19. package/plugins/city-weather/README.md +145 -0
  20. package/plugins/city-weather/commands/now.md +56 -0
  21. package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
  22. package/plugins/cobol-modernization/README.md +201 -0
  23. package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
  24. package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
  25. package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
  26. package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
  27. package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
  28. package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
  29. package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
  30. package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
  31. package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
  32. package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
  33. package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
  34. package/plugins/issue-duplicate-checker/README.md +51 -0
  35. package/plugins/issue-duplicate-checker/action.yml +349 -0
  36. package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
  37. package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
  38. package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
  39. package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
  40. package/plugins/magic-test/.plugin/plugin.json +13 -0
  41. package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
  42. package/plugins/migration-scoring/.plugin/plugin.json +19 -0
  43. package/plugins/migration-scoring/README.md +244 -0
  44. package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
  45. package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
  46. package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
  47. package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
  48. package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
  49. package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
  50. package/plugins/onboarding/.plugin/plugin.json +20 -0
  51. package/plugins/onboarding/README.md +30 -0
  52. package/plugins/onboarding/references/criteria.md +144 -0
  53. package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
  54. package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
  55. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
  56. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
  57. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
  58. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
  59. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
  60. package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
  61. package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
  62. package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
  63. package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
  64. package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
  65. package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
  66. package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
  67. package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
  68. package/plugins/openhands/.plugin/plugin.json +13 -0
  69. package/plugins/openhands/README.md +52 -0
  70. package/plugins/openhands/SKILL.md +61 -0
  71. package/plugins/openhands/commands/create.md +55 -0
  72. package/plugins/openhands/commands/openhands-cloud.md +8 -0
  73. package/plugins/openhands/scripts/run.sh +69 -0
  74. package/plugins/pr-review/.plugin/plugin.json +13 -0
  75. package/plugins/pr-review/README.md +393 -0
  76. package/plugins/pr-review/action.yml +298 -0
  77. package/plugins/pr-review/scripts/agent_script.py +1282 -0
  78. package/plugins/pr-review/scripts/evaluate_review.py +655 -0
  79. package/plugins/pr-review/scripts/prompt.py +260 -0
  80. package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
  81. package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
  82. package/plugins/qa-changes/.plugin/plugin.json +11 -0
  83. package/plugins/qa-changes/README.md +185 -0
  84. package/plugins/qa-changes/action.yml +181 -0
  85. package/plugins/qa-changes/scripts/agent_script.py +406 -0
  86. package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
  87. package/plugins/qa-changes/scripts/prompt.py +174 -0
  88. package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
  89. package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
  90. package/plugins/release-notes/.plugin/plugin.json +19 -0
  91. package/plugins/release-notes/README.md +283 -0
  92. package/plugins/release-notes/SKILL.md +83 -0
  93. package/plugins/release-notes/action.yml +117 -0
  94. package/plugins/release-notes/commands/release-notes.md +8 -0
  95. package/plugins/release-notes/scripts/agent_script.py +292 -0
  96. package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
  97. package/plugins/release-notes/scripts/prompt.py +90 -0
  98. package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
  99. package/plugins/release-notes/workflows/release-notes.yml +76 -0
  100. package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
  101. package/plugins/vulnerability-remediation/README.md +217 -0
  102. package/plugins/vulnerability-remediation/action.yml +187 -0
  103. package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
  104. package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
  105. package/pyproject.toml +12 -0
  106. package/release-please-config.json +16 -0
  107. package/scripts/sync_extensions.py +494 -0
  108. package/scripts/sync_openhands_sdk_skill.py +264 -0
  109. package/skills/README.md +159 -0
  110. package/skills/add-javadoc/.plugin/plugin.json +18 -0
  111. package/skills/add-javadoc/README.md +40 -0
  112. package/skills/add-javadoc/SKILL.md +35 -0
  113. package/skills/add-javadoc/references/example.md +32 -0
  114. package/skills/add-skill/.plugin/plugin.json +18 -0
  115. package/skills/add-skill/README.md +67 -0
  116. package/skills/add-skill/SKILL.md +47 -0
  117. package/skills/add-skill/scripts/fetch_skill.py +259 -0
  118. package/skills/agent-creator/.plugin/plugin.json +20 -0
  119. package/skills/agent-creator/README.md +104 -0
  120. package/skills/agent-creator/SKILL.md +190 -0
  121. package/skills/agent-creator/commands/agent-creator.md +8 -0
  122. package/skills/agent-creator/references/fallback.md +117 -0
  123. package/skills/agent-memory/.plugin/plugin.json +18 -0
  124. package/skills/agent-memory/README.md +35 -0
  125. package/skills/agent-memory/SKILL.md +30 -0
  126. package/skills/agent-memory/commands/remember.md +8 -0
  127. package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
  128. package/skills/agent-sdk-builder/README.md +40 -0
  129. package/skills/agent-sdk-builder/SKILL.md +37 -0
  130. package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
  131. package/skills/azure-devops/.plugin/plugin.json +18 -0
  132. package/skills/azure-devops/README.md +55 -0
  133. package/skills/azure-devops/SKILL.md +50 -0
  134. package/skills/bitbucket/.plugin/plugin.json +17 -0
  135. package/skills/bitbucket/README.md +50 -0
  136. package/skills/bitbucket/SKILL.md +45 -0
  137. package/skills/code-review/.plugin/plugin.json +19 -0
  138. package/skills/code-review/README.md +18 -0
  139. package/skills/code-review/SKILL.md +208 -0
  140. package/skills/code-review/commands/codereview-roasted.md +8 -0
  141. package/skills/code-review/commands/codereview.md +8 -0
  142. package/skills/code-review/references/risk-evaluation.md +41 -0
  143. package/skills/code-review/references/supply-chain-security.md +31 -0
  144. package/skills/code-simplifier/.plugin/plugin.json +21 -0
  145. package/skills/code-simplifier/README.md +30 -0
  146. package/skills/code-simplifier/SKILL.md +91 -0
  147. package/skills/code-simplifier/commands/simplify.md +8 -0
  148. package/skills/code-simplifier/references/code-quality-review.md +86 -0
  149. package/skills/code-simplifier/references/code-reuse-review.md +63 -0
  150. package/skills/code-simplifier/references/efficiency-review.md +81 -0
  151. package/skills/datadog/.plugin/plugin.json +19 -0
  152. package/skills/datadog/README.md +100 -0
  153. package/skills/datadog/SKILL.md +95 -0
  154. package/skills/deno/.plugin/plugin.json +18 -0
  155. package/skills/deno/README.md +5 -0
  156. package/skills/deno/SKILL.md +99 -0
  157. package/skills/deno/references/README.md +6 -0
  158. package/skills/discord/.plugin/plugin.json +18 -0
  159. package/skills/discord/README.md +31 -0
  160. package/skills/discord/SKILL.md +109 -0
  161. package/skills/discord/__init__.py +0 -0
  162. package/skills/discord/references/REFERENCE.md +78 -0
  163. package/skills/discord/scripts/__init__.py +0 -0
  164. package/skills/discord/scripts/_http.py +127 -0
  165. package/skills/discord/scripts/post_webhook.py +106 -0
  166. package/skills/discord/scripts/send_message.py +102 -0
  167. package/skills/docker/.plugin/plugin.json +17 -0
  168. package/skills/docker/README.md +34 -0
  169. package/skills/docker/SKILL.md +29 -0
  170. package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
  171. package/skills/evidence-based-citations/README.md +31 -0
  172. package/skills/evidence-based-citations/SKILL.md +59 -0
  173. package/skills/flarglebargle/.plugin/plugin.json +16 -0
  174. package/skills/flarglebargle/README.md +14 -0
  175. package/skills/flarglebargle/SKILL.md +9 -0
  176. package/skills/frontend-design/.plugin/plugin.json +21 -0
  177. package/skills/frontend-design/LICENSE.txt +177 -0
  178. package/skills/frontend-design/README.md +42 -0
  179. package/skills/frontend-design/SKILL.md +42 -0
  180. package/skills/github/.plugin/plugin.json +19 -0
  181. package/skills/github/README.md +42 -0
  182. package/skills/github/SKILL.md +106 -0
  183. package/skills/github-pr-review/.plugin/plugin.json +18 -0
  184. package/skills/github-pr-review/README.md +145 -0
  185. package/skills/github-pr-review/SKILL.md +148 -0
  186. package/skills/github-pr-review/commands/github-pr-review.md +8 -0
  187. package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
  188. package/skills/github-pr-reviewer/README.md +34 -0
  189. package/skills/github-pr-reviewer/SKILL.md +89 -0
  190. package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
  191. package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
  192. package/skills/github-repo-monitor/README.md +70 -0
  193. package/skills/github-repo-monitor/SKILL.md +316 -0
  194. package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
  195. package/skills/github-repo-monitor/references/github-api.md +241 -0
  196. package/skills/github-repo-monitor/references/state-schema.md +160 -0
  197. package/skills/github-repo-monitor/scripts/main.py +915 -0
  198. package/skills/github-repo-monitor/tests/test_main.py +400 -0
  199. package/skills/gitlab/.plugin/plugin.json +17 -0
  200. package/skills/gitlab/README.md +37 -0
  201. package/skills/gitlab/SKILL.md +32 -0
  202. package/skills/incident-retrospective/.plugin/plugin.json +21 -0
  203. package/skills/incident-retrospective/README.md +34 -0
  204. package/skills/incident-retrospective/SKILL.md +98 -0
  205. package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
  206. package/skills/iterate/.plugin/plugin.json +13 -0
  207. package/skills/iterate/README.md +25 -0
  208. package/skills/iterate/SKILL.md +399 -0
  209. package/skills/iterate/commands/babysit.md +8 -0
  210. package/skills/iterate/commands/iterate.md +8 -0
  211. package/skills/iterate/commands/verify.md +8 -0
  212. package/skills/iterate/references/heuristics.md +58 -0
  213. package/skills/iterate/references/verification.md +96 -0
  214. package/skills/jupyter/.plugin/plugin.json +18 -0
  215. package/skills/jupyter/README.md +55 -0
  216. package/skills/jupyter/SKILL.md +50 -0
  217. package/skills/kubernetes/.plugin/plugin.json +18 -0
  218. package/skills/kubernetes/README.md +53 -0
  219. package/skills/kubernetes/SKILL.md +48 -0
  220. package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
  221. package/skills/learn-from-code-review/README.md +64 -0
  222. package/skills/learn-from-code-review/SKILL.md +186 -0
  223. package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
  224. package/skills/linear/.plugin/plugin.json +19 -0
  225. package/skills/linear/README.md +58 -0
  226. package/skills/linear/SKILL.md +213 -0
  227. package/skills/linear-triage/.plugin/plugin.json +21 -0
  228. package/skills/linear-triage/README.md +34 -0
  229. package/skills/linear-triage/SKILL.md +91 -0
  230. package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
  231. package/skills/notion/.plugin/plugin.json +17 -0
  232. package/skills/notion/README.md +114 -0
  233. package/skills/notion/SKILL.md +109 -0
  234. package/skills/npm/.plugin/plugin.json +17 -0
  235. package/skills/npm/README.md +14 -0
  236. package/skills/npm/SKILL.md +9 -0
  237. package/skills/openhands-api/.plugin/plugin.json +22 -0
  238. package/skills/openhands-api/README.md +48 -0
  239. package/skills/openhands-api/SKILL.md +399 -0
  240. package/skills/openhands-api/references/README.md +33 -0
  241. package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  242. package/skills/openhands-api/references/example_prompt.md +12 -0
  243. package/skills/openhands-api/scripts/openhands_api.py +606 -0
  244. package/skills/openhands-api/scripts/openhands_api.ts +252 -0
  245. package/skills/openhands-automation/.plugin/plugin.json +19 -0
  246. package/skills/openhands-automation/README.md +89 -0
  247. package/skills/openhands-automation/SKILL.md +875 -0
  248. package/skills/openhands-automation/commands/automation:create.md +8 -0
  249. package/skills/openhands-automation/references/ab-testing.md +185 -0
  250. package/skills/openhands-automation/references/custom-automation.md +644 -0
  251. package/skills/openhands-sdk/.plugin/plugin.json +20 -0
  252. package/skills/openhands-sdk/README.md +22 -0
  253. package/skills/openhands-sdk/SKILL.md +229 -0
  254. package/skills/openhands-sdk/commands/sdk.md +8 -0
  255. package/skills/pdflatex/.plugin/plugin.json +18 -0
  256. package/skills/pdflatex/README.md +39 -0
  257. package/skills/pdflatex/SKILL.md +34 -0
  258. package/skills/prd/.plugin/plugin.json +19 -0
  259. package/skills/prd/README.md +28 -0
  260. package/skills/prd/SKILL.md +237 -0
  261. package/skills/prd/commands/prd.md +8 -0
  262. package/skills/qa-changes/README.md +18 -0
  263. package/skills/qa-changes/SKILL.md +229 -0
  264. package/skills/qa-changes/commands/qa-changes.md +8 -0
  265. package/skills/release-notes/README.md +24 -0
  266. package/skills/release-notes/SKILL.md +19 -0
  267. package/skills/release-notes/commands/release-notes.md +8 -0
  268. package/skills/research-brief/.plugin/plugin.json +20 -0
  269. package/skills/research-brief/README.md +34 -0
  270. package/skills/research-brief/SKILL.md +99 -0
  271. package/skills/research-brief/commands/research-brief:setup.md +8 -0
  272. package/skills/security/.plugin/plugin.json +18 -0
  273. package/skills/security/README.md +38 -0
  274. package/skills/security/SKILL.md +33 -0
  275. package/skills/skill-creator/.plugin/plugin.json +17 -0
  276. package/skills/skill-creator/LICENSE.txt +202 -0
  277. package/skills/skill-creator/README.md +182 -0
  278. package/skills/skill-creator/SKILL.md +545 -0
  279. package/skills/skill-creator/references/output-patterns.md +82 -0
  280. package/skills/skill-creator/references/workflows.md +28 -0
  281. package/skills/skill-creator/scripts/init_skill.py +303 -0
  282. package/skills/skill-creator/scripts/quick_validate.py +95 -0
  283. package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
  284. package/skills/slack-channel-monitor/README.md +91 -0
  285. package/skills/slack-channel-monitor/SKILL.md +276 -0
  286. package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
  287. package/skills/slack-channel-monitor/references/slack-api.md +207 -0
  288. package/skills/slack-channel-monitor/references/state-schema.md +180 -0
  289. package/skills/slack-channel-monitor/scripts/main.py +962 -0
  290. package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
  291. package/skills/slack-standup-digest/README.md +34 -0
  292. package/skills/slack-standup-digest/SKILL.md +92 -0
  293. package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
  294. package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
  295. package/skills/spark-version-upgrade/README.md +54 -0
  296. package/skills/spark-version-upgrade/SKILL.md +233 -0
  297. package/skills/ssh/.plugin/plugin.json +18 -0
  298. package/skills/ssh/README.md +140 -0
  299. package/skills/ssh/SKILL.md +135 -0
  300. package/skills/swift-linux/.plugin/plugin.json +17 -0
  301. package/skills/swift-linux/README.md +86 -0
  302. package/skills/swift-linux/SKILL.md +81 -0
  303. package/skills/theme-factory/.plugin/plugin.json +19 -0
  304. package/skills/theme-factory/LICENSE.txt +202 -0
  305. package/skills/theme-factory/README.md +58 -0
  306. package/skills/theme-factory/SKILL.md +59 -0
  307. package/skills/theme-factory/theme-showcase.pdf +0 -0
  308. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  309. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  310. package/skills/theme-factory/themes/desert-rose.md +19 -0
  311. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  312. package/skills/theme-factory/themes/golden-hour.md +19 -0
  313. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  314. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  315. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  316. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  317. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  318. package/skills/uv/.plugin/plugin.json +18 -0
  319. package/skills/uv/README.md +5 -0
  320. package/skills/uv/SKILL.md +95 -0
  321. package/skills/uv/references/README.md +5 -0
  322. package/skills/vercel/.plugin/plugin.json +18 -0
  323. package/skills/vercel/README.md +108 -0
  324. package/skills/vercel/SKILL.md +103 -0
  325. package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
  326. package/tests/test_catalogs.py +109 -0
  327. package/tests/test_code_review_risk_evaluation.py +94 -0
  328. package/tests/test_issue_duplicate_checker.py +240 -0
  329. package/tests/test_openhands_api_python.py +152 -0
  330. package/tests/test_plugin_manifest.py +83 -0
  331. package/tests/test_pr_review_diff_payload.py +202 -0
  332. package/tests/test_pr_review_feedback.py +263 -0
  333. package/tests/test_pr_review_prompt.py +152 -0
  334. package/tests/test_pr_review_review_context.py +253 -0
  335. package/tests/test_qa_changes.py +232 -0
  336. package/tests/test_qa_changes_evaluation.py +259 -0
  337. package/tests/test_release_notes_generator.py +990 -0
  338. package/tests/test_sdk_loading.py +150 -0
  339. package/tests/test_skill_plugin_loading.py +149 -0
  340. package/tests/test_skills_have_readme.py +66 -0
  341. package/tests/test_sync_extensions.py +292 -0
  342. package/tests/test_workflow_sync.py +46 -0
  343. package/utils/analysis/README.md +7 -0
  344. package/utils/analysis/laminar_signals/README.md +211 -0
  345. package/utils/analysis/laminar_signals/analyze.py +780 -0
  346. package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
  347. package/utils/analysis/laminar_signals/templates/pr_review.j2 +61 -0
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env python3
2
+ """Keep the OpenHands extensions registry in sync.
3
+
4
+ Four sync tasks, runnable individually or all at once:
5
+
6
+ 1. **commands** — generate Claude Code ``commands/<trigger>.md`` files from
7
+ SKILL.md slash-triggers.
8
+ 2. **catalog** — regenerate the auto-generated catalog section in README.md.
9
+ 3. **coverage** — warn when a skill/plugin directory is not listed in any
10
+ marketplace, or a marketplace entry points to a missing directory.
11
+ 4. **symlinks** — enforce ``.plugin/`` as the canonical manifest directory
12
+ with vendor symlinks (``.claude-plugin``, ``.codex-plugin``).
13
+
14
+ Usage:
15
+ python scripts/sync_extensions.py # run all, write changes
16
+ python scripts/sync_extensions.py --check # CI mode — exit 1 if anything is out of sync
17
+ python scripts/sync_extensions.py commands # only sync command files
18
+ python scripts/sync_extensions.py catalog # only regenerate README catalog
19
+ python scripts/sync_extensions.py coverage # only check marketplace coverage
20
+ python scripts/sync_extensions.py symlinks # only check/fix vendor symlinks
21
+ python scripts/sync_extensions.py commands catalog # combine sub-commands
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import json
28
+ import re
29
+ import sys
30
+ from dataclasses import dataclass
31
+ from pathlib import Path
32
+
33
+ import yaml # requires: pip install pyyaml (also in pyproject.toml [test] group)
34
+
35
+ REPO_ROOT = Path(__file__).resolve().parent.parent
36
+ README_PATH = REPO_ROOT / "README.md"
37
+ SKILL_DIRS = [REPO_ROOT / "skills", REPO_ROOT / "plugins"]
38
+ MARKETPLACES_DIR = REPO_ROOT / "marketplaces"
39
+ # Max description length in the catalog table. 120 chars fits GitHub's
40
+ # diff viewer and rendered Markdown tables without horizontal scroll.
41
+ MAX_DESC_LEN = 120
42
+
43
+ # Sentinel markers in README.md
44
+ CATALOG_BEGIN = "<!-- BEGIN AUTO-GENERATED CATALOG -->"
45
+ CATALOG_END = "<!-- END AUTO-GENERATED CATALOG -->"
46
+
47
+ # Marker for auto-generated Claude Code command files (YAML comment inside frontmatter)
48
+ CMD_MARKER = "# auto-generated by sync_extensions.py"
49
+ # Also recognize the legacy HTML-comment headers from older versions of the script
50
+ _LEGACY_CMD_MARKERS = [
51
+ "<!-- AUTO-GENERATED by scripts/sync_extensions.py",
52
+ "<!-- AUTO-GENERATED by scripts/sync_claude_commands.py",
53
+ ]
54
+
55
+
56
+ # ── Frontmatter helpers ──────────────────────────────────────────────
57
+
58
+ def parse_frontmatter(text: str) -> dict[str, str | list[str]]:
59
+ """Extract name, description, and triggers from YAML frontmatter.
60
+
61
+ Uses PyYAML for robust parsing. Returns an empty dict when the
62
+ frontmatter is missing or malformed.
63
+ """
64
+ m = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
65
+ if not m:
66
+ return {}
67
+ block = m.group(1)
68
+ try:
69
+ data = yaml.safe_load(block)
70
+ except yaml.YAMLError as exc:
71
+ print(f"[warning] malformed YAML frontmatter: {exc}", file=sys.stderr)
72
+ return {}
73
+ if not isinstance(data, dict):
74
+ return {}
75
+ result: dict[str, str | list[str]] = {}
76
+ for key in ("name", "description"):
77
+ if key in data and data[key] is not None:
78
+ result[key] = str(data[key]).strip()
79
+ triggers = data.get("triggers")
80
+ if isinstance(triggers, list):
81
+ result["triggers"] = [str(t).strip() for t in triggers if t is not None]
82
+ else:
83
+ result["triggers"] = []
84
+ return result
85
+
86
+
87
+ def slash_triggers(meta: dict) -> list[str]:
88
+ return [t for t in meta.get("triggers", []) if isinstance(t, str) and t.startswith("/")]
89
+
90
+
91
+ # ── Marketplace loading ──────────────────────────────────────────────
92
+
93
+ def load_marketplaces() -> list[dict]:
94
+ """Return list of parsed marketplace dicts, sorted by filename."""
95
+ mps: list[dict] = []
96
+ for mp_file in sorted(MARKETPLACES_DIR.glob("*.json")):
97
+ try:
98
+ with open(mp_file) as f:
99
+ data = json.load(f)
100
+ except (json.JSONDecodeError, OSError) as exc:
101
+ print(f"[warning] skipping {mp_file.name}: {exc}", file=sys.stderr)
102
+ continue
103
+ data["_file"] = mp_file
104
+ mps.append(data)
105
+ return mps
106
+
107
+
108
+ # ── 1. Claude Code command sync ──────────────────────────────────────
109
+
110
+ def build_command_content(description: str) -> str:
111
+ lines = [
112
+ "---",
113
+ CMD_MARKER,
114
+ f"description: {description}",
115
+ "---",
116
+ "",
117
+ "Read and follow the complete instructions in the SKILL.md file located in this skill's directory.",
118
+ "",
119
+ "$ARGUMENTS",
120
+ "",
121
+ ]
122
+ return "\n".join(lines)
123
+
124
+
125
+ @dataclass
126
+ class CommandSpec:
127
+ """A Claude Code command file that should exist for a slash trigger."""
128
+ path: Path
129
+ trigger: str
130
+ description: str
131
+
132
+
133
+ def collect_needed_commands() -> list[CommandSpec]:
134
+ """Return a CommandSpec for every slash trigger found in SKILL.md files."""
135
+ needed: list[CommandSpec] = []
136
+ for base in SKILL_DIRS:
137
+ if not base.is_dir():
138
+ continue
139
+ for skill_dir in sorted(base.iterdir()):
140
+ skill_md = skill_dir / "SKILL.md"
141
+ if not skill_md.is_file():
142
+ continue
143
+ meta = parse_frontmatter(skill_md.read_text())
144
+ desc = str(meta.get("description", ""))
145
+ for trigger in slash_triggers(meta):
146
+ cmd_name = trigger.lstrip("/")
147
+ cmd_path = skill_dir / "commands" / f"{cmd_name}.md"
148
+ needed.append(CommandSpec(path=cmd_path, trigger=trigger, description=desc))
149
+ return needed
150
+
151
+
152
+ def sync_commands(*, check: bool) -> list[str]:
153
+ """Returns list of problem descriptions (empty = all good)."""
154
+ problems: list[str] = []
155
+ for spec in collect_needed_commands():
156
+ expected = build_command_content(spec.description)
157
+ cmd_path = spec.path
158
+ if cmd_path.is_file():
159
+ existing = cmd_path.read_text()
160
+ if existing == expected:
161
+ continue
162
+ is_auto = CMD_MARKER in existing or any(m in existing for m in _LEGACY_CMD_MARKERS)
163
+ if not is_auto:
164
+ rel = cmd_path.relative_to(REPO_ROOT)
165
+ msg = (
166
+ f"{rel} exists but has no auto-generated marker "
167
+ f"— it won't be updated. Add the marker or delete "
168
+ f"the file to let sync manage it."
169
+ )
170
+ if check:
171
+ problems.append(f"manually-edited: {rel}")
172
+ else:
173
+ print(f"[warning] {msg}", file=sys.stderr)
174
+ continue
175
+ problems.append(f"stale: {cmd_path.relative_to(REPO_ROOT)}")
176
+ else:
177
+ problems.append(f"missing: {cmd_path.relative_to(REPO_ROOT)}")
178
+
179
+ if not check:
180
+ cmd_path.parent.mkdir(parents=True, exist_ok=True)
181
+ cmd_path.write_text(expected)
182
+
183
+ return problems
184
+
185
+
186
+ # ── 2. README catalog generation ─────────────────────────────────────
187
+
188
+ def _entry_type(source: str) -> str:
189
+ """Classify a marketplace entry as ``"skill"`` or ``"plugin"``.
190
+
191
+ Checks the source path prefix first, then falls back to examining
192
+ the resolved directory contents (SKILL.md → skill, hooks/ or
193
+ scripts/ → plugin). Returns ``"skill"`` as the default when the
194
+ type cannot be determined, since most entries are skills.
195
+ """
196
+ if source.startswith("./skills/") or source.startswith("../skills/"):
197
+ return "skill"
198
+ if source.startswith("./plugins/") or source.startswith("../plugins/"):
199
+ return "plugin"
200
+ resolved = REPO_ROOT / source.lstrip("./")
201
+ if resolved.is_dir():
202
+ if "skills" in resolved.parts:
203
+ return "skill"
204
+ if "plugins" in resolved.parts:
205
+ return "plugin"
206
+ # Infer from directory contents
207
+ if (resolved / "hooks").is_dir() or (resolved / "scripts").is_dir():
208
+ return "plugin"
209
+ return "skill"
210
+
211
+
212
+ _slash_cmd_cache: dict[str, list[str]] = {}
213
+
214
+
215
+ def _get_slash_commands(source: str) -> list[str]:
216
+ """Read SKILL.md from source path to extract slash triggers (cached)."""
217
+ if source in _slash_cmd_cache:
218
+ return _slash_cmd_cache[source]
219
+ skill_md = REPO_ROOT / source.lstrip("./") / "SKILL.md"
220
+ if not skill_md.is_file():
221
+ _slash_cmd_cache[source] = []
222
+ return []
223
+ meta = parse_frontmatter(skill_md.read_text())
224
+ result = slash_triggers(meta)
225
+ _slash_cmd_cache[source] = result
226
+ return result
227
+
228
+
229
+ def _format_marketplace_section(mp: dict) -> list[str]:
230
+ """Format a single marketplace as Markdown lines."""
231
+ mp_name = mp.get("name", mp["_file"].stem)
232
+ mp_desc = mp.get("metadata", {}).get("description", "")
233
+ plugins = mp.get("plugins", [])
234
+ skill_count = sum(1 for p in plugins if _entry_type(p.get("source", "")) == "skill")
235
+ plugin_count = sum(1 for p in plugins if _entry_type(p.get("source", "")) == "plugin")
236
+
237
+ lines: list[str] = []
238
+ lines.append(f"### {mp_name}")
239
+ lines.append("")
240
+ if mp_desc:
241
+ lines.append(mp_desc)
242
+ lines.append("")
243
+ lines.append(f"**{len(plugins)} extensions** ({skill_count} skills, {plugin_count} plugins)")
244
+ lines.append("")
245
+ lines.append("| Name | Type | Description | Commands |")
246
+ lines.append("|------|------|-------------|----------|")
247
+ for p in sorted(plugins, key=lambda x: x["name"]):
248
+ name = p["name"]
249
+ etype = _entry_type(p.get("source", ""))
250
+ desc = (p.get("description") or "").replace("|", "\\|")
251
+ if len(desc) > MAX_DESC_LEN:
252
+ desc = desc[:MAX_DESC_LEN - 3] + "..."
253
+ cmds = _get_slash_commands(p.get("source", ""))
254
+ cmds_str = ", ".join(f"`{c}`" for c in cmds) if cmds else "—"
255
+ lines.append(f"| {name} | {etype} | {desc} | {cmds_str} |")
256
+
257
+ lines.append("")
258
+ return lines
259
+
260
+
261
+ def generate_catalog() -> str:
262
+ """Build the Markdown catalog section."""
263
+ mps = load_marketplaces()
264
+ total_skills = 0
265
+ total_plugins = 0
266
+
267
+ for mp in mps:
268
+ for p in mp.get("plugins", []):
269
+ t = _entry_type(p.get("source", ""))
270
+ if t == "skill":
271
+ total_skills += 1
272
+ elif t == "plugin":
273
+ total_plugins += 1
274
+
275
+ lines: list[str] = [""]
276
+ total = sum(len(mp.get("plugins", [])) for mp in mps)
277
+ lines.append(
278
+ f"This repository contains **{len(mps)} marketplace(s)** "
279
+ f"with **{total} extensions** ({total_skills} skills, {total_plugins} plugins)."
280
+ )
281
+ lines.append("")
282
+
283
+ for mp in mps:
284
+ lines.extend(_format_marketplace_section(mp))
285
+ return "\n".join(lines)
286
+
287
+
288
+ def sync_catalog(*, check: bool) -> list[str]:
289
+ """Returns list of problem descriptions (empty = all good)."""
290
+ if not README_PATH.is_file():
291
+ return ["README.md not found"]
292
+
293
+ readme = README_PATH.read_text()
294
+ if CATALOG_BEGIN not in readme or CATALOG_END not in readme:
295
+ return [
296
+ f"README.md missing catalog markers ({CATALOG_BEGIN} / {CATALOG_END}). "
297
+ "Add them where the catalog should appear."
298
+ ]
299
+
300
+ before = readme[: readme.index(CATALOG_BEGIN) + len(CATALOG_BEGIN)]
301
+ after = readme[readme.index(CATALOG_END) :]
302
+ existing_catalog = readme[len(before) : readme.index(CATALOG_END)]
303
+
304
+ new_catalog = generate_catalog()
305
+
306
+ if existing_catalog == new_catalog:
307
+ return []
308
+
309
+ if check:
310
+ return ["README.md catalog section is out of date"]
311
+
312
+ README_PATH.write_text(before + new_catalog + after)
313
+ return []
314
+
315
+
316
+ # ── 3. Marketplace coverage ─────────────────────────────────────────
317
+
318
+ def sync_coverage(*, check: bool) -> list[str]:
319
+ """Check that every directory is in a marketplace and vice versa."""
320
+ # Collect all dirs with SKILL.md or .plugin/plugin.json (or vendor symlinks like .claude-plugin)
321
+ all_dirs: set[str] = set()
322
+ for base in SKILL_DIRS:
323
+ if not base.is_dir():
324
+ continue
325
+ for d in base.iterdir():
326
+ if not d.is_dir() or d.name.startswith("."):
327
+ continue
328
+ has_skill = (d / "SKILL.md").exists()
329
+ has_plugin = (
330
+ (d / ".claude-plugin" / "plugin.json").exists()
331
+ or (d / ".plugin" / "plugin.json").exists()
332
+ )
333
+ if has_skill or has_plugin:
334
+ all_dirs.add(f"./{base.name}/{d.name}")
335
+
336
+ # Collect all marketplace sources
337
+ all_sources: set[str] = set()
338
+ for mp in load_marketplaces():
339
+ for p in mp.get("plugins", []):
340
+ all_sources.add(p.get("source", ""))
341
+
342
+ # Collect skill dirs that are symlink targets inside a plugin's skills/
343
+ # subdirectory — these are covered by the parent plugin entry.
344
+ symlink_targets: set[str] = set()
345
+ plugins_dir = REPO_ROOT / "plugins"
346
+ if plugins_dir.is_dir():
347
+ for plugin_dir in plugins_dir.iterdir():
348
+ skills_sub = plugin_dir / "skills"
349
+ if not skills_sub.is_dir():
350
+ continue
351
+ for entry in skills_sub.iterdir():
352
+ if entry.is_symlink():
353
+ resolved = entry.resolve()
354
+ try:
355
+ rel = f"./{resolved.relative_to(REPO_ROOT)}"
356
+ symlink_targets.add(rel)
357
+ except ValueError:
358
+ pass
359
+
360
+ problems: list[str] = []
361
+
362
+ missing_from_mp = sorted(all_dirs - all_sources - symlink_targets)
363
+ for d in missing_from_mp:
364
+ problems.append(f"not in any marketplace: {d}")
365
+
366
+ ghost_entries = sorted(all_sources - all_dirs)
367
+ for s in ghost_entries:
368
+ p = REPO_ROOT / s.lstrip("./")
369
+ if not p.is_dir():
370
+ problems.append(f"marketplace entry points to missing directory: {s}")
371
+ else:
372
+ problems.append(f"marketplace entry has no SKILL.md or plugin.json: {s}")
373
+
374
+ return problems
375
+
376
+
377
+ # ── 4. Vendor symlinks ────────────────────────────────────────────────
378
+
379
+ VENDOR_SYMLINKS = [".claude-plugin", ".codex-plugin"] # add new vendors here
380
+
381
+
382
+ def _check_vendor_symlinks(directory: Path, check: bool) -> list[str]:
383
+ """Check/fix vendor symlinks for a single directory with .plugin/."""
384
+ problems: list[str] = []
385
+ canon = directory / ".plugin"
386
+ if not canon.is_dir():
387
+ return problems
388
+ for vendor in VENDOR_SYMLINKS:
389
+ link = directory / vendor
390
+ if link.is_symlink():
391
+ target = link.resolve()
392
+ if target == canon.resolve():
393
+ continue
394
+ problems.append(f"wrong target: {link.relative_to(REPO_ROOT)} → {link.readlink()}")
395
+ elif link.exists():
396
+ problems.append(f"not a symlink: {link.relative_to(REPO_ROOT)}")
397
+ continue
398
+ else:
399
+ problems.append(f"missing: {link.relative_to(REPO_ROOT)}")
400
+ if not check:
401
+ link.unlink(missing_ok=True)
402
+ link.symlink_to(".plugin")
403
+ return problems
404
+
405
+
406
+ def sync_symlinks(*, check: bool) -> list[str]:
407
+ """Ensure every directory with .plugin/ also has vendor symlinks.
408
+
409
+ Scans both plugins/ and skills/ directories. Skills that ship a
410
+ ``.plugin/`` manifest (e.g. those with ``commands/``) need vendor
411
+ symlinks so that Codex and Claude Code can discover them.
412
+ """
413
+ problems: list[str] = []
414
+ for base in SKILL_DIRS:
415
+ if not base.is_dir():
416
+ continue
417
+ for entry_dir in sorted(base.iterdir()):
418
+ if not entry_dir.is_dir() or entry_dir.name.startswith("."):
419
+ continue
420
+ problems.extend(_check_vendor_symlinks(entry_dir, check))
421
+ return problems
422
+
423
+
424
+ # ── Main ─────────────────────────────────────────────────────────────
425
+
426
+ ALL_SYNCS = {
427
+ "commands": sync_commands,
428
+ "catalog": sync_catalog,
429
+ "coverage": sync_coverage,
430
+ "symlinks": sync_symlinks,
431
+ }
432
+
433
+
434
+ def main() -> int:
435
+ parser = argparse.ArgumentParser(
436
+ description="Keep the OpenHands extensions registry in sync.",
437
+ formatter_class=argparse.RawDescriptionHelpFormatter,
438
+ epilog=__doc__,
439
+ )
440
+ valid_tasks = list(ALL_SYNCS.keys())
441
+ parser.add_argument(
442
+ "tasks",
443
+ nargs="*",
444
+ default=[],
445
+ metavar="TASK",
446
+ help=f"Which sync tasks to run: {', '.join(valid_tasks)} (default: all).",
447
+ )
448
+ parser.add_argument(
449
+ "--check",
450
+ action="store_true",
451
+ help="Check mode: exit 1 if anything is out of sync (for CI).",
452
+ )
453
+ args = parser.parse_args()
454
+ for t in args.tasks:
455
+ if t not in ALL_SYNCS:
456
+ parser.error(f"unknown task: {t!r} (choose from {', '.join(valid_tasks)})")
457
+ tasks = args.tasks or list(ALL_SYNCS.keys())
458
+
459
+ all_problems: dict[str, list[str]] = {}
460
+ for task in tasks:
461
+ fn = ALL_SYNCS[task]
462
+ problems = fn(check=args.check)
463
+ if problems:
464
+ all_problems[task] = problems
465
+
466
+ if all_problems:
467
+ for task, problems in all_problems.items():
468
+ severity = "warning" if task == "coverage" else "error"
469
+ print(f"\n[{severity}] {task}:")
470
+ for p in problems:
471
+ print(f" {p}")
472
+ if args.check:
473
+ has_errors = any(k != "coverage" for k in all_problems)
474
+ has_warnings = "coverage" in all_problems
475
+ if has_errors:
476
+ print(f"\nRun `python scripts/sync_extensions.py` to fix.")
477
+ return 1
478
+ if has_warnings:
479
+ # Coverage warnings don't fail CI, just warn
480
+ print(f"\n⚠️ Coverage warnings above are non-blocking.")
481
+ return 0
482
+ else:
483
+ print("\nSync complete.")
484
+ return 0
485
+
486
+ if args.check:
487
+ print("All extensions in sync. ✓")
488
+ else:
489
+ print("Everything already up to date.")
490
+ return 0
491
+
492
+
493
+ if __name__ == "__main__":
494
+ sys.exit(main())