@polymorphism-tech/morph-spec 7.9.0 → 7.9.1

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 (167) hide show
  1. package/README.md +2 -2
  2. package/bin/validate.js +379 -379
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +5 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/commands/commit.md +166 -166
  7. package/framework/commands/morph-infra.md +215 -215
  8. package/framework/commands/morph-preflight.md +205 -205
  9. package/framework/commands/morph-troubleshoot.md +124 -124
  10. package/framework/hooks/claude-code/statusline.py +679 -679
  11. package/framework/hooks/claude-code/statusline.sh +11 -11
  12. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  13. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  14. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  15. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  16. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  17. package/framework/hooks/shared/activity-logger.js +160 -160
  18. package/framework/hooks/shared/hook-response.js +45 -45
  19. package/framework/hooks/shared/payload-utils.js +78 -78
  20. package/framework/hooks/shared/stdin-reader.js +26 -26
  21. package/framework/rubrics/backend.json +41 -41
  22. package/framework/rules/ai-agents-standards.md +45 -45
  23. package/framework/rules/dotnet-templates.md +86 -86
  24. package/framework/rules/frontend-standards.md +13 -13
  25. package/framework/rules/infrastructure-standards.md +27 -27
  26. package/framework/rules/neon-standards.md +25 -25
  27. package/framework/rules/nextjs-standards.md +20 -20
  28. package/framework/rules/nextjs-templates.md +40 -40
  29. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  30. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  31. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  32. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  33. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  34. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  35. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  36. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  37. package/framework/skills/core/morph-review/SKILL.md +1 -1
  38. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  39. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  40. package/framework/standards/ai-agents/agent-session.md +160 -160
  41. package/framework/standards/ai-agents/batching.md +193 -193
  42. package/framework/standards/ai-agents/context-providers.md +188 -188
  43. package/framework/standards/ai-agents/mcp-server.md +250 -250
  44. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  45. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  46. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  47. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  48. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  49. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  50. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  51. package/framework/standards/ai-agents/production.md +275 -275
  52. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  53. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  54. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  55. package/framework/standards/ai-agents/structured-output.md +249 -249
  56. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  57. package/framework/templates/IDE-SNIPPETS.md +262 -262
  58. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  59. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  60. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  61. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  62. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  63. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  64. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  65. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  66. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  67. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  68. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  69. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  70. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  71. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  72. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  73. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  74. package/framework/templates/code/typescript/contracts.ts +167 -167
  75. package/framework/templates/docs/clarifications.md +253 -253
  76. package/framework/templates/docs/schema-analysis.md +119 -119
  77. package/framework/templates/docs/ui-components.md +52 -52
  78. package/framework/templates/docs/ui-design-system.md +76 -76
  79. package/framework/templates/docs/ui-flows.md +167 -167
  80. package/framework/templates/docs/ui-mockups.md +98 -98
  81. package/framework/templates/feature/decisions.md +122 -122
  82. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  83. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  84. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  85. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  86. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  87. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  88. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  89. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  90. package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
  91. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  92. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  93. package/framework/templates/infrastructure/github/README.md +56 -56
  94. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  95. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  96. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  97. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  98. package/framework/templates/integrations/asaas-client.cs +387 -387
  99. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  100. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  101. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  102. package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -113
  103. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  104. package/framework/templates/project/validate.js +120 -120
  105. package/framework/templates/state.template.json +17 -17
  106. package/framework/templates/ui/design-system.css +226 -226
  107. package/package.json +2 -1
  108. package/src/commands/dashboard/dashboard.js +53 -53
  109. package/src/commands/index.js +13 -13
  110. package/src/commands/mcp/mcp-setup.js +519 -519
  111. package/src/commands/project/checkpoint.js +209 -209
  112. package/src/commands/project/update.js +4 -1
  113. package/src/core/state/phase-state-machine.js +197 -197
  114. package/src/core/templates/template-renderer.js +477 -477
  115. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  116. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  117. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  118. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  119. package/src/dashboard/app/main.jsx +13 -13
  120. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  121. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  122. package/src/dashboard/app/pages/LiveState.jsx +239 -239
  123. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  124. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  125. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  126. package/src/dashboard/app/pages/Standards.jsx +332 -332
  127. package/src/dashboard/app/styles/index.css +47 -47
  128. package/src/dashboard/index.html +12 -12
  129. package/src/dashboard/package.json +24 -24
  130. package/src/dashboard/scanner/hooks-scanner.js +41 -41
  131. package/src/dashboard/scanner/phases-scanner.js +15 -15
  132. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  133. package/src/dashboard/scanner/rules-scanner.js +37 -37
  134. package/src/dashboard/scanner/skills-scanner.js +43 -43
  135. package/src/dashboard/scanner/squads-scanner.js +13 -13
  136. package/src/dashboard/scanner/standards-scanner.js +26 -26
  137. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  138. package/src/dashboard/vite.config.js +24 -24
  139. package/src/lib/agents/metrics-aggregator.js +56 -56
  140. package/src/lib/agents/metrics-calculator.js +125 -125
  141. package/src/lib/agents/output-collector.js +73 -73
  142. package/src/lib/detectors/claude-config-detector.js +152 -152
  143. package/src/lib/detectors/design-system-detector.js +231 -231
  144. package/src/lib/detectors/index.js +217 -217
  145. package/src/lib/generators/architecture-scanner.js +639 -639
  146. package/src/lib/generators/design-system-generator.js +508 -508
  147. package/src/lib/generators/index.js +13 -13
  148. package/src/lib/generators/settings-generator.js +209 -209
  149. package/src/lib/hooks/hook-executor.js +167 -167
  150. package/src/lib/installers/mcp-installer.js +719 -719
  151. package/src/lib/tasks/task-classifier.js +30 -30
  152. package/src/lib/tasks/task-parser.js +209 -209
  153. package/src/lib/tasks/test-runner.js +81 -81
  154. package/src/lib/validators/architecture/architecture-validator.js +259 -259
  155. package/src/lib/validators/checkpoint-persistence.js +39 -39
  156. package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
  157. package/src/lib/validators/css/css-validator.js +340 -340
  158. package/src/lib/validators/packages/package-validator.js +347 -347
  159. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  160. package/src/lib/validators/shared/issue-counter.js +14 -14
  161. package/src/lib/validators/validation-runner.js +349 -349
  162. package/src/scripts/setup-infra.js +385 -385
  163. package/src/utils/agent-id-mapper.js +11 -11
  164. package/src/utils/claude-settings-manager.js +300 -300
  165. package/src/utils/skills-installer.js +174 -174
  166. package/framework/templates/.idea/morph-templates.xml +0 -92
  167. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -1,174 +1,174 @@
