@mrpatronz/nexusflow 0.1.12 → 0.2.1
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/detect-apis.d.ts.map +1 -1
- package/dist/analyzers/detect-apis.js +168 -39
- package/dist/analyzers/detect-apis.js.map +1 -1
- package/dist/analyzers/detect-deps.d.ts +26 -4
- package/dist/analyzers/detect-deps.d.ts.map +1 -1
- package/dist/analyzers/detect-deps.js +228 -66
- package/dist/analyzers/detect-deps.js.map +1 -1
- package/dist/analyzers/index.d.ts +1 -1
- package/dist/analyzers/index.d.ts.map +1 -1
- package/dist/analyzers/index.js +7 -3
- package/dist/analyzers/index.js.map +1 -1
- package/dist/analyzers/readme-summarizer.d.ts +3 -1
- package/dist/analyzers/readme-summarizer.d.ts.map +1 -1
- package/dist/analyzers/readme-summarizer.js +43 -19
- package/dist/analyzers/readme-summarizer.js.map +1 -1
- package/dist/analyzers/tech-stack.d.ts +2 -2
- package/dist/analyzers/tech-stack.d.ts.map +1 -1
- package/dist/analyzers/tech-stack.js +257 -232
- package/dist/analyzers/tech-stack.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +15 -8
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +31 -0
- package/dist/commands/sync.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +25 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +1 -2
- package/dist/core/graph.js.map +1 -1
- package/dist/core/packer.d.ts +2 -1
- package/dist/core/packer.d.ts.map +1 -1
- package/dist/core/packer.js +36 -30
- package/dist/core/packer.js.map +1 -1
- package/dist/core/packer.test.js +4 -5
- package/dist/core/packer.test.js.map +1 -1
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +22 -12
- package/dist/core/workspace.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +63 -31
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +17 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/map-generator.d.ts +15 -0
- package/dist/generators/map-generator.d.ts.map +1 -0
- package/dist/generators/map-generator.js +385 -0
- package/dist/generators/map-generator.js.map +1 -0
- package/dist/generators/map-generator.test.d.ts +2 -0
- package/dist/generators/map-generator.test.d.ts.map +1 -0
- package/dist/generators/map-generator.test.js +74 -0
- package/dist/generators/map-generator.test.js.map +1 -0
- package/dist/generators/plan-generator.d.ts +2 -0
- package/dist/generators/plan-generator.d.ts.map +1 -1
- package/dist/generators/plan-generator.js +104 -8
- package/dist/generators/plan-generator.js.map +1 -1
- package/dist/server.d.ts +0 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +9 -6
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/analyzers/detect-apis.ts +192 -40
- package/src/analyzers/detect-deps.ts +246 -69
- package/src/analyzers/index.ts +7 -3
- package/src/analyzers/readme-summarizer.ts +48 -19
- package/src/analyzers/tech-stack.ts +222 -194
- package/src/commands/create.ts +16 -10
- package/src/commands/sync.ts +35 -0
- package/src/core/config.ts +25 -0
- package/src/core/graph.ts +1 -2
- package/src/core/packer.test.ts +4 -6
- package/src/core/packer.ts +42 -43
- package/src/core/workspace.ts +23 -13
- package/src/generators/base.ts +67 -30
- package/src/generators/index.ts +17 -0
- package/src/generators/map-generator.test.ts +81 -0
- package/src/generators/map-generator.ts +405 -0
- package/src/generators/plan-generator.ts +117 -7
- package/src/server.ts +9 -6
- package/src/types.ts +13 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module generators/map-generator
|
|
3
|
+
* Generates a nexusflow-map-<repo>.md file for each repository in the workspace.
|
|
4
|
+
* Provides a localized, token-efficient architectural map for AI assistants.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs/promises';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { globby } from 'globby';
|
|
10
|
+
import type { ProjectAnalysis, RepoInfo } from '../types.js';
|
|
11
|
+
import { loadConfig } from '../core/config.js';
|
|
12
|
+
|
|
13
|
+
interface PatternRule {
|
|
14
|
+
name: string;
|
|
15
|
+
label: string;
|
|
16
|
+
regex: RegExp;
|
|
17
|
+
description: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LANG_PATTERNS: Record<string, PatternRule[]> = {
|
|
21
|
+
csharp: [
|
|
22
|
+
{
|
|
23
|
+
name: 'FluentValidation',
|
|
24
|
+
label: 'FluentValidation Validator subclasses',
|
|
25
|
+
regex: /:\s*AbstractValidator\s*</g,
|
|
26
|
+
description: 'Used for domain/command validation rules.'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'MediatR Handlers',
|
|
30
|
+
label: 'MediatR Request Handlers',
|
|
31
|
+
regex: /:\s*IRequestHandler\s*</g,
|
|
32
|
+
description: 'Used for CQRS command/query handler pattern.'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'MediatR Requests',
|
|
36
|
+
label: 'MediatR Requests/Commands',
|
|
37
|
+
regex: /:\s*IRequest\b(?!Handler)/g,
|
|
38
|
+
description: 'Used for MediatR command/query dispatching.'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'AutoMapper Profiles',
|
|
42
|
+
label: 'AutoMapper Mapping Profiles',
|
|
43
|
+
regex: /:\s*Profile\b/g,
|
|
44
|
+
description: 'Used for object-to-object mappings.'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'DataAnnotations',
|
|
48
|
+
label: 'DataAnnotations Validation Attributes',
|
|
49
|
+
regex: /using\s+System\.ComponentModel\.DataAnnotations;/g,
|
|
50
|
+
description: 'Used for property-level attribute validation.'
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
typescript: [
|
|
54
|
+
{
|
|
55
|
+
name: 'Zod Schemas',
|
|
56
|
+
label: 'Zod Object Schemas',
|
|
57
|
+
regex: /z\.object\s*\(/g,
|
|
58
|
+
description: 'Used for schema parsing/validation.'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'NestJS Controllers',
|
|
62
|
+
label: 'NestJS Routing Controllers',
|
|
63
|
+
regex: /@Controller\s*\(/g,
|
|
64
|
+
description: 'Used for controller routing endpoints.'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'NestJS Injectables',
|
|
68
|
+
label: 'NestJS Injectable Services',
|
|
69
|
+
regex: /@Injectable\s*\(\)/g,
|
|
70
|
+
description: 'Used for dependency injection services.'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'React Components',
|
|
74
|
+
label: 'React Functional Components',
|
|
75
|
+
regex: /React\.FC/g,
|
|
76
|
+
description: 'Used for component UI views.'
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
javascript: [
|
|
80
|
+
{
|
|
81
|
+
name: 'Zod Schemas',
|
|
82
|
+
label: 'Zod Object Schemas',
|
|
83
|
+
regex: /z\.object\s*\(/g,
|
|
84
|
+
description: 'Used for schema parsing/validation.'
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
python: [
|
|
88
|
+
{
|
|
89
|
+
name: 'Pydantic Models',
|
|
90
|
+
label: 'Pydantic BaseModel subclasses',
|
|
91
|
+
regex: /class\s+\w+\s*\(\s*BaseModel\s*\)/g,
|
|
92
|
+
description: 'Used for data schemas and validation.'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'FastAPI Routers',
|
|
96
|
+
label: 'FastAPI APIRouter instances',
|
|
97
|
+
regex: /=([\s\S]*?)APIRouter\s*\(/g,
|
|
98
|
+
description: 'Used for endpoint routing.'
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
go: [
|
|
102
|
+
{
|
|
103
|
+
name: 'Go JSON tags',
|
|
104
|
+
label: 'Go JSON struct tags',
|
|
105
|
+
regex: /`json:"[^"]+"`/g,
|
|
106
|
+
description: 'Used for struct serialization/deserialization.'
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generates a `nexusflow-map-<repo>.md` file in the workspace root.
|
|
113
|
+
*
|
|
114
|
+
* @param repo - The repository metadata.
|
|
115
|
+
* @param analysis - The repository analysis result.
|
|
116
|
+
* @param workspacePath - Absolute path to the workspace root directory.
|
|
117
|
+
*/
|
|
118
|
+
export async function generateRepoMap(
|
|
119
|
+
repo: RepoInfo,
|
|
120
|
+
analysis: ProjectAnalysis,
|
|
121
|
+
workspacePath: string,
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
const repoName = repo.name;
|
|
124
|
+
const worktreePath = path.join(workspacePath, repoName);
|
|
125
|
+
|
|
126
|
+
const md: string[] = [];
|
|
127
|
+
|
|
128
|
+
md.push(`# Repository Architecture Map — ${repoName}`);
|
|
129
|
+
md.push('');
|
|
130
|
+
md.push(`> **Repository Path**: \`${worktreePath}\``);
|
|
131
|
+
md.push(`> **Generated At**: ${new Date().toISOString()} (UTC)`);
|
|
132
|
+
md.push(`> **Regeneration Command**: Run \`nexusflow sync\` to update this map and workspace planning files.`);
|
|
133
|
+
md.push(`> **Note**: Maps are advisory snapshots of the codebase. Always verify route parameters, patterns, and filenames before relying on them.`);
|
|
134
|
+
md.push('');
|
|
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
|
+
|
|
144
|
+
// 1. Solution/Project Layout
|
|
145
|
+
md.push('## 🏗️ Project Layout');
|
|
146
|
+
md.push('');
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const slnFiles = await globby('**/*.sln', {
|
|
150
|
+
cwd: worktreePath,
|
|
151
|
+
ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const csprojFiles = await globby('**/*.csproj', {
|
|
155
|
+
cwd: worktreePath,
|
|
156
|
+
ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const packageJsons = await globby('**/package.json', {
|
|
160
|
+
cwd: worktreePath,
|
|
161
|
+
ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (slnFiles.length > 0) {
|
|
165
|
+
md.push('### .NET Solutions');
|
|
166
|
+
for (const sln of slnFiles) {
|
|
167
|
+
md.push(`- **Solution**: \`${sln}\``);
|
|
168
|
+
}
|
|
169
|
+
md.push('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (csprojFiles.length > 0) {
|
|
173
|
+
md.push('### .NET Projects');
|
|
174
|
+
for (const csproj of csprojFiles) {
|
|
175
|
+
const isTest = csproj.toLowerCase().includes('test') || csproj.toLowerCase().includes('spec');
|
|
176
|
+
const role = isTest ? 'Test Suite' : 'App/Library';
|
|
177
|
+
md.push(`- \`${csproj}\` (${role})`);
|
|
178
|
+
}
|
|
179
|
+
md.push('');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (packageJsons.length > 0) {
|
|
183
|
+
md.push('### npm Packages & Modules');
|
|
184
|
+
for (const pj of packageJsons) {
|
|
185
|
+
md.push(`- \`${pj}\``);
|
|
186
|
+
}
|
|
187
|
+
md.push('');
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
md.push('_No project layout files discovered or error occurred._');
|
|
191
|
+
md.push('');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 2. Extensible Usage Pattern Scanning
|
|
195
|
+
md.push('## 💡 Detected Architectural Patterns & Usages');
|
|
196
|
+
md.push('');
|
|
197
|
+
|
|
198
|
+
const detectedLanguages = analysis.techStack.languages;
|
|
199
|
+
const patternCounts = new Map<string, { label: string; count: number; description: string }>();
|
|
200
|
+
|
|
201
|
+
// Initialize counts
|
|
202
|
+
for (const lang of detectedLanguages) {
|
|
203
|
+
const rules = LANG_PATTERNS[lang] || [];
|
|
204
|
+
for (const rule of rules) {
|
|
205
|
+
patternCounts.set(rule.name, { label: rule.label, count: 0, description: rule.description });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Scan files for pattern usage
|
|
210
|
+
try {
|
|
211
|
+
const extensions = detectedLanguages.map(l => {
|
|
212
|
+
if (l === 'csharp') return 'cs';
|
|
213
|
+
if (l === 'typescript') return 'ts';
|
|
214
|
+
if (l === 'javascript') return 'js';
|
|
215
|
+
if (l === 'python') return 'py';
|
|
216
|
+
if (l === 'go') return 'go';
|
|
217
|
+
return '';
|
|
218
|
+
}).filter(ext => ext !== '');
|
|
219
|
+
|
|
220
|
+
if (extensions.length > 0) {
|
|
221
|
+
const globPattern = extensions.length === 1 ? `**/*.${extensions[0]}` : `**/*.{${extensions.join(',')}}`;
|
|
222
|
+
const srcFiles = await globby(globPattern, {
|
|
223
|
+
cwd: worktreePath,
|
|
224
|
+
ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
for (const file of srcFiles) {
|
|
228
|
+
const fullPath = path.join(worktreePath, file);
|
|
229
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
230
|
+
|
|
231
|
+
for (const lang of detectedLanguages) {
|
|
232
|
+
const rules = LANG_PATTERNS[lang] || [];
|
|
233
|
+
for (const rule of rules) {
|
|
234
|
+
rule.regex.lastIndex = 0;
|
|
235
|
+
const matches = content.match(rule.regex);
|
|
236
|
+
if (matches) {
|
|
237
|
+
const val = patternCounts.get(rule.name)!;
|
|
238
|
+
val.count += matches.length;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// Ignore errors
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
md.push('### Static Analysis Findings');
|
|
249
|
+
if (patternCounts.size > 0) {
|
|
250
|
+
for (const [, v] of patternCounts) {
|
|
251
|
+
md.push(`- **${v.label}**: Found ${v.count} occurrence(s). _(${v.description})_`);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
md.push('_No architectural usage patterns detected via static analysis._');
|
|
255
|
+
}
|
|
256
|
+
md.push('');
|
|
257
|
+
|
|
258
|
+
md.push('### Packages Present (Dependencies)');
|
|
259
|
+
if (analysis.dependencies.length > 0) {
|
|
260
|
+
for (const dep of analysis.dependencies) {
|
|
261
|
+
md.push(`- \`${dep.name}\` (${dep.version || 'unknown version'})`);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
md.push('_No package dependencies detected._');
|
|
265
|
+
}
|
|
266
|
+
md.push('');
|
|
267
|
+
|
|
268
|
+
// 3. Endpoint Inventory
|
|
269
|
+
md.push('## 🔌 API Endpoints');
|
|
270
|
+
md.push('');
|
|
271
|
+
if (analysis.endpoints.length > 0) {
|
|
272
|
+
md.push('| Method | Route | Controller/Source File |');
|
|
273
|
+
md.push('|:---|:---|:---|');
|
|
274
|
+
for (const ep of analysis.endpoints) {
|
|
275
|
+
const sourceLink = ep.source
|
|
276
|
+
? `[${path.basename(ep.source)}](file:///${path.join(worktreePath, ep.source)})`
|
|
277
|
+
: '—';
|
|
278
|
+
md.push(`| \`${ep.method}\` | \`${ep.path}\` | ${sourceLink} |`);
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
md.push('_No endpoints detected._');
|
|
282
|
+
}
|
|
283
|
+
md.push('');
|
|
284
|
+
|
|
285
|
+
// 4. Test Landscape
|
|
286
|
+
md.push('## 🧪 Test Landscape & Command');
|
|
287
|
+
md.push('');
|
|
288
|
+
const testFrameworks: string[] = [];
|
|
289
|
+
let testCommand = '';
|
|
290
|
+
|
|
291
|
+
for (const dep of analysis.dependencies) {
|
|
292
|
+
const name = dep.name.toLowerCase();
|
|
293
|
+
if (name.includes('xunit')) testFrameworks.push('xUnit');
|
|
294
|
+
if (name.includes('mstest') || name.includes('microsoft.testplatform')) testFrameworks.push('MSTest');
|
|
295
|
+
if (name.includes('nunit')) testFrameworks.push('NUnit');
|
|
296
|
+
if (name.includes('jest')) testFrameworks.push('Jest');
|
|
297
|
+
if (name.includes('vitest')) testFrameworks.push('Vitest');
|
|
298
|
+
if (name.includes('cypress')) testFrameworks.push('Cypress');
|
|
299
|
+
if (name.includes('playwright')) testFrameworks.push('Playwright');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (analysis.techStack.languages.includes('csharp')) {
|
|
303
|
+
testCommand = 'dotnet test';
|
|
304
|
+
try {
|
|
305
|
+
const slns = await globby('**/*.sln', {
|
|
306
|
+
cwd: worktreePath,
|
|
307
|
+
ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
|
|
308
|
+
});
|
|
309
|
+
if (slns.length > 0) {
|
|
310
|
+
testCommand = `dotnet test ${slns[0]}`;
|
|
311
|
+
}
|
|
312
|
+
} catch {}
|
|
313
|
+
} else if (analysis.techStack.languages.includes('typescript') || analysis.techStack.languages.includes('javascript')) {
|
|
314
|
+
testCommand = 'npm test';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (testFrameworks.length > 0) {
|
|
318
|
+
md.push(`- **Frameworks**: ${testFrameworks.join(', ')}`);
|
|
319
|
+
} else {
|
|
320
|
+
md.push('- **Frameworks**: None explicitly detected');
|
|
321
|
+
}
|
|
322
|
+
if (testCommand) {
|
|
323
|
+
md.push(`- **Run Command**: \`${testCommand}\``);
|
|
324
|
+
}
|
|
325
|
+
md.push('');
|
|
326
|
+
|
|
327
|
+
// 5. Custom Skills
|
|
328
|
+
md.push('## 🛠️ Custom Agent Skills');
|
|
329
|
+
md.push('');
|
|
330
|
+
try {
|
|
331
|
+
const skills = await globby('**/SKILL.md', {
|
|
332
|
+
cwd: worktreePath,
|
|
333
|
+
absolute: true,
|
|
334
|
+
ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (skills.length > 0) {
|
|
338
|
+
md.push('The following custom agent skills are available in this repository:');
|
|
339
|
+
for (const skill of skills) {
|
|
340
|
+
const relativeSkill = path.relative(worktreePath, skill);
|
|
341
|
+
const skillName = path.basename(path.dirname(skill));
|
|
342
|
+
md.push(`- **${skillName}**: [${relativeSkill}](file:///${skill})`);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
md.push('_No custom agent skills found in this repository._');
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
md.push('_No custom agent skills found in this repository._');
|
|
349
|
+
}
|
|
350
|
+
md.push('');
|
|
351
|
+
|
|
352
|
+
// 6. AI Configurations
|
|
353
|
+
md.push('## 📄 AI Assistant Configurations');
|
|
354
|
+
md.push('');
|
|
355
|
+
if (analysis.existingAIConfigs.length > 0) {
|
|
356
|
+
md.push('Incorporate instructions from these local configurations:');
|
|
357
|
+
for (const config of analysis.existingAIConfigs) {
|
|
358
|
+
md.push(`- [${config.relativePath}](file:///${path.join(worktreePath, config.relativePath)}) (${config.assistant})`);
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
md.push('_No pre-existing AI configurations found in this repository._');
|
|
362
|
+
}
|
|
363
|
+
md.push('');
|
|
364
|
+
|
|
365
|
+
// 7. Project-Specific Conventions (Agent-Defined)
|
|
366
|
+
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
|
+
let customConventions = '';
|
|
392
|
+
try {
|
|
393
|
+
customConventions = await fs.readFile(conventionsFile, 'utf-8');
|
|
394
|
+
} catch {}
|
|
395
|
+
|
|
396
|
+
if (customConventions) {
|
|
397
|
+
md.push('## 📝 Project-Specific Conventions (Agent-Defined)');
|
|
398
|
+
md.push('');
|
|
399
|
+
md.push(customConventions);
|
|
400
|
+
md.push('');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const outPath = path.join(workspacePath, `nexusflow-map-${repoName}.md`);
|
|
404
|
+
await fs.writeFile(outPath, md.join('\n'), 'utf-8');
|
|
405
|
+
}
|
|
@@ -59,19 +59,48 @@ export function buildDependencyGraph(
|
|
|
59
59
|
if (a) analysisByName.set(repo.name, a);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
// ── 1. Produced/consumed package dependencies ──────────────────────────
|
|
63
|
+
// Map each produced package name to the repo name that produces it
|
|
64
|
+
const packageToRepo = new Map<string, string>();
|
|
65
|
+
for (const repo of repos) {
|
|
66
|
+
const a = analysisByName.get(repo.name);
|
|
67
|
+
if (!a) continue;
|
|
68
|
+
|
|
69
|
+
// Map the repo name itself as a produced product (for direct matching)
|
|
70
|
+
packageToRepo.set(repo.name.toLowerCase(), repo.name);
|
|
71
|
+
|
|
72
|
+
if (a.produces) {
|
|
73
|
+
for (const product of a.produces) {
|
|
74
|
+
packageToRepo.set(product.name.toLowerCase(), repo.name);
|
|
75
|
+
// Map basename (e.g. Hogia.EmploymentService.Client -> Client)
|
|
76
|
+
const base = product.name.split('.').pop() ?? product.name;
|
|
77
|
+
if (base && base.length > 3) {
|
|
78
|
+
packageToRepo.set(base.toLowerCase(), repo.name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
|
|
64
|
-
// ── 1. Shared-package dependencies ───────────────────────────────────
|
|
65
84
|
for (const repo of repos) {
|
|
66
85
|
const a = analysisByName.get(repo.name);
|
|
67
86
|
if (!a) continue;
|
|
68
87
|
|
|
69
88
|
for (const dep of a.dependencies) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
const depNameLower = dep.name.toLowerCase();
|
|
90
|
+
|
|
91
|
+
// 1. Direct match with a produced package
|
|
92
|
+
if (packageToRepo.has(depNameLower)) {
|
|
93
|
+
const targetRepo = packageToRepo.get(depNameLower)!;
|
|
94
|
+
if (targetRepo !== repo.name) {
|
|
95
|
+
addEdge(graph, repo.name, targetRepo);
|
|
96
|
+
}
|
|
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
|
+
}
|
|
75
104
|
}
|
|
76
105
|
}
|
|
77
106
|
}
|
|
@@ -191,6 +220,8 @@ export function topologicalSort(graph: DependencyGraph): string[][] {
|
|
|
191
220
|
* - A Mermaid dependency diagram
|
|
192
221
|
* - Phased implementation order derived from topological sort
|
|
193
222
|
* - A dependency cross-reference table
|
|
223
|
+
* - A package relations table
|
|
224
|
+
* - Actionable local dev tips
|
|
194
225
|
*
|
|
195
226
|
* @param ctx The current workspace context (feature + repos + analysis).
|
|
196
227
|
* @param workspacePath Absolute path to the workspace root directory.
|
|
@@ -328,6 +359,85 @@ export async function generateImplementationPlan(
|
|
|
328
359
|
|
|
329
360
|
md.push('');
|
|
330
361
|
|
|
362
|
+
// ── Contracts & Clients Table ───────────────────────────────────────
|
|
363
|
+
md.push('## 📦 Contracts & Clients');
|
|
364
|
+
md.push('');
|
|
365
|
+
md.push('| Package | Contributing Projects | Producing Repo | Consuming Repos (Version) | Feed Source | Type |');
|
|
366
|
+
md.push('|:---|:---|:---|:---|:---|:---|');
|
|
367
|
+
|
|
368
|
+
// Build package relations
|
|
369
|
+
interface PackageRelation {
|
|
370
|
+
pkgName: string;
|
|
371
|
+
contributing?: string[];
|
|
372
|
+
producer: string;
|
|
373
|
+
consumers: { repoName: string; version?: string }[];
|
|
374
|
+
type: 'npm' | 'nuget' | 'other';
|
|
375
|
+
feeds?: { name: string; url: string }[];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const packageRelations: PackageRelation[] = [];
|
|
379
|
+
|
|
380
|
+
// Find all produced packages
|
|
381
|
+
for (const [repoPath, a] of analysis) {
|
|
382
|
+
if (a.produces) {
|
|
383
|
+
for (const product of a.produces) {
|
|
384
|
+
// Find consumers
|
|
385
|
+
const consumers: { repoName: string; version?: string }[] = [];
|
|
386
|
+
for (const [otherPath, otherA] of analysis) {
|
|
387
|
+
if (otherPath === repoPath) continue;
|
|
388
|
+
for (const dep of otherA.dependencies) {
|
|
389
|
+
if (dep.name.toLowerCase() === product.name.toLowerCase()) {
|
|
390
|
+
consumers.push({ repoName: otherA.name, version: dep.version });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
packageRelations.push({
|
|
395
|
+
pkgName: product.name,
|
|
396
|
+
contributing: (product as any).contributing,
|
|
397
|
+
producer: a.name,
|
|
398
|
+
consumers,
|
|
399
|
+
type: product.type,
|
|
400
|
+
feeds: a.nugetFeeds,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (packageRelations.length > 0) {
|
|
407
|
+
for (const rel of packageRelations) {
|
|
408
|
+
const contribStr = rel.contributing && rel.contributing.length > 0
|
|
409
|
+
? rel.contributing.map(c => `\`${c}\``).join(', ')
|
|
410
|
+
: '—';
|
|
411
|
+
const consumerStr = rel.consumers.length > 0
|
|
412
|
+
? rel.consumers.map(c => `\`${c.repoName}\` (${c.version || 'pinned'})`).join(', ')
|
|
413
|
+
: '_None_';
|
|
414
|
+
const feedStr = rel.feeds && rel.feeds.length > 0
|
|
415
|
+
? rel.feeds.map(f => `\`${f.name}\` (${f.url})`).join('<br>')
|
|
416
|
+
: '—';
|
|
417
|
+
md.push(`| \`${rel.pkgName}\` | ${contribStr} | \`${rel.producer}\` | ${consumerStr} | ${feedStr} | \`${rel.type}\` |`);
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
md.push('| _No package relations detected_ | | | | | |');
|
|
421
|
+
}
|
|
422
|
+
md.push('');
|
|
423
|
+
|
|
424
|
+
// ── Local Package Development Loop Tip ──────────────────────────────
|
|
425
|
+
md.push('## 💡 Local Package Development Loop');
|
|
426
|
+
md.push('');
|
|
427
|
+
md.push('When making changes to a shared contract or client library package, follow this standard local feed loop to test and verify consumers before pushing:');
|
|
428
|
+
md.push('');
|
|
429
|
+
md.push('### For .NET / NuGet packages:');
|
|
430
|
+
md.push('1. **Pack locally**: Run `dotnet pack -c Release -o ./local-packages` inside the producing project folder.');
|
|
431
|
+
md.push('2. **Add local feed**: Configure a local feed in your consumer project\'s `NuGet.config` pointing to the `./local-packages` directory.');
|
|
432
|
+
md.push('3. **Reference local version**: Reference the package with a local development version (e.g. `3.41.0-local`) in the consuming `.csproj`.');
|
|
433
|
+
md.push('4. **Revert before merging**: Verify changes compile and tests pass, then **revert** the consuming project\'s package version reference to the official release before merging to master.');
|
|
434
|
+
md.push('');
|
|
435
|
+
md.push('### For Node.js / npm packages:');
|
|
436
|
+
md.push('1. **Link locally**: Run `npm link` inside the producing package folder.');
|
|
437
|
+
md.push('2. **Use link**: Run `npm link <package-name>` inside the consuming folder to link it.');
|
|
438
|
+
md.push('3. **Revert before merging**: Uninstall the linked package and install the official package version before committing.');
|
|
439
|
+
md.push('');
|
|
440
|
+
|
|
331
441
|
// ── Write file ──────────────────────────────────────────────────────
|
|
332
442
|
const outPath = path.join(workspacePath, 'nexusflow-plan.md');
|
|
333
443
|
await fse.outputFile(outPath, md.join('\n'));
|
package/src/server.ts
CHANGED
|
@@ -897,15 +897,18 @@ app.get('/', async (c) => {
|
|
|
897
897
|
// Serve static assets from GUI build folder
|
|
898
898
|
app.use('/*', serveStatic({ root: path.relative(process.cwd(), guiPath) }));
|
|
899
899
|
|
|
900
|
-
/**
|
|
901
|
-
* Starts the local GUI web server.
|
|
902
|
-
*
|
|
903
|
-
* @param port - Port to run on.
|
|
904
|
-
*/
|
|
905
900
|
export function startServer(port = 3000): Promise<{ port: number; server: any }> {
|
|
906
|
-
return new Promise((resolve) => {
|
|
901
|
+
return new Promise((resolve, reject) => {
|
|
907
902
|
const server = serve({ fetch: app.fetch, port }, (info) => {
|
|
908
903
|
resolve({ port: info.port, server });
|
|
904
|
+
}) as import('node:http').Server;
|
|
905
|
+
|
|
906
|
+
server.on('error', (e: any) => {
|
|
907
|
+
if (e.code === 'EADDRINUSE') {
|
|
908
|
+
resolve(startServer(port + 1));
|
|
909
|
+
} else {
|
|
910
|
+
reject(e);
|
|
911
|
+
}
|
|
909
912
|
});
|
|
910
913
|
});
|
|
911
914
|
}
|
package/src/types.ts
CHANGED
|
@@ -23,6 +23,12 @@ export interface NexusFlowConfig {
|
|
|
23
23
|
/** How many directory levels deep to scan for git repos. Default: 2 */
|
|
24
24
|
scanDepth: number;
|
|
25
25
|
|
|
26
|
+
/** Global patterns to exclude when packing/analyzing repositories. */
|
|
27
|
+
excludePatterns?: string[];
|
|
28
|
+
|
|
29
|
+
/** Whether to pack codebase context into XML format. Default: true */
|
|
30
|
+
packContextXml?: boolean;
|
|
31
|
+
|
|
26
32
|
/** ISO timestamp of the last update check. */
|
|
27
33
|
lastUpdateCheck?: string;
|
|
28
34
|
|
|
@@ -112,6 +118,9 @@ export interface Feature {
|
|
|
112
118
|
/** Absolute paths to the repos included in this feature. */
|
|
113
119
|
repos: string[];
|
|
114
120
|
|
|
121
|
+
/** Absolute paths to the original repositories. */
|
|
122
|
+
originalRepos?: string[];
|
|
123
|
+
|
|
115
124
|
/** AI assistants enabled for this feature workspace. */
|
|
116
125
|
assistants: AIAssistant[];
|
|
117
126
|
|
|
@@ -235,6 +244,10 @@ export interface ProjectAnalysis {
|
|
|
235
244
|
readmeSummary: string | null;
|
|
236
245
|
/** Existing AI config files found in the repo. */
|
|
237
246
|
existingAIConfigs: ExistingAIConfig[];
|
|
247
|
+
/** Produced/published packages by this repo. */
|
|
248
|
+
produces?: { name: string; type: 'npm' | 'nuget' | 'other'; version?: string; contributing?: string[] }[];
|
|
249
|
+
/** NuGet feeds detected in the repo's NuGet.config files. */
|
|
250
|
+
nugetFeeds?: { name: string; url: string }[];
|
|
238
251
|
}
|
|
239
252
|
|
|
240
253
|
/** An existing AI configuration file found in a repo. */
|
package/vitest.config.ts
ADDED