@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,219 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ const {
4
+ mockListWorkspaces,
5
+ mockGetWorkspaceSessions,
6
+ mockPrompt,
7
+ mockWriteFile,
8
+ } = vi.hoisted(() => ({
9
+ mockListWorkspaces: vi.fn(),
10
+ mockGetWorkspaceSessions: vi.fn(),
11
+ mockPrompt: vi.fn(),
12
+ mockWriteFile: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('../utils/workspace-meta', () => ({
16
+ listWorkspaces: mockListWorkspaces,
17
+ }));
18
+
19
+ vi.mock('../utils/claude-sessions', () => ({
20
+ getWorkspaceSessions: mockGetWorkspaceSessions,
21
+ }));
22
+
23
+ vi.mock('../utils/logger', () => ({
24
+ logInfo: vi.fn(),
25
+ logError: vi.fn(),
26
+ }));
27
+
28
+ vi.mock('../utils/colors', () => ({
29
+ colorize: (text: string) => text,
30
+ }));
31
+
32
+ vi.mock('fs/promises', () => ({
33
+ writeFile: mockWriteFile,
34
+ }));
35
+
36
+ vi.mock('inquirer', () => ({
37
+ default: {
38
+ prompt: mockPrompt,
39
+ registerPrompt: vi.fn(),
40
+ },
41
+ }));
42
+
43
+ vi.mock('inquirer-autocomplete-prompt', () => ({
44
+ default: vi.fn(),
45
+ }));
46
+
47
+ vi.mock('fuzzy', () => ({
48
+ filter: vi.fn(),
49
+ }));
50
+
51
+ import { main } from './list';
52
+ import { logInfo } from '../utils/logger';
53
+
54
+ function makeWorkspaceList(...names: string[]) {
55
+ return names.map(name => ({
56
+ name,
57
+ path: `/test-workspaces/${name}`,
58
+ metadata: {
59
+ repositories: [{ name: 'repo-a', directoryName: 'repo-a', status: 'success' }],
60
+ createdAt: '2025-01-01T00:00:00.000Z',
61
+ },
62
+ }));
63
+ }
64
+
65
+ function makeArchivedWorkspaceList(...names: string[]) {
66
+ return names.map(name => ({
67
+ name,
68
+ path: `/test-workspaces/${name}`,
69
+ metadata: {
70
+ repositories: [{ name: 'repo-a', directoryName: 'repo-a', status: 'success' }],
71
+ createdAt: '2025-01-01T00:00:00.000Z',
72
+ archivedAt: new Date().toISOString(),
73
+ },
74
+ }));
75
+ }
76
+
77
+ describe('list-workspaces main', () => {
78
+ const originalArgv = process.argv;
79
+
80
+ beforeEach(() => {
81
+ vi.clearAllMocks();
82
+ process.argv = ['node', 'list-workspaces.js'];
83
+ mockWriteFile.mockResolvedValue(undefined);
84
+ mockGetWorkspaceSessions.mockResolvedValue([]);
85
+ vi.spyOn(console, 'log').mockImplementation(() => {});
86
+ });
87
+
88
+ afterEach(() => {
89
+ process.argv = originalArgv;
90
+ });
91
+
92
+ it('lists workspaces and prompts for selection', async () => {
93
+ mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('ws-a', 'ws-b'));
94
+ mockPrompt.mockResolvedValueOnce({ selected: 'ws-a' });
95
+
96
+ await main();
97
+
98
+ expect(mockPrompt).toHaveBeenCalledTimes(1);
99
+ expect(mockWriteFile).toHaveBeenCalledWith(
100
+ expect.stringContaining('.workspace-last-go'),
101
+ '/test-workspaces/ws-a',
102
+ 'utf-8',
103
+ );
104
+ });
105
+
106
+ it('does not prompt when no workspaces exist', async () => {
107
+ mockListWorkspaces.mockResolvedValueOnce([]);
108
+
109
+ await main();
110
+
111
+ expect(logInfo).toHaveBeenCalledWith('No workspaces found');
112
+ expect(mockPrompt).not.toHaveBeenCalled();
113
+ });
114
+
115
+ it('does not prompt when no archived workspaces exist', async () => {
116
+ process.argv = ['node', 'list-workspaces.js', '--archived'];
117
+ mockListWorkspaces.mockResolvedValueOnce([]);
118
+
119
+ await main();
120
+
121
+ expect(logInfo).toHaveBeenCalledWith('No archived workspaces found');
122
+ expect(mockPrompt).not.toHaveBeenCalled();
123
+ });
124
+
125
+ it('writes temp file on selection', async () => {
126
+ mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('my-workspace'));
127
+ mockPrompt.mockResolvedValueOnce({ selected: 'my-workspace' });
128
+
129
+ await main();
130
+
131
+ expect(mockWriteFile).toHaveBeenCalledWith(
132
+ expect.stringContaining('.workspace-last-go'),
133
+ '/test-workspaces/my-workspace',
134
+ 'utf-8',
135
+ );
136
+ });
137
+
138
+ it('writes resume flag when workspace has an existing session', async () => {
139
+ mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('my-workspace'));
140
+ mockGetWorkspaceSessions.mockResolvedValueOnce([{
141
+ workspaceName: 'my-workspace',
142
+ workspacePath: '/test-workspaces/my-workspace',
143
+ sessionId: 'session-123',
144
+ lastActiveAt: new Date(),
145
+ lastActiveLabel: '2m ago',
146
+ agentType: 'pi',
147
+ }]);
148
+ mockPrompt.mockResolvedValueOnce({ selected: 'my-workspace' });
149
+
150
+ await main();
151
+
152
+ expect(mockWriteFile).toHaveBeenCalledWith(
153
+ expect.stringContaining('.workspace-resume-session'),
154
+ JSON.stringify({ sessionId: 'session-123', agentType: 'pi' }),
155
+ 'utf-8',
156
+ );
157
+ });
158
+
159
+ it('does not write resume flag when workspace has no session', async () => {
160
+ mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('my-workspace'));
161
+ mockGetWorkspaceSessions.mockResolvedValueOnce([]);
162
+ mockPrompt.mockResolvedValueOnce({ selected: 'my-workspace' });
163
+
164
+ await main();
165
+
166
+ const resumeCalls = mockWriteFile.mock.calls.filter(
167
+ (c: any[]) => typeof c[0] === 'string' && c[0].includes('.workspace-resume-session')
168
+ );
169
+ expect(resumeCalls).toHaveLength(0);
170
+ });
171
+
172
+ it('lists archived workspaces with --archived flag and prompts for selection', async () => {
173
+ process.argv = ['node', 'list-workspaces.js', '--archived'];
174
+ const archived = makeArchivedWorkspaceList('old-ws');
175
+ mockListWorkspaces.mockResolvedValueOnce(archived);
176
+ mockPrompt.mockResolvedValueOnce({ selected: 'old-ws' });
177
+
178
+ await main();
179
+
180
+ expect(mockPrompt).toHaveBeenCalledTimes(1);
181
+ expect(mockWriteFile).toHaveBeenCalledWith(
182
+ expect.stringContaining('.workspace-last-go'),
183
+ '/test-workspaces/old-ws',
184
+ 'utf-8',
185
+ );
186
+ });
187
+
188
+ it('displays workspace name in the list output', async () => {
189
+ const logSpy = vi.spyOn(console, 'log');
190
+ mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('ws-a'));
191
+ mockPrompt.mockResolvedValueOnce({ selected: 'ws-a' });
192
+
193
+ await main();
194
+
195
+ const messages = logSpy.mock.calls.map(c => c[0]);
196
+ expect(messages).toContainEqual(expect.stringContaining('ws-a'));
197
+ });
198
+
199
+ it('sorts workspaces with sessions before those without', async () => {
200
+ mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('no-session', 'has-session'));
201
+ mockGetWorkspaceSessions.mockResolvedValueOnce([{
202
+ workspaceName: 'has-session',
203
+ workspacePath: '/test-workspaces/has-session',
204
+ sessionId: 'session-456',
205
+ lastActiveAt: new Date(),
206
+ lastActiveLabel: '5m ago',
207
+ }]);
208
+ mockPrompt.mockResolvedValueOnce({ selected: 'has-session' });
209
+
210
+ await main();
211
+
212
+ // The prompt source function should show has-session first
213
+ const promptCall = mockPrompt.mock.calls[0][0][0];
214
+ const source = promptCall.source;
215
+ const items = await source(null, '');
216
+ expect(items[0].value).toBe('has-session');
217
+ expect(items[1].value).toBe('no-session');
218
+ });
219
+ });
@@ -0,0 +1,176 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import * as fsPromises from 'fs/promises';
5
+ import { listWorkspaces } from '../utils/workspace-meta';
6
+ import { getWorkspaceSessions } from '../utils/claude-sessions';
7
+ import { logInfo, logError } from '../utils/logger';
8
+ import { colorize } from '../utils/colors';
9
+ import inquirer from 'inquirer';
10
+ import autocompletePrompt from 'inquirer-autocomplete-prompt';
11
+ import * as fuzzy from 'fuzzy';
12
+
13
+ inquirer.registerPrompt('autocomplete', autocompletePrompt);
14
+
15
+ const TEMP_FILE = path.join(os.homedir(), '.workspace-last-go');
16
+ const RESUME_FLAG_FILE = path.join(os.homedir(), '.workspace-resume-session');
17
+
18
+ interface WorkspaceItem {
19
+ name: string;
20
+ wsPath: string;
21
+ repoCount: number;
22
+ createdAt: string;
23
+ lastActiveLabel: string | null;
24
+ lastActiveAt: number;
25
+ hasSession: boolean;
26
+ }
27
+
28
+ export function registerListCommand(parent: Command) {
29
+ parent
30
+ .command('list')
31
+ .alias('l')
32
+ .description('List workspaces and navigate to one')
33
+ .option('-a, --archived', 'Show archived workspaces')
34
+ .action(async (opts) => {
35
+ await handleList(opts);
36
+ });
37
+ }
38
+
39
+ async function handleList(opts: { archived?: boolean }) {
40
+ const showArchived = opts.archived ?? false;
41
+ const title = showArchived ? 'Archived Workspaces' : 'Existing Workspaces';
42
+
43
+ try {
44
+ const [workspaces, sessions] = await Promise.all([
45
+ showArchived
46
+ ? listWorkspaces(true).then(ws => ws.filter(w => w.metadata?.archivedAt))
47
+ : listWorkspaces(false),
48
+ getWorkspaceSessions(),
49
+ ]);
50
+
51
+ if (workspaces.length === 0) {
52
+ console.log('\n' + '='.repeat(60));
53
+ console.log(colorize(title, 'bright'));
54
+ console.log('='.repeat(60) + '\n');
55
+ if (showArchived) {
56
+ logInfo('No archived workspaces found');
57
+ } else {
58
+ logInfo('No workspaces found');
59
+ console.log('\nCreate a new workspace with:');
60
+ console.log(' workspace create\n');
61
+ }
62
+ return;
63
+ }
64
+
65
+ const sessionMap = new Map(sessions.map(s => [s.workspaceName, s]));
66
+
67
+ const items: WorkspaceItem[] = workspaces.map(ws => {
68
+ const session = sessionMap.get(ws.name);
69
+ const repoCount = ws.metadata?.repositories?.length ?? 0;
70
+ return {
71
+ name: ws.name,
72
+ wsPath: ws.path,
73
+ repoCount,
74
+ createdAt: ws.metadata?.createdAt ?? '',
75
+ lastActiveLabel: session?.lastActiveLabel ?? null,
76
+ lastActiveAt: session?.lastActiveAt.getTime() ?? 0,
77
+ hasSession: !!session,
78
+ };
79
+ });
80
+
81
+ items.sort((a, b) => {
82
+ if (a.hasSession && b.hasSession) return b.lastActiveAt - a.lastActiveAt;
83
+ if (a.hasSession) return -1;
84
+ if (b.hasSession) return 1;
85
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
86
+ });
87
+
88
+ console.log('');
89
+ console.log(colorize(' ' + title, 'bright') + colorize(' (sorted by last active)', 'dim'));
90
+ console.log(colorize(' ' + '─'.repeat(54), 'dim'));
91
+
92
+ const maxNameLen = Math.max(...items.map(i => i.name.length));
93
+
94
+ for (const item of items) {
95
+ const name = item.name.padEnd(maxNameLen);
96
+ const active = item.lastActiveLabel
97
+ ? colorize(item.lastActiveLabel, 'yellow')
98
+ : colorize('no session', 'dim');
99
+ const repos = colorize(`${item.repoCount} repos`, 'dim');
100
+
101
+ if (showArchived) {
102
+ const ws = workspaces.find(w => w.name === item.name);
103
+ if (ws?.metadata?.archivedAt) {
104
+ const daysSinceArchive = (Date.now() - new Date(ws.metadata.archivedAt).getTime()) / (1000 * 60 * 60 * 24);
105
+ const daysRemaining = Math.max(0, Math.ceil(30 - daysSinceArchive));
106
+ const archiveLabel = colorize(`${daysRemaining}d left`, 'red');
107
+ console.log(` ${colorize(name, 'cyan')} ${active} ${repos} ${archiveLabel}`);
108
+ }
109
+ } else {
110
+ console.log(` ${colorize(name, 'cyan')} ${active} ${repos}`);
111
+ }
112
+ }
113
+ console.log('');
114
+
115
+ if (items.length > 0 && (process.stdout.isTTY || process.env.VITEST === 'true')) {
116
+ const { selected } = await inquirer.prompt([
117
+ {
118
+ type: 'autocomplete',
119
+ name: 'selected',
120
+ message: 'Select workspace to open:',
121
+ source: async (_answersSoFar: any, input: string | undefined) => {
122
+ const searchInput = input || '';
123
+
124
+ const source = items.map(item => ({
125
+ name: `${item.name} ${item.lastActiveLabel ? colorize(item.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${item.repoCount} repos`, 'dim')}`,
126
+ value: item.name,
127
+ }));
128
+
129
+ if (!searchInput) return source;
130
+
131
+ const results = fuzzy.filter(searchInput, items, {
132
+ extract: (item) => item.name,
133
+ });
134
+
135
+ return results.map(result => ({
136
+ name: `${result.original.name} ${result.original.lastActiveLabel ? colorize(result.original.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${result.original.repoCount} repos`, 'dim')}`,
137
+ value: result.original.name,
138
+ }));
139
+ },
140
+ pageSize: 15,
141
+ } as any,
142
+ ]);
143
+
144
+ const selectedItem = items.find(i => i.name === selected);
145
+ if (selectedItem) {
146
+ await fsPromises.writeFile(TEMP_FILE, selectedItem.wsPath, 'utf-8');
147
+ if (selectedItem.hasSession) {
148
+ const session = sessionMap.get(selectedItem.name);
149
+ if (session) {
150
+ await fsPromises.writeFile(RESUME_FLAG_FILE, JSON.stringify({
151
+ sessionId: session.sessionId,
152
+ agentType: session.agentType,
153
+ }), 'utf-8');
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ } catch (error) {
160
+ logError('Failed to list workspaces');
161
+ if (error instanceof Error) {
162
+ logError(error.message);
163
+ }
164
+ process.exit(1);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Legacy main() for backward compatibility with tests.
170
+ * Reads process.argv to determine options.
171
+ */
172
+ export async function main() {
173
+ const args = process.argv.slice(2);
174
+ const archived = args.includes('--archived') || args.includes('-a');
175
+ await handleList({ archived });
176
+ }
@@ -0,0 +1,39 @@
1
+ import { Command } from 'commander';
2
+
3
+ export function registerMcpCommands(parent: Command) {
4
+ const mcp = parent
5
+ .command('mcp')
6
+ .description('MCP server management');
7
+
8
+ mcp
9
+ .command('install')
10
+ .description('Register MCP server with Claude Code')
11
+ .action(async () => {
12
+ const { main } = await import('../../mcp/install');
13
+ await main('install');
14
+ });
15
+
16
+ mcp
17
+ .command('uninstall')
18
+ .description('Unregister the MCP server')
19
+ .action(async () => {
20
+ const { main } = await import('../../mcp/install');
21
+ await main('uninstall');
22
+ });
23
+
24
+ mcp
25
+ .command('upgrade')
26
+ .description('Update hooks and skills (no MCP re-register)')
27
+ .action(async () => {
28
+ const { main } = await import('../../mcp/install');
29
+ await main('upgrade');
30
+ });
31
+
32
+ mcp
33
+ .command('status')
34
+ .description('Check MCP server registration')
35
+ .action(async () => {
36
+ const { main } = await import('../../mcp/install');
37
+ await main('status');
38
+ });
39
+ }