@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,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Session resume picker for the agent dashboard.
4
+ * Shows recent sessions sorted by last active, launches
5
+ * the appropriate agent CLI with --resume <id> --fork in a new dashboard pane.
6
+ */
7
+
8
+ import { getWorkspaceSessions, WorkspaceSession } from '../../utils/claude-sessions';
9
+ import { launchAgentPane } from './launcher';
10
+ import { getAgentPaths, getPrimaryAgent, ConcreteAgentType } from '../../utils/agent-config';
11
+ import { colorize } from '../../utils/colors';
12
+ import inquirer from 'inquirer';
13
+ import autocompletePrompt from 'inquirer-autocomplete-prompt';
14
+ import * as fuzzy from 'fuzzy';
15
+
16
+ inquirer.registerPrompt('autocomplete', autocompletePrompt);
17
+
18
+ async function main() {
19
+ try {
20
+ const sessions = await getWorkspaceSessions();
21
+
22
+ if (sessions.length === 0) {
23
+ console.log('No sessions found. Launch a new agent with n.');
24
+ process.exit(1);
25
+ }
26
+
27
+ const items = sessions.map(session => ({
28
+ session,
29
+ displayName: session.workspaceName,
30
+ timeLabel: session.lastActiveLabel,
31
+ }));
32
+
33
+ const { selected } = await inquirer.prompt([{
34
+ type: 'autocomplete',
35
+ name: 'selected',
36
+ message: 'Select session to resume:',
37
+ source: async (_answersSoFar: any, input: string | undefined) => {
38
+ const searchInput = input || '';
39
+ const source = items.map(item => ({
40
+ name: `${item.displayName} ${colorize(item.timeLabel, 'dim')}`,
41
+ value: item,
42
+ }));
43
+ if (!searchInput) return source;
44
+ const results = fuzzy.filter(searchInput, items, { extract: (item) => item.displayName });
45
+ return results.map(result => ({
46
+ name: `${result.original.displayName} ${colorize(result.original.timeLabel, 'dim')}`,
47
+ value: result.original,
48
+ }));
49
+ },
50
+ pageSize: 15,
51
+ } as any]);
52
+
53
+ const item = selected as { session: WorkspaceSession };
54
+ const session = item.session;
55
+ const rawSessionId = session.sessionId;
56
+ // Sanitize session ID before interpolating into shell command
57
+ if (!/^[a-zA-Z0-9\-]{1,100}$/.test(rawSessionId)) {
58
+ console.error('Invalid session ID format — skipping');
59
+ process.exit(1);
60
+ }
61
+ // Use the session's agent type for resume, fall back to primary if missing
62
+ // (sessions from before the multi-agent upgrade won't have agentType)
63
+ const agentType = session.agentType || getPrimaryAgent().type;
64
+ const agent = getAgentPaths(agentType as ConcreteAgentType);
65
+ launchAgentPane(session.workspacePath, agent.resumeCommand(rawSessionId));
66
+ console.log(`\nResuming ${agentType} session in ${session.workspaceName}`);
67
+ } catch (error) {
68
+ if ((error as any)?.name === 'ExitPromptError') process.exit(0);
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ main();
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Dashboard sidebar entry point.
4
+ *
5
+ * This is a plain .js file (not .ts) to preserve the real dynamic import().
6
+ * TypeScript compiles `await import('ink')` into `require('ink')` in CJS mode,
7
+ * which fails because ink v5 is ESM-only with top-level await.
8
+ *
9
+ * By keeping this as .js, tsc copies it as-is to dist/ and the real
10
+ * import() call is preserved at runtime.
11
+ */
12
+
13
+ async function main() {
14
+ try {
15
+ const React = require('react');
16
+ const path = require('path');
17
+
18
+ // Real dynamic ESM import — NOT compiled by tsc
19
+ const { render, Box, Text, useInput, useApp } = await import('ink');
20
+
21
+ // CJS require for our compiled TSX components
22
+ const { DashboardSidebar } = require(path.join(__dirname, '..', '..', 'cli', 'dashboard'));
23
+
24
+ const App = () => React.createElement(DashboardSidebar, {
25
+ Box,
26
+ Text,
27
+ useInput,
28
+ useApp,
29
+ });
30
+
31
+ render(React.createElement(App), { exitOnCtrlC: false });
32
+ } catch (error) {
33
+ console.error('Dashboard sidebar failed to start:', error.message);
34
+ // Keep the pane alive so user can see the error
35
+ await new Promise(() => {});
36
+ }
37
+ }
38
+
39
+ main();
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Workspace picker for the agent dashboard.
4
+ * Runs inside a tmux popup — lets the user select a workspace,
5
+ * then spawns a fresh claude agent pane in the dashboard session.
6
+ */
7
+
8
+ import { listWorkspaces } from '../../utils/workspace-meta';
9
+ import { launchAgentPane } from './launcher';
10
+ import { getPrimaryAgent } from '../../utils/agent-config';
11
+ import inquirer from 'inquirer';
12
+ import autocompletePrompt from 'inquirer-autocomplete-prompt';
13
+ import * as fuzzy from 'fuzzy';
14
+
15
+ inquirer.registerPrompt('autocomplete', autocompletePrompt);
16
+
17
+ async function main() {
18
+ try {
19
+ const workspaces = await listWorkspaces();
20
+
21
+ if (workspaces.length === 0) {
22
+ console.log('No workspaces found. Create one with: workspace create');
23
+ process.exit(1);
24
+ }
25
+
26
+ const items = workspaces.map(ws => ({
27
+ name: ws.name,
28
+ path: ws.path,
29
+ repoCount: ws.metadata?.repositories?.length ?? 0,
30
+ }));
31
+
32
+ const { selected } = await inquirer.prompt([{
33
+ type: 'autocomplete',
34
+ name: 'selected',
35
+ message: 'Select workspace to launch agent in:',
36
+ source: async (_answersSoFar: any, input: string | undefined) => {
37
+ const searchInput = input || '';
38
+ const source = items.map(item => ({
39
+ name: `${item.name} (${item.repoCount} repos)`,
40
+ value: item,
41
+ }));
42
+ if (!searchInput) return source;
43
+ const results = fuzzy.filter(searchInput, items, { extract: (item) => item.name });
44
+ return results.map(result => ({
45
+ name: `${result.original.name} (${result.original.repoCount} repos)`,
46
+ value: result.original,
47
+ }));
48
+ },
49
+ pageSize: 15,
50
+ } as any]);
51
+
52
+ const workspace = selected as { name: string; path: string };
53
+ const agent = getPrimaryAgent();
54
+ launchAgentPane(workspace.path, agent.launchCommand);
55
+ console.log(`\nLaunched agent in ${workspace.name}`);
56
+ } catch (error) {
57
+ if ((error as any)?.name === 'ExitPromptError') process.exit(0);
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ main();
@@ -0,0 +1,250 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ const {
4
+ mockRm,
5
+ mockListWorkspaces,
6
+ mockPromptMultiWorkspaceSelection,
7
+ mockPrompt,
8
+ } = vi.hoisted(() => ({
9
+ mockRm: vi.fn(),
10
+ mockListWorkspaces: vi.fn(),
11
+ mockPromptMultiWorkspaceSelection: vi.fn(),
12
+ mockPrompt: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('fs/promises', () => ({
16
+ rm: mockRm,
17
+ }));
18
+
19
+ vi.mock('../utils/config', () => ({
20
+ WORKSPACES_DIR: '/test-workspaces',
21
+ }));
22
+
23
+ vi.mock('../utils/workspace-meta', () => ({
24
+ listWorkspaces: mockListWorkspaces,
25
+ }));
26
+
27
+ vi.mock('../utils/prompts', () => ({
28
+ promptMultiWorkspaceSelection: mockPromptMultiWorkspaceSelection,
29
+ }));
30
+
31
+ vi.mock('../utils/logger', () => ({
32
+ logInfo: vi.fn(),
33
+ logSuccess: vi.fn(),
34
+ logError: vi.fn(),
35
+ logWarning: vi.fn(),
36
+ }));
37
+
38
+ vi.mock('../utils/colors', () => ({
39
+ colorize: (text: string) => text,
40
+ }));
41
+
42
+ vi.mock('inquirer', () => ({
43
+ default: {
44
+ prompt: mockPrompt,
45
+ },
46
+ }));
47
+
48
+ import { main } from './delete';
49
+ import { logInfo, logSuccess, logError } from '../utils/logger';
50
+
51
+ function makeWorkspaceList(...names: string[]) {
52
+ return names.map(name => ({
53
+ name,
54
+ path: `/test-workspaces/${name}`,
55
+ metadata: {
56
+ repositories: [{ name: 'repo-a' }],
57
+ createdAt: '2025-01-01T00:00:00.000Z',
58
+ },
59
+ }));
60
+ }
61
+
62
+ describe('delete-workspace main', () => {
63
+ beforeEach(() => {
64
+ vi.clearAllMocks();
65
+ mockRm.mockResolvedValue(undefined);
66
+ vi.spyOn(console, 'log').mockImplementation(() => {});
67
+ });
68
+
69
+ it('exits immediately when no workspaces exist', async () => {
70
+ mockListWorkspaces.mockResolvedValueOnce([]);
71
+
72
+ await main();
73
+
74
+ expect(logInfo).toHaveBeenCalledWith('No more workspaces remaining');
75
+ expect(mockPromptMultiWorkspaceSelection).not.toHaveBeenCalled();
76
+ });
77
+
78
+ it('deletes selected workspaces and exits when user declines "delete more"', async () => {
79
+ mockListWorkspaces
80
+ .mockResolvedValueOnce(makeWorkspaceList('ws-a', 'ws-b')) // initial fetch
81
+ .mockResolvedValueOnce(makeWorkspaceList('ws-b')); // after deletion check
82
+
83
+ mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a']);
84
+
85
+ mockPrompt
86
+ .mockResolvedValueOnce({ confirmed: true }) // confirm deletion
87
+ .mockResolvedValueOnce({ deleteMore: false }); // don't delete more
88
+
89
+ await main();
90
+
91
+ expect(mockRm).toHaveBeenCalledWith('/test-workspaces/ws-a', { recursive: true, force: true });
92
+ expect(logSuccess).toHaveBeenCalledWith(expect.stringContaining('ws-a'));
93
+ });
94
+
95
+ it('deletes multiple workspaces in one batch', async () => {
96
+ mockListWorkspaces
97
+ .mockResolvedValueOnce(makeWorkspaceList('ws-a', 'ws-b', 'ws-c'))
98
+ .mockResolvedValueOnce(makeWorkspaceList('ws-c'));
99
+
100
+ mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a', 'ws-b']);
101
+
102
+ mockPrompt
103
+ .mockResolvedValueOnce({ confirmed: true })
104
+ .mockResolvedValueOnce({ deleteMore: false });
105
+
106
+ await main();
107
+
108
+ expect(mockRm).toHaveBeenCalledTimes(2);
109
+ expect(mockRm).toHaveBeenCalledWith('/test-workspaces/ws-a', { recursive: true, force: true });
110
+ expect(mockRm).toHaveBeenCalledWith('/test-workspaces/ws-b', { recursive: true, force: true });
111
+ });
112
+
113
+ it('skips deletion when user cancels confirmation', async () => {
114
+ mockListWorkspaces
115
+ .mockResolvedValueOnce(makeWorkspaceList('ws-a'))
116
+ .mockResolvedValueOnce(makeWorkspaceList('ws-a'));
117
+
118
+ mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a']);
119
+
120
+ mockPrompt
121
+ .mockResolvedValueOnce({ confirmed: false }) // cancel deletion
122
+ .mockResolvedValueOnce({ deleteMore: false }); // don't delete more
123
+
124
+ await main();
125
+
126
+ expect(mockRm).not.toHaveBeenCalled();
127
+ // Should still ask "delete more?" — not exit immediately
128
+ expect(mockPrompt).toHaveBeenCalledTimes(2);
129
+ });
130
+
131
+ it('loops when user wants to delete more', async () => {
132
+ mockListWorkspaces
133
+ .mockResolvedValueOnce(makeWorkspaceList('ws-a', 'ws-b')) // 1st iteration
134
+ .mockResolvedValueOnce(makeWorkspaceList('ws-b')) // after 1st deletion
135
+ .mockResolvedValueOnce(makeWorkspaceList('ws-b')) // 2nd iteration
136
+ .mockResolvedValueOnce([]); // after 2nd deletion
137
+
138
+ mockPromptMultiWorkspaceSelection
139
+ .mockResolvedValueOnce(['ws-a'])
140
+ .mockResolvedValueOnce(['ws-b']);
141
+
142
+ mockPrompt
143
+ .mockResolvedValueOnce({ confirmed: true }) // confirm 1st deletion
144
+ .mockResolvedValueOnce({ deleteMore: true }) // delete more
145
+ .mockResolvedValueOnce({ confirmed: true }); // confirm 2nd deletion
146
+
147
+ await main();
148
+
149
+ expect(mockRm).toHaveBeenCalledTimes(2);
150
+ expect(mockRm).toHaveBeenCalledWith('/test-workspaces/ws-a', { recursive: true, force: true });
151
+ expect(mockRm).toHaveBeenCalledWith('/test-workspaces/ws-b', { recursive: true, force: true });
152
+ // Should see "no more workspaces remaining" after all deleted
153
+ expect(logInfo).toHaveBeenCalledWith('No more workspaces remaining');
154
+ });
155
+
156
+ it('breaks out of loop when no workspaces remain after deletion', async () => {
157
+ mockListWorkspaces
158
+ .mockResolvedValueOnce(makeWorkspaceList('ws-only'))
159
+ .mockResolvedValueOnce([]); // none left after deletion
160
+
161
+ mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-only']);
162
+ mockPrompt.mockResolvedValueOnce({ confirmed: true });
163
+
164
+ await main();
165
+
166
+ expect(mockRm).toHaveBeenCalledTimes(1);
167
+ expect(logInfo).toHaveBeenCalledWith('No more workspaces remaining');
168
+ // Should NOT have asked "delete more?" since there are none left
169
+ expect(mockPrompt).toHaveBeenCalledTimes(1); // only the confirm prompt
170
+ });
171
+
172
+ it('handles individual deletion failure without stopping others', async () => {
173
+ mockListWorkspaces
174
+ .mockResolvedValueOnce(makeWorkspaceList('ws-a', 'ws-b', 'ws-c'))
175
+ .mockResolvedValueOnce(makeWorkspaceList('ws-c'));
176
+
177
+ mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a', 'ws-b']);
178
+
179
+ mockRm
180
+ .mockRejectedValueOnce(new Error('permission denied')) // ws-a fails
181
+ .mockResolvedValueOnce(undefined); // ws-b succeeds
182
+
183
+ mockPrompt
184
+ .mockResolvedValueOnce({ confirmed: true })
185
+ .mockResolvedValueOnce({ deleteMore: false });
186
+
187
+ await main();
188
+
189
+ expect(mockRm).toHaveBeenCalledTimes(2);
190
+ expect(logError).toHaveBeenCalledWith(expect.stringContaining('ws-a'));
191
+ expect(logSuccess).toHaveBeenCalledWith(expect.stringContaining('ws-b'));
192
+ });
193
+
194
+ it('uses singular message for single workspace confirmation', async () => {
195
+ mockListWorkspaces
196
+ .mockResolvedValueOnce(makeWorkspaceList('ws-alpha'))
197
+ .mockResolvedValueOnce([]);
198
+
199
+ mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-alpha']);
200
+ mockPrompt.mockResolvedValueOnce({ confirmed: true });
201
+
202
+ await main();
203
+
204
+ expect(mockPrompt).toHaveBeenCalledWith(
205
+ expect.arrayContaining([
206
+ expect.objectContaining({
207
+ message: 'Delete workspace ws-alpha?',
208
+ default: true,
209
+ }),
210
+ ])
211
+ );
212
+ });
213
+
214
+ it('uses plural message for multiple workspace confirmation', async () => {
215
+ mockListWorkspaces
216
+ .mockResolvedValueOnce(makeWorkspaceList('ws-a', 'ws-b', 'ws-c'))
217
+ .mockResolvedValueOnce([]);
218
+
219
+ mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a', 'ws-b', 'ws-c']);
220
+ mockPrompt.mockResolvedValueOnce({ confirmed: true });
221
+
222
+ await main();
223
+
224
+ expect(mockPrompt).toHaveBeenCalledWith(
225
+ expect.arrayContaining([
226
+ expect.objectContaining({
227
+ message: 'Delete these 3 workspaces?',
228
+ default: true,
229
+ }),
230
+ ])
231
+ );
232
+ });
233
+
234
+ it('shows workspace details including repo count and creation date', async () => {
235
+ const logSpy = vi.spyOn(console, 'log');
236
+
237
+ mockListWorkspaces
238
+ .mockResolvedValueOnce(makeWorkspaceList('ws-a'))
239
+ .mockResolvedValueOnce([]);
240
+
241
+ mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a']);
242
+ mockPrompt.mockResolvedValueOnce({ confirmed: true });
243
+
244
+ await main();
245
+
246
+ const messages = logSpy.mock.calls.map(c => c[0]);
247
+ expect(messages).toContainEqual(expect.stringContaining('Repositories: 1'));
248
+ expect(messages).toContainEqual(expect.stringContaining('/test-workspaces/ws-a'));
249
+ });
250
+ });
@@ -0,0 +1,171 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs/promises';
3
+ import { WORKSPACES_DIR } from '../utils/config';
4
+ import * as path from 'path';
5
+ import { listWorkspaces } from '../utils/workspace-meta';
6
+ import { promptMultiWorkspaceSelection } from '../utils/prompts';
7
+ import { logInfo, logSuccess, logError, logWarning } from '../utils/logger';
8
+ import { colorize } from '../utils/colors';
9
+ import inquirer from 'inquirer';
10
+ import { getGlobalOpts, parseList } from '../utils/command-helpers';
11
+
12
+ export function registerDeleteCommand(parent: Command) {
13
+ parent
14
+ .command('delete')
15
+ .aliases(['d', 'del'])
16
+ .description('Delete a workspace')
17
+ .option('-w, --workspace <names>', 'Comma-separated workspace names')
18
+ .action(async (opts, cmd) => {
19
+ const globalOpts = getGlobalOpts(cmd);
20
+ await handleDelete({ ...opts, ...globalOpts });
21
+ });
22
+ }
23
+
24
+ async function handleDelete(opts: {
25
+ workspace?: string;
26
+ yes: boolean;
27
+ forceRefresh: boolean;
28
+ }) {
29
+ console.log('\n' + '='.repeat(60));
30
+ console.log(colorize('Delete Workspace', 'bright'));
31
+ console.log('='.repeat(60) + '\n');
32
+
33
+ try {
34
+ // Non-interactive path: workspace names provided via flag
35
+ if (opts.workspace) {
36
+ const selectedNames = parseList(opts.workspace);
37
+ const workspaces = await listWorkspaces();
38
+
39
+ for (const name of selectedNames) {
40
+ const workspace = workspaces.find(ws => ws.name === name);
41
+ const workspacePath = path.join(WORKSPACES_DIR, name);
42
+ if (workspace?.metadata) {
43
+ console.log(`${colorize(name, 'cyan')}`);
44
+ console.log(` Repositories: ${workspace.metadata.repositories.length}`);
45
+ console.log(` Path: ${workspacePath}`);
46
+ } else {
47
+ console.log(`${colorize(name, 'cyan')}: ${workspacePath}`);
48
+ }
49
+ }
50
+ console.log('');
51
+
52
+ logWarning('This will permanently delete all cloned repositories in the selected workspaces!');
53
+
54
+ if (!opts.yes) {
55
+ const { confirmed } = await inquirer.prompt([
56
+ {
57
+ type: 'confirm',
58
+ name: 'confirmed',
59
+ message: selectedNames.length === 1
60
+ ? `Delete workspace ${selectedNames[0]}?`
61
+ : `Delete these ${selectedNames.length} workspaces?`,
62
+ default: true,
63
+ },
64
+ ]);
65
+ if (!confirmed) {
66
+ logInfo('Deletion cancelled');
67
+ process.exit(0);
68
+ }
69
+ }
70
+
71
+ for (const name of selectedNames) {
72
+ const workspacePath = path.join(WORKSPACES_DIR, name);
73
+ try {
74
+ await fs.rm(workspacePath, { recursive: true, force: true });
75
+ logSuccess(`Deleted "${colorize(name, 'cyan')}"`);
76
+ } catch (error) {
77
+ logError(`Failed to delete "${name}"`);
78
+ if (error instanceof Error) logError(error.message);
79
+ }
80
+ }
81
+ return;
82
+ }
83
+
84
+ // Interactive path
85
+ while (true) {
86
+ const workspaces = await listWorkspaces();
87
+
88
+ if (workspaces.length === 0) {
89
+ logInfo('No more workspaces remaining');
90
+ break;
91
+ }
92
+
93
+ const selectedNames = await promptMultiWorkspaceSelection(workspaces);
94
+
95
+ for (const name of selectedNames) {
96
+ const workspace = workspaces.find(ws => ws.name === name);
97
+ const workspacePath = path.join(WORKSPACES_DIR, name);
98
+ if (workspace?.metadata) {
99
+ console.log(`${colorize(name, 'cyan')}`);
100
+ console.log(` Repositories: ${workspace.metadata.repositories.length}`);
101
+ console.log(` Created: ${new Date(workspace.metadata.createdAt).toLocaleString()}`);
102
+ console.log(` Path: ${workspacePath}`);
103
+ } else {
104
+ console.log(`${colorize(name, 'cyan')}`);
105
+ console.log(` Path: ${workspacePath}`);
106
+ }
107
+ }
108
+ console.log('');
109
+
110
+ logWarning('This will permanently delete all cloned repositories in the selected workspaces!');
111
+
112
+ const confirmMessage = selectedNames.length === 1
113
+ ? `Delete workspace ${selectedNames[0]}?`
114
+ : `Delete these ${selectedNames.length} workspaces?`;
115
+
116
+ const { confirmed } = await inquirer.prompt([
117
+ {
118
+ type: 'confirm',
119
+ name: 'confirmed',
120
+ message: confirmMessage,
121
+ default: true,
122
+ },
123
+ ]);
124
+
125
+ if (confirmed) {
126
+ for (const name of selectedNames) {
127
+ const workspacePath = path.join(WORKSPACES_DIR, name);
128
+ try {
129
+ await fs.rm(workspacePath, { recursive: true, force: true });
130
+ logSuccess(`Deleted "${colorize(name, 'cyan')}"`);
131
+ } catch (error) {
132
+ logError(`Failed to delete "${name}"`);
133
+ if (error instanceof Error) logError(error.message);
134
+ }
135
+ }
136
+ }
137
+
138
+ const remaining = await listWorkspaces();
139
+ if (remaining.length === 0) {
140
+ logInfo('No more workspaces remaining');
141
+ break;
142
+ }
143
+
144
+ const { deleteMore } = await inquirer.prompt([
145
+ {
146
+ type: 'confirm',
147
+ name: 'deleteMore',
148
+ message: 'Delete more workspaces?',
149
+ default: false,
150
+ },
151
+ ]);
152
+
153
+ if (!deleteMore) {
154
+ break;
155
+ }
156
+ }
157
+ } catch (error) {
158
+ logError('Failed to delete workspace');
159
+ if (error instanceof Error) {
160
+ logError(error.message);
161
+ }
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Legacy main() for backward compatibility with tests.
168
+ */
169
+ export async function main() {
170
+ await handleDelete({ yes: false, forceRefresh: false });
171
+ }
@@ -0,0 +1,55 @@
1
+ import { Command } from 'commander';
2
+
3
+ interface DeprecatedAlias {
4
+ name: string;
5
+ target: [string, string]; // [group, subcommand]
6
+ }
7
+
8
+ const DEPRECATED_SHORT_ALIASES: DeprecatedAlias[] = [
9
+ { name: 'sc', target: ['suite', 'create'] },
10
+ { name: 'sls', target: ['suite', 'list'] },
11
+ { name: 'sd', target: ['suite', 'delete'] },
12
+ { name: 'se', target: ['suite', 'export'] },
13
+ { name: 'si', target: ['suite', 'import'] },
14
+ { name: 'fs', target: ['suite', 'use'] },
15
+ { name: 'sb', target: ['branch', 'switch'] },
16
+ { name: 'bc', target: ['branch', 'create'] },
17
+ { name: 'bm', target: ['branch', 'merge'] },
18
+ { name: 'br', target: ['branch', 'rebase'] },
19
+ { name: 'uc', target: ['cache', 'refresh'] },
20
+ ];
21
+
22
+ const DEPRECATED_FLAT_COMMANDS: DeprecatedAlias[] = [
23
+ { name: 'suite-create', target: ['suite', 'create'] },
24
+ { name: 'suite-list', target: ['suite', 'list'] },
25
+ { name: 'suite-delete', target: ['suite', 'delete'] },
26
+ { name: 'suite-export', target: ['suite', 'export'] },
27
+ { name: 'suite-import', target: ['suite', 'import'] },
28
+ { name: 'from-suite', target: ['suite', 'use'] },
29
+ { name: 'switch-branch', target: ['branch', 'switch'] },
30
+ { name: 'branch-create', target: ['branch', 'create'] },
31
+ { name: 'branch-merge', target: ['branch', 'merge'] },
32
+ { name: 'branch-rebase', target: ['branch', 'rebase'] },
33
+ { name: 'update-cache', target: ['cache', 'refresh'] },
34
+ ];
35
+
36
+ export function registerDeprecatedAliases(program: Command) {
37
+ const all = [...DEPRECATED_SHORT_ALIASES, ...DEPRECATED_FLAT_COMMANDS];
38
+
39
+ for (const alias of all) {
40
+ program
41
+ .command(alias.name, { hidden: true })
42
+ .allowUnknownOption()
43
+ .allowExcessArguments()
44
+ .action(async () => {
45
+ const [group, sub] = alias.target;
46
+ process.stderr.write(
47
+ `[nemus] "w ${alias.name}" is deprecated, use "w ${group} ${sub}" instead\n`
48
+ );
49
+ // Re-parse with the correct group + subcommand
50
+ await program.parseAsync(
51
+ [process.argv[0], process.argv[1], group, sub, ...process.argv.slice(3)]
52
+ );
53
+ });
54
+ }
55
+ }