@nemus-cli/nemus 0.2.1

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/CHANGELOG.md +86 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/assets/README.md +53 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/banner.svg +66 -0
  7. package/assets/favicon-32.png +0 -0
  8. package/assets/favicon-64.png +0 -0
  9. package/assets/icon-256.png +0 -0
  10. package/assets/icon.svg +23 -0
  11. package/assets/logo-256.png +0 -0
  12. package/assets/logo-512.png +0 -0
  13. package/assets/logo-wordmark.svg +35 -0
  14. package/assets/logo.svg +32 -0
  15. package/bin/workspace.js +130 -0
  16. package/dist/cli/ai-prompt.js +421 -0
  17. package/dist/cli/dashboard/AgentList.js +61 -0
  18. package/dist/cli/dashboard/DashboardSidebar.js +280 -0
  19. package/dist/cli/dashboard/HelpFooter.js +73 -0
  20. package/dist/cli/dashboard/StatusBadge.js +50 -0
  21. package/dist/cli/dashboard/index.js +11 -0
  22. package/dist/cli/dashboard/types.js +2 -0
  23. package/dist/cli/tui.js +48 -0
  24. package/dist/commands/ai-prompt.js +22 -0
  25. package/dist/commands/analyze-deps.js +138 -0
  26. package/dist/commands/archive.js +113 -0
  27. package/dist/commands/branch/create.js +119 -0
  28. package/dist/commands/branch/index.js +104 -0
  29. package/dist/commands/branch/merge.js +90 -0
  30. package/dist/commands/branch/rebase.js +84 -0
  31. package/dist/commands/branch/switch.js +165 -0
  32. package/dist/commands/cache/index.js +80 -0
  33. package/dist/commands/cache/manager.js +198 -0
  34. package/dist/commands/cleanup.js +153 -0
  35. package/dist/commands/configure-claude.js +55 -0
  36. package/dist/commands/configure.js +231 -0
  37. package/dist/commands/create.js +204 -0
  38. package/dist/commands/dashboard/hook-handler.js +162 -0
  39. package/dist/commands/dashboard/index.js +258 -0
  40. package/dist/commands/dashboard/launcher.js +40 -0
  41. package/dist/commands/dashboard/session-picker.js +106 -0
  42. package/dist/commands/dashboard/sidebar.js +39 -0
  43. package/dist/commands/dashboard/workspace-picker.js +95 -0
  44. package/dist/commands/delete.js +194 -0
  45. package/dist/commands/deprecated-aliases.js +44 -0
  46. package/dist/commands/diff.js +129 -0
  47. package/dist/commands/doctor.js +141 -0
  48. package/dist/commands/generate-docs.js +77 -0
  49. package/dist/commands/ghq-status.js +72 -0
  50. package/dist/commands/go.js +154 -0
  51. package/dist/commands/history.js +124 -0
  52. package/dist/commands/list.js +191 -0
  53. package/dist/commands/mcp/index.js +69 -0
  54. package/dist/commands/migrate.js +296 -0
  55. package/dist/commands/remove-repo.js +217 -0
  56. package/dist/commands/report-bug.js +112 -0
  57. package/dist/commands/run.js +147 -0
  58. package/dist/commands/save-context.js +171 -0
  59. package/dist/commands/sessions.js +129 -0
  60. package/dist/commands/status.js +138 -0
  61. package/dist/commands/suite/create.js +212 -0
  62. package/dist/commands/suite/delete.js +64 -0
  63. package/dist/commands/suite/export.js +126 -0
  64. package/dist/commands/suite/import.js +128 -0
  65. package/dist/commands/suite/index.js +94 -0
  66. package/dist/commands/suite/list.js +49 -0
  67. package/dist/commands/suite/use.js +227 -0
  68. package/dist/commands/sync.js +117 -0
  69. package/dist/commands/update.js +203 -0
  70. package/dist/mcp/install.js +550 -0
  71. package/dist/mcp/server.js +398 -0
  72. package/dist/mcp/tools.js +922 -0
  73. package/dist/pi-extensions/permission-sync.ts.template +41 -0
  74. package/dist/pi-extensions/permissions-gate.ts.template +69 -0
  75. package/dist/pi-extensions/pr-fix-trigger.ts.template +124 -0
  76. package/dist/pi-extensions/review-reminder.ts.template +58 -0
  77. package/dist/pi-extensions/workspace-input-status.ts.template +919 -0
  78. package/dist/program.js +136 -0
  79. package/dist/scripts/workspace-table.py +394 -0
  80. package/dist/types/dashboard.js +11 -0
  81. package/dist/types/index.js +2 -0
  82. package/dist/utils/agent-config.js +263 -0
  83. package/dist/utils/agent-state.js +178 -0
  84. package/dist/utils/banner.js +24 -0
  85. package/dist/utils/branch-operations.js +232 -0
  86. package/dist/utils/bug-report.js +355 -0
  87. package/dist/utils/cache.js +168 -0
  88. package/dist/utils/claude-integration.js +617 -0
  89. package/dist/utils/claude-sessions.js +260 -0
  90. package/dist/utils/cleanup-operations.js +120 -0
  91. package/dist/utils/colors.js +26 -0
  92. package/dist/utils/command-helpers.js +44 -0
  93. package/dist/utils/config.js +150 -0
  94. package/dist/utils/context-file.js +33 -0
  95. package/dist/utils/dashboard-hooks.js +172 -0
  96. package/dist/utils/dependency-analyzer.js +234 -0
  97. package/dist/utils/diff-operations.js +80 -0
  98. package/dist/utils/doc-generator.js +184 -0
  99. package/dist/utils/ghq-integration.js +332 -0
  100. package/dist/utils/git-operations.js +237 -0
  101. package/dist/utils/git-status.js +187 -0
  102. package/dist/utils/github.js +68 -0
  103. package/dist/utils/health-checks.js +345 -0
  104. package/dist/utils/history.js +150 -0
  105. package/dist/utils/hooks.js +100 -0
  106. package/dist/utils/logger.js +40 -0
  107. package/dist/utils/permission-sync.js +681 -0
  108. package/dist/utils/pi-extensions.js +224 -0
  109. package/dist/utils/progress.js +92 -0
  110. package/dist/utils/prompts.js +279 -0
  111. package/dist/utils/repo-resolver.js +260 -0
  112. package/dist/utils/retry.js +65 -0
  113. package/dist/utils/run-operations.js +30 -0
  114. package/dist/utils/suite.js +159 -0
  115. package/dist/utils/sync-operations.js +107 -0
  116. package/dist/utils/tmux-dashboard.js +184 -0
  117. package/dist/utils/validation.js +153 -0
  118. package/dist/utils/version-check.js +121 -0
  119. package/dist/utils/workspace-meta.js +185 -0
  120. package/install-shell-integration.sh +532 -0
  121. package/install.sh +401 -0
  122. package/package.json +90 -0
  123. package/remove-shell-block.awk +44 -0
  124. package/scripts/postinstall.js +85 -0
  125. package/skills/analyze-deps.md +23 -0
  126. package/skills/archive-workspace.md +26 -0
  127. package/skills/branch-create.md +18 -0
  128. package/skills/create-workspace.md +39 -0
  129. package/skills/delete-workspace.md +37 -0
  130. package/skills/list-org-repos.md +26 -0
  131. package/skills/list-suites.md +24 -0
  132. package/skills/list-workspaces.md +27 -0
  133. package/skills/refresh-workspace-docs.md +89 -0
  134. package/skills/remove-repo.md +25 -0
  135. package/skills/run-command.md +23 -0
  136. package/skills/save-context.md +68 -0
  137. package/skills/search-repos.md +31 -0
  138. package/skills/snapshot-list.md +18 -0
  139. package/skills/snapshot-save.md +12 -0
  140. package/skills/switch-branch.md +18 -0
  141. package/skills/update-cache.md +21 -0
  142. package/skills/update-workspace.md +66 -0
  143. package/skills/workspace-cleanup.md +18 -0
  144. package/skills/workspace-diff.md +23 -0
  145. package/skills/workspace-doctor.md +23 -0
  146. package/skills/workspace-info.md +45 -0
  147. package/skills/workspace-manager/SKILL.md +201 -0
  148. package/skills/workspace-manager/references/ai-prompt.md +44 -0
  149. package/skills/workspace-manager/references/analyze-deps.md +45 -0
  150. package/skills/workspace-manager/references/archive-workspace.md +38 -0
  151. package/skills/workspace-manager/references/branch-create.md +31 -0
  152. package/skills/workspace-manager/references/branch-merge.md +22 -0
  153. package/skills/workspace-manager/references/branch-rebase.md +22 -0
  154. package/skills/workspace-manager/references/cache-clear.md +18 -0
  155. package/skills/workspace-manager/references/cache-info.md +22 -0
  156. package/skills/workspace-manager/references/configure-claude.md +31 -0
  157. package/skills/workspace-manager/references/configure.md +35 -0
  158. package/skills/workspace-manager/references/create-workspace.md +45 -0
  159. package/skills/workspace-manager/references/dashboard.md +61 -0
  160. package/skills/workspace-manager/references/delete-workspace.md +33 -0
  161. package/skills/workspace-manager/references/generate-docs.md +36 -0
  162. package/skills/workspace-manager/references/ghq-status.md +28 -0
  163. package/skills/workspace-manager/references/go.md +33 -0
  164. package/skills/workspace-manager/references/history.md +41 -0
  165. package/skills/workspace-manager/references/list-org-repos.md +26 -0
  166. package/skills/workspace-manager/references/list-suites.md +21 -0
  167. package/skills/workspace-manager/references/list-workspaces.md +27 -0
  168. package/skills/workspace-manager/references/mcp.md +49 -0
  169. package/skills/workspace-manager/references/remove-repo.md +26 -0
  170. package/skills/workspace-manager/references/run-command.md +44 -0
  171. package/skills/workspace-manager/references/search-repos.md +24 -0
  172. package/skills/workspace-manager/references/sessions.md +27 -0
  173. package/skills/workspace-manager/references/suite-create.md +22 -0
  174. package/skills/workspace-manager/references/suite-delete.md +21 -0
  175. package/skills/workspace-manager/references/suite-export.md +19 -0
  176. package/skills/workspace-manager/references/suite-import.md +19 -0
  177. package/skills/workspace-manager/references/suite-use.md +27 -0
  178. package/skills/workspace-manager/references/switch-branch.md +25 -0
  179. package/skills/workspace-manager/references/update-cache.md +22 -0
  180. package/skills/workspace-manager/references/update-workspace.md +50 -0
  181. package/skills/workspace-manager/references/workspace-cleanup.md +33 -0
  182. package/skills/workspace-manager/references/workspace-diff.md +27 -0
  183. package/skills/workspace-manager/references/workspace-doctor.md +27 -0
  184. package/skills/workspace-manager/references/workspace-info.md +33 -0
  185. package/skills/workspace-manager/references/workspace-status.md +43 -0
  186. package/skills/workspace-manager/references/workspace-sync.md +26 -0
  187. package/skills/workspace-status.md +26 -0
  188. package/skills/workspace-sync.md +23 -0
  189. package/src/cli/ai-prompt.test.ts +667 -0
  190. package/src/cli/ai-prompt.ts +421 -0
  191. package/src/cli/dashboard/AgentList.tsx +44 -0
  192. package/src/cli/dashboard/DashboardSidebar.tsx +291 -0
  193. package/src/cli/dashboard/HelpFooter.tsx +22 -0
  194. package/src/cli/dashboard/StatusBadge.tsx +23 -0
  195. package/src/cli/dashboard/index.ts +5 -0
  196. package/src/cli/dashboard/types.ts +16 -0
  197. package/src/cli/tui.tsx +51 -0
  198. package/src/cli/workspace-cli.test.ts +30 -0
  199. package/src/commands/ai-prompt.ts +21 -0
  200. package/src/commands/analyze-deps.ts +119 -0
  201. package/src/commands/archive.ts +114 -0
  202. package/src/commands/branch/create.ts +94 -0
  203. package/src/commands/branch/index.ts +74 -0
  204. package/src/commands/branch/merge.ts +61 -0
  205. package/src/commands/branch/rebase.ts +54 -0
  206. package/src/commands/branch/switch.ts +150 -0
  207. package/src/commands/cache/index.ts +51 -0
  208. package/src/commands/cache/manager.ts +168 -0
  209. package/src/commands/cleanup.ts +132 -0
  210. package/src/commands/configure-claude.ts +55 -0
  211. package/src/commands/configure.ts +204 -0
  212. package/src/commands/create.ts +196 -0
  213. package/src/commands/dashboard/hook-handler.ts +138 -0
  214. package/src/commands/dashboard/index.ts +248 -0
  215. package/src/commands/dashboard/launcher.ts +42 -0
  216. package/src/commands/dashboard/session-picker.ts +73 -0
  217. package/src/commands/dashboard/sidebar.js +39 -0
  218. package/src/commands/dashboard/workspace-picker.ts +62 -0
  219. package/src/commands/delete.test.ts +250 -0
  220. package/src/commands/delete.ts +171 -0
  221. package/src/commands/deprecated-aliases.ts +55 -0
  222. package/src/commands/diff.ts +106 -0
  223. package/src/commands/doctor.ts +110 -0
  224. package/src/commands/generate-docs.ts +45 -0
  225. package/src/commands/ghq-status.ts +70 -0
  226. package/src/commands/go.ts +125 -0
  227. package/src/commands/history.ts +136 -0
  228. package/src/commands/list.test.ts +219 -0
  229. package/src/commands/list.ts +176 -0
  230. package/src/commands/mcp/index.ts +39 -0
  231. package/src/commands/migrate.ts +277 -0
  232. package/src/commands/remove-repo.ts +194 -0
  233. package/src/commands/report-bug.ts +88 -0
  234. package/src/commands/run.ts +125 -0
  235. package/src/commands/save-context.test.ts +113 -0
  236. package/src/commands/save-context.ts +141 -0
  237. package/src/commands/sessions.ts +99 -0
  238. package/src/commands/status.ts +111 -0
  239. package/src/commands/suite/create.ts +240 -0
  240. package/src/commands/suite/delete.ts +65 -0
  241. package/src/commands/suite/export.ts +97 -0
  242. package/src/commands/suite/import.ts +100 -0
  243. package/src/commands/suite/index.ts +66 -0
  244. package/src/commands/suite/list.ts +54 -0
  245. package/src/commands/suite/use.ts +232 -0
  246. package/src/commands/sync.ts +97 -0
  247. package/src/commands/update.ts +197 -0
  248. package/src/mcp/install.ts +529 -0
  249. package/src/mcp/server.ts +579 -0
  250. package/src/mcp/tools.test.ts +545 -0
  251. package/src/mcp/tools.ts +1019 -0
  252. package/src/mcp/update-workspace.test.ts +220 -0
  253. package/src/pi-extensions/permission-sync.ts.template +41 -0
  254. package/src/pi-extensions/permissions-gate.ts.template +69 -0
  255. package/src/pi-extensions/pr-fix-trigger.ts.template +124 -0
  256. package/src/pi-extensions/review-reminder.ts.template +58 -0
  257. package/src/pi-extensions/workspace-input-status.ts.template +919 -0
  258. package/src/program.ts +112 -0
  259. package/src/scripts/workspace-table.py +394 -0
  260. package/src/types/dashboard.ts +29 -0
  261. package/src/types/index.ts +109 -0
  262. package/src/types/ink.d.ts +171 -0
  263. package/src/utils/agent-config.test.ts +303 -0
  264. package/src/utils/agent-config.ts +264 -0
  265. package/src/utils/agent-state.test.ts +166 -0
  266. package/src/utils/agent-state.ts +135 -0
  267. package/src/utils/banner.ts +24 -0
  268. package/src/utils/branch-operations.ts +237 -0
  269. package/src/utils/bug-report.test.ts +174 -0
  270. package/src/utils/bug-report.ts +355 -0
  271. package/src/utils/cache.ts +150 -0
  272. package/src/utils/claude-integration.test.ts +381 -0
  273. package/src/utils/claude-integration.ts +649 -0
  274. package/src/utils/claude-sessions.test.ts +262 -0
  275. package/src/utils/claude-sessions.ts +251 -0
  276. package/src/utils/cleanup-operations.ts +94 -0
  277. package/src/utils/colors.ts +25 -0
  278. package/src/utils/command-helpers.ts +46 -0
  279. package/src/utils/config.test.ts +203 -0
  280. package/src/utils/config.ts +133 -0
  281. package/src/utils/context-file.ts +36 -0
  282. package/src/utils/dashboard-hooks.test.ts +168 -0
  283. package/src/utils/dashboard-hooks.ts +163 -0
  284. package/src/utils/dependency-analyzer.ts +243 -0
  285. package/src/utils/diff-operations.ts +59 -0
  286. package/src/utils/doc-generator.ts +187 -0
  287. package/src/utils/ghq-integration.test.ts +195 -0
  288. package/src/utils/ghq-integration.ts +317 -0
  289. package/src/utils/git-operations.test.ts +265 -0
  290. package/src/utils/git-operations.ts +233 -0
  291. package/src/utils/git-status.ts +171 -0
  292. package/src/utils/github.ts +74 -0
  293. package/src/utils/health-checks.ts +359 -0
  294. package/src/utils/history.ts +140 -0
  295. package/src/utils/hooks.ts +92 -0
  296. package/src/utils/logger.ts +37 -0
  297. package/src/utils/permission-sync.test.ts +593 -0
  298. package/src/utils/permission-sync.ts +720 -0
  299. package/src/utils/pi-extensions.test.ts +182 -0
  300. package/src/utils/pi-extensions.ts +226 -0
  301. package/src/utils/progress.ts +73 -0
  302. package/src/utils/prompts.test.ts +169 -0
  303. package/src/utils/prompts.ts +296 -0
  304. package/src/utils/repo-resolver.test.ts +313 -0
  305. package/src/utils/repo-resolver.ts +294 -0
  306. package/src/utils/retry.test.ts +124 -0
  307. package/src/utils/retry.ts +85 -0
  308. package/src/utils/run-operations.ts +38 -0
  309. package/src/utils/suite.test.ts +256 -0
  310. package/src/utils/suite.ts +144 -0
  311. package/src/utils/sync-operations.ts +87 -0
  312. package/src/utils/tmux-dashboard.ts +193 -0
  313. package/src/utils/validation.test.ts +139 -0
  314. package/src/utils/validation.ts +128 -0
  315. package/src/utils/version-check.test.ts +108 -0
  316. package/src/utils/version-check.ts +96 -0
  317. package/src/utils/workspace-meta.ts +173 -0
  318. package/sync-permissions.sh +137 -0
  319. package/tsconfig.json +20 -0
  320. package/uninstall.sh +162 -0
