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.
- package/README.md +69 -92
- package/bin/cli.js +191 -0
- package/dist/client/assets/AppContent-M14Au3SB.js +542 -0
- package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-TFKm2NDJ.js} +2 -2
- package/dist/client/assets/DashboardPanel-C88HjsCh.js +1 -0
- package/dist/client/assets/FileTree-DvO1xnDE.js +1 -0
- package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-D-slVlyy.js} +2 -2
- package/dist/client/assets/LoginModal-Chi4SYcr.js +21 -0
- package/{client/dist/assets/MarkdownPreview-CESjI261.js → dist/client/assets/MarkdownPreview-CuIix2u9.js} +1 -1
- package/dist/client/assets/MermaidBlock-Dq9uFv82.js +2 -0
- package/dist/client/assets/Onboarding-QYXx24dX.js +1 -0
- package/{client/dist/assets/PreviewPanel-CqCa92Tf.js → dist/client/assets/PreviewPanel-Dd8q-jo0.js} +1 -1
- package/dist/client/assets/SetupForm-CrspaUva.js +1 -0
- package/dist/client/assets/WorkflowsPanel-DIlYAdhB.js +1 -0
- package/dist/client/assets/index-CnNNzw9A.css +1 -0
- package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-rUkK9FDP.js} +26 -26
- package/{client/dist/assets/vendor-codemirror-D2ALgpaX.js → dist/client/assets/vendor-codemirror-jc6nyJQg.js} +1 -1
- package/{client/dist/assets/vendor-diff-DNQpbhrT.js → dist/client/assets/vendor-diff-THJmAcEI.js} +1 -1
- package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-CfjIpdrD.js} +145 -155
- package/{client/dist/assets/vendor-markdown-CimbIo6Y.js → dist/client/assets/vendor-markdown-Cdm6NEGf.js} +1 -1
- package/dist/client/assets/vendor-mermaid-DTPaBx-U.js +2559 -0
- package/{client/dist/assets/vendor-react-96lCPsRK.js → dist/client/assets/vendor-react-wFkb6mSf.js} +1 -1
- package/{client/dist/assets/vendor-syntax-LS_Nt30I.js → dist/client/assets/vendor-syntax-C_UZR7tc.js} +1 -1
- package/dist/client/favicon.png +0 -0
- package/dist/client/icons/icon-128x128.png +0 -0
- package/dist/client/icons/icon-144x144.png +0 -0
- package/dist/client/icons/icon-152x152.png +0 -0
- package/dist/client/icons/icon-192x192.png +0 -0
- package/dist/client/icons/icon-384x384.png +0 -0
- package/dist/client/icons/icon-512x512.png +0 -0
- package/dist/client/icons/icon-72x72.png +0 -0
- package/dist/client/icons/icon-96x96.png +0 -0
- package/{client/dist → dist/client}/index.html +37 -36
- package/dist/client/logo-128.png +0 -0
- package/dist/client/logo-256.png +0 -0
- package/dist/client/logo-32.png +0 -0
- package/dist/client/logo-512.png +0 -0
- package/dist/client/logo-64.png +0 -0
- package/dist/client/logo.png +0 -0
- package/{client/dist → dist/client}/manifest.json +12 -12
- package/{client/dist → dist/client}/mcp-docs.html +1 -1
- package/{client/dist → dist/client}/sw.js +2 -2
- package/package.json +56 -105
- package/scripts/postinstall.js +9 -0
- package/scripts/prepublish.js +77 -0
- package/src/animation.js +228 -0
- package/src/auth.js +142 -0
- package/src/config.js +40 -0
- package/src/connect.js +416 -0
- package/src/launch.js +81 -0
- package/src/mcp.js +57 -0
- package/src/permissions.js +140 -0
- package/src/persistent-shell.js +261 -0
- package/src/server.js +54 -0
- package/client/dist/assets/AppContent-CwrTP6TW.js +0 -545
- package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
- package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
- package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
- package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
- package/client/dist/assets/LoginModal-CImJHRjX.js +0 -13
- package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
- package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
- package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
- package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
- package/client/dist/assets/index-46kkVu2i.css +0 -1
- package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
- package/client/dist/assets/vendor-canvas-BZV40eAE.css +0 -1
- package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
- package/client/dist/assets/vendor-mermaid-DucWyDEe.js +0 -2556
- package/client/dist/favicon.png +0 -0
- package/client/dist/icons/icon-128x128.png +0 -0
- package/client/dist/icons/icon-144x144.png +0 -0
- package/client/dist/icons/icon-152x152.png +0 -0
- package/client/dist/icons/icon-192x192.png +0 -0
- package/client/dist/icons/icon-384x384.png +0 -0
- package/client/dist/icons/icon-512x512.png +0 -0
- package/client/dist/icons/icon-72x72.png +0 -0
- package/client/dist/icons/icon-96x96.png +0 -0
- package/client/dist/logo-128.png +0 -0
- package/client/dist/logo-256.png +0 -0
- package/client/dist/logo-32.png +0 -0
- package/client/dist/logo-512.png +0 -0
- package/client/dist/logo-64.png +0 -0
- package/commands/upfynai-connect.md +0 -59
- package/commands/upfynai-disconnect.md +0 -31
- package/commands/upfynai-doctor.md +0 -99
- package/commands/upfynai-export.md +0 -49
- package/commands/upfynai-local.md +0 -82
- package/commands/upfynai-status.md +0 -75
- package/commands/upfynai-stop.md +0 -49
- package/commands/upfynai-uninstall.md +0 -58
- package/commands/upfynai.md +0 -69
- package/scripts/build-client.js +0 -17
- package/scripts/fix-node-pty.js +0 -67
- package/scripts/install-commands.js +0 -78
- package/server/agent-loop.js +0 -242
- package/server/auto-compact.js +0 -99
- package/server/browser.js +0 -131
- package/server/claude-sdk.js +0 -797
- package/server/cli-ui.js +0 -798
- package/server/cli.js +0 -751
- package/server/constants/config.js +0 -31
- package/server/cursor-cli.js +0 -270
- package/server/database/auth.db +0 -0
- package/server/database/db.js +0 -1547
- package/server/database/init.sql +0 -70
- package/server/index.js +0 -3813
- package/server/load-env.js +0 -26
- package/server/mcp-server.js +0 -621
- package/server/middleware/auth.js +0 -184
- package/server/middleware/relayHelpers.js +0 -44
- package/server/middleware/sandboxRouter.js +0 -174
- package/server/openai-codex.js +0 -403
- package/server/openrouter.js +0 -137
- package/server/projects.js +0 -1807
- package/server/provider-factory.js +0 -174
- package/server/relay-client.js +0 -390
- package/server/routes/agent.js +0 -1234
- package/server/routes/auth.js +0 -559
- package/server/routes/browser.js +0 -419
- package/server/routes/canvas.js +0 -53
- package/server/routes/cli-auth.js +0 -263
- package/server/routes/codex.js +0 -396
- package/server/routes/commands.js +0 -707
- package/server/routes/composio.js +0 -176
- package/server/routes/cursor.js +0 -770
- package/server/routes/dashboard.js +0 -295
- package/server/routes/git.js +0 -1208
- package/server/routes/keys.js +0 -34
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -661
- package/server/routes/payments.js +0 -227
- package/server/routes/projects.js +0 -754
- package/server/routes/sessions.js +0 -146
- package/server/routes/settings.js +0 -261
- package/server/routes/taskmaster.js +0 -1928
- package/server/routes/user.js +0 -106
- package/server/routes/vapi-chat.js +0 -624
- package/server/routes/voice.js +0 -235
- package/server/routes/webhooks.js +0 -166
- package/server/routes/workflows.js +0 -312
- package/server/sandbox.js +0 -120
- package/server/services/browser-ai.js +0 -154
- package/server/services/composio.js +0 -204
- package/server/services/sessionRegistry.js +0 -139
- package/server/services/whisperService.js +0 -84
- package/server/services/workflowScheduler.js +0 -211
- package/server/tests/relay-flow.test.js +0 -570
- package/server/tests/sessions.test.js +0 -259
- package/server/utils/commandParser.js +0 -303
- package/server/utils/email.js +0 -66
- package/server/utils/gitConfig.js +0 -24
- package/server/utils/mcp-detector.js +0 -198
- package/server/utils/taskmaster-websocket.js +0 -129
- package/shared/integrationCatalog.d.ts +0 -12
- package/shared/integrationCatalog.js +0 -172
- package/shared/modelConstants.js +0 -96
- /package/{shared → dist}/agents/claude.js +0 -0
- /package/{shared → dist}/agents/codex.js +0 -0
- /package/{shared → dist}/agents/cursor.js +0 -0
- /package/{shared → dist}/agents/detect.js +0 -0
- /package/{shared → dist}/agents/exec.js +0 -0
- /package/{shared → dist}/agents/files.js +0 -0
- /package/{shared → dist}/agents/git.js +0 -0
- /package/{shared → dist}/agents/gitagent.js +0 -0
- /package/{shared → dist}/agents/index.js +0 -0
- /package/{shared → dist}/agents/shell.js +0 -0
- /package/{shared → dist}/agents/utils.js +0 -0
- /package/{client/dist → dist/client}/api-docs.html +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
- /package/{client/dist → dist/client}/clear-cache.html +0 -0
- /package/{client/dist → dist/client}/convert-icons.md +0 -0
- /package/{client/dist → dist/client}/favicon.svg +0 -0
- /package/{client/dist → dist/client}/generate-icons.js +0 -0
- /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
- /package/{client/dist → dist/client}/logo.svg +0 -0
- /package/{client/dist → dist/client}/offline.html +0 -0
- /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
- /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
- /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
- /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
- /package/{shared → dist}/gitagent/index.js +0 -0
- /package/{shared → dist}/gitagent/parser.js +0 -0
- /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
package/server/routes/git.js
DELETED
|
@@ -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;
|