@mrpatronz/nexusflow 0.2.17 → 0.2.19

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 (320) hide show
  1. package/.agents/skills/ccc/SKILL.md +71 -0
  2. package/.agents/skills/ccc/references/management.md +110 -0
  3. package/.agents/skills/ccc/references/settings.md +126 -0
  4. package/.github/dependabot.yml +52 -52
  5. package/.github/workflows/ci.yml +44 -0
  6. package/.github/workflows/dependabot-merge.yml +53 -53
  7. package/.github/workflows/release-desktop.yml +67 -0
  8. package/.github/workflows/release-npm.yml +65 -0
  9. package/.github/workflows/release-vscode.yml +61 -0
  10. package/.vscode/launch.json +17 -17
  11. package/.vscode/tasks.json +17 -17
  12. package/GETTING_STARTED.md +114 -114
  13. package/README.md +364 -336
  14. package/desktop/build-installer.js +228 -228
  15. package/desktop/build.js +118 -118
  16. package/desktop/installer/nexusflow.iss +35 -35
  17. package/desktop/neutralino.config.json +38 -38
  18. package/desktop/package-lock.json +1060 -1060
  19. package/desktop/package.json +14 -14
  20. package/dist/commands/adapter.d.ts +27 -0
  21. package/dist/commands/adapter.d.ts.map +1 -0
  22. package/dist/commands/adapter.js +318 -0
  23. package/dist/commands/adapter.js.map +1 -0
  24. package/dist/commands/add-repo.d.ts.map +1 -1
  25. package/dist/commands/add-repo.js +0 -1
  26. package/dist/commands/add-repo.js.map +1 -1
  27. package/dist/commands/commands.test.js +2 -2
  28. package/dist/commands/commands.test.js.map +1 -1
  29. package/dist/commands/config.d.ts +22 -0
  30. package/dist/commands/config.d.ts.map +1 -0
  31. package/dist/commands/config.js +50 -0
  32. package/dist/commands/config.js.map +1 -0
  33. package/dist/commands/create.d.ts.map +1 -1
  34. package/dist/commands/create.js +14 -17
  35. package/dist/commands/create.js.map +1 -1
  36. package/dist/commands/doctor.d.ts.map +1 -1
  37. package/dist/commands/doctor.js +11 -0
  38. package/dist/commands/doctor.js.map +1 -1
  39. package/dist/commands/refresh.d.ts +1 -0
  40. package/dist/commands/refresh.d.ts.map +1 -1
  41. package/dist/commands/refresh.js +2 -14
  42. package/dist/commands/refresh.js.map +1 -1
  43. package/dist/commands/sync.d.ts.map +1 -1
  44. package/dist/commands/sync.js +36 -54
  45. package/dist/commands/sync.js.map +1 -1
  46. package/dist/commands/tui.d.ts.map +1 -1
  47. package/dist/commands/tui.js +63 -10
  48. package/dist/commands/tui.js.map +1 -1
  49. package/dist/core/adapters/local-storage.d.ts +14 -0
  50. package/dist/core/adapters/local-storage.d.ts.map +1 -0
  51. package/dist/core/adapters/local-storage.js +58 -0
  52. package/dist/core/adapters/local-storage.js.map +1 -0
  53. package/dist/core/adapters/obsidian-storage.d.ts +28 -0
  54. package/dist/core/adapters/obsidian-storage.d.ts.map +1 -0
  55. package/dist/core/adapters/obsidian-storage.js +135 -0
  56. package/dist/core/adapters/obsidian-storage.js.map +1 -0
  57. package/dist/core/adapters/registry.d.ts +8 -0
  58. package/dist/core/adapters/registry.d.ts.map +1 -0
  59. package/dist/core/adapters/registry.js +37 -0
  60. package/dist/core/adapters/registry.js.map +1 -0
  61. package/dist/core/adapters/storage.test.d.ts +2 -0
  62. package/dist/core/adapters/storage.test.d.ts.map +1 -0
  63. package/dist/core/adapters/storage.test.js +106 -0
  64. package/dist/core/adapters/storage.test.js.map +1 -0
  65. package/dist/core/adapters/vault-storage.d.ts +16 -0
  66. package/dist/core/adapters/vault-storage.d.ts.map +1 -0
  67. package/dist/core/adapters/vault-storage.js +77 -0
  68. package/dist/core/adapters/vault-storage.js.map +1 -0
  69. package/dist/core/config.d.ts.map +1 -1
  70. package/dist/core/config.js +28 -4
  71. package/dist/core/config.js.map +1 -1
  72. package/dist/core/plugins/index.d.ts +14 -0
  73. package/dist/core/plugins/index.d.ts.map +1 -0
  74. package/dist/core/plugins/index.js +2 -0
  75. package/dist/core/plugins/index.js.map +1 -0
  76. package/dist/core/plugins/loader.d.ts +2 -0
  77. package/dist/core/plugins/loader.d.ts.map +1 -0
  78. package/dist/core/plugins/loader.js +34 -0
  79. package/dist/core/plugins/loader.js.map +1 -0
  80. package/dist/core/ports/storage.d.ts +59 -0
  81. package/dist/core/ports/storage.d.ts.map +1 -0
  82. package/dist/core/ports/storage.js +10 -0
  83. package/dist/core/ports/storage.js.map +1 -0
  84. package/dist/core/storage.d.ts +10 -0
  85. package/dist/core/storage.d.ts.map +1 -0
  86. package/dist/core/storage.js +29 -0
  87. package/dist/core/storage.js.map +1 -0
  88. package/dist/core/sync.d.ts +44 -0
  89. package/dist/core/sync.d.ts.map +1 -0
  90. package/dist/core/sync.js +82 -0
  91. package/dist/core/sync.js.map +1 -0
  92. package/dist/core/workspace-state.d.ts +51 -0
  93. package/dist/core/workspace-state.d.ts.map +1 -0
  94. package/dist/core/workspace-state.js +108 -0
  95. package/dist/core/workspace-state.js.map +1 -0
  96. package/dist/core/workspace-state.test.d.ts +2 -0
  97. package/dist/core/workspace-state.test.d.ts.map +1 -0
  98. package/dist/core/workspace-state.test.js +84 -0
  99. package/dist/core/workspace-state.test.js.map +1 -0
  100. package/dist/core/workspace.d.ts.map +1 -1
  101. package/dist/core/workspace.js +110 -16
  102. package/dist/core/workspace.js.map +1 -1
  103. package/dist/generators/base.d.ts.map +1 -1
  104. package/dist/generators/base.js +80 -79
  105. package/dist/generators/base.js.map +1 -1
  106. package/dist/generators/codex.js +11 -11
  107. package/dist/generators/copilot.js +17 -17
  108. package/dist/generators/cursor.js +5 -5
  109. package/dist/generators/diff-context.d.ts +6 -0
  110. package/dist/generators/diff-context.d.ts.map +1 -0
  111. package/dist/generators/diff-context.js +68 -0
  112. package/dist/generators/diff-context.js.map +1 -0
  113. package/dist/generators/index.d.ts +1 -1
  114. package/dist/generators/index.d.ts.map +1 -1
  115. package/dist/generators/index.js +125 -68
  116. package/dist/generators/index.js.map +1 -1
  117. package/dist/generators/map-generator.d.ts.map +1 -1
  118. package/dist/generators/map-generator.js +21 -25
  119. package/dist/generators/map-generator.js.map +1 -1
  120. package/dist/generators/plan-generator.d.ts.map +1 -1
  121. package/dist/generators/plan-generator.js +3 -5
  122. package/dist/generators/plan-generator.js.map +1 -1
  123. package/dist/generators/skills-generator.js +28 -28
  124. package/dist/gui/assets/index-Bdbld6yY.css +1 -0
  125. package/dist/gui/assets/index-D8iJPvnk.js +26 -0
  126. package/dist/gui/icons.svg +24 -24
  127. package/dist/gui/index.html +18 -18
  128. package/dist/index.js +131 -20
  129. package/dist/index.js.map +1 -1
  130. package/dist/mcp/server.d.ts.map +1 -1
  131. package/dist/mcp/server.js +42 -0
  132. package/dist/mcp/server.js.map +1 -1
  133. package/dist/server.d.ts.map +1 -1
  134. package/dist/server.js +140 -56
  135. package/dist/server.js.map +1 -1
  136. package/dist/server.test.js +105 -48
  137. package/dist/server.test.js.map +1 -1
  138. package/dist/types.d.ts +38 -2
  139. package/dist/types.d.ts.map +1 -1
  140. package/dist/utils/local-ai.d.ts.map +1 -1
  141. package/dist/utils/local-ai.js +7 -3
  142. package/dist/utils/local-ai.js.map +1 -1
  143. package/dist/utils/multi-git.d.ts +33 -8
  144. package/dist/utils/multi-git.d.ts.map +1 -1
  145. package/dist/utils/multi-git.js +91 -19
  146. package/dist/utils/multi-git.js.map +1 -1
  147. package/dist/utils/multi-git.test.d.ts +2 -0
  148. package/dist/utils/multi-git.test.d.ts.map +1 -0
  149. package/dist/utils/multi-git.test.js +125 -0
  150. package/dist/utils/multi-git.test.js.map +1 -0
  151. package/dist/utils/update-check.d.ts.map +1 -1
  152. package/dist/utils/update-check.js +0 -28
  153. package/dist/utils/update-check.js.map +1 -1
  154. package/dist/utils/workflow-advisor.d.ts +16 -0
  155. package/dist/utils/workflow-advisor.d.ts.map +1 -0
  156. package/dist/utils/workflow-advisor.js +100 -0
  157. package/dist/utils/workflow-advisor.js.map +1 -0
  158. package/extension/package-lock.json +6044 -2673
  159. package/extension/package.json +117 -108
  160. package/extension/src/extension.ts +653 -631
  161. package/extension/tsconfig.json +21 -21
  162. package/gui/README.md +73 -73
  163. package/gui/e2e/dashboard.spec.ts +61 -0
  164. package/gui/e2e/wizard.spec.ts +346 -346
  165. package/gui/eslint.config.js +26 -26
  166. package/gui/index.html +17 -17
  167. package/gui/package-lock.json +3137 -3079
  168. package/gui/package.json +37 -36
  169. package/gui/playwright.config.ts +41 -41
  170. package/gui/public/icons.svg +24 -24
  171. package/gui/src/App.css +1 -1
  172. package/gui/src/App.tsx +3573 -3407
  173. package/gui/src/app/AppSidebar.tsx +69 -0
  174. package/gui/src/assets/vite.svg +1 -1
  175. package/gui/src/components/AddRepoPicker.tsx +81 -0
  176. package/gui/src/components/ui/Button.tsx +40 -0
  177. package/gui/src/components/ui/Card.tsx +6 -0
  178. package/gui/src/components/ui/EmptyState.tsx +22 -0
  179. package/gui/src/components/ui/Input.tsx +21 -0
  180. package/gui/src/components/ui/Menu.tsx +71 -0
  181. package/gui/src/components/ui/Modal.tsx +39 -0
  182. package/gui/src/components/ui/PageHeader.tsx +21 -0
  183. package/gui/src/components/ui/RepoStatusStrip.tsx +33 -0
  184. package/gui/src/components/ui/Skeleton.tsx +5 -0
  185. package/gui/src/components/ui/StatusPill.tsx +36 -0
  186. package/gui/src/components/ui/Tabs.tsx +40 -0
  187. package/gui/src/components/ui/cn.ts +4 -0
  188. package/gui/src/components/ui/index.ts +16 -0
  189. package/gui/src/features/changes/ChangesViewer.tsx +388 -388
  190. package/gui/src/features/knowledge/KnowledgeBase.tsx +84 -84
  191. package/gui/src/features/onboarding/OnboardingWizard.tsx +230 -230
  192. package/gui/src/features/plan/ImplementationPlan.tsx +32 -32
  193. package/gui/src/features/services/ServiceConsole.tsx +215 -250
  194. package/gui/src/features/sessions/SessionHistory.tsx +195 -195
  195. package/gui/src/index.css +167 -152
  196. package/gui/src/lib/status.ts +24 -0
  197. package/gui/src/main.tsx +94 -94
  198. package/gui/src/pages/DashboardPage.tsx +129 -0
  199. package/gui/src/pages/WorkspacesPage.tsx +352 -0
  200. package/gui/src/types.ts +81 -62
  201. package/gui/tsconfig.app.json +25 -25
  202. package/gui/tsconfig.json +7 -7
  203. package/gui/tsconfig.node.json +24 -24
  204. package/gui/vite.config.ts +12 -12
  205. package/package.json +55 -56
  206. package/resources/workflows/plan-implement-review.md +8 -8
  207. package/resources/workflows/research-verify.md +6 -6
  208. package/resources/workflows/solo-developer.md +3 -3
  209. package/scripts/simulate-workspaces.ts +55 -55
  210. package/skills-lock.json +11 -0
  211. package/src/analyzers/detect-apis.ts +290 -290
  212. package/src/analyzers/detect-deps.ts +315 -315
  213. package/src/analyzers/detect-existing.ts +74 -74
  214. package/src/analyzers/detect-ports.ts +110 -110
  215. package/src/analyzers/index.ts +97 -97
  216. package/src/analyzers/messaging-analyzer.ts +254 -254
  217. package/src/analyzers/readme-summarizer.ts +102 -102
  218. package/src/analyzers/run-analyzer.ts +269 -269
  219. package/src/analyzers/tech-stack.ts +283 -283
  220. package/src/commands/adapter.ts +348 -0
  221. package/src/commands/add-repo.ts +156 -157
  222. package/src/commands/commands.test.ts +156 -155
  223. package/src/commands/commit.ts +131 -131
  224. package/src/commands/config.ts +52 -0
  225. package/src/commands/create.ts +204 -207
  226. package/src/commands/desktop.ts +128 -128
  227. package/src/commands/diff.ts +125 -125
  228. package/src/commands/doctor.ts +370 -357
  229. package/src/commands/handoff.ts +266 -266
  230. package/src/commands/init.ts +134 -134
  231. package/src/commands/list.ts +46 -46
  232. package/src/commands/logs.ts +63 -63
  233. package/src/commands/mcp.ts +94 -92
  234. package/src/commands/open.ts +129 -129
  235. package/src/commands/refresh.ts +137 -147
  236. package/src/commands/remove.ts +98 -98
  237. package/src/commands/start.ts +117 -117
  238. package/src/commands/status.ts +54 -54
  239. package/src/commands/stop.ts +55 -55
  240. package/src/commands/sync.ts +125 -151
  241. package/src/commands/tui.ts +484 -424
  242. package/src/commands/ui.ts +111 -111
  243. package/src/core/adapters/local-storage.ts +66 -0
  244. package/src/core/adapters/obsidian-storage.ts +151 -0
  245. package/src/core/adapters/registry.ts +44 -0
  246. package/src/core/adapters/storage.test.ts +174 -0
  247. package/src/core/adapters/vault-storage.ts +86 -0
  248. package/src/core/config.test.ts +96 -96
  249. package/src/core/config.ts +140 -115
  250. package/src/core/graph.ts +344 -344
  251. package/src/core/plugins/index.ts +17 -0
  252. package/src/core/plugins/loader.ts +36 -0
  253. package/src/core/ports/storage.ts +71 -0
  254. package/src/core/scanner.test.ts +61 -61
  255. package/src/core/scanner.ts +91 -91
  256. package/src/core/storage.ts +37 -0
  257. package/src/core/sync.ts +121 -0
  258. package/src/core/workspace-state.test.ts +108 -0
  259. package/src/core/workspace-state.ts +130 -0
  260. package/src/core/workspace.ts +487 -380
  261. package/src/core/worktree.ts +120 -120
  262. package/src/generators/antigravity.ts +32 -32
  263. package/src/generators/base.ts +243 -240
  264. package/src/generators/claude.ts +34 -34
  265. package/src/generators/codex.ts +48 -48
  266. package/src/generators/copilot.ts +56 -56
  267. package/src/generators/cursor.ts +44 -44
  268. package/src/generators/diff-context.ts +75 -0
  269. package/src/generators/index.ts +322 -250
  270. package/src/generators/map-generator.test.ts +159 -159
  271. package/src/generators/map-generator.ts +539 -542
  272. package/src/generators/plan-generator.ts +483 -481
  273. package/src/generators/skills-generator.ts +259 -259
  274. package/src/index.ts +539 -426
  275. package/src/mcp/server.ts +338 -294
  276. package/src/orchestration/detect.ts +313 -313
  277. package/src/orchestration/index.ts +7 -7
  278. package/src/orchestration/runner.ts +283 -283
  279. package/src/server.test.ts +618 -555
  280. package/src/server.ts +1402 -1308
  281. package/src/types.ts +451 -408
  282. package/src/utils/detect-ai.test.ts +44 -44
  283. package/src/utils/detect-ai.ts +84 -84
  284. package/src/utils/detect-editors.test.ts +48 -48
  285. package/src/utils/detect-editors.ts +57 -57
  286. package/src/utils/git.test.ts +74 -74
  287. package/src/utils/git.ts +117 -117
  288. package/src/utils/local-ai.test.ts +130 -130
  289. package/src/utils/local-ai.ts +116 -111
  290. package/src/utils/multi-git.test.ts +158 -0
  291. package/src/utils/multi-git.ts +405 -313
  292. package/src/utils/prompts.ts +209 -209
  293. package/src/utils/session-finder.ts +483 -483
  294. package/src/utils/system-scanner.test.ts +89 -89
  295. package/src/utils/system-scanner.ts +96 -96
  296. package/src/utils/update-check.ts +286 -309
  297. package/src/utils/workflow-advisor.ts +118 -0
  298. package/src/utils/workflows.test.ts +114 -114
  299. package/src/utils/workflows.ts +178 -178
  300. package/tsconfig.json +19 -19
  301. package/vitest.config.ts +14 -14
  302. package/.github/workflows/release.yml +0 -112
  303. package/dist/commands/pack.d.ts +0 -7
  304. package/dist/commands/pack.d.ts.map +0 -1
  305. package/dist/commands/pack.js +0 -64
  306. package/dist/commands/pack.js.map +0 -1
  307. package/dist/core/packer.d.ts +0 -14
  308. package/dist/core/packer.d.ts.map +0 -1
  309. package/dist/core/packer.js +0 -87
  310. package/dist/core/packer.js.map +0 -1
  311. package/dist/core/packer.test.d.ts +0 -2
  312. package/dist/core/packer.test.d.ts.map +0 -1
  313. package/dist/core/packer.test.js +0 -82
  314. package/dist/core/packer.test.js.map +0 -1
  315. package/dist/gui/assets/index-Ct3qq-X4.js +0 -25
  316. package/dist/gui/assets/index-P_ZrgHXg.css +0 -1
  317. package/gui/src/features/workspace/WorkspaceList.tsx +0 -666
  318. package/src/commands/pack.ts +0 -73
  319. package/src/core/packer.test.ts +0 -99
  320. package/src/core/packer.ts +0 -107