1
- /**
2
- * Skills Installer
3
- *
4
- * Copies framework skills to .claude/skills/ during morph-spec init.
5
- * Installs only the `core/` skill set — the canonical NLH skills referenced by
6
- * MORPH.md (morph-brainstorming, morph-stack-scan, morph-plan, morph-implement,
7
- * morph-eval, morph-review, morph-standards). The 3-tier system was collapsed
8
- * into a single source of truth in framework/skills/core/.
9
- *
10
- * Source skills are directory-based: framework/skills/core/{skill-name}/SKILL.md
11
- * (plus optional scripts/, references/, assets/ subdirectories).
12
- *
13
- * Directory-based skills have their entire directory copied so Claude Code can
14
- * execute scripts and load references on demand.
15
- */
16
-
17
- import { mkdirSync, copyFileSync, existsSync, readdirSync, statSync, readFileSync, rmSync } from 'fs';
18
- import { join } from 'path';
19
- import { fileURLToPath } from 'url';
20
- import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
21
-
22
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
23
- const FRAMEWORK_SKILLS_DIR = join(__dirname, '..', '..', 'framework', 'skills');
24
-
25
- /**
26
- * Skill levels to install in .claude/skills/.
27
- * Only `core/` survives — single source of truth aligned with MORPH.md.
28
- */
29
- const SKILL_LEVELS_TO_INSTALL = ['core'];
30
-
31
- /**
32
- * Check if a skill should be installed based on its stacks: frontmatter.
33
- * @param {string} skillContent - Raw SKILL.md content
34
- * @param {string[]} projectTags - Stack tags from config
35
- * @returns {boolean}
36
- */
37
- export function shouldInstallSkill(skillContent, projectTags) {
38
- return shouldInstall(parseStacks(skillContent), projectTags);
39
- }
40
-
41
- /**
42
- * Recursively copy a directory tree from src to dest.
43
- * Creates dest if it doesn't exist. Skips README.md files.
44
- *
45
- * @param {string} src - Source directory path
46
- * @param {string} dest - Destination directory path
47
- */
48
- function copyDirectory(src, dest) {
49
- mkdirSync(dest, { recursive: true });
50
- for (const entry of readdirSync(src)) {
51
- if (entry === 'README.md') continue;
52
- const srcPath = join(src, entry);
53
- const destPath = join(dest, entry);
54
- if (statSync(srcPath).isDirectory()) {
55
- copyDirectory(srcPath, destPath);
56
- } else {
57
- copyFileSync(srcPath, destPath);
58
- }
59
- }
60
- }
61
-
62
- /**
63
- * Walk srcDir and install skills into destDir.
64
- *
65
- * Handles two source formats:
66
- * 1. Directory-based skill: srcDir/{skill-name}/SKILL.md
67
- * → copies entire directory (SKILL.md + scripts/ + references/ + assets/)
68
- * → destDir/{skill-name}/
69
- *
70
- * 2. Flat .md skill (legacy): srcDir/{skill-name}.md
71
- * → destDir/{skill-name}/SKILL.md
72
- *
73
- * The `core/` level directory is traversed recursively. A directory is
74
- * recognized as a skill directory when it contains a SKILL.md file.
75
- *
76
- * @param {string} srcDir - Source directory to walk
77
- * @param {string} destDir - Destination base directory (.claude/skills/)
78
- */
79
- function installSkillsFromDir(srcDir, destDir, projectTags) {
80
- const entries = readdirSync(srcDir);
81
- for (const entry of entries) {
82
- const srcPath = join(srcDir, entry);
83
- const stat = statSync(srcPath);
84
-
85
- if (stat.isDirectory()) {
86
- const skillMdPath = join(srcPath, 'SKILL.md');
87
- if (existsSync(skillMdPath)) {
88
- // Directory-based skill — check stack filter before copying
89
- const content = readFileSync(skillMdPath, 'utf-8');
90
- if (!shouldInstallSkill(content, projectTags)) continue;
91
- const skillDestDir = join(destDir, entry);
92
- copyDirectory(srcPath, skillDestDir);
93
- } else {
94
- // Level/category directory — recurse
95
- installSkillsFromDir(srcPath, destDir, projectTags);
96
- }
97
- } else if (entry.endsWith('.md') && entry !== 'README.md') {
98
- // Legacy flat .md skill — check stack filter before copying
99
- const content = readFileSync(srcPath, 'utf-8');
100
- if (!shouldInstallSkill(content, projectTags)) continue;
101
- const skillName = entry.slice(0, -3);
102
- const skillDir = join(destDir, skillName);
103
- mkdirSync(skillDir, { recursive: true });
104
- copyFileSync(srcPath, join(skillDir, 'SKILL.md'));
105
- }
106
- }
107
- }
108
-
109
- function collectSourceSkillNames() {
110
- const names = new Set();
111
- for (const level of SKILL_LEVELS_TO_INSTALL) {
112
- const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
113
- if (!existsSync(levelDir)) continue;
114
- collectNamesFromDir(levelDir, names);
115
- }
116
- return names;
117
- }
118
-
119
- function collectNamesFromDir(dir, names) {
120
- for (const entry of readdirSync(dir)) {
121
- const fullPath = join(dir, entry);
122
- if (!statSync(fullPath).isDirectory()) continue;
123
- if (existsSync(join(fullPath, 'SKILL.md'))) {
124
- names.add(entry);
125
- } else {
126
- collectNamesFromDir(fullPath, names);
127
- }
128
- }
129
- }
130
-
131
- /**
132
- * Install morph framework skills to .claude/skills/ in the target project.
133
- * Skills are copied into subdirectories as SKILL.md files so Claude Code can
134
- * discover them natively via /<skill-name> syntax (e.g., /phase-design,
135
- * /nextjs-expert). Directory-based skills also get their scripts/,
136
- * references/, and assets/ subdirectories copied for on-demand loading.
137
- *
138
- * Stack filtering: if projectTags is provided, only skills whose `stacks:`
139
- * frontmatter matches the project tags are installed. Skills without a
140
- * `stacks:` field are always installed (universal).
141
- *
142
- * @param {string} projectDir - Target project root directory
143
- * @param {Object} [options]
144
- * @param {string[]} [options.projectTags=[]] - Stack tags from config
145
- */
146
- export async function installSkills(projectDir, { projectTags = [] } = {}) {
147
- const claudeSkillsDir = join(projectDir, '.claude', 'skills');
148
- mkdirSync(claudeSkillsDir, { recursive: true });
149
-
150
- for (const level of SKILL_LEVELS_TO_INSTALL) {
151
- const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
152
- if (!existsSync(levelDir)) continue;
153
- installSkillsFromDir(levelDir, claudeSkillsDir, projectTags);
154
- }
155
-
156
- // Collect installed morph-* skill names
157
- const installed = new Set();
158
- for (const entry of readdirSync(claudeSkillsDir)) {
159
- if (existsSync(join(claudeSkillsDir, entry, 'SKILL.md'))) {
160
- installed.add(entry);
161
- }
162
- }
163
-
164
- // Cleanup orphan morph-* skill dirs that exist in source but were filtered out
165
- const allSourceSkills = collectSourceSkillNames();
166
- for (const entry of readdirSync(claudeSkillsDir)) {
167
- if (!entry.startsWith('morph-')) continue;
168
- if (!installed.has(entry) && allSourceSkills.has(entry)) {
169
- rmSync(join(claudeSkillsDir, entry), { recursive: true, force: true });
170
- }
171
- }
172
-
173
- return { installed: installed.size };
174
- }
1
+ /**
2
+ * Skills Installer
3
+ *
4
+ * Copies framework skills to .claude/skills/ during morph-spec init.
5
+ * Installs only the `core/` skill set — the canonical NLH skills referenced by
6
+ * MORPH.md (morph-brainstorming, morph-stack-scan, morph-plan, morph-implement,
7
+ * morph-eval, morph-review, morph-standards). The 3-tier system was collapsed
8
+ * into a single source of truth in framework/skills/core/.
9
+ *
10
+ * Source skills are directory-based: framework/skills/core/{skill-name}/SKILL.md
11
+ * (plus optional scripts/, references/, assets/ subdirectories).
12
+ *
13
+ * Directory-based skills have their entire directory copied so Claude Code can
14
+ * execute scripts and load references on demand.
15
+ */
16
+
17
+ import { mkdirSync, copyFileSync, existsSync, readdirSync, statSync, readFileSync, rmSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { fileURLToPath } from 'url';
20
+ import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
21
+
22
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
23
+ const FRAMEWORK_SKILLS_DIR = join(__dirname, '..', '..', 'framework', 'skills');
24
+
25
+ /**
26
+ * Skill levels to install in .claude/skills/.
27
+ * Only `core/` survives — single source of truth aligned with MORPH.md.
28
+ */
29
+ const SKILL_LEVELS_TO_INSTALL = ['core'];
30
+
31
+ /**
32
+ * Check if a skill should be installed based on its stacks: frontmatter.
33
+ * @param {string} skillContent - Raw SKILL.md content
34
+ * @param {string[]} projectTags - Stack tags from config
35
+ * @returns {boolean}
36
+ */
37
+ export function shouldInstallSkill(skillContent, projectTags) {
38
+ return shouldInstall(parseStacks(skillContent), projectTags);
39
+ }
40
+
41
+ /**
42
+ * Recursively copy a directory tree from src to dest.
43
+ * Creates dest if it doesn't exist. Skips README.md files.
44
+ *
45
+ * @param {string} src - Source directory path
46
+ * @param {string} dest - Destination directory path
47
+ */
48
+ function copyDirectory(src, dest) {
49
+ mkdirSync(dest, { recursive: true });
50
+ for (const entry of readdirSync(src)) {
51
+ if (entry === 'README.md') continue;
52
+ const srcPath = join(src, entry);
53
+ const destPath = join(dest, entry);
54
+ if (statSync(srcPath).isDirectory()) {
55
+ copyDirectory(srcPath, destPath);
56
+ } else {
57
+ copyFileSync(srcPath, destPath);
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Walk srcDir and install skills into destDir.
64
+ *
65
+ * Handles two source formats:
66
+ * 1. Directory-based skill: srcDir/{skill-name}/SKILL.md
67
+ * → copies entire directory (SKILL.md + scripts/ + references/ + assets/)
68
+ * → destDir/{skill-name}/
69
+ *
70
+ * 2. Flat .md skill (legacy): srcDir/{skill-name}.md
71
+ * → destDir/{skill-name}/SKILL.md
72
+ *
73
+ * The `core/` level directory is traversed recursively. A directory is
74
+ * recognized as a skill directory when it contains a SKILL.md file.
75
+ *
76
+ * @param {string} srcDir - Source directory to walk
77
+ * @param {string} destDir - Destination base directory (.claude/skills/)
78
+ */
79
+ function installSkillsFromDir(srcDir, destDir, projectTags) {
80
+ const entries = readdirSync(srcDir);
81
+ for (const entry of entries) {
82
+ const srcPath = join(srcDir, entry);
83
+ const stat = statSync(srcPath);
84
+
85
+ if (stat.isDirectory()) {
86
+ const skillMdPath = join(srcPath, 'SKILL.md');
87
+ if (existsSync(skillMdPath)) {
88
+ // Directory-based skill — check stack filter before copying
89
+ const content = readFileSync(skillMdPath, 'utf-8');
90
+ if (!shouldInstallSkill(content, projectTags)) continue;
91
+ const skillDestDir = join(destDir, entry);
92
+ copyDirectory(srcPath, skillDestDir);
93
+ } else {
94
+ // Level/category directory — recurse
95
+ installSkillsFromDir(srcPath, destDir, projectTags);
96
+ }
97
+ } else if (entry.endsWith('.md') && entry !== 'README.md') {
98
+ // Legacy flat .md skill — check stack filter before copying
99
+ const content = readFileSync(srcPath, 'utf-8');
100
+ if (!shouldInstallSkill(content, projectTags)) continue;
101
+ const skillName = entry.slice(0, -3);
102
+ const skillDir = join(destDir, skillName);
103
+ mkdirSync(skillDir, { recursive: true });
104
+ copyFileSync(srcPath, join(skillDir, 'SKILL.md'));
105
+ }
106
+ }
107
+ }
108
+
109
+ function collectSourceSkillNames() {
110
+ const names = new Set();
111
+ for (const level of SKILL_LEVELS_TO_INSTALL) {
112
+ const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
113
+ if (!existsSync(levelDir)) continue;
114
+ collectNamesFromDir(levelDir, names);
115
+ }
116
+ return names;
117
+ }
118
+
119
+ function collectNamesFromDir(dir, names) {
120
+ for (const entry of readdirSync(dir)) {
121
+ const fullPath = join(dir, entry);
122
+ if (!statSync(fullPath).isDirectory()) continue;
123
+ if (existsSync(join(fullPath, 'SKILL.md'))) {
124
+ names.add(entry);
125
+ } else {
126
+ collectNamesFromDir(fullPath, names);
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Install morph framework skills to .claude/skills/ in the target project.
133
+ * Skills are copied into subdirectories as SKILL.md files so Claude Code can
134
+ * discover them natively via /<skill-name> syntax (e.g., /phase-design,
135
+ * /nextjs-expert). Directory-based skills also get their scripts/,
136
+ * references/, and assets/ subdirectories copied for on-demand loading.
137
+ *
138
+ * Stack filtering: if projectTags is provided, only skills whose `stacks:`
139
+ * frontmatter matches the project tags are installed. Skills without a
140
+ * `stacks:` field are always installed (universal).
141
+ *
142
+ * @param {string} projectDir - Target project root directory
143
+ * @param {Object} [options]
144
+ * @param {string[]} [options.projectTags=[]] - Stack tags from config
145
+ */
146
+ export async function installSkills(projectDir, { projectTags = [] } = {}) {
147
+ const claudeSkillsDir = join(projectDir, '.claude', 'skills');
148
+ mkdirSync(claudeSkillsDir, { recursive: true });
149
+
150
+ for (const level of SKILL_LEVELS_TO_INSTALL) {
151
+ const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
152
+ if (!existsSync(levelDir)) continue;
153
+ installSkillsFromDir(levelDir, claudeSkillsDir, projectTags);
154
+ }
155
+
156
+ // Collect installed morph-* skill names
157
+ const installed = new Set();
158
+ for (const entry of readdirSync(claudeSkillsDir)) {
159
+ if (existsSync(join(claudeSkillsDir, entry, 'SKILL.md'))) {
160
+ installed.add(entry);
161
+ }
162
+ }
163
+
164
+ // Cleanup orphan morph-* skill dirs that exist in source but were filtered out
165
+ const allSourceSkills = collectSourceSkillNames();
166
+ for (const entry of readdirSync(claudeSkillsDir)) {
167
+ if (!entry.startsWith('morph-')) continue;
168
+ if (!installed.has(entry) && allSourceSkills.has(entry)) {
169
+ rmSync(join(claudeSkillsDir, entry), { recursive: true, force: true });
170
+ }
171
+ }
172
+
173
+ return { installed: installed.size };
174
+ }
@@ -1,92 +0,0 @@
1
- <templateSet group="MORPH Templates">
2
- <template name="morph-pascal" value="{{pascalCase $FEATURE_NAME$}}" description="Convert to PascalCase (UserAuth)" toReformat="false" toShortenFQNames="true">
3
- <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
4
- <context>
5
- <option name="OTHER" value="true" />
6
- </context>
7
- </template>
8
-
9
- <template name="morph-camel" value="{{camelCase $FEATURE_NAME$}}" description="Convert to camelCase (userAuth)" toReformat="false" toShortenFQNames="true">
10
- <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
11
- <context>
12
- <option name="OTHER" value="true" />
13
- </context>
14
- </template>
15
-
16
- <template name="morph-snake" value="{{snakeCase $FEATURE_NAME$}}" description="Convert to snake_case (user_auth)" toReformat="false" toShortenFQNames="true">
17
- <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
18
- <context>
19
- <option name="OTHER" value="true" />
20
- </context>
21
- </template>
22
-
23
- <template name="morph-title" value="{{titleCase $FEATURE_NAME$}}" description="Convert to Title Case (User Auth)" toReformat="false" toShortenFQNames="true">
24
- <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
25
- <context>
26
- <option name="OTHER" value="true" />
27
- </context>
28
- </template>
29
-
30
- <template name="morph-kebab" value="{{kebabCase $FEATURE_NAME$}}" description="Convert to kebab-case (user-auth)" toReformat="false" toShortenFQNames="true">
31
- <variable name="FEATURE_NAME" expression="" defaultValue="&quot;FEATURE_NAME&quot;" alwaysStopAt="true" />
32
- <context>
33
- <option name="OTHER" value="true" />
34
- </context>
35
- </template>
36
-
37
- <template name="morph-if" value="{{#if $CONDITION$}}&#10; $END$&#10;{{/if}}" description="Handlebars if block" toReformat="false" toShortenFQNames="true">
38
- <variable name="CONDITION" expression="" defaultValue="&quot;condition&quot;" alwaysStopAt="true" />
39
- <context>
40
- <option name="OTHER" value="true" />
41
- </context>
42
- </template>
43
-
44
- <template name="morph-if-else" value="{{#if $CONDITION$}}&#10; $TRUE_BRANCH$&#10;{{else}}&#10; $FALSE_BRANCH$&#10;{{/if}}" description="Handlebars if-else block" toReformat="false" toShortenFQNames="true">
45
- <variable name="CONDITION" expression="" defaultValue="&quot;condition&quot;" alwaysStopAt="true" />
46
- <variable name="TRUE_BRANCH" expression="" defaultValue="&quot;&quot;" alwaysStopAt="true" />
47
- <variable name="FALSE_BRANCH" expression="" defaultValue="&quot;&quot;" alwaysStopAt="true" />
48
- <context>
49
- <option name="OTHER" value="true" />
50
- </context>
51
- </template>
52
-
53
- <template name="morph-each" value="{{#each $ITEMS$}}&#10; {{this.$PROPERTY$}}&#10;{{/each}}" description="Handlebars each loop" toReformat="false" toShortenFQNames="true">
54
- <variable name="ITEMS" expression="" defaultValue="&quot;items&quot;" alwaysStopAt="true" />
55
- <variable name="PROPERTY" expression="" defaultValue="&quot;name&quot;" alwaysStopAt="true" />
56
- <context>
57
- <option name="OTHER" value="true" />
58
- </context>
59
- </template>
60
-
61
- <template name="morph-service" value="namespace {{NAMESPACE}}.Application.Features.{{pascalCase FEATURE_NAME}}.Services;&#10;&#10;public class {{pascalCase FEATURE_NAME}}Service(&#10; I{{pascalCase FEATURE_NAME}}Repository repository,&#10; ILogger&lt;{{pascalCase FEATURE_NAME}}Service&gt; logger) : I{{pascalCase FEATURE_NAME}}Service&#10;{&#10; public async Task&lt;{{pascalCase FEATURE_NAME}}Dto?&gt; GetByIdAsync(int id, CancellationToken cancellationToken = default)&#10; {&#10; $END$&#10; }&#10;}" description="MORPH .NET Service class template" toReformat="false" toShortenFQNames="true">
62
- <context>
63
- <option name="C_SHARP" value="true" />
64
- </context>
65
- </template>
66
-
67
- <template name="morph-repository" value="public class {{pascalCase FEATURE_NAME}}Repository(AppDbContext context) : I{{pascalCase FEATURE_NAME}}Repository&#10;{&#10; private readonly DbSet&lt;{{pascalCase FEATURE_NAME}}&gt; _dbSet = context.Set&lt;{{pascalCase FEATURE_NAME}}&gt;();&#10;&#10; public async Task&lt;{{pascalCase FEATURE_NAME}}?&gt; GetByIdAsync(int id, CancellationToken cancellationToken = default)&#10; {&#10; $END$&#10; }&#10;}" description="MORPH .NET Repository class template" toReformat="false" toShortenFQNames="true">
68
- <context>
69
- <option name="C_SHARP" value="true" />
70
- </context>
71
- </template>
72
-
73
- <template name="morph-component" value="@page &quot;/{{kebabCase FEATURE_NAME}}&quot;&#10;@inject I{{pascalCase FEATURE_NAME}}Service {{pascalCase FEATURE_NAME}}Service&#10;@inject ILogger&lt;{{pascalCase FEATURE_NAME}}Page&gt; Logger&#10;&#10;&lt;PageTitle&gt;{{pascalCase FEATURE_NAME}}&lt;/PageTitle&gt;&#10;&#10;&lt;div class=&quot;container-fluid&quot;&gt;&#10; $END$&#10;&lt;/div&gt;&#10;&#10;@code {&#10;&#10;}" description="MORPH Blazor component template" toReformat="false" toShortenFQNames="true">
74
- <context>
75
- <option name="HTML" value="true" />
76
- </context>
77
- </template>
78
-
79
- <template name="morph-migration" value="public partial class Add{{pascalCase FEATURE_NAME}} : Migration&#10;{&#10; protected override void Up(MigrationBuilder migrationBuilder)&#10; {&#10; migrationBuilder.CreateTable(&#10; name: &quot;{{pascalCase FEATURE_NAME}}s&quot;,&#10; columns: table =&gt; new&#10; {&#10; Id = table.Column&lt;int&gt;(type: &quot;int&quot;, nullable: false)&#10; .Annotation(&quot;SqlServer:Identity&quot;, &quot;1, 1&quot;),&#10; $END$&#10; },&#10; constraints: table =&gt;&#10; {&#10; table.PrimaryKey(&quot;PK_{{pascalCase FEATURE_NAME}}s&quot;, x =&gt; x.Id);&#10; });&#10; }&#10;&#10; protected override void Down(MigrationBuilder migrationBuilder)&#10; {&#10; migrationBuilder.DropTable(name: &quot;{{pascalCase FEATURE_NAME}}s&quot;);&#10; }&#10;}" description="MORPH EF Core migration template" toReformat="false" toShortenFQNames="true">
80
- <context>
81
- <option name="C_SHARP" value="true" />
82
- </context>
83
- </template>
84
-
85
- <template name="morph-ts-contract" value="export interface {{pascalCase FEATURE_NAME}} {&#10; id: number;&#10; $FIELD1$&#10;}&#10;&#10;export interface Create{{pascalCase FEATURE_NAME}}Request {&#10; $FIELD2$&#10;}&#10;&#10;export interface Update{{pascalCase FEATURE_NAME}}Request {&#10; $END$&#10;}" description="MORPH TypeScript contract template" toReformat="false" toShortenFQNames="true">
86
- <variable name="FIELD1" expression="" defaultValue="&quot;&quot;" alwaysStopAt="true" />
87
- <variable name="FIELD2" expression="" defaultValue="&quot;&quot;" alwaysStopAt="true" />
88
- <context>
89
- <option name="TypeScript" value="true" />
90
- </context>
91
- </template>
92
- </templateSet>
@@ -1,186 +0,0 @@
1
- {
2
- "MORPH Template - PascalCase": {
3
- "prefix": "morph-pascal",
4
- "body": ["{{pascalCase ${1:FEATURE_NAME}}}"],
5
- "description": "Convert to PascalCase (UserAuth)"
6
- },
7
- "MORPH Template - camelCase": {
8
- "prefix": "morph-camel",
9
- "body": ["{{camelCase ${1:FEATURE_NAME}}}"],
10
- "description": "Convert to camelCase (userAuth)"
11
- },
12
- "MORPH Template - snake_case": {
13
- "prefix": "morph-snake",
14
- "body": ["{{snakeCase ${1:FEATURE_NAME}}}"],
15
- "description": "Convert to snake_case (user_auth)"
16
- },
17
- "MORPH Template - Title Case": {
18
- "prefix": "morph-title",
19
- "body": ["{{titleCase ${1:FEATURE_NAME}}}"],
20
- "description": "Convert to Title Case (User Auth)"
21
- },
22
- "MORPH Template - kebab-case": {
23
- "prefix": "morph-kebab",
24
- "body": ["{{kebabCase ${1:FEATURE_NAME}}}"],
25
- "description": "Convert to kebab-case (user-auth)"
26
- },
27
- "MORPH Template - UPPER_SNAKE_CASE": {
28
- "prefix": "morph-upper-snake",
29
- "body": ["{{upperSnakeCase ${1:FEATURE_NAME}}}"],
30
- "description": "Convert to UPPER_SNAKE_CASE (USER_AUTH)"
31
- },
32
- "MORPH Template - Conditional If": {
33
- "prefix": "morph-if",
34
- "body": [
35
- "{{#if ${1:condition}}}",
36
- " $2",
37
- "{{/if}}"
38
- ],
39
- "description": "Handlebars if block"
40
- },
41
- "MORPH Template - Conditional If-Else": {
42
- "prefix": "morph-if-else",
43
- "body": [
44
- "{{#if ${1:condition}}}",
45
- " $2",
46
- "{{else}}",
47
- " $3",
48
- "{{/if}}"
49
- ],
50
- "description": "Handlebars if-else block"
51
- },
52
- "MORPH Template - Each Loop": {
53
- "prefix": "morph-each",
54
- "body": [
55
- "{{#each ${1:items}}}",
56
- " {{this.$2}}",
57
- "{{/each}}"
58
- ],
59
- "description": "Handlebars each loop"
60
- },
61
- "MORPH Template - Unless": {
62
- "prefix": "morph-unless",
63
- "body": [
64
- "{{#unless ${1:condition}}}",
65
- " $2",
66
- "{{/unless}}"
67
- ],
68
- "description": "Handlebars unless block"
69
- },
70
- "MORPH Template - .NET Service Class": {
71
- "prefix": "morph-service",
72
- "body": [
73
- "namespace {{NAMESPACE}}.Application.Features.{{pascalCase FEATURE_NAME}}.Services;",
74
- "",
75
- "public class {{pascalCase FEATURE_NAME}}Service(",
76
- " I{{pascalCase FEATURE_NAME}}Repository repository,",
77
- " ILogger<{{pascalCase FEATURE_NAME}}Service> logger) : I{{pascalCase FEATURE_NAME}}Service",
78
- "{",
79
- " public async Task<{{pascalCase FEATURE_NAME}}Dto?> GetByIdAsync(int id, CancellationToken cancellationToken = default)",
80
- " {",
81
- " $1",
82
- " }",
83
- "}"
84
- ],
85
- "description": "MORPH .NET Service class template"
86
- },
87
- "MORPH Template - .NET Repository Class": {
88
- "prefix": "morph-repository",
89
- "body": [
90
- "public class {{pascalCase FEATURE_NAME}}Repository(AppDbContext context) : I{{pascalCase FEATURE_NAME}}Repository",
91
- "{",
92
- " private readonly DbSet<{{pascalCase FEATURE_NAME}}> _dbSet = context.Set<{{pascalCase FEATURE_NAME}}>();",
93
- "",
94
- " public async Task<{{pascalCase FEATURE_NAME}}?> GetByIdAsync(int id, CancellationToken cancellationToken = default)",
95
- " {",
96
- " $1",
97
- " }",
98
- "}"
99
- ],
100
- "description": "MORPH .NET Repository class template"
101
- },
102
- "MORPH Template - Blazor Component": {
103
- "prefix": "morph-component",
104
- "body": [
105
- "@page \"/{{kebabCase FEATURE_NAME}}\"",
106
- "@inject I{{pascalCase FEATURE_NAME}}Service {{pascalCase FEATURE_NAME}}Service",
107
- "@inject ILogger<{{pascalCase FEATURE_NAME}}Page> Logger",
108
- "",
109
- "<PageTitle>{{pascalCase FEATURE_NAME}}</PageTitle>",
110
- "",
111
- "<div class=\"container-fluid\">",
112
- " $1",
113
- "</div>",
114
- "",
115
- "@code {",
116
- " $2",
117
- "}"
118
- ],
119
- "description": "MORPH Blazor component template"
120
- },
121
- "MORPH Template - EF Core Migration": {
122
- "prefix": "morph-migration",
123
- "body": [
124
- "public partial class Add{{pascalCase FEATURE_NAME}} : Migration",
125
- "{",
126
- " protected override void Up(MigrationBuilder migrationBuilder)",
127
- " {",
128
- " migrationBuilder.CreateTable(",
129
- " name: \"{{pascalCase FEATURE_NAME}}s\",",
130
- " columns: table => new",
131
- " {",
132
- " Id = table.Column<int>(type: \"int\", nullable: false)",
133
- " .Annotation(\"SqlServer:Identity\", \"1, 1\"),",
134
- " $1",
135
- " },",
136
- " constraints: table =>",
137
- " {",
138
- " table.PrimaryKey(\"PK_{{pascalCase FEATURE_NAME}}s\", x => x.Id);",
139
- " });",
140
- " }",
141
- "",
142
- " protected override void Down(MigrationBuilder migrationBuilder)",
143
- " {",
144
- " migrationBuilder.DropTable(name: \"{{pascalCase FEATURE_NAME}}s\");",
145
- " }",
146
- "}"
147
- ],
148
- "description": "MORPH EF Core migration template"
149
- },
150
- "MORPH Template - Hangfire Job": {
151
- "prefix": "morph-job",
152
- "body": [
153
- "public class {{pascalCase FEATURE_NAME}}ProcessorJob(",
154
- " I{{pascalCase FEATURE_NAME}}Service service,",
155
- " ILogger<{{pascalCase FEATURE_NAME}}ProcessorJob> logger) : I{{pascalCase FEATURE_NAME}}ProcessorJob",
156
- "{",
157
- " [AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]",
158
- " [Queue(\"default\")]",
159
- " [JobDisplayName(\"{{pascalCase FEATURE_NAME}} Processing - ID: {0}\")]",
160
- " public async Task ExecuteAsync(int id, CancellationToken cancellationToken)",
161
- " {",
162
- " $1",
163
- " }",
164
- "}"
165
- ],
166
- "description": "MORPH Hangfire job template"
167
- },
168
- "MORPH Template - TypeScript Contract": {
169
- "prefix": "morph-ts-contract",
170
- "body": [
171
- "export interface {{pascalCase FEATURE_NAME}} {",
172
- " id: number;",
173
- " $1",
174
- "}",
175
- "",
176
- "export interface Create{{pascalCase FEATURE_NAME}}Request {",
177
- " $2",
178
- "}",
179
- "",
180
- "export interface Update{{pascalCase FEATURE_NAME}}Request {",
181
- " $3",
182
- "}"
183
- ],
184
- "description": "MORPH TypeScript contract template"
185
- }
186
- }