chore: fixed lock file
Browse files- .husky/pre-commit +12 -3
- app/components/chat/BaseChat.tsx +1 -1
- app/components/chat/GitCloneButton.tsx +24 -18
- app/lib/hooks/useGit.ts +231 -227
- package-lock.json +0 -0
- package.json +3 -1
- pnpm-lock.yaml +0 -0
.husky/pre-commit
CHANGED
@@ -2,15 +2,24 @@
|
|
2 |
|
3 |
echo "π Running pre-commit hook to check the code looks good... π"
|
4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
if ! pnpm typecheck; then
|
6 |
-
|
7 |
-
|
8 |
-
|
|
|
9 |
fi
|
10 |
|
|
|
11 |
if ! pnpm lint; then
|
12 |
echo "β Linting failed! 'pnpm lint:check' will help you fix the easy ones."
|
13 |
echo "Once you're done, don't forget to add your beautification to the commit! π€©"
|
|
|
14 |
exit 1
|
15 |
fi
|
16 |
|
|
|
2 |
|
3 |
echo "π Running pre-commit hook to check the code looks good... π"
|
4 |
|
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:check' 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/BaseChat.tsx
CHANGED
@@ -260,7 +260,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
260 |
<GitCloneButton />
|
261 |
<Separator.Root className="my-[15px] bg-gray6 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px" />
|
262 |
<div className="flex items-center gap-3"></div>
|
263 |
-
|
264 |
<div className={isModelSettingsCollapsed ? 'hidden' : ''}>
|
265 |
<ModelSelector
|
266 |
key={provider?.name + ':' + modelList.length}
|
|
|
260 |
<GitCloneButton />
|
261 |
<Separator.Root className="my-[15px] bg-gray6 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px" />
|
262 |
<div className="flex items-center gap-3"></div>
|
263 |
+
|
264 |
<div className={isModelSettingsCollapsed ? 'hidden' : ''}>
|
265 |
<ModelSelector
|
266 |
key={provider?.name + ':' + modelList.length}
|
app/components/chat/GitCloneButton.tsx
CHANGED
@@ -1,24 +1,30 @@
|
|
1 |
-
import { IconButton } from '
|
2 |
-
import
|
3 |
-
import http from 'isomorphic-git/http/web'
|
4 |
-
import { useGit } from '~/lib/hooks/useGit'
|
5 |
|
6 |
export default function GitCloneButton() {
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
if (repoUrl) {
|
12 |
-
await gitClone(repoUrl)
|
13 |
-
}
|
14 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
return (
|
16 |
-
<IconButton
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
|
|
|
|
|
|
22 |
</IconButton>
|
23 |
-
)
|
24 |
}
|
|
|
1 |
+
import { IconButton } from '~/components/ui/IconButton';
|
2 |
+
import { useGit } from '~/lib/hooks/useGit';
|
|
|
|
|
3 |
|
4 |
export default function GitCloneButton() {
|
5 |
+
const { ready, gitClone } = useGit();
|
6 |
+
const onClick = async (_e: any) => {
|
7 |
+
if (!ready) {
|
8 |
+
return;
|
|
|
|
|
|
|
9 |
}
|
10 |
+
|
11 |
+
const repoUrl = prompt('Enter the Git url');
|
12 |
+
|
13 |
+
if (repoUrl) {
|
14 |
+
await gitClone(repoUrl);
|
15 |
+
}
|
16 |
+
};
|
17 |
+
|
18 |
return (
|
19 |
+
<IconButton
|
20 |
+
onClick={(e) => {
|
21 |
+
onClick(e);
|
22 |
+
}}
|
23 |
+
className="w-full justify-center"
|
24 |
+
title="Clone A Git Repo"
|
25 |
+
>
|
26 |
+
<span className="mr-2 text-xs lg:text-sm">Clone A Git Repo</span>
|
27 |
+
<div className="i-ph:git-branch" />
|
28 |
</IconButton>
|
29 |
+
);
|
30 |
}
|
app/lib/hooks/useGit.ts
CHANGED
@@ -1,254 +1,258 @@
|
|
1 |
-
import type { WebContainer
|
2 |
-
import { useCallback, useEffect, useState } from
|
3 |
-
import { webcontainer as webcontainerPromise } from
|
4 |
-
import git, { type PromiseFsClient } from 'isomorphic-git'
|
5 |
-
import http from 'isomorphic-git/http/web'
|
6 |
import Cookies from 'js-cookie';
|
7 |
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
export function useGit() {
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
return parsedCreds
|
26 |
-
}
|
27 |
-
}
|
28 |
-
return;
|
29 |
-
|
30 |
-
} catch (error) {
|
31 |
-
console.error('Error saving API keys to cookies:', error);
|
32 |
-
return;
|
33 |
}
|
34 |
-
|
35 |
-
useEffect(()=>{
|
36 |
-
webcontainerPromise.then(container=>{
|
37 |
-
setWebcontainer(container);
|
38 |
-
setFs(getFs(container));
|
39 |
-
setReady(true);
|
40 |
-
})
|
41 |
-
},[])
|
42 |
-
|
43 |
-
const gitClone= useCallback(async (url:string)=>{
|
44 |
-
if (!webcontainer||!fs||!ready) {
|
45 |
-
return;
|
46 |
-
}
|
47 |
-
let repo = await git.clone({
|
48 |
-
fs,
|
49 |
-
http,
|
50 |
-
dir: webcontainer.workdir,
|
51 |
-
url: url,
|
52 |
-
depth: 1,
|
53 |
-
singleBranch: true,
|
54 |
-
corsProxy: 'https://cors.isomorphic-git.org',
|
55 |
-
onAuth: url => {
|
56 |
-
let auth = lookupSavedPassword(url)
|
57 |
-
if (auth) return auth
|
58 |
-
|
59 |
-
if (confirm('This repo is password protected. Ready to enter a username & password?')) {
|
60 |
-
auth = {
|
61 |
-
username: prompt('Enter username'),
|
62 |
-
password: prompt('Enter password'),
|
63 |
-
}
|
64 |
-
return auth
|
65 |
-
} else {
|
66 |
-
return { cancel: true }
|
67 |
-
}
|
68 |
-
}
|
69 |
-
})
|
70 |
-
console.log(repo)
|
71 |
-
}, [webcontainer])
|
72 |
-
return {ready,gitClone}
|
73 |
-
}
|
74 |
-
|
75 |
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
mkdir:FileSystemAPI['mkdir'];
|
81 |
-
readdir:FileSystemAPI['readdir'];
|
82 |
-
rm:FileSystemAPI['rm'];
|
83 |
-
unlink(path: string): Promise<void>;
|
84 |
-
stat(path: string): Promise<any>;
|
85 |
-
lstat(path: string): Promise<any>;
|
86 |
-
rmdir(path: string): Promise<void>;
|
87 |
-
readlink?(path: string): Promise<string>;
|
88 |
-
symlink?(target: string, path: string): Promise<void>;
|
89 |
-
chmod?(path: string, mode: number): Promise<void>;
|
90 |
}
|
91 |
-
}
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
return
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
unlink: async (path: string) => {
|
131 |
-
// unlink is just removing a single file
|
132 |
-
let relativePath = pathUtils.relative(webcontainer.workdir, path);
|
133 |
-
return await webcontainer.fs.rm(relativePath, { recursive: false });
|
134 |
},
|
|
|
|
|
|
|
|
|
|
|
135 |
|
136 |
-
|
137 |
-
|
138 |
-
let relativePath = pathUtils.relative(webcontainer.workdir, path);
|
139 |
-
let resp = await webcontainer.fs.readdir(pathUtils.dirname(relativePath),{withFileTypes:true})
|
140 |
-
let name = pathUtils.basename(relativePath)
|
141 |
-
let fileInfo=resp.find(x=>x.name==name)
|
142 |
-
if(!fileInfo){
|
143 |
-
throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
|
144 |
-
}
|
145 |
-
return {
|
146 |
-
isFile: () => fileInfo.isFile(),
|
147 |
-
isDirectory: () => fileInfo.isDirectory(),
|
148 |
-
isSymbolicLink: () => false,
|
149 |
-
size: 1,
|
150 |
-
mode: 0o666, // Default permissions
|
151 |
-
mtimeMs: Date.now(),
|
152 |
-
uid: 1000,
|
153 |
-
gid: 1000
|
154 |
-
};
|
155 |
-
} catch (error) {
|
156 |
-
const err = new Error(`ENOENT: no such file or directory, stat '${path}'`) as NodeJS.ErrnoException;
|
157 |
-
err.code = 'ENOENT';
|
158 |
-
err.errno = -2;
|
159 |
-
err.syscall = 'stat';
|
160 |
-
err.path = path;
|
161 |
-
throw err;
|
162 |
-
}
|
163 |
-
},
|
164 |
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
|
|
170 |
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
|
|
176 |
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
}
|
189 |
-
})
|
190 |
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
|
|
195 |
|
196 |
-
|
197 |
-
|
|
|
|
|
|
|
198 |
|
199 |
-
|
200 |
-
return path.split('/').slice(0, -1).join('/') || '/';
|
201 |
},
|
202 |
|
203 |
-
|
204 |
-
|
205 |
-
|
|
|
|
|
|
|
206 |
|
207 |
-
|
208 |
-
|
|
|
|
|
|
|
|
|
209 |
|
210 |
-
|
211 |
-
|
212 |
-
return base.slice(0, -ext.length);
|
213 |
}
|
214 |
|
215 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
216 |
},
|
217 |
-
relative: (from: string, to: string): string => {
|
218 |
-
// Handle empty inputs
|
219 |
-
if (!from || !to) return '.';
|
220 |
-
|
221 |
-
// Normalize paths by removing trailing slashes and splitting
|
222 |
-
const normalizePathParts = (p: string) =>
|
223 |
-
p.replace(/\/+$/, '')
|
224 |
-
.split('/')
|
225 |
-
.filter(Boolean);
|
226 |
-
|
227 |
-
const fromParts = normalizePathParts(from);
|
228 |
-
const toParts = normalizePathParts(to);
|
229 |
-
|
230 |
-
// Find common parts at the start of both paths
|
231 |
-
let commonLength = 0;
|
232 |
-
const minLength = Math.min(fromParts.length, toParts.length);
|
233 |
-
|
234 |
-
for (let i = 0; i < minLength; i++) {
|
235 |
-
if (fromParts[i] !== toParts[i]) break;
|
236 |
-
commonLength++;
|
237 |
-
}
|
238 |
|
239 |
-
|
240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
241 |
|
242 |
-
|
243 |
-
|
|
|
244 |
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
...remainingPath
|
249 |
-
];
|
250 |
|
251 |
-
|
252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
253 |
}
|
254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { WebContainer } from '@webcontainer/api';
|
2 |
+
import { useCallback, useEffect, useState } from 'react';
|
3 |
+
import { webcontainer as webcontainerPromise } from '~/lib/webcontainer';
|
4 |
+
import git, { type PromiseFsClient } from 'isomorphic-git';
|
5 |
+
import http from 'isomorphic-git/http/web';
|
6 |
import Cookies from 'js-cookie';
|
7 |
|
|
|
|
|
|
|
8 |
export function useGit() {
|
9 |
+
const [ready, setReady] = useState(false);
|
10 |
+
const [webcontainer, setWebcontainer] = useState<WebContainer>();
|
11 |
+
const [fs, setFs] = useState<PromiseFsClient>();
|
12 |
+
const lookupSavedPassword: (url: string) => any | null = (url: string) => {
|
13 |
+
try {
|
14 |
+
// Save updated API keys to cookies with 30 day expiry and secure settings
|
15 |
+
const creds = Cookies.get(`git:${url}`);
|
16 |
+
|
17 |
+
if (creds) {
|
18 |
+
const parsedCreds = JSON.parse(creds);
|
19 |
+
|
20 |
+
if (typeof parsedCreds === 'object' && parsedCreds !== null) {
|
21 |
+
return parsedCreds;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
}
|
23 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
|
25 |
+
return null;
|
26 |
+
} catch (error) {
|
27 |
+
console.error('Error saving API keys to cookies:', error);
|
28 |
+
return null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
}
|
30 |
+
};
|
31 |
+
useEffect(() => {
|
32 |
+
webcontainerPromise.then((container) => {
|
33 |
+
setWebcontainer(container);
|
34 |
+
setFs(getFs(container));
|
35 |
+
setReady(true);
|
36 |
+
});
|
37 |
+
}, []);
|
38 |
+
|
39 |
+
const gitClone = useCallback(
|
40 |
+
async (url: string) => {
|
41 |
+
if (!webcontainer || !fs || !ready) {
|
42 |
+
return;
|
43 |
+
}
|
44 |
+
|
45 |
+
const repo = await git.clone({
|
46 |
+
fs,
|
47 |
+
http,
|
48 |
+
dir: webcontainer.workdir,
|
49 |
+
url,
|
50 |
+
depth: 1,
|
51 |
+
singleBranch: true,
|
52 |
+
corsProxy: 'https://cors.isomorphic-git.org',
|
53 |
+
onAuth: (url) => {
|
54 |
+
let auth = lookupSavedPassword(url);
|
55 |
+
|
56 |
+
if (auth) {
|
57 |
+
return auth;
|
58 |
+
}
|
59 |
+
|
60 |
+
if (confirm('This repo is password protected. Ready to enter a username & password?')) {
|
61 |
+
auth = {
|
62 |
+
username: prompt('Enter username'),
|
63 |
+
password: prompt('Enter password'),
|
64 |
+
};
|
65 |
+
return auth;
|
66 |
+
} else {
|
67 |
+
return { cancel: true };
|
68 |
+
}
|
|
|
|
|
|
|
|
|
69 |
},
|
70 |
+
});
|
71 |
+
console.log(repo);
|
72 |
+
},
|
73 |
+
[webcontainer],
|
74 |
+
);
|
75 |
|
76 |
+
return { ready, gitClone };
|
77 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
|
79 |
+
const getFs: (c: WebContainer) => PromiseFsClient = (webcontainer: WebContainer) => ({
|
80 |
+
promises: {
|
81 |
+
readFile: async (path: string, options: any) => {
|
82 |
+
const encoding = options.encoding;
|
83 |
+
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
84 |
+
console.log('readFile', relativePath, encoding);
|
85 |
|
86 |
+
return await webcontainer.fs.readFile(relativePath, encoding);
|
87 |
+
},
|
88 |
+
writeFile: async (path: string, data: any, options: any) => {
|
89 |
+
const encoding = options.encoding;
|
90 |
+
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
91 |
+
console.log('writeFile', { relativePath, data, encoding });
|
92 |
|
93 |
+
return await webcontainer.fs.writeFile(relativePath, data, { ...options, encoding });
|
94 |
+
},
|
95 |
+
mkdir: async (path: string, options: any) => {
|
96 |
+
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
97 |
+
console.log('mkdir', relativePath, options);
|
98 |
|
99 |
+
return await webcontainer.fs.mkdir(relativePath, { ...options, recursive: true });
|
100 |
+
},
|
101 |
+
readdir: async (path: string, options: any) => {
|
102 |
+
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
103 |
+
console.log('readdir', relativePath, options);
|
|
|
|
|
104 |
|
105 |
+
return await webcontainer.fs.readdir(relativePath, options);
|
106 |
+
},
|
107 |
+
rm: async (path: string, options: any) => {
|
108 |
+
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
109 |
+
console.log('rm', relativePath, options);
|
110 |
|
111 |
+
return await webcontainer.fs.rm(relativePath, { ...(options || {}) });
|
112 |
+
},
|
113 |
+
rmdir: async (path: string, options: any) => {
|
114 |
+
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
115 |
+
console.log('rmdir', relativePath, options);
|
116 |
|
117 |
+
return await webcontainer.fs.rm(relativePath, { recursive: true, ...options });
|
|
|
118 |
},
|
119 |
|
120 |
+
// Mock implementations for missing functions
|
121 |
+
unlink: async (path: string) => {
|
122 |
+
// unlink is just removing a single file
|
123 |
+
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
124 |
+
return await webcontainer.fs.rm(relativePath, { recursive: false });
|
125 |
+
},
|
126 |
|
127 |
+
stat: async (path: string) => {
|
128 |
+
try {
|
129 |
+
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
130 |
+
const resp = await webcontainer.fs.readdir(pathUtils.dirname(relativePath), { withFileTypes: true });
|
131 |
+
const name = pathUtils.basename(relativePath);
|
132 |
+
const fileInfo = resp.find((x) => x.name == name);
|
133 |
|
134 |
+
if (!fileInfo) {
|
135 |
+
throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
|
|
|
136 |
}
|
137 |
|
138 |
+
return {
|
139 |
+
isFile: () => fileInfo.isFile(),
|
140 |
+
isDirectory: () => fileInfo.isDirectory(),
|
141 |
+
isSymbolicLink: () => false,
|
142 |
+
size: 1,
|
143 |
+
mode: 0o666, // Default permissions
|
144 |
+
mtimeMs: Date.now(),
|
145 |
+
uid: 1000,
|
146 |
+
gid: 1000,
|
147 |
+
};
|
148 |
+
} catch (error: any) {
|
149 |
+
console.log(error?.message);
|
150 |
+
|
151 |
+
const err = new Error(`ENOENT: no such file or directory, stat '${path}'`) as NodeJS.ErrnoException;
|
152 |
+
err.code = 'ENOENT';
|
153 |
+
err.errno = -2;
|
154 |
+
err.syscall = 'stat';
|
155 |
+
err.path = path;
|
156 |
+
throw err;
|
157 |
+
}
|
158 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
|
160 |
+
lstat: async (path: string) => {
|
161 |
+
/*
|
162 |
+
* For basic usage, lstat can return the same as stat
|
163 |
+
* since we're not handling symbolic links
|
164 |
+
*/
|
165 |
+
return await getFs(webcontainer).promises.stat(path);
|
166 |
+
},
|
167 |
+
|
168 |
+
readlink: async (path: string) => {
|
169 |
+
/*
|
170 |
+
* Since WebContainer doesn't support symlinks,
|
171 |
+
* we'll throw a "not a symbolic link" error
|
172 |
+
*/
|
173 |
+
throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
|
174 |
+
},
|
175 |
+
|
176 |
+
symlink: async (target: string, path: string) => {
|
177 |
+
/*
|
178 |
+
* Since WebContainer doesn't support symlinks,
|
179 |
+
* we'll throw a "operation not supported" error
|
180 |
+
*/
|
181 |
+
throw new Error(`EPERM: operation not permitted, symlink '${target}' -> '${path}'`);
|
182 |
+
},
|
183 |
+
|
184 |
+
chmod: async (_path: string, _mode: number) => {
|
185 |
+
/*
|
186 |
+
* WebContainer doesn't support changing permissions,
|
187 |
+
* but we can pretend it succeeded for compatibility
|
188 |
+
*/
|
189 |
+
return await Promise.resolve();
|
190 |
+
},
|
191 |
+
},
|
192 |
+
});
|
193 |
+
|
194 |
+
const pathUtils = {
|
195 |
+
dirname: (path: string) => {
|
196 |
+
// Handle empty or just filename cases
|
197 |
+
if (!path || !path.includes('/')) {
|
198 |
+
return '.';
|
199 |
+
}
|
200 |
+
|
201 |
+
// Remove trailing slashes
|
202 |
+
path = path.replace(/\/+$/, '');
|
203 |
|
204 |
+
// Get directory part
|
205 |
+
return path.split('/').slice(0, -1).join('/') || '/';
|
206 |
+
},
|
207 |
|
208 |
+
basename: (path: string, ext?: string) => {
|
209 |
+
// Remove trailing slashes
|
210 |
+
path = path.replace(/\/+$/, '');
|
|
|
|
|
211 |
|
212 |
+
// Get the last part of the path
|
213 |
+
const base = path.split('/').pop() || '';
|
214 |
+
|
215 |
+
// If extension is provided, remove it from the result
|
216 |
+
if (ext && base.endsWith(ext)) {
|
217 |
+
return base.slice(0, -ext.length);
|
218 |
+
}
|
219 |
+
|
220 |
+
return base;
|
221 |
+
},
|
222 |
+
relative: (from: string, to: string): string => {
|
223 |
+
// Handle empty inputs
|
224 |
+
if (!from || !to) {
|
225 |
+
return '.';
|
226 |
}
|
227 |
+
|
228 |
+
// Normalize paths by removing trailing slashes and splitting
|
229 |
+
const normalizePathParts = (p: string) => p.replace(/\/+$/, '').split('/').filter(Boolean);
|
230 |
+
|
231 |
+
const fromParts = normalizePathParts(from);
|
232 |
+
const toParts = normalizePathParts(to);
|
233 |
+
|
234 |
+
// Find common parts at the start of both paths
|
235 |
+
let commonLength = 0;
|
236 |
+
const minLength = Math.min(fromParts.length, toParts.length);
|
237 |
+
|
238 |
+
for (let i = 0; i < minLength; i++) {
|
239 |
+
if (fromParts[i] !== toParts[i]) {
|
240 |
+
break;
|
241 |
+
}
|
242 |
+
|
243 |
+
commonLength++;
|
244 |
+
}
|
245 |
+
|
246 |
+
// Calculate the number of "../" needed
|
247 |
+
const upCount = fromParts.length - commonLength;
|
248 |
+
|
249 |
+
// Get the remaining path parts we need to append
|
250 |
+
const remainingPath = toParts.slice(commonLength);
|
251 |
+
|
252 |
+
// Construct the relative path
|
253 |
+
const relativeParts = [...Array(upCount).fill('..'), ...remainingPath];
|
254 |
+
|
255 |
+
// Handle empty result case
|
256 |
+
return relativeParts.length === 0 ? '.' : relativeParts.join('/');
|
257 |
+
},
|
258 |
+
};
|
package-lock.json
DELETED
The diff for this file is too large to render.
See raw diff
|
|
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",
|
@@ -109,6 +110,7 @@
|
|
109 |
"husky": "9.1.7",
|
110 |
"is-ci": "^3.0.1",
|
111 |
"node-fetch": "^3.3.2",
|
|
|
112 |
"prettier": "^3.4.1",
|
113 |
"sass-embedded": "^1.81.0",
|
114 |
"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",
|
|
|
110 |
"husky": "9.1.7",
|
111 |
"is-ci": "^3.0.1",
|
112 |
"node-fetch": "^3.3.2",
|
113 |
+
"pnpm": "^9.14.4",
|
114 |
"prettier": "^3.4.1",
|
115 |
"sass-embedded": "^1.81.0",
|
116 |
"typescript": "^5.7.2",
|
pnpm-lock.yaml
CHANGED
The diff for this file is too large to render.
See raw diff
|
|