@polymorphism-tech/morph-spec 3.1.0 → 3.2.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 (51) hide show
  1. package/CLAUDE.md +534 -0
  2. package/README.md +78 -4
  3. package/bin/morph-spec.js +50 -1
  4. package/bin/render-template.js +56 -10
  5. package/bin/task-manager.cjs +101 -7
  6. package/docs/cli-auto-detection.md +219 -0
  7. package/docs/llm-interaction-config.md +735 -0
  8. package/docs/troubleshooting.md +269 -0
  9. package/package.json +5 -1
  10. package/src/commands/advance-phase.js +93 -2
  11. package/src/commands/approve.js +221 -0
  12. package/src/commands/capture-pattern.js +121 -0
  13. package/src/commands/generate.js +128 -1
  14. package/src/commands/init.js +37 -0
  15. package/src/commands/migrate-state.js +158 -0
  16. package/src/commands/search-patterns.js +126 -0
  17. package/src/commands/spawn-team.js +172 -0
  18. package/src/commands/task.js +2 -2
  19. package/src/commands/update.js +36 -0
  20. package/src/commands/upgrade.js +346 -0
  21. package/src/generator/.gitkeep +0 -0
  22. package/src/generator/config-generator.js +206 -0
  23. package/src/generator/templates/config.json.template +40 -0
  24. package/src/generator/templates/project.md.template +67 -0
  25. package/src/lib/checkpoint-hooks.js +258 -0
  26. package/src/lib/metadata-extractor.js +380 -0
  27. package/src/lib/phase-state-machine.js +214 -0
  28. package/src/lib/state-manager.js +120 -0
  29. package/src/lib/template-data-sources.js +325 -0
  30. package/src/lib/validators/content-validator.js +351 -0
  31. package/src/llm/.gitkeep +0 -0
  32. package/src/llm/analyzer.js +215 -0
  33. package/src/llm/environment-detector.js +43 -0
  34. package/src/llm/few-shot-examples.js +216 -0
  35. package/src/llm/project-config-schema.json +188 -0
  36. package/src/llm/prompt-builder.js +96 -0
  37. package/src/llm/schema-validator.js +121 -0
  38. package/src/orchestrator.js +206 -0
  39. package/src/sanitizer/.gitkeep +0 -0
  40. package/src/sanitizer/context-sanitizer.js +221 -0
  41. package/src/sanitizer/patterns.js +163 -0
  42. package/src/scanner/.gitkeep +0 -0
  43. package/src/scanner/project-scanner.js +242 -0
  44. package/src/types/index.js +477 -0
  45. package/src/ui/.gitkeep +0 -0
  46. package/src/ui/diff-display.js +91 -0
  47. package/src/ui/interactive-wizard.js +96 -0
  48. package/src/ui/user-review.js +211 -0
  49. package/src/ui/wizard-questions.js +190 -0
  50. package/src/writer/.gitkeep +0 -0
  51. package/src/writer/file-writer.js +86 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * @fileoverview UserReview - Prompts user for approval of generated configs
