upfynai-code 3.0.4 → 3.2.0

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 (258) hide show
  1. package/README.md +69 -92
  2. package/bin/cli.js +191 -0
  3. package/dist/client/assets/AppContent-M14Au3SB.js +542 -0
  4. package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-TFKm2NDJ.js} +2 -2
  5. package/dist/client/assets/DashboardPanel-C88HjsCh.js +1 -0
  6. package/dist/client/assets/FileTree-DvO1xnDE.js +1 -0
  7. package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-D-slVlyy.js} +2 -2
  8. package/dist/client/assets/LoginModal-Chi4SYcr.js +21 -0
  9. package/{client/dist/assets/MarkdownPreview-CESjI261.js → dist/client/assets/MarkdownPreview-CuIix2u9.js} +1 -1
  10. package/dist/client/assets/MermaidBlock-Dq9uFv82.js +2 -0
  11. package/dist/client/assets/Onboarding-QYXx24dX.js +1 -0
  12. package/{client/dist/assets/PreviewPanel-CqCa92Tf.js → dist/client/assets/PreviewPanel-Dd8q-jo0.js} +1 -1
  13. package/dist/client/assets/SetupForm-CrspaUva.js +1 -0
  14. package/dist/client/assets/WorkflowsPanel-DIlYAdhB.js +1 -0
  15. package/dist/client/assets/index-CnNNzw9A.css +1 -0
  16. package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-rUkK9FDP.js} +26 -26
  17. package/{client/dist/assets/vendor-codemirror-D2ALgpaX.js → dist/client/assets/vendor-codemirror-jc6nyJQg.js} +1 -1
  18. package/{client/dist/assets/vendor-diff-DNQpbhrT.js → dist/client/assets/vendor-diff-THJmAcEI.js} +1 -1
  19. package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-CfjIpdrD.js} +145 -155
  20. package/{client/dist/assets/vendor-markdown-CimbIo6Y.js → dist/client/assets/vendor-markdown-Cdm6NEGf.js} +1 -1
  21. package/dist/client/assets/vendor-mermaid-DTPaBx-U.js +2559 -0
  22. package/{client/dist/assets/vendor-react-96lCPsRK.js → dist/client/assets/vendor-react-wFkb6mSf.js} +1 -1
  23. package/{client/dist/assets/vendor-syntax-LS_Nt30I.js → dist/client/assets/vendor-syntax-C_UZR7tc.js} +1 -1
  24. package/dist/client/favicon.png +0 -0
  25. package/dist/client/icons/icon-128x128.png +0 -0
  26. package/dist/client/icons/icon-144x144.png +0 -0
  27. package/dist/client/icons/icon-152x152.png +0 -0
  28. package/dist/client/icons/icon-192x192.png +0 -0
  29. package/dist/client/icons/icon-384x384.png +0 -0
  30. package/dist/client/icons/icon-512x512.png +0 -0
  31. package/dist/client/icons/icon-72x72.png +0 -0
  32. package/dist/client/icons/icon-96x96.png +0 -0
  33. package/{client/dist → dist/client}/index.html +37 -36
  34. package/dist/client/logo-128.png +0 -0
  35. package/dist/client/logo-256.png +0 -0
  36. package/dist/client/logo-32.png +0 -0
  37. package/dist/client/logo-512.png +0 -0
  38. package/dist/client/logo-64.png +0 -0
  39. package/dist/client/logo.png +0 -0
  40. package/{client/dist → dist/client}/manifest.json +12 -12
  41. package/{client/dist → dist/client}/mcp-docs.html +1 -1
  42. package/{client/dist → dist/client}/sw.js +2 -2
  43. package/package.json +56 -105
  44. package/scripts/postinstall.js +9 -0
  45. package/scripts/prepublish.js +77 -0
  46. package/src/animation.js +228 -0
  47. package/src/auth.js +142 -0
  48. package/src/config.js +40 -0
  49. package/src/connect.js +416 -0
  50. package/src/launch.js +81 -0
  51. package/src/mcp.js +57 -0
  52. package/src/permissions.js +140 -0
  53. package/src/persistent-shell.js +261 -0
  54. package/src/server.js +54 -0
  55. package/client/dist/assets/AppContent-CwrTP6TW.js +0 -545
  56. package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
  57. package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
  58. package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
  59. package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
  60. package/client/dist/assets/LoginModal-CImJHRjX.js +0 -13
  61. package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
  62. package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
  63. package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
  64. package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
  65. package/client/dist/assets/index-46kkVu2i.css +0 -1
  66. package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
  67. package/client/dist/assets/vendor-canvas-BZV40eAE.css +0 -1
  68. package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
  69. package/client/dist/assets/vendor-mermaid-DucWyDEe.js +0 -2556
  70. package/client/dist/favicon.png +0 -0
  71. package/client/dist/icons/icon-128x128.png +0 -0
  72. package/client/dist/icons/icon-144x144.png +0 -0
  73. package/client/dist/icons/icon-152x152.png +0 -0
  74. package/client/dist/icons/icon-192x192.png +0 -0
  75. package/client/dist/icons/icon-384x384.png +0 -0
  76. package/client/dist/icons/icon-512x512.png +0 -0
  77. package/client/dist/icons/icon-72x72.png +0 -0
  78. package/client/dist/icons/icon-96x96.png +0 -0
  79. package/client/dist/logo-128.png +0 -0
  80. package/client/dist/logo-256.png +0 -0
  81. package/client/dist/logo-32.png +0 -0
  82. package/client/dist/logo-512.png +0 -0
  83. package/client/dist/logo-64.png +0 -0
  84. package/commands/upfynai-connect.md +0 -59
  85. package/commands/upfynai-disconnect.md +0 -31
  86. package/commands/upfynai-doctor.md +0 -99
  87. package/commands/upfynai-export.md +0 -49
  88. package/commands/upfynai-local.md +0 -82
  89. package/commands/upfynai-status.md +0 -75
  90. package/commands/upfynai-stop.md +0 -49
  91. package/commands/upfynai-uninstall.md +0 -58
  92. package/commands/upfynai.md +0 -69
  93. package/scripts/build-client.js +0 -17
  94. package/scripts/fix-node-pty.js +0 -67
  95. package/scripts/install-commands.js +0 -78
  96. package/server/agent-loop.js +0 -242
  97. package/server/auto-compact.js +0 -99
  98. package/server/browser.js +0 -131
  99. package/server/claude-sdk.js +0 -797
  100. package/server/cli-ui.js +0 -798
  101. package/server/cli.js +0 -751
  102. package/server/constants/config.js +0 -31
  103. package/server/cursor-cli.js +0 -270
  104. package/server/database/auth.db +0 -0
  105. package/server/database/db.js +0 -1547
  106. package/server/database/init.sql +0 -70
  107. package/server/index.js +0 -3813
  108. package/server/load-env.js +0 -26
  109. package/server/mcp-server.js +0 -621
  110. package/server/middleware/auth.js +0 -184
  111. package/server/middleware/relayHelpers.js +0 -44
  112. package/server/middleware/sandboxRouter.js +0 -174
  113. package/server/openai-codex.js +0 -403
  114. package/server/openrouter.js +0 -137
  115. package/server/projects.js +0 -1807
  116. package/server/provider-factory.js +0 -174
  117. package/server/relay-client.js +0 -390
  118. package/server/routes/agent.js +0 -1234
  119. package/server/routes/auth.js +0 -559
  120. package/server/routes/browser.js +0 -419
  121. package/server/routes/canvas.js +0 -53
  122. package/server/routes/cli-auth.js +0 -263
  123. package/server/routes/codex.js +0 -396
  124. package/server/routes/commands.js +0 -707
  125. package/server/routes/composio.js +0 -176
  126. package/server/routes/cursor.js +0 -770
  127. package/server/routes/dashboard.js +0 -295
  128. package/server/routes/git.js +0 -1208
  129. package/server/routes/keys.js +0 -34
  130. package/server/routes/mcp-utils.js +0 -48
  131. package/server/routes/mcp.js +0 -661
  132. package/server/routes/payments.js +0 -227
  133. package/server/routes/projects.js +0 -754
  134. package/server/routes/sessions.js +0 -146
  135. package/server/routes/settings.js +0 -261
  136. package/server/routes/taskmaster.js +0 -1928
  137. package/server/routes/user.js +0 -106
  138. package/server/routes/vapi-chat.js +0 -624
  139. package/server/routes/voice.js +0 -235
  140. package/server/routes/webhooks.js +0 -166
  141. package/server/routes/workflows.js +0 -312
  142. package/server/sandbox.js +0 -120
  143. package/server/services/browser-ai.js +0 -154
  144. package/server/services/composio.js +0 -204
  145. package/server/services/sessionRegistry.js +0 -139
  146. package/server/services/whisperService.js +0 -84
  147. package/server/services/workflowScheduler.js +0 -211
  148. package/server/tests/relay-flow.test.js +0 -570
  149. package/server/tests/sessions.test.js +0 -259
  150. package/server/utils/commandParser.js +0 -303
  151. package/server/utils/email.js +0 -66
  152. package/server/utils/gitConfig.js +0 -24
  153. package/server/utils/mcp-detector.js +0 -198
  154. package/server/utils/taskmaster-websocket.js +0 -129
  155. package/shared/integrationCatalog.d.ts +0 -12
  156. package/shared/integrationCatalog.js +0 -172
  157. package/shared/modelConstants.js +0 -96
  158. /package/{shared → dist}/agents/claude.js +0 -0
  159. /package/{shared → dist}/agents/codex.js +0 -0
  160. /package/{shared → dist}/agents/cursor.js +0 -0
  161. /package/{shared → dist}/agents/detect.js +0 -0
  162. /package/{shared → dist}/agents/exec.js +0 -0
  163. /package/{shared → dist}/agents/files.js +0 -0
  164. /package/{shared → dist}/agents/git.js +0 -0
  165. /package/{shared → dist}/agents/gitagent.js +0 -0
  166. /package/{shared → dist}/agents/index.js +0 -0
  167. /package/{shared → dist}/agents/shell.js +0 -0
  168. /package/{shared → dist}/agents/utils.js +0 -0
  169. /package/{client/dist → dist/client}/api-docs.html +0 -0
  170. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  171. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  172. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  173. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  174. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  175. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  176. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  177. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  178. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  179. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  180. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  181. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  182. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  183. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  184. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  185. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  186. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  187. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  188. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  189. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  190. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  191. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  192. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  193. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  194. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  195. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  196. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  197. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  198. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  199. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  200. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  201. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  202. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  203. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  204. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  205. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  206. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  207. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  208. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  209. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  210. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  211. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  212. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  213. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  214. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  215. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  216. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  217. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  218. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  219. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  220. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  221. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  222. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  223. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  224. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  225. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  226. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  227. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  228. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  229. /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
  230. /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
  231. /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
  232. /package/{client/dist → dist/client}/clear-cache.html +0 -0
  233. /package/{client/dist → dist/client}/convert-icons.md +0 -0
  234. /package/{client/dist → dist/client}/favicon.svg +0 -0
  235. /package/{client/dist → dist/client}/generate-icons.js +0 -0
  236. /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
  237. /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
  238. /package/{client/dist → dist/client}/icons/codex.svg +0 -0
  239. /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
  240. /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
  241. /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
  242. /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
  243. /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
  244. /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
  245. /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
  246. /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
  247. /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
  248. /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
  249. /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
  250. /package/{client/dist → dist/client}/logo.svg +0 -0
  251. /package/{client/dist → dist/client}/offline.html +0 -0
  252. /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
  253. /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
  254. /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
  255. /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
  256. /package/{shared → dist}/gitagent/index.js +0 -0
  257. /package/{shared → dist}/gitagent/parser.js +0 -0
  258. /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
