@mrpatronz/nexusflow 0.2.12 → 0.2.17

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 (233) hide show
  1. package/.github/dependabot.yml +52 -52
  2. package/.github/workflows/dependabot-merge.yml +53 -0
  3. package/.github/workflows/release.yml +112 -100
  4. package/.vscode/launch.json +17 -17
  5. package/.vscode/tasks.json +17 -17
  6. package/GETTING_STARTED.md +114 -109
  7. package/README.md +336 -311
  8. package/desktop/build-installer.js +228 -0
  9. package/desktop/build.js +118 -0
  10. package/desktop/installer/nexusflow.iss +35 -0
  11. package/desktop/neutralino.config.json +39 -0
  12. package/desktop/package-lock.json +1060 -0
  13. package/desktop/package.json +14 -0
  14. package/dist/commands/add-repo.d.ts.map +1 -1
  15. package/dist/commands/add-repo.js +24 -14
  16. package/dist/commands/add-repo.js.map +1 -1
  17. package/dist/commands/commit.js +1 -1
  18. package/dist/commands/commit.js.map +1 -1
  19. package/dist/commands/desktop.d.ts +12 -0
  20. package/dist/commands/desktop.d.ts.map +1 -0
  21. package/dist/commands/desktop.js +119 -0
  22. package/dist/commands/desktop.js.map +1 -0
  23. package/dist/commands/diff.js +1 -1
  24. package/dist/commands/diff.js.map +1 -1
  25. package/dist/commands/doctor.js +1 -1
  26. package/dist/commands/doctor.js.map +1 -1
  27. package/dist/commands/handoff.js +1 -1
  28. package/dist/commands/handoff.js.map +1 -1
  29. package/dist/commands/logs.js +1 -1
  30. package/dist/commands/logs.js.map +1 -1
  31. package/dist/commands/open.d.ts.map +1 -1
  32. package/dist/commands/open.js +12 -7
  33. package/dist/commands/open.js.map +1 -1
  34. package/dist/commands/pack.js +1 -1
  35. package/dist/commands/pack.js.map +1 -1
  36. package/dist/commands/refresh.js +13 -8
  37. package/dist/commands/refresh.js.map +1 -1
  38. package/dist/commands/remove.d.ts.map +1 -1
  39. package/dist/commands/remove.js +12 -7
  40. package/dist/commands/remove.js.map +1 -1
  41. package/dist/commands/start.js +1 -1
  42. package/dist/commands/start.js.map +1 -1
  43. package/dist/commands/status.js +1 -1
  44. package/dist/commands/status.js.map +1 -1
  45. package/dist/commands/stop.js +1 -1
  46. package/dist/commands/stop.js.map +1 -1
  47. package/dist/commands/sync.d.ts.map +1 -1
  48. package/dist/commands/sync.js +14 -11
  49. package/dist/commands/sync.js.map +1 -1
  50. package/dist/commands/tui.d.ts +8 -0
  51. package/dist/commands/tui.d.ts.map +1 -0
  52. package/dist/commands/tui.js +386 -0
  53. package/dist/commands/tui.js.map +1 -0
  54. package/dist/commands/ui.d.ts +3 -1
  55. package/dist/commands/ui.d.ts.map +1 -1
  56. package/dist/commands/ui.js +76 -19
  57. package/dist/commands/ui.js.map +1 -1
  58. package/dist/core/workspace.d.ts +7 -0
  59. package/dist/core/workspace.d.ts.map +1 -1
  60. package/dist/core/workspace.js +45 -1
  61. package/dist/core/workspace.js.map +1 -1
  62. package/dist/generators/base.d.ts.map +1 -1
  63. package/dist/generators/base.js +78 -66
  64. package/dist/generators/base.js.map +1 -1
  65. package/dist/generators/codex.js +11 -11
  66. package/dist/generators/copilot.js +17 -17
  67. package/dist/generators/cursor.js +5 -5
  68. package/dist/generators/index.d.ts.map +1 -1
  69. package/dist/generators/index.js +64 -50
  70. package/dist/generators/index.js.map +1 -1
  71. package/dist/generators/map-generator.js +14 -14
  72. package/dist/generators/skills-generator.js +28 -28
  73. package/dist/gui/app_logo.png +0 -0
  74. package/dist/gui/assets/index-Ct3qq-X4.js +25 -0
  75. package/dist/gui/assets/index-P_ZrgHXg.css +1 -0
  76. package/dist/gui/icons.svg +24 -24
  77. package/dist/gui/index.html +18 -18
  78. package/dist/index.js +43 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/orchestration/runner.d.ts +5 -6
  81. package/dist/orchestration/runner.d.ts.map +1 -1
  82. package/dist/orchestration/runner.js +105 -102
  83. package/dist/orchestration/runner.js.map +1 -1
  84. package/dist/server.d.ts.map +1 -1
  85. package/dist/server.js +235 -2
  86. package/dist/server.js.map +1 -1
  87. package/dist/server.test.js +78 -0
  88. package/dist/server.test.js.map +1 -1
  89. package/dist/types.d.ts +4 -0
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/utils/detect-editors.js +1 -1
  92. package/dist/utils/detect-editors.js.map +1 -1
  93. package/dist/utils/detect-editors.test.js +3 -3
  94. package/dist/utils/detect-editors.test.js.map +1 -1
  95. package/dist/utils/git.d.ts +16 -0
  96. package/dist/utils/git.d.ts.map +1 -1
  97. package/dist/utils/git.js +52 -0
  98. package/dist/utils/git.js.map +1 -1
  99. package/dist/utils/multi-git.d.ts +2 -0
  100. package/dist/utils/multi-git.d.ts.map +1 -1
  101. package/dist/utils/multi-git.js +5 -2
  102. package/dist/utils/multi-git.js.map +1 -1
  103. package/dist/utils/prompts.d.ts +1 -0
  104. package/dist/utils/prompts.d.ts.map +1 -1
  105. package/dist/utils/prompts.js +77 -15
  106. package/dist/utils/prompts.js.map +1 -1
  107. package/dist/utils/update-check.d.ts +3 -1
  108. package/dist/utils/update-check.d.ts.map +1 -1
  109. package/dist/utils/update-check.js +23 -6
  110. package/dist/utils/update-check.js.map +1 -1
  111. package/dist/utils/workflows.d.ts +11 -0
  112. package/dist/utils/workflows.d.ts.map +1 -0
  113. package/dist/utils/workflows.js +156 -0
  114. package/dist/utils/workflows.js.map +1 -0
  115. package/dist/utils/workflows.test.d.ts +2 -0
  116. package/dist/utils/workflows.test.d.ts.map +1 -0
  117. package/dist/utils/workflows.test.js +86 -0
  118. package/dist/utils/workflows.test.js.map +1 -0
  119. package/extension/media/icon.svg +1 -0
  120. package/extension/package-lock.json +2673 -2811
  121. package/extension/package.json +108 -61
  122. package/extension/src/extension.ts +631 -168
  123. package/extension/tsconfig.json +21 -19
  124. package/gui/README.md +73 -73
  125. package/gui/e2e/wizard.spec.ts +346 -327
  126. package/gui/eslint.config.js +26 -22
  127. package/gui/index.html +17 -17
  128. package/gui/package-lock.json +3079 -3118
  129. package/gui/package.json +36 -36
  130. package/gui/playwright.config.ts +41 -41
  131. package/gui/public/app_logo.png +0 -0
  132. package/gui/public/icons.svg +24 -24
  133. package/gui/src/App.css +1 -1
  134. package/gui/src/App.tsx +3407 -2498
  135. package/gui/src/assets/vite.svg +1 -1
  136. package/gui/src/features/changes/ChangesViewer.tsx +388 -212
  137. package/gui/src/features/knowledge/KnowledgeBase.tsx +84 -84
  138. package/gui/src/features/onboarding/OnboardingWizard.tsx +230 -230
  139. package/gui/src/features/plan/ImplementationPlan.tsx +32 -32
  140. package/gui/src/features/services/ServiceConsole.tsx +250 -159
  141. package/gui/src/features/sessions/SessionHistory.tsx +195 -195
  142. package/gui/src/features/workspace/WorkspaceList.tsx +666 -514
  143. package/gui/src/index.css +152 -17
  144. package/gui/src/main.tsx +94 -10
  145. package/gui/src/types.ts +62 -62
  146. package/gui/tsconfig.app.json +25 -25
  147. package/gui/tsconfig.json +7 -7
  148. package/gui/tsconfig.node.json +24 -24
  149. package/gui/vite.config.ts +12 -12
  150. package/package.json +56 -55
  151. package/resources/workflows/plan-implement-review.md +8 -0
  152. package/resources/workflows/research-verify.md +6 -0
  153. package/resources/workflows/solo-developer.md +3 -0
  154. package/scripts/simulate-workspaces.ts +55 -55
  155. package/src/analyzers/detect-apis.ts +290 -290
  156. package/src/analyzers/detect-deps.ts +315 -315
  157. package/src/analyzers/detect-existing.ts +74 -74
  158. package/src/analyzers/detect-ports.ts +110 -110
  159. package/src/analyzers/index.ts +97 -97
  160. package/src/analyzers/messaging-analyzer.ts +254 -254
  161. package/src/analyzers/readme-summarizer.ts +102 -102
  162. package/src/analyzers/run-analyzer.ts +269 -269
  163. package/src/analyzers/tech-stack.ts +283 -283
  164. package/src/commands/add-repo.ts +157 -141
  165. package/src/commands/commands.test.ts +155 -155
  166. package/src/commands/commit.ts +131 -131
  167. package/src/commands/create.ts +207 -207
  168. package/src/commands/desktop.ts +128 -0
  169. package/src/commands/diff.ts +125 -125
  170. package/src/commands/doctor.ts +357 -357
  171. package/src/commands/handoff.ts +266 -266
  172. package/src/commands/init.ts +134 -134
  173. package/src/commands/list.ts +46 -46
  174. package/src/commands/logs.ts +63 -63
  175. package/src/commands/mcp.ts +92 -92
  176. package/src/commands/open.ts +129 -121
  177. package/src/commands/pack.ts +73 -73
  178. package/src/commands/refresh.ts +147 -139
  179. package/src/commands/remove.ts +98 -90
  180. package/src/commands/start.ts +117 -117
  181. package/src/commands/status.ts +54 -54
  182. package/src/commands/stop.ts +55 -55
  183. package/src/commands/sync.ts +151 -145
  184. package/src/commands/tui.ts +424 -0
  185. package/src/commands/ui.ts +111 -47
  186. package/src/core/config.test.ts +96 -96
  187. package/src/core/config.ts +115 -115
  188. package/src/core/graph.ts +344 -344
  189. package/src/core/packer.test.ts +99 -99
  190. package/src/core/packer.ts +107 -107
  191. package/src/core/scanner.test.ts +61 -61
  192. package/src/core/scanner.ts +91 -91
  193. package/src/core/workspace.ts +380 -333
  194. package/src/core/worktree.ts +120 -120
  195. package/src/generators/antigravity.ts +32 -32
  196. package/src/generators/base.ts +240 -227
  197. package/src/generators/claude.ts +34 -34
  198. package/src/generators/codex.ts +48 -48
  199. package/src/generators/copilot.ts +56 -56
  200. package/src/generators/cursor.ts +44 -44
  201. package/src/generators/index.ts +250 -230
  202. package/src/generators/map-generator.test.ts +159 -159
  203. package/src/generators/map-generator.ts +542 -542
  204. package/src/generators/plan-generator.ts +481 -481
  205. package/src/generators/skills-generator.ts +259 -259
  206. package/src/index.ts +426 -382
  207. package/src/mcp/server.ts +294 -294
  208. package/src/orchestration/detect.ts +313 -313
  209. package/src/orchestration/index.ts +7 -7
  210. package/src/orchestration/runner.ts +283 -279
  211. package/src/server.test.ts +555 -464
  212. package/src/server.ts +1308 -1047
  213. package/src/types.ts +408 -403
  214. package/src/utils/detect-ai.test.ts +44 -44
  215. package/src/utils/detect-ai.ts +84 -84
  216. package/src/utils/detect-editors.test.ts +48 -48
  217. package/src/utils/detect-editors.ts +57 -57
  218. package/src/utils/git.test.ts +74 -74
  219. package/src/utils/git.ts +117 -61
  220. package/src/utils/local-ai.test.ts +130 -130
  221. package/src/utils/local-ai.ts +111 -111
  222. package/src/utils/multi-git.ts +313 -306
  223. package/src/utils/prompts.ts +209 -135
  224. package/src/utils/session-finder.ts +483 -483
  225. package/src/utils/system-scanner.test.ts +89 -89
  226. package/src/utils/system-scanner.ts +96 -96
  227. package/src/utils/update-check.ts +309 -275
  228. package/src/utils/workflows.test.ts +114 -0
  229. package/src/utils/workflows.ts +178 -0
  230. package/tsconfig.json +19 -19
  231. package/vitest.config.ts +14 -14
  232. package/dist/gui/assets/index-Ca2VRMrQ.js +0 -22
  233. package/dist/gui/assets/index-fj0RJiOb.css +0 -2
