@cloudcli-ai/cloudcli 1.37.0 → 1.37.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 (242) hide show
  1. package/dist/assets/abnfDiagram-N423BO3Z-C4UAoHJb.js +1 -0
  2. package/dist/assets/arc-B8jF-ja9.js +1 -0
  3. package/dist/assets/architectureDiagram-T3A2C74G-C_Wur_CU.js +36 -0
  4. package/dist/assets/blockDiagram-VBNYF7ZC-Cq4P1i8t.js +132 -0
  5. package/dist/assets/c4Diagram-5PPSVZJV-C1wG4eUj.js +10 -0
  6. package/dist/assets/channel-_falYbU1.js +1 -0
  7. package/dist/assets/chunk-2GRJ4B5K-B8fnBG6E.js +1 -0
  8. package/dist/assets/chunk-2Q5K7J3B-WdfPooPJ.js +1 -0
  9. package/dist/assets/chunk-5RXB4S5H-DhZJuG2E.js +231 -0
  10. package/dist/assets/chunk-5VM5RSS4-B0rjslal.js +15 -0
  11. package/dist/assets/chunk-6Q2QTUOP-CG2Oj4jL.js +88 -0
  12. package/dist/assets/chunk-GF5L2VYU-rrl2lRxY.js +206 -0
  13. package/dist/assets/chunk-JWPE2WC7-CjVbrVMA.js +1 -0
  14. package/dist/assets/chunk-KBJHAD2P-CRzKgU8S.js +1 -0
  15. package/dist/assets/chunk-RYQCIY6F-DBz4GTZF.js +1 -0
  16. package/dist/assets/chunk-XXDRQBXY-wHxalsXk.js +1 -0
  17. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +1 -0
  18. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +1 -0
  19. package/dist/assets/cose-bilkent-JH36ORCC-s1hygY8e.js +1 -0
  20. package/dist/assets/cynefin-VYW2F7L2-C4LGV15i.js +178 -0
  21. package/dist/assets/cynefinDiagram-MW4NZA55-C5uhu7B3.js +62 -0
  22. package/dist/assets/cytoscape.esm-C10L9Icf.js +331 -0
  23. package/dist/assets/dagre-VZM6K2ZE-B-OBTewm.js +4 -0
  24. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  25. package/dist/assets/diagram-7IWD3JNH-BjGKQxih.js +30 -0
  26. package/dist/assets/diagram-B4RE2ZJO-BMKz1X2L.js +3 -0
  27. package/dist/assets/diagram-LBJQPF4R-DNh-GhKC.js +24 -0
  28. package/dist/assets/diagram-Q27KOJAE-BaTG9jPT.js +24 -0
  29. package/dist/assets/diagram-UB23O5K3-BUwo2OvJ.js +41 -0
  30. package/dist/assets/ebnfDiagram-BXEA7PRR-DAZgfzX2.js +1 -0
  31. package/dist/assets/erDiagram-JOGREHBK-1w1CcGEb.js +85 -0
  32. package/dist/assets/flowDiagram-UKHOOZJN-D4EII8Jj.js +156 -0
  33. package/dist/assets/ganttDiagram-PKOTCBZU-ChvJZAgB.js +292 -0
  34. package/dist/assets/gitGraphDiagram-DS77QQ5N-DtOn41S8.js +106 -0
  35. package/dist/assets/graph-Cx-EqisW.js +1 -0
  36. package/dist/assets/index-CzPLBvXT.css +32 -0
  37. package/dist/assets/index-DFkBBuSA.js +1549 -0
  38. package/dist/assets/infoDiagram-6WML65LV-DN_PVEJZ.js +2 -0
  39. package/dist/assets/init-Gi6I4Gst.js +1 -0
  40. package/dist/assets/ishikawaDiagram-WSZJBQD7-DbkF1kXb.js +70 -0
  41. package/dist/assets/journeyDiagram-NVQOT4AX-pJwmhddE.js +139 -0
  42. package/dist/assets/kanban-definition-27J2QSJJ-PPAPgNp5.js +89 -0
  43. package/dist/assets/layout-D6BstSzF.js +1 -0
  44. package/dist/assets/linear-iPR-J9f7.js +1 -0
  45. package/dist/assets/map-CV9uif0g.js +1 -0
  46. package/dist/assets/mermaid.core-BEUL86Z6.js +312 -0
  47. package/dist/assets/mindmap-definition-FAOFIHXS-BvZoBib7.js +96 -0
  48. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  49. package/dist/assets/pegDiagram-VL7TDLO6-DhYreaUd.js +1 -0
  50. package/dist/assets/pieDiagram-7S7Q4E2Y-CrOHlyD0.js +39 -0
  51. package/dist/assets/quadrantDiagram-CIZ2JOQS-D4g5yH3t.js +7 -0
  52. package/dist/assets/railroadDiagram-AXF67PYL-Dj39lHlf.js +1 -0
  53. package/dist/assets/requirementDiagram-LRYGKXZP-1AS4Zgm_.js +84 -0
  54. package/dist/assets/sankeyDiagram-W5VNT64P-CKlMn19P.js +40 -0
  55. package/dist/assets/sequenceDiagram-SI44F4Z6-B_j3qyvo.js +162 -0
  56. package/dist/assets/sizeCapture-X5ZJPWSS-Bp2Op16v.js +1 -0
  57. package/dist/assets/stateDiagram-OKZ733FA-C0cN6DoY.js +1 -0
  58. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +1 -0
  59. package/dist/assets/swimlanes-SLNWSIFB-BEvXOV9b.js +2 -0
  60. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +8 -0
  61. package/dist/assets/timeline-definition-Z64GVDOM-zdJQinvZ.js +120 -0
  62. package/dist/assets/vennDiagram-T6HMQDX7-DuF1uZMY.js +34 -0
  63. package/dist/assets/wardleyDiagram-T6FBY63Y-B9AlLQup.js +78 -0
  64. package/dist/assets/xychartDiagram-ELKLHX3M-DTJX48Qg.js +7 -0
  65. package/dist/index.html +2 -2
  66. package/dist-server/server/index.js +1 -2
  67. package/dist-server/server/index.js.map +1 -1
  68. package/dist-server/server/modules/agent/agent.module.js +2 -1
  69. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  70. package/dist-server/server/modules/auth/auth.middleware.js +1 -1
  71. package/dist-server/server/modules/auth/auth.middleware.js.map +1 -1
  72. package/dist-server/server/modules/browser-use/browser-use-runtime.js +10 -0
  73. package/dist-server/server/modules/browser-use/browser-use-runtime.js.map +1 -0
  74. package/dist-server/server/modules/browser-use/browser-use.service.js +3 -6
  75. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
  76. package/dist-server/server/modules/browser-use/index.js +2 -0
  77. package/dist-server/server/modules/browser-use/index.js.map +1 -1
  78. package/dist-server/server/modules/cli/cli.js +9 -3
  79. package/dist-server/server/modules/cli/cli.js.map +1 -1
  80. package/dist-server/server/modules/cli/cli.module.js +0 -1
  81. package/dist-server/server/modules/cli/cli.module.js.map +1 -1
  82. package/dist-server/server/modules/cli/cli.service.js +0 -18
  83. package/dist-server/server/modules/cli/cli.service.js.map +1 -1
  84. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +154 -0
  85. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js.map +1 -0
  86. package/dist-server/server/modules/cli/tests/cli.service.test.js +0 -3
  87. package/dist-server/server/modules/cli/tests/cli.service.test.js.map +1 -1
  88. package/dist-server/server/modules/commands/commands.routes.js +3 -3
  89. package/dist-server/server/modules/commands/commands.routes.js.map +1 -1
  90. package/dist-server/server/modules/commands/tests/commands.test.js +2 -7
  91. package/dist-server/server/modules/commands/tests/commands.test.js.map +1 -1
  92. package/dist-server/server/modules/database/index.js +2 -0
  93. package/dist-server/server/modules/database/index.js.map +1 -1
  94. package/dist-server/server/modules/database/migrations.js +18 -1
  95. package/dist-server/server/modules/database/migrations.js.map +1 -1
  96. package/dist-server/server/modules/database/repositories/provider-models.js +112 -0
  97. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -0
  98. package/dist-server/server/modules/database/repositories/sessions.db.js +63 -7
  99. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  100. package/dist-server/server/modules/database/schema.js +28 -3
  101. package/dist-server/server/modules/database/schema.js.map +1 -1
  102. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +102 -0
  103. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -0
  104. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -1
  105. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
  106. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +22 -0
  107. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  108. package/dist-server/server/modules/file-tree/file-tree.module.js +5 -1
  109. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  110. package/dist-server/server/modules/file-tree/file-tree.service.js +60 -20
  111. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  112. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +143 -16
  113. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  114. package/dist-server/server/modules/git/git-branch.service.js +15 -0
  115. package/dist-server/server/modules/git/git-branch.service.js.map +1 -0
  116. package/dist-server/server/modules/git/git.routes.js +15 -7
  117. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  118. package/dist-server/server/modules/git/tests/git-branch.service.test.js +52 -0
  119. package/dist-server/server/modules/git/tests/git-branch.service.test.js.map +1 -0
  120. package/dist-server/server/modules/git/tests/git-init.routes.test.js +84 -0
  121. package/dist-server/server/modules/git/tests/git-init.routes.test.js.map +1 -1
  122. package/dist-server/server/modules/notifications/index.js +3 -1
  123. package/dist-server/server/modules/notifications/index.js.map +1 -1
  124. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +22 -1
  125. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  126. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +46 -14
  127. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  128. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +259 -60
  129. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  130. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +58 -71
  131. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  132. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +1 -54
  133. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  134. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +7 -7
  135. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  136. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +32 -669
  137. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
  138. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +105 -328
  139. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  140. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +0 -12
  141. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  142. package/dist-server/server/modules/providers/provider.routes.js +144 -31
  143. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  144. package/dist-server/server/modules/providers/services/provider-models.service.js +167 -199
  145. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  146. package/dist-server/server/modules/providers/services/provider-runtime.service.js +2 -2
  147. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  148. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +75 -11
  149. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  150. package/dist-server/server/modules/providers/services/sessions.service.js +62 -3
  151. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  152. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +53 -17
  153. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  154. package/dist-server/server/modules/providers/tests/opencode-models.test.js +15 -126
  155. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  156. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +5 -5
  157. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  158. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +175 -292
  159. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  160. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +2 -6
  161. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -1
  162. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +1 -0
  163. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  164. package/dist-server/server/modules/providers/tests/provider.routes.test.js +161 -0
  165. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -0
  166. package/dist-server/server/modules/providers/tests/sessions.service.test.js +87 -0
  167. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -0
  168. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +5 -1
  169. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  170. package/dist-server/server/shared/utils.js +8 -0
  171. package/dist-server/server/shared/utils.js.map +1 -1
  172. package/package.json +8 -4
  173. package/scripts/promote-dist-server.mjs +50 -0
  174. package/server/index.ts +1 -2
  175. package/server/modules/agent/agent.module.ts +2 -1
  176. package/server/modules/auth/auth.middleware.ts +2 -2
  177. package/server/modules/browser-use/browser-use-runtime.ts +10 -0
  178. package/server/modules/browser-use/browser-use.service.ts +5 -8
  179. package/server/modules/browser-use/index.ts +3 -0
  180. package/server/modules/cli/cli.module.ts +0 -1
  181. package/server/modules/cli/cli.service.ts +0 -20
  182. package/server/modules/cli/cli.ts +9 -3
  183. package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -0
  184. package/server/modules/cli/tests/cli.service.test.ts +0 -3
  185. package/server/modules/commands/commands.routes.ts +3 -3
  186. package/server/modules/commands/tests/commands.test.ts +2 -7
  187. package/server/modules/database/index.ts +2 -0
  188. package/server/modules/database/migrations.ts +20 -0
  189. package/server/modules/database/repositories/provider-models.ts +168 -0
  190. package/server/modules/database/repositories/sessions.db.ts +85 -8
  191. package/server/modules/database/schema.ts +29 -3
  192. package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -0
  193. package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -1
  194. package/server/modules/database/tests/sessions.db.integration.test.ts +32 -0
  195. package/server/modules/file-tree/file-tree.module.ts +5 -1
  196. package/server/modules/file-tree/file-tree.service.ts +81 -22
  197. package/server/modules/file-tree/tests/file-tree.service.test.ts +167 -16
  198. package/server/modules/git/git-branch.service.ts +43 -0
  199. package/server/modules/git/git.routes.ts +16 -9
  200. package/server/modules/git/tests/git-branch.service.test.ts +60 -0
  201. package/server/modules/git/tests/git-init.routes.test.ts +83 -0
  202. package/server/modules/notifications/index.ts +2 -0
  203. package/server/modules/notifications/services/notification-orchestrator.service.js +24 -1
  204. package/server/modules/providers/list/claude/claude-models.provider.ts +46 -14
  205. package/server/modules/providers/list/claude/claude-runtime.provider.js +280 -67
  206. package/server/modules/providers/list/codex/codex-models.provider.ts +58 -99
  207. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +1 -60
  208. package/server/modules/providers/list/codex/codex-sessions.provider.ts +7 -7
  209. package/server/modules/providers/list/cursor/cursor-models.provider.ts +32 -710
  210. package/server/modules/providers/list/opencode/opencode-models.provider.ts +105 -413
  211. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +0 -13
  212. package/server/modules/providers/provider.routes.ts +190 -33
  213. package/server/modules/providers/services/provider-models.service.ts +223 -288
  214. package/server/modules/providers/services/provider-runtime.service.ts +2 -3
  215. package/server/modules/providers/services/session-conversations-search.service.ts +106 -11
  216. package/server/modules/providers/services/sessions.service.ts +87 -3
  217. package/server/modules/providers/tests/codex-sessions.test.ts +57 -18
  218. package/server/modules/providers/tests/opencode-models.test.ts +34 -135
  219. package/server/modules/providers/tests/opencode-sessions.test.ts +5 -5
  220. package/server/modules/providers/tests/provider-models.service.test.ts +353 -450
  221. package/server/modules/providers/tests/provider-runtime.service.test.ts +2 -6
  222. package/server/modules/providers/tests/provider-token-usage.service.test.ts +1 -0
  223. package/server/modules/providers/tests/provider.routes.test.ts +244 -0
  224. package/server/modules/providers/tests/sessions.service.test.ts +127 -0
  225. package/server/modules/websocket/services/chat-websocket.service.ts +5 -1
  226. package/server/shared/interfaces.ts +4 -6
  227. package/server/shared/types.ts +29 -18
  228. package/server/shared/utils.ts +10 -0
  229. package/server/tsconfig.json +6 -2
  230. package/README.de.md +0 -257
  231. package/README.ja.md +0 -249
  232. package/README.ko.md +0 -249
  233. package/README.md +0 -269
  234. package/README.ru.md +0 -257
  235. package/README.tr.md +0 -258
  236. package/README.zh-CN.md +0 -249
  237. package/README.zh-TW.md +0 -249
  238. package/dist/assets/index-DSlds633.css +0 -32
  239. package/dist/assets/index-eGqPeIEu.js +0 -1531
  240. package/dist-server/server/constants/config.js +0 -6
  241. package/dist-server/server/constants/config.js.map +0 -1
  242. package/server/constants/config.ts +0 -5
