@c0x12c/ai-toolkit 1.15.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 (255) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/.claude-plugin/plugin.json +12 -0
  3. package/README.md +439 -0
  4. package/VERSION +1 -0
  5. package/agents/design-critic.md +127 -0
  6. package/agents/idea-killer.md +72 -0
  7. package/agents/infrastructure-expert.md +49 -0
  8. package/agents/micronaut-backend-expert.md +45 -0
  9. package/agents/phase-reviewer.md +150 -0
  10. package/agents/research-planner.md +70 -0
  11. package/agents/solution-architect-cto.md +49 -0
  12. package/agents/sre-architect.md +49 -0
  13. package/agents/team-coordinator.md +111 -0
  14. package/bin/cli.js +780 -0
  15. package/claude-md/00-header.md +39 -0
  16. package/claude-md/01-core.md +105 -0
  17. package/claude-md/05-database.md +20 -0
  18. package/claude-md/11-backend-micronaut.md +19 -0
  19. package/claude-md/20-frontend-react.md +44 -0
  20. package/claude-md/25-ux-design.md +56 -0
  21. package/claude-md/30-infrastructure.md +24 -0
  22. package/claude-md/30-project-mgmt.md +119 -0
  23. package/claude-md/40-product.md +39 -0
  24. package/claude-md/50-ops.md +34 -0
  25. package/claude-md/60-research.md +27 -0
  26. package/claude-md/90-footer.md +21 -0
  27. package/commands/spartan/brainstorm.md +134 -0
  28. package/commands/spartan/brownfield.md +157 -0
  29. package/commands/spartan/build.md +435 -0
  30. package/commands/spartan/careful.md +94 -0
  31. package/commands/spartan/commit-message.md +112 -0
  32. package/commands/spartan/content.md +17 -0
  33. package/commands/spartan/context-save.md +161 -0
  34. package/commands/spartan/contribute.md +140 -0
  35. package/commands/spartan/daily.md +42 -0
  36. package/commands/spartan/debug.md +308 -0
  37. package/commands/spartan/deep-dive.md +55 -0
  38. package/commands/spartan/deploy.md +207 -0
  39. package/commands/spartan/e2e.md +264 -0
  40. package/commands/spartan/env-setup.md +166 -0
  41. package/commands/spartan/epic.md +199 -0
  42. package/commands/spartan/fe-review.md +181 -0
  43. package/commands/spartan/figma-to-code.md +260 -0
  44. package/commands/spartan/forensics.md +46 -0
  45. package/commands/spartan/freeze.md +84 -0
  46. package/commands/spartan/fundraise.md +53 -0
  47. package/commands/spartan/gate-review.md +229 -0
  48. package/commands/spartan/gsd-upgrade.md +376 -0
  49. package/commands/spartan/guard.md +42 -0
  50. package/commands/spartan/init-project.md +178 -0
  51. package/commands/spartan/init-rules.md +298 -0
  52. package/commands/spartan/interview.md +154 -0
  53. package/commands/spartan/kickoff.md +73 -0
  54. package/commands/spartan/kotlin-service.md +109 -0
  55. package/commands/spartan/lean-canvas.md +222 -0
  56. package/commands/spartan/lint-rules.md +122 -0
  57. package/commands/spartan/map-codebase.md +124 -0
  58. package/commands/spartan/migration.md +82 -0
  59. package/commands/spartan/next-app.md +317 -0
  60. package/commands/spartan/next-feature.md +212 -0
  61. package/commands/spartan/onboard.md +326 -0
  62. package/commands/spartan/outreach.md +16 -0
  63. package/commands/spartan/phase.md +142 -0
  64. package/commands/spartan/pitch.md +18 -0
  65. package/commands/spartan/plan.md +210 -0
  66. package/commands/spartan/pr-ready.md +202 -0
  67. package/commands/spartan/project.md +106 -0
  68. package/commands/spartan/qa.md +222 -0
  69. package/commands/spartan/research.md +254 -0
  70. package/commands/spartan/review.md +132 -0
  71. package/commands/spartan/scan-rules.md +173 -0
  72. package/commands/spartan/sessions.md +143 -0
  73. package/commands/spartan/spec.md +131 -0
  74. package/commands/spartan/startup.md +257 -0
  75. package/commands/spartan/team.md +570 -0
  76. package/commands/spartan/teardown.md +161 -0
  77. package/commands/spartan/testcontainer.md +97 -0
  78. package/commands/spartan/tf-cost.md +123 -0
  79. package/commands/spartan/tf-deploy.md +116 -0
  80. package/commands/spartan/tf-drift.md +100 -0
  81. package/commands/spartan/tf-import.md +107 -0
  82. package/commands/spartan/tf-module.md +121 -0
  83. package/commands/spartan/tf-plan.md +100 -0
  84. package/commands/spartan/tf-review.md +106 -0
  85. package/commands/spartan/tf-scaffold.md +109 -0
  86. package/commands/spartan/tf-security.md +147 -0
  87. package/commands/spartan/think.md +221 -0
  88. package/commands/spartan/unfreeze.md +13 -0
  89. package/commands/spartan/update.md +134 -0
  90. package/commands/spartan/ux.md +1233 -0
  91. package/commands/spartan/validate.md +193 -0
  92. package/commands/spartan/web-to-prd.md +706 -0
  93. package/commands/spartan/workstreams.md +109 -0
  94. package/commands/spartan/write.md +16 -0
  95. package/commands/spartan.md +386 -0
  96. package/frameworks/00-framework-comparison-guide.md +317 -0
  97. package/frameworks/01-lean-canvas.md +196 -0
  98. package/frameworks/02-design-sprint.md +304 -0
  99. package/frameworks/03-foundation-sprint.md +337 -0
  100. package/frameworks/04-business-model-canvas.md +391 -0
  101. package/frameworks/05-customer-development.md +426 -0
  102. package/frameworks/06-jobs-to-be-done.md +358 -0
  103. package/frameworks/07-mom-test.md +392 -0
  104. package/frameworks/08-value-proposition-canvas.md +488 -0
  105. package/frameworks/09-javelin-board.md +428 -0
  106. package/frameworks/10-build-measure-learn.md +467 -0
  107. package/frameworks/11-mvp-approaches.md +533 -0
  108. package/frameworks/think-before-build.md +593 -0
  109. package/lib/assembler.js +197 -0
  110. package/lib/assembler.test.js +159 -0
  111. package/lib/detector.js +166 -0
  112. package/lib/detector.test.js +221 -0
  113. package/lib/packs.js +16 -0
  114. package/lib/resolver.js +272 -0
  115. package/lib/resolver.test.js +298 -0
  116. package/lib/worktree.sh +104 -0
  117. package/package.json +50 -0
  118. package/packs/backend-micronaut.yaml +35 -0
  119. package/packs/backend-nodejs.yaml +15 -0
  120. package/packs/backend-python.yaml +15 -0
  121. package/packs/core.yaml +37 -0
  122. package/packs/database.yaml +21 -0
  123. package/packs/frontend-react.yaml +24 -0
  124. package/packs/infrastructure.yaml +40 -0
  125. package/packs/ops.yaml +16 -0
  126. package/packs/packs.compiled.json +371 -0
  127. package/packs/product.yaml +22 -0
  128. package/packs/project-mgmt.yaml +24 -0
  129. package/packs/research.yaml +39 -0
  130. package/packs/shared-backend.yaml +14 -0
  131. package/packs/ux-design.yaml +21 -0
  132. package/rules/backend-micronaut/API_DESIGN.md +313 -0
  133. package/rules/backend-micronaut/BATCH_PROCESSING.md +92 -0
  134. package/rules/backend-micronaut/CONTROLLERS.md +388 -0
  135. package/rules/backend-micronaut/KOTLIN.md +414 -0
  136. package/rules/backend-micronaut/RETROFIT_PLACEMENT.md +290 -0
  137. package/rules/backend-micronaut/SERVICES_AND_BEANS.md +325 -0
  138. package/rules/core/NAMING_CONVENTIONS.md +208 -0
  139. package/rules/core/SKILL_AUTHORING.md +174 -0
  140. package/rules/core/TIMEZONE.md +316 -0
  141. package/rules/database/ORM_AND_REPO.md +289 -0
  142. package/rules/database/SCHEMA.md +146 -0
  143. package/rules/database/TRANSACTIONS.md +311 -0
  144. package/rules/frontend-react/FRONTEND.md +344 -0
  145. package/rules/infrastructure/MODULES.md +260 -0
  146. package/rules/infrastructure/NAMING.md +196 -0
  147. package/rules/infrastructure/PROVIDERS.md +309 -0
  148. package/rules/infrastructure/SECURITY.md +310 -0
  149. package/rules/infrastructure/STATE_AND_BACKEND.md +237 -0
  150. package/rules/infrastructure/STRUCTURE.md +234 -0
  151. package/rules/infrastructure/VARIABLES.md +285 -0
  152. package/rules/shared-backend/ARCHITECTURE.md +46 -0
  153. package/rules/ux-design/DESIGN_PROCESS.md +176 -0
  154. package/skills/api-endpoint-creator/SKILL.md +455 -0
  155. package/skills/api-endpoint-creator/error-handling-guide.md +244 -0
  156. package/skills/api-endpoint-creator/examples.md +522 -0
  157. package/skills/api-endpoint-creator/testing-patterns.md +302 -0
  158. package/skills/article-writing/SKILL.md +109 -0
  159. package/skills/article-writing/examples.md +59 -0
  160. package/skills/backend-api-design/SKILL.md +84 -0
  161. package/skills/backend-api-design/code-patterns.md +138 -0
  162. package/skills/brainstorm/SKILL.md +95 -0
  163. package/skills/browser-qa/SKILL.md +87 -0
  164. package/skills/browser-qa/playwright-snippets.md +110 -0
  165. package/skills/ci-cd-patterns/SKILL.md +108 -0
  166. package/skills/ci-cd-patterns/workflows.md +149 -0
  167. package/skills/competitive-teardown/SKILL.md +93 -0
  168. package/skills/competitive-teardown/example-analysis.md +50 -0
  169. package/skills/content-engine/SKILL.md +131 -0
  170. package/skills/content-engine/examples.md +72 -0
  171. package/skills/database-patterns/SKILL.md +72 -0
  172. package/skills/database-patterns/code-templates.md +114 -0
  173. package/skills/database-table-creator/SKILL.md +141 -0
  174. package/skills/database-table-creator/examples.md +552 -0
  175. package/skills/database-table-creator/kotlin-templates.md +400 -0
  176. package/skills/database-table-creator/migration-template.sql +68 -0
  177. package/skills/database-table-creator/validation-checklist.md +337 -0
  178. package/skills/deep-research/SKILL.md +80 -0
  179. package/skills/design-intelligence/SKILL.md +268 -0
  180. package/skills/design-workflow/SKILL.md +127 -0
  181. package/skills/design-workflow/checklists.md +45 -0
  182. package/skills/idea-validation/SKILL.md +129 -0
  183. package/skills/idea-validation/example-report.md +50 -0
  184. package/skills/investor-materials/SKILL.md +122 -0
  185. package/skills/investor-materials/example-outline.md +70 -0
  186. package/skills/investor-outreach/SKILL.md +112 -0
  187. package/skills/investor-outreach/examples.md +76 -0
  188. package/skills/kotlin-best-practices/SKILL.md +58 -0
  189. package/skills/kotlin-best-practices/code-patterns.md +132 -0
  190. package/skills/market-research/SKILL.md +99 -0
  191. package/skills/security-checklist/SKILL.md +65 -0
  192. package/skills/security-checklist/audit-reference.md +95 -0
  193. package/skills/service-debugging/SKILL.md +116 -0
  194. package/skills/service-debugging/common-issues.md +65 -0
  195. package/skills/startup-pipeline/SKILL.md +152 -0
  196. package/skills/terraform-best-practices/SKILL.md +244 -0
  197. package/skills/terraform-module-creator/SKILL.md +284 -0
  198. package/skills/terraform-review/SKILL.md +222 -0
  199. package/skills/terraform-security-audit/SKILL.md +280 -0
  200. package/skills/terraform-service-scaffold/SKILL.md +574 -0
  201. package/skills/testing-strategies/SKILL.md +116 -0
  202. package/skills/testing-strategies/examples.md +103 -0
  203. package/skills/testing-strategies/integration-test-setup.md +71 -0
  204. package/skills/ui-ux-pro-max/SKILL.md +238 -0
  205. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  206. package/skills/ui-ux-pro-max/data/colors.csv +97 -0
  207. package/skills/ui-ux-pro-max/data/icons.csv +101 -0
  208. package/skills/ui-ux-pro-max/data/landing.csv +31 -0
  209. package/skills/ui-ux-pro-max/data/products.csv +97 -0
  210. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  211. package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  212. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  213. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  214. package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  215. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  216. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  217. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  218. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  219. package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  220. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  221. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  222. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  223. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  224. package/skills/ui-ux-pro-max/data/styles.csv +68 -0
  225. package/skills/ui-ux-pro-max/data/typography.csv +58 -0
  226. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  227. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  228. package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  229. package/skills/ui-ux-pro-max/python-setup.md +146 -0
  230. package/skills/ui-ux-pro-max/scripts/core.py +253 -0
  231. package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  232. package/skills/ui-ux-pro-max/scripts/search.py +114 -0
  233. package/skills/web-to-prd/SKILL.md +478 -0
  234. package/templates/build-config.yaml +44 -0
  235. package/templates/commands-config.yaml +55 -0
  236. package/templates/competitor-analysis.md +60 -0
  237. package/templates/content/AGENT_TEMPLATE.md +47 -0
  238. package/templates/content/COMMAND_TEMPLATE.md +27 -0
  239. package/templates/content/RULE_TEMPLATE.md +40 -0
  240. package/templates/content/SKILL_TEMPLATE.md +41 -0
  241. package/templates/design-config.md +105 -0
  242. package/templates/design-doc.md +207 -0
  243. package/templates/epic.md +100 -0
  244. package/templates/feature-spec.md +181 -0
  245. package/templates/idea-canvas.md +47 -0
  246. package/templates/implementation-plan.md +159 -0
  247. package/templates/prd-template.md +86 -0
  248. package/templates/preamble.md +89 -0
  249. package/templates/project-readme.md +35 -0
  250. package/templates/quality-gates.md +230 -0
  251. package/templates/spartan-config.yaml +164 -0
  252. package/templates/user-interview.md +69 -0
  253. package/templates/validation-checklist.md +108 -0
  254. package/templates/workflow-backend-micronaut.md +409 -0
  255. package/templates/workflow-frontend-react.md +233 -0
