@mrpatronz/nexusflow 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/analyzers/detect-apis.d.ts.map +1 -1
  2. package/dist/analyzers/detect-apis.js +168 -39
  3. package/dist/analyzers/detect-apis.js.map +1 -1
  4. package/dist/analyzers/detect-deps.d.ts +26 -4
  5. package/dist/analyzers/detect-deps.d.ts.map +1 -1
  6. package/dist/analyzers/detect-deps.js +228 -66
  7. package/dist/analyzers/detect-deps.js.map +1 -1
  8. package/dist/analyzers/index.d.ts +1 -1
  9. package/dist/analyzers/index.d.ts.map +1 -1
  10. package/dist/analyzers/index.js +7 -3
  11. package/dist/analyzers/index.js.map +1 -1
  12. package/dist/analyzers/readme-summarizer.d.ts +3 -1
  13. package/dist/analyzers/readme-summarizer.d.ts.map +1 -1
  14. package/dist/analyzers/readme-summarizer.js +43 -19
  15. package/dist/analyzers/readme-summarizer.js.map +1 -1
  16. package/dist/analyzers/tech-stack.d.ts +2 -2
  17. package/dist/analyzers/tech-stack.d.ts.map +1 -1
  18. package/dist/analyzers/tech-stack.js +257 -232
  19. package/dist/analyzers/tech-stack.js.map +1 -1
  20. package/dist/commands/create.d.ts.map +1 -1
  21. package/dist/commands/create.js +15 -8
  22. package/dist/commands/create.js.map +1 -1
  23. package/dist/commands/sync.d.ts.map +1 -1
  24. package/dist/commands/sync.js +31 -0
  25. package/dist/commands/sync.js.map +1 -1
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +25 -0
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/graph.d.ts.map +1 -1
  30. package/dist/core/graph.js +1 -2
  31. package/dist/core/graph.js.map +1 -1
  32. package/dist/core/packer.d.ts +2 -1
  33. package/dist/core/packer.d.ts.map +1 -1
  34. package/dist/core/packer.js +36 -30
  35. package/dist/core/packer.js.map +1 -1
  36. package/dist/core/packer.test.js +4 -5
  37. package/dist/core/packer.test.js.map +1 -1
  38. package/dist/core/workspace.d.ts.map +1 -1
  39. package/dist/core/workspace.js +22 -12
  40. package/dist/core/workspace.js.map +1 -1
  41. package/dist/generators/base.d.ts.map +1 -1
  42. package/dist/generators/base.js +64 -31
  43. package/dist/generators/base.js.map +1 -1
  44. package/dist/generators/index.d.ts.map +1 -1
  45. package/dist/generators/index.js +17 -0
  46. package/dist/generators/index.js.map +1 -1
  47. package/dist/generators/map-generator.d.ts +15 -0
  48. package/dist/generators/map-generator.d.ts.map +1 -0
  49. package/dist/generators/map-generator.js +385 -0
  50. package/dist/generators/map-generator.js.map +1 -0
  51. package/dist/generators/map-generator.test.d.ts +2 -0
  52. package/dist/generators/map-generator.test.d.ts.map +1 -0
  53. package/dist/generators/map-generator.test.js +74 -0
  54. package/dist/generators/map-generator.test.js.map +1 -0
  55. package/dist/generators/plan-generator.d.ts +2 -0
  56. package/dist/generators/plan-generator.d.ts.map +1 -1
  57. package/dist/generators/plan-generator.js +104 -8
  58. package/dist/generators/plan-generator.js.map +1 -1
  59. package/dist/types.d.ts +18 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/package.json +2 -1
  62. package/src/analyzers/detect-apis.ts +192 -40
  63. package/src/analyzers/detect-deps.ts +246 -69
  64. package/src/analyzers/index.ts +7 -3
  65. package/src/analyzers/readme-summarizer.ts +48 -19
  66. package/src/analyzers/tech-stack.ts +222 -194
  67. package/src/commands/create.ts +16 -10
  68. package/src/commands/sync.ts +35 -0
  69. package/src/core/config.ts +25 -0
  70. package/src/core/graph.ts +1 -2
  71. package/src/core/packer.test.ts +4 -6
  72. package/src/core/packer.ts +42 -43
  73. package/src/core/workspace.ts +23 -13
  74. package/src/generators/base.ts +68 -30
  75. package/src/generators/index.ts +17 -0
  76. package/src/generators/map-generator.test.ts +81 -0
  77. package/src/generators/map-generator.ts +405 -0
  78. package/src/generators/plan-generator.ts +117 -7
  79. package/src/types.ts +13 -0
  80. 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
- const repoNames = new Set(repos.map((r) => r.name));
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
- // Direct name match — e.g. "@acme/shared-contracts" contains "shared-contracts"
71
- for (const otherName of repoNames) {
72
- if (otherName === repo.name) continue;
73
- if (dep.name === otherName || dep.name.includes(otherName)) {
74
- addEdge(graph, repo.name, otherName);
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/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. */
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ exclude: [
6
+ '**/node_modules/**',
7
+ '**/dist/**',
8
+ '**/cypress/**',
9
+ '**/.{idea,git,cache,output,temp}/**',
10
+ '**/scratch/**'
11
+ ]
12
+ }
13
+ });