vite-plugin-aipanel 1.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +72 -0
  2. package/es/client/vue-devtools-bridge.d.ts +58 -0
  3. package/es/client/vue-devtools-bridge.mjs +150 -0
  4. package/es/core/injector.d.ts +2 -0
  5. package/es/core/injector.mjs +15 -0
  6. package/es/core/mcp-chrome.d.ts +56 -0
  7. package/es/core/mcp-chrome.mjs +133 -0
  8. package/es/core/mcp-proxy.d.ts +23 -0
  9. package/es/core/mcp-proxy.mjs +212 -0
  10. package/es/core/mcp-tools.d.ts +15 -0
  11. package/es/core/mcp-tools.mjs +671 -0
  12. package/es/core/provider-loader.d.ts +19 -0
  13. package/es/core/provider-loader.mjs +30 -0
  14. package/es/core/proxy-server.d.ts +12 -0
  15. package/es/core/proxy-server.mjs +148 -0
  16. package/es/core/service.d.ts +39 -0
  17. package/es/core/service.mjs +267 -0
  18. package/es/endpoints/context.d.ts +3 -0
  19. package/es/endpoints/context.mjs +106 -0
  20. package/es/endpoints/index.d.ts +9 -0
  21. package/es/endpoints/index.mjs +30 -0
  22. package/es/endpoints/logs.d.ts +9 -0
  23. package/es/endpoints/logs.mjs +86 -0
  24. package/es/endpoints/mcp.d.ts +6 -0
  25. package/es/endpoints/mcp.mjs +610 -0
  26. package/es/endpoints/sessions.d.ts +3 -0
  27. package/es/endpoints/sessions.mjs +62 -0
  28. package/es/endpoints/sse.d.ts +3 -0
  29. package/es/endpoints/sse.mjs +44 -0
  30. package/es/endpoints/start.d.ts +3 -0
  31. package/es/endpoints/start.mjs +21 -0
  32. package/es/endpoints/types.d.ts +34 -0
  33. package/es/endpoints/types.mjs +0 -0
  34. package/es/endpoints/vue-devtools.d.ts +9 -0
  35. package/es/endpoints/vue-devtools.mjs +107 -0
  36. package/es/endpoints/warmup.d.ts +3 -0
  37. package/es/endpoints/warmup.mjs +42 -0
  38. package/es/endpoints/widget.d.ts +3 -0
  39. package/es/endpoints/widget.mjs +36 -0
  40. package/es/env.d.ts +1 -0
  41. package/es/index.d.ts +22 -0
  42. package/es/index.mjs +342 -0
  43. package/es/utils/format-bridge.d.ts +9 -0
  44. package/es/utils/format-bridge.mjs +80 -0
  45. package/es/utils/paths.d.ts +3 -0
  46. package/es/utils/paths.mjs +45 -0
  47. package/es/utils/system.d.ts +3 -0
  48. package/es/utils/system.mjs +76 -0
  49. package/lib/client/vue-devtools-bridge.cjs +150 -0
  50. package/lib/client/vue-devtools-bridge.d.ts +58 -0
  51. package/lib/client.css +2 -0
  52. package/lib/client.js +6950 -0
  53. package/lib/core/injector.cjs +34 -0
  54. package/lib/core/injector.d.ts +2 -0
  55. package/lib/core/mcp-chrome.cjs +161 -0
  56. package/lib/core/mcp-chrome.d.ts +56 -0
  57. package/lib/core/mcp-proxy.cjs +244 -0
  58. package/lib/core/mcp-proxy.d.ts +23 -0
  59. package/lib/core/mcp-tools.cjs +694 -0
  60. package/lib/core/mcp-tools.d.ts +15 -0
  61. package/lib/core/provider-loader.cjs +53 -0
  62. package/lib/core/provider-loader.d.ts +19 -0
  63. package/lib/core/proxy-server.cjs +181 -0
  64. package/lib/core/proxy-server.d.ts +12 -0
  65. package/lib/core/service.cjs +289 -0
  66. package/lib/core/service.d.ts +39 -0
  67. package/lib/endpoints/context.cjs +129 -0
  68. package/lib/endpoints/context.d.ts +3 -0
  69. package/lib/endpoints/index.cjs +58 -0
  70. package/lib/endpoints/index.d.ts +9 -0
  71. package/lib/endpoints/logs.cjs +107 -0
  72. package/lib/endpoints/logs.d.ts +9 -0
  73. package/lib/endpoints/mcp.cjs +624 -0
  74. package/lib/endpoints/mcp.d.ts +6 -0
  75. package/lib/endpoints/sessions.cjs +85 -0
  76. package/lib/endpoints/sessions.d.ts +3 -0
  77. package/lib/endpoints/sse.cjs +67 -0
  78. package/lib/endpoints/sse.d.ts +3 -0
  79. package/lib/endpoints/start.cjs +44 -0
  80. package/lib/endpoints/start.d.ts +3 -0
  81. package/lib/endpoints/types.cjs +15 -0
  82. package/lib/endpoints/types.d.ts +34 -0
  83. package/lib/endpoints/vue-devtools.cjs +132 -0
  84. package/lib/endpoints/vue-devtools.d.ts +9 -0
  85. package/lib/endpoints/warmup.cjs +65 -0
  86. package/lib/endpoints/warmup.d.ts +3 -0
  87. package/lib/endpoints/widget.cjs +69 -0
  88. package/lib/endpoints/widget.d.ts +3 -0
  89. package/lib/env.d.ts +1 -0
  90. package/lib/index.cjs +361 -0
  91. package/lib/index.d.ts +22 -0
  92. package/lib/utils/format-bridge.cjs +102 -0
  93. package/lib/utils/format-bridge.d.ts +9 -0
  94. package/lib/utils/paths.cjs +80 -0
  95. package/lib/utils/paths.d.ts +3 -0
  96. package/lib/utils/system.cjs +110 -0
  97. package/lib/utils/system.d.ts +3 -0
  98. package/package.json +68 -0
