Eduards commited on
Commit
b1f9380
·
unverified ·
1 Parent(s): 31e03ce

fix: introduce our own cors proxy for git import to fix 403 errors on isometric git cors proxy (#924)

Browse files

* Exploration of improving git import

* Fix our own git proxy

* Clean out file counting for progress, does not seem to work well anyways

app/components/chat/GitCloneButton.tsx CHANGED
@@ -3,6 +3,9 @@ import { useGit } from '~/lib/hooks/useGit';
3
  import type { Message } from 'ai';
4
  import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
5
  import { generateId } from '~/utils/fileUtils';
 
 
 
6
 
7
  const IGNORE_PATTERNS = [
8
  'node_modules/**',
@@ -37,6 +40,8 @@ interface GitCloneButtonProps {
37
 
38
  export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
39
  const { ready, gitClone } = useGit();
 
 
40
  const onClick = async (_e: any) => {
41
  if (!ready) {
42
  return;
@@ -45,33 +50,34 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
45
  const repoUrl = prompt('Enter the Git url');
46
 
47
  if (repoUrl) {
48
- const { workdir, data } = await gitClone(repoUrl);
49
-
50
- if (importChat) {
51
- const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
52
- console.log(filePaths);
53
-
54
- const textDecoder = new TextDecoder('utf-8');
55
-
56
- // Convert files to common format for command detection
57
- const fileContents = filePaths
58
- .map((filePath) => {
59
- const { data: content, encoding } = data[filePath];
60
- return {
61
- path: filePath,
62
- content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
63
- };
64
- })
65
- .filter((f) => f.content);
66
-
67
- // Detect and create commands message
68
- const commands = await detectProjectCommands(fileContents);
69
- const commandsMessage = createCommandsMessage(commands);
70
-
71
- // Create files message
72
- const filesMessage: Message = {
73
- role: 'assistant',
74
- content: `Cloning the repo ${repoUrl} into ${workdir}
 
75
  <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
76
  ${fileContents
77
  .map(
@@ -82,29 +88,38 @@ ${file.content}
82
  )
83
  .join('\n')}
84
  </boltArtifact>`,
85
- id: generateId(),
86
- createdAt: new Date(),
87
- };
88
 
89
- const messages = [filesMessage];
90
 
91
- if (commandsMessage) {
92
- messages.push(commandsMessage);
93
- }
94
 
95
- await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
 
 
 
 
 
 
96
  }
97
  }
98
  };
99
 
100
  return (
101
- <button
102
- onClick={onClick}
103
- title="Clone a Git Repo"
104
- className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
105
- >
106
- <span className="i-ph:git-branch" />
107
- Clone a Git Repo
108
- </button>
 
 
 
109
  );
110
  }
 
3
  import type { Message } from 'ai';
4
  import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
5
  import { generateId } from '~/utils/fileUtils';
6
+ import { useState } from 'react';
7
+ import { toast } from 'react-toastify';
8
+ import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
9
 
10
  const IGNORE_PATTERNS = [
11
  'node_modules/**',
 
40
 
41
  export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
42
  const { ready, gitClone } = useGit();
43
+ const [loading, setLoading] = useState(false);
44
+
45
  const onClick = async (_e: any) => {
46
  if (!ready) {
47
  return;
 
50
  const repoUrl = prompt('Enter the Git url');
51
 
52
  if (repoUrl) {
53
+ setLoading(true);
54
+
55
+ try {
56
+ const { workdir, data } = await gitClone(repoUrl);
57
+
58
+ if (importChat) {
59
+ const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
60
+ console.log(filePaths);
61
+
62
+ const textDecoder = new TextDecoder('utf-8');
63
+
64
+ const fileContents = filePaths
65
+ .map((filePath) => {
66
+ const { data: content, encoding } = data[filePath];
67
+ return {
68
+ path: filePath,
69
+ content:
70
+ encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
71
+ };
72
+ })
73
+ .filter((f) => f.content);
74
+
75
+ const commands = await detectProjectCommands(fileContents);
76
+ const commandsMessage = createCommandsMessage(commands);
77
+
78
+ const filesMessage: Message = {
79
+ role: 'assistant',
80
+ content: `Cloning the repo ${repoUrl} into ${workdir}
81
  <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
82
  ${fileContents
83
  .map(
 
88
  )
89
  .join('\n')}
90
  </boltArtifact>`,
91
+ id: generateId(),
92
+ createdAt: new Date(),
93
+ };
94
 
95
+ const messages = [filesMessage];
96
 
97
+ if (commandsMessage) {
98
+ messages.push(commandsMessage);
99
+ }
100
 
101
+ await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
102
+ }
103
+ } catch (error) {
104
+ console.error('Error during import:', error);
105
+ toast.error('Failed to import repository');
106
+ } finally {
107
+ setLoading(false);
108
  }
109
  }
110
  };
111
 
112
  return (
113
+ <>
114
+ <button
115
+ onClick={onClick}
116
+ title="Clone a Git Repo"
117
+ className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
118
+ >
119
+ <span className="i-ph:git-branch" />
120
+ Clone a Git Repo
121
+ </button>
122
+ {loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
123
+ </>
124
  );
125
  }
app/components/git/GitUrlImport.client.tsx CHANGED
@@ -49,33 +49,32 @@ export function GitUrlImport() {
49
 
50
  if (repoUrl) {
51
  const ig = ignore().add(IGNORE_PATTERNS);
52
- const { workdir, data } = await gitClone(repoUrl);
53
-
54
- if (importChat) {
55
- const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
56
-
57
- const textDecoder = new TextDecoder('utf-8');
58
-
59
- // Convert files to common format for command detection
60
- const fileContents = filePaths
61
- .map((filePath) => {
62
- const { data: content, encoding } = data[filePath];
63
- return {
64
- path: filePath,
65
- content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
66
- };
67
- })
68
- .filter((f) => f.content);
69
-
70
- // Detect and create commands message
71
- const commands = await detectProjectCommands(fileContents);
72
- const commandsMessage = createCommandsMessage(commands);
73
-
74
- // Create files message
75
- const filesMessage: Message = {
76
- role: 'assistant',
77
- content: `Cloning the repo ${repoUrl} into ${workdir}
78
- <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
79
  ${fileContents
80
  .map(
81
  (file) =>
@@ -85,17 +84,25 @@ ${file.content}
85
  )
86
  .join('\n')}
87
  </boltArtifact>`,
88
- id: generateId(),
89
- createdAt: new Date(),
90
- };
 
 
91
 
92
- const messages = [filesMessage];
 
 
93
 
94
- if (commandsMessage) {
95
- messages.push(commandsMessage);
96
  }
 
 
 
 
 
97
 
98
- await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
99
  }
100
  }
101
  };
 
49
 
50
  if (repoUrl) {
51
  const ig = ignore().add(IGNORE_PATTERNS);
52
+
53
+ try {
54
+ const { workdir, data } = await gitClone(repoUrl);
55
+
56
+ if (importChat) {
57
+ const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
58
+ const textDecoder = new TextDecoder('utf-8');
59
+
60
+ const fileContents = filePaths
61
+ .map((filePath) => {
62
+ const { data: content, encoding } = data[filePath];
63
+ return {
64
+ path: filePath,
65
+ content:
66
+ encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
67
+ };
68
+ })
69
+ .filter((f) => f.content);
70
+
71
+ const commands = await detectProjectCommands(fileContents);
72
+ const commandsMessage = createCommandsMessage(commands);
73
+
74
+ const filesMessage: Message = {
75
+ role: 'assistant',
76
+ content: `Cloning the repo ${repoUrl} into ${workdir}
77
+ <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
 
78
  ${fileContents
79
  .map(
80
  (file) =>
 
84
  )
85
  .join('\n')}
86
  </boltArtifact>`,
87
+ id: generateId(),
88
+ createdAt: new Date(),
89
+ };
90
+
91
+ const messages = [filesMessage];
92
 
93
+ if (commandsMessage) {
94
+ messages.push(commandsMessage);
95
+ }
96
 
97
+ await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
 
98
  }
99
+ } catch (error) {
100
+ console.error('Error during import:', error);
101
+ toast.error('Failed to import repository');
102
+ setLoading(false);
103
+ window.location.href = '/';
104
 
105
+ return;
106
  }
107
  }
108
  };
app/components/ui/LoadingOverlay.tsx CHANGED
@@ -1,13 +1,31 @@
1
- export const LoadingOverlay = ({ message = 'Loading...' }) => {
 
 
 
 
 
 
 
 
2
  return (
3
  <div className="fixed inset-0 flex items-center justify-center bg-black/80 z-50 backdrop-blur-sm">
4
- {/* Loading content */}
5
  <div className="relative flex flex-col items-center gap-4 p-8 rounded-lg bg-bolt-elements-background-depth-2 shadow-lg">
6
  <div
7
  className={'i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress'}
8
  style={{ fontSize: '2rem' }}
9
  ></div>
10
  <p className="text-lg text-bolt-elements-textTertiary">{message}</p>
 
 
 
 
 
 
 
 
 
 
 
11
  </div>
12
  </div>
13
  );
 
1
+ export const LoadingOverlay = ({
2
+ message = 'Loading...',
3
+ progress,
4
+ progressText,
5
+ }: {
6
+ message?: string;
7
+ progress?: number;
8
+ progressText?: string;
9
+ }) => {
10
  return (
11
  <div className="fixed inset-0 flex items-center justify-center bg-black/80 z-50 backdrop-blur-sm">
 
12
  <div className="relative flex flex-col items-center gap-4 p-8 rounded-lg bg-bolt-elements-background-depth-2 shadow-lg">
13
  <div
14
  className={'i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress'}
15
  style={{ fontSize: '2rem' }}
16
  ></div>
17
  <p className="text-lg text-bolt-elements-textTertiary">{message}</p>
18
+ {progress !== undefined && (
19
+ <div className="w-64 flex flex-col gap-2">
20
+ <div className="w-full h-2 bg-bolt-elements-background-depth-1 rounded-full overflow-hidden">
21
+ <div
22
+ className="h-full bg-bolt-elements-loader-progress transition-all duration-300 ease-out rounded-full"
23
+ style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}
24
+ />
25
+ </div>
26
+ {progressText && <p className="text-sm text-bolt-elements-textTertiary text-center">{progressText}</p>}
27
+ </div>
28
+ )}
29
  </div>
30
  </div>
31
  );
app/lib/hooks/useGit.ts CHANGED
@@ -49,50 +49,54 @@ export function useGit() {
49
  }
50
 
51
  fileData.current = {};
52
- await git.clone({
53
- fs,
54
- http,
55
- dir: webcontainer.workdir,
56
- url,
57
- depth: 1,
58
- singleBranch: true,
59
- corsProxy: 'https://cors.isomorphic-git.org',
60
- onAuth: (url) => {
61
- // let domain=url.split("/")[2]
62
-
63
- let auth = lookupSavedPassword(url);
64
-
65
- if (auth) {
66
- return auth;
67
- }
68
-
69
- if (confirm('This repo is password protected. Ready to enter a username & password?')) {
70
- auth = {
71
- username: prompt('Enter username'),
72
- password: prompt('Enter password'),
73
- };
74
- return auth;
75
- } else {
76
- return { cancel: true };
77
- }
78
- },
79
- onAuthFailure: (url, _auth) => {
80
- toast.error(`Error Authenticating with ${url.split('/')[2]}`);
81
- },
82
- onAuthSuccess: (url, auth) => {
83
- saveGitAuth(url, auth);
84
- },
85
- });
86
-
87
- const data: Record<string, { data: any; encoding?: string }> = {};
88
-
89
- for (const [key, value] of Object.entries(fileData.current)) {
90
- data[key] = value;
91
- }
92
 
93
- return { workdir: webcontainer.workdir, data };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  },
95
- [webcontainer],
96
  );
97
 
98
  return { ready, gitClone };
@@ -104,55 +108,86 @@ const getFs = (
104
  ) => ({
105
  promises: {
106
  readFile: async (path: string, options: any) => {
107
- const encoding = options.encoding;
108
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
109
- console.log('readFile', relativePath, encoding);
110
 
111
- return await webcontainer.fs.readFile(relativePath, encoding);
 
 
 
 
 
 
112
  },
113
  writeFile: async (path: string, data: any, options: any) => {
114
  const encoding = options.encoding;
115
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
116
- console.log('writeFile', { relativePath, data, encoding });
117
 
118
  if (record.current) {
119
  record.current[relativePath] = { data, encoding };
120
  }
121
 
122
- return await webcontainer.fs.writeFile(relativePath, data, { ...options, encoding });
 
 
 
 
 
 
123
  },
124
  mkdir: async (path: string, options: any) => {
125
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
126
- console.log('mkdir', relativePath, options);
127
 
128
- return await webcontainer.fs.mkdir(relativePath, { ...options, recursive: true });
 
 
 
 
 
 
129
  },
130
  readdir: async (path: string, options: any) => {
131
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
132
- console.log('readdir', relativePath, options);
133
 
134
- return await webcontainer.fs.readdir(relativePath, options);
 
 
 
 
 
 
135
  },
136
  rm: async (path: string, options: any) => {
137
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
138
- console.log('rm', relativePath, options);
139
 
140
- return await webcontainer.fs.rm(relativePath, { ...(options || {}) });
 
 
 
 
 
 
141
  },
142
  rmdir: async (path: string, options: any) => {
143
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
144
- console.log('rmdir', relativePath, options);
145
 
146
- return await webcontainer.fs.rm(relativePath, { recursive: true, ...options });
147
- },
148
 
149
- // Mock implementations for missing functions
 
 
 
 
150
  unlink: async (path: string) => {
151
- // unlink is just removing a single file
152
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
153
- return await webcontainer.fs.rm(relativePath, { recursive: false });
154
- },
155
 
 
 
 
 
 
 
156
  stat: async (path: string) => {
157
  try {
158
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
@@ -185,23 +220,12 @@ const getFs = (
185
  throw err;
186
  }
187
  },
188
-
189
  lstat: async (path: string) => {
190
- /*
191
- * For basic usage, lstat can return the same as stat
192
- * since we're not handling symbolic links
193
- */
194
  return await getFs(webcontainer, record).promises.stat(path);
195
  },
196
-
197
  readlink: async (path: string) => {
198
- /*
199
- * Since WebContainer doesn't support symlinks,
200
- * we'll throw a "not a symbolic link" error
201
- */
202
  throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
203
  },
204
-
205
  symlink: async (target: string, path: string) => {
206
  /*
207
  * Since WebContainer doesn't support symlinks,
 
49
  }
50
 
51
  fileData.current = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ try {
54
+ await git.clone({
55
+ fs,
56
+ http,
57
+ dir: webcontainer.workdir,
58
+ url,
59
+ depth: 1,
60
+ singleBranch: true,
61
+ corsProxy: '/api/git-proxy',
62
+ onAuth: (url) => {
63
+ let auth = lookupSavedPassword(url);
64
+
65
+ if (auth) {
66
+ return auth;
67
+ }
68
+
69
+ if (confirm('This repo is password protected. Ready to enter a username & password?')) {
70
+ auth = {
71
+ username: prompt('Enter username'),
72
+ password: prompt('Enter password'),
73
+ };
74
+ return auth;
75
+ } else {
76
+ return { cancel: true };
77
+ }
78
+ },
79
+ onAuthFailure: (url, _auth) => {
80
+ toast.error(`Error Authenticating with ${url.split('/')[2]}`);
81
+ },
82
+ onAuthSuccess: (url, auth) => {
83
+ saveGitAuth(url, auth);
84
+ },
85
+ });
86
+
87
+ const data: Record<string, { data: any; encoding?: string }> = {};
88
+
89
+ for (const [key, value] of Object.entries(fileData.current)) {
90
+ data[key] = value;
91
+ }
92
+
93
+ return { workdir: webcontainer.workdir, data };
94
+ } catch (error) {
95
+ console.error('Git clone error:', error);
96
+ throw error;
97
+ }
98
  },
99
+ [webcontainer, fs, ready],
100
  );
101
 
102
  return { ready, gitClone };
 
108
  ) => ({
109
  promises: {
110
  readFile: async (path: string, options: any) => {
111
+ const encoding = options?.encoding;
112
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
 
113
 
114
+ try {
115
+ const result = await webcontainer.fs.readFile(relativePath, encoding);
116
+
117
+ return result;
118
+ } catch (error) {
119
+ throw error;
120
+ }
121
  },
122
  writeFile: async (path: string, data: any, options: any) => {
123
  const encoding = options.encoding;
124
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
 
125
 
126
  if (record.current) {
127
  record.current[relativePath] = { data, encoding };
128
  }
129
 
130
+ try {
131
+ const result = await webcontainer.fs.writeFile(relativePath, data, { ...options, encoding });
132
+
133
+ return result;
134
+ } catch (error) {
135
+ throw error;
136
+ }
137
  },
138
  mkdir: async (path: string, options: any) => {
139
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
 
140
 
141
+ try {
142
+ const result = await webcontainer.fs.mkdir(relativePath, { ...options, recursive: true });
143
+
144
+ return result;
145
+ } catch (error) {
146
+ throw error;
147
+ }
148
  },
149
  readdir: async (path: string, options: any) => {
150
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
 
151
 
152
+ try {
153
+ const result = await webcontainer.fs.readdir(relativePath, options);
154
+
155
+ return result;
156
+ } catch (error) {
157
+ throw error;
158
+ }
159
  },
160
  rm: async (path: string, options: any) => {
161
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
 
162
 
163
+ try {
164
+ const result = await webcontainer.fs.rm(relativePath, { ...(options || {}) });
165
+
166
+ return result;
167
+ } catch (error) {
168
+ throw error;
169
+ }
170
  },
171
  rmdir: async (path: string, options: any) => {
172
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
 
173
 
174
+ try {
175
+ const result = await webcontainer.fs.rm(relativePath, { recursive: true, ...options });
176
 
177
+ return result;
178
+ } catch (error) {
179
+ throw error;
180
+ }
181
+ },
182
  unlink: async (path: string) => {
 
183
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
 
 
184
 
185
+ try {
186
+ return await webcontainer.fs.rm(relativePath, { recursive: false });
187
+ } catch (error) {
188
+ throw error;
189
+ }
190
+ },
191
  stat: async (path: string) => {
192
  try {
193
  const relativePath = pathUtils.relative(webcontainer.workdir, path);
 
220
  throw err;
221
  }
222
  },
 
223
  lstat: async (path: string) => {
 
 
 
 
224
  return await getFs(webcontainer, record).promises.stat(path);
225
  },
 
226
  readlink: async (path: string) => {
 
 
 
 
227
  throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
228
  },
 
229
  symlink: async (target: string, path: string) => {
230
  /*
231
  * Since WebContainer doesn't support symlinks,
app/routes/api.git-proxy.$.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { json } from '@remix-run/cloudflare';
2
+ import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/cloudflare';
3
+
4
+ // Handle all HTTP methods
5
+ export async function action({ request, params }: ActionFunctionArgs) {
6
+ return handleProxyRequest(request, params['*']);
7
+ }
8
+
9
+ export async function loader({ request, params }: LoaderFunctionArgs) {
10
+ return handleProxyRequest(request, params['*']);
11
+ }
12
+
13
+ async function handleProxyRequest(request: Request, path: string | undefined) {
14
+ try {
15
+ if (!path) {
16
+ return json({ error: 'Invalid proxy URL format' }, { status: 400 });
17
+ }
18
+
19
+ const url = new URL(request.url);
20
+
21
+ // Reconstruct the target URL
22
+ const targetURL = `https://${path}${url.search}`;
23
+
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),
29
+
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
+ });
35
+
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': '*',
41
+ };
42
+
43
+ // Handle preflight requests
44
+ if (request.method === 'OPTIONS') {
45
+ return new Response(null, {
46
+ headers: corsHeaders,
47
+ status: 204,
48
+ });
49
+ }
50
+
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
+ });
56
+
57
+ return new Response(response.body, {
58
+ status: response.status,
59
+ headers: responseHeaders,
60
+ });
61
+ } catch (error) {
62
+ console.error('Git proxy error:', error);
63
+ return json({ error: 'Proxy error' }, { status: 500 });
64
+ }
65
+ }