@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.
Files changed (63) hide show
  1. package/dist/analyzers/index.d.ts +2 -0
  2. package/dist/analyzers/index.d.ts.map +1 -1
  3. package/dist/analyzers/index.js +9 -1
  4. package/dist/analyzers/index.js.map +1 -1
  5. package/dist/analyzers/messaging-analyzer.d.ts +17 -0
  6. package/dist/analyzers/messaging-analyzer.d.ts.map +1 -0
  7. package/dist/analyzers/messaging-analyzer.js +229 -0
  8. package/dist/analyzers/messaging-analyzer.js.map +1 -0
  9. package/dist/analyzers/readme-summarizer.d.ts.map +1 -1
  10. package/dist/analyzers/readme-summarizer.js +8 -4
  11. package/dist/analyzers/readme-summarizer.js.map +1 -1
  12. package/dist/analyzers/run-analyzer.d.ts +15 -0
  13. package/dist/analyzers/run-analyzer.d.ts.map +1 -0
  14. package/dist/analyzers/run-analyzer.js +246 -0
  15. package/dist/analyzers/run-analyzer.js.map +1 -0
  16. package/dist/core/config.js +1 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/generators/base.d.ts.map +1 -1
  19. package/dist/generators/base.js +18 -57
  20. package/dist/generators/base.js.map +1 -1
  21. package/dist/generators/index.d.ts.map +1 -1
  22. package/dist/generators/index.js +30 -14
  23. package/dist/generators/index.js.map +1 -1
  24. package/dist/generators/map-generator.d.ts +1 -1
  25. package/dist/generators/map-generator.d.ts.map +1 -1
  26. package/dist/generators/map-generator.js +157 -54
  27. package/dist/generators/map-generator.js.map +1 -1
  28. package/dist/generators/map-generator.test.js +71 -0
  29. package/dist/generators/map-generator.test.js.map +1 -1
  30. package/dist/generators/plan-generator.d.ts +2 -2
  31. package/dist/generators/plan-generator.d.ts.map +1 -1
  32. package/dist/generators/plan-generator.js +47 -67
  33. package/dist/generators/plan-generator.js.map +1 -1
  34. package/dist/generators/skills-generator.d.ts +15 -0
  35. package/dist/generators/skills-generator.d.ts.map +1 -0
  36. package/dist/generators/skills-generator.js +225 -0
  37. package/dist/generators/skills-generator.js.map +1 -0
  38. package/dist/gui/assets/index-B3PIuWJS.js +22 -0
  39. package/dist/gui/assets/index-D-VigurY.css +2 -0
  40. package/dist/gui/index.html +2 -2
  41. package/dist/mcp/server.d.ts.map +1 -1
  42. package/dist/mcp/server.js +16 -85
  43. package/dist/mcp/server.js.map +1 -1
  44. package/dist/types.d.ts +46 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/gui/src/App.tsx +85 -3
  47. package/gui/src/features/workspace/WorkspaceList.tsx +85 -3
  48. package/package.json +1 -1
  49. package/src/analyzers/index.ts +9 -1
  50. package/src/analyzers/messaging-analyzer.ts +254 -0
  51. package/src/analyzers/readme-summarizer.ts +9 -4
  52. package/src/analyzers/run-analyzer.ts +269 -0
  53. package/src/core/config.ts +1 -1
  54. package/src/generators/base.ts +19 -56
  55. package/src/generators/index.ts +32 -14
  56. package/src/generators/map-generator.test.ts +78 -0
  57. package/src/generators/map-generator.ts +164 -53
  58. package/src/generators/plan-generator.ts +53 -75
  59. package/src/generators/skills-generator.ts +255 -0
  60. package/src/mcp/server.ts +16 -89
  61. package/src/types.ts +54 -0
  62. package/dist/gui/assets/index-C8W6FoPa.js +0 -21
  63. package/dist/gui/assets/index-CJn2LW8K.css +0 -2
