Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2925271

Browse files
committedMar 8, 2025·
Merge branch 'extended-files' of https://github.com/Toddyclipsgg/moddiy into extended-files
2 parents 2e6b0e0 + 2849495 commit 2925271

File tree

10 files changed

+493
-101
lines changed

10 files changed

+493
-101
lines changed
 

‎.github/workflows/docker.yaml

+21-20
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ name: Docker Publish
22

33
on:
44
push:
5-
branches:
6-
- main
7-
- stable
5+
branches: [main, stable]
6+
tags: ['v*', '*.*.*']
87
workflow_dispatch:
98

9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
1013
permissions:
1114
packages: write
1215
contents: read
@@ -18,13 +21,14 @@ env:
1821
jobs:
1922
docker-build-publish:
2023
runs-on: ubuntu-latest
24+
# timeout-minutes: 30
2125
steps:
2226
- name: Checkout code
2327
uses: actions/checkout@v4
24-
28+
2529
- name: Set up Docker Buildx
2630
uses: docker/setup-buildx-action@v3
27-
31+
2832
- name: Log in to GitHub Container Registry
2933
uses: docker/login-action@v3
3034
with:
@@ -37,25 +41,22 @@ jobs:
3741
uses: docker/metadata-action@v4
3842
with:
3943
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
40-
41-
- name: Build and push Docker image for main
42-
if: github.ref == 'refs/heads/main'
43-
uses: docker/build-push-action@v6
44-
with:
45-
context: .
46-
push: true
4744
tags: |
48-
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
49-
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
50-
labels: ${{ steps.meta.outputs.labels }}
45+
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
46+
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' }}
47+
type=ref,event=tag
48+
type=sha,format=short
49+
type=raw,value=${{ github.ref_name }},enable=${{ startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/stable' }}
5150
52-
- name: Build and push Docker image for stable
53-
if: github.ref == 'refs/heads/stable'
51+
- name: Build and push Docker image
5452
uses: docker/build-push-action@v6
5553
with:
5654
context: .
55+
platforms: linux/amd64,linux/arm64
56+
target: bolt-ai-production
5757
push: true
58-
tags: |
59-
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:stable
60-
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
58+
tags: ${{ steps.meta.outputs.tags }}
6159
labels: ${{ steps.meta.outputs.labels }}
60+
61+
- name: Check manifest
62+
run: docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}

‎.tool-versions

-2
This file was deleted.

‎app/components/@settings/tabs/connections/GithubConnection.tsx

+57-36
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion';
33
import { toast } from 'react-toastify';
44
import { logStore } from '~/lib/stores/logs';
55
import { classNames } from '~/utils/classNames';
6+
import Cookies from 'js-cookie';
67

78
interface GitHubUserResponse {
89
login: string;
@@ -76,6 +77,46 @@ export function GithubConnection() {
7677
const [isFetchingStats, setIsFetchingStats] = useState(false);
7778
const [isStatsExpanded, setIsStatsExpanded] = useState(false);
7879

80+
const fetchGithubUser = async (token: string) => {
81+
try {
82+
setIsConnecting(true);
83+
84+
const response = await fetch('https://api.github.com/user', {
85+
headers: {
86+
Authorization: `Bearer ${token}`,
87+
},
88+
});
89+
90+
if (!response.ok) {
91+
throw new Error('Invalid token or unauthorized');
92+
}
93+
94+
const data = (await response.json()) as GitHubUserResponse;
95+
const newConnection: GitHubConnection = {
96+
user: data,
97+
token,
98+
tokenType: connection.tokenType,
99+
};
100+
101+
localStorage.setItem('github_connection', JSON.stringify(newConnection));
102+
Cookies.set('githubToken', token);
103+
Cookies.set('githubUsername', data.login);
104+
Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' }));
105+
106+
setConnection(newConnection);
107+
108+
await fetchGitHubStats(token);
109+
110+
toast.success('Successfully connected to GitHub');
111+
} catch (error) {
112+
logStore.logError('Failed to authenticate with GitHub', { error });
113+
toast.error('Failed to connect to GitHub');
114+
setConnection({ user: null, token: '', tokenType: 'classic' });
115+
} finally {
116+
setIsConnecting(false);
117+
}
118+
};
119+
79120
const fetchGitHubStats = async (token: string) => {
80121
try {
81122
setIsFetchingStats(true);
@@ -175,51 +216,31 @@ export function GithubConnection() {
175216
if (parsed.user && parsed.token) {
176217
fetchGitHubStats(parsed.token);
177218
}
219+
} else if (import.meta.env.VITE_GITHUB_ACCESS_TOKEN) {
220+
fetchGithubUser(import.meta.env.VITE_GITHUB_ACCESS_TOKEN);
178221
}
179222

180223
setIsLoading(false);
181224
}, []);
225+
useEffect(() => {
226+
if (!connection) {
227+
return;
228+
}
229+
230+
const token = connection.token;
231+
const data = connection.user;
232+
Cookies.set('githubToken', token);
233+
Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' }));
234+
235+
if (data) {
236+
Cookies.set('githubUsername', data.login);
237+
}
238+
}, [connection]);
182239

183240
if (isLoading || isConnecting || isFetchingStats) {
184241
return <LoadingSpinner />;
185242
}
186243

187-
const fetchGithubUser = async (token: string) => {
188-
try {
189-
setIsConnecting(true);
190-
191-
const response = await fetch('https://api.github.com/user', {
192-
headers: {
193-
Authorization: `Bearer ${token}`,
194-
},
195-
});
196-
197-
if (!response.ok) {
198-
throw new Error('Invalid token or unauthorized');
199-
}
200-
201-
const data = (await response.json()) as GitHubUserResponse;
202-
const newConnection: GitHubConnection = {
203-
user: data,
204-
token,
205-
tokenType: connection.tokenType,
206-
};
207-
208-
localStorage.setItem('github_connection', JSON.stringify(newConnection));
209-
setConnection(newConnection);
210-
211-
await fetchGitHubStats(token);
212-
213-
toast.success('Successfully connected to GitHub');
214-
} catch (error) {
215-
logStore.logError('Failed to authenticate with GitHub', { error });
216-
toast.error('Failed to connect to GitHub');
217-
setConnection({ user: null, token: '', tokenType: 'classic' });
218-
} finally {
219-
setIsConnecting(false);
220-
}
221-
};
222-
223244
const handleConnect = async (event: React.FormEvent) => {
224245
event.preventDefault();
225246
await fetchGithubUser(connection.token);

‎app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx

+16-3
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,24 @@ export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: Reposit
292292

293293
const connection = getLocalStorage('github_connection');
294294
const headers: HeadersInit = connection?.token ? { Authorization: `Bearer ${connection.token}` } : {};
295-
296-
// Fetch repository tree
297-
const treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/main?recursive=1`, {
295+
const repoObjResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
298296
headers,
299297
});
298+
const repoObjData = (await repoObjResponse.json()) as any;
299+
300+
if (!repoObjData.default_branch) {
301+
throw new Error('Failed to fetch repository branch');
302+
}
303+
304+
const defaultBranch = repoObjData.default_branch;
305+
306+
// Fetch repository tree
307+
const treeResponse = await fetch(
308+
`https://api.github.com/repos/${owner}/${repo}/git/trees/${defaultBranch}?recursive=1`,
309+
{
310+
headers,
311+
},
312+
);
300313

301314
if (!treeResponse.ok) {
302315
throw new Error('Failed to fetch repository structure');

‎app/components/chat/Chat.client.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,15 @@ export const ChatImpl = memo(
441441
},
442442
]);
443443
reload();
444+
setInput('');
445+
Cookies.remove(PROMPT_COOKIE_KEY);
446+
447+
setUploadedFiles([]);
448+
setImageDataList([]);
449+
450+
resetEnhancer();
451+
452+
textareaRef.current?.blur();
444453
setFakeLoading(false);
445454

446455
return;
@@ -480,6 +489,15 @@ export const ChatImpl = memo(
480489
]);
481490
reload();
482491
setFakeLoading(false);
492+
setInput('');
493+
Cookies.remove(PROMPT_COOKIE_KEY);
494+
495+
setUploadedFiles([]);
496+
setImageDataList([]);
497+
498+
resetEnhancer();
499+
500+
textareaRef.current?.blur();
483501

484502
return;
485503
}