@@ -0,0 +1,197 @@
1
+ // Spartan AI Toolkit — Assembler
2
+ // Builds CLAUDE.md and AGENTS.md from section files based on selected packs.
3
+
4
+ import { readFileSync, existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ const ALWAYS_INCLUDE_TOP = ['00-header.md', '01-core.md'];
8
+ const ALWAYS_INCLUDE_BOTTOM = ['90-footer.md'];
9
+
10
+ /**
11
+ * Assemble CLAUDE.md from section files.
12
+ * @param {string} claudeMdDir - Path to the claude-md/ directory
13
+ * @param {string[]} selectedPacks - Pack names the user picked
14
+ * @param {object} packDefs - The PACKS object from packs.js
15
+ * @returns {string} The full CLAUDE.md content
16
+ */
17
+ export function assembleCLAUDEmd(claudeMdDir, selectedPacks, packDefs) {
18
+ let parts = [];
19
+
20
+ // Always include header + core
21
+ for (const file of ALWAYS_INCLUDE_TOP) {
22
+ const fp = join(claudeMdDir, file);
23
+ if (existsSync(fp)) {
24
+ parts.push(readFileSync(fp, 'utf-8'));
25
+ }
26
+ }
27
+
28
+ // Pack-specific sections (in pack order, not random)
29
+ const seen = new Set();
30
+ for (const pack of selectedPacks) {
31
+ const def = packDefs[pack];
32
+ if (!def) continue;
33
+ for (const section of def.claudeSections) {
34
+ if (seen.has(section)) continue;
35
+ seen.add(section);
36
+ const fp = join(claudeMdDir, section);
37
+ if (existsSync(fp)) {
38
+ parts.push(readFileSync(fp, 'utf-8'));
39
+ }
40
+ }
41
+ }
42
+
43
+ // Always include footer
44
+ for (const file of ALWAYS_INCLUDE_BOTTOM) {
45
+ const fp = join(claudeMdDir, file);
46
+ if (existsSync(fp)) {
47
+ parts.push(readFileSync(fp, 'utf-8'));
48
+ }
49
+ }
50
+
51
+ return parts.join('\n');
52
+ }
53
+
54
+ /**
55
+ * Assemble AGENTS.md from section files + agent definitions.
56
+ * Follows the AGENTS.md spec: Project Context, pack content, Agents, Boundaries.
57
+ * @param {string} claudeMdDir - Path to the claude-md/ directory
58
+ * @param {string} agentsDir - Path to the agents/ directory
59
+ * @param {string[]} selectedPacks - Pack names the user picked
60
+ * @param {object} packDefs - The PACKS object from packs.js
61
+ * @returns {string} The full AGENTS.md content
62
+ */
63
+ export function assembleAGENTSmd(claudeMdDir, agentsDir, selectedPacks, packDefs) {
64
+ const parts = [];
65
+
66
+ // Header
67
+ parts.push('# AGENTS.md');
68
+ parts.push('');
69
+ parts.push('> Generated by [Spartan AI Toolkit](https://github.com/c0x12c/ai-toolkit)');
70
+ parts.push('');
71
+
72
+ // Project Context — pull from header if it exists
73
+ parts.push('## Project Context');
74
+ parts.push('');
75
+ const headerFile = join(claudeMdDir, '00-header.md');
76
+ if (existsSync(headerFile)) {
77
+ const header = readFileSync(headerFile, 'utf-8');
78
+ // Strip the top-level heading, keep the useful content
79
+ const lines = header.split('\n');
80
+ const contentLines = lines.filter(l => !l.startsWith('# '));
81
+ const trimmed = contentLines.join('\n').trim();
82
+ if (trimmed) {
83
+ parts.push(trimmed);
84
+ parts.push('');
85
+ }
86
+ }
87
+
88
+ // Core principles — compact version
89
+ const coreFile = join(claudeMdDir, '01-core.md');
90
+ if (existsSync(coreFile)) {
91
+ const core = readFileSync(coreFile, 'utf-8');
92
+ parts.push(extractSection(core));
93
+ parts.push('');
94
+ }
95
+
96
+ // Pack-specific sections
97
+ const seen = new Set();
98
+ for (const pack of selectedPacks) {
99
+ const def = packDefs[pack];
100
+ if (!def) continue;
101
+ for (const section of def.claudeSections) {
102
+ if (seen.has(section)) continue;
103
+ seen.add(section);
104
+ const fp = join(claudeMdDir, section);
105
+ if (existsSync(fp)) {
106
+ parts.push(extractSection(readFileSync(fp, 'utf-8')));
107
+ parts.push('');
108
+ }
109
+ }
110
+ }
111
+
112
+ // Agents — list available agents from selected packs
113
+ const agentFiles = gatherAgents(selectedPacks, packDefs);
114
+ if (agentFiles.length > 0 && agentsDir) {
115
+ parts.push('## Available Agents');
116
+ parts.push('');
117
+ for (const agentFile of agentFiles) {
118
+ const fp = join(agentsDir, agentFile);
119
+ if (!existsSync(fp)) continue;
120
+ const content = readFileSync(fp, 'utf-8');
121
+ const name = extractFrontmatterField(content, 'name') || agentFile.replace('.md', '');
122
+ const desc = extractFrontmatterField(content, 'description') || '';
123
+ // Handle multiline descriptions — get first meaningful sentence
124
+ const cleanDesc = desc.replace(/\\n/g, ' ').split('.')[0].trim();
125
+ parts.push(`- **${name}**: ${cleanDesc}`);
126
+ }
127
+ parts.push('');
128
+ }
129
+
130
+ // Boundaries — from footer "What NOT to Do" section
131
+ const footerFile = join(claudeMdDir, '90-footer.md');
132
+ if (existsSync(footerFile)) {
133
+ const footer = readFileSync(footerFile, 'utf-8');
134
+ const neverItems = extractNotToDoItems(footer);
135
+ if (neverItems.length > 0) {
136
+ parts.push('## Boundaries');
137
+ parts.push('');
138
+ parts.push('**NEVER:**');
139
+ for (const item of neverItems) {
140
+ parts.push(`- ${item}`);
141
+ }
142
+ parts.push('');
143
+ }
144
+ }
145
+
146
+ return parts.join('\n').trimEnd() + '\n';
147
+ }
148
+
149
+ // ── Helpers ──────────────────────────────────────────────────────
150
+
151
+ /** Strip leading separator (---) and return the content. */
152
+ function extractSection(content) {
153
+ return content.replace(/^\s*---\s*\n?/, '').trim();
154
+ }
155
+
156
+ /** Get deduplicated agent files from selected packs. */
157
+ function gatherAgents(selectedPacks, packDefs) {
158
+ const seen = new Set();
159
+ const result = [];
160
+ for (const pack of selectedPacks) {
161
+ const def = packDefs[pack];
162
+ if (!def) continue;
163
+ for (const agent of (def.agents || [])) {
164
+ if (!seen.has(agent)) {
165
+ seen.add(agent);
166
+ result.push(agent);
167
+ }
168
+ }
169
+ }
170
+ return result;
171
+ }
172
+
173
+ /** Extract a YAML frontmatter field value (simple single-line). */
174
+ function extractFrontmatterField(content, field) {
175
+ const match = content.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
176
+ return match ? match[1].trim().replace(/^["']|["']$/g, '') : null;
177
+ }
178
+
179
+ /** Extract items from the "What NOT to Do" section only. */
180
+ function extractNotToDoItems(content) {
181
+ const lines = content.split('\n');
182
+ let inSection = false;
183
+ const items = [];
184
+ for (const line of lines) {
185
+ if (line.match(/what not to do/i)) {
186
+ inSection = true;
187
+ continue;
188
+ }
189
+ if (inSection && line.match(/^#{1,3}\s/)) {
190
+ break; // hit next section
191
+ }
192
+ if (inSection && line.match(/^-\s+/)) {
193
+ items.push(line.replace(/^-\s+/, '').trim());
194
+ }
195
+ }
196
+ return items;
197
+ }
@@ -0,0 +1,159 @@
1
+ // Spartan AI Toolkit — Assembler Tests
2
+ // Run: node --test toolkit/lib/assembler.test.js
3
+
4
+ import { describe, it } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { assembleCLAUDEmd, assembleAGENTSmd } from './assembler.js';
9
+
10
+ // ── Test fixtures ────────────────────────────────────────────────
11
+ const TMP = join(import.meta.dirname, '..', '.test-tmp-assembler');
12
+
13
+ function setupFixtures() {
14
+ rmSync(TMP, { recursive: true, force: true });
15
+
16
+ // claude-md sections
17
+ const claudeMd = join(TMP, 'claude-md');
18
+ mkdirSync(claudeMd, { recursive: true });
19
+ writeFileSync(join(claudeMd, '00-header.md'), '# Spartan AI Toolkit\n\nIntro text here.');
20
+ writeFileSync(join(claudeMd, '01-core.md'), '\n---\n\n## Core Principles\n\n- TDD always\n- Spec before code');
21
+ writeFileSync(join(claudeMd, '11-backend.md'), '\n---\n\n## Backend\n\nKotlin + Micronaut stack.\n\n| Command | Purpose |\n|---|---|\n| `/spartan:build` | Build feature |');
22
+ writeFileSync(join(claudeMd, '20-frontend.md'), '\n---\n\n## Frontend\n\nReact + Next.js stack.');
23
+ writeFileSync(join(claudeMd, '90-footer.md'), '\n---\n\n## What NOT to Do\n\n- Don\'t skip tests');
24
+
25
+ // agents
26
+ const agents = join(TMP, 'agents');
27
+ mkdirSync(agents, { recursive: true });
28
+ writeFileSync(join(agents, 'backend-expert.md'), '---\nname: backend-expert\ndescription: Backend expert\nmodel: sonnet\n---\n\nYou are a backend expert.');
29
+ writeFileSync(join(agents, 'cto.md'), '---\nname: cto\ndescription: CTO advisor\nmodel: sonnet\n---\n\nYou are a CTO.');
30
+
31
+ return { claudeMd, agents };
32
+ }
33
+
34
+ function cleanup() {
35
+ rmSync(TMP, { recursive: true, force: true });
36
+ }
37
+
38
+ // Mock pack definitions
39
+ const packDefs = {
40
+ core: { claudeSections: [], rules: [], skills: [], agents: [], commands: [] },
41
+ backend: {
42
+ claudeSections: ['11-backend.md'],
43
+ rules: ['backend/KOTLIN.md'],
44
+ skills: ['api-endpoint-creator'],
45
+ agents: ['backend-expert.md'],
46
+ commands: ['build'],
47
+ },
48
+ frontend: {
49
+ claudeSections: ['20-frontend.md'],
50
+ rules: ['frontend/FRONTEND.md'],
51
+ skills: ['ui-ux-pro-max'],
52
+ agents: [],
53
+ commands: ['next-app'],
54
+ },
55
+ };
56
+
57
+ // ── Tests ────────────────────────────────────────────────────────
58
+
59
+ describe('assembleCLAUDEmd', () => {
60
+ it('includes header, core, pack sections, and footer', () => {
61
+ const { claudeMd } = setupFixtures();
62
+ try {
63
+ const result = assembleCLAUDEmd(claudeMd, ['backend'], packDefs);
64
+ assert.ok(result.includes('# Spartan AI Toolkit'), 'should have header');
65
+ assert.ok(result.includes('Core Principles'), 'should have core');
66
+ assert.ok(result.includes('Backend'), 'should have backend section');
67
+ assert.ok(result.includes('What NOT to Do'), 'should have footer');
68
+ assert.ok(!result.includes('Frontend'), 'should NOT have frontend');
69
+ } finally {
70
+ cleanup();
71
+ }
72
+ });
73
+ });
74
+
75
+ describe('assembleAGENTSmd', () => {
76
+ it('returns a string with AGENTS.md header', () => {
77
+ const { claudeMd, agents } = setupFixtures();
78
+ try {
79
+ const result = assembleAGENTSmd(claudeMd, agents, ['backend'], packDefs);
80
+ assert.ok(typeof result === 'string');
81
+ assert.ok(result.includes('# AGENTS.md'), 'should have AGENTS.md header');
82
+ } finally {
83
+ cleanup();
84
+ }
85
+ });
86
+
87
+ it('includes project context section', () => {
88
+ const { claudeMd, agents } = setupFixtures();
89
+ try {
90
+ const result = assembleAGENTSmd(claudeMd, agents, ['backend'], packDefs);
91
+ assert.ok(result.includes('## Project Context'), 'should have project context');
92
+ assert.ok(result.includes('Spartan AI Toolkit'), 'should mention toolkit name');
93
+ } finally {
94
+ cleanup();
95
+ }
96
+ });
97
+
98
+ it('includes pack-specific content', () => {
99
+ const { claudeMd, agents } = setupFixtures();
100
+ try {
101
+ const result = assembleAGENTSmd(claudeMd, agents, ['backend'], packDefs);
102
+ assert.ok(result.includes('Backend') || result.includes('backend'), 'should have backend content');
103
+ } finally {
104
+ cleanup();
105
+ }
106
+ });
107
+
108
+ it('does NOT include packs the user did not pick', () => {
109
+ const { claudeMd, agents } = setupFixtures();
110
+ try {
111
+ const result = assembleAGENTSmd(claudeMd, agents, ['backend'], packDefs);
112
+ assert.ok(!result.includes('## Frontend'), 'should NOT have frontend section');
113
+ } finally {
114
+ cleanup();
115
+ }
116
+ });
117
+
118
+ it('includes agents section when agents exist', () => {
119
+ const { claudeMd, agents } = setupFixtures();
120
+ try {
121
+ const result = assembleAGENTSmd(claudeMd, agents, ['backend'], packDefs);
122
+ assert.ok(result.includes('backend-expert') || result.includes('Agents'), 'should have agents');
123
+ } finally {
124
+ cleanup();
125
+ }
126
+ });
127
+
128
+ it('includes boundaries section', () => {
129
+ const { claudeMd, agents } = setupFixtures();
130
+ try {
131
+ const result = assembleAGENTSmd(claudeMd, agents, ['backend'], packDefs);
132
+ assert.ok(result.includes('## Boundaries') || result.includes('NEVER'), 'should have boundaries');
133
+ } finally {
134
+ cleanup();
135
+ }
136
+ });
137
+
138
+ it('handles multiple packs', () => {
139
+ const { claudeMd, agents } = setupFixtures();
140
+ try {
141
+ const result = assembleAGENTSmd(claudeMd, agents, ['backend', 'frontend'], packDefs);
142
+ assert.ok(result.includes('Backend') || result.includes('backend'), 'should have backend');
143
+ assert.ok(result.includes('Frontend') || result.includes('frontend'), 'should have frontend');
144
+ } finally {
145
+ cleanup();
146
+ }
147
+ });
148
+
149
+ it('handles empty packs gracefully', () => {
150
+ const { claudeMd, agents } = setupFixtures();
151
+ try {
152
+ const result = assembleAGENTSmd(claudeMd, agents, ['core'], packDefs);
153
+ assert.ok(typeof result === 'string');
154
+ assert.ok(result.includes('# AGENTS.md'));
155
+ } finally {
156
+ cleanup();
157
+ }
158
+ });
159
+ });
@@ -0,0 +1,166 @@
1
+ // Spartan AI Toolkit — Stack Detector
2
+ // Scans a project directory to detect tech stacks and map them to packs.
3
+
4
+ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ // Packs that are "coming soon" — detect but don't auto-select
8
+ const COMING_SOON = new Set(['backend-nodejs', 'backend-python']);
9
+
10
+ // Skip these directories when scanning 1 level deep
11
+ const SKIP_DIRS = new Set([
12
+ 'node_modules', '.git', '.gradle', 'build', 'dist', 'target',
13
+ '.next', '.nuxt', '__pycache__', '.venv', 'venv', '.idea',
14
+ ]);
15
+
16
+ /**
17
+ * Detect tech stacks in a project directory.
18
+ * Scans root + 1 level deep for stack markers.
19
+ * @param {string} cwd - Directory to scan
20
+ * @returns {{ detected: Array<{pack: string, reason: string}>, comingSoon: Array<{pack: string, reason: string}> }}
21
+ */
22
+ export function detectStacks(cwd) {
23
+ const detected = [];
24
+ const comingSoon = [];
25
+ const seen = new Set();
26
+
27
+ // Scan root
28
+ scanDir(cwd, '', detected, comingSoon, seen);
29
+
30
+ // Scan 1 level deep
31
+ try {
32
+ const entries = readdirSync(cwd);
33
+ for (const entry of entries) {
34
+ if (SKIP_DIRS.has(entry) || entry.startsWith('.')) continue;
35
+ const full = join(cwd, entry);
36
+ try {
37
+ if (statSync(full).isDirectory()) {
38
+ scanDir(full, entry, detected, comingSoon, seen);
39
+ }
40
+ } catch { /* ignore permission errors */ }
41
+ }
42
+ } catch { /* ignore if cwd can't be read */ }
43
+
44
+ return { detected, comingSoon };
45
+ }
46
+
47
+ function scanDir(dir, label, detected, comingSoon, seen) {
48
+ // ── Kotlin + Micronaut ───────────────────────────────────────
49
+ if (!seen.has('backend-micronaut')) {
50
+ const gradleFile = join(dir, 'build.gradle.kts');
51
+ const pomFile = join(dir, 'pom.xml');
52
+ const mnCliFile = join(dir, 'micronaut-cli.yml');
53
+
54
+ if (existsSync(gradleFile)) {
55
+ const content = readSafe(gradleFile);
56
+ if (content.includes('io.micronaut')) {
57
+ addResult('backend-micronaut', `build.gradle.kts${label ? ` in ${label}/` : ''} → Micronaut`, detected, comingSoon, seen);
58
+ }
59
+ }
60
+ if (!seen.has('backend-micronaut') && existsSync(pomFile)) {
61
+ const content = readSafe(pomFile);
62
+ if (content.includes('io.micronaut') || content.includes('micronaut-')) {
63
+ addResult('backend-micronaut', `pom.xml${label ? ` in ${label}/` : ''} → Micronaut`, detected, comingSoon, seen);
64
+ }
65
+ }
66
+ if (!seen.has('backend-micronaut') && existsSync(mnCliFile)) {
67
+ addResult('backend-micronaut', `micronaut-cli.yml${label ? ` in ${label}/` : ''}`, detected, comingSoon, seen);
68
+ }
69
+ }
70
+
71
+ // ── React / Next.js ──────────────────────────────────────────
72
+ if (!seen.has('frontend-react')) {
73
+ // Check next.config.* files first (high confidence)
74
+ for (const cfg of ['next.config.js', 'next.config.ts', 'next.config.mjs']) {
75
+ if (existsSync(join(dir, cfg))) {
76
+ addResult('frontend-react', `${cfg}${label ? ` in ${label}/` : ''} → Next.js`, detected, comingSoon, seen);
77
+ break;
78
+ }
79
+ }
80
+
81
+ // Check package.json for next or react
82
+ if (!seen.has('frontend-react')) {
83
+ const pkg = readPackageJson(dir);
84
+ if (pkg) {
85
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
86
+ if (allDeps.next) {
87
+ addResult('frontend-react', `package.json${label ? ` in ${label}/` : ''} → Next.js`, detected, comingSoon, seen);
88
+ } else if (allDeps.react) {
89
+ addResult('frontend-react', `package.json${label ? ` in ${label}/` : ''} → React`, detected, comingSoon, seen);
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ // ── Node.js Backend ──────────────────────────────────────────
96
+ // Only check if frontend-react was NOT found in this same package.json
97
+ if (!seen.has('backend-nodejs') && !seen.has('frontend-react-from-pkg-' + dir)) {
98
+ const pkg = readPackageJson(dir);
99
+ if (pkg) {
100
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
101
+ // Skip if this package has next or react (it's frontend, not backend)
102
+ if (!allDeps.next && !allDeps.react) {
103
+ const nodeBackendFrameworks = ['express', 'fastify', '@nestjs/core', 'koa', '@hapi/hapi', 'hapi'];
104
+ const found = nodeBackendFrameworks.find(f => allDeps[f]);
105
+ if (found) {
106
+ addResult('backend-nodejs', `package.json${label ? ` in ${label}/` : ''} → ${found}`, detected, comingSoon, seen);
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ // ── Python Backend ───────────────────────────────────────────
113
+ if (!seen.has('backend-python')) {
114
+ const pythonFrameworks = ['fastapi', 'django', 'flask', 'starlette'];
115
+
116
+ // manage.py → Django
117
+ if (existsSync(join(dir, 'manage.py'))) {
118
+ addResult('backend-python', `manage.py${label ? ` in ${label}/` : ''} → Django`, detected, comingSoon, seen);
119
+ }
120
+
121
+ // pyproject.toml
122
+ if (!seen.has('backend-python') && existsSync(join(dir, 'pyproject.toml'))) {
123
+ const content = readSafe(join(dir, 'pyproject.toml')).toLowerCase();
124
+ const found = pythonFrameworks.find(f => content.includes(f));
125
+ if (found) {
126
+ addResult('backend-python', `pyproject.toml${label ? ` in ${label}/` : ''} → ${found}`, detected, comingSoon, seen);
127
+ }
128
+ }
129
+
130
+ // requirements.txt
131
+ if (!seen.has('backend-python') && existsSync(join(dir, 'requirements.txt'))) {
132
+ const content = readSafe(join(dir, 'requirements.txt')).toLowerCase();
133
+ const found = pythonFrameworks.find(f => content.includes(f));
134
+ if (found) {
135
+ addResult('backend-python', `requirements.txt${label ? ` in ${label}/` : ''} → ${found}`, detected, comingSoon, seen);
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ function addResult(pack, reason, detected, comingSoon, seen) {
142
+ seen.add(pack);
143
+ if (COMING_SOON.has(pack)) {
144
+ comingSoon.push({ pack, reason });
145
+ } else {
146
+ detected.push({ pack, reason });
147
+ }
148
+ }
149
+
150
+ function readSafe(filePath) {
151
+ try {
152
+ return readFileSync(filePath, 'utf-8');
153
+ } catch {
154
+ return '';
155
+ }
156
+ }
157
+
158
+ function readPackageJson(dir) {
159
+ const pkgPath = join(dir, 'package.json');
160
+ if (!existsSync(pkgPath)) return null;
161
+ try {
162
+ return JSON.parse(readFileSync(pkgPath, 'utf-8'));
163
+ } catch {
164
+ return null;
165
+ }
166
+ }