@polymorphism-tech/morph-spec 1.0.2 → 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.
- package/CLAUDE.md +1381 -0
- package/LICENSE +72 -0
- package/README.md +114 -12
- 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 +71 -46
- 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 +63 -0
- package/src/utils/version-checker.js +175 -0
package/src/commands/doctor.js
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -10,12 +10,15 @@ import {
|
|
|
10
10
|
readJson,
|
|
11
11
|
writeJson,
|
|
12
12
|
ensureDir,
|
|
13
|
-
writeFile
|
|
13
|
+
writeFile,
|
|
14
|
+
readFile,
|
|
15
|
+
updateGitignore
|
|
14
16
|
} from '../utils/file-copier.js';
|
|
17
|
+
import { saveProjectMorphVersion, getInstalledCLIVersion } from '../utils/version-checker.js';
|
|
15
18
|
|
|
16
19
|
export async function initCommand(options) {
|
|
17
20
|
const targetPath = options.path || process.cwd();
|
|
18
|
-
const
|
|
21
|
+
const claudeMdPath = join(import.meta.dirname, '..', '..', 'CLAUDE.md');
|
|
19
22
|
|
|
20
23
|
logger.header('MORPH-SPEC Framework Installation');
|
|
21
24
|
logger.dim(`Target: ${targetPath}`);
|
|
@@ -37,80 +40,102 @@ export async function initCommand(options) {
|
|
|
37
40
|
try {
|
|
38
41
|
// 1. Copy CLAUDE.md
|
|
39
42
|
spinner.text = 'Copying CLAUDE.md...';
|
|
40
|
-
const claudeMdSrc = join(contentDir, 'CLAUDE.md');
|
|
41
43
|
const claudeMdDest = join(targetPath, 'CLAUDE.md');
|
|
42
44
|
|
|
43
45
|
// Backup existing CLAUDE.md if not from MORPH
|
|
44
46
|
if (await pathExists(claudeMdDest)) {
|
|
45
|
-
const existingContent = await
|
|
47
|
+
const existingContent = await readFile(claudeMdDest);
|
|
46
48
|
if (!existingContent.includes('MORPH-SPEC')) {
|
|
47
49
|
await copyFile(claudeMdDest, `${claudeMdDest}.backup`);
|
|
48
50
|
logger.dim('Backed up existing CLAUDE.md');
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
|
-
await copyFile(
|
|
52
|
-
|
|
53
|
-
// 2. Copy .morph folder
|
|
54
|
-
spinner.text = 'Copying .morph/...';
|
|
55
|
-
const morphSrc = join(contentDir, '.morph');
|
|
56
|
-
await copyDirectory(morphSrc, morphPath);
|
|
57
|
-
|
|
58
|
-
// 3. Create empty directories with .gitkeep
|
|
59
|
-
for (const dir of ['specs', 'features', 'archive']) {
|
|
60
|
-
const dirPath = join(morphPath, dir);
|
|
61
|
-
await ensureDir(dirPath);
|
|
62
|
-
const gitkeepPath = join(dirPath, '.gitkeep');
|
|
63
|
-
if (!(await pathExists(gitkeepPath))) {
|
|
64
|
-
await writeFile(gitkeepPath, '');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
53
|
+
await copyFile(claudeMdPath, claudeMdDest);
|
|
67
54
|
|
|
68
|
-
//
|
|
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
|
|
69
63
|
spinner.text = 'Generating config.json...';
|
|
70
|
-
const configPath = join(morphPath, 'config
|
|
71
|
-
const
|
|
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
|
+
};
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
const config = await readJson(templatePath);
|
|
77
|
+
await writeJson(configPath, config);
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
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
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
+
## Overview
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
spinner.text = 'Copying .claude/...';
|
|
87
|
-
const claudeSrc = join(contentDir, '.claude');
|
|
88
|
-
const claudeDest = join(targetPath, '.claude');
|
|
86
|
+
This file will be auto-updated by MORPH-SPEC detection system.
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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);
|
|
93
110
|
|
|
94
111
|
spinner.succeed('MORPH-SPEC installed successfully!');
|
|
95
112
|
|
|
96
113
|
// Show next steps
|
|
97
114
|
logger.blank();
|
|
115
|
+
logger.success(`MORPH-SPEC v${cliVersion} installed successfully!`);
|
|
116
|
+
logger.blank();
|
|
98
117
|
logger.header('Next Steps');
|
|
99
118
|
|
|
100
|
-
logger.step(1, '
|
|
101
|
-
logger.step(2, 'Review .morph/config/
|
|
119
|
+
logger.step(1, 'Run detection: morph-spec detect');
|
|
120
|
+
logger.step(2, 'Review .morph/config.json and .morph/project/standards/inferred.md');
|
|
102
121
|
logger.step(3, 'Open project in VS Code with Claude Code');
|
|
103
122
|
logger.step(4, 'Start your first feature:');
|
|
104
123
|
logger.blank();
|
|
105
124
|
logger.box([
|
|
106
|
-
'
|
|
125
|
+
'Ask Claude Code to implement a feature'
|
|
107
126
|
]);
|
|
108
127
|
logger.blank();
|
|
109
128
|
|
|
110
|
-
logger.info('
|
|
111
|
-
logger.dim(`CLAUDE.md
|
|
112
|
-
logger.dim(
|
|
113
|
-
logger.dim(
|
|
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`);
|
|
114
139
|
logger.blank();
|
|
115
140
|
|
|
116
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
|
+
}
|