@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
package/src/program.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { colors } from './utils/colors';
5
+
6
+ // Read version from package.json
7
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
8
+
9
+ const g = colors.green;
10
+ const d = colors.dim;
11
+ const b = colors.bright;
12
+ const r = colors.reset;
13
+
14
+ const INNER = 38;
15
+ const titleLine = `>_ Nemus`;
16
+ const titlePad = ' '.repeat(Math.max(0, INNER - 2 - titleLine.length));
17
+ const versionLine = `v${pkg.version} · multi-repo workspaces`;
18
+ const versionPad = ' '.repeat(Math.max(0, INNER - 7 - versionLine.length));
19
+ const bar = '─'.repeat(INNER);
20
+ const bannerText = `
21
+ ${d} ╭${bar}╮${r}
22
+ ${d} │${r} ${g}>_${r} ${b}Nemus${r}${titlePad}${d}│${r}
23
+ ${d} │${r} ${d}${versionLine}${r}${versionPad}${d}│${r}
24
+ ${d} ╰${bar}╯${r}
25
+ `;
26
+
27
+ export const program = new Command();
28
+
29
+ program
30
+ .name('workspace')
31
+ .description('Multi-repo workspace manager')
32
+ .version(pkg.version, '-V, --version')
33
+ .option('-f, --force-refresh', 'Force refresh GitHub repos (skip cache)')
34
+ .option('-y, --yes', 'Skip confirmations')
35
+ .addHelpText('before', bannerText);
36
+
37
+ // Register top-level commands
38
+ import { registerCreateCommand } from './commands/create';
39
+ import { registerListCommand } from './commands/list';
40
+ import { registerUpdateCommand } from './commands/update';
41
+ import { registerDeleteCommand } from './commands/delete';
42
+ import { registerSyncCommand } from './commands/sync';
43
+ import { registerStatusCommand } from './commands/status';
44
+ import { registerDiffCommand } from './commands/diff';
45
+ import { registerRunCommand } from './commands/run';
46
+ import { registerGoCommand } from './commands/go';
47
+ import { registerDoctorCommand } from './commands/doctor';
48
+ import { registerAnalyzeDepsCommand } from './commands/analyze-deps';
49
+ import { registerHistoryCommand } from './commands/history';
50
+ import { registerCleanupCommand } from './commands/cleanup';
51
+ import { registerRemoveRepoCommand } from './commands/remove-repo';
52
+ import { registerArchiveCommand } from './commands/archive';
53
+ import { registerSessionsCommand } from './commands/sessions';
54
+ import { registerGenerateDocsCommand } from './commands/generate-docs';
55
+ import { registerConfigureCommand } from './commands/configure';
56
+ import { registerConfigureClaudeCommand } from './commands/configure-claude';
57
+ import { registerGhqStatusCommand } from './commands/ghq-status';
58
+ import { registerSaveContextCommand } from './commands/save-context';
59
+ import { registerMigrateCommand } from './commands/migrate';
60
+ import { registerReportBugCommand } from './commands/report-bug';
61
+
62
+ registerCreateCommand(program);
63
+ registerListCommand(program);
64
+ registerUpdateCommand(program);
65
+ registerDeleteCommand(program);
66
+ registerSyncCommand(program);
67
+ registerStatusCommand(program);
68
+ registerDiffCommand(program);
69
+ registerRunCommand(program);
70
+ registerGoCommand(program);
71
+ registerDoctorCommand(program);
72
+ registerAnalyzeDepsCommand(program);
73
+ registerHistoryCommand(program);
74
+ registerCleanupCommand(program);
75
+ registerRemoveRepoCommand(program);
76
+ registerArchiveCommand(program);
77
+ registerSessionsCommand(program);
78
+ registerGenerateDocsCommand(program);
79
+ registerConfigureCommand(program);
80
+ registerConfigureClaudeCommand(program);
81
+ registerGhqStatusCommand(program);
82
+ registerSaveContextCommand(program);
83
+ registerMigrateCommand(program);
84
+ registerReportBugCommand(program);
85
+
86
+ // Register TUI (delegates to existing Ink/React implementation)
87
+ program
88
+ .command('tui')
89
+ .description('Launch interactive terminal UI')
90
+ .action(async () => {
91
+ const { main } = await import('./cli/tui');
92
+ await main();
93
+ });
94
+
95
+ // Register dashboard command
96
+ import { registerDashboardCommand } from './commands/dashboard';
97
+ registerDashboardCommand(program);
98
+
99
+ // Register grouped commands
100
+ import { registerSuiteCommands } from './commands/suite';
101
+ import { registerBranchCommands } from './commands/branch';
102
+ import { registerCacheCommands } from './commands/cache';
103
+ import { registerMcpCommands } from './commands/mcp';
104
+
105
+ registerSuiteCommands(program);
106
+ registerBranchCommands(program);
107
+ registerCacheCommands(program);
108
+ registerMcpCommands(program);
109
+
110
+ // Register deprecated aliases
111
+ import { registerDeprecatedAliases } from './commands/deprecated-aliases';
112
+ registerDeprecatedAliases(program);
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ workspace-table.py — Workspace Manager status-line extension for Claude Code
4
+ (auto-generated by workspace-manager)
5
+
6
+ Outputs ONLY the workspace repo table (branch · PR · CI · dirty state).
7
+ Claude Code already renders model / cost / context itself, so this script
8
+ adds only what it doesn't show.
9
+
10
+ Configure in ~/.claude/settings.json:
11
+ "statusLine": {
12
+ "type": "command",
13
+ "command": "~/.workspace-manager-table.py",
14
+ "padding": 0
15
+ }
16
+
17
+ Claude Code passes session data as JSON on stdin.
18
+ """
19
+
20
+ import hashlib
21
+ import json
22
+ import os
23
+ import subprocess
24
+ import sys
25
+ import time
26
+ from concurrent.futures import ThreadPoolExecutor
27
+ from pathlib import Path
28
+
29
+ # ─── Cache ───────────────────────────────────────────────────────────────────
30
+ CACHE_FILE = Path.home() / ".workspace-manager-cache" / "pr-status.json"
31
+ CACHE_TTL = 300 # seconds
32
+
33
+ # ─── ANSI helpers ─────────────────────────────────────────────────────────────
34
+ RESET = "\033[0m"
35
+ BOLD = "\033[1m"
36
+ DIM = "\033[2m"
37
+ CYAN = "\033[38;5;87m"
38
+ BLUE = "\033[38;5;75m"
39
+ MAGENTA = "\033[38;5;213m"
40
+ GREEN = "\033[38;5;120m"
41
+ YELLOW = "\033[38;5;221m"
42
+ RED = "\033[38;5;203m"
43
+ GRAY = "\033[38;5;245m"
44
+ PURPLE = "\033[38;5;141m"
45
+
46
+
47
+ def color(text: str, c: str) -> str:
48
+ return f"{c}{text}{RESET}"
49
+
50
+
51
+ def hyperlink(text: str, url: str | None) -> str:
52
+ if not url:
53
+ return text
54
+ return f"\033]8;;{url}\033\\{text}\033]8;;\033\\"
55
+
56
+
57
+ # ─── Cache helpers ────────────────────────────────────────────────────────────
58
+ def load_cache() -> dict:
59
+ try:
60
+ return json.loads(CACHE_FILE.read_text())
61
+ except Exception:
62
+ return {}
63
+
64
+
65
+ def save_cache(cache: dict) -> None:
66
+ try:
67
+ CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
68
+ CACHE_FILE.write_text(json.dumps(cache))
69
+ except Exception:
70
+ pass
71
+
72
+
73
+ # ─── Git / PR helpers ─────────────────────────────────────────────────────────
74
+ def repo_web_url(cwd: str) -> str | None:
75
+ try:
76
+ r = subprocess.run(
77
+ ["git", "-C", cwd, "remote", "get-url", "origin"],
78
+ capture_output=True, text=True, timeout=0.3,
79
+ )
80
+ if r.returncode != 0:
81
+ return None
82
+ url = r.stdout.strip()
83
+ if url.startswith("git@"):
84
+ host, path = url[4:].split(":", 1)
85
+ url = f"https://{host}/{path}"
86
+ if url.endswith(".git"):
87
+ url = url[:-4]
88
+ return url
89
+ except Exception:
90
+ return None
91
+
92
+
93
+ def pr_info(cwd: str, branch: str) -> dict:
94
+ """Return cached PR info, firing a background refresh if stale."""
95
+ repo_url = repo_web_url(cwd)
96
+ if not repo_url:
97
+ return {"url": None, "fallback": None, "number": None,
98
+ "state": None, "isDraft": None, "ci": None}
99
+ key = hashlib.sha1(f"{repo_url}@{branch}".encode()).hexdigest()
100
+ cache = load_cache()
101
+ entry = cache.get(key)
102
+ now = time.time()
103
+ fresh = entry and now - entry.get("ts", 0) < CACHE_TTL
104
+ fallback = f"{repo_url}/tree/{branch}"
105
+ if not fresh:
106
+ try:
107
+ subprocess.Popen(
108
+ [sys.executable, str(Path(__file__).resolve()), "--refresh-pr", key, cwd, branch],
109
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
110
+ stdin=subprocess.DEVNULL, start_new_session=True,
111
+ )
112
+ except Exception:
113
+ pass
114
+ e = entry or {}
115
+ return {
116
+ "url": e.get("url"),
117
+ "fallback": fallback,
118
+ "number": e.get("number"),
119
+ "state": e.get("state"),
120
+ "isDraft": e.get("isDraft"),
121
+ "ci": e.get("ci"),
122
+ }
123
+
124
+
125
+ def refresh_pr(key: str, cwd: str, branch: str) -> None:
126
+ info: dict = {"url": None, "number": None, "state": None,
127
+ "isDraft": None, "ci": None}
128
+ try:
129
+ r = subprocess.run(
130
+ ["gh", "pr", "view", "--json",
131
+ "url,number,state,isDraft,statusCheckRollup"],
132
+ capture_output=True, text=True, timeout=10, cwd=cwd,
133
+ )
134
+ if r.returncode == 0 and r.stdout.strip():
135
+ data = json.loads(r.stdout)
136
+ info["url"] = data.get("url")
137
+ info["number"] = data.get("number")
138
+ info["state"] = data.get("state")
139
+ info["isDraft"] = data.get("isDraft")
140
+ # Summarise CI rollup
141
+ rollup = data.get("statusCheckRollup") or []
142
+ info["ci"] = _rollup_ci(rollup)
143
+ except Exception:
144
+ pass
145
+ cache = load_cache()
146
+ cache[key] = {**info, "ts": time.time()}
147
+ save_cache(cache)
148
+
149
+
150
+ def _rollup_ci(rollup: list) -> str | None:
151
+ if not rollup:
152
+ return None
153
+ has_fail = has_pending = False
154
+ for c in rollup:
155
+ s = (c.get("state") or c.get("conclusion") or c.get("status") or "").upper()
156
+ if s in ("FAILURE", "TIMED_OUT", "CANCELLED", "ERROR"):
157
+ has_fail = True
158
+ elif s in ("PENDING", "IN_PROGRESS", "QUEUED", "", "EXPECTED"):
159
+ has_pending = True
160
+ if has_fail:
161
+ return "failure"
162
+ if has_pending:
163
+ return "pending"
164
+ return "success"
165
+
166
+
167
+ def git_info(cwd: str) -> tuple[str, int | None] | None:
168
+ try:
169
+ r = subprocess.run(
170
+ ["git", "-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"],
171
+ capture_output=True, text=True, timeout=1.0,
172
+ )
173
+ if r.returncode != 0:
174
+ return None
175
+ branch = r.stdout.strip()
176
+ except Exception:
177
+ return None
178
+ dirty: int | None = None
179
+ try:
180
+ s = subprocess.run(
181
+ ["git", "-C", cwd, "status", "--porcelain"],
182
+ capture_output=True, text=True, timeout=2.0,
183
+ )
184
+ if s.returncode == 0:
185
+ dirty = len([l for l in s.stdout.splitlines() if l.strip()])
186
+ except Exception:
187
+ pass
188
+ return branch, dirty
189
+
190
+
191
+ # ─── Workspace discovery ───────────────────────────────────────────────────────
192
+ DEFAULT_BRANCHES = {"main", "master", "develop", "production", "trunk"}
193
+
194
+ CI_ICON = {"success": "✓", "failure": "✗", "pending": "◌"}
195
+ CI_COLOR = {"success": GREEN, "failure": RED, "pending": YELLOW}
196
+ PR_ICONS = {
197
+ "open": "\ue725", # nf-cod-git_pull_request
198
+ "draft": "\uea64", # nf-oct-git_pull_request_draft
199
+ "merged": "\uea6a", # nf-oct-git_merge
200
+ "closed": "\ue729", # nf-cod-git_pull_request_closed
201
+ }
202
+
203
+
204
+ def find_workspace(cwd: str) -> tuple[Path, dict] | None:
205
+ p = Path(cwd).resolve()
206
+ for candidate in [p, *p.parents]:
207
+ meta = candidate / ".workspace-meta.json"
208
+ if meta.is_file():
209
+ try:
210
+ data = json.loads(meta.read_text())
211
+ return candidate, data
212
+ except Exception:
213
+ return None
214
+ return None
215
+
216
+
217
+ def gather_repo_row(repo_path: Path, is_active: bool) -> dict:
218
+ cwd_str = str(repo_path)
219
+ g = git_info(cwd_str)
220
+ name = repo_path.name
221
+ if not g:
222
+ return {"active": is_active, "name": name, "branch": None,
223
+ "branch_url": None, "pr": None, "dirty": None, "no_git": True}
224
+ branch, dirty = g
225
+ pi = pr_info(cwd_str, branch)
226
+ return {
227
+ "active": is_active,
228
+ "name": name,
229
+ "branch": branch,
230
+ "branch_url": pi.get("url") or pi.get("fallback"),
231
+ "pr_num": pi.get("number"),
232
+ "pr_url": pi.get("url"),
233
+ "pr_state": pi.get("state"),
234
+ "pr_draft": pi.get("isDraft"),
235
+ "pr_ci": pi.get("ci"),
236
+ "dirty": dirty,
237
+ "no_git": False,
238
+ }
239
+
240
+
241
+ # ─── Cell renderers ────────────────────────────────────────────────────────────
242
+ def pr_plain(row: dict) -> str:
243
+ if row["no_git"] or not row.get("pr_num"):
244
+ return "—" if not row["no_git"] and row.get("branch") else ""
245
+ s = f"#{row['pr_num']}"
246
+ if row.get("pr_draft"): s += " draft"
247
+ elif row.get("pr_state") == "MERGED": s += " merged"
248
+ elif row.get("pr_state") == "CLOSED": s += " closed"
249
+ ci = row.get("pr_ci")
250
+ if ci:
251
+ s += f" {CI_ICON.get(ci, '')}"
252
+ return s
253
+
254
+
255
+ def pr_styled(row: dict) -> str:
256
+ if row["no_git"] or not row.get("pr_num"):
257
+ return color("—", GRAY) if not row["no_git"] and row.get("branch") else ""
258
+ num = f"#{row['pr_num']}"
259
+ draft = row.get("pr_draft")
260
+ state = row.get("pr_state", "OPEN")
261
+ icon = PR_ICONS.get("draft" if draft else state.lower(), PR_ICONS["open"])
262
+ if draft: c = GRAY
263
+ elif state == "MERGED": c = PURPLE
264
+ elif state == "CLOSED": c = RED
265
+ else: c = GREEN
266
+ label = f"{icon} {num} {'draft' if draft else state.lower()}"
267
+ ci = row.get("pr_ci")
268
+ ci_part = f" {color(CI_ICON.get(ci,''), CI_COLOR.get(ci, GRAY))}" if ci else ""
269
+ return hyperlink(color(label, c), row.get("pr_url")) + ci_part
270
+
271
+
272
+ def dirty_plain(row: dict) -> str:
273
+ if row["no_git"]: return "(no git)"
274
+ if row["dirty"] is None: return "*?"
275
+ return f"*{row['dirty']}" if row["dirty"] > 0 else ""
276
+
277
+
278
+ def dirty_styled(row: dict) -> str:
279
+ if row["no_git"]: return color("(no git)", GRAY)
280
+ if row["dirty"] is None: return color("*?", GRAY)
281
+ return color(f"*{row['dirty']}", RED) if row["dirty"] > 0 else ""
282
+
283
+
284
+ # ─── Table renderer ────────────────────────────────────────────────────────────
285
+ def render_repo_table(rows: list[dict]) -> list[str]:
286
+ headers = ("REPO", "BRANCH", "PR", "DIRTY")
287
+ plain_rows = [(r["name"], r.get("branch") or "", pr_plain(r), dirty_plain(r))
288
+ for r in rows]
289
+ plain_rows.insert(0, headers)
290
+ widths = [max(len(p[i]) for p in plain_rows) for i in range(4)]
291
+
292
+ def pad(s: str, w: int) -> str:
293
+ return s + " " * (w - len(s))
294
+
295
+ header = " " + " ".join(
296
+ color(pad(h, widths[i]), DIM + GRAY) for i, h in enumerate(headers)
297
+ )
298
+
299
+ result = [header]
300
+ for r in rows:
301
+ marker = color("▸ ", PURPLE) if r["active"] else " "
302
+ name_c = PURPLE + BOLD if r["active"] else BLUE
303
+ name_cell = color(r["name"], name_c if not r["no_git"] else DIM + GRAY)
304
+ name_cell += " " * (widths[0] - len(r["name"]))
305
+
306
+ br_plain = r.get("branch") or ""
307
+ br_styled = (hyperlink(color(br_plain, MAGENTA), r.get("branch_url"))
308
+ if br_plain else "")
309
+ br_cell = br_styled + " " * (widths[1] - len(br_plain))
310
+
311
+ pp = pr_plain(r)
312
+ pr_cell = pr_styled(r) + " " * (widths[2] - len(pp))
313
+
314
+ dp = dirty_plain(r)
315
+ d_cell = dirty_styled(r) + " " * (widths[3] - len(dp))
316
+
317
+ result.append(f"{marker}{name_cell} {br_cell} {pr_cell} {d_cell}".rstrip())
318
+ return result
319
+
320
+
321
+ # ─── Main ─────────────────────────────────────────────────────────────────────
322
+ def main() -> None:
323
+ """
324
+ Claude Code calls this script and shows whatever we write to stdout
325
+ as the statusLine — an *additional* line shown below the editor.
326
+ Claude Code's own header (model · tokens · cost) is always shown
327
+ regardless of what we output here.
328
+
329
+ Behaviour contract:
330
+ - Not in a workspace → output nothing (CC shows its default footer)
331
+ - In workspace, all repos on default branch → output nothing
332
+ - In workspace with feature-branch repos → output the repo table only
333
+
334
+ We NEVER output model info, cost, or anything that duplicates CC's
335
+ own display. We NEVER modify the user's existing styling.
336
+ """
337
+ try:
338
+ data = json.load(sys.stdin)
339
+ except Exception:
340
+ data = {}
341
+
342
+ cwd = (data.get("workspace") or {}).get("current_dir") or os.getcwd()
343
+ ws = find_workspace(cwd)
344
+
345
+ if not ws:
346
+ # Not in a workspace — output nothing so CC's default footer is shown unchanged.
347
+ return
348
+
349
+ ws_path, ws_meta = ws
350
+
351
+ # Find which repo (if any) is currently active
352
+ active_repo: str | None = None
353
+ try:
354
+ rel = Path(cwd).resolve().relative_to(ws_path)
355
+ if rel.parts:
356
+ active_repo = rel.parts[0]
357
+ except ValueError:
358
+ pass
359
+
360
+ repo_tasks = []
361
+ for repo in ws_meta.get("repositories") or []:
362
+ dir_name = repo.get("directoryName") or repo.get("name")
363
+ if not dir_name:
364
+ continue
365
+ repo_path = ws_path / dir_name
366
+ if not repo_path.is_dir():
367
+ continue
368
+ repo_tasks.append((repo_path, dir_name == active_repo))
369
+
370
+ if not repo_tasks:
371
+ return
372
+
373
+ max_workers = min(8, len(repo_tasks))
374
+ with ThreadPoolExecutor(max_workers=max_workers) as ex:
375
+ rows = list(ex.map(lambda t: gather_repo_row(*t), repo_tasks))
376
+
377
+ # Filter: only repos on non-default branches (toggle not available in CC)
378
+ rows = [r for r in rows if r.get("branch")
379
+ and r["branch"] not in DEFAULT_BRANCHES]
380
+
381
+ if not rows:
382
+ # All repos are on default branches — nothing interesting to show.
383
+ # Output nothing to preserve CC's default footer.
384
+ return
385
+
386
+ # Output ONLY the table. CC's model/cost header is shown above this independently.
387
+ sys.stdout.write("\n".join(render_repo_table(rows)))
388
+
389
+
390
+ if __name__ == "__main__":
391
+ if len(sys.argv) >= 4 and sys.argv[1] == "--refresh-pr":
392
+ refresh_pr(sys.argv[2], sys.argv[3], sys.argv[4] if len(sys.argv) > 4 else "")
393
+ else:
394
+ main()
@@ -0,0 +1,29 @@
1
+ export type AgentStatus = 'idle' | 'working' | 'waiting' | 'stopped';
2
+
3
+ export interface AgentState {
4
+ sessionId: string;
5
+ workspace: string;
6
+ workspacePath: string;
7
+ pid: number;
8
+ status: AgentStatus;
9
+ startedAt: string;
10
+ lastUpdatedAt: string;
11
+ tmuxPane?: string;
12
+ }
13
+
14
+ export interface DashboardConfig {
15
+ sessionName: string;
16
+ sidebarWidthPercent: number;
17
+ pollIntervalMs: number;
18
+ staleTtlMs: number;
19
+ }
20
+
21
+ export const DASHBOARD_DEFAULTS: DashboardConfig = {
22
+ sessionName: 'ws-dashboard',
23
+ sidebarWidthPercent: 20,
24
+ pollIntervalMs: 2000,
25
+ staleTtlMs: 300_000,
26
+ };
27
+
28
+ /** Marker string embedded in hook commands for detection/removal */
29
+ export const HOOK_MARKER = 'workspace-dashboard-hook';
@@ -0,0 +1,109 @@
1
+ export interface GitHubRepo {
2
+ name: string;
3
+ url: string;
4
+ sshUrl: string;
5
+ owner: {
6
+ login: string;
7
+ };
8
+ description: string;
9
+ isPrivate: boolean;
10
+ }
11
+
12
+ export interface CloneResult {
13
+ repo: GitHubRepo;
14
+ directoryName: string;
15
+ status: 'success' | 'failed';
16
+ error?: string;
17
+ clonedAt?: string;
18
+ }
19
+
20
+ export interface CloneProgress {
21
+ total: number;
22
+ completed: number;
23
+ current: string;
24
+ }
25
+
26
+ export interface RepositoryMetadata {
27
+ name: string;
28
+ directoryName: string;
29
+ owner: string;
30
+ clonedAt: string;
31
+ cloneUrl: string;
32
+ status: 'success' | 'failed';
33
+ error?: string;
34
+ // Enhanced tracking
35
+ lastSynced?: string;
36
+ lastBranchSwitch?: string;
37
+ lastHealthCheck?: string;
38
+ healthStatus?: 'healthy' | 'warning' | 'error';
39
+ }
40
+
41
+ export interface DependencyInfo {
42
+ dependsOn: string[];
43
+ dependedBy: string[];
44
+ lastAnalyzed: string;
45
+ }
46
+
47
+ export interface WorkspaceMetadata {
48
+ workspaceName: string;
49
+ createdAt: string;
50
+ lastModified?: string;
51
+ repositories: RepositoryMetadata[];
52
+ // Original prompt used to create the workspace (from 'nemus -- <prompt>')
53
+ prompt?: string;
54
+ // Dependency information
55
+ dependencies?: {
56
+ [repoName: string]: DependencyInfo;
57
+ };
58
+ // Custom metadata
59
+ tags?: string[];
60
+ description?: string;
61
+ // Archive support
62
+ archivedAt?: string;
63
+ }
64
+
65
+ export interface HealthCheckResult {
66
+ category: string;
67
+ status: 'healthy' | 'warning' | 'error';
68
+ message: string;
69
+ details?: string;
70
+ actionable?: string;
71
+ }
72
+
73
+ export interface GitStatus {
74
+ repo: string;
75
+ branch: string;
76
+ clean: boolean;
77
+ ahead: number;
78
+ behind: number;
79
+ modifiedFiles: number;
80
+ untrackedFiles: number;
81
+ hasRemote: boolean;
82
+ detachedHead: boolean;
83
+ }
84
+
85
+ export interface SuiteEntry {
86
+ repoName: string;
87
+ directoryName: string;
88
+ }
89
+
90
+ export interface PostCloneHook {
91
+ repoName?: string; // If omitted, runs for all repos
92
+ commands: string[]; // Run sequentially in repo dir
93
+ description?: string;
94
+ continueOnError?: boolean; // Default false
95
+ }
96
+
97
+ export interface WorkspaceSuite {
98
+ name: string;
99
+ description: string;
100
+ entries: SuiteEntry[];
101
+ createdAt: string;
102
+ updatedAt: string;
103
+ postCloneHooks?: PostCloneHook[];
104
+ }
105
+
106
+ export interface SuitesStore {
107
+ version: 1;
108
+ suites: WorkspaceSuite[];
109
+ }