codacus commited on
Commit
1774bf6
Β·
unverified Β·
2 Parent(s): 2af32b0 60dd0b0

Merge pull request #421 from thecodacus/github-import

Browse files
.husky/pre-commit CHANGED
@@ -5,15 +5,21 @@ echo "πŸ” Running pre-commit hook to check the code looks good... πŸ”"
5
  export NVM_DIR="$HOME/.nvm"
6
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load nvm if you're using i
7
 
 
 
 
8
  if ! pnpm typecheck; then
9
- echo "❌ Type checking failed! Please review TypeScript types."
10
- echo "Once you're done, don't forget to add your changes to the commit! πŸš€"
11
- exit 1
 
12
  fi
13
 
 
14
  if ! pnpm lint; then
15
  echo "❌ Linting failed! 'pnpm lint:fix' will help you fix the easy ones."
16
  echo "Once you're done, don't forget to add your beautification to the commit! 🀩"
 
17
  exit 1
18
  fi
19
 
 
5
  export NVM_DIR="$HOME/.nvm"
6
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load nvm if you're using i
7
 
8
+ echo "Running typecheck..."
9
+ which pnpm
10
+
11
  if ! pnpm typecheck; then
12
+ echo "❌ Type checking failed! Please review TypeScript types."
13
+ echo "Once you're done, don't forget to add your changes to the commit! πŸš€"
14
+ echo "Typecheck exit code: $?"
15
+ exit 1
16
  fi
17
 
18
+ echo "Running lint..."
19
  if ! pnpm lint; then
20
  echo "❌ Linting failed! 'pnpm lint:fix' will help you fix the easy ones."
21
  echo "Once you're done, don't forget to add your beautification to the commit! 🀩"
22
+ echo "lint exit code: $?"
23
  exit 1
24
  fi
25
 
app/components/chat/Artifact.tsx CHANGED
@@ -52,7 +52,7 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
52
  if (actions.length !== 0 && artifact.type === 'bundled') {
53
  const finished = !actions.find((action) => action.status !== 'complete');
54
 
55
- if (finished != allActionFinished) {
56
  setAllActionFinished(finished);
57
  }
58
  }
 
52
  if (actions.length !== 0 && artifact.type === 'bundled') {
53
  const finished = !actions.find((action) => action.status !== 'complete');
54
 
55
+ if (allActionFinished !== finished) {
56
  setAllActionFinished(finished);
57
  }
58
  }
app/components/chat/BaseChat.tsx CHANGED
@@ -21,6 +21,7 @@ import type { ProviderInfo } from '~/utils/types';
21
  import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
22
  import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
23
  import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
 
24
 
25
  import FilePreview from './FilePreview';
26
  import { ModelSelector } from '~/components/chat/ModelSelector';
@@ -513,7 +514,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
513
  </div>
514
  </div>
515
  </div>
516
- {!chatStarted && ImportButtons(importChat)}
 
 
 
 
 
