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