@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.
- package/package.json +1 -1
- package/src/commands/create.ts +31 -31
- package/src/commands/workspace.ts +112 -105
- package/src/lib/copy.ts +117 -12
- package/templates/agent/.prettierignore +6 -0
- package/templates/agent/.prettierrc +9 -0
- package/templates/agent/commitlint.config.js +8 -0
- package/templates/agent/electron/main.js +46 -0
- package/templates/agent/electron/preload.js +5 -0
- package/templates/agent/electron-builder.json +40 -0
- package/templates/agent/eslint.config.mjs +2 -0
- package/templates/agent/package.json +75 -2
- package/templates/agent/src/mainview/App.tsx +29 -26
- package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
- package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
- package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
- package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +3 -3
- package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/agent/src/mainview/lib/api-client.ts +1 -4
- package/templates/agent/src/mainview/main.tsx +4 -10
- package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
- package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
- package/templates/agent/src/server-config.ts +1 -1
- package/templates/agent/src/server.ts +1 -2
- package/templates/agent/src/shared/handlers/chat.ts +5 -5
- package/templates/agent/src/shared/handlers/debug.ts +5 -1
- package/templates/agent/src/shared/handlers/git.ts +286 -243
- package/templates/agent/src/shared/http-routes.ts +1 -1
- package/templates/agent/src/shared/lib/bash-security.ts +43 -43
- package/templates/agent/tsconfig.ipc.json +5 -1
- package/templates/agent/tsconfig.json +3 -1
- package/templates/chat/package.json +3 -0
- package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/chat/src/mainview/lib/api-client.ts +2 -5
- package/templates/chat/src/mainview/main.tsx +10 -7
- package/templates/chat/src/server-config.ts +1 -1
- package/templates/chat/src/server.ts +1 -2
- package/templates/chat/src/shared/handlers/chat.ts +5 -5
- package/templates/chat/src/shared/handlers/debug.ts +5 -1
- package/templates/chat/src/shared/http-routes.ts +1 -1
- package/templates/chat/tsconfig.ipc.json +5 -1
- package/templates/chat/tsconfig.json +12 -2
- package/templates/general/package.json +3 -0
- package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
- package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
- package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/general/src/mainview/lib/api-client.ts +2 -5
- package/templates/general/src/mainview/main.tsx +10 -7
- package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
- package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
- package/templates/general/src/server-config.ts +1 -1
- package/templates/general/src/server.ts +1 -2
- package/templates/general/src/shared/handlers/chat.ts +5 -5
- package/templates/general/src/shared/handlers/debug.ts +5 -1
- package/templates/general/src/shared/handlers/git.ts +286 -243
- package/templates/general/src/shared/http-routes.ts +1 -1
- package/templates/general/tsconfig.ipc.json +5 -1
- package/templates/general/tsconfig.json +12 -2
- package/templates/shared/components/ErrorBoundary.tsx +50 -49
- package/templates/shared/http-routes.ts +210 -190
|
@@ -1,58 +1,59 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
interface Props {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
fallback?: ReactNode;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
interface State {
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
req: IncomingMessage,
|
|
211
|
+
destPath: string | null,
|
|
212
|
+
res: ServerResponse,
|
|
213
|
+
maxUploadSize: number
|
|
197
214
|
): Promise<void> {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
}
|