‎app/components/workbench/DiffView.tsx

+19-1
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,25 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }
556556
useEffect(() => {
557557
getHighlighter({
558558
themes: ['github-dark', 'github-light'],
559-
langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx'],
559+
langs: [
560+
'typescript',
561+
'javascript',
562+
'json',
563+
'html',
564+
'css',
565+
'jsx',
566+
'tsx',
567+
'python',
568+
'php',
569+
'java',
570+
'c',
571+
'cpp',
572+
'csharp',
573+
'go',
574+
'ruby',
575+
'rust',
576+
'plaintext',
577+
],
560578
}).then(setHighlighter);
561579
}, []);
562580

‎app/lib/modules/llm/providers/anthropic.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ export default class AnthropicProvider extends BaseProvider {
1313
};
1414

1515
staticModels: ModelInfo[] = [
16+
{
17+
name: 'claude-3-7-sonnet-20250219',
18+
label: 'Claude 3.7 Sonnet',
19+
provider: 'Anthropic',
20+
maxTokenAllowed: 8000,
21+
},
1622
{
1723
name: 'claude-3-5-sonnet-latest',
1824
label: 'Claude 3.5 Sonnet (new)',
@@ -46,7 +52,7 @@ export default class AnthropicProvider extends BaseProvider {
4652
providerSettings: settings,
4753
serverEnv: serverEnv as any,
4854
defaultBaseUrlKey: '',
49-
defaultApiTokenKey: 'OPENAI_API_KEY',
55+
defaultApiTokenKey: 'ANTHROPIC_API_KEY',
5056
});
5157

5258
if (!apiKey) {

‎app/lib/persistence/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { FileMap } from '~/lib/stores/files';
2+
3+
export interface Snapshot {
4+
chatIndex: string;
5+
files: FileMap;
6+
summary?: string;
7+
}

‎app/lib/persistence/useChatHistory.ts

+210-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useLoaderData, useNavigate, useSearchParams } from '@remix-run/react';
2-
import { useState, useEffect } from 'react';
2+
import { useState, useEffect, useCallback } from 'react';
33
import { atom } from 'nanostores';
4-
import type { Message } from 'ai';
4+
import { generateId, type JSONValue, type Message } from 'ai';
55
import { toast } from 'react-toastify';
66
import { workbenchStore } from '~/lib/stores/workbench';
77
import { logStore } from '~/lib/stores/logs'; // Import logStore
@@ -15,6 +15,11 @@ import {
1515
createChatFromMessages,
1616
type IChatMetadata,
1717
} from './db';
18+
import type { FileMap } from '~/lib/stores/files';
19+
import type { Snapshot } from './types';
20+
import { webcontainer } from '~/lib/webcontainer';
21+
import { createCommandsMessage, detectProjectCommands } from '~/utils/projectCommands';
22+
import type { ContextAnnotation } from '~/types/context';
1823

1924
export interface ChatHistoryItem {
2025
id: string;
@@ -37,6 +42,7 @@ export function useChatHistory() {
3742
const { id: mixedId } = useLoaderData<{ id?: string }>();
3843
const [searchParams] = useSearchParams();
3944

45+
const [archivedMessages, setArchivedMessages] = useState<Message[]>([]);
4046
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
4147
const [ready, setReady] = useState<boolean>(false);
4248
const [urlId, setUrlId] = useState<string | undefined>();
@@ -56,14 +62,128 @@ export function useChatHistory() {
5662

5763
if (mixedId) {
5864
getMessages(db, mixedId)
59-
.then((storedMessages) => {
65+
.then(async (storedMessages) => {
6066
if (storedMessages && storedMessages.messages.length > 0) {
67+
const snapshotStr = localStorage.getItem(`snapshot:${mixedId}`);
68+
const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} };
69+
const summary = snapshot.summary;
70+
6171
const rewindId = searchParams.get('rewindTo');
62-
const filteredMessages = rewindId
63-
? storedMessages.messages.slice(0, storedMessages.messages.findIndex((m) => m.id === rewindId) + 1)
64-
: storedMessages.messages;
72+
let startingIdx = -1;
73+
const endingIdx = rewindId
74+
? storedMessages.messages.findIndex((m) => m.id === rewindId) + 1
75+
: storedMessages.messages.length;
76+
const snapshotIndex = storedMessages.messages.findIndex((m) => m.id === snapshot.chatIndex);
77+
78+
if (snapshotIndex >= 0 && snapshotIndex < endingIdx) {
79+
startingIdx = snapshotIndex;
80+
}
81+
82+
if (snapshotIndex > 0 && storedMessages.messages[snapshotIndex].id == rewindId) {
83+
startingIdx = -1;
84+
}
85+
86+
let filteredMessages = storedMessages.messages.slice(startingIdx + 1, endingIdx);
87+
let archivedMessages: Message[] = [];
88+
89+
if (startingIdx >= 0) {
90+
archivedMessages = storedMessages.messages.slice(0, startingIdx + 1);
91+
}
92+
93+
setArchivedMessages(archivedMessages);
94+
95+
if (startingIdx > 0) {
96+
const files = Object.entries(snapshot?.files || {})
97+
.map(([key, value]) => {
98+
if (value?.type !== 'file') {
99+
return null;
100+
}
101+
102+
return {
103+
content: value.content,
104+
path: key,
105+
};
106+
})
107+
.filter((x) => !!x);
108+
const projectCommands = await detectProjectCommands(files);
109+
const commands = createCommandsMessage(projectCommands);
110+
111+
filteredMessages = [
112+
{
113+
id: generateId(),
114+
role: 'user',
115+
content: `Restore project from snapshot
116+
`,
117+
annotations: ['no-store', 'hidden'],
118+
},
119+
{
120+
id: storedMessages.messages[snapshotIndex].id,
121+
role: 'assistant',
122+
content: ` 📦 Chat Restored from snapshot, You can revert this message to load the full chat history
123+
<boltArtifact id="imported-files" title="Project Files Snapshot" type="bundled">
124+
${Object.entries(snapshot?.files || {})
125+
.filter((x) => !x[0].endsWith('lock.json'))
126+
.map(([key, value]) => {
127+
if (value?.type === 'file') {
128+
return `
129+
<boltAction type="file" filePath="${key}">
130+
${value.content}
131+
</boltAction>
132+
`;
133+
} else {
134+
return ``;
135+
}
136+
})
137+
.join('\n')}
138+
</boltArtifact>
139+
`,
140+
annotations: [
141+
'no-store',
142+
...(summary
143+
? [
144+
{
145+
chatId: storedMessages.messages[snapshotIndex].id,
146+
type: 'chatSummary',
147+
summary,
148+
} satisfies ContextAnnotation,
149+
]
150+
: []),
151+
],
152+
},
153+
...(commands !== null
154+
? [
155+
{
156+
id: `${storedMessages.messages[snapshotIndex].id}-2`,
157+
role: 'user' as const,
158+
content: `setup project`,
159+
annotations: ['no-store', 'hidden'],
160+
},
161+
{
162+
...commands,
163+
id: `${storedMessages.messages[snapshotIndex].id}-3`,
164+
annotations: [
165+
'no-store',
166+
...(commands.annotations || []),
167+
...(summary
168+
? [
169+
{
170+
chatId: `${storedMessages.messages[snapshotIndex].id}-3`,
171+
type: 'chatSummary',
172+
summary,
173+
} satisfies ContextAnnotation,
174+
]
175+
: []),
176+
],
177+
},
178+
]
179+
: []),
180+
...filteredMessages,
181+
];
182+
restoreSnapshot(mixedId);
183+
}
65184

66185
setInitialMessages(filteredMessages);
186+
67187
setUrlId(storedMessages.urlId);
68188
description.set(storedMessages.description);
69189
chatId.set(storedMessages.id);
@@ -75,10 +195,64 @@ export function useChatHistory() {
75195
setReady(true);
76196
})
77197
.catch((error) => {
198+
console.error(error);
199+
78200
logStore.logError('Failed to load chat messages', error);
79201
toast.error(error.message);
80202
});
81203
}
204+
}, [mixedId]);
205+
206+
const takeSnapshot = useCallback(
207+
async (chatIdx: string, files: FileMap, _chatId?: string | undefined, chatSummary?: string) => {
208+
const id = _chatId || chatId;
209+
210+
if (!id) {
211+
return;
212+
}
213+
214+
const snapshot: Snapshot = {
215+
chatIndex: chatIdx,
216+
files,
217+
summary: chatSummary,
218+
};
219+
localStorage.setItem(`snapshot:${id}`, JSON.stringify(snapshot));
220+
},
221+
[chatId],
222+
);
223+
224+
const restoreSnapshot = useCallback(async (id: string) => {
225+
const snapshotStr = localStorage.getItem(`snapshot:${id}`);
226+
const container = await webcontainer;
227+
228+
// if (snapshotStr)setSnapshot(JSON.parse(snapshotStr));
229+
const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} };
230+
231+
if (!snapshot?.files) {
232+
return;
233+
}
234+
235+
Object.entries(snapshot.files).forEach(async ([key, value]) => {
236+
if (key.startsWith(container.workdir)) {
237+
key = key.replace(container.workdir, '');
238+
}
239+
240+
if (value?.type === 'folder') {
241+
await container.fs.mkdir(key, { recursive: true });
242+
}
243+
});
244+
Object.entries(snapshot.files).forEach(async ([key, value]) => {
245+
if (value?.type === 'file') {
246+
if (key.startsWith(container.workdir)) {
247+
key = key.replace(container.workdir, '');
248+
}
249+
250+
await container.fs.writeFile(key, value.content, { encoding: value.isBinary ? undefined : 'utf8' });
251+
} else {
252+
}
253+
});
254+
255+
// workbenchStore.files.setKey(snapshot?.files)
82256
}, []);
83257

84258
return {
@@ -105,14 +279,34 @@ export function useChatHistory() {
105279
}
106280

107281
const { firstArtifact } = workbenchStore;
282+
messages = messages.filter((m) => !m.annotations?.includes('no-store'));
283+
284+
let _urlId = urlId;
108285

109286
if (!urlId && firstArtifact?.id) {
110287
const urlId = await getUrlId(db, firstArtifact.id);
111-
288+
_urlId = urlId;
112289
navigateChat(urlId);
113290
setUrlId(urlId);
114291
}
115292

293+
let chatSummary: string | undefined = undefined;
294+
const lastMessage = messages[messages.length - 1];
295+
296+
if (lastMessage.role === 'assistant') {
297+
const annotations = lastMessage.annotations as JSONValue[];
298+
const filteredAnnotations = (annotations?.filter(
299+
(annotation: JSONValue) =>
300+
annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
301+
) || []) as { type: string; value: any } & { [key: string]: any }[];
302+
303+
if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) {
304+
chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary;
305+
}
306+
}
307+
308+
takeSnapshot(messages[messages.length - 1].id, workbenchStore.files.get(), _urlId, chatSummary);
309+
116310
if (!description.get() && firstArtifact?.title) {
117311
description.set(firstArtifact?.title);
118312
}
@@ -127,7 +321,15 @@ export function useChatHistory() {
127321
}
128322
}
129323

130-
await setMessages(db, chatId.get() as string, messages, urlId, description.get(), undefined, chatMetadata.get());
324+
await setMessages(
325+
db,
326+
chatId.get() as string,
327+
[...archivedMessages, ...messages],
328+
urlId,
329+
description.get(),
330+
undefined,
331+
chatMetadata.get(),
332+
);
131333
},
132334
duplicateCurrentChat: async (listItemId: string) => {
133335
if (!db || (!mixedId && !listItemId)) {

‎app/routes/api.git-proxy.$.ts

+138-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
11
import { json } from '@remix-run/cloudflare';
22
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/cloudflare';
33

4+
// Allowed headers to forward to the target server
5+
const ALLOW_HEADERS = [
6+
'accept-encoding',
7+
'accept-language',
8+
'accept',
9+
'access-control-allow-origin',
10+
'authorization',
11+
'cache-control',
12+
'connection',
13+
'content-length',
14+
'content-type',
15+
'dnt',
16+
'pragma',
17+
'range',
18+
'referer',
19+
'user-agent',
20+
'x-authorization',
21+
'x-http-method-override',
22+
'x-requested-with',
23+
];
24+
25+
// Headers to expose from the target server's response
26+
const EXPOSE_HEADERS = [
27+
'accept-ranges',
28+
'age',
29+
'cache-control',
30+
'content-length',
31+
'content-language',
32+
'content-type',
33+
'date',
34+
'etag',
35+
'expires',
36+
'last-modified',
37+
'pragma',
38+
'server',
39+
'transfer-encoding',
40+
'vary',
41+
'x-github-request-id',
42+
'x-redirected-url',
43+
];
44+
445
// Handle all HTTP methods
546
export async function action({ request, params }: ActionFunctionArgs) {
647
return handleProxyRequest(request, params['*']);
@@ -16,50 +57,117 @@ async function handleProxyRequest(request: Request, path: string | undefined) {
1657
return json({ error: 'Invalid proxy URL format' }, { status: 400 });
1758
}
1859

60+
// Handle CORS preflight request
61+
if (request.method === 'OPTIONS') {
62+
return new Response(null, {
63+
status: 200,
64+
headers: {
65+
'Access-Control-Allow-Origin': '*',
66+
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
67+
'Access-Control-Allow-Headers': ALLOW_HEADERS.join(', '),
68+
'Access-Control-Expose-Headers': EXPOSE_HEADERS.join(', '),
69+
'Access-Control-Max-Age': '86400',
70+
},
71+
});
72+
}
73+
74+
// Extract domain and remaining path
75+
const parts = path.match(/([^\/]+)\/?(.*)/);
76+
77+
if (!parts) {
78+
return json({ error: 'Invalid path format' }, { status: 400 });
79+
}
80+
81+
const domain = parts[1];
82+
const remainingPath = parts[2] || '';
83+
84+
// Reconstruct the target URL with query parameters
1985
const url = new URL(request.url);
86+
const targetURL = `https://${domain}/${remainingPath}${url.search}`;
2087

21-
// Reconstruct the target URL
22-
const targetURL = `https://${path}${url.search}`;
88+
console.log('Target URL:', targetURL);
2389

24-
// Forward the request to the target URL
25-
const response = await fetch(targetURL, {
26-
method: request.method,
27-
headers: {
28-
...Object.fromEntries(request.headers),
90+
// Filter and prepare headers
91+
const headers = new Headers();
2992

30-
// Override host header with the target host
31-
host: new URL(targetURL).host,
32-
},
33-
body: ['GET', 'HEAD'].includes(request.method) ? null : await request.arrayBuffer(),
34-
});
93+
// Only forward allowed headers
94+
for (const header of ALLOW_HEADERS) {
95+
if (request.headers.has(header)) {
96+
headers.set(header, request.headers.get(header)!);
97+
}
98+
}
99+
100+
// Set the host header
101+
headers.set('Host', domain);
102+
103+
// Set Git user agent if not already present
104+
if (!headers.has('user-agent') || !headers.get('user-agent')?.startsWith('git/')) {
105+
headers.set('User-Agent', 'git/@isomorphic-git/cors-proxy');
106+
}
107+
108+
console.log('Request headers:', Object.fromEntries(headers.entries()));
35109

36-
// Create response with CORS headers
37-
const corsHeaders = {
38-
'Access-Control-Allow-Origin': '*',
39-
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
40-
'Access-Control-Allow-Headers': '*',
110+
// Prepare fetch options
111+
const fetchOptions: RequestInit = {
112+
method: request.method,
113+
headers,
114+
redirect: 'follow',
41115
};
42116

43-
// Handle preflight requests
44-
if (request.method === 'OPTIONS') {
45-
return new Response(null, {
46-
headers: corsHeaders,
47-
status: 204,
48-
});
117+
// Add body and duplex option for non-GET/HEAD requests
118+
if (!['GET', 'HEAD'].includes(request.method)) {
119+
fetchOptions.body = request.body;
120+
fetchOptions.duplex = 'half'; // This fixes the "duplex option is required when sending a body" error
49121
}
50122

51-
// Forward the response with CORS headers
52-
const responseHeaders = new Headers(response.headers);
53-
Object.entries(corsHeaders).forEach(([key, value]) => {
54-
responseHeaders.set(key, value);
55-
});
123+
// Forward the request to the target URL
124+
const response = await fetch(targetURL, fetchOptions);
125+
126+
console.log('Response status:', response.status);
56127

128+
// Create response headers
129+
const responseHeaders = new Headers();
130+
131+
// Add CORS headers
132+
responseHeaders.set('Access-Control-Allow-Origin', '*');
133+
responseHeaders.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
134+
responseHeaders.set('Access-Control-Allow-Headers', ALLOW_HEADERS.join(', '));
135+
responseHeaders.set('Access-Control-Expose-Headers', EXPOSE_HEADERS.join(', '));
136+
137+
// Copy exposed headers from the target response
138+
for (const header of EXPOSE_HEADERS) {
139+
// Skip content-length as we'll use the original response's content-length
140+
if (header === 'content-length') {
141+
continue;
142+
}
143+
144+
if (response.headers.has(header)) {
145+
responseHeaders.set(header, response.headers.get(header)!);
146+
}
147+
}
148+
149+
// If the response was redirected, add the x-redirected-url header
150+
if (response.redirected) {
151+
responseHeaders.set('x-redirected-url', response.url);
152+
}
153+
154+
console.log('Response headers:', Object.fromEntries(responseHeaders.entries()));
155+
156+
// Return the response with the target's body stream piped directly
57157
return new Response(response.body, {
58158
status: response.status,
159+
statusText: response.statusText,
59160
headers: responseHeaders,
60161
});
61162
} catch (error) {
62-
console.error('Git proxy error:', error);
63-
return json({ error: 'Proxy error' }, { status: 500 });
163+
console.error('Proxy error:', error);
164+
return json(
165+
{
166+
error: 'Proxy error',
167+
message: error instanceof Error ? error.message : 'Unknown error',
168+
url: path ? `https://${path}` : 'Invalid URL',
169+
},
170+
{ status: 500 },
171+
);
64172
}
65173
}

0 commit comments

Comments
 (0)
Please sign in to comment.