@cloudcli-ai/cloudcli 1.36.0 → 1.36.2

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 (217) hide show
  1. package/README.de.md +17 -18
  2. package/README.ja.md +16 -17
  3. package/README.ko.md +17 -18
  4. package/README.md +19 -20
  5. package/README.ru.md +17 -18
  6. package/README.tr.md +17 -18
  7. package/README.zh-CN.md +17 -18
  8. package/README.zh-TW.md +17 -18
  9. package/dist/api-docs.html +2 -3
  10. package/dist/assets/{index-S6cIy7hj.js → index-CDGzV0LN.js} +264 -259
  11. package/dist/assets/index-DLmhQ15L.css +32 -0
  12. package/dist/assets/{vendor-xterm-CJZjLICi.js → vendor-xterm-CS4rQ5Mr.js} +12 -12
  13. package/dist/index.html +3 -3
  14. package/dist-server/server/claude-sdk.js +37 -90
  15. package/dist-server/server/claude-sdk.js.map +1 -1
  16. package/dist-server/server/cli.js +1 -4
  17. package/dist-server/server/cli.js.map +1 -1
  18. package/dist-server/server/cursor-cli.js +12 -11
  19. package/dist-server/server/cursor-cli.js.map +1 -1
  20. package/dist-server/server/index.js +10 -141
  21. package/dist-server/server/index.js.map +1 -1
  22. package/dist-server/server/modules/assets/assets.routes.js +89 -0
  23. package/dist-server/server/modules/assets/assets.routes.js.map +1 -0
  24. package/dist-server/server/modules/assets/index.js +4 -0
  25. package/dist-server/server/modules/assets/index.js.map +1 -0
  26. package/dist-server/server/modules/assets/services/image-assets.service.js +57 -0
  27. package/dist-server/server/modules/assets/services/image-assets.service.js.map +1 -0
  28. package/dist-server/server/modules/assets/tests/image-assets.service.test.js +36 -0
  29. package/dist-server/server/modules/assets/tests/image-assets.service.test.js.map +1 -0
  30. package/dist-server/server/modules/browser-use/browser-use.service.js +6 -3
  31. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
  32. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -2
  33. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
  34. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
  35. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  36. package/dist-server/server/modules/projects/services/project-clone.service.js +2 -1
  37. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +28 -0
  39. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  40. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -1
  41. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  42. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +36 -1
  43. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +3 -17
  45. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
  47. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  48. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +49 -13
  49. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +0 -1
  51. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +8 -14
  53. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  54. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +24 -5
  55. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +10 -21
  57. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  58. package/dist-server/server/modules/providers/provider.registry.js +0 -2
  59. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  60. package/dist-server/server/modules/providers/provider.routes.js +0 -1
  61. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  62. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +7 -14
  63. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  64. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  65. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  66. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +3 -77
  67. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  68. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +0 -1
  69. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +0 -13
  71. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  72. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +96 -0
  73. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
  74. package/dist-server/server/modules/providers/tests/mcp.test.js +3 -29
  75. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  76. package/dist-server/server/modules/providers/tests/opencode-models.test.js +7 -0
  77. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  78. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +117 -0
  79. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  80. package/dist-server/server/modules/providers/tests/provider-image-history.test.js +147 -0
  81. package/dist-server/server/modules/providers/tests/provider-image-history.test.js.map +1 -0
  82. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +8 -8
  83. package/dist-server/server/modules/providers/tests/skills.test.js +3 -26
  84. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  85. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +14 -0
  86. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  87. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +36 -2
  88. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  89. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +3 -12
  90. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  91. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js +33 -0
  92. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js.map +1 -0
  93. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +37 -4
  94. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  95. package/dist-server/server/openai-codex.js +8 -2
  96. package/dist-server/server/openai-codex.js.map +1 -1
  97. package/dist-server/server/opencode-cli.js +43 -6
  98. package/dist-server/server/opencode-cli.js.map +1 -1
  99. package/dist-server/server/opencode-cli.test.js +103 -3
  100. package/dist-server/server/opencode-cli.test.js.map +1 -1
  101. package/dist-server/server/routes/agent.js +9 -19
  102. package/dist-server/server/routes/agent.js.map +1 -1
  103. package/dist-server/server/routes/commands.js +1 -2
  104. package/dist-server/server/routes/commands.js.map +1 -1
  105. package/dist-server/server/routes/git.js +179 -53
  106. package/dist-server/server/routes/git.js.map +1 -1
  107. package/dist-server/server/routes/git.test.js +80 -0
  108. package/dist-server/server/routes/git.test.js.map +1 -0
  109. package/dist-server/server/routes/taskmaster.js +3 -1
  110. package/dist-server/server/routes/taskmaster.js.map +1 -1
  111. package/dist-server/server/routes/user.js +2 -1
  112. package/dist-server/server/routes/user.js.map +1 -1
  113. package/dist-server/server/shared/image-attachments.js +270 -0
  114. package/dist-server/server/shared/image-attachments.js.map +1 -0
  115. package/dist-server/server/shared/tests/image-attachments.test.js +220 -0
  116. package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
  117. package/dist-server/server/shared/utils.js +43 -0
  118. package/dist-server/server/shared/utils.js.map +1 -1
  119. package/dist-server/server/utils/gitConfig.js +2 -1
  120. package/dist-server/server/utils/gitConfig.js.map +1 -1
  121. package/dist-server/server/utils/plugin-process-manager.js +2 -1
  122. package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
  123. package/electron/launcher/launcher.js +1 -1
  124. package/package.json +4 -4
  125. package/public/api-docs.html +2 -3
  126. package/server/claude-sdk.js +37 -105
  127. package/server/cli.js +1 -4
  128. package/server/cursor-cli.js +13 -12
  129. package/server/index.js +11 -159
  130. package/server/modules/assets/assets.routes.ts +103 -0
  131. package/server/modules/assets/index.ts +3 -0
  132. package/server/modules/assets/services/image-assets.service.ts +82 -0
  133. package/server/modules/assets/tests/image-assets.service.test.ts +46 -0
  134. package/server/modules/browser-use/browser-use.service.ts +7 -3
  135. package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -2
  136. package/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
  137. package/server/modules/projects/services/project-clone.service.ts +3 -1
  138. package/server/modules/providers/README.md +1 -7
  139. package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
  140. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +60 -1
  141. package/server/modules/providers/list/codex/codex-sessions.provider.ts +41 -1
  142. package/server/modules/providers/list/cursor/cursor-models.provider.ts +3 -17
  143. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +7 -1
  144. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +56 -15
  145. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +0 -1
  146. package/server/modules/providers/list/opencode/opencode-models.provider.ts +15 -19
  147. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +25 -4
  148. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +10 -21
  149. package/server/modules/providers/provider.registry.ts +0 -2
  150. package/server/modules/providers/provider.routes.ts +0 -1
  151. package/server/modules/providers/services/provider-capabilities.service.ts +7 -14
  152. package/server/modules/providers/services/provider-models.service.ts +1 -1
  153. package/server/modules/providers/services/session-conversations-search.service.ts +3 -93
  154. package/server/modules/providers/services/session-synchronizer.service.ts +0 -1
  155. package/server/modules/providers/services/sessions-watcher.service.ts +0 -14
  156. package/server/modules/providers/tests/codex-sessions.test.ts +111 -0
  157. package/server/modules/providers/tests/mcp.test.ts +3 -34
  158. package/server/modules/providers/tests/opencode-models.test.ts +7 -0
  159. package/server/modules/providers/tests/opencode-sessions.test.ts +139 -0
  160. package/server/modules/providers/tests/provider-image-history.test.ts +180 -0
  161. package/server/modules/providers/tests/provider-models.service.test.ts +8 -8
  162. package/server/modules/providers/tests/skills.test.ts +3 -42
  163. package/server/modules/websocket/services/chat-run-registry.service.ts +16 -0
  164. package/server/modules/websocket/services/chat-websocket.service.ts +41 -2
  165. package/server/modules/websocket/services/shell-websocket.service.ts +1 -11
  166. package/server/modules/websocket/tests/chat-image-filter.test.ts +44 -0
  167. package/server/modules/websocket/tests/chat-run-registry.test.ts +42 -4
  168. package/server/openai-codex.js +9 -1
  169. package/server/opencode-cli.js +44 -6
  170. package/server/opencode-cli.test.js +112 -3
  171. package/server/routes/agent.js +9 -19
  172. package/server/routes/commands.js +1 -2
  173. package/server/routes/git.js +201 -60
  174. package/server/routes/git.test.js +106 -0
  175. package/server/routes/taskmaster.js +3 -1
  176. package/server/routes/user.js +2 -1
  177. package/server/shared/image-attachments.ts +341 -0
  178. package/server/shared/tests/image-attachments.test.ts +298 -0
  179. package/server/shared/types.ts +1 -1
  180. package/server/shared/utils.ts +45 -0
  181. package/server/utils/gitConfig.js +2 -1
  182. package/server/utils/plugin-process-manager.js +3 -1
  183. package/dist/assets/index-uLUE6Qt1.css +0 -32
  184. package/dist/icons/gemini-ai-icon.svg +0 -1
  185. package/dist-server/server/gemini-cli.js +0 -551
  186. package/dist-server/server/gemini-cli.js.map +0 -1
  187. package/dist-server/server/gemini-response-handler.js +0 -106
  188. package/dist-server/server/gemini-response-handler.js.map +0 -1
  189. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +0 -254
  190. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +0 -1
  191. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +0 -82
  192. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +0 -1
  193. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +0 -24
  194. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +0 -1
  195. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +0 -312
  196. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +0 -1
  197. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +0 -474
  198. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +0 -1
  199. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +0 -40
  200. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +0 -1
  201. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +0 -19
  202. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +0 -1
  203. package/dist-server/server/routes/gemini.js +0 -21
  204. package/dist-server/server/routes/gemini.js.map +0 -1
  205. package/dist-server/server/sessionManager.js +0 -194
  206. package/dist-server/server/sessionManager.js.map +0 -1
  207. package/server/gemini-cli.js +0 -638
  208. package/server/gemini-response-handler.js +0 -117
  209. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +0 -307
  210. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +0 -110
  211. package/server/modules/providers/list/gemini/gemini-models.provider.ts +0 -39
  212. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +0 -405
  213. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +0 -540
  214. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +0 -44
  215. package/server/modules/providers/list/gemini/gemini.provider.ts +0 -27
  216. package/server/routes/gemini.js +0 -25
  217. package/server/sessionManager.js +0 -226
