@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,196 @@
1
+ import { Command } from 'commander';
2
+ import { WORKSPACES_DIR } from '../utils/config';
3
+ import * as path from 'path';
4
+ import { verifyGhAuth, fetchOrgRepos, displayAuthInstructions } from '../utils/github';
5
+ import { promptRepositorySelection, promptWorkspaceName, confirmWorkspaceCreation, type RepoSelection } from '../utils/prompts';
6
+ import { cloneRepositories, reportCloneResults } from '../utils/git-operations';
7
+ import { warnIfGhqMissing } from '../utils/ghq-integration';
8
+ import { createMetadata, saveMetadata } from '../utils/workspace-meta';
9
+ import { generateClaudeContext } from '../utils/claude-integration';
10
+ import { logInfo, logSuccess, logError, logStep } from '../utils/logger';
11
+ import { colorize } from '../utils/colors';
12
+ import { printBanner } from '../utils/banner';
13
+ import { validateWorkspaceName, checkWorkspaceExists, sanitizeWorkspaceName, resolveWorkspaceNameConflict } from '../utils/validation';
14
+ import { getGlobalOpts, parseList } from '../utils/command-helpers';
15
+ import { resolveRepoSpecs } from '../utils/repo-resolver';
16
+
17
+ export function registerCreateCommand(parent: Command) {
18
+ parent
19
+ .command('create')
20
+ .alias('c')
21
+ .description('Create a new workspace')
22
+ .option('-w, --workspace <name>', 'Workspace name')
23
+ .option('-r, --repos <repos>', 'Comma-separated repository names. Use repo:suffix to add the same repo more than once under a separate folder, e.g. casper:cas-101 -> casper-cas-101')
24
+ .option('--prompt <prompt>', 'Original prompt that triggered workspace creation (saved to metadata)')
25
+ .option('--allow-empty', 'Create the workspace with no repositories (e.g. investigate-first: the agent adds repos later)')
26
+ .action(async (opts, cmd) => {
27
+ const globalOpts = getGlobalOpts(cmd);
28
+ await handleCreate({ ...opts, ...globalOpts });
29
+ });
30
+ }
31
+
32
+ async function handleCreate(opts: {
33
+ workspace?: string;
34
+ repos?: string;
35
+ prompt?: string;
36
+ allowEmpty?: boolean;
37
+ forceRefresh: boolean;
38
+ yes: boolean;
39
+ }) {
40
+ printBanner();
41
+
42
+ try {
43
+ // Step 1: Verify gh CLI authentication
44
+ logStep(1, 6, 'Verifying GitHub CLI authentication...');
45
+ const isAuthenticated = await verifyGhAuth();
46
+
47
+ if (!isAuthenticated) {
48
+ logError('GitHub CLI is not authenticated');
49
+ displayAuthInstructions();
50
+ process.exit(1);
51
+ }
52
+
53
+ logSuccess('GitHub CLI authenticated');
54
+
55
+ // Investigate-first empty workspace: no repos to resolve, so skip the org
56
+ // catalog fetch entirely — otherwise an empty/failed catalog would abort
57
+ // creation on the guard below even though we need no repos.
58
+ const investigateEmpty = !!opts.allowEmpty && !opts.repos;
59
+
60
+ // Step 2: Fetch repositories (only when we actually need to resolve repos)
61
+ let repos: Awaited<ReturnType<typeof fetchOrgRepos>> = [];
62
+ if (!investigateEmpty) {
63
+ logStep(2, 6, 'Fetching repositories...');
64
+ repos = await fetchOrgRepos({ forceRefresh: opts.forceRefresh });
65
+
66
+ if (repos.length === 0) {
67
+ logError('No repositories found');
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+ // Step 3: Select repositories
73
+ logStep(3, 6, 'Select repositories to clone...');
74
+ let selectedEntries: RepoSelection[];
75
+
76
+ if (investigateEmpty) {
77
+ // Investigate-first: start with no repos; the in-session agent discovers
78
+ // and adds the relevant repos itself.
79
+ logInfo('Creating an empty workspace (no repositories) — repos can be added later.');
80
+ selectedEntries = [];
81
+ } else if (opts.repos) {
82
+ const repoSpecs = parseList(opts.repos);
83
+ const { resolved, notFound, invalidSuffix } = resolveRepoSpecs(repoSpecs, repos);
84
+
85
+ if (invalidSuffix.length > 0) {
86
+ logError(`Invalid instance suffix in: ${invalidSuffix.join(', ')}`);
87
+ logInfo('Use repo:suffix (e.g. casper:cas-101). Suffix may contain letters, numbers, hyphens, underscores.');
88
+ process.exit(1);
89
+ }
90
+
91
+ // Warn about fuzzy-resolved names so the user knows what was used
92
+ for (const r of resolved) {
93
+ if (!r.exact) {
94
+ logInfo(`Repo "${r.input}" not found exactly — using "${colorize(r.repo.name, 'cyan')}" (best match)`);
95
+ }
96
+ }
97
+
98
+ if (notFound.length > 0) {
99
+ logError(`Repositories not found: ${notFound.join(', ')}`);
100
+ process.exit(1);
101
+ }
102
+
103
+ selectedEntries = resolved.map(r => ({ repo: r.repo, directoryName: r.directoryName }));
104
+ } else {
105
+ selectedEntries = await promptRepositorySelection(repos);
106
+ }
107
+
108
+ if (selectedEntries.length === 0 && !opts.allowEmpty) {
109
+ logInfo('No repositories selected');
110
+ return;
111
+ }
112
+
113
+ // Step 4: Get workspace name
114
+ logStep(4, 6, 'Configure workspace...');
115
+ let workspaceName: string;
116
+
117
+ if (opts.workspace) {
118
+ workspaceName = sanitizeWorkspaceName(opts.workspace);
119
+ const nameError = validateWorkspaceName(workspaceName);
120
+ if (nameError !== true) {
121
+ logError(typeof nameError === 'string' ? nameError : 'Invalid workspace name');
122
+ process.exit(1);
123
+ }
124
+ const exists = await checkWorkspaceExists(workspaceName);
125
+ if (exists) {
126
+ const resolved = await resolveWorkspaceNameConflict(workspaceName, selectedEntries.map(e => e.directoryName));
127
+ logInfo(`Workspace "${workspaceName}" already exists — using "${colorize(resolved, 'cyan')}" instead.`);
128
+ workspaceName = resolved;
129
+ }
130
+ } else {
131
+ workspaceName = await promptWorkspaceName();
132
+ // Auto-resolve conflict in case the interactively chosen name already exists
133
+ // (the prompt validates format but not uniqueness at this point)
134
+ const interactiveExists = await checkWorkspaceExists(workspaceName);
135
+ if (interactiveExists) {
136
+ const resolved = await resolveWorkspaceNameConflict(workspaceName, selectedEntries.map(e => e.directoryName));
137
+ logInfo(`Workspace "${workspaceName}" already exists — using "${colorize(resolved, 'cyan')}" instead.`);
138
+ workspaceName = resolved;
139
+ }
140
+ }
141
+
142
+ // Step 5: Confirm
143
+ const workspacePath = path.join(WORKSPACES_DIR, workspaceName);
144
+ if (!opts.yes) {
145
+ const confirmed = await confirmWorkspaceCreation(workspaceName, selectedEntries.length, workspacePath);
146
+ if (!confirmed) {
147
+ logInfo('Operation cancelled');
148
+ return;
149
+ }
150
+ }
151
+
152
+ // Step 6: Clone repositories
153
+ logStep(5, 6, 'Creating workspace...');
154
+ const { mkdir } = await import('fs/promises');
155
+ await mkdir(workspacePath, { recursive: true });
156
+
157
+ if (selectedEntries.length > 0) await warnIfGhqMissing();
158
+ const results = await cloneRepositories(selectedEntries, workspacePath);
159
+ reportCloneResults(results);
160
+
161
+ // Step 7: Save metadata & generate context
162
+ logStep(6, 6, 'Saving workspace metadata...');
163
+ const metadata = createMetadata(workspaceName, results, { prompt: opts.prompt });
164
+ await saveMetadata(workspacePath, metadata);
165
+
166
+ const successfulRepos = results
167
+ .filter(r => r.status === 'success')
168
+ .map(r => r.repo);
169
+
170
+ // Generate context + .mcp.json even for an empty (investigate-first)
171
+ // workspace, so the in-session agent lands with the MCP tools (search-repos
172
+ // / update-workspace) needed to discover and add repos.
173
+ if (successfulRepos.length > 0 || opts.allowEmpty) {
174
+ await generateClaudeContext(workspacePath, workspaceName, successfulRepos, metadata);
175
+ }
176
+
177
+ logSuccess(`Workspace "${colorize(workspaceName, 'cyan')}" created successfully!`);
178
+
179
+ // Write workspace path to temp file for shell integration auto-CD
180
+ const { writeFile } = await import('fs/promises');
181
+ const os = await import('os');
182
+ const tempFile = path.join(os.homedir(), '.workspace-last-created');
183
+ try {
184
+ await writeFile(tempFile, workspacePath, 'utf-8');
185
+ } catch {
186
+ // Non-critical — shell integration CD won't work but workspace was created
187
+ }
188
+
189
+ } catch (error) {
190
+ logError('Failed to create workspace');
191
+ if (error instanceof Error) {
192
+ logError(error.message);
193
+ }
194
+ process.exit(1);
195
+ }
196
+ }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hook handler for Claude Code hooks.
4
+ * Invoked via ~/.claude/settings.json hooks — reads JSON from stdin,
5
+ * maps the event to an agent status, and writes to state file.
6
+ *
7
+ * Status model:
8
+ * idle — agent is not working (new, finished, waiting for input)
9
+ * working — agent is actively processing (thinking, calling tools)
10
+ * waiting — agent needs user action (permission approval)
11
+ * stopped — session ended
12
+ *
13
+ * Event mapping:
14
+ * SessionStart → idle (session opened, not working yet)
15
+ * UserPromptSubmit → working (user sent prompt, agent starts)
16
+ * PreToolUse → working (agent calling a tool)
17
+ * PostToolUse → working (tool done, agent still processing)
18
+ * Notification:
19
+ * permission_prompt → waiting (needs user to approve tool use)
20
+ * idle_prompt → idle (agent finished, waiting for input)
21
+ * Stop → idle (agent finished its turn)
22
+ * SessionEnd → stopped (session ended)
23
+ *
24
+ * IMPORTANT: Must never write to stderr or throw — always exit 0.
25
+ */
26
+
27
+ import * as fs from 'fs';
28
+ import * as path from 'path';
29
+ import { AgentState, AgentStatus } from '../../types/dashboard';
30
+ import { readAgentState, writeAgentState, ensureStateDir } from '../../utils/agent-state';
31
+ import { WORKSPACES_DIR } from '../../utils/config';
32
+
33
+ function main() {
34
+ try {
35
+ const args = process.argv.slice(2);
36
+ const eventIdx = args.indexOf('--event');
37
+ const event = eventIdx >= 0 ? args[eventIdx + 1] : null;
38
+
39
+ if (!event) {
40
+ process.exit(0);
41
+ }
42
+
43
+ let input: any;
44
+ try {
45
+ const raw = fs.readFileSync('/dev/stdin', 'utf-8');
46
+ input = JSON.parse(raw);
47
+ } catch {
48
+ process.exit(0);
49
+ }
50
+
51
+ const sessionId = input.session_id;
52
+ if (!sessionId || !/^[a-zA-Z0-9\-]{1,100}$/.test(sessionId)) {
53
+ process.exit(0); // reject missing or invalid session IDs
54
+ }
55
+
56
+ ensureStateDir();
57
+
58
+ let newStatus: AgentStatus;
59
+ switch (event) {
60
+ case 'SessionStart':
61
+ newStatus = 'idle';
62
+ break;
63
+ case 'UserPromptSubmit':
64
+ newStatus = 'working';
65
+ break;
66
+ case 'PreToolUse':
67
+ newStatus = 'working';
68
+ break;
69
+ case 'PostToolUse':
70
+ newStatus = 'working';
71
+ break;
72
+ case 'Notification': {
73
+ const notifType = input.notification_type || '';
74
+ if (notifType === 'permission_prompt') {
75
+ newStatus = 'waiting'; // needs user to approve
76
+ } else if (notifType === 'idle_prompt') {
77
+ newStatus = 'idle'; // just idle, not a special state
78
+ } else {
79
+ process.exit(0);
80
+ }
81
+ break;
82
+ }
83
+ case 'Stop':
84
+ newStatus = 'idle';
85
+ break;
86
+ case 'SessionEnd':
87
+ newStatus = 'stopped';
88
+ break;
89
+ default:
90
+ process.exit(0);
91
+ }
92
+
93
+ const now = new Date().toISOString();
94
+
95
+ const existing = readAgentState(sessionId);
96
+ if (existing) {
97
+ existing.status = newStatus;
98
+ existing.lastUpdatedAt = now;
99
+ // Always update PID — handles resumed sessions where the old
100
+ // process is dead but session ID is reused with a new process
101
+ existing.pid = process.ppid || existing.pid;
102
+ writeAgentState(existing);
103
+ process.exit(0);
104
+ }
105
+
106
+ // New session — infer workspace from cwd
107
+ const cwd = input.cwd || process.cwd();
108
+ let workspace = 'unknown';
109
+ let workspacePath = cwd;
110
+
111
+ const workspacesDir = WORKSPACES_DIR;
112
+ if (cwd.startsWith(workspacesDir + path.sep) || cwd === workspacesDir) {
113
+ const relative = cwd.slice(workspacesDir.length + 1);
114
+ const parts = relative.split(path.sep);
115
+ if (parts[0]) {
116
+ workspace = parts[0];
117
+ workspacePath = path.join(workspacesDir, workspace);
118
+ }
119
+ }
120
+
121
+ const state: AgentState = {
122
+ sessionId,
123
+ workspace,
124
+ workspacePath,
125
+ pid: process.ppid || 0,
126
+ status: newStatus,
127
+ startedAt: now,
128
+ lastUpdatedAt: now,
129
+ };
130
+
131
+ writeAgentState(state);
132
+ process.exit(0);
133
+ } catch {
134
+ process.exit(0);
135
+ }
136
+ }
137
+
138
+ main();
@@ -0,0 +1,248 @@
1
+ import { Command } from 'commander';
2
+ import { execFileSync, spawnSync } from 'child_process';
3
+ import * as path from 'path';
4
+ import { logError, logInfo, logSuccess } from '../../utils/logger';
5
+ import { colorize } from '../../utils/colors';
6
+ import { areDashboardHooksInstalled, installDashboardHooks, uninstallDashboardHooks } from '../../utils/dashboard-hooks';
7
+ import { DASHBOARD_DEFAULTS } from '../../types/dashboard';
8
+ import { readAllAgentStates, removeAgentState } from '../../utils/agent-state';
9
+ import { getAgentPanes, findPaneForAgent, resetLayout } from '../../utils/tmux-dashboard';
10
+ import { getPrimaryAgent } from '../../utils/agent-config';
11
+
12
+ const { sessionName, sidebarWidthPercent } = DASHBOARD_DEFAULTS;
13
+
14
+ function isTmuxAvailable(): boolean {
15
+ try {
16
+ execFileSync('which', ['tmux'], { stdio: 'pipe' });
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ function isInsideTmux(): boolean {
24
+ return !!process.env.TMUX;
25
+ }
26
+
27
+ export function registerDashboardCommand(parent: Command) {
28
+ parent
29
+ .command('dashboard')
30
+ .alias('dash')
31
+ .description('Launch multi-agent management dashboard')
32
+ .option('--install-hooks', 'Install Claude status hooks without launching dashboard')
33
+ .option('--uninstall-hooks', 'Remove dashboard hooks from Claude settings')
34
+ .option('--list', 'List running agents as JSON (non-interactive)')
35
+ .option('--status', 'Show agent status summary (non-interactive)')
36
+ .option('--kill <sessionId>', 'Kill agent by session ID (non-interactive)')
37
+ .option('--launch <workspace>', 'Launch a new agent in a workspace (non-interactive)')
38
+ .action(async (opts) => {
39
+ await handleDashboard(opts);
40
+ });
41
+ }
42
+
43
+ async function handleDashboard(opts: {
44
+ installHooks?: boolean;
45
+ uninstallHooks?: boolean;
46
+ list?: boolean;
47
+ status?: boolean;
48
+ kill?: string;
49
+ launch?: string;
50
+ }) {
51
+ // Handle hook-only commands
52
+ if (opts.installHooks) {
53
+ installDashboardHooks();
54
+ logSuccess('Dashboard hooks installed in ~/.claude/settings.json');
55
+ return;
56
+ }
57
+
58
+ if (opts.uninstallHooks) {
59
+ uninstallDashboardHooks();
60
+ logSuccess('Dashboard hooks removed from ~/.claude/settings.json');
61
+ return;
62
+ }
63
+
64
+ // --list: print running agents as JSON
65
+ if (opts.list) {
66
+ const agents = readAllAgentStates().filter(a => a.status !== 'stopped');
67
+ console.log(JSON.stringify(agents, null, 2));
68
+ return;
69
+ }
70
+
71
+ // --status: human-readable status summary
72
+ if (opts.status) {
73
+ const agents = readAllAgentStates().filter(a => a.status !== 'stopped');
74
+ if (agents.length === 0) {
75
+ console.log('No agents running.');
76
+ return;
77
+ }
78
+ for (const a of agents) {
79
+ console.log(`${colorize(a.workspace, 'cyan')} ${colorize(a.status, 'yellow')} ${a.sessionId.slice(0, 8)}`);
80
+ }
81
+ return;
82
+ }
83
+
84
+ // --kill: kill agent by session ID
85
+ if (opts.kill) {
86
+ const sessionId = opts.kill;
87
+ const agents = readAllAgentStates();
88
+ const agent = agents.find(a => a.sessionId === sessionId || a.sessionId.startsWith(sessionId));
89
+ if (!agent) {
90
+ logError(`No agent found with session ID: ${sessionId}`);
91
+ process.exit(1);
92
+ }
93
+ // Try to kill the tmux pane if dashboard is running
94
+ const panes = getAgentPanes();
95
+ const pane = findPaneForAgent(agent, panes);
96
+ if (pane) {
97
+ spawnSync('tmux', ['kill-pane', '-t', `${sessionName}:0.${pane.index}`], { stdio: 'pipe' });
98
+ setTimeout(() => resetLayout(), 100);
99
+ } else {
100
+ // Kill the process directly (guard: pid 0 would kill entire process group)
101
+ if (agent.pid > 0) {
102
+ try { process.kill(agent.pid); } catch { /* already dead */ }
103
+ }
104
+ }
105
+ removeAgentState(agent.sessionId);
106
+ logSuccess(`Killed agent: ${agent.workspace} (${agent.sessionId.slice(0, 8)})`);
107
+ return;
108
+ }
109
+
110
+ // --launch: launch a new agent in a workspace
111
+ if (opts.launch) {
112
+ const workspaceName = opts.launch;
113
+ const { listWorkspaces } = await import('../../utils/workspace-meta');
114
+ const workspaces = await listWorkspaces();
115
+ const ws = workspaces.find(w => w.name === workspaceName);
116
+ if (!ws) {
117
+ logError(`Workspace not found: ${workspaceName}`);
118
+ logInfo(`Available: ${workspaces.map(w => w.name).join(', ')}`);
119
+ process.exit(1);
120
+ }
121
+ // Check if dashboard is running by verifying the session exists
122
+ const sessionCheck = spawnSync('tmux', ['has-session', '-t', sessionName], { stdio: 'pipe' });
123
+ if (sessionCheck.status === 0) {
124
+ // Dashboard running — split a pane
125
+ spawnSync('tmux', [
126
+ 'split-window', '-t', `${sessionName}:0`, '-h',
127
+ '-c', ws.path,
128
+ getPrimaryAgent().launchCommand,
129
+ ], { stdio: 'pipe' });
130
+ spawnSync('tmux', ['select-layout', '-t', sessionName, 'main-vertical'], { stdio: 'pipe' });
131
+ spawnSync('tmux', ['resize-pane', '-t', `${sessionName}:0.0`, '-x', '32'], { stdio: 'pipe' });
132
+ logSuccess(`Launched agent in workspace: ${workspaceName}`);
133
+ } else {
134
+ logInfo(`Dashboard not running. Start it with: w dash`);
135
+ }
136
+ return;
137
+ }
138
+
139
+ // Check tmux for interactive dashboard
140
+ if (!isTmuxAvailable()) {
141
+ logError('tmux is required for the agent dashboard');
142
+ console.log(colorize('Install tmux: brew install tmux', 'gray'));
143
+ process.exit(1);
144
+ }
145
+
146
+ // Auto-install hooks if not present
147
+ if (!areDashboardHooksInstalled()) {
148
+ installDashboardHooks();
149
+ logInfo('Installed dashboard hooks in ~/.claude/settings.json');
150
+ }
151
+
152
+ // Check if dashboard session already exists — reconnect if so
153
+ const sessionExists = spawnSync('tmux', [
154
+ 'has-session', '-t', sessionName,
155
+ ], { stdio: 'pipe' }).status === 0;
156
+
157
+ if (sessionExists) {
158
+ logInfo('Reconnecting to existing dashboard session...');
159
+ if (isInsideTmux()) {
160
+ spawnSync('tmux', ['switch-client', '-t', sessionName], { stdio: 'inherit' });
161
+ } else {
162
+ spawnSync('tmux', ['attach-session', '-t', sessionName], { stdio: 'inherit' });
163
+ }
164
+ logSuccess('Dashboard session ended');
165
+ return;
166
+ }
167
+
168
+ // Resolve paths to compiled scripts
169
+ const sidebarScript = path.join(__dirname, 'sidebar.js');
170
+
171
+ // Create tmux session with sidebar in the left pane
172
+ const newSessionResult = spawnSync('tmux', [
173
+ 'new-session', '-d', '-s', sessionName,
174
+ '-x', '200', '-y', '50',
175
+ `node "${sidebarScript}"`,
176
+ ], { stdio: 'pipe' });
177
+
178
+ if (newSessionResult.status !== 0) {
179
+ logError('Failed to create tmux session');
180
+ const stderr = newSessionResult.stderr?.toString();
181
+ if (stderr) logError(stderr);
182
+ process.exit(1);
183
+ }
184
+
185
+ // Split right pane (80%) with placeholder message
186
+ spawnSync('tmux', [
187
+ 'split-window', '-h', '-t', sessionName,
188
+ '-p', String(100 - sidebarWidthPercent),
189
+ 'bash', '-c',
190
+ 'echo ""; echo " No agents running."; echo " Press n in the sidebar to launch an agent."; echo ""; sleep infinity',
191
+ ], { stdio: 'pipe' });
192
+
193
+ // Focus the sidebar pane (left)
194
+ spawnSync('tmux', ['select-pane', '-t', `${sessionName}:0.0`], { stdio: 'pipe' });
195
+
196
+ // ── Session-scoped key bindings ──
197
+ // Use set-hook to add bindings only while this session is active and
198
+ // restore them on detach — avoids polluting global tmux key tables.
199
+ // prefix + M → focus sidebar + reset layout (M chosen to avoid overriding standard keys)
200
+ spawnSync('tmux', [
201
+ 'set-hook', '-t', sessionName, 'session-created',
202
+ `bind-key -T prefix M run-shell "tmux select-layout -t ${sessionName} main-vertical \\\\; resize-pane -t ${sessionName}:0.0 -x 32 \\\\; select-pane -t ${sessionName}:0.0"`,
203
+ ], { stdio: 'pipe' });
204
+
205
+ // Bind immediately for the current session
206
+ spawnSync('tmux', [
207
+ 'bind-key', '-T', 'prefix', 'M',
208
+ 'run-shell',
209
+ `tmux select-layout -t ${sessionName} main-vertical \\; resize-pane -t ${sessionName}:0.0 -x 32 \\; select-pane -t ${sessionName}:0.0`,
210
+ ], { stdio: 'pipe' });
211
+
212
+ // Unbind prefix+M when the dashboard session ends (cleanup)
213
+ spawnSync('tmux', [
214
+ 'set-hook', '-t', sessionName, 'session-closed',
215
+ 'unbind-key -T prefix M',
216
+ ], { stdio: 'pipe' });
217
+
218
+ // ── Status bar ──
219
+ spawnSync('tmux', [
220
+ 'set-option', '-t', sessionName,
221
+ 'status-left', ' 🤖 Agent Dashboard ',
222
+ ], { stdio: 'pipe' });
223
+ spawnSync('tmux', [
224
+ 'set-option', '-t', sessionName,
225
+ 'status-right', ' prefix+M:sidebar/reset q:detach Q:quit ',
226
+ ], { stdio: 'pipe' });
227
+ spawnSync('tmux', [
228
+ 'set-option', '-t', sessionName,
229
+ 'status-style', 'bg=colour235,fg=colour255',
230
+ ], { stdio: 'pipe' });
231
+
232
+ // Enable mouse support for pane selection
233
+ spawnSync('tmux', [
234
+ 'set-option', '-t', sessionName,
235
+ 'mouse', 'on',
236
+ ], { stdio: 'pipe' });
237
+
238
+ logInfo('Launching agent dashboard...');
239
+
240
+ // Attach to the session
241
+ if (isInsideTmux()) {
242
+ spawnSync('tmux', ['switch-client', '-t', sessionName], { stdio: 'inherit' });
243
+ } else {
244
+ spawnSync('tmux', ['attach-session', '-t', sessionName], { stdio: 'inherit' });
245
+ }
246
+
247
+ logSuccess('Agent dashboard session ended');
248
+ }
@@ -0,0 +1,42 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { DASHBOARD_DEFAULTS } from '../../types/dashboard';
3
+
4
+ const { sessionName } = DASHBOARD_DEFAULTS;
5
+
6
+ /**
7
+ * Launch an agent in the dashboard by spawning a new tmux pane.
8
+ * Kills any placeholder pane (sleep infinity), splits a new pane,
9
+ * and rebalances the layout.
10
+ *
11
+ * @param workspacePath - absolute path to the workspace directory
12
+ * @param command - shell command to run in the new pane (e.g. 'claude' or 'claude --resume <id> --fork-session')
13
+ */
14
+ export function launchAgentPane(workspacePath: string, command: string): void {
15
+ // Kill placeholder pane (sleep infinity) if present
16
+ const paneResult = spawnSync('tmux', [
17
+ 'list-panes', '-t', sessionName, '-F',
18
+ '#{pane_index} #{pane_current_command}',
19
+ ], { stdio: 'pipe' });
20
+
21
+ if (paneResult.stdout) {
22
+ const lines = paneResult.stdout.toString().trim().split('\n');
23
+ for (const line of lines) {
24
+ const parts = line.split(' ');
25
+ if (parts[0] !== '0' && parts.slice(1).join(' ').includes('sleep')) {
26
+ spawnSync('tmux', ['kill-pane', '-t', `${sessionName}:0.${parts[0]}`], { stdio: 'pipe' });
27
+ break;
28
+ }
29
+ }
30
+ }
31
+
32
+ // Create a new pane for the agent
33
+ spawnSync('tmux', [
34
+ 'split-window', '-t', `${sessionName}:0`, '-h',
35
+ '-c', workspacePath,
36
+ command,
37
+ ], { stdio: 'pipe' });
38
+
39
+ // Rebalance layout: sidebar left, agents stacked right
40
+ spawnSync('tmux', ['select-layout', '-t', sessionName, 'main-vertical'], { stdio: 'pipe' });
41
+ spawnSync('tmux', ['resize-pane', '-t', `${sessionName}:0.0`, '-x', '32'], { stdio: 'pipe' });
42
+ }