@mrpatronz/nexusflow 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzers/index.d.ts +2 -0
- package/dist/analyzers/index.d.ts.map +1 -1
- package/dist/analyzers/index.js +9 -1
- package/dist/analyzers/index.js.map +1 -1
- package/dist/analyzers/messaging-analyzer.d.ts +17 -0
- package/dist/analyzers/messaging-analyzer.d.ts.map +1 -0
- package/dist/analyzers/messaging-analyzer.js +229 -0
- package/dist/analyzers/messaging-analyzer.js.map +1 -0
- package/dist/analyzers/readme-summarizer.d.ts.map +1 -1
- package/dist/analyzers/readme-summarizer.js +8 -4
- package/dist/analyzers/readme-summarizer.js.map +1 -1
- package/dist/analyzers/run-analyzer.d.ts +15 -0
- package/dist/analyzers/run-analyzer.d.ts.map +1 -0
- package/dist/analyzers/run-analyzer.js +246 -0
- package/dist/analyzers/run-analyzer.js.map +1 -0
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +18 -57
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +30 -14
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/map-generator.d.ts +1 -1
- package/dist/generators/map-generator.d.ts.map +1 -1
- package/dist/generators/map-generator.js +157 -54
- package/dist/generators/map-generator.js.map +1 -1
- package/dist/generators/map-generator.test.js +71 -0
- package/dist/generators/map-generator.test.js.map +1 -1
- package/dist/generators/plan-generator.d.ts +2 -2
- package/dist/generators/plan-generator.d.ts.map +1 -1
- package/dist/generators/plan-generator.js +47 -67
- package/dist/generators/plan-generator.js.map +1 -1
- package/dist/generators/skills-generator.d.ts +15 -0
- package/dist/generators/skills-generator.d.ts.map +1 -0
- package/dist/generators/skills-generator.js +225 -0
- package/dist/generators/skills-generator.js.map +1 -0
- package/dist/gui/assets/index-B3PIuWJS.js +22 -0
- package/dist/gui/assets/index-D-VigurY.css +2 -0
- package/dist/gui/index.html +2 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +16 -85
- package/dist/mcp/server.js.map +1 -1
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/gui/src/App.tsx +85 -3
- package/gui/src/features/workspace/WorkspaceList.tsx +85 -3
- package/package.json +1 -1
- package/src/analyzers/index.ts +9 -1
- package/src/analyzers/messaging-analyzer.ts +254 -0
- package/src/analyzers/readme-summarizer.ts +9 -4
- package/src/analyzers/run-analyzer.ts +269 -0
- package/src/core/config.ts +1 -1
- package/src/generators/base.ts +19 -56
- package/src/generators/index.ts +32 -14
- package/src/generators/map-generator.test.ts +78 -0
- package/src/generators/map-generator.ts +164 -53
- package/src/generators/plan-generator.ts +53 -75
- package/src/generators/skills-generator.ts +255 -0
- package/src/mcp/server.ts +16 -89
- package/src/types.ts +54 -0
- package/dist/gui/assets/index-C8W6FoPa.js +0 -21
- package/dist/gui/assets/index-CJn2LW8K.css +0 -2
|
@@ -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
|
|
84
|
-
if (
|
|
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
|
+
}
|
package/src/core/config.ts
CHANGED
|
@@ -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:
|
|
35
|
+
packContextXml: false,
|
|
36
36
|
excludePatterns: [
|
|
37
37
|
'**/node_modules/**',
|
|
38
38
|
'**/bin/**',
|
package/src/generators/base.ts
CHANGED
|
@@ -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**:
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
+
const taskSection = `## First Steps
|
|
184
155
|
|
|
185
|
-
|
|
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
|
-
|
|
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
|
|