@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.
- package/dist/analyzers/detect-apis.d.ts.map +1 -1
- package/dist/analyzers/detect-apis.js +168 -39
- package/dist/analyzers/detect-apis.js.map +1 -1
- package/dist/analyzers/detect-deps.d.ts +26 -4
- package/dist/analyzers/detect-deps.d.ts.map +1 -1
- package/dist/analyzers/detect-deps.js +228 -66
- package/dist/analyzers/detect-deps.js.map +1 -1
- package/dist/analyzers/index.d.ts +1 -1
- package/dist/analyzers/index.d.ts.map +1 -1
- package/dist/analyzers/index.js +7 -3
- package/dist/analyzers/index.js.map +1 -1
- package/dist/analyzers/readme-summarizer.d.ts +3 -1
- package/dist/analyzers/readme-summarizer.d.ts.map +1 -1
- package/dist/analyzers/readme-summarizer.js +43 -19
- package/dist/analyzers/readme-summarizer.js.map +1 -1
- package/dist/analyzers/tech-stack.d.ts +2 -2
- package/dist/analyzers/tech-stack.d.ts.map +1 -1
- package/dist/analyzers/tech-stack.js +257 -232
- package/dist/analyzers/tech-stack.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +15 -8
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +31 -0
- package/dist/commands/sync.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +25 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +1 -2
- package/dist/core/graph.js.map +1 -1
- package/dist/core/packer.d.ts +2 -1
- package/dist/core/packer.d.ts.map +1 -1
- package/dist/core/packer.js +36 -30
- package/dist/core/packer.js.map +1 -1
- package/dist/core/packer.test.js +4 -5
- package/dist/core/packer.test.js.map +1 -1
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +22 -12
- package/dist/core/workspace.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +63 -31
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +17 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/map-generator.d.ts +15 -0
- package/dist/generators/map-generator.d.ts.map +1 -0
- package/dist/generators/map-generator.js +385 -0
- package/dist/generators/map-generator.js.map +1 -0
- package/dist/generators/map-generator.test.d.ts +2 -0
- package/dist/generators/map-generator.test.d.ts.map +1 -0
- package/dist/generators/map-generator.test.js +74 -0
- package/dist/generators/map-generator.test.js.map +1 -0
- package/dist/generators/plan-generator.d.ts +2 -0
- package/dist/generators/plan-generator.d.ts.map +1 -1
- package/dist/generators/plan-generator.js +104 -8
- package/dist/generators/plan-generator.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/analyzers/detect-apis.ts +192 -40
- package/src/analyzers/detect-deps.ts +246 -69
- package/src/analyzers/index.ts +7 -3
- package/src/analyzers/readme-summarizer.ts +48 -19
- package/src/analyzers/tech-stack.ts +222 -194
- package/src/commands/create.ts +16 -10
- package/src/commands/sync.ts +35 -0
- package/src/core/config.ts +25 -0
- package/src/core/graph.ts +1 -2
- package/src/core/packer.test.ts +4 -6
- package/src/core/packer.ts +42 -43
- package/src/core/workspace.ts +23 -13
- package/src/generators/base.ts +67 -30
- package/src/generators/index.ts +17 -0
- package/src/generators/map-generator.test.ts +81 -0
- package/src/generators/map-generator.ts +405 -0
- package/src/generators/plan-generator.ts +117 -7
- package/src/types.ts +13 -0
- package/vitest.config.ts +13 -0
package/src/commands/sync.ts
CHANGED
|
@@ -11,6 +11,9 @@ import * as fs from 'node:fs/promises';
|
|
|
11
11
|
import { loadConfig } from '../core/config.js';
|
|
12
12
|
import { listWorkspaces, loadFeatureConfig } from '../core/workspace.js';
|
|
13
13
|
import { getWorkspaceRepos, rebaseRepo } from '../utils/multi-git.js';
|
|
14
|
+
import { analyzeAllRepos } from '../analyzers/index.js';
|
|
15
|
+
import { generateContextFiles } from '../generators/index.js';
|
|
16
|
+
import type { WorkspaceContext } from '../types.js';
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Executes the sync command.
|
|
@@ -70,6 +73,38 @@ export async function syncCommand(workspaceArg?: string): Promise<void> {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
console.log(`\n📊 ${chalk.bold('Summary:')} ${syncedCount} synced, ${conflictCount} conflict(s)\n`);
|
|
76
|
+
|
|
77
|
+
if (syncedCount > 0) {
|
|
78
|
+
console.log(chalk.cyan('Regenerating architecture maps and context...'));
|
|
79
|
+
try {
|
|
80
|
+
const allRepos = await Promise.all(feature.repos.map(r => {
|
|
81
|
+
const repoName = path.basename(r);
|
|
82
|
+
return {
|
|
83
|
+
name: repoName,
|
|
84
|
+
path: r,
|
|
85
|
+
defaultBranch: 'main',
|
|
86
|
+
};
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
const analysis = await analyzeAllRepos(allRepos);
|
|
90
|
+
const ctx: WorkspaceContext = {
|
|
91
|
+
feature,
|
|
92
|
+
repos: allRepos,
|
|
93
|
+
analysis,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await generateContextFiles(ctx, feature.assistants, workspacePath);
|
|
97
|
+
|
|
98
|
+
const config = await loadConfig();
|
|
99
|
+
if (config.packContextXml) {
|
|
100
|
+
const { packWorkspace } = await import('../core/packer.js');
|
|
101
|
+
await packWorkspace(workspacePath);
|
|
102
|
+
}
|
|
103
|
+
console.log(chalk.green('✅ Workspace maps and contexts successfully updated.\n'));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(chalk.red(`✖ Failed to regenerate maps: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
73
108
|
}
|
|
74
109
|
|
|
75
110
|
/**
|
package/src/core/config.ts
CHANGED
|
@@ -32,6 +32,31 @@ export function getDefaultConfig(): NexusFlowConfig {
|
|
|
32
32
|
workspacesDir: path.join(os.homedir(), 'dev', 'workspaces'),
|
|
33
33
|
defaultAssistant: null,
|
|
34
34
|
scanDepth: 2,
|
|
35
|
+
packContextXml: true,
|
|
36
|
+
excludePatterns: [
|
|
37
|
+
'**/node_modules/**',
|
|
38
|
+
'**/bin/**',
|
|
39
|
+
'**/obj/**',
|
|
40
|
+
'**/dist/**',
|
|
41
|
+
'**/out/**',
|
|
42
|
+
'**/.git/**',
|
|
43
|
+
'**/*.lock',
|
|
44
|
+
'**/package-lock.json',
|
|
45
|
+
'**/pnpm-lock.yaml',
|
|
46
|
+
'**/yarn.lock',
|
|
47
|
+
'**/*.png',
|
|
48
|
+
'**/*.jpg',
|
|
49
|
+
'**/*.jpeg',
|
|
50
|
+
'**/*.gif',
|
|
51
|
+
'**/*.svg',
|
|
52
|
+
'**/*.ico',
|
|
53
|
+
'**/*.pdf',
|
|
54
|
+
'**/*.zip',
|
|
55
|
+
'**/*.tar.gz',
|
|
56
|
+
'**/.vs/**',
|
|
57
|
+
'**/.vscode/**',
|
|
58
|
+
'**/.idea/**',
|
|
59
|
+
],
|
|
35
60
|
};
|
|
36
61
|
}
|
|
37
62
|
|
package/src/core/graph.ts
CHANGED
|
@@ -128,10 +128,9 @@ export async function buildWorkspaceGraph(
|
|
|
128
128
|
|
|
129
129
|
// 3. Add inter-repo DEPENDS_ON edges
|
|
130
130
|
if (analysis) {
|
|
131
|
-
const repoAnalyses = new Map(repos.map((r) => [r.path, analysis.get(r.path)?.dependencies || []]));
|
|
132
131
|
const repoNames = new Map(repos.map((r) => [r.path, r.name]));
|
|
133
132
|
const { findInterRepoDependencies } = await import('../analyzers/detect-deps.js');
|
|
134
|
-
const interDeps = findInterRepoDependencies(
|
|
133
|
+
const interDeps = findInterRepoDependencies(analysis, repoNames);
|
|
135
134
|
|
|
136
135
|
for (const [caller, callees] of interDeps) {
|
|
137
136
|
for (const callee of callees) {
|
package/src/core/packer.test.ts
CHANGED
|
@@ -64,11 +64,10 @@ describe('packWorkspace', () => {
|
|
|
64
64
|
expect(result.totalCharacters).toBe(9001);
|
|
65
65
|
expect(result.fileSize).toBeGreaterThan(0);
|
|
66
66
|
|
|
67
|
-
// Read the generated output file and verify
|
|
67
|
+
// Read the generated output file and verify
|
|
68
68
|
const outputContent = await fs.readFile(result.outputPath, 'utf-8');
|
|
69
|
-
expect(outputContent).toContain('<workspace id="test-feature">');
|
|
70
|
-
expect(outputContent).toContain('<repository name="repo-a">');
|
|
71
69
|
expect(outputContent).toContain('<file path="test.txt">mock repomix content</file>');
|
|
70
|
+
expect(result.outputPaths).toContain(result.outputPath);
|
|
72
71
|
|
|
73
72
|
// Verify repomix was called correctly
|
|
74
73
|
expect(repomix.runCli).toHaveBeenCalledTimes(1);
|
|
@@ -88,9 +87,8 @@ describe('packWorkspace', () => {
|
|
|
88
87
|
|
|
89
88
|
expect(result.totalFiles).toBe(0);
|
|
90
89
|
expect(result.totalCharacters).toBe(0);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(outputContent).not.toContain('<repository'); // Should have no repositories
|
|
90
|
+
expect(result.outputPath).toBe('');
|
|
91
|
+
expect(result.outputPaths?.length).toBe(0);
|
|
94
92
|
});
|
|
95
93
|
|
|
96
94
|
it('should throw if workspace feature config is not found', async () => {
|
package/src/core/packer.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
|
-
import { createReadStream, createWriteStream } from 'node:fs';
|
|
3
|
-
import { pipeline } from 'node:stream/promises';
|
|
4
2
|
import * as path from 'node:path';
|
|
5
3
|
import { runCli } from 'repomix';
|
|
6
4
|
import { loadFeatureConfig } from './workspace.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
7
6
|
|
|
8
7
|
export interface PackResult {
|
|
9
8
|
outputPath: string;
|
|
9
|
+
outputPaths?: string[];
|
|
10
10
|
totalFiles: number;
|
|
11
11
|
totalCharacters: number;
|
|
12
12
|
fileSize: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Packs all repositories in a workspace into
|
|
16
|
+
* Packs all repositories in a workspace into individual XML files using Repomix.
|
|
17
17
|
*/
|
|
18
18
|
export async function packWorkspace(
|
|
19
19
|
workspacePath: string,
|
|
@@ -24,19 +24,14 @@ export async function packWorkspace(
|
|
|
24
24
|
throw new Error(`Workspace not found at ${workspacePath}`);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const config = await loadConfig();
|
|
28
|
+
const ignorePatterns = (config.excludePatterns || []).join(',');
|
|
27
29
|
const compress = options.compress !== false; // default true
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Use a write stream to avoid buffering huge workspaces in memory
|
|
31
|
-
const outStream = createWriteStream(outputPath, { encoding: 'utf-8' });
|
|
32
|
-
|
|
33
|
-
outStream.write('<?xml version="1.0" encoding="UTF-8"?>\n');
|
|
34
|
-
outStream.write(`<workspace id="${feature.id}">\n`);
|
|
35
|
-
outStream.write(` <description><![CDATA[${feature.description}]]></description>\n`);
|
|
36
|
-
outStream.write(' <repositories>\n');
|
|
37
|
-
|
|
30
|
+
|
|
38
31
|
let totalFilesCount = 0;
|
|
39
32
|
let totalCharsCount = 0;
|
|
33
|
+
let totalFileSize = 0;
|
|
34
|
+
const outputPaths: string[] = [];
|
|
40
35
|
|
|
41
36
|
for (const repoPath of feature.repos) {
|
|
42
37
|
const repoName = path.basename(repoPath);
|
|
@@ -48,15 +43,37 @@ export async function packWorkspace(
|
|
|
48
43
|
continue;
|
|
49
44
|
}
|
|
50
45
|
|
|
51
|
-
const
|
|
46
|
+
const outputXmlPath = path.join(workspacePath, `nexusflow-context-${repoName}.xml`);
|
|
47
|
+
const instructionsPath = path.join(worktreePath, 'repomix-instruction.md');
|
|
48
|
+
|
|
49
|
+
// Generate dynamic repomix-instruction.md for this repo
|
|
50
|
+
const mapPath = path.join(workspacePath, `nexusflow-map-${repoName}.md`).replace(/\\/g, '/');
|
|
51
|
+
const planPath = path.join(workspacePath, `nexusflow-plan.md`).replace(/\\/g, '/');
|
|
52
|
+
|
|
53
|
+
const instructionsContent = [
|
|
54
|
+
`# AI Assistant Instructions for ${repoName}`,
|
|
55
|
+
'',
|
|
56
|
+
`This XML file contains the packed codebase for the repository \`${repoName}\` in the workspace \`${feature.id}\`.`,
|
|
57
|
+
'',
|
|
58
|
+
`When working with this code, you MUST follow these guidelines:`,
|
|
59
|
+
`1. **Explore Locally**: Do not rely on this XML snapshot as the source of truth for edits. Always use your native search, grep, and view tools on the live files in: \`${worktreePath.replace(/\\/g, '/')}\`.`,
|
|
60
|
+
`2. **Read the Architecture Map**: Before implementing any changes, read the generated map file at: [nexusflow-map-${repoName}.md](file:///${mapPath}) to understand its layout, API endpoints, test commands, and detected usage patterns.`,
|
|
61
|
+
`3. **Follow the Implementation Order**: See the phased plan at: [nexusflow-plan.md](file:///${planPath}) to avoid cross-repo dependency build ordering issues.`,
|
|
62
|
+
`4. **Git Operations**: Run git commands (status, add, commit) strictly inside the repository subdirectories (e.g. \`cd ${repoName} && git commit -m "..."\`), NOT in the workspace root.`,
|
|
63
|
+
'',
|
|
64
|
+
].join('\n');
|
|
52
65
|
|
|
53
66
|
try {
|
|
54
|
-
//
|
|
67
|
+
// Write instructions to repo worktree root so Repomix picks it up and appends it at the end
|
|
68
|
+
await fs.writeFile(instructionsPath, instructionsContent, 'utf-8');
|
|
69
|
+
|
|
70
|
+
// Run repomix programmatically
|
|
55
71
|
const result = await runCli(['.'], worktreePath, {
|
|
56
72
|
style: 'xml',
|
|
57
|
-
output:
|
|
73
|
+
output: outputXmlPath,
|
|
58
74
|
compress,
|
|
59
75
|
quiet: true,
|
|
76
|
+
ignore: ignorePatterns,
|
|
60
77
|
});
|
|
61
78
|
|
|
62
79
|
if (result && result.packResult) {
|
|
@@ -64,45 +81,27 @@ export async function packWorkspace(
|
|
|
64
81
|
totalCharsCount += result.packResult.totalCharacters;
|
|
65
82
|
}
|
|
66
83
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
createReadStream(tempXmlPath, { encoding: 'utf-8' }),
|
|
72
|
-
outStream,
|
|
73
|
-
{ end: false }
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
outStream.write('\n </repository>\n');
|
|
84
|
+
const stats = await fs.stat(outputXmlPath);
|
|
85
|
+
totalFileSize += stats.size;
|
|
86
|
+
outputPaths.push(outputXmlPath);
|
|
87
|
+
|
|
77
88
|
} catch (error: any) {
|
|
78
89
|
console.error(`Error packing repository ${repoName}:`, error.message);
|
|
79
90
|
} finally {
|
|
80
|
-
// Clean up temporary
|
|
91
|
+
// Clean up the temporary instruction file from the repo's worktree
|
|
81
92
|
try {
|
|
82
|
-
await fs.unlink(
|
|
93
|
+
await fs.unlink(instructionsPath);
|
|
83
94
|
} catch {
|
|
84
95
|
// ignore
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
}
|
|
88
99
|
|
|
89
|
-
outStream.write(' </repositories>\n');
|
|
90
|
-
outStream.write('</workspace>\n');
|
|
91
|
-
outStream.end();
|
|
92
|
-
|
|
93
|
-
// Wait for stream to finish writing
|
|
94
|
-
await new Promise<void>((resolve, reject) => {
|
|
95
|
-
outStream.on('finish', resolve);
|
|
96
|
-
outStream.on('error', reject);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const stats = await fs.stat(outputPath);
|
|
100
|
-
|
|
101
100
|
return {
|
|
102
|
-
outputPath,
|
|
101
|
+
outputPath: outputPaths[0] || '',
|
|
102
|
+
outputPaths,
|
|
103
103
|
totalFiles: totalFilesCount,
|
|
104
104
|
totalCharacters: totalCharsCount,
|
|
105
|
-
fileSize:
|
|
105
|
+
fileSize: totalFileSize,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
-
|
package/src/core/workspace.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { detectDefaultBranch } from '../utils/git.js';
|
|
|
15
15
|
import { analyzeAllRepos } from '../analyzers/index.js';
|
|
16
16
|
import { generateContextFiles } from '../generators/index.js';
|
|
17
17
|
import { packWorkspace } from './packer.js';
|
|
18
|
+
import { loadConfig } from './config.js';
|
|
18
19
|
|
|
19
20
|
/** Name of the per-workspace manifest file. */
|
|
20
21
|
const MANIFEST_FILE = 'nexusflow.json';
|
|
@@ -191,17 +192,19 @@ export async function deleteWorkspace(
|
|
|
191
192
|
): Promise<void> {
|
|
192
193
|
const feature = await loadFeatureConfig(workspacePath);
|
|
193
194
|
if (feature) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const worktreePath =
|
|
195
|
+
const origRepos = feature.originalRepos || [];
|
|
196
|
+
for (let i = 0; i < feature.repos.length; i++) {
|
|
197
|
+
const worktreePath = feature.repos[i]!;
|
|
198
|
+
const originalPath = origRepos[i] || worktreePath;
|
|
199
|
+
const repoName = path.basename(worktreePath);
|
|
197
200
|
try {
|
|
198
|
-
await removeWorktree(
|
|
201
|
+
await removeWorktree(originalPath, worktreePath, true);
|
|
199
202
|
} catch (error) {
|
|
200
|
-
console.warn(`Warning: failed to remove worktree for ${repoName} in ${
|
|
203
|
+
console.warn(`Warning: failed to remove worktree for ${repoName} in ${originalPath}:`, error);
|
|
201
204
|
try {
|
|
202
|
-
await execa('git', ['worktree', 'prune'], { cwd:
|
|
205
|
+
await execa('git', ['worktree', 'prune'], { cwd: originalPath });
|
|
203
206
|
} catch (pruneError) {
|
|
204
|
-
console.warn(`Warning: failed to prune worktrees in ${
|
|
207
|
+
console.warn(`Warning: failed to prune worktrees in ${originalPath}:`, pruneError);
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
210
|
}
|
|
@@ -253,13 +256,13 @@ export async function addRepoToWorkspace(
|
|
|
253
256
|
throw new Error(`Workspace manifest not found at ${workspacePath}`);
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
if (feature.repos.includes(repoPath)) {
|
|
257
|
-
throw new Error(`Repository ${repoPath} is already in the workspace`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
259
|
const newRepoInfo = await resolveRepoInfo(repoPath);
|
|
261
260
|
const worktreeTarget = path.join(workspacePath, newRepoInfo.name);
|
|
262
261
|
|
|
262
|
+
if (feature.repos.includes(worktreeTarget)) {
|
|
263
|
+
throw new Error(`Repository ${repoPath} is already in the workspace`);
|
|
264
|
+
}
|
|
265
|
+
|
|
263
266
|
// 1. Create the worktree
|
|
264
267
|
await createWorktree(
|
|
265
268
|
newRepoInfo.path,
|
|
@@ -269,7 +272,11 @@ export async function addRepoToWorkspace(
|
|
|
269
272
|
);
|
|
270
273
|
|
|
271
274
|
// 2. Update manifest
|
|
272
|
-
feature.repos.push(
|
|
275
|
+
feature.repos.push(worktreeTarget);
|
|
276
|
+
if (!feature.originalRepos) {
|
|
277
|
+
feature.originalRepos = [];
|
|
278
|
+
}
|
|
279
|
+
feature.originalRepos.push(repoPath);
|
|
273
280
|
await saveFeatureConfig(workspacePath, feature);
|
|
274
281
|
|
|
275
282
|
// 3. Update .gitignore at workspace root
|
|
@@ -303,5 +310,8 @@ export async function addRepoToWorkspace(
|
|
|
303
310
|
};
|
|
304
311
|
|
|
305
312
|
await generateContextFiles(ctx, feature.assistants, workspacePath);
|
|
306
|
-
await
|
|
313
|
+
const config = await loadConfig();
|
|
314
|
+
if (config.packContextXml) {
|
|
315
|
+
await packWorkspace(workspacePath);
|
|
316
|
+
}
|
|
307
317
|
}
|
package/src/generators/base.ts
CHANGED
|
@@ -4,16 +4,21 @@
|
|
|
4
4
|
* use as their foundation. Now includes rich project analysis data when available.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
7
9
|
import type { WorkspaceContext, ProjectAnalysis } from '../types.js';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Formats a ProjectAnalysis into a readable markdown section.
|
|
11
13
|
*/
|
|
12
|
-
function formatProjectSection(analysis: ProjectAnalysis): string {
|
|
14
|
+
function formatProjectSection(analysis: ProjectAnalysis, workspacePath: string): string {
|
|
13
15
|
const lines: string[] = [];
|
|
14
16
|
|
|
15
17
|
lines.push(`### ${analysis.name}`);
|
|
16
18
|
|
|
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.`);
|
|
21
|
+
|
|
17
22
|
// Tech stack
|
|
18
23
|
const { techStack } = analysis;
|
|
19
24
|
if (techStack.languages.length > 0 && techStack.languages[0] !== 'other') {
|
|
@@ -74,6 +79,7 @@ function formatProjectSection(analysis: ProjectAnalysis): string {
|
|
|
74
79
|
*/
|
|
75
80
|
export function buildContextContent(ctx: WorkspaceContext): string {
|
|
76
81
|
const { feature, repos, analysis } = ctx;
|
|
82
|
+
const workspacePath = feature.workspacePath;
|
|
77
83
|
|
|
78
84
|
// Build project sections — rich if analysis is available, simple if not
|
|
79
85
|
let projectSections: string;
|
|
@@ -81,7 +87,7 @@ export function buildContextContent(ctx: WorkspaceContext): string {
|
|
|
81
87
|
if (analysis && analysis.size > 0) {
|
|
82
88
|
const sections = repos.map((r) => {
|
|
83
89
|
const a = analysis.get(r.path);
|
|
84
|
-
if (a) return formatProjectSection(a);
|
|
90
|
+
if (a) return formatProjectSection(a, workspacePath);
|
|
85
91
|
return `### ${r.name}\n- **Path**: \`${r.path}\``;
|
|
86
92
|
});
|
|
87
93
|
projectSections = sections.join('\n\n');
|
|
@@ -117,11 +123,32 @@ ${allConfigs.join('\n')}
|
|
|
117
123
|
// Resumption commands section
|
|
118
124
|
let resumptionSection = '';
|
|
119
125
|
if (feature.resumption) {
|
|
120
|
-
|
|
126
|
+
let { testCommand, mockCommand, startCommand } = feature.resumption;
|
|
121
127
|
const parts: string[] = [];
|
|
122
128
|
if (mockCommand) parts.push(`- **Setup/Mock Command**: \`${mockCommand}\``);
|
|
123
129
|
if (startCommand) parts.push(`- **Start/Run Command**: \`${startCommand}\``);
|
|
124
|
-
|
|
130
|
+
|
|
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) {
|
|
149
|
+
parts.push(`- **Verification/Test Command**: \`${testCommand}\``);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
125
152
|
|
|
126
153
|
if (parts.length > 0) {
|
|
127
154
|
resumptionSection = `
|
|
@@ -136,14 +163,47 @@ ${parts.join('\n')}
|
|
|
136
163
|
}
|
|
137
164
|
}
|
|
138
165
|
|
|
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
|
|
182
|
+
|
|
183
|
+
Your very first task upon entering this workspace is to analyze the codebase and document it in a universal reference file:
|
|
184
|
+
|
|
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.
|
|
193
|
+
|
|
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.
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const knowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md').replace(/\\/g, '/');
|
|
199
|
+
|
|
139
200
|
return `# Multi-Repo Workspace Context
|
|
140
201
|
|
|
141
202
|
## Feature: ${feature.id}
|
|
142
203
|
|
|
143
204
|
**Description:** ${feature.description}
|
|
144
205
|
|
|
145
|
-
|
|
146
|
-
**Created:** ${feature.createdAt}
|
|
206
|
+
> For the detailed feature specification, architecture decisions, and session memory, see [nexusflow-knowledge.md](file:///${knowledgePath}).
|
|
147
207
|
|
|
148
208
|
---
|
|
149
209
|
|
|
@@ -157,30 +217,7 @@ ${existingConfigsSection}
|
|
|
157
217
|
${resumptionSection}
|
|
158
218
|
---
|
|
159
219
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
A packed, token-efficient version of the entire multi-repo codebase is automatically generated and updated at the workspace root:
|
|
163
|
-
- **File**: \`nexusflow-context.xml\`
|
|
164
|
-
- **Purpose**: Contains the aggregated source code and structure of all projects in the workspace.
|
|
165
|
-
- **Usage**: You can read this file directly to understand relationships or search for code patterns across all projects without needing to manually traverse directories.
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
## Task & Step-by-Step Initialization
|
|
170
|
-
|
|
171
|
-
Your very first task upon entering this workspace is to analyze the codebase and document it in a universal reference file:
|
|
172
|
-
|
|
173
|
-
1. **Create \`nexusflow-overview.md\`** at the workspace root.
|
|
174
|
-
2. **Project Assumptions**: For each project, write down a clear assumption of what it does, its primary tech stack, and its core responsibilities.
|
|
175
|
-
3. **Inter-Repo Relationships**: Document how the repos relate:
|
|
176
|
-
- Shared libraries/packages (producers and consumers).
|
|
177
|
-
- API boundaries (which repos expose APIs, which ones consume them).
|
|
178
|
-
- Data flows and dependencies.
|
|
179
|
-
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"**.
|
|
180
|
-
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.
|
|
181
|
-
|
|
182
|
-
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.
|
|
183
|
-
|
|
220
|
+
${taskSection}
|
|
184
221
|
---
|
|
185
222
|
|
|
186
223
|
## Guidelines
|
package/src/generators/index.ts
CHANGED
|
@@ -139,6 +139,23 @@ export async function generateContextFiles(
|
|
|
139
139
|
);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// Generate per-repo architecture maps
|
|
143
|
+
if (ctx.analysis) {
|
|
144
|
+
for (const repo of ctx.repos) {
|
|
145
|
+
const a = ctx.analysis.get(repo.path);
|
|
146
|
+
if (a) {
|
|
147
|
+
try {
|
|
148
|
+
const { generateRepoMap } = await import('./map-generator.js');
|
|
149
|
+
await generateRepoMap(repo, a, workspacePath);
|
|
150
|
+
console.log(chalk.green(' ✔'), `Generated Architecture Map for ${chalk.bold(repo.name)}`);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
console.error(chalk.red(' ✖'), `Failed to generate Architecture Map for ${repo.name}: ${message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
142
159
|
for (const assistant of assistants) {
|
|
143
160
|
const entry = GENERATORS[assistant];
|
|
144
161
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { generateRepoMap } from './map-generator.js';
|
|
6
|
+
import type { Language, Framework, ProjectAnalysis } from '../types.js';
|
|
7
|
+
import * as globby from 'globby';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
vi.mock('node:fs/promises');
|
|
13
|
+
vi.mock('globby');
|
|
14
|
+
|
|
15
|
+
describe('generateRepoMap', () => {
|
|
16
|
+
const workspacePath = path.join(__dirname, '..', '..', 'temp-test-workspace');
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should generate a markdown map file with correct structure and links', async () => {
|
|
23
|
+
// 1. Mock inputs
|
|
24
|
+
const mockRepo = {
|
|
25
|
+
name: 'test-repo',
|
|
26
|
+
path: '/original/path/test-repo',
|
|
27
|
+
defaultBranch: 'main',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const mockAnalysis: ProjectAnalysis = {
|
|
31
|
+
name: 'test-repo',
|
|
32
|
+
path: '/original/path/test-repo',
|
|
33
|
+
techStack: {
|
|
34
|
+
languages: ['typescript' as Language],
|
|
35
|
+
frameworks: ['react' as Framework],
|
|
36
|
+
buildTools: ['vite'],
|
|
37
|
+
projectType: 'frontend' as const,
|
|
38
|
+
},
|
|
39
|
+
endpoints: [
|
|
40
|
+
{ method: 'GET', path: '/api/v1/users', source: 'src/controllers/users.ts' }
|
|
41
|
+
],
|
|
42
|
+
dependencies: [
|
|
43
|
+
{ name: 'react', type: 'npm' as const }
|
|
44
|
+
],
|
|
45
|
+
ports: [],
|
|
46
|
+
readmeSummary: 'A test repository.',
|
|
47
|
+
existingAIConfigs: [],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Mock globby returns
|
|
51
|
+
vi.spyOn(globby, 'globby').mockImplementation(async (pattern: any) => {
|
|
52
|
+
const pat = typeof pattern === 'string' ? pattern : (pattern[0] || '');
|
|
53
|
+
if (pat.includes('**/*.sln')) return [];
|
|
54
|
+
if (pat.includes('**/*.csproj')) return [];
|
|
55
|
+
if (pat.includes('package.json')) return ['package.json'];
|
|
56
|
+
if (pat.includes('SKILL.md')) return ['skills/my-skill/SKILL.md'];
|
|
57
|
+
return [];
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const writtenFiles: Record<string, string> = {};
|
|
61
|
+
vi.spyOn(fs, 'writeFile').mockImplementation(async (filePath: any, content: any) => {
|
|
62
|
+
writtenFiles[filePath as string] = content as string;
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 2. Run generator
|
|
67
|
+
await generateRepoMap(mockRepo, mockAnalysis, workspacePath);
|
|
68
|
+
|
|
69
|
+
// 3. Assertions
|
|
70
|
+
const expectedOutPath = path.join(workspacePath, 'nexusflow-map-test-repo.md');
|
|
71
|
+
expect(fs.writeFile).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(writtenFiles[expectedOutPath]).toBeDefined();
|
|
73
|
+
|
|
74
|
+
const content = writtenFiles[expectedOutPath]!;
|
|
75
|
+
expect(content).toContain('# Repository Architecture Map — test-repo');
|
|
76
|
+
expect(content).toContain('package.json');
|
|
77
|
+
expect(content).toContain('my-skill');
|
|
78
|
+
expect(content).toContain('GET');
|
|
79
|
+
expect(content).toContain('/api/v1/users');
|
|
80
|
+
});
|
|
81
|
+
});
|