@@ -0,0 +1,291 @@
1
+ import * as React from 'react';
2
+ import { spawnSync } from 'child_process';
3
+ import * as path from 'path';
4
+ import { readAllAgentStates } from '../../utils/agent-state';
5
+ import { AgentState, DASHBOARD_DEFAULTS } from '../../types/dashboard';
6
+ import { InkComponents, Key } from './types';
7
+ import { AgentList } from './AgentList';
8
+ import { HelpFooter } from './HelpFooter';
9
+ import {
10
+ getAgentPanes,
11
+ getHiddenPanes,
12
+ findPaneForAgent,
13
+ sortAgentsByPaneOrder,
14
+ sortAgentsByPidOrder,
15
+ resetLayout,
16
+ restoreHiddenPanes,
17
+ zoomPane,
18
+ } from '../../utils/tmux-dashboard';
19
+
20
+ const { sessionName, pollIntervalMs } = DASHBOARD_DEFAULTS;
21
+
22
+ export interface DashboardSidebarProps extends InkComponents {
23
+ useInput: (handler: (input: string, key: Key) => void) => void;
24
+ useApp: () => { exit: (error?: Error) => void };
25
+ }
26
+
27
+ export const DashboardSidebar: React.FC<DashboardSidebarProps> = ({ Box, Text, useInput, useApp }) => {
28
+ const [agents, setAgents] = React.useState<AgentState[]>([]);
29
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
30
+ const [isZoomed, setIsZoomed] = React.useState(false);
31
+ const { exit } = useApp();
32
+
33
+ // Refs for always-current values inside the useInput closure
34
+ const agentsRef = React.useRef<AgentState[]>([]);
35
+ const selectedIndexRef = React.useRef(0);
36
+ const isZoomedRef = React.useRef(false);
37
+ // Store original pane PID order before zoom so we can restore it
38
+ const originalPaneOrderRef = React.useRef<number[]>([]);
39
+
40
+ // Poll agent states every 2s
41
+ React.useEffect(() => {
42
+ function refresh() {
43
+ const visiblePanes = getAgentPanes();
44
+ const hiddenPanes = getHiddenPanes();
45
+ const allPanes = [...visiblePanes, ...hiddenPanes];
46
+ const panePids = new Set(allPanes.map(p => p.pid));
47
+
48
+ const allStates = readAllAgentStates().filter(s => {
49
+ if (s.status === 'stopped') return false;
50
+ return panePids.has(s.pid);
51
+ });
52
+
53
+ // When zoomed, sort by the original pane order so the sidebar
54
+ // display stays stable (zoomed pane doesn't jump to the top)
55
+ const originalOrder = originalPaneOrderRef.current;
56
+ let sorted: AgentState[];
57
+ if (originalOrder.length > 0) {
58
+ sorted = sortAgentsByPidOrder(allStates, originalOrder);
59
+ } else {
60
+ sorted = sortAgentsByPaneOrder(allStates, allPanes);
61
+ }
62
+
63
+ agentsRef.current = sorted;
64
+ setAgents(sorted);
65
+ }
66
+ refresh();
67
+ const timer = setInterval(refresh, pollIntervalMs);
68
+ return () => clearInterval(timer);
69
+ }, []);
70
+
71
+ // Keep selectedIndex ref in sync
72
+ React.useEffect(() => {
73
+ selectedIndexRef.current = selectedIndex;
74
+ }, [selectedIndex]);
75
+
76
+ // Keep isZoomed ref in sync
77
+ React.useEffect(() => {
78
+ isZoomedRef.current = isZoomed;
79
+ }, [isZoomed]);
80
+
81
+ // Clamp selection — handles both too-high and negative values
82
+ const clampedIndex = Math.min(Math.max(selectedIndex, 0), Math.max(0, agents.length - 1));
83
+ if (clampedIndex !== selectedIndex) {
84
+ setSelectedIndex(clampedIndex);
85
+ selectedIndexRef.current = clampedIndex;
86
+ }
87
+
88
+ /**
89
+ * Apply zoom to a specific agent index.
90
+ */
91
+ function applyZoom(agentIndex: number): void {
92
+ const currentAgents = agentsRef.current;
93
+ if (agentIndex >= currentAgents.length) return;
94
+
95
+ if (isZoomedRef.current) {
96
+ // Already zoomed — restore panes in original order, then re-zoom
97
+ restoreHiddenPanes(originalPaneOrderRef.current);
98
+ } else {
99
+ // First zoom — capture the current pane order before hiding anything
100
+ const panes = getAgentPanes();
101
+ originalPaneOrderRef.current = panes.map(p => p.pid);
102
+ restoreHiddenPanes(); // no-op if nothing hidden
103
+ }
104
+
105
+ spawnSync('tmux', ['select-layout', '-t', sessionName, 'main-vertical'], { stdio: 'pipe' });
106
+ spawnSync('tmux', ['resize-pane', '-t', `${sessionName}:0.0`, '-x', '32'], { stdio: 'pipe' });
107
+
108
+ const panes = getAgentPanes();
109
+ const pane = findPaneForAgent(currentAgents[agentIndex], panes);
110
+ if (pane) {
111
+ zoomPane(pane);
112
+ setIsZoomed(true);
113
+ isZoomedRef.current = true;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Unzoom and reset layout, restoring original pane order.
119
+ */
120
+ function unzoom(): void {
121
+ restoreHiddenPanes(originalPaneOrderRef.current);
122
+ originalPaneOrderRef.current = [];
123
+ spawnSync('tmux', ['select-layout', '-t', sessionName, 'main-vertical'], { stdio: 'pipe' });
124
+ spawnSync('tmux', ['resize-pane', '-t', `${sessionName}:0.0`, '-x', '32'], { stdio: 'pipe' });
125
+ spawnSync('tmux', ['select-pane', '-t', `${sessionName}:0.0`], { stdio: 'pipe' });
126
+ setIsZoomed(false);
127
+ isZoomedRef.current = false;
128
+ }
129
+
130
+ // Keyboard handling — reads from refs for always-current data
131
+ useInput((input: string, key: Key) => {
132
+ const currentAgents = agentsRef.current;
133
+ const currentIdx = selectedIndexRef.current;
134
+ const currentZoomed = isZoomedRef.current;
135
+ const clampedIdx = Math.min(Math.max(currentIdx, 0), Math.max(0, currentAgents.length - 1));
136
+
137
+ // q — detach (session stays running in background, agents keep working)
138
+ if (input === 'q') {
139
+ restoreHiddenPanes();
140
+ spawnSync('tmux', ['detach-client', '-s', sessionName], { stdio: 'pipe' });
141
+ // Do NOT call exit() — the sidebar process must keep running so
142
+ // the session is alive when the user re-attaches with 'w dash'
143
+ return;
144
+ }
145
+
146
+ // Q (shift+q) — kill session and all agents
147
+ if (input === 'Q') {
148
+ restoreHiddenPanes();
149
+ spawnSync('tmux', ['kill-session', '-t', sessionName], { stdio: 'pipe' });
150
+ exit();
151
+ return;
152
+ }
153
+
154
+ // Navigation
155
+ if (key.upArrow || input === 'K') {
156
+ const next = Math.max(0, clampedIdx - 1);
157
+ setSelectedIndex(next);
158
+ selectedIndexRef.current = next;
159
+ return;
160
+ }
161
+ if (key.downArrow || input === 'J') {
162
+ const next = Math.max(0, Math.min(currentAgents.length - 1, clampedIdx + 1));
163
+ setSelectedIndex(next);
164
+ selectedIndexRef.current = next;
165
+ return;
166
+ }
167
+
168
+ // Number keys 1-9
169
+ if (input >= '1' && input <= '9') {
170
+ const idx = parseInt(input, 10) - 1;
171
+ if (idx < currentAgents.length) {
172
+ setSelectedIndex(idx);
173
+ selectedIndexRef.current = idx;
174
+ }
175
+ return;
176
+ }
177
+
178
+ // Focus — switch to agent pane
179
+ if (input === 'f' || key.return) {
180
+ if (clampedIdx < currentAgents.length) {
181
+ if (currentZoomed) {
182
+ applyZoom(clampedIdx);
183
+ const panes = getAgentPanes();
184
+ const pane = findPaneForAgent(currentAgents[clampedIdx], panes);
185
+ if (pane) {
186
+ spawnSync('tmux', ['select-pane', '-t', `${sessionName}:0.${pane.index}`], { stdio: 'pipe' });
187
+ }
188
+ } else {
189
+ const panes = getAgentPanes();
190
+ const pane = findPaneForAgent(currentAgents[clampedIdx], panes);
191
+ if (pane) {
192
+ spawnSync('tmux', ['select-pane', '-t', `${sessionName}:0.${pane.index}`], { stdio: 'pipe' });
193
+ }
194
+ }
195
+ }
196
+ return;
197
+ }
198
+
199
+ // Zoom toggle
200
+ if (input === 'z') {
201
+ if (currentZoomed) {
202
+ unzoom();
203
+ isZoomedRef.current = false;
204
+ } else {
205
+ applyZoom(clampedIdx);
206
+ }
207
+ return;
208
+ }
209
+
210
+ // Reset layout
211
+ if (input === 'r') {
212
+ unzoom();
213
+ isZoomedRef.current = false;
214
+ return;
215
+ }
216
+
217
+ // New agent
218
+ if (input === 'n') {
219
+ if (currentZoomed) {
220
+ unzoom();
221
+ isZoomedRef.current = false;
222
+ }
223
+ const pickerScript = path.join(__dirname, '..', '..', 'commands', 'dashboard', 'workspace-picker.js');
224
+ const popupResult = spawnSync('tmux', [
225
+ 'display-popup', '-E', '-w', '60%', '-h', '60%',
226
+ '-t', sessionName,
227
+ `node "${pickerScript}"`,
228
+ ], { stdio: 'pipe' });
229
+
230
+ if (popupResult.status !== 0) {
231
+ spawnSync('tmux', ['split-window', '-t', sessionName, '-v', `node "${pickerScript}"`], { stdio: 'pipe' });
232
+ }
233
+ setTimeout(() => resetLayout(), 500);
234
+ return;
235
+ }
236
+
237
+ // Resume session
238
+ if (input === 's') {
239
+ if (currentZoomed) {
240
+ unzoom();
241
+ isZoomedRef.current = false;
242
+ }
243
+ const sessionPickerScript = path.join(__dirname, '..', '..', 'commands', 'dashboard', 'session-picker.js');
244
+ const popupResult = spawnSync('tmux', [
245
+ 'display-popup', '-E', '-w', '60%', '-h', '60%',
246
+ '-t', sessionName,
247
+ `node "${sessionPickerScript}"`,
248
+ ], { stdio: 'pipe' });
249
+
250
+ if (popupResult.status !== 0) {
251
+ spawnSync('tmux', ['split-window', '-t', sessionName, '-v', `node "${sessionPickerScript}"`], { stdio: 'pipe' });
252
+ }
253
+ setTimeout(() => resetLayout(), 500);
254
+ return;
255
+ }
256
+
257
+ // Kill / close session
258
+ if (input === 'x') {
259
+ if (clampedIdx < currentAgents.length) {
260
+ if (currentZoomed) {
261
+ unzoom();
262
+ isZoomedRef.current = false;
263
+ }
264
+ const panes = getAgentPanes();
265
+ const pane = findPaneForAgent(currentAgents[clampedIdx], panes);
266
+ if (pane) {
267
+ spawnSync('tmux', ['kill-pane', '-t', `${sessionName}:0.${pane.index}`], { stdio: 'pipe' });
268
+ setTimeout(() => resetLayout(), 100);
269
+ }
270
+ }
271
+ return;
272
+ }
273
+ });
274
+
275
+ return (
276
+ <Box flexDirection="column">
277
+ <Box marginBottom={1}>
278
+ <Text bold color="cyan">{'─ Agent Dashboard ─'}</Text>
279
+ {isZoomed && <Text color="yellow"> [ZOOM]</Text>}
280
+ </Box>
281
+ <AgentList
282
+ agents={agents}
283
+ selectedIndex={clampedIndex}
284
+ onSelect={setSelectedIndex}
285
+ Box={Box}
286
+ Text={Text}
287
+ />
288
+ <HelpFooter isZoomed={isZoomed} Box={Box} Text={Text} />
289
+ </Box>
290
+ );
291
+ };
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+ import { InkComponents } from './types';
3
+
4
+ interface HelpFooterProps extends InkComponents {
5
+ isZoomed: boolean;
6
+ }
7
+
8
+ export const HelpFooter: React.FC<HelpFooterProps> = ({ isZoomed, Box, Text }) => {
9
+ return (
10
+ <Box flexDirection="column" marginTop={1}>
11
+ <Text dimColor>───────────────────</Text>
12
+ <Text><Text color="yellow">↑↓</Text> select <Text color="yellow">f</Text>/⏎ focus</Text>
13
+ <Text><Text color="yellow">z</Text> {isZoomed ? 'unzoom' : 'zoom'} <Text color="yellow">x</Text> kill</Text>
14
+ <Text><Text color="yellow">n</Text> new <Text color="yellow">q</Text> detach</Text>
15
+ <Text><Text color="yellow">s</Text> resume <Text color="yellow">Q</Text> quit all</Text>
16
+ <Text><Text color="yellow">r</Text> reset layout</Text>
17
+ <Text dimColor>───────────────────</Text>
18
+ <Text dimColor>From agent pane:</Text>
19
+ <Text> <Text color="yellow">prefix+M</Text> sidebar/reset</Text>
20
+ </Box>
21
+ );
22
+ };
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+ import { AgentStatus, InkComponents } from './types';
3
+
4
+ interface StatusBadgeProps extends InkComponents {
5
+ status: AgentStatus;
6
+ }
7
+
8
+ const STATUS_CONFIG: Record<AgentStatus, { icon: string; color: string; label: string }> = {
9
+ working: { icon: '●', color: 'green', label: 'working' },
10
+ waiting: { icon: '●', color: 'yellow', label: 'approve' },
11
+ idle: { icon: '◌', color: 'gray', label: 'idle' },
12
+ stopped: { icon: '✕', color: 'red', label: 'stopped' },
13
+ };
14
+
15
+ export const StatusBadge: React.FC<StatusBadgeProps> = ({ status, Box, Text }) => {
16
+ const config = STATUS_CONFIG[status] || STATUS_CONFIG.idle;
17
+ return (
18
+ <Box gap={1}>
19
+ <Text color={config.color}>{config.icon}</Text>
20
+ <Text color={config.color}>{config.label}</Text>
21
+ </Box>
22
+ );
23
+ };
@@ -0,0 +1,5 @@
1
+ export { DashboardSidebar } from './DashboardSidebar';
2
+ export { AgentList } from './AgentList';
3
+ export { StatusBadge } from './StatusBadge';
4
+ export { HelpFooter } from './HelpFooter';
5
+ export type { InkComponents } from './types';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared types for ink dashboard components.
3
+ * ink components are passed as props to avoid direct ESM imports
4
+ * (ink v5 is ESM-only; the project uses CJS).
5
+ */
6
+ import { FC } from 'react';
7
+ import { BoxProps, TextProps, Key } from 'ink';
8
+ import { AgentState, AgentStatus } from '../../types/dashboard';
9
+
10
+ /** ink components passed through from the dynamic import bridge */
11
+ export interface InkComponents {
12
+ Box: FC<BoxProps>;
13
+ Text: FC<TextProps>;
14
+ }
15
+
16
+ export type { AgentState, AgentStatus, Key };
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ // TUI implementation - simplified version without ink for now
3
+ // Full ink-based implementation can be added after dependencies are installed
4
+
5
+ import { listWorkspaces } from '../utils/workspace-meta';
6
+ import { logError, logInfo } from '../utils/logger';
7
+ import { colorize } from '../utils/colors';
8
+
9
+ export const main = async () => {
10
+ try {
11
+ logInfo('Interactive TUI mode');
12
+ console.log(colorize('\n╔══════════════════════════════════════════╗', 'cyan'));
13
+ console.log(colorize('║ Workspace Manager - Interactive UI ║', 'cyan'));
14
+ console.log(colorize('╚══════════════════════════════════════════╝\n', 'cyan'));
15
+
16
+ const workspaces = await listWorkspaces();
17
+
18
+ if (workspaces.length === 0) {
19
+ logError('No workspaces found');
20
+ console.log(colorize('Create a workspace with: workspace create', 'gray'));
21
+ process.exit(1);
22
+ }
23
+
24
+ console.log(colorize('Available Workspaces:', 'bright'));
25
+ for (let i = 0; i < workspaces.length; i++) {
26
+ const ws = workspaces[i];
27
+ const repoCount = ws.metadata?.repositories.length || 0;
28
+ console.log(` ${i + 1}. ${colorize(ws.name, 'cyan')} (${repoCount} repos)`);
29
+ }
30
+
31
+ console.log('\n' + colorize('Commands:', 'bright'));
32
+ console.log(' workspace status <name> - Check git status');
33
+ console.log(' workspace doctor <name> - Run health checks');
34
+ console.log(' workspace sync <name> - Pull latest changes');
35
+ console.log(' wgo <name> - Navigate to workspace');
36
+ console.log('');
37
+
38
+ logInfo('Full interactive TUI with ink will be available after running: npm install');
39
+ } catch (error) {
40
+ logError('TUI failed');
41
+ if (error instanceof Error) {
42
+ logError(error.message);
43
+ }
44
+ process.exit(1);
45
+ }
46
+ };
47
+
48
+ // Auto-execute only when run directly (not when imported by Commander)
49
+ if (require.main === module) {
50
+ main();
51
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+ import * as path from 'path';
4
+
5
+ const BIN = path.join(__dirname, '..', '..', 'bin', 'workspace.js');
6
+
7
+ describe('w CLI entry point', () => {
8
+ it('--version prints version number', () => {
9
+ const output = execSync(`node ${BIN} --version 2>/dev/null`).toString().trim();
10
+ expect(output).toMatch(/^\d+\.\d+\.\d+$/);
11
+ });
12
+
13
+ it('-V prints version number', () => {
14
+ const output = execSync(`node ${BIN} -V 2>/dev/null`).toString().trim();
15
+ expect(output).toMatch(/^\d+\.\d+\.\d+$/);
16
+ });
17
+
18
+ it('--version and -V print the same version', () => {
19
+ const v1 = execSync(`node ${BIN} --version 2>/dev/null`).toString().trim();
20
+ const v2 = execSync(`node ${BIN} -V 2>/dev/null`).toString().trim();
21
+ expect(v1).toBe(v2);
22
+ });
23
+
24
+ it('help banner includes version', () => {
25
+ const output = execSync(`node ${BIN} --help 2>/dev/null`).toString();
26
+ expect(output).toContain('multi-repo workspaces');
27
+ // Version should appear in format "vX.Y.Z"
28
+ expect(output).toMatch(/v\d+\.\d+\.\d+/);
29
+ });
30
+ });
@@ -0,0 +1,21 @@
1
+ import { logError } from '../utils/logger';
2
+ import { colorize } from '../utils/colors';
3
+ import { run } from '../cli/ai-prompt';
4
+
5
+ /**
6
+ * Handle the `nemus -- <prompt>` command.
7
+ * This is called directly from bin/workspace.js before Commander parses,
8
+ * because Commander treats `--` as end-of-options.
9
+ */
10
+ export async function handleAiPrompt(prompt: string): Promise<void> {
11
+ if (!prompt || !prompt.trim()) {
12
+ logError('No prompt provided');
13
+ console.log(`\n Usage: ${colorize('nemus -- <prompt>', 'green')}`);
14
+ console.log(` Example: ${colorize('nemus -- create a workspace for the payments team', 'gray')}\n`);
15
+ process.exit(1);
16
+ return;
17
+ }
18
+
19
+ const exitCode = await run(prompt.trim());
20
+ process.exit(exitCode);
21
+ }
@@ -0,0 +1,119 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import { WORKSPACES_DIR } from '../utils/config';
4
+ import { loadMetadata, saveMetadata } from '../utils/workspace-meta';
5
+ import {
6
+ analyzeDependencies,
7
+ detectCircularDependencies,
8
+ generateMermaidDiagram,
9
+ updateWorkspaceMetadata,
10
+ } from '../utils/dependency-analyzer';
11
+ import { logError, logInfo, logStep, logSuccess } from '../utils/logger';
12
+ import { colorize } from '../utils/colors';
13
+ import inquirer from 'inquirer';
14
+ import { resolveWorkspace } from '../utils/command-helpers';
15
+
16
+ const displayDependencyAnalysis = (analyses: Map<string, any>) => {
17
+ console.log('\n' + colorize('Dependency Analysis', 'bright'));
18
+ console.log(colorize('═'.repeat(100), 'gray'));
19
+
20
+ for (const [repoName, analysis] of analyses) {
21
+ console.log(`\n${colorize(repoName, 'cyan')}`);
22
+ if (analysis.dependencies.length > 0) {
23
+ console.log(` ${colorize('Depends on:', 'gray')} ${analysis.dependencies.join(', ')}`);
24
+ }
25
+ if (analysis.dependents.length > 0) {
26
+ console.log(` ${colorize('Depended by:', 'gray')} ${analysis.dependents.join(', ')}`);
27
+ }
28
+ if (analysis.missingDependencies.length > 0) {
29
+ console.log(` ${colorize('⚠ Missing deps:', 'yellow')} ${analysis.missingDependencies.join(', ')}`);
30
+ }
31
+ if (analysis.dependencies.length === 0 && analysis.dependents.length === 0 && analysis.missingDependencies.length === 0) {
32
+ console.log(` ${colorize('No dependencies detected', 'gray')}`);
33
+ }
34
+ }
35
+
36
+ console.log('\n' + colorize('═'.repeat(100), 'gray'));
37
+ };
38
+
39
+ export function registerAnalyzeDepsCommand(parent: Command) {
40
+ parent
41
+ .command('analyze-deps')
42
+ .alias('ad')
43
+ .description('Analyze inter-repo dependencies')
44
+ .argument('[workspace]', 'Workspace name')
45
+ .action(async (workspace) => {
46
+ await handleAnalyzeDeps(workspace);
47
+ });
48
+ }
49
+
50
+ async function handleAnalyzeDeps(workspaceArg?: string) {
51
+ try {
52
+ const selectedWorkspace = await resolveWorkspace(workspaceArg);
53
+ const workspacePath = path.join(WORKSPACES_DIR, selectedWorkspace);
54
+ const metadata = await loadMetadata(workspacePath);
55
+
56
+ if (!metadata) {
57
+ logError(`Workspace metadata not found for: ${selectedWorkspace}`);
58
+ process.exit(1);
59
+ }
60
+
61
+ logStep(`Analyzing dependencies for workspace: ${colorize(selectedWorkspace, 'cyan')}`);
62
+ logInfo('Scanning package.json, Dockerfile, and docker-compose.yml files...');
63
+
64
+ const repoNames = metadata.repositories.map(r => r.name);
65
+ const analyses = await analyzeDependencies(workspacePath, repoNames);
66
+
67
+ displayDependencyAnalysis(analyses);
68
+
69
+ const cycles = detectCircularDependencies(analyses);
70
+ if (cycles.length > 0) {
71
+ console.log('\n' + colorize('⚠ Circular Dependencies Detected:', 'yellow'));
72
+ for (const cycle of cycles) {
73
+ console.log(` ${cycle.join(' → ')}`);
74
+ }
75
+ }
76
+
77
+ const diagram = generateMermaidDiagram(analyses);
78
+ console.log('\n' + colorize('Dependency Graph (Mermaid):', 'bright'));
79
+ console.log(diagram);
80
+
81
+ const missingDeps = new Set<string>();
82
+ for (const [, analysis] of analyses) {
83
+ for (const dep of analysis.missingDependencies) {
84
+ missingDeps.add(dep);
85
+ }
86
+ }
87
+
88
+ if (missingDeps.size > 0) {
89
+ console.log('\n' + colorize('💡 Suggested Missing Repositories:', 'yellow'));
90
+ for (const dep of missingDeps) {
91
+ console.log(` • ${dep}`);
92
+ }
93
+ logInfo(`Consider adding these repositories with: workspace update ${selectedWorkspace}`);
94
+ }
95
+
96
+ if (process.stdout.isTTY) {
97
+ const { saveToMetadata } = await inquirer.prompt([
98
+ {
99
+ type: 'confirm',
100
+ name: 'saveToMetadata',
101
+ message: 'Save dependency analysis to workspace metadata?',
102
+ default: true,
103
+ },
104
+ ]);
105
+
106
+ if (saveToMetadata) {
107
+ const updatedMetadata = updateWorkspaceMetadata(metadata, analyses);
108
+ await saveMetadata(workspacePath, updatedMetadata);
109
+ logSuccess('Dependency analysis saved to metadata');
110
+ }
111
+ }
112
+ } catch (error) {
113
+ logError('Failed to analyze dependencies');
114
+ if (error instanceof Error) {
115
+ logError(error.message);
116
+ }
117
+ process.exit(1);
118
+ }
119
+ }