@dyyz1993/create-agent 2.0.1 → 2.1.1

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.
Files changed (60) hide show
  1. package/package.json +1 -1
  2. package/src/commands/create.ts +31 -31
  3. package/src/commands/workspace.ts +112 -105
  4. package/src/lib/copy.ts +117 -12
  5. package/templates/agent/.prettierignore +6 -0
  6. package/templates/agent/.prettierrc +9 -0
  7. package/templates/agent/commitlint.config.js +8 -0
  8. package/templates/agent/electron/main.js +46 -0
  9. package/templates/agent/electron/preload.js +5 -0
  10. package/templates/agent/electron-builder.json +40 -0
  11. package/templates/agent/eslint.config.mjs +2 -0
  12. package/templates/agent/package.json +75 -2
  13. package/templates/agent/src/mainview/App.tsx +29 -26
  14. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
  15. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
  16. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
  17. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +3 -3
  18. package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
  19. package/templates/agent/src/mainview/lib/api-client.ts +1 -4
  20. package/templates/agent/src/mainview/main.tsx +4 -10
  21. package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
  22. package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
  23. package/templates/agent/src/server-config.ts +1 -1
  24. package/templates/agent/src/server.ts +1 -2
  25. package/templates/agent/src/shared/handlers/chat.ts +5 -5
  26. package/templates/agent/src/shared/handlers/debug.ts +5 -1
  27. package/templates/agent/src/shared/handlers/git.ts +286 -243
  28. package/templates/agent/src/shared/http-routes.ts +1 -1
  29. package/templates/agent/src/shared/lib/bash-security.ts +43 -43
  30. package/templates/agent/tsconfig.ipc.json +5 -1
  31. package/templates/agent/tsconfig.json +3 -1
  32. package/templates/chat/package.json +3 -0
  33. package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
  34. package/templates/chat/src/mainview/lib/api-client.ts +2 -5
  35. package/templates/chat/src/mainview/main.tsx +10 -7
  36. package/templates/chat/src/server-config.ts +1 -1
  37. package/templates/chat/src/server.ts +1 -2
  38. package/templates/chat/src/shared/handlers/chat.ts +5 -5
  39. package/templates/chat/src/shared/handlers/debug.ts +5 -1
  40. package/templates/chat/src/shared/http-routes.ts +1 -1
  41. package/templates/chat/tsconfig.ipc.json +5 -1
  42. package/templates/chat/tsconfig.json +12 -2
  43. package/templates/general/package.json +3 -0
  44. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
  45. package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
  46. package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
  47. package/templates/general/src/mainview/lib/api-client.ts +2 -5
  48. package/templates/general/src/mainview/main.tsx +10 -7
  49. package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
  50. package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
  51. package/templates/general/src/server-config.ts +1 -1
  52. package/templates/general/src/server.ts +1 -2
  53. package/templates/general/src/shared/handlers/chat.ts +5 -5
  54. package/templates/general/src/shared/handlers/debug.ts +5 -1
  55. package/templates/general/src/shared/handlers/git.ts +286 -243
  56. package/templates/general/src/shared/http-routes.ts +1 -1
  57. package/templates/general/tsconfig.ipc.json +5 -1
  58. package/templates/general/tsconfig.json +12 -2
  59. package/templates/shared/components/ErrorBoundary.tsx +50 -49
  60. package/templates/shared/http-routes.ts +210 -190
@@ -1,58 +1,59 @@
1
- import React, { Component, type ErrorInfo, type ReactNode } from "react";
1
+ import { Component, type ErrorInfo, type ReactNode } from "react";
2
2
 
3
3
  interface Props {
4
- children: ReactNode;
5
- fallback?: ReactNode;
4
+ children: ReactNode;
5
+ fallback?: ReactNode;
6
6
  }
7
7
 
8
8
  interface State {
9
- hasError: boolean;
10
- error: Error | null;
9
+ hasError: boolean;
10
+ error: Error | null;
11
11
  }