@@ -1,481 +1,483 @@
1
- /**
2
- * @module plan-generator
3
- * Analyzes inter-repo dependencies within a workspace and generates a
4
- * `nexusflow-plan.md` implementation plan with build-order phases.
5
- */
6
-
7
- import path from 'node:path';
8
- import fse from 'fs-extra';
9
- import chalk from 'chalk';
10
- import type {
11
- WorkspaceContext,
12
- ProjectAnalysis,
13
- RepoInfo,
14
- DependencyNode,
15
- DependencyGraph,
16
- } from '../types.js';
17
-
18
- // ─── Dependency Graph Builder ─────────────────────────────────────────────
19
-
20
- /**
21
- * Build a dependency graph by analysing package dependencies
22
- * across the workspace repos.
23
- *
24
- * @param analysis Per-repo analysis results, keyed by repo path.
25
- * @param repos Metadata for every repo in the workspace.
26
- * @returns A map of repo name {@link DependencyNode}.
27
- */
28
- export function buildDependencyGraph(
29
- analysis: Map<string, ProjectAnalysis>,
30
- repos: RepoInfo[],
31
- ): DependencyGraph {
32
- const graph: DependencyGraph = new Map();
33
-
34
- // ── Initialise a node for each repo ──────────────────────────────────
35
- for (const repo of repos) {
36
- graph.set(repo.name, {
37
- repoName: repo.name,
38
- repoPath: repo.path,
39
- dependsOn: [],
40
- dependedOnBy: [],
41
- });
42
- }
43
-
44
- // Build a quick lookup: repo name → ProjectAnalysis
45
- const analysisByName = new Map<string, ProjectAnalysis>();
46
- for (const repo of repos) {
47
- const a = analysis.get(repo.path);
48
- if (a) analysisByName.set(repo.name, a);
49
- }
50
-
51
- // ── 1. Produced/consumed package dependencies ──────────────────────────
52
- // Map each produced package name to the repo name that produces it
53
- const packageToRepo = new Map<string, string>();
54
- for (const repo of repos) {
55
- const a = analysisByName.get(repo.name);
56
- if (!a) continue;
57
-
58
- // Map the repo name itself as a produced product (for direct matching)
59
- packageToRepo.set(repo.name.toLowerCase(), repo.name);
60
-
61
- if (a.produces) {
62
- for (const product of a.produces) {
63
- packageToRepo.set(product.name.toLowerCase(), repo.name);
64
- // Map basename (e.g. Hogia.EmploymentService.Client -> Client)
65
- const base = product.name.split('.').pop() ?? product.name;
66
- if (base && base.length > 3) {
67
- packageToRepo.set(base.toLowerCase(), repo.name);
68
- }
69
- }
70
- }
71
- }
72
-
73
- for (const repo of repos) {
74
- const a = analysisByName.get(repo.name);
75
- if (!a) continue;
76
-
77
- for (const dep of a.dependencies) {
78
- const depNameLower = dep.name.toLowerCase();
79
-
80
- // Direct match with a produced package
81
- if (packageToRepo.has(depNameLower)) {
82
- const targetRepo = packageToRepo.get(depNameLower)!;
83
- if (targetRepo !== repo.name) {
84
- addEdge(graph, repo.name, targetRepo);
85
- }
86
- }
87
- }
88
- }
89
-
90
- return graph;
91
- }
92
-
93
- // ─── Topological Sort ─────────────────────────────────────────────────────
94
-
95
- /**
96
- * Topologically sort the dependency graph into build phases.
97
- * Each phase is a group of repos that can be built in parallel
98
- * because all of their dependencies appear in earlier phases.
99
- *
100
- * If a cycle is detected, the remaining nodes are placed in a final phase
101
- * with a warning logged to the console.
102
- *
103
- * @param graph The workspace dependency graph.
104
- * @returns An array of phases, where each phase is an array of repo names.
105
- */
106
- export function topologicalSort(graph: DependencyGraph): string[][] {
107
- // Calculate in-degrees
108
- const inDegree = new Map<string, number>();
109
- for (const [name, node] of graph) {
110
- inDegree.set(name, node.dependsOn.length);
111
- }
112
-
113
- const phases: string[][] = [];
114
- const placed = new Set<string>();
115
-
116
- while (placed.size < graph.size) {
117
- // Collect nodes whose in-degree is 0 and haven't been placed yet
118
- const phase: string[] = [];
119
- for (const [name, degree] of inDegree) {
120
- if (degree === 0 && !placed.has(name)) {
121
- phase.push(name);
122
- }
123
- }
124
-
125
- // Cycle detection — no zero-in-degree nodes remain
126
- if (phase.length === 0) {
127
- const remaining = [...graph.keys()].filter((n) => !placed.has(n));
128
- console.log(
129
- chalk.yellow(' ⚠'),
130
- `Dependency cycle detected among: ${remaining.join(', ')}`,
131
- );
132
- phases.push(remaining);
133
- break;
134
- }
135
-
136
- phase.sort(); // Deterministic ordering within a phase
137
- phases.push(phase);
138
-
139
- // "Remove" placed nodes and decrement dependents' in-degrees
140
- for (const name of phase) {
141
- placed.add(name);
142
- const node = graph.get(name)!;
143
- for (const dependent of node.dependedOnBy) {
144
- inDegree.set(dependent, (inDegree.get(dependent) ?? 1) - 1);
145
- }
146
- }
147
- }
148
-
149
- return phases;
150
- }
151
-
152
- // ─── Plan Generator ───────────────────────────────────────────────────────
153
-
154
- /**
155
- * Generate a `nexusflow-plan.md` implementation plan for the workspace.
156
- *
157
- * The plan includes:
158
- * - A Mermaid dependency diagram
159
- * - Phased implementation order derived from topological sort
160
- * - A dependency cross-reference table
161
- * - A package relations table
162
- * - Actionable local dev tips
163
- *
164
- * @param ctx The current workspace context (feature + repos + analysis).
165
- * @param workspacePath Absolute path to the workspace root directory.
166
- */
167
- export async function generateImplementationPlan(
168
- ctx: WorkspaceContext,
169
- workspacePath: string,
170
- ): Promise<void> {
171
- try {
172
- const { feature, repos, analysis } = ctx;
173
-
174
- // ── Fallback: no analysis available ─────────────────────────────────
175
- if (!analysis || analysis.size === 0) {
176
- const lines = [
177
- `# Implementation Plan — ${feature.id}`,
178
- '',
179
- '> Auto-generated by NexusFlow.',
180
- '> No project analysis data was available, so repos are listed alphabetically.',
181
- '',
182
- '## Repos',
183
- '',
184
- ...repos
185
- .map((r) => r.name)
186
- .sort()
187
- .map((n) => `- ${n}`),
188
- '',
189
- ];
190
- await fse.outputFile(
191
- path.join(workspacePath, 'nexusflow-plan.md'),
192
- lines.join('\n'),
193
- );
194
- console.log(chalk.green(' ✔'), 'Generated nexusflow-plan.md');
195
- return;
196
- }
197
-
198
- // ── Build graph & sort ──────────────────────────────────────────────
199
- const graph = buildDependencyGraph(analysis, repos);
200
- const phases = topologicalSort(graph);
201
-
202
- // ── Render markdown ─────────────────────────────────────────────────
203
- const md: string[] = [];
204
-
205
- md.push(`# Implementation Plan ${feature.id}`);
206
- md.push('');
207
- md.push(`> **Generated At**: ${new Date().toISOString()} (UTC)`);
208
- md.push(`> **Regeneration Command**: Run \`nexusflow refresh\` to update this plan.`);
209
- md.push('');
210
- md.push(
211
- '> Auto-generated by NexusFlow based on dependency analysis between repos.',
212
- );
213
- md.push(
214
- '> Follow the phase order to avoid blocking yourself on cross-repo dependencies.',
215
- );
216
- md.push('');
217
-
218
- // ── Mermaid diagram ─────────────────────────────────────────────────
219
- md.push('## Dependency Diagram');
220
- md.push('');
221
- md.push('```mermaid');
222
- md.push('graph TD');
223
-
224
- const alias = buildAliasMap(graph);
225
-
226
- for (const [name, node] of graph) {
227
- if (node.dependsOn.length === 0 && node.dependedOnBy.length === 0) {
228
- // Isolated node — still show it
229
- md.push(` ${alias.get(name)}["${name}"]`);
230
- }
231
- for (const dep of node.dependsOn) {
232
- // Arrow: dependency → dependent (dep is built first)
233
- md.push(
234
- ` ${alias.get(dep)}["${dep}"] --> ${alias.get(name)}["${name}"]`,
235
- );
236
- }
237
- }
238
-
239
- md.push('```');
240
- md.push('');
241
- md.push('> ⚠️ This diagram is derived from detected package dependencies (`package.json`, `.csproj`, etc.) only.');
242
- md.push('> If you changed a package, the producing repo must release/build before consumer repos can merge.');
243
- md.push('');
244
-
245
- // ── Phase descriptions ──────────────────────────────────────────────
246
- md.push('## Suggested Implementation Order');
247
- md.push('');
248
-
249
- for (let i = 0; i < phases.length; i++) {
250
- const phase = phases[i];
251
- const ordinal = ordinalWord(i + 1);
252
-
253
- md.push(`### Phase ${i + 1}`);
254
- md.push('');
255
- md.push(`**Repos:** ${phase.join(', ')}`);
256
- md.push('');
257
-
258
- if (i === 0) {
259
- md.push(
260
- `**Why first:** These repos have no dependencies on other workspace repos. Other repos depend on them.`,
261
- );
262
- } else if (i === phases.length - 1) {
263
- md.push(
264
- `**Why ${ordinal}:** Depends on APIs and types from earlier phases.`,
265
- );
266
- } else {
267
- const prevPhases = phases
268
- .slice(0, i)
269
- .flat()
270
- .join(', ');
271
- md.push(
272
- `**Why ${ordinal}:** Depends on Phase ${i === 1 ? '1' : `1–${i}`} repos (${prevPhases}). Build these before the consumers.`,
273
- );
274
- }
275
-
276
- md.push('');
277
- }
278
-
279
- // ── Dependency table ────────────────────────────────────────────────
280
- md.push('## Dependency Table');
281
- md.push('');
282
- md.push('| Repo | Depends On | Depended On By |');
283
- md.push('|:---|:---|:---|');
284
-
285
- // Sort repos by phase order for a natural reading experience
286
- const orderedNames = phases.flat();
287
- for (const name of orderedNames) {
288
- const node = graph.get(name)!;
289
- const deps = node.dependsOn.length > 0 ? node.dependsOn.join(', ') : '—';
290
- const rdeps =
291
- node.dependedOnBy.length > 0 ? node.dependedOnBy.join(', ') : '—';
292
- md.push(`| ${name} | ${deps} | ${rdeps} |`);
293
- }
294
-
295
- md.push('');
296
-
297
- // ── Contracts & Clients Table ───────────────────────────────────────
298
- md.push('## 📦 Contracts & Clients');
299
- md.push('');
300
- md.push('| Package | Contributing Projects | Producing Repo | Consuming Repos (Version) | Feed Source | Type |');
301
- md.push('|:---|:---|:---|:---|:---|:---|');
302
-
303
- // Build package relations
304
- interface PackageRelation {
305
- pkgName: string;
306
- contributing?: string[];
307
- producer: string;
308
- consumers: { repoName: string; version?: string }[];
309
- type: 'npm' | 'nuget' | 'other';
310
- feeds?: { name: string; url: string }[];
311
- }
312
-
313
- const packageRelations: PackageRelation[] = [];
314
-
315
- // Find all produced packages
316
- for (const [repoPath, a] of analysis) {
317
- if (a.produces) {
318
- for (const product of a.produces) {
319
- // Find consumers
320
- const consumers: { repoName: string; version?: string }[] = [];
321
- for (const [otherPath, otherA] of analysis) {
322
- if (otherPath === repoPath) continue;
323
- for (const dep of otherA.dependencies) {
324
- if (dep.name.toLowerCase() === product.name.toLowerCase()) {
325
- consumers.push({ repoName: otherA.name, version: dep.version });
326
- }
327
- }
328
- }
329
- packageRelations.push({
330
- pkgName: product.name,
331
- contributing: (product as any).contributing,
332
- producer: a.name,
333
- consumers,
334
- type: product.type,
335
- feeds: a.nugetFeeds,
336
- });
337
- }
338
- }
339
- }
340
-
341
- if (packageRelations.length > 0) {
342
- for (const rel of packageRelations) {
343
- const contribStr = rel.contributing && rel.contributing.length > 0
344
- ? rel.contributing.map(c => `\`${c}\``).join(', ')
345
- : '—';
346
- const consumerStr = rel.consumers.length > 0
347
- ? rel.consumers.map(c => `\`${c.repoName}\` (${c.version || 'pinned'})`).join(', ')
348
- : '_None_';
349
- const feedStr = rel.feeds && rel.feeds.length > 0
350
- ? rel.feeds.map(f => `\`${f.name}\` (${f.url})`).join('<br>')
351
- : '';
352
- md.push(`| \`${rel.pkgName}\` | ${contribStr} | \`${rel.producer}\` | ${consumerStr} | ${feedStr} | \`${rel.type}\` |`);
353
- }
354
- } else {
355
- md.push('| _No package relations detected_ | | | | | |');
356
- }
357
- md.push('');
358
-
359
- // ── Cross-Repo Messaging Roll-up ────────────────────────────────────
360
- md.push('## 📨 Cross-Repo Messaging');
361
- md.push('');
362
- md.push('| Publisher Repo | Message | → Subscriber Repo | Handler |');
363
- md.push('|---|---|---|---|');
364
-
365
- interface CrossRepoMessage {
366
- pubRepo: string;
367
- message: string;
368
- subRepo: string;
369
- handler: string;
370
- }
371
- const crossRepoMessages: CrossRepoMessage[] = [];
372
-
373
- for (const [pubPath, pubA] of analysis) {
374
- if (!pubA.messaging || !pubA.messaging.publishers) continue;
375
- for (const pub of pubA.messaging.publishers) {
376
- // Find subscribers in other repos matching this contract type
377
- for (const [subPath, subA] of analysis) {
378
- if (subPath === pubPath) continue;
379
- if (!subA.messaging || !subA.messaging.subscribers) continue;
380
- for (const sub of subA.messaging.subscribers) {
381
- const pubContract = pub.contractType.toLowerCase().trim();
382
- const subContract = sub.contractType.toLowerCase().trim();
383
- if (pubContract === subContract && pubContract !== 'goservicebusmessage' && pubContract !== 'servicebusmessage') {
384
- crossRepoMessages.push({
385
- pubRepo: pubA.name,
386
- message: pub.contractType,
387
- subRepo: subA.name,
388
- handler: sub.handlerFile,
389
- });
390
- }
391
- }
392
- }
393
- }
394
- }
395
-
396
- if (crossRepoMessages.length > 0) {
397
- for (const m of crossRepoMessages) {
398
- md.push(`| \`${m.pubRepo}\` | \`${m.message}\` | \`${m.subRepo}\` | \`${m.handler}\` |`);
399
- }
400
- } else {
401
- md.push('| _No cross-repo messaging detected_ | | | |');
402
- }
403
- md.push('');
404
-
405
- // ── Local Package Development Loop Tip ──────────────────────────────
406
- md.push('## 💡 Local Package Development Loop');
407
- md.push('');
408
- md.push('When making changes to a shared contract or client library package, follow this standard local feed loop to test and verify consumers before pushing:');
409
- md.push('');
410
- md.push('### For .NET / NuGet packages:');
411
- md.push('1. **Pack locally**: Run `dotnet pack -c Release -o ./local-packages` inside the producing project folder.');
412
- md.push('2. **Add local feed**: Configure a local feed in your consumer project\'s `NuGet.config` pointing to the `./local-packages` directory.');
413
- md.push('3. **Reference local version**: Reference the package with a local development version (e.g. `3.41.0-local`) in the consuming `.csproj`.');
414
- md.push('4. **Revert before merging**: Verify changes compile and tests pass, then **revert** the consuming project\'s package version reference to the official release before merging to master.');
415
- md.push('');
416
- md.push('### For Node.js / npm packages:');
417
- md.push('1. **Link locally**: Run `npm link` inside the producing package folder.');
418
- md.push('2. **Use link**: Run `npm link <package-name>` inside the consuming folder to link it.');
419
- md.push('3. **Revert before merging**: Uninstall the linked package and install the official package version before committing.');
420
- md.push('');
421
-
422
- // ── Write file ──────────────────────────────────────────────────────
423
- const outPath = path.join(workspacePath, 'nexusflow-plan.md');
424
- await fse.outputFile(outPath, md.join('\n'));
425
- console.log(chalk.green(' ✔'), 'Generated nexusflow-plan.md');
426
- } catch (error) {
427
- const message = error instanceof Error ? error.message : String(error);
428
- console.error(
429
- chalk.red(' ✖'),
430
- `Failed to generate implementation plan: ${message}`,
431
- );
432
- }
433
- }
434
-
435
- // ─── Helpers ──────────────────────────────────────────────────────────────
436
-
437
- /**
438
- * Add a directed edge: `from` depends on `to`.
439
- * Idempotent — duplicate edges are ignored.
440
- */
441
- function addEdge(graph: DependencyGraph, from: string, to: string): void {
442
- const fromNode = graph.get(from);
443
- const toNode = graph.get(to);
444
- if (!fromNode || !toNode) return;
445
-
446
- if (!fromNode.dependsOn.includes(to)) {
447
- fromNode.dependsOn.push(to);
448
- }
449
- if (!toNode.dependedOnBy.includes(from)) {
450
- toNode.dependedOnBy.push(from);
451
- }
452
- }
453
-
454
- /**
455
- * Build a short single-letter alias map for Mermaid node IDs.
456
- * Falls back to sanitised names when there are more than 26 repos.
457
- */
458
- function buildAliasMap(graph: DependencyGraph): Map<string, string> {
459
- const map = new Map<string, string>();
460
- const names = [...graph.keys()].sort();
461
-
462
- if (names.length <= 26) {
463
- let code = 65; // 'A'
464
- for (const name of names) {
465
- map.set(name, String.fromCharCode(code++));
466
- }
467
- } else {
468
- for (const name of names) {
469
- map.set(name, name.replace(/[^a-zA-Z0-9]/g, '_'));
470
- }
471
- }
472
-
473
- return map;
474
- }
475
-
476
- /** Return an ordinal word for small numbers, or "nth" for larger ones. */
477
- function ordinalWord(n: number): string {
478
- const words = ['first', 'second', 'third', 'fourth', 'fifth'];
479
- if (n >= 1 && n <= words.length) return words[n - 1];
480
- return `${n}th`;
481
- }
1
+ /**
2
+ * @module plan-generator
3
+ * Analyzes inter-repo dependencies within a workspace and generates a
4
+ * `nexusflow-plan.md` implementation plan with build-order phases.
5
+ */
6
+
7
+ import path from 'node:path';
8
+ import fse from 'fs-extra';
9
+ import chalk from 'chalk';
10
+ import { writeWorkspaceFile } from '../core/storage.js';
11
+ import type {
12
+ WorkspaceContext,
13
+ ProjectAnalysis,
14
+ RepoInfo,
15
+ DependencyNode,
16
+ DependencyGraph,
17
+ } from '../types.js';
18
+
19
+ // ─── Dependency Graph Builder ─────────────────────────────────────────────
20
+
21
+ /**
22
+ * Build a dependency graph by analysing package dependencies
23
+ * across the workspace repos.
24
+ *
25
+ * @param analysis Per-repo analysis results, keyed by repo path.
26
+ * @param repos Metadata for every repo in the workspace.
27
+ * @returns A map of repo name → {@link DependencyNode}.
28
+ */
29
+ export function buildDependencyGraph(
30
+ analysis: Map<string, ProjectAnalysis>,
31
+ repos: RepoInfo[],
32
+ ): DependencyGraph {
33
+ const graph: DependencyGraph = new Map();
34
+
35
+ // ── Initialise a node for each repo ──────────────────────────────────
36
+ for (const repo of repos) {
37
+ graph.set(repo.name, {
38
+ repoName: repo.name,
39
+ repoPath: repo.path,
40
+ dependsOn: [],
41
+ dependedOnBy: [],
42
+ });
43
+ }
44
+
45
+ // Build a quick lookup: repo name → ProjectAnalysis
46
+ const analysisByName = new Map<string, ProjectAnalysis>();
47
+ for (const repo of repos) {
48
+ const a = analysis.get(repo.path);
49
+ if (a) analysisByName.set(repo.name, a);
50
+ }
51
+
52
+ // ── 1. Produced/consumed package dependencies ──────────────────────────
53
+ // Map each produced package name to the repo name that produces it
54
+ const packageToRepo = new Map<string, string>();
55
+ for (const repo of repos) {
56
+ const a = analysisByName.get(repo.name);
57
+ if (!a) continue;
58
+
59
+ // Map the repo name itself as a produced product (for direct matching)
60
+ packageToRepo.set(repo.name.toLowerCase(), repo.name);
61
+
62
+ if (a.produces) {
63
+ for (const product of a.produces) {
64
+ packageToRepo.set(product.name.toLowerCase(), repo.name);
65
+ // Map basename (e.g. Hogia.EmploymentService.Client -> Client)
66
+ const base = product.name.split('.').pop() ?? product.name;
67
+ if (base && base.length > 3) {
68
+ packageToRepo.set(base.toLowerCase(), repo.name);
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ for (const repo of repos) {
75
+ const a = analysisByName.get(repo.name);
76
+ if (!a) continue;
77
+
78
+ for (const dep of a.dependencies) {
79
+ const depNameLower = dep.name.toLowerCase();
80
+
81
+ // Direct match with a produced package
82
+ if (packageToRepo.has(depNameLower)) {
83
+ const targetRepo = packageToRepo.get(depNameLower)!;
84
+ if (targetRepo !== repo.name) {
85
+ addEdge(graph, repo.name, targetRepo);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ return graph;
92
+ }
93
+
94
+ // ─── Topological Sort ─────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Topologically sort the dependency graph into build phases.
98
+ * Each phase is a group of repos that can be built in parallel
99
+ * because all of their dependencies appear in earlier phases.
100
+ *
101
+ * If a cycle is detected, the remaining nodes are placed in a final phase
102
+ * with a warning logged to the console.
103
+ *
104
+ * @param graph The workspace dependency graph.
105
+ * @returns An array of phases, where each phase is an array of repo names.
106
+ */
107
+ export function topologicalSort(graph: DependencyGraph): string[][] {
108
+ // Calculate in-degrees
109
+ const inDegree = new Map<string, number>();
110
+ for (const [name, node] of graph) {
111
+ inDegree.set(name, node.dependsOn.length);
112
+ }
113
+
114
+ const phases: string[][] = [];
115
+ const placed = new Set<string>();
116
+
117
+ while (placed.size < graph.size) {
118
+ // Collect nodes whose in-degree is 0 and haven't been placed yet
119
+ const phase: string[] = [];
120
+ for (const [name, degree] of inDegree) {
121
+ if (degree === 0 && !placed.has(name)) {
122
+ phase.push(name);
123
+ }
124
+ }
125
+
126
+ // Cycle detection no zero-in-degree nodes remain
127
+ if (phase.length === 0) {
128
+ const remaining = [...graph.keys()].filter((n) => !placed.has(n));
129
+ console.log(
130
+ chalk.yellow(''),
131
+ `Dependency cycle detected among: ${remaining.join(', ')}`,
132
+ );
133
+ phases.push(remaining);
134
+ break;
135
+ }
136
+
137
+ phase.sort(); // Deterministic ordering within a phase
138
+ phases.push(phase);
139
+
140
+ // "Remove" placed nodes and decrement dependents' in-degrees
141
+ for (const name of phase) {
142
+ placed.add(name);
143
+ const node = graph.get(name)!;
144
+ for (const dependent of node.dependedOnBy) {
145
+ inDegree.set(dependent, (inDegree.get(dependent) ?? 1) - 1);
146
+ }
147
+ }
148
+ }
149
+
150
+ return phases;
151
+ }
152
+
153
+ // ─── Plan Generator ───────────────────────────────────────────────────────
154
+
155
+ /**
156
+ * Generate a `nexusflow-plan.md` implementation plan for the workspace.
157
+ *
158
+ * The plan includes:
159
+ * - A Mermaid dependency diagram
160
+ * - Phased implementation order derived from topological sort
161
+ * - A dependency cross-reference table
162
+ * - A package relations table
163
+ * - Actionable local dev tips
164
+ *
165
+ * @param ctx The current workspace context (feature + repos + analysis).
166
+ * @param workspacePath Absolute path to the workspace root directory.
167
+ */
168
+ export async function generateImplementationPlan(
169
+ ctx: WorkspaceContext,
170
+ workspacePath: string,
171
+ ): Promise<void> {
172
+ try {
173
+ const { feature, repos, analysis } = ctx;
174
+
175
+ // ── Fallback: no analysis available ─────────────────────────────────
176
+ if (!analysis || analysis.size === 0) {
177
+ const lines = [
178
+ `# Implementation Plan — ${feature.id}`,
179
+ '',
180
+ '> Auto-generated by NexusFlow.',
181
+ '> No project analysis data was available, so repos are listed alphabetically.',
182
+ '',
183
+ '## Repos',
184
+ '',
185
+ ...repos
186
+ .map((r) => r.name)
187
+ .sort()
188
+ .map((n) => `- ${n}`),
189
+ '',
190
+ ];
191
+ await writeWorkspaceFile(
192
+ workspacePath,
193
+ feature.id,
194
+ 'nexusflow-plan.md',
195
+ lines.join('\n'),
196
+ );
197
+ console.log(chalk.green(' ✔'), 'Generated nexusflow-plan.md');
198
+ return;
199
+ }
200
+
201
+ // ── Build graph & sort ──────────────────────────────────────────────
202
+ const graph = buildDependencyGraph(analysis, repos);
203
+ const phases = topologicalSort(graph);
204
+
205
+ // ── Render markdown ─────────────────────────────────────────────────
206
+ const md: string[] = [];
207
+
208
+ md.push(`# Implementation Plan ${feature.id}`);
209
+ md.push('');
210
+ md.push(`> **Generated At**: ${new Date().toISOString()} (UTC)`);
211
+ md.push(`> **Regeneration Command**: Run \`nexusflow refresh\` to update this plan.`);
212
+ md.push('');
213
+ md.push(
214
+ '> Auto-generated by NexusFlow based on dependency analysis between repos.',
215
+ );
216
+ md.push(
217
+ '> Follow the phase order to avoid blocking yourself on cross-repo dependencies.',
218
+ );
219
+ md.push('');
220
+
221
+ // ── Mermaid diagram ─────────────────────────────────────────────────
222
+ md.push('## Dependency Diagram');
223
+ md.push('');
224
+ md.push('```mermaid');
225
+ md.push('graph TD');
226
+
227
+ const alias = buildAliasMap(graph);
228
+
229
+ for (const [name, node] of graph) {
230
+ if (node.dependsOn.length === 0 && node.dependedOnBy.length === 0) {
231
+ // Isolated node still show it
232
+ md.push(` ${alias.get(name)}["${name}"]`);
233
+ }
234
+ for (const dep of node.dependsOn) {
235
+ // Arrow: dependency → dependent (dep is built first)
236
+ md.push(
237
+ ` ${alias.get(dep)}["${dep}"] --> ${alias.get(name)}["${name}"]`,
238
+ );
239
+ }
240
+ }
241
+
242
+ md.push('```');
243
+ md.push('');
244
+ md.push('> ⚠️ This diagram is derived from detected package dependencies (`package.json`, `.csproj`, etc.) only.');
245
+ md.push('> If you changed a package, the producing repo must release/build before consumer repos can merge.');
246
+ md.push('');
247
+
248
+ // ── Phase descriptions ──────────────────────────────────────────────
249
+ md.push('## Suggested Implementation Order');
250
+ md.push('');
251
+
252
+ for (let i = 0; i < phases.length; i++) {
253
+ const phase = phases[i];
254
+ const ordinal = ordinalWord(i + 1);
255
+
256
+ md.push(`### Phase ${i + 1}`);
257
+ md.push('');
258
+ md.push(`**Repos:** ${phase.join(', ')}`);
259
+ md.push('');
260
+
261
+ if (i === 0) {
262
+ md.push(
263
+ `**Why first:** These repos have no dependencies on other workspace repos. Other repos depend on them.`,
264
+ );
265
+ } else if (i === phases.length - 1) {
266
+ md.push(
267
+ `**Why ${ordinal}:** Depends on APIs and types from earlier phases.`,
268
+ );
269
+ } else {
270
+ const prevPhases = phases
271
+ .slice(0, i)
272
+ .flat()
273
+ .join(', ');
274
+ md.push(
275
+ `**Why ${ordinal}:** Depends on Phase ${i === 1 ? '1' : `1–${i}`} repos (${prevPhases}). Build these before the consumers.`,
276
+ );
277
+ }
278
+
279
+ md.push('');
280
+ }
281
+
282
+ // ── Dependency table ────────────────────────────────────────────────
283
+ md.push('## Dependency Table');
284
+ md.push('');
285
+ md.push('| Repo | Depends On | Depended On By |');
286
+ md.push('|:---|:---|:---|');
287
+
288
+ // Sort repos by phase order for a natural reading experience
289
+ const orderedNames = phases.flat();
290
+ for (const name of orderedNames) {
291
+ const node = graph.get(name)!;
292
+ const deps = node.dependsOn.length > 0 ? node.dependsOn.join(', ') : '—';
293
+ const rdeps =
294
+ node.dependedOnBy.length > 0 ? node.dependedOnBy.join(', ') : '—';
295
+ md.push(`| ${name} | ${deps} | ${rdeps} |`);
296
+ }
297
+
298
+ md.push('');
299
+
300
+ // ── Contracts & Clients Table ───────────────────────────────────────
301
+ md.push('## 📦 Contracts & Clients');
302
+ md.push('');
303
+ md.push('| Package | Contributing Projects | Producing Repo | Consuming Repos (Version) | Feed Source | Type |');
304
+ md.push('|:---|:---|:---|:---|:---|:---|');
305
+
306
+ // Build package relations
307
+ interface PackageRelation {
308
+ pkgName: string;
309
+ contributing?: string[];
310
+ producer: string;
311
+ consumers: { repoName: string; version?: string }[];
312
+ type: 'npm' | 'nuget' | 'other';
313
+ feeds?: { name: string; url: string }[];
314
+ }
315
+
316
+ const packageRelations: PackageRelation[] = [];
317
+
318
+ // Find all produced packages
319
+ for (const [repoPath, a] of analysis) {
320
+ if (a.produces) {
321
+ for (const product of a.produces) {
322
+ // Find consumers
323
+ const consumers: { repoName: string; version?: string }[] = [];
324
+ for (const [otherPath, otherA] of analysis) {
325
+ if (otherPath === repoPath) continue;
326
+ for (const dep of otherA.dependencies) {
327
+ if (dep.name.toLowerCase() === product.name.toLowerCase()) {
328
+ consumers.push({ repoName: otherA.name, version: dep.version });
329
+ }
330
+ }
331
+ }
332
+ packageRelations.push({
333
+ pkgName: product.name,
334
+ contributing: (product as any).contributing,
335
+ producer: a.name,
336
+ consumers,
337
+ type: product.type,
338
+ feeds: a.nugetFeeds,
339
+ });
340
+ }
341
+ }
342
+ }
343
+
344
+ if (packageRelations.length > 0) {
345
+ for (const rel of packageRelations) {
346
+ const contribStr = rel.contributing && rel.contributing.length > 0
347
+ ? rel.contributing.map(c => `\`${c}\``).join(', ')
348
+ : '';
349
+ const consumerStr = rel.consumers.length > 0
350
+ ? rel.consumers.map(c => `\`${c.repoName}\` (${c.version || 'pinned'})`).join(', ')
351
+ : '_None_';
352
+ const feedStr = rel.feeds && rel.feeds.length > 0
353
+ ? rel.feeds.map(f => `\`${f.name}\` (${f.url})`).join('<br>')
354
+ : '—';
355
+ md.push(`| \`${rel.pkgName}\` | ${contribStr} | \`${rel.producer}\` | ${consumerStr} | ${feedStr} | \`${rel.type}\` |`);
356
+ }
357
+ } else {
358
+ md.push('| _No package relations detected_ | | | | | |');
359
+ }
360
+ md.push('');
361
+
362
+ // ── Cross-Repo Messaging Roll-up ────────────────────────────────────
363
+ md.push('## 📨 Cross-Repo Messaging');
364
+ md.push('');
365
+ md.push('| Publisher Repo | Message | → Subscriber Repo | Handler |');
366
+ md.push('|---|---|---|---|');
367
+
368
+ interface CrossRepoMessage {
369
+ pubRepo: string;
370
+ message: string;
371
+ subRepo: string;
372
+ handler: string;
373
+ }
374
+ const crossRepoMessages: CrossRepoMessage[] = [];
375
+
376
+ for (const [pubPath, pubA] of analysis) {
377
+ if (!pubA.messaging || !pubA.messaging.publishers) continue;
378
+ for (const pub of pubA.messaging.publishers) {
379
+ // Find subscribers in other repos matching this contract type
380
+ for (const [subPath, subA] of analysis) {
381
+ if (subPath === pubPath) continue;
382
+ if (!subA.messaging || !subA.messaging.subscribers) continue;
383
+ for (const sub of subA.messaging.subscribers) {
384
+ const pubContract = pub.contractType.toLowerCase().trim();
385
+ const subContract = sub.contractType.toLowerCase().trim();
386
+ if (pubContract === subContract && pubContract !== 'goservicebusmessage' && pubContract !== 'servicebusmessage') {
387
+ crossRepoMessages.push({
388
+ pubRepo: pubA.name,
389
+ message: pub.contractType,
390
+ subRepo: subA.name,
391
+ handler: sub.handlerFile,
392
+ });
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+
399
+ if (crossRepoMessages.length > 0) {
400
+ for (const m of crossRepoMessages) {
401
+ md.push(`| \`${m.pubRepo}\` | \`${m.message}\` | \`${m.subRepo}\` | \`${m.handler}\` |`);
402
+ }
403
+ } else {
404
+ md.push('| _No cross-repo messaging detected_ | | | |');
405
+ }
406
+ md.push('');
407
+
408
+ // ── Local Package Development Loop Tip ──────────────────────────────
409
+ md.push('## 💡 Local Package Development Loop');
410
+ md.push('');
411
+ md.push('When making changes to a shared contract or client library package, follow this standard local feed loop to test and verify consumers before pushing:');
412
+ md.push('');
413
+ md.push('### For .NET / NuGet packages:');
414
+ md.push('1. **Pack locally**: Run `dotnet pack -c Release -o ./local-packages` inside the producing project folder.');
415
+ md.push('2. **Add local feed**: Configure a local feed in your consumer project\'s `NuGet.config` pointing to the `./local-packages` directory.');
416
+ md.push('3. **Reference local version**: Reference the package with a local development version (e.g. `3.41.0-local`) in the consuming `.csproj`.');
417
+ md.push('4. **Revert before merging**: Verify changes compile and tests pass, then **revert** the consuming project\'s package version reference to the official release before merging to master.');
418
+ md.push('');
419
+ md.push('### For Node.js / npm packages:');
420
+ md.push('1. **Link locally**: Run `npm link` inside the producing package folder.');
421
+ md.push('2. **Use link**: Run `npm link <package-name>` inside the consuming folder to link it.');
422
+ md.push('3. **Revert before merging**: Uninstall the linked package and install the official package version before committing.');
423
+ md.push('');
424
+
425
+ // ── Write file ──────────────────────────────────────────────────────
426
+ await writeWorkspaceFile(workspacePath, feature.id, 'nexusflow-plan.md', md.join('\n'));
427
+ console.log(chalk.green(' ✔'), 'Generated nexusflow-plan.md');
428
+ } catch (error) {
429
+ const message = error instanceof Error ? error.message : String(error);
430
+ console.error(
431
+ chalk.red(' ✖'),
432
+ `Failed to generate implementation plan: ${message}`,
433
+ );
434
+ }
435
+ }
436
+
437
+ // ─── Helpers ──────────────────────────────────────────────────────────────
438
+
439
+ /**
440
+ * Add a directed edge: `from` depends on `to`.
441
+ * Idempotent duplicate edges are ignored.
442
+ */
443
+ function addEdge(graph: DependencyGraph, from: string, to: string): void {
444
+ const fromNode = graph.get(from);
445
+ const toNode = graph.get(to);
446
+ if (!fromNode || !toNode) return;
447
+
448
+ if (!fromNode.dependsOn.includes(to)) {
449
+ fromNode.dependsOn.push(to);
450
+ }
451
+ if (!toNode.dependedOnBy.includes(from)) {
452
+ toNode.dependedOnBy.push(from);
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Build a short single-letter alias map for Mermaid node IDs.
458
+ * Falls back to sanitised names when there are more than 26 repos.
459
+ */
460
+ function buildAliasMap(graph: DependencyGraph): Map<string, string> {
461
+ const map = new Map<string, string>();
462
+ const names = [...graph.keys()].sort();
463
+
464
+ if (names.length <= 26) {
465
+ let code = 65; // 'A'
466
+ for (const name of names) {
467
+ map.set(name, String.fromCharCode(code++));
468
+ }
469
+ } else {
470
+ for (const name of names) {
471
+ map.set(name, name.replace(/[^a-zA-Z0-9]/g, '_'));
472
+ }
473
+ }
474
+
475
+ return map;
476
+ }
477
+
478
+ /** Return an ordinal word for small numbers, or "nth" for larger ones. */
479
+ function ordinalWord(n: number): string {
480
+ const words = ['first', 'second', 'third', 'fourth', 'fifth'];
481
+ if (n >= 1 && n <= words.length) return words[n - 1];
482
+ return `${n}th`;
483
+ }