@@ -19,6 +19,18 @@ function createDirectoryEntry(name: string, directory: boolean): FileTreeDirecto
19
19
  };
20
20
  }
21
21
 
22
+ /**
23
+ * Adapts a path-keyed listing to the streaming directory contract so tests keep
24
+ * describing directories as plain arrays.
25
+ */
26
+ function createDirectoryReader(
27
+ listDirectory: (directoryPath: string) => FileTreeDirectoryEntry[],
28
+ ): FileTreeFileSystem['openDirectory'] {
29
+ return async function* openDirectory(directoryPath) {
30
+ yield* listDirectory(directoryPath);
31
+ };
32
+ }
33
+
22
34
  function createStats(directory: boolean, mode: number): FileTreeStats {
23
35
  return {
24
36
  size: directory ? 0 : 24,
@@ -40,7 +52,9 @@ function createFakeFileSystem(
40
52
  access: unexpectedOperation,
41
53
  stat: unexpectedOperation,
42
54
  lstat: unexpectedOperation,
43
- readdir: unexpectedOperation,
55
+ openDirectory: () => ({
56
+ [Symbol.asyncIterator]: () => ({ next: unexpectedOperation }),
57
+ }),
44
58
  realpath: unexpectedOperation,
45
59
  readTextFile: unexpectedOperation,
46
60
  writeTextFile: unexpectedOperation,
@@ -73,37 +87,67 @@ function createDependencies(
73
87
  };
74
88
  }
75
89
 
76
- test('listProjectFiles builds a sorted tree and skips generated directories', async () => {
90
+ test('listProjectFiles applies gitignore alongside hard directory exclusions', async () => {
77
91
  const projectRoot = path.resolve('file-tree-test-project');
92
+ const documentationDirectory = path.join(projectRoot, 'docs');
93
+ const buildDocumentationDirectory = path.join(documentationDirectory, 'build');
94
+ const gitDirectory = path.join(projectRoot, '.git');
95
+ const nodeModulesDirectory = path.join(projectRoot, 'node_modules');
78
96
  const sourceDirectory = path.join(projectRoot, 'src');
97
+ const readDirectories: string[] = [];
79
98
  const fileSystem = createFakeFileSystem({
80
99
  access: async () => undefined,
81
- readdir: async (directoryPath) => {
100
+ readTextFile: async (filePath) => {
101
+ assert.equal(filePath, path.join(projectRoot, '.gitignore'));
102
+ return '*.log';
103
+ },
104
+ openDirectory: createDirectoryReader((directoryPath) => {
105
+ readDirectories.push(directoryPath);
82
106
  if (directoryPath === projectRoot) {
83
107
  return [
108
+ createDirectoryEntry('.git', true),
84
109
  createDirectoryEntry('node_modules', true),
85
110
  createDirectoryEntry('README.md', false),
111
+ createDirectoryEntry('docs', true),
86
112
  createDirectoryEntry('src', true),
87
113
  ];
88
114
  }
115
+ if (directoryPath === documentationDirectory) {
116
+ return [createDirectoryEntry('build', true)];
117
+ }
118
+ if (directoryPath === buildDocumentationDirectory) {
119
+ return [createDirectoryEntry('foo.md', false)];
120
+ }
89
121
  if (directoryPath === sourceDirectory) {
90
122
  return [createDirectoryEntry('index.ts', false)];
91
123
  }
92
124
  return [];
93
- },
94
- lstat: async (candidatePath) => createStats(candidatePath === sourceDirectory, 0o754),
125
+ }),
126
+ lstat: async (candidatePath) => createStats(
127
+ candidatePath === documentationDirectory
128
+ || candidatePath === buildDocumentationDirectory
129
+ || candidatePath === sourceDirectory
130
+ || candidatePath === gitDirectory
131
+ || candidatePath === nodeModulesDirectory,
132
+ 0o754,
133
+ ),
95
134
  });
96
135
  const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
97
136
 
98
- const tree = await service.listProjectFiles('project-1');
137
+ const tree = await service.listProjectFiles('project-1', { respectGitignore: true });
99
138
 
100
- assert.deepEqual(tree.map((entry) => entry.name), ['src', 'README.md']);
101
- const sourceEntry = tree[0];
139
+ assert.deepEqual(tree.map((entry) => entry.name), ['docs', 'src', 'README.md']);
140
+ const documentationEntry = tree[0];
141
+ assert.deepEqual(documentationEntry?.children?.map((entry) => entry.name), ['build']);
142
+ assert.deepEqual(documentationEntry?.children?.[0]?.children?.map((entry) => entry.name), ['foo.md']);
143
+ const sourceEntry = tree[1];
102
144
  assert.ok(sourceEntry);
103
145
  assert.equal(sourceEntry.type, 'directory');
104
146
  assert.equal(sourceEntry.permissions, '754');
105
147
  assert.equal(sourceEntry.permissionsRwx, 'rwxr-xr--');
106
148
  assert.deepEqual(sourceEntry.children?.map((entry) => entry.name), ['index.ts']);
149
+ assert.equal(readDirectories.includes(gitDirectory), false);
150
+ assert.equal(readDirectories.includes(nodeModulesDirectory), false);
107
151
  });
108
152
 
109
153
  test('listProjectFiles excludes gitignored entries only when requested', async () => {
@@ -117,7 +161,7 @@ test('listProjectFiles excludes gitignored entries only when requested', async (
117
161
  assert.equal(filePath, path.join(projectRoot, '.gitignore'));
118
162
  return ['*.log', '!keep.log', 'cache/', 'src/generated.ts'].join('\n');
119
163
  },
120
- readdir: async (directoryPath) => {
164
+ openDirectory: createDirectoryReader((directoryPath) => {
121
165
  readDirectories.push(directoryPath);
122
166
  if (directoryPath === projectRoot) {
123
167
  return [
@@ -138,7 +182,7 @@ test('listProjectFiles excludes gitignored entries only when requested', async (
138
182
  ];
139
183
  }
140
184
  return [];
141
- },
185
+ }),
142
186
  lstat: async (candidatePath) => createStats(
143
187
  candidatePath === cacheDirectory || candidatePath === sourceDirectory,
144
188
  0o644,
@@ -153,23 +197,130 @@ test('listProjectFiles excludes gitignored entries only when requested', async (
153
197
  assert.equal(readDirectories.includes(cacheDirectory), false);
154
198
  });
155
199
 
156
- test('listProjectFiles returns the normal tree when no gitignore exists', async () => {
200
+ test('listProjectFiles falls back to conventional directory names when no gitignore exists', async () => {
157
201
  const projectRoot = path.resolve('file-tree-test-project');
202
+ const documentationDirectory = path.join(projectRoot, 'docs');
203
+ const buildDocumentationDirectory = path.join(documentationDirectory, 'build');
204
+ const nodeModulesDirectory = path.join(projectRoot, 'node_modules');
205
+ const readDirectories: string[] = [];
158
206
  const fileSystem = createFakeFileSystem({
159
207
  access: async () => undefined,
160
208
  readTextFile: async () => {
161
209
  throw Object.assign(new Error('missing'), { code: 'ENOENT' });
162
210
  },
163
- readdir: async (directoryPath) => directoryPath === projectRoot
164
- ? [createDirectoryEntry('debug.log', false)]
165
- : [],
166
- lstat: async () => createStats(false, 0o644),
211
+ openDirectory: createDirectoryReader((directoryPath) => {
212
+ readDirectories.push(directoryPath);
213
+ if (directoryPath === projectRoot) {
214
+ return [
215
+ createDirectoryEntry('debug.log', false),
216
+ createDirectoryEntry('docs', true),
217
+ createDirectoryEntry('node_modules', true),
218
+ ];
219
+ }
220
+ if (directoryPath === documentationDirectory) {
221
+ return [
222
+ createDirectoryEntry('build', true),
223
+ createDirectoryEntry('guide.md', false),
224
+ ];
225
+ }
226
+ if (directoryPath === buildDocumentationDirectory) {
227
+ return [createDirectoryEntry('generated.md', false)];
228
+ }
229
+ return [];
230
+ }),
231
+ lstat: async (candidatePath) => createStats(candidatePath === documentationDirectory, 0o644),
167
232
  });
168
233
  const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
169
234
 
170
235
  const tree = await service.listProjectFiles('project-1', { respectGitignore: true });
171
236
 
172
- assert.deepEqual(tree.map((entry) => entry.name), ['debug.log']);
237
+ assert.deepEqual(tree.map((entry) => entry.name), ['docs', 'debug.log']);
238
+ assert.deepEqual(tree[0]?.children?.map((entry) => entry.name), ['guide.md']);
239
+ assert.equal(readDirectories.includes(nodeModulesDirectory), false);
240
+ assert.equal(readDirectories.includes(buildDocumentationDirectory), false);
241
+ });
242
+
243
+ test('listProjectFiles rejects a tree that exceeds the server entry limit', async () => {
244
+ const projectRoot = path.resolve('file-tree-test-project');
245
+ const fileSystem = createFakeFileSystem({
246
+ access: async () => undefined,
247
+ openDirectory: createDirectoryReader((directoryPath) => directoryPath === projectRoot
248
+ ? Array.from({ length: 10_001 }, (_, index) => createDirectoryEntry(`file-${index}.txt`, false))
249
+ : []),
250
+ lstat: async () => createStats(false, 0o644),
251
+ });
252
+ const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
253
+
254
+ await assert.rejects(
255
+ service.listProjectFiles('project-1'),
256
+ (error: unknown) => error instanceof AppError
257
+ && error.code === 'FILE_TREE_TOO_LARGE'
258
+ && error.statusCode === 413,
259
+ );
260
+ });
261
+
262
+ test('listProjectFiles abandons a directory stream as soon as the entry limit is passed', async () => {
263
+ const projectRoot = path.resolve('file-tree-test-project');
264
+ let streamedEntries = 0;
265
+ const fileSystem = createFakeFileSystem({
266
+ access: async () => undefined,
267
+ // Endless on purpose: the walk has to stop consuming the stream itself
268
+ // instead of waiting for the directory listing to be materialized.
269
+ openDirectory: async function* (directoryPath) {
270
+ if (directoryPath !== projectRoot) {
271
+ return;
272
+ }
273
+ for (let index = 0; ; index += 1) {
274
+ streamedEntries += 1;
275
+ yield createDirectoryEntry(`file-${index}.txt`, false);
276
+ }
277
+ },
278
+ lstat: async () => createStats(false, 0o644),
279
+ });
280
+ const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
281
+
282
+ await assert.rejects(
283
+ service.listProjectFiles('project-1'),
284
+ (error: unknown) => error instanceof AppError
285
+ && error.code === 'FILE_TREE_TOO_LARGE'
286
+ && error.statusCode === 413,
287
+ );
288
+ // The budget plus the single entry that proves it was exceeded.
289
+ assert.equal(streamedEntries, 10_001);
290
+ });
291
+
292
+ test('listProjectFiles shares the entry limit across nested directories', async () => {
293
+ const projectRoot = path.resolve('file-tree-test-project');
294
+ const firstDirectory = path.join(projectRoot, 'first');
295
+ const secondDirectory = path.join(projectRoot, 'second');
296
+ const directoryPaths = new Set([firstDirectory, secondDirectory]);
297
+ const fileSystem = createFakeFileSystem({
298
+ access: async () => undefined,
299
+ openDirectory: createDirectoryReader((directoryPath) => {
300
+ if (directoryPath === projectRoot) {
301
+ return [
302
+ createDirectoryEntry('first', true),
303
+ createDirectoryEntry('second', true),
304
+ ];
305
+ }
306
+ if (directoryPaths.has(directoryPath)) {
307
+ return Array.from(
308
+ { length: 5_000 },
309
+ (_, index) => createDirectoryEntry(`${path.basename(directoryPath)}-${index}.txt`, false),
310
+ );
311
+ }
312
+ return [];
313
+ }),
314
+ lstat: async (candidatePath) => createStats(directoryPaths.has(candidatePath), 0o644),
315
+ });
316
+ const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
317
+
318
+ await assert.rejects(
319
+ service.listProjectFiles('project-1'),
320
+ (error: unknown) => error instanceof AppError
321
+ && error.code === 'FILE_TREE_TOO_LARGE'
322
+ && error.statusCode === 413,
323
+ );
173
324
  });
174
325
 
175
326
  test('readTextFile rejects traversal before invoking the filesystem adapter', async () => {
@@ -0,0 +1,43 @@
1
+ import { AppError } from '@/shared/utils.js';
2
+
3
+ type GitCommandResult = {
4
+ stdout: string;
5
+ stderr: string;
6
+ };
7
+
8
+ type GitCommandRunner = (
9
+ command: string,
10
+ args: string[],
11
+ options: { cwd: string },
12
+ ) => Promise<GitCommandResult>;
13
+
14
+ type DeleteLocalBranchInput = {
15
+ projectPath: string;
16
+ branch: string;
17
+ force: boolean;
18
+ runCommand: GitCommandRunner;
19
+ };
20
+
21
+ /** Used by the Git routes module to safely delete a non-current local branch. */
22
+ export async function deleteLocalBranch(input: DeleteLocalBranchInput): Promise<string> {
23
+ const { stdout: currentBranch } = await input.runCommand(
24
+ 'git',
25
+ ['branch', '--show-current'],
26
+ { cwd: input.projectPath },
27
+ );
28
+
29
+ if (currentBranch.trim() === input.branch) {
30
+ throw new AppError('Cannot delete the currently checked-out branch', {
31
+ code: 'GIT_CURRENT_BRANCH_DELETE',
32
+ statusCode: 400,
33
+ });
34
+ }
35
+
36
+ const deleteFlag = input.force ? '-D' : '-d';
37
+ const { stdout } = await input.runCommand(
38
+ 'git',
39
+ ['branch', deleteFlag, '--', input.branch],
40
+ { cwd: input.projectPath },
41
+ );
42
+ return stdout;
43
+ }
@@ -8,6 +8,7 @@ import { AppError } from '@/shared/utils.js';
8
8
 
9
9
  // cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
10
10
  import { parseGitLogWithStats, parseGitStatusOutput } from './git-parsing.service.js';
11
+ import { deleteLocalBranch } from './git-branch.service.js';
11
12
 
12
13
  type GitRouterDependencies = {
13
14
  fileSystem: typeof import('node:fs/promises');
@@ -839,26 +840,32 @@ router.post('/create-branch', async (req, res) => {
839
840
 
840
841
  // Delete a local branch
841
842
  router.post('/delete-branch', async (req, res) => {
842
- const { project, branch } = req.body;
843
+ const { project, branch, force = false } = req.body;
843
844
 
844
845
  if (!project || !branch) {
845
846
  return res.status(400).json({ error: 'Project id and branch name are required' });
846
847
  }
847
848
 
849
+ if (typeof force !== 'boolean') {
850
+ return res.status(400).json({ error: 'force must be a boolean' });
851
+ }
852
+
848
853
  try {
849
854
  const projectPath = await getActualProjectPath(project);
850
855
  await validateGitRepository(projectPath);
851
-
852
- // Safety: cannot delete the currently checked-out branch
853
- const { stdout: currentBranch } = await spawnAsync('git', ['branch', '--show-current'], { cwd: projectPath });
854
- if (currentBranch.trim() === branch) {
855
- return res.status(400).json({ error: 'Cannot delete the currently checked-out branch' });
856
- }
857
-
858
- const { stdout } = await spawnAsync('git', ['branch', '-d', branch], { cwd: projectPath });
856
+ validateBranchName(branch);
857
+ const stdout = await deleteLocalBranch({
858
+ projectPath,
859
+ branch,
860
+ force,
861
+ runCommand: spawnAsync,
862
+ });
859
863
  res.json({ success: true, output: stdout });
860
864
  } catch (error) {
861
865
  console.error('Git delete branch error:', error);
866
+ if (error instanceof AppError) {
867
+ return res.status(error.statusCode).json({ error: error.message });
868
+ }
862
869
  res.status(500).json({ error: error.message });
863
870
  }
864
871
  });
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { deleteLocalBranch } from '../git-branch.service.js';
5
+
6
+ test('deleteLocalBranch uses safe deletion by default', async () => {
7
+ const commands: string[][] = [];
8
+ const output = await deleteLocalBranch({
9
+ projectPath: '/workspace/repo',
10
+ branch: 'feature/safe-delete',
11
+ force: false,
12
+ runCommand: async (_command, args) => {
13
+ commands.push(args);
14
+ return {
15
+ stdout: args.includes('--show-current') ? 'main\n' : 'Deleted branch feature/safe-delete.\n',
16
+ stderr: '',
17
+ };
18
+ },
19
+ });
20
+
21
+ assert.equal(output, 'Deleted branch feature/safe-delete.\n');
22
+ assert.deepEqual(commands, [
23
+ ['branch', '--show-current'],
24
+ ['branch', '-d', '--', 'feature/safe-delete'],
25
+ ]);
26
+ });
27
+
28
+ test('deleteLocalBranch uses force deletion only when requested', async () => {
29
+ const commands: string[][] = [];
30
+ await deleteLocalBranch({
31
+ projectPath: '/workspace/repo',
32
+ branch: 'feature/unmerged',
33
+ force: true,
34
+ runCommand: async (_command, args) => {
35
+ commands.push(args);
36
+ return { stdout: args.includes('--show-current') ? 'main\n' : '', stderr: '' };
37
+ },
38
+ });
39
+
40
+ assert.deepEqual(commands, [
41
+ ['branch', '--show-current'],
42
+ ['branch', '-D', '--', 'feature/unmerged'],
43
+ ]);
44
+ });
45
+
46
+ test('deleteLocalBranch refuses to delete the current branch even when forced', async () => {
47
+ await assert.rejects(
48
+ deleteLocalBranch({
49
+ projectPath: '/workspace/repo',
50
+ branch: 'feature/current',
51
+ force: true,
52
+ runCommand: async () => ({ stdout: 'feature/current\n', stderr: '' }),
53
+ }),
54
+ (error: unknown) => {
55
+ assert.equal((error as { code?: string }).code, 'GIT_CURRENT_BRANCH_DELETE');
56
+ assert.equal((error as { statusCode?: number }).statusCode, 400);
57
+ return true;
58
+ },
59
+ );
60
+ });
@@ -50,3 +50,86 @@ test('git init does not run when repository validation fails for an execution er
50
50
 
51
51
  assert.deepEqual(commands, [['rev-parse', '--is-inside-work-tree']]);
52
52
  });
53
+
54
+ test('delete branch parses force and uses Git force deletion', async () => {
55
+ const commands: string[][] = [];
56
+ const spawnProcess = ((_command: string, args: string[]) => {
57
+ commands.push(args);
58
+ const child = new EventEmitter() as EventEmitter & { stdout: PassThrough; stderr: PassThrough };
59
+ child.stdout = new PassThrough();
60
+ child.stderr = new PassThrough();
61
+ process.nextTick(() => {
62
+ if (args.includes('--is-inside-work-tree')) child.stdout.write('true\n');
63
+ if (args.includes('--show-toplevel')) child.stdout.write('/workspace/repo\n');
64
+ if (args.includes('--show-current')) child.stdout.write('main\n');
65
+ if (args.includes('-D')) child.stdout.write('Deleted branch feature/unmerged.\n');
66
+ child.stdout.end();
67
+ child.stderr.end();
68
+ child.emit('close', 0);
69
+ });
70
+ return child;
71
+ }) as Parameters<typeof createGitRouter>[0]['spawnProcess'];
72
+ const unexpectedProvider = async (): Promise<never> => { throw new Error('unexpected provider call'); };
73
+ const router = createGitRouter({
74
+ fileSystem: { access: async () => undefined } as unknown as Parameters<typeof createGitRouter>[0]['fileSystem'],
75
+ spawnProcess,
76
+ resolveProjectPathById: () => '/workspace/repo',
77
+ queryClaude: unexpectedProvider,
78
+ queryCursor: unexpectedProvider,
79
+ });
80
+ const app = express();
81
+ app.use(express.json());
82
+ app.use('/api/git', router);
83
+ const server = app.listen(0, '127.0.0.1');
84
+ await once(server, 'listening');
85
+
86
+ try {
87
+ const address = server.address() as AddressInfo;
88
+ const response = await fetch(`http://127.0.0.1:${address.port}/api/git/delete-branch`, {
89
+ method: 'POST',
90
+ headers: { 'content-type': 'application/json' },
91
+ body: JSON.stringify({ project: 'project-1', branch: 'feature/unmerged', force: true }),
92
+ });
93
+ const body = await response.json() as { success: boolean; output: string };
94
+ assert.equal(response.status, 200);
95
+ assert.equal(body.success, true);
96
+ assert.match(body.output, /Deleted branch/);
97
+ } finally {
98
+ await new Promise<void>((resolve) => server.close(() => resolve()));
99
+ }
100
+
101
+ assert.deepEqual(commands.at(-1), ['branch', '-D', '--', 'feature/unmerged']);
102
+ });
103
+
104
+ test('delete branch rejects a non-boolean force value before running Git', async () => {
105
+ const spawnProcess = (() => {
106
+ throw new Error('Git must not run for invalid input');
107
+ }) as Parameters<typeof createGitRouter>[0]['spawnProcess'];
108
+ const unexpectedProvider = async (): Promise<never> => { throw new Error('unexpected provider call'); };
109
+ const router = createGitRouter({
110
+ fileSystem: { access: async () => undefined } as unknown as Parameters<typeof createGitRouter>[0]['fileSystem'],
111
+ spawnProcess,
112
+ resolveProjectPathById: () => '/workspace/repo',
113
+ queryClaude: unexpectedProvider,
114
+ queryCursor: unexpectedProvider,
115
+ });
116
+ const app = express();
117
+ app.use(express.json());
118
+ app.use('/api/git', router);
119
+ const server = app.listen(0, '127.0.0.1');
120
+ await once(server, 'listening');
121
+
122
+ try {
123
+ const address = server.address() as AddressInfo;
124
+ const response = await fetch(`http://127.0.0.1:${address.port}/api/git/delete-branch`, {
125
+ method: 'POST',
126
+ headers: { 'content-type': 'application/json' },
127
+ body: JSON.stringify({ project: 'project-1', branch: 'feature/unmerged', force: 'yes' }),
128
+ });
129
+ const body = await response.json() as { error: string };
130
+ assert.equal(response.status, 400);
131
+ assert.equal(body.error, 'force must be a boolean');
132
+ } finally {
133
+ await new Promise<void>((resolve) => server.close(() => resolve()));
134
+ }
135
+ });
@@ -9,6 +9,8 @@ export {
9
9
  notifyRunFailed,
10
10
  // Used by provider runtimes to report stopped or completed agent runs.
11
11
  notifyRunStopped,
12
+ // Used by provider runtimes to report background work that finished after its turn ended.
13
+ notifyBackgroundWorkCompleted,
12
14
  } from '@/modules/notifications/services/notification-orchestrator.service.js';
13
15
  export {
14
16
  registerDesktopNotificationClient,
@@ -155,6 +155,7 @@ function buildNotificationPayload(event) {
155
155
  ? `Action Required: Tool "${normalizedEvent.meta.toolName}" needs approval`
156
156
  : 'Action Required: A tool needs your approval',
157
157
  'run.stopped': normalizedEvent.meta?.stopReason || 'Run Stopped: The run has stopped',
158
+ 'run.background_completed': 'Background work finished',
158
159
  'run.failed': normalizedEvent.meta?.error ? `Run Failed: ${normalizedEvent.meta.error}` : 'Run Failed: The run encountered an error',
159
160
  'agent.notification': normalizedEvent.meta?.message ? String(normalizedEvent.meta.message) : 'You have a new notification',
160
161
  'push.enabled': 'Push notifications are now enabled!'
@@ -261,6 +262,27 @@ function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'co
261
262
  });
262
263
  }
263
264
 
265
+ /**
266
+ * Reports background work that finished after its turn had already completed.
267
+ *
268
+ * Uses the `stop` kind so it rides the existing "run stopped" preference rather
269
+ * than needing a new opt-in that would default to off. No explicit dedupeKey, so
270
+ * the default composite key collapses repeats inside the dedupe window.
271
+ */
272
+ function notifyBackgroundWorkCompleted({ userId, provider, sessionId = null, sessionName = null }) {
273
+ notifyUserIfEnabled({
274
+ userId,
275
+ event: createNotificationEvent({
276
+ provider,
277
+ sessionId,
278
+ kind: 'stop',
279
+ code: 'run.background_completed',
280
+ meta: { sessionName },
281
+ severity: 'info'
282
+ })
283
+ });
284
+ }
285
+
264
286
  function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null }) {
265
287
  const errorMessage = normalizeErrorMessage(error);
266
288
 
@@ -283,5 +305,6 @@ export {
283
305
  createNotificationEvent,
284
306
  notifyUserIfEnabled,
285
307
  notifyRunStopped,
286
- notifyRunFailed
308
+ notifyRunFailed,
309
+ notifyBackgroundWorkCompleted
287
310
  };