@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.
- package/CLAUDE.md +534 -0
- package/README.md +78 -4
- package/bin/morph-spec.js +50 -1
- package/bin/render-template.js +56 -10
- package/bin/task-manager.cjs +101 -7
- package/docs/cli-auto-detection.md +219 -0
- package/docs/llm-interaction-config.md +735 -0
- package/docs/troubleshooting.md +269 -0
- package/package.json +5 -1
- package/src/commands/advance-phase.js +93 -2
- package/src/commands/approve.js +221 -0
- package/src/commands/capture-pattern.js +121 -0
- package/src/commands/generate.js +128 -1
- package/src/commands/init.js +37 -0
- package/src/commands/migrate-state.js +158 -0
- package/src/commands/search-patterns.js +126 -0
- package/src/commands/spawn-team.js +172 -0
- package/src/commands/task.js +2 -2
- package/src/commands/update.js +36 -0
- package/src/commands/upgrade.js +346 -0
- package/src/generator/.gitkeep +0 -0
- package/src/generator/config-generator.js +206 -0
- package/src/generator/templates/config.json.template +40 -0
- package/src/generator/templates/project.md.template +67 -0
- package/src/lib/checkpoint-hooks.js +258 -0
- package/src/lib/metadata-extractor.js +380 -0
- package/src/lib/phase-state-machine.js +214 -0
- package/src/lib/state-manager.js +120 -0
- package/src/lib/template-data-sources.js +325 -0
- package/src/lib/validators/content-validator.js +351 -0
- package/src/llm/.gitkeep +0 -0
- package/src/llm/analyzer.js +215 -0
- package/src/llm/environment-detector.js +43 -0
- package/src/llm/few-shot-examples.js +216 -0
- package/src/llm/project-config-schema.json +188 -0
- package/src/llm/prompt-builder.js +96 -0
- package/src/llm/schema-validator.js +121 -0
- package/src/orchestrator.js +206 -0
- package/src/sanitizer/.gitkeep +0 -0
- package/src/sanitizer/context-sanitizer.js +221 -0
- package/src/sanitizer/patterns.js +163 -0
- package/src/scanner/.gitkeep +0 -0
- package/src/scanner/project-scanner.js +242 -0
- package/src/types/index.js +477 -0
- package/src/ui/.gitkeep +0 -0
- package/src/ui/diff-display.js +91 -0
- package/src/ui/interactive-wizard.js +96 -0
- package/src/ui/user-review.js +211 -0
- package/src/ui/wizard-questions.js +190 -0
- package/src/writer/.gitkeep +0 -0
- 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
|
+
}
|