@polymorphism-tech/morph-spec 1.0.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1381 -0
- package/LICENSE +72 -0
- package/README.md +89 -6
- package/bin/detect-agents.js +225 -0
- package/bin/morph-spec.js +120 -0
- package/bin/render-template.js +302 -0
- package/bin/semantic-detect-agents.js +246 -0
- package/bin/validate-agents-skills.js +239 -0
- package/bin/validate-agents.js +69 -0
- package/bin/validate-phase.js +263 -0
- package/content/.azure/README.md +293 -0
- package/content/.azure/docs/azure-devops-setup.md +454 -0
- package/content/.azure/docs/branch-strategy.md +398 -0
- package/content/.azure/docs/local-development.md +515 -0
- package/content/.azure/pipelines/pipeline-variables.yml +34 -0
- package/content/.azure/pipelines/prod-pipeline.yml +319 -0
- package/content/.azure/pipelines/staging-pipeline.yml +234 -0
- package/content/.azure/pipelines/templates/build-dotnet.yml +75 -0
- package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -0
- package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -0
- package/content/.azure/pipelines/templates/infra-deploy.yml +90 -0
- package/content/.claude/commands/morph-apply.md +118 -26
- package/content/.claude/commands/morph-archive.md +9 -9
- package/content/.claude/commands/morph-clarify.md +184 -0
- package/content/.claude/commands/morph-design.md +275 -0
- package/content/.claude/commands/morph-proposal.md +56 -15
- package/content/.claude/commands/morph-setup.md +100 -0
- package/content/.claude/commands/morph-status.md +47 -32
- package/content/.claude/commands/morph-tasks.md +319 -0
- package/content/.claude/commands/morph-uiux.md +211 -0
- package/content/.claude/skills/specialists/ai-system-architect.md +604 -0
- package/content/.claude/skills/specialists/ms-agent-expert.md +143 -89
- package/content/.claude/skills/specialists/ui-ux-designer.md +744 -9
- package/content/.claude/skills/stacks/dotnet-blazor.md +244 -8
- package/content/.claude/skills/stacks/dotnet-nextjs.md +2 -2
- package/content/.morph/.morphversion +5 -0
- package/content/.morph/config/agents.json +101 -8
- package/content/.morph/config/azure-pricing.json +70 -0
- package/content/.morph/config/azure-pricing.schema.json +50 -0
- package/content/.morph/config/config.template.json +15 -3
- package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -0
- package/content/.morph/hooks/README.md +239 -0
- package/content/.morph/hooks/pre-commit-agents.sh +24 -0
- package/content/.morph/hooks/pre-commit-all.sh +48 -0
- package/content/.morph/hooks/pre-commit-costs.sh +91 -0
- package/content/.morph/hooks/pre-commit-specs.sh +49 -0
- package/content/.morph/hooks/pre-commit-tests.sh +60 -0
- package/content/.morph/project.md +5 -4
- package/content/.morph/schemas/agent.schema.json +296 -0
- package/content/.morph/standards/agent-framework-setup.md +453 -0
- package/content/.morph/standards/architecture.md +142 -7
- package/content/.morph/standards/azure.md +218 -23
- package/content/.morph/standards/coding.md +47 -12
- package/content/.morph/standards/dotnet10-migration.md +494 -0
- package/content/.morph/standards/fluent-ui-setup.md +590 -0
- package/content/.morph/standards/migration-guide.md +514 -0
- package/content/.morph/standards/passkeys-auth.md +423 -0
- package/content/.morph/standards/vector-search-rag.md +536 -0
- package/content/.morph/state.json +18 -0
- package/content/.morph/templates/FluentDesignTheme.cs +149 -0
- package/content/.morph/templates/MudTheme.cs +281 -0
- package/content/.morph/templates/contracts.cs +55 -55
- package/content/.morph/templates/decisions.md +4 -4
- package/content/.morph/templates/design-system.css +226 -0
- package/content/.morph/templates/infra/.dockerignore.example +89 -0
- package/content/.morph/templates/infra/Dockerfile.example +82 -0
- package/content/.morph/templates/infra/README.md +286 -0
- package/content/.morph/templates/infra/app-service.bicep +164 -0
- package/content/.morph/templates/infra/deploy.ps1 +229 -0
- package/content/.morph/templates/infra/deploy.sh +208 -0
- package/content/.morph/templates/infra/main.bicep +41 -7
- package/content/.morph/templates/infra/parameters.dev.json +6 -0
- package/content/.morph/templates/infra/parameters.prod.json +6 -0
- package/content/.morph/templates/infra/parameters.staging.json +29 -0
- package/content/.morph/templates/proposal.md +3 -3
- package/content/.morph/templates/recap.md +3 -3
- package/content/.morph/templates/spec.md +9 -8
- package/content/.morph/templates/sprint-status.yaml +68 -0
- package/content/.morph/templates/state.template.json +222 -0
- package/content/.morph/templates/story.md +143 -0
- package/content/.morph/templates/tasks.md +1 -1
- package/content/.morph/templates/ui-components.md +276 -0
- package/content/.morph/templates/ui-design-system.md +286 -0
- package/content/.morph/templates/ui-flows.md +336 -0
- package/content/.morph/templates/ui-mockups.md +133 -0
- package/content/.morph/test-infra/example.bicep +59 -0
- package/content/CLAUDE.md +124 -0
- package/content/README.md +79 -0
- package/detectors/config-detector.js +223 -0
- package/detectors/conversation-analyzer.js +163 -0
- package/detectors/index.js +84 -0
- package/detectors/standards-generator.js +275 -0
- package/detectors/structure-detector.js +221 -0
- package/docs/README.md +149 -0
- package/docs/api/cost-calculator.js.html +513 -0
- package/docs/api/design-system-generator.js.html +382 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/api/global.html +5263 -0
- package/docs/api/index.html +96 -0
- package/docs/api/scripts/collapse.js +39 -0
- package/docs/api/scripts/commonNav.js +28 -0
- package/docs/api/scripts/linenumber.js +25 -0
- package/docs/api/scripts/nav.js +12 -0
- package/docs/api/scripts/polyfill.js +4 -0
- package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/api/scripts/prettify/lang-css.js +2 -0
- package/docs/api/scripts/prettify/prettify.js +28 -0
- package/docs/api/scripts/search.js +99 -0
- package/docs/api/state-manager.js.html +423 -0
- package/docs/api/styles/jsdoc.css +776 -0
- package/docs/api/styles/prettify.css +80 -0
- package/docs/examples.md +328 -0
- package/docs/getting-started.md +302 -0
- package/docs/installation.md +361 -0
- package/docs/templates.md +418 -0
- package/docs/validation-checklist.md +266 -0
- package/package.json +39 -12
- package/src/commands/cost.js +181 -0
- package/src/commands/create-story.js +283 -0
- package/src/commands/detect.js +104 -0
- package/src/commands/doctor.js +67 -0
- package/src/commands/generate.js +149 -0
- package/src/commands/init.js +69 -45
- package/src/commands/shard-spec.js +224 -0
- package/src/commands/sprint-status.js +250 -0
- package/src/commands/state.js +333 -0
- package/src/commands/sync.js +167 -0
- package/src/commands/update-pricing.js +206 -0
- package/src/commands/update.js +88 -13
- package/src/lib/complexity-analyzer.js +292 -0
- package/src/lib/cost-calculator.js +429 -0
- package/src/lib/design-system-generator.js +298 -0
- package/src/lib/state-manager.js +340 -0
- package/src/utils/file-copier.js +59 -0
- package/src/utils/version-checker.js +175 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Design System Generator Library
|
|
3
|
+
*
|
|
4
|
+
* Reads ui-design-system.md and generates:
|
|
5
|
+
* - wwwroot/css/design-system.css (CSS variables)
|
|
6
|
+
* - Themes/FluentDesignTheme.cs (Fluent UI theme)
|
|
7
|
+
* - Themes/MudTheme.cs (MudBlazor theme)
|
|
8
|
+
*
|
|
9
|
+
* Used both by CLI commands and internal automation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Parsing Functions
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse colors from markdown
|
|
20
|
+
* @param {string} markdown - Markdown content
|
|
21
|
+
* @returns {Object} Colors object with primary, secondary, neutral, semantic
|
|
22
|
+
*/
|
|
23
|
+
export function parseColors(markdown) {
|
|
24
|
+
const colors = { primary: {}, secondary: {}, neutral: {}, semantic: {} };
|
|
25
|
+
|
|
26
|
+
// Match color definitions like: - **Primary 500** (`#...`) - ...
|
|
27
|
+
const colorRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^\(]*\(`(#[0-9A-Fa-f]{6})`\)/gm;
|
|
28
|
+
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = colorRegex.exec(markdown)) !== null) {
|
|
31
|
+
const [, name, hex] = match;
|
|
32
|
+
const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
|
|
33
|
+
|
|
34
|
+
if (name.includes('Primary')) {
|
|
35
|
+
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
36
|
+
colors.primary[variant] = hex;
|
|
37
|
+
} else if (name.includes('Secondary')) {
|
|
38
|
+
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
39
|
+
colors.secondary[variant] = hex;
|
|
40
|
+
} else if (name.includes('Neutral') || name.includes('Gray')) {
|
|
41
|
+
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
42
|
+
colors.neutral[variant] = hex;
|
|
43
|
+
} else if (name.includes('Success') || name.includes('Error') || name.includes('Warning') || name.includes('Info')) {
|
|
44
|
+
const type = name.toLowerCase().replace(/\s+/g, '-');
|
|
45
|
+
colors.semantic[type] = hex;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return colors;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse typography from markdown
|
|
54
|
+
* @param {string} markdown - Markdown content
|
|
55
|
+
* @returns {Object} Typography object with fontFamily, fontSize, fontWeight, lineHeight
|
|
56
|
+
*/
|
|
57
|
+
export function parseTypography(markdown) {
|
|
58
|
+
const typography = { fontFamily: {}, fontSize: {}, fontWeight: {}, lineHeight: {} };
|
|
59
|
+
|
|
60
|
+
// Match font family
|
|
61
|
+
const fontFamilyRegex = /(?:font[- ]?family|typeface)[:\s]+([^\n]+)/gi;
|
|
62
|
+
const fontMatch = fontFamilyRegex.exec(markdown);
|
|
63
|
+
if (fontMatch) {
|
|
64
|
+
typography.fontFamily.primary = fontMatch[1].trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Match font sizes (but exclude spacing-related)
|
|
68
|
+
const fontSizeRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^:]*:\s*(\d+(?:\.\d+)?(?:rem|px|em))/gm;
|
|
69
|
+
let match;
|
|
70
|
+
while ((match = fontSizeRegex.exec(markdown)) !== null) {
|
|
71
|
+
const [, name, size] = match;
|
|
72
|
+
const lowerName = name.toLowerCase();
|
|
73
|
+
// Skip if it's spacing-related
|
|
74
|
+
if (lowerName.includes('spacing') || lowerName.includes('gap') || lowerName.includes('margin') || lowerName.includes('padding')) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
|
|
78
|
+
typography.fontSize[cleanName] = size;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return typography;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse spacing from markdown
|
|
86
|
+
* @param {string} markdown - Markdown content
|
|
87
|
+
* @returns {Object} Spacing object with spacing values
|
|
88
|
+
*/
|
|
89
|
+
export function parseSpacing(markdown) {
|
|
90
|
+
const spacing = {};
|
|
91
|
+
|
|
92
|
+
// Match spacing definitions
|
|
93
|
+
const spacingRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^:]*:\s*(\d+(?:\.\d+)?(?:rem|px|em))/gm;
|
|
94
|
+
let match;
|
|
95
|
+
while ((match = spacingRegex.exec(markdown)) !== null) {
|
|
96
|
+
const [, name, value] = match;
|
|
97
|
+
if (name.toLowerCase().includes('spacing') || name.toLowerCase().includes('gap') || name.toLowerCase().includes('margin') || name.toLowerCase().includes('padding')) {
|
|
98
|
+
const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
|
|
99
|
+
spacing[cleanName] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return spacing;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse all design system properties from markdown
|
|
108
|
+
* @param {string} markdownPath - Path to ui-design-system.md file
|
|
109
|
+
* @returns {Object} Parsed design system with colors, typography, spacing
|
|
110
|
+
*/
|
|
111
|
+
export function parseDesignSystem(markdownPath) {
|
|
112
|
+
if (!existsSync(markdownPath)) {
|
|
113
|
+
throw new Error(`File not found: ${markdownPath}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const markdown = readFileSync(markdownPath, 'utf-8');
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
colors: parseColors(markdown),
|
|
120
|
+
typography: parseTypography(markdown),
|
|
121
|
+
spacing: parseSpacing(markdown)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Generation Functions
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate CSS variables file
|
|
131
|
+
* @param {Object} colors - Colors object
|
|
132
|
+
* @param {Object} typography - Typography object
|
|
133
|
+
* @param {Object} spacing - Spacing object
|
|
134
|
+
* @returns {string} CSS content
|
|
135
|
+
*/
|
|
136
|
+
export function generateCSS(colors, typography, spacing) {
|
|
137
|
+
let css = `/* Auto-generated by MORPH-SPEC Design System Generator */\n`;
|
|
138
|
+
css += `/* Do not edit manually - regenerate from ui-design-system.md */\n\n`;
|
|
139
|
+
css += `:root {\n`;
|
|
140
|
+
|
|
141
|
+
// Colors
|
|
142
|
+
if (Object.keys(colors.primary).length > 0) {
|
|
143
|
+
css += ` /* Primary Colors */\n`;
|
|
144
|
+
Object.entries(colors.primary).forEach(([variant, hex]) => {
|
|
145
|
+
css += ` --color-primary-${variant}: ${hex};\n`;
|
|
146
|
+
});
|
|
147
|
+
css += `\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Object.keys(colors.secondary).length > 0) {
|
|
151
|
+
css += ` /* Secondary Colors */\n`;
|
|
152
|
+
Object.entries(colors.secondary).forEach(([variant, hex]) => {
|
|
153
|
+
css += ` --color-secondary-${variant}: ${hex};\n`;
|
|
154
|
+
});
|
|
155
|
+
css += `\n`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (Object.keys(colors.neutral).length > 0) {
|
|
159
|
+
css += ` /* Neutral Colors */\n`;
|
|
160
|
+
Object.entries(colors.neutral).forEach(([variant, hex]) => {
|
|
161
|
+
css += ` --color-neutral-${variant}: ${hex};\n`;
|
|
162
|
+
});
|
|
163
|
+
css += `\n`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (Object.keys(colors.semantic).length > 0) {
|
|
167
|
+
css += ` /* Semantic Colors */\n`;
|
|
168
|
+
Object.entries(colors.semantic).forEach(([type, hex]) => {
|
|
169
|
+
css += ` --color-${type}: ${hex};\n`;
|
|
170
|
+
});
|
|
171
|
+
css += `\n`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Typography
|
|
175
|
+
if (typography.fontFamily.primary) {
|
|
176
|
+
css += ` /* Typography */\n`;
|
|
177
|
+
css += ` --font-family-primary: ${typography.fontFamily.primary};\n`;
|
|
178
|
+
css += `\n`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (Object.keys(typography.fontSize).length > 0) {
|
|
182
|
+
css += ` /* Font Sizes */\n`;
|
|
183
|
+
Object.entries(typography.fontSize).forEach(([name, size]) => {
|
|
184
|
+
css += ` --font-size-${name}: ${size};\n`;
|
|
185
|
+
});
|
|
186
|
+
css += `\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Spacing
|
|
190
|
+
if (Object.keys(spacing).length > 0) {
|
|
191
|
+
css += ` /* Spacing */\n`;
|
|
192
|
+
Object.entries(spacing).forEach(([name, value]) => {
|
|
193
|
+
css += ` --${name}: ${value};\n`;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
css += `}\n`;
|
|
198
|
+
|
|
199
|
+
return css;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Generate Fluent UI theme (C#)
|
|
204
|
+
* @param {Object} colors - Colors object
|
|
205
|
+
* @param {Object} typography - Typography object
|
|
206
|
+
* @param {string} namespace - C# namespace (default: YourProject.Themes)
|
|
207
|
+
* @returns {string} C# code
|
|
208
|
+
*/
|
|
209
|
+
export function generateFluentTheme(colors, typography, namespace = 'YourProject.Themes') {
|
|
210
|
+
const primary500 = colors.primary['500'] || '#0078d4';
|
|
211
|
+
|
|
212
|
+
let csharp = `// Auto-generated by MORPH-SPEC Design System Generator\n`;
|
|
213
|
+
csharp += `// Do not edit manually - regenerate from ui-design-system.md\n\n`;
|
|
214
|
+
csharp += `using Microsoft.FluentUI.AspNetCore.Components;\n\n`;
|
|
215
|
+
csharp += `namespace ${namespace};\n\n`;
|
|
216
|
+
csharp += `public static class FluentDesignTheme\n{\n`;
|
|
217
|
+
csharp += ` public static DesignThemePalette GetPalette()\n`;
|
|
218
|
+
csharp += ` {\n`;
|
|
219
|
+
csharp += ` return new DesignThemePalette\n`;
|
|
220
|
+
csharp += ` {\n`;
|
|
221
|
+
csharp += ` Accent = "${primary500}",\n`;
|
|
222
|
+
csharp += ` // Add more theme properties as needed\n`;
|
|
223
|
+
csharp += ` };\n`;
|
|
224
|
+
csharp += ` }\n`;
|
|
225
|
+
csharp += `}\n`;
|
|
226
|
+
|
|
227
|
+
return csharp;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generate MudBlazor theme (C#)
|
|
232
|
+
* @param {Object} colors - Colors object
|
|
233
|
+
* @param {Object} typography - Typography object
|
|
234
|
+
* @param {string} namespace - C# namespace (default: YourProject.Themes)
|
|
235
|
+
* @returns {string} C# code
|
|
236
|
+
*/
|
|
237
|
+
export function generateMudTheme(colors, typography, namespace = 'YourProject.Themes') {
|
|
238
|
+
const primary500 = colors.primary['500'] || '#594ae2';
|
|
239
|
+
const secondary500 = colors.secondary['500'] || '#ff4081';
|
|
240
|
+
|
|
241
|
+
let csharp = `// Auto-generated by MORPH-SPEC Design System Generator\n`;
|
|
242
|
+
csharp += `// Do not edit manually - regenerate from ui-design-system.md\n\n`;
|
|
243
|
+
csharp += `using MudBlazor;\n\n`;
|
|
244
|
+
csharp += `namespace ${namespace};\n\n`;
|
|
245
|
+
csharp += `public static class MudDesignTheme\n{\n`;
|
|
246
|
+
csharp += ` public static MudTheme GetTheme()\n`;
|
|
247
|
+
csharp += ` {\n`;
|
|
248
|
+
csharp += ` return new MudTheme\n`;
|
|
249
|
+
csharp += ` {\n`;
|
|
250
|
+
csharp += ` Palette = new PaletteLight\n`;
|
|
251
|
+
csharp += ` {\n`;
|
|
252
|
+
csharp += ` Primary = "${primary500}",\n`;
|
|
253
|
+
csharp += ` Secondary = "${secondary500}",\n`;
|
|
254
|
+
csharp += ` // Add more palette colors as needed\n`;
|
|
255
|
+
csharp += ` }\n`;
|
|
256
|
+
csharp += ` };\n`;
|
|
257
|
+
csharp += ` }\n`;
|
|
258
|
+
csharp += `}\n`;
|
|
259
|
+
|
|
260
|
+
return csharp;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Generate all design system files
|
|
265
|
+
* @param {string} designSystemPath - Path to ui-design-system.md
|
|
266
|
+
* @param {Object} options - Generation options
|
|
267
|
+
* @param {string} options.mode - Generation mode: 'fluent', 'mud', or 'both'
|
|
268
|
+
* @param {string} options.namespace - C# namespace
|
|
269
|
+
* @returns {Object} Generated files content
|
|
270
|
+
*/
|
|
271
|
+
export function generateDesignSystem(designSystemPath, options = {}) {
|
|
272
|
+
const { mode = 'both', namespace = 'YourProject.Themes' } = options;
|
|
273
|
+
|
|
274
|
+
// Parse design system
|
|
275
|
+
const { colors, typography, spacing } = parseDesignSystem(designSystemPath);
|
|
276
|
+
|
|
277
|
+
const generated = {
|
|
278
|
+
css: generateCSS(colors, typography, spacing),
|
|
279
|
+
stats: {
|
|
280
|
+
primaryColors: Object.keys(colors.primary).length,
|
|
281
|
+
neutralColors: Object.keys(colors.neutral).length,
|
|
282
|
+
semanticColors: Object.keys(colors.semantic).length,
|
|
283
|
+
fontSizes: Object.keys(typography.fontSize).length,
|
|
284
|
+
spacingValues: Object.keys(spacing).length
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Generate themes based on mode
|
|
289
|
+
if (mode === 'fluent' || mode === 'both') {
|
|
290
|
+
generated.fluentTheme = generateFluentTheme(colors, typography, namespace);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (mode === 'mud' || mode === 'both') {
|
|
294
|
+
generated.mudTheme = generateMudTheme(colors, typography, namespace);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return generated;
|
|
298
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC State Manager Library
|
|
3
|
+
*
|
|
4
|
+
* Manages state.json for tracking features, progress, agents, and checkpoints.
|
|
5
|
+
* Used both by CLI commands and internal automation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
|
|
11
|
+
const STATE_FILE_NAME = '.morph/state.json';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Core Functions
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the state file path (looks in current working directory)
|
|
19
|
+
*/
|
|
20
|
+
export function getStatePath() {
|
|
21
|
+
return join(process.cwd(), STATE_FILE_NAME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if state file exists
|
|
26
|
+
*/
|
|
27
|
+
export function stateExists() {
|
|
28
|
+
return existsSync(getStatePath());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load state from disk
|
|
33
|
+
* @param {boolean} throwOnError - If false, returns null instead of throwing
|
|
34
|
+
* @returns {Object|null} State object or null
|
|
35
|
+
*/
|
|
36
|
+
export function loadState(throwOnError = true) {
|
|
37
|
+
const statePath = getStatePath();
|
|
38
|
+
|
|
39
|
+
if (!existsSync(statePath)) {
|
|
40
|
+
if (throwOnError) {
|
|
41
|
+
throw new Error(`State file not found: ${statePath}\nRun 'morph-spec state init' first.`);
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const content = readFileSync(statePath, 'utf8');
|
|
48
|
+
return JSON.parse(content);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (throwOnError) {
|
|
51
|
+
throw new Error(`Failed to parse state.json: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save state to disk
|
|
59
|
+
* @param {Object} state - State object to save
|
|
60
|
+
*/
|
|
61
|
+
export function saveState(state) {
|
|
62
|
+
state.metadata = state.metadata || {};
|
|
63
|
+
state.metadata.lastUpdated = new Date().toISOString();
|
|
64
|
+
|
|
65
|
+
const statePath = getStatePath();
|
|
66
|
+
const stateDir = dirname(statePath);
|
|
67
|
+
|
|
68
|
+
// Ensure .morph directory exists
|
|
69
|
+
if (!existsSync(stateDir)) {
|
|
70
|
+
mkdirSync(stateDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initialize new state file
|
|
78
|
+
* @param {Object} options - Options
|
|
79
|
+
* @param {boolean} options.force - Overwrite existing file
|
|
80
|
+
* @param {string} options.projectName - Project name
|
|
81
|
+
* @param {string} options.projectType - Project type (e.g., 'blazor-server')
|
|
82
|
+
* @returns {Object} Initial state
|
|
83
|
+
*/
|
|
84
|
+
export function initState(options = {}) {
|
|
85
|
+
const { force = false, projectName = '{PROJECT_NAME}', projectType = 'blazor-server' } = options;
|
|
86
|
+
|
|
87
|
+
if (stateExists() && !force) {
|
|
88
|
+
throw new Error('State file already exists. Use force=true to overwrite.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const initialState = {
|
|
92
|
+
version: "2.1.1",
|
|
93
|
+
project: {
|
|
94
|
+
name: projectName,
|
|
95
|
+
type: projectType,
|
|
96
|
+
createdAt: new Date().toISOString(),
|
|
97
|
+
updatedAt: new Date().toISOString()
|
|
98
|
+
},
|
|
99
|
+
features: {},
|
|
100
|
+
metadata: {
|
|
101
|
+
totalFeatures: 0,
|
|
102
|
+
completedFeatures: 0,
|
|
103
|
+
totalCostEstimated: 0,
|
|
104
|
+
totalTimeSpent: 0,
|
|
105
|
+
lastUpdated: new Date().toISOString()
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
saveState(initialState);
|
|
110
|
+
return initialState;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Feature Operations
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get feature from state
|
|
119
|
+
* @param {string} featureName - Feature name
|
|
120
|
+
* @returns {Object|null} Feature object or null
|
|
121
|
+
*/
|
|
122
|
+
export function getFeature(featureName) {
|
|
123
|
+
const state = loadState();
|
|
124
|
+
return state.features[featureName] || null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create or get feature with default structure
|
|
129
|
+
* @param {string} featureName - Feature name
|
|
130
|
+
* @returns {Object} Feature object
|
|
131
|
+
*/
|
|
132
|
+
function ensureFeature(featureName) {
|
|
133
|
+
const state = loadState();
|
|
134
|
+
|
|
135
|
+
if (!state.features[featureName]) {
|
|
136
|
+
state.features[featureName] = {
|
|
137
|
+
status: "draft",
|
|
138
|
+
phase: "proposal",
|
|
139
|
+
workflow: "auto", // auto | fast-track | standard | full-morph
|
|
140
|
+
createdAt: new Date().toISOString(),
|
|
141
|
+
updatedAt: new Date().toISOString(),
|
|
142
|
+
activeAgents: [],
|
|
143
|
+
outputs: {
|
|
144
|
+
proposal: { created: false, path: `.morph/project/outputs/${featureName}/proposal.md` },
|
|
145
|
+
spec: { created: false, path: `.morph/project/outputs/${featureName}/spec.md` },
|
|
146
|
+
contracts: { created: false, path: `.morph/project/outputs/${featureName}/contracts.cs` },
|
|
147
|
+
tasks: { created: false, path: `.morph/project/outputs/${featureName}/tasks.json` },
|
|
148
|
+
uiDesignSystem: { created: false, path: `.morph/project/outputs/${featureName}/ui-design-system.md` },
|
|
149
|
+
uiMockups: { created: false, path: `.morph/project/outputs/${featureName}/ui-mockups.md` },
|
|
150
|
+
uiComponents: { created: false, path: `.morph/project/outputs/${featureName}/ui-components.md` },
|
|
151
|
+
uiFlows: { created: false, path: `.morph/project/outputs/${featureName}/ui-flows.md` },
|
|
152
|
+
decisions: { created: false, path: `.morph/project/outputs/${featureName}/decisions.md` },
|
|
153
|
+
recap: { created: false, path: `.morph/project/outputs/${featureName}/recap.md` }
|
|
154
|
+
},
|
|
155
|
+
tasks: {
|
|
156
|
+
total: 0,
|
|
157
|
+
completed: 0,
|
|
158
|
+
inProgress: 0,
|
|
159
|
+
pending: 0
|
|
160
|
+
},
|
|
161
|
+
checkpoints: [],
|
|
162
|
+
costs: {
|
|
163
|
+
estimated: 0,
|
|
164
|
+
approved: false,
|
|
165
|
+
approvedBy: null,
|
|
166
|
+
approvedAt: null
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
state.metadata.totalFeatures++;
|
|
171
|
+
saveState(state);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return state.features[featureName];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Update feature property (supports nested keys like "tasks.completed")
|
|
179
|
+
* @param {string} featureName - Feature name
|
|
180
|
+
* @param {string} key - Property key (supports dot notation)
|
|
181
|
+
* @param {any} value - Value to set
|
|
182
|
+
*/
|
|
183
|
+
export function updateFeature(featureName, key, value) {
|
|
184
|
+
ensureFeature(featureName);
|
|
185
|
+
const state = loadState(); // Load AFTER ensuring feature exists
|
|
186
|
+
|
|
187
|
+
const keys = key.split('.');
|
|
188
|
+
let target = state.features[featureName];
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
191
|
+
if (!target[keys[i]]) {
|
|
192
|
+
target[keys[i]] = {};
|
|
193
|
+
}
|
|
194
|
+
target = target[keys[i]];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const finalKey = keys[keys.length - 1];
|
|
198
|
+
target[finalKey] = value;
|
|
199
|
+
state.features[featureName].updatedAt = new Date().toISOString();
|
|
200
|
+
|
|
201
|
+
saveState(state);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Update multiple feature properties at once
|
|
206
|
+
* @param {string} featureName - Feature name
|
|
207
|
+
* @param {Object} updates - Object with key-value pairs to update
|
|
208
|
+
*/
|
|
209
|
+
export function updateFeatureMultiple(featureName, updates) {
|
|
210
|
+
ensureFeature(featureName);
|
|
211
|
+
const state = loadState(); // Load AFTER ensuring feature exists
|
|
212
|
+
|
|
213
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
214
|
+
const keys = key.split('.');
|
|
215
|
+
let target = state.features[featureName];
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
218
|
+
if (!target[keys[i]]) {
|
|
219
|
+
target[keys[i]] = {};
|
|
220
|
+
}
|
|
221
|
+
target = target[keys[i]];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const finalKey = keys[keys.length - 1];
|
|
225
|
+
target[finalKey] = value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
state.features[featureName].updatedAt = new Date().toISOString();
|
|
229
|
+
saveState(state);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add checkpoint to feature
|
|
234
|
+
* @param {string} featureName - Feature name
|
|
235
|
+
* @param {string} note - Checkpoint note
|
|
236
|
+
* @returns {Object} Checkpoint object
|
|
237
|
+
*/
|
|
238
|
+
export function addCheckpoint(featureName, note) {
|
|
239
|
+
ensureFeature(featureName);
|
|
240
|
+
const state = loadState();
|
|
241
|
+
const feature = state.features[featureName];
|
|
242
|
+
|
|
243
|
+
const checkpoint = {
|
|
244
|
+
timestamp: new Date().toISOString(),
|
|
245
|
+
phase: feature.phase,
|
|
246
|
+
completedTasks: feature.tasks.completed,
|
|
247
|
+
note: note
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
feature.checkpoints.push(checkpoint);
|
|
251
|
+
feature.updatedAt = new Date().toISOString();
|
|
252
|
+
|
|
253
|
+
saveState(state);
|
|
254
|
+
return checkpoint;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Add agent to feature
|
|
259
|
+
* @param {string} featureName - Feature name
|
|
260
|
+
* @param {string} agentId - Agent ID
|
|
261
|
+
* @returns {boolean} True if added, false if already exists
|
|
262
|
+
*/
|
|
263
|
+
export function addAgent(featureName, agentId) {
|
|
264
|
+
ensureFeature(featureName);
|
|
265
|
+
const state = loadState(); // Load AFTER ensuring feature exists
|
|
266
|
+
|
|
267
|
+
if (!state.features[featureName].activeAgents.includes(agentId)) {
|
|
268
|
+
state.features[featureName].activeAgents.push(agentId);
|
|
269
|
+
state.features[featureName].updatedAt = new Date().toISOString();
|
|
270
|
+
saveState(state);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Remove agent from feature
|
|
279
|
+
* @param {string} featureName - Feature name
|
|
280
|
+
* @param {string} agentId - Agent ID
|
|
281
|
+
* @returns {boolean} True if removed, false if not found
|
|
282
|
+
*/
|
|
283
|
+
export function removeAgent(featureName, agentId) {
|
|
284
|
+
const state = loadState();
|
|
285
|
+
|
|
286
|
+
if (!state.features[featureName]) {
|
|
287
|
+
throw new Error(`Feature '${featureName}' not found.`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const index = state.features[featureName].activeAgents.indexOf(agentId);
|
|
291
|
+
if (index > -1) {
|
|
292
|
+
state.features[featureName].activeAgents.splice(index, 1);
|
|
293
|
+
state.features[featureName].updatedAt = new Date().toISOString();
|
|
294
|
+
saveState(state);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Mark output as created
|
|
303
|
+
* @param {string} featureName - Feature name
|
|
304
|
+
* @param {string} outputType - Output type (proposal, spec, contracts, etc.)
|
|
305
|
+
*/
|
|
306
|
+
export function markOutput(featureName, outputType) {
|
|
307
|
+
ensureFeature(featureName);
|
|
308
|
+
const state = loadState();
|
|
309
|
+
|
|
310
|
+
if (!state.features[featureName].outputs[outputType]) {
|
|
311
|
+
throw new Error(`Output type '${outputType}' not valid. Valid types: proposal, spec, contracts, tasks, uiDesignSystem, uiMockups, uiComponents, uiFlows, decisions, recap`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
state.features[featureName].outputs[outputType].created = true;
|
|
315
|
+
state.features[featureName].updatedAt = new Date().toISOString();
|
|
316
|
+
|
|
317
|
+
saveState(state);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* List all features
|
|
322
|
+
* @returns {Array} Array of [featureName, featureObject] tuples
|
|
323
|
+
*/
|
|
324
|
+
export function listFeatures() {
|
|
325
|
+
const state = loadState();
|
|
326
|
+
return Object.entries(state.features);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get project summary
|
|
331
|
+
* @returns {Object} Summary with metadata
|
|
332
|
+
*/
|
|
333
|
+
export function getSummary() {
|
|
334
|
+
const state = loadState();
|
|
335
|
+
return {
|
|
336
|
+
project: state.project,
|
|
337
|
+
metadata: state.metadata,
|
|
338
|
+
featuresCount: Object.keys(state.features).length
|
|
339
|
+
};
|
|
340
|
+
}
|
package/src/utils/file-copier.js
CHANGED
|
@@ -52,3 +52,62 @@ export async function removeDir(path) {
|
|
|
52
52
|
export async function readFile(path) {
|
|
53
53
|
return fs.readFile(path, 'utf8');
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
export async function updateGitignore(projectPath) {
|
|
57
|
+
const gitignorePath = join(projectPath, '.gitignore');
|
|
58
|
+
|
|
59
|
+
const morphRules = [
|
|
60
|
+
'',
|
|
61
|
+
'# MORPH-SPEC',
|
|
62
|
+
'.morph/examples/',
|
|
63
|
+
'.morph/templates/',
|
|
64
|
+
'.claude/settings.local.json',
|
|
65
|
+
''
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
let content = '';
|
|
69
|
+
let hasMorphSection = false;
|
|
70
|
+
|
|
71
|
+
// Read existing .gitignore if it exists
|
|
72
|
+
if (await pathExists(gitignorePath)) {
|
|
73
|
+
content = await readFile(gitignorePath);
|
|
74
|
+
hasMorphSection = content.includes('# MORPH-SPEC');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If MORPH section already exists, check if rules are up to date
|
|
78
|
+
if (hasMorphSection) {
|
|
79
|
+
const lines = content.split('\n');
|
|
80
|
+
const morphStartIndex = lines.findIndex(line => line.trim() === '# MORPH-SPEC');
|
|
81
|
+
|
|
82
|
+
// Find the end of MORPH section (next empty line or section header)
|
|
83
|
+
let morphEndIndex = morphStartIndex + 1;
|
|
84
|
+
while (morphEndIndex < lines.length &&
|
|
85
|
+
lines[morphEndIndex].trim() !== '' &&
|
|
86
|
+
!lines[morphEndIndex].startsWith('#')) {
|
|
87
|
+
morphEndIndex++;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Extract current MORPH rules
|
|
91
|
+
const currentMorphRules = lines.slice(morphStartIndex, morphEndIndex + 1);
|
|
92
|
+
const expectedMorphRules = morphRules.slice(1, -1); // Remove empty lines from comparison
|
|
93
|
+
|
|
94
|
+
// Check if all expected rules are present
|
|
95
|
+
const missingRules = expectedMorphRules.filter(rule =>
|
|
96
|
+
rule.startsWith('#') || !currentMorphRules.some(line => line.trim() === rule)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (missingRules.length > 0) {
|
|
100
|
+
// Update the section with all rules
|
|
101
|
+
lines.splice(morphStartIndex, morphEndIndex - morphStartIndex, ...morphRules.slice(1, -1));
|
|
102
|
+
content = lines.join('\n');
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Add MORPH section
|
|
106
|
+
if (content && !content.endsWith('\n')) {
|
|
107
|
+
content += '\n';
|
|
108
|
+
}
|
|
109
|
+
content += morphRules.join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await writeFile(gitignorePath, content);
|
|
113
|
+
}
|