@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,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ProjectScanner - Scans project directory and collects context
|
|
3
|
+
* @module morph-spec/scanner/project-scanner
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, access } from 'fs/promises';
|
|
7
|
+
import { join, dirname, relative } from 'path';
|
|
8
|
+
import { readdir, stat } from 'fs/promises';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { glob } from 'glob';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {import('../types/index.js').ProjectContext} ProjectContext
|
|
14
|
+
* @typedef {import('../types/index.js').PackageJson} PackageJson
|
|
15
|
+
* @typedef {import('../types/index.js').DirectoryStructure} DirectoryStructure
|
|
16
|
+
* @typedef {import('../types/index.js').InfraFiles} InfraFiles
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ProjectScanner - Scans project directory and collects context for LLM analysis
|
|
21
|
+
* @class
|
|
22
|
+
*/
|
|
23
|
+
export class ProjectScanner {
|
|
24
|
+
/**
|
|
25
|
+
* Scan the project directory and collect complete context
|
|
26
|
+
* @param {string} cwd - Current working directory (absolute path)
|
|
27
|
+
* @returns {Promise<ProjectContext>}
|
|
28
|
+
*/
|
|
29
|
+
async scan(cwd) {
|
|
30
|
+
const [
|
|
31
|
+
packageJson,
|
|
32
|
+
csprojFiles,
|
|
33
|
+
solutionFile,
|
|
34
|
+
readme,
|
|
35
|
+
claudeMd,
|
|
36
|
+
structure,
|
|
37
|
+
infraFiles,
|
|
38
|
+
gitRemote
|
|
39
|
+
] = await Promise.all([
|
|
40
|
+
this.readPackageJson(cwd),
|
|
41
|
+
this.findCsprojFiles(cwd),
|
|
42
|
+
this.findSolutionFile(cwd),
|
|
43
|
+
this.readFileIfExists(join(cwd, 'README.md')),
|
|
44
|
+
this.readFileIfExists(join(cwd, 'CLAUDE.md')),
|
|
45
|
+
this.detectDirectoryStructure(cwd),
|
|
46
|
+
this.findInfraFiles(cwd),
|
|
47
|
+
this.getGitRemote(cwd)
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
cwd,
|
|
52
|
+
packageJson,
|
|
53
|
+
csprojFiles,
|
|
54
|
+
solutionFile,
|
|
55
|
+
readme,
|
|
56
|
+
claudeMd,
|
|
57
|
+
structure,
|
|
58
|
+
infraFiles,
|
|
59
|
+
gitRemote,
|
|
60
|
+
scannedAt: new Date()
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read and parse package.json
|
|
66
|
+
* @param {string} cwd - Current working directory
|
|
67
|
+
* @returns {Promise<PackageJson|null>}
|
|
68
|
+
*/
|
|
69
|
+
async readPackageJson(cwd) {
|
|
70
|
+
try {
|
|
71
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
72
|
+
const content = await readFile(packageJsonPath, 'utf-8');
|
|
73
|
+
return JSON.parse(content);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// No package.json or parse error
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Find all .csproj files recursively
|
|
82
|
+
* @param {string} cwd - Current working directory
|
|
83
|
+
* @returns {Promise<string[]>}
|
|
84
|
+
*/
|
|
85
|
+
async findCsprojFiles(cwd) {
|
|
86
|
+
try {
|
|
87
|
+
const pattern = '**/*.csproj';
|
|
88
|
+
const options = {
|
|
89
|
+
cwd,
|
|
90
|
+
ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/.git/**'],
|
|
91
|
+
absolute: false
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const files = await glob(pattern, options);
|
|
95
|
+
return files;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Find solution file (.sln)
|
|
103
|
+
* @param {string} cwd - Current working directory
|
|
104
|
+
* @returns {Promise<string|null>}
|
|
105
|
+
*/
|
|
106
|
+
async findSolutionFile(cwd) {
|
|
107
|
+
try {
|
|
108
|
+
const pattern = '**/*.sln';
|
|
109
|
+
const options = {
|
|
110
|
+
cwd,
|
|
111
|
+
ignore: ['**/node_modules/**', '**/.git/**'],
|
|
112
|
+
absolute: false
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const files = await glob(pattern, options);
|
|
116
|
+
return files.length > 0 ? files[0] : null;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detect directory structure and patterns
|
|
124
|
+
* @param {string} cwd - Current working directory
|
|
125
|
+
* @returns {Promise<DirectoryStructure>}
|
|
126
|
+
*/
|
|
127
|
+
async detectDirectoryStructure(cwd) {
|
|
128
|
+
try {
|
|
129
|
+
const entries = await readdir(cwd, { withFileTypes: true });
|
|
130
|
+
const dirs = entries
|
|
131
|
+
.filter(entry => entry.isDirectory())
|
|
132
|
+
.map(entry => entry.name)
|
|
133
|
+
.filter(name => !name.startsWith('.') && name !== 'node_modules');
|
|
134
|
+
|
|
135
|
+
const hasSrc = dirs.includes('src');
|
|
136
|
+
const hasBackend = dirs.some(d => ['backend', 'server', 'api'].includes(d.toLowerCase()));
|
|
137
|
+
const hasFrontend = dirs.some(d => ['frontend', 'client', 'web', 'app'].includes(d.toLowerCase()));
|
|
138
|
+
const hasTests = dirs.some(d => ['test', 'tests', '__tests__'].includes(d.toLowerCase()));
|
|
139
|
+
|
|
140
|
+
// Detect pattern
|
|
141
|
+
let pattern = 'single-project';
|
|
142
|
+
if (hasBackend && hasFrontend) {
|
|
143
|
+
pattern = 'multi-stack';
|
|
144
|
+
} else if (dirs.length > 5 && dirs.some(d => d.includes('packages') || d.includes('apps'))) {
|
|
145
|
+
pattern = 'monorepo';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
hasSrc,
|
|
150
|
+
hasBackend,
|
|
151
|
+
hasFrontend,
|
|
152
|
+
hasTests,
|
|
153
|
+
topLevelDirs: dirs,
|
|
154
|
+
pattern
|
|
155
|
+
};
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
hasSrc: false,
|
|
159
|
+
hasBackend: false,
|
|
160
|
+
hasFrontend: false,
|
|
161
|
+
hasTests: false,
|
|
162
|
+
topLevelDirs: [],
|
|
163
|
+
pattern: 'single-project'
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Find infrastructure files (Docker, Bicep, pipelines)
|
|
170
|
+
* @param {string} cwd - Current working directory
|
|
171
|
+
* @returns {Promise<InfraFiles>}
|
|
172
|
+
*/
|
|
173
|
+
async findInfraFiles(cwd) {
|
|
174
|
+
try {
|
|
175
|
+
const [dockerfiles, dockerComposeFiles, bicepFiles, pipelines] = await Promise.all([
|
|
176
|
+
glob('**/Dockerfile*', { cwd, ignore: ['**/node_modules/**', '**/.git/**'], absolute: false }),
|
|
177
|
+
glob('**/docker-compose*.yml', { cwd, ignore: ['**/node_modules/**', '**/.git/**'], absolute: false }),
|
|
178
|
+
glob('**/*.bicep', { cwd, ignore: ['**/node_modules/**', '**/.git/**'], absolute: false }),
|
|
179
|
+
glob('**/{azure-pipelines.yml,.github/workflows/*.yml,.gitlab-ci.yml}', {
|
|
180
|
+
cwd,
|
|
181
|
+
ignore: ['**/node_modules/**'],
|
|
182
|
+
absolute: false
|
|
183
|
+
})
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
dockerfiles,
|
|
188
|
+
dockerComposeFiles,
|
|
189
|
+
bicepFiles,
|
|
190
|
+
pipelines,
|
|
191
|
+
hasAzure: bicepFiles.length > 0 || pipelines.some(p => p.includes('azure')),
|
|
192
|
+
hasDocker: dockerfiles.length > 0 || dockerComposeFiles.length > 0,
|
|
193
|
+
hasDevOps: pipelines.length > 0
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return {
|
|
197
|
+
dockerfiles: [],
|
|
198
|
+
dockerComposeFiles: [],
|
|
199
|
+
bicepFiles: [],
|
|
200
|
+
pipelines: [],
|
|
201
|
+
hasAzure: false,
|
|
202
|
+
hasDocker: false,
|
|
203
|
+
hasDevOps: false
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get git remote URL
|
|
210
|
+
* @param {string} cwd - Current working directory
|
|
211
|
+
* @returns {Promise<string|null>}
|
|
212
|
+
*/
|
|
213
|
+
async getGitRemote(cwd) {
|
|
214
|
+
try {
|
|
215
|
+
const remote = execSync('git remote get-url origin', {
|
|
216
|
+
cwd,
|
|
217
|
+
encoding: 'utf-8',
|
|
218
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
219
|
+
}).trim();
|
|
220
|
+
|
|
221
|
+
return remote || null;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// Not a git repo or no remote
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Helper: Read file if it exists
|
|
230
|
+
* @param {string} filepath - Absolute file path
|
|
231
|
+
* @returns {Promise<string|null>}
|
|
232
|
+
*/
|
|
233
|
+
async readFileIfExists(filepath) {
|
|
234
|
+
try {
|
|
235
|
+
await access(filepath);
|
|
236
|
+
const content = await readFile(filepath, 'utf-8');
|
|
237
|
+
return content;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|