@polymorphism-tech/morph-spec 1.0.4 → 2.0.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
@@ -2,6 +2,11 @@ import { join } from 'path';
2
2
  import chalk from 'chalk';
3
3
  import { logger } from '../utils/logger.js';
4
4
  import { pathExists, readJson } from '../utils/file-copier.js';
5
+ import {
6
+ checkCLIOutdated,
7
+ checkProjectOutdated,
8
+ getInstalledCLIVersion
9
+ } from '../utils/version-checker.js';
5
10
 
6
11
  export async function doctorCommand() {
7
12
  const targetPath = process.cwd();
@@ -10,6 +15,58 @@ export async function doctorCommand() {
10
15
 
11
16
  const checks = [];
12
17
  let hasErrors = false;
18
+ let hasWarnings = false;
19
+
20
+ // Check versions first
21
+ const cliCheck = await checkCLIOutdated();
22
+ const projectCheck = await checkProjectOutdated(targetPath);
23
+
24
+ // CLI Version
25
+ if (cliCheck.latest) {
26
+ if (cliCheck.isOutdated) {
27
+ checks.push({
28
+ name: `CLI version (${cliCheck.current})`,
29
+ status: 'warn',
30
+ msg: `outdated, ${cliCheck.latest} available`
31
+ });
32
+ hasWarnings = true;
33
+ } else {
34
+ checks.push({
35
+ name: `CLI version (${cliCheck.current})`,
36
+ status: 'ok',
37
+ msg: 'latest'
38
+ });
39
+ }
40
+ } else {
41
+ checks.push({
42
+ name: `CLI version (${cliCheck.current})`,
43
+ status: 'ok'
44
+ });
45
+ }
46
+
47
+ // Project MORPH version
48
+ if (projectCheck.current) {
49
+ if (projectCheck.isOutdated) {
50
+ checks.push({
51
+ name: `Project MORPH version (${projectCheck.current})`,
52
+ status: 'warn',
53
+ msg: `outdated, ${projectCheck.cliVersion} available`
54
+ });
55
+ hasWarnings = true;
56
+ } else {
57
+ checks.push({
58
+ name: `Project MORPH version (${projectCheck.current})`,
59
+ status: 'ok'
60
+ });
61
+ }
62
+ } else {
63
+ checks.push({
64
+ name: 'Project MORPH version',
65
+ status: 'warn',
66
+ msg: 'not found (legacy installation)'
67
+ });
68
+ hasWarnings = true;
69
+ }
13
70
 
14
71
  // Check CLAUDE.md
15
72
  const claudeMd = join(targetPath, 'CLAUDE.md');
@@ -126,6 +183,16 @@ export async function doctorCommand() {
126
183
  if (hasErrors) {
127
184
  logger.error('Some checks failed. Run "morph-spec init --force" to fix.');
128
185
  process.exit(1);
186
+ } else if (hasWarnings) {
187
+ logger.warn('Some checks need attention.');
188
+ logger.blank();
189
+
190
+ if (cliCheck.isOutdated || projectCheck.isOutdated) {
191
+ logger.info('To update:');
192
+ logger.dim(' 1. Update the CLI: npm install -g @polymorphism-tech/morph-spec@latest');
193
+ logger.dim(' 2. Update the project: morph-spec update');
194
+ logger.blank();
195
+ }
129
196
  } else {
130
197
  logger.success('All checks passed!');
131
198
  }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * MORPH-SPEC Generate Command
3
+ * CLI wrapper for code generation operations
4
+ */
5
+
6
+ import { mkdirSync, writeFileSync } from 'fs';
7
+ import { dirname } from 'path';
8
+ import ora from 'ora';
9
+ import chalk from 'chalk';
10
+ import { logger } from '../utils/logger.js';
11
+ import * as DesignSystemGenerator from '../lib/design-system-generator.js';
12
+
13
+ // ============================================================================
14
+ // Design System Subcommand
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Generate design system files (CSS + themes) from ui-design-system.md
19
+ * @param {string} designSystemPath - Path to ui-design-system.md
20
+ * @param {Object} options - CLI options
21
+ */
22
+ export async function generateDesignSystemCommand(designSystemPath, options) {
23
+ if (!designSystemPath) {
24
+ logger.error('Design system file path required');
25
+ logger.dim(' Usage: morph-spec generate design-system <ui-design-system.md>');
26
+ logger.blank();
27
+ logger.dim(' Example:');
28
+ logger.dim(' morph-spec generate design-system .morph/project/outputs/my-feature/ui-design-system.md');
29
+ process.exit(1);
30
+ }
31
+
32
+ logger.header('MORPH-SPEC Design System Generator');
33
+ logger.blank();
34
+
35
+ const spinner = ora('Parsing design system...').start();
36
+
37
+ try {
38
+ // Determine mode
39
+ let mode = 'both';
40
+ if (options.fluent && !options.mud) mode = 'fluent';
41
+ if (options.mud && !options.fluent) mode = 'mud';
42
+
43
+ // Generate design system
44
+ const generated = DesignSystemGenerator.generateDesignSystem(designSystemPath, {
45
+ mode,
46
+ namespace: options.namespace || 'YourProject.Themes'
47
+ });
48
+
49
+ spinner.succeed('Design system parsed!');
50
+ logger.blank();
51
+
52
+ // Display stats
53
+ logger.header('Parsed Design System:');
54
+ logger.info(`Primary Colors: ${chalk.cyan(generated.stats.primaryColors)}`);
55
+ logger.info(`Neutral Colors: ${chalk.cyan(generated.stats.neutralColors)}`);
56
+ logger.info(`Semantic Colors: ${chalk.cyan(generated.stats.semanticColors)}`);
57
+ logger.info(`Font Sizes: ${chalk.cyan(generated.stats.fontSizes)}`);
58
+ logger.info(`Spacing Values: ${chalk.cyan(generated.stats.spacingValues)}`);
59
+ logger.blank();
60
+
61
+ if (options.dryRun) {
62
+ logger.warn('Dry run - files not written');
63
+ logger.blank();
64
+ logger.header('Would generate:');
65
+ logger.dim(' - wwwroot/css/design-system.css');
66
+ if (generated.fluentTheme) logger.dim(' - Themes/FluentDesignTheme.cs');
67
+ if (generated.mudTheme) logger.dim(' - Themes/MudDesignTheme.cs');
68
+ logger.blank();
69
+ return;
70
+ }
71
+
72
+ // Write files
73
+ const writeSpinner = ora('Writing files...').start();
74
+
75
+ // Write CSS
76
+ const cssPath = options.cssOutput || 'wwwroot/css/design-system.css';
77
+ mkdirSync(dirname(cssPath), { recursive: true });
78
+ writeFileSync(cssPath, generated.css);
79
+ writeSpinner.text = `Created ${cssPath}`;
80
+
81
+ // Write Fluent theme
82
+ if (generated.fluentTheme) {
83
+ const fluentPath = options.fluentOutput || 'Themes/FluentDesignTheme.cs';
84
+ mkdirSync(dirname(fluentPath), { recursive: true });
85
+ writeFileSync(fluentPath, generated.fluentTheme);
86
+ writeSpinner.text = `Created ${fluentPath}`;
87
+ }
88
+
89
+ // Write MudBlazor theme
90
+ if (generated.mudTheme) {
91
+ const mudPath = options.mudOutput || 'Themes/MudDesignTheme.cs';
92
+ mkdirSync(dirname(mudPath), { recursive: true });
93
+ writeFileSync(mudPath, generated.mudTheme);
94
+ writeSpinner.text = `Created ${mudPath}`;
95
+ }
96
+
97
+ writeSpinner.succeed('Files generated!');
98
+ logger.blank();
99
+
100
+ // List generated files
101
+ logger.header('Generated Files:');
102
+ logger.success(` ✓ ${cssPath}`);
103
+ if (generated.fluentTheme) logger.success(` ✓ ${options.fluentOutput || 'Themes/FluentDesignTheme.cs'}`);
104
+ if (generated.mudTheme) logger.success(` ✓ ${options.mudOutput || 'Themes/MudDesignTheme.cs'}`);
105
+ logger.blank();
106
+
107
+ // Next steps
108
+ logger.header('Next Steps:');
109
+ logger.dim(' 1. Reference design-system.css in your layout');
110
+ logger.dim(' 2. Register theme in Program.cs');
111
+ logger.dim(' 3. Apply theme to your components');
112
+ logger.blank();
113
+
114
+ } catch (error) {
115
+ spinner.fail('Generation failed');
116
+ logger.error(error.message);
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ // ============================================================================
122
+ // Main Generate Command (router for future subcommands)
123
+ // ============================================================================
124
+
125
+ /**
126
+ * Main generate command router
127
+ * Future: Could add more generation commands here
128
+ */
129
+ export async function generateCommand(subcommand, args, options) {
130
+ switch (subcommand) {
131
+ case 'design-system':
132
+ await generateDesignSystemCommand(args[0], options);
133
+ break;
134
+
135
+ // Future: Add more generation commands
136
+ // case 'component':
137
+ // await generateComponentCommand(args[0], options);
138
+ // break;
139
+
140
+ default:
141
+ logger.error(`Unknown subcommand: ${subcommand}`);
142
+ logger.blank();
143
+ logger.info('Available subcommands:');
144
+ logger.dim(' design-system Generate CSS + theme files from ui-design-system.md');
145
+ logger.blank();
146
+ logger.dim('Run "morph-spec generate --help" for more information');
147
+ process.exit(1);
148
+ }
149
+ }
@@ -11,12 +11,14 @@ import {
11
11
  writeJson,
12
12
  ensureDir,
13
13
  writeFile,
14
- readFile
14
+ readFile,
15
+ updateGitignore
15
16
  } from '../utils/file-copier.js';
17
+ import { saveProjectMorphVersion, getInstalledCLIVersion } from '../utils/version-checker.js';
16
18
 
17
19
  export async function initCommand(options) {
18
20
  const targetPath = options.path || process.cwd();
19
- const contentDir = getContentDir();
21
+ const claudeMdPath = join(import.meta.dirname, '..', '..', 'CLAUDE.md');
20
22
 
21
23
  logger.header('MORPH-SPEC Framework Installation');
22
24
  logger.dim(`Target: ${targetPath}`);
@@ -38,7 +40,6 @@ export async function initCommand(options) {
38
40
  try {
39
41
  // 1. Copy CLAUDE.md
40
42
  spinner.text = 'Copying CLAUDE.md...';
41
- const claudeMdSrc = join(contentDir, 'CLAUDE.md');
42
43
  const claudeMdDest = join(targetPath, 'CLAUDE.md');
43
44
 
44
45
  // Backup existing CLAUDE.md if not from MORPH
@@ -49,69 +50,92 @@ export async function initCommand(options) {
49
50
  logger.dim('Backed up existing CLAUDE.md');
50
51
  }
51
52
  }
52
- await copyFile(claudeMdSrc, claudeMdDest);
53
-
54
- // 2. Copy .morph folder
55
- spinner.text = 'Copying .morph/...';
56
- const morphSrc = join(contentDir, '.morph');
57
- await copyDirectory(morphSrc, morphPath);
58
-
59
- // 3. Create empty directories with .gitkeep
60
- for (const dir of ['specs', 'features', 'archive']) {
61
- const dirPath = join(morphPath, dir);
62
- await ensureDir(dirPath);
63
- const gitkeepPath = join(dirPath, '.gitkeep');
64
- if (!(await pathExists(gitkeepPath))) {
65
- await writeFile(gitkeepPath, '');
66
- }
67
- }
53
+ await copyFile(claudeMdPath, claudeMdDest);
68
54
 
69
- // 4. Create config.json from template
55
+ // 2. Create .morph/project structure
56
+ spinner.text = 'Creating .morph/project structure...';
57
+ const projectPath = join(morphPath, 'project');
58
+ await ensureDir(join(projectPath, 'context'));
59
+ await ensureDir(join(projectPath, 'standards'));
60
+ await ensureDir(join(projectPath, 'outputs'));
61
+
62
+ // 3. Create config.json
70
63
  spinner.text = 'Generating config.json...';
71
- const configPath = join(morphPath, 'config', 'config.json');
72
- const templatePath = join(morphPath, 'config', 'config.template.json');
64
+ const configPath = join(morphPath, 'config.json');
65
+ const dirName = targetPath.split(/[\\/]/).pop();
66
+
67
+ const config = {
68
+ framework: 'global',
69
+ frameworkVersion: getInstalledCLIVersion(),
70
+ project: {
71
+ name: dirName,
72
+ stack: 'unknown',
73
+ architecture: 'unknown'
74
+ }
75
+ };
73
76
 
74
- if (await pathExists(templatePath)) {
75
- const config = await readJson(templatePath);
77
+ await writeJson(configPath, config);
76
78
 
77
- // Try to detect project name from directory or .sln
78
- const dirName = targetPath.split(/[\\/]/).pop();
79
- config.project = config.project || {};
80
- config.project.name = dirName;
81
- config.project.description = `Projeto ${dirName} gerenciado pelo MORPH-SPEC Framework`;
79
+ // 4. Create context/README.md
80
+ spinner.text = 'Creating context README...';
81
+ const contextReadme = join(projectPath, 'context', 'README.md');
82
+ const readmeContent = `# ${dirName} - Project Context
82
83
 
83
- await writeJson(configPath, config);
84
- }
84
+ ## Overview
85
85
 
86
- // 5. Copy .claude folder
87
- spinner.text = 'Copying .claude/...';
88
- const claudeSrc = join(contentDir, '.claude');
89
- const claudeDest = join(targetPath, '.claude');
86
+ This file will be auto-updated by MORPH-SPEC detection system.
90
87
 
91
- if (await pathExists(claudeSrc)) {
92
- await copyDirectory(claudeSrc, claudeDest);
93
- }
88
+ ## Stack
89
+
90
+ Run \`morph-spec detect\` to analyze your project.
91
+
92
+ ## Technologies
93
+
94
+ (To be detected)
95
+
96
+ ## Architecture
97
+
98
+ (To be detected)
99
+ `;
100
+ await writeFile(contextReadme, readmeContent);
101
+
102
+ // 5. Save version info
103
+ spinner.text = 'Saving version info...';
104
+ const cliVersion = getInstalledCLIVersion();
105
+ await saveProjectMorphVersion(targetPath, cliVersion);
106
+
107
+ // 6. Update .gitignore
108
+ spinner.text = 'Updating .gitignore...';
109
+ await updateGitignore(targetPath);
94
110
 
95
111
  spinner.succeed('MORPH-SPEC installed successfully!');
96
112
 
97
113
  // Show next steps
98
114
  logger.blank();
115
+ logger.success(`MORPH-SPEC v${cliVersion} installed successfully!`);
116
+ logger.blank();
99
117
  logger.header('Next Steps');
100
118
 
101
- logger.step(1, 'Edit .morph/project.md with your project details');
102
- logger.step(2, 'Review .morph/config/config.json');
119
+ logger.step(1, 'Run detection: morph-spec detect');
120
+ logger.step(2, 'Review .morph/config.json and .morph/project/standards/inferred.md');
103
121
  logger.step(3, 'Open project in VS Code with Claude Code');
104
122
  logger.step(4, 'Start your first feature:');
105
123
  logger.blank();
106
124
  logger.box([
107
- '/morph-proposal [feature-name]'
125
+ 'Ask Claude Code to implement a feature'
108
126
  ]);
109
127
  logger.blank();
110
128
 
111
- logger.info('Documentation:');
112
- logger.dim(`CLAUDE.md → ${join(targetPath, 'CLAUDE.md')}`);
113
- logger.dim(`Standards ${join(targetPath, '.morph', 'standards')}`);
114
- logger.dim(`Templates ${join(targetPath, '.morph', 'templates')}`);
129
+ logger.info('Structure created:');
130
+ logger.dim(`CLAUDE.md → ${join(targetPath, 'CLAUDE.md')}`);
131
+ logger.dim(`.morph/config.json Project config`);
132
+ logger.dim(`.morph/project/context/ Project context`);
133
+ logger.dim(`.morph/project/standards/→ Project standards`);
134
+ logger.dim(`.morph/project/outputs/ → Feature outputs`);
135
+ logger.blank();
136
+
137
+ logger.info('Content (via npm package):');
138
+ logger.dim(`Templates and standards available via morph-spec update`);
115
139
  logger.blank();
116
140
 
117
141
  } catch (error) {
@@ -0,0 +1,224 @@
1
+ /**
2
+ * MORPH-SPEC Document Sharding
3
+ * Splits large spec.md files into manageable shards (BMAD-inspired)
4
+ * Achieves 90% token savings by loading only relevant sections
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import ora from 'ora';
10
+ import chalk from 'chalk';
11
+ import { logger } from '../utils/logger.js';
12
+ import { ensureDir, writeFile } from '../utils/file-copier.js';
13
+
14
+ // ============================================================================
15
+ // Helper Functions
16
+ // ============================================================================
17
+
18
+ function parseSpec(specContent) {
19
+ const sections = [];
20
+ const lines = specContent.split('\n');
21
+
22
+ let currentSection = null;
23
+ let currentContent = [];
24
+
25
+ for (const line of lines) {
26
+ // Detect level 2 heading (## Heading)
27
+ if (line.match(/^## /)) {
28
+ // Save previous section
29
+ if (currentSection) {
30
+ sections.push({
31
+ title: currentSection,
32
+ slug: toSlug(currentSection),
33
+ content: currentContent.join('\n').trim(),
34
+ });
35
+ }
36
+
37
+ // Start new section
38
+ currentSection = line.replace(/^## /, '').trim();
39
+ currentContent = [line]; // Include heading in content
40
+ } else {
41
+ currentContent.push(line);
42
+ }
43
+ }
44
+
45
+ // Save last section
46
+ if (currentSection) {
47
+ sections.push({
48
+ title: currentSection,
49
+ slug: toSlug(currentSection),
50
+ content: currentContent.join('\n').trim(),
51
+ });
52
+ }
53
+
54
+ return sections;
55
+ }
56
+
57
+ function toSlug(title) {
58
+ return title
59
+ .toLowerCase()
60
+ .replace(/[^a-z0-9]+/g, '-')
61
+ .replace(/^-|-$/g, '');
62
+ }
63
+
64
+ function generateIndex(sections, featureName) {
65
+ let index = `# ${toTitleCase(featureName)} - Technical Specification\n\n`;
66
+ index += `> **Sharded Spec:** This spec has been split into sections for optimal token usage\n`;
67
+ index += `> **BMAD Pattern:** Load only the section you need during implementation\n\n`;
68
+ index += `---\n\n`;
69
+ index += `## 📋 Table of Contents\n\n`;
70
+
71
+ sections.forEach((section, i) => {
72
+ index += `${i + 1}. **[${section.title}](${section.slug}.md)**\n`;
73
+
74
+ // Add first 100 chars as description
75
+ const firstLine = section.content.split('\n').find(l => l.trim() && !l.startsWith('#'));
76
+ const description = firstLine ? firstLine.substring(0, 100) + '...' : 'See section for details';
77
+ index += ` > ${description}\n\n`;
78
+ });
79
+
80
+ index += `---\n\n`;
81
+ index += `## 💡 How to Use Sharded Specs\n\n`;
82
+ index += `### In Planning Phases (1-3)\n`;
83
+ index += `Load **all sections** to get complete context:\n`;
84
+ index += `\`\`\`bash\n`;
85
+ index += `# Read index.md + all section files\n`;
86
+ index += `\`\`\`\n\n`;
87
+
88
+ index += `### In Implementation Phase (4)\n`;
89
+ index += `Load **only the relevant section** for your current story:\n`;
90
+ index += `\`\`\`bash\n`;
91
+ index += `# Example: Working on entity design story\n`;
92
+ index += `# Read: spec/index.md + spec/entity-design.md (not the other 10 sections!)\n`;
93
+ index += `\`\`\`\n\n`;
94
+
95
+ index += `**Token Savings:** For a 10-section spec (30k tokens), selective loading = **90% savings**\n\n`;
96
+ index += `---\n\n`;
97
+ index += `*Generated by MORPH-SPEC Framework - Document Sharding*\n`;
98
+ index += `*Inspired by BMAD Method: 90% token reduction for large specs*\n`;
99
+
100
+ return index;
101
+ }
102
+
103
+ function toTitleCase(str) {
104
+ return str
105
+ .split('-')
106
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
107
+ .join(' ');
108
+ }
109
+
110
+ function estimateTokens(text) {
111
+ // Rough estimate: 1 token ≈ 4 characters
112
+ return Math.ceil(text.length / 4);
113
+ }
114
+
115
+ // ============================================================================
116
+ // Command Function
117
+ // ============================================================================
118
+
119
+ export async function shardSpecCommand(feature, options) {
120
+ logger.header('MORPH-SPEC Document Sharding');
121
+ logger.dim(`Feature: ${feature}`);
122
+ logger.blank();
123
+
124
+ const spinner = ora('Analyzing spec.md...').start();
125
+
126
+ try {
127
+ // Find spec.md
128
+ const specPath = path.join(process.cwd(), `.morph/project/outputs/${feature}/spec.md`);
129
+ if (!fs.existsSync(specPath)) {
130
+ throw new Error(`Spec not found: ${specPath}`);
131
+ }
132
+
133
+ // Read spec content
134
+ const specContent = fs.readFileSync(specPath, 'utf-8');
135
+ const totalTokens = estimateTokens(specContent);
136
+
137
+ spinner.info(`Original spec.md: ~${totalTokens.toLocaleString()} tokens`);
138
+ logger.blank();
139
+
140
+ // Parse into sections
141
+ const sections = parseSpec(specContent);
142
+
143
+ if (sections.length < 3) {
144
+ spinner.warn(`Spec has only ${sections.length} sections - sharding not recommended`);
145
+ logger.dim(' Sharding is most beneficial for specs with 5+ sections');
146
+ return;
147
+ }
148
+
149
+ spinner.succeed(`Detected ${sections.length} sections`);
150
+ logger.blank();
151
+
152
+ // Generate index
153
+ const indexContent = generateIndex(sections, feature);
154
+
155
+ // Calculate savings
156
+ const avgShardTokens = sections.reduce((sum, s) => sum + estimateTokens(s.content), 0) / sections.length;
157
+ const savingsPercent = Math.round(((totalTokens - avgShardTokens) / totalTokens) * 100);
158
+
159
+ logger.header('Token Savings (selective load):');
160
+ logger.info(` ${chalk.green(`~${savingsPercent}%`)} reduction`);
161
+ logger.dim(` - Full load: ~${totalTokens.toLocaleString()} tokens`);
162
+ logger.dim(` - Selective load: ~${Math.round(avgShardTokens).toLocaleString()} tokens (index + 1 shard)`);
163
+ logger.blank();
164
+
165
+ if (options.verbose) {
166
+ logger.header('Shard Details:');
167
+ logger.blank();
168
+ sections.forEach((section, i) => {
169
+ const tokens = estimateTokens(section.content);
170
+ logger.dim(` ${i + 1}. ${section.title}`);
171
+ logger.dim(` File: ${section.slug}.md`);
172
+ logger.dim(` Tokens: ~${tokens.toLocaleString()}`);
173
+ logger.blank();
174
+ });
175
+ }
176
+
177
+ // Output
178
+ const outputDir = path.join(process.cwd(), `.morph/project/outputs/${feature}/spec`);
179
+
180
+ if (options.dryRun) {
181
+ logger.header('DRY RUN - Would create:');
182
+ logger.dim(` ${outputDir}/index.md`);
183
+ sections.forEach(s => logger.dim(` ${outputDir}/${s.slug}.md`));
184
+ logger.blank();
185
+ logger.success('Run without --dry-run to create files');
186
+ } else {
187
+ const createSpinner = ora('Creating sharded spec files...').start();
188
+
189
+ // Create directory
190
+ await ensureDir(outputDir);
191
+
192
+ // Write index
193
+ await writeFile(path.join(outputDir, 'index.md'), indexContent);
194
+ createSpinner.text = 'Created index.md';
195
+
196
+ // Write shards
197
+ for (const section of sections) {
198
+ const shardPath = path.join(outputDir, `${section.slug}.md`);
199
+ await writeFile(shardPath, section.content);
200
+ createSpinner.text = `Created ${section.slug}.md`;
201
+ }
202
+
203
+ createSpinner.succeed('Sharded spec created!');
204
+ logger.blank();
205
+
206
+ // Archive original spec.md
207
+ const archivePath = specPath.replace('.md', '.original.md');
208
+ fs.renameSync(specPath, archivePath);
209
+ logger.dim(' Archived original: spec.original.md');
210
+ logger.blank();
211
+
212
+ logger.header('Next steps:');
213
+ logger.dim(` 1. Review sharded spec in .morph/project/outputs/${feature}/spec/`);
214
+ logger.dim(` 2. Create stories: morph-spec story create ${feature} SR-001`);
215
+ logger.dim(' 3. Stories will auto-load only relevant shards (90% token savings)');
216
+ logger.blank();
217
+ }
218
+
219
+ } catch (error) {
220
+ spinner.fail('Sharding failed');
221
+ logger.error(error.message);
222
+ process.exit(1);
223
+ }
224
+ }