@@ -1,5 +1,6 @@
1
1
  import express from 'express';
2
- import { spawn } from 'child_process';
2
+ // cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
3
+ import spawn from 'cross-spawn';
3
4
  import path from 'path';
4
5
  import { promises as fs } from 'fs';
5
6
  import { projectsDb } from '../modules/database/index.js';
@@ -293,6 +294,76 @@ async function resolveRepositoryFilePath(projectPath, filePath) {
293
294
  }
294
295
 
295
296
  // Get git status for a project
297
+ /**
298
+ * Parses `git status --porcelain=v1 -z` output into the response shape the
299
+ * git panel consumes. NUL-separated entries carry no path quoting, so names
300
+ * with spaces/unicode survive intact (the plain porcelain output quotes and
301
+ * escapes them, which broke the old line-based parser).
302
+ *
303
+ * `staged` lists paths with index-side changes. The UI renders its "Staged"
304
+ * section from this list so it always mirrors the real git index (including
305
+ * files staged outside the app, e.g. via VSCode or the terminal).
306
+ *
307
+ * Exported for tests.
308
+ */
309
+ export function parseGitStatusOutput(statusOutput) {
310
+ const modified = [];
311
+ const added = [];
312
+ const deleted = [];
313
+ const untracked = [];
314
+ const staged = [];
315
+
316
+ const statusEntries = statusOutput.split('\0');
317
+ for (let entryIndex = 0; entryIndex < statusEntries.length; entryIndex++) {
318
+ const entry = statusEntries[entryIndex];
319
+ if (!entry || entry.length < 4) continue;
320
+
321
+ // Porcelain v1: X = index (staged) status, Y = worktree (unstaged) status.
322
+ const indexStatus = entry[0];
323
+ const worktreeStatus = entry[1];
324
+ const file = entry.slice(3);
325
+
326
+ // Renames/copies carry the original path as the following NUL entry;
327
+ // the UI tracks the post-rename path only.
328
+ if (indexStatus === 'R' || indexStatus === 'C') {
329
+ entryIndex += 1;
330
+ }
331
+
332
+ if (indexStatus === '?') {
333
+ untracked.push(file);
334
+ continue;
335
+ }
336
+ if (indexStatus === '!') {
337
+ continue; // ignored files are never reported
338
+ }
339
+
340
+ const isConflict =
341
+ indexStatus === 'U' || worktreeStatus === 'U' ||
342
+ (indexStatus === 'A' && worktreeStatus === 'A') ||
343
+ (indexStatus === 'D' && worktreeStatus === 'D');
344
+ if (isConflict) {
345
+ // Merge conflicts must be resolved in the worktree first; surface them
346
+ // as modified and never as staged.
347
+ modified.push(file);
348
+ continue;
349
+ }
350
+
351
+ if (indexStatus !== ' ') {
352
+ staged.push(file);
353
+ }
354
+
355
+ if (indexStatus === 'D' || worktreeStatus === 'D') {
356
+ deleted.push(file);
357
+ } else if (indexStatus === 'A' || worktreeStatus === 'A') {
358
+ added.push(file);
359
+ } else {
360
+ modified.push(file);
361
+ }
362
+ }
363
+
364
+ return { modified, added, deleted, untracked, staged };
365
+ }
366
+
296
367
  router.get('/status', async (req, res) => {
297
368
  const { project } = req.query;
298
369
 
@@ -309,30 +380,8 @@ router.get('/status', async (req, res) => {
309
380
  const branch = await getCurrentBranchName(projectPath);
310
381
  const hasCommits = await repositoryHasCommits(projectPath);
311
382
 
312
- // Get git status
313
- const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain'], { cwd: projectPath });
314
-
315
- const modified = [];
316
- const added = [];
317
- const deleted = [];
318
- const untracked = [];
319
-
320
- statusOutput.split('\n').forEach(line => {
321
- if (!line.trim()) return;
322
-
323
- const status = line.substring(0, 2);
324
- const file = line.substring(3);
325
-
326
- if (status === 'M ' || status === ' M' || status === 'MM') {
327
- modified.push(file);
328
- } else if (status === 'A ' || status === 'AM') {
329
- added.push(file);
330
- } else if (status === 'D ' || status === ' D') {
331
- deleted.push(file);
332
- } else if (status === '??') {
333
- untracked.push(file);
334
- }
335
- });
383
+ const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain=v1', '-z'], { cwd: projectPath });
384
+ const { modified, added, deleted, untracked, staged } = parseGitStatusOutput(statusOutput);
336
385
 
337
386
  res.json({
338
387
  branch,
@@ -340,7 +389,8 @@ router.get('/status', async (req, res) => {
340
389
  modified,
341
390
  added,
342
391
  deleted,
343
- untracked
392
+ untracked,
393
+ staged
344
394
  });
345
395
  } catch (error) {
346
396
  console.error('Git status error:', error);
@@ -593,6 +643,64 @@ router.post('/commit', async (req, res) => {
593
643
  }
594
644
  });
595
645
 
646
+ // Stage files (git add). Mirrors what the UI shows as the "Staged" section,
647
+ // so the app's staging state and the real git index never drift apart.
648
+ router.post('/stage', async (req, res) => {
649
+ const { project, files } = req.body;
650
+
651
+ if (!project || !Array.isArray(files) || files.length === 0) {
652
+ return res.status(400).json({ error: 'Project id and files are required' });
653
+ }
654
+
655
+ try {
656
+ const projectPath = await getActualProjectPath(project);
657
+ await validateGitRepository(projectPath);
658
+ const repositoryRootPath = await getRepositoryRootPath(projectPath);
659
+
660
+ for (const file of files) {
661
+ const { repositoryRelativeFilePath } = await resolveRepositoryFilePath(projectPath, file);
662
+ await spawnAsync('git', ['add', '--', repositoryRelativeFilePath], { cwd: repositoryRootPath });
663
+ }
664
+
665
+ res.json({ success: true });
666
+ } catch (error) {
667
+ console.error('Git stage error:', error);
668
+ res.status(500).json({ error: error.message });
669
+ }
670
+ });
671
+
672
+ // Unstage files (remove from the index, keep the worktree changes)
673
+ router.post('/unstage', async (req, res) => {
674
+ const { project, files } = req.body;
675
+
676
+ if (!project || !Array.isArray(files) || files.length === 0) {
677
+ return res.status(400).json({ error: 'Project id and files are required' });
678
+ }
679
+
680
+ try {
681
+ const projectPath = await getActualProjectPath(project);
682
+ await validateGitRepository(projectPath);
683
+ const repositoryRootPath = await getRepositoryRootPath(projectPath);
684
+ const hasCommits = await repositoryHasCommits(projectPath);
685
+
686
+ for (const file of files) {
687
+ const { repositoryRelativeFilePath } = await resolveRepositoryFilePath(projectPath, file);
688
+ if (hasCommits) {
689
+ await spawnAsync('git', ['reset', 'HEAD', '--', repositoryRelativeFilePath], { cwd: repositoryRootPath });
690
+ } else {
691
+ // No HEAD to reset against before the first commit; dropping the
692
+ // index entry is the only way to unstage while keeping the file.
693
+ await spawnAsync('git', ['rm', '--cached', '-r', '--force', '--', repositoryRelativeFilePath], { cwd: repositoryRootPath });
694
+ }
695
+ }
696
+
697
+ res.json({ success: true });
698
+ } catch (error) {
699
+ console.error('Git unstage error:', error);
700
+ res.status(500).json({ error: error.message });
701
+ }
702
+ });
703
+
596
704
  // Revert latest local commit (keeps changes staged)
597
705
  router.post('/revert-local-commit', async (req, res) => {
598
706
  const { project } = req.body;
@@ -754,10 +862,57 @@ router.post('/delete-branch', async (req, res) => {
754
862
  }
755
863
  });
756
864
 
757
- // Get recent commits
865
+ // Fields are joined with the ASCII unit separator so pipes (or anything else
866
+ // typed into a commit subject) cannot break parsing.
867
+ const GIT_LOG_FIELD_SEPARATOR = '\u001f';
868
+ const GIT_LOG_PRETTY_FORMAT = '%H%x1f%P%x1f%D%x1f%an%x1f%ae%x1f%ad%x1f%s';
869
+
870
+ /**
871
+ * Parses `git log --shortstat` output produced with GIT_LOG_PRETTY_FORMAT.
872
+ *
873
+ * Each commit is one format line (hash, parent hashes, ref decorations,
874
+ * author, email, date, subject) optionally followed by its `--shortstat`
875
+ * summary line ("N files changed, ..."). Parents and refs feed the commit
876
+ * graph rendered by the History view; merge commits carry no shortstat line,
877
+ * so their `stats` stays empty.
878
+ *
879
+ * Exported for tests.
880
+ */
881
+ export function parseGitLogWithStats(stdout) {
882
+ const commits = [];
883
+
884
+ for (const rawLine of stdout.split('\n')) {
885
+ const line = rawLine.trimEnd();
886
+ if (!line.trim()) continue;
887
+
888
+ if (line.includes(GIT_LOG_FIELD_SEPARATOR)) {
889
+ const [hash, parents, refs, author, email, date, ...messageParts] = line.split(GIT_LOG_FIELD_SEPARATOR);
890
+ commits.push({
891
+ hash,
892
+ parents: parents ? parents.split(' ').filter(Boolean) : [],
893
+ // `%D` decorations, e.g. "HEAD -> main", "origin/main", "tag: v1.0".
894
+ refs: refs ? refs.split(', ').filter(Boolean) : [],
895
+ author,
896
+ email,
897
+ date,
898
+ message: messageParts.join(GIT_LOG_FIELD_SEPARATOR),
899
+ stats: ''
900
+ });
901
+ continue;
902
+ }
903
+
904
+ if (commits.length > 0 && /files? changed/.test(line)) {
905
+ commits[commits.length - 1].stats = line.trim();
906
+ }
907
+ }
908
+
909
+ return commits;
910
+ }
911
+
912
+ // Get recent commits (across all branches, in graph order)
758
913
  router.get('/commits', async (req, res) => {
759
914
  const { project, limit = 10 } = req.query;
760
-
915
+
761
916
  if (!project) {
762
917
  return res.status(400).json({ error: 'Project id is required' });
763
918
  }
@@ -769,42 +924,28 @@ router.get('/commits', async (req, res) => {
769
924
  const safeLimit = Number.isFinite(parsedLimit) && parsedLimit > 0
770
925
  ? Math.min(parsedLimit, 100)
771
926
  : 10;
772
-
773
- // Get commit log with stats
927
+
928
+ // Branches/remotes/tags (not --all, which would drag in refs/stash) with
929
+ // `--topo-order` guarantee children appear before their parents across
930
+ // every branch, which the frontend lane-assignment relies on.
931
+ // `--shortstat` replaces the previous per-commit `git show --stat` calls.
774
932
  const { stdout } = await spawnAsync(
775
933
  'git',
776
- ['log', '--pretty=format:%H|%an|%ae|%ad|%s', '--date=iso-strict', '-n', String(safeLimit)],
934
+ [
935
+ 'log',
936
+ '--branches',
937
+ '--remotes',
938
+ '--tags',
939
+ '--topo-order',
940
+ '--shortstat',
941
+ `--pretty=format:${GIT_LOG_PRETTY_FORMAT}`,
942
+ '--date=iso-strict',
943
+ '-n', String(safeLimit)
944
+ ],
777
945
  { cwd: projectPath },
778
946
  );
779
-
780
- const commits = stdout
781
- .split('\n')
782
- .filter(line => line.trim())
783
- .map(line => {
784
- const [hash, author, email, date, ...messageParts] = line.split('|');
785
- return {
786
- hash,
787
- author,
788
- email,
789
- date,
790
- message: messageParts.join('|')
791
- };
792
- });
793
-
794
- // Get stats for each commit
795
- for (const commit of commits) {
796
- try {
797
- const { stdout: stats } = await spawnAsync(
798
- 'git', ['show', '--stat', '--format=', commit.hash],
799
- { cwd: projectPath }
800
- );
801
- commit.stats = stats.trim().split('\n').pop(); // Get the summary line
802
- } catch (error) {
803
- commit.stats = '';
804
- }
805
- }
806
-
807
- res.json({ commits });
947
+
948
+ res.json({ commits: parseGitLogWithStats(stdout) });
808
949
  } catch (error) {
809
950
  console.error('Git commits error:', error);
810
951
  res.json({ error: error.message });
@@ -0,0 +1,106 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { parseGitLogWithStats, parseGitStatusOutput } from './git.js';
5
+
6
+ // Builds `git status --porcelain=v1 -z` output: NUL-separated entries with a
7
+ // trailing NUL, exactly as git emits it.
8
+ const porcelain = (...entries) => entries.join('\0') + '\0';
9
+
10
+ test('parseGitStatusOutput buckets files and reports index-side staging', () => {
11
+ const output = porcelain(
12
+ 'M staged-modified.ts',
13
+ ' M unstaged-modified.ts',
14
+ 'MM staged-and-unstaged.ts',
15
+ 'A staged-new.ts',
16
+ 'D staged-deleted.ts',
17
+ ' D unstaged-deleted.ts',
18
+ '?? untracked.ts',
19
+ );
20
+
21
+ const result = parseGitStatusOutput(output);
22
+
23
+ assert.deepEqual(result.modified, ['staged-modified.ts', 'unstaged-modified.ts', 'staged-and-unstaged.ts']);
24
+ assert.deepEqual(result.added, ['staged-new.ts']);
25
+ assert.deepEqual(result.deleted, ['staged-deleted.ts', 'unstaged-deleted.ts']);
26
+ assert.deepEqual(result.untracked, ['untracked.ts']);
27
+ // Only index-side (X) changes count as staged.
28
+ assert.deepEqual(result.staged, [
29
+ 'staged-modified.ts',
30
+ 'staged-and-unstaged.ts',
31
+ 'staged-new.ts',
32
+ 'staged-deleted.ts',
33
+ ]);
34
+ });
35
+
36
+ test('parseGitStatusOutput keeps paths with spaces intact (-z output has no quoting)', () => {
37
+ const result = parseGitStatusOutput(porcelain('M src/my folder/some file.ts'));
38
+ assert.deepEqual(result.modified, ['src/my folder/some file.ts']);
39
+ assert.deepEqual(result.staged, ['src/my folder/some file.ts']);
40
+ });
41
+
42
+ test('parseGitStatusOutput tracks the post-rename path and skips the original', () => {
43
+ const output = porcelain('R renamed-to.ts', 'renamed-from.ts', ' M other.ts');
44
+ const result = parseGitStatusOutput(output);
45
+
46
+ assert.deepEqual(result.modified, ['renamed-to.ts', 'other.ts']);
47
+ assert.deepEqual(result.staged, ['renamed-to.ts']);
48
+ // The pre-rename path is metadata, not a change entry.
49
+ assert.equal(JSON.stringify(result).includes('renamed-from.ts'), false);
50
+ });
51
+
52
+ test('parseGitStatusOutput never reports merge conflicts as staged', () => {
53
+ const output = porcelain('UU conflicted.ts', 'AA both-added.ts', 'DD both-deleted.ts');
54
+ const result = parseGitStatusOutput(output);
55
+
56
+ assert.deepEqual(result.modified, ['conflicted.ts', 'both-added.ts', 'both-deleted.ts']);
57
+ assert.deepEqual(result.staged, []);
58
+ });
59
+
60
+ test('parseGitStatusOutput handles empty output', () => {
61
+ assert.deepEqual(parseGitStatusOutput(''), {
62
+ modified: [],
63
+ added: [],
64
+ deleted: [],
65
+ untracked: [],
66
+ staged: [],
67
+ });
68
+ });
69
+
70
+ // Builds one `git log --pretty=format:%H%x1f%P%x1f%D%x1f%an%x1f%ae%x1f%ad%x1f%s` line.
71
+ const US = '';
72
+ const logLine = (hash, parents, refs, subject) =>
73
+ [hash, parents, refs, 'Alice', 'a@x.com', '2026-07-06T10:00:00+03:00', subject].join(US);
74
+
75
+ test('parseGitLogWithStats parses commits with parents, refs, and shortstat lines', () => {
76
+ const output = [
77
+ logLine('c3', 'c2', 'HEAD -> main, origin/main, tag: v1.0', 'feat: add | pipes | to subject'),
78
+ ' 3 files changed, 10 insertions(+), 2 deletions(-)',
79
+ '',
80
+ logLine('c2', 'c1 c0', '', 'Merge branch feature'),
81
+ '',
82
+ logLine('c0', '', '', 'initial commit'),
83
+ ' 1 file changed, 1 insertion(+)',
84
+ ].join('\n');
85
+
86
+ const commits = parseGitLogWithStats(output);
87
+
88
+ assert.equal(commits.length, 3);
89
+ assert.deepEqual(commits[0].parents, ['c2']);
90
+ assert.deepEqual(commits[0].refs, ['HEAD -> main', 'origin/main', 'tag: v1.0']);
91
+ // Pipes in the subject survive because fields are joined with .
92
+ assert.equal(commits[0].message, 'feat: add | pipes | to subject');
93
+ assert.equal(commits[0].stats, '3 files changed, 10 insertions(+), 2 deletions(-)');
94
+
95
+ // Merge commit: two parents, no shortstat line.
96
+ assert.deepEqual(commits[1].parents, ['c1', 'c0']);
97
+ assert.equal(commits[1].stats, '');
98
+
99
+ // Root commit: no parents.
100
+ assert.deepEqual(commits[2].parents, []);
101
+ assert.equal(commits[2].stats, '1 file changed, 1 insertion(+)');
102
+ });
103
+
104
+ test('parseGitLogWithStats handles empty output', () => {
105
+ assert.deepEqual(parseGitLogWithStats(''), []);
106
+ });
@@ -12,7 +12,9 @@ import express from 'express';
12
12
  import fs from 'fs';
13
13
  import path from 'path';
14
14
  import { promises as fsPromises } from 'fs';
15
- import { spawn } from 'child_process';
15
+ // cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution — required
16
+ // here since task-master/npx are .cmd shims on Windows.
17
+ import spawn from 'cross-spawn';
16
18
  import { projectsDb } from '../modules/database/index.js';
17
19
  import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
18
20
  import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
@@ -1,8 +1,9 @@
1
1
  import express from 'express';
2
+ // cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
3
+ import spawn from 'cross-spawn';
2
4
  import { userDb } from '../modules/database/index.js';
3
5
  import { authenticateToken } from '../middleware/auth.js';
4
6
  import { getSystemGitConfig } from '../utils/gitConfig.js';
5
- import { spawn } from 'child_process';
6
7
 
7
8
  const router = express.Router();
8
9