javi-forge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitignore.template +105 -0
- package/.releaserc +44 -0
- package/README.md +45 -0
- package/ai-config/.skillignore +15 -0
- package/ai-config/AUTO_INVOKE.md +300 -0
- package/ai-config/agents/_TEMPLATE.md +93 -0
- package/ai-config/agents/business/api-designer.md +1657 -0
- package/ai-config/agents/business/business-analyst.md +1331 -0
- package/ai-config/agents/business/product-strategist.md +206 -0
- package/ai-config/agents/business/project-manager.md +178 -0
- package/ai-config/agents/business/requirements-analyst.md +1277 -0
- package/ai-config/agents/business/technical-writer.md +1679 -0
- package/ai-config/agents/creative/ux-designer.md +205 -0
- package/ai-config/agents/data-ai/ai-engineer.md +487 -0
- package/ai-config/agents/data-ai/analytics-engineer.md +953 -0
- package/ai-config/agents/data-ai/data-engineer.md +173 -0
- package/ai-config/agents/data-ai/data-scientist.md +672 -0
- package/ai-config/agents/data-ai/mlops-engineer.md +814 -0
- package/ai-config/agents/data-ai/prompt-engineer.md +772 -0
- package/ai-config/agents/development/angular-expert.md +620 -0
- package/ai-config/agents/development/backend-architect.md +795 -0
- package/ai-config/agents/development/database-specialist.md +212 -0
- package/ai-config/agents/development/frontend-specialist.md +686 -0
- package/ai-config/agents/development/fullstack-engineer.md +668 -0
- package/ai-config/agents/development/golang-pro.md +338 -0
- package/ai-config/agents/development/java-enterprise.md +400 -0
- package/ai-config/agents/development/javascript-pro.md +422 -0
- package/ai-config/agents/development/nextjs-pro.md +474 -0
- package/ai-config/agents/development/python-pro.md +570 -0
- package/ai-config/agents/development/react-pro.md +487 -0
- package/ai-config/agents/development/rust-pro.md +246 -0
- package/ai-config/agents/development/spring-boot-4-expert.md +326 -0
- package/ai-config/agents/development/typescript-pro.md +336 -0
- package/ai-config/agents/development/vue-specialist.md +605 -0
- package/ai-config/agents/infrastructure/cloud-architect.md +472 -0
- package/ai-config/agents/infrastructure/deployment-manager.md +358 -0
- package/ai-config/agents/infrastructure/devops-engineer.md +455 -0
- package/ai-config/agents/infrastructure/incident-responder.md +519 -0
- package/ai-config/agents/infrastructure/kubernetes-expert.md +705 -0
- package/ai-config/agents/infrastructure/monitoring-specialist.md +674 -0
- package/ai-config/agents/infrastructure/performance-engineer.md +658 -0
- package/ai-config/agents/orchestrator.md +241 -0
- package/ai-config/agents/quality/accessibility-auditor.md +1204 -0
- package/ai-config/agents/quality/code-reviewer-compact.md +123 -0
- package/ai-config/agents/quality/code-reviewer.md +363 -0
- package/ai-config/agents/quality/dependency-manager.md +743 -0
- package/ai-config/agents/quality/e2e-test-specialist.md +1005 -0
- package/ai-config/agents/quality/performance-tester.md +1086 -0
- package/ai-config/agents/quality/security-auditor.md +133 -0
- package/ai-config/agents/quality/test-engineer.md +453 -0
- package/ai-config/agents/specialists/api-designer.md +87 -0
- package/ai-config/agents/specialists/backend-architect.md +73 -0
- package/ai-config/agents/specialists/code-reviewer.md +77 -0
- package/ai-config/agents/specialists/db-optimizer.md +75 -0
- package/ai-config/agents/specialists/devops-engineer.md +83 -0
- package/ai-config/agents/specialists/documentation-writer.md +78 -0
- package/ai-config/agents/specialists/frontend-developer.md +75 -0
- package/ai-config/agents/specialists/performance-analyst.md +82 -0
- package/ai-config/agents/specialists/refactor-specialist.md +74 -0
- package/ai-config/agents/specialists/security-auditor.md +74 -0
- package/ai-config/agents/specialists/test-engineer.md +81 -0
- package/ai-config/agents/specialists/ux-consultant.md +76 -0
- package/ai-config/agents/specialized/agent-generator.md +1190 -0
- package/ai-config/agents/specialized/blockchain-developer.md +149 -0
- package/ai-config/agents/specialized/code-migrator.md +892 -0
- package/ai-config/agents/specialized/context-manager.md +978 -0
- package/ai-config/agents/specialized/documentation-writer.md +1078 -0
- package/ai-config/agents/specialized/ecommerce-expert.md +1756 -0
- package/ai-config/agents/specialized/embedded-engineer.md +1714 -0
- package/ai-config/agents/specialized/error-detective.md +1034 -0
- package/ai-config/agents/specialized/fintech-specialist.md +1659 -0
- package/ai-config/agents/specialized/freelance-project-planner-v2.md +1988 -0
- package/ai-config/agents/specialized/freelance-project-planner-v3.md +2136 -0
- package/ai-config/agents/specialized/freelance-project-planner-v4.md +4503 -0
- package/ai-config/agents/specialized/freelance-project-planner.md +722 -0
- package/ai-config/agents/specialized/game-developer.md +1963 -0
- package/ai-config/agents/specialized/healthcare-dev.md +1620 -0
- package/ai-config/agents/specialized/mobile-developer.md +188 -0
- package/ai-config/agents/specialized/parallel-plan-executor.md +506 -0
- package/ai-config/agents/specialized/plan-executor.md +485 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/00-INDEX.md +485 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/01-CORE.md +3493 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/02-SELF-CORRECTION.md +778 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/03-PROGRESSIVE-SETUP.md +918 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/04-DEPLOYMENT.md +1537 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/05-TESTING.md +2633 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/06-OPERATIONS.md +5610 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/INSTALL.md +335 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/QUICK-REFERENCE.txt +215 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/README.md +260 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/START-HERE.md +379 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/WORKFLOW-DIAGRAM.md +355 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/solo-dev-planner.md +279 -0
- package/ai-config/agents/specialized/template-writer.md +347 -0
- package/ai-config/agents/specialized/test-runner.md +99 -0
- package/ai-config/agents/specialized/vibekanban-smart-worker.md +244 -0
- package/ai-config/agents/specialized/wave-executor.md +138 -0
- package/ai-config/agents/specialized/workflow-optimizer.md +1114 -0
- package/ai-config/commands/git/changelog.md +32 -0
- package/ai-config/commands/git/ci-local.md +70 -0
- package/ai-config/commands/git/commit.md +35 -0
- package/ai-config/commands/git/fix-issue.md +23 -0
- package/ai-config/commands/git/pr-create.md +42 -0
- package/ai-config/commands/git/pr-review.md +50 -0
- package/ai-config/commands/git/worktree.md +39 -0
- package/ai-config/commands/refactoring/cleanup.md +24 -0
- package/ai-config/commands/refactoring/dead-code.md +40 -0
- package/ai-config/commands/refactoring/extract.md +31 -0
- package/ai-config/commands/testing/e2e.md +30 -0
- package/ai-config/commands/testing/tdd.md +36 -0
- package/ai-config/commands/testing/test-coverage.md +30 -0
- package/ai-config/commands/testing/test-fix.md +24 -0
- package/ai-config/commands/workflow/generate-agents-md.md +85 -0
- package/ai-config/commands/workflow/planning.md +47 -0
- package/ai-config/commands/workflows/compound.md +89 -0
- package/ai-config/commands/workflows/plan.md +77 -0
- package/ai-config/commands/workflows/review.md +78 -0
- package/ai-config/commands/workflows/work.md +75 -0
- package/ai-config/config.yaml +18 -0
- package/ai-config/hooks/_TEMPLATE.md +96 -0
- package/ai-config/hooks/block-dangerous-commands.md +75 -0
- package/ai-config/hooks/commit-guard.md +90 -0
- package/ai-config/hooks/context-loader.md +73 -0
- package/ai-config/hooks/improve-prompt.md +91 -0
- package/ai-config/hooks/learning-log.md +72 -0
- package/ai-config/hooks/model-router.md +86 -0
- package/ai-config/hooks/secret-scanner.md +64 -0
- package/ai-config/hooks/skill-validator.md +102 -0
- package/ai-config/hooks/task-artifact.md +114 -0
- package/ai-config/hooks/validate-workflow.md +100 -0
- package/ai-config/prompts/base.md +71 -0
- package/ai-config/prompts/modes/debug.md +34 -0
- package/ai-config/prompts/modes/deploy.md +40 -0
- package/ai-config/prompts/modes/research.md +32 -0
- package/ai-config/prompts/modes/review.md +33 -0
- package/ai-config/prompts/review-policy.md +79 -0
- package/ai-config/skills/_TEMPLATE.md +157 -0
- package/ai-config/skills/backend/api-gateway/SKILL.md +254 -0
- package/ai-config/skills/backend/bff-concepts/SKILL.md +239 -0
- package/ai-config/skills/backend/bff-spring/SKILL.md +364 -0
- package/ai-config/skills/backend/chi-router/SKILL.md +396 -0
- package/ai-config/skills/backend/error-handling/SKILL.md +255 -0
- package/ai-config/skills/backend/exceptions-spring/SKILL.md +323 -0
- package/ai-config/skills/backend/fastapi/SKILL.md +302 -0
- package/ai-config/skills/backend/gateway-spring/SKILL.md +390 -0
- package/ai-config/skills/backend/go-backend/SKILL.md +457 -0
- package/ai-config/skills/backend/gradle-multimodule/SKILL.md +274 -0
- package/ai-config/skills/backend/graphql-concepts/SKILL.md +352 -0
- package/ai-config/skills/backend/graphql-spring/SKILL.md +398 -0
- package/ai-config/skills/backend/grpc-concepts/SKILL.md +283 -0
- package/ai-config/skills/backend/grpc-spring/SKILL.md +445 -0
- package/ai-config/skills/backend/jwt-auth/SKILL.md +412 -0
- package/ai-config/skills/backend/notifications-concepts/SKILL.md +259 -0
- package/ai-config/skills/backend/recommendations-concepts/SKILL.md +261 -0
- package/ai-config/skills/backend/search-concepts/SKILL.md +263 -0
- package/ai-config/skills/backend/search-spring/SKILL.md +375 -0
- package/ai-config/skills/backend/spring-boot-4/SKILL.md +172 -0
- package/ai-config/skills/backend/websockets/SKILL.md +532 -0
- package/ai-config/skills/data-ai/ai-ml/SKILL.md +423 -0
- package/ai-config/skills/data-ai/analytics-concepts/SKILL.md +195 -0
- package/ai-config/skills/data-ai/analytics-spring/SKILL.md +340 -0
- package/ai-config/skills/data-ai/duckdb-analytics/SKILL.md +440 -0
- package/ai-config/skills/data-ai/langchain/SKILL.md +238 -0
- package/ai-config/skills/data-ai/mlflow/SKILL.md +302 -0
- package/ai-config/skills/data-ai/onnx-inference/SKILL.md +290 -0
- package/ai-config/skills/data-ai/powerbi/SKILL.md +352 -0
- package/ai-config/skills/data-ai/pytorch/SKILL.md +274 -0
- package/ai-config/skills/data-ai/scikit-learn/SKILL.md +321 -0
- package/ai-config/skills/data-ai/vector-db/SKILL.md +301 -0
- package/ai-config/skills/database/graph-databases/SKILL.md +218 -0
- package/ai-config/skills/database/graph-spring/SKILL.md +361 -0
- package/ai-config/skills/database/pgx-postgres/SKILL.md +512 -0
- package/ai-config/skills/database/redis-cache/SKILL.md +343 -0
- package/ai-config/skills/database/sqlite-embedded/SKILL.md +388 -0
- package/ai-config/skills/database/timescaledb/SKILL.md +320 -0
- package/ai-config/skills/docs/api-documentation/SKILL.md +293 -0
- package/ai-config/skills/docs/docs-spring/SKILL.md +377 -0
- package/ai-config/skills/docs/mustache-templates/SKILL.md +190 -0
- package/ai-config/skills/docs/technical-docs/SKILL.md +447 -0
- package/ai-config/skills/frontend/astro-ssr/SKILL.md +441 -0
- package/ai-config/skills/frontend/frontend-design/SKILL.md +54 -0
- package/ai-config/skills/frontend/frontend-web/SKILL.md +368 -0
- package/ai-config/skills/frontend/mantine-ui/SKILL.md +396 -0
- package/ai-config/skills/frontend/tanstack-query/SKILL.md +439 -0
- package/ai-config/skills/frontend/zod-validation/SKILL.md +417 -0
- package/ai-config/skills/frontend/zustand-state/SKILL.md +350 -0
- package/ai-config/skills/infrastructure/chaos-engineering/SKILL.md +244 -0
- package/ai-config/skills/infrastructure/chaos-spring/SKILL.md +378 -0
- package/ai-config/skills/infrastructure/devops-infra/SKILL.md +435 -0
- package/ai-config/skills/infrastructure/docker-containers/SKILL.md +420 -0
- package/ai-config/skills/infrastructure/kubernetes/SKILL.md +456 -0
- package/ai-config/skills/infrastructure/opentelemetry/SKILL.md +546 -0
- package/ai-config/skills/infrastructure/traefik-proxy/SKILL.md +474 -0
- package/ai-config/skills/infrastructure/woodpecker-ci/SKILL.md +315 -0
- package/ai-config/skills/mobile/ionic-capacitor/SKILL.md +504 -0
- package/ai-config/skills/mobile/mobile-ionic/SKILL.md +448 -0
- package/ai-config/skills/prompt-improver/SKILL.md +125 -0
- package/ai-config/skills/quality/ghagga-review/SKILL.md +216 -0
- package/ai-config/skills/references/hooks-patterns/SKILL.md +238 -0
- package/ai-config/skills/references/mcp-servers/SKILL.md +275 -0
- package/ai-config/skills/references/plugins-reference/SKILL.md +110 -0
- package/ai-config/skills/references/skills-reference/SKILL.md +420 -0
- package/ai-config/skills/references/subagent-templates/SKILL.md +193 -0
- package/ai-config/skills/systems-iot/modbus-protocol/SKILL.md +410 -0
- package/ai-config/skills/systems-iot/mqtt-rumqttc/SKILL.md +408 -0
- package/ai-config/skills/systems-iot/rust-systems/SKILL.md +386 -0
- package/ai-config/skills/systems-iot/tokio-async/SKILL.md +324 -0
- package/ai-config/skills/testing/playwright-e2e/SKILL.md +289 -0
- package/ai-config/skills/testing/testcontainers/SKILL.md +299 -0
- package/ai-config/skills/testing/vitest-testing/SKILL.md +381 -0
- package/ai-config/skills/workflow/ci-local-guide/SKILL.md +118 -0
- package/ai-config/skills/workflow/claude-automation-recommender/SKILL.md +299 -0
- package/ai-config/skills/workflow/claude-md-improver/SKILL.md +158 -0
- package/ai-config/skills/workflow/finishing-a-development-branch/SKILL.md +117 -0
- package/ai-config/skills/workflow/git-github/SKILL.md +334 -0
- package/ai-config/skills/workflow/git-github/references/examples.md +160 -0
- package/ai-config/skills/workflow/git-workflow/SKILL.md +214 -0
- package/ai-config/skills/workflow/ide-plugins/SKILL.md +277 -0
- package/ai-config/skills/workflow/ide-plugins-intellij/SKILL.md +401 -0
- package/ai-config/skills/workflow/obsidian-brain-workflow/SKILL.md +199 -0
- package/ai-config/skills/workflow/using-git-worktrees/SKILL.md +100 -0
- package/ai-config/skills/workflow/verification-before-completion/SKILL.md +73 -0
- package/ai-config/skills/workflow/wave-workflow/SKILL.md +178 -0
- package/ci-local/README.md +170 -0
- package/ci-local/ci-local.sh +297 -0
- package/ci-local/hooks/commit-msg +74 -0
- package/ci-local/hooks/pre-commit +162 -0
- package/ci-local/hooks/pre-push +41 -0
- package/ci-local/install.sh +49 -0
- package/ci-local/semgrep.yml +214 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +55 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/analyze.test.d.ts +2 -0
- package/dist/commands/analyze.test.d.ts.map +1 -0
- package/dist/commands/analyze.test.js +145 -0
- package/dist/commands/analyze.test.js.map +1 -0
- package/dist/commands/doctor.d.ts +7 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +158 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +2 -0
- package/dist/commands/doctor.test.d.ts.map +1 -0
- package/dist/commands/doctor.test.js +200 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +283 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +2 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +271 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +201 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +57 -0
- package/dist/constants.js.map +1 -0
- package/dist/e2e/aggressive.e2e.test.d.ts +2 -0
- package/dist/e2e/aggressive.e2e.test.d.ts.map +1 -0
- package/dist/e2e/aggressive.e2e.test.js +350 -0
- package/dist/e2e/aggressive.e2e.test.js.map +1 -0
- package/dist/e2e/commands.e2e.test.d.ts +2 -0
- package/dist/e2e/commands.e2e.test.d.ts.map +1 -0
- package/dist/e2e/commands.e2e.test.js +213 -0
- package/dist/e2e/commands.e2e.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/common.d.ts +17 -0
- package/dist/lib/common.d.ts.map +1 -0
- package/dist/lib/common.js +111 -0
- package/dist/lib/common.js.map +1 -0
- package/dist/lib/common.test.d.ts +2 -0
- package/dist/lib/common.test.d.ts.map +1 -0
- package/dist/lib/common.test.js +316 -0
- package/dist/lib/common.test.js.map +1 -0
- package/dist/lib/frontmatter.d.ts +18 -0
- package/dist/lib/frontmatter.d.ts.map +1 -0
- package/dist/lib/frontmatter.js +61 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/frontmatter.test.d.ts +2 -0
- package/dist/lib/frontmatter.test.d.ts.map +1 -0
- package/dist/lib/frontmatter.test.js +257 -0
- package/dist/lib/frontmatter.test.js.map +1 -0
- package/dist/lib/template.d.ts +24 -0
- package/dist/lib/template.d.ts.map +1 -0
- package/dist/lib/template.js +78 -0
- package/dist/lib/template.js.map +1 -0
- package/dist/lib/template.test.d.ts +2 -0
- package/dist/lib/template.test.d.ts.map +1 -0
- package/dist/lib/template.test.js +201 -0
- package/dist/lib/template.test.js.map +1 -0
- package/dist/types/index.d.ts +48 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/AnalyzeUI.d.ts +7 -0
- package/dist/ui/AnalyzeUI.d.ts.map +1 -0
- package/dist/ui/AnalyzeUI.js +100 -0
- package/dist/ui/AnalyzeUI.js.map +1 -0
- package/dist/ui/App.d.ts +13 -0
- package/dist/ui/App.d.ts.map +1 -0
- package/dist/ui/App.js +100 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/CIContext.d.ts +9 -0
- package/dist/ui/CIContext.d.ts.map +1 -0
- package/dist/ui/CIContext.js +9 -0
- package/dist/ui/CIContext.js.map +1 -0
- package/dist/ui/CISelector.d.ts +8 -0
- package/dist/ui/CISelector.d.ts.map +1 -0
- package/dist/ui/CISelector.js +45 -0
- package/dist/ui/CISelector.js.map +1 -0
- package/dist/ui/Doctor.d.ts +3 -0
- package/dist/ui/Doctor.d.ts.map +1 -0
- package/dist/ui/Doctor.js +89 -0
- package/dist/ui/Doctor.js.map +1 -0
- package/dist/ui/Header.d.ts +8 -0
- package/dist/ui/Header.d.ts.map +1 -0
- package/dist/ui/Header.js +30 -0
- package/dist/ui/Header.js.map +1 -0
- package/dist/ui/MemorySelector.d.ts +8 -0
- package/dist/ui/MemorySelector.d.ts.map +1 -0
- package/dist/ui/MemorySelector.js +46 -0
- package/dist/ui/MemorySelector.js.map +1 -0
- package/dist/ui/NameInput.d.ts +8 -0
- package/dist/ui/NameInput.d.ts.map +1 -0
- package/dist/ui/NameInput.js +69 -0
- package/dist/ui/NameInput.js.map +1 -0
- package/dist/ui/OptionSelector.d.ts +12 -0
- package/dist/ui/OptionSelector.d.ts.map +1 -0
- package/dist/ui/OptionSelector.js +69 -0
- package/dist/ui/OptionSelector.js.map +1 -0
- package/dist/ui/Progress.d.ts +11 -0
- package/dist/ui/Progress.d.ts.map +1 -0
- package/dist/ui/Progress.js +58 -0
- package/dist/ui/Progress.js.map +1 -0
- package/dist/ui/StackSelector.d.ts +9 -0
- package/dist/ui/StackSelector.d.ts.map +1 -0
- package/dist/ui/StackSelector.js +65 -0
- package/dist/ui/StackSelector.js.map +1 -0
- package/dist/ui/Summary.d.ts +12 -0
- package/dist/ui/Summary.d.ts.map +1 -0
- package/dist/ui/Summary.js +114 -0
- package/dist/ui/Summary.js.map +1 -0
- package/dist/ui/SyncUI.d.ts +10 -0
- package/dist/ui/SyncUI.d.ts.map +1 -0
- package/dist/ui/SyncUI.js +64 -0
- package/dist/ui/SyncUI.js.map +1 -0
- package/dist/ui/Welcome.d.ts +7 -0
- package/dist/ui/Welcome.d.ts.map +1 -0
- package/dist/ui/Welcome.js +45 -0
- package/dist/ui/Welcome.js.map +1 -0
- package/dist/ui/theme.d.ts +10 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +9 -0
- package/dist/ui/theme.js.map +1 -0
- package/modules/engram/.gitignore-snippet.txt +6 -0
- package/modules/engram/.mcp-config-snippet.json +11 -0
- package/modules/engram/README.md +146 -0
- package/modules/engram/install-engram.sh +216 -0
- package/modules/ghagga/.env.example +43 -0
- package/modules/ghagga/README.md +153 -0
- package/modules/ghagga/docker-compose.yml +80 -0
- package/modules/ghagga/setup-ghagga.sh +139 -0
- package/modules/memory-simple/.project/NOTES.md +22 -0
- package/modules/memory-simple/README.md +23 -0
- package/modules/obsidian-brain/.obsidian/app.json +23 -0
- package/modules/obsidian-brain/.obsidian/appearance.json +5 -0
- package/modules/obsidian-brain/.obsidian/bookmarks.json +34 -0
- package/modules/obsidian-brain/.obsidian/community-plugins.json +1 -0
- package/modules/obsidian-brain/.obsidian/core-plugins-migration.json +21 -0
- package/modules/obsidian-brain/.obsidian/core-plugins.json +18 -0
- package/modules/obsidian-brain/.obsidian/daily-notes.json +5 -0
- package/modules/obsidian-brain/.obsidian/graph.json +37 -0
- package/modules/obsidian-brain/.obsidian/hotkeys.json +14 -0
- package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +25 -0
- package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +29 -0
- package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +18 -0
- package/modules/obsidian-brain/.obsidian/snippets/project-memory.css +71 -0
- package/modules/obsidian-brain/.obsidian-gitignore-snippet.txt +8 -0
- package/modules/obsidian-brain/.project/Attachments/.gitkeep +0 -0
- package/modules/obsidian-brain/.project/Memory/BLOCKERS.md +78 -0
- package/modules/obsidian-brain/.project/Memory/CONTEXT.md +102 -0
- package/modules/obsidian-brain/.project/Memory/DASHBOARD.md +73 -0
- package/modules/obsidian-brain/.project/Memory/DECISIONS.md +87 -0
- package/modules/obsidian-brain/.project/Memory/KANBAN.md +15 -0
- package/modules/obsidian-brain/.project/Memory/README.md +61 -0
- package/modules/obsidian-brain/.project/Memory/WAVES.md +78 -0
- package/modules/obsidian-brain/.project/Sessions/TEMPLATE.md +99 -0
- package/modules/obsidian-brain/.project/Templates/ADR.md +33 -0
- package/modules/obsidian-brain/.project/Templates/Blocker.md +21 -0
- package/modules/obsidian-brain/.project/Templates/Session.md +88 -0
- package/modules/obsidian-brain/README.md +268 -0
- package/modules/obsidian-brain/new-wave.sh +182 -0
- package/package.json +51 -0
- package/schemas/agent.schema.json +34 -0
- package/schemas/ai-config.schema.json +28 -0
- package/schemas/skill.schema.json +44 -0
- package/src/commands/analyze.test.ts +145 -0
- package/src/commands/analyze.ts +69 -0
- package/src/commands/doctor.test.ts +208 -0
- package/src/commands/doctor.ts +163 -0
- package/src/commands/init.test.ts +298 -0
- package/src/commands/init.ts +285 -0
- package/src/constants.ts +69 -0
- package/src/e2e/aggressive.e2e.test.ts +557 -0
- package/src/e2e/commands.e2e.test.ts +298 -0
- package/src/index.tsx +106 -0
- package/src/lib/common.test.ts +318 -0
- package/src/lib/common.ts +127 -0
- package/src/lib/frontmatter.test.ts +291 -0
- package/src/lib/frontmatter.ts +77 -0
- package/src/lib/template.test.ts +226 -0
- package/src/lib/template.ts +99 -0
- package/src/types/index.ts +53 -0
- package/src/ui/AnalyzeUI.tsx +133 -0
- package/src/ui/App.tsx +175 -0
- package/src/ui/CIContext.tsx +25 -0
- package/src/ui/CISelector.tsx +72 -0
- package/src/ui/Doctor.tsx +122 -0
- package/src/ui/Header.tsx +48 -0
- package/src/ui/MemorySelector.tsx +73 -0
- package/src/ui/NameInput.tsx +82 -0
- package/src/ui/OptionSelector.tsx +100 -0
- package/src/ui/Progress.tsx +88 -0
- package/src/ui/StackSelector.tsx +101 -0
- package/src/ui/Summary.tsx +134 -0
- package/src/ui/Welcome.tsx +54 -0
- package/src/ui/theme.ts +10 -0
- package/stryker.config.json +19 -0
- package/tasks/_TEMPLATE/files-edited.md +3 -0
- package/tasks/_TEMPLATE/plan.md +3 -0
- package/tasks/_TEMPLATE/research.md +3 -0
- package/tasks/_TEMPLATE/verification.md +5 -0
- package/templates/common/dependabot/cargo.yml +11 -0
- package/templates/common/dependabot/github-actions.yml +16 -0
- package/templates/common/dependabot/gomod.yml +15 -0
- package/templates/common/dependabot/gradle.yml +15 -0
- package/templates/common/dependabot/header.yml +3 -0
- package/templates/common/dependabot/maven.yml +15 -0
- package/templates/common/dependabot/npm.yml +20 -0
- package/templates/common/dependabot/pip.yml +11 -0
- package/templates/dependabot.yml +162 -0
- package/templates/github/ci-go.yml +41 -0
- package/templates/github/ci-java.yml +45 -0
- package/templates/github/ci-monorepo.yml +150 -0
- package/templates/github/ci-node.yml +42 -0
- package/templates/github/ci-python.yml +42 -0
- package/templates/github/ci-rust.yml +42 -0
- package/templates/github/dependabot-automerge.yml +40 -0
- package/templates/gitlab/gitlab-ci-go.yml +88 -0
- package/templates/gitlab/gitlab-ci-java.yml +79 -0
- package/templates/gitlab/gitlab-ci-monorepo.yml +126 -0
- package/templates/gitlab/gitlab-ci-node.yml +63 -0
- package/templates/gitlab/gitlab-ci-python.yml +147 -0
- package/templates/gitlab/gitlab-ci-rust.yml +67 -0
- package/templates/global/claude-settings.json +98 -0
- package/templates/global/codex-config.toml +8 -0
- package/templates/global/copilot-instructions/base-rules.instructions.md +13 -0
- package/templates/global/copilot-instructions/sdd-orchestrator.instructions.md +37 -0
- package/templates/global/gemini-commands/cleanup.toml +20 -0
- package/templates/global/gemini-commands/commit.toml +15 -0
- package/templates/global/gemini-commands/dead-code.toml +22 -0
- package/templates/global/gemini-commands/plan.toml +30 -0
- package/templates/global/gemini-commands/review.toml +17 -0
- package/templates/global/gemini-commands/sdd-apply.toml +22 -0
- package/templates/global/gemini-commands/sdd-ff.toml +14 -0
- package/templates/global/gemini-commands/sdd-new.toml +21 -0
- package/templates/global/gemini-commands/sdd-verify.toml +21 -0
- package/templates/global/gemini-commands/tdd.toml +26 -0
- package/templates/global/gemini-settings.json +8 -0
- package/templates/global/opencode-config.json +44 -0
- package/templates/global/sdd-instructions.md +47 -0
- package/templates/global/sdd-orchestrator-claude.md +46 -0
- package/templates/global/sdd-orchestrator-copilot.md +34 -0
- package/templates/renovate.json +69 -0
- package/templates/woodpecker/monorepo/backend.yml +34 -0
- package/templates/woodpecker/monorepo/frontend.yml +34 -0
- package/templates/woodpecker/monorepo/summary.yml +25 -0
- package/templates/woodpecker/woodpecker-go.yml +51 -0
- package/templates/woodpecker/woodpecker-java.yml +67 -0
- package/templates/woodpecker/woodpecker-node.yml +47 -0
- package/templates/woodpecker/woodpecker-python.yml +108 -0
- package/templates/woodpecker/woodpecker-rust.yml +57 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +16 -0
- package/workflows/reusable-build-go.yml +111 -0
- package/workflows/reusable-build-java.yml +120 -0
- package/workflows/reusable-build-node.yml +145 -0
- package/workflows/reusable-build-python.yml +159 -0
- package/workflows/reusable-build-rust.yml +135 -0
- package/workflows/reusable-docker.yml +120 -0
- package/workflows/reusable-ghagga-review.yml +165 -0
- package/workflows/reusable-release.yml +91 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { parseFrontmatter, validateFrontmatter } from './frontmatter.js'
|
|
3
|
+
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
// parseFrontmatter
|
|
6
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
describe('parseFrontmatter', () => {
|
|
8
|
+
it('parses valid frontmatter with content', () => {
|
|
9
|
+
const raw = `---
|
|
10
|
+
name: my-skill
|
|
11
|
+
description: A test skill
|
|
12
|
+
---
|
|
13
|
+
Some body content here.`
|
|
14
|
+
const result = parseFrontmatter(raw)
|
|
15
|
+
expect(result).not.toBeNull()
|
|
16
|
+
expect(result!.data).toEqual({ name: 'my-skill', description: 'A test skill' })
|
|
17
|
+
expect(result!.content).toBe('Some body content here.')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns null when no closing ---', () => {
|
|
21
|
+
const raw = `---
|
|
22
|
+
name: broken
|
|
23
|
+
description: missing close`
|
|
24
|
+
expect(parseFrontmatter(raw)).toBeNull()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns null for non-frontmatter text', () => {
|
|
28
|
+
const raw = 'Just some regular markdown text.'
|
|
29
|
+
expect(parseFrontmatter(raw)).toBeNull()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns null for empty YAML block', () => {
|
|
33
|
+
const raw = `---
|
|
34
|
+
---
|
|
35
|
+
Body content`
|
|
36
|
+
// An empty YAML block parses to null, which triggers the null check
|
|
37
|
+
expect(parseFrontmatter(raw)).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('returns null for invalid YAML', () => {
|
|
41
|
+
const raw = `---
|
|
42
|
+
: : : invalid: [yaml
|
|
43
|
+
---
|
|
44
|
+
Body`
|
|
45
|
+
expect(parseFrontmatter(raw)).toBeNull()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns null when YAML parses to a non-object (string)', () => {
|
|
49
|
+
const raw = `---
|
|
50
|
+
just a string
|
|
51
|
+
---
|
|
52
|
+
Body`
|
|
53
|
+
expect(parseFrontmatter(raw)).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('returns null when YAML parses to null', () => {
|
|
57
|
+
const raw = `---
|
|
58
|
+
null
|
|
59
|
+
---
|
|
60
|
+
Body`
|
|
61
|
+
expect(parseFrontmatter(raw)).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('handles frontmatter with empty content body', () => {
|
|
65
|
+
const raw = `---
|
|
66
|
+
name: test
|
|
67
|
+
---`
|
|
68
|
+
const result = parseFrontmatter(raw)
|
|
69
|
+
expect(result).not.toBeNull()
|
|
70
|
+
expect(result!.data).toEqual({ name: 'test' })
|
|
71
|
+
expect(result!.content).toBe('')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('only uses first pair of --- delimiters', () => {
|
|
75
|
+
const raw = `---
|
|
76
|
+
name: first
|
|
77
|
+
---
|
|
78
|
+
Some content
|
|
79
|
+
---
|
|
80
|
+
name: second
|
|
81
|
+
---`
|
|
82
|
+
const result = parseFrontmatter(raw)
|
|
83
|
+
expect(result).not.toBeNull()
|
|
84
|
+
expect(result!.data).toEqual({ name: 'first' })
|
|
85
|
+
expect(result!.content).toContain('name: second')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('handles complex nested YAML', () => {
|
|
89
|
+
const raw = `---
|
|
90
|
+
name: complex-skill
|
|
91
|
+
metadata:
|
|
92
|
+
version: 1.2.3
|
|
93
|
+
tags:
|
|
94
|
+
- typescript
|
|
95
|
+
- react
|
|
96
|
+
config:
|
|
97
|
+
nested: true
|
|
98
|
+
count: 42
|
|
99
|
+
---
|
|
100
|
+
Body content`
|
|
101
|
+
const result = parseFrontmatter(raw)
|
|
102
|
+
expect(result).not.toBeNull()
|
|
103
|
+
expect(result!.data['name']).toBe('complex-skill')
|
|
104
|
+
expect(result!.data['metadata']).toEqual({
|
|
105
|
+
version: '1.2.3',
|
|
106
|
+
tags: ['typescript', 'react'],
|
|
107
|
+
config: { nested: true, count: 42 },
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('handles leading whitespace before frontmatter', () => {
|
|
112
|
+
const raw = `
|
|
113
|
+
---
|
|
114
|
+
name: trimmed
|
|
115
|
+
---
|
|
116
|
+
Body`
|
|
117
|
+
const result = parseFrontmatter(raw)
|
|
118
|
+
expect(result).not.toBeNull()
|
|
119
|
+
expect(result!.data['name']).toBe('trimmed')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('verifies slice indices — content excludes closing delimiter', () => {
|
|
123
|
+
const raw = `---
|
|
124
|
+
key: value
|
|
125
|
+
---
|
|
126
|
+
Exact content`
|
|
127
|
+
const result = parseFrontmatter(raw)
|
|
128
|
+
expect(result).not.toBeNull()
|
|
129
|
+
// Verify yamlBlock is trimmed correctly (no leading/trailing whitespace artifacts)
|
|
130
|
+
expect(result!.data).toEqual({ key: 'value' })
|
|
131
|
+
// Content should be exactly what follows after the closing ---
|
|
132
|
+
expect(result!.content).toBe('Exact content')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('trims whitespace from yaml block', () => {
|
|
136
|
+
const raw = `---
|
|
137
|
+
spaced: true
|
|
138
|
+
---
|
|
139
|
+
Body`
|
|
140
|
+
const result = parseFrontmatter(raw)
|
|
141
|
+
expect(result).not.toBeNull()
|
|
142
|
+
expect(result!.data).toEqual({ spaced: true })
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('rejects input starting with text then ---', () => {
|
|
146
|
+
// This should NOT match — the --- must be at the very start (after trimStart)
|
|
147
|
+
const raw = `text before
|
|
148
|
+
---
|
|
149
|
+
name: test
|
|
150
|
+
---`
|
|
151
|
+
expect(parseFrontmatter(raw)).toBeNull()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
156
|
+
// validateFrontmatter
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
158
|
+
describe('validateFrontmatter', () => {
|
|
159
|
+
it('returns no errors for valid agent frontmatter', () => {
|
|
160
|
+
const fm = { name: 'my-agent', description: 'A valid agent description' }
|
|
161
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
162
|
+
expect(errors).toEqual([])
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('returns no errors for valid skill with Trigger:', () => {
|
|
166
|
+
const fm = { name: 'my-skill', description: 'A valid skill. Trigger: when testing' }
|
|
167
|
+
const errors = validateFrontmatter(fm, 'skill')
|
|
168
|
+
expect(errors).toEqual([])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('returns error for missing name', () => {
|
|
172
|
+
const fm = { description: 'Valid description here' }
|
|
173
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
174
|
+
expect(errors).toHaveLength(1)
|
|
175
|
+
expect(errors[0].field).toBe('name')
|
|
176
|
+
expect(errors[0].message).toContain('required')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('returns error for non-string name', () => {
|
|
180
|
+
const fm = { name: 42, description: 'Valid description here' }
|
|
181
|
+
const errors = validateFrontmatter(fm as any, 'agent')
|
|
182
|
+
expect(errors.some(e => e.field === 'name')).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('returns error for name too short (1 char)', () => {
|
|
186
|
+
const fm = { name: 'a', description: 'Valid description here' }
|
|
187
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
188
|
+
expect(errors.some(e => e.field === 'name' && e.message.includes('2-60'))).toBe(true)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('accepts name exactly 2 chars', () => {
|
|
192
|
+
const fm = { name: 'ab', description: 'Valid description here' }
|
|
193
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
194
|
+
expect(errors).toEqual([])
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('accepts name exactly 60 chars', () => {
|
|
198
|
+
const name = 'a'.repeat(60)
|
|
199
|
+
const fm = { name, description: 'Valid description here' }
|
|
200
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
201
|
+
// Name is 60 'a's — valid length but fails kebab-case (no hyphens needed for single segment of lowercase)
|
|
202
|
+
// Actually 'a' repeated 60 times IS valid kebab-case: /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
203
|
+
expect(errors.filter(e => e.message.includes('2-60'))).toEqual([])
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('returns error for name 61 chars', () => {
|
|
207
|
+
const name = 'a'.repeat(61)
|
|
208
|
+
const fm = { name, description: 'Valid description here' }
|
|
209
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
210
|
+
expect(errors.some(e => e.field === 'name' && e.message.includes('2-60'))).toBe(true)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('returns error for uppercase name', () => {
|
|
214
|
+
const fm = { name: 'MyAgent', description: 'Valid description here' }
|
|
215
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
216
|
+
expect(errors.some(e => e.field === 'name' && e.message.includes('kebab-case'))).toBe(true)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('returns error for name with spaces', () => {
|
|
220
|
+
const fm = { name: 'my agent', description: 'Valid description here' }
|
|
221
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
222
|
+
expect(errors.some(e => e.field === 'name' && e.message.includes('kebab-case'))).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('returns error for missing description', () => {
|
|
226
|
+
const fm = { name: 'my-agent' }
|
|
227
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
228
|
+
expect(errors.some(e => e.field === 'description')).toBe(true)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('returns error for description too short (9 chars)', () => {
|
|
232
|
+
const fm = { name: 'my-agent', description: '123456789' }
|
|
233
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
234
|
+
expect(errors.some(e => e.field === 'description' && e.message.includes('10'))).toBe(true)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('accepts description exactly 10 chars', () => {
|
|
238
|
+
const fm = { name: 'my-agent', description: '1234567890' }
|
|
239
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
240
|
+
expect(errors.filter(e => e.field === 'description')).toEqual([])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('returns error for skill without Trigger:', () => {
|
|
244
|
+
const fm = { name: 'my-skill', description: 'A valid skill description' }
|
|
245
|
+
const errors = validateFrontmatter(fm, 'skill')
|
|
246
|
+
expect(errors.some(e => e.field === 'description' && e.message.includes('Trigger:'))).toBe(true)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('returns no Trigger: error for agent type', () => {
|
|
250
|
+
const fm = { name: 'my-agent', description: 'A valid agent description' }
|
|
251
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
252
|
+
expect(errors.some(e => e.message.includes('Trigger:'))).toBe(false)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('returns error for empty string name', () => {
|
|
256
|
+
const fm = { name: '', description: 'Valid description here' }
|
|
257
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
258
|
+
expect(errors.some(e => e.field === 'name')).toBe(true)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('returns error for empty string description', () => {
|
|
262
|
+
const fm = { name: 'my-agent', description: '' }
|
|
263
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
264
|
+
expect(errors.some(e => e.field === 'description')).toBe(true)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('returns multiple errors for multiple violations', () => {
|
|
268
|
+
const fm = { name: 'AB', description: 'short' }
|
|
269
|
+
const errors = validateFrontmatter(fm, 'skill')
|
|
270
|
+
// Name is uppercase → kebab error, description < 10 → length error, no Trigger: → skill error
|
|
271
|
+
expect(errors.length).toBeGreaterThanOrEqual(2)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('validates name with numbers in kebab-case', () => {
|
|
275
|
+
const fm = { name: 'my-skill-v2', description: 'A valid description for testing' }
|
|
276
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
277
|
+
expect(errors).toEqual([])
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('rejects name starting with hyphen', () => {
|
|
281
|
+
const fm = { name: '-invalid', description: 'A valid description for testing' }
|
|
282
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
283
|
+
expect(errors.some(e => e.field === 'name' && e.message.includes('kebab-case'))).toBe(true)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('rejects name ending with hyphen', () => {
|
|
287
|
+
const fm = { name: 'invalid-', description: 'A valid description for testing' }
|
|
288
|
+
const errors = validateFrontmatter(fm, 'agent')
|
|
289
|
+
expect(errors.some(e => e.field === 'name' && e.message.includes('kebab-case'))).toBe(true)
|
|
290
|
+
})
|
|
291
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import YAML from 'yaml'
|
|
2
|
+
|
|
3
|
+
export interface FrontmatterResult {
|
|
4
|
+
data: Record<string, unknown>
|
|
5
|
+
content: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract YAML frontmatter from a markdown string.
|
|
10
|
+
* Frontmatter is delimited by --- at the start of the file.
|
|
11
|
+
*/
|
|
12
|
+
export function parseFrontmatter(raw: string): FrontmatterResult | null {
|
|
13
|
+
const trimmed = raw.trimStart()
|
|
14
|
+
if (!trimmed.startsWith('---')) return null
|
|
15
|
+
|
|
16
|
+
const endIdx = trimmed.indexOf('---', 3)
|
|
17
|
+
if (endIdx === -1) return null
|
|
18
|
+
|
|
19
|
+
const yamlBlock = trimmed.slice(3, endIdx).trim()
|
|
20
|
+
const content = trimmed.slice(endIdx + 3).trim()
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const data = YAML.parse(yamlBlock) as Record<string, unknown>
|
|
24
|
+
if (typeof data !== 'object' || data === null) return null
|
|
25
|
+
return { data, content }
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ValidationError {
|
|
32
|
+
field: string
|
|
33
|
+
message: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const KEBAB_CASE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate frontmatter against schema rules for agent or skill definitions.
|
|
40
|
+
*/
|
|
41
|
+
export function validateFrontmatter(
|
|
42
|
+
frontmatter: Record<string, unknown>,
|
|
43
|
+
type: 'agent' | 'skill'
|
|
44
|
+
): ValidationError[] {
|
|
45
|
+
const errors: ValidationError[] = []
|
|
46
|
+
|
|
47
|
+
// name: required, kebab-case, 2-60 chars
|
|
48
|
+
const name = frontmatter['name']
|
|
49
|
+
if (typeof name !== 'string' || !name) {
|
|
50
|
+
errors.push({ field: 'name', message: 'name is required and must be a string' })
|
|
51
|
+
} else {
|
|
52
|
+
if (name.length < 2 || name.length > 60) {
|
|
53
|
+
errors.push({ field: 'name', message: 'name must be 2-60 characters' })
|
|
54
|
+
}
|
|
55
|
+
if (!KEBAB_CASE_RE.test(name)) {
|
|
56
|
+
errors.push({ field: 'name', message: 'name must be kebab-case (e.g. my-skill-name)' })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// description: required, non-empty, min 10 chars
|
|
61
|
+
const desc = frontmatter['description']
|
|
62
|
+
if (typeof desc !== 'string' || !desc) {
|
|
63
|
+
errors.push({ field: 'description', message: 'description is required and must be a string' })
|
|
64
|
+
} else if (desc.length < 10) {
|
|
65
|
+
errors.push({ field: 'description', message: 'description must be at least 10 characters' })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// skill-specific: description should include "Trigger:" hint
|
|
69
|
+
if (type === 'skill' && typeof desc === 'string' && !desc.includes('Trigger:')) {
|
|
70
|
+
errors.push({
|
|
71
|
+
field: 'description',
|
|
72
|
+
message: 'skill description should include a "Trigger:" hint for auto-invoke',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return errors
|
|
77
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
// ── Mock fs-extra ────────────────────────────────────────────────────────────
|
|
5
|
+
vi.mock('fs-extra', () => {
|
|
6
|
+
const mockFs = {
|
|
7
|
+
pathExists: vi.fn(),
|
|
8
|
+
readFile: vi.fn(),
|
|
9
|
+
readJson: vi.fn(),
|
|
10
|
+
copy: vi.fn(),
|
|
11
|
+
ensureDir: vi.fn(),
|
|
12
|
+
}
|
|
13
|
+
return { default: mockFs, ...mockFs }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
import fs from 'fs-extra'
|
|
17
|
+
import { renderTemplate, generateDependabotYml, getCITemplatePath, getCIDestination, generateCIWorkflow } from './template.js'
|
|
18
|
+
import type { Stack, CIProvider } from '../types/index.js'
|
|
19
|
+
|
|
20
|
+
const mockedFs = vi.mocked(fs)
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.resetAllMocks()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
// getCITemplatePath (pure function)
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
describe('getCITemplatePath', () => {
|
|
30
|
+
it('returns correct path for node + github', () => {
|
|
31
|
+
const result = getCITemplatePath('node', 'github')
|
|
32
|
+
expect(result).not.toBeNull()
|
|
33
|
+
expect(result).toContain('github')
|
|
34
|
+
expect(result).toContain('ci-node.yml')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns correct path for python + gitlab', () => {
|
|
38
|
+
const result = getCITemplatePath('python', 'gitlab')
|
|
39
|
+
expect(result).not.toBeNull()
|
|
40
|
+
expect(result).toContain('gitlab')
|
|
41
|
+
expect(result).toContain('gitlab-ci-python.yml')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns null for unknown stack', () => {
|
|
45
|
+
const result = getCITemplatePath('haskell' as Stack, 'github')
|
|
46
|
+
expect(result).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns null for unknown provider', () => {
|
|
50
|
+
const result = getCITemplatePath('node', 'bitbucket' as CIProvider)
|
|
51
|
+
expect(result).toBeNull()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns non-null for all valid stack+provider combinations', () => {
|
|
55
|
+
const stacks: Stack[] = ['node', 'python', 'go', 'rust', 'java-gradle', 'java-maven']
|
|
56
|
+
const providers: CIProvider[] = ['github', 'gitlab', 'woodpecker']
|
|
57
|
+
|
|
58
|
+
for (const stack of stacks) {
|
|
59
|
+
for (const provider of providers) {
|
|
60
|
+
const result = getCITemplatePath(stack, provider)
|
|
61
|
+
expect(result).not.toBeNull()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('returns null for elixir (no CI template)', () => {
|
|
67
|
+
// Elixir is not in the STACK_CI_MAP entries
|
|
68
|
+
const result = getCITemplatePath('elixir', 'github')
|
|
69
|
+
expect(result).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
// getCIDestination (pure function)
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
76
|
+
describe('getCIDestination', () => {
|
|
77
|
+
it('returns .github/workflows for github', () => {
|
|
78
|
+
expect(getCIDestination('github')).toBe('.github/workflows/ci.yml')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('returns .gitlab-ci.yml for gitlab', () => {
|
|
82
|
+
expect(getCIDestination('gitlab')).toBe('.gitlab-ci.yml')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('returns .woodpecker.yml for woodpecker', () => {
|
|
86
|
+
expect(getCIDestination('woodpecker')).toBe('.woodpecker.yml')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
91
|
+
// renderTemplate
|
|
92
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
93
|
+
describe('renderTemplate', () => {
|
|
94
|
+
it('replaces a single placeholder', async () => {
|
|
95
|
+
mockedFs.readFile.mockResolvedValue('Hello __NAME__!' as never)
|
|
96
|
+
const result = await renderTemplate('/tpl/test.yml', { NAME: 'World' })
|
|
97
|
+
expect(result).toBe('Hello World!')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('replaces multiple different placeholders', async () => {
|
|
101
|
+
mockedFs.readFile.mockResolvedValue('__GREETING__ __NAME__, age __AGE__' as never)
|
|
102
|
+
const result = await renderTemplate('/tpl/test.yml', {
|
|
103
|
+
GREETING: 'Hi',
|
|
104
|
+
NAME: 'Alice',
|
|
105
|
+
AGE: '30',
|
|
106
|
+
})
|
|
107
|
+
expect(result).toBe('Hi Alice, age 30')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('returns content unchanged when no placeholders', async () => {
|
|
111
|
+
mockedFs.readFile.mockResolvedValue('No placeholders here.' as never)
|
|
112
|
+
const result = await renderTemplate('/tpl/test.yml', { NAME: 'World' })
|
|
113
|
+
expect(result).toBe('No placeholders here.')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('replaces placeholder appearing multiple times', async () => {
|
|
117
|
+
mockedFs.readFile.mockResolvedValue('__X__ and __X__ again' as never)
|
|
118
|
+
const result = await renderTemplate('/tpl/test.yml', { X: 'val' })
|
|
119
|
+
expect(result).toBe('val and val again')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('handles empty variable value', async () => {
|
|
123
|
+
mockedFs.readFile.mockResolvedValue('prefix__EMPTY__suffix' as never)
|
|
124
|
+
const result = await renderTemplate('/tpl/test.yml', { EMPTY: '' })
|
|
125
|
+
expect(result).toBe('prefixsuffix')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
130
|
+
// generateDependabotYml
|
|
131
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
132
|
+
describe('generateDependabotYml', () => {
|
|
133
|
+
it('generates for a single stack', async () => {
|
|
134
|
+
mockedFs.readFile.mockImplementation(async (filePath: unknown) => {
|
|
135
|
+
const p = String(filePath)
|
|
136
|
+
if (p.includes('header.yml')) return 'version: 2\nupdates:\n' as never
|
|
137
|
+
if (p.includes('github-actions.yml')) return ' - package-ecosystem: github-actions\n' as never
|
|
138
|
+
if (p.includes('npm.yml')) return ' - package-ecosystem: npm\n' as never
|
|
139
|
+
return '' as never
|
|
140
|
+
})
|
|
141
|
+
mockedFs.pathExists.mockResolvedValue(true as never)
|
|
142
|
+
|
|
143
|
+
const result = await generateDependabotYml(['node'], true)
|
|
144
|
+
expect(result).toContain('version: 2')
|
|
145
|
+
expect(result).toContain('github-actions')
|
|
146
|
+
expect(result).toContain('npm')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('deduplicates fragments for multiple stacks', async () => {
|
|
150
|
+
let npmCallCount = 0
|
|
151
|
+
mockedFs.readFile.mockImplementation(async (filePath: unknown) => {
|
|
152
|
+
const p = String(filePath)
|
|
153
|
+
if (p.includes('header.yml')) return 'header\n' as never
|
|
154
|
+
if (p.includes('github-actions.yml')) return 'gh-actions\n' as never
|
|
155
|
+
if (p.includes('npm.yml')) {
|
|
156
|
+
npmCallCount++
|
|
157
|
+
return 'npm-fragment\n' as never
|
|
158
|
+
}
|
|
159
|
+
return '' as never
|
|
160
|
+
})
|
|
161
|
+
mockedFs.pathExists.mockResolvedValue(true as never)
|
|
162
|
+
|
|
163
|
+
// Two node stacks — 'npm' fragment should only appear once
|
|
164
|
+
const result = await generateDependabotYml(['node', 'node'], true)
|
|
165
|
+
expect(npmCallCount).toBe(1)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('includes github-actions fragment when includeGitHubActions is true', async () => {
|
|
169
|
+
let githubActionsRead = false
|
|
170
|
+
mockedFs.readFile.mockImplementation(async (filePath: unknown) => {
|
|
171
|
+
const p = String(filePath)
|
|
172
|
+
if (p.includes('header.yml')) return 'header\n' as never
|
|
173
|
+
if (p.includes('github-actions.yml')) {
|
|
174
|
+
githubActionsRead = true
|
|
175
|
+
return 'gh-actions\n' as never
|
|
176
|
+
}
|
|
177
|
+
return '' as never
|
|
178
|
+
})
|
|
179
|
+
mockedFs.pathExists.mockResolvedValue(true as never)
|
|
180
|
+
|
|
181
|
+
await generateDependabotYml([], true)
|
|
182
|
+
expect(githubActionsRead).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('excludes github-actions fragment when includeGitHubActions is false', async () => {
|
|
186
|
+
let githubActionsRead = false
|
|
187
|
+
mockedFs.readFile.mockImplementation(async (filePath: unknown) => {
|
|
188
|
+
const p = String(filePath)
|
|
189
|
+
if (p.includes('header.yml')) return 'header\n' as never
|
|
190
|
+
if (p.includes('github-actions.yml')) {
|
|
191
|
+
githubActionsRead = true
|
|
192
|
+
return 'gh-actions\n' as never
|
|
193
|
+
}
|
|
194
|
+
return '' as never
|
|
195
|
+
})
|
|
196
|
+
mockedFs.pathExists.mockResolvedValue(true as never)
|
|
197
|
+
|
|
198
|
+
await generateDependabotYml([], false)
|
|
199
|
+
expect(githubActionsRead).toBe(false)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
204
|
+
// generateCIWorkflow
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
206
|
+
describe('generateCIWorkflow', () => {
|
|
207
|
+
it('returns file content for valid stack+provider', async () => {
|
|
208
|
+
mockedFs.pathExists.mockResolvedValue(true as never)
|
|
209
|
+
mockedFs.readFile.mockResolvedValue('name: CI\non: push' as never)
|
|
210
|
+
|
|
211
|
+
const result = await generateCIWorkflow('node', 'github')
|
|
212
|
+
expect(result).not.toBeNull()
|
|
213
|
+
expect(result).toContain('name: CI')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('returns null when no template mapping exists', async () => {
|
|
217
|
+
const result = await generateCIWorkflow('elixir', 'github')
|
|
218
|
+
expect(result).toBeNull()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('returns null when template file does not exist on disk', async () => {
|
|
222
|
+
mockedFs.pathExists.mockResolvedValue(false as never)
|
|
223
|
+
const result = await generateCIWorkflow('node', 'github')
|
|
224
|
+
expect(result).toBeNull()
|
|
225
|
+
})
|
|
226
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import type { Stack, CIProvider } from '../types/index.js'
|
|
4
|
+
import {
|
|
5
|
+
TEMPLATES_DIR,
|
|
6
|
+
DEPENDABOT_FRAGMENTS_DIR,
|
|
7
|
+
STACK_DEPENDABOT_MAP,
|
|
8
|
+
STACK_CI_MAP,
|
|
9
|
+
} from '../constants.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read a template file and replace __VAR_NAME__ placeholders.
|
|
13
|
+
*/
|
|
14
|
+
export async function renderTemplate(
|
|
15
|
+
templatePath: string,
|
|
16
|
+
vars: Record<string, string>
|
|
17
|
+
): Promise<string> {
|
|
18
|
+
let content = await fs.readFile(templatePath, 'utf-8')
|
|
19
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
20
|
+
const placeholder = `__${key}__`
|
|
21
|
+
content = content.replaceAll(placeholder, value)
|
|
22
|
+
}
|
|
23
|
+
return content
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Assemble a dependabot.yml from the header + stack-specific fragments.
|
|
28
|
+
* Always includes github-actions fragment when provider is GitHub.
|
|
29
|
+
*/
|
|
30
|
+
export async function generateDependabotYml(
|
|
31
|
+
stacks: Stack[],
|
|
32
|
+
includeGitHubActions = true
|
|
33
|
+
): Promise<string> {
|
|
34
|
+
const headerPath = path.join(DEPENDABOT_FRAGMENTS_DIR, 'header.yml')
|
|
35
|
+
let content = await fs.readFile(headerPath, 'utf-8')
|
|
36
|
+
|
|
37
|
+
// Collect unique fragment names
|
|
38
|
+
const fragments = new Set<string>()
|
|
39
|
+
if (includeGitHubActions) fragments.add('github-actions')
|
|
40
|
+
|
|
41
|
+
for (const stack of stacks) {
|
|
42
|
+
const stackFragments = STACK_DEPENDABOT_MAP[stack] ?? []
|
|
43
|
+
for (const f of stackFragments) fragments.add(f)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Append each fragment
|
|
47
|
+
for (const fragmentName of fragments) {
|
|
48
|
+
const fragmentPath = path.join(DEPENDABOT_FRAGMENTS_DIR, `${fragmentName}.yml`)
|
|
49
|
+
if (await fs.pathExists(fragmentPath)) {
|
|
50
|
+
const fragment = await fs.readFile(fragmentPath, 'utf-8')
|
|
51
|
+
content += '\n' + fragment
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return content
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the CI workflow template path for a given stack + provider combination.
|
|
60
|
+
* Returns null if no template exists for that combination.
|
|
61
|
+
*/
|
|
62
|
+
export function getCITemplatePath(stack: Stack, provider: CIProvider): string | null {
|
|
63
|
+
const providerMap = STACK_CI_MAP[provider]
|
|
64
|
+
if (!providerMap) return null
|
|
65
|
+
|
|
66
|
+
const filename = providerMap[stack]
|
|
67
|
+
if (!filename) return null
|
|
68
|
+
|
|
69
|
+
return path.join(TEMPLATES_DIR, provider, filename)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate the CI workflow file content for a given stack + provider.
|
|
74
|
+
*/
|
|
75
|
+
export async function generateCIWorkflow(
|
|
76
|
+
stack: Stack,
|
|
77
|
+
provider: CIProvider
|
|
78
|
+
): Promise<string | null> {
|
|
79
|
+
const templatePath = getCITemplatePath(stack, provider)
|
|
80
|
+
if (!templatePath) return null
|
|
81
|
+
|
|
82
|
+
if (!await fs.pathExists(templatePath)) return null
|
|
83
|
+
|
|
84
|
+
return fs.readFile(templatePath, 'utf-8')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the destination path for a CI workflow file within a project.
|
|
89
|
+
*/
|
|
90
|
+
export function getCIDestination(provider: CIProvider): string {
|
|
91
|
+
switch (provider) {
|
|
92
|
+
case 'github':
|
|
93
|
+
return '.github/workflows/ci.yml'
|
|
94
|
+
case 'gitlab':
|
|
95
|
+
return '.gitlab-ci.yml'
|
|
96
|
+
case 'woodpecker':
|
|
97
|
+
return '.woodpecker.yml'
|
|
98
|
+
}
|
|
99
|
+
}
|