package/es/index.mjs ADDED
@@ -0,0 +1,342 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import Inspector from "unplugin-vue-inspector/vite";
4
+ import {
5
+ CONTEXT_API_PATH,
6
+ DEFAULT_PROXY_PORT,
7
+ MCP_API_PATH,
8
+ resolvePluginConfig,
9
+ setVerbose
10
+ } from "@aipanel/core";
11
+ import { createLogger, initProcessLogCapture } from "@aipanel/core/node";
12
+ import { setupMiddlewares, LOGS_API_PATH, VUE_DEVTOOLS_API_PATH } from "./endpoints/index.mjs";
13
+ import { injectWidget } from "./core/injector.mjs";
14
+ import { loadProvider } from "./core/provider-loader.mjs";
15
+ import { AIPanelService } from "./core/service.mjs";
16
+ import { McpProxy } from "./core/mcp-proxy.mjs";
17
+ import {
18
+ resolveWidgetPath,
19
+ resolveWidgetStylePath,
20
+ resolveVueDevtoolsBridgePath
21
+ } from "./utils/paths.mjs";
22
+ import { findGitRoot } from "./utils/system.mjs";
23
+ const DEVTOOLS_BRIDGE_IMPORTEE = "virtual:aipanel-vue-devtools-bridge";
24
+ const DEVTOOLS_BRIDGE_QUERY = "aipanel_vue_devtools_bridge";
25
+ const BRIDGE_SOURCE_PATH = resolveVueDevtoolsBridgePath();
26
+ const SILENT_CONTEXT_SCRIPT = `<script>
27
+ (function () {
28
+ var API = "/__aipanel_context__";
29
+ var last = "";
30
+ function report(force) {
31
+ if (document.visibilityState !== "visible") return;
32
+ var url = location.href;
33
+ var title = document.title;
34
+ var key = url + "|" + title;
35
+ if (!force && key === last) return;
36
+ last = key;
37
+ var sessionId = "";
38
+ try { sessionId = sessionStorage.getItem("_aipanel_pk") || ""; } catch (e) {}
39
+ fetch(API, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify({ url: url, title: title, sessionId: sessionId }),
43
+ }).catch(function () {});
44
+ }
45
+ var push = history.pushState.bind(history);
46
+ var replace = history.replaceState.bind(history);
47
+ history.pushState = function () { push.apply(this, arguments); setTimeout(function () { report(false); }, 0); };
48
+ history.replaceState = function () { replace.apply(this, arguments); setTimeout(function () { report(false); }, 0); };
49
+ window.addEventListener("popstate", function () { report(false); });
50
+ window.addEventListener("hashchange", function () { report(false); });
51
+ document.addEventListener("visibilitychange", function () {
52
+ // \u5207\u56DE\u672C\u6807\u7B7E\u65F6\u5F3A\u5236\u4E0A\u62A5\uFF1A\u672C\u9875 url/title \u672A\u53D8\uFF0C\u4F46\u670D\u52A1\u7AEF\u4E0A\u4E0B\u6587\u53EF\u80FD\u5DF2\u88AB\u5176\u4ED6\u6807\u7B7E\u8986\u76D6
53
+ if (document.visibilityState === "visible") report(true);
54
+ });
55
+ window.addEventListener("focus", function () {
56
+ // \u591A\u7A97\u53E3\uFF08\u5206\u5C4F/\u591A\u663E\u793A\u5668\uFF09\u4E0B\u6240\u6709\u6807\u7B7E\u9875\u90FD visible\uFF0Cvisibilitychange \u4E0D\u89E6\u53D1\uFF1B
57
+ // \u7A97\u53E3\u83B7\u5F97\u7126\u70B9\u662F"\u7528\u6237\u6B63\u5728\u770B\u8FD9\u4E2A\u9875\u9762"\u7684\u6700\u51C6\u786E\u4FE1\u53F7
58
+ report(true);
59
+ });
60
+ try {
61
+ new MutationObserver(function () { report(false); })
62
+ .observe(document.head, { childList: true, subtree: true });
63
+ } catch (e) {}
64
+ report(false);
65
+ })();
66
+ </script>`;
67
+ function aipanelPlugin(options = {}) {
68
+ const plugins = [];
69
+ plugins.push(
70
+ ...Inspector({
71
+ enabled: false,
72
+ toggleButtonVisibility: "never",
73
+ toggleComboKey: false
74
+ })
75
+ );
76
+ plugins.push(createAIPanelPlugin(options));
77
+ return plugins;
78
+ }
79
+ function createAIPanelPlugin(options = {}) {
80
+ const config = resolvePluginConfig(options);
81
+ setVerbose(config.verbose);
82
+ initProcessLogCapture({ maxSize: 500 });
83
+ const log = createLogger("Plugin");
84
+ let actualWebPort = config.webPort;
85
+ let actualProxyPort = config.proxyPort ?? DEFAULT_PROXY_PORT;
86
+ let projectRoot = "";
87
+ let vueDevtoolsApiUrl = "";
88
+ const pageContext = { url: "", title: "" };
89
+ const DEFAULT_TAB = "default";
90
+ const pageContexts = /* @__PURE__ */ new Map([[DEFAULT_TAB, pageContext]]);
91
+ let activeTabId = DEFAULT_TAB;
92
+ const serviceInstanceId = crypto.randomUUID();
93
+ const sseClients = /* @__PURE__ */ new Set();
94
+ const mcpProxy = new McpProxy({ idleTimeout: 5 * 60 * 1e3 });
95
+ let provider = null;
96
+ const service = new AIPanelService(
97
+ config,
98
+ sseClients,
99
+ (port) => {
100
+ actualWebPort = port;
101
+ },
102
+ (port) => {
103
+ actualProxyPort = port;
104
+ }
105
+ );
106
+ service.workspaceRoot = findGitRoot(process.cwd());
107
+ return {
108
+ name: "vite-plugin-aipanel",
109
+ apply(_viteConfig, env) {
110
+ if (!config.enabled) return false;
111
+ return env.command === "serve" && process.env.NODE_ENV !== "test";
112
+ },
113
+ async configureServer(server) {
114
+ const timer = log.timer("configureServer");
115
+ projectRoot = server.config.root;
116
+ let viteOrigin = "";
117
+ setupMiddlewares(
118
+ server,
119
+ {
120
+ get webUrl() {
121
+ return actualWebPort ? `http://${config.hostname}:${actualWebPort}` : null;
122
+ },
123
+ get sseClients() {
124
+ return sseClients;
125
+ },
126
+ getPageContext() {
127
+ return pageContexts.get(activeTabId) || pageContexts.get(DEFAULT_TAB) || { url: "", title: "" };
128
+ },
129
+ setPageContext(tabId, ctx) {
130
+ pageContexts.set(tabId || DEFAULT_TAB, ctx);
131
+ },
132
+ setActiveTabId(tabId) {
133
+ activeTabId = tabId;
134
+ },
135
+ clearSelectedElements() {
136
+ const ctx = pageContexts.get(activeTabId);
137
+ if (ctx) {
138
+ ctx.selectedElements = [];
139
+ pageContexts.set(activeTabId, ctx);
140
+ }
141
+ const defaultCtx = pageContexts.get(DEFAULT_TAB);
142
+ if (defaultCtx) {
143
+ defaultCtx.selectedElements = [];
144
+ }
145
+ },
146
+ get isServiceStarted() {
147
+ return service.isStarted;
148
+ },
149
+ get currentTask() {
150
+ return service.currentTask;
151
+ },
152
+ get actualProxyPort() {
153
+ return actualProxyPort;
154
+ },
155
+ get actualWebPort() {
156
+ return actualWebPort;
157
+ },
158
+ get serviceInstanceId() {
159
+ return serviceInstanceId;
160
+ },
161
+ getSessions: () => {
162
+ if (!provider) return Promise.reject(new Error("Provider \u672A\u521D\u59CB\u5316"));
163
+ return provider.listSessions(service.workspaceRoot);
164
+ },
165
+ createSession: () => {
166
+ if (!provider) return Promise.reject(new Error("Provider \u672A\u521D\u59CB\u5316"));
167
+ return provider.createSession(service.workspaceRoot);
168
+ },
169
+ deleteSession: (id) => {
170
+ if (!provider) return Promise.reject(new Error("Provider \u672A\u521D\u59CB\u5316"));
171
+ return provider.deleteSession ? provider.deleteSession(id) : Promise.reject(new Error("\u5F53\u524D Provider \u4E0D\u652F\u6301\u5220\u9664\u4F1A\u8BDD"));
172
+ },
173
+ getCapabilities: () => provider?.capabilities ?? {},
174
+ resolveWidgetPath,
175
+ resolveWidgetStylePath,
176
+ retryWarmupChromeMcp: () => service.retryWarmupChromeMcp()
177
+ },
178
+ mcpProxy,
179
+ config.logFiles
180
+ );
181
+ if (!config.mcpOnly) {
182
+ try {
183
+ provider = await loadProvider(config.provider, {
184
+ hostname: config.hostname,
185
+ chromeDevtoolsPort: config.chromeDevtoolsPort,
186
+ getWebPort: () => actualWebPort,
187
+ getProxyPort: () => actualProxyPort,
188
+ options: config
189
+ });
190
+ } catch (e) {
191
+ log.error("\u52A0\u8F7D Web Provider \u5931\u8D25", {
192
+ provider: config.provider,
193
+ error: e instanceof Error ? e.message : String(e)
194
+ });
195
+ service.currentTask = {
196
+ task: "provider_not_installed",
197
+ data: { error: e instanceof Error ? e.message : String(e) }
198
+ };
199
+ timer.end("\u274C Provider \u52A0\u8F7D\u5931\u8D25");
200
+ return;
201
+ }
202
+ provider.applyConfig?.({ theme: config.theme });
203
+ service.setProvider(provider);
204
+ }
205
+ server.httpServer?.on("listening", async () => {
206
+ log.debug("Vite server listening event fired");
207
+ const address = server.httpServer?.address();
208
+ let vitePort;
209
+ let viteHost;
210
+ if (address && typeof address === "object") {
211
+ vitePort = address.port;
212
+ const addr = address.address;
213
+ if (addr === "::" || addr === "::1" || addr === "0.0.0.0" || !addr) {
214
+ viteHost = "localhost";
215
+ } else {
216
+ viteHost = addr;
217
+ }
218
+ } else {
219
+ const host = server.config.server.host;
220
+ vitePort = server.config.server.port || 5173;
221
+ viteHost = typeof host === "string" && host !== "0.0.0.0" && host !== "::" && host !== "::1" ? host : "localhost";
222
+ }
223
+ viteOrigin = `http://${viteHost}:${vitePort}`;
224
+ const contextApiUrl = `http://${viteHost}:${vitePort}${CONTEXT_API_PATH}`;
225
+ const logsApiUrl = `http://${viteHost}:${vitePort}${LOGS_API_PATH}`;
226
+ vueDevtoolsApiUrl = `http://${viteHost}:${vitePort}${VUE_DEVTOOLS_API_PATH}`;
227
+ const mcpApiUrl = `http://${viteHost}:${vitePort}${MCP_API_PATH}`;
228
+ log.debug("Vite server ready", {
229
+ vitePort,
230
+ viteHost,
231
+ viteOrigin,
232
+ contextApiUrl,
233
+ logsApiUrl,
234
+ vueDevtoolsApiUrl,
235
+ mcpApiUrl
236
+ });
237
+ log.info(`MCP endpoint: ${mcpApiUrl}`);
238
+ try {
239
+ await mcpProxy.start();
240
+ if (config.warmupChromeMcp) {
241
+ const warmup = await mcpProxy.verify();
242
+ if (!warmup.ok) {
243
+ log.debug("Chrome MCP warmup failed", { error: warmup.error });
244
+ }
245
+ }
246
+ if (config.mcpOnly) {
247
+ return;
248
+ }
249
+ await service.start(
250
+ vitePort,
251
+ [viteOrigin],
252
+ contextApiUrl,
253
+ logsApiUrl,
254
+ viteOrigin,
255
+ mcpProxy,
256
+ vueDevtoolsApiUrl
257
+ );
258
+ } catch (e) {
259
+ log.error("Failed to start services", { error: e });
260
+ }
261
+ });
262
+ server.httpServer?.on("close", () => {
263
+ log.debug("HTTP server closing");
264
+ service.stop();
265
+ mcpProxy.stop();
266
+ });
267
+ const cleanup = async () => {
268
+ log.debug("Process cleanup triggered");
269
+ mcpProxy.stop();
270
+ await service.stop();
271
+ process.exit(0);
272
+ };
273
+ process.on("SIGINT", cleanup);
274
+ process.on("SIGTERM", cleanup);
275
+ timer.end("\u2713 Server configured");
276
+ },
277
+ resolveId(id) {
278
+ if (id === DEVTOOLS_BRIDGE_IMPORTEE) {
279
+ return `${BRIDGE_SOURCE_PATH}?${DEVTOOLS_BRIDGE_QUERY}`;
280
+ }
281
+ return void 0;
282
+ },
283
+ load(id) {
284
+ const [filePath, query] = id.split("?", 2);
285
+ if (query === DEVTOOLS_BRIDGE_QUERY) {
286
+ return fs.readFileSync(filePath, "utf-8");
287
+ }
288
+ return void 0;
289
+ },
290
+ transformIndexHtml(html) {
291
+ const timer = log.timer("transformIndexHtml");
292
+ const tags = [
293
+ {
294
+ tag: "script",
295
+ injectTo: "head-prepend",
296
+ attrs: {
297
+ type: "module",
298
+ src: `/@id/${DEVTOOLS_BRIDGE_IMPORTEE}`
299
+ }
300
+ }
301
+ ];
302
+ const titleInject = `<script>
303
+ (function () {
304
+ var KEY = "_aipanel_pk";
305
+ if (!sessionStorage.getItem(KEY)) {
306
+ sessionStorage.setItem(KEY, "[" + Math.random().toString(36).slice(2, 10) + "]");
307
+ }
308
+ })();
309
+ </script>`;
310
+ if (config.mcpOnly) {
311
+ timer.end("\u2713 mcp-only (skip widget bubble, keep vue-devtools bridge + context report)");
312
+ return {
313
+ html: html.replace("</body>", `${titleInject}
314
+ ${SILENT_CONTEXT_SCRIPT}</body>`),
315
+ tags
316
+ };
317
+ }
318
+ const widget = injectWidget({
319
+ theme: config.theme,
320
+ open: config.open,
321
+ hotkey: config.hotkey,
322
+ proxyPort: actualProxyPort,
323
+ proxyHost: config.hostname,
324
+ displayMode: config.displayMode === "extension" ? "extension-selector" : config.displayMode,
325
+ splitMode: config.splitMode,
326
+ serviceInstanceId,
327
+ webPort: actualWebPort,
328
+ projectRoot,
329
+ verbose: config.verbose
330
+ });
331
+ timer.end();
332
+ return {
333
+ html: html.replace("</body>", `${titleInject}
334
+ ${widget}</body>`),
335
+ tags
336
+ };
337
+ }
338
+ };
339
+ }
340
+ export {
341
+ aipanelPlugin as default
342
+ };
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenCode VS Code 格式化桥接脚本
4
+ *
5
+ * VS Code 扩展可用时通过 HTTP API 格式化,不可用时直接返回(由 OpenCode 内置 prettier 兜底)。
6
+ *
7
+ * 用法: node format-bridge.cjs <filePath>
8
+ */
9
+ export {};
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import http from "http";
4
+ import path from "path";
5
+ const filePath = process.argv[2];
6
+ if (!filePath) {
7
+ console.error("Usage: node format-bridge.cjs <filePath>");
8
+ process.exit(1);
9
+ }
10
+ const absPath = path.resolve(filePath);
11
+ const PORT = parseInt(process.env.AIPANEL_VSCODE_PORT || "", 10) || 51939;
12
+ function touchFile() {
13
+ const now = /* @__PURE__ */ new Date();
14
+ fs.utimesSync(absPath, now, now);
15
+ }
16
+ function probeHealth() {
17
+ return new Promise((resolve) => {
18
+ const req = http.get(`http://127.0.0.1:${PORT}/health`, (res) => {
19
+ res.resume();
20
+ resolve(res.statusCode === 200);
21
+ });
22
+ req.setTimeout(500, () => {
23
+ req.destroy();
24
+ resolve(false);
25
+ });
26
+ req.on("error", () => resolve(false));
27
+ });
28
+ }
29
+ function formatViaExtension() {
30
+ return new Promise((resolve, reject) => {
31
+ const body = JSON.stringify({ filePath: absPath });
32
+ const req = http.request(
33
+ {
34
+ hostname: "127.0.0.1",
35
+ port: PORT,
36
+ path: "/format",
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ timeout: 5e3
40
+ },
41
+ (res) => {
42
+ const chunks = [];
43
+ res.on("data", (c) => chunks.push(c));
44
+ res.on("end", () => {
45
+ try {
46
+ const result = JSON.parse(Buffer.concat(chunks).toString());
47
+ if (result.error) {
48
+ reject(new Error(result.error));
49
+ } else {
50
+ resolve(result);
51
+ }
52
+ } catch (e) {
53
+ reject(e);
54
+ }
55
+ });
56
+ }
57
+ );
58
+ req.on("error", reject);
59
+ req.on("timeout", () => {
60
+ req.destroy();
61
+ reject(new Error("Timeout"));
62
+ });
63
+ req.write(body);
64
+ req.end();
65
+ });
66
+ }
67
+ async function main() {
68
+ const healthy = await probeHealth();
69
+ if (!healthy) {
70
+ touchFile();
71
+ process.exit(0);
72
+ }
73
+ try {
74
+ await formatViaExtension();
75
+ } catch {
76
+ }
77
+ touchFile();
78
+ process.exit(0);
79
+ }
80
+ main();
@@ -0,0 +1,3 @@
1
+ export declare function resolveWidgetPath(): string;
2
+ export declare function resolveWidgetStylePath(): string;
3
+ export declare function resolveVueDevtoolsBridgePath(): string;
@@ -0,0 +1,45 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { resolvePackageDir } from "@aipanel/core/node";
4
+ const packageDir = resolvePackageDir("vite-plugin-aipanel");
5
+ function resolveWidgetPath() {
6
+ const candidatePaths = [
7
+ path.join(packageDir, "es", "client.js"),
8
+ path.join(packageDir, "lib", "client.js")
9
+ ];
10
+ for (const candidatePath of candidatePaths) {
11
+ if (fs.existsSync(candidatePath)) {
12
+ return candidatePath;
13
+ }
14
+ }
15
+ return candidatePaths[0];
16
+ }
17
+ function resolveWidgetStylePath() {
18
+ const candidatePaths = [
19
+ path.join(packageDir, "es", "client.css"),
20
+ path.join(packageDir, "lib", "client.css")
21
+ ];
22
+ for (const candidatePath of candidatePaths) {
23
+ if (fs.existsSync(candidatePath)) {
24
+ return candidatePath;
25
+ }
26
+ }
27
+ return candidatePaths[0];
28
+ }
29
+ function resolveVueDevtoolsBridgePath() {
30
+ const candidatePaths = [
31
+ path.join(packageDir, "es", "client", "vue-devtools-bridge.mjs"),
32
+ path.join(packageDir, "lib", "client", "vue-devtools-bridge.cjs")
33
+ ];
34
+ for (const candidatePath of candidatePaths) {
35
+ if (fs.existsSync(candidatePath)) {
36
+ return candidatePath;
37
+ }
38
+ }
39
+ return candidatePaths[0];
40
+ }
41
+ export {
42
+ resolveVueDevtoolsBridgePath,
43
+ resolveWidgetPath,
44
+ resolveWidgetStylePath
45
+ };
@@ -0,0 +1,3 @@
1
+ import type { ResultPromise } from "execa";
2
+ export declare function waitForServer(url: string, timeout?: number, process?: ResultPromise): Promise<void>;
3
+ export declare function findGitRoot(startDir: string, maxDepth?: number): string;
@@ -0,0 +1,76 @@
1
+ import fs from "fs";
2
+ import http from "http";
3
+ import path from "path";
4
+ import { SERVER_CHECK_INTERVAL } from "@aipanel/core";
5
+ import { PerformanceTimer, createLogger } from "@aipanel/core/node";
6
+ const log = createLogger("Utils");
7
+ function waitForServer(url, timeout = 1e4, process) {
8
+ const timer = new PerformanceTimer("waitForServer", { url, timeout });
9
+ return new Promise((resolve, reject) => {
10
+ const startTime = Date.now();
11
+ let attempts = 0;
12
+ const check = () => {
13
+ attempts++;
14
+ log.debug(`Checking server availability (attempt ${attempts})`, { url });
15
+ if (process?.exitCode !== null && process?.exitCode !== void 0) {
16
+ timer.end(`\u274C Process exited with code ${process.exitCode}`);
17
+ reject(new Error(`Process exited with code ${process.exitCode}`));
18
+ return;
19
+ }
20
+ const req = http.get(url, (res) => {
21
+ if (res.statusCode && res.statusCode < 500) {
22
+ timer.end(`\u2713 Server ready after ${attempts} attempts`);
23
+ resolve();
24
+ } else {
25
+ log.debug(`Server returned status ${res.statusCode}, retrying...`);
26
+ retryOrReject();
27
+ }
28
+ });
29
+ req.on("error", (err) => {
30
+ log.debug(`Server check failed: ${err.message}`);
31
+ retryOrReject();
32
+ });
33
+ };
34
+ const retryOrReject = () => {
35
+ const elapsed = Date.now() - startTime;
36
+ if (elapsed < timeout) {
37
+ setTimeout(check, SERVER_CHECK_INTERVAL);
38
+ } else {
39
+ timer.end("\u274C Timeout");
40
+ reject(new Error(`Server not ready after ${timeout}ms (${attempts} attempts)`));
41
+ }
42
+ };
43
+ check();
44
+ });
45
+ }
46
+ function findGitRoot(startDir, maxDepth = 10) {
47
+ const timer = log.timer("findGitRoot", { startDir, maxDepth });
48
+ let currentDir = startDir;
49
+ let depth = 0;
50
+ while (depth < maxDepth) {
51
+ const gitDir = path.join(currentDir, ".git");
52
+ try {
53
+ if (fs.existsSync(gitDir)) {
54
+ timer.end(`\u2713 Found git root at depth ${depth}: ${currentDir}`);
55
+ return currentDir;
56
+ }
57
+ } catch (err) {
58
+ log.debug(`Error checking .git directory at ${currentDir}`, {
59
+ error: err.message
60
+ });
61
+ }
62
+ const parentDir = path.dirname(currentDir);
63
+ if (parentDir === currentDir) {
64
+ log.debug("Reached filesystem root");
65
+ break;
66
+ }
67
+ currentDir = parentDir;
68
+ depth++;
69
+ }
70
+ timer.end(`\u274C No git root found after ${depth} levels, using start directory`);
71
+ return startDir;
72
+ }
73
+ export {
74
+ findGitRoot,
75
+ waitForServer
76
+ };