@openhands/extensions 0.0.1-alpha → 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.
- package/.agents/skills/custom-codereview-guide.md +25 -0
- package/.github/pull_request_template.md +38 -0
- package/.github/release.yml +14 -0
- package/.github/workflows/check-extensions.yml +72 -0
- package/.github/workflows/npm-publish.yml +89 -0
- package/.github/workflows/pr.yml +30 -0
- package/.github/workflows/release.yml +24 -0
- package/.github/workflows/tests.yml +25 -0
- package/.github/workflows/vulnerability-scan.yml +87 -0
- package/.release-please-manifest.json +3 -0
- package/AGENTS.md +132 -0
- package/README.md +10 -0
- package/analysis_results.md +162 -0
- package/marketplaces/large-codebase.json +66 -0
- package/marketplaces/openhands-extensions.json +682 -0
- package/package.json +4 -10
- package/plugins/README.md +30 -0
- package/plugins/city-weather/.plugin/plugin.json +13 -0
- package/plugins/city-weather/README.md +145 -0
- package/plugins/city-weather/commands/now.md +56 -0
- package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
- package/plugins/cobol-modernization/README.md +201 -0
- package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
- package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
- package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
- package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
- package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
- package/plugins/issue-duplicate-checker/README.md +51 -0
- package/plugins/issue-duplicate-checker/action.yml +349 -0
- package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
- package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
- package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
- package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
- package/plugins/magic-test/.plugin/plugin.json +13 -0
- package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
- package/plugins/migration-scoring/.plugin/plugin.json +19 -0
- package/plugins/migration-scoring/README.md +244 -0
- package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
- package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
- package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
- package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
- package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
- package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
- package/plugins/onboarding/.plugin/plugin.json +20 -0
- package/plugins/onboarding/README.md +30 -0
- package/plugins/onboarding/references/criteria.md +144 -0
- package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
- package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
- package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
- package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
- package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
- package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
- package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
- package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
- package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
- package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
- package/plugins/openhands/.plugin/plugin.json +13 -0
- package/plugins/openhands/README.md +52 -0
- package/plugins/openhands/SKILL.md +61 -0
- package/plugins/openhands/commands/create.md +55 -0
- package/plugins/openhands/commands/openhands-cloud.md +8 -0
- package/plugins/openhands/scripts/run.sh +69 -0
- package/plugins/pr-review/.plugin/plugin.json +13 -0
- package/plugins/pr-review/README.md +393 -0
- package/plugins/pr-review/action.yml +298 -0
- package/plugins/pr-review/scripts/agent_script.py +1282 -0
- package/plugins/pr-review/scripts/evaluate_review.py +655 -0
- package/plugins/pr-review/scripts/prompt.py +260 -0
- package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
- package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
- package/plugins/qa-changes/.plugin/plugin.json +11 -0
- package/plugins/qa-changes/README.md +185 -0
- package/plugins/qa-changes/action.yml +181 -0
- package/plugins/qa-changes/scripts/agent_script.py +406 -0
- package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
- package/plugins/qa-changes/scripts/prompt.py +174 -0
- package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
- package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
- package/plugins/release-notes/.plugin/plugin.json +19 -0
- package/plugins/release-notes/README.md +283 -0
- package/plugins/release-notes/SKILL.md +83 -0
- package/plugins/release-notes/action.yml +117 -0
- package/plugins/release-notes/commands/release-notes.md +8 -0
- package/plugins/release-notes/scripts/agent_script.py +292 -0
- package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
- package/plugins/release-notes/scripts/prompt.py +90 -0
- package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
- package/plugins/release-notes/workflows/release-notes.yml +76 -0
- package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/README.md +217 -0
- package/plugins/vulnerability-remediation/action.yml +187 -0
- package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
- package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
- package/pyproject.toml +12 -0
- package/release-please-config.json +16 -0
- package/scripts/sync_extensions.py +494 -0
- package/scripts/sync_openhands_sdk_skill.py +264 -0
- package/skills/README.md +159 -0
- package/skills/add-javadoc/.plugin/plugin.json +18 -0
- package/skills/add-javadoc/README.md +40 -0
- package/skills/add-javadoc/SKILL.md +35 -0
- package/skills/add-javadoc/references/example.md +32 -0
- package/skills/add-skill/.plugin/plugin.json +18 -0
- package/skills/add-skill/README.md +67 -0
- package/skills/add-skill/SKILL.md +47 -0
- package/skills/add-skill/scripts/fetch_skill.py +259 -0
- package/skills/agent-creator/.plugin/plugin.json +20 -0
- package/skills/agent-creator/README.md +104 -0
- package/skills/agent-creator/SKILL.md +190 -0
- package/skills/agent-creator/commands/agent-creator.md +8 -0
- package/skills/agent-creator/references/fallback.md +117 -0
- package/skills/agent-memory/.plugin/plugin.json +18 -0
- package/skills/agent-memory/README.md +35 -0
- package/skills/agent-memory/SKILL.md +30 -0
- package/skills/agent-memory/commands/remember.md +8 -0
- package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
- package/skills/agent-sdk-builder/README.md +40 -0
- package/skills/agent-sdk-builder/SKILL.md +37 -0
- package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
- package/skills/azure-devops/.plugin/plugin.json +18 -0
- package/skills/azure-devops/README.md +55 -0
- package/skills/azure-devops/SKILL.md +50 -0
- package/skills/bitbucket/.plugin/plugin.json +17 -0
- package/skills/bitbucket/README.md +50 -0
- package/skills/bitbucket/SKILL.md +45 -0
- package/skills/code-review/.plugin/plugin.json +19 -0
- package/skills/code-review/README.md +18 -0
- package/skills/code-review/SKILL.md +208 -0
- package/skills/code-review/commands/codereview-roasted.md +8 -0
- package/skills/code-review/commands/codereview.md +8 -0
- package/skills/code-review/references/risk-evaluation.md +41 -0
- package/skills/code-review/references/supply-chain-security.md +31 -0
- package/skills/code-simplifier/.plugin/plugin.json +21 -0
- package/skills/code-simplifier/README.md +30 -0
- package/skills/code-simplifier/SKILL.md +91 -0
- package/skills/code-simplifier/commands/simplify.md +8 -0
- package/skills/code-simplifier/references/code-quality-review.md +86 -0
- package/skills/code-simplifier/references/code-reuse-review.md +63 -0
- package/skills/code-simplifier/references/efficiency-review.md +81 -0
- package/skills/datadog/.plugin/plugin.json +19 -0
- package/skills/datadog/README.md +100 -0
- package/skills/datadog/SKILL.md +95 -0
- package/skills/deno/.plugin/plugin.json +18 -0
- package/skills/deno/README.md +5 -0
- package/skills/deno/SKILL.md +99 -0
- package/skills/deno/references/README.md +6 -0
- package/skills/discord/.plugin/plugin.json +18 -0
- package/skills/discord/README.md +31 -0
- package/skills/discord/SKILL.md +109 -0
- package/skills/discord/__init__.py +0 -0
- package/skills/discord/references/REFERENCE.md +78 -0
- package/skills/discord/scripts/__init__.py +0 -0
- package/skills/discord/scripts/_http.py +127 -0
- package/skills/discord/scripts/post_webhook.py +106 -0
- package/skills/discord/scripts/send_message.py +102 -0
- package/skills/docker/.plugin/plugin.json +17 -0
- package/skills/docker/README.md +34 -0
- package/skills/docker/SKILL.md +29 -0
- package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
- package/skills/evidence-based-citations/README.md +31 -0
- package/skills/evidence-based-citations/SKILL.md +59 -0
- package/skills/flarglebargle/.plugin/plugin.json +16 -0
- package/skills/flarglebargle/README.md +14 -0
- package/skills/flarglebargle/SKILL.md +9 -0
- package/skills/frontend-design/.plugin/plugin.json +21 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/README.md +42 -0
- package/skills/frontend-design/SKILL.md +42 -0
- package/skills/github/.plugin/plugin.json +19 -0
- package/skills/github/README.md +42 -0
- package/skills/github/SKILL.md +106 -0
- package/skills/github-pr-review/.plugin/plugin.json +18 -0
- package/skills/github-pr-review/README.md +145 -0
- package/skills/github-pr-review/SKILL.md +148 -0
- package/skills/github-pr-review/commands/github-pr-review.md +8 -0
- package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
- package/skills/github-pr-reviewer/README.md +34 -0
- package/skills/github-pr-reviewer/SKILL.md +89 -0
- package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
- package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
- package/skills/github-repo-monitor/README.md +70 -0
- package/skills/github-repo-monitor/SKILL.md +316 -0
- package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
- package/skills/github-repo-monitor/references/github-api.md +241 -0
- package/skills/github-repo-monitor/references/state-schema.md +160 -0
- package/skills/github-repo-monitor/scripts/main.py +915 -0
- package/skills/github-repo-monitor/tests/test_main.py +400 -0
- package/skills/gitlab/.plugin/plugin.json +17 -0
- package/skills/gitlab/README.md +37 -0
- package/skills/gitlab/SKILL.md +32 -0
- package/skills/incident-retrospective/.plugin/plugin.json +21 -0
- package/skills/incident-retrospective/README.md +34 -0
- package/skills/incident-retrospective/SKILL.md +98 -0
- package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
- package/skills/iterate/.plugin/plugin.json +13 -0
- package/skills/iterate/README.md +25 -0
- package/skills/iterate/SKILL.md +399 -0
- package/skills/iterate/commands/babysit.md +8 -0
- package/skills/iterate/commands/iterate.md +8 -0
- package/skills/iterate/commands/verify.md +8 -0
- package/skills/iterate/references/heuristics.md +58 -0
- package/skills/iterate/references/verification.md +96 -0
- package/skills/jupyter/.plugin/plugin.json +18 -0
- package/skills/jupyter/README.md +55 -0
- package/skills/jupyter/SKILL.md +50 -0
- package/skills/kubernetes/.plugin/plugin.json +18 -0
- package/skills/kubernetes/README.md +53 -0
- package/skills/kubernetes/SKILL.md +48 -0
- package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
- package/skills/learn-from-code-review/README.md +64 -0
- package/skills/learn-from-code-review/SKILL.md +186 -0
- package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
- package/skills/linear/.plugin/plugin.json +19 -0
- package/skills/linear/README.md +58 -0
- package/skills/linear/SKILL.md +213 -0
- package/skills/linear-triage/.plugin/plugin.json +21 -0
- package/skills/linear-triage/README.md +34 -0
- package/skills/linear-triage/SKILL.md +91 -0
- package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
- package/skills/notion/.plugin/plugin.json +17 -0
- package/skills/notion/README.md +114 -0
- package/skills/notion/SKILL.md +109 -0
- package/skills/npm/.plugin/plugin.json +17 -0
- package/skills/npm/README.md +14 -0
- package/skills/npm/SKILL.md +9 -0
- package/skills/openhands-api/.plugin/plugin.json +22 -0
- package/skills/openhands-api/README.md +48 -0
- package/skills/openhands-api/SKILL.md +399 -0
- package/skills/openhands-api/references/README.md +33 -0
- package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
- package/skills/openhands-api/references/example_prompt.md +12 -0
- package/skills/openhands-api/scripts/openhands_api.py +606 -0
- package/skills/openhands-api/scripts/openhands_api.ts +252 -0
- package/skills/openhands-automation/.plugin/plugin.json +19 -0
- package/skills/openhands-automation/README.md +89 -0
- package/skills/openhands-automation/SKILL.md +875 -0
- package/skills/openhands-automation/commands/automation:create.md +8 -0
- package/skills/openhands-automation/references/ab-testing.md +185 -0
- package/skills/openhands-automation/references/custom-automation.md +644 -0
- package/skills/openhands-sdk/.plugin/plugin.json +20 -0
- package/skills/openhands-sdk/README.md +22 -0
- package/skills/openhands-sdk/SKILL.md +229 -0
- package/skills/openhands-sdk/commands/sdk.md +8 -0
- package/skills/pdflatex/.plugin/plugin.json +18 -0
- package/skills/pdflatex/README.md +39 -0
- package/skills/pdflatex/SKILL.md +34 -0
- package/skills/prd/.plugin/plugin.json +19 -0
- package/skills/prd/README.md +28 -0
- package/skills/prd/SKILL.md +237 -0
- package/skills/prd/commands/prd.md +8 -0
- package/skills/qa-changes/README.md +18 -0
- package/skills/qa-changes/SKILL.md +229 -0
- package/skills/qa-changes/commands/qa-changes.md +8 -0
- package/skills/release-notes/README.md +24 -0
- package/skills/release-notes/SKILL.md +19 -0
- package/skills/release-notes/commands/release-notes.md +8 -0
- package/skills/research-brief/.plugin/plugin.json +20 -0
- package/skills/research-brief/README.md +34 -0
- package/skills/research-brief/SKILL.md +99 -0
- package/skills/research-brief/commands/research-brief:setup.md +8 -0
- package/skills/security/.plugin/plugin.json +18 -0
- package/skills/security/README.md +38 -0
- package/skills/security/SKILL.md +33 -0
- package/skills/skill-creator/.plugin/plugin.json +17 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/README.md +182 -0
- package/skills/skill-creator/SKILL.md +545 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/init_skill.py +303 -0
- package/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
- package/skills/slack-channel-monitor/README.md +91 -0
- package/skills/slack-channel-monitor/SKILL.md +276 -0
- package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
- package/skills/slack-channel-monitor/references/slack-api.md +207 -0
- package/skills/slack-channel-monitor/references/state-schema.md +180 -0
- package/skills/slack-channel-monitor/scripts/main.py +962 -0
- package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
- package/skills/slack-standup-digest/README.md +34 -0
- package/skills/slack-standup-digest/SKILL.md +92 -0
- package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
- package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
- package/skills/spark-version-upgrade/README.md +54 -0
- package/skills/spark-version-upgrade/SKILL.md +233 -0
- package/skills/ssh/.plugin/plugin.json +18 -0
- package/skills/ssh/README.md +140 -0
- package/skills/ssh/SKILL.md +135 -0
- package/skills/swift-linux/.plugin/plugin.json +17 -0
- package/skills/swift-linux/README.md +86 -0
- package/skills/swift-linux/SKILL.md +81 -0
- package/skills/theme-factory/.plugin/plugin.json +19 -0
- package/skills/theme-factory/LICENSE.txt +202 -0
- package/skills/theme-factory/README.md +58 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/uv/.plugin/plugin.json +18 -0
- package/skills/uv/README.md +5 -0
- package/skills/uv/SKILL.md +95 -0
- package/skills/uv/references/README.md +5 -0
- package/skills/vercel/.plugin/plugin.json +18 -0
- package/skills/vercel/README.md +108 -0
- package/skills/vercel/SKILL.md +103 -0
- package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
- package/tests/test_catalogs.py +109 -0
- package/tests/test_code_review_risk_evaluation.py +94 -0
- package/tests/test_issue_duplicate_checker.py +240 -0
- package/tests/test_openhands_api_python.py +152 -0
- package/tests/test_plugin_manifest.py +83 -0
- package/tests/test_pr_review_diff_payload.py +202 -0
- package/tests/test_pr_review_feedback.py +263 -0
- package/tests/test_pr_review_prompt.py +152 -0
- package/tests/test_pr_review_review_context.py +253 -0
- package/tests/test_qa_changes.py +232 -0
- package/tests/test_qa_changes_evaluation.py +259 -0
- package/tests/test_release_notes_generator.py +990 -0
- package/tests/test_sdk_loading.py +150 -0
- package/tests/test_skill_plugin_loading.py +149 -0
- package/tests/test_skills_have_readme.py +66 -0
- package/tests/test_sync_extensions.py +292 -0
- package/tests/test_workflow_sync.py +46 -0
- package/utils/analysis/README.md +7 -0
- package/utils/analysis/laminar_signals/README.md +211 -0
- package/utils/analysis/laminar_signals/analyze.py +780 -0
- package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
- 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())
|