@@ -1,99 +1,99 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } 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 { packWorkspace } from './packer.js';
6
- import * as workspace from './workspace.js';
7
- import * as repomix from 'repomix';
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
-
12
- // Mock dependencies
13
- vi.mock('./workspace.js');
14
- vi.mock('repomix');
15
-
16
- describe('packWorkspace', () => {
17
- const workspacePath = path.join(__dirname, '..', '..', 'temp-test-workspace');
18
-
19
- beforeEach(async () => {
20
- await fs.mkdir(workspacePath, { recursive: true });
21
- });
22
-
23
- afterEach(async () => {
24
- await fs.rm(workspacePath, { recursive: true, force: true });
25
- vi.clearAllMocks();
26
- });
27
-
28
- it('should successfully pack a workspace and stream repomix output', async () => {
29
- // 1. Mock the workspace config
30
- const mockFeature = {
31
- id: 'test-feature',
32
- description: 'Test description',
33
- repos: [path.join(workspacePath, 'repo-a')]
34
- };
35
- vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue(mockFeature as any);
36
-
37
- // Create fake repo directory so fs.access passes
38
- await fs.mkdir(path.join(workspacePath, 'repo-a'), { recursive: true });
39
-
40
- // 2. Mock repomix runCli
41
- vi.spyOn(repomix, 'runCli').mockImplementation(async (dirs, cwd, options) => {
42
- // Create the expected output file to simulate repomix success
43
- if (options && options.output) {
44
- await fs.writeFile(options.output as string, '<file path="test.txt">mock repomix content</file>\n', 'utf-8');
45
- }
46
- return {
47
- packResult: {
48
- totalFiles: 42,
49
- totalCharacters: 9001,
50
- totalTokens: 100,
51
- fileCharCounts: {},
52
- fileTokenCounts: {}
53
- },
54
- suspiciousFilesResults: [],
55
- config: {} as any
56
- } as any;
57
- });
58
-
59
- // 3. Run the packer
60
- const result = await packWorkspace(workspacePath);
61
-
62
- // 4. Assertions
63
- expect(result.totalFiles).toBe(42);
64
- expect(result.totalCharacters).toBe(9001);
65
- expect(result.fileSize).toBeGreaterThan(0);
66
-
67
- // Read the generated output file and verify
68
- const outputContent = await fs.readFile(result.outputPath, 'utf-8');
69
- expect(outputContent).toContain('<file path="test.txt">mock repomix content</file>');
70
- expect(result.outputPaths).toContain(result.outputPath);
71
-
72
- // Verify repomix was called correctly
73
- expect(repomix.runCli).toHaveBeenCalledTimes(1);
74
- });
75
-
76
- it('should skip repos that do not exist', async () => {
77
- const mockFeature = {
78
- id: 'test-feature',
79
- description: 'Test description',
80
- repos: [path.join(workspacePath, 'non-existent-repo')]
81
- };
82
- vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue(mockFeature as any);
83
-
84
- // We intentionally don't create the directory 'non-existent-repo'
85
-
86
- const result = await packWorkspace(workspacePath);
87
-
88
- expect(result.totalFiles).toBe(0);
89
- expect(result.totalCharacters).toBe(0);
90
- expect(result.outputPath).toBe('');
91
- expect(result.outputPaths?.length).toBe(0);
92
- });
93
-
94
- it('should throw if workspace feature config is not found', async () => {
95
- vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue(null);
96
-
97
- await expect(packWorkspace(workspacePath)).rejects.toThrow(/Workspace not found/);
98
- });
99
- });
1
+ import { describe, it, expect, vi, beforeEach, afterEach } 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 { packWorkspace } from './packer.js';
6
+ import * as workspace from './workspace.js';
7
+ import * as repomix from 'repomix';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ // Mock dependencies
13
+ vi.mock('./workspace.js');
14
+ vi.mock('repomix');
15
+
16
+ describe('packWorkspace', () => {
17
+ const workspacePath = path.join(__dirname, '..', '..', 'temp-test-workspace');
18
+
19
+ beforeEach(async () => {
20
+ await fs.mkdir(workspacePath, { recursive: true });
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await fs.rm(workspacePath, { recursive: true, force: true });
25
+ vi.clearAllMocks();
26
+ });
27
+
28
+ it('should successfully pack a workspace and stream repomix output', async () => {
29
+ // 1. Mock the workspace config
30
+ const mockFeature = {
31
+ id: 'test-feature',
32
+ description: 'Test description',
33
+ repos: [path.join(workspacePath, 'repo-a')]
34
+ };
35
+ vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue(mockFeature as any);
36
+
37
+ // Create fake repo directory so fs.access passes
38
+ await fs.mkdir(path.join(workspacePath, 'repo-a'), { recursive: true });
39
+
40
+ // 2. Mock repomix runCli
41
+ vi.spyOn(repomix, 'runCli').mockImplementation(async (dirs, cwd, options) => {
42
+ // Create the expected output file to simulate repomix success
43
+ if (options && options.output) {
44
+ await fs.writeFile(options.output as string, '<file path="test.txt">mock repomix content</file>\n', 'utf-8');
45
+ }
46
+ return {
47
+ packResult: {
48
+ totalFiles: 42,
49
+ totalCharacters: 9001,
50
+ totalTokens: 100,
51
+ fileCharCounts: {},
52
+ fileTokenCounts: {}
53
+ },
54
+ suspiciousFilesResults: [],
55
+ config: {} as any
56
+ } as any;
57
+ });
58
+
59
+ // 3. Run the packer
60
+ const result = await packWorkspace(workspacePath);
61
+
62
+ // 4. Assertions
63
+ expect(result.totalFiles).toBe(42);
64
+ expect(result.totalCharacters).toBe(9001);
65
+ expect(result.fileSize).toBeGreaterThan(0);
66
+
67
+ // Read the generated output file and verify
68
+ const outputContent = await fs.readFile(result.outputPath, 'utf-8');
69
+ expect(outputContent).toContain('<file path="test.txt">mock repomix content</file>');
70
+ expect(result.outputPaths).toContain(result.outputPath);
71
+
72
+ // Verify repomix was called correctly
73
+ expect(repomix.runCli).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ it('should skip repos that do not exist', async () => {
77
+ const mockFeature = {
78
+ id: 'test-feature',
79
+ description: 'Test description',
80
+ repos: [path.join(workspacePath, 'non-existent-repo')]
81
+ };
82
+ vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue(mockFeature as any);
83
+
84
+ // We intentionally don't create the directory 'non-existent-repo'
85
+
86
+ const result = await packWorkspace(workspacePath);
87
+
88
+ expect(result.totalFiles).toBe(0);
89
+ expect(result.totalCharacters).toBe(0);
90
+ expect(result.outputPath).toBe('');
91
+ expect(result.outputPaths?.length).toBe(0);
92
+ });
93
+
94
+ it('should throw if workspace feature config is not found', async () => {
95
+ vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue(null);
96
+
97
+ await expect(packWorkspace(workspacePath)).rejects.toThrow(/Workspace not found/);
98
+ });
99
+ });
@@ -1,107 +1,107 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import { runCli } from 'repomix';
4
- import { loadFeatureConfig } from './workspace.js';
5
- import { loadConfig } from './config.js';
6
-
7
- export interface PackResult {
8
- outputPath: string;
9
- outputPaths?: string[];
10
- totalFiles: number;
11
- totalCharacters: number;
12
- fileSize: number;
13
- }
14
-
15
- /**
16
- * Packs all repositories in a workspace into individual XML files using Repomix.
17
- */
18
- export async function packWorkspace(
19
- workspacePath: string,
20
- options: { compress?: boolean } = {}
21
- ): Promise<PackResult> {
22
- const feature = await loadFeatureConfig(workspacePath);
23
- if (!feature) {
24
- throw new Error(`Workspace not found at ${workspacePath}`);
25
- }
26
-
27
- const config = await loadConfig();
28
- const ignorePatterns = (config.excludePatterns || []).join(',');
29
- const compress = options.compress !== false; // default true
30
-
31
- let totalFilesCount = 0;
32
- let totalCharsCount = 0;
33
- let totalFileSize = 0;
34
- const outputPaths: string[] = [];
35
-
36
- for (const repoPath of feature.repos) {
37
- const repoName = path.basename(repoPath);
38
- const worktreePath = path.join(workspacePath, repoName);
39
-
40
- try {
41
- await fs.access(worktreePath);
42
- } catch {
43
- continue;
44
- }
45
-
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');
65
-
66
- try {
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
71
- const result = await runCli(['.'], worktreePath, {
72
- style: 'xml',
73
- output: outputXmlPath,
74
- compress,
75
- quiet: true,
76
- ignore: ignorePatterns,
77
- });
78
-
79
- if (result && result.packResult) {
80
- totalFilesCount += result.packResult.totalFiles;
81
- totalCharsCount += result.packResult.totalCharacters;
82
- }
83
-
84
- const stats = await fs.stat(outputXmlPath);
85
- totalFileSize += stats.size;
86
- outputPaths.push(outputXmlPath);
87
-
88
- } catch (error: any) {
89
- console.error(`Error packing repository ${repoName}:`, error.message);
90
- } finally {
91
- // Clean up the temporary instruction file from the repo's worktree
92
- try {
93
- await fs.unlink(instructionsPath);
94
- } catch {
95
- // ignore
96
- }
97
- }
98
- }
99
-
100
- return {
101
- outputPath: outputPaths[0] || '',
102
- outputPaths,
103
- totalFiles: totalFilesCount,
104
- totalCharacters: totalCharsCount,
105
- fileSize: totalFileSize,
106
- };
107
- }
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { runCli } from 'repomix';
4
+ import { loadFeatureConfig } from './workspace.js';
5
+ import { loadConfig } from './config.js';
6
+
7
+ export interface PackResult {
8
+ outputPath: string;
9
+ outputPaths?: string[];
10
+ totalFiles: number;
11
+ totalCharacters: number;
12
+ fileSize: number;
13
+ }
14
+
15
+ /**
16
+ * Packs all repositories in a workspace into individual XML files using Repomix.
17
+ */
18
+ export async function packWorkspace(
19
+ workspacePath: string,
20
+ options: { compress?: boolean } = {}
21
+ ): Promise<PackResult> {
22
+ const feature = await loadFeatureConfig(workspacePath);
23
+ if (!feature) {
24
+ throw new Error(`Workspace not found at ${workspacePath}`);
25
+ }
26
+
27
+ const config = await loadConfig();
28
+ const ignorePatterns = (config.excludePatterns || []).join(',');
29
+ const compress = options.compress !== false; // default true
30
+
31
+ let totalFilesCount = 0;
32
+ let totalCharsCount = 0;
33
+ let totalFileSize = 0;
34
+ const outputPaths: string[] = [];
35
+
36
+ for (const repoPath of feature.repos) {
37
+ const repoName = path.basename(repoPath);
38
+ const worktreePath = path.join(workspacePath, repoName);
39
+
40
+ try {
41
+ await fs.access(worktreePath);
42
+ } catch {
43
+ continue;
44
+ }
45
+
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');
65
+
66
+ try {
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
71
+ const result = await runCli(['.'], worktreePath, {
72
+ style: 'xml',
73
+ output: outputXmlPath,
74
+ compress,
75
+ quiet: true,
76
+ ignore: ignorePatterns,
77
+ });
78
+
79
+ if (result && result.packResult) {
80
+ totalFilesCount += result.packResult.totalFiles;
81
+ totalCharsCount += result.packResult.totalCharacters;
82
+ }
83
+
84
+ const stats = await fs.stat(outputXmlPath);
85
+ totalFileSize += stats.size;
86
+ outputPaths.push(outputXmlPath);
87
+
88
+ } catch (error: any) {
89
+ console.error(`Error packing repository ${repoName}:`, error.message);
90
+ } finally {
91
+ // Clean up the temporary instruction file from the repo's worktree
92
+ try {
93
+ await fs.unlink(instructionsPath);
94
+ } catch {
95
+ // ignore
96
+ }
97
+ }
98
+ }
99
+
100
+ return {
101
+ outputPath: outputPaths[0] || '',
102
+ outputPaths,
103
+ totalFiles: totalFilesCount,
104
+ totalCharacters: totalCharsCount,
105
+ fileSize: totalFileSize,
106
+ };
107
+ }
@@ -1,61 +1,61 @@
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 { scanForRepos } from './scanner.js';
5
- import * as gitUtils from '../utils/git.js';
6
-
7
- vi.mock('node:fs/promises');
8
- vi.mock('../utils/git.js');
9
-
10
- describe('scanner core module', () => {
11
- beforeEach(() => {
12
- vi.clearAllMocks();
13
- });
14
-
15
- it('should return repository info if the root dir is a git repo', async () => {
16
- vi.mocked(gitUtils.isGitRepo).mockResolvedValue(true);
17
- vi.mocked(gitUtils.detectDefaultBranch).mockResolvedValue('main');
18
-
19
- const result = await scanForRepos('/mock/dev', 2);
20
-
21
- expect(result).toEqual([
22
- { name: 'dev', path: '/mock/dev', defaultBranch: 'main' }
23
- ]);
24
- expect(gitUtils.isGitRepo).toHaveBeenCalledWith('/mock/dev');
25
- });
26
-
27
- it('should recursively scan subdirectories up to limit and return repos found', async () => {
28
- // Root is not a git repo
29
- vi.mocked(gitUtils.isGitRepo).mockImplementation(async (dir) => {
30
- const normalized = dir.replace(/\\/g, '/');
31
- return normalized === '/mock/dev/repo1' || normalized === '/mock/dev/sub/repo2';
32
- });
33
- vi.mocked(gitUtils.detectDefaultBranch).mockResolvedValue('main');
34
-
35
- // Root directory entries
36
- vi.mocked(fs.readdir).mockImplementation(async (dir: any) => {
37
- const normalized = dir.replace(/\\/g, '/');
38
- if (normalized === '/mock/dev') {
39
- return [
40
- { name: 'repo1', isDirectory: () => true },
41
- { name: 'sub', isDirectory: () => true },
42
- { name: 'ignored_file.txt', isDirectory: () => false },
43
- { name: 'node_modules', isDirectory: () => true },
44
- ] as any[];
45
- }
46
- if (normalized === '/mock/dev/sub') {
47
- return [
48
- { name: 'repo2', isDirectory: () => true },
49
- ] as any[];
50
- }
51
- return [];
52
- });
53
-
54
- const result = await scanForRepos('/mock/dev', 2);
55
-
56
- expect(result).toEqual([
57
- { name: 'repo1', path: path.join('/mock/dev', 'repo1'), defaultBranch: 'main' },
58
- { name: 'repo2', path: path.join('/mock/dev', 'sub', 'repo2'), defaultBranch: 'main' }
59
- ]);
60
- });
61
- });
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 { scanForRepos } from './scanner.js';
5
+ import * as gitUtils from '../utils/git.js';
6
+
7
+ vi.mock('node:fs/promises');
8
+ vi.mock('../utils/git.js');
9
+
10
+ describe('scanner core module', () => {
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+
15
+ it('should return repository info if the root dir is a git repo', async () => {
16
+ vi.mocked(gitUtils.isGitRepo).mockResolvedValue(true);
17
+ vi.mocked(gitUtils.detectDefaultBranch).mockResolvedValue('main');
18
+
19
+ const result = await scanForRepos('/mock/dev', 2);
20
+
21
+ expect(result).toEqual([
22
+ { name: 'dev', path: '/mock/dev', defaultBranch: 'main' }
23
+ ]);
24
+ expect(gitUtils.isGitRepo).toHaveBeenCalledWith('/mock/dev');
25
+ });
26
+
27
+ it('should recursively scan subdirectories up to limit and return repos found', async () => {
28
+ // Root is not a git repo
29
+ vi.mocked(gitUtils.isGitRepo).mockImplementation(async (dir) => {
30
+ const normalized = dir.replace(/\\/g, '/');
31
+ return normalized === '/mock/dev/repo1' || normalized === '/mock/dev/sub/repo2';
32
+ });
33
+ vi.mocked(gitUtils.detectDefaultBranch).mockResolvedValue('main');
34
+
35
+ // Root directory entries
36
+ vi.mocked(fs.readdir).mockImplementation(async (dir: any) => {
37
+ const normalized = dir.replace(/\\/g, '/');
38
+ if (normalized === '/mock/dev') {
39
+ return [
40
+ { name: 'repo1', isDirectory: () => true },
41
+ { name: 'sub', isDirectory: () => true },
42
+ { name: 'ignored_file.txt', isDirectory: () => false },
43
+ { name: 'node_modules', isDirectory: () => true },
44
+ ] as any[];
45
+ }
46
+ if (normalized === '/mock/dev/sub') {
47
+ return [
48
+ { name: 'repo2', isDirectory: () => true },
49
+ ] as any[];
50
+ }
51
+ return [];
52
+ });
53
+
54
+ const result = await scanForRepos('/mock/dev', 2);
55
+
56
+ expect(result).toEqual([
57
+ { name: 'repo1', path: path.join('/mock/dev', 'repo1'), defaultBranch: 'main' },
58
+ { name: 'repo2', path: path.join('/mock/dev', 'sub', 'repo2'), defaultBranch: 'main' }
59
+ ]);
60
+ });
61
+ });