@@ -0,0 +1,254 @@
1
+ /**
2
+ * @module analyzers/messaging-analyzer
3
+ * Detects publishers and subscribers for pub/sub messaging topologies
4
+ * across multiple programming languages and frameworks.
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 { MessagingTopology, MessagePublisher, MessageSubscriber } from '../types.js';
11
+
12
+ /**
13
+ * Analyzes messaging/event topology in a repository.
14
+ *
15
+ * Scans C#, JS/TS, Python, and Go source files for publish and subscribe
16
+ * patterns and extracts message/event contracts.
17
+ *
18
+ * @param repoPath - Absolute path to the repository root.
19
+ * @returns Detected publishers and subscribers.
20
+ */
21
+ export async function analyzeMessaging(repoPath: string): Promise<MessagingTopology> {
22
+ const publishers: MessagePublisher[] = [];
23
+ const subscribers: MessageSubscriber[] = [];
24
+
25
+ try {
26
+ const files = await globby(
27
+ ['**/*.ts', '**/*.js', '**/*.cs', '**/*.py', '**/*.go'],
28
+ {
29
+ cwd: repoPath,
30
+ absolute: true,
31
+ ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
32
+ }
33
+ );
34
+
35
+ for (const file of files) {
36
+ try {
37
+ const stat = await fs.stat(file);
38
+ if (!stat.isFile() || stat.size > 200_000) continue; // Skip large files
39
+
40
+ const content = await fs.readFile(file, 'utf-8');
41
+ const relPath = path.relative(repoPath, file).replace(/\\/g, '/');
42
+
43
+ if (file.endsWith('.cs')) {
44
+ // ── C# Messaging Patterns ────────────────────────────────────────
45
+
46
+ // MediatR notification handler
47
+ // INotificationHandler<MyNotification>
48
+ const mediatrSubRegex = /:\s*INotificationHandler\s*<\s*(\w+)\s*>/g;
49
+ let match: RegExpExecArray | null;
50
+ while ((match = mediatrSubRegex.exec(content)) !== null) {
51
+ subscribers.push({
52
+ contractType: match[1]!,
53
+ handlerFile: relPath,
54
+ registrationFile: relPath,
55
+ });
56
+ }
57
+
58
+ // MediatR command/query handler
59
+ // IRequestHandler<MyRequest, MyResponse> or IRequestHandler<MyRequest>
60
+ const requestHandlerRegex = /:\s*IRequestHandler\s*<\s*(\w+)(?:\s*,\s*\w+)?\s*>/g;
61
+ while ((match = requestHandlerRegex.exec(content)) !== null) {
62
+ subscribers.push({
63
+ contractType: match[1]!,
64
+ handlerFile: relPath,
65
+ registrationFile: relPath,
66
+ });
67
+ }
68
+
69
+ // MassTransit Consumer: IConsumer<MyMessage>
70
+ const massTransitSubRegex = /:\s*IConsumer\s*<\s*(\w+)\s*>/g;
71
+ while ((match = massTransitSubRegex.exec(content)) !== null) {
72
+ subscribers.push({
73
+ contractType: match[1]!,
74
+ handlerFile: relPath,
75
+ registrationFile: relPath,
76
+ });
77
+ }
78
+
79
+ // NServiceBus Handler: IHandleMessages<MyMessage>
80
+ const nserviceBusSubRegex = /:\s*IHandleMessages\s*<\s*(\w+)\s*>/g;
81
+ while ((match = nserviceBusSubRegex.exec(content)) !== null) {
82
+ subscribers.push({
83
+ contractType: match[1]!,
84
+ handlerFile: relPath,
85
+ registrationFile: relPath,
86
+ });
87
+ }
88
+
89
+ // Azure Service Bus triggers: [ServiceBusTrigger("queueOrTopicName")]
90
+ const sbtRegex = /\[ServiceBusTrigger\s*\(\s*"([^"]+)"(?:\s*,\s*"[^"]+")?\s*\)\]/g;
91
+ while ((match = sbtRegex.exec(content)) !== null) {
92
+ subscribers.push({
93
+ contractType: 'ServiceBusMessage',
94
+ handlerFile: relPath,
95
+ registrationFile: relPath,
96
+ });
97
+ }
98
+
99
+ // Publishers in C#
100
+ // .Publish<MyEvent>( or .PublishAsync<MyEvent>( or .Send<MyCommand>( or .SendAsync<MyCommand>(
101
+ const csPubRegex = /\b(?:Publish|PublishAsync|Send|SendAsync)\s*<\s*(\w+)\s*>\s*\(/g;
102
+ while ((match = csPubRegex.exec(content)) !== null) {
103
+ publishers.push({
104
+ contractType: match[1]!,
105
+ topicOrQueue: 'direct/inferred',
106
+ publisherFile: relPath,
107
+ });
108
+ }
109
+
110
+ const mediatorSendRegex = /\bmediator\s*\.\s*(?:Send|Publish)\s*\(\s*new\s+(\w+)\s*\(/gi;
111
+ while ((match = mediatorSendRegex.exec(content)) !== null) {
112
+ publishers.push({
113
+ contractType: match[1]!,
114
+ topicOrQueue: 'mediator',
115
+ publisherFile: relPath,
116
+ });
117
+ }
118
+
119
+ } else if (file.endsWith('.ts') || file.endsWith('.js')) {
120
+ // ── TS/JS Messaging Patterns ─────────────────────────────────────
121
+
122
+ // EventEmitter emitters: emit('event-name', ...)
123
+ const tsEmitRegex = /\bemit\s*\(\s*['"`]([^'"`]+)['"`]/g;
124
+ let match: RegExpExecArray | null;
125
+ while ((match = tsEmitRegex.exec(content)) !== null) {
126
+ publishers.push({
127
+ contractType: match[1]!,
128
+ topicOrQueue: 'EventEmitter',
129
+ publisherFile: relPath,
130
+ });
131
+ }
132
+
133
+ // EventEmitter listeners: on('event-name', ...) or addListener('event-name', ...)
134
+ const tsOnRegex = /\b(?:on|addListener)\s*\(\s*['"`]([^'"`]+)['"`]/g;
135
+ while ((match = tsOnRegex.exec(content)) !== null) {
136
+ subscribers.push({
137
+ contractType: match[1]!,
138
+ handlerFile: relPath,
139
+ registrationFile: relPath,
140
+ });
141
+ }
142
+
143
+ // Kafka/RabbitMQ subscribe: topic: 'topic-name' inside subscription object
144
+ const tsSubTopicRegex = /topic\s*:\s*['"`]([^'"`]+)['"`]/g;
145
+ while ((match = tsSubTopicRegex.exec(content)) !== null) {
146
+ subscribers.push({
147
+ contractType: 'Kafka/MQ Message',
148
+ handlerFile: relPath,
149
+ registrationFile: relPath,
150
+ });
151
+ }
152
+
153
+ // BullMQ add: queue.add('job-name', ...)
154
+ const bullmqAddRegex = /\badd\s*\(\s*['"`]([^'"`]+)['"`]/g;
155
+ while ((match = bullmqAddRegex.exec(content)) !== null) {
156
+ publishers.push({
157
+ contractType: match[1]!,
158
+ topicOrQueue: 'BullMQ',
159
+ publisherFile: relPath,
160
+ });
161
+ }
162
+
163
+ } else if (file.endsWith('.py')) {
164
+ // ── Python Messaging Patterns ────────────────────────────────────
165
+
166
+ // Celery task definitions: @app.task or @shared_task
167
+ const celeryTaskRegex = /@(?:[a-zA-Z0-9_]+\.)?(?:task|shared_task)(?:\([^)]*\))?\s*\n\s*def\s+(\w+)\s*\(/g;
168
+ let match: RegExpExecArray | null;
169
+ while ((match = celeryTaskRegex.exec(content)) !== null) {
170
+ subscribers.push({
171
+ contractType: match[1]!,
172
+ handlerFile: relPath,
173
+ registrationFile: relPath,
174
+ });
175
+ }
176
+
177
+ // Celery task invocations: task.delay(...) or task.apply_async(...)
178
+ const celeryDelayRegex = /\b(\w+)\s*\.\s*(?:delay|apply_async)\s*\(/g;
179
+ while ((match = celeryDelayRegex.exec(content)) !== null) {
180
+ publishers.push({
181
+ contractType: match[1]!,
182
+ topicOrQueue: 'Celery',
183
+ publisherFile: relPath,
184
+ });
185
+ }
186
+
187
+ // RabbitMQ basic_publish: routing_key='key'
188
+ const pikaPublishRegex = /routing_key\s*=\s*['"`]([^'"`]+)['"`]/g;
189
+ while ((match = pikaPublishRegex.exec(content)) !== null) {
190
+ publishers.push({
191
+ contractType: match[1]!,
192
+ topicOrQueue: 'RabbitMQ',
193
+ publisherFile: relPath,
194
+ });
195
+ }
196
+
197
+ } else if (file.endsWith('.go')) {
198
+ // ── Go Messaging Patterns ────────────────────────────────────────
199
+
200
+ // Go publish/produce calls: Publish("topic", ...) or Produce("topic", ...)
201
+ const goPubRegex = /\b(?:Publish|Produce|Send)\s*\(\s*['"`]([^'"`]+)['"`]/g;
202
+ let match: RegExpExecArray | null;
203
+ while ((match = goPubRegex.exec(content)) !== null) {
204
+ publishers.push({
205
+ contractType: 'GoMessage',
206
+ topicOrQueue: match[1]!,
207
+ publisherFile: relPath,
208
+ });
209
+ }
210
+
211
+ // Go subscribe/consume calls: Subscribe("topic", ...) or Consume("topic", ...)
212
+ const goSubRegex = /\b(?:Subscribe|Consume)\s*\(\s*['"`]([^'"`]+)['"`]/g;
213
+ while ((match = goSubRegex.exec(content)) !== null) {
214
+ subscribers.push({
215
+ contractType: 'GoMessage',
216
+ handlerFile: relPath,
217
+ registrationFile: relPath,
218
+ });
219
+ }
220
+ }
221
+ } catch {
222
+ // Skip unreadable files
223
+ }
224
+ }
225
+ } catch {
226
+ // Ignore errors
227
+ }
228
+
229
+ // De-duplicate publishers & subscribers
230
+ const uniquePublishers: MessagePublisher[] = [];
231
+ const seenPub = new Set<string>();
232
+ for (const p of publishers) {
233
+ const key = `${p.contractType}:${p.topicOrQueue}:${p.publisherFile}`;
234
+ if (!seenPub.has(key)) {
235
+ seenPub.add(key);
236
+ uniquePublishers.push(p);
237
+ }
238
+ }
239
+
240
+ const uniqueSubscribers: MessageSubscriber[] = [];
241
+ const seenSub = new Set<string>();
242
+ for (const s of subscribers) {
243
+ const key = `${s.contractType}:${s.handlerFile}:${s.registrationFile}`;
244
+ if (!seenSub.has(key)) {
245
+ seenSub.add(key);
246
+ uniqueSubscribers.push(s);
247
+ }
248
+ }
249
+
250
+ return {
251
+ publishers: uniquePublishers,
252
+ subscribers: uniqueSubscribers,
253
+ };
254
+ }
@@ -75,13 +75,18 @@ export async function extractReadmeSummary(
75
75
  if (trimmed.startsWith('#')) continue;
76
76
 
77
77
  // Detect Table of Contents (TOC) lists
78
- // Skip lines that look like: - [About](#about) or * 1. [Section](#section)
79
- if (/^[-*+]\s*(\d+\.)?\s*\[[^\]]+\]\(#[^)]+\)/.test(trimmed)) {
78
+ // Skip lines that look like: - [About](#about) or 1. [About](#about) or * 1. [Section](#section)
79
+ if (/^(?:[-*+]\s*|\d+\.\s*)(\d+\.)?\s*\[[^\]]+\]\(#[^)]+\)/.test(trimmed)) {
80
80
  continue;
81
81
  }
82
82
 
83
- // Skip standard bullet lists if we are searching for prose (e.g. at the top of README before prose)
84
- if (proseLines.length === 0 && /^[-*+]\s+/.test(trimmed)) {
83
+ // Skip link-only lines (e.g., "[API Docs](https://...)")
84
+ if (/^\[[^\]]+\]\([^)]+\)$/.test(trimmed)) {
85
+ continue;
86
+ }
87
+
88
+ // Skip standard bullet and numbered lists if we are searching for prose (e.g. at the top of README before prose)
89
+ if (proseLines.length === 0 && (/^[-*+]\s+/.test(trimmed) || /^\d+\.\s+/.test(trimmed))) {
85
90
  continue;
86
91
  }
87
92
 
@@ -0,0 +1,269 @@
1
+ /**
2
+ * @module analyzers/run-analyzer
3
+ * Analyzes projects to discover local run configurations, entry points,
4
+ * database dependencies, external services, and flags shared staging/test
5
+ * infrastructure or committed secrets.
6
+ */
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as fsp from 'node:fs/promises';
10
+ import * as path from 'node:path';
11
+ import { globby } from 'globby';
12
+ import type { RunConfig, RunConfigEntryPoint, RunConfigDatabase, RunConfigSharedInfraWarning, RunConfigSecret } from '../types.js';
13
+
14
+ /**
15
+ * Analyzes local run configurations, databases, and dependencies in a repository.
16
+ *
17
+ * @param repoPath - Absolute path to the repository root.
18
+ * @returns Run configuration analysis.
19
+ */
20
+ export async function analyzeRunConfig(repoPath: string): Promise<RunConfig> {
21
+ const entryPoints: RunConfigEntryPoint[] = [];
22
+ const databases: RunConfigDatabase[] = [];
23
+ const sharedInfraWarnings: RunConfigSharedInfraWarning[] = [];
24
+ const committedSecrets: RunConfigSecret[] = [];
25
+ const externalDependencies: string[] = [];
26
+
27
+ const isLocalHost = (host: string): boolean => {
28
+ const h = host.toLowerCase().trim();
29
+ return (
30
+ h.includes('localhost') ||
31
+ h.includes('127.0.0.1') ||
32
+ h.includes('[::1]') ||
33
+ h.includes('(localdb)') ||
34
+ h === 'db' ||
35
+ h === 'postgres' ||
36
+ h === 'redis' ||
37
+ h === 'mongo'
38
+ );
39
+ };
40
+
41
+ try {
42
+ // ── 1. Entry Points Detection ──────────────────────────────────────────
43
+ // A. Check for C# Web/Worker apps
44
+ const csprojFiles = await globby('**/*.csproj', {
45
+ cwd: repoPath,
46
+ absolute: true,
47
+ ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
48
+ });
49
+
50
+ for (const file of csprojFiles) {
51
+ try {
52
+ const content = await fsp.readFile(file, 'utf-8');
53
+ const relPath = path.relative(repoPath, file).replace(/\\/g, '/');
54
+ if (content.includes('Sdk="Microsoft.NET.Sdk.Web"')) {
55
+ entryPoints.push({
56
+ projectPath: relPath,
57
+ type: 'aspnet',
58
+ command: 'dotnet run',
59
+ });
60
+ } else if (content.includes('Sdk="Microsoft.NET.Sdk.Worker"')) {
61
+ entryPoints.push({
62
+ projectPath: relPath,
63
+ type: 'worker',
64
+ command: 'dotnet run',
65
+ });
66
+ }
67
+ } catch {}
68
+ }
69
+
70
+ // B. Check for package.json (Node/JS/TS)
71
+ const packageJsonPath = path.join(repoPath, 'package.json');
72
+ try {
73
+ const raw = await fsp.readFile(packageJsonPath, 'utf-8');
74
+ const pkg = JSON.parse(raw);
75
+ if (pkg.scripts && (pkg.scripts.start || pkg.scripts.dev)) {
76
+ entryPoints.push({
77
+ projectPath: 'package.json',
78
+ type: pkg.dependencies && pkg.dependencies.next ? 'nextjs' : 'node',
79
+ command: pkg.scripts.dev ? 'npm run dev' : 'npm start',
80
+ });
81
+ }
82
+ } catch {}
83
+
84
+ // C. Check for Python/Go
85
+ const goModFiles = await globby('**/go.mod', { cwd: repoPath, absolute: true });
86
+ if (goModFiles.length > 0) {
87
+ entryPoints.push({
88
+ projectPath: path.relative(repoPath, goModFiles[0]!).replace(/\\/g, '/'),
89
+ type: 'go',
90
+ command: 'go run .',
91
+ });
92
+ }
93
+
94
+ const pyFiles = await globby(['**/requirements.txt', '**/Pipfile', '**/pyproject.toml'], { cwd: repoPath, absolute: true });
95
+ if (pyFiles.length > 0) {
96
+ entryPoints.push({
97
+ projectPath: path.relative(repoPath, pyFiles[0]!).replace(/\\/g, '/'),
98
+ type: 'python',
99
+ command: 'python main.py',
100
+ });
101
+ }
102
+
103
+ // ── 2. Config Files Parsing (DBs, Shared Infra, Secrets) ───────────────
104
+ const configFiles = await globby(
105
+ ['**/appsettings.json', '**/appsettings.Development.json', '**/appsettings.*.json', '**/.env', '**/.env.development', '**/.env.local'],
106
+ {
107
+ cwd: repoPath,
108
+ absolute: true,
109
+ ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
110
+ }
111
+ );
112
+
113
+ for (const file of configFiles) {
114
+ try {
115
+ const content = await fsp.readFile(file, 'utf-8');
116
+ const relPath = path.relative(repoPath, file).replace(/\\/g, '/');
117
+
118
+ // Secrets Check (Universal regex for high probability secrets)
119
+ const secretRegex = /(?:password|pwd|secret|key|token|privatekey|accountkey|sharedaccesskey)\s*[=:]\s*['"`]?([^'";\s]{12,})['"`]?/gi;
120
+ let secretMatch: RegExpExecArray | null;
121
+ while ((secretMatch = secretRegex.exec(content)) !== null) {
122
+ const matchedVal = secretMatch[1]!;
123
+ // Skip if it looks like a placeholder
124
+ if (!matchedVal.includes('placeholder') && !matchedVal.includes('<your') && !matchedVal.toLowerCase().includes('your_')) {
125
+ committedSecrets.push({
126
+ file: relPath,
127
+ lineHint: secretMatch[0]!.split(/[=:]/)[0]!.trim(),
128
+ });
129
+ }
130
+ }
131
+
132
+ if (file.endsWith('.json')) {
133
+ const parsed = JSON.parse(content);
134
+
135
+ // connectionStrings
136
+ if (parsed.ConnectionStrings) {
137
+ for (const [key, value] of Object.entries(parsed.ConnectionStrings)) {
138
+ if (typeof value === 'string') {
139
+ let provider = 'unknown';
140
+ if (value.toLowerCase().includes('sql server') || value.toLowerCase().includes('sqlexpress') || value.toLowerCase().includes('server=')) {
141
+ provider = 'SQL Server';
142
+ }
143
+ if (value.toLowerCase().includes('postgresql') || value.toLowerCase().includes('port=5432')) {
144
+ provider = 'PostgreSQL';
145
+ }
146
+
147
+ // Extract Host
148
+ const hostMatch = value.match(/Server=([^;]+)/i) ?? value.match(/Host=([^;]+)/i) ?? value.match(/Data Source=([^;]+)/i);
149
+ const host = hostMatch ? hostMatch[1]!.trim() : 'unknown';
150
+
151
+ databases.push({
152
+ provider,
153
+ host,
154
+ configFile: relPath,
155
+ });
156
+
157
+ if (host !== 'unknown' && !isLocalHost(host)) {
158
+ sharedInfraWarnings.push({
159
+ resource: key,
160
+ host,
161
+ configFile: relPath,
162
+ warning: `⚠️ SHARED INFRA: ${relPath} binds ConnectionString "${key}" to non-local host (${host}). Running locally may connect to shared test/production databases.`,
163
+ });
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ // Service Bus or MQ settings
170
+ const rabbitRegex = /"HostName"\s*:\s*"([^"]+)"/gi;
171
+ let rabbitMatch: RegExpExecArray | null;
172
+ while ((rabbitMatch = rabbitRegex.exec(content)) !== null) {
173
+ const host = rabbitMatch[1]!;
174
+ if (!isLocalHost(host)) {
175
+ sharedInfraWarnings.push({
176
+ resource: 'RabbitMQ',
177
+ host,
178
+ configFile: relPath,
179
+ warning: `⚠️ SHARED INFRA: ${relPath} binds RabbitMQ host to non-local host (${host}).`,
180
+ });
181
+ }
182
+ }
183
+ } else {
184
+ // B. .env parsing
185
+ const lines = content.split('\n');
186
+ for (const line of lines) {
187
+ const cleanLine = line.trim();
188
+ if (cleanLine.startsWith('#') || !cleanLine.includes('=')) continue;
189
+
190
+ const [key, val] = cleanLine.split('=', 2);
191
+ if (!key || !val) continue;
192
+
193
+ const cleanKey = key.trim();
194
+ const cleanVal = val.trim().replace(/^['"]|['"]$/g, '');
195
+
196
+ // DB UrL e.g. DATABASE_URL=postgres://user:pass@host:port/db
197
+ if (cleanKey.includes('DATABASE_URL') || cleanKey.includes('MONGODB_URI') || cleanKey.includes('REDIS_URL')) {
198
+ let provider = 'unknown';
199
+ if (cleanVal.startsWith('postgres')) provider = 'PostgreSQL';
200
+ else if (cleanVal.startsWith('mongodb')) provider = 'MongoDB';
201
+ else if (cleanVal.startsWith('redis')) provider = 'Redis';
202
+
203
+ // Extract Host from URL
204
+ const hostMatch = cleanVal.match(/@([^:/]+)/);
205
+ const host = hostMatch ? hostMatch[1]! : cleanVal;
206
+
207
+ databases.push({
208
+ provider,
209
+ host,
210
+ configFile: relPath,
211
+ });
212
+
213
+ if (!isLocalHost(host)) {
214
+ sharedInfraWarnings.push({
215
+ resource: cleanKey,
216
+ host,
217
+ configFile: relPath,
218
+ warning: `⚠️ SHARED INFRA: .env variable "${cleanKey}" points to non-local host (${host}).`,
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+ } catch {}
225
+ }
226
+
227
+ } catch {
228
+ // Ignore errors
229
+ }
230
+
231
+ // De-duplicate external dependencies / warnings
232
+ const uniqueInfraWarnings: RunConfigSharedInfraWarning[] = [];
233
+ const seenInfra = new Set<string>();
234
+ for (const w of sharedInfraWarnings) {
235
+ const key = `${w.resource}:${w.host}:${w.configFile}`;
236
+ if (!seenInfra.has(key)) {
237
+ seenInfra.add(key);
238
+ uniqueInfraWarnings.push(w);
239
+ }
240
+ }
241
+
242
+ const uniqueDatabases: RunConfigDatabase[] = [];
243
+ const seenDb = new Set<string>();
244
+ for (const db of databases) {
245
+ const key = `${db.provider}:${db.host}:${db.configFile}`;
246
+ if (!seenDb.has(key)) {
247
+ seenDb.add(key);
248
+ uniqueDatabases.push(db);
249
+ }
250
+ }
251
+
252
+ const uniqueSecrets: RunConfigSecret[] = [];
253
+ const seenSecret = new Set<string>();
254
+ for (const s of committedSecrets) {
255
+ const key = `${s.file}:${s.lineHint}`;
256
+ if (!seenSecret.has(key)) {
257
+ seenSecret.add(key);
258
+ uniqueSecrets.push(s);
259
+ }
260
+ }
261
+
262
+ return {
263
+ entryPoints,
264
+ databases: uniqueDatabases,
265
+ sharedInfraWarnings: uniqueInfraWarnings,
266
+ committedSecrets: uniqueSecrets,
267
+ externalDependencies,
268
+ };
269
+ }
@@ -32,7 +32,7 @@ export function getDefaultConfig(): NexusFlowConfig {
32
32
  workspacesDir: path.join(os.homedir(), 'dev', 'workspaces'),
33
33
  defaultAssistant: null,
34
34
  scanDepth: 2,
35
- packContextXml: true,
35
+ packContextXml: false,
36
36
  excludePatterns: [
37
37
  '**/node_modules/**',
38
38
  '**/bin/**',
@@ -17,7 +17,7 @@ function formatProjectSection(analysis: ProjectAnalysis, workspacePath: string):
17
17
  lines.push(`### ${analysis.name}`);
18
18
 
19
19
  const mapPath = path.join(workspacePath, `nexusflow-map-${analysis.name}.md`).replace(/\\/g, '/');
20
- lines.push(`- **Architecture Map**: [nexusflow-map-${analysis.name}.md](file:///${mapPath}) — **Instruction**: You MUST read this architecture map before exploring or modifying the \`${analysis.name}\` repository to understand its layout, API endpoints, test commands, and detected usage patterns.`);
20
+ lines.push(`- **Architecture Map**: [nexusflow-map-${analysis.name}.md](file:///${mapPath}) — **Instruction**: Before modifying this repository, read its architecture map. For exploration, consult the map's section index on demand.`);
21
21
 
22
22
  // Tech stack
23
23
  const { techStack } = analysis;
@@ -47,15 +47,7 @@ function formatProjectSection(analysis: ProjectAnalysis, workspacePath: string):
47
47
 
48
48
  // API endpoints
49
49
  if (analysis.endpoints.length > 0) {
50
- lines.push(`- **API endpoints** (${analysis.endpoints.length} detected):`);
51
- // Show up to 10 endpoints
52
- const shown = analysis.endpoints.slice(0, 10);
53
- for (const ep of shown) {
54
- lines.push(` - \`${ep.method} ${ep.path}\``);
55
- }
56
- if (analysis.endpoints.length > 10) {
57
- lines.push(` - _...and ${analysis.endpoints.length - 10} more_`);
58
- }
50
+ lines.push(`- **API surface**: ${analysis.endpoints.length} endpoints — see architecture map for details`);
59
51
  }
60
52
 
61
53
  // Ports
@@ -128,24 +120,18 @@ ${allConfigs.join('\n')}
128
120
  if (mockCommand) parts.push(`- **Setup/Mock Command**: \`${mockCommand}\``);
129
121
  if (startCommand) parts.push(`- **Start/Run Command**: \`${startCommand}\``);
130
122
 
123
+ const standardCommands = [
124
+ 'npm run test', 'npm test', 'npm t', 'yarn test', 'yarn t', 'pnpm test', 'pnpm t', 'bun test',
125
+ 'dotnet test',
126
+ 'pytest', 'python -m unittest', 'python -m pytest',
127
+ 'go test', 'go test ./...',
128
+ 'cargo test',
129
+ ];
130
+
131
131
  if (testCommand) {
132
- if (testCommand === 'npm run test') {
133
- const hasJs = repos.some(r => {
134
- const a = analysis?.get(r.path);
135
- return a?.techStack.languages.includes('typescript') || a?.techStack.languages.includes('javascript');
136
- });
137
- const hasCsharp = repos.some(r => {
138
- const a = analysis?.get(r.path);
139
- return a?.techStack.languages.includes('csharp');
140
- });
141
-
142
- if (hasCsharp && !hasJs) {
143
- testCommand = 'dotnet test';
144
- } else if (!hasJs && !hasCsharp) {
145
- testCommand = undefined;
146
- }
147
- }
148
- if (testCommand) {
132
+ const normalizedCmd = testCommand.trim().toLowerCase();
133
+ const isStandard = standardCommands.some(cmd => normalizedCmd === cmd || normalizedCmd.startsWith(cmd + ' '));
134
+ if (!isStandard) {
149
135
  parts.push(`- **Verification/Test Command**: \`${testCommand}\``);
150
136
  }
151
137
  }
@@ -163,39 +149,16 @@ ${parts.join('\n')}
163
149
  }
164
150
  }
165
151
 
166
- // Check if overview.md already exists
167
- const overviewFile = path.join(workspacePath, 'nexusflow-overview.md');
168
- const hasOverview = fs.existsSync(overviewFile);
169
-
170
- let taskSection = '';
171
- if (hasOverview) {
172
- taskSection = `## Task & Step-by-Step Maintenance
173
-
174
- The universal reference file **\`nexusflow-overview.md\`** has already been created. Your task is to:
175
-
176
- 1. **Keep it Updated**: Maintain and update \`nexusflow-overview.md\` with any new architectural findings, layout changes, or assumptions.
177
- 2. **Review Assumptions**: Ensure that inter-repo relationships and package dependencies documented there reflect the current codebase.
178
- 3. **Address Open Questions**: If there are outstanding items in the "Clarifying Questions for the User" section, discuss them with the user.
179
- `;
180
- } else {
181
- taskSection = `## Task & Step-by-Step Initialization
152
+ const knowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md').replace(/\\/g, '/');
182
153
 
183
- Your very first task upon entering this workspace is to analyze the codebase and document it in a universal reference file:
154
+ const taskSection = `## First Steps
184
155
 
185
- 1. **Create \`nexusflow-overview.md\`** at the workspace root.
186
- 2. **Project Assumptions**: For each project, write down a clear assumption of what it does, its primary tech stack, and its core responsibilities.
187
- 3. **Inter-Repo Relationships**: Document how the repos relate:
188
- - Shared libraries/packages (producers and consumers).
189
- - API boundaries (which repos expose APIs, which ones consume them).
190
- - Data flows and dependencies.
191
- 4. **Clarifying Questions**: If any feature requirements, architectural patterns, or API contracts are unclear, list them explicitly under a section called **"Clarifying Questions for the User"**.
192
- 5. **Universal Reference**: Keep this file updated. This acts as a universal reference so that any LLM assistant (Claude, Antigravity, Codex, Cursor, Copilot) joining this workspace instantly understands the project landscape.
156
+ Your very first task upon entering this workspace is to explore the codebase and align with the user:
193
157
 
194
- Once you have created \`nexusflow-overview.md\` and compiled your questions, ask the user to verify your assumptions and answer your questions before proceeding to write code.
158
+ 1. **Verify Assumptions**: Open [nexusflow-knowledge.md](file:///${knowledgePath}) and fill in the **Project Assumptions** section with a brief description of what each project does, its tech stack, and responsibilities.
159
+ 2. **Raise Questions**: Document any outstanding uncertainties or architectural questions in the **Clarifying Questions for the User** section.
160
+ 3. **Obtain Approval**: Ask the user to confirm your assumptions and answer your questions before writing any code.
195
161
  `;
196
- }
197
-
198
- const knowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md').replace(/\\/g, '/');
199
162
 
200
163
  return `# Multi-Repo Workspace Context
201
164