@mrpatronz/nexusflow 0.2.1 → 0.2.3
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/dist/analyzers/index.d.ts +2 -0
- package/dist/analyzers/index.d.ts.map +1 -1
- package/dist/analyzers/index.js +9 -1
- package/dist/analyzers/index.js.map +1 -1
- package/dist/analyzers/messaging-analyzer.d.ts +17 -0
- package/dist/analyzers/messaging-analyzer.d.ts.map +1 -0
- package/dist/analyzers/messaging-analyzer.js +229 -0
- package/dist/analyzers/messaging-analyzer.js.map +1 -0
- package/dist/analyzers/readme-summarizer.d.ts.map +1 -1
- package/dist/analyzers/readme-summarizer.js +8 -4
- package/dist/analyzers/readme-summarizer.js.map +1 -1
- package/dist/analyzers/run-analyzer.d.ts +15 -0
- package/dist/analyzers/run-analyzer.d.ts.map +1 -0
- package/dist/analyzers/run-analyzer.js +246 -0
- package/dist/analyzers/run-analyzer.js.map +1 -0
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +18 -57
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +30 -14
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/map-generator.d.ts +1 -1
- package/dist/generators/map-generator.d.ts.map +1 -1
- package/dist/generators/map-generator.js +157 -54
- package/dist/generators/map-generator.js.map +1 -1
- package/dist/generators/map-generator.test.js +71 -0
- package/dist/generators/map-generator.test.js.map +1 -1
- package/dist/generators/plan-generator.d.ts +2 -2
- package/dist/generators/plan-generator.d.ts.map +1 -1
- package/dist/generators/plan-generator.js +47 -67
- package/dist/generators/plan-generator.js.map +1 -1
- package/dist/generators/skills-generator.d.ts +15 -0
- package/dist/generators/skills-generator.d.ts.map +1 -0
- package/dist/generators/skills-generator.js +225 -0
- package/dist/generators/skills-generator.js.map +1 -0
- package/dist/gui/assets/index-B3PIuWJS.js +22 -0
- package/dist/gui/assets/index-D-VigurY.css +2 -0
- package/dist/gui/index.html +2 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +16 -85
- package/dist/mcp/server.js.map +1 -1
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/gui/src/App.tsx +85 -3
- package/gui/src/features/workspace/WorkspaceList.tsx +85 -3
- package/package.json +1 -1
- package/src/analyzers/index.ts +9 -1
- package/src/analyzers/messaging-analyzer.ts +254 -0
- package/src/analyzers/readme-summarizer.ts +9 -4
- package/src/analyzers/run-analyzer.ts +269 -0
- package/src/core/config.ts +1 -1
- package/src/generators/base.ts +19 -56
- package/src/generators/index.ts +32 -14
- package/src/generators/map-generator.test.ts +78 -0
- package/src/generators/map-generator.ts +164 -53
- package/src/generators/plan-generator.ts +53 -75
- package/src/generators/skills-generator.ts +255 -0
- package/src/mcp/server.ts +16 -89
- package/src/types.ts +54 -0
- package/dist/gui/assets/index-C8W6FoPa.js +0 -21
- package/dist/gui/assets/index-CJn2LW8K.css +0 -2
package/src/generators/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { generateCopilotConfig } from './copilot.js';
|
|
|
8
8
|
import { generateCursorConfig } from './cursor.js';
|
|
9
9
|
import { buildContextContent } from './base.js';
|
|
10
10
|
import { generateImplementationPlan } from './plan-generator.js';
|
|
11
|
-
import {
|
|
11
|
+
import { generateSkills } from './skills-generator.js';
|
|
12
12
|
|
|
13
13
|
/** Maps each assistant to its generator function and the file it produces. */
|
|
14
14
|
const GENERATORS: Record<
|
|
@@ -65,6 +65,18 @@ ${feature.description}
|
|
|
65
65
|
|
|
66
66
|
${repoList}
|
|
67
67
|
|
|
68
|
+
## Project Assumptions (verify with user)
|
|
69
|
+
|
|
70
|
+
<!-- AI assistants: Fill this in during your first session. List each project and describe what you assume its main purpose, tech stack, and responsibilities are. -->
|
|
71
|
+
|
|
72
|
+
_(No assumptions recorded yet. AI assistant to populate.)_
|
|
73
|
+
|
|
74
|
+
## Clarifying Questions for the User
|
|
75
|
+
|
|
76
|
+
<!-- AI assistants: List any clarifying questions or ambiguities about requirements/architecture here. -->
|
|
77
|
+
|
|
78
|
+
_(No open questions recorded yet. AI assistant to populate.)_
|
|
79
|
+
|
|
68
80
|
## Architecture Decisions
|
|
69
81
|
|
|
70
82
|
<!-- AI assistants: append decisions here as they are made during development.
|
|
@@ -86,12 +98,6 @@ ${progressItems}
|
|
|
86
98
|
the same debugging. -->
|
|
87
99
|
|
|
88
100
|
_(No gotchas recorded yet.)_
|
|
89
|
-
|
|
90
|
-
## Open Questions
|
|
91
|
-
|
|
92
|
-
<!-- Add questions that need human input before the AI can proceed. -->
|
|
93
|
-
|
|
94
|
-
_(No open questions.)_
|
|
95
101
|
`;
|
|
96
102
|
}
|
|
97
103
|
|
|
@@ -141,12 +147,23 @@ export async function generateContextFiles(
|
|
|
141
147
|
|
|
142
148
|
// Generate per-repo architecture maps
|
|
143
149
|
if (ctx.analysis) {
|
|
150
|
+
const allProduced = new Set<string>();
|
|
151
|
+
for (const [, a] of ctx.analysis) {
|
|
152
|
+
if (a.produces) {
|
|
153
|
+
for (const p of a.produces) {
|
|
154
|
+
allProduced.add(p.name.toLowerCase());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Also treat the repository name as a produced package concept
|
|
158
|
+
allProduced.add(a.name.toLowerCase());
|
|
159
|
+
}
|
|
160
|
+
|
|
144
161
|
for (const repo of ctx.repos) {
|
|
145
162
|
const a = ctx.analysis.get(repo.path);
|
|
146
163
|
if (a) {
|
|
147
164
|
try {
|
|
148
165
|
const { generateRepoMap } = await import('./map-generator.js');
|
|
149
|
-
await generateRepoMap(repo, a, workspacePath);
|
|
166
|
+
await generateRepoMap(repo, a, workspacePath, allProduced);
|
|
150
167
|
console.log(chalk.green(' ✔'), `Generated Architecture Map for ${chalk.bold(repo.name)}`);
|
|
151
168
|
} catch (error) {
|
|
152
169
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -174,25 +191,26 @@ export async function generateContextFiles(
|
|
|
174
191
|
}
|
|
175
192
|
}
|
|
176
193
|
|
|
177
|
-
|
|
194
|
+
|
|
195
|
+
// Generate implementation plan from dependency analysis (if analysis data available)
|
|
178
196
|
try {
|
|
179
|
-
await
|
|
197
|
+
await generateImplementationPlan(ctx, workspacePath);
|
|
180
198
|
} catch (error) {
|
|
181
199
|
const message = error instanceof Error ? error.message : String(error);
|
|
182
200
|
console.error(
|
|
183
201
|
chalk.red(' ✖'),
|
|
184
|
-
`Failed to generate
|
|
202
|
+
`Failed to generate implementation plan: ${message}`,
|
|
185
203
|
);
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
// Generate
|
|
206
|
+
// Generate skills files for selected assistants
|
|
189
207
|
try {
|
|
190
|
-
await
|
|
208
|
+
await generateSkills(ctx, assistants, workspacePath);
|
|
191
209
|
} catch (error) {
|
|
192
210
|
const message = error instanceof Error ? error.message : String(error);
|
|
193
211
|
console.error(
|
|
194
212
|
chalk.red(' ✖'),
|
|
195
|
-
`Failed to generate
|
|
213
|
+
`Failed to generate skills: ${message}`,
|
|
196
214
|
);
|
|
197
215
|
}
|
|
198
216
|
}
|
|
@@ -78,4 +78,82 @@ describe('generateRepoMap', () => {
|
|
|
78
78
|
expect(content).toContain('GET');
|
|
79
79
|
expect(content).toContain('/api/v1/users');
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
it('should render messaging topology, run config, and group endpoints by module', async () => {
|
|
83
|
+
const mockRepo = {
|
|
84
|
+
name: 'test-repo',
|
|
85
|
+
path: '/original/path/test-repo',
|
|
86
|
+
defaultBranch: 'main',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const mockAnalysis: ProjectAnalysis = {
|
|
90
|
+
name: 'test-repo',
|
|
91
|
+
path: '/original/path/test-repo',
|
|
92
|
+
techStack: {
|
|
93
|
+
languages: ['typescript' as Language],
|
|
94
|
+
frameworks: ['react' as Framework],
|
|
95
|
+
buildTools: ['vite'],
|
|
96
|
+
projectType: 'frontend' as const,
|
|
97
|
+
},
|
|
98
|
+
endpoints: [
|
|
99
|
+
{ method: 'GET', path: '/api/v1/users', source: 'src/routes/users.ts' },
|
|
100
|
+
{ method: 'POST', path: '/api/v1/users', source: 'src/routes/users.ts' }
|
|
101
|
+
],
|
|
102
|
+
dependencies: [
|
|
103
|
+
{ name: 'my-internal-package', type: 'npm' as const },
|
|
104
|
+
{ name: 'eslint', type: 'npm' as const }
|
|
105
|
+
],
|
|
106
|
+
ports: [],
|
|
107
|
+
readmeSummary: 'A test repository.',
|
|
108
|
+
existingAIConfigs: [],
|
|
109
|
+
messaging: {
|
|
110
|
+
publishers: [
|
|
111
|
+
{ contractType: 'OrderCreated', topicOrQueue: 'order-events', publisherFile: 'src/services/order.ts' }
|
|
112
|
+
],
|
|
113
|
+
subscribers: [
|
|
114
|
+
{ contractType: 'OrderCreated', handlerFile: 'src/handlers/order.ts', registrationFile: 'src/index.ts' }
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
runConfig: {
|
|
118
|
+
entryPoints: [
|
|
119
|
+
{ projectPath: 'package.json', type: 'node', command: 'npm run dev' }
|
|
120
|
+
],
|
|
121
|
+
databases: [
|
|
122
|
+
{ provider: 'PostgreSQL', host: 'localhost', configFile: '.env' }
|
|
123
|
+
],
|
|
124
|
+
sharedInfraWarnings: [
|
|
125
|
+
{ resource: 'Database', host: 'staging-db.org', configFile: '.env', warning: '⚠️ SHARED INFRA warning' }
|
|
126
|
+
],
|
|
127
|
+
committedSecrets: [
|
|
128
|
+
{ file: '.env', lineHint: 'DATABASE_PASSWORD' }
|
|
129
|
+
],
|
|
130
|
+
externalDependencies: []
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
vi.spyOn(globby, 'globby').mockImplementation(async () => []);
|
|
135
|
+
|
|
136
|
+
const writtenFiles: Record<string, string> = {};
|
|
137
|
+
vi.spyOn(fs, 'writeFile').mockImplementation(async (filePath: any, content: any) => {
|
|
138
|
+
writtenFiles[filePath as string] = content as string;
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
});
|
|
141
|
+
vi.spyOn(fs, 'readFile').mockImplementation(async () => '## custom rule here');
|
|
142
|
+
|
|
143
|
+
await generateRepoMap(mockRepo, mockAnalysis, workspacePath, new Set(['my-internal-package']));
|
|
144
|
+
|
|
145
|
+
const expectedOutPath = path.join(workspacePath, 'nexusflow-map-test-repo.md');
|
|
146
|
+
const content = writtenFiles[expectedOutPath]!;
|
|
147
|
+
|
|
148
|
+
expect(content).toContain('## 📨 Messaging Topology');
|
|
149
|
+
expect(content).toContain('OrderCreated');
|
|
150
|
+
expect(content).toContain('order-events');
|
|
151
|
+
expect(content).toContain('## ▶️ Running Locally');
|
|
152
|
+
expect(content).toContain('### Entry Points');
|
|
153
|
+
expect(content).toContain('### ⚠️ Shared Infrastructure Warnings');
|
|
154
|
+
expect(content).toContain('my-internal-package');
|
|
155
|
+
expect(content).not.toContain('eslint');
|
|
156
|
+
expect(content).toContain('Endpoint Group (Router/Module/File)');
|
|
157
|
+
expect(content).toContain('users');
|
|
158
|
+
});
|
|
81
159
|
});
|
|
@@ -8,7 +8,6 @@ import * as fs from 'node:fs/promises';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { globby } from 'globby';
|
|
10
10
|
import type { ProjectAnalysis, RepoInfo } from '../types.js';
|
|
11
|
-
import { loadConfig } from '../core/config.js';
|
|
12
11
|
|
|
13
12
|
interface PatternRule {
|
|
14
13
|
name: string;
|
|
@@ -119,6 +118,7 @@ export async function generateRepoMap(
|
|
|
119
118
|
repo: RepoInfo,
|
|
120
119
|
analysis: ProjectAnalysis,
|
|
121
120
|
workspacePath: string,
|
|
121
|
+
allProducedPackages: Set<string> = new Set(),
|
|
122
122
|
): Promise<void> {
|
|
123
123
|
const repoName = repo.name;
|
|
124
124
|
const worktreePath = path.join(workspacePath, repoName);
|
|
@@ -133,13 +133,6 @@ export async function generateRepoMap(
|
|
|
133
133
|
md.push(`> **Note**: Maps are advisory snapshots of the codebase. Always verify route parameters, patterns, and filenames before relying on them.`);
|
|
134
134
|
md.push('');
|
|
135
135
|
|
|
136
|
-
const config = await loadConfig();
|
|
137
|
-
if (config.packContextXml) {
|
|
138
|
-
const contextXmlPath = path.join(workspacePath, `nexusflow-context-${repoName}.xml`).replace(/\\/g, '/');
|
|
139
|
-
md.push(`> **AI-friendly Packed Context**: [nexusflow-context-${repoName}.xml](file:///${contextXmlPath})`);
|
|
140
|
-
md.push(`> — **Instruction**: If you need a complete, AI-friendly XML snapshot of this repository's codebase, read this file.`);
|
|
141
|
-
md.push('');
|
|
142
|
-
}
|
|
143
136
|
|
|
144
137
|
// 1. Solution/Project Layout
|
|
145
138
|
md.push('## 🏗️ Project Layout');
|
|
@@ -191,18 +184,55 @@ export async function generateRepoMap(
|
|
|
191
184
|
md.push('');
|
|
192
185
|
}
|
|
193
186
|
|
|
187
|
+
// 1.5. Messaging Topology
|
|
188
|
+
md.push('## 📨 Messaging Topology');
|
|
189
|
+
md.push('');
|
|
190
|
+
if (analysis.messaging && (analysis.messaging.publishers.length > 0 || analysis.messaging.subscribers.length > 0)) {
|
|
191
|
+
if (analysis.messaging.publishers.length > 0) {
|
|
192
|
+
md.push('### Publishes');
|
|
193
|
+
md.push('| Message Contract | Topic/Queue/Channel | Publisher (file) |');
|
|
194
|
+
md.push('|---|---|---|');
|
|
195
|
+
for (const p of analysis.messaging.publishers) {
|
|
196
|
+
const fileLink = p.publisherFile
|
|
197
|
+
? `[${path.basename(p.publisherFile)}](file:///${path.join(worktreePath, p.publisherFile)})`
|
|
198
|
+
: '—';
|
|
199
|
+
md.push(`| ${p.contractType} | ${p.topicOrQueue} | ${fileLink} |`);
|
|
200
|
+
}
|
|
201
|
+
md.push('');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (analysis.messaging.subscribers.length > 0) {
|
|
205
|
+
md.push('### Subscribes');
|
|
206
|
+
md.push('| Message Contract | Handler (file) | Registered in |');
|
|
207
|
+
md.push('|---|---|---|');
|
|
208
|
+
for (const s of analysis.messaging.subscribers) {
|
|
209
|
+
const handlerLink = s.handlerFile
|
|
210
|
+
? `[${path.basename(s.handlerFile)}](file:///${path.join(worktreePath, s.handlerFile)})`
|
|
211
|
+
: '—';
|
|
212
|
+
const regLink = s.registrationFile
|
|
213
|
+
? `[${path.basename(s.registrationFile)}](file:///${path.join(worktreePath, s.registrationFile)})`
|
|
214
|
+
: '—';
|
|
215
|
+
md.push(`| ${s.contractType} | ${handlerLink} | ${regLink} |`);
|
|
216
|
+
}
|
|
217
|
+
md.push('');
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
md.push('_No pub/sub messaging patterns detected in this repository._');
|
|
221
|
+
md.push('');
|
|
222
|
+
}
|
|
223
|
+
|
|
194
224
|
// 2. Extensible Usage Pattern Scanning
|
|
195
225
|
md.push('## 💡 Detected Architectural Patterns & Usages');
|
|
196
226
|
md.push('');
|
|
197
227
|
|
|
198
228
|
const detectedLanguages = analysis.techStack.languages;
|
|
199
|
-
const
|
|
229
|
+
const patternExamples = new Map<string, { label: string; firstFile?: string; description: string }>();
|
|
200
230
|
|
|
201
|
-
// Initialize
|
|
231
|
+
// Initialize examples
|
|
202
232
|
for (const lang of detectedLanguages) {
|
|
203
233
|
const rules = LANG_PATTERNS[lang] || [];
|
|
204
234
|
for (const rule of rules) {
|
|
205
|
-
|
|
235
|
+
patternExamples.set(rule.name, { label: rule.label, description: rule.description });
|
|
206
236
|
}
|
|
207
237
|
}
|
|
208
238
|
|
|
@@ -231,11 +261,12 @@ export async function generateRepoMap(
|
|
|
231
261
|
for (const lang of detectedLanguages) {
|
|
232
262
|
const rules = LANG_PATTERNS[lang] || [];
|
|
233
263
|
for (const rule of rules) {
|
|
264
|
+
const val = patternExamples.get(rule.name)!;
|
|
265
|
+
if (val.firstFile) continue; // Already found an example
|
|
266
|
+
|
|
234
267
|
rule.regex.lastIndex = 0;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const val = patternCounts.get(rule.name)!;
|
|
238
|
-
val.count += matches.length;
|
|
268
|
+
if (rule.regex.test(content)) {
|
|
269
|
+
val.firstFile = file.replace(/\\/g, '/');
|
|
239
270
|
}
|
|
240
271
|
}
|
|
241
272
|
}
|
|
@@ -246,22 +277,41 @@ export async function generateRepoMap(
|
|
|
246
277
|
}
|
|
247
278
|
|
|
248
279
|
md.push('### Static Analysis Findings');
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
280
|
+
let hasFindings = false;
|
|
281
|
+
if (patternExamples.size > 0) {
|
|
282
|
+
for (const [, v] of patternExamples) {
|
|
283
|
+
if (v.firstFile) {
|
|
284
|
+
hasFindings = true;
|
|
285
|
+
const fileLink = `[${path.basename(v.firstFile)}](file:///${path.join(worktreePath, v.firstFile).replace(/\\/g, '/')})`;
|
|
286
|
+
md.push(`- **${v.label}** example: ${fileLink} _(${v.description})_`);
|
|
287
|
+
}
|
|
252
288
|
}
|
|
253
|
-
}
|
|
289
|
+
}
|
|
290
|
+
if (!hasFindings) {
|
|
254
291
|
md.push('_No architectural usage patterns detected via static analysis._');
|
|
255
292
|
}
|
|
256
293
|
md.push('');
|
|
257
294
|
|
|
258
295
|
md.push('### Packages Present (Dependencies)');
|
|
259
|
-
|
|
260
|
-
|
|
296
|
+
// Filter dependencies: only show internal packages produced by repos in this workspace,
|
|
297
|
+
// or those matching a common organization namespace prefix (e.g. if one package is MyOrg.Common, then MyOrg.*).
|
|
298
|
+
const internalPrefixes = Array.from(allProducedPackages).map(p => {
|
|
299
|
+
const parts = p.split('.');
|
|
300
|
+
return parts.length > 1 ? parts[0] + '.' : null;
|
|
301
|
+
}).filter((p): p is string => p !== null);
|
|
302
|
+
|
|
303
|
+
const filteredDeps = analysis.dependencies.filter(dep => {
|
|
304
|
+
const depNameLower = dep.name.toLowerCase();
|
|
305
|
+
if (allProducedPackages.has(depNameLower)) return true;
|
|
306
|
+
return internalPrefixes.some(prefix => depNameLower.startsWith(prefix));
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (filteredDeps.length > 0) {
|
|
310
|
+
for (const dep of filteredDeps) {
|
|
261
311
|
md.push(`- \`${dep.name}\` (${dep.version || 'unknown version'})`);
|
|
262
312
|
}
|
|
263
313
|
} else {
|
|
264
|
-
md.push('_No package dependencies detected._');
|
|
314
|
+
md.push('_No cross-repo or organization-internal package dependencies detected._');
|
|
265
315
|
}
|
|
266
316
|
md.push('');
|
|
267
317
|
|
|
@@ -269,13 +319,46 @@ export async function generateRepoMap(
|
|
|
269
319
|
md.push('## 🔌 API Endpoints');
|
|
270
320
|
md.push('');
|
|
271
321
|
if (analysis.endpoints.length > 0) {
|
|
272
|
-
md.push('|
|
|
273
|
-
md.push('
|
|
322
|
+
md.push('| Endpoint Group (Router/Module/File) | Route Prefix / Pattern | Verbs | Source File |');
|
|
323
|
+
md.push('|:---|:---|:---|:---|');
|
|
324
|
+
|
|
325
|
+
// Group endpoints by source file
|
|
326
|
+
const grouped = new Map<string, typeof analysis.endpoints>();
|
|
274
327
|
for (const ep of analysis.endpoints) {
|
|
275
|
-
const
|
|
276
|
-
|
|
328
|
+
const src = ep.source || 'Unknown';
|
|
329
|
+
if (!grouped.has(src)) {
|
|
330
|
+
grouped.set(src, []);
|
|
331
|
+
}
|
|
332
|
+
grouped.get(src)!.push(ep);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const findCommonPrefix = (paths: string[]): string => {
|
|
336
|
+
if (paths.length === 0) return '';
|
|
337
|
+
if (paths.length === 1) return paths[0]!;
|
|
338
|
+
const sorted = [...paths].sort();
|
|
339
|
+
const first = sorted[0]!.split('/');
|
|
340
|
+
const last = sorted[sorted.length - 1]!.split('/');
|
|
341
|
+
const common: string[] = [];
|
|
342
|
+
for (let i = 0; i < first.length; i++) {
|
|
343
|
+
if (first[i] === last[i]) {
|
|
344
|
+
common.push(first[i]!);
|
|
345
|
+
} else {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const prefix = common.join('/');
|
|
350
|
+
return prefix || '/';
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
for (const [src, eps] of grouped) {
|
|
354
|
+
const groupName = src !== 'Unknown' ? path.basename(src, path.extname(src)) : 'Inferred';
|
|
355
|
+
const paths = eps.map(e => e.path);
|
|
356
|
+
const commonPrefix = findCommonPrefix(paths);
|
|
357
|
+
const verbs = Array.from(new Set(eps.map(e => e.method.toUpperCase()))).join(', ');
|
|
358
|
+
const sourceLink = src !== 'Unknown'
|
|
359
|
+
? `[${path.basename(src)}](file:///${path.join(worktreePath, src).replace(/\\/g, '/')})`
|
|
277
360
|
: '—';
|
|
278
|
-
md.push(`| \`${
|
|
361
|
+
md.push(`| ${groupName} | \`${commonPrefix}\` | \`${verbs}\` | ${sourceLink} |`);
|
|
279
362
|
}
|
|
280
363
|
} else {
|
|
281
364
|
md.push('_No endpoints detected._');
|
|
@@ -324,6 +407,50 @@ export async function generateRepoMap(
|
|
|
324
407
|
}
|
|
325
408
|
md.push('');
|
|
326
409
|
|
|
410
|
+
// 4.5. Running Locally
|
|
411
|
+
md.push('## ▶️ Running Locally');
|
|
412
|
+
md.push('');
|
|
413
|
+
if (analysis.runConfig) {
|
|
414
|
+
const { entryPoints, databases, sharedInfraWarnings, committedSecrets } = analysis.runConfig;
|
|
415
|
+
|
|
416
|
+
if (entryPoints.length > 0) {
|
|
417
|
+
md.push('### Entry Points');
|
|
418
|
+
for (const ep of entryPoints) {
|
|
419
|
+
md.push(`- **${ep.type.toUpperCase()} App**: \`${ep.command || 'dotnet run'}\` (configured in \`${ep.projectPath}\`)`);
|
|
420
|
+
}
|
|
421
|
+
md.push('');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (databases.length > 0) {
|
|
425
|
+
md.push('### Databases & Data Stores');
|
|
426
|
+
md.push('| Provider | Target Host | Config File |');
|
|
427
|
+
md.push('|---|---|---|');
|
|
428
|
+
for (const db of databases) {
|
|
429
|
+
md.push(`| ${db.provider} | ${db.host} | \`${db.configFile}\` |`);
|
|
430
|
+
}
|
|
431
|
+
md.push('');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (sharedInfraWarnings.length > 0) {
|
|
435
|
+
md.push('### ⚠️ Shared Infrastructure Warnings');
|
|
436
|
+
for (const w of sharedInfraWarnings) {
|
|
437
|
+
md.push(`- ${w.warning}`);
|
|
438
|
+
}
|
|
439
|
+
md.push('');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (committedSecrets.length > 0) {
|
|
443
|
+
md.push('### 🔒 Potential Committed Secrets');
|
|
444
|
+
for (const s of committedSecrets) {
|
|
445
|
+
md.push(`- **Warning**: Possible plaintext secret/key/password found in \`${s.file}\` (key hint: \`${s.lineHint}\`)`);
|
|
446
|
+
}
|
|
447
|
+
md.push('');
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
md.push('_No run configurations detected._');
|
|
451
|
+
md.push('');
|
|
452
|
+
}
|
|
453
|
+
|
|
327
454
|
// 5. Custom Skills
|
|
328
455
|
md.push('## 🛠️ Custom Agent Skills');
|
|
329
456
|
md.push('');
|
|
@@ -363,41 +490,25 @@ export async function generateRepoMap(
|
|
|
363
490
|
md.push('');
|
|
364
491
|
|
|
365
492
|
// 7. Project-Specific Conventions (Agent-Defined)
|
|
493
|
+
md.push('## 📝 Discovered Conventions');
|
|
494
|
+
md.push('');
|
|
495
|
+
|
|
366
496
|
const conventionsFile = path.join(workspacePath, `nexusflow-conventions-${repoName}.md`);
|
|
367
|
-
let hasConventions = false;
|
|
368
|
-
try {
|
|
369
|
-
await fs.access(conventionsFile);
|
|
370
|
-
hasConventions = true;
|
|
371
|
-
} catch {}
|
|
372
|
-
|
|
373
|
-
if (!hasConventions) {
|
|
374
|
-
const starterContent = [
|
|
375
|
-
`# Project Conventions — ${repoName}`,
|
|
376
|
-
'',
|
|
377
|
-
`<!--`,
|
|
378
|
-
`This file is dedicated for the AI assistant and developers to document project-specific conventions.`,
|
|
379
|
-
`Any corrections or guidelines discovered during implementation should be appended here.`,
|
|
380
|
-
`The NexusFlow generator will automatically merge these into the architecture map during sync.`,
|
|
381
|
-
`-->`,
|
|
382
|
-
'',
|
|
383
|
-
`## 📌 Custom Rules & Discovered Conventions`,
|
|
384
|
-
'- ',
|
|
385
|
-
].join('\n');
|
|
386
|
-
try {
|
|
387
|
-
await fs.writeFile(conventionsFile, starterContent, 'utf-8');
|
|
388
|
-
} catch {}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
497
|
let customConventions = '';
|
|
392
498
|
try {
|
|
393
499
|
customConventions = await fs.readFile(conventionsFile, 'utf-8');
|
|
500
|
+
// Strip the title from the old conventions file if present
|
|
501
|
+
customConventions = customConventions.replace(/^#\s+.+\n?/, '').trim();
|
|
394
502
|
} catch {}
|
|
395
503
|
|
|
396
504
|
if (customConventions) {
|
|
397
|
-
md.push('## 📝 Project-Specific Conventions (Agent-Defined)');
|
|
398
|
-
md.push('');
|
|
399
505
|
md.push(customConventions);
|
|
400
506
|
md.push('');
|
|
507
|
+
} else {
|
|
508
|
+
md.push('<!-- AI assistants: Document any project-specific conventions, gotchas, or coding rules discovered here. -->');
|
|
509
|
+
md.push('');
|
|
510
|
+
md.push('- None recorded yet.');
|
|
511
|
+
md.push('');
|
|
401
512
|
}
|
|
402
513
|
|
|
403
514
|
const outPath = path.join(workspacePath, `nexusflow-map-${repoName}.md`);
|
|
@@ -15,22 +15,11 @@ import type {
|
|
|
15
15
|
DependencyGraph,
|
|
16
16
|
} from '../types.js';
|
|
17
17
|
|
|
18
|
-
// ─── Constants ────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
/** Repo-name substrings that signal a shared/foundation package. */
|
|
21
|
-
const SHARED_PACKAGE_KEYWORDS = ['shared', 'common', 'contracts', 'types'];
|
|
22
|
-
|
|
23
|
-
/** Project types that act as backend producers. */
|
|
24
|
-
const BACKEND_TYPES = ['api', 'backend', 'service'];
|
|
25
|
-
|
|
26
|
-
/** Project types that act as frontend consumers. */
|
|
27
|
-
const FRONTEND_TYPES = ['frontend', 'webapp', 'app'];
|
|
28
|
-
|
|
29
18
|
// ─── Dependency Graph Builder ─────────────────────────────────────────────
|
|
30
19
|
|
|
31
20
|
/**
|
|
32
|
-
* Build a dependency graph by analysing package dependencies
|
|
33
|
-
*
|
|
21
|
+
* Build a dependency graph by analysing package dependencies
|
|
22
|
+
* across the workspace repos.
|
|
34
23
|
*
|
|
35
24
|
* @param analysis Per-repo analysis results, keyed by repo path.
|
|
36
25
|
* @param repos Metadata for every repo in the workspace.
|
|
@@ -88,67 +77,16 @@ export function buildDependencyGraph(
|
|
|
88
77
|
for (const dep of a.dependencies) {
|
|
89
78
|
const depNameLower = dep.name.toLowerCase();
|
|
90
79
|
|
|
91
|
-
//
|
|
80
|
+
// Direct match with a produced package
|
|
92
81
|
if (packageToRepo.has(depNameLower)) {
|
|
93
82
|
const targetRepo = packageToRepo.get(depNameLower)!;
|
|
94
83
|
if (targetRepo !== repo.name) {
|
|
95
84
|
addEdge(graph, repo.name, targetRepo);
|
|
96
85
|
}
|
|
97
|
-
} else {
|
|
98
|
-
// 2. Check if the dependency contains or is contained by a produced package name
|
|
99
|
-
for (const [prodPkg, targetRepo] of packageToRepo) {
|
|
100
|
-
if (targetRepo === repo.name) continue;
|
|
101
|
-
if (depNameLower.includes(prodPkg) || prodPkg.includes(depNameLower)) {
|
|
102
|
-
addEdge(graph, repo.name, targetRepo);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── 2. API relationships (frontend → backend heuristic) ─────────────
|
|
110
|
-
for (const repoA of repos) {
|
|
111
|
-
const analysisA = analysisByName.get(repoA.name);
|
|
112
|
-
if (!analysisA) continue;
|
|
113
|
-
|
|
114
|
-
const typeA = analysisA.techStack.projectType;
|
|
115
|
-
|
|
116
|
-
if (!BACKEND_TYPES.includes(typeA)) continue;
|
|
117
|
-
|
|
118
|
-
for (const repoB of repos) {
|
|
119
|
-
if (repoB.name === repoA.name) continue;
|
|
120
|
-
|
|
121
|
-
const analysisB = analysisByName.get(repoB.name);
|
|
122
|
-
if (!analysisB) continue;
|
|
123
|
-
|
|
124
|
-
const typeB = analysisB.techStack.projectType;
|
|
125
|
-
if (FRONTEND_TYPES.includes(typeB)) {
|
|
126
|
-
// B (frontend) depends on A (backend)
|
|
127
|
-
addEdge(graph, repoB.name, repoA.name);
|
|
128
86
|
}
|
|
129
87
|
}
|
|
130
88
|
}
|
|
131
89
|
|
|
132
|
-
// ── 3. Shared-type packages are always foundation ───────────────────
|
|
133
|
-
for (const repo of repos) {
|
|
134
|
-
const isShared = SHARED_PACKAGE_KEYWORDS.some((kw) =>
|
|
135
|
-
repo.name.toLowerCase().includes(kw),
|
|
136
|
-
);
|
|
137
|
-
if (!isShared) continue;
|
|
138
|
-
|
|
139
|
-
const node = graph.get(repo.name);
|
|
140
|
-
if (!node) continue;
|
|
141
|
-
|
|
142
|
-
// Ensure no outgoing deps (it's a leaf producer)
|
|
143
|
-
node.dependsOn = [];
|
|
144
|
-
|
|
145
|
-
// Every other repo that doesn't already depend on it — add edge
|
|
146
|
-
for (const other of repos) {
|
|
147
|
-
if (other.name === repo.name) continue;
|
|
148
|
-
addEdge(graph, other.name, repo.name);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
90
|
return graph;
|
|
153
91
|
}
|
|
154
92
|
|
|
@@ -297,25 +235,19 @@ export async function generateImplementationPlan(
|
|
|
297
235
|
|
|
298
236
|
md.push('```');
|
|
299
237
|
md.push('');
|
|
238
|
+
md.push('> ⚠️ This diagram is derived from detected package dependencies (`package.json`, `.csproj`, etc.) only.');
|
|
239
|
+
md.push('> If you changed a package, the producing repo must release/build before consumer repos can merge.');
|
|
240
|
+
md.push('');
|
|
300
241
|
|
|
301
242
|
// ── Phase descriptions ──────────────────────────────────────────────
|
|
302
243
|
md.push('## Suggested Implementation Order');
|
|
303
244
|
md.push('');
|
|
304
245
|
|
|
305
|
-
const phaseLabels = [
|
|
306
|
-
'Foundation',
|
|
307
|
-
'Core Services',
|
|
308
|
-
'Integration Layer',
|
|
309
|
-
'Consumers',
|
|
310
|
-
'Final',
|
|
311
|
-
];
|
|
312
|
-
|
|
313
246
|
for (let i = 0; i < phases.length; i++) {
|
|
314
247
|
const phase = phases[i];
|
|
315
|
-
const label = phaseLabels[Math.min(i, phaseLabels.length - 1)];
|
|
316
248
|
const ordinal = ordinalWord(i + 1);
|
|
317
249
|
|
|
318
|
-
md.push(`### Phase ${i + 1}
|
|
250
|
+
md.push(`### Phase ${i + 1}`);
|
|
319
251
|
md.push('');
|
|
320
252
|
md.push(`**Repos:** ${phase.join(', ')}`);
|
|
321
253
|
md.push('');
|
|
@@ -421,6 +353,52 @@ export async function generateImplementationPlan(
|
|
|
421
353
|
}
|
|
422
354
|
md.push('');
|
|
423
355
|
|
|
356
|
+
// ── Cross-Repo Messaging Roll-up ────────────────────────────────────
|
|
357
|
+
md.push('## 📨 Cross-Repo Messaging');
|
|
358
|
+
md.push('');
|
|
359
|
+
md.push('| Publisher Repo | Message | → Subscriber Repo | Handler |');
|
|
360
|
+
md.push('|---|---|---|---|');
|
|
361
|
+
|
|
362
|
+
interface CrossRepoMessage {
|
|
363
|
+
pubRepo: string;
|
|
364
|
+
message: string;
|
|
365
|
+
subRepo: string;
|
|
366
|
+
handler: string;
|
|
367
|
+
}
|
|
368
|
+
const crossRepoMessages: CrossRepoMessage[] = [];
|
|
369
|
+
|
|
370
|
+
for (const [pubPath, pubA] of analysis) {
|
|
371
|
+
if (!pubA.messaging || !pubA.messaging.publishers) continue;
|
|
372
|
+
for (const pub of pubA.messaging.publishers) {
|
|
373
|
+
// Find subscribers in other repos matching this contract type
|
|
374
|
+
for (const [subPath, subA] of analysis) {
|
|
375
|
+
if (subPath === pubPath) continue;
|
|
376
|
+
if (!subA.messaging || !subA.messaging.subscribers) continue;
|
|
377
|
+
for (const sub of subA.messaging.subscribers) {
|
|
378
|
+
const pubContract = pub.contractType.toLowerCase().trim();
|
|
379
|
+
const subContract = sub.contractType.toLowerCase().trim();
|
|
380
|
+
if (pubContract === subContract && pubContract !== 'goservicebusmessage' && pubContract !== 'servicebusmessage') {
|
|
381
|
+
crossRepoMessages.push({
|
|
382
|
+
pubRepo: pubA.name,
|
|
383
|
+
message: pub.contractType,
|
|
384
|
+
subRepo: subA.name,
|
|
385
|
+
handler: sub.handlerFile,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (crossRepoMessages.length > 0) {
|
|
394
|
+
for (const m of crossRepoMessages) {
|
|
395
|
+
md.push(`| \`${m.pubRepo}\` | \`${m.message}\` | \`${m.subRepo}\` | \`${m.handler}\` |`);
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
md.push('| _No cross-repo messaging detected_ | | | |');
|
|
399
|
+
}
|
|
400
|
+
md.push('');
|
|
401
|
+
|
|
424
402
|
// ── Local Package Development Loop Tip ──────────────────────────────
|
|
425
403
|
md.push('## 💡 Local Package Development Loop');
|
|
426
404
|
md.push('');
|