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.
Files changed (500) hide show
  1. package/.gitignore.template +105 -0
  2. package/.releaserc +44 -0
  3. package/README.md +45 -0
  4. package/ai-config/.skillignore +15 -0
  5. package/ai-config/AUTO_INVOKE.md +300 -0
  6. package/ai-config/agents/_TEMPLATE.md +93 -0
  7. package/ai-config/agents/business/api-designer.md +1657 -0
  8. package/ai-config/agents/business/business-analyst.md +1331 -0
  9. package/ai-config/agents/business/product-strategist.md +206 -0
  10. package/ai-config/agents/business/project-manager.md +178 -0
  11. package/ai-config/agents/business/requirements-analyst.md +1277 -0
  12. package/ai-config/agents/business/technical-writer.md +1679 -0
  13. package/ai-config/agents/creative/ux-designer.md +205 -0
  14. package/ai-config/agents/data-ai/ai-engineer.md +487 -0
  15. package/ai-config/agents/data-ai/analytics-engineer.md +953 -0
  16. package/ai-config/agents/data-ai/data-engineer.md +173 -0
  17. package/ai-config/agents/data-ai/data-scientist.md +672 -0
  18. package/ai-config/agents/data-ai/mlops-engineer.md +814 -0
  19. package/ai-config/agents/data-ai/prompt-engineer.md +772 -0
  20. package/ai-config/agents/development/angular-expert.md +620 -0
  21. package/ai-config/agents/development/backend-architect.md +795 -0
  22. package/ai-config/agents/development/database-specialist.md +212 -0
  23. package/ai-config/agents/development/frontend-specialist.md +686 -0
  24. package/ai-config/agents/development/fullstack-engineer.md +668 -0
  25. package/ai-config/agents/development/golang-pro.md +338 -0
  26. package/ai-config/agents/development/java-enterprise.md +400 -0
  27. package/ai-config/agents/development/javascript-pro.md +422 -0
  28. package/ai-config/agents/development/nextjs-pro.md +474 -0
  29. package/ai-config/agents/development/python-pro.md +570 -0
  30. package/ai-config/agents/development/react-pro.md +487 -0
  31. package/ai-config/agents/development/rust-pro.md +246 -0
  32. package/ai-config/agents/development/spring-boot-4-expert.md +326 -0
  33. package/ai-config/agents/development/typescript-pro.md +336 -0
  34. package/ai-config/agents/development/vue-specialist.md +605 -0
  35. package/ai-config/agents/infrastructure/cloud-architect.md +472 -0
  36. package/ai-config/agents/infrastructure/deployment-manager.md +358 -0
  37. package/ai-config/agents/infrastructure/devops-engineer.md +455 -0
  38. package/ai-config/agents/infrastructure/incident-responder.md +519 -0
  39. package/ai-config/agents/infrastructure/kubernetes-expert.md +705 -0
  40. package/ai-config/agents/infrastructure/monitoring-specialist.md +674 -0
  41. package/ai-config/agents/infrastructure/performance-engineer.md +658 -0
  42. package/ai-config/agents/orchestrator.md +241 -0
  43. package/ai-config/agents/quality/accessibility-auditor.md +1204 -0
  44. package/ai-config/agents/quality/code-reviewer-compact.md +123 -0
  45. package/ai-config/agents/quality/code-reviewer.md +363 -0
  46. package/ai-config/agents/quality/dependency-manager.md +743 -0
  47. package/ai-config/agents/quality/e2e-test-specialist.md +1005 -0
  48. package/ai-config/agents/quality/performance-tester.md +1086 -0
  49. package/ai-config/agents/quality/security-auditor.md +133 -0
  50. package/ai-config/agents/quality/test-engineer.md +453 -0
  51. package/ai-config/agents/specialists/api-designer.md +87 -0
  52. package/ai-config/agents/specialists/backend-architect.md +73 -0
  53. package/ai-config/agents/specialists/code-reviewer.md +77 -0
  54. package/ai-config/agents/specialists/db-optimizer.md +75 -0
  55. package/ai-config/agents/specialists/devops-engineer.md +83 -0
  56. package/ai-config/agents/specialists/documentation-writer.md +78 -0
  57. package/ai-config/agents/specialists/frontend-developer.md +75 -0
  58. package/ai-config/agents/specialists/performance-analyst.md +82 -0
  59. package/ai-config/agents/specialists/refactor-specialist.md +74 -0
  60. package/ai-config/agents/specialists/security-auditor.md +74 -0
  61. package/ai-config/agents/specialists/test-engineer.md +81 -0
  62. package/ai-config/agents/specialists/ux-consultant.md +76 -0
  63. package/ai-config/agents/specialized/agent-generator.md +1190 -0
  64. package/ai-config/agents/specialized/blockchain-developer.md +149 -0
  65. package/ai-config/agents/specialized/code-migrator.md +892 -0
  66. package/ai-config/agents/specialized/context-manager.md +978 -0
  67. package/ai-config/agents/specialized/documentation-writer.md +1078 -0
  68. package/ai-config/agents/specialized/ecommerce-expert.md +1756 -0
  69. package/ai-config/agents/specialized/embedded-engineer.md +1714 -0
  70. package/ai-config/agents/specialized/error-detective.md +1034 -0
  71. package/ai-config/agents/specialized/fintech-specialist.md +1659 -0
  72. package/ai-config/agents/specialized/freelance-project-planner-v2.md +1988 -0
  73. package/ai-config/agents/specialized/freelance-project-planner-v3.md +2136 -0
  74. package/ai-config/agents/specialized/freelance-project-planner-v4.md +4503 -0
  75. package/ai-config/agents/specialized/freelance-project-planner.md +722 -0
  76. package/ai-config/agents/specialized/game-developer.md +1963 -0
  77. package/ai-config/agents/specialized/healthcare-dev.md +1620 -0
  78. package/ai-config/agents/specialized/mobile-developer.md +188 -0
  79. package/ai-config/agents/specialized/parallel-plan-executor.md +506 -0
  80. package/ai-config/agents/specialized/plan-executor.md +485 -0
  81. package/ai-config/agents/specialized/solo-dev-planner-modular/00-INDEX.md +485 -0
  82. package/ai-config/agents/specialized/solo-dev-planner-modular/01-CORE.md +3493 -0
  83. package/ai-config/agents/specialized/solo-dev-planner-modular/02-SELF-CORRECTION.md +778 -0
  84. package/ai-config/agents/specialized/solo-dev-planner-modular/03-PROGRESSIVE-SETUP.md +918 -0
  85. package/ai-config/agents/specialized/solo-dev-planner-modular/04-DEPLOYMENT.md +1537 -0
  86. package/ai-config/agents/specialized/solo-dev-planner-modular/05-TESTING.md +2633 -0
  87. package/ai-config/agents/specialized/solo-dev-planner-modular/06-OPERATIONS.md +5610 -0
  88. package/ai-config/agents/specialized/solo-dev-planner-modular/INSTALL.md +335 -0
  89. package/ai-config/agents/specialized/solo-dev-planner-modular/QUICK-REFERENCE.txt +215 -0
  90. package/ai-config/agents/specialized/solo-dev-planner-modular/README.md +260 -0
  91. package/ai-config/agents/specialized/solo-dev-planner-modular/START-HERE.md +379 -0
  92. package/ai-config/agents/specialized/solo-dev-planner-modular/WORKFLOW-DIAGRAM.md +355 -0
  93. package/ai-config/agents/specialized/solo-dev-planner-modular/solo-dev-planner.md +279 -0
  94. package/ai-config/agents/specialized/template-writer.md +347 -0
  95. package/ai-config/agents/specialized/test-runner.md +99 -0
  96. package/ai-config/agents/specialized/vibekanban-smart-worker.md +244 -0
  97. package/ai-config/agents/specialized/wave-executor.md +138 -0
  98. package/ai-config/agents/specialized/workflow-optimizer.md +1114 -0
  99. package/ai-config/commands/git/changelog.md +32 -0
  100. package/ai-config/commands/git/ci-local.md +70 -0
  101. package/ai-config/commands/git/commit.md +35 -0
  102. package/ai-config/commands/git/fix-issue.md +23 -0
  103. package/ai-config/commands/git/pr-create.md +42 -0
  104. package/ai-config/commands/git/pr-review.md +50 -0
  105. package/ai-config/commands/git/worktree.md +39 -0
  106. package/ai-config/commands/refactoring/cleanup.md +24 -0
  107. package/ai-config/commands/refactoring/dead-code.md +40 -0
  108. package/ai-config/commands/refactoring/extract.md +31 -0
  109. package/ai-config/commands/testing/e2e.md +30 -0
  110. package/ai-config/commands/testing/tdd.md +36 -0
  111. package/ai-config/commands/testing/test-coverage.md +30 -0
  112. package/ai-config/commands/testing/test-fix.md +24 -0
  113. package/ai-config/commands/workflow/generate-agents-md.md +85 -0
  114. package/ai-config/commands/workflow/planning.md +47 -0
  115. package/ai-config/commands/workflows/compound.md +89 -0
  116. package/ai-config/commands/workflows/plan.md +77 -0
  117. package/ai-config/commands/workflows/review.md +78 -0
  118. package/ai-config/commands/workflows/work.md +75 -0
  119. package/ai-config/config.yaml +18 -0
  120. package/ai-config/hooks/_TEMPLATE.md +96 -0
  121. package/ai-config/hooks/block-dangerous-commands.md +75 -0
  122. package/ai-config/hooks/commit-guard.md +90 -0
  123. package/ai-config/hooks/context-loader.md +73 -0
  124. package/ai-config/hooks/improve-prompt.md +91 -0
  125. package/ai-config/hooks/learning-log.md +72 -0
  126. package/ai-config/hooks/model-router.md +86 -0
  127. package/ai-config/hooks/secret-scanner.md +64 -0
  128. package/ai-config/hooks/skill-validator.md +102 -0
  129. package/ai-config/hooks/task-artifact.md +114 -0
  130. package/ai-config/hooks/validate-workflow.md +100 -0
  131. package/ai-config/prompts/base.md +71 -0
  132. package/ai-config/prompts/modes/debug.md +34 -0
  133. package/ai-config/prompts/modes/deploy.md +40 -0
  134. package/ai-config/prompts/modes/research.md +32 -0
  135. package/ai-config/prompts/modes/review.md +33 -0
  136. package/ai-config/prompts/review-policy.md +79 -0
  137. package/ai-config/skills/_TEMPLATE.md +157 -0
  138. package/ai-config/skills/backend/api-gateway/SKILL.md +254 -0
  139. package/ai-config/skills/backend/bff-concepts/SKILL.md +239 -0
  140. package/ai-config/skills/backend/bff-spring/SKILL.md +364 -0
  141. package/ai-config/skills/backend/chi-router/SKILL.md +396 -0
  142. package/ai-config/skills/backend/error-handling/SKILL.md +255 -0
  143. package/ai-config/skills/backend/exceptions-spring/SKILL.md +323 -0
  144. package/ai-config/skills/backend/fastapi/SKILL.md +302 -0
  145. package/ai-config/skills/backend/gateway-spring/SKILL.md +390 -0
  146. package/ai-config/skills/backend/go-backend/SKILL.md +457 -0
  147. package/ai-config/skills/backend/gradle-multimodule/SKILL.md +274 -0
  148. package/ai-config/skills/backend/graphql-concepts/SKILL.md +352 -0
  149. package/ai-config/skills/backend/graphql-spring/SKILL.md +398 -0
  150. package/ai-config/skills/backend/grpc-concepts/SKILL.md +283 -0
  151. package/ai-config/skills/backend/grpc-spring/SKILL.md +445 -0
  152. package/ai-config/skills/backend/jwt-auth/SKILL.md +412 -0
  153. package/ai-config/skills/backend/notifications-concepts/SKILL.md +259 -0
  154. package/ai-config/skills/backend/recommendations-concepts/SKILL.md +261 -0
  155. package/ai-config/skills/backend/search-concepts/SKILL.md +263 -0
  156. package/ai-config/skills/backend/search-spring/SKILL.md +375 -0
  157. package/ai-config/skills/backend/spring-boot-4/SKILL.md +172 -0
  158. package/ai-config/skills/backend/websockets/SKILL.md +532 -0
  159. package/ai-config/skills/data-ai/ai-ml/SKILL.md +423 -0
  160. package/ai-config/skills/data-ai/analytics-concepts/SKILL.md +195 -0
  161. package/ai-config/skills/data-ai/analytics-spring/SKILL.md +340 -0
  162. package/ai-config/skills/data-ai/duckdb-analytics/SKILL.md +440 -0
  163. package/ai-config/skills/data-ai/langchain/SKILL.md +238 -0
  164. package/ai-config/skills/data-ai/mlflow/SKILL.md +302 -0
  165. package/ai-config/skills/data-ai/onnx-inference/SKILL.md +290 -0
  166. package/ai-config/skills/data-ai/powerbi/SKILL.md +352 -0
  167. package/ai-config/skills/data-ai/pytorch/SKILL.md +274 -0
  168. package/ai-config/skills/data-ai/scikit-learn/SKILL.md +321 -0
  169. package/ai-config/skills/data-ai/vector-db/SKILL.md +301 -0
  170. package/ai-config/skills/database/graph-databases/SKILL.md +218 -0
  171. package/ai-config/skills/database/graph-spring/SKILL.md +361 -0
  172. package/ai-config/skills/database/pgx-postgres/SKILL.md +512 -0
  173. package/ai-config/skills/database/redis-cache/SKILL.md +343 -0
  174. package/ai-config/skills/database/sqlite-embedded/SKILL.md +388 -0
  175. package/ai-config/skills/database/timescaledb/SKILL.md +320 -0
  176. package/ai-config/skills/docs/api-documentation/SKILL.md +293 -0
  177. package/ai-config/skills/docs/docs-spring/SKILL.md +377 -0
  178. package/ai-config/skills/docs/mustache-templates/SKILL.md +190 -0
  179. package/ai-config/skills/docs/technical-docs/SKILL.md +447 -0
  180. package/ai-config/skills/frontend/astro-ssr/SKILL.md +441 -0
  181. package/ai-config/skills/frontend/frontend-design/SKILL.md +54 -0
  182. package/ai-config/skills/frontend/frontend-web/SKILL.md +368 -0
  183. package/ai-config/skills/frontend/mantine-ui/SKILL.md +396 -0
  184. package/ai-config/skills/frontend/tanstack-query/SKILL.md +439 -0
  185. package/ai-config/skills/frontend/zod-validation/SKILL.md +417 -0
  186. package/ai-config/skills/frontend/zustand-state/SKILL.md +350 -0
  187. package/ai-config/skills/infrastructure/chaos-engineering/SKILL.md +244 -0
  188. package/ai-config/skills/infrastructure/chaos-spring/SKILL.md +378 -0
  189. package/ai-config/skills/infrastructure/devops-infra/SKILL.md +435 -0
  190. package/ai-config/skills/infrastructure/docker-containers/SKILL.md +420 -0
  191. package/ai-config/skills/infrastructure/kubernetes/SKILL.md +456 -0
  192. package/ai-config/skills/infrastructure/opentelemetry/SKILL.md +546 -0
  193. package/ai-config/skills/infrastructure/traefik-proxy/SKILL.md +474 -0
  194. package/ai-config/skills/infrastructure/woodpecker-ci/SKILL.md +315 -0
  195. package/ai-config/skills/mobile/ionic-capacitor/SKILL.md +504 -0
  196. package/ai-config/skills/mobile/mobile-ionic/SKILL.md +448 -0
  197. package/ai-config/skills/prompt-improver/SKILL.md +125 -0
  198. package/ai-config/skills/quality/ghagga-review/SKILL.md +216 -0
  199. package/ai-config/skills/references/hooks-patterns/SKILL.md +238 -0
  200. package/ai-config/skills/references/mcp-servers/SKILL.md +275 -0
  201. package/ai-config/skills/references/plugins-reference/SKILL.md +110 -0
  202. package/ai-config/skills/references/skills-reference/SKILL.md +420 -0
  203. package/ai-config/skills/references/subagent-templates/SKILL.md +193 -0
  204. package/ai-config/skills/systems-iot/modbus-protocol/SKILL.md +410 -0
  205. package/ai-config/skills/systems-iot/mqtt-rumqttc/SKILL.md +408 -0
  206. package/ai-config/skills/systems-iot/rust-systems/SKILL.md +386 -0
  207. package/ai-config/skills/systems-iot/tokio-async/SKILL.md +324 -0
  208. package/ai-config/skills/testing/playwright-e2e/SKILL.md +289 -0
  209. package/ai-config/skills/testing/testcontainers/SKILL.md +299 -0
  210. package/ai-config/skills/testing/vitest-testing/SKILL.md +381 -0
  211. package/ai-config/skills/workflow/ci-local-guide/SKILL.md +118 -0
  212. package/ai-config/skills/workflow/claude-automation-recommender/SKILL.md +299 -0
  213. package/ai-config/skills/workflow/claude-md-improver/SKILL.md +158 -0
  214. package/ai-config/skills/workflow/finishing-a-development-branch/SKILL.md +117 -0
  215. package/ai-config/skills/workflow/git-github/SKILL.md +334 -0
  216. package/ai-config/skills/workflow/git-github/references/examples.md +160 -0
  217. package/ai-config/skills/workflow/git-workflow/SKILL.md +214 -0
  218. package/ai-config/skills/workflow/ide-plugins/SKILL.md +277 -0
  219. package/ai-config/skills/workflow/ide-plugins-intellij/SKILL.md +401 -0
  220. package/ai-config/skills/workflow/obsidian-brain-workflow/SKILL.md +199 -0
  221. package/ai-config/skills/workflow/using-git-worktrees/SKILL.md +100 -0
  222. package/ai-config/skills/workflow/verification-before-completion/SKILL.md +73 -0
  223. package/ai-config/skills/workflow/wave-workflow/SKILL.md +178 -0
  224. package/ci-local/README.md +170 -0
  225. package/ci-local/ci-local.sh +297 -0
  226. package/ci-local/hooks/commit-msg +74 -0
  227. package/ci-local/hooks/pre-commit +162 -0
  228. package/ci-local/hooks/pre-push +41 -0
  229. package/ci-local/install.sh +49 -0
  230. package/ci-local/semgrep.yml +214 -0
  231. package/dist/commands/analyze.d.ts +9 -0
  232. package/dist/commands/analyze.d.ts.map +1 -0
  233. package/dist/commands/analyze.js +55 -0
  234. package/dist/commands/analyze.js.map +1 -0
  235. package/dist/commands/analyze.test.d.ts +2 -0
  236. package/dist/commands/analyze.test.d.ts.map +1 -0
  237. package/dist/commands/analyze.test.js +145 -0
  238. package/dist/commands/analyze.test.js.map +1 -0
  239. package/dist/commands/doctor.d.ts +7 -0
  240. package/dist/commands/doctor.d.ts.map +1 -0
  241. package/dist/commands/doctor.js +158 -0
  242. package/dist/commands/doctor.js.map +1 -0
  243. package/dist/commands/doctor.test.d.ts +2 -0
  244. package/dist/commands/doctor.test.d.ts.map +1 -0
  245. package/dist/commands/doctor.test.js +200 -0
  246. package/dist/commands/doctor.test.js.map +1 -0
  247. package/dist/commands/init.d.ts +9 -0
  248. package/dist/commands/init.d.ts.map +1 -0
  249. package/dist/commands/init.js +283 -0
  250. package/dist/commands/init.js.map +1 -0
  251. package/dist/commands/init.test.d.ts +2 -0
  252. package/dist/commands/init.test.d.ts.map +1 -0
  253. package/dist/commands/init.test.js +271 -0
  254. package/dist/commands/init.test.js.map +1 -0
  255. package/dist/commands/sync.d.ts +8 -0
  256. package/dist/commands/sync.d.ts.map +1 -0
  257. package/dist/commands/sync.js +201 -0
  258. package/dist/commands/sync.js.map +1 -0
  259. package/dist/constants.d.ts +21 -0
  260. package/dist/constants.d.ts.map +1 -0
  261. package/dist/constants.js +57 -0
  262. package/dist/constants.js.map +1 -0
  263. package/dist/e2e/aggressive.e2e.test.d.ts +2 -0
  264. package/dist/e2e/aggressive.e2e.test.d.ts.map +1 -0
  265. package/dist/e2e/aggressive.e2e.test.js +350 -0
  266. package/dist/e2e/aggressive.e2e.test.js.map +1 -0
  267. package/dist/e2e/commands.e2e.test.d.ts +2 -0
  268. package/dist/e2e/commands.e2e.test.d.ts.map +1 -0
  269. package/dist/e2e/commands.e2e.test.js +213 -0
  270. package/dist/e2e/commands.e2e.test.js.map +1 -0
  271. package/dist/index.d.ts +3 -0
  272. package/dist/index.d.ts.map +1 -0
  273. package/dist/index.js +82 -0
  274. package/dist/index.js.map +1 -0
  275. package/dist/lib/common.d.ts +17 -0
  276. package/dist/lib/common.d.ts.map +1 -0
  277. package/dist/lib/common.js +111 -0
  278. package/dist/lib/common.js.map +1 -0
  279. package/dist/lib/common.test.d.ts +2 -0
  280. package/dist/lib/common.test.d.ts.map +1 -0
  281. package/dist/lib/common.test.js +316 -0
  282. package/dist/lib/common.test.js.map +1 -0
  283. package/dist/lib/frontmatter.d.ts +18 -0
  284. package/dist/lib/frontmatter.d.ts.map +1 -0
  285. package/dist/lib/frontmatter.js +61 -0
  286. package/dist/lib/frontmatter.js.map +1 -0
  287. package/dist/lib/frontmatter.test.d.ts +2 -0
  288. package/dist/lib/frontmatter.test.d.ts.map +1 -0
  289. package/dist/lib/frontmatter.test.js +257 -0
  290. package/dist/lib/frontmatter.test.js.map +1 -0
  291. package/dist/lib/template.d.ts +24 -0
  292. package/dist/lib/template.d.ts.map +1 -0
  293. package/dist/lib/template.js +78 -0
  294. package/dist/lib/template.js.map +1 -0
  295. package/dist/lib/template.test.d.ts +2 -0
  296. package/dist/lib/template.test.d.ts.map +1 -0
  297. package/dist/lib/template.test.js +201 -0
  298. package/dist/lib/template.test.js.map +1 -0
  299. package/dist/types/index.d.ts +48 -0
  300. package/dist/types/index.d.ts.map +1 -0
  301. package/dist/types/index.js +2 -0
  302. package/dist/types/index.js.map +1 -0
  303. package/dist/ui/AnalyzeUI.d.ts +7 -0
  304. package/dist/ui/AnalyzeUI.d.ts.map +1 -0
  305. package/dist/ui/AnalyzeUI.js +100 -0
  306. package/dist/ui/AnalyzeUI.js.map +1 -0
  307. package/dist/ui/App.d.ts +13 -0
  308. package/dist/ui/App.d.ts.map +1 -0
  309. package/dist/ui/App.js +100 -0
  310. package/dist/ui/App.js.map +1 -0
  311. package/dist/ui/CIContext.d.ts +9 -0
  312. package/dist/ui/CIContext.d.ts.map +1 -0
  313. package/dist/ui/CIContext.js +9 -0
  314. package/dist/ui/CIContext.js.map +1 -0
  315. package/dist/ui/CISelector.d.ts +8 -0
  316. package/dist/ui/CISelector.d.ts.map +1 -0
  317. package/dist/ui/CISelector.js +45 -0
  318. package/dist/ui/CISelector.js.map +1 -0
  319. package/dist/ui/Doctor.d.ts +3 -0
  320. package/dist/ui/Doctor.d.ts.map +1 -0
  321. package/dist/ui/Doctor.js +89 -0
  322. package/dist/ui/Doctor.js.map +1 -0
  323. package/dist/ui/Header.d.ts +8 -0
  324. package/dist/ui/Header.d.ts.map +1 -0
  325. package/dist/ui/Header.js +30 -0
  326. package/dist/ui/Header.js.map +1 -0
  327. package/dist/ui/MemorySelector.d.ts +8 -0
  328. package/dist/ui/MemorySelector.d.ts.map +1 -0
  329. package/dist/ui/MemorySelector.js +46 -0
  330. package/dist/ui/MemorySelector.js.map +1 -0
  331. package/dist/ui/NameInput.d.ts +8 -0
  332. package/dist/ui/NameInput.d.ts.map +1 -0
  333. package/dist/ui/NameInput.js +69 -0
  334. package/dist/ui/NameInput.js.map +1 -0
  335. package/dist/ui/OptionSelector.d.ts +12 -0
  336. package/dist/ui/OptionSelector.d.ts.map +1 -0
  337. package/dist/ui/OptionSelector.js +69 -0
  338. package/dist/ui/OptionSelector.js.map +1 -0
  339. package/dist/ui/Progress.d.ts +11 -0
  340. package/dist/ui/Progress.d.ts.map +1 -0
  341. package/dist/ui/Progress.js +58 -0
  342. package/dist/ui/Progress.js.map +1 -0
  343. package/dist/ui/StackSelector.d.ts +9 -0
  344. package/dist/ui/StackSelector.d.ts.map +1 -0
  345. package/dist/ui/StackSelector.js +65 -0
  346. package/dist/ui/StackSelector.js.map +1 -0
  347. package/dist/ui/Summary.d.ts +12 -0
  348. package/dist/ui/Summary.d.ts.map +1 -0
  349. package/dist/ui/Summary.js +114 -0
  350. package/dist/ui/Summary.js.map +1 -0
  351. package/dist/ui/SyncUI.d.ts +10 -0
  352. package/dist/ui/SyncUI.d.ts.map +1 -0
  353. package/dist/ui/SyncUI.js +64 -0
  354. package/dist/ui/SyncUI.js.map +1 -0
  355. package/dist/ui/Welcome.d.ts +7 -0
  356. package/dist/ui/Welcome.d.ts.map +1 -0
  357. package/dist/ui/Welcome.js +45 -0
  358. package/dist/ui/Welcome.js.map +1 -0
  359. package/dist/ui/theme.d.ts +10 -0
  360. package/dist/ui/theme.d.ts.map +1 -0
  361. package/dist/ui/theme.js +9 -0
  362. package/dist/ui/theme.js.map +1 -0
  363. package/modules/engram/.gitignore-snippet.txt +6 -0
  364. package/modules/engram/.mcp-config-snippet.json +11 -0
  365. package/modules/engram/README.md +146 -0
  366. package/modules/engram/install-engram.sh +216 -0
  367. package/modules/ghagga/.env.example +43 -0
  368. package/modules/ghagga/README.md +153 -0
  369. package/modules/ghagga/docker-compose.yml +80 -0
  370. package/modules/ghagga/setup-ghagga.sh +139 -0
  371. package/modules/memory-simple/.project/NOTES.md +22 -0
  372. package/modules/memory-simple/README.md +23 -0
  373. package/modules/obsidian-brain/.obsidian/app.json +23 -0
  374. package/modules/obsidian-brain/.obsidian/appearance.json +5 -0
  375. package/modules/obsidian-brain/.obsidian/bookmarks.json +34 -0
  376. package/modules/obsidian-brain/.obsidian/community-plugins.json +1 -0
  377. package/modules/obsidian-brain/.obsidian/core-plugins-migration.json +21 -0
  378. package/modules/obsidian-brain/.obsidian/core-plugins.json +18 -0
  379. package/modules/obsidian-brain/.obsidian/daily-notes.json +5 -0
  380. package/modules/obsidian-brain/.obsidian/graph.json +37 -0
  381. package/modules/obsidian-brain/.obsidian/hotkeys.json +14 -0
  382. package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +25 -0
  383. package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +29 -0
  384. package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +18 -0
  385. package/modules/obsidian-brain/.obsidian/snippets/project-memory.css +71 -0
  386. package/modules/obsidian-brain/.obsidian-gitignore-snippet.txt +8 -0
  387. package/modules/obsidian-brain/.project/Attachments/.gitkeep +0 -0
  388. package/modules/obsidian-brain/.project/Memory/BLOCKERS.md +78 -0
  389. package/modules/obsidian-brain/.project/Memory/CONTEXT.md +102 -0
  390. package/modules/obsidian-brain/.project/Memory/DASHBOARD.md +73 -0
  391. package/modules/obsidian-brain/.project/Memory/DECISIONS.md +87 -0
  392. package/modules/obsidian-brain/.project/Memory/KANBAN.md +15 -0
  393. package/modules/obsidian-brain/.project/Memory/README.md +61 -0
  394. package/modules/obsidian-brain/.project/Memory/WAVES.md +78 -0
  395. package/modules/obsidian-brain/.project/Sessions/TEMPLATE.md +99 -0
  396. package/modules/obsidian-brain/.project/Templates/ADR.md +33 -0
  397. package/modules/obsidian-brain/.project/Templates/Blocker.md +21 -0
  398. package/modules/obsidian-brain/.project/Templates/Session.md +88 -0
  399. package/modules/obsidian-brain/README.md +268 -0
  400. package/modules/obsidian-brain/new-wave.sh +182 -0
  401. package/package.json +51 -0
  402. package/schemas/agent.schema.json +34 -0
  403. package/schemas/ai-config.schema.json +28 -0
  404. package/schemas/skill.schema.json +44 -0
  405. package/src/commands/analyze.test.ts +145 -0
  406. package/src/commands/analyze.ts +69 -0
  407. package/src/commands/doctor.test.ts +208 -0
  408. package/src/commands/doctor.ts +163 -0
  409. package/src/commands/init.test.ts +298 -0
  410. package/src/commands/init.ts +285 -0
  411. package/src/constants.ts +69 -0
  412. package/src/e2e/aggressive.e2e.test.ts +557 -0
  413. package/src/e2e/commands.e2e.test.ts +298 -0
  414. package/src/index.tsx +106 -0
  415. package/src/lib/common.test.ts +318 -0
  416. package/src/lib/common.ts +127 -0
  417. package/src/lib/frontmatter.test.ts +291 -0
  418. package/src/lib/frontmatter.ts +77 -0
  419. package/src/lib/template.test.ts +226 -0
  420. package/src/lib/template.ts +99 -0
  421. package/src/types/index.ts +53 -0
  422. package/src/ui/AnalyzeUI.tsx +133 -0
  423. package/src/ui/App.tsx +175 -0
  424. package/src/ui/CIContext.tsx +25 -0
  425. package/src/ui/CISelector.tsx +72 -0
  426. package/src/ui/Doctor.tsx +122 -0
  427. package/src/ui/Header.tsx +48 -0
  428. package/src/ui/MemorySelector.tsx +73 -0
  429. package/src/ui/NameInput.tsx +82 -0
  430. package/src/ui/OptionSelector.tsx +100 -0
  431. package/src/ui/Progress.tsx +88 -0
  432. package/src/ui/StackSelector.tsx +101 -0
  433. package/src/ui/Summary.tsx +134 -0
  434. package/src/ui/Welcome.tsx +54 -0
  435. package/src/ui/theme.ts +10 -0
  436. package/stryker.config.json +19 -0
  437. package/tasks/_TEMPLATE/files-edited.md +3 -0
  438. package/tasks/_TEMPLATE/plan.md +3 -0
  439. package/tasks/_TEMPLATE/research.md +3 -0
  440. package/tasks/_TEMPLATE/verification.md +5 -0
  441. package/templates/common/dependabot/cargo.yml +11 -0
  442. package/templates/common/dependabot/github-actions.yml +16 -0
  443. package/templates/common/dependabot/gomod.yml +15 -0
  444. package/templates/common/dependabot/gradle.yml +15 -0
  445. package/templates/common/dependabot/header.yml +3 -0
  446. package/templates/common/dependabot/maven.yml +15 -0
  447. package/templates/common/dependabot/npm.yml +20 -0
  448. package/templates/common/dependabot/pip.yml +11 -0
  449. package/templates/dependabot.yml +162 -0
  450. package/templates/github/ci-go.yml +41 -0
  451. package/templates/github/ci-java.yml +45 -0
  452. package/templates/github/ci-monorepo.yml +150 -0
  453. package/templates/github/ci-node.yml +42 -0
  454. package/templates/github/ci-python.yml +42 -0
  455. package/templates/github/ci-rust.yml +42 -0
  456. package/templates/github/dependabot-automerge.yml +40 -0
  457. package/templates/gitlab/gitlab-ci-go.yml +88 -0
  458. package/templates/gitlab/gitlab-ci-java.yml +79 -0
  459. package/templates/gitlab/gitlab-ci-monorepo.yml +126 -0
  460. package/templates/gitlab/gitlab-ci-node.yml +63 -0
  461. package/templates/gitlab/gitlab-ci-python.yml +147 -0
  462. package/templates/gitlab/gitlab-ci-rust.yml +67 -0
  463. package/templates/global/claude-settings.json +98 -0
  464. package/templates/global/codex-config.toml +8 -0
  465. package/templates/global/copilot-instructions/base-rules.instructions.md +13 -0
  466. package/templates/global/copilot-instructions/sdd-orchestrator.instructions.md +37 -0
  467. package/templates/global/gemini-commands/cleanup.toml +20 -0
  468. package/templates/global/gemini-commands/commit.toml +15 -0
  469. package/templates/global/gemini-commands/dead-code.toml +22 -0
  470. package/templates/global/gemini-commands/plan.toml +30 -0
  471. package/templates/global/gemini-commands/review.toml +17 -0
  472. package/templates/global/gemini-commands/sdd-apply.toml +22 -0
  473. package/templates/global/gemini-commands/sdd-ff.toml +14 -0
  474. package/templates/global/gemini-commands/sdd-new.toml +21 -0
  475. package/templates/global/gemini-commands/sdd-verify.toml +21 -0
  476. package/templates/global/gemini-commands/tdd.toml +26 -0
  477. package/templates/global/gemini-settings.json +8 -0
  478. package/templates/global/opencode-config.json +44 -0
  479. package/templates/global/sdd-instructions.md +47 -0
  480. package/templates/global/sdd-orchestrator-claude.md +46 -0
  481. package/templates/global/sdd-orchestrator-copilot.md +34 -0
  482. package/templates/renovate.json +69 -0
  483. package/templates/woodpecker/monorepo/backend.yml +34 -0
  484. package/templates/woodpecker/monorepo/frontend.yml +34 -0
  485. package/templates/woodpecker/monorepo/summary.yml +25 -0
  486. package/templates/woodpecker/woodpecker-go.yml +51 -0
  487. package/templates/woodpecker/woodpecker-java.yml +67 -0
  488. package/templates/woodpecker/woodpecker-node.yml +47 -0
  489. package/templates/woodpecker/woodpecker-python.yml +108 -0
  490. package/templates/woodpecker/woodpecker-rust.yml +57 -0
  491. package/tsconfig.json +19 -0
  492. package/vitest.config.ts +16 -0
  493. package/workflows/reusable-build-go.yml +111 -0
  494. package/workflows/reusable-build-java.yml +120 -0
  495. package/workflows/reusable-build-node.yml +145 -0
  496. package/workflows/reusable-build-python.yml +159 -0
  497. package/workflows/reusable-build-rust.yml +135 -0
  498. package/workflows/reusable-docker.yml +120 -0
  499. package/workflows/reusable-ghagga-review.yml +165 -0
  500. 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
+ }