@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,424 +1,484 @@
1
- /**
2
- * @module commands/tui
3
- * Fully interactive Terminal User Interface (TUI) for NexusFlow workspaces.
4
- */
5
-
6
- import readline from 'node:readline';
7
- import path from 'node:path';
8
- import fs from 'node:fs/promises';
9
- import { fileURLToPath } from 'node:url';
10
- import chalk from 'chalk';
11
- import { execa } from 'execa';
12
-
13
- import { loadConfig } from '../core/config.js';
14
- import { loadFeatureConfig, listWorkspaces } from '../core/workspace.js';
15
- import { getWorkspaceRepos, getRepoStatus, type WorkspaceRepo, type RepoStatus } from '../utils/multi-git.js';
16
-
17
- const COMMANDS = ['sync', 'doctor', 'refresh', 'status', 'start', 'stop', 'logs', 'list', 'create', 'open', 'pack', 'remove', 'add-repo', 'help'];
18
-
19
- interface TuiState {
20
- workspacePath: string;
21
- branchName: string;
22
- repos: WorkspaceRepo[];
23
- repoStatuses: Record<string, RepoStatus>;
24
- consoleLogs: string[];
25
- inputMode: boolean;
26
- inputValue: string;
27
- cursorIndex: number;
28
- activeCommandRunning: boolean;
29
- activeSuggestionIndex: number;
30
- }
31
-
32
- export async function tuiCommand(options: { workspace?: string }): Promise<void> {
33
- // Resolve active workspace
34
- let workspacePath = options.workspace ? path.resolve(options.workspace) : '';
35
- let branchName = 'Unknown';
36
-
37
- if (!workspacePath) {
38
- const cwdFeature = await loadFeatureConfig(process.cwd());
39
- if (cwdFeature) {
40
- workspacePath = cwdFeature.workspacePath;
41
- branchName = cwdFeature.branchName;
42
- } else {
43
- const config = await loadConfig();
44
- const workspaces = await listWorkspaces(config.workspacesDir);
45
- if (workspaces.length === 0) {
46
- console.log(chalk.yellow('\nNo workspaces found. Run "nexusflow create" to initialize one.\n'));
47
- return;
48
- }
49
- // Use the first active workspace as default
50
- workspacePath = workspaces[0].workspacePath;
51
- branchName = workspaces[0].branchName;
52
- }
53
- }
54
-
55
- // Load repositories inside workspace
56
- let repos: WorkspaceRepo[] = [];
57
- try {
58
- repos = await getWorkspaceRepos(workspacePath);
59
- } catch {
60
- console.log(chalk.red(`\nCould not resolve repos. Make sure nexusflow.json exists in ${workspacePath}\n`));
61
- return;
62
- }
63
-
64
- // Initial State Setup
65
- const state: TuiState = {
66
- workspacePath,
67
- branchName,
68
- repos,
69
- repoStatuses: {},
70
- consoleLogs: [
71
- 'NexusFlow TUI Command Center v2.0 - Initialized Successfully.',
72
- 'Connecting workspace streams...',
73
- `Active Workspace Path: ${workspacePath}`,
74
- 'Ready. Press [/] to type commands, or [s] to sync, [d] to run diagnostics.',
75
- ],
76
- inputMode: false,
77
- inputValue: '',
78
- cursorIndex: 0,
79
- activeCommandRunning: false,
80
- activeSuggestionIndex: 0,
81
- };
82
-
83
- // Helper to append log lines
84
- const log = (msg: string) => {
85
- state.consoleLogs.push(msg);
86
- if (state.consoleLogs.length > 50) {
87
- state.consoleLogs.shift();
88
- }
89
- };
90
-
91
- // Fetch Git status for all worktrees
92
- const updateGitStatus = async () => {
93
- for (const repo of state.repos) {
94
- const status = await getRepoStatus(repo.path);
95
- state.repoStatuses[repo.name] = status;
96
- }
97
- };
98
-
99
- // Run initial git fetch
100
- await updateGitStatus();
101
-
102
- // Enter Full Screen Screen Buffer
103
- process.stdout.write('\x1B[?1049h'); // Switch to alternate screen buffer
104
- process.stdout.write('\x1B[?25l'); // Hide cursor
105
-
106
- // Setup raw terminal mode for key capture
107
- readline.emitKeypressEvents(process.stdin);
108
- if (process.stdin.isTTY) {
109
- process.stdin.setRawMode(true);
110
- }
111
-
112
- // Drawing Screen Logic
113
- const draw = () => {
114
- const cols = process.stdout.columns || 80;
115
- const rows = process.stdout.rows || 24;
116
-
117
- // Clear Screen Buffer
118
- process.stdout.write('\x1B[H');
119
-
120
- // Row 0: Top Header Banner
121
- const bannerTitle = ` ⚔ NexusFlow Workspace Console [🌿 ${state.branchName}] `;
122
- const statusText = ` SYSTEM: ONLINE (Port 3000) `;
123
- const headerFill = cols - bannerTitle.length - statusText.length - 2;
124
- process.stdout.write(
125
- chalk.bgCyan.black(bannerTitle) +
126
- chalk.bgGray.white('─'.repeat(Math.max(0, headerFill))) +
127
- chalk.bgGreen.black(statusText) +
128
- '\n'
129
- );
130
-
131
- // Grid Panel Calculations
132
- const leftWidth = Math.floor(cols * 0.35);
133
- const rightWidth = cols - leftWidth - 1;
134
- const mainHeight = rows - (state.inputMode ? 14 : 13); // Adjust height when suggestion line is visible
135
-
136
- // Left Panel contents array
137
- const leftLines: string[] = [];
138
- leftLines.push(chalk.cyan.bold(' šŸ“‚ Repositories & Worktrees '));
139
- leftLines.push(' '.repeat(leftWidth));
140
-
141
- for (const repo of state.repos) {
142
- const status = state.repoStatuses[repo.name];
143
- const statusIndicator = status?.hasChanges
144
- ? chalk.yellow(`(${status.changedFiles.length} files modified)`)
145
- : chalk.green('(clean)');
146
- leftLines.push(` 🌿 ${chalk.bold(repo.name)} ${statusIndicator}`);
147
-
148
- // Changed files list
149
- if (status?.hasChanges) {
150
- status.changedFiles.slice(0, 4).forEach((file) => {
151
- leftLines.push(` ${chalk.yellow('[M]')} ${path.basename(file)}`);
152
- });
153
- if (status.changedFiles.length > 4) {
154
- leftLines.push(` ... and ${status.changedFiles.length - 4} more`);
155
- }
156
- }
157
- leftLines.push(' '.repeat(leftWidth));
158
- }
159
-
160
- // Right Panel contents array
161
- const rightLines: string[] = [];
162
- rightLines.push(chalk.cyan.bold(' šŸ¤– Model Context Protocol (MCP) & Status '));
163
- rightLines.push(' '.repeat(rightWidth));
164
- rightLines.push(` ${chalk.bold('MCP Server:')} nexusflow-mcp (State: ${chalk.green('Active')})`);
165
- rightLines.push(' Registered tools:');
166
- rightLines.push(` āš™ ${chalk.cyan('search_workspace')} Search workspace files fast`);
167
- rightLines.push(` āš™ ${chalk.cyan('get_service_logs')} Tail running logs`);
168
- rightLines.push(` āš™ ${chalk.cyan('delegate_to_local_agent')} Run local LLM diagnostics`);
169
- rightLines.push(' '.repeat(rightWidth));
170
- rightLines.push(chalk.cyan.bold(' 🧩 Custom Agent Skills '));
171
- rightLines.push(` • ${chalk.bold('dotnet-local-pack')} - Local Nuget compiler helper`);
172
- rightLines.push(` • ${chalk.bold('npm-link-helper')} - Automated worktree registry link`);
173
- rightLines.push(` • ${chalk.bold('antigravity-guide')} - Agent instruction manual`);
174
- rightLines.push(' '.repeat(rightWidth));
175
- rightLines.push(` ${chalk.bold('Health Scan:')} 🩺 doctor diagnostics: ${chalk.green('OK (0 warnings)')}`);
176
-
177
- // Render Middle Grid Rows
178
- for (let r = 0; r < mainHeight; r++) {
179
- const leftPart = leftLines[r] || '';
180
- const rightPart = rightLines[r] || '';
181
-
182
- const leftFilled = leftPart + ' '.repeat(Math.max(0, leftWidth - stripAnsi(leftPart).length));
183
- const rightFilled = rightPart + ' '.repeat(Math.max(0, rightWidth - stripAnsi(rightPart).length));
184
-
185
- process.stdout.write(leftFilled + chalk.gray('│') + rightFilled + '\n');
186
- }
187
-
188
- // Middle Horizontal Divider
189
- process.stdout.write(
190
- chalk.gray('ā”œ' + '─'.repeat(leftWidth) + '┼' + '─'.repeat(rightWidth) + '┤') + '\n'
191
- );
192
-
193
- // Logs Panel Header
194
- process.stdout.write(chalk.cyan.bold(' šŸ–„ļø Command Output Stream & Logs ') + '\n');
195
-
196
- // Render Logs Stream (Tails last 8 lines)
197
- const logHeight = 8;
198
- const startIdx = Math.max(0, state.consoleLogs.length - logHeight);
199
- const visibleLogs = state.consoleLogs.slice(startIdx, startIdx + logHeight);
200
-
201
- for (let i = 0; i < logHeight; i++) {
202
- const line = visibleLogs[i] || '';
203
- const filledLine = line + ' '.repeat(Math.max(0, cols - stripAnsi(line).length));
204
- process.stdout.write(filledLine + '\n');
205
- }
206
-
207
- // Bottom Divider
208
- process.stdout.write(chalk.gray('─'.repeat(cols)) + '\n');
209
-
210
- // Bottom Panel Prompt
211
- if (state.inputMode) {
212
- // Find matches
213
- const cmd = state.inputValue.trim().toLowerCase();
214
- const matches = COMMANDS.filter(c => c.startsWith(cmd));
215
-
216
- // Keep active index in bounds
217
- if (matches.length > 0) {
218
- state.activeSuggestionIndex = (state.activeSuggestionIndex + matches.length) % matches.length;
219
- } else {
220
- state.activeSuggestionIndex = 0;
221
- }
222
-
223
- let suggestionLine = '';
224
- if (matches.length > 0) {
225
- suggestionLine = chalk.dim(' Suggestions: ') + matches.map((m, idx) => {
226
- if (idx === state.activeSuggestionIndex) return chalk.black.bgCyan(` ${m} `); // Highlight active selection
227
- return chalk.cyan(m);
228
- }).join(' ');
229
- } else {
230
- suggestionLine = chalk.red(' No matching commands.');
231
- }
232
-
233
- const filledSuggestion = suggestionLine + ' '.repeat(Math.max(0, cols - stripAnsi(suggestionLine).length));
234
- process.stdout.write(filledSuggestion + '\n');
235
-
236
- // Focus Input mode
237
- const promptText = ` nexusflow > ${state.inputValue}`;
238
- process.stdout.write(chalk.bold.cyan(promptText) + ' '.repeat(Math.max(0, cols - promptText.length - 12)) + chalk.dim('[ESC] Cancel') + '\n');
239
- } else {
240
- // General Keyboard Command mode
241
- const controls = ' [s] Sync | [d] Doctor | [r] Refresh | [/] CLI Cmd | [q] Exit ';
242
- process.stdout.write(chalk.bgCyan.black(controls) + ' '.repeat(Math.max(0, cols - controls.length)) + '\n');
243
- }
244
- };
245
-
246
- // Strip ANSI color characters to calculate line lengths precisely
247
- const stripAnsi = (str: string) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
248
-
249
- // Render loop refresh
250
- draw();
251
-
252
- // Resize Listener
253
- const resizeHandler = () => {
254
- draw();
255
- };
256
- process.stdout.on('resize', resizeHandler);
257
-
258
- // Status Polling Loop
259
- const pollInterval = setInterval(async () => {
260
- await updateGitStatus();
261
- if (!state.inputMode && !state.activeCommandRunning) {
262
- draw();
263
- }
264
- }, 5000);
265
-
266
- // Exit Cleanup
267
- const cleanup = () => {
268
- clearInterval(pollInterval);
269
- process.stdout.off('resize', resizeHandler);
270
- process.stdout.write('\x1B[?1049l'); // Restore screen buffer
271
- process.stdout.write('\x1B[?25h'); // Show cursor
272
- if (process.stdin.isTTY) {
273
- process.stdin.setRawMode(false);
274
- }
275
- };
276
-
277
- // Keyboard Event Handlers
278
- process.stdin.on('keypress', async (str, key) => {
279
- if (key.ctrl && key.name === 'c') {
280
- cleanup();
281
- process.exit(0);
282
- }
283
-
284
- if (state.inputMode) {
285
- // --- Handle input text writing mode ---
286
- if (key.name === 'return') {
287
- let cmd = state.inputValue.trim();
288
- const matches = COMMANDS.filter(c => c.startsWith(cmd.toLowerCase()));
289
- if (matches.length > 0) {
290
- cmd = matches[state.activeSuggestionIndex % matches.length];
291
- }
292
-
293
- state.inputMode = false;
294
- state.inputValue = '';
295
- state.activeSuggestionIndex = 0;
296
-
297
- if (cmd) {
298
- state.activeCommandRunning = true;
299
- log(`Running: nexusflow ${cmd}...`);
300
- draw();
301
-
302
- try {
303
- // Run command via execa
304
- const script = fileURLToPath(new URL('../index.js', import.meta.url));
305
- const args = cmd.split(' ');
306
- const { stdout, stderr } = await execa('node', [script, ...args], { cwd: workspacePath });
307
-
308
- if (stdout) {
309
- stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
310
- }
311
- if (stderr) {
312
- stderr.split('\n').filter(Boolean).forEach((l) => log(` ${chalk.red(l)}`));
313
- }
314
- log(chalk.green('āœ” Command completed successfully.'));
315
- } catch (err: any) {
316
- log(chalk.red(`āœ– Command failed: ${err.message}`));
317
- } finally {
318
- state.activeCommandRunning = false;
319
- await updateGitStatus();
320
- draw();
321
- }
322
- } else {
323
- draw();
324
- }
325
- } else if (key.name === 'escape') {
326
- state.inputMode = false;
327
- state.inputValue = '';
328
- draw();
329
- } else if (key.name === 'tab') {
330
- const cmd = state.inputValue.trim().toLowerCase();
331
- const matches = COMMANDS.filter(c => c.startsWith(cmd));
332
- if (matches.length > 0) {
333
- const selected = matches[state.activeSuggestionIndex % matches.length];
334
- state.inputValue = selected + ' ';
335
- state.activeSuggestionIndex = 0;
336
- draw();
337
- }
338
- } else if (key.name === 'right' || key.name === 'down') {
339
- const cmd = state.inputValue.trim().toLowerCase();
340
- const matches = COMMANDS.filter(c => c.startsWith(cmd));
341
- if (matches.length > 0) {
342
- state.activeSuggestionIndex = (state.activeSuggestionIndex + 1) % matches.length;
343
- draw();
344
- }
345
- } else if (key.name === 'left' || key.name === 'up') {
346
- const cmd = state.inputValue.trim().toLowerCase();
347
- const matches = COMMANDS.filter(c => c.startsWith(cmd));
348
- if (matches.length > 0) {
349
- state.activeSuggestionIndex = (state.activeSuggestionIndex - 1 + matches.length) % matches.length;
350
- draw();
351
- }
352
- } else if (key.name === 'backspace') {
353
- state.inputValue = state.inputValue.slice(0, -1);
354
- state.activeSuggestionIndex = 0;
355
- draw();
356
- } else if (str && str.length === 1 && !key.ctrl && !key.meta) {
357
- state.inputValue += str;
358
- state.activeSuggestionIndex = 0;
359
- draw();
360
- }
361
- } else {
362
- // --- Handle single key shortcuts mode ---
363
- if (key.name === 'q') {
364
- cleanup();
365
- console.log(chalk.bold.cyan('\nšŸ‘‹ Exited NexusFlow TUI Dashboard.\n'));
366
- process.exit(0);
367
- } else if (str === '/') {
368
- state.inputMode = true;
369
- draw();
370
- } else if (key.name === 's') {
371
- state.activeCommandRunning = true;
372
- log('šŸ”„ Running Workspace Sync rebase...');
373
- draw();
374
-
375
- try {
376
- const script = fileURLToPath(new URL('../index.js', import.meta.url));
377
- const { stdout } = await execa('node', [script, 'sync'], { cwd: workspacePath });
378
- stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
379
- log(chalk.green('āœ” Sync completed successfully.'));
380
- } catch (err: any) {
381
- log(chalk.red(`āœ– Sync failed: ${err.message}`));
382
- } finally {
383
- state.activeCommandRunning = false;
384
- await updateGitStatus();
385
- draw();
386
- }
387
- } else if (key.name === 'd') {
388
- state.activeCommandRunning = true;
389
- log('🩺 Running doctor diagnostics...');
390
- draw();
391
-
392
- try {
393
- const script = fileURLToPath(new URL('../index.js', import.meta.url));
394
- const { stdout } = await execa('node', [script, 'doctor'], { cwd: workspacePath });
395
- stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
396
- log(chalk.green('āœ” Diagnostics check complete.'));
397
- } catch (err: any) {
398
- log(chalk.red(`āœ– Diagnostics failed: ${err.message}`));
399
- } finally {
400
- state.activeCommandRunning = false;
401
- await updateGitStatus();
402
- draw();
403
- }
404
- } else if (key.name === 'r') {
405
- state.activeCommandRunning = true;
406
- log('šŸ”„ Regenerating context manifest files...');
407
- draw();
408
-
409
- try {
410
- const script = fileURLToPath(new URL('../index.js', import.meta.url));
411
- const { stdout } = await execa('node', [script, 'refresh'], { cwd: workspacePath });
412
- stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
413
- log(chalk.green('āœ” Context successfully refreshed.'));
414
- } catch (err: any) {
415
- log(chalk.red(`āœ– Refresh failed: ${err.message}`));
416
- } finally {
417
- state.activeCommandRunning = false;
418
- await updateGitStatus();
419
- draw();
420
- }
421
- }
422
- }
423
- });
424
- }
1
+ /**
2
+ * @module commands/tui
3
+ * Fully interactive Terminal User Interface (TUI) for NexusFlow workspaces.
4
+ */
5
+
6
+ import readline from 'node:readline';
7
+ import path from 'node:path';
8
+ import fs from 'node:fs/promises';
9
+ import { fileURLToPath } from 'node:url';
10
+ import chalk from 'chalk';
11
+ import { execa } from 'execa';
12
+
13
+ import { loadConfig } from '../core/config.js';
14
+ import { loadFeatureConfig, listWorkspaces } from '../core/workspace.js';
15
+ import { getWorkspaceRepos, getRepoStatus, type WorkspaceRepo, type RepoStatus } from '../utils/multi-git.js';
16
+
17
+ const COMMANDS = ['sync', 'doctor', 'refresh', 'status', 'start', 'stop', 'logs', 'list', 'create', 'open', 'pack', 'remove', 'add-repo', 'help'];
18
+
19
+ interface TuiState {
20
+ workspacePath: string;
21
+ branchName: string;
22
+ repos: WorkspaceRepo[];
23
+ repoStatuses: Record<string, RepoStatus>;
24
+ consoleLogs: string[];
25
+ inputMode: boolean;
26
+ inputValue: string;
27
+ cursorIndex: number;
28
+ activeCommandRunning: boolean;
29
+ activeSuggestionIndex: number;
30
+ }
31
+
32
+ export async function tuiCommand(options: { workspace?: string }): Promise<void> {
33
+ // Resolve active workspace
34
+ let workspacePath = options.workspace ? path.resolve(options.workspace) : '';
35
+ let branchName = 'Unknown';
36
+
37
+ if (!workspacePath) {
38
+ const cwdFeature = await loadFeatureConfig(process.cwd());
39
+ if (cwdFeature) {
40
+ workspacePath = cwdFeature.workspacePath;
41
+ branchName = cwdFeature.branchName;
42
+ } else {
43
+ const config = await loadConfig();
44
+ const workspaces = await listWorkspaces(config.workspacesDir);
45
+ if (workspaces.length === 0) {
46
+ console.log(chalk.yellow('\nNo workspaces found. Run "nexusflow create" to initialize one.\n'));
47
+ return;
48
+ }
49
+ // Use the first active workspace as default
50
+ workspacePath = workspaces[0].workspacePath;
51
+ branchName = workspaces[0].branchName;
52
+ }
53
+ }
54
+
55
+ // Load repositories inside workspace
56
+ let repos: WorkspaceRepo[] = [];
57
+ try {
58
+ repos = await getWorkspaceRepos(workspacePath);
59
+ } catch {
60
+ console.log(chalk.red(`\nCould not resolve repos. Make sure nexusflow.json exists in ${workspacePath}\n`));
61
+ return;
62
+ }
63
+
64
+ // Initial State Setup
65
+ const state: TuiState = {
66
+ workspacePath,
67
+ branchName,
68
+ repos,
69
+ repoStatuses: {},
70
+ consoleLogs: [
71
+ 'NexusFlow TUI Command Center v2.0 - Initialized Successfully.',
72
+ 'Connecting workspace streams...',
73
+ `Active Workspace Path: ${workspacePath}`,
74
+ 'Ready. Press [/] to type commands, or [s] to sync, [d] to run diagnostics.',
75
+ ],
76
+ inputMode: false,
77
+ inputValue: '',
78
+ cursorIndex: 0,
79
+ activeCommandRunning: false,
80
+ activeSuggestionIndex: 0,
81
+ };
82
+
83
+ // Helper to append log lines
84
+ const log = (msg: string) => {
85
+ state.consoleLogs.push(msg);
86
+ if (state.consoleLogs.length > 50) {
87
+ state.consoleLogs.shift();
88
+ }
89
+ };
90
+
91
+ // Fetch Git status for all worktrees
92
+ const updateGitStatus = async () => {
93
+ for (const repo of state.repos) {
94
+ const status = await getRepoStatus(repo.path);
95
+ state.repoStatuses[repo.name] = status;
96
+ }
97
+ };
98
+
99
+ // Run initial git fetch
100
+ await updateGitStatus();
101
+
102
+ // Enter Full Screen Screen Buffer
103
+ process.stdout.write('\x1B[?1049h'); // Switch to alternate screen buffer
104
+ process.stdout.write('\x1B[?25l'); // Hide cursor
105
+
106
+ // Setup raw terminal mode for key capture
107
+ readline.emitKeypressEvents(process.stdin);
108
+ if (process.stdin.isTTY) {
109
+ process.stdin.setRawMode(true);
110
+ }
111
+
112
+ // Drawing Screen Logic
113
+ const draw = () => {
114
+ const cols = process.stdout.columns || 80;
115
+ const rows = process.stdout.rows || 24;
116
+
117
+ // Clear Screen Buffer
118
+ process.stdout.write('\x1B[H');
119
+
120
+ // Row 0: Top Header Banner
121
+ const bannerTitle = ` ⚔ NexusFlow Workspace Console [🌿 ${state.branchName}] `;
122
+ const statusText = ` SYSTEM: ONLINE (Port 3000) `;
123
+ const headerFill = cols - bannerTitle.length - statusText.length - 2;
124
+ process.stdout.write(
125
+ chalk.bgCyan.black(bannerTitle) +
126
+ chalk.bgGray.white('─'.repeat(Math.max(0, headerFill))) +
127
+ chalk.bgGreen.black(statusText) +
128
+ '\n'
129
+ );
130
+
131
+ // Grid Panel Calculations
132
+ const leftWidth = Math.floor(cols * 0.35);
133
+ const rightWidth = cols - leftWidth - 1;
134
+ const mainHeight = rows - (state.inputMode ? 14 : 13); // Adjust height when suggestion line is visible
135
+
136
+ // Left Panel contents array
137
+ const leftLines: string[] = [];
138
+ leftLines.push(chalk.cyan.bold(' šŸ“‚ Repositories & Worktrees '));
139
+ leftLines.push(' '.repeat(leftWidth));
140
+
141
+ for (const repo of state.repos) {
142
+ const status = state.repoStatuses[repo.name];
143
+ const statusIndicator = status?.hasChanges
144
+ ? chalk.yellow(`(${status.changedFiles.length} files modified)`)
145
+ : chalk.green('(clean)');
146
+ leftLines.push(` 🌿 ${chalk.bold(repo.name)} ${statusIndicator}`);
147
+
148
+ // Changed files list
149
+ if (status?.hasChanges) {
150
+ status.changedFiles.slice(0, 4).forEach((file) => {
151
+ leftLines.push(` ${chalk.yellow('[M]')} ${path.basename(file)}`);
152
+ });
153
+ if (status.changedFiles.length > 4) {
154
+ leftLines.push(` ... and ${status.changedFiles.length - 4} more`);
155
+ }
156
+ }
157
+ leftLines.push(' '.repeat(leftWidth));
158
+ }
159
+
160
+ // Right Panel contents array
161
+ const rightLines: string[] = [];
162
+ rightLines.push(chalk.cyan.bold(' šŸ¤– Model Context Protocol (MCP) & Status '));
163
+ rightLines.push(' '.repeat(rightWidth));
164
+ rightLines.push(` ${chalk.bold('MCP Server:')} nexusflow-mcp (State: ${chalk.green('Active')})`);
165
+ rightLines.push(' Registered tools:');
166
+ rightLines.push(` āš™ ${chalk.cyan('search_workspace')} Search workspace files fast`);
167
+ rightLines.push(` āš™ ${chalk.cyan('get_service_logs')} Tail running logs`);
168
+ rightLines.push(` āš™ ${chalk.cyan('delegate_to_local_agent')} Run local LLM diagnostics`);
169
+ rightLines.push(' '.repeat(rightWidth));
170
+ rightLines.push(chalk.cyan.bold(' 🧩 Custom Agent Skills '));
171
+ rightLines.push(` • ${chalk.bold('dotnet-local-pack')} - Local Nuget compiler helper`);
172
+ rightLines.push(` • ${chalk.bold('npm-link-helper')} - Automated worktree registry link`);
173
+ rightLines.push(` • ${chalk.bold('antigravity-guide')} - Agent instruction manual`);
174
+ rightLines.push(' '.repeat(rightWidth));
175
+ rightLines.push(` ${chalk.bold('Health Scan:')} 🩺 doctor diagnostics: ${chalk.green('OK (0 warnings)')}`);
176
+
177
+ // Render Middle Grid Rows
178
+ for (let r = 0; r < mainHeight; r++) {
179
+ const leftPart = leftLines[r] || '';
180
+ const rightPart = rightLines[r] || '';
181
+
182
+ const leftFilled = leftPart + ' '.repeat(Math.max(0, leftWidth - stripAnsi(leftPart).length));
183
+ const rightFilled = rightPart + ' '.repeat(Math.max(0, rightWidth - stripAnsi(rightPart).length));
184
+
185
+ process.stdout.write(leftFilled + chalk.gray('│') + rightFilled + '\n');
186
+ }
187
+
188
+ // Middle Horizontal Divider
189
+ process.stdout.write(
190
+ chalk.gray('ā”œ' + '─'.repeat(leftWidth) + '┼' + '─'.repeat(rightWidth) + '┤') + '\n'
191
+ );
192
+
193
+ // Logs Panel Header
194
+ process.stdout.write(chalk.cyan.bold(' šŸ–„ļø Command Output Stream & Logs ') + '\n');
195
+
196
+ // Render Logs Stream (Tails last 8 lines)
197
+ const logHeight = 8;
198
+ const startIdx = Math.max(0, state.consoleLogs.length - logHeight);
199
+ const visibleLogs = state.consoleLogs.slice(startIdx, startIdx + logHeight);
200
+
201
+ for (let i = 0; i < logHeight; i++) {
202
+ const line = visibleLogs[i] || '';
203
+ const filledLine = line + ' '.repeat(Math.max(0, cols - stripAnsi(line).length));
204
+ process.stdout.write(filledLine + '\n');
205
+ }
206
+
207
+ // Bottom Divider
208
+ process.stdout.write(chalk.gray('─'.repeat(cols)) + '\n');
209
+
210
+ // Bottom Panel Prompt
211
+ if (state.inputMode) {
212
+ // Find matches
213
+ const cmd = state.inputValue.trim().toLowerCase();
214
+ const matches = COMMANDS.filter(c => c.startsWith(cmd));
215
+
216
+ // Keep active index in bounds
217
+ if (matches.length > 0) {
218
+ state.activeSuggestionIndex = (state.activeSuggestionIndex + matches.length) % matches.length;
219
+ } else {
220
+ state.activeSuggestionIndex = 0;
221
+ }
222
+
223
+ let suggestionLine = '';
224
+ if (matches.length > 0) {
225
+ suggestionLine = chalk.dim(' Suggestions: ') + matches.map((m, idx) => {
226
+ if (idx === state.activeSuggestionIndex) return chalk.black.bgCyan(` ${m} `); // Highlight active selection
227
+ return chalk.cyan(m);
228
+ }).join(' ');
229
+ } else {
230
+ suggestionLine = chalk.red(' No matching commands.');
231
+ }
232
+
233
+ const filledSuggestion = suggestionLine + ' '.repeat(Math.max(0, cols - stripAnsi(suggestionLine).length));
234
+ process.stdout.write(filledSuggestion + '\n');
235
+
236
+ // Focus Input mode
237
+ const promptText = ` nexusflow > ${state.inputValue}`;
238
+ process.stdout.write(chalk.bold.cyan(promptText) + ' '.repeat(Math.max(0, cols - promptText.length - 12)) + chalk.dim('[ESC] Cancel') + '\n');
239
+ } else {
240
+ // General Keyboard Command mode
241
+ const controls = ' [s] Sync | [d] Doctor | [r] Refresh | [/] CLI Cmd | [q] Exit ';
242
+ process.stdout.write(chalk.bgCyan.black(controls) + ' '.repeat(Math.max(0, cols - controls.length)) + '\n');
243
+ }
244
+ };
245
+
246
+ // Strip ANSI color characters to calculate line lengths precisely
247
+ const stripAnsi = (str: string) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
248
+
249
+ // Render loop refresh
250
+ draw();
251
+
252
+ // Resize Listener
253
+ const resizeHandler = () => {
254
+ draw();
255
+ };
256
+ process.stdout.on('resize', resizeHandler);
257
+
258
+ // Status Polling Loop
259
+ let pollInterval = setInterval(async () => {
260
+ await updateGitStatus();
261
+ if (!state.inputMode && !state.activeCommandRunning) {
262
+ draw();
263
+ }
264
+ }, 5000);
265
+
266
+ // Exit Cleanup
267
+ const cleanup = () => {
268
+ clearInterval(pollInterval);
269
+ process.stdout.off('resize', resizeHandler);
270
+ process.stdout.write('\x1B[?1049l'); // Restore screen buffer
271
+ process.stdout.write('\x1B[?25h'); // Show cursor
272
+ if (process.stdin.isTTY) {
273
+ process.stdin.setRawMode(false);
274
+ }
275
+ };
276
+
277
+ // Keyboard Event Handlers
278
+ const keypressHandler = async (str: any, key: any) => {
279
+ if (key.ctrl && key.name === 'c') {
280
+ cleanup();
281
+ process.exit(0);
282
+ }
283
+
284
+ if (state.inputMode) {
285
+ // --- Handle input text writing mode ---
286
+ if (key.name === 'return') {
287
+ let cmd = state.inputValue.trim();
288
+ const matches = COMMANDS.filter(c => c.startsWith(cmd.toLowerCase()));
289
+ if (matches.length > 0) {
290
+ cmd = matches[state.activeSuggestionIndex % matches.length];
291
+ }
292
+
293
+ state.inputMode = false;
294
+ state.inputValue = '';
295
+ state.activeSuggestionIndex = 0;
296
+
297
+ if (cmd) {
298
+ state.activeCommandRunning = true;
299
+ log(`Running: nexusflow ${cmd}...`);
300
+ draw();
301
+
302
+ try {
303
+ const script = fileURLToPath(new URL('../index.js', import.meta.url));
304
+ const args = cmd.split(' ');
305
+
306
+ // Check if it's an interactive command
307
+ const INTERACTIVE_COMMANDS = ['create', 'remove', 'rm', 'open', 'add-repo', 'add', 'init'];
308
+ const isInteractive = INTERACTIVE_COMMANDS.some(
309
+ (ic) => cmd.toLowerCase() === ic || cmd.toLowerCase().startsWith(ic + ' ')
310
+ );
311
+
312
+ if (isInteractive) {
313
+ // Pause TUI and run command interactively
314
+ process.stdin.off('keypress', keypressHandler);
315
+ cleanup();
316
+
317
+ // Clear alternate screen and show standard terminal
318
+ console.log(chalk.bold.cyan(`\nEntering interactive mode for "nexusflow ${cmd}"...\n`));
319
+
320
+ try {
321
+ await execa('node', [script, ...args], {
322
+ cwd: workspacePath,
323
+ stdio: 'inherit',
324
+ shell: process.platform === 'win32',
325
+ });
326
+ } catch (err: any) {
327
+ console.error(chalk.red(`\nāœ– Command failed: ${err.message}`));
328
+ }
329
+
330
+ // Check if workspace still exists (in case it was deleted by 'remove')
331
+ let workspaceExists = true;
332
+ try {
333
+ await fs.access(workspacePath);
334
+ } catch {
335
+ workspaceExists = false;
336
+ }
337
+
338
+ if (!workspaceExists) {
339
+ console.log(chalk.bold.cyan('\nšŸ‘‹ Workspace deleted. Exiting NexusFlow TUI Dashboard.\n'));
340
+ process.exit(0);
341
+ }
342
+
343
+ // Re-initialize TUI state
344
+ process.stdout.write('\x1B[?1049h'); // Switch to alternate screen buffer
345
+ process.stdout.write('\x1B[?25l'); // Hide cursor
346
+ if (process.stdin.isTTY) {
347
+ process.stdin.setRawMode(true);
348
+ }
349
+ process.stdout.on('resize', resizeHandler);
350
+ process.stdin.on('keypress', keypressHandler);
351
+
352
+ pollInterval = setInterval(async () => {
353
+ await updateGitStatus();
354
+ if (!state.inputMode && !state.activeCommandRunning) {
355
+ draw();
356
+ }
357
+ }, 5000);
358
+
359
+ state.activeCommandRunning = false;
360
+ await updateGitStatus();
361
+ draw();
362
+ } else {
363
+ // Run background command via execa
364
+ const { stdout, stderr } = await execa('node', [script, ...args], { cwd: workspacePath });
365
+
366
+ if (stdout) {
367
+ stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
368
+ }
369
+ if (stderr) {
370
+ stderr.split('\n').filter(Boolean).forEach((l) => log(` ${chalk.red(l)}`));
371
+ }
372
+ log(chalk.green('āœ” Command completed successfully.'));
373
+ }
374
+ } catch (err: any) {
375
+ log(chalk.red(`āœ– Command failed: ${err.message}`));
376
+ } finally {
377
+ state.activeCommandRunning = false;
378
+ await updateGitStatus();
379
+ draw();
380
+ }
381
+ } else {
382
+ draw();
383
+ }
384
+ } else if (key.name === 'escape') {
385
+ state.inputMode = false;
386
+ state.inputValue = '';
387
+ draw();
388
+ } else if (key.name === 'tab') {
389
+ const cmd = state.inputValue.trim().toLowerCase();
390
+ const matches = COMMANDS.filter(c => c.startsWith(cmd));
391
+ if (matches.length > 0) {
392
+ const selected = matches[state.activeSuggestionIndex % matches.length];
393
+ state.inputValue = selected + ' ';
394
+ state.activeSuggestionIndex = 0;
395
+ draw();
396
+ }
397
+ } else if (key.name === 'right' || key.name === 'down') {
398
+ const cmd = state.inputValue.trim().toLowerCase();
399
+ const matches = COMMANDS.filter(c => c.startsWith(cmd));
400
+ if (matches.length > 0) {
401
+ state.activeSuggestionIndex = (state.activeSuggestionIndex + 1) % matches.length;
402
+ draw();
403
+ }
404
+ } else if (key.name === 'left' || key.name === 'up') {
405
+ const cmd = state.inputValue.trim().toLowerCase();
406
+ const matches = COMMANDS.filter(c => c.startsWith(cmd));
407
+ if (matches.length > 0) {
408
+ state.activeSuggestionIndex = (state.activeSuggestionIndex - 1 + matches.length) % matches.length;
409
+ draw();
410
+ }
411
+ } else if (key.name === 'backspace') {
412
+ state.inputValue = state.inputValue.slice(0, -1);
413
+ state.activeSuggestionIndex = 0;
414
+ draw();
415
+ } else if (str && str.length === 1 && !key.ctrl && !key.meta) {
416
+ state.inputValue += str;
417
+ state.activeSuggestionIndex = 0;
418
+ draw();
419
+ }
420
+ } else {
421
+ // --- Handle single key shortcuts mode ---
422
+ if (key.name === 'q') {
423
+ cleanup();
424
+ console.log(chalk.bold.cyan('\nšŸ‘‹ Exited NexusFlow TUI Dashboard.\n'));
425
+ process.exit(0);
426
+ } else if (str === '/') {
427
+ state.inputMode = true;
428
+ draw();
429
+ } else if (key.name === 's') {
430
+ state.activeCommandRunning = true;
431
+ log('šŸ”„ Running Workspace Sync rebase...');
432
+ draw();
433
+
434
+ try {
435
+ const script = fileURLToPath(new URL('../index.js', import.meta.url));
436
+ const { stdout } = await execa('node', [script, 'sync'], { cwd: workspacePath });
437
+ stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
438
+ log(chalk.green('āœ” Sync completed successfully.'));
439
+ } catch (err: any) {
440
+ log(chalk.red(`āœ– Sync failed: ${err.message}`));
441
+ } finally {
442
+ state.activeCommandRunning = false;
443
+ await updateGitStatus();
444
+ draw();
445
+ }
446
+ } else if (key.name === 'd') {
447
+ state.activeCommandRunning = true;
448
+ log('🩺 Running doctor diagnostics...');
449
+ draw();
450
+
451
+ try {
452
+ const script = fileURLToPath(new URL('../index.js', import.meta.url));
453
+ const { stdout } = await execa('node', [script, 'doctor'], { cwd: workspacePath });
454
+ stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
455
+ log(chalk.green('āœ” Diagnostics check complete.'));
456
+ } catch (err: any) {
457
+ log(chalk.red(`āœ– Diagnostics failed: ${err.message}`));
458
+ } finally {
459
+ state.activeCommandRunning = false;
460
+ await updateGitStatus();
461
+ draw();
462
+ }
463
+ } else if (key.name === 'r') {
464
+ state.activeCommandRunning = true;
465
+ log('šŸ”„ Regenerating context manifest files...');
466
+ draw();
467
+
468
+ try {
469
+ const script = fileURLToPath(new URL('../index.js', import.meta.url));
470
+ const { stdout } = await execa('node', [script, 'refresh'], { cwd: workspacePath });
471
+ stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
472
+ log(chalk.green('āœ” Context successfully refreshed.'));
473
+ } catch (err: any) {
474
+ log(chalk.red(`āœ– Refresh failed: ${err.message}`));
475
+ } finally {
476
+ state.activeCommandRunning = false;
477
+ await updateGitStatus();
478
+ draw();
479
+ }
480
+ }
481
+ }
482
+ };
483
+ process.stdin.on('keypress', keypressHandler);
484
+ }