12
12
 
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
14
  export class ErrorBoundary extends Component<Props, State> {
14
- constructor(props: Props) {
15
- super(props);
16
- this.state = { hasError: false, error: null };
17
- }
18
-
19
- static getDerivedStateFromError(error: Error): State {
20
- return { hasError: true, error };
21
- }
22
-
23
- componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
24
- console.error("[ErrorBoundary]", error, errorInfo);
25
- }
26
-
27
- handleRetry = (): void => {
28
- this.setState({ hasError: false, error: null });
29
- };
30
-
31
- render(): ReactNode {
32
- if (this.state.hasError) {
33
- if (this.props.fallback) {
34
- return this.props.fallback;
35
- }
36
-
37
- return (
38
- <div className="flex flex-col items-center justify-center h-full p-8 bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
39
- <div className="text-4xl mb-4">⚠️</div>
40
- <h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
41
- <p className="text-sm text-[var(--color-text-secondary)] mb-4 max-w-md text-center">
42
- {this.state.error?.message || "An unexpected error occurred"}
43
- </p>
44
- <button
45
- onClick={this.handleRetry}
46
- className="px-4 py-2 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-white rounded-lg transition-colors text-sm"
47
- role="button"
48
- aria-label="Retry"
49
- >
50
- Retry
51
- </button>
52
- </div>
53
- );
54
- }
55
-
56
- return this.props.children;
57
- }
15
+ constructor(props: Props) {
16
+ super(props);
17
+ this.state = { hasError: false, error: null };
18
+ }
19
+
20
+ static getDerivedStateFromError(error: Error): State {
21
+ return { hasError: true, error };
22
+ }
23
+
24
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
25
+ console.error("[ErrorBoundary]", error, errorInfo);
26
+ }
27
+
28
+ handleRetry = (): void => {
29
+ this.setState({ hasError: false, error: null });
30
+ };
31
+
32
+ render(): ReactNode {
33
+ if (this.state.hasError) {
34
+ if (this.props.fallback) {
35
+ return this.props.fallback;
36
+ }
37
+
38
+ return (
39
+ <div className="flex flex-col items-center justify-center h-full p-8 bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
40
+ <div className="text-4xl mb-4">⚠️</div>
41
+ <h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
42
+ <p className="text-sm text-[var(--color-text-secondary)] mb-4 max-w-md text-center">
43
+ {this.state.error?.message || "An unexpected error occurred"}
44
+ </p>
45
+ <button
46
+ onClick={this.handleRetry}
47
+ className="px-4 py-2 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-white rounded-lg transition-colors text-sm"
48
+ role="button"
49
+ aria-label="Retry"
50
+ >
51
+ Retry
52
+ </button>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ return this.props.children;
58
+ }
58
59
  }
@@ -13,218 +13,238 @@ import { createLogger } from "./logger";
13
13
  const log = createLogger("gateway");
14
14
 
15
15
  function safeEqual(a: string, b: string): boolean {
16
- const bufA = Buffer.from(a);
17
- const bufB = Buffer.from(b);
18
- if (bufA.length !== bufB.length) return false;
19
- return timingSafeEqual(bufA, bufB);
16
+ const bufA = Buffer.from(a);
17
+ const bufB = Buffer.from(b);
18
+ if (bufA.length !== bufB.length) return false;
19
+ return timingSafeEqual(bufA, bufB);
20
20
  }
21
21
 
22
22
  const MIME_TYPES: Record<string, string> = {
23
- ".html": "text/html",
24
- ".css": "text/css",
25
- ".js": "application/javascript",
26
- ".json": "application/json",
27
- ".png": "image/png",
28
- ".jpg": "image/jpeg",
29
- ".jpeg": "image/jpeg",
30
- ".gif": "image/gif",
31
- ".svg": "image/svg+xml",
32
- ".ico": "image/x-icon",
33
- ".txt": "text/plain",
34
- ".md": "text/markdown",
35
- ".ts": "text/plain",
36
- ".tsx": "text/plain",
37
- ".py": "text/plain",
38
- ".pdf": "application/pdf",
39
- ".zip": "application/zip",
40
- ".mp4": "video/mp4",
41
- ".mp3": "audio/mpeg",
42
- ".wav": "audio/wav",
23
+ ".html": "text/html",
24
+ ".css": "text/css",
25
+ ".js": "application/javascript",
26
+ ".json": "application/json",
27
+ ".png": "image/png",
28
+ ".jpg": "image/jpeg",
29
+ ".jpeg": "image/jpeg",
30
+ ".gif": "image/gif",
31
+ ".svg": "image/svg+xml",
32
+ ".ico": "image/x-icon",
33
+ ".txt": "text/plain",
34
+ ".md": "text/markdown",
35
+ ".ts": "text/plain",
36
+ ".tsx": "text/plain",
37
+ ".py": "text/plain",
38
+ ".pdf": "application/pdf",
39
+ ".zip": "application/zip",
40
+ ".mp4": "video/mp4",
41
+ ".mp3": "audio/mpeg",
42
+ ".wav": "audio/wav",
43
43
  };
44
44
 
45
45
  const ALLOWED_ROOTS = [resolve(process.cwd())];
46
46
  function isPathAllowed(requestedPath: string): boolean {
47
- const resolved = resolve(requestedPath);
48
- return ALLOWED_ROOTS.some((root) => resolved === root || resolved.startsWith(root + "/"));
47
+ const resolved = resolve(requestedPath);
48
+ return ALLOWED_ROOTS.some((root) => resolved === root || resolved.startsWith(root + "/"));
49
49
  }
50
50
 
51
51
  function verifyToken(req: IncomingMessage, authToken: string): boolean {
52
- const auth = req.headers["authorization"];
53
- if (auth && safeEqual(auth, `Bearer ${authToken}`)) return true;
54
-
55
- if (req.url) {
56
- try {
57
- const url = new URL(req.url, "http://localhost");
58
- const token = url.searchParams.get("token");
59
- if (token && safeEqual(token, authToken)) return true;
60
- } catch { /* invalid URL */ }
61
- }
62
- return false;
52
+ const auth = req.headers["authorization"];
53
+ if (auth && safeEqual(auth, `Bearer ${authToken}`)) return true;
54
+
55
+ if (req.url) {
56
+ try {
57
+ const url = new URL(req.url, "http://localhost");
58
+ const token = url.searchParams.get("token");
59
+ if (token && safeEqual(token, authToken)) return true;
60
+ } catch {
61
+ /* invalid URL */
62
+ }
63
+ }
64
+ return false;
63
65
  }
64
66
 
65
67
  export interface HttpRouteDeps {
66
- config: { readonly port: number; readonly authToken: string; readonly maxUploadSize: number; readonly corsOrigin: string };
67
- getWebSocketClientCount: () => number;
68
+ config: {
69
+ readonly port: number;
70
+ readonly authToken: string;
71
+ readonly maxUploadSize: number;
72
+ readonly corsOrigin: string;
73
+ };
74
+ getWebSocketClientCount: () => number;
68
75
  }
69
76
 
70
- export function createHttpHandler(deps: HttpRouteDeps): (req: IncomingMessage, res: ServerResponse) => void {
71
- const { config: cfg, getWebSocketClientCount } = deps;
72
-
73
- return async (req, res) => {
74
- res.setHeader("Access-Control-Allow-Origin", cfg.corsOrigin);
75
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
76
- res.setHeader("Access-Control-Allow-Headers", "Authorization, Range, Content-Type");
77
- if (req.method === "OPTIONS") {
78
- res.writeHead(204).end();
79
- return;
80
- }
81
-
82
- if (!req.url) {
83
- res.writeHead(400).end();
84
- return;
85
- }
86
-
87
- const url = new URL(req.url, "http://localhost");
88
-
89
- if (url.pathname === "/health") {
90
- res.writeHead(200, { "Content-Type": "application/json" });
91
- res.end(JSON.stringify({ status: "ok", clients: getWebSocketClientCount() }));
92
- return;
93
- }
94
-
95
- if (!verifyToken(req, cfg.authToken)) {
96
- log.warn("Auth failed", { path: url.pathname });
97
- res.writeHead(401, { "Content-Type": "application/json" });
98
- res.end(JSON.stringify({ error: "Unauthorized" }));
99
- return;
100
- }
101
-
102
- if (url.pathname.startsWith("/info/")) {
103
- await handleFileInfo(url.pathname.slice(6), res);
104
- return;
105
- }
106
-
107
- if (url.pathname.startsWith("/file/")) {
108
- if (url.pathname === "/file/upload" && req.method === "POST") {
109
- await handleFileUpload(req, url.searchParams.get("path"), res, cfg.maxUploadSize);
110
- return;
111
- }
112
- await handleFileContent(url.pathname.slice(6), req, res);
113
- return;
114
- }
115
-
116
- res.writeHead(404);
117
- res.end();
118
- };
77
+ export function createHttpHandler(
78
+ deps: HttpRouteDeps
79
+ ): (req: IncomingMessage, res: ServerResponse) => void {
80
+ const { config: cfg, getWebSocketClientCount } = deps;
81
+
82
+ return async (req, res) => {
83
+ res.setHeader("Access-Control-Allow-Origin", cfg.corsOrigin);
84
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
85
+ res.setHeader("Access-Control-Allow-Headers", "Authorization, Range, Content-Type");
86
+ if (req.method === "OPTIONS") {
87
+ res.writeHead(204).end();
88
+ return;
89
+ }
90
+
91
+ if (!req.url) {
92
+ res.writeHead(400).end();
93
+ return;
94
+ }
95
+
96
+ const url = new URL(req.url, "http://localhost");
97
+
98
+ if (url.pathname === "/health") {
99
+ res.writeHead(200, { "Content-Type": "application/json" });
100
+ res.end(JSON.stringify({ status: "ok", clients: getWebSocketClientCount() }));
101
+ return;
102
+ }
103
+
104
+ if (!verifyToken(req, cfg.authToken)) {
105
+ log.warn("Auth failed", { path: url.pathname });
106
+ res.writeHead(401, { "Content-Type": "application/json" });
107
+ res.end(JSON.stringify({ error: "Unauthorized" }));
108
+ return;
109
+ }
110
+
111
+ if (url.pathname.startsWith("/info/")) {
112
+ await handleFileInfo(url.pathname.slice(6), res);
113
+ return;
114
+ }
115
+
116
+ if (url.pathname.startsWith("/file/")) {
117
+ if (url.pathname === "/file/upload" && req.method === "POST") {
118
+ await handleFileUpload(req, url.searchParams.get("path"), res, cfg.maxUploadSize);
119
+ return;
120
+ }
121
+ await handleFileContent(url.pathname.slice(6), req, res);
122
+ return;
123
+ }
124
+
125
+ res.writeHead(404);
126
+ res.end();
127
+ };
119
128
  }
120
129
 
121
130
  async function handleFileInfo(encodedPath: string, res: ServerResponse): Promise<void> {
122
- const filePath = decodeURIComponent(encodedPath);
123
- if (!isPathAllowed(filePath)) {
124
- res.writeHead(403, { "Content-Type": "application/json" });
125
- res.end(JSON.stringify({ error: "Path not allowed" }));
126
- return;
127
- }
128
- try {
129
- const s = await stat(filePath);
130
- res.writeHead(200, { "Content-Type": "application/json" });
131
- res.end(JSON.stringify({
132
- name: basename(filePath),
133
- path: filePath,
134
- size: s.size,
135
- isDirectory: s.isDirectory(),
136
- modified: s.mtime.toISOString(),
137
- mimeType: s.isFile() ? (MIME_TYPES[extname(filePath)] || "application/octet-stream") : undefined,
138
- }));
139
- } catch {
140
- res.writeHead(404, { "Content-Type": "application/json" });
141
- res.end(JSON.stringify({ error: "File not found" }));
142
- }
131
+ const filePath = decodeURIComponent(encodedPath);
132
+ if (!isPathAllowed(filePath)) {
133
+ res.writeHead(403, { "Content-Type": "application/json" });
134
+ res.end(JSON.stringify({ error: "Path not allowed" }));
135
+ return;
136
+ }
137
+ try {
138
+ const s = await stat(filePath);
139
+ res.writeHead(200, { "Content-Type": "application/json" });
140
+ res.end(
141
+ JSON.stringify({
142
+ name: basename(filePath),
143
+ path: filePath,
144
+ size: s.size,
145
+ isDirectory: s.isDirectory(),
146
+ modified: s.mtime.toISOString(),
147
+ mimeType: s.isFile()
148
+ ? MIME_TYPES[extname(filePath)] || "application/octet-stream"
149
+ : undefined,
150
+ })
151
+ );
152
+ } catch {
153
+ res.writeHead(404, { "Content-Type": "application/json" });
154
+ res.end(JSON.stringify({ error: "File not found" }));
155
+ }
143
156
  }
144
157
 
145
- async function handleFileContent(encodedPath: string, req: IncomingMessage, res: ServerResponse): Promise<void> {
146
- const filePath = decodeURIComponent(encodedPath);
147
- if (!isPathAllowed(filePath)) {
148
- res.writeHead(403, { "Content-Type": "application/json" });
149
- res.end(JSON.stringify({ error: "Path not allowed" }));
150
- return;
151
- }
152
- try {
153
- if (!existsSync(filePath)) {
154
- res.writeHead(404, { "Content-Type": "application/json" });
155
- res.end(JSON.stringify({ error: "File not found" }));
156
- return;
157
- }
158
- const s = await stat(filePath);
159
- const mimeType = MIME_TYPES[extname(filePath)] || "application/octet-stream";
160
-
161
- const range = req.headers["range"];
162
- if (range) {
163
- const parts = range.replace(/bytes=/, "").split("-");
164
- const start = parseInt(parts[0], 10);
165
- const end = parts[1] ? parseInt(parts[1], 10) : s.size - 1;
166
- const chunkSize = end - start + 1;
167
-
168
- res.writeHead(206, {
169
- "Content-Range": `bytes ${start}-${end}/${s.size}`,
170
- "Accept-Ranges": "bytes",
171
- "Content-Length": chunkSize,
172
- "Content-Type": mimeType,
173
- });
174
- const buffer = await readFile(filePath);
175
- res.end(buffer.subarray(start, end + 1));
176
- } else {
177
- res.writeHead(200, {
178
- "Content-Length": s.size,
179
- "Content-Type": mimeType,
180
- "Accept-Ranges": "bytes",
181
- });
182
- const buffer = await readFile(filePath);
183
- res.end(buffer);
184
- }
185
- log.info("File served", { path: filePath });
186
- } catch {
187
- res.writeHead(500, { "Content-Type": "application/json" });
188
- res.end(JSON.stringify({ error: "Failed to read file" }));
189
- }
158
+ async function handleFileContent(
159
+ encodedPath: string,
160
+ req: IncomingMessage,
161
+ res: ServerResponse
162
+ ): Promise<void> {
163
+ const filePath = decodeURIComponent(encodedPath);
164
+ if (!isPathAllowed(filePath)) {
165
+ res.writeHead(403, { "Content-Type": "application/json" });
166
+ res.end(JSON.stringify({ error: "Path not allowed" }));
167
+ return;
168
+ }
169
+ try {
170
+ if (!existsSync(filePath)) {
171
+ res.writeHead(404, { "Content-Type": "application/json" });
172
+ res.end(JSON.stringify({ error: "File not found" }));
173
+ return;
174
+ }
175
+ const s = await stat(filePath);
176
+ const mimeType = MIME_TYPES[extname(filePath)] || "application/octet-stream";
177
+
178
+ const range = req.headers["range"];
179
+ if (range) {
180
+ const parts = range.replace(/bytes=/, "").split("-");
181
+ const start = parseInt(parts[0]!, 10);
182
+ const end = parts[1] ? parseInt(parts[1], 10) : s.size - 1;
183
+ const chunkSize = end - start + 1;
184
+
185
+ res.writeHead(206, {
186
+ "Content-Range": `bytes ${start}-${end}/${s.size}`,
187
+ "Accept-Ranges": "bytes",
188
+ "Content-Length": chunkSize,
189
+ "Content-Type": mimeType,
190
+ });
191
+ const buffer = await readFile(filePath);
192
+ res.end(buffer.subarray(start, end + 1));
193
+ } else {
194
+ res.writeHead(200, {
195
+ "Content-Length": s.size,
196
+ "Content-Type": mimeType,
197
+ "Accept-Ranges": "bytes",
198
+ });
199
+ const buffer = await readFile(filePath);
200
+ res.end(buffer);
201
+ }
202
+ log.info("File served", { path: filePath });
203
+ } catch {
204
+ res.writeHead(500, { "Content-Type": "application/json" });
205
+ res.end(JSON.stringify({ error: "Failed to read file" }));
206
+ }
190
207
  }
191
208
 
192
209
  async function handleFileUpload(
193
- req: IncomingMessage,
194
- destPath: string | null,
195
- res: ServerResponse,
196
- maxUploadSize: number,
210
+ req: IncomingMessage,
211
+ destPath: string | null,
212
+ res: ServerResponse,
213
+ maxUploadSize: number
197
214
  ): Promise<void> {
198
- if (!destPath) {
199
- res.writeHead(400, { "Content-Type": "application/json" });
200
- res.end(JSON.stringify({ error: "Missing path parameter" }));
201
- return;
202
- }
203
- if (!isPathAllowed(destPath)) {
204
- res.writeHead(403, { "Content-Type": "application/json" });
205
- res.end(JSON.stringify({ error: "Path not allowed" }));
206
- return;
207
- }
208
- const contentLength = parseInt(req.headers["content-length"] || "0", 10);
209
- if (contentLength > maxUploadSize) {
210
- res.writeHead(413, { "Content-Type": "application/json" });
211
- res.end(JSON.stringify({ error: `File too large, max ${maxUploadSize / 1024 / 1024}MB` }));
212
- return;
213
- }
214
- try {
215
- const chunks: Uint8Array[] = [];
216
- for await (const chunk of req) {
217
- const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk as ArrayBuffer);
218
- chunks.push(bytes);
219
- }
220
- const body = Buffer.concat(chunks);
221
- await mkdir(dirname(destPath), { recursive: true });
222
- await writeFile(destPath, body);
223
- log.info("File uploaded", { path: destPath, size: body.length });
224
- res.writeHead(200, { "Content-Type": "application/json" });
225
- res.end(JSON.stringify({ ok: true, path: destPath, size: body.length }));
226
- } catch (err) {
227
- res.writeHead(500, { "Content-Type": "application/json" });
228
- res.end(JSON.stringify({ error: err instanceof Error ? err.message : "Upload failed" }));
229
- }
215
+ if (!destPath) {
216
+ res.writeHead(400, { "Content-Type": "application/json" });
217
+ res.end(JSON.stringify({ error: "Missing path parameter" }));
218
+ return;
219
+ }
220
+ if (!isPathAllowed(destPath)) {
221
+ res.writeHead(403, { "Content-Type": "application/json" });
222
+ res.end(JSON.stringify({ error: "Path not allowed" }));
223
+ return;
224
+ }
225
+ const contentLength = parseInt(req.headers["content-length"] || "0", 10);
226
+ if (contentLength > maxUploadSize) {
227
+ res.writeHead(413, { "Content-Type": "application/json" });
228
+ res.end(JSON.stringify({ error: `File too large, max ${maxUploadSize / 1024 / 1024}MB` }));
229
+ return;
230
+ }
231
+ try {
232
+ const chunks: Uint8Array[] = [];
233
+ for await (const chunk of req) {
234
+ const bytes =
235
+ typeof chunk === "string"
236
+ ? new TextEncoder().encode(chunk)
237
+ : new Uint8Array(chunk as ArrayBuffer);
238
+ chunks.push(bytes);
239
+ }
240
+ const body = Buffer.concat(chunks);
241
+ await mkdir(dirname(destPath), { recursive: true });
242
+ await writeFile(destPath, body);
243
+ log.info("File uploaded", { path: destPath, size: body.length });
244
+ res.writeHead(200, { "Content-Type": "application/json" });
245
+ res.end(JSON.stringify({ ok: true, path: destPath, size: body.length }));
246
+ } catch (err) {
247
+ res.writeHead(500, { "Content-Type": "application/json" });
248
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : "Upload failed" }));
249
+ }
230
250
  }