vite-plugin-opencode-assistant 1.0.13 → 1.0.15
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/es/client/index.js +384 -86
- package/es/core/api.d.ts +1 -0
- package/es/core/api.js +97 -15
- package/es/core/proxy-server.d.ts +5 -1
- package/es/core/proxy-server.js +75 -69
- package/es/core/service.d.ts +8 -1
- package/es/core/service.js +128 -28
- package/es/endpoints/index.js +2 -0
- package/es/endpoints/sse.js +14 -5
- package/es/endpoints/types.d.ts +7 -1
- package/es/endpoints/warmup.d.ts +3 -0
- package/es/endpoints/warmup.js +45 -0
- package/es/index.js +12 -3
- package/es/utils/system.d.ts +2 -1
- package/es/utils/system.js +11 -10
- package/lib/client/index.js +380 -85
- package/lib/client.js +2880 -2619
- package/lib/core/api.d.ts +1 -0
- package/lib/core/api.js +97 -15
- package/lib/core/proxy-server.d.ts +5 -1
- package/lib/core/proxy-server.js +75 -69
- package/lib/core/service.d.ts +8 -1
- package/lib/core/service.js +127 -28
- package/lib/endpoints/index.js +2 -0
- package/lib/endpoints/sse.js +14 -5
- package/lib/endpoints/types.d.ts +7 -1
- package/lib/endpoints/warmup.d.ts +3 -0
- package/lib/endpoints/warmup.js +68 -0
- package/lib/index.js +12 -3
- package/lib/style.css +1 -1
- package/lib/utils/system.d.ts +2 -1
- package/lib/utils/system.js +10 -5
- package/package.json +5 -5
package/es/core/api.js
CHANGED
|
@@ -35,13 +35,36 @@ function sleep(ms) {
|
|
|
35
35
|
function base64Encode(str) {
|
|
36
36
|
return Buffer.from(str).toString("base64");
|
|
37
37
|
}
|
|
38
|
+
function extractTextFromResponse(data) {
|
|
39
|
+
if (!data || typeof data !== "object") return null;
|
|
40
|
+
const obj = data;
|
|
41
|
+
if (obj.parts && Array.isArray(obj.parts)) {
|
|
42
|
+
const textParts = obj.parts.filter(
|
|
43
|
+
(p) => p && typeof p === "object" && p.type === "text"
|
|
44
|
+
).map((p) => p.text).filter(Boolean);
|
|
45
|
+
if (textParts.length > 0) return textParts.join("");
|
|
46
|
+
}
|
|
47
|
+
if (obj.text && typeof obj.text === "string") {
|
|
48
|
+
return obj.text;
|
|
49
|
+
}
|
|
50
|
+
if (obj.content && typeof obj.content === "string") {
|
|
51
|
+
return obj.content;
|
|
52
|
+
}
|
|
53
|
+
if (obj.message && typeof obj.message === "string") {
|
|
54
|
+
return obj.message;
|
|
55
|
+
}
|
|
56
|
+
if (typeof data === "string") {
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
38
61
|
class OpenCodeAPI {
|
|
39
62
|
constructor(hostname, getPort, warmupChromeMcpConfig = false) {
|
|
40
63
|
__publicField(this, "hostname", hostname);
|
|
41
64
|
__publicField(this, "getPort", getPort);
|
|
42
65
|
__publicField(this, "warmupChromeMcpConfig", warmupChromeMcpConfig);
|
|
43
66
|
}
|
|
44
|
-
createHttpRequest(options, body) {
|
|
67
|
+
createHttpRequest(options, body, timeout) {
|
|
45
68
|
const timer = new PerformanceTimer("HTTP Request", {
|
|
46
69
|
operation: `${options.method || "GET"} ${options.path}`
|
|
47
70
|
});
|
|
@@ -64,6 +87,13 @@ class OpenCodeAPI {
|
|
|
64
87
|
timer.end("\u274C Request failed");
|
|
65
88
|
reject(e);
|
|
66
89
|
});
|
|
90
|
+
if (timeout) {
|
|
91
|
+
req.setTimeout(timeout, () => {
|
|
92
|
+
timer.end("\u274C Request timeout");
|
|
93
|
+
req.destroy();
|
|
94
|
+
reject(new Error(`Request timeout after ${timeout}ms`));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
67
97
|
if (body) req.write(body);
|
|
68
98
|
req.end();
|
|
69
99
|
});
|
|
@@ -217,24 +247,16 @@ class OpenCodeAPI {
|
|
|
217
247
|
try {
|
|
218
248
|
const warmupSession = yield this.createSession(DEFAULT_RETRIES, "__chrome_mcp_warmup__");
|
|
219
249
|
warmupSessionId = warmupSession.id;
|
|
220
|
-
let chromeToolIds;
|
|
221
|
-
try {
|
|
222
|
-
const toolIds = yield this.getToolIds();
|
|
223
|
-
chromeToolIds = toolIds.filter((toolId) => /chrome[-_]?devtools/i.test(toolId));
|
|
224
|
-
log.debug("Resolved Chrome MCP tool ids", {
|
|
225
|
-
chromeToolIds
|
|
226
|
-
});
|
|
227
|
-
} catch (e) {
|
|
228
|
-
log.debug("Failed to resolve Chrome MCP tool ids", { error: e });
|
|
229
|
-
}
|
|
230
250
|
const prompt = [
|
|
231
251
|
"Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
|
|
232
252
|
viteOrigin ? `If there are no pages, call new_page with ${viteOrigin}.` : "If there are no pages, call new_page with about:blank.",
|
|
233
253
|
"Do not read or modify project files.",
|
|
234
254
|
"Do not use any non-browser tools.",
|
|
235
|
-
"After the tool call is complete, reply with exactly: ready"
|
|
255
|
+
"After the tool call is complete, reply with exactly: ready",
|
|
256
|
+
"If the tool call fails, reply with exactly: fail"
|
|
236
257
|
].join(" ");
|
|
237
|
-
|
|
258
|
+
const WARMUP_TIMEOUT = 3e4;
|
|
259
|
+
const data = yield this.createHttpRequest(
|
|
238
260
|
{
|
|
239
261
|
hostname: this.hostname,
|
|
240
262
|
port: this.getPort(),
|
|
@@ -244,14 +266,19 @@ class OpenCodeAPI {
|
|
|
244
266
|
},
|
|
245
267
|
JSON.stringify({
|
|
246
268
|
system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
|
|
247
|
-
tools: (chromeToolIds == null ? void 0 : chromeToolIds.length) ? chromeToolIds : void 0,
|
|
248
269
|
parts: [{ type: "text", text: prompt }]
|
|
249
|
-
})
|
|
270
|
+
}),
|
|
271
|
+
WARMUP_TIMEOUT
|
|
250
272
|
);
|
|
273
|
+
const responseText = extractTextFromResponse(data);
|
|
274
|
+
if (!(responseText == null ? void 0 : responseText.toLowerCase().includes("ready"))) {
|
|
275
|
+
throw new Error(`Chrome MCP warmup failed: ${responseText || "No response"}`);
|
|
276
|
+
}
|
|
251
277
|
timer.end("Chrome MCP warmed up");
|
|
252
278
|
} catch (e) {
|
|
253
279
|
log.warn("Failed to warm up Chrome MCP", { error: e });
|
|
254
280
|
timer.end("Chrome MCP warmup skipped");
|
|
281
|
+
throw e;
|
|
255
282
|
} finally {
|
|
256
283
|
if (warmupSessionId) {
|
|
257
284
|
try {
|
|
@@ -288,6 +315,61 @@ class OpenCodeAPI {
|
|
|
288
315
|
return url;
|
|
289
316
|
});
|
|
290
317
|
}
|
|
318
|
+
retryWarmupChromeMcp(viteOrigin) {
|
|
319
|
+
return __async(this, null, function* () {
|
|
320
|
+
const timer = log.timer("retryWarmupChromeMcp", { viteOrigin });
|
|
321
|
+
let warmupSessionId = null;
|
|
322
|
+
try {
|
|
323
|
+
const warmupSession = yield this.createSession(DEFAULT_RETRIES, "__chrome_mcp_warmup__");
|
|
324
|
+
warmupSessionId = warmupSession.id;
|
|
325
|
+
const prompt = [
|
|
326
|
+
"Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
|
|
327
|
+
viteOrigin ? `If there are no pages, call new_page with ${viteOrigin}.` : "If there are no pages, call new_page with about:blank.",
|
|
328
|
+
"Do not read or modify project files.",
|
|
329
|
+
"Do not use any non-browser tools.",
|
|
330
|
+
"After the tool call is complete, reply with exactly: ready",
|
|
331
|
+
"If the tool call fails, reply with exactly: fail"
|
|
332
|
+
].join(" ");
|
|
333
|
+
const WARMUP_TIMEOUT = 6e4;
|
|
334
|
+
const data = yield this.createHttpRequest(
|
|
335
|
+
{
|
|
336
|
+
hostname: this.hostname,
|
|
337
|
+
port: this.getPort(),
|
|
338
|
+
path: `/session/${warmupSessionId}/message`,
|
|
339
|
+
method: "POST",
|
|
340
|
+
headers: { "Content-Type": "application/json" }
|
|
341
|
+
},
|
|
342
|
+
JSON.stringify({
|
|
343
|
+
system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
|
|
344
|
+
parts: [{ type: "text", text: prompt }]
|
|
345
|
+
}),
|
|
346
|
+
WARMUP_TIMEOUT
|
|
347
|
+
);
|
|
348
|
+
log.debug("Chrome MCP warmup response:", { data });
|
|
349
|
+
const responseText = extractTextFromResponse(data);
|
|
350
|
+
if (!(responseText == null ? void 0 : responseText.toLowerCase().includes("ready"))) {
|
|
351
|
+
throw new Error(`Chrome MCP warmup failed: ${responseText || "No response"}`);
|
|
352
|
+
}
|
|
353
|
+
timer.end("Chrome MCP warmed up successfully");
|
|
354
|
+
return true;
|
|
355
|
+
} catch (e) {
|
|
356
|
+
log.warn("Failed to retry warm up Chrome MCP", { error: e });
|
|
357
|
+
timer.end("Chrome MCP warmup retry failed");
|
|
358
|
+
return false;
|
|
359
|
+
} finally {
|
|
360
|
+
if (warmupSessionId) {
|
|
361
|
+
try {
|
|
362
|
+
yield this.deleteSession(warmupSessionId, 5);
|
|
363
|
+
} catch (e) {
|
|
364
|
+
log.warn("Failed to delete warmup session after retries", {
|
|
365
|
+
error: e,
|
|
366
|
+
warmupSessionId
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
291
373
|
}
|
|
292
374
|
export {
|
|
293
375
|
OpenCodeAPI
|
|
@@ -8,4 +8,8 @@ export interface ProxyServerOptions {
|
|
|
8
8
|
/** OpenCode 内部设置 */
|
|
9
9
|
settings?: OpenCodeSettings;
|
|
10
10
|
}
|
|
11
|
-
export
|
|
11
|
+
export interface ProxyServerResult {
|
|
12
|
+
server: http.Server;
|
|
13
|
+
actualPort: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function startProxyServer(targetUrl: string, port: number, options?: ProxyServerOptions): Promise<ProxyServerResult>;
|
package/es/core/proxy-server.js
CHANGED
|
@@ -114,80 +114,86 @@ function generateBridgeScript(options) {
|
|
|
114
114
|
`;
|
|
115
115
|
}
|
|
116
116
|
function startProxyServer(targetUrl, port, options = {}) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
res.end(body);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const options2 = {
|
|
131
|
-
hostname: target.hostname,
|
|
132
|
-
port: target.port,
|
|
133
|
-
path: req.url,
|
|
134
|
-
method: req.method,
|
|
135
|
-
headers: __spreadProps(__spreadValues({}, req.headers), {
|
|
136
|
-
host: target.host,
|
|
137
|
-
// Don't accept compressed responses so we can modify HTML
|
|
138
|
-
"accept-encoding": "identity"
|
|
139
|
-
})
|
|
140
|
-
};
|
|
141
|
-
const proxyReq = http.request(options2, (proxyRes) => {
|
|
142
|
-
var _a;
|
|
143
|
-
const rawContentType = proxyRes.headers["content-type"];
|
|
144
|
-
const contentType = Array.isArray(rawContentType) ? (_a = rawContentType[0]) != null ? _a : "" : rawContentType != null ? rawContentType : "";
|
|
145
|
-
if (contentType.includes("text/html")) {
|
|
146
|
-
const chunks = [];
|
|
147
|
-
proxyRes.on("data", (chunk) => {
|
|
148
|
-
chunks.push(chunk);
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const target = new URL(targetUrl);
|
|
119
|
+
const bridgeScript = generateBridgeScript(options);
|
|
120
|
+
const server = http.createServer((req, res) => {
|
|
121
|
+
if (req.url === "/__opencode_bridge__.js") {
|
|
122
|
+
const body = bridgeScript;
|
|
123
|
+
res.writeHead(200, {
|
|
124
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
125
|
+
"cache-control": "no-store",
|
|
126
|
+
"content-length": Buffer.byteLength(body)
|
|
149
127
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (body.match(/<\/head>/i)) {
|
|
153
|
-
body = body.replace(
|
|
154
|
-
/<\/head>/i,
|
|
155
|
-
'<script src="/__opencode_bridge__.js"></script></head>'
|
|
156
|
-
);
|
|
157
|
-
} else if (body.match(/<\/body>/i)) {
|
|
158
|
-
body = body.replace(
|
|
159
|
-
/<\/body>/i,
|
|
160
|
-
'<script src="/__opencode_bridge__.js"></script></body>'
|
|
161
|
-
);
|
|
162
|
-
} else {
|
|
163
|
-
body += '<script src="/__opencode_bridge__.js"></script>';
|
|
164
|
-
}
|
|
165
|
-
const headers = {};
|
|
166
|
-
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
167
|
-
if (value !== void 0 && key !== "content-encoding" && key !== "transfer-encoding" && key !== "content-length") {
|
|
168
|
-
headers[key] = value;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
headers["content-length"] = Buffer.byteLength(body);
|
|
172
|
-
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
173
|
-
res.end(body);
|
|
174
|
-
});
|
|
175
|
-
} else {
|
|
176
|
-
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
177
|
-
proxyRes.pipe(res);
|
|
128
|
+
res.end(body);
|
|
129
|
+
return;
|
|
178
130
|
}
|
|
131
|
+
const requestOptions = {
|
|
132
|
+
hostname: target.hostname,
|
|
133
|
+
port: target.port,
|
|
134
|
+
path: req.url,
|
|
135
|
+
method: req.method,
|
|
136
|
+
headers: __spreadProps(__spreadValues({}, req.headers), {
|
|
137
|
+
host: target.host,
|
|
138
|
+
"accept-encoding": "identity"
|
|
139
|
+
})
|
|
140
|
+
};
|
|
141
|
+
const proxyReq = http.request(requestOptions, (proxyRes) => {
|
|
142
|
+
var _a;
|
|
143
|
+
const rawContentType = proxyRes.headers["content-type"];
|
|
144
|
+
const contentType = Array.isArray(rawContentType) ? (_a = rawContentType[0]) != null ? _a : "" : rawContentType != null ? rawContentType : "";
|
|
145
|
+
if (contentType.includes("text/html")) {
|
|
146
|
+
const chunks = [];
|
|
147
|
+
proxyRes.on("data", (chunk) => {
|
|
148
|
+
chunks.push(chunk);
|
|
149
|
+
});
|
|
150
|
+
proxyRes.on("end", () => {
|
|
151
|
+
let body = Buffer.concat(chunks).toString("utf-8");
|
|
152
|
+
if (body.match(/<\/head>/i)) {
|
|
153
|
+
body = body.replace(
|
|
154
|
+
/<\/head>/i,
|
|
155
|
+
'<script src="/__opencode_bridge__.js"></script></head>'
|
|
156
|
+
);
|
|
157
|
+
} else if (body.match(/<\/body>/i)) {
|
|
158
|
+
body = body.replace(
|
|
159
|
+
/<\/body>/i,
|
|
160
|
+
'<script src="/__opencode_bridge__.js"></script></body>'
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
body += '<script src="/__opencode_bridge__.js"></script>';
|
|
164
|
+
}
|
|
165
|
+
const headers = {};
|
|
166
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
167
|
+
if (value !== void 0 && key !== "content-encoding" && key !== "transfer-encoding" && key !== "content-length") {
|
|
168
|
+
headers[key] = value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
headers["content-length"] = Buffer.byteLength(body);
|
|
172
|
+
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
173
|
+
res.end(body);
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
177
|
+
proxyRes.pipe(res);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
proxyReq.on("error", (err) => {
|
|
181
|
+
log.error("Proxy error", { error: err.message, url: req.url });
|
|
182
|
+
res.writeHead(502);
|
|
183
|
+
res.end("Proxy error");
|
|
184
|
+
});
|
|
185
|
+
req.pipe(proxyReq);
|
|
179
186
|
});
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
server.on("error", (err) => {
|
|
188
|
+
reject(err);
|
|
189
|
+
});
|
|
190
|
+
server.listen(port, () => {
|
|
191
|
+
const address = server.address();
|
|
192
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
193
|
+
log.info(`Proxy server started on port ${actualPort} -> ${targetUrl}`);
|
|
194
|
+
resolve({ server, actualPort });
|
|
184
195
|
});
|
|
185
|
-
req.pipe(proxyReq);
|
|
186
|
-
});
|
|
187
|
-
server.listen(port, () => {
|
|
188
|
-
log.info(`Proxy server started on port ${port} -> ${targetUrl}`);
|
|
189
196
|
});
|
|
190
|
-
return server;
|
|
191
197
|
}
|
|
192
198
|
export {
|
|
193
199
|
startProxyServer
|
package/es/core/service.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResultPromise } from "execa";
|
|
2
2
|
import type http from "http";
|
|
3
|
-
import type { OpenCodeOptions } from "@vite-plugin-opencode-assistant/shared";
|
|
3
|
+
import type { OpenCodeOptions, ServiceStartupTask } from "@vite-plugin-opencode-assistant/shared";
|
|
4
4
|
import type { OpenCodeAPI } from "./api.js";
|
|
5
5
|
export declare class OpenCodeService {
|
|
6
6
|
private config;
|
|
@@ -15,7 +15,14 @@ export declare class OpenCodeService {
|
|
|
15
15
|
private startPromise;
|
|
16
16
|
sessionUrl: string | null;
|
|
17
17
|
private proxyServer;
|
|
18
|
+
chromeMcpWarmupFailed: boolean;
|
|
19
|
+
currentTask: {
|
|
20
|
+
task: ServiceStartupTask;
|
|
21
|
+
data?: Record<string, unknown>;
|
|
22
|
+
} | null;
|
|
18
23
|
constructor(config: Required<OpenCodeOptions>, api: OpenCodeAPI, sseClients: Set<http.ServerResponse>, onPortAllocated: (port: number) => void, onProxyPortAllocated: (port: number) => void);
|
|
24
|
+
private sendTaskUpdate;
|
|
19
25
|
start(corsOrigins?: string[], contextApiUrl?: string, viteOrigin?: string): Promise<void>;
|
|
26
|
+
retryWarmupChromeMcp(viteOrigin?: string): Promise<boolean>;
|
|
20
27
|
stop(): Promise<void>;
|
|
21
28
|
}
|
package/es/core/service.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
2
5
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
3
17
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
18
|
var __async = (__this, __arguments, generator) => {
|
|
5
19
|
return new Promise((resolve, reject) => {
|
|
@@ -49,10 +63,24 @@ class OpenCodeService {
|
|
|
49
63
|
__publicField(this, "startPromise", null);
|
|
50
64
|
__publicField(this, "sessionUrl", null);
|
|
51
65
|
__publicField(this, "proxyServer", null);
|
|
66
|
+
__publicField(this, "chromeMcpWarmupFailed", false);
|
|
67
|
+
__publicField(this, "currentTask", null);
|
|
52
68
|
var _a;
|
|
53
69
|
this.actualWebPort = config.webPort;
|
|
54
70
|
this.actualProxyPort = (_a = config.proxyPort) != null ? _a : DEFAULT_PROXY_PORT;
|
|
55
71
|
}
|
|
72
|
+
sendTaskUpdate(task, data) {
|
|
73
|
+
this.currentTask = __spreadValues({ task }, data);
|
|
74
|
+
this.sseClients.forEach((client) => {
|
|
75
|
+
try {
|
|
76
|
+
client.write(`data: ${JSON.stringify(__spreadValues({ type: "TASK_UPDATE", task }, data))}
|
|
77
|
+
|
|
78
|
+
`);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
log.debug("Failed to send TASK_UPDATE event", { error: e });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
56
84
|
start(corsOrigins, contextApiUrl, viteOrigin) {
|
|
57
85
|
return __async(this, null, function* () {
|
|
58
86
|
if (this.isStarted && this.webProcess) {
|
|
@@ -64,7 +92,7 @@ class OpenCodeService {
|
|
|
64
92
|
return this.startPromise;
|
|
65
93
|
}
|
|
66
94
|
this.startPromise = (() => __async(this, null, function* () {
|
|
67
|
-
var _a, _b, _c;
|
|
95
|
+
var _a, _b, _c, _d, _e;
|
|
68
96
|
const timer = log.timer("startServices", {
|
|
69
97
|
corsOrigins,
|
|
70
98
|
contextApiUrl,
|
|
@@ -75,6 +103,7 @@ class OpenCodeService {
|
|
|
75
103
|
if (orphanCount > 0) {
|
|
76
104
|
log.debug(`Killed ${orphanCount} orphan OpenCode process(es)`);
|
|
77
105
|
}
|
|
106
|
+
this.sendTaskUpdate("checking_opencode");
|
|
78
107
|
if (!(yield checkOpenCodeInstalled())) {
|
|
79
108
|
log.error(`OpenCode is not installed!
|
|
80
109
|
|
|
@@ -94,10 +123,13 @@ Please install OpenCode first:
|
|
|
94
123
|
mise use -g opencode # Any OS
|
|
95
124
|
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
|
|
96
125
|
`);
|
|
126
|
+
this.sendTaskUpdate("opencode_not_installed");
|
|
127
|
+
this.startPromise = null;
|
|
97
128
|
timer.end("\u274C OpenCode not installed");
|
|
98
129
|
return;
|
|
99
130
|
}
|
|
100
131
|
timer.checkpoint("OpenCode installation verified");
|
|
132
|
+
this.sendTaskUpdate("allocating_port");
|
|
101
133
|
this.actualWebPort = yield findAvailablePort(this.config.webPort, this.config.hostname);
|
|
102
134
|
this.onPortAllocated(this.actualWebPort);
|
|
103
135
|
if (this.actualWebPort !== this.config.webPort) {
|
|
@@ -106,8 +138,10 @@ Please install OpenCode first:
|
|
|
106
138
|
log.debug(`Using port ${this.actualWebPort}`);
|
|
107
139
|
}
|
|
108
140
|
timer.checkpoint("Port allocated");
|
|
141
|
+
this.sendTaskUpdate("preparing_runtime");
|
|
109
142
|
const configDir = prepareOpenCodeRuntime(process.cwd());
|
|
110
143
|
timer.checkpoint("Plugin setup complete");
|
|
144
|
+
this.sendTaskUpdate("starting_web");
|
|
111
145
|
log.debug("Starting OpenCode Web process...", {
|
|
112
146
|
port: this.actualWebPort,
|
|
113
147
|
hostname: this.config.hostname,
|
|
@@ -125,53 +159,119 @@ Please install OpenCode first:
|
|
|
125
159
|
timer.checkpoint("Web process started");
|
|
126
160
|
const webUrl = `http://${this.config.hostname}:${this.actualWebPort}`;
|
|
127
161
|
log.info(`Waiting for OpenCode Web to become ready at ${webUrl}...`);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
(_a = this.
|
|
132
|
-
|
|
133
|
-
|
|
162
|
+
this.sendTaskUpdate("waiting_web_ready");
|
|
163
|
+
try {
|
|
164
|
+
yield waitForServer(webUrl, SERVER_START_TIMEOUT, this.webProcess);
|
|
165
|
+
if (((_a = this.webProcess) == null ? void 0 : _a.exitCode) !== null && ((_b = this.webProcess) == null ? void 0 : _b.exitCode) !== void 0) {
|
|
166
|
+
throw new Error(`OpenCode process exited with code ${this.webProcess.exitCode}`);
|
|
167
|
+
}
|
|
168
|
+
log.info(`OpenCode Web started at ${webUrl}`);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
log.error("OpenCode Web failed to start", { error: e });
|
|
171
|
+
this.sendTaskUpdate("web_start_timeout");
|
|
172
|
+
this.startPromise = null;
|
|
173
|
+
timer.end("\u274C Web start timeout");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.sendTaskUpdate("starting_proxy");
|
|
177
|
+
let proxyStartPort = (_c = this.config.proxyPort) != null ? _c : DEFAULT_PROXY_PORT;
|
|
178
|
+
if (proxyStartPort === this.actualWebPort) {
|
|
179
|
+
proxyStartPort = this.actualWebPort + 1;
|
|
180
|
+
log.debug(`Proxy start port conflicts with web port, using ${proxyStartPort} instead`);
|
|
181
|
+
}
|
|
182
|
+
this.actualProxyPort = yield findAvailablePort(proxyStartPort, this.config.hostname);
|
|
134
183
|
this.onProxyPortAllocated(this.actualProxyPort);
|
|
135
|
-
if (this.actualProxyPort !== ((
|
|
184
|
+
if (this.actualProxyPort !== ((_d = this.config.proxyPort) != null ? _d : DEFAULT_PROXY_PORT)) {
|
|
136
185
|
log.info(
|
|
137
|
-
`Proxy port ${(
|
|
186
|
+
`Proxy port ${(_e = this.config.proxyPort) != null ? _e : DEFAULT_PROXY_PORT} is in use, using ${this.actualProxyPort} instead`
|
|
138
187
|
);
|
|
139
188
|
} else {
|
|
140
189
|
log.debug(`Using proxy port ${this.actualProxyPort}`);
|
|
141
190
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
191
|
+
try {
|
|
192
|
+
const result = yield startProxyServer(webUrl, this.actualProxyPort, {
|
|
193
|
+
theme: this.config.theme,
|
|
194
|
+
language: this.config.language,
|
|
195
|
+
settings: this.config.settings
|
|
196
|
+
});
|
|
197
|
+
this.proxyServer = result.server;
|
|
198
|
+
if (result.actualPort !== this.actualProxyPort) {
|
|
199
|
+
log.info(
|
|
200
|
+
`Proxy port ${this.actualProxyPort} was taken, using ${result.actualPort} instead`
|
|
201
|
+
);
|
|
202
|
+
this.actualProxyPort = result.actualPort;
|
|
203
|
+
this.onProxyPortAllocated(this.actualProxyPort);
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
const nodeErr = err;
|
|
207
|
+
if (nodeErr.code === "EADDRINUSE") {
|
|
208
|
+
log.warn(`Proxy port ${this.actualProxyPort} became unavailable, trying next port...`);
|
|
209
|
+
const nextPort = yield findAvailablePort(this.actualProxyPort + 1, this.config.hostname);
|
|
210
|
+
const result = yield startProxyServer(webUrl, nextPort, {
|
|
211
|
+
theme: this.config.theme,
|
|
212
|
+
language: this.config.language,
|
|
213
|
+
settings: this.config.settings
|
|
214
|
+
});
|
|
215
|
+
this.proxyServer = result.server;
|
|
216
|
+
this.actualProxyPort = result.actualPort;
|
|
217
|
+
this.onProxyPortAllocated(this.actualProxyPort);
|
|
218
|
+
log.info(`Proxy server started on fallback port ${this.actualProxyPort}`);
|
|
219
|
+
} else {
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
147
223
|
timer.checkpoint("Proxy server started");
|
|
148
|
-
|
|
149
|
-
|
|
224
|
+
this.sendTaskUpdate("warming_up_chrome");
|
|
225
|
+
let warmupFailed = false;
|
|
226
|
+
try {
|
|
227
|
+
yield this.api.warmupChromeMcp(viteOrigin);
|
|
228
|
+
timer.checkpoint("Chrome MCP warmup complete");
|
|
229
|
+
} catch (e) {
|
|
230
|
+
log.warn("Chrome MCP warmup failed", { error: e });
|
|
231
|
+
this.chromeMcpWarmupFailed = true;
|
|
232
|
+
warmupFailed = true;
|
|
233
|
+
}
|
|
234
|
+
this.sendTaskUpdate("creating_session");
|
|
235
|
+
let sessionFailed = false;
|
|
150
236
|
try {
|
|
151
237
|
this.sessionUrl = yield this.api.getOrCreateSession();
|
|
152
238
|
timer.checkpoint("Session created");
|
|
153
239
|
log.debug(`Session URL: ${this.sessionUrl}`);
|
|
154
|
-
this.sseClients.forEach((client) => {
|
|
155
|
-
try {
|
|
156
|
-
client.write(
|
|
157
|
-
`data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl: this.sessionUrl })}
|
|
158
|
-
|
|
159
|
-
`
|
|
160
|
-
);
|
|
161
|
-
} catch (e) {
|
|
162
|
-
log.debug("Failed to send SESSION_READY event", { error: e });
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
240
|
} catch (e) {
|
|
166
241
|
log.warn("Failed to get/create session", { error: e });
|
|
242
|
+
sessionFailed = true;
|
|
243
|
+
}
|
|
244
|
+
if (sessionFailed) {
|
|
245
|
+
this.sendTaskUpdate("session_creation_failed");
|
|
246
|
+
this.isStarted = false;
|
|
247
|
+
this.startPromise = null;
|
|
248
|
+
} else if (warmupFailed) {
|
|
249
|
+
this.sendTaskUpdate("chrome_mcp_failed", { sessionUrl: this.sessionUrl });
|
|
250
|
+
this.isStarted = true;
|
|
251
|
+
} else {
|
|
252
|
+
this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
|
|
253
|
+
}
|
|
254
|
+
if (!sessionFailed) {
|
|
255
|
+
this.isStarted = true;
|
|
256
|
+
} else {
|
|
257
|
+
this.sessionUrl = null;
|
|
167
258
|
}
|
|
168
|
-
this.isStarted = true;
|
|
169
259
|
log.debug(`OpenCode services started successfully: ${this.sessionUrl || webUrl}`);
|
|
170
260
|
timer.end("\u2713 Services started successfully");
|
|
171
261
|
}))();
|
|
172
262
|
return this.startPromise;
|
|
173
263
|
});
|
|
174
264
|
}
|
|
265
|
+
retryWarmupChromeMcp(viteOrigin) {
|
|
266
|
+
return __async(this, null, function* () {
|
|
267
|
+
const success = yield this.api.retryWarmupChromeMcp(viteOrigin);
|
|
268
|
+
if (success) {
|
|
269
|
+
this.chromeMcpWarmupFailed = false;
|
|
270
|
+
this.sendTaskUpdate("ready", { sessionUrl: this.sessionUrl });
|
|
271
|
+
}
|
|
272
|
+
return success;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
175
275
|
stop() {
|
|
176
276
|
return __async(this, null, function* () {
|
|
177
277
|
const timer = log.timer("stopServices");
|
package/es/endpoints/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { setupContextEndpoint } from "./context.js";
|
|
|
3
3
|
import { setupStartEndpoint } from "./start.js";
|
|
4
4
|
import { setupSseEndpoint } from "./sse.js";
|
|
5
5
|
import { setupSessionsEndpoint } from "./sessions.js";
|
|
6
|
+
import { setupWarmupEndpoint } from "./warmup.js";
|
|
6
7
|
export * from "./types.js";
|
|
7
8
|
function setupMiddlewares(server, ctx) {
|
|
8
9
|
setupWidgetEndpoints(server, ctx);
|
|
@@ -10,6 +11,7 @@ function setupMiddlewares(server, ctx) {
|
|
|
10
11
|
setupStartEndpoint(server, ctx);
|
|
11
12
|
setupSseEndpoint(server, ctx);
|
|
12
13
|
setupSessionsEndpoint(server, ctx);
|
|
14
|
+
setupWarmupEndpoint(server, ctx);
|
|
13
15
|
}
|
|
14
16
|
export {
|
|
15
17
|
setupMiddlewares
|
package/es/endpoints/sse.js
CHANGED
|
@@ -35,13 +35,22 @@ function setupSseEndpoint(server, ctx) {
|
|
|
35
35
|
res.write(`data: ${JSON.stringify({ type: "CONNECTED" })}
|
|
36
36
|
|
|
37
37
|
`);
|
|
38
|
+
const statusPayload = { type: "STATUS_SYNC" };
|
|
39
|
+
if (ctx.isServiceStarted !== void 0) {
|
|
40
|
+
statusPayload.isStarted = ctx.isServiceStarted;
|
|
41
|
+
}
|
|
42
|
+
if (ctx.currentTask) {
|
|
43
|
+
statusPayload.task = ctx.currentTask.task;
|
|
44
|
+
if (ctx.currentTask.data) {
|
|
45
|
+
Object.assign(statusPayload, ctx.currentTask.data);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
38
48
|
if (ctx.sessionUrl) {
|
|
39
|
-
|
|
40
|
-
`data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl: ctx.sessionUrl })}
|
|
41
|
-
|
|
42
|
-
`
|
|
43
|
-
);
|
|
49
|
+
statusPayload.sessionUrl = ctx.sessionUrl;
|
|
44
50
|
}
|
|
51
|
+
res.write(`data: ${JSON.stringify(statusPayload)}
|
|
52
|
+
|
|
53
|
+
`);
|
|
45
54
|
req.on("close", () => {
|
|
46
55
|
ctx.sseClients.delete(res);
|
|
47
56
|
log.debug("SSE client disconnected", {
|
package/es/endpoints/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PageContext, SessionInfo } from "@vite-plugin-opencode-assistant/shared";
|
|
1
|
+
import type { PageContext, SessionInfo, ServiceStartupTask } from "@vite-plugin-opencode-assistant/shared";
|
|
2
2
|
import type http from "http";
|
|
3
3
|
export interface EndpointContext {
|
|
4
4
|
get sessionUrl(): string | null;
|
|
@@ -6,9 +6,15 @@ export interface EndpointContext {
|
|
|
6
6
|
get sseClients(): Set<http.ServerResponse>;
|
|
7
7
|
get pageContext(): PageContext;
|
|
8
8
|
set pageContext(ctx: PageContext);
|
|
9
|
+
get isServiceStarted(): boolean;
|
|
10
|
+
get currentTask(): {
|
|
11
|
+
task: ServiceStartupTask;
|
|
12
|
+
data?: Record<string, unknown>;
|
|
13
|
+
} | null;
|
|
9
14
|
getSessions: () => Promise<SessionInfo[]>;
|
|
10
15
|
createSession: () => Promise<SessionInfo>;
|
|
11
16
|
deleteSession: (id: string) => Promise<void>;
|
|
12
17
|
resolveWidgetPath: () => string;
|
|
13
18
|
resolveWidgetStylePath: () => string;
|
|
19
|
+
retryWarmupChromeMcp: () => Promise<boolean>;
|
|
14
20
|
}
|