vite-plugin-opencode-assistant 1.0.3 → 1.0.4
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/README.md +189 -184
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +19 -18
- package/dist/constants.js.map +1 -1
- package/dist/opencode/plugins/page-context.js +41 -14
- package/dist/opencode/plugins/page-context.js.map +1 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.js.map +1 -1
- package/dist/vite/client.js +580 -400
- package/dist/vite/client.js.map +1 -1
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +348 -171
- package/dist/vite/index.js.map +1 -1
- package/package.json +4 -1
package/dist/vite/index.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import fs from
|
|
3
|
-
import http from
|
|
4
|
-
import { fileURLToPath } from
|
|
5
|
-
import Inspector from
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import http from "http";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import Inspector from "unplugin-vue-inspector/vite";
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = path.dirname(__filename);
|
|
8
|
-
import { startOpenCodeWeb } from
|
|
9
|
-
import { injectWidget } from
|
|
10
|
-
import { checkOpenCodeInstalled, findAvailablePort, waitForServer, killOrphanOpenCodeProcesses } from
|
|
11
|
-
import { DEFAULT_CONFIG, DEFAULT_RETRIES, RETRY_DELAY, WIDGET_SCRIPT_PATH, CONTEXT_API_PATH, START_API_PATH, SESSIONS_API_PATH, SSE_EVENTS_PATH, SERVER_START_TIMEOUT, } from
|
|
12
|
-
import { setVerbose, PerformanceTimer, RequestContext, createLogger, } from
|
|
8
|
+
import { startOpenCodeWeb } from "../opencode/web.js";
|
|
9
|
+
import { injectWidget } from "./injector.js";
|
|
10
|
+
import { checkOpenCodeInstalled, findAvailablePort, waitForServer, killOrphanOpenCodeProcesses, } from "./utils.js";
|
|
11
|
+
import { DEFAULT_CONFIG, DEFAULT_RETRIES, RETRY_DELAY, WIDGET_SCRIPT_PATH, CONTEXT_API_PATH, START_API_PATH, SESSIONS_API_PATH, SSE_EVENTS_PATH, SERVER_START_TIMEOUT, } from "../constants.js";
|
|
12
|
+
import { setVerbose, PerformanceTimer, RequestContext, createLogger, } from "../logger.js";
|
|
13
13
|
export default function opencodePlugin(options = {}) {
|
|
14
14
|
const plugins = [];
|
|
15
15
|
plugins.push(...Inspector({
|
|
16
16
|
enabled: false,
|
|
17
|
-
toggleButtonVisibility:
|
|
17
|
+
toggleButtonVisibility: "never",
|
|
18
18
|
toggleComboKey: false,
|
|
19
19
|
}));
|
|
20
20
|
plugins.push(createOpenCodePlugin(options));
|
|
@@ -26,39 +26,39 @@ function createOpenCodePlugin(options = {}) {
|
|
|
26
26
|
let actualWebPort = DEFAULT_CONFIG.webPort;
|
|
27
27
|
let isStarted = false;
|
|
28
28
|
let startPromise = null;
|
|
29
|
-
let pageContext = { url:
|
|
29
|
+
let pageContext = { url: "", title: "" };
|
|
30
30
|
const sseClients = new Set();
|
|
31
31
|
const config = { ...DEFAULT_CONFIG, ...options };
|
|
32
32
|
setVerbose(config.verbose);
|
|
33
|
-
const log = createLogger(
|
|
33
|
+
const log = createLogger("Plugin");
|
|
34
34
|
function base64Encode(str) {
|
|
35
|
-
return Buffer.from(str).toString(
|
|
35
|
+
return Buffer.from(str).toString("base64");
|
|
36
36
|
}
|
|
37
37
|
function sleep(ms) {
|
|
38
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
38
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
39
|
}
|
|
40
40
|
function createHttpRequest(options, body) {
|
|
41
|
-
const timer = new PerformanceTimer(
|
|
42
|
-
operation: `${options.method ||
|
|
41
|
+
const timer = new PerformanceTimer("HTTP Request", {
|
|
42
|
+
operation: `${options.method || "GET"} ${options.path}`,
|
|
43
43
|
});
|
|
44
44
|
return new Promise((resolve, reject) => {
|
|
45
45
|
const req = http.request(options, (res) => {
|
|
46
|
-
let data =
|
|
47
|
-
res.on(
|
|
48
|
-
res.on(
|
|
46
|
+
let data = "";
|
|
47
|
+
res.on("data", (chunk) => (data += chunk));
|
|
48
|
+
res.on("end", () => {
|
|
49
49
|
try {
|
|
50
50
|
const result = JSON.parse(data);
|
|
51
51
|
timer.end(`✓ Status: ${res.statusCode}`);
|
|
52
52
|
resolve(result);
|
|
53
53
|
}
|
|
54
54
|
catch (e) {
|
|
55
|
-
timer.end(
|
|
55
|
+
timer.end("❌ JSON parse error");
|
|
56
56
|
reject(new Error(`JSON parse error: ${data.substring(0, 100)}`));
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
|
-
req.on(
|
|
61
|
-
timer.end(
|
|
60
|
+
req.on("error", (e) => {
|
|
61
|
+
timer.end("❌ Request failed");
|
|
62
62
|
reject(e);
|
|
63
63
|
});
|
|
64
64
|
if (body)
|
|
@@ -67,158 +67,301 @@ function createOpenCodePlugin(options = {}) {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
async function getSessions(retries = DEFAULT_RETRIES) {
|
|
70
|
-
const timer = log.timer(
|
|
70
|
+
const timer = log.timer("getSessions", { retries });
|
|
71
71
|
let lastError = null;
|
|
72
72
|
for (let i = 0; i < retries; i++) {
|
|
73
73
|
try {
|
|
74
|
-
log.debug(`Attempt ${i + 1}/${retries}`, { operation:
|
|
74
|
+
log.debug(`Attempt ${i + 1}/${retries}`, { operation: "getSessions" });
|
|
75
75
|
const sessions = await createHttpRequest({
|
|
76
76
|
hostname: config.hostname,
|
|
77
77
|
port: actualWebPort,
|
|
78
|
-
path:
|
|
78
|
+
path: "/session",
|
|
79
79
|
});
|
|
80
80
|
timer.end(`Found ${sessions.length} sessions`);
|
|
81
81
|
return sessions;
|
|
82
82
|
}
|
|
83
83
|
catch (e) {
|
|
84
84
|
lastError = e instanceof Error ? e : new Error(String(e));
|
|
85
|
-
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
85
|
+
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
86
|
+
operation: "getSessions",
|
|
87
|
+
});
|
|
86
88
|
if (i < retries - 1) {
|
|
87
|
-
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
89
|
+
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
90
|
+
operation: "getSessions",
|
|
91
|
+
});
|
|
88
92
|
await sleep(RETRY_DELAY);
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
|
-
timer.end(
|
|
96
|
+
timer.end("❌ All retries exhausted");
|
|
93
97
|
throw lastError;
|
|
94
98
|
}
|
|
95
|
-
async function createSession(retries = DEFAULT_RETRIES) {
|
|
96
|
-
const timer = log.timer(
|
|
99
|
+
async function createSession(retries = DEFAULT_RETRIES, title) {
|
|
100
|
+
const timer = log.timer("createSession", { retries, title });
|
|
97
101
|
let lastError = null;
|
|
98
102
|
for (let i = 0; i < retries; i++) {
|
|
99
103
|
try {
|
|
100
|
-
log.debug(`Attempt ${i + 1}/${retries}`, {
|
|
104
|
+
log.debug(`Attempt ${i + 1}/${retries}`, {
|
|
105
|
+
operation: "createSession",
|
|
106
|
+
title,
|
|
107
|
+
});
|
|
108
|
+
const requestBody = title ? JSON.stringify({ title }) : undefined;
|
|
101
109
|
const session = await createHttpRequest({
|
|
102
110
|
hostname: config.hostname,
|
|
103
111
|
port: actualWebPort,
|
|
104
|
-
path:
|
|
105
|
-
method:
|
|
106
|
-
|
|
112
|
+
path: "/session",
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: requestBody
|
|
115
|
+
? { "Content-Type": "application/json" }
|
|
116
|
+
: undefined,
|
|
117
|
+
}, requestBody);
|
|
107
118
|
timer.end(`Created session: ${session.id}`);
|
|
108
119
|
return session;
|
|
109
120
|
}
|
|
110
121
|
catch (e) {
|
|
111
122
|
lastError = e instanceof Error ? e : new Error(String(e));
|
|
112
|
-
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
123
|
+
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
124
|
+
operation: "createSession",
|
|
125
|
+
});
|
|
113
126
|
if (i < retries - 1) {
|
|
114
|
-
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
127
|
+
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
128
|
+
operation: "createSession",
|
|
129
|
+
});
|
|
115
130
|
await sleep(RETRY_DELAY);
|
|
116
131
|
}
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
|
-
timer.end(
|
|
134
|
+
timer.end("❌ All retries exhausted");
|
|
120
135
|
throw lastError;
|
|
121
136
|
}
|
|
122
137
|
async function deleteSession(sessionId, retries = DEFAULT_RETRIES) {
|
|
123
|
-
const timer = log.timer(
|
|
138
|
+
const timer = log.timer("deleteSession", { sessionId, retries });
|
|
124
139
|
let lastError = null;
|
|
125
140
|
for (let i = 0; i < retries; i++) {
|
|
126
141
|
try {
|
|
127
|
-
log.debug(`Attempt ${i + 1}/${retries}`, {
|
|
142
|
+
log.debug(`Attempt ${i + 1}/${retries}`, {
|
|
143
|
+
operation: "deleteSession",
|
|
144
|
+
sessionId,
|
|
145
|
+
});
|
|
128
146
|
await createHttpRequest({
|
|
129
147
|
hostname: config.hostname,
|
|
130
148
|
port: actualWebPort,
|
|
131
149
|
path: `/session/${sessionId}`,
|
|
132
|
-
method:
|
|
150
|
+
method: "DELETE",
|
|
133
151
|
});
|
|
134
152
|
timer.end(`Deleted session: ${sessionId}`);
|
|
135
153
|
return;
|
|
136
154
|
}
|
|
137
155
|
catch (e) {
|
|
138
156
|
lastError = e instanceof Error ? e : new Error(String(e));
|
|
139
|
-
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
157
|
+
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
158
|
+
operation: "deleteSession",
|
|
159
|
+
sessionId,
|
|
160
|
+
});
|
|
140
161
|
if (i < retries - 1) {
|
|
141
|
-
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
162
|
+
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
163
|
+
operation: "deleteSession",
|
|
164
|
+
sessionId,
|
|
165
|
+
});
|
|
142
166
|
await sleep(RETRY_DELAY);
|
|
143
167
|
}
|
|
144
168
|
}
|
|
145
169
|
}
|
|
146
|
-
timer.end(
|
|
170
|
+
timer.end("❌ All retries exhausted");
|
|
147
171
|
throw lastError;
|
|
148
172
|
}
|
|
173
|
+
async function getToolIds(retries = DEFAULT_RETRIES) {
|
|
174
|
+
const timer = log.timer("getToolIds", { retries });
|
|
175
|
+
let lastError = null;
|
|
176
|
+
for (let i = 0; i < retries; i++) {
|
|
177
|
+
try {
|
|
178
|
+
log.debug(`Attempt ${i + 1}/${retries}`, {
|
|
179
|
+
operation: "getToolIds",
|
|
180
|
+
});
|
|
181
|
+
const toolIds = await createHttpRequest({
|
|
182
|
+
hostname: config.hostname,
|
|
183
|
+
port: actualWebPort,
|
|
184
|
+
path: "/experimental/tool/ids",
|
|
185
|
+
});
|
|
186
|
+
timer.end(`Found ${toolIds.length} tools`);
|
|
187
|
+
return toolIds;
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
191
|
+
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
192
|
+
operation: "getToolIds",
|
|
193
|
+
});
|
|
194
|
+
if (i < retries - 1) {
|
|
195
|
+
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
196
|
+
operation: "getToolIds",
|
|
197
|
+
});
|
|
198
|
+
await sleep(RETRY_DELAY);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
timer.end("❌ All retries exhausted");
|
|
203
|
+
throw lastError;
|
|
204
|
+
}
|
|
205
|
+
async function warmupChromeMcp(viteOrigin) {
|
|
206
|
+
if (!config.warmupChromeMcp)
|
|
207
|
+
return;
|
|
208
|
+
const timer = log.timer("warmupChromeMcp", { viteOrigin });
|
|
209
|
+
let warmupSessionId = null;
|
|
210
|
+
try {
|
|
211
|
+
const warmupSession = await createSession(DEFAULT_RETRIES, "__chrome_mcp_warmup__");
|
|
212
|
+
warmupSessionId = warmupSession.id;
|
|
213
|
+
let chromeToolIds;
|
|
214
|
+
try {
|
|
215
|
+
const toolIds = await getToolIds();
|
|
216
|
+
chromeToolIds = toolIds.filter((toolId) => /chrome[-_]?devtools/i.test(toolId));
|
|
217
|
+
log.debug("Resolved Chrome MCP tool ids", {
|
|
218
|
+
chromeToolIds,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
log.debug("Failed to resolve Chrome MCP tool ids", { error: e });
|
|
223
|
+
}
|
|
224
|
+
const prompt = [
|
|
225
|
+
"Call the browser tool list_pages immediately to establish the Chrome DevTools MCP connection.",
|
|
226
|
+
viteOrigin
|
|
227
|
+
? `If there are no pages, call new_page with ${viteOrigin}.`
|
|
228
|
+
: "If there are no pages, call new_page with about:blank.",
|
|
229
|
+
"Do not read or modify project files.",
|
|
230
|
+
"Do not use any non-browser tools.",
|
|
231
|
+
"After the tool call is complete, reply with exactly: ready",
|
|
232
|
+
].join(" ");
|
|
233
|
+
await createHttpRequest({
|
|
234
|
+
hostname: config.hostname,
|
|
235
|
+
port: actualWebPort,
|
|
236
|
+
path: `/session/${warmupSessionId}/message`,
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
}, JSON.stringify({
|
|
240
|
+
system: "You are warming up Chrome DevTools MCP during startup. You must use the available browser tools immediately before replying.",
|
|
241
|
+
tools: chromeToolIds?.length ? chromeToolIds : undefined,
|
|
242
|
+
parts: [{ type: "text", text: prompt }],
|
|
243
|
+
}));
|
|
244
|
+
timer.end("Chrome MCP warmed up");
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
log.warn("Failed to warm up Chrome MCP", { error: e });
|
|
248
|
+
timer.end("Chrome MCP warmup skipped");
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
if (warmupSessionId) {
|
|
252
|
+
try {
|
|
253
|
+
await deleteSession(warmupSessionId);
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
log.debug("Failed to delete warmup session", {
|
|
257
|
+
error: e,
|
|
258
|
+
warmupSessionId,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
149
264
|
async function getOrCreateSession() {
|
|
150
|
-
const timer = log.timer(
|
|
265
|
+
const timer = log.timer("getOrCreateSession");
|
|
151
266
|
const projectDir = process.cwd();
|
|
152
|
-
log.debug(
|
|
267
|
+
log.debug("Getting sessions...", { projectDir });
|
|
153
268
|
const sessions = await getSessions();
|
|
154
269
|
log.debug(`Found ${sessions.length} sessions`, {
|
|
155
|
-
sessions: sessions.map(s => ({ id: s.id, directory: s.directory }))
|
|
270
|
+
sessions: sessions.map((s) => ({ id: s.id, directory: s.directory })),
|
|
156
271
|
});
|
|
157
|
-
const matchingSession = sessions.find(s => s.directory === projectDir);
|
|
272
|
+
const matchingSession = sessions.find((s) => s.directory === projectDir);
|
|
158
273
|
if (matchingSession) {
|
|
159
274
|
const url = `http://${config.hostname}:${actualWebPort}/${base64Encode(projectDir)}/session/${matchingSession.id}`;
|
|
160
275
|
timer.end(`Using existing session: ${matchingSession.id}`);
|
|
161
276
|
return url;
|
|
162
277
|
}
|
|
163
|
-
log.debug(
|
|
278
|
+
log.debug("Creating new session...", { projectDir });
|
|
164
279
|
const newSession = await createSession();
|
|
165
280
|
const url = `http://${config.hostname}:${actualWebPort}/${base64Encode(projectDir)}/session/${newSession.id}`;
|
|
166
281
|
timer.end(`Created new session: ${newSession.id}`);
|
|
167
282
|
return url;
|
|
168
283
|
}
|
|
169
284
|
function setupOpenCodePlugin() {
|
|
170
|
-
const timer = log.timer(
|
|
285
|
+
const timer = log.timer("setupOpenCodePlugin");
|
|
171
286
|
const projectDir = process.cwd();
|
|
172
|
-
const cacheDir = path.join(projectDir,
|
|
173
|
-
const pluginsDir = path.join(cacheDir,
|
|
174
|
-
log.debug(
|
|
287
|
+
const cacheDir = path.join(projectDir, "node_modules", ".cache", "opencode");
|
|
288
|
+
const pluginsDir = path.join(cacheDir, "plugins");
|
|
289
|
+
log.debug("Setting up plugin directory", { cacheDir, pluginsDir });
|
|
175
290
|
if (!fs.existsSync(pluginsDir)) {
|
|
176
291
|
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
177
|
-
log.debug(
|
|
292
|
+
log.debug("Created plugins directory", { pluginsDir });
|
|
178
293
|
}
|
|
179
|
-
const pluginSourcePath = path.join(__dirname,
|
|
180
|
-
const pluginTargetPath = path.join(pluginsDir,
|
|
294
|
+
const pluginSourcePath = path.join(__dirname, "..", "opencode", "plugins", "page-context.js");
|
|
295
|
+
const pluginTargetPath = path.join(pluginsDir, "page-context.js");
|
|
181
296
|
if (fs.existsSync(pluginSourcePath)) {
|
|
182
297
|
fs.copyFileSync(pluginSourcePath, pluginTargetPath);
|
|
183
|
-
log.debug(
|
|
298
|
+
log.debug("Plugin installed", {
|
|
299
|
+
source: pluginSourcePath,
|
|
300
|
+
target: pluginTargetPath,
|
|
301
|
+
});
|
|
184
302
|
}
|
|
185
303
|
else {
|
|
186
|
-
log.warn(
|
|
304
|
+
log.warn("Plugin source not found", { path: pluginSourcePath });
|
|
187
305
|
}
|
|
306
|
+
// 生成内置的 Chrome DevTools MCP 配置
|
|
307
|
+
const mcpConfig = {
|
|
308
|
+
mcp: {
|
|
309
|
+
"chrome-devtools": {
|
|
310
|
+
type: "local",
|
|
311
|
+
command: ["npx", "-y", "chrome-devtools-mcp@latest", "--autoConnect"],
|
|
312
|
+
enabled: true,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
const mcpConfigPath = path.join(cacheDir, "opencode.json");
|
|
317
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
|
|
318
|
+
log.debug("Created OpenCode MCP config", { mcpConfigPath });
|
|
188
319
|
timer.end();
|
|
189
320
|
return cacheDir;
|
|
190
321
|
}
|
|
191
|
-
async function startServices(corsOrigins, contextApiUrl) {
|
|
322
|
+
async function startServices(corsOrigins, contextApiUrl, viteOrigin) {
|
|
192
323
|
if (isStarted && webProcess) {
|
|
193
|
-
log.debug(
|
|
324
|
+
log.debug("Services already started, skipping");
|
|
194
325
|
return;
|
|
195
326
|
}
|
|
196
327
|
if (startPromise) {
|
|
197
|
-
log.debug(
|
|
328
|
+
log.debug("Waiting for existing start promise");
|
|
198
329
|
return startPromise;
|
|
199
330
|
}
|
|
200
331
|
startPromise = (async () => {
|
|
201
|
-
const timer = log.timer(
|
|
202
|
-
|
|
332
|
+
const timer = log.timer("startServices", {
|
|
333
|
+
corsOrigins,
|
|
334
|
+
contextApiUrl,
|
|
335
|
+
viteOrigin,
|
|
336
|
+
});
|
|
337
|
+
log.info("Starting OpenCode services...");
|
|
203
338
|
const orphanCount = await killOrphanOpenCodeProcesses();
|
|
204
339
|
if (orphanCount > 0) {
|
|
205
340
|
log.debug(`Killed ${orphanCount} orphan OpenCode process(es)`);
|
|
206
341
|
}
|
|
207
|
-
if (!await checkOpenCodeInstalled()) {
|
|
342
|
+
if (!(await checkOpenCodeInstalled())) {
|
|
208
343
|
log.error(`OpenCode is not installed!
|
|
209
344
|
|
|
210
345
|
Please install OpenCode first:
|
|
211
346
|
|
|
212
|
-
#
|
|
213
|
-
brew install opencode-ai/tap/opencode
|
|
214
|
-
|
|
215
|
-
# Or using the install script
|
|
347
|
+
# YOLO
|
|
216
348
|
curl -fsSL https://opencode.ai/install | bash
|
|
349
|
+
|
|
350
|
+
# Package managers
|
|
351
|
+
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
|
352
|
+
scoop install opencode # Windows
|
|
353
|
+
choco install opencode # Windows
|
|
354
|
+
brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
|
|
355
|
+
brew install opencode # macOS and Linux (official brew formula, updated less)
|
|
356
|
+
sudo pacman -S opencode # Arch Linux (Stable)
|
|
357
|
+
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
|
358
|
+
mise use -g opencode # Any OS
|
|
359
|
+
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
|
|
217
360
|
`);
|
|
218
|
-
timer.end(
|
|
361
|
+
timer.end("❌ OpenCode not installed");
|
|
219
362
|
return;
|
|
220
363
|
}
|
|
221
|
-
timer.checkpoint(
|
|
364
|
+
timer.checkpoint("OpenCode installation verified");
|
|
222
365
|
actualWebPort = await findAvailablePort(config.webPort, config.hostname);
|
|
223
366
|
if (actualWebPort !== config.webPort) {
|
|
224
367
|
log.info(`Port ${config.webPort} is in use, using ${actualWebPort} instead`);
|
|
@@ -226,111 +369,128 @@ Please install OpenCode first:
|
|
|
226
369
|
else {
|
|
227
370
|
log.debug(`Using port ${actualWebPort}`);
|
|
228
371
|
}
|
|
229
|
-
timer.checkpoint(
|
|
372
|
+
timer.checkpoint("Port allocated");
|
|
230
373
|
const configDir = setupOpenCodePlugin();
|
|
231
|
-
timer.checkpoint(
|
|
232
|
-
log.debug(
|
|
374
|
+
timer.checkpoint("Plugin setup complete");
|
|
375
|
+
log.debug("Starting OpenCode Web process...", {
|
|
233
376
|
port: actualWebPort,
|
|
234
377
|
hostname: config.hostname,
|
|
235
|
-
configDir
|
|
378
|
+
configDir,
|
|
236
379
|
});
|
|
237
380
|
webProcess = startOpenCodeWeb({
|
|
238
381
|
port: actualWebPort,
|
|
239
382
|
hostname: config.hostname,
|
|
240
|
-
serverUrl:
|
|
383
|
+
serverUrl: "",
|
|
241
384
|
cwd: process.cwd(),
|
|
242
385
|
configDir,
|
|
243
386
|
corsOrigins,
|
|
244
387
|
contextApiUrl,
|
|
245
388
|
});
|
|
246
|
-
timer.checkpoint(
|
|
247
|
-
|
|
248
|
-
log.info(`OpenCode Web
|
|
389
|
+
timer.checkpoint("Web process started");
|
|
390
|
+
const webUrl = `http://${config.hostname}:${actualWebPort}`;
|
|
391
|
+
log.info(`Waiting for OpenCode Web to become ready at ${webUrl}...`);
|
|
392
|
+
await waitForServer(webUrl, SERVER_START_TIMEOUT);
|
|
393
|
+
log.info(`OpenCode Web started at ${webUrl}`);
|
|
394
|
+
await warmupChromeMcp(viteOrigin);
|
|
395
|
+
timer.checkpoint("Chrome MCP warmup complete");
|
|
249
396
|
try {
|
|
250
397
|
sessionUrl = await getOrCreateSession();
|
|
251
|
-
timer.checkpoint(
|
|
398
|
+
timer.checkpoint("Session created");
|
|
252
399
|
log.debug(`Session URL: ${sessionUrl}`);
|
|
400
|
+
sseClients.forEach((client) => {
|
|
401
|
+
try {
|
|
402
|
+
client.write(`data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl })}\n\n`);
|
|
403
|
+
}
|
|
404
|
+
catch (e) {
|
|
405
|
+
log.debug("Failed to send SESSION_READY event", { error: e });
|
|
406
|
+
}
|
|
407
|
+
});
|
|
253
408
|
}
|
|
254
409
|
catch (e) {
|
|
255
|
-
log.warn(
|
|
410
|
+
log.warn("Failed to get/create session", { error: e });
|
|
256
411
|
}
|
|
257
412
|
isStarted = true;
|
|
258
|
-
|
|
413
|
+
log.debug(`OpenCode services started successfully: ${sessionUrl || webUrl}`);
|
|
414
|
+
timer.end("✓ Services started successfully");
|
|
259
415
|
})();
|
|
260
416
|
return startPromise;
|
|
261
417
|
}
|
|
262
418
|
async function stopServices() {
|
|
263
|
-
const timer = log.timer(
|
|
264
|
-
log.info(
|
|
419
|
+
const timer = log.timer("stopServices");
|
|
420
|
+
log.info("Stopping OpenCode services...");
|
|
265
421
|
if (webProcess) {
|
|
266
|
-
log.debug(
|
|
267
|
-
webProcess.kill(
|
|
422
|
+
log.debug("Killing web process", { pid: webProcess.pid });
|
|
423
|
+
webProcess.kill("SIGTERM");
|
|
268
424
|
webProcess = null;
|
|
269
425
|
}
|
|
270
426
|
isStarted = false;
|
|
271
427
|
startPromise = null;
|
|
272
|
-
timer.end(
|
|
428
|
+
timer.end("✓ Services stopped");
|
|
273
429
|
}
|
|
274
430
|
function handleContextRequest(req, res) {
|
|
275
|
-
const ctx = new RequestContext(req.method ||
|
|
276
|
-
res.setHeader(
|
|
277
|
-
res.setHeader(
|
|
278
|
-
res.setHeader(
|
|
279
|
-
res.setHeader(
|
|
280
|
-
if (req.method ===
|
|
431
|
+
const ctx = new RequestContext(req.method || "GET", CONTEXT_API_PATH);
|
|
432
|
+
res.setHeader("Content-Type", "application/json");
|
|
433
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
434
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
435
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
436
|
+
if (req.method === "OPTIONS") {
|
|
281
437
|
res.writeHead(200);
|
|
282
438
|
res.end();
|
|
283
439
|
ctx.end(200);
|
|
284
440
|
return;
|
|
285
441
|
}
|
|
286
|
-
if (req.method ===
|
|
442
|
+
if (req.method === "GET") {
|
|
287
443
|
res.writeHead(200);
|
|
288
444
|
res.end(JSON.stringify(pageContext));
|
|
289
445
|
ctx.end(200);
|
|
290
446
|
return;
|
|
291
447
|
}
|
|
292
|
-
if (req.method ===
|
|
448
|
+
if (req.method === "DELETE") {
|
|
293
449
|
pageContext.selectedElements = [];
|
|
294
|
-
log.debug(
|
|
450
|
+
log.debug("Selected elements cleared", { sseClients: sseClients.size });
|
|
295
451
|
let sentCount = 0;
|
|
296
|
-
sseClients.forEach(client => {
|
|
452
|
+
sseClients.forEach((client) => {
|
|
297
453
|
try {
|
|
298
|
-
client.write(`data: ${JSON.stringify({ type:
|
|
454
|
+
client.write(`data: ${JSON.stringify({ type: "CLEAR_ELEMENTS" })}\n\n`);
|
|
299
455
|
sentCount++;
|
|
300
456
|
}
|
|
301
457
|
catch (e) {
|
|
302
|
-
log.debug(
|
|
458
|
+
log.debug("Failed to send SSE message", { error: e });
|
|
303
459
|
}
|
|
304
460
|
});
|
|
305
|
-
log.debug(
|
|
461
|
+
log.debug("SSE messages sent", {
|
|
462
|
+
count: sentCount,
|
|
463
|
+
totalClients: sseClients.size,
|
|
464
|
+
});
|
|
306
465
|
res.writeHead(200);
|
|
307
466
|
res.end(JSON.stringify({ success: true }));
|
|
308
467
|
ctx.end(200);
|
|
309
468
|
return;
|
|
310
469
|
}
|
|
311
|
-
if (req.method ===
|
|
312
|
-
let body =
|
|
313
|
-
req.on(
|
|
314
|
-
req.on(
|
|
470
|
+
if (req.method === "POST") {
|
|
471
|
+
let body = "";
|
|
472
|
+
req.on("data", (chunk) => (body += chunk));
|
|
473
|
+
req.on("end", () => {
|
|
315
474
|
try {
|
|
316
475
|
const data = JSON.parse(body);
|
|
317
476
|
pageContext = {
|
|
318
|
-
url: data.url ||
|
|
319
|
-
title: data.title ||
|
|
477
|
+
url: data.url || "",
|
|
478
|
+
title: data.title || "",
|
|
320
479
|
selectedElements: data.selectedElements || [],
|
|
321
480
|
};
|
|
322
|
-
log.debug(
|
|
481
|
+
log.debug("Context updated", {
|
|
323
482
|
url: pageContext.url,
|
|
324
483
|
title: pageContext.title,
|
|
325
|
-
selectedElementsCount: pageContext.selectedElements?.length || 0
|
|
484
|
+
selectedElementsCount: pageContext.selectedElements?.length || 0,
|
|
326
485
|
});
|
|
327
|
-
if (pageContext.selectedElements &&
|
|
328
|
-
|
|
329
|
-
|
|
486
|
+
if (pageContext.selectedElements &&
|
|
487
|
+
pageContext.selectedElements.length > 0) {
|
|
488
|
+
log.debug("Selected elements details", {
|
|
489
|
+
elements: pageContext.selectedElements.map((el) => ({
|
|
330
490
|
filePath: el.filePath,
|
|
331
491
|
line: el.line,
|
|
332
|
-
text: el.innerText?.substring(0, 50)
|
|
333
|
-
}))
|
|
492
|
+
text: el.innerText?.substring(0, 50),
|
|
493
|
+
})),
|
|
334
494
|
});
|
|
335
495
|
}
|
|
336
496
|
res.writeHead(200);
|
|
@@ -338,38 +498,39 @@ Please install OpenCode first:
|
|
|
338
498
|
ctx.end(200);
|
|
339
499
|
}
|
|
340
500
|
catch (e) {
|
|
341
|
-
log.debug(
|
|
501
|
+
log.debug("Invalid JSON in request body", { error: e });
|
|
342
502
|
res.writeHead(400);
|
|
343
|
-
res.end(JSON.stringify({ error:
|
|
503
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
344
504
|
ctx.end(400);
|
|
345
505
|
}
|
|
346
506
|
});
|
|
347
507
|
return;
|
|
348
508
|
}
|
|
349
509
|
res.writeHead(405);
|
|
350
|
-
res.end(JSON.stringify({ error:
|
|
510
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
351
511
|
ctx.end(405);
|
|
352
512
|
}
|
|
353
513
|
return {
|
|
354
|
-
name:
|
|
514
|
+
name: "vite-plugin-opencode",
|
|
515
|
+
apply(_viteConfig, env) {
|
|
516
|
+
if (!config.enabled)
|
|
517
|
+
return false;
|
|
518
|
+
return env.command === "serve" && process.env.NODE_ENV !== "test";
|
|
519
|
+
},
|
|
355
520
|
async configureServer(server) {
|
|
356
|
-
|
|
357
|
-
log.debug('Plugin disabled, skipping configuration');
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
const timer = log.timer('configureServer');
|
|
521
|
+
const timer = log.timer("configureServer");
|
|
361
522
|
server.middlewares.use(WIDGET_SCRIPT_PATH, async (_req, res) => {
|
|
362
|
-
const ctx = new RequestContext(
|
|
363
|
-
const widgetPath = path.join(__dirname,
|
|
523
|
+
const ctx = new RequestContext("GET", WIDGET_SCRIPT_PATH);
|
|
524
|
+
const widgetPath = path.join(__dirname, "client.js");
|
|
364
525
|
if (fs.existsSync(widgetPath)) {
|
|
365
|
-
res.setHeader(
|
|
366
|
-
res.setHeader(
|
|
526
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
527
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
367
528
|
fs.createReadStream(widgetPath).pipe(res);
|
|
368
529
|
ctx.end(200);
|
|
369
530
|
}
|
|
370
531
|
else {
|
|
371
532
|
res.writeHead(404);
|
|
372
|
-
res.end(
|
|
533
|
+
res.end("Widget script not found");
|
|
373
534
|
ctx.end(404);
|
|
374
535
|
}
|
|
375
536
|
});
|
|
@@ -377,63 +538,68 @@ Please install OpenCode first:
|
|
|
377
538
|
handleContextRequest(req, res);
|
|
378
539
|
});
|
|
379
540
|
server.middlewares.use(START_API_PATH, async (_req, res) => {
|
|
380
|
-
const ctx = new RequestContext(
|
|
381
|
-
res.setHeader(
|
|
382
|
-
res.setHeader(
|
|
541
|
+
const ctx = new RequestContext("GET", START_API_PATH);
|
|
542
|
+
res.setHeader("Content-Type", "application/json");
|
|
543
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
383
544
|
res.writeHead(200);
|
|
384
545
|
res.end(JSON.stringify({ success: true, sessionUrl }));
|
|
385
546
|
ctx.end(200);
|
|
386
547
|
});
|
|
387
548
|
server.middlewares.use(SSE_EVENTS_PATH, async (req, res) => {
|
|
388
|
-
const ctx = new RequestContext(
|
|
549
|
+
const ctx = new RequestContext("GET", SSE_EVENTS_PATH);
|
|
389
550
|
res.writeHead(200, {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
551
|
+
"Content-Type": "text/event-stream",
|
|
552
|
+
"Cache-Control": "no-cache",
|
|
553
|
+
Connection: "keep-alive",
|
|
554
|
+
"Access-Control-Allow-Origin": "*",
|
|
394
555
|
});
|
|
395
556
|
sseClients.add(res);
|
|
396
|
-
log.debug(
|
|
397
|
-
res.write(`data: ${JSON.stringify({ type:
|
|
398
|
-
|
|
557
|
+
log.debug("SSE client connected", { totalClients: sseClients.size });
|
|
558
|
+
res.write(`data: ${JSON.stringify({ type: "CONNECTED" })}\n\n`);
|
|
559
|
+
if (sessionUrl) {
|
|
560
|
+
res.write(`data: ${JSON.stringify({ type: "SESSION_READY", sessionUrl })}\n\n`);
|
|
561
|
+
}
|
|
562
|
+
req.on("close", () => {
|
|
399
563
|
sseClients.delete(res);
|
|
400
|
-
log.debug(
|
|
564
|
+
log.debug("SSE client disconnected", {
|
|
565
|
+
totalClients: sseClients.size,
|
|
566
|
+
});
|
|
401
567
|
});
|
|
402
568
|
ctx.end(200);
|
|
403
569
|
});
|
|
404
570
|
server.middlewares.use(SESSIONS_API_PATH, async (req, res) => {
|
|
405
|
-
const ctx = new RequestContext(req.method ||
|
|
406
|
-
res.setHeader(
|
|
407
|
-
res.setHeader(
|
|
408
|
-
res.setHeader(
|
|
409
|
-
res.setHeader(
|
|
410
|
-
if (req.method ===
|
|
571
|
+
const ctx = new RequestContext(req.method || "GET", SESSIONS_API_PATH);
|
|
572
|
+
res.setHeader("Content-Type", "application/json");
|
|
573
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
574
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
575
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
576
|
+
if (req.method === "OPTIONS") {
|
|
411
577
|
res.writeHead(200);
|
|
412
578
|
res.end();
|
|
413
579
|
ctx.end(200);
|
|
414
580
|
return;
|
|
415
581
|
}
|
|
416
582
|
try {
|
|
417
|
-
if (req.method ===
|
|
418
|
-
ctx.checkpoint(
|
|
583
|
+
if (req.method === "GET") {
|
|
584
|
+
ctx.checkpoint("Fetching sessions");
|
|
419
585
|
const sessions = await getSessions();
|
|
420
586
|
res.writeHead(200);
|
|
421
587
|
res.end(JSON.stringify(sessions));
|
|
422
588
|
ctx.end(200);
|
|
423
589
|
}
|
|
424
|
-
else if (req.method ===
|
|
425
|
-
ctx.checkpoint(
|
|
590
|
+
else if (req.method === "POST") {
|
|
591
|
+
ctx.checkpoint("Creating session");
|
|
426
592
|
const newSession = await createSession();
|
|
427
593
|
res.writeHead(200);
|
|
428
594
|
res.end(JSON.stringify(newSession));
|
|
429
595
|
ctx.end(200);
|
|
430
596
|
}
|
|
431
|
-
else if (req.method ===
|
|
432
|
-
const url = new URL(req.url ||
|
|
433
|
-
const sessionId = url.searchParams.get(
|
|
597
|
+
else if (req.method === "DELETE") {
|
|
598
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
599
|
+
const sessionId = url.searchParams.get("id");
|
|
434
600
|
if (!sessionId) {
|
|
435
601
|
res.writeHead(400);
|
|
436
|
-
res.end(JSON.stringify({ error:
|
|
602
|
+
res.end(JSON.stringify({ error: "Session ID is required" }));
|
|
437
603
|
ctx.end(400);
|
|
438
604
|
return;
|
|
439
605
|
}
|
|
@@ -445,27 +611,27 @@ Please install OpenCode first:
|
|
|
445
611
|
}
|
|
446
612
|
else {
|
|
447
613
|
res.writeHead(405);
|
|
448
|
-
res.end(JSON.stringify({ error:
|
|
614
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
449
615
|
ctx.end(405);
|
|
450
616
|
}
|
|
451
617
|
}
|
|
452
618
|
catch (e) {
|
|
453
|
-
log.error(
|
|
619
|
+
log.error("Session API error", { error: e, method: req.method });
|
|
454
620
|
res.writeHead(500);
|
|
455
621
|
res.end(JSON.stringify({ error: String(e) }));
|
|
456
622
|
ctx.error(e);
|
|
457
623
|
}
|
|
458
624
|
});
|
|
459
|
-
server.httpServer?.on(
|
|
460
|
-
log.debug(
|
|
625
|
+
server.httpServer?.on("listening", async () => {
|
|
626
|
+
log.debug("Vite server listening event fired");
|
|
461
627
|
const address = server.httpServer?.address();
|
|
462
628
|
let vitePort;
|
|
463
629
|
let viteHost;
|
|
464
|
-
if (address && typeof address ===
|
|
630
|
+
if (address && typeof address === "object") {
|
|
465
631
|
vitePort = address.port;
|
|
466
632
|
const addr = address.address;
|
|
467
|
-
if (addr ===
|
|
468
|
-
viteHost =
|
|
633
|
+
if (addr === "::" || addr === "::1" || addr === "0.0.0.0" || !addr) {
|
|
634
|
+
viteHost = "localhost";
|
|
469
635
|
}
|
|
470
636
|
else {
|
|
471
637
|
viteHost = addr;
|
|
@@ -474,33 +640,44 @@ Please install OpenCode first:
|
|
|
474
640
|
else {
|
|
475
641
|
const host = server.config.server.host;
|
|
476
642
|
vitePort = server.config.server.port || 5173;
|
|
477
|
-
viteHost =
|
|
643
|
+
viteHost =
|
|
644
|
+
typeof host === "string" &&
|
|
645
|
+
host !== "0.0.0.0" &&
|
|
646
|
+
host !== "::" &&
|
|
647
|
+
host !== "::1"
|
|
648
|
+
? host
|
|
649
|
+
: "localhost";
|
|
478
650
|
}
|
|
479
651
|
const viteOrigin = `http://${viteHost}:${vitePort}`;
|
|
480
652
|
const contextApiUrl = `http://${viteHost}:${vitePort}${CONTEXT_API_PATH}`;
|
|
481
|
-
log.debug(
|
|
653
|
+
log.debug("Vite server ready", {
|
|
654
|
+
vitePort,
|
|
655
|
+
viteHost,
|
|
656
|
+
viteOrigin,
|
|
657
|
+
contextApiUrl,
|
|
658
|
+
});
|
|
482
659
|
try {
|
|
483
|
-
await startServices([viteOrigin], contextApiUrl);
|
|
660
|
+
await startServices([viteOrigin], contextApiUrl, viteOrigin);
|
|
484
661
|
}
|
|
485
662
|
catch (e) {
|
|
486
|
-
log.error(
|
|
663
|
+
log.error("Failed to start services", { error: e });
|
|
487
664
|
}
|
|
488
665
|
});
|
|
489
|
-
server.httpServer?.on(
|
|
490
|
-
log.debug(
|
|
666
|
+
server.httpServer?.on("close", () => {
|
|
667
|
+
log.debug("HTTP server closing");
|
|
491
668
|
stopServices();
|
|
492
669
|
});
|
|
493
670
|
const cleanup = async () => {
|
|
494
|
-
log.debug(
|
|
671
|
+
log.debug("Process cleanup triggered");
|
|
495
672
|
await stopServices();
|
|
496
673
|
process.exit(0);
|
|
497
674
|
};
|
|
498
|
-
process.on(
|
|
499
|
-
process.on(
|
|
500
|
-
timer.end(
|
|
675
|
+
process.on("SIGINT", cleanup);
|
|
676
|
+
process.on("SIGTERM", cleanup);
|
|
677
|
+
timer.end("✓ Server configured");
|
|
501
678
|
},
|
|
502
679
|
transformIndexHtml(html) {
|
|
503
|
-
const timer = log.timer(
|
|
680
|
+
const timer = log.timer("transformIndexHtml");
|
|
504
681
|
const widget = injectWidget({
|
|
505
682
|
webUrl: `http://${config.hostname}:${actualWebPort}`,
|
|
506
683
|
serverUrl: `http://${config.hostname}:${actualWebPort}`,
|
|
@@ -513,7 +690,7 @@ Please install OpenCode first:
|
|
|
513
690
|
hotkey: config.hotkey,
|
|
514
691
|
});
|
|
515
692
|
timer.end();
|
|
516
|
-
return html.replace(
|
|
693
|
+
return html.replace("</body>", `${widget}</body>`);
|
|
517
694
|
},
|
|
518
695
|
};
|
|
519
696
|
}
|