517
  {!chatStarted &&
518
  ExamplePrompts((event, messageInput) => {
519
  if (isStreaming) {
 
21
  import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
22
  import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
23
  import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
24
+ import GitCloneButton from './GitCloneButton';
25
 
26
  import FilePreview from './FilePreview';
27
  import { ModelSelector } from '~/components/chat/ModelSelector';
 
514
  </div>
515
  </div>
516
  </div>
517
+ {!chatStarted && (
518
+ <div className="flex justify-center gap-2">
519
+ {ImportButtons(importChat)}
520
+ <GitCloneButton importChat={importChat} />
521
+ </div>
522
+ )}
523
  {!chatStarted &&
524
  ExamplePrompts((event, messageInput) => {
525
  if (isStreaming) {
app/components/chat/GitCloneButton.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ignore from 'ignore';
2
+ import { useGit } from '~/lib/hooks/useGit';
3
+ import type { Message } from 'ai';
4
+ import WithTooltip from '~/components/ui/Tooltip';
5
+
6
+ const IGNORE_PATTERNS = [
7
+ 'node_modules/**',
8
+ '.git/**',
9
+ '.github/**',
10
+ '.vscode/**',
11
+ '**/*.jpg',
12
+ '**/*.jpeg',
13
+ '**/*.png',
14
+ 'dist/**',
15
+ 'build/**',
16
+ '.next/**',
17
+ 'coverage/**',
18
+ '.cache/**',
19
+ '.vscode/**',
20
+ '.idea/**',
21
+ '**/*.log',
22
+ '**/.DS_Store',
23
+ '**/npm-debug.log*',
24
+ '**/yarn-debug.log*',
25
+ '**/yarn-error.log*',
26
+ '**/*lock.json',
27
+ '**/*lock.yaml',
28
+ ];
29
+
30
+ const ig = ignore().add(IGNORE_PATTERNS);
31
+ const generateId = () => Math.random().toString(36).substring(2, 15);
32
+
33
+ interface GitCloneButtonProps {
34
+ className?: string;
35
+ importChat?: (description: string, messages: Message[]) => Promise<void>;
36
+ }
37
+
38
+ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
39
+ const { ready, gitClone } = useGit();
40
+ const onClick = async (_e: any) => {
41
+ if (!ready) {
42
+ return;
43
+ }
44
+
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
+ const message: Message = {
56
+ role: 'assistant',
57
+ content: `Cloning the repo ${repoUrl} into ${workdir}
58
+ <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled" >
59
+ ${filePaths
60
+ .map((filePath) => {
61
+ const { data: content, encoding } = data[filePath];
62
+
63
+ if (encoding === 'utf8') {
64
+ return `<boltAction type="file" filePath="${filePath}">
65
+ ${content}
66
+ </boltAction>`;
67
+ } else if (content instanceof Uint8Array) {
68
+ return `<boltAction type="file" filePath="${filePath}">
69
+ ${textDecoder.decode(content)}
70
+ </boltAction>`;
71
+ } else {
72
+ return '';
73
+ }
74
+ })
75
+ .join('\n')}
76
+ </boltArtifact>`,
77
+ id: generateId(),
78
+ createdAt: new Date(),
79
+ };
80
+ console.log(JSON.stringify(message));
81
+
82
+ importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, [message]);
83
+
84
+ // console.log(files);
85
+ }
86
+ }
87
+ };
88
+
89
+ return (
90
+ <WithTooltip tooltip="Clone A Git Repo">
91
+ <button
92
+ onClick={(e) => {
93
+ onClick(e);
94
+ }}
95
+ title="Clone A Git Repo"
96
+ 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"
97
+ >
98
+ <span className="i-ph:git-branch" />
99
+ Clone A Git Repo
100
+ </button>
101
+ </WithTooltip>
102
+ );
103
+ }
app/components/chat/chatExportAndImport/ImportButtons.tsx CHANGED
@@ -5,7 +5,7 @@ import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
5
 
6
  export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
7
  return (
8
- <div className="flex flex-col items-center justify-center flex-1 p-4">
9
  <input
10
  type="file"
11
  id="chat-import"
 
5
 
6
  export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
7
  return (
8
+ <div className="flex flex-col items-center justify-center w-auto">
9
  <input
10
  type="file"
11
  id="chat-import"
app/lib/hooks/useGit.ts ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { WebContainer } from '@webcontainer/api';
2
+ import { useCallback, useEffect, useRef, useState, type MutableRefObject } from 'react';
3
+ import { webcontainer as webcontainerPromise } from '~/lib/webcontainer';
4
+ import git, { type GitAuth, type PromiseFsClient } from 'isomorphic-git';
5
+ import http from 'isomorphic-git/http/web';
6
+ import Cookies from 'js-cookie';
7
+ import { toast } from 'react-toastify';
8
+
9
+ const lookupSavedPassword = (url: string) => {
10
+ const domain = url.split('/')[2];
11
+ const gitCreds = Cookies.get(`git:${domain}`);
12
+
13
+ if (!gitCreds) {
14
+ return null;
15
+ }
16
+
17
+ try {
18
+ const { username, password } = JSON.parse(gitCreds || '{}');
19
+ return { username, password };
20
+ } catch (error) {
21
+ console.log(`Failed to parse Git Cookie ${error}`);
22
+ return null;
23
+ }
24
+ };
25
+
26
+ const saveGitAuth = (url: string, auth: GitAuth) => {
27
+ const domain = url.split('/')[2];
28
+ Cookies.set(`git:${domain}`, JSON.stringify(auth));
29
+ };
30
+
31
+ export function useGit() {
32
+ const [ready, setReady] = useState(false);
33
+ const [webcontainer, setWebcontainer] = useState<WebContainer>();
34
+ const [fs, setFs] = useState<PromiseFsClient>();
35
+ const fileData = useRef<Record<string, { data: any; encoding?: string }>>({});
36
+ useEffect(() => {
37
+ webcontainerPromise.then((container) => {
38
+ fileData.current = {};
39
+ setWebcontainer(container);
40
+ setFs(getFs(container, fileData));
41
+ setReady(true);
42
+ });
43
+ }, []);
44
+
45
+ const gitClone = useCallback(
46
+ async (url: string) => {
47
+ if (!webcontainer || !fs || !ready) {
48
+ throw 'Webcontainer not initialized';
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 };
99
+ }
100
+
101
+ const getFs = (
102
+ webcontainer: WebContainer,
103
+ record: MutableRefObject<Record<string, { data: any; encoding?: string }>>,
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);
159
+ const resp = await webcontainer.fs.readdir(pathUtils.dirname(relativePath), { withFileTypes: true });
160
+ const name = pathUtils.basename(relativePath);
161
+ const fileInfo = resp.find((x) => x.name == name);
162
+
163
+ if (!fileInfo) {
164
+ throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
165
+ }
166
+
167
+ return {
168
+ isFile: () => fileInfo.isFile(),
169
+ isDirectory: () => fileInfo.isDirectory(),
170
+ isSymbolicLink: () => false,
171
+ size: 1,
172
+ mode: 0o666, // Default permissions
173
+ mtimeMs: Date.now(),
174
+ uid: 1000,
175
+ gid: 1000,
176
+ };
177
+ } catch (error: any) {
178
+ console.log(error?.message);
179
+
180
+ const err = new Error(`ENOENT: no such file or directory, stat '${path}'`) as NodeJS.ErrnoException;
181
+ err.code = 'ENOENT';
182
+ err.errno = -2;
183
+ err.syscall = 'stat';
184
+ err.path = path;
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,
208
+ * we'll throw a "operation not supported" error
209
+ */
210
+ throw new Error(`EPERM: operation not permitted, symlink '${target}' -> '${path}'`);
211
+ },
212
+
213
+ chmod: async (_path: string, _mode: number) => {
214
+ /*
215
+ * WebContainer doesn't support changing permissions,
216
+ * but we can pretend it succeeded for compatibility
217
+ */
218
+ return await Promise.resolve();
219
+ },
220
+ },
221
+ });
222
+
223
+ const pathUtils = {
224
+ dirname: (path: string) => {
225
+ // Handle empty or just filename cases
226
+ if (!path || !path.includes('/')) {
227
+ return '.';
228
+ }
229
+
230
+ // Remove trailing slashes
231
+ path = path.replace(/\/+$/, '');
232
+
233
+ // Get directory part
234
+ return path.split('/').slice(0, -1).join('/') || '/';
235
+ },
236
+
237
+ basename: (path: string, ext?: string) => {
238
+ // Remove trailing slashes
239
+ path = path.replace(/\/+$/, '');
240
+
241
+ // Get the last part of the path
242
+ const base = path.split('/').pop() || '';
243
+
244
+ // If extension is provided, remove it from the result
245
+ if (ext && base.endsWith(ext)) {
246
+ return base.slice(0, -ext.length);
247
+ }
248
+
249
+ return base;
250
+ },
251
+ relative: (from: string, to: string): string => {
252
+ // Handle empty inputs
253
+ if (!from || !to) {
254
+ return '.';
255
+ }
256
+
257
+ // Normalize paths by removing trailing slashes and splitting
258
+ const normalizePathParts = (p: string) => p.replace(/\/+$/, '').split('/').filter(Boolean);
259
+
260
+ const fromParts = normalizePathParts(from);
261
+ const toParts = normalizePathParts(to);
262
+
263
+ // Find common parts at the start of both paths
264
+ let commonLength = 0;
265
+ const minLength = Math.min(fromParts.length, toParts.length);
266
+
267
+ for (let i = 0; i < minLength; i++) {
268
+ if (fromParts[i] !== toParts[i]) {
269
+ break;
270
+ }
271
+
272
+ commonLength++;
273
+ }
274
+
275
+ // Calculate the number of "../" needed
276
+ const upCount = fromParts.length - commonLength;
277
+
278
+ // Get the remaining path parts we need to append
279
+ const remainingPath = toParts.slice(commonLength);
280
+
281
+ // Construct the relative path
282
+ const relativeParts = [...Array(upCount).fill('..'), ...remainingPath];
283
+
284
+ // Handle empty result case
285
+ return relativeParts.length === 0 ? '.' : relativeParts.join('/');
286
+ },
287
+ };
package.json CHANGED
@@ -58,6 +58,7 @@
58
  "@openrouter/ai-sdk-provider": "^0.0.5",
59
  "@radix-ui/react-dialog": "^1.1.2",
60
  "@radix-ui/react-dropdown-menu": "^2.1.2",
 
61
  "@radix-ui/react-tooltip": "^1.1.4",
62
  "@remix-run/cloudflare": "^2.15.0",
63
  "@remix-run/cloudflare-pages": "^2.15.0",
@@ -75,13 +76,13 @@
75
  "framer-motion": "^11.12.0",
76
  "ignore": "^6.0.2",
77
  "isbot": "^4.4.0",
 
78
  "istextorbinary": "^9.5.0",
79
  "jose": "^5.9.6",
80
  "js-cookie": "^3.0.5",
81
  "jszip": "^3.10.1",
82
  "nanostores": "^0.10.3",
83
  "ollama-ai-provider": "^0.15.2",
84
- "pnpm": "^9.14.4",
85
  "react": "^18.3.1",
86
  "react-dom": "^18.3.1",
87
  "react-hotkeys-hook": "^4.6.1",
@@ -110,6 +111,7 @@
110
  "husky": "9.1.7",
111
  "is-ci": "^3.0.1",
112
  "node-fetch": "^3.3.2",
 
113
  "prettier": "^3.4.1",
114
  "sass-embedded": "^1.81.0",
115
  "typescript": "^5.7.2",
 
58
  "@openrouter/ai-sdk-provider": "^0.0.5",
59
  "@radix-ui/react-dialog": "^1.1.2",
60
  "@radix-ui/react-dropdown-menu": "^2.1.2",
61
+ "@radix-ui/react-separator": "^1.1.0",
62
  "@radix-ui/react-tooltip": "^1.1.4",
63
  "@remix-run/cloudflare": "^2.15.0",
64
  "@remix-run/cloudflare-pages": "^2.15.0",
 
76
  "framer-motion": "^11.12.0",
77
  "ignore": "^6.0.2",
78
  "isbot": "^4.4.0",
79
+ "isomorphic-git": "^1.27.2",
80
  "istextorbinary": "^9.5.0",
81
  "jose": "^5.9.6",
82
  "js-cookie": "^3.0.5",
83
  "jszip": "^3.10.1",
84
  "nanostores": "^0.10.3",
85
  "ollama-ai-provider": "^0.15.2",
 
86
  "react": "^18.3.1",
87
  "react-dom": "^18.3.1",
88
  "react-hotkeys-hook": "^4.6.1",
 
111
  "husky": "9.1.7",
112
  "is-ci": "^3.0.1",
113
  "node-fetch": "^3.3.2",
114
+ "pnpm": "^9.14.4",
115
  "prettier": "^3.4.1",
116
  "sass-embedded": "^1.81.0",
117
  "typescript": "^5.7.2",
pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff