@openchamber/web 1.3.2 → 1.3.3
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/dist/assets/{ToolOutputDialog-DYy4uTNv.js → ToolOutputDialog-v89bayPj.js} +1 -1
- package/dist/assets/{index-0wDVT6MK.js → index-BAp9MHgd.js} +2 -2
- package/dist/assets/index-DST7Nr3C.css +1 -0
- package/dist/assets/main-BGkJ2buW.js +118 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/server/index.js +240 -9
- package/server/lib/git-service.js +63 -31
- package/dist/assets/index-a7wSdHza.css +0 -1
- package/dist/assets/main-K-504lt8.js +0 -118
package/dist/index.html
CHANGED
|
@@ -160,10 +160,10 @@
|
|
|
160
160
|
pointer-events: none;
|
|
161
161
|
}
|
|
162
162
|
</style>
|
|
163
|
-
<script type="module" crossorigin src="/assets/index-
|
|
163
|
+
<script type="module" crossorigin src="/assets/index-BAp9MHgd.js"></script>
|
|
164
164
|
<link rel="modulepreload" crossorigin href="/assets/vendor-.pnpm-CFPpXnpS.js">
|
|
165
165
|
<link rel="stylesheet" crossorigin href="/assets/vendor--B3aGWKBE.css">
|
|
166
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
166
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DST7Nr3C.css">
|
|
167
167
|
</head>
|
|
168
168
|
<body class="h-full bg-background text-foreground">
|
|
169
169
|
<div id="root" class="h-full">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openchamber/web",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./server/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
|
26
26
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
|
27
27
|
"@ibm/plex": "^6.4.1",
|
|
28
|
-
"@opencode-ai/sdk": "^1.0.
|
|
28
|
+
"@opencode-ai/sdk": "^1.0.185",
|
|
29
29
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
30
30
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
31
31
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
package/server/index.js
CHANGED
|
@@ -37,6 +37,27 @@ const FILE_SEARCH_EXCLUDED_DIRS = new Set([
|
|
|
37
37
|
'logs'
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
|
+
const normalizeDirectoryPath = (value) => {
|
|
41
|
+
if (typeof value !== 'string') {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const trimmed = value.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (trimmed === '~') {
|
|
51
|
+
return os.homedir();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
55
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return trimmed;
|
|
59
|
+
};
|
|
60
|
+
|
|
40
61
|
const normalizeRelativeSearchPath = (rootPath, targetPath) => {
|
|
41
62
|
const relative = path.relative(rootPath, targetPath) || path.basename(targetPath);
|
|
42
63
|
return relative.split(path.sep).join('/') || targetPath;
|
|
@@ -677,6 +698,89 @@ function buildOpenCodeUrl(path, prefixOverride) {
|
|
|
677
698
|
return `http://localhost:${openCodePort}${fullPath}`;
|
|
678
699
|
}
|
|
679
700
|
|
|
701
|
+
function parseSseDataPayload(block) {
|
|
702
|
+
if (!block || typeof block !== 'string') {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
const dataLines = block
|
|
706
|
+
.split('\n')
|
|
707
|
+
.filter((line) => line.startsWith('data:'))
|
|
708
|
+
.map((line) => line.slice(5).replace(/^\s/, ''));
|
|
709
|
+
|
|
710
|
+
if (dataLines.length === 0) {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const payloadText = dataLines.join('\n').trim();
|
|
715
|
+
if (!payloadText) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
try {
|
|
720
|
+
return JSON.parse(payloadText);
|
|
721
|
+
} catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function deriveSessionActivity(payload) {
|
|
727
|
+
if (!payload || typeof payload !== 'object') {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (payload.type === 'session.status') {
|
|
732
|
+
const status = payload.properties?.status;
|
|
733
|
+
const sessionId = payload.properties?.sessionID;
|
|
734
|
+
const statusType = status?.type;
|
|
735
|
+
|
|
736
|
+
if (typeof sessionId === 'string' && sessionId.length > 0 && typeof statusType === 'string') {
|
|
737
|
+
const phase = statusType === 'busy' || statusType === 'retry' ? 'busy' : 'idle';
|
|
738
|
+
return { sessionId, phase };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (payload.type === 'message.updated') {
|
|
743
|
+
const info = payload.properties?.info;
|
|
744
|
+
const sessionId = info?.sessionID;
|
|
745
|
+
const role = info?.role;
|
|
746
|
+
const finish = info?.finish;
|
|
747
|
+
if (typeof sessionId === 'string' && sessionId.length > 0 && role === 'assistant' && finish === 'stop') {
|
|
748
|
+
return { sessionId, phase: 'cooldown' };
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (payload.type === 'message.part.updated') {
|
|
753
|
+
const info = payload.properties?.info;
|
|
754
|
+
const part = payload.properties?.part;
|
|
755
|
+
const sessionId = info?.sessionID ?? part?.sessionID ?? payload.properties?.sessionID;
|
|
756
|
+
const role = info?.role;
|
|
757
|
+
const partType = part?.type;
|
|
758
|
+
const reason = part?.reason;
|
|
759
|
+
if (
|
|
760
|
+
typeof sessionId === 'string' &&
|
|
761
|
+
sessionId.length > 0 &&
|
|
762
|
+
role === 'assistant' &&
|
|
763
|
+
partType === 'step-finish' &&
|
|
764
|
+
reason === 'stop'
|
|
765
|
+
) {
|
|
766
|
+
return { sessionId, phase: 'cooldown' };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (payload.type === 'session.idle') {
|
|
771
|
+
const sessionId = payload.properties?.sessionID;
|
|
772
|
+
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
|
773
|
+
return { sessionId, phase: 'idle' };
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function writeSseEvent(res, payload) {
|
|
781
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
782
|
+
}
|
|
783
|
+
|
|
680
784
|
function extractApiPrefixFromUrl(urlString, expectedSuffix) {
|
|
681
785
|
if (!urlString) {
|
|
682
786
|
return null;
|
|
@@ -1764,6 +1868,129 @@ async function main(options = {}) {
|
|
|
1764
1868
|
}
|
|
1765
1869
|
});
|
|
1766
1870
|
|
|
1871
|
+
app.get('/api/event', async (req, res) => {
|
|
1872
|
+
if (!openCodePort) {
|
|
1873
|
+
return res.status(503).json({ error: 'OpenCode service unavailable' });
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
if (!openCodeApiPrefixDetected) {
|
|
1877
|
+
try {
|
|
1878
|
+
await detectOpenCodeApiPrefix();
|
|
1879
|
+
} catch {
|
|
1880
|
+
// ignore detection failures
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
let targetUrl;
|
|
1885
|
+
try {
|
|
1886
|
+
const prefix = openCodeApiPrefixDetected ? openCodeApiPrefix : '';
|
|
1887
|
+
targetUrl = new URL(buildOpenCodeUrl('/event', prefix));
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
return res.status(503).json({ error: 'OpenCode service unavailable' });
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const directoryParam = Array.isArray(req.query.directory)
|
|
1893
|
+
? req.query.directory[0]
|
|
1894
|
+
: req.query.directory;
|
|
1895
|
+
if (typeof directoryParam === 'string' && directoryParam.trim().length > 0) {
|
|
1896
|
+
targetUrl.searchParams.set('directory', directoryParam.trim());
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const headers = {
|
|
1900
|
+
Accept: 'text/event-stream',
|
|
1901
|
+
'Cache-Control': 'no-cache',
|
|
1902
|
+
Connection: 'keep-alive'
|
|
1903
|
+
};
|
|
1904
|
+
|
|
1905
|
+
const lastEventId = req.header('Last-Event-ID');
|
|
1906
|
+
if (typeof lastEventId === 'string' && lastEventId.length > 0) {
|
|
1907
|
+
headers['Last-Event-ID'] = lastEventId;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const controller = new AbortController();
|
|
1911
|
+
const cleanup = () => {
|
|
1912
|
+
if (!controller.signal.aborted) {
|
|
1913
|
+
controller.abort();
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1917
|
+
req.on('close', cleanup);
|
|
1918
|
+
req.on('error', cleanup);
|
|
1919
|
+
|
|
1920
|
+
let upstream;
|
|
1921
|
+
try {
|
|
1922
|
+
upstream = await fetch(targetUrl.toString(), {
|
|
1923
|
+
headers,
|
|
1924
|
+
signal: controller.signal,
|
|
1925
|
+
});
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
return res.status(502).json({ error: 'Failed to connect to OpenCode event stream' });
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
if (!upstream.ok || !upstream.body) {
|
|
1931
|
+
return res.status(502).json({ error: `OpenCode event stream unavailable (${upstream.status})` });
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
1935
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1936
|
+
res.setHeader('Connection', 'keep-alive');
|
|
1937
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
1938
|
+
|
|
1939
|
+
if (typeof res.flushHeaders === 'function') {
|
|
1940
|
+
res.flushHeaders();
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
const decoder = new TextDecoder();
|
|
1944
|
+
const reader = upstream.body.getReader();
|
|
1945
|
+
let buffer = '';
|
|
1946
|
+
|
|
1947
|
+
const forwardBlock = (block) => {
|
|
1948
|
+
if (!block) return;
|
|
1949
|
+
res.write(`${block}\n\n`);
|
|
1950
|
+
const payload = parseSseDataPayload(block);
|
|
1951
|
+
const activity = deriveSessionActivity(payload);
|
|
1952
|
+
if (activity) {
|
|
1953
|
+
writeSseEvent(res, {
|
|
1954
|
+
type: 'openchamber:session-activity',
|
|
1955
|
+
properties: {
|
|
1956
|
+
sessionId: activity.sessionId,
|
|
1957
|
+
phase: activity.phase,
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
try {
|
|
1964
|
+
while (true) {
|
|
1965
|
+
const { value, done } = await reader.read();
|
|
1966
|
+
if (done) break;
|
|
1967
|
+
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, '\n');
|
|
1968
|
+
|
|
1969
|
+
let separatorIndex;
|
|
1970
|
+
while ((separatorIndex = buffer.indexOf('\n\n')) !== -1) {
|
|
1971
|
+
const block = buffer.slice(0, separatorIndex);
|
|
1972
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
1973
|
+
forwardBlock(block);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
if (buffer.trim().length > 0) {
|
|
1978
|
+
forwardBlock(buffer.trim());
|
|
1979
|
+
}
|
|
1980
|
+
} catch (error) {
|
|
1981
|
+
if (!controller.signal.aborted) {
|
|
1982
|
+
console.warn('SSE proxy stream error:', error);
|
|
1983
|
+
}
|
|
1984
|
+
} finally {
|
|
1985
|
+
cleanup();
|
|
1986
|
+
try {
|
|
1987
|
+
res.end();
|
|
1988
|
+
} catch {
|
|
1989
|
+
// ignore
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
|
|
1767
1994
|
app.get('/api/config/settings', async (_req, res) => {
|
|
1768
1995
|
try {
|
|
1769
1996
|
const settings = await readSettingsFromDisk();
|
|
@@ -2497,8 +2724,11 @@ async function main(options = {}) {
|
|
|
2497
2724
|
const worktrees = await getWorktrees(directory);
|
|
2498
2725
|
res.json(worktrees);
|
|
2499
2726
|
} catch (error) {
|
|
2500
|
-
|
|
2501
|
-
|
|
2727
|
+
// Worktrees are an optional feature. Avoid repeated 500s (and repeated client retries)
|
|
2728
|
+
// when the directory isn't a git repo or uses shell shorthand like "~/".
|
|
2729
|
+
console.warn('Failed to get worktrees, returning empty list:', error?.message || error);
|
|
2730
|
+
res.setHeader('X-OpenChamber-Warning', 'git worktrees unavailable');
|
|
2731
|
+
res.json([]);
|
|
2502
2732
|
}
|
|
2503
2733
|
});
|
|
2504
2734
|
|
|
@@ -2637,15 +2867,16 @@ async function main(options = {}) {
|
|
|
2637
2867
|
return res.status(400).json({ error: 'Path is required' });
|
|
2638
2868
|
}
|
|
2639
2869
|
|
|
2640
|
-
const
|
|
2870
|
+
const expandedPath = normalizeDirectoryPath(dirPath);
|
|
2871
|
+
const normalizedPath = path.normalize(expandedPath);
|
|
2641
2872
|
if (normalizedPath.includes('..')) {
|
|
2642
2873
|
return res.status(400).json({ error: 'Invalid path: path traversal not allowed' });
|
|
2643
2874
|
}
|
|
2644
2875
|
|
|
2645
|
-
|
|
2646
|
-
|
|
2876
|
+
const resolvedPath = path.resolve(expandedPath);
|
|
2877
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
2647
2878
|
|
|
2648
|
-
res.json({ success: true, path:
|
|
2879
|
+
res.json({ success: true, path: resolvedPath });
|
|
2649
2880
|
} catch (error) {
|
|
2650
2881
|
console.error('Failed to create directory:', error);
|
|
2651
2882
|
res.status(500).json({ error: error.message || 'Failed to create directory' });
|
|
@@ -2659,7 +2890,7 @@ async function main(options = {}) {
|
|
|
2659
2890
|
return res.status(400).json({ error: 'Path is required' });
|
|
2660
2891
|
}
|
|
2661
2892
|
|
|
2662
|
-
const resolvedPath = path.resolve(requestedPath);
|
|
2893
|
+
const resolvedPath = path.resolve(normalizeDirectoryPath(requestedPath));
|
|
2663
2894
|
let stats;
|
|
2664
2895
|
try {
|
|
2665
2896
|
stats = await fsPromises.stat(resolvedPath);
|
|
@@ -2705,7 +2936,7 @@ async function main(options = {}) {
|
|
|
2705
2936
|
: os.homedir();
|
|
2706
2937
|
|
|
2707
2938
|
try {
|
|
2708
|
-
const resolvedPath = path.resolve(rawPath);
|
|
2939
|
+
const resolvedPath = path.resolve(normalizeDirectoryPath(rawPath));
|
|
2709
2940
|
|
|
2710
2941
|
const stats = await fsPromises.stat(resolvedPath);
|
|
2711
2942
|
if (!stats.isDirectory()) {
|
|
@@ -2770,7 +3001,7 @@ async function main(options = {}) {
|
|
|
2770
3001
|
const limit = Math.max(1, Math.min(parsedLimit, MAX_FILE_SEARCH_LIMIT));
|
|
2771
3002
|
|
|
2772
3003
|
try {
|
|
2773
|
-
const resolvedRoot = path.resolve(rawRoot);
|
|
3004
|
+
const resolvedRoot = path.resolve(normalizeDirectoryPath(rawRoot));
|
|
2774
3005
|
const stats = await fsPromises.stat(resolvedRoot);
|
|
2775
3006
|
if (!stats.isDirectory()) {
|
|
2776
3007
|
return res.status(400).json({ error: 'Specified root is not a directory' });
|
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
import simpleGit from 'simple-git';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
4
5
|
import { execFile } from 'child_process';
|
|
5
6
|
import { promisify } from 'util';
|
|
6
7
|
|
|
7
8
|
const fsp = fs.promises;
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
10
|
|
|
11
|
+
const normalizeDirectoryPath = (value) => {
|
|
12
|
+
if (typeof value !== 'string') {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (!trimmed) {
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (trimmed === '~') {
|
|
22
|
+
return os.homedir();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
26
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return trimmed;
|
|
30
|
+
};
|
|
31
|
+
|
|
10
32
|
export async function isGitRepository(directory) {
|
|
11
|
-
|
|
33
|
+
const directoryPath = normalizeDirectoryPath(directory);
|
|
34
|
+
if (!directoryPath || !fs.existsSync(directoryPath)) {
|
|
12
35
|
return false;
|
|
13
36
|
}
|
|
14
37
|
|
|
15
|
-
const gitDir = path.join(
|
|
38
|
+
const gitDir = path.join(directoryPath, '.git');
|
|
16
39
|
return fs.existsSync(gitDir);
|
|
17
40
|
}
|
|
18
41
|
|
|
19
42
|
export async function ensureOpenChamberIgnored(directory) {
|
|
20
|
-
|
|
43
|
+
const directoryPath = normalizeDirectoryPath(directory);
|
|
44
|
+
if (!directoryPath || !fs.existsSync(directoryPath)) {
|
|
21
45
|
return false;
|
|
22
46
|
}
|
|
23
47
|
|
|
24
|
-
const gitDir = path.join(
|
|
48
|
+
const gitDir = path.join(directoryPath, '.git');
|
|
25
49
|
if (!fs.existsSync(gitDir)) {
|
|
26
50
|
return false;
|
|
27
51
|
}
|
|
@@ -78,7 +102,7 @@ export async function getGlobalIdentity() {
|
|
|
78
102
|
}
|
|
79
103
|
|
|
80
104
|
export async function getCurrentIdentity(directory) {
|
|
81
|
-
const git = simpleGit(directory);
|
|
105
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
82
106
|
|
|
83
107
|
try {
|
|
84
108
|
|
|
@@ -110,7 +134,7 @@ export async function getCurrentIdentity(directory) {
|
|
|
110
134
|
}
|
|
111
135
|
|
|
112
136
|
export async function setLocalIdentity(directory, profile) {
|
|
113
|
-
const git = simpleGit(directory);
|
|
137
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
114
138
|
|
|
115
139
|
try {
|
|
116
140
|
|
|
@@ -134,7 +158,8 @@ export async function setLocalIdentity(directory, profile) {
|
|
|
134
158
|
}
|
|
135
159
|
|
|
136
160
|
export async function getStatus(directory) {
|
|
137
|
-
const
|
|
161
|
+
const directoryPath = normalizeDirectoryPath(directory);
|
|
162
|
+
const git = simpleGit(directoryPath);
|
|
138
163
|
|
|
139
164
|
try {
|
|
140
165
|
// Use -uall to show all untracked files individually, not just directories
|
|
@@ -194,7 +219,7 @@ export async function getStatus(directory) {
|
|
|
194
219
|
return null;
|
|
195
220
|
}
|
|
196
221
|
|
|
197
|
-
const absolutePath = path.join(
|
|
222
|
+
const absolutePath = path.join(directoryPath, file.path);
|
|
198
223
|
|
|
199
224
|
try {
|
|
200
225
|
const stat = await fsp.stat(absolutePath);
|
|
@@ -266,7 +291,7 @@ export async function getStatus(directory) {
|
|
|
266
291
|
}
|
|
267
292
|
|
|
268
293
|
export async function getDiff(directory, { path, staged = false, contextLines = 3 } = {}) {
|
|
269
|
-
const git = simpleGit(directory);
|
|
294
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
270
295
|
|
|
271
296
|
try {
|
|
272
297
|
const args = ['diff', '--no-color'];
|
|
@@ -338,7 +363,8 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
|
|
|
338
363
|
throw new Error('directory and path are required for getFileDiff');
|
|
339
364
|
}
|
|
340
365
|
|
|
341
|
-
const
|
|
366
|
+
const directoryPath = normalizeDirectoryPath(directory);
|
|
367
|
+
const git = simpleGit(directoryPath);
|
|
342
368
|
const isImage = isImageFile(filePath);
|
|
343
369
|
const mimeType = isImage ? getImageMimeType(filePath) : null;
|
|
344
370
|
|
|
@@ -348,7 +374,7 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
|
|
|
348
374
|
// For images, use git show with raw output and convert to base64
|
|
349
375
|
try {
|
|
350
376
|
const { stdout } = await execFileAsync('git', ['show', `HEAD:${filePath}`], {
|
|
351
|
-
cwd:
|
|
377
|
+
cwd: directoryPath,
|
|
352
378
|
encoding: 'buffer',
|
|
353
379
|
maxBuffer: 50 * 1024 * 1024, // 50MB max
|
|
354
380
|
});
|
|
@@ -365,7 +391,7 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
|
|
|
365
391
|
original = '';
|
|
366
392
|
}
|
|
367
393
|
|
|
368
|
-
const fullPath = path.join(
|
|
394
|
+
const fullPath = path.join(directoryPath, filePath);
|
|
369
395
|
let modified = '';
|
|
370
396
|
try {
|
|
371
397
|
const stat = await fsp.stat(fullPath);
|
|
@@ -395,8 +421,9 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
|
|
|
395
421
|
}
|
|
396
422
|
|
|
397
423
|
export async function revertFile(directory, filePath) {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
424
|
+
const directoryPath = normalizeDirectoryPath(directory);
|
|
425
|
+
const git = simpleGit(directoryPath);
|
|
426
|
+
const repoRoot = path.resolve(directoryPath);
|
|
400
427
|
const absoluteTarget = path.resolve(repoRoot, filePath);
|
|
401
428
|
|
|
402
429
|
if (!absoluteTarget.startsWith(repoRoot + path.sep) && absoluteTarget !== repoRoot) {
|
|
@@ -460,7 +487,7 @@ export async function collectDiffs(directory, files = []) {
|
|
|
460
487
|
}
|
|
461
488
|
|
|
462
489
|
export async function pull(directory, options = {}) {
|
|
463
|
-
const git = simpleGit(directory);
|
|
490
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
464
491
|
|
|
465
492
|
try {
|
|
466
493
|
const result = await git.pull(
|
|
@@ -483,7 +510,7 @@ export async function pull(directory, options = {}) {
|
|
|
483
510
|
}
|
|
484
511
|
|
|
485
512
|
export async function push(directory, options = {}) {
|
|
486
|
-
const git = simpleGit(directory);
|
|
513
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
487
514
|
|
|
488
515
|
try {
|
|
489
516
|
const result = await git.push(
|
|
@@ -510,7 +537,7 @@ export async function deleteRemoteBranch(directory, options = {}) {
|
|
|
510
537
|
throw new Error('branch is required to delete remote branch');
|
|
511
538
|
}
|
|
512
539
|
|
|
513
|
-
const git = simpleGit(directory);
|
|
540
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
514
541
|
const targetBranch = branch.startsWith('refs/heads/')
|
|
515
542
|
? branch.substring('refs/heads/'.length)
|
|
516
543
|
: branch;
|
|
@@ -526,7 +553,7 @@ export async function deleteRemoteBranch(directory, options = {}) {
|
|
|
526
553
|
}
|
|
527
554
|
|
|
528
555
|
export async function fetch(directory, options = {}) {
|
|
529
|
-
const git = simpleGit(directory);
|
|
556
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
530
557
|
|
|
531
558
|
try {
|
|
532
559
|
await git.fetch(
|
|
@@ -543,7 +570,7 @@ export async function fetch(directory, options = {}) {
|
|
|
543
570
|
}
|
|
544
571
|
|
|
545
572
|
export async function commit(directory, message, options = {}) {
|
|
546
|
-
const git = simpleGit(directory);
|
|
573
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
547
574
|
|
|
548
575
|
try {
|
|
549
576
|
|
|
@@ -573,7 +600,7 @@ export async function commit(directory, message, options = {}) {
|
|
|
573
600
|
}
|
|
574
601
|
|
|
575
602
|
export async function getBranches(directory) {
|
|
576
|
-
const git = simpleGit(directory);
|
|
603
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
577
604
|
|
|
578
605
|
try {
|
|
579
606
|
const result = await git.branch();
|
|
@@ -627,7 +654,7 @@ async function filterActiveRemoteBranches(git, remoteBranches) {
|
|
|
627
654
|
}
|
|
628
655
|
|
|
629
656
|
export async function createBranch(directory, branchName, options = {}) {
|
|
630
|
-
const git = simpleGit(directory);
|
|
657
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
631
658
|
|
|
632
659
|
try {
|
|
633
660
|
await git.checkoutBranch(branchName, options.startPoint || 'HEAD');
|
|
@@ -639,7 +666,7 @@ export async function createBranch(directory, branchName, options = {}) {
|
|
|
639
666
|
}
|
|
640
667
|
|
|
641
668
|
export async function checkoutBranch(directory, branchName) {
|
|
642
|
-
const git = simpleGit(directory);
|
|
669
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
643
670
|
|
|
644
671
|
try {
|
|
645
672
|
await git.checkout(branchName);
|
|
@@ -651,7 +678,12 @@ export async function checkoutBranch(directory, branchName) {
|
|
|
651
678
|
}
|
|
652
679
|
|
|
653
680
|
export async function getWorktrees(directory) {
|
|
654
|
-
const
|
|
681
|
+
const directoryPath = normalizeDirectoryPath(directory);
|
|
682
|
+
if (!directoryPath || !fs.existsSync(directoryPath) || !fs.existsSync(path.join(directoryPath, '.git'))) {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const git = simpleGit(directoryPath);
|
|
655
687
|
|
|
656
688
|
try {
|
|
657
689
|
const result = await git.raw(['worktree', 'list', '--porcelain']);
|
|
@@ -684,13 +716,13 @@ export async function getWorktrees(directory) {
|
|
|
684
716
|
|
|
685
717
|
return worktrees;
|
|
686
718
|
} catch (error) {
|
|
687
|
-
console.
|
|
688
|
-
|
|
719
|
+
console.warn('Failed to list worktrees, returning empty list:', error?.message || error);
|
|
720
|
+
return [];
|
|
689
721
|
}
|
|
690
722
|
}
|
|
691
723
|
|
|
692
724
|
export async function addWorktree(directory, worktreePath, branch, options = {}) {
|
|
693
|
-
const git = simpleGit(directory);
|
|
725
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
694
726
|
|
|
695
727
|
try {
|
|
696
728
|
const args = ['worktree', 'add'];
|
|
@@ -719,7 +751,7 @@ export async function addWorktree(directory, worktreePath, branch, options = {})
|
|
|
719
751
|
}
|
|
720
752
|
|
|
721
753
|
export async function removeWorktree(directory, worktreePath, options = {}) {
|
|
722
|
-
const git = simpleGit(directory);
|
|
754
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
723
755
|
|
|
724
756
|
try {
|
|
725
757
|
const args = ['worktree', 'remove', worktreePath];
|
|
@@ -738,7 +770,7 @@ export async function removeWorktree(directory, worktreePath, options = {}) {
|
|
|
738
770
|
}
|
|
739
771
|
|
|
740
772
|
export async function deleteBranch(directory, branch, options = {}) {
|
|
741
|
-
const git = simpleGit(directory);
|
|
773
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
742
774
|
|
|
743
775
|
try {
|
|
744
776
|
const branchName = branch.startsWith('refs/heads/')
|
|
@@ -754,7 +786,7 @@ export async function deleteBranch(directory, branch, options = {}) {
|
|
|
754
786
|
}
|
|
755
787
|
|
|
756
788
|
export async function getLog(directory, options = {}) {
|
|
757
|
-
const git = simpleGit(directory);
|
|
789
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
758
790
|
|
|
759
791
|
try {
|
|
760
792
|
const maxCount = options.maxCount || 50;
|
|
@@ -852,7 +884,7 @@ export async function getLog(directory, options = {}) {
|
|
|
852
884
|
}
|
|
853
885
|
|
|
854
886
|
export async function isLinkedWorktree(directory) {
|
|
855
|
-
const git = simpleGit(directory);
|
|
887
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
856
888
|
try {
|
|
857
889
|
const [gitDir, gitCommonDir] = await Promise.all([
|
|
858
890
|
git.raw(['rev-parse', '--git-dir']).then((output) => output.trim()),
|
|
@@ -866,7 +898,7 @@ export async function isLinkedWorktree(directory) {
|
|
|
866
898
|
}
|
|
867
899
|
|
|
868
900
|
export async function getCommitFiles(directory, commitHash) {
|
|
869
|
-
const git = simpleGit(directory);
|
|
901
|
+
const git = simpleGit(normalizeDirectoryPath(directory));
|
|
870
902
|
|
|
871
903
|
try {
|
|
872
904
|
|