vite-plugin-opencode-assistant 1.0.2 → 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 +4 -6
- package/dist/constants.js +21 -23
- package/dist/constants.js.map +1 -0
- package/dist/logger.d.ts +64 -0
- package/dist/logger.js +311 -0
- package/dist/logger.js.map +1 -0
- package/dist/{plugins → opencode/plugins}/page-context.d.ts +0 -13
- package/dist/opencode/plugins/page-context.js +372 -0
- package/dist/opencode/plugins/page-context.js.map +1 -0
- package/dist/opencode/web.d.ts +3 -0
- package/dist/opencode/web.js +81 -0
- package/dist/opencode/web.js.map +1 -0
- package/dist/types.d.ts +4 -8
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -0
- package/dist/vite/client.js +2468 -0
- package/dist/vite/client.js.map +1 -0
- package/dist/vite/index.d.ts +3 -0
- package/dist/vite/index.js +697 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/vite/injector.d.ts +2 -0
- package/dist/vite/injector.js +6 -0
- package/dist/vite/injector.js.map +1 -0
- package/dist/vite/utils.d.ts +5 -0
- package/dist/vite/utils.js +206 -0
- package/dist/vite/utils.js.map +1 -0
- package/package.json +17 -6
- package/dist/client.js +0 -1549
- package/dist/index.d.ts +0 -23
- package/dist/index.js +0 -451
- package/dist/injector.d.ts +0 -20
- package/dist/injector.js +0 -24
- package/dist/plugins/page-context.js +0 -121
- package/dist/utils.d.ts +0 -42
- package/dist/utils.js +0 -156
- package/dist/web.d.ts +0 -18
- package/dist/web.js +0 -95
- /package/dist/{client.d.ts → vite/client.d.ts} +0 -0
|
@@ -0,0 +1,697 @@
|
|
|
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
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
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
|
+
export default function opencodePlugin(options = {}) {
|
|
14
|
+
const plugins = [];
|
|
15
|
+
plugins.push(...Inspector({
|
|
16
|
+
enabled: false,
|
|
17
|
+
toggleButtonVisibility: "never",
|
|
18
|
+
toggleComboKey: false,
|
|
19
|
+
}));
|
|
20
|
+
plugins.push(createOpenCodePlugin(options));
|
|
21
|
+
return plugins;
|
|
22
|
+
}
|
|
23
|
+
function createOpenCodePlugin(options = {}) {
|
|
24
|
+
let webProcess = null;
|
|
25
|
+
let sessionUrl = null;
|
|
26
|
+
let actualWebPort = DEFAULT_CONFIG.webPort;
|
|
27
|
+
let isStarted = false;
|
|
28
|
+
let startPromise = null;
|
|
29
|
+
let pageContext = { url: "", title: "" };
|
|
30
|
+
const sseClients = new Set();
|
|
31
|
+
const config = { ...DEFAULT_CONFIG, ...options };
|
|
32
|
+
setVerbose(config.verbose);
|
|
33
|
+
const log = createLogger("Plugin");
|
|
34
|
+
function base64Encode(str) {
|
|
35
|
+
return Buffer.from(str).toString("base64");
|
|
36
|
+
}
|
|
37
|
+
function sleep(ms) {
|
|
38
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
|
+
}
|
|
40
|
+
function createHttpRequest(options, body) {
|
|
41
|
+
const timer = new PerformanceTimer("HTTP Request", {
|
|
42
|
+
operation: `${options.method || "GET"} ${options.path}`,
|
|
43
|
+
});
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const req = http.request(options, (res) => {
|
|
46
|
+
let data = "";
|
|
47
|
+
res.on("data", (chunk) => (data += chunk));
|
|
48
|
+
res.on("end", () => {
|
|
49
|
+
try {
|
|
50
|
+
const result = JSON.parse(data);
|
|
51
|
+
timer.end(`✓ Status: ${res.statusCode}`);
|
|
52
|
+
resolve(result);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
timer.end("❌ JSON parse error");
|
|
56
|
+
reject(new Error(`JSON parse error: ${data.substring(0, 100)}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
req.on("error", (e) => {
|
|
61
|
+
timer.end("❌ Request failed");
|
|
62
|
+
reject(e);
|
|
63
|
+
});
|
|
64
|
+
if (body)
|
|
65
|
+
req.write(body);
|
|
66
|
+
req.end();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function getSessions(retries = DEFAULT_RETRIES) {
|
|
70
|
+
const timer = log.timer("getSessions", { retries });
|
|
71
|
+
let lastError = null;
|
|
72
|
+
for (let i = 0; i < retries; i++) {
|
|
73
|
+
try {
|
|
74
|
+
log.debug(`Attempt ${i + 1}/${retries}`, { operation: "getSessions" });
|
|
75
|
+
const sessions = await createHttpRequest({
|
|
76
|
+
hostname: config.hostname,
|
|
77
|
+
port: actualWebPort,
|
|
78
|
+
path: "/session",
|
|
79
|
+
});
|
|
80
|
+
timer.end(`Found ${sessions.length} sessions`);
|
|
81
|
+
return sessions;
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
85
|
+
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
86
|
+
operation: "getSessions",
|
|
87
|
+
});
|
|
88
|
+
if (i < retries - 1) {
|
|
89
|
+
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
90
|
+
operation: "getSessions",
|
|
91
|
+
});
|
|
92
|
+
await sleep(RETRY_DELAY);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
timer.end("❌ All retries exhausted");
|
|
97
|
+
throw lastError;
|
|
98
|
+
}
|
|
99
|
+
async function createSession(retries = DEFAULT_RETRIES, title) {
|
|
100
|
+
const timer = log.timer("createSession", { retries, title });
|
|
101
|
+
let lastError = null;
|
|
102
|
+
for (let i = 0; i < retries; i++) {
|
|
103
|
+
try {
|
|
104
|
+
log.debug(`Attempt ${i + 1}/${retries}`, {
|
|
105
|
+
operation: "createSession",
|
|
106
|
+
title,
|
|
107
|
+
});
|
|
108
|
+
const requestBody = title ? JSON.stringify({ title }) : undefined;
|
|
109
|
+
const session = await createHttpRequest({
|
|
110
|
+
hostname: config.hostname,
|
|
111
|
+
port: actualWebPort,
|
|
112
|
+
path: "/session",
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: requestBody
|
|
115
|
+
? { "Content-Type": "application/json" }
|
|
116
|
+
: undefined,
|
|
117
|
+
}, requestBody);
|
|
118
|
+
timer.end(`Created session: ${session.id}`);
|
|
119
|
+
return session;
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
123
|
+
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
124
|
+
operation: "createSession",
|
|
125
|
+
});
|
|
126
|
+
if (i < retries - 1) {
|
|
127
|
+
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
128
|
+
operation: "createSession",
|
|
129
|
+
});
|
|
130
|
+
await sleep(RETRY_DELAY);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
timer.end("❌ All retries exhausted");
|
|
135
|
+
throw lastError;
|
|
136
|
+
}
|
|
137
|
+
async function deleteSession(sessionId, retries = DEFAULT_RETRIES) {
|
|
138
|
+
const timer = log.timer("deleteSession", { sessionId, retries });
|
|
139
|
+
let lastError = null;
|
|
140
|
+
for (let i = 0; i < retries; i++) {
|
|
141
|
+
try {
|
|
142
|
+
log.debug(`Attempt ${i + 1}/${retries}`, {
|
|
143
|
+
operation: "deleteSession",
|
|
144
|
+
sessionId,
|
|
145
|
+
});
|
|
146
|
+
await createHttpRequest({
|
|
147
|
+
hostname: config.hostname,
|
|
148
|
+
port: actualWebPort,
|
|
149
|
+
path: `/session/${sessionId}`,
|
|
150
|
+
method: "DELETE",
|
|
151
|
+
});
|
|
152
|
+
timer.end(`Deleted session: ${sessionId}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
157
|
+
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
|
|
158
|
+
operation: "deleteSession",
|
|
159
|
+
sessionId,
|
|
160
|
+
});
|
|
161
|
+
if (i < retries - 1) {
|
|
162
|
+
log.debug(`Retrying in ${RETRY_DELAY}ms...`, {
|
|
163
|
+
operation: "deleteSession",
|
|
164
|
+
sessionId,
|
|
165
|
+
});
|
|
166
|
+
await sleep(RETRY_DELAY);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
timer.end("❌ All retries exhausted");
|
|
171
|
+
throw lastError;
|
|
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
|
+
}
|
|
264
|
+
async function getOrCreateSession() {
|
|
265
|
+
const timer = log.timer("getOrCreateSession");
|
|
266
|
+
const projectDir = process.cwd();
|
|
267
|
+
log.debug("Getting sessions...", { projectDir });
|
|
268
|
+
const sessions = await getSessions();
|
|
269
|
+
log.debug(`Found ${sessions.length} sessions`, {
|
|
270
|
+
sessions: sessions.map((s) => ({ id: s.id, directory: s.directory })),
|
|
271
|
+
});
|
|
272
|
+
const matchingSession = sessions.find((s) => s.directory === projectDir);
|
|
273
|
+
if (matchingSession) {
|
|
274
|
+
const url = `http://${config.hostname}:${actualWebPort}/${base64Encode(projectDir)}/session/${matchingSession.id}`;
|
|
275
|
+
timer.end(`Using existing session: ${matchingSession.id}`);
|
|
276
|
+
return url;
|
|
277
|
+
}
|
|
278
|
+
log.debug("Creating new session...", { projectDir });
|
|
279
|
+
const newSession = await createSession();
|
|
280
|
+
const url = `http://${config.hostname}:${actualWebPort}/${base64Encode(projectDir)}/session/${newSession.id}`;
|
|
281
|
+
timer.end(`Created new session: ${newSession.id}`);
|
|
282
|
+
return url;
|
|
283
|
+
}
|
|
284
|
+
function setupOpenCodePlugin() {
|
|
285
|
+
const timer = log.timer("setupOpenCodePlugin");
|
|
286
|
+
const projectDir = process.cwd();
|
|
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 });
|
|
290
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
291
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
292
|
+
log.debug("Created plugins directory", { pluginsDir });
|
|
293
|
+
}
|
|
294
|
+
const pluginSourcePath = path.join(__dirname, "..", "opencode", "plugins", "page-context.js");
|
|
295
|
+
const pluginTargetPath = path.join(pluginsDir, "page-context.js");
|
|
296
|
+
if (fs.existsSync(pluginSourcePath)) {
|
|
297
|
+
fs.copyFileSync(pluginSourcePath, pluginTargetPath);
|
|
298
|
+
log.debug("Plugin installed", {
|
|
299
|
+
source: pluginSourcePath,
|
|
300
|
+
target: pluginTargetPath,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
log.warn("Plugin source not found", { path: pluginSourcePath });
|
|
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 });
|
|
319
|
+
timer.end();
|
|
320
|
+
return cacheDir;
|
|
321
|
+
}
|
|
322
|
+
async function startServices(corsOrigins, contextApiUrl, viteOrigin) {
|
|
323
|
+
if (isStarted && webProcess) {
|
|
324
|
+
log.debug("Services already started, skipping");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (startPromise) {
|
|
328
|
+
log.debug("Waiting for existing start promise");
|
|
329
|
+
return startPromise;
|
|
330
|
+
}
|
|
331
|
+
startPromise = (async () => {
|
|
332
|
+
const timer = log.timer("startServices", {
|
|
333
|
+
corsOrigins,
|
|
334
|
+
contextApiUrl,
|
|
335
|
+
viteOrigin,
|
|
336
|
+
});
|
|
337
|
+
log.info("Starting OpenCode services...");
|
|
338
|
+
const orphanCount = await killOrphanOpenCodeProcesses();
|
|
339
|
+
if (orphanCount > 0) {
|
|
340
|
+
log.debug(`Killed ${orphanCount} orphan OpenCode process(es)`);
|
|
341
|
+
}
|
|
342
|
+
if (!(await checkOpenCodeInstalled())) {
|
|
343
|
+
log.error(`OpenCode is not installed!
|
|
344
|
+
|
|
345
|
+
Please install OpenCode first:
|
|
346
|
+
|
|
347
|
+
# YOLO
|
|
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
|
|
360
|
+
`);
|
|
361
|
+
timer.end("❌ OpenCode not installed");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
timer.checkpoint("OpenCode installation verified");
|
|
365
|
+
actualWebPort = await findAvailablePort(config.webPort, config.hostname);
|
|
366
|
+
if (actualWebPort !== config.webPort) {
|
|
367
|
+
log.info(`Port ${config.webPort} is in use, using ${actualWebPort} instead`);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
log.debug(`Using port ${actualWebPort}`);
|
|
371
|
+
}
|
|
372
|
+
timer.checkpoint("Port allocated");
|
|
373
|
+
const configDir = setupOpenCodePlugin();
|
|
374
|
+
timer.checkpoint("Plugin setup complete");
|
|
375
|
+
log.debug("Starting OpenCode Web process...", {
|
|
376
|
+
port: actualWebPort,
|
|
377
|
+
hostname: config.hostname,
|
|
378
|
+
configDir,
|
|
379
|
+
});
|
|
380
|
+
webProcess = startOpenCodeWeb({
|
|
381
|
+
port: actualWebPort,
|
|
382
|
+
hostname: config.hostname,
|
|
383
|
+
serverUrl: "",
|
|
384
|
+
cwd: process.cwd(),
|
|
385
|
+
configDir,
|
|
386
|
+
corsOrigins,
|
|
387
|
+
contextApiUrl,
|
|
388
|
+
});
|
|
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");
|
|
396
|
+
try {
|
|
397
|
+
sessionUrl = await getOrCreateSession();
|
|
398
|
+
timer.checkpoint("Session created");
|
|
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
|
+
});
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
log.warn("Failed to get/create session", { error: e });
|
|
411
|
+
}
|
|
412
|
+
isStarted = true;
|
|
413
|
+
log.debug(`OpenCode services started successfully: ${sessionUrl || webUrl}`);
|
|
414
|
+
timer.end("✓ Services started successfully");
|
|
415
|
+
})();
|
|
416
|
+
return startPromise;
|
|
417
|
+
}
|
|
418
|
+
async function stopServices() {
|
|
419
|
+
const timer = log.timer("stopServices");
|
|
420
|
+
log.info("Stopping OpenCode services...");
|
|
421
|
+
if (webProcess) {
|
|
422
|
+
log.debug("Killing web process", { pid: webProcess.pid });
|
|
423
|
+
webProcess.kill("SIGTERM");
|
|
424
|
+
webProcess = null;
|
|
425
|
+
}
|
|
426
|
+
isStarted = false;
|
|
427
|
+
startPromise = null;
|
|
428
|
+
timer.end("✓ Services stopped");
|
|
429
|
+
}
|
|
430
|
+
function handleContextRequest(req, res) {
|
|
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") {
|
|
437
|
+
res.writeHead(200);
|
|
438
|
+
res.end();
|
|
439
|
+
ctx.end(200);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (req.method === "GET") {
|
|
443
|
+
res.writeHead(200);
|
|
444
|
+
res.end(JSON.stringify(pageContext));
|
|
445
|
+
ctx.end(200);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (req.method === "DELETE") {
|
|
449
|
+
pageContext.selectedElements = [];
|
|
450
|
+
log.debug("Selected elements cleared", { sseClients: sseClients.size });
|
|
451
|
+
let sentCount = 0;
|
|
452
|
+
sseClients.forEach((client) => {
|
|
453
|
+
try {
|
|
454
|
+
client.write(`data: ${JSON.stringify({ type: "CLEAR_ELEMENTS" })}\n\n`);
|
|
455
|
+
sentCount++;
|
|
456
|
+
}
|
|
457
|
+
catch (e) {
|
|
458
|
+
log.debug("Failed to send SSE message", { error: e });
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
log.debug("SSE messages sent", {
|
|
462
|
+
count: sentCount,
|
|
463
|
+
totalClients: sseClients.size,
|
|
464
|
+
});
|
|
465
|
+
res.writeHead(200);
|
|
466
|
+
res.end(JSON.stringify({ success: true }));
|
|
467
|
+
ctx.end(200);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (req.method === "POST") {
|
|
471
|
+
let body = "";
|
|
472
|
+
req.on("data", (chunk) => (body += chunk));
|
|
473
|
+
req.on("end", () => {
|
|
474
|
+
try {
|
|
475
|
+
const data = JSON.parse(body);
|
|
476
|
+
pageContext = {
|
|
477
|
+
url: data.url || "",
|
|
478
|
+
title: data.title || "",
|
|
479
|
+
selectedElements: data.selectedElements || [],
|
|
480
|
+
};
|
|
481
|
+
log.debug("Context updated", {
|
|
482
|
+
url: pageContext.url,
|
|
483
|
+
title: pageContext.title,
|
|
484
|
+
selectedElementsCount: pageContext.selectedElements?.length || 0,
|
|
485
|
+
});
|
|
486
|
+
if (pageContext.selectedElements &&
|
|
487
|
+
pageContext.selectedElements.length > 0) {
|
|
488
|
+
log.debug("Selected elements details", {
|
|
489
|
+
elements: pageContext.selectedElements.map((el) => ({
|
|
490
|
+
filePath: el.filePath,
|
|
491
|
+
line: el.line,
|
|
492
|
+
text: el.innerText?.substring(0, 50),
|
|
493
|
+
})),
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
res.writeHead(200);
|
|
497
|
+
res.end(JSON.stringify({ success: true }));
|
|
498
|
+
ctx.end(200);
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
log.debug("Invalid JSON in request body", { error: e });
|
|
502
|
+
res.writeHead(400);
|
|
503
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
504
|
+
ctx.end(400);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
res.writeHead(405);
|
|
510
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
511
|
+
ctx.end(405);
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
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
|
+
},
|
|
520
|
+
async configureServer(server) {
|
|
521
|
+
const timer = log.timer("configureServer");
|
|
522
|
+
server.middlewares.use(WIDGET_SCRIPT_PATH, async (_req, res) => {
|
|
523
|
+
const ctx = new RequestContext("GET", WIDGET_SCRIPT_PATH);
|
|
524
|
+
const widgetPath = path.join(__dirname, "client.js");
|
|
525
|
+
if (fs.existsSync(widgetPath)) {
|
|
526
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
527
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
528
|
+
fs.createReadStream(widgetPath).pipe(res);
|
|
529
|
+
ctx.end(200);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
res.writeHead(404);
|
|
533
|
+
res.end("Widget script not found");
|
|
534
|
+
ctx.end(404);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
server.middlewares.use(CONTEXT_API_PATH, async (req, res) => {
|
|
538
|
+
handleContextRequest(req, res);
|
|
539
|
+
});
|
|
540
|
+
server.middlewares.use(START_API_PATH, async (_req, res) => {
|
|
541
|
+
const ctx = new RequestContext("GET", START_API_PATH);
|
|
542
|
+
res.setHeader("Content-Type", "application/json");
|
|
543
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
544
|
+
res.writeHead(200);
|
|
545
|
+
res.end(JSON.stringify({ success: true, sessionUrl }));
|
|
546
|
+
ctx.end(200);
|
|
547
|
+
});
|
|
548
|
+
server.middlewares.use(SSE_EVENTS_PATH, async (req, res) => {
|
|
549
|
+
const ctx = new RequestContext("GET", SSE_EVENTS_PATH);
|
|
550
|
+
res.writeHead(200, {
|
|
551
|
+
"Content-Type": "text/event-stream",
|
|
552
|
+
"Cache-Control": "no-cache",
|
|
553
|
+
Connection: "keep-alive",
|
|
554
|
+
"Access-Control-Allow-Origin": "*",
|
|
555
|
+
});
|
|
556
|
+
sseClients.add(res);
|
|
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", () => {
|
|
563
|
+
sseClients.delete(res);
|
|
564
|
+
log.debug("SSE client disconnected", {
|
|
565
|
+
totalClients: sseClients.size,
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
ctx.end(200);
|
|
569
|
+
});
|
|
570
|
+
server.middlewares.use(SESSIONS_API_PATH, async (req, res) => {
|
|
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") {
|
|
577
|
+
res.writeHead(200);
|
|
578
|
+
res.end();
|
|
579
|
+
ctx.end(200);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
if (req.method === "GET") {
|
|
584
|
+
ctx.checkpoint("Fetching sessions");
|
|
585
|
+
const sessions = await getSessions();
|
|
586
|
+
res.writeHead(200);
|
|
587
|
+
res.end(JSON.stringify(sessions));
|
|
588
|
+
ctx.end(200);
|
|
589
|
+
}
|
|
590
|
+
else if (req.method === "POST") {
|
|
591
|
+
ctx.checkpoint("Creating session");
|
|
592
|
+
const newSession = await createSession();
|
|
593
|
+
res.writeHead(200);
|
|
594
|
+
res.end(JSON.stringify(newSession));
|
|
595
|
+
ctx.end(200);
|
|
596
|
+
}
|
|
597
|
+
else if (req.method === "DELETE") {
|
|
598
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
599
|
+
const sessionId = url.searchParams.get("id");
|
|
600
|
+
if (!sessionId) {
|
|
601
|
+
res.writeHead(400);
|
|
602
|
+
res.end(JSON.stringify({ error: "Session ID is required" }));
|
|
603
|
+
ctx.end(400);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
ctx.checkpoint(`Deleting session: ${sessionId}`);
|
|
607
|
+
await deleteSession(sessionId);
|
|
608
|
+
res.writeHead(200);
|
|
609
|
+
res.end(JSON.stringify({ success: true }));
|
|
610
|
+
ctx.end(200);
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
res.writeHead(405);
|
|
614
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
615
|
+
ctx.end(405);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch (e) {
|
|
619
|
+
log.error("Session API error", { error: e, method: req.method });
|
|
620
|
+
res.writeHead(500);
|
|
621
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
622
|
+
ctx.error(e);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
server.httpServer?.on("listening", async () => {
|
|
626
|
+
log.debug("Vite server listening event fired");
|
|
627
|
+
const address = server.httpServer?.address();
|
|
628
|
+
let vitePort;
|
|
629
|
+
let viteHost;
|
|
630
|
+
if (address && typeof address === "object") {
|
|
631
|
+
vitePort = address.port;
|
|
632
|
+
const addr = address.address;
|
|
633
|
+
if (addr === "::" || addr === "::1" || addr === "0.0.0.0" || !addr) {
|
|
634
|
+
viteHost = "localhost";
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
viteHost = addr;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
const host = server.config.server.host;
|
|
642
|
+
vitePort = server.config.server.port || 5173;
|
|
643
|
+
viteHost =
|
|
644
|
+
typeof host === "string" &&
|
|
645
|
+
host !== "0.0.0.0" &&
|
|
646
|
+
host !== "::" &&
|
|
647
|
+
host !== "::1"
|
|
648
|
+
? host
|
|
649
|
+
: "localhost";
|
|
650
|
+
}
|
|
651
|
+
const viteOrigin = `http://${viteHost}:${vitePort}`;
|
|
652
|
+
const contextApiUrl = `http://${viteHost}:${vitePort}${CONTEXT_API_PATH}`;
|
|
653
|
+
log.debug("Vite server ready", {
|
|
654
|
+
vitePort,
|
|
655
|
+
viteHost,
|
|
656
|
+
viteOrigin,
|
|
657
|
+
contextApiUrl,
|
|
658
|
+
});
|
|
659
|
+
try {
|
|
660
|
+
await startServices([viteOrigin], contextApiUrl, viteOrigin);
|
|
661
|
+
}
|
|
662
|
+
catch (e) {
|
|
663
|
+
log.error("Failed to start services", { error: e });
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
server.httpServer?.on("close", () => {
|
|
667
|
+
log.debug("HTTP server closing");
|
|
668
|
+
stopServices();
|
|
669
|
+
});
|
|
670
|
+
const cleanup = async () => {
|
|
671
|
+
log.debug("Process cleanup triggered");
|
|
672
|
+
await stopServices();
|
|
673
|
+
process.exit(0);
|
|
674
|
+
};
|
|
675
|
+
process.on("SIGINT", cleanup);
|
|
676
|
+
process.on("SIGTERM", cleanup);
|
|
677
|
+
timer.end("✓ Server configured");
|
|
678
|
+
},
|
|
679
|
+
transformIndexHtml(html) {
|
|
680
|
+
const timer = log.timer("transformIndexHtml");
|
|
681
|
+
const widget = injectWidget({
|
|
682
|
+
webUrl: `http://${config.hostname}:${actualWebPort}`,
|
|
683
|
+
serverUrl: `http://${config.hostname}:${actualWebPort}`,
|
|
684
|
+
position: config.position,
|
|
685
|
+
theme: config.theme,
|
|
686
|
+
open: config.open,
|
|
687
|
+
autoReload: config.autoReload,
|
|
688
|
+
cwd: process.cwd(),
|
|
689
|
+
sessionUrl: sessionUrl || undefined,
|
|
690
|
+
hotkey: config.hotkey,
|
|
691
|
+
});
|
|
692
|
+
timer.end();
|
|
693
|
+
return html.replace("</body>", `${widget}</body>`);
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
//# sourceMappingURL=index.js.map
|