@mrpatronz/nexusflow 0.1.12 → 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 +63 -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 +67 -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
@@ -6,6 +6,7 @@
6
6
 
7
7
  import * as fs from 'node:fs/promises';
8
8
  import * as path from 'node:path';
9
+ import { globby } from 'globby';
9
10
 
10
11
  import type { ApiEndpoint } from '../types.js';
11
12
 
@@ -77,62 +78,213 @@ async function scanOpenApiFiles(repoPath: string): Promise<ApiEndpoint[]> {
77
78
  return endpoints;
78
79
  }
79
80
 
80
- /** Scans source files for common route patterns. Limited to top-level files. */
81
- async function scanRoutePatterns(repoPath: string): Promise<ApiEndpoint[]> {
81
+ /** Scans C# files for controllers and Minimal APIs. */
82
+ function extractCsEndpoints(content: string, relPath: string): ApiEndpoint[] {
82
83
  const endpoints: ApiEndpoint[] = [];
83
84
 
84
- // Look in common source directories
85
- const sourceDirs = ['src', 'app', 'routes', 'controllers', 'Controllers', '.'];
86
- const routeRegex = /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
87
- const aspnetRegex = /\[Http(Get|Post|Put|Patch|Delete)\s*\(\s*"([^"]+)"\s*\)\]/gi;
85
+ // 1. Controller style routing
86
+ const classRegex = /class\s+(\w+Controller)/gi;
87
+ const classes: { name: string; index: number }[] = [];
88
+ let classMatch: RegExpExecArray | null;
89
+ while ((classMatch = classRegex.exec(content)) !== null) {
90
+ classes.push({
91
+ name: classMatch[1],
92
+ index: classMatch.index,
93
+ });
94
+ }
88
95
 
89
- for (const dir of sourceDirs) {
90
- const dirPath = path.join(repoPath, dir);
96
+ const routeAttrRegex = /\[Route\s*\(\s*"([^"]*)"\s*\)\]/gi;
97
+ const classRoutes: { route: string; index: number }[] = [];
98
+ let routeMatch: RegExpExecArray | null;
99
+ while ((routeMatch = routeAttrRegex.exec(content)) !== null) {
100
+ classRoutes.push({
101
+ route: routeMatch[1],
102
+ index: routeMatch.index,
103
+ });
104
+ }
91
105
 
92
- let entries: string[];
93
- try {
94
- entries = await fs.readdir(dirPath);
95
- } catch {
96
- continue;
106
+ const versionAttrRegex = /\[ApiVersion\s*\(\s*["']?([^"'\s\)]+)["']?\s*\)\]/gi;
107
+ const apiVersions: { version: string; index: number }[] = [];
108
+ let versionMatch: RegExpExecArray | null;
109
+ while ((versionMatch = versionAttrRegex.exec(content)) !== null) {
110
+ apiVersions.push({
111
+ version: versionMatch[1],
112
+ index: versionMatch.index,
113
+ });
114
+ }
115
+
116
+ const controllerRoutes: {
117
+ route: string;
118
+ controllerName: string;
119
+ version?: string;
120
+ classIndex: number;
121
+ }[] = [];
122
+
123
+ for (const cr of classRoutes) {
124
+ const targetClass = classes.find(c => c.index > cr.index);
125
+ if (targetClass) {
126
+ const targetVersion = apiVersions.find(v => v.index > cr.index - 100 && v.index < targetClass.index);
127
+ controllerRoutes.push({
128
+ route: cr.route,
129
+ controllerName: targetClass.name,
130
+ version: targetVersion?.version,
131
+ classIndex: targetClass.index,
132
+ });
133
+ }
134
+ }
135
+
136
+ const httpAttrRegex = /\[Http(Get|Post|Put|Patch|Delete)(?:\s*\(\s*"([^"]*)"\s*\))?\]/gi;
137
+ let httpMatch: RegExpExecArray | null;
138
+ while ((httpMatch = httpAttrRegex.exec(content)) !== null) {
139
+ const method = httpMatch[1].toUpperCase();
140
+ const actionRoute = httpMatch[2] || '';
141
+ const index = httpMatch.index;
142
+
143
+ let matchedController = controllerRoutes[0];
144
+ for (const cr of controllerRoutes) {
145
+ if (cr.classIndex < index) {
146
+ matchedController = cr;
147
+ } else {
148
+ break;
149
+ }
150
+ }
151
+
152
+ let resolvedRoute = '';
153
+ if (matchedController) {
154
+ let baseRoute = matchedController.route;
155
+ const controllerBaseName = matchedController.controllerName.replace(/Controller$/i, '');
156
+ baseRoute = baseRoute.replace(/\[controller\]/gi, controllerBaseName);
157
+
158
+ const ver = matchedController.version;
159
+ const verReplacement = ver ? 'v' + ver.split('.')[0] : 'v1';
160
+ baseRoute = baseRoute.replace(/\{version(:apiVersion)?\}/gi, verReplacement);
161
+
162
+ if (actionRoute) {
163
+ resolvedRoute = baseRoute.endsWith('/') || actionRoute.startsWith('/')
164
+ ? `${baseRoute}${actionRoute}`
165
+ : `${baseRoute}/${actionRoute}`;
166
+ } else {
167
+ resolvedRoute = baseRoute;
168
+ }
169
+ } else {
170
+ resolvedRoute = actionRoute;
171
+ }
172
+
173
+ resolvedRoute = resolvedRoute
174
+ .replace(/\/+/g, '/')
175
+ .replace(/:[a-zA-Z0-9\?]+/g, '');
176
+
177
+ if (resolvedRoute && !resolvedRoute.startsWith('/')) {
178
+ resolvedRoute = '/' + resolvedRoute;
179
+ }
180
+
181
+ if (resolvedRoute) {
182
+ endpoints.push({
183
+ method,
184
+ path: resolvedRoute,
185
+ source: relPath,
186
+ });
187
+ }
188
+ }
189
+
190
+ // 2. Minimal API style routing
191
+ const groupRegex = /(?:const|var|let)?\s*(\w+)\s*=\s*(?:\w+)\.MapGroup\s*\(\s*"([^"]+)"/gi;
192
+ const groups = new Map<string, string>();
193
+ let groupMatch: RegExpExecArray | null;
194
+ while ((groupMatch = groupRegex.exec(content)) !== null) {
195
+ groups.set(groupMatch[1], groupMatch[2]);
196
+ }
197
+
198
+ const minimalApiRegex = /\b(\w+)?\.?Map(Get|Post|Put|Patch|Delete)\s*\(\s*"([^"]+)"/gi;
199
+ let minMatch: RegExpExecArray | null;
200
+ while ((minMatch = minimalApiRegex.exec(content)) !== null) {
201
+ const varName = minMatch[1];
202
+ const method = minMatch[2].toUpperCase();
203
+ const actionRoute = minMatch[3];
204
+
205
+ let fullPath = actionRoute;
206
+ if (varName && groups.has(varName)) {
207
+ const groupPath = groups.get(varName)!;
208
+ fullPath = groupPath.endsWith('/') || actionRoute.startsWith('/')
209
+ ? `${groupPath}${actionRoute}`
210
+ : `${groupPath}/${actionRoute}`;
211
+ }
212
+
213
+ fullPath = fullPath
214
+ .replace(/\/+/g, '/')
215
+ .replace(/:[a-zA-Z0-9\?]+/g, '');
216
+
217
+ if (fullPath && !fullPath.startsWith('/')) {
218
+ fullPath = '/' + fullPath;
219
+ }
220
+
221
+ if (fullPath) {
222
+ endpoints.push({
223
+ method,
224
+ path: fullPath,
225
+ source: relPath,
226
+ });
97
227
  }
228
+ }
229
+
230
+ return endpoints;
231
+ }
232
+
233
+ /** Scans source files for common route patterns recursively using globby. */
234
+ async function scanRoutePatterns(repoPath: string): Promise<ApiEndpoint[]> {
235
+ const endpoints: ApiEndpoint[] = [];
236
+ const routeRegex = /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
98
237
 
99
- for (const entry of entries) {
100
- const ext = path.extname(entry).toLowerCase();
101
- if (!['.ts', '.js', '.cs', '.py'].includes(ext)) continue;
238
+ try {
239
+ const files = await globby(
240
+ ['**/*.ts', '**/*.js', '**/*.cs', '**/*.py'],
241
+ {
242
+ cwd: repoPath,
243
+ absolute: true,
244
+ ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
245
+ }
246
+ );
102
247
 
103
- const filePath = path.join(dirPath, entry);
248
+ for (const file of files) {
104
249
  try {
105
- const stat = await fs.stat(filePath);
250
+ const stat = await fs.stat(file);
106
251
  if (!stat.isFile() || stat.size > 100_000) continue; // Skip large files
107
252
 
108
- const content = await fs.readFile(filePath, 'utf-8');
109
- const relPath = path.relative(repoPath, filePath);
110
-
111
- // Express/Fastify/Hono style routes
112
- let match: RegExpExecArray | null;
113
- routeRegex.lastIndex = 0;
114
- while ((match = routeRegex.exec(content)) !== null) {
115
- endpoints.push({
116
- method: match[1]!.toUpperCase(),
117
- path: match[2]!,
118
- source: relPath,
119
- });
120
- }
253
+ const content = await fs.readFile(file, 'utf-8');
254
+ const relPath = path.relative(repoPath, file);
121
255
 
122
- // ASP.NET style routes
123
- aspnetRegex.lastIndex = 0;
124
- while ((match = aspnetRegex.exec(content)) !== null) {
125
- endpoints.push({
126
- method: match[1]!.toUpperCase(),
127
- path: match[2]!,
128
- source: relPath,
129
- });
256
+ if (file.endsWith('.cs')) {
257
+ endpoints.push(...extractCsEndpoints(content, relPath));
258
+ } else {
259
+ // Express/Fastify/Hono/etc style routes
260
+ let match: RegExpExecArray | null;
261
+ routeRegex.lastIndex = 0;
262
+ while ((match = routeRegex.exec(content)) !== null) {
263
+ endpoints.push({
264
+ method: match[1]!.toUpperCase(),
265
+ path: match[2]!,
266
+ source: relPath,
267
+ });
268
+ }
130
269
  }
131
270
  } catch {
132
271
  continue;
133
272
  }
134
273
  }
274
+ } catch {
275
+ // Ignore errors
135
276
  }
136
277
 
137
- return endpoints;
278
+ // De-duplicate endpoints
279
+ const seen = new Set<string>();
280
+ const uniqueEndpoints: ApiEndpoint[] = [];
281
+ for (const ep of endpoints) {
282
+ const key = `${ep.method}:${ep.path}`;
283
+ if (!seen.has(key)) {
284
+ seen.add(key);
285
+ uniqueEndpoints.push(ep);
286
+ }
287
+ }
288
+
289
+ return uniqueEndpoints;
138
290
  }
@@ -7,11 +7,12 @@
7
7
 
8
8
  import * as fs from 'node:fs/promises';
9
9
  import * as path from 'node:path';
10
+ import { globby } from 'globby';
10
11
 
11
- import type { RepoDependency } from '../types.js';
12
+ import type { RepoDependency, ProjectAnalysis } from '../types.js';
12
13
 
13
14
  /**
14
- * Extracts declared dependencies from a repository's manifest files.
15
+ * Extracts declared dependencies from a repository's manifest files recursively.
15
16
  *
16
17
  * Supports:
17
18
  * - npm (package.json)
@@ -25,107 +26,246 @@ import type { RepoDependency } from '../types.js';
25
26
  export async function detectDependencies(repoPath: string): Promise<RepoDependency[]> {
26
27
  const deps: RepoDependency[] = [];
27
28
 
28
- // ── npm ───────────────────────────────────────────────────────────
29
29
  try {
30
- const raw = await fs.readFile(path.join(repoPath, 'package.json'), 'utf-8');
31
- const pkg = JSON.parse(raw) as Record<string, unknown>;
30
+ const files = await globby(
31
+ ['**/package.json', '**/*.csproj', '**/requirements.txt', '**/go.mod'],
32
+ {
33
+ cwd: repoPath,
34
+ absolute: true,
35
+ ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
36
+ }
37
+ );
32
38
 
33
- const allDeps: Record<string, string> = {
34
- ...(pkg.dependencies as Record<string, string> | undefined),
35
- ...(pkg.devDependencies as Record<string, string> | undefined),
36
- };
39
+ for (const file of files) {
40
+ const filename = path.basename(file);
37
41
 
38
- for (const [name, version] of Object.entries(allDeps)) {
39
- deps.push({ name, type: 'npm', version });
40
- }
41
- } catch {
42
- // No package.json or parse error
43
- }
42
+ // ── npm ───────────────────────────────────────────────────────────
43
+ if (filename === 'package.json') {
44
+ try {
45
+ const raw = await fs.readFile(file, 'utf-8');
46
+ const pkg = JSON.parse(raw) as Record<string, unknown>;
44
47
 
45
- // ── NuGet (.csproj) ───────────────────────────────────────────────
46
- try {
47
- const entries = await fs.readdir(repoPath);
48
- const csprojFiles = entries.filter((e) => e.endsWith('.csproj'));
49
-
50
- for (const csproj of csprojFiles) {
51
- const content = await fs.readFile(path.join(repoPath, csproj), 'utf-8');
52
- const packageRefRegex = /<PackageReference\s+Include="([^"]+)"\s+Version="([^"]*)"/gi;
53
- let match: RegExpExecArray | null;
54
- while ((match = packageRefRegex.exec(content)) !== null) {
55
- deps.push({ name: match[1]!, type: 'nuget', version: match[2] });
48
+ const allDeps: Record<string, string> = {
49
+ ...(pkg.dependencies as Record<string, string> | undefined),
50
+ ...(pkg.devDependencies as Record<string, string> | undefined),
51
+ };
52
+
53
+ for (const [name, version] of Object.entries(allDeps)) {
54
+ deps.push({ name, type: 'npm', version });
55
+ }
56
+ } catch {
57
+ // Parse error or skip
58
+ }
59
+ }
60
+
61
+ // ── NuGet (.csproj) ───────────────────────────────────────────────
62
+ else if (filename.endsWith('.csproj')) {
63
+ try {
64
+ const content = await fs.readFile(file, 'utf-8');
65
+ const packageRefRegex = /<PackageReference\s+Include="([^"]+)"\s+Version="([^"]*)"/gi;
66
+ let match: RegExpExecArray | null;
67
+ while ((match = packageRefRegex.exec(content)) !== null) {
68
+ deps.push({ name: match[1]!, type: 'nuget', version: match[2] });
69
+ }
70
+ } catch {
71
+ // Skip
72
+ }
73
+ }
74
+
75
+ // ── pip (requirements.txt) ────────────────────────────────────────
76
+ else if (filename === 'requirements.txt') {
77
+ try {
78
+ const content = await fs.readFile(file, 'utf-8');
79
+ const lines = content.split('\n').filter((l) => l.trim() && !l.startsWith('#'));
80
+
81
+ for (const line of lines) {
82
+ const match = line.match(/^([a-zA-Z0-9_-]+)\s*([>=<~!]*\s*[\d.*]+)?/);
83
+ if (match) {
84
+ deps.push({
85
+ name: match[1]!,
86
+ type: 'pip',
87
+ version: match[2]?.trim() || undefined,
88
+ });
89
+ }
90
+ }
91
+ } catch {
92
+ // Skip
93
+ }
94
+ }
95
+
96
+ // ── Go (go.mod) ──────────────────────────────────────────────────
97
+ else if (filename === 'go.mod') {
98
+ try {
99
+ const content = await fs.readFile(file, 'utf-8');
100
+ const requireRegex = /require\s*\(([\s\S]*?)\)/g;
101
+ const modRegex = /^\s*([\S]+)\s+(v[\S]+)/gm;
102
+
103
+ let match: RegExpExecArray | null;
104
+ while ((match = requireRegex.exec(content)) !== null) {
105
+ const block = match[1]!;
106
+ let modMatch: RegExpExecArray | null;
107
+ modRegex.lastIndex = 0;
108
+ while ((modMatch = modRegex.exec(block)) !== null) {
109
+ deps.push({ name: modMatch[1]!, type: 'go', version: modMatch[2] });
110
+ }
111
+ }
112
+ } catch {
113
+ // Skip
114
+ }
56
115
  }
57
116
  }
58
117
  } catch {
59
- // No .csproj files
118
+ // Ignore errors
60
119
  }
61
120
 
62
- // ── pip (requirements.txt) ────────────────────────────────────────
63
- try {
64
- const content = await fs.readFile(path.join(repoPath, 'requirements.txt'), 'utf-8');
65
- const lines = content.split('\n').filter((l) => l.trim() && !l.startsWith('#'));
66
-
67
- for (const line of lines) {
68
- const match = line.match(/^([a-zA-Z0-9_-]+)\s*([>=<~!]*\s*[\d.*]+)?/);
69
- if (match) {
70
- deps.push({
71
- name: match[1]!,
72
- type: 'pip',
73
- version: match[2]?.trim() || undefined,
74
- });
75
- }
121
+ // De-duplicate dependencies to keep context clean
122
+ const seen = new Set<string>();
123
+ const uniqueDeps: RepoDependency[] = [];
124
+ for (const dep of deps) {
125
+ const key = `${dep.type}:${dep.name}`;
126
+ if (!seen.has(key)) {
127
+ seen.add(key);
128
+ uniqueDeps.push(dep);
76
129
  }
77
- } catch {
78
- // No requirements.txt
79
130
  }
80
131
 
81
- // ── Go (go.mod) ──────────────────────────────────────────────────
132
+ return uniqueDeps;
133
+ }
134
+
135
+ /**
136
+ * Scans recursively for packages produced or published by the repository.
137
+ *
138
+ * @param repoPath - Absolute path to the repository root.
139
+ * @returns Array of produced package metadata.
140
+ */
141
+ export async function detectProducedPackages(
142
+ repoPath: string,
143
+ ): Promise<{ name: string; type: 'npm' | 'nuget' | 'other'; version?: string; contributing?: string[] }[]> {
144
+ const products: { name: string; type: 'npm' | 'nuget' | 'other'; version?: string; contributing?: string[] }[] = [];
145
+
82
146
  try {
83
- const content = await fs.readFile(path.join(repoPath, 'go.mod'), 'utf-8');
84
- const requireRegex = /require\s*\(([\s\S]*?)\)/g;
85
- const modRegex = /^\s*([\S]+)\s+(v[\S]+)/gm;
86
-
87
- let match: RegExpExecArray | null;
88
- // Parse require blocks
89
- while ((match = requireRegex.exec(content)) !== null) {
90
- const block = match[1]!;
91
- let modMatch: RegExpExecArray | null;
92
- modRegex.lastIndex = 0;
93
- while ((modMatch = modRegex.exec(block)) !== null) {
94
- deps.push({ name: modMatch[1]!, type: 'go', version: modMatch[2] });
147
+ const files = await globby(
148
+ ['**/package.json', '**/*.csproj'],
149
+ {
150
+ cwd: repoPath,
151
+ absolute: true,
152
+ ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
153
+ }
154
+ );
155
+
156
+ for (const file of files) {
157
+ const filename = path.basename(file);
158
+
159
+ if (filename === 'package.json') {
160
+ try {
161
+ const raw = await fs.readFile(file, 'utf-8');
162
+ const pkg = JSON.parse(raw) as Record<string, unknown>;
163
+ if (pkg.name && pkg.name !== 'workspace' && !pkg.private) {
164
+ products.push({
165
+ name: pkg.name as string,
166
+ type: 'npm',
167
+ version: (pkg.version as string) || undefined,
168
+ });
169
+ }
170
+ } catch {
171
+ // Skip
172
+ }
173
+ } else if (filename.endsWith('.csproj')) {
174
+ try {
175
+ const content = await fs.readFile(file, 'utf-8');
176
+ const packageIdMatch = /<PackageId>([^<]+)<\/PackageId>/i.exec(content);
177
+ const assemblyNameMatch = /<AssemblyName>([^<]+)<\/AssemblyName>/i.exec(content);
178
+ const versionMatch = /<Version>([^<]+)<\/Version>/i.exec(content);
179
+
180
+ const name =
181
+ packageIdMatch?.[1]?.trim() ||
182
+ assemblyNameMatch?.[1]?.trim() ||
183
+ path.basename(file, '.csproj');
184
+
185
+ // Extract project references to find contributing sub-projects
186
+ const projRefRegex = /<ProjectReference\s+Include="([^"]+)"/gi;
187
+ const contributing: string[] = [];
188
+ let projMatch: RegExpExecArray | null;
189
+ while ((projMatch = projRefRegex.exec(content)) !== null) {
190
+ const refPath = projMatch[1];
191
+ const refName = path.basename(refPath, '.csproj');
192
+ contributing.push(refName);
193
+ }
194
+
195
+ products.push({
196
+ name,
197
+ type: 'nuget',
198
+ version: versionMatch?.[1]?.trim() || undefined,
199
+ contributing: contributing.length > 0 ? contributing : undefined,
200
+ });
201
+ } catch {
202
+ // Skip
203
+ }
95
204
  }
96
205
  }
97
206
  } catch {
98
- // No go.mod
207
+ // Ignore errors
99
208
  }
100
209
 
101
- return deps;
210
+ return products;
102
211
  }
103
212
 
104
213
  /**
105
214
  * Given analysis of multiple repos, find which repos depend on each other.
106
215
  * Returns a map of repo name → list of repo names it depends on.
107
216
  *
108
- * @param repoAnalyses - Map of repo path to its detected dependencies.
217
+ * @param repoAnalyses - Map of repo path to its full analysis result.
109
218
  * @param repoNames - Map of repo path to its name.
110
219
  * @returns Map of repo name → list of repo names it depends on.
111
220
  */
112
221
  export function findInterRepoDependencies(
113
- repoAnalyses: Map<string, RepoDependency[]>,
222
+ repoAnalyses: Map<string, ProjectAnalysis>,
114
223
  repoNames: Map<string, string>,
115
224
  ): Map<string, string[]> {
116
- const nameSet = new Set(repoNames.values());
117
225
  const connections = new Map<string, string[]>();
118
226
 
119
- for (const [repoPath, deps] of repoAnalyses) {
120
- const thisName = repoNames.get(repoPath) ?? repoPath;
227
+ // Map each produced package name to the repo name that produces it
228
+ const packageToRepo = new Map<string, string>();
229
+ for (const [repoPath, a] of repoAnalyses) {
230
+ const thisName = repoNames.get(repoPath) ?? a.name;
231
+ // Map the repo name itself as a produced product (for direct matching)
232
+ packageToRepo.set(thisName.toLowerCase(), thisName);
233
+
234
+ if (a.produces) {
235
+ for (const product of a.produces) {
236
+ packageToRepo.set(product.name.toLowerCase(), thisName);
237
+ // Also map basename (e.g. Hogia.EmploymentService.Client -> Client)
238
+ const base = product.name.split('.').pop() ?? product.name;
239
+ if (base && base.length > 3) {
240
+ packageToRepo.set(base.toLowerCase(), thisName);
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ for (const [repoPath, a] of repoAnalyses) {
247
+ const thisName = repoNames.get(repoPath) ?? a.name;
121
248
  const dependsOn: string[] = [];
122
249
 
123
- for (const dep of deps) {
124
- // Check if the dependency name matches another repo name
125
- // (e.g., an npm package name matching a repo folder name)
126
- const depBaseName = dep.name.split('/').pop() ?? dep.name;
127
- if (nameSet.has(depBaseName) && depBaseName !== thisName) {
128
- dependsOn.push(depBaseName);
250
+ for (const dep of a.dependencies) {
251
+ const depNameLower = dep.name.toLowerCase();
252
+
253
+ // 1. Direct match with a produced package
254
+ if (packageToRepo.has(depNameLower)) {
255
+ const targetRepo = packageToRepo.get(depNameLower)!;
256
+ if (targetRepo !== thisName && !dependsOn.includes(targetRepo)) {
257
+ dependsOn.push(targetRepo);
258
+ }
259
+ } else {
260
+ // 2. Check if the dependency contains or is contained by a produced package name
261
+ for (const [prodPkg, targetRepo] of packageToRepo) {
262
+ if (targetRepo === thisName) continue;
263
+ if (depNameLower.includes(prodPkg) || prodPkg.includes(depNameLower)) {
264
+ if (!dependsOn.includes(targetRepo)) {
265
+ dependsOn.push(targetRepo);
266
+ }
267
+ }
268
+ }
129
269
  }
130
270
  }
131
271
 
@@ -136,3 +276,40 @@ export function findInterRepoDependencies(
136
276
 
137
277
  return connections;
138
278
  }
279
+
280
+ /**
281
+ * Scans for NuGet.config files recursively in the repository and extracts configured package source feeds.
282
+ *
283
+ * @param repoPath - Absolute path to the repository root.
284
+ * @returns Array of package sources (key, value).
285
+ */
286
+ export async function detectNuGetFeeds(repoPath: string): Promise<{ name: string; url: string }[]> {
287
+ const feeds: { name: string; url: string }[] = [];
288
+ try {
289
+ const configFiles = await globby('**/NuGet.config', {
290
+ cwd: repoPath,
291
+ absolute: true,
292
+ ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
293
+ });
294
+
295
+ for (const file of configFiles) {
296
+ try {
297
+ const content = await fs.readFile(file, 'utf-8');
298
+ // Scan for <add key="..." value="..." /> elements under <packageSources>
299
+ const addRegex = /<add\s+key="([^"]+)"\s+value="([^"]+)"/gi;
300
+ let match: RegExpExecArray | null;
301
+ while ((match = addRegex.exec(content)) !== null) {
302
+ const key = match[1];
303
+ const value = match[2];
304
+ // Exclude default public nuget feed to keep output focused on private feeds
305
+ if (!key.toLowerCase().includes('nuget.org') && !value.toLowerCase().includes('api.nuget.org')) {
306
+ feeds.push({ name: key, url: value });
307
+ }
308
+ }
309
+ } catch {}
310
+ }
311
+ } catch {
312
+ // Ignore errors
313
+ }
314
+ return feeds;
315
+ }
@@ -10,7 +10,7 @@ import ora from 'ora';
10
10
  import type { ProjectAnalysis, RepoInfo } from '../types.js';
11
11
  import { detectTechStack } from './tech-stack.js';
12
12
  import { detectApis } from './detect-apis.js';
13
- import { detectDependencies } from './detect-deps.js';
13
+ import { detectDependencies, detectProducedPackages, detectNuGetFeeds } from './detect-deps.js';
14
14
  import { detectPorts } from './detect-ports.js';
15
15
  import { detectExistingAIConfigs } from './detect-existing.js';
16
16
  import { extractReadmeSummary } from './readme-summarizer.js';
@@ -23,11 +23,13 @@ import { extractReadmeSummary } from './readme-summarizer.js';
23
23
  * @returns Full analysis result.
24
24
  */
25
25
  export async function analyzeRepo(repo: RepoInfo): Promise<ProjectAnalysis> {
26
- const [techStack, endpoints, dependencies, ports, existingAIConfigs, readmeSummary] =
26
+ const [techStack, endpoints, dependencies, produces, nugetFeeds, ports, existingAIConfigs, readmeSummary] =
27
27
  await Promise.all([
28
28
  detectTechStack(repo.path),
29
29
  detectApis(repo.path),
30
30
  detectDependencies(repo.path),
31
+ detectProducedPackages(repo.path),
32
+ detectNuGetFeeds(repo.path),
31
33
  detectPorts(repo.path),
32
34
  detectExistingAIConfigs(repo.path),
33
35
  extractReadmeSummary(repo.path),
@@ -39,6 +41,8 @@ export async function analyzeRepo(repo: RepoInfo): Promise<ProjectAnalysis> {
39
41
  techStack,
40
42
  endpoints,
41
43
  dependencies,
44
+ produces,
45
+ nugetFeeds,
42
46
  ports,
43
47
  existingAIConfigs,
44
48
  readmeSummary,
@@ -79,7 +83,7 @@ export async function analyzeAllRepos(
79
83
  // Re-export individual analyzers
80
84
  export { detectTechStack } from './tech-stack.js';
81
85
  export { detectApis } from './detect-apis.js';
82
- export { detectDependencies, findInterRepoDependencies } from './detect-deps.js';
86
+ export { detectDependencies, findInterRepoDependencies, detectNuGetFeeds } from './detect-deps.js';
83
87
  export { detectPorts } from './detect-ports.js';
84
88
  export { detectExistingAIConfigs } from './detect-existing.js';
85
89
  export { extractReadmeSummary } from './readme-summarizer.js';