@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.
Files changed (152) hide show
  1. package/CLAUDE.md +1381 -0
  2. package/LICENSE +72 -0
  3. package/README.md +89 -6
  4. package/bin/detect-agents.js +225 -0
  5. package/bin/morph-spec.js +120 -0
  6. package/bin/render-template.js +302 -0
  7. package/bin/semantic-detect-agents.js +246 -0
  8. package/bin/validate-agents-skills.js +239 -0
  9. package/bin/validate-agents.js +69 -0
  10. package/bin/validate-phase.js +263 -0
  11. package/content/.azure/README.md +293 -0
  12. package/content/.azure/docs/azure-devops-setup.md +454 -0
  13. package/content/.azure/docs/branch-strategy.md +398 -0
  14. package/content/.azure/docs/local-development.md +515 -0
  15. package/content/.azure/pipelines/pipeline-variables.yml +34 -0
  16. package/content/.azure/pipelines/prod-pipeline.yml +319 -0
  17. package/content/.azure/pipelines/staging-pipeline.yml +234 -0
  18. package/content/.azure/pipelines/templates/build-dotnet.yml +75 -0
  19. package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -0
  20. package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -0
  21. package/content/.azure/pipelines/templates/infra-deploy.yml +90 -0
  22. package/content/.claude/commands/morph-apply.md +118 -26
  23. package/content/.claude/commands/morph-archive.md +9 -9
  24. package/content/.claude/commands/morph-clarify.md +184 -0
  25. package/content/.claude/commands/morph-design.md +275 -0
  26. package/content/.claude/commands/morph-proposal.md +56 -15
  27. package/content/.claude/commands/morph-setup.md +100 -0
  28. package/content/.claude/commands/morph-status.md +47 -32
  29. package/content/.claude/commands/morph-tasks.md +319 -0
  30. package/content/.claude/commands/morph-uiux.md +211 -0
  31. package/content/.claude/skills/specialists/ai-system-architect.md +604 -0
  32. package/content/.claude/skills/specialists/ms-agent-expert.md +143 -89
  33. package/content/.claude/skills/specialists/ui-ux-designer.md +744 -9
  34. package/content/.claude/skills/stacks/dotnet-blazor.md +244 -8
  35. package/content/.claude/skills/stacks/dotnet-nextjs.md +2 -2
  36. package/content/.morph/.morphversion +5 -0
  37. package/content/.morph/config/agents.json +101 -8
  38. package/content/.morph/config/azure-pricing.json +70 -0
  39. package/content/.morph/config/azure-pricing.schema.json +50 -0
  40. package/content/.morph/config/config.template.json +15 -3
  41. package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -0
  42. package/content/.morph/hooks/README.md +239 -0
  43. package/content/.morph/hooks/pre-commit-agents.sh +24 -0
  44. package/content/.morph/hooks/pre-commit-all.sh +48 -0
  45. package/content/.morph/hooks/pre-commit-costs.sh +91 -0
  46. package/content/.morph/hooks/pre-commit-specs.sh +49 -0
  47. package/content/.morph/hooks/pre-commit-tests.sh +60 -0
  48. package/content/.morph/project.md +5 -4
  49. package/content/.morph/schemas/agent.schema.json +296 -0
  50. package/content/.morph/standards/agent-framework-setup.md +453 -0
  51. package/content/.morph/standards/architecture.md +142 -7
  52. package/content/.morph/standards/azure.md +218 -23
  53. package/content/.morph/standards/coding.md +47 -12
  54. package/content/.morph/standards/dotnet10-migration.md +494 -0
  55. package/content/.morph/standards/fluent-ui-setup.md +590 -0
  56. package/content/.morph/standards/migration-guide.md +514 -0
  57. package/content/.morph/standards/passkeys-auth.md +423 -0
  58. package/content/.morph/standards/vector-search-rag.md +536 -0
  59. package/content/.morph/state.json +18 -0
  60. package/content/.morph/templates/FluentDesignTheme.cs +149 -0
  61. package/content/.morph/templates/MudTheme.cs +281 -0
  62. package/content/.morph/templates/contracts.cs +55 -55
  63. package/content/.morph/templates/decisions.md +4 -4
  64. package/content/.morph/templates/design-system.css +226 -0
  65. package/content/.morph/templates/infra/.dockerignore.example +89 -0
  66. package/content/.morph/templates/infra/Dockerfile.example +82 -0
  67. package/content/.morph/templates/infra/README.md +286 -0
  68. package/content/.morph/templates/infra/app-service.bicep +164 -0
  69. package/content/.morph/templates/infra/deploy.ps1 +229 -0
  70. package/content/.morph/templates/infra/deploy.sh +208 -0
  71. package/content/.morph/templates/infra/main.bicep +41 -7
  72. package/content/.morph/templates/infra/parameters.dev.json +6 -0
  73. package/content/.morph/templates/infra/parameters.prod.json +6 -0
  74. package/content/.morph/templates/infra/parameters.staging.json +29 -0
  75. package/content/.morph/templates/proposal.md +3 -3
  76. package/content/.morph/templates/recap.md +3 -3
  77. package/content/.morph/templates/spec.md +9 -8
  78. package/content/.morph/templates/sprint-status.yaml +68 -0
  79. package/content/.morph/templates/state.template.json +222 -0
  80. package/content/.morph/templates/story.md +143 -0
  81. package/content/.morph/templates/tasks.md +1 -1
  82. package/content/.morph/templates/ui-components.md +276 -0
  83. package/content/.morph/templates/ui-design-system.md +286 -0
  84. package/content/.morph/templates/ui-flows.md +336 -0
  85. package/content/.morph/templates/ui-mockups.md +133 -0
  86. package/content/.morph/test-infra/example.bicep +59 -0
  87. package/content/CLAUDE.md +124 -0
  88. package/content/README.md +79 -0
  89. package/detectors/config-detector.js +223 -0
  90. package/detectors/conversation-analyzer.js +163 -0
  91. package/detectors/index.js +84 -0
  92. package/detectors/standards-generator.js +275 -0
  93. package/detectors/structure-detector.js +221 -0
  94. package/docs/README.md +149 -0
  95. package/docs/api/cost-calculator.js.html +513 -0
  96. package/docs/api/design-system-generator.js.html +382 -0
  97. package/docs/api/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  98. package/docs/api/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  99. package/docs/api/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  100. package/docs/api/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  101. package/docs/api/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  102. package/docs/api/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  103. package/docs/api/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  104. package/docs/api/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  105. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  106. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  107. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  108. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  109. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  110. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  111. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  112. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  113. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  114. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  115. package/docs/api/global.html +5263 -0
  116. package/docs/api/index.html +96 -0
  117. package/docs/api/scripts/collapse.js +39 -0
  118. package/docs/api/scripts/commonNav.js +28 -0
  119. package/docs/api/scripts/linenumber.js +25 -0
  120. package/docs/api/scripts/nav.js +12 -0
  121. package/docs/api/scripts/polyfill.js +4 -0
  122. package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
  123. package/docs/api/scripts/prettify/lang-css.js +2 -0
  124. package/docs/api/scripts/prettify/prettify.js +28 -0
  125. package/docs/api/scripts/search.js +99 -0
  126. package/docs/api/state-manager.js.html +423 -0
  127. package/docs/api/styles/jsdoc.css +776 -0
  128. package/docs/api/styles/prettify.css +80 -0
  129. package/docs/examples.md +328 -0
  130. package/docs/getting-started.md +302 -0
  131. package/docs/installation.md +361 -0
  132. package/docs/templates.md +418 -0
  133. package/docs/validation-checklist.md +266 -0
  134. package/package.json +39 -12
  135. package/src/commands/cost.js +181 -0
  136. package/src/commands/create-story.js +283 -0
  137. package/src/commands/detect.js +104 -0
  138. package/src/commands/doctor.js +67 -0
  139. package/src/commands/generate.js +149 -0
  140. package/src/commands/init.js +69 -45
  141. package/src/commands/shard-spec.js +224 -0
  142. package/src/commands/sprint-status.js +250 -0
  143. package/src/commands/state.js +333 -0
  144. package/src/commands/sync.js +167 -0
  145. package/src/commands/update-pricing.js +206 -0
  146. package/src/commands/update.js +88 -13
  147. package/src/lib/complexity-analyzer.js +292 -0
  148. package/src/lib/cost-calculator.js +429 -0
  149. package/src/lib/design-system-generator.js +298 -0
  150. package/src/lib/state-manager.js +340 -0
  151. package/src/utils/file-copier.js +59 -0
  152. 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
+ }
@@ -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
+ }