@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,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
+ }