3
+ * @module morph-spec/ui/user-review
4
+ */
5
+
6
+ import inquirer from 'inquirer';
7
+ import chalk from 'chalk';
8
+ import { displayDiff, displayConfigSummary } from './diff-display.js';
9
+ import { writeFile, readFile, access } from 'fs/promises';
10
+ import { join } from 'path';
11
+ import { execSync } from 'child_process';
12
+ import { tmpdir } from 'os';
13
+
14
+ /**
15
+ * @typedef {import('../types/index.js').GeneratedConfigs} GeneratedConfigs
16
+ * @typedef {import('../types/index.js').ProjectConfig} ProjectConfig
17
+ * @typedef {import('../types/index.js').ApprovalResponse} ApprovalResponse
18
+ */
19
+
20
+ /**
21
+ * UserReview - Handles user review and approval of generated configs
22
+ * @class
23
+ */
24
+ export class UserReview {
25
+ /**
26
+ * Prompt user for approval of generated configs
27
+ * @param {GeneratedConfigs} configs - Generated configs
28
+ * @param {ProjectConfig} projectConfig - Detected project config
29
+ * @param {Object} [existingConfigs] - Existing configs (if updating)
30
+ * @returns {Promise<ApprovalResponse>}
31
+ */
32
+ async promptForApproval(configs, projectConfig, existingConfigs = null) {
33
+ console.log(chalk.bold.cyan('\nšŸ” Auto-Detected Project Configuration\n'));
34
+
35
+ // Display summary
36
+ this.displayPreview(projectConfig);
37
+
38
+ // If updating existing configs, show diff
39
+ if (existingConfigs) {
40
+ if (existingConfigs.projectMd) {
41
+ displayDiff(existingConfigs.projectMd, configs.projectMd, 'project.md');
42
+ }
43
+ if (existingConfigs.configJson) {
44
+ try {
45
+ const oldConfig = JSON.parse(existingConfigs.configJson);
46
+ displayConfigSummary(oldConfig, configs.configObject);
47
+ } catch (error) {
48
+ // Couldn't parse old config, skip summary
49
+ }
50
+ }
51
+ }
52
+
53
+ // Prompt for action
54
+ const { action } = await inquirer.prompt([
55
+ {
56
+ type: 'list',
57
+ name: 'action',
58
+ message: 'What would you like to do?',
59
+ choices: [
60
+ { name: 'āœ… Approve and save configs', value: 'approve' },
61
+ { name: 'āœļø Edit configs before saving', value: 'edit' },
62
+ { name: 'āŒ Cancel and exit', value: 'cancel' }
63
+ ],
64
+ default: 'approve'
65
+ }
66
+ ]);
67
+
68
+ switch (action) {
69
+ case 'approve':
70
+ return { action: 'approve' };
71
+
72
+ case 'edit':
73
+ const editedConfigs = await this.openInEditor(configs);
74
+ return { action: 'approve', editedConfigs };
75
+
76
+ case 'cancel':
77
+ const { reason } = await inquirer.prompt([
78
+ {
79
+ type: 'input',
80
+ name: 'reason',
81
+ message: 'Why are you canceling? (optional)',
82
+ default: 'User canceled'
83
+ }
84
+ ]);
85
+ return { action: 'cancel', cancelReason: reason };
86
+
87
+ default:
88
+ return { action: 'cancel', cancelReason: 'Unknown action' };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Display preview of detected configuration
94
+ * @param {ProjectConfig} projectConfig - Detected project config
95
+ */
96
+ displayPreview(projectConfig) {
97
+ console.log(chalk.bold(' Name: ') + chalk.cyan(projectConfig.name));
98
+ console.log(chalk.bold(' Type: ') + chalk.yellow(projectConfig.type));
99
+ console.log(chalk.bold(' Description: ') + chalk.white(projectConfig.description));
100
+ console.log();
101
+
102
+ console.log(chalk.bold(' Stack:'));
103
+ if (projectConfig.stack.frontend) {
104
+ console.log(chalk.gray(' Frontend: ') + chalk.white(`${projectConfig.stack.frontend.tech} ${projectConfig.stack.frontend.version}`));
105
+ }
106
+ console.log(chalk.gray(' Backend: ') + chalk.white(`${projectConfig.stack.backend.tech} ${projectConfig.stack.backend.version}`));
107
+ if (projectConfig.stack.database) {
108
+ console.log(chalk.gray(' Database: ') + chalk.white(`${projectConfig.stack.database.tech} ${projectConfig.stack.database.version}`));
109
+ }
110
+ if (projectConfig.stack.hosting) {
111
+ console.log(chalk.gray(' Hosting: ') + chalk.white(projectConfig.stack.hosting));
112
+ }
113
+ console.log();
114
+
115
+ console.log(chalk.bold(' Architecture: ') + chalk.magenta(projectConfig.architecture));
116
+ console.log(chalk.bold(' Conventions: ') + chalk.white(projectConfig.conventions));
117
+ console.log();
118
+
119
+ // Infrastructure flags
120
+ const flags = [];
121
+ if (projectConfig.hasAzure) flags.push(chalk.blue('Azure'));
122
+ if (projectConfig.hasDocker) flags.push(chalk.cyan('Docker'));
123
+ if (projectConfig.hasDevOps) flags.push(chalk.green('CI/CD'));
124
+
125
+ if (flags.length > 0) {
126
+ console.log(chalk.bold(' Infrastructure: ') + flags.join(' • '));
127
+ console.log();
128
+ }
129
+
130
+ // Confidence and warnings
131
+ const confidenceColor = projectConfig.confidence >= 90 ? chalk.green : projectConfig.confidence >= 70 ? chalk.yellow : chalk.red;
132
+ console.log(chalk.bold(' Confidence: ') + confidenceColor(`${projectConfig.confidence}%`));
133
+
134
+ if (projectConfig.warnings && projectConfig.warnings.length > 0) {
135
+ console.log(chalk.bold.yellow('\n āš ļø Warnings:'));
136
+ projectConfig.warnings.forEach(warning => {
137
+ console.log(chalk.yellow(` • ${warning}`));
138
+ });
139
+ }
140
+
141
+ console.log();
142
+ }
143
+
144
+ /**
145
+ * Display diff between current and generated configs
146
+ * @param {string} current - Current config content
147
+ * @param {string} generated - Generated config content
148
+ */
149
+ displayDiff(current, generated) {
150
+ displayDiff(current, generated, 'config');
151
+ }
152
+
153
+ /**
154
+ * Open configs in editor for manual editing
155
+ * @param {GeneratedConfigs} configs - Configs to edit
156
+ * @returns {Promise<GeneratedConfigs>} Edited configs
157
+ */
158
+ async openInEditor(configs) {
159
+ console.log(chalk.cyan('\nšŸ“ Opening configs in editor...\n'));
160
+
161
+ // Write to temp files
162
+ const tempDir = tmpdir();
163
+ const projectMdPath = join(tempDir, 'morph-project.md');
164
+ const configJsonPath = join(tempDir, 'morph-config.json');
165
+
166
+ await Promise.all([
167
+ writeFile(projectMdPath, configs.projectMd, 'utf-8'),
168
+ writeFile(configJsonPath, configs.configJson, 'utf-8')
169
+ ]);
170
+
171
+ // Get editor from environment or use default
172
+ const editor = process.env.EDITOR || process.env.VISUAL || 'nano';
173
+
174
+ console.log(chalk.dim(` Using editor: ${editor}`));
175
+ console.log(chalk.dim(` Files: ${projectMdPath}, ${configJsonPath}`));
176
+ console.log(chalk.yellow('\n Press ENTER when done editing...'));
177
+
178
+ // Open editor (blocking)
179
+ try {
180
+ execSync(`${editor} "${projectMdPath}" "${configJsonPath}"`, {
181
+ stdio: 'inherit'
182
+ });
183
+ } catch (error) {
184
+ console.log(chalk.yellow('\nāš ļø Editor exited with error, using original configs\n'));
185
+ return configs;
186
+ }
187
+
188
+ // Read edited files
189
+ const [editedProjectMd, editedConfigJson] = await Promise.all([
190
+ readFile(projectMdPath, 'utf-8'),
191
+ readFile(configJsonPath, 'utf-8')
192
+ ]);
193
+
194
+ // Parse config.json
195
+ let editedConfigObject;
196
+ try {
197
+ editedConfigObject = JSON.parse(editedConfigJson);
198
+ } catch (error) {
199
+ console.log(chalk.red('\nāŒ Edited config.json is not valid JSON. Using original.\n'));
200
+ return configs;
201
+ }
202
+
203
+ console.log(chalk.green('\nāœ… Configs edited successfully\n'));
204
+
205
+ return {
206
+ projectMd: editedProjectMd,
207
+ configJson: editedConfigJson,
208
+ configObject: editedConfigObject
209
+ };
210
+ }
211
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @fileoverview Wizard Questions - Defines interactive wizard questions
3
+ * @module morph-spec/ui/wizard-questions
4
+ */
5
+
6
+ /**
7
+ * Get wizard questions for interactive mode
8
+ * @returns {Array} Inquirer questions
9
+ */
10
+ export function getWizardQuestions() {
11
+ return [
12
+ {
13
+ type: 'input',
14
+ name: 'name',
15
+ message: '1/7 Project name:',
16
+ default: process.cwd().split(/[/\\]/).pop(),
17
+ validate: (input) => {
18
+ if (!input || input.trim().length === 0) {
19
+ return 'Project name is required';
20
+ }
21
+ return true;
22
+ }
23
+ },
24
+ {
25
+ type: 'input',
26
+ name: 'description',
27
+ message: '2/7 Project description (1-2 sentences):',
28
+ validate: (input) => {
29
+ if (!input || input.trim().length < 10) {
30
+ return 'Description must be at least 10 characters';
31
+ }
32
+ return true;
33
+ }
34
+ },
35
+ {
36
+ type: 'list',
37
+ name: 'type',
38
+ message: '3/7 Project type:',
39
+ choices: [
40
+ { name: 'Blazor Server (.NET)', value: 'blazor-server' },
41
+ { name: 'Next.js (React)', value: 'nextjs' },
42
+ { name: '.NET Web API', value: 'dotnet-api' },
43
+ { name: 'CLI Tool', value: 'cli-tool' },
44
+ { name: 'Monorepo (multiple projects)', value: 'monorepo' },
45
+ { name: 'Other', value: 'other' }
46
+ ]
47
+ },
48
+ {
49
+ type: 'list',
50
+ name: 'frontend',
51
+ message: '4/7 Frontend technology:',
52
+ choices: [
53
+ { name: 'Blazor Server', value: 'blazor' },
54
+ { name: 'Next.js', value: 'nextjs' },
55
+ { name: 'React', value: 'react' },
56
+ { name: 'Vue.js', value: 'vue' },
57
+ { name: 'Angular', value: 'angular' },
58
+ { name: 'None (backend-only)', value: null }
59
+ ],
60
+ when: (answers) => answers.type !== 'cli-tool' && answers.type !== 'dotnet-api'
61
+ },
62
+ {
63
+ type: 'list',
64
+ name: 'backend',
65
+ message: '5/7 Backend technology:',
66
+ choices: [
67
+ { name: '.NET 10', value: 'dotnet-10' },
68
+ { name: '.NET 9', value: 'dotnet-9' },
69
+ { name: '.NET 8', value: 'dotnet-8' },
70
+ { name: 'Node.js', value: 'nodejs' },
71
+ { name: 'Supabase', value: 'supabase' },
72
+ { name: 'Firebase', value: 'firebase' },
73
+ { name: 'Other', value: 'other' }
74
+ ],
75
+ default: 'dotnet-10'
76
+ },
77
+ {
78
+ type: 'list',
79
+ name: 'database',
80
+ message: '6/7 Database:',
81
+ choices: [
82
+ { name: 'Azure SQL Database', value: 'azure-sql' },
83
+ { name: 'PostgreSQL', value: 'postgresql' },
84
+ { name: 'Cosmos DB', value: 'cosmosdb' },
85
+ { name: 'MongoDB', value: 'mongodb' },
86
+ { name: 'SQLite', value: 'sqlite' },
87
+ { name: 'Supabase (PostgreSQL)', value: 'supabase' },
88
+ { name: 'None (no database)', value: null }
89
+ ]
90
+ },
91
+ {
92
+ type: 'confirm',
93
+ name: 'hasAzure',
94
+ message: '7a/7 Uses Azure infrastructure?',
95
+ default: false
96
+ },
97
+ {
98
+ type: 'confirm',
99
+ name: 'hasDocker',
100
+ message: '7b/7 Uses Docker containerization?',
101
+ default: true
102
+ }
103
+ ];
104
+ }
105
+
106
+ /**
107
+ * Map wizard answers to ProjectConfig
108
+ * @param {Object} answers - Wizard answers from inquirer
109
+ * @returns {ProjectConfig} Mapped project config
110
+ */
111
+ export function mapAnswersToConfig(answers) {
112
+ // Parse backend tech and version
113
+ const backendParts = answers.backend.split('-');
114
+ const backendTech = backendParts[0] === 'dotnet' ? '.NET' :
115
+ backendParts[0] === 'nodejs' ? 'Node.js' :
116
+ answers.backend;
117
+ const backendVersion = backendParts[1] || 'latest';
118
+
119
+ // Map frontend
120
+ let frontend = null;
121
+ if (answers.frontend) {
122
+ const frontendMap = {
123
+ 'blazor': { tech: 'Blazor', version: backendVersion, details: 'Blazor Server' },
124
+ 'nextjs': { tech: 'Next.js', version: '15', details: 'App Router' },
125
+ 'react': { tech: 'React', version: '18', details: null },
126
+ 'vue': { tech: 'Vue.js', version: '3', details: null },
127
+ 'angular': { tech: 'Angular', version: '17', details: null }
128
+ };
129
+ frontend = frontendMap[answers.frontend] || null;
130
+ }
131
+
132
+ // Map database
133
+ let database = null;
134
+ if (answers.database) {
135
+ const databaseMap = {
136
+ 'azure-sql': { tech: 'Azure SQL', version: 'latest', details: 'Managed SQL Database' },
137
+ 'postgresql': { tech: 'PostgreSQL', version: '16', details: null },
138
+ 'cosmosdb': { tech: 'Cosmos DB', version: 'latest', details: 'NoSQL' },
139
+ 'mongodb': { tech: 'MongoDB', version: '7', details: null },
140
+ 'sqlite': { tech: 'SQLite', version: '3', details: null },
141
+ 'supabase': { tech: 'PostgreSQL', version: '16', details: 'Managed by Supabase' }
142
+ };
143
+ database = databaseMap[answers.database] || null;
144
+ }
145
+
146
+ // Infer architecture from project type
147
+ const architectureMap = {
148
+ 'blazor-server': 'clean-architecture',
149
+ 'nextjs': 'layered',
150
+ 'dotnet-api': 'clean-architecture',
151
+ 'cli-tool': 'monolith',
152
+ 'monorepo': 'microservices',
153
+ 'other': 'layered'
154
+ };
155
+
156
+ const architecture = architectureMap[answers.type] || 'layered';
157
+
158
+ // Infer hosting
159
+ let hosting = null;
160
+ if (answers.hasAzure) {
161
+ hosting = 'Azure';
162
+ } else if (answers.hasDocker) {
163
+ hosting = 'Docker';
164
+ }
165
+
166
+ return {
167
+ name: answers.name,
168
+ description: answers.description,
169
+ type: answers.type,
170
+ stack: {
171
+ frontend,
172
+ backend: {
173
+ tech: backendTech,
174
+ version: backendVersion,
175
+ details: null
176
+ },
177
+ database,
178
+ hosting
179
+ },
180
+ architecture,
181
+ projectStructure: 'User-specified configuration via interactive wizard',
182
+ conventions: 'Standard conventions for ' + backendTech,
183
+ repository: null,
184
+ hasAzure: answers.hasAzure,
185
+ hasDocker: answers.hasDocker,
186
+ hasDevOps: false,
187
+ confidence: 100, // User input is 100% confident
188
+ warnings: ['Configuration was manually entered, not auto-detected']
189
+ };
190
+ }
File without changes
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @fileoverview FileWriter - Saves generated configs to filesystem
3
+ * @module morph-spec/writer/file-writer
4
+ */
5
+
6
+ import { writeFile, mkdir, access } from 'fs/promises';
7
+ import { join, dirname } from 'path';
8
+ import chalk from 'chalk';
9
+
10
+ /**
11
+ * @typedef {import('../types/index.js').GeneratedConfigs} GeneratedConfigs
12
+ */
13
+
14
+ /**
15
+ * FileWriter - Saves configuration files to .morph/ directory
16
+ * @class
17
+ */
18
+ export class FileWriter {
19
+ /**
20
+ * Save generated configs to filesystem
21
+ * @param {string} cwd - Current working directory
22
+ * @param {GeneratedConfigs} configs - Configs to save
23
+ * @returns {Promise<void>}
24
+ */
25
+ async save(cwd, configs) {
26
+ const morphDir = join(cwd, '.morph');
27
+ const configDir = join(morphDir, 'config');
28
+
29
+ // Ensure directories exist
30
+ await this.ensureDirectoryExists(morphDir);
31
+ await this.ensureDirectoryExists(configDir);
32
+
33
+ // Write files
34
+ const projectMdPath = join(morphDir, 'project.md');
35
+ const configJsonPath = join(configDir, 'config.json');
36
+
37
+ await Promise.all([
38
+ this.writeProjectMd(projectMdPath, configs.projectMd),
39
+ this.writeConfigJson(configJsonPath, configs.configJson)
40
+ ]);
41
+
42
+ // Display success message
43
+ console.log(chalk.bold.green('\nāœ… Configuration files saved:\n'));
44
+ console.log(chalk.cyan(` šŸ“„ ${projectMdPath}`));
45
+ console.log(chalk.cyan(` āš™ļø ${configJsonPath}`));
46
+ console.log();
47
+ }
48
+
49
+ /**
50
+ * Write project.md file
51
+ * @param {string} filepath - File path
52
+ * @param {string} content - File content
53
+ * @returns {Promise<void>}
54
+ */
55
+ async writeProjectMd(filepath, content) {
56
+ await writeFile(filepath, content, 'utf-8');
57
+ }
58
+
59
+ /**
60
+ * Write config.json file (with pretty formatting)
61
+ * @param {string} filepath - File path
62
+ * @param {string} content - File content (JSON string)
63
+ * @returns {Promise<void>}
64
+ */
65
+ async writeConfigJson(filepath, content) {
66
+ // Parse and re-stringify with pretty formatting
67
+ const parsed = JSON.parse(content);
68
+ const formatted = JSON.stringify(parsed, null, 2);
69
+
70
+ await writeFile(filepath, formatted, 'utf-8');
71
+ }
72
+
73
+ /**
74
+ * Ensure directory exists (create if not)
75
+ * @param {string} dirPath - Directory path
76
+ * @returns {Promise<void>}
77
+ */
78
+ async ensureDirectoryExists(dirPath) {
79
+ try {
80
+ await access(dirPath);
81
+ } catch (error) {
82
+ // Directory doesn't exist, create it
83
+ await mkdir(dirPath, { recursive: true });
84
+ }
85
+ }
86
+ }