@cloudcli-ai/cloudcli 1.35.1 → 1.36.1
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 +8 -3
- package/dist/assets/{index-D1IdCTT6.js → index-Bz9vXV2m.js} +265 -260
- 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 +62 -116
- 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-models.provider.js +70 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.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-models.provider.js +52 -11
- package/dist-server/server/modules/providers/list/codex/codex-models.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 +145 -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 +11 -13
- 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 +2 -2
- 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 +65 -1
- 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 +16 -9
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +62 -7
- 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 +22 -21
- 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 +3 -3
- package/public/api-docs.html +8 -3
- package/server/claude-sdk.js +69 -131
- 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-models.provider.ts +73 -1
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +59 -11
- 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 +193 -18
- 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 +13 -13
- package/server/modules/providers/services/provider-models.service.ts +2 -2
- 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 +69 -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 +18 -8
- package/server/opencode-cli.js +64 -7
- package/server/opencode-cli.test.js +112 -3
- package/server/routes/agent.js +22 -21
- 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 +8 -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-BvX30NW5.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/agent.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 os from 'os';
|
|
5
6
|
import { promises as fs } from 'fs';
|
|
@@ -8,7 +9,6 @@ import { userDb, apiKeysDb, githubTokensDb, projectsDb } from '../modules/databa
|
|
|
8
9
|
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
9
10
|
import { spawnCursor } from '../cursor-cli.js';
|
|
10
11
|
import { queryCodex } from '../openai-codex.js';
|
|
11
|
-
import { spawnGemini } from '../gemini-cli.js';
|
|
12
12
|
import { spawnOpenCode } from '../opencode-cli.js';
|
|
13
13
|
import { Octokit } from '@octokit/rest';
|
|
14
14
|
import { providerModelsService } from '../modules/providers/services/provider-models.service.js';
|
|
@@ -636,7 +636,7 @@ class ResponseCollector {
|
|
|
636
636
|
* - Source for auto-generated branch names (if createBranch=true and no branchName)
|
|
637
637
|
* - Fallback for PR title if no commits are made
|
|
638
638
|
*
|
|
639
|
-
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | '
|
|
639
|
+
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'opencode'
|
|
640
640
|
* Default: 'claude'
|
|
641
641
|
*
|
|
642
642
|
* @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
|
|
@@ -646,12 +646,17 @@ class ResponseCollector {
|
|
|
646
646
|
*
|
|
647
647
|
* @param {string} model - (Optional) Model identifier for providers.
|
|
648
648
|
*
|
|
649
|
-
* Claude models: 'sonnet'
|
|
649
|
+
* Claude models: 'default', 'sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]', 'fable'
|
|
650
650
|
* Cursor models: 'gpt-5' (default), 'gpt-5.2', 'gpt-5.2-high', 'sonnet-4.5', 'opus-4.5',
|
|
651
|
-
* '
|
|
651
|
+
* 'composer-1', 'auto', 'gpt-5.1', 'gpt-5.1-high',
|
|
652
652
|
* 'gpt-5.1-codex', 'gpt-5.1-codex-high', 'gpt-5.1-codex-max',
|
|
653
653
|
* 'gpt-5.1-codex-max-high', 'opus-4.1', 'grok', and thinking variants
|
|
654
|
-
* Codex models: 'gpt-5.
|
|
654
|
+
* Codex models: 'gpt-5.4' (default), 'gpt-5.5', 'gpt-5.4-mini'
|
|
655
|
+
*
|
|
656
|
+
* @param {string} effort - (Optional) Reasoning effort for providers/models that support it.
|
|
657
|
+
* Claude supports: 'low', 'medium', 'high', 'xhigh', 'max' depending on model.
|
|
658
|
+
* Codex supports: 'low', 'medium', 'high', 'xhigh'.
|
|
659
|
+
* 'default' or omission lets the provider decide.
|
|
655
660
|
*
|
|
656
661
|
* @param {boolean} cleanup - (Optional) Auto-cleanup project directory after completion.
|
|
657
662
|
* Default: true
|
|
@@ -754,7 +759,7 @@ class ResponseCollector {
|
|
|
754
759
|
* Input Validations (400 Bad Request):
|
|
755
760
|
* - Either githubUrl OR projectPath must be provided (not neither)
|
|
756
761
|
* - message must be non-empty string
|
|
757
|
-
* - provider must be 'claude', 'cursor', 'codex',
|
|
762
|
+
* - provider must be 'claude', 'cursor', 'codex', or 'opencode'
|
|
758
763
|
* - createBranch/createPR requires githubUrl OR projectPath (not neither)
|
|
759
764
|
* - branchName must pass Git naming rules (if provided)
|
|
760
765
|
*
|
|
@@ -844,6 +849,9 @@ class ResponseCollector {
|
|
|
844
849
|
*/
|
|
845
850
|
router.post('/', validateExternalApiKey, async (req, res) => {
|
|
846
851
|
const { githubUrl, projectPath, message, provider = 'claude', model, githubToken, branchName, sessionId } = req.body;
|
|
852
|
+
const effort = typeof req.body.effort === 'string' && req.body.effort.trim()
|
|
853
|
+
? req.body.effort.trim()
|
|
854
|
+
: undefined;
|
|
847
855
|
|
|
848
856
|
// Parse stream and cleanup as booleans (handle string "true"/"false" from curl)
|
|
849
857
|
const stream = req.body.stream === undefined ? true : (req.body.stream === true || req.body.stream === 'true');
|
|
@@ -862,8 +870,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
862
870
|
return res.status(400).json({ error: 'message is required' });
|
|
863
871
|
}
|
|
864
872
|
|
|
865
|
-
if (!['claude', 'cursor', 'codex', '
|
|
866
|
-
return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex",
|
|
873
|
+
if (!['claude', 'cursor', 'codex', 'opencode'].includes(provider)) {
|
|
874
|
+
return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", or "opencode"' });
|
|
867
875
|
}
|
|
868
876
|
|
|
869
877
|
// Validate GitHub branch/PR creation requirements
|
|
@@ -942,7 +950,6 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
942
950
|
}
|
|
943
951
|
|
|
944
952
|
const codexModels = (await providerModelsService.getProviderModels('codex')).models;
|
|
945
|
-
const geminiModels = (await providerModelsService.getProviderModels('gemini')).models;
|
|
946
953
|
const opencodeModels = (await providerModelsService.getProviderModels('opencode')).models;
|
|
947
954
|
|
|
948
955
|
// Start the appropriate session
|
|
@@ -954,6 +961,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
954
961
|
cwd: finalProjectPath,
|
|
955
962
|
sessionId: sessionId || null,
|
|
956
963
|
model: model,
|
|
964
|
+
effort,
|
|
957
965
|
permissionMode: 'bypassPermissions' // Bypass all permissions for API calls
|
|
958
966
|
}, writer);
|
|
959
967
|
|
|
@@ -975,18 +983,9 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
975
983
|
cwd: finalProjectPath,
|
|
976
984
|
sessionId: sessionId || null,
|
|
977
985
|
model: model || codexModels.DEFAULT,
|
|
986
|
+
effort,
|
|
978
987
|
permissionMode: 'bypassPermissions'
|
|
979
988
|
}, writer);
|
|
980
|
-
} else if (provider === 'gemini') {
|
|
981
|
-
console.log('✨ Starting Gemini CLI session');
|
|
982
|
-
|
|
983
|
-
await spawnGemini(message.trim(), {
|
|
984
|
-
projectPath: finalProjectPath,
|
|
985
|
-
cwd: finalProjectPath,
|
|
986
|
-
sessionId: sessionId || null,
|
|
987
|
-
model: model || geminiModels.DEFAULT,
|
|
988
|
-
skipPermissions: true // CLI mode bypasses permissions
|
|
989
|
-
}, writer);
|
|
990
989
|
} else if (provider === 'opencode') {
|
|
991
990
|
console.log('Starting OpenCode CLI session');
|
|
992
991
|
|
|
@@ -994,7 +993,9 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
994
993
|
projectPath: finalProjectPath,
|
|
995
994
|
cwd: finalProjectPath,
|
|
996
995
|
sessionId: sessionId || null,
|
|
997
|
-
model: model || opencodeModels.DEFAULT
|
|
996
|
+
model: model || opencodeModels.DEFAULT,
|
|
997
|
+
effort,
|
|
998
|
+
permissionMode: 'bypassPermissions' // Agent runs are non-interactive, like the other providers above
|
|
998
999
|
}, writer);
|
|
999
1000
|
}
|
|
1000
1001
|
|
|
@@ -15,13 +15,12 @@ const APP_ROOT = findAppRoot(__dirname);
|
|
|
15
15
|
|
|
16
16
|
const router = express.Router();
|
|
17
17
|
|
|
18
|
-
const MODEL_PROVIDERS = ["claude", "cursor", "codex", "
|
|
18
|
+
const MODEL_PROVIDERS = ["claude", "cursor", "codex", "opencode"];
|
|
19
19
|
|
|
20
20
|
const MODEL_PROVIDER_LABELS = {
|
|
21
21
|
claude: "Claude",
|
|
22
22
|
cursor: "Cursor",
|
|
23
23
|
codex: "Codex",
|
|
24
|
-
gemini: "Gemini",
|
|
25
24
|
opencode: "OpenCode",
|
|
26
25
|
};
|
|
27
26
|
|
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
|
|