@@ -1,1208 +0,0 @@
1
- import express from 'express';
2
- import { exec, spawn } from 'child_process';
3
- import { promisify } from 'util';
4
- import path from 'path';
5
- import { promises as fs } from 'fs';
6
- import { extractProjectDirectory } from '../projects.js';
7
- import { queryClaudeSDK } from '../claude-sdk.js';
8
- import { spawnCursor } from '../cursor-cli.js';
9
-
10
- const router = express.Router();
11
- const execAsync = promisify(exec);
12
-
13
- function spawnAsync(command, args, options = {}) {
14
- return new Promise((resolve, reject) => {
15
- const child = spawn(command, args, {
16
- ...options,
17
- shell: false,
18
- });
19
-
20
- let stdout = '';
21
- let stderr = '';
22
-
23
- child.stdout.on('data', (data) => {
24
- stdout += data.toString();
25
- });
26
-
27
- child.stderr.on('data', (data) => {
28
- stderr += data.toString();
29
- });
30
-
31
- child.on('error', (error) => {
32
- reject(error);
33
- });
34
-
35
- child.on('close', (code) => {
36
- if (code === 0) {
37
- resolve({ stdout, stderr });
38
- return;
39
- }
40
-
41
- const error = new Error(`Command failed: ${command} ${args.join(' ')}`);
42
- error.code = code;
43
- error.stdout = stdout;
44
- error.stderr = stderr;
45
- reject(error);
46
- });
47
- });
48
- }
49
-
50
- /**
51
- * Relay-aware git command execution.
52
- * In cloud mode with relay connected, sends git command to user's machine.
53
- * Otherwise falls back to local execAsync.
54
- */
55
- async function relayGit(req, gitArgs, cwd, timeout = 15000) {
56
- if (req.isCloud && req.hasRelay()) {
57
- const result = await req.sendRelay('shell-command', { command: `git ${gitArgs}`, cwd }, timeout);
58
- return { stdout: result.stdout || result.output || '', stderr: result.stderr || '' };
59
- }
60
- return execAsync(`git ${gitArgs}`, { cwd });
61
- }
62
-
63
- /**
64
- * Relay-aware spawn (for commands needing array args like git log).
65
- * Falls back to local spawnAsync when no relay.
66
- */
67
- async function relaySpawn(req, command, args, options = {}, timeout = 15000) {
68
- if (req.isCloud && req.hasRelay()) {
69
- const fullCommand = `${command} ${args.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
70
- const result = await req.sendRelay('shell-command', { command: fullCommand, cwd: options.cwd }, timeout);
71
- return { stdout: result.stdout || result.output || '', stderr: result.stderr || '' };
72
- }
73
- return spawnAsync(command, args, options);
74
- }
75
-
76
- /**
77
- * Relay-aware git repository validation.
78
- */
79
- async function validateGitRepo(req, projectPath) {
80
- if (req.isCloud && req.hasRelay()) {
81
- try {
82
- const result = await req.sendRelay('shell-command', {
83
- command: 'git rev-parse --is-inside-work-tree',
84
- cwd: projectPath
85
- }, 10000);
86
- const output = (result.stdout || result.output || '').trim();
87
- if (output !== 'true') {
88
- throw new Error('Not inside a git work tree');
89
- }
90
- await req.sendRelay('shell-command', {
91
- command: 'git rev-parse --show-toplevel',
92
- cwd: projectPath
93
- }, 10000);
94
- } catch {
95
- throw new Error('Not a git repository. This directory does not contain a .git folder. Initialize a git repository with "git init" to use source control features.');
96
- }
97
- return;
98
- }
99
- // Local validation
100
- return validateGitRepository(projectPath);
101
- }
102
-
103
- // Helper function to get the actual project path from the encoded project name
104
- async function getActualProjectPath(projectName) {
105
- try {
106
- return await extractProjectDirectory(projectName);
107
- } catch (error) {
108
- // project directory extraction error
109
- // Fallback to the old method
110
- return projectName.replace(/-/g, '/');
111
- }
112
- }
113
-
114
- // Helper function to strip git diff headers
115
- function stripDiffHeaders(diff) {
116
- if (!diff) return '';
117
-
118
- const lines = diff.split('\n');
119
- const filteredLines = [];
120
- let startIncluding = false;
121
-
122
- for (const line of lines) {
123
- // Skip all header lines including diff --git, index, file mode, and --- / +++ file paths
124
- if (line.startsWith('diff --git') ||
125
- line.startsWith('index ') ||
126
- line.startsWith('new file mode') ||
127
- line.startsWith('deleted file mode') ||
128
- line.startsWith('---') ||
129
- line.startsWith('+++')) {
130
- continue;
131
- }
132
-
133
- // Start including lines from @@ hunk headers onwards
134
- if (line.startsWith('@@') || startIncluding) {
135
- startIncluding = true;
136
- filteredLines.push(line);
137
- }
138
- }
139
-
140
- return filteredLines.join('\n');
141
- }
142
-
143
- // Helper function to validate git repository
144
- async function validateGitRepository(projectPath) {
145
- try {
146
- // Check if directory exists
147
- await fs.access(projectPath);
148
- } catch {
149
- throw new Error(`Project path not found: ${projectPath}`);
150
- }
151
-
152
- try {
153
- // Allow any directory that is inside a work tree (repo root or nested folder).
154
- const { stdout: insideWorkTreeOutput } = await execAsync('git rev-parse --is-inside-work-tree', { cwd: projectPath });
155
- const isInsideWorkTree = insideWorkTreeOutput.trim() === 'true';
156
- if (!isInsideWorkTree) {
157
- throw new Error('Not inside a git work tree');
158
- }
159
-
160
- // Ensure git can resolve the repository root for this directory.
161
- await execAsync('git rev-parse --show-toplevel', { cwd: projectPath });
162
- } catch {
163
- throw new Error('Not a git repository. This directory does not contain a .git folder. Initialize a git repository with "git init" to use source control features.');
164
- }
165
- }
166
-
167
- // Get git status for a project
168
- router.get('/status', async (req, res) => {
169
- const { project } = req.query;
170
-
171
- if (!project) {
172
- return res.status(400).json({ error: 'Project name is required' });
173
- }
174
-
175
- try {
176
- const projectPath = await getActualProjectPath(project);
177
-
178
- // Cloud mode guard
179
- if (req.isCloud && !req.hasRelay()) {
180
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
181
- }
182
-
183
- // Validate git repository
184
- await validateGitRepo(req, projectPath);
185
-
186
- // Get current branch - handle case where there are no commits yet
187
- let branch = 'main';
188
- let hasCommits = true;
189
- try {
190
- const { stdout: branchOutput } = await relayGit(req, 'rev-parse --abbrev-ref HEAD', projectPath);
191
- branch = branchOutput.trim();
192
- } catch (error) {
193
- if (error.message.includes('unknown revision') || error.message.includes('ambiguous argument')) {
194
- hasCommits = false;
195
- branch = 'main';
196
- } else {
197
- throw error;
198
- }
199
- }
200
-
201
- // Get git status
202
- const { stdout: statusOutput } = await relayGit(req, 'status --porcelain', projectPath);
203
-
204
- const modified = [];
205
- const added = [];
206
- const deleted = [];
207
- const untracked = [];
208
-
209
- statusOutput.split('\n').forEach(line => {
210
- if (!line.trim()) return;
211
-
212
- const status = line.substring(0, 2);
213
- const file = line.substring(3);
214
-
215
- if (status === 'M ' || status === ' M' || status === 'MM') {
216
- modified.push(file);
217
- } else if (status === 'A ' || status === 'AM') {
218
- added.push(file);
219
- } else if (status === 'D ' || status === ' D') {
220
- deleted.push(file);
221
- } else if (status === '??') {
222
- untracked.push(file);
223
- }
224
- });
225
-
226
- res.json({
227
- branch,
228
- hasCommits,
229
- modified,
230
- added,
231
- deleted,
232
- untracked
233
- });
234
- } catch (error) {
235
- // git status error
236
- const isNotRepo = error.message?.includes('not a git repository');
237
- res.json({
238
- error: isNotRepo ? 'Not a git repository' : 'Git operation failed'
239
- });
240
- }
241
- });
242
-
243
- // Get diff for a specific file
244
- router.get('/diff', async (req, res) => {
245
- const { project, file } = req.query;
246
-
247
- if (!project || !file) {
248
- return res.status(400).json({ error: 'Project name and file path are required' });
249
- }
250
-
251
- try {
252
- const projectPath = await getActualProjectPath(project);
253
-
254
- if (req.isCloud && !req.hasRelay()) {
255
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
256
- }
257
-
258
- await validateGitRepo(req, projectPath);
259
-
260
- const { stdout: statusOutput } = await relayGit(req, `status --porcelain "${file}"`, projectPath);
261
- const isUntracked = statusOutput.startsWith('??');
262
- const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
263
-
264
- let diff;
265
- if (isUntracked) {
266
- if (req.isCloud && req.hasRelay()) {
267
- // Read file via relay for untracked files
268
- try {
269
- const result = await req.sendRelay('file-read', { filePath: `${projectPath}/${file}` }, 15000);
270
- const fileContent = result.content || '';
271
- const lines = fileContent.split('\n');
272
- diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
273
- lines.map(line => `+${line}`).join('\n');
274
- } catch {
275
- diff = `New file: ${file}\n(Unable to read file content)`;
276
- }
277
- } else {
278
- const filePath = path.join(projectPath, file);
279
- const stats = await fs.stat(filePath);
280
- if (stats.isDirectory()) {
281
- diff = `Directory: ${file}\n(Cannot show diff for directories)`;
282
- } else {
283
- const fileContent = await fs.readFile(filePath, 'utf-8');
284
- const lines = fileContent.split('\n');
285
- diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
286
- lines.map(line => `+${line}`).join('\n');
287
- }
288
- }
289
- } else if (isDeleted) {
290
- const { stdout: fileContent } = await relayGit(req, `show HEAD:"${file}"`, projectPath);
291
- const lines = fileContent.split('\n');
292
- diff = `--- a/${file}\n+++ /dev/null\n@@ -1,${lines.length} +0,0 @@\n` +
293
- lines.map(line => `-${line}`).join('\n');
294
- } else {
295
- const { stdout: unstagedDiff } = await relayGit(req, `diff -- "${file}"`, projectPath);
296
- if (unstagedDiff) {
297
- diff = stripDiffHeaders(unstagedDiff);
298
- } else {
299
- const { stdout: stagedDiff } = await relayGit(req, `diff --cached -- "${file}"`, projectPath);
300
- diff = stripDiffHeaders(stagedDiff) || '';
301
- }
302
- }
303
-
304
- res.json({ diff });
305
- } catch (error) {
306
- res.json({ error: 'Git operation failed' });
307
- }
308
- });
309
-
310
- // Get file content with diff information for CodeEditor
311
- router.get('/file-with-diff', async (req, res) => {
312
- const { project, file } = req.query;
313
-
314
- if (!project || !file) {
315
- return res.status(400).json({ error: 'Project name and file path are required' });
316
- }
317
-
318
- try {
319
- const projectPath = await getActualProjectPath(project);
320
-
321
- if (req.isCloud && !req.hasRelay()) {
322
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
323
- }
324
-
325
- await validateGitRepo(req, projectPath);
326
-
327
- const { stdout: statusOutput } = await relayGit(req, `status --porcelain "${file}"`, projectPath);
328
- const isUntracked = statusOutput.startsWith('??');
329
- const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
330
-
331
- let currentContent = '';
332
- let oldContent = '';
333
-
334
- if (isDeleted) {
335
- const { stdout: headContent } = await relayGit(req, `show HEAD:"${file}"`, projectPath);
336
- oldContent = headContent;
337
- currentContent = headContent;
338
- } else {
339
- // Get current file content
340
- if (req.isCloud && req.hasRelay()) {
341
- try {
342
- const result = await req.sendRelay('file-read', { filePath: `${projectPath}/${file}` }, 15000);
343
- currentContent = result.content || '';
344
- } catch {
345
- return res.status(500).json({ error: 'Failed to read file via relay' });
346
- }
347
- } else {
348
- const filePath = path.join(projectPath, file);
349
- const stats = await fs.stat(filePath);
350
- if (stats.isDirectory()) {
351
- return res.status(400).json({ error: 'Cannot show diff for directories' });
352
- }
353
- currentContent = await fs.readFile(filePath, 'utf-8');
354
- }
355
-
356
- if (!isUntracked) {
357
- try {
358
- const { stdout: headContent } = await relayGit(req, `show HEAD:"${file}"`, projectPath);
359
- oldContent = headContent;
360
- } catch (error) {
361
- oldContent = '';
362
- }
363
- }
364
- }
365
-
366
- res.json({ currentContent, oldContent, isDeleted, isUntracked });
367
- } catch (error) {
368
- res.json({ error: 'Git operation failed' });
369
- }
370
- });
371
-
372
- // Create initial commit
373
- router.post('/initial-commit', async (req, res) => {
374
- const { project } = req.body;
375
-
376
- if (!project) {
377
- return res.status(400).json({ error: 'Project name is required' });
378
- }
379
-
380
- try {
381
- const projectPath = await getActualProjectPath(project);
382
-
383
- if (req.isCloud && !req.hasRelay()) {
384
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
385
- }
386
-
387
- await validateGitRepo(req, projectPath);
388
-
389
- try {
390
- await relayGit(req, 'rev-parse HEAD', projectPath);
391
- return res.status(400).json({ error: 'Repository already has commits. Use regular commit instead.' });
392
- } catch (error) {
393
- // No HEAD - good, we can create initial commit
394
- }
395
-
396
- await relayGit(req, 'add .', projectPath);
397
- const { stdout } = await relayGit(req, 'commit -m "Initial commit"', projectPath);
398
-
399
- res.json({ success: true, output: stdout, message: 'Initial commit created successfully' });
400
- } catch (error) {
401
- if (error.message.includes('nothing to commit')) {
402
- return res.status(400).json({
403
- error: 'Nothing to commit',
404
- details: 'No files found in the repository. Add some files first.'
405
- });
406
- }
407
- res.status(500).json({ error: 'Git operation failed' });
408
- }
409
- });
410
-
411
- // Commit changes
412
- router.post('/commit', async (req, res) => {
413
- const { project, message, files } = req.body;
414
-
415
- if (!project || !message || !files || files.length === 0) {
416
- return res.status(400).json({ error: 'Project name, commit message, and files are required' });
417
- }
418
-
419
- try {
420
- const projectPath = await getActualProjectPath(project);
421
-
422
- if (req.isCloud && !req.hasRelay()) {
423
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
424
- }
425
-
426
- await validateGitRepo(req, projectPath);
427
-
428
- for (const file of files) {
429
- await relayGit(req, `add "${file}"`, projectPath);
430
- }
431
-
432
- const { stdout } = await relayGit(req, `commit -m "${message.replace(/"/g, '\\"')}"`, projectPath);
433
-
434
- res.json({ success: true, output: stdout });
435
- } catch (error) {
436
- res.status(500).json({ error: 'Git operation failed' });
437
- }
438
- });
439
-
440
- // Get list of branches
441
- router.get('/branches', async (req, res) => {
442
- const { project } = req.query;
443
-
444
- if (!project) {
445
- return res.status(400).json({ error: 'Project name is required' });
446
- }
447
-
448
- try {
449
- const projectPath = await getActualProjectPath(project);
450
-
451
- if (req.isCloud && !req.hasRelay()) {
452
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
453
- }
454
-
455
- await validateGitRepo(req, projectPath);
456
-
457
- const { stdout } = await relayGit(req, 'branch -a', projectPath);
458
-
459
- // Parse branches
460
- const branches = stdout
461
- .split('\n')
462
- .map(branch => branch.trim())
463
- .filter(branch => branch && !branch.includes('->')) // Remove empty lines and HEAD pointer
464
- .map(branch => {
465
- // Remove asterisk from current branch
466
- if (branch.startsWith('* ')) {
467
- return branch.substring(2);
468
- }
469
- // Remove remotes/ prefix
470
- if (branch.startsWith('remotes/origin/')) {
471
- return branch.substring(15);
472
- }
473
- return branch;
474
- })
475
- .filter((branch, index, self) => self.indexOf(branch) === index); // Remove duplicates
476
-
477
- res.json({ branches });
478
- } catch (error) {
479
- // git branches error
480
- res.json({ error: 'Git operation failed' });
481
- }
482
- });
483
-
484
- // Checkout branch
485
- router.post('/checkout', async (req, res) => {
486
- const { project, branch } = req.body;
487
-
488
- if (!project || !branch) {
489
- return res.status(400).json({ error: 'Project name and branch are required' });
490
- }
491
-
492
- try {
493
- const projectPath = await getActualProjectPath(project);
494
-
495
- if (req.isCloud && !req.hasRelay()) {
496
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
497
- }
498
-
499
- const { stdout } = await relayGit(req, `checkout "${branch}"`, projectPath);
500
-
501
- res.json({ success: true, output: stdout });
502
- } catch (error) {
503
- res.status(500).json({ error: 'Git operation failed' });
504
- }
505
- });
506
-
507
- // Create new branch
508
- router.post('/create-branch', async (req, res) => {
509
- const { project, branch } = req.body;
510
-
511
- if (!project || !branch) {
512
- return res.status(400).json({ error: 'Project name and branch name are required' });
513
- }
514
-
515
- try {
516
- const projectPath = await getActualProjectPath(project);
517
-
518
- if (req.isCloud && !req.hasRelay()) {
519
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
520
- }
521
-
522
- const { stdout } = await relayGit(req, `checkout -b "${branch}"`, projectPath);
523
-
524
- res.json({ success: true, output: stdout });
525
- } catch (error) {
526
- res.status(500).json({ error: 'Git operation failed' });
527
- }
528
- });
529
-
530
- // Get recent commits
531
- router.get('/commits', async (req, res) => {
532
- const { project, limit = 10 } = req.query;
533
-
534
- if (!project) {
535
- return res.status(400).json({ error: 'Project name is required' });
536
- }
537
-
538
- try {
539
- const projectPath = await getActualProjectPath(project);
540
-
541
- if (req.isCloud && !req.hasRelay()) {
542
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
543
- }
544
-
545
- await validateGitRepo(req, projectPath);
546
- const parsedLimit = Number.parseInt(String(limit), 10);
547
- const safeLimit = Number.isFinite(parsedLimit) && parsedLimit > 0
548
- ? Math.min(parsedLimit, 100)
549
- : 10;
550
-
551
- const { stdout } = await relaySpawn(
552
- req,
553
- 'git',
554
- ['log', '--pretty=format:%H|%an|%ae|%ad|%s', '--date=relative', '-n', String(safeLimit)],
555
- { cwd: projectPath },
556
- );
557
-
558
- const commits = stdout
559
- .split('\n')
560
- .filter(line => line.trim())
561
- .map(line => {
562
- const [hash, author, email, date, ...messageParts] = line.split('|');
563
- return { hash, author, email, date, message: messageParts.join('|') };
564
- });
565
-
566
- for (const commit of commits) {
567
- try {
568
- const { stdout: stats } = await relayGit(req, `show --stat --format='' ${commit.hash}`, projectPath);
569
- commit.stats = stats.trim().split('\n').pop();
570
- } catch (error) {
571
- commit.stats = '';
572
- }
573
- }
574
-
575
- res.json({ commits });
576
- } catch (error) {
577
- res.json({ error: 'Git operation failed' });
578
- }
579
- });
580
-
581
- // Get diff for a specific commit
582
- router.get('/commit-diff', async (req, res) => {
583
- const { project, commit } = req.query;
584
-
585
- if (!project || !commit) {
586
- return res.status(400).json({ error: 'Project name and commit hash are required' });
587
- }
588
-
589
- try {
590
- const projectPath = await getActualProjectPath(project);
591
-
592
- if (req.isCloud && !req.hasRelay()) {
593
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
594
- }
595
-
596
- const { stdout } = await relayGit(req, `show ${commit}`, projectPath);
597
-
598
- res.json({ diff: stdout });
599
- } catch (error) {
600
- res.json({ error: 'Git operation failed' });
601
- }
602
- });
603
-
604
- // Generate commit message based on staged changes using AI
605
- router.post('/generate-commit-message', async (req, res) => {
606
- const { project, files, provider = 'claude' } = req.body;
607
-
608
- if (!project || !files || files.length === 0) {
609
- return res.status(400).json({ error: 'Project name and files are required' });
610
- }
611
-
612
- if (!['claude', 'cursor'].includes(provider)) {
613
- return res.status(400).json({ error: 'provider must be "claude" or "cursor"' });
614
- }
615
-
616
- try {
617
- const projectPath = await getActualProjectPath(project);
618
-
619
- if (req.isCloud && !req.hasRelay()) {
620
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
621
- }
622
-
623
- let diffContext = '';
624
- for (const file of files) {
625
- try {
626
- const { stdout } = await relayGit(req, `diff HEAD -- "${file}"`, projectPath);
627
- if (stdout) {
628
- diffContext += `\n--- ${file} ---\n${stdout}`;
629
- }
630
- } catch (error) {
631
- // diff error
632
- }
633
- }
634
-
635
- if (!diffContext.trim()) {
636
- for (const file of files) {
637
- try {
638
- if (req.isCloud && req.hasRelay()) {
639
- const result = await req.sendRelay('file-read', { filePath: `${projectPath}/${file}` }, 15000);
640
- const content = result.content || '';
641
- diffContext += `\n--- ${file} (new file) ---\n${content.substring(0, 1000)}\n`;
642
- } else {
643
- const filePath = path.join(projectPath, file);
644
- const stats = await fs.stat(filePath);
645
- if (!stats.isDirectory()) {
646
- const content = await fs.readFile(filePath, 'utf-8');
647
- diffContext += `\n--- ${file} (new file) ---\n${content.substring(0, 1000)}\n`;
648
- } else {
649
- diffContext += `\n--- ${file} (new directory) ---\n`;
650
- }
651
- }
652
- } catch (error) {
653
- // file read error
654
- }
655
- }
656
- }
657
-
658
- const message = await generateCommitMessageWithAI(files, diffContext, provider, projectPath);
659
-
660
- res.json({ message });
661
- } catch (error) {
662
- res.status(500).json({ error: 'Git operation failed' });
663
- }
664
- });
665
-
666
- /**
667
- * Generates a commit message using AI (Claude SDK or Cursor CLI)
668
- * @param {Array<string>} files - List of changed files
669
- * @param {string} diffContext - Git diff content
670
- * @param {string} provider - 'claude' or 'cursor'
671
- * @param {string} projectPath - Project directory path
672
- * @returns {Promise<string>} Generated commit message
673
- */
674
- async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) {
675
- // Create the prompt
676
- const prompt = `Generate a conventional commit message for these changes.
677
-
678
- REQUIREMENTS:
679
- - Format: type(scope): subject
680
- - Include body explaining what changed and why
681
- - Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
682
- - Subject under 50 chars, body wrapped at 72 chars
683
- - Focus on user-facing changes, not implementation details
684
- - Consider what's being added AND removed
685
- - Return ONLY the commit message (no markdown, explanations, or code blocks)
686
-
687
- FILES CHANGED:
688
- ${files.map(f => `- ${f}`).join('\n')}
689
-
690
- DIFFS:
691
- ${diffContext.substring(0, 4000)}
692
-
693
- Generate the commit message:`;
694
-
695
- try {
696
- // Create a simple writer that collects the response
697
- let responseText = '';
698
- const writer = {
699
- send: (data) => {
700
- try {
701
- const parsed = typeof data === 'string' ? JSON.parse(data) : data;
702
-
703
- // Handle different message formats from Claude SDK and Cursor CLI
704
- // Claude SDK sends: {type: 'claude-response', data: {message: {content: [...]}}}
705
- if (parsed.type === 'claude-response' && parsed.data) {
706
- const message = parsed.data.message || parsed.data;
707
- if (message.content && Array.isArray(message.content)) {
708
- // Extract text from content array
709
- for (const item of message.content) {
710
- if (item.type === 'text' && item.text) {
711
- responseText += item.text;
712
- }
713
- }
714
- }
715
- }
716
- // Cursor CLI sends: {type: 'cursor-output', output: '...'}
717
- else if (parsed.type === 'cursor-output' && parsed.output) {
718
- responseText += parsed.output;
719
- }
720
- // Also handle direct text messages
721
- else if (parsed.type === 'text' && parsed.text) {
722
- responseText += parsed.text;
723
- }
724
- } catch (e) {
725
- // Ignore parse errors
726
- }
727
- },
728
- setSessionId: () => {}, // No-op for this use case
729
- };
730
-
731
-
732
- // Call the appropriate agent
733
- if (provider === 'claude') {
734
- await queryClaudeSDK(prompt, {
735
- cwd: projectPath,
736
- permissionMode: 'bypassPermissions',
737
- model: 'sonnet'
738
- }, writer);
739
- } else if (provider === 'cursor') {
740
- await spawnCursor(prompt, {
741
- cwd: projectPath,
742
- skipPermissions: true
743
- }, writer);
744
- }
745
-
746
-
747
- // Clean up the response
748
- const cleanedMessage = cleanCommitMessage(responseText);
749
-
750
- return cleanedMessage || 'chore: update files';
751
- } catch (error) {
752
- // AI commit message error
753
- // Fallback to simple message
754
- return `chore: update ${files.length} file${files.length !== 1 ? 's' : ''}`;
755
- }
756
- }
757
-
758
- /**
759
- * Cleans the AI-generated commit message by removing markdown, code blocks, and extra formatting
760
- * @param {string} text - Raw AI response
761
- * @returns {string} Clean commit message
762
- */
763
- function cleanCommitMessage(text) {
764
- if (!text || !text.trim()) {
765
- return '';
766
- }
767
-
768
- let cleaned = text.trim();
769
-
770
- // Remove markdown code blocks
771
- cleaned = cleaned.replace(/```[a-z]*\n/g, '');
772
- cleaned = cleaned.replace(/```/g, '');
773
-
774
- // Remove markdown headers
775
- cleaned = cleaned.replace(/^#+\s*/gm, '');
776
-
777
- // Remove leading/trailing quotes
778
- cleaned = cleaned.replace(/^["']|["']$/g, '');
779
-
780
- // If there are multiple lines, take everything (subject + body)
781
- // Just clean up extra blank lines
782
- cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
783
-
784
- // Remove any explanatory text before the actual commit message
785
- // Look for conventional commit pattern and start from there
786
- const conventionalCommitMatch = cleaned.match(/(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+?\))?:.+/s);
787
- if (conventionalCommitMatch) {
788
- cleaned = cleaned.substring(cleaned.indexOf(conventionalCommitMatch[0]));
789
- }
790
-
791
- return cleaned.trim();
792
- }
793
-
794
- // Get remote status (ahead/behind commits with smart remote detection)
795
- router.get('/remote-status', async (req, res) => {
796
- const { project } = req.query;
797
-
798
- if (!project) {
799
- return res.status(400).json({ error: 'Project name is required' });
800
- }
801
-
802
- try {
803
- const projectPath = await getActualProjectPath(project);
804
-
805
- if (req.isCloud && !req.hasRelay()) {
806
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
807
- }
808
-
809
- await validateGitRepo(req, projectPath);
810
-
811
- const { stdout: currentBranch } = await relayGit(req, 'rev-parse --abbrev-ref HEAD', projectPath);
812
- const branch = currentBranch.trim();
813
-
814
- let trackingBranch;
815
- let remoteName;
816
- try {
817
- const { stdout } = await relayGit(req, `rev-parse --abbrev-ref ${branch}@{upstream}`, projectPath);
818
- trackingBranch = stdout.trim();
819
- remoteName = trackingBranch.split('/')[0];
820
- } catch (error) {
821
- let hasRemote = false;
822
- let remoteName = null;
823
- try {
824
- const { stdout } = await relayGit(req, 'remote', projectPath);
825
- const remotes = stdout.trim().split('\n').filter(r => r.trim());
826
- if (remotes.length > 0) {
827
- hasRemote = true;
828
- remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
829
- }
830
- } catch (remoteError) {
831
- // No remotes configured
832
- }
833
-
834
- return res.json({
835
- hasRemote,
836
- hasUpstream: false,
837
- branch,
838
- remoteName,
839
- message: 'No remote tracking branch configured'
840
- });
841
- }
842
-
843
- const { stdout: countOutput } = await relayGit(req, `rev-list --count --left-right ${trackingBranch}...HEAD`, projectPath);
844
-
845
- const [behind, ahead] = countOutput.trim().split('\t').map(Number);
846
-
847
- res.json({
848
- hasRemote: true,
849
- hasUpstream: true,
850
- branch,
851
- remoteBranch: trackingBranch,
852
- remoteName,
853
- ahead: ahead || 0,
854
- behind: behind || 0,
855
- isUpToDate: ahead === 0 && behind === 0
856
- });
857
- } catch (error) {
858
- res.json({ error: 'Git operation failed' });
859
- }
860
- });
861
-
862
- // Fetch from remote (using smart remote detection)
863
- router.post('/fetch', async (req, res) => {
864
- const { project } = req.body;
865
-
866
- if (!project) {
867
- return res.status(400).json({ error: 'Project name is required' });
868
- }
869
-
870
- try {
871
- const projectPath = await getActualProjectPath(project);
872
-
873
- if (req.isCloud && !req.hasRelay()) {
874
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
875
- }
876
-
877
- await validateGitRepo(req, projectPath);
878
-
879
- const { stdout: currentBranch } = await relayGit(req, 'rev-parse --abbrev-ref HEAD', projectPath);
880
- const branch = currentBranch.trim();
881
-
882
- let remoteName = 'origin';
883
- try {
884
- const { stdout } = await relayGit(req, `rev-parse --abbrev-ref ${branch}@{upstream}`, projectPath);
885
- remoteName = stdout.trim().split('/')[0];
886
- } catch (error) {
887
- // No upstream, try to fetch from origin anyway
888
- }
889
-
890
- const { stdout } = await relayGit(req, `fetch ${remoteName}`, projectPath, 30000);
891
-
892
- res.json({ success: true, output: stdout || 'Fetch completed successfully', remoteName });
893
- } catch (error) {
894
- const msg = error.message || '';
895
- res.status(500).json({
896
- error: 'Fetch failed',
897
- details: msg.includes('Could not resolve hostname')
898
- ? 'Unable to connect to remote repository. Check your internet connection.'
899
- : msg.includes('does not appear to be a git repository')
900
- ? 'No remote repository configured.'
901
- : 'Failed to fetch from remote'
902
- });
903
- }
904
- });
905
-
906
- // Pull from remote (fetch + merge using smart remote detection)
907
- router.post('/pull', async (req, res) => {
908
- const { project } = req.body;
909
-
910
- if (!project) {
911
- return res.status(400).json({ error: 'Project name is required' });
912
- }
913
-
914
- try {
915
- const projectPath = await getActualProjectPath(project);
916
-
917
- if (req.isCloud && !req.hasRelay()) {
918
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
919
- }
920
-
921
- await validateGitRepo(req, projectPath);
922
-
923
- const { stdout: currentBranch } = await relayGit(req, 'rev-parse --abbrev-ref HEAD', projectPath);
924
- const branch = currentBranch.trim();
925
-
926
- let remoteName = 'origin';
927
- let remoteBranch = branch;
928
- try {
929
- const { stdout } = await relayGit(req, `rev-parse --abbrev-ref ${branch}@{upstream}`, projectPath);
930
- const tracking = stdout.trim();
931
- remoteName = tracking.split('/')[0];
932
- remoteBranch = tracking.split('/').slice(1).join('/');
933
- } catch (error) {
934
- // No upstream, use fallback
935
- }
936
-
937
- const { stdout } = await relayGit(req, `pull ${remoteName} ${remoteBranch}`, projectPath, 30000);
938
-
939
- res.json({
940
- success: true,
941
- output: stdout || 'Pull completed successfully',
942
- remoteName,
943
- remoteBranch
944
- });
945
- } catch (error) {
946
- let errorMessage = 'Pull failed';
947
- let details = 'Operation failed';
948
-
949
- if (error.message.includes('CONFLICT')) {
950
- errorMessage = 'Merge conflicts detected';
951
- details = 'Pull created merge conflicts. Please resolve conflicts manually in the editor, then commit the changes.';
952
- } else if (error.message.includes('Please commit your changes or stash them')) {
953
- errorMessage = 'Uncommitted changes detected';
954
- details = 'Please commit or stash your local changes before pulling.';
955
- } else if (error.message.includes('Could not resolve hostname')) {
956
- errorMessage = 'Network error';
957
- details = 'Unable to connect to remote repository. Check your internet connection.';
958
- } else if (error.message.includes('fatal: \'origin\' does not appear to be a git repository')) {
959
- errorMessage = 'Remote not configured';
960
- details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
961
- } else if (error.message.includes('diverged')) {
962
- errorMessage = 'Branches have diverged';
963
- details = 'Your local branch and remote branch have diverged. Consider fetching first to review changes.';
964
- }
965
-
966
- res.status(500).json({ error: errorMessage, details });
967
- }
968
- });
969
-
970
- // Push commits to remote repository
971
- router.post('/push', async (req, res) => {
972
- const { project } = req.body;
973
-
974
- if (!project) {
975
- return res.status(400).json({ error: 'Project name is required' });
976
- }
977
-
978
- try {
979
- const projectPath = await getActualProjectPath(project);
980
-
981
- if (req.isCloud && !req.hasRelay()) {
982
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
983
- }
984
-
985
- await validateGitRepo(req, projectPath);
986
-
987
- const { stdout: currentBranch } = await relayGit(req, 'rev-parse --abbrev-ref HEAD', projectPath);
988
- const branch = currentBranch.trim();
989
-
990
- let remoteName = 'origin';
991
- let remoteBranch = branch;
992
- try {
993
- const { stdout } = await relayGit(req, `rev-parse --abbrev-ref ${branch}@{upstream}`, projectPath);
994
- const tracking = stdout.trim();
995
- remoteName = tracking.split('/')[0];
996
- remoteBranch = tracking.split('/').slice(1).join('/');
997
- } catch (error) {
998
- // No upstream, use fallback
999
- }
1000
-
1001
- const { stdout } = await relayGit(req, `push ${remoteName} ${remoteBranch}`, projectPath, 30000);
1002
-
1003
- res.json({
1004
- success: true,
1005
- output: stdout || 'Push completed successfully',
1006
- remoteName,
1007
- remoteBranch
1008
- });
1009
- } catch (error) {
1010
- let errorMessage = 'Push failed';
1011
- let details = 'Operation failed';
1012
-
1013
- if (error.message.includes('rejected')) {
1014
- errorMessage = 'Push rejected';
1015
- details = 'The remote has newer commits. Pull first to merge changes before pushing.';
1016
- } else if (error.message.includes('non-fast-forward')) {
1017
- errorMessage = 'Non-fast-forward push';
1018
- details = 'Your branch is behind the remote. Pull the latest changes first.';
1019
- } else if (error.message.includes('Could not resolve hostname')) {
1020
- errorMessage = 'Network error';
1021
- details = 'Unable to connect to remote repository. Check your internet connection.';
1022
- } else if (error.message.includes('fatal: \'origin\' does not appear to be a git repository')) {
1023
- errorMessage = 'Remote not configured';
1024
- details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
1025
- } else if (error.message.includes('Permission denied')) {
1026
- errorMessage = 'Authentication failed';
1027
- details = 'Permission denied. Check your credentials or SSH keys.';
1028
- } else if (error.message.includes('no upstream branch')) {
1029
- errorMessage = 'No upstream branch';
1030
- details = 'No upstream branch configured. Use: git push --set-upstream origin <branch>';
1031
- }
1032
-
1033
- res.status(500).json({ error: errorMessage, details });
1034
- }
1035
- });
1036
-
1037
- // Publish branch to remote (set upstream and push)
1038
- router.post('/publish', async (req, res) => {
1039
- const { project, branch } = req.body;
1040
-
1041
- if (!project || !branch) {
1042
- return res.status(400).json({ error: 'Project name and branch are required' });
1043
- }
1044
-
1045
- try {
1046
- const projectPath = await getActualProjectPath(project);
1047
-
1048
- if (req.isCloud && !req.hasRelay()) {
1049
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
1050
- }
1051
-
1052
- await validateGitRepo(req, projectPath);
1053
-
1054
- const { stdout: currentBranch } = await relayGit(req, 'rev-parse --abbrev-ref HEAD', projectPath);
1055
- const currentBranchName = currentBranch.trim();
1056
-
1057
- if (currentBranchName !== branch) {
1058
- return res.status(400).json({
1059
- error: `Branch mismatch. Current branch is ${currentBranchName}, but trying to publish ${branch}`
1060
- });
1061
- }
1062
-
1063
- let remoteName = 'origin';
1064
- try {
1065
- const { stdout } = await relayGit(req, 'remote', projectPath);
1066
- const remotes = stdout.trim().split('\n').filter(r => r.trim());
1067
- if (remotes.length === 0) {
1068
- return res.status(400).json({
1069
- error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
1070
- });
1071
- }
1072
- remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
1073
- } catch (error) {
1074
- return res.status(400).json({
1075
- error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
1076
- });
1077
- }
1078
-
1079
- const { stdout } = await relayGit(req, `push --set-upstream ${remoteName} ${branch}`, projectPath, 30000);
1080
-
1081
- res.json({
1082
- success: true,
1083
- output: stdout || 'Branch published successfully',
1084
- remoteName,
1085
- branch
1086
- });
1087
- } catch (error) {
1088
- let errorMessage = 'Publish failed';
1089
- let details = 'Operation failed';
1090
-
1091
- if (error.message.includes('rejected')) {
1092
- errorMessage = 'Publish rejected';
1093
- details = 'The remote branch already exists and has different commits. Use push instead.';
1094
- } else if (error.message.includes('Could not resolve hostname')) {
1095
- errorMessage = 'Network error';
1096
- details = 'Unable to connect to remote repository. Check your internet connection.';
1097
- } else if (error.message.includes('Permission denied')) {
1098
- errorMessage = 'Authentication failed';
1099
- details = 'Permission denied. Check your credentials or SSH keys.';
1100
- } else if (error.message.includes('fatal:') && error.message.includes('does not appear to be a git repository')) {
1101
- errorMessage = 'Remote not configured';
1102
- details = 'Remote repository not properly configured. Check your remote URL.';
1103
- }
1104
-
1105
- res.status(500).json({ error: errorMessage, details });
1106
- }
1107
- });
1108
-
1109
- // Discard changes for a specific file
1110
- router.post('/discard', async (req, res) => {
1111
- const { project, file } = req.body;
1112
-
1113
- if (!project || !file) {
1114
- return res.status(400).json({ error: 'Project name and file path are required' });
1115
- }
1116
-
1117
- try {
1118
- const projectPath = await getActualProjectPath(project);
1119
-
1120
- if (req.isCloud && !req.hasRelay()) {
1121
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
1122
- }
1123
-
1124
- await validateGitRepo(req, projectPath);
1125
-
1126
- const { stdout: statusOutput } = await relayGit(req, `status --porcelain "${file}"`, projectPath);
1127
-
1128
- if (!statusOutput.trim()) {
1129
- return res.status(400).json({ error: 'No changes to discard for this file' });
1130
- }
1131
-
1132
- const status = statusOutput.substring(0, 2);
1133
-
1134
- if (status === '??') {
1135
- if (req.isCloud && req.hasRelay()) {
1136
- // Delete untracked file via relay shell command
1137
- await req.sendRelay('shell-command', { command: `rm -rf "${file}"`, cwd: projectPath }, 15000);
1138
- } else {
1139
- const filePath = path.join(projectPath, file);
1140
- const stats = await fs.stat(filePath);
1141
- if (stats.isDirectory()) {
1142
- await fs.rm(filePath, { recursive: true, force: true });
1143
- } else {
1144
- await fs.unlink(filePath);
1145
- }
1146
- }
1147
- } else if (status.includes('M') || status.includes('D')) {
1148
- await relayGit(req, `restore "${file}"`, projectPath);
1149
- } else if (status.includes('A')) {
1150
- await relayGit(req, `reset HEAD "${file}"`, projectPath);
1151
- }
1152
-
1153
- res.json({ success: true, message: `Changes discarded for ${file}` });
1154
- } catch (error) {
1155
- res.status(500).json({ error: 'Git operation failed' });
1156
- }
1157
- });
1158
-
1159
- // Delete untracked file
1160
- router.post('/delete-untracked', async (req, res) => {
1161
- const { project, file } = req.body;
1162
-
1163
- if (!project || !file) {
1164
- return res.status(400).json({ error: 'Project name and file path are required' });
1165
- }
1166
-
1167
- try {
1168
- const projectPath = await getActualProjectPath(project);
1169
-
1170
- if (req.isCloud && !req.hasRelay()) {
1171
- return res.status(503).json({ error: 'Machine not connected', message: 'Run "uc connect" on your local machine to use this feature.', code: 'RELAY_NOT_CONNECTED' });
1172
- }
1173
-
1174
- await validateGitRepo(req, projectPath);
1175
-
1176
- const { stdout: statusOutput } = await relayGit(req, `status --porcelain "${file}"`, projectPath);
1177
-
1178
- if (!statusOutput.trim()) {
1179
- return res.status(400).json({ error: 'File is not untracked or does not exist' });
1180
- }
1181
-
1182
- const status = statusOutput.substring(0, 2);
1183
-
1184
- if (status !== '??') {
1185
- return res.status(400).json({ error: 'File is not untracked. Use discard for tracked files.' });
1186
- }
1187
-
1188
- if (req.isCloud && req.hasRelay()) {
1189
- await req.sendRelay('shell-command', { command: `rm -rf "${file}"`, cwd: projectPath }, 15000);
1190
- res.json({ success: true, message: `Untracked file/directory ${file} deleted successfully` });
1191
- } else {
1192
- const filePath = path.join(projectPath, file);
1193
- const stats = await fs.stat(filePath);
1194
-
1195
- if (stats.isDirectory()) {
1196
- await fs.rm(filePath, { recursive: true, force: true });
1197
- res.json({ success: true, message: `Untracked directory ${file} deleted successfully` });
1198
- } else {
1199
- await fs.unlink(filePath);
1200
- res.json({ success: true, message: `Untracked file ${file} deleted successfully` });
1201
- }
1202
- }
1203
- } catch (error) {
1204
- res.status(500).json({ error: 'Git operation failed' });
1205
- }
1206
- });
1207
-
1208
- export default router;