wogiflow 1.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.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Project Analyzer
|
|
5
|
+
*
|
|
6
|
+
* Analyzes a project and populates config.json with hybrid projectContext settings.
|
|
7
|
+
* Called during onboarding to ensure the local LLM has all the context it needs.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node flow-project-analyzer.js [project-root]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { getProjectRoot } = require('./flow-utils');
|
|
16
|
+
|
|
17
|
+
const PROJECT_ROOT = process.argv[2] || getProjectRoot();
|
|
18
|
+
const CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// Detection Functions
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Detect UI framework from package.json and project files
|
|
26
|
+
*/
|
|
27
|
+
function detectUIFramework() {
|
|
28
|
+
const packageJsonPath = path.join(PROJECT_ROOT, 'package.json');
|
|
29
|
+
if (!fs.existsSync(packageJsonPath)) return null;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
33
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
34
|
+
|
|
35
|
+
if (deps['next']) return 'next';
|
|
36
|
+
if (deps['@angular/core']) return 'angular';
|
|
37
|
+
if (deps['vue']) return 'vue';
|
|
38
|
+
if (deps['svelte']) return 'svelte';
|
|
39
|
+
if (deps['react-native']) return 'react-native';
|
|
40
|
+
if (deps['react']) return 'react';
|
|
41
|
+
if (deps['@nestjs/core']) return 'nestjs';
|
|
42
|
+
if (deps['express']) return 'express';
|
|
43
|
+
if (deps['fastify']) return 'fastify';
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Detect styling approach from dependencies and project files
|
|
53
|
+
*/
|
|
54
|
+
function detectStylingApproach() {
|
|
55
|
+
const packageJsonPath = path.join(PROJECT_ROOT, 'package.json');
|
|
56
|
+
if (!fs.existsSync(packageJsonPath)) return null;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
60
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
61
|
+
|
|
62
|
+
// Check dependencies
|
|
63
|
+
if (deps['styled-components']) return 'styled-components';
|
|
64
|
+
if (deps['@emotion/react'] || deps['@emotion/styled']) return 'emotion';
|
|
65
|
+
if (deps['tailwindcss']) return 'tailwind';
|
|
66
|
+
if (deps['sass'] || deps['node-sass']) return 'sass';
|
|
67
|
+
if (deps['less']) return 'less';
|
|
68
|
+
|
|
69
|
+
// Check for tailwind config
|
|
70
|
+
if (fs.existsSync(path.join(PROJECT_ROOT, 'tailwind.config.js')) ||
|
|
71
|
+
fs.existsSync(path.join(PROJECT_ROOT, 'tailwind.config.ts'))) {
|
|
72
|
+
return 'tailwind';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for CSS modules usage
|
|
76
|
+
const srcDir = path.join(PROJECT_ROOT, 'src');
|
|
77
|
+
if (fs.existsSync(srcDir)) {
|
|
78
|
+
const hasCSSModules = findFiles(srcDir, /\.module\.css$/).length > 0;
|
|
79
|
+
if (hasCSSModules) return 'css-modules';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find files matching a pattern in a directory
|
|
90
|
+
*/
|
|
91
|
+
function findFiles(dir, pattern, results = [], depth = 0) {
|
|
92
|
+
if (depth > 5) return results; // Limit depth
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
96
|
+
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const fullPath = path.join(dir, entry.name);
|
|
99
|
+
|
|
100
|
+
// Skip common excluded directories
|
|
101
|
+
if (entry.isDirectory()) {
|
|
102
|
+
if (['node_modules', '.git', 'dist', 'build', '.next', '.nuxt'].includes(entry.name)) continue;
|
|
103
|
+
findFiles(fullPath, pattern, results, depth + 1);
|
|
104
|
+
} else if (pattern.test(entry.name)) {
|
|
105
|
+
results.push(fullPath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Ignore read errors
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Find component directories in the project
|
|
117
|
+
*/
|
|
118
|
+
function findComponentDirs() {
|
|
119
|
+
const possibleDirs = [
|
|
120
|
+
'src/components',
|
|
121
|
+
'components',
|
|
122
|
+
'src/shared/components',
|
|
123
|
+
'apps/web/src/components',
|
|
124
|
+
'packages/ui/src',
|
|
125
|
+
'src/ui',
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
return possibleDirs.filter(dir => {
|
|
129
|
+
const fullPath = path.join(PROJECT_ROOT, dir);
|
|
130
|
+
return fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Find type file directories/patterns
|
|
136
|
+
*/
|
|
137
|
+
function findTypeDirs() {
|
|
138
|
+
const possiblePatterns = [
|
|
139
|
+
'src/types',
|
|
140
|
+
'types',
|
|
141
|
+
'src/@types',
|
|
142
|
+
'@types',
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const foundDirs = possiblePatterns.filter(dir => {
|
|
146
|
+
const fullPath = path.join(PROJECT_ROOT, dir);
|
|
147
|
+
return fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Also look for types.ts files in features
|
|
151
|
+
const typeFiles = findFiles(path.join(PROJECT_ROOT, 'src'), /types\.ts$/);
|
|
152
|
+
if (typeFiles.length > 0) {
|
|
153
|
+
// Extract patterns from found type files
|
|
154
|
+
const patterns = new Set();
|
|
155
|
+
for (const file of typeFiles) {
|
|
156
|
+
const relative = path.relative(PROJECT_ROOT, file);
|
|
157
|
+
// Create a pattern from the path
|
|
158
|
+
if (relative.includes('features/')) {
|
|
159
|
+
patterns.add('src/features/*/api/types.ts');
|
|
160
|
+
} else if (relative.includes('modules/')) {
|
|
161
|
+
patterns.add('src/modules/*/types.ts');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
foundDirs.push(...patterns);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return foundDirs.length > 0 ? foundDirs : ['src/types/*.ts'];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Scan a component directory and extract available components with their exports
|
|
172
|
+
*/
|
|
173
|
+
function scanComponentExports(componentDir) {
|
|
174
|
+
const components = {};
|
|
175
|
+
const fullDir = path.join(PROJECT_ROOT, componentDir);
|
|
176
|
+
|
|
177
|
+
if (!fs.existsSync(fullDir)) return components;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const entries = fs.readdirSync(fullDir, { withFileTypes: true });
|
|
181
|
+
|
|
182
|
+
for (const entry of entries) {
|
|
183
|
+
if (!entry.isDirectory()) continue;
|
|
184
|
+
|
|
185
|
+
const compPath = path.join(fullDir, entry.name);
|
|
186
|
+
const indexPath = path.join(compPath, 'index.ts');
|
|
187
|
+
const indexTsxPath = path.join(compPath, 'index.tsx');
|
|
188
|
+
const mainFile = path.join(compPath, `${entry.name}.tsx`);
|
|
189
|
+
|
|
190
|
+
let exports = [];
|
|
191
|
+
|
|
192
|
+
// Try to find exports from index file
|
|
193
|
+
for (const indexFile of [indexPath, indexTsxPath]) {
|
|
194
|
+
if (fs.existsSync(indexFile)) {
|
|
195
|
+
const content = fs.readFileSync(indexFile, 'utf-8');
|
|
196
|
+
|
|
197
|
+
// Match export { X, Y, Z }
|
|
198
|
+
const reExports = content.match(/export\s+{\s*([^}]+)\s*}/g);
|
|
199
|
+
if (reExports) {
|
|
200
|
+
for (const match of reExports) {
|
|
201
|
+
const names = match.replace(/export\s*{\s*/, '').replace(/\s*}/, '').split(',');
|
|
202
|
+
exports.push(...names.map(n => n.trim().split(' ')[0]).filter(n => n));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Match export const/function/class X
|
|
207
|
+
const namedExports = content.match(/export\s+(?:const|function|class)\s+(\w+)/g);
|
|
208
|
+
if (namedExports) {
|
|
209
|
+
for (const match of namedExports) {
|
|
210
|
+
const name = match.split(/\s+/).pop();
|
|
211
|
+
if (name && !exports.includes(name)) exports.push(name);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// If no index, try main file
|
|
220
|
+
if (exports.length === 0 && fs.existsSync(mainFile)) {
|
|
221
|
+
const content = fs.readFileSync(mainFile, 'utf-8');
|
|
222
|
+
const namedExports = content.match(/export\s+(?:const|function|class)\s+(\w+)/g);
|
|
223
|
+
if (namedExports) {
|
|
224
|
+
for (const match of namedExports) {
|
|
225
|
+
const name = match.split(/\s+/).pop();
|
|
226
|
+
if (name) exports.push(name);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (exports.length > 0) {
|
|
232
|
+
components[entry.name] = {
|
|
233
|
+
exports: [...new Set(exports)],
|
|
234
|
+
importPath: `@/components/${entry.name}`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Ignore scan errors
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return components;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Generate glob patterns for component discovery based on detected framework
|
|
247
|
+
* This is a simplified, one-time detection that generates patterns for later use
|
|
248
|
+
*/
|
|
249
|
+
function generateComponentGlobPatterns(uiFramework, componentDirs) {
|
|
250
|
+
const patterns = [];
|
|
251
|
+
|
|
252
|
+
// Base component patterns
|
|
253
|
+
for (const dir of componentDirs) {
|
|
254
|
+
patterns.push(`${dir}/**/*.tsx`);
|
|
255
|
+
patterns.push(`${dir}/**/*.jsx`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Framework-specific patterns
|
|
259
|
+
switch (uiFramework) {
|
|
260
|
+
case 'next':
|
|
261
|
+
// Next.js app router components
|
|
262
|
+
patterns.push('app/**/*.tsx');
|
|
263
|
+
patterns.push('app/**/page.tsx');
|
|
264
|
+
patterns.push('app/**/layout.tsx');
|
|
265
|
+
// Pages router
|
|
266
|
+
patterns.push('pages/**/*.tsx');
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
case 'react':
|
|
270
|
+
case 'react-native':
|
|
271
|
+
// Common React patterns
|
|
272
|
+
patterns.push('src/**/*.tsx');
|
|
273
|
+
patterns.push('src/**/*.jsx');
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case 'vue':
|
|
277
|
+
patterns.push('src/**/*.vue');
|
|
278
|
+
patterns.push('components/**/*.vue');
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case 'angular':
|
|
282
|
+
patterns.push('src/**/*.component.ts');
|
|
283
|
+
patterns.push('src/**/*.component.html');
|
|
284
|
+
break;
|
|
285
|
+
|
|
286
|
+
case 'svelte':
|
|
287
|
+
patterns.push('src/**/*.svelte');
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
case 'nestjs':
|
|
291
|
+
// NestJS modules and controllers
|
|
292
|
+
patterns.push('src/**/*.module.ts');
|
|
293
|
+
patterns.push('src/**/*.controller.ts');
|
|
294
|
+
patterns.push('src/**/*.service.ts');
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case 'express':
|
|
298
|
+
case 'fastify':
|
|
299
|
+
patterns.push('src/**/*.ts');
|
|
300
|
+
patterns.push('src/routes/**/*.ts');
|
|
301
|
+
patterns.push('src/controllers/**/*.ts');
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Default fallback if no framework detected
|
|
306
|
+
if (patterns.length === 0) {
|
|
307
|
+
patterns.push('src/**/*.ts');
|
|
308
|
+
patterns.push('src/**/*.tsx');
|
|
309
|
+
patterns.push('src/**/*.js');
|
|
310
|
+
patterns.push('src/**/*.jsx');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return [...new Set(patterns)]; // Dedupe
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Generate simplified framework config for storing in config.json
|
|
318
|
+
* This provides all the info needed without re-detection
|
|
319
|
+
*/
|
|
320
|
+
function generateFrameworkConfig(analysis) {
|
|
321
|
+
return {
|
|
322
|
+
framework: analysis.uiFramework,
|
|
323
|
+
styling: analysis.stylingApproach,
|
|
324
|
+
componentPatterns: generateComponentGlobPatterns(analysis.uiFramework, analysis.componentDirs),
|
|
325
|
+
testPatterns: generateTestGlobPatterns(analysis.uiFramework),
|
|
326
|
+
configFiles: detectConfigFiles(),
|
|
327
|
+
detectedAt: new Date().toISOString()
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Generate test file glob patterns based on framework
|
|
333
|
+
*/
|
|
334
|
+
function generateTestGlobPatterns(uiFramework) {
|
|
335
|
+
const patterns = [
|
|
336
|
+
'**/*.test.ts',
|
|
337
|
+
'**/*.test.tsx',
|
|
338
|
+
'**/*.spec.ts',
|
|
339
|
+
'**/*.spec.tsx',
|
|
340
|
+
'__tests__/**/*.ts',
|
|
341
|
+
'__tests__/**/*.tsx',
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
// Framework-specific test patterns
|
|
345
|
+
if (uiFramework === 'angular') {
|
|
346
|
+
patterns.push('**/*.spec.ts');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (uiFramework === 'nestjs') {
|
|
350
|
+
patterns.push('test/**/*.e2e-spec.ts');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return patterns;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Detect important config files in the project
|
|
358
|
+
*/
|
|
359
|
+
function detectConfigFiles() {
|
|
360
|
+
const configFiles = {};
|
|
361
|
+
const checkFiles = [
|
|
362
|
+
'tsconfig.json',
|
|
363
|
+
'package.json',
|
|
364
|
+
'tailwind.config.js',
|
|
365
|
+
'tailwind.config.ts',
|
|
366
|
+
'next.config.js',
|
|
367
|
+
'next.config.mjs',
|
|
368
|
+
'vite.config.ts',
|
|
369
|
+
'webpack.config.js',
|
|
370
|
+
'.eslintrc',
|
|
371
|
+
'.eslintrc.js',
|
|
372
|
+
'.eslintrc.json',
|
|
373
|
+
'jest.config.js',
|
|
374
|
+
'vitest.config.ts',
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
for (const file of checkFiles) {
|
|
378
|
+
const fullPath = path.join(PROJECT_ROOT, file);
|
|
379
|
+
if (fs.existsSync(fullPath)) {
|
|
380
|
+
configFiles[file] = true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return configFiles;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Detect type import locations based on project structure
|
|
389
|
+
*/
|
|
390
|
+
function detectTypeLocations() {
|
|
391
|
+
const locations = {};
|
|
392
|
+
|
|
393
|
+
// Check for common patterns
|
|
394
|
+
const featureTypesExist = findFiles(path.join(PROJECT_ROOT, 'src'), /\/api\/types\.ts$/).length > 0;
|
|
395
|
+
if (featureTypesExist) {
|
|
396
|
+
locations['features'] = '../api/types';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const sharedTypesDir = path.join(PROJECT_ROOT, 'src/types');
|
|
400
|
+
if (fs.existsSync(sharedTypesDir)) {
|
|
401
|
+
locations['shared'] = '@/types';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return locations;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Generate warnings based on detected framework
|
|
409
|
+
*/
|
|
410
|
+
function generateWarnings(uiFramework, stylingApproach) {
|
|
411
|
+
const warnings = [];
|
|
412
|
+
|
|
413
|
+
// Framework-specific warnings
|
|
414
|
+
if (uiFramework === 'react' || uiFramework === 'next') {
|
|
415
|
+
// Check React version for JSX transform
|
|
416
|
+
const packageJsonPath = path.join(PROJECT_ROOT, 'package.json');
|
|
417
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
418
|
+
try {
|
|
419
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
420
|
+
const reactVersion = pkg.dependencies?.react || pkg.devDependencies?.react || '';
|
|
421
|
+
if (reactVersion && !reactVersion.includes('16.')) {
|
|
422
|
+
warnings.push("Don't import React directly - use named imports (useState, useCallback)");
|
|
423
|
+
}
|
|
424
|
+
} catch {}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Styling-specific warnings
|
|
429
|
+
if (stylingApproach === 'styled-components') {
|
|
430
|
+
warnings.push('Use transient props ($propName) to prevent DOM warnings');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (stylingApproach === 'tailwind') {
|
|
434
|
+
// Check if cn utility exists
|
|
435
|
+
const utilsPath = path.join(PROJECT_ROOT, 'src/lib/utils.ts');
|
|
436
|
+
if (!fs.existsSync(utilsPath)) {
|
|
437
|
+
warnings.push("cn() utility may not exist - use clsx or className directly");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return warnings;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Detect directories to exclude from type scanning
|
|
446
|
+
*/
|
|
447
|
+
function detectExcludeDirectories() {
|
|
448
|
+
// Always exclude these
|
|
449
|
+
const excludes = ['__tests__', '__mocks__', 'node_modules', '.git', 'dist', 'build'];
|
|
450
|
+
|
|
451
|
+
// Check for monorepo structure and add internal packages
|
|
452
|
+
const packagesDir = path.join(PROJECT_ROOT, 'packages');
|
|
453
|
+
if (fs.existsSync(packagesDir)) {
|
|
454
|
+
try {
|
|
455
|
+
const packages = fs.readdirSync(packagesDir);
|
|
456
|
+
// Internal packages often have types that aren't relevant to app code
|
|
457
|
+
// User can customize this via config
|
|
458
|
+
} catch {}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return excludes;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Detect type patterns to exclude (project-specific internal types)
|
|
466
|
+
*/
|
|
467
|
+
function detectExcludeTypePatterns() {
|
|
468
|
+
// Start with empty - let users configure this per project
|
|
469
|
+
// During onboarding, we'll ask if there are internal types to exclude
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ============================================================
|
|
474
|
+
// Main Analysis Function
|
|
475
|
+
// ============================================================
|
|
476
|
+
|
|
477
|
+
function analyzeProject() {
|
|
478
|
+
console.log('Analyzing project for hybrid mode configuration...\n');
|
|
479
|
+
|
|
480
|
+
const analysis = {
|
|
481
|
+
uiFramework: detectUIFramework(),
|
|
482
|
+
stylingApproach: detectStylingApproach(),
|
|
483
|
+
componentDirs: findComponentDirs(),
|
|
484
|
+
typeDirs: findTypeDirs(),
|
|
485
|
+
availableComponents: {},
|
|
486
|
+
typeLocations: detectTypeLocations(),
|
|
487
|
+
doNotImport: ['React'], // Default for React 17+
|
|
488
|
+
excludeTypePatterns: detectExcludeTypePatterns(),
|
|
489
|
+
excludeDirectories: detectExcludeDirectories(),
|
|
490
|
+
projectWarnings: [],
|
|
491
|
+
customRules: [],
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Scan components
|
|
495
|
+
for (const dir of analysis.componentDirs) {
|
|
496
|
+
const components = scanComponentExports(dir);
|
|
497
|
+
Object.assign(analysis.availableComponents, components);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Generate warnings
|
|
501
|
+
analysis.projectWarnings = generateWarnings(analysis.uiFramework, analysis.stylingApproach);
|
|
502
|
+
|
|
503
|
+
// Report findings
|
|
504
|
+
console.log(`UI Framework: ${analysis.uiFramework || 'not detected'}`);
|
|
505
|
+
console.log(`Styling: ${analysis.stylingApproach || 'not detected'}`);
|
|
506
|
+
console.log(`Component dirs: ${analysis.componentDirs.length > 0 ? analysis.componentDirs.join(', ') : 'none found'}`);
|
|
507
|
+
console.log(`Components found: ${Object.keys(analysis.availableComponents).length}`);
|
|
508
|
+
console.log(`Type locations: ${Object.keys(analysis.typeLocations).length > 0 ? JSON.stringify(analysis.typeLocations) : 'default'}`);
|
|
509
|
+
console.log('');
|
|
510
|
+
|
|
511
|
+
return analysis;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Update config.json with analyzed project context
|
|
516
|
+
*/
|
|
517
|
+
function updateConfig(analysis) {
|
|
518
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
519
|
+
console.log('Warning: config.json not found. Run flow init first.');
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
525
|
+
|
|
526
|
+
// Ensure hybrid section exists
|
|
527
|
+
if (!config.hybrid) config.hybrid = {};
|
|
528
|
+
if (!config.hybrid.projectContext) config.hybrid.projectContext = {};
|
|
529
|
+
|
|
530
|
+
// Update project context
|
|
531
|
+
const ctx = config.hybrid.projectContext;
|
|
532
|
+
ctx.uiFramework = analysis.uiFramework;
|
|
533
|
+
ctx.stylingApproach = analysis.stylingApproach;
|
|
534
|
+
ctx.componentDirs = analysis.componentDirs;
|
|
535
|
+
ctx.typeDirs = analysis.typeDirs;
|
|
536
|
+
ctx.availableComponents = analysis.availableComponents;
|
|
537
|
+
ctx.typeLocations = analysis.typeLocations;
|
|
538
|
+
ctx.doNotImport = analysis.doNotImport;
|
|
539
|
+
ctx.excludeTypePatterns = analysis.excludeTypePatterns;
|
|
540
|
+
ctx.excludeDirectories = analysis.excludeDirectories;
|
|
541
|
+
ctx.projectWarnings = analysis.projectWarnings;
|
|
542
|
+
ctx.customRules = analysis.customRules;
|
|
543
|
+
|
|
544
|
+
// Add simplified framework config with glob patterns
|
|
545
|
+
// This is the one-time detection result that can be used without re-scanning
|
|
546
|
+
config.frameworkConfig = generateFrameworkConfig(analysis);
|
|
547
|
+
|
|
548
|
+
// Write back
|
|
549
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
550
|
+
|
|
551
|
+
console.log('✓ Updated config.json with project context');
|
|
552
|
+
console.log(` Framework: ${analysis.uiFramework || 'not detected'}`);
|
|
553
|
+
console.log(` Styling: ${analysis.stylingApproach || 'not detected'}`);
|
|
554
|
+
console.log(` Component patterns: ${config.frameworkConfig.componentPatterns.length}`);
|
|
555
|
+
return true;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.log(`Error updating config: ${err.message}`);
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Delete cached context to force regeneration
|
|
564
|
+
*/
|
|
565
|
+
function clearContextCache() {
|
|
566
|
+
const cachePath = path.join(PROJECT_ROOT, '.workflow/state/hybrid-context.md');
|
|
567
|
+
if (fs.existsSync(cachePath)) {
|
|
568
|
+
fs.unlinkSync(cachePath);
|
|
569
|
+
console.log('✓ Cleared hybrid context cache');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ============================================================
|
|
574
|
+
// Codebase Insights Generation
|
|
575
|
+
// ============================================================
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Detect architecture pattern based on directory structure
|
|
579
|
+
*/
|
|
580
|
+
function detectArchitecturePattern() {
|
|
581
|
+
const indicators = {
|
|
582
|
+
monorepo: fs.existsSync(path.join(PROJECT_ROOT, 'packages')) ||
|
|
583
|
+
fs.existsSync(path.join(PROJECT_ROOT, 'apps')),
|
|
584
|
+
modular: fs.existsSync(path.join(PROJECT_ROOT, 'src/modules')) ||
|
|
585
|
+
fs.existsSync(path.join(PROJECT_ROOT, 'src/features')),
|
|
586
|
+
layered: fs.existsSync(path.join(PROJECT_ROOT, 'src/controllers')) &&
|
|
587
|
+
fs.existsSync(path.join(PROJECT_ROOT, 'src/services')),
|
|
588
|
+
componentBased: fs.existsSync(path.join(PROJECT_ROOT, 'src/components')),
|
|
589
|
+
pagesBased: fs.existsSync(path.join(PROJECT_ROOT, 'pages')) ||
|
|
590
|
+
fs.existsSync(path.join(PROJECT_ROOT, 'app'))
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
if (indicators.monorepo) return { pattern: 'Monorepo', description: 'Multi-package workspace with shared code' };
|
|
594
|
+
if (indicators.modular) return { pattern: 'Modular Monolith', description: 'Feature-based module structure in single deployable' };
|
|
595
|
+
if (indicators.layered) return { pattern: 'Layered Architecture', description: 'Separated controllers, services, and repositories' };
|
|
596
|
+
if (indicators.pagesBased && indicators.componentBased) return { pattern: 'Page-Component Architecture', description: 'Page-based routing with shared components' };
|
|
597
|
+
if (indicators.componentBased) return { pattern: 'Component-Based', description: 'UI component focused structure' };
|
|
598
|
+
|
|
599
|
+
return { pattern: 'Simple/Flat', description: 'Basic project structure' };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Detect naming conventions from file samples
|
|
604
|
+
*/
|
|
605
|
+
function detectConventions() {
|
|
606
|
+
const conventions = {
|
|
607
|
+
files: 'unknown',
|
|
608
|
+
components: 'unknown',
|
|
609
|
+
functions: 'unknown',
|
|
610
|
+
constants: 'unknown',
|
|
611
|
+
imports: 'unknown'
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const srcDir = path.join(PROJECT_ROOT, 'src');
|
|
615
|
+
if (!fs.existsSync(srcDir)) return conventions;
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
// Sample some files
|
|
619
|
+
const tsFiles = findFiles(srcDir, /\.tsx?$/).slice(0, 20);
|
|
620
|
+
|
|
621
|
+
// Check file naming
|
|
622
|
+
const fileNames = tsFiles.map(f => path.basename(f, path.extname(f)));
|
|
623
|
+
const kebabCount = fileNames.filter(n => /^[a-z]+(-[a-z]+)*$/.test(n)).length;
|
|
624
|
+
const pascalCount = fileNames.filter(n => /^[A-Z][a-zA-Z]*$/.test(n)).length;
|
|
625
|
+
const camelCount = fileNames.filter(n => /^[a-z][a-zA-Z]*$/.test(n)).length;
|
|
626
|
+
|
|
627
|
+
if (kebabCount > pascalCount && kebabCount > camelCount) conventions.files = 'kebab-case';
|
|
628
|
+
else if (pascalCount > camelCount) conventions.files = 'PascalCase';
|
|
629
|
+
else if (camelCount > 0) conventions.files = 'camelCase';
|
|
630
|
+
|
|
631
|
+
// Check component naming (from .tsx files)
|
|
632
|
+
const componentFiles = tsFiles.filter(f => f.endsWith('.tsx'));
|
|
633
|
+
if (componentFiles.length > 0) {
|
|
634
|
+
const content = fs.readFileSync(componentFiles[0], 'utf-8');
|
|
635
|
+
if (/export\s+(default\s+)?function\s+[A-Z]/.test(content)) {
|
|
636
|
+
conventions.components = 'PascalCase function components';
|
|
637
|
+
} else if (/const\s+[A-Z][a-zA-Z]+\s*=\s*\(/.test(content)) {
|
|
638
|
+
conventions.components = 'PascalCase arrow function components';
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Check import style
|
|
643
|
+
if (tsFiles.length > 0) {
|
|
644
|
+
const content = fs.readFileSync(tsFiles[0], 'utf-8');
|
|
645
|
+
if (content.includes('@/')) conventions.imports = 'Absolute with @/ alias';
|
|
646
|
+
else if (content.includes('~/')) conventions.imports = 'Absolute with ~/ alias';
|
|
647
|
+
else if (/from\s+['"]\.\.?\//.test(content)) conventions.imports = 'Relative imports';
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
} catch {
|
|
651
|
+
// Ignore errors
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return conventions;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Detect potential issues in the codebase
|
|
659
|
+
*/
|
|
660
|
+
function detectPotentialIssues() {
|
|
661
|
+
const issues = [];
|
|
662
|
+
const srcDir = path.join(PROJECT_ROOT, 'src');
|
|
663
|
+
|
|
664
|
+
if (!fs.existsSync(srcDir)) return issues;
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
// Check for large files (>500 lines)
|
|
668
|
+
const allFiles = findFiles(srcDir, /\.(ts|tsx|js|jsx)$/);
|
|
669
|
+
for (const file of allFiles.slice(0, 100)) {
|
|
670
|
+
try {
|
|
671
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
672
|
+
const lines = content.split('\n').length;
|
|
673
|
+
if (lines > 500) {
|
|
674
|
+
issues.push({
|
|
675
|
+
type: 'large-file',
|
|
676
|
+
severity: 'warning',
|
|
677
|
+
file: path.relative(PROJECT_ROOT, file),
|
|
678
|
+
message: `Large file (${lines} lines) - consider splitting`
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
} catch {}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Check for files without tests
|
|
685
|
+
const componentFiles = allFiles.filter(f =>
|
|
686
|
+
f.includes('/components/') &&
|
|
687
|
+
!f.includes('.test.') &&
|
|
688
|
+
!f.includes('.spec.') &&
|
|
689
|
+
!f.includes('.stories.')
|
|
690
|
+
);
|
|
691
|
+
const testFiles = findFiles(srcDir, /\.(test|spec)\.(ts|tsx|js|jsx)$/);
|
|
692
|
+
const testedComponents = new Set(
|
|
693
|
+
testFiles.map(f => path.basename(f).replace(/\.(test|spec)\.(ts|tsx|js|jsx)$/, ''))
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
for (const comp of componentFiles.slice(0, 20)) {
|
|
697
|
+
const baseName = path.basename(comp).replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
698
|
+
if (!testedComponents.has(baseName) && baseName !== 'index') {
|
|
699
|
+
issues.push({
|
|
700
|
+
type: 'missing-test',
|
|
701
|
+
severity: 'info',
|
|
702
|
+
file: path.relative(PROJECT_ROOT, comp),
|
|
703
|
+
message: 'No test file found'
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Check for console.log statements
|
|
709
|
+
let consoleCount = 0;
|
|
710
|
+
for (const file of allFiles.slice(0, 50)) {
|
|
711
|
+
try {
|
|
712
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
713
|
+
const matches = content.match(/console\.(log|warn|error)\(/g);
|
|
714
|
+
if (matches) consoleCount += matches.length;
|
|
715
|
+
} catch {}
|
|
716
|
+
}
|
|
717
|
+
if (consoleCount > 10) {
|
|
718
|
+
issues.push({
|
|
719
|
+
type: 'console-statements',
|
|
720
|
+
severity: 'info',
|
|
721
|
+
message: `Found ${consoleCount} console statements - consider removing for production`
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
} catch {
|
|
726
|
+
// Ignore errors
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return issues;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Gather project statistics
|
|
734
|
+
*/
|
|
735
|
+
function gatherStatistics() {
|
|
736
|
+
const stats = {
|
|
737
|
+
totalFiles: 0,
|
|
738
|
+
typeScriptFiles: 0,
|
|
739
|
+
javaScriptFiles: 0,
|
|
740
|
+
testFiles: 0,
|
|
741
|
+
componentCount: 0,
|
|
742
|
+
hookCount: 0,
|
|
743
|
+
serviceCount: 0
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const srcDir = path.join(PROJECT_ROOT, 'src');
|
|
747
|
+
if (!fs.existsSync(srcDir)) return stats;
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
const allFiles = findFiles(srcDir, /\.(ts|tsx|js|jsx)$/);
|
|
751
|
+
stats.totalFiles = allFiles.length;
|
|
752
|
+
stats.typeScriptFiles = allFiles.filter(f => /\.tsx?$/.test(f)).length;
|
|
753
|
+
stats.javaScriptFiles = allFiles.filter(f => /\.jsx?$/.test(f)).length;
|
|
754
|
+
stats.testFiles = allFiles.filter(f => /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(f)).length;
|
|
755
|
+
|
|
756
|
+
// Count components (files in components/ directory or .tsx files with PascalCase names)
|
|
757
|
+
stats.componentCount = allFiles.filter(f =>
|
|
758
|
+
(f.includes('/components/') || f.includes('/ui/')) &&
|
|
759
|
+
!f.includes('.test.') &&
|
|
760
|
+
!f.includes('.stories.')
|
|
761
|
+
).length;
|
|
762
|
+
|
|
763
|
+
// Count hooks (use*.ts files)
|
|
764
|
+
stats.hookCount = allFiles.filter(f =>
|
|
765
|
+
/\/use[A-Z][a-zA-Z]*\.(ts|tsx)$/.test(f)
|
|
766
|
+
).length;
|
|
767
|
+
|
|
768
|
+
// Count services
|
|
769
|
+
stats.serviceCount = allFiles.filter(f =>
|
|
770
|
+
f.includes('.service.') || f.includes('/services/')
|
|
771
|
+
).length;
|
|
772
|
+
|
|
773
|
+
} catch {
|
|
774
|
+
// Ignore errors
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return stats;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Generate codebase insights markdown file
|
|
782
|
+
*/
|
|
783
|
+
function generateCodebaseInsights() {
|
|
784
|
+
const architecture = detectArchitecturePattern();
|
|
785
|
+
const conventions = detectConventions();
|
|
786
|
+
const issues = detectPotentialIssues();
|
|
787
|
+
const stats = gatherStatistics();
|
|
788
|
+
const framework = detectUIFramework();
|
|
789
|
+
const styling = detectStylingApproach();
|
|
790
|
+
|
|
791
|
+
const tsRatio = stats.totalFiles > 0
|
|
792
|
+
? Math.round((stats.typeScriptFiles / stats.totalFiles) * 100)
|
|
793
|
+
: 0;
|
|
794
|
+
|
|
795
|
+
let markdown = `# Codebase Insights
|
|
796
|
+
|
|
797
|
+
Generated: ${new Date().toISOString().split('T')[0]}
|
|
798
|
+
|
|
799
|
+
## Architecture Pattern
|
|
800
|
+
|
|
801
|
+
**${architecture.pattern}**
|
|
802
|
+
|
|
803
|
+
${architecture.description}
|
|
804
|
+
|
|
805
|
+
## Tech Stack
|
|
806
|
+
|
|
807
|
+
- **Framework**: ${framework || 'Not detected'}
|
|
808
|
+
- **Styling**: ${styling || 'Not detected'}
|
|
809
|
+
- **TypeScript**: ${tsRatio}% of codebase
|
|
810
|
+
|
|
811
|
+
## Conventions Detected
|
|
812
|
+
|
|
813
|
+
| Aspect | Convention |
|
|
814
|
+
|--------|------------|
|
|
815
|
+
| Files | ${conventions.files} |
|
|
816
|
+
| Components | ${conventions.components} |
|
|
817
|
+
| Imports | ${conventions.imports} |
|
|
818
|
+
|
|
819
|
+
## Statistics
|
|
820
|
+
|
|
821
|
+
| Metric | Count |
|
|
822
|
+
|--------|-------|
|
|
823
|
+
| Total source files | ${stats.totalFiles} |
|
|
824
|
+
| TypeScript files | ${stats.typeScriptFiles} |
|
|
825
|
+
| Test files | ${stats.testFiles} |
|
|
826
|
+
| Components | ${stats.componentCount} |
|
|
827
|
+
| Hooks | ${stats.hookCount} |
|
|
828
|
+
| Services | ${stats.serviceCount} |
|
|
829
|
+
|
|
830
|
+
`;
|
|
831
|
+
|
|
832
|
+
// Add issues section if any
|
|
833
|
+
if (issues.length > 0) {
|
|
834
|
+
markdown += `## Potential Issues
|
|
835
|
+
|
|
836
|
+
`;
|
|
837
|
+
const grouped = {
|
|
838
|
+
'large-file': [],
|
|
839
|
+
'missing-test': [],
|
|
840
|
+
'console-statements': [],
|
|
841
|
+
'other': []
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
for (const issue of issues) {
|
|
845
|
+
const group = grouped[issue.type] || grouped['other'];
|
|
846
|
+
group.push(issue);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (grouped['large-file'].length > 0) {
|
|
850
|
+
markdown += `### Large Files\n`;
|
|
851
|
+
for (const issue of grouped['large-file'].slice(0, 5)) {
|
|
852
|
+
markdown += `- [ ] \`${issue.file}\` - ${issue.message}\n`;
|
|
853
|
+
}
|
|
854
|
+
markdown += '\n';
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (grouped['missing-test'].length > 0) {
|
|
858
|
+
markdown += `### Missing Tests (${grouped['missing-test'].length} files)\n`;
|
|
859
|
+
for (const issue of grouped['missing-test'].slice(0, 5)) {
|
|
860
|
+
markdown += `- [ ] \`${issue.file}\`\n`;
|
|
861
|
+
}
|
|
862
|
+
if (grouped['missing-test'].length > 5) {
|
|
863
|
+
markdown += `- ... and ${grouped['missing-test'].length - 5} more\n`;
|
|
864
|
+
}
|
|
865
|
+
markdown += '\n';
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (grouped['console-statements'].length > 0) {
|
|
869
|
+
markdown += `### Code Quality\n`;
|
|
870
|
+
for (const issue of grouped['console-statements']) {
|
|
871
|
+
markdown += `- [ ] ${issue.message}\n`;
|
|
872
|
+
}
|
|
873
|
+
markdown += '\n';
|
|
874
|
+
}
|
|
875
|
+
} else {
|
|
876
|
+
markdown += `## Code Health
|
|
877
|
+
|
|
878
|
+
No significant issues detected.\n`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return markdown;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Save codebase insights to file
|
|
886
|
+
*/
|
|
887
|
+
function saveCodebaseInsights() {
|
|
888
|
+
const insightsPath = path.join(PROJECT_ROOT, '.workflow', 'state', 'codebase-insights.md');
|
|
889
|
+
const dir = path.dirname(insightsPath);
|
|
890
|
+
|
|
891
|
+
if (!fs.existsSync(dir)) {
|
|
892
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const markdown = generateCodebaseInsights();
|
|
896
|
+
fs.writeFileSync(insightsPath, markdown);
|
|
897
|
+
|
|
898
|
+
console.log(`✓ Generated codebase insights: ${insightsPath}`);
|
|
899
|
+
return insightsPath;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// ============================================================
|
|
903
|
+
// CLI
|
|
904
|
+
// ============================================================
|
|
905
|
+
|
|
906
|
+
function printUsage() {
|
|
907
|
+
console.log(`
|
|
908
|
+
Wogi Flow - Project Analyzer
|
|
909
|
+
|
|
910
|
+
Analyzes your project and configures hybrid mode settings so the local LLM
|
|
911
|
+
has all the context it needs to generate correct code.
|
|
912
|
+
|
|
913
|
+
Usage:
|
|
914
|
+
node flow-project-analyzer.js [project-root]
|
|
915
|
+
|
|
916
|
+
What it detects:
|
|
917
|
+
- UI framework (React, Next.js, Vue, Angular, etc.)
|
|
918
|
+
- Styling approach (styled-components, Tailwind, CSS modules, etc.)
|
|
919
|
+
- Component directories and their exports
|
|
920
|
+
- Type file locations
|
|
921
|
+
- Import conventions
|
|
922
|
+
|
|
923
|
+
The results are saved to config.json -> hybrid.projectContext
|
|
924
|
+
`);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Main
|
|
928
|
+
if (require.main === module) {
|
|
929
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
930
|
+
printUsage();
|
|
931
|
+
process.exit(0);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Generate insights only
|
|
935
|
+
if (process.argv.includes('--insights')) {
|
|
936
|
+
saveCodebaseInsights();
|
|
937
|
+
process.exit(0);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const analysis = analyzeProject();
|
|
941
|
+
const success = updateConfig(analysis);
|
|
942
|
+
clearContextCache();
|
|
943
|
+
|
|
944
|
+
// Also generate codebase insights during full analysis
|
|
945
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
946
|
+
if (config.codebaseInsights?.enabled !== false) {
|
|
947
|
+
saveCodebaseInsights();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (success) {
|
|
951
|
+
console.log('\n✓ Project analysis complete!');
|
|
952
|
+
console.log(' The local LLM will now have accurate context about your project.');
|
|
953
|
+
console.log(' Run "flow hybrid enable" to start using hybrid mode.');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
process.exit(success ? 0 : 1);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
module.exports = {
|
|
960
|
+
analyzeProject,
|
|
961
|
+
updateConfig,
|
|
962
|
+
detectUIFramework,
|
|
963
|
+
detectStylingApproach,
|
|
964
|
+
scanComponentExports,
|
|
965
|
+
generateComponentGlobPatterns,
|
|
966
|
+
generateFrameworkConfig,
|
|
967
|
+
detectConfigFiles,
|
|
968
|
+
// Codebase insights
|
|
969
|
+
detectArchitecturePattern,
|
|
970
|
+
detectConventions,
|
|
971
|
+
detectPotentialIssues,
|
|
972
|
+
gatherStatistics,
|
|
973
|
+
generateCodebaseInsights,
|
|
974
|
+
saveCodebaseInsights
|
|
975
|
+
};
|