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.
- package/package.json +1 -1
- package/templates/.agents/agents/backend-specialist.md +263 -0
- package/templates/.agents/agents/code-archaeologist.md +106 -0
- package/templates/.agents/agents/database-architect.md +226 -0
- package/templates/.agents/agents/debugger.md +225 -0
- package/templates/.agents/agents/devops-engineer.md +242 -0
- package/templates/.agents/agents/documentation-writer.md +104 -0
- package/templates/.agents/agents/explorer-agent.md +73 -0
- package/templates/.agents/agents/frontend-specialist.md +593 -0
- package/templates/.agents/agents/game-developer.md +162 -0
- package/templates/.agents/agents/mobile-developer.md +377 -0
- package/templates/.agents/agents/orchestrator.md +416 -0
- package/templates/.agents/agents/penetration-tester.md +188 -0
- package/templates/.agents/agents/performance-optimizer.md +187 -0
- package/templates/.agents/agents/product-manager.md +112 -0
- package/templates/.agents/agents/product-owner.md +95 -0
- package/templates/.agents/agents/project-planner.md +406 -0
- package/templates/.agents/agents/qa-automation-engineer.md +103 -0
- package/templates/.agents/agents/security-auditor.md +170 -0
- package/templates/.agents/agents/seo-specialist.md +111 -0
- package/templates/.agents/agents/test-engineer.md +158 -0
- package/templates/.agents/rules/GEMINI.md +219 -0
- package/templates/.agents/scripts/auto_preview.py +148 -0
- package/templates/.agents/scripts/checklist.py +217 -0
- package/templates/.agents/scripts/session_manager.py +120 -0
- package/templates/.agents/scripts/verify_all.py +327 -0
- package/templates/.agents/workflows/brainstorm.md +113 -0
- package/templates/.agents/workflows/create.md +59 -0
- package/templates/.agents/workflows/debug.md +103 -0
- package/templates/.agents/workflows/deploy.md +176 -0
- package/templates/.agents/workflows/enhance.md +63 -0
- package/templates/.agents/workflows/orchestrate.md +237 -0
- package/templates/.agents/workflows/plan.md +89 -0
- package/templates/.agents/workflows/preview.md +81 -0
- package/templates/.agents/workflows/setup-brain.md +39 -0
- package/templates/.agents/workflows/status.md +86 -0
- package/templates/.agents/workflows/test.md +144 -0
- package/templates/.agents/workflows/ui-ux-pro-max.md +296 -0
- package/templates/skills_normal/api-patterns/scripts/api_validator.py +211 -0
- package/templates/skills_normal/database-design/scripts/schema_validator.py +172 -0
- package/templates/skills_normal/frontend-design/scripts/accessibility_checker.py +183 -0
- package/templates/skills_normal/frontend-design/scripts/ux_audit.py +722 -0
- package/templates/skills_normal/git-pushing/scripts/smart_commit.sh +19 -0
- package/templates/skills_normal/lint-and-validate/scripts/lint_runner.py +184 -0
- package/templates/skills_normal/lint-and-validate/scripts/type_coverage.py +173 -0
- package/templates/skills_normal/performance-profiling/scripts/lighthouse_audit.py +76 -0
- package/templates/skills_normal/senior-fullstack/scripts/code_quality_analyzer.py +114 -0
- package/templates/skills_normal/senior-fullstack/scripts/fullstack_scaffolder.py +114 -0
- package/templates/skills_normal/senior-fullstack/scripts/project_scaffolder.py +114 -0
- package/templates/skills_normal/seo-fundamentals/scripts/seo_checker.py +219 -0
- package/templates/skills_normal/testing-patterns/scripts/test_runner.py +219 -0
- package/templates/skills_normal/vulnerability-scanner/scripts/security_scan.py +458 -0
- package/templates/vault/007/scripts/config.py +472 -0
- package/templates/vault/007/scripts/full_audit.py +1306 -0
- package/templates/vault/007/scripts/quick_scan.py +481 -0
- package/templates/vault/007/scripts/requirements.txt +26 -0
- package/templates/vault/007/scripts/scanners/__init__.py +0 -0
- package/templates/vault/007/scripts/scanners/dependency_scanner.py +1305 -0
- package/templates/vault/007/scripts/scanners/injection_scanner.py +1104 -0
- package/templates/vault/007/scripts/scanners/secrets_scanner.py +1008 -0
- package/templates/vault/007/scripts/score_calculator.py +693 -0
- package/templates/vault/agent-orchestrator/scripts/match_skills.py +329 -0
- package/templates/vault/agent-orchestrator/scripts/orchestrate.py +304 -0
- package/templates/vault/agent-orchestrator/scripts/requirements.txt +1 -0
- package/templates/vault/agent-orchestrator/scripts/scan_registry.py +508 -0
- package/templates/vault/ai-studio-image/scripts/config.py +613 -0
- package/templates/vault/ai-studio-image/scripts/generate.py +630 -0
- package/templates/vault/ai-studio-image/scripts/prompt_engine.py +424 -0
- package/templates/vault/ai-studio-image/scripts/requirements.txt +4 -0
- package/templates/vault/ai-studio-image/scripts/templates.py +349 -0
- package/templates/vault/android_ui_verification/scripts/verify_ui.sh +32 -0
- package/templates/vault/apify-audience-analysis/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-brand-reputation-monitoring/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-competitor-intelligence/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-content-analytics/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-ecommerce/reference/scripts/package.json +3 -0
- package/templates/vault/apify-ecommerce/reference/scripts/run_actor.js +369 -0
- package/templates/vault/apify-influencer-discovery/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-lead-generation/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-market-research/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-trend-analysis/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-ultimate-scraper/reference/scripts/run_actor.js +363 -0
- package/templates/vault/audio-transcriber/scripts/install-requirements.sh +190 -0
- package/templates/vault/audio-transcriber/scripts/transcribe.py +486 -0
- package/templates/vault/claude-monitor/scripts/api_bench.py +240 -0
- package/templates/vault/claude-monitor/scripts/config.py +69 -0
- package/templates/vault/claude-monitor/scripts/health_check.py +362 -0
- package/templates/vault/claude-monitor/scripts/monitor.py +296 -0
- package/templates/vault/content-creator/scripts/brand_voice_analyzer.py +185 -0
- package/templates/vault/content-creator/scripts/seo_optimizer.py +419 -0
- package/templates/vault/context-agent/scripts/active_context.py +227 -0
- package/templates/vault/context-agent/scripts/compressor.py +149 -0
- package/templates/vault/context-agent/scripts/config.py +69 -0
- package/templates/vault/context-agent/scripts/context_loader.py +155 -0
- package/templates/vault/context-agent/scripts/context_manager.py +302 -0
- package/templates/vault/context-agent/scripts/models.py +103 -0
- package/templates/vault/context-agent/scripts/project_registry.py +132 -0
- package/templates/vault/context-agent/scripts/requirements.txt +6 -0
- package/templates/vault/context-agent/scripts/search.py +115 -0
- package/templates/vault/context-agent/scripts/session_parser.py +206 -0
- package/templates/vault/context-agent/scripts/session_summary.py +319 -0
- package/templates/vault/context-guardian/scripts/context_snapshot.py +229 -0
- package/templates/vault/docx/ooxml/scripts/pack.py +159 -0
- package/templates/vault/docx/ooxml/scripts/unpack.py +29 -0
- package/templates/vault/docx/ooxml/scripts/validate.py +69 -0
- package/templates/vault/docx/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/vault/docx/ooxml/scripts/validation/base.py +951 -0
- package/templates/vault/docx/ooxml/scripts/validation/docx.py +274 -0
- package/templates/vault/docx/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/vault/docx/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/vault/docx/scripts/__init__.py +1 -0
- package/templates/vault/docx/scripts/document.py +1276 -0
- package/templates/vault/docx/scripts/templates/comments.xml +3 -0
- package/templates/vault/docx/scripts/templates/commentsExtended.xml +3 -0
- package/templates/vault/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/templates/vault/docx/scripts/templates/commentsIds.xml +3 -0
- package/templates/vault/docx/scripts/templates/people.xml +3 -0
- package/templates/vault/docx/scripts/utilities.py +374 -0
- package/templates/vault/docx-official/ooxml/scripts/pack.py +159 -0
- package/templates/vault/docx-official/ooxml/scripts/unpack.py +29 -0
- package/templates/vault/docx-official/ooxml/scripts/validate.py +69 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/base.py +951 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/docx.py +274 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/vault/docx-official/scripts/__init__.py +1 -0
- package/templates/vault/docx-official/scripts/document.py +1276 -0
- package/templates/vault/docx-official/scripts/templates/comments.xml +3 -0
- package/templates/vault/docx-official/scripts/templates/commentsExtended.xml +3 -0
- package/templates/vault/docx-official/scripts/templates/commentsExtensible.xml +3 -0
- package/templates/vault/docx-official/scripts/templates/commentsIds.xml +3 -0
- package/templates/vault/docx-official/scripts/templates/people.xml +3 -0
- package/templates/vault/docx-official/scripts/utilities.py +374 -0
- package/templates/vault/geo-fundamentals/scripts/geo_checker.py +289 -0
- package/templates/vault/helm-chart-scaffolding/scripts/validate-chart.sh +244 -0
- package/templates/vault/i18n-localization/scripts/i18n_checker.py +241 -0
- package/templates/vault/instagram/scripts/account_setup.py +233 -0
- package/templates/vault/instagram/scripts/analyze.py +221 -0
- package/templates/vault/instagram/scripts/api_client.py +444 -0
- package/templates/vault/instagram/scripts/auth.py +411 -0
- package/templates/vault/instagram/scripts/comments.py +160 -0
- package/templates/vault/instagram/scripts/config.py +111 -0
- package/templates/vault/instagram/scripts/db.py +467 -0
- package/templates/vault/instagram/scripts/export.py +138 -0
- package/templates/vault/instagram/scripts/governance.py +233 -0
- package/templates/vault/instagram/scripts/hashtags.py +114 -0
- package/templates/vault/instagram/scripts/insights.py +170 -0
- package/templates/vault/instagram/scripts/media.py +65 -0
- package/templates/vault/instagram/scripts/messages.py +103 -0
- package/templates/vault/instagram/scripts/profile.py +58 -0
- package/templates/vault/instagram/scripts/publish.py +449 -0
- package/templates/vault/instagram/scripts/requirements.txt +5 -0
- package/templates/vault/instagram/scripts/run_all.py +189 -0
- package/templates/vault/instagram/scripts/schedule.py +189 -0
- package/templates/vault/instagram/scripts/serve_api.py +234 -0
- package/templates/vault/instagram/scripts/templates.py +155 -0
- package/templates/vault/junta-leiloeiros/scripts/db.py +216 -0
- package/templates/vault/junta-leiloeiros/scripts/export.py +137 -0
- package/templates/vault/junta-leiloeiros/scripts/requirements.txt +15 -0
- package/templates/vault/junta-leiloeiros/scripts/run_all.py +190 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/__init__.py +4 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/base_scraper.py +209 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/generic_scraper.py +110 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucap.py +110 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/juceac.py +72 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/juceal.py +72 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/juceb.py +68 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucec.py +63 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucema.py +211 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucemg.py +218 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucep.py +70 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucepa.py +74 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucepar.py +80 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucepe.py +78 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucepi.py +69 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucer.py +256 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucerja.py +170 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucern.py +71 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucesc.py +89 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucesp.py +233 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucetins.py +134 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucis_df.py +63 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucisrs.py +299 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/states.py +99 -0
- package/templates/vault/junta-leiloeiros/scripts/serve_api.py +164 -0
- package/templates/vault/junta-leiloeiros/scripts/web_scraper_fallback.py +233 -0
- package/templates/vault/last30days/scripts/last30days.py +521 -0
- package/templates/vault/last30days/scripts/lib/__init__.py +1 -0
- package/templates/vault/last30days/scripts/lib/cache.py +152 -0
- package/templates/vault/last30days/scripts/lib/dates.py +124 -0
- package/templates/vault/last30days/scripts/lib/dedupe.py +120 -0
- package/templates/vault/last30days/scripts/lib/env.py +149 -0
- package/templates/vault/last30days/scripts/lib/http.py +152 -0
- package/templates/vault/last30days/scripts/lib/models.py +175 -0
- package/templates/vault/last30days/scripts/lib/normalize.py +160 -0
- package/templates/vault/last30days/scripts/lib/openai_reddit.py +230 -0
- package/templates/vault/last30days/scripts/lib/reddit_enrich.py +232 -0
- package/templates/vault/last30days/scripts/lib/render.py +383 -0
- package/templates/vault/last30days/scripts/lib/schema.py +336 -0
- package/templates/vault/last30days/scripts/lib/score.py +311 -0
- package/templates/vault/last30days/scripts/lib/ui.py +324 -0
- package/templates/vault/last30days/scripts/lib/websearch.py +401 -0
- package/templates/vault/last30days/scripts/lib/xai_x.py +217 -0
- package/templates/vault/leiloeiro-avaliacao/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-avaliacao/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-edital/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-edital/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-ia/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-ia/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-juridico/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-juridico/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-mercado/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-mercado/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-risco/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-risco/scripts/requirements.txt +1 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/database.ts +24 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/db.ts +35 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/index.ts +2 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/migrations.ts +31 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/schema.sql +8 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/index.ts +44 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/routes/todos.ts +155 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/types/index.ts +35 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/App.css +384 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/App.tsx +81 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/api/todos.ts +57 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/ConfirmDialog.tsx +26 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/EmptyState.tsx +8 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/TodoForm.tsx +43 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/TodoItem.tsx +36 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/TodoList.tsx +27 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/hooks/useTodos.ts +81 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/index.css +48 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/main.tsx +10 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/vite-env.d.ts +1 -0
- package/templates/vault/loki-mode/scripts/export-to-vibe-kanban.sh +178 -0
- package/templates/vault/loki-mode/scripts/loki-wrapper.sh +281 -0
- package/templates/vault/loki-mode/scripts/take-screenshots.js +55 -0
- package/templates/vault/matematico-tao/scripts/complexity_analyzer.py +544 -0
- package/templates/vault/matematico-tao/scripts/dependency_graph.py +538 -0
- package/templates/vault/mcp-builder/scripts/connections.py +151 -0
- package/templates/vault/mcp-builder/scripts/evaluation.py +373 -0
- package/templates/vault/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/templates/vault/mcp-builder/scripts/requirements.txt +2 -0
- package/templates/vault/mobile-design/scripts/mobile_audit.py +670 -0
- package/templates/vault/notebooklm/scripts/__init__.py +81 -0
- package/templates/vault/notebooklm/scripts/ask_question.py +256 -0
- package/templates/vault/notebooklm/scripts/auth_manager.py +358 -0
- package/templates/vault/notebooklm/scripts/browser_session.py +255 -0
- package/templates/vault/notebooklm/scripts/browser_utils.py +107 -0
- package/templates/vault/notebooklm/scripts/cleanup_manager.py +302 -0
- package/templates/vault/notebooklm/scripts/config.py +44 -0
- package/templates/vault/notebooklm/scripts/notebook_manager.py +410 -0
- package/templates/vault/notebooklm/scripts/run.py +102 -0
- package/templates/vault/notebooklm/scripts/setup_environment.py +204 -0
- package/templates/vault/pdf/scripts/check_bounding_boxes.py +70 -0
- package/templates/vault/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/templates/vault/pdf/scripts/check_fillable_fields.py +12 -0
- package/templates/vault/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/templates/vault/pdf/scripts/create_validation_image.py +41 -0
- package/templates/vault/pdf/scripts/extract_form_field_info.py +152 -0
- package/templates/vault/pdf/scripts/fill_fillable_fields.py +114 -0
- package/templates/vault/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/templates/vault/pdf-official/scripts/check_bounding_boxes.py +70 -0
- package/templates/vault/pdf-official/scripts/check_bounding_boxes_test.py +226 -0
- package/templates/vault/pdf-official/scripts/check_fillable_fields.py +12 -0
- package/templates/vault/pdf-official/scripts/convert_pdf_to_images.py +35 -0
- package/templates/vault/pdf-official/scripts/create_validation_image.py +41 -0
- package/templates/vault/pdf-official/scripts/extract_form_field_info.py +152 -0
- package/templates/vault/pdf-official/scripts/fill_fillable_fields.py +114 -0
- package/templates/vault/pdf-official/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/templates/vault/planning-with-files/scripts/check-complete.sh +44 -0
- package/templates/vault/planning-with-files/scripts/init-session.sh +120 -0
- package/templates/vault/pptx/ooxml/scripts/pack.py +159 -0
- package/templates/vault/pptx/ooxml/scripts/unpack.py +29 -0
- package/templates/vault/pptx/ooxml/scripts/validate.py +69 -0
- package/templates/vault/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/vault/pptx/ooxml/scripts/validation/base.py +951 -0
- package/templates/vault/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/templates/vault/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/vault/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/vault/pptx/scripts/html2pptx.js +979 -0
- package/templates/vault/pptx/scripts/inventory.py +1020 -0
- package/templates/vault/pptx/scripts/rearrange.py +231 -0
- package/templates/vault/pptx/scripts/replace.py +385 -0
- package/templates/vault/pptx/scripts/thumbnail.py +450 -0
- package/templates/vault/pptx-official/ooxml/scripts/pack.py +159 -0
- package/templates/vault/pptx-official/ooxml/scripts/unpack.py +29 -0
- package/templates/vault/pptx-official/ooxml/scripts/validate.py +69 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/base.py +951 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/docx.py +274 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/vault/pptx-official/scripts/html2pptx.js +979 -0
- package/templates/vault/pptx-official/scripts/inventory.py +1020 -0
- package/templates/vault/pptx-official/scripts/rearrange.py +231 -0
- package/templates/vault/pptx-official/scripts/replace.py +385 -0
- package/templates/vault/pptx-official/scripts/thumbnail.py +450 -0
- package/templates/vault/product-manager-toolkit/scripts/customer_interview_analyzer.py +441 -0
- package/templates/vault/product-manager-toolkit/scripts/rice_prioritizer.py +296 -0
- package/templates/vault/prompt-engineering-patterns/scripts/optimize-prompt.py +279 -0
- package/templates/vault/scripts/.skill_cache.json +7538 -0
- package/templates/vault/scripts/skill_search.py +228 -0
- package/templates/vault/senior-architect/scripts/architecture_diagram_generator.py +114 -0
- package/templates/vault/senior-architect/scripts/dependency_analyzer.py +114 -0
- package/templates/vault/senior-architect/scripts/project_architect.py +114 -0
- package/templates/vault/shopify-development/scripts/requirements.txt +19 -0
- package/templates/vault/shopify-development/scripts/shopify_graphql.py +428 -0
- package/templates/vault/shopify-development/scripts/shopify_init.py +441 -0
- package/templates/vault/shopify-development/scripts/tests/test_shopify_init.py +379 -0
- package/templates/vault/skill-creator/scripts/init_skill.py +303 -0
- package/templates/vault/skill-creator/scripts/package_skill.py +110 -0
- package/templates/vault/skill-creator/scripts/quick_validate.py +95 -0
- package/templates/vault/skill-installer/scripts/detect_skills.py +318 -0
- package/templates/vault/skill-installer/scripts/install_skill.py +1708 -0
- package/templates/vault/skill-installer/scripts/package_skill.py +417 -0
- package/templates/vault/skill-installer/scripts/requirements.txt +1 -0
- package/templates/vault/skill-installer/scripts/validate_skill.py +430 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/__init__.py +13 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/code_quality.py +247 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/cross_skill.py +134 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/dependencies.py +121 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/documentation.py +189 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/governance_audit.py +153 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/performance.py +164 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/security.py +189 -0
- package/templates/vault/skill-sentinel/scripts/config.py +158 -0
- package/templates/vault/skill-sentinel/scripts/cost_optimizer.py +146 -0
- package/templates/vault/skill-sentinel/scripts/db.py +354 -0
- package/templates/vault/skill-sentinel/scripts/governance.py +58 -0
- package/templates/vault/skill-sentinel/scripts/recommender.py +228 -0
- package/templates/vault/skill-sentinel/scripts/report_generator.py +224 -0
- package/templates/vault/skill-sentinel/scripts/requirements.txt +1 -0
- package/templates/vault/skill-sentinel/scripts/run_audit.py +290 -0
- package/templates/vault/skill-sentinel/scripts/scanner.py +271 -0
- package/templates/vault/stability-ai/scripts/config.py +266 -0
- package/templates/vault/stability-ai/scripts/generate.py +687 -0
- package/templates/vault/stability-ai/scripts/requirements.txt +4 -0
- package/templates/vault/stability-ai/scripts/styles.py +174 -0
- package/templates/vault/telegram/assets/boilerplate/nodejs/src/bot-client.ts +86 -0
- package/templates/vault/telegram/assets/boilerplate/nodejs/src/handlers.ts +79 -0
- package/templates/vault/telegram/assets/boilerplate/nodejs/src/index.ts +32 -0
- package/templates/vault/telegram/scripts/send_message.py +143 -0
- package/templates/vault/telegram/scripts/setup_project.py +103 -0
- package/templates/vault/telegram/scripts/test_bot.py +144 -0
- package/templates/vault/typescript-expert/scripts/ts_diagnostic.py +203 -0
- package/templates/vault/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc +0 -0
- package/templates/vault/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
- package/templates/vault/ui-ux-pro-max/scripts/core.py +257 -0
- package/templates/vault/ui-ux-pro-max/scripts/design_system.py +487 -0
- package/templates/vault/ui-ux-pro-max/scripts/search.py +76 -0
- package/templates/vault/videodb/scripts/ws_listener.py +204 -0
- package/templates/vault/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/templates/vault/web-artifacts-builder/scripts/init-artifact.sh +322 -0
- package/templates/vault/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/templates/vault/webapp-testing/scripts/with_server.py +106 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/index.ts +125 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/template-manager.ts +67 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/types.ts +216 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts +173 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/whatsapp-client.ts +193 -0
- package/templates/vault/whatsapp-cloud-api/scripts/send_test_message.py +137 -0
- package/templates/vault/whatsapp-cloud-api/scripts/setup_project.py +118 -0
- package/templates/vault/whatsapp-cloud-api/scripts/validate_config.py +190 -0
- package/templates/vault/youtube-summarizer/scripts/extract-transcript.py +65 -0
- 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()
|