@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.
Files changed (152) hide show
  1. package/CLAUDE.md +1381 -0
  2. package/LICENSE +72 -0
  3. package/README.md +114 -12
  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 +71 -46
  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 +63 -0
  152. package/src/utils/version-checker.js +175 -0
@@ -0,0 +1,167 @@
1
+ import { join } from 'path';
2
+ import { readdirSync, existsSync } from 'fs';
3
+ import ora from 'ora';
4
+ import chalk from 'chalk';
5
+ import { logger } from '../utils/logger.js';
6
+ import { readFile, writeFile, ensureDir } from '../utils/file-copier.js';
7
+ import { analyzeConversation } from '../../detectors/conversation-analyzer.js';
8
+
9
+ export async function syncCommand(options) {
10
+ const targetPath = options.path || process.cwd();
11
+
12
+ logger.header('MORPH-SPEC Standards Sync');
13
+ logger.dim(`Project: ${targetPath}`);
14
+ logger.blank();
15
+
16
+ const spinner = ora('Analyzing feature decisions...').start();
17
+
18
+ try {
19
+ // 1. Find all decisions.md files
20
+ const outputsPath = join(targetPath, '.morph', 'project', 'outputs');
21
+
22
+ if (!existsSync(outputsPath)) {
23
+ spinner.warn('No features found');
24
+ logger.dim('Create features first using MORPH workflow.');
25
+ return;
26
+ }
27
+
28
+ const features = readdirSync(outputsPath, { withFileTypes: true })
29
+ .filter(dirent => dirent.isDirectory())
30
+ .map(dirent => dirent.name);
31
+
32
+ if (features.length === 0) {
33
+ spinner.warn('No features found');
34
+ return;
35
+ }
36
+
37
+ spinner.text = `Found ${features.length} features, analyzing decisions...`;
38
+
39
+ // 2. Analyze all decisions
40
+ const conversation = await analyzeConversation(targetPath);
41
+
42
+ if (conversation.decisions.length === 0) {
43
+ spinner.warn('No decisions found to sync');
44
+ logger.dim('Features may not have decisions.md files yet.');
45
+ return;
46
+ }
47
+
48
+ spinner.succeed(`Found ${conversation.decisions.length} decisions`);
49
+ logger.blank();
50
+
51
+ // 3. Group by category
52
+ const byCategory = groupByCategory(conversation.decisions);
53
+
54
+ // 4. Display candidates
55
+ logger.header('Decisions by Category:');
56
+ logger.blank();
57
+
58
+ let totalCandidates = 0;
59
+
60
+ for (const [category, decisions] of Object.entries(byCategory)) {
61
+ if (decisions.length > 0) {
62
+ logger.info(chalk.cyan(`${formatCategory(category)} (${decisions.length})`));
63
+ decisions.slice(0, 3).forEach(d => {
64
+ const preview = d.text.split('\n')[0].substring(0, 70);
65
+ logger.dim(` - ${preview}...`);
66
+ });
67
+ if (decisions.length > 3) {
68
+ logger.dim(` ... and ${decisions.length - 3} more`);
69
+ }
70
+ logger.blank();
71
+ totalCandidates += decisions.length;
72
+ }
73
+ }
74
+
75
+ // 5. Sync (for now, just report - interactive approval would be added later)
76
+ if (options.dryRun) {
77
+ logger.warn('Dry run mode - no files updated');
78
+ logger.dim(`Would update ${Object.keys(byCategory).length} standard files`);
79
+ return;
80
+ }
81
+
82
+ spinner.start('Updating standards...');
83
+
84
+ const standardsDir = join(targetPath, '.morph', 'project', 'standards');
85
+ await ensureDir(standardsDir);
86
+
87
+ let updatedFiles = [];
88
+
89
+ for (const [category, decisions] of Object.entries(byCategory)) {
90
+ if (decisions.length === 0) continue;
91
+
92
+ const fileName = `${category}.md`;
93
+ const filePath = join(standardsDir, fileName);
94
+
95
+ // Read existing or create new
96
+ let content = '';
97
+ if (existsSync(filePath)) {
98
+ content = await readFile(filePath);
99
+ } else {
100
+ content = `# ${formatCategory(category)} Standards\n\n> Project-specific standards for ${category}\n\n`;
101
+ }
102
+
103
+ // Append new decisions
104
+ content += `\n## Decisions from Features (${new Date().toISOString().split('T')[0]})\n\n`;
105
+
106
+ decisions.forEach((d, idx) => {
107
+ const preview = d.text.split('\n').slice(0, 5).join('\n');
108
+ content += `### Decision ${idx + 1}\n\n${preview}\n\n`;
109
+ });
110
+
111
+ await writeFile(filePath, content);
112
+ updatedFiles.push(fileName);
113
+ }
114
+
115
+ spinner.succeed(`Updated ${updatedFiles.length} standard files`);
116
+
117
+ updatedFiles.forEach(file => {
118
+ logger.dim(` - ${file}`);
119
+ });
120
+
121
+ logger.blank();
122
+ logger.success('Standards sync complete!');
123
+ logger.dim('Commit these changes with a sync: message.');
124
+
125
+ } catch (error) {
126
+ spinner.fail('Sync failed');
127
+ logger.error(error.message);
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Group decisions by category
134
+ */
135
+ function groupByCategory(decisions) {
136
+ const categories = {
137
+ 'coding': [],
138
+ 'architecture': [],
139
+ 'azure': [],
140
+ 'integrations': [],
141
+ 'ui-ux': []
142
+ };
143
+
144
+ decisions.forEach(decision => {
145
+ const category = decision.category || 'coding';
146
+ if (categories[category]) {
147
+ categories[category].push(decision);
148
+ }
149
+ });
150
+
151
+ return categories;
152
+ }
153
+
154
+ /**
155
+ * Format category name
156
+ */
157
+ function formatCategory(category) {
158
+ const names = {
159
+ 'coding': 'Coding Standards',
160
+ 'architecture': 'Architecture Patterns',
161
+ 'azure': 'Azure & Infrastructure',
162
+ 'integrations': 'Integration Patterns',
163
+ 'ui-ux': 'UI/UX Standards'
164
+ };
165
+
166
+ return names[category] || category;
167
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Update Azure Pricing Command
3
+ *
4
+ * Updates azure-pricing.json with latest pricing data
5
+ */
6
+
7
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
8
+ import { join, dirname } from 'path';
9
+ import chalk from 'chalk';
10
+ import Ajv from 'ajv';
11
+ import addFormats from 'ajv-formats';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ /**
18
+ * Get pricing file path
19
+ * @returns {string} Path to azure-pricing.json
20
+ */
21
+ function getPricingPath() {
22
+ return join(__dirname, '..', '..', 'content', '.morph', 'config', 'azure-pricing.json');
23
+ }
24
+
25
+ /**
26
+ * Get schema file path
27
+ * @returns {string} Path to azure-pricing.schema.json
28
+ */
29
+ function getSchemaPath() {
30
+ return join(__dirname, '..', '..', 'content', '.morph', 'config', 'azure-pricing.schema.json');
31
+ }
32
+
33
+ /**
34
+ * Validate pricing data against JSON schema
35
+ * @param {Object} pricingData - Pricing data to validate
36
+ * @returns {boolean} True if valid
37
+ * @throws {Error} If validation fails
38
+ */
39
+ function validatePricingData(pricingData) {
40
+ const schemaPath = getSchemaPath();
41
+
42
+ if (!existsSync(schemaPath)) {
43
+ throw new Error(`Schema file not found: ${schemaPath}`);
44
+ }
45
+
46
+ const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
47
+
48
+ const ajv = new Ajv();
49
+ addFormats(ajv);
50
+ const validate = ajv.compile(schema);
51
+
52
+ const valid = validate(pricingData);
53
+
54
+ if (!valid) {
55
+ const errors = validate.errors.map(err => {
56
+ return ` - ${err.instancePath || 'root'}: ${err.message}`;
57
+ }).join('\n');
58
+ throw new Error(`Pricing data validation failed:\n${errors}`);
59
+ }
60
+
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Update pricing for a specific resource type
66
+ * @param {Object} options - Command options
67
+ * @param {string} options.resourceType - Resource type (e.g., "Microsoft.Sql/servers/databases")
68
+ * @param {string} options.sku - SKU name (e.g., "S0")
69
+ * @param {number} options.price - Monthly price
70
+ * @param {boolean} options.dryRun - Preview without saving
71
+ * @param {boolean} options.validate - Validate after update
72
+ */
73
+ export async function updateResourcePricing(options) {
74
+ const { resourceType, sku, price, dryRun = false, validate = true } = options;
75
+
76
+ const pricingPath = getPricingPath();
77
+
78
+ if (!existsSync(pricingPath)) {
79
+ console.log(chalk.red(`✗ Pricing file not found: ${pricingPath}`));
80
+ process.exit(1);
81
+ }
82
+
83
+ // Load current pricing
84
+ const pricingData = JSON.parse(readFileSync(pricingPath, 'utf8'));
85
+
86
+ // Ensure resource type exists
87
+ if (!pricingData.resources[resourceType]) {
88
+ pricingData.resources[resourceType] = {};
89
+ console.log(chalk.yellow(` Creating new resource type: ${resourceType}`));
90
+ }
91
+
92
+ // Update SKU price
93
+ const oldPrice = pricingData.resources[resourceType][sku];
94
+ pricingData.resources[resourceType][sku] = price;
95
+
96
+ // Update metadata
97
+ pricingData.lastUpdated = new Date().toISOString();
98
+
99
+ // Validate if requested
100
+ if (validate) {
101
+ try {
102
+ validatePricingData(pricingData);
103
+ console.log(chalk.green('✓ Validation passed'));
104
+ } catch (err) {
105
+ console.log(chalk.red(`✗ Validation failed: ${err.message}`));
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ // Show changes
111
+ console.log(chalk.blue('\nChanges:'));
112
+ console.log(` ${resourceType}`);
113
+ console.log(` ${sku}: ${oldPrice !== undefined ? `$${oldPrice}` : 'NEW'} → $${price}/month`);
114
+
115
+ // Save if not dry run
116
+ if (!dryRun) {
117
+ writeFileSync(pricingPath, JSON.stringify(pricingData, null, 2) + '\n', 'utf8');
118
+ console.log(chalk.green(`\n✓ Updated ${pricingPath}`));
119
+ } else {
120
+ console.log(chalk.yellow('\n⚠ Dry run - no changes saved'));
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Show current pricing
126
+ * @param {Object} options - Command options
127
+ * @param {string} options.resourceType - Optional resource type filter
128
+ * @param {string} options.format - Output format (table or json)
129
+ */
130
+ export async function showPricing(options) {
131
+ const { resourceType = null, format = 'table' } = options;
132
+
133
+ const pricingPath = getPricingPath();
134
+
135
+ if (!existsSync(pricingPath)) {
136
+ console.log(chalk.red(`✗ Pricing file not found: ${pricingPath}`));
137
+ process.exit(1);
138
+ }
139
+
140
+ const pricingData = JSON.parse(readFileSync(pricingPath, 'utf8'));
141
+
142
+ if (format === 'json') {
143
+ console.log(JSON.stringify(resourceType ? pricingData.resources[resourceType] : pricingData, null, 2));
144
+ return;
145
+ }
146
+
147
+ // Table format
148
+ console.log(chalk.bold('\nAzure Pricing Table'));
149
+ console.log(chalk.dim(`Region: ${pricingData.region} | Currency: ${pricingData.currency}`));
150
+ console.log(chalk.dim(`Last Updated: ${pricingData.lastUpdated}`));
151
+ console.log('');
152
+
153
+ const resourceTypes = resourceType
154
+ ? [resourceType]
155
+ : Object.keys(pricingData.resources).sort();
156
+
157
+ for (const type of resourceTypes) {
158
+ if (!pricingData.resources[type]) {
159
+ console.log(chalk.red(`✗ Resource type not found: ${type}`));
160
+ continue;
161
+ }
162
+
163
+ console.log(chalk.cyan(type));
164
+
165
+ const skus = Object.entries(pricingData.resources[type]).sort((a, b) => a[1] - b[1]);
166
+
167
+ for (const [skuName, price] of skus) {
168
+ const priceStr = price === 0 ? chalk.green('FREE') : `$${price.toFixed(2)}/month`;
169
+ console.log(` ${skuName.padEnd(20)} ${priceStr}`);
170
+ }
171
+
172
+ console.log('');
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Validate pricing file
178
+ */
179
+ export async function validatePricing() {
180
+ const pricingPath = getPricingPath();
181
+
182
+ if (!existsSync(pricingPath)) {
183
+ console.log(chalk.red(`✗ Pricing file not found: ${pricingPath}`));
184
+ process.exit(1);
185
+ }
186
+
187
+ const pricingData = JSON.parse(readFileSync(pricingPath, 'utf8'));
188
+
189
+ console.log(chalk.blue('Validating pricing data...'));
190
+
191
+ try {
192
+ validatePricingData(pricingData);
193
+ console.log(chalk.green('✓ Pricing data is valid'));
194
+
195
+ // Show stats
196
+ const resourceCount = Object.keys(pricingData.resources).length;
197
+ const skuCount = Object.values(pricingData.resources).reduce((sum, skus) => sum + Object.keys(skus).length, 0);
198
+
199
+ console.log(chalk.dim(`\nStats:`));
200
+ console.log(chalk.dim(` Resource types: ${resourceCount}`));
201
+ console.log(chalk.dim(` SKUs: ${skuCount}`));
202
+ } catch (err) {
203
+ console.log(chalk.red(`✗ Validation failed: ${err.message}`));
204
+ process.exit(1);
205
+ }
206
+ }
@@ -1,11 +1,19 @@
1
1
  import { join } from 'path';
2
2
  import ora from 'ora';
3
+ import chalk from 'chalk';
3
4
  import { logger } from '../utils/logger.js';
4
5
  import {
5
6
  getContentDir,
6
7
  copyDirectory,
7
8
  pathExists
8
9
  } from '../utils/file-copier.js';
10
+ import {
11
+ checkCLIOutdated,
12
+ checkProjectOutdated,
13
+ saveProjectMorphVersion,
14
+ getUpdateInstructions,
15
+ detectInstallMethod
16
+ } from '../utils/version-checker.js';
9
17
 
10
18
  export async function updateCommand(options) {
11
19
  const targetPath = process.cwd();
@@ -21,7 +29,57 @@ export async function updateCommand(options) {
21
29
  process.exit(1);
22
30
  }
23
31
 
24
- const spinner = ora('Updating MORPH-SPEC...').start();
32
+ // Check CLI version
33
+ const spinner = ora('Checking versions...').start();
34
+ const cliCheck = await checkCLIOutdated();
35
+ const projectCheck = await checkProjectOutdated(targetPath);
36
+
37
+ spinner.stop();
38
+
39
+ // Display version info
40
+ logger.blank();
41
+ logger.info('Version Status:');
42
+ logger.dim(` CLI version: ${cliCheck.current}`);
43
+
44
+ if (projectCheck.current) {
45
+ logger.dim(` Project MORPH version: ${projectCheck.current}`);
46
+ } else {
47
+ logger.dim(' Project MORPH version: not found (legacy installation)');
48
+ }
49
+
50
+ if (cliCheck.latest) {
51
+ const latestColor = cliCheck.isOutdated ? chalk.yellow : chalk.green;
52
+ logger.dim(` Latest available: ${latestColor(cliCheck.latest)}`);
53
+ }
54
+ logger.blank();
55
+
56
+ // If CLI is outdated, stop and instruct user
57
+ if (cliCheck.isOutdated && cliCheck.latest) {
58
+ logger.warn(`Your CLI is outdated (${cliCheck.current} → ${cliCheck.latest})`);
59
+ logger.blank();
60
+ logger.info('Please update the CLI first:');
61
+ logger.blank();
62
+
63
+ const method = detectInstallMethod();
64
+ const instructions = getUpdateInstructions(method);
65
+
66
+ instructions.forEach(line => {
67
+ if (line === '') {
68
+ logger.blank();
69
+ } else if (line.startsWith('Or ')) {
70
+ logger.dim(line);
71
+ } else {
72
+ logger.box([line]);
73
+ }
74
+ });
75
+
76
+ logger.blank();
77
+ logger.dim('Then run "morph-spec update" again.');
78
+ process.exit(0);
79
+ }
80
+
81
+ // Proceed with update
82
+ const updateSpinner = ora('Updating MORPH-SPEC...').start();
25
83
 
26
84
  try {
27
85
  const updateTemplates = !options.standards || options.templates;
@@ -29,55 +87,72 @@ export async function updateCommand(options) {
29
87
 
30
88
  // Update templates
31
89
  if (updateTemplates) {
32
- spinner.text = 'Updating templates...';
90
+ updateSpinner.text = 'Updating templates...';
33
91
  const templatesSrc = join(contentDir, '.morph', 'templates');
34
92
  const templatesDest = join(morphPath, 'templates');
35
93
  await copyDirectory(templatesSrc, templatesDest);
36
- logger.dim('Updated .morph/templates/');
37
94
  }
38
95
 
39
96
  // Update standards
40
97
  if (updateStandards) {
41
- spinner.text = 'Updating standards...';
98
+ updateSpinner.text = 'Updating standards...';
42
99
  const standardsSrc = join(contentDir, '.morph', 'standards');
43
100
  const standardsDest = join(morphPath, 'standards');
44
101
  await copyDirectory(standardsSrc, standardsDest);
45
- logger.dim('Updated .morph/standards/');
46
102
  }
47
103
 
48
104
  // Update agents.json
49
- spinner.text = 'Updating agents configuration...';
105
+ updateSpinner.text = 'Updating agents configuration...';
50
106
  const agentsSrc = join(contentDir, '.morph', 'config', 'agents.json');
51
107
  const agentsDest = join(morphPath, 'config', 'agents.json');
52
108
  if (await pathExists(agentsSrc)) {
53
109
  await copyDirectory(agentsSrc, agentsDest);
54
- logger.dim('Updated .morph/config/agents.json');
55
110
  }
56
111
 
57
112
  // Update .claude commands and skills
58
- spinner.text = 'Updating Claude commands and skills...';
113
+ updateSpinner.text = 'Updating Claude commands and skills...';
59
114
  const claudeSrc = join(contentDir, '.claude');
60
115
  const claudeDest = join(targetPath, '.claude');
61
116
  if (await pathExists(claudeSrc)) {
62
117
  await copyDirectory(claudeSrc, claudeDest);
63
- logger.dim('Updated .claude/commands/ and .claude/skills/');
64
118
  }
65
119
 
66
120
  // Update CLAUDE.md
67
- spinner.text = 'Updating CLAUDE.md...';
121
+ updateSpinner.text = 'Updating CLAUDE.md...';
68
122
  const claudeMdSrc = join(contentDir, 'CLAUDE.md');
69
123
  const claudeMdDest = join(targetPath, 'CLAUDE.md');
70
124
  await copyDirectory(claudeMdSrc, claudeMdDest);
71
- logger.dim('Updated CLAUDE.md');
72
125
 
73
- spinner.succeed('MORPH-SPEC updated successfully!');
126
+ // Update .morphversion
127
+ updateSpinner.text = 'Saving version info...';
128
+ await saveProjectMorphVersion(targetPath, cliCheck.current);
129
+
130
+ updateSpinner.succeed('MORPH-SPEC updated successfully!');
131
+ logger.blank();
132
+
133
+ // Show version change
134
+ if (projectCheck.current && projectCheck.current !== cliCheck.current) {
135
+ logger.success(`Updated: ${projectCheck.current} → ${cliCheck.current}`);
136
+ } else if (!projectCheck.current) {
137
+ logger.success(`Updated to v${cliCheck.current}`);
138
+ } else {
139
+ logger.success(`Already up to date (v${cliCheck.current})`);
140
+ }
141
+
142
+ logger.blank();
143
+ logger.info('Updated files:');
144
+ if (updateTemplates) logger.dim(' ✓ .morph/templates/');
145
+ if (updateStandards) logger.dim(' ✓ .morph/standards/');
146
+ logger.dim(' ✓ .morph/config/agents.json');
147
+ logger.dim(' ✓ .claude/commands/ and .claude/skills/');
148
+ logger.dim(' ✓ CLAUDE.md');
74
149
  logger.blank();
75
150
  logger.info('Your config.json was preserved.');
76
151
  logger.dim('Review the updated files for any new features.');
77
152
  logger.blank();
78
153
 
79
154
  } catch (error) {
80
- spinner.fail('Update failed');
155
+ updateSpinner.fail('Update failed');
81
156
  logger.error(error.message);
82
157
  process.exit(1);
83
158
  }