yo-bug 0.1.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.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/cli.ts +17 -0
- package/bin/install.ts +34 -0
- package/bin/mcp.ts +2 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +19 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bin/install.d.ts +3 -0
- package/dist/bin/install.d.ts.map +1 -0
- package/dist/bin/install.js +28 -0
- package/dist/bin/install.js.map +1 -0
- package/dist/bin/mcp.d.ts +3 -0
- package/dist/bin/mcp.d.ts.map +1 -0
- package/dist/bin/mcp.js +3 -0
- package/dist/bin/mcp.js.map +1 -0
- package/dist/src/detect/dev-server.d.ts +11 -0
- package/dist/src/detect/dev-server.d.ts.map +1 -0
- package/dist/src/detect/dev-server.js +92 -0
- package/dist/src/detect/dev-server.js.map +1 -0
- package/dist/src/detect/spawn-dev.d.ts +4 -0
- package/dist/src/detect/spawn-dev.d.ts.map +1 -0
- package/dist/src/detect/spawn-dev.js +37 -0
- package/dist/src/detect/spawn-dev.js.map +1 -0
- package/dist/src/install/detect-ai-tool.d.ts +7 -0
- package/dist/src/install/detect-ai-tool.d.ts.map +1 -0
- package/dist/src/install/detect-ai-tool.js +38 -0
- package/dist/src/install/detect-ai-tool.js.map +1 -0
- package/dist/src/install/write-mcp-config.d.ts +6 -0
- package/dist/src/install/write-mcp-config.d.ts.map +1 -0
- package/dist/src/install/write-mcp-config.js +47 -0
- package/dist/src/install/write-mcp-config.js.map +1 -0
- package/dist/src/mcp/index.d.ts +2 -0
- package/dist/src/mcp/index.d.ts.map +1 -0
- package/dist/src/mcp/index.js +263 -0
- package/dist/src/mcp/index.js.map +1 -0
- package/dist/src/mcp/tools/checklist.d.ts +22 -0
- package/dist/src/mcp/tools/checklist.d.ts.map +1 -0
- package/dist/src/mcp/tools/checklist.js +58 -0
- package/dist/src/mcp/tools/checklist.js.map +1 -0
- package/dist/src/mcp/tools/get-feedback.d.ts +13 -0
- package/dist/src/mcp/tools/get-feedback.d.ts.map +1 -0
- package/dist/src/mcp/tools/get-feedback.js +27 -0
- package/dist/src/mcp/tools/get-feedback.js.map +1 -0
- package/dist/src/mcp/tools/list-feedbacks.d.ts +20 -0
- package/dist/src/mcp/tools/list-feedbacks.d.ts.map +1 -0
- package/dist/src/mcp/tools/list-feedbacks.js +29 -0
- package/dist/src/mcp/tools/list-feedbacks.js.map +1 -0
- package/dist/src/mcp/tools/resolve-feedback.d.ts +8 -0
- package/dist/src/mcp/tools/resolve-feedback.d.ts.map +1 -0
- package/dist/src/mcp/tools/resolve-feedback.js +28 -0
- package/dist/src/mcp/tools/resolve-feedback.js.map +1 -0
- package/dist/src/mcp/tools/start-session.d.ts +11 -0
- package/dist/src/mcp/tools/start-session.d.ts.map +1 -0
- package/dist/src/mcp/tools/start-session.js +56 -0
- package/dist/src/mcp/tools/start-session.js.map +1 -0
- package/dist/src/mcp/tools/stop-session.d.ts +6 -0
- package/dist/src/mcp/tools/stop-session.d.ts.map +1 -0
- package/dist/src/mcp/tools/stop-session.js +80 -0
- package/dist/src/mcp/tools/stop-session.js.map +1 -0
- package/dist/src/mcp/tools/test-history.d.ts +33 -0
- package/dist/src/mcp/tools/test-history.d.ts.map +1 -0
- package/dist/src/mcp/tools/test-history.js +66 -0
- package/dist/src/mcp/tools/test-history.js.map +1 -0
- package/dist/src/server/feedback-api.d.ts +4 -0
- package/dist/src/server/feedback-api.d.ts.map +1 -0
- package/dist/src/server/feedback-api.js +152 -0
- package/dist/src/server/feedback-api.js.map +1 -0
- package/dist/src/server/html-injector.d.ts +10 -0
- package/dist/src/server/html-injector.d.ts.map +1 -0
- package/dist/src/server/html-injector.js +34 -0
- package/dist/src/server/html-injector.js.map +1 -0
- package/dist/src/server/proxy-server.d.ts +8 -0
- package/dist/src/server/proxy-server.d.ts.map +1 -0
- package/dist/src/server/proxy-server.js +87 -0
- package/dist/src/server/proxy-server.js.map +1 -0
- package/dist/src/server/sdk-serve.d.ts +3 -0
- package/dist/src/server/sdk-serve.d.ts.map +1 -0
- package/dist/src/server/sdk-serve.js +20 -0
- package/dist/src/server/sdk-serve.js.map +1 -0
- package/dist/src/storage/store.d.ts +21 -0
- package/dist/src/storage/store.d.ts.map +1 -0
- package/dist/src/storage/store.js +138 -0
- package/dist/src/storage/store.js.map +1 -0
- package/dist/src/storage/types.d.ts +74 -0
- package/dist/src/storage/types.d.ts.map +1 -0
- package/dist/src/storage/types.js +2 -0
- package/dist/src/storage/types.js.map +1 -0
- package/dist/vibe-feedback.js +399 -0
- package/package.json +67 -0
- package/src/client/annotation-mode/canvas-editor.ts +178 -0
- package/src/client/annotation-mode/history.ts +32 -0
- package/src/client/annotation-mode/region-selector.ts +123 -0
- package/src/client/annotation-mode/screenshot.ts +17 -0
- package/src/client/annotation-mode/toolbar.ts +139 -0
- package/src/client/annotation-mode/tools/arrow.ts +57 -0
- package/src/client/annotation-mode/tools/base-tool.ts +25 -0
- package/src/client/annotation-mode/tools/circle.ts +37 -0
- package/src/client/annotation-mode/tools/freehand.ts +23 -0
- package/src/client/annotation-mode/tools/rect.ts +32 -0
- package/src/client/annotation-mode/tools/text.ts +93 -0
- package/src/client/api/client.ts +48 -0
- package/src/client/capture/action-recorder.ts +157 -0
- package/src/client/capture/console-interceptor.ts +65 -0
- package/src/client/capture/context-buffer.ts +23 -0
- package/src/client/capture/error-interceptor.ts +52 -0
- package/src/client/capture/network-interceptor.ts +143 -0
- package/src/client/core/event-bus.ts +20 -0
- package/src/client/core/i18n.ts +83 -0
- package/src/client/core/sdk.ts +373 -0
- package/src/client/core/shadow-host.ts +27 -0
- package/src/client/element-mode/highlighter.ts +79 -0
- package/src/client/element-mode/inspector.ts +73 -0
- package/src/client/element-mode/selector.ts +22 -0
- package/src/client/index.ts +10 -0
- package/src/client/styles/sdk.css.ts +222 -0
- package/src/client/ui/checklist-panel.ts +279 -0
- package/src/client/ui/feedback-panel.ts +149 -0
- package/src/client/ui/floating-button.ts +103 -0
- package/src/client/ui/toast.ts +17 -0
- package/src/client/ui/verify-panel.ts +111 -0
- package/src/detect/dev-server.ts +110 -0
- package/src/detect/spawn-dev.ts +50 -0
- package/src/install/detect-ai-tool.ts +49 -0
- package/src/install/write-mcp-config.ts +49 -0
- package/src/mcp/index.ts +327 -0
- package/src/mcp/tools/checklist.ts +61 -0
- package/src/mcp/tools/get-feedback.ts +34 -0
- package/src/mcp/tools/list-feedbacks.ts +37 -0
- package/src/mcp/tools/resolve-feedback.ts +34 -0
- package/src/mcp/tools/start-session.ts +65 -0
- package/src/mcp/tools/stop-session.ts +93 -0
- package/src/mcp/tools/test-history.ts +97 -0
- package/src/server/feedback-api.ts +164 -0
- package/src/server/html-injector.ts +41 -0
- package/src/server/proxy-server.ts +107 -0
- package/src/server/sdk-serve.ts +24 -0
- package/src/storage/store.ts +172 -0
- package/src/storage/types.ts +68 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { t } from '../core/i18n.js';
|
|
2
|
+
|
|
3
|
+
const SEEN_KEY = 'yo-bug-hint-seen';
|
|
4
|
+
|
|
5
|
+
export class FloatingButton {
|
|
6
|
+
private el: HTMLButtonElement;
|
|
7
|
+
private recDot: HTMLDivElement;
|
|
8
|
+
private active = false;
|
|
9
|
+
|
|
10
|
+
constructor(shadowRoot: ShadowRoot, private onClick: () => void) {
|
|
11
|
+
this.el = document.createElement('button');
|
|
12
|
+
this.el.className = 'vf-fab';
|
|
13
|
+
this.el.innerHTML = '👁'; // eye
|
|
14
|
+
this.el.title = 'yo-bug';
|
|
15
|
+
this.el.addEventListener('click', () => {
|
|
16
|
+
this.dismissHint();
|
|
17
|
+
this.onClick();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Recording indicator dot
|
|
21
|
+
this.recDot = document.createElement('div');
|
|
22
|
+
this.recDot.style.cssText = `
|
|
23
|
+
position: absolute; top: 2px; right: 2px;
|
|
24
|
+
width: 10px; height: 10px; border-radius: 50%;
|
|
25
|
+
background: #ef4444; border: 2px solid #1e293b;
|
|
26
|
+
animation: vf-pulse 1.5s infinite;
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const style = document.createElement('style');
|
|
30
|
+
style.textContent = `
|
|
31
|
+
@keyframes vf-pulse {
|
|
32
|
+
0%, 100% { opacity: 1; }
|
|
33
|
+
50% { opacity: 0.4; }
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
shadowRoot.appendChild(style);
|
|
37
|
+
|
|
38
|
+
this.el.style.position = 'relative';
|
|
39
|
+
this.el.appendChild(this.recDot);
|
|
40
|
+
shadowRoot.appendChild(this.el);
|
|
41
|
+
|
|
42
|
+
// First-time hint bubble
|
|
43
|
+
this.showHintIfNeeded(shadowRoot);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setActive(active: boolean): void {
|
|
47
|
+
this.active = active;
|
|
48
|
+
this.el.classList.toggle('active', active);
|
|
49
|
+
this.el.innerHTML = active ? '✕' : '👁';
|
|
50
|
+
this.el.appendChild(this.recDot);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
isActive(): boolean {
|
|
54
|
+
return this.active;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
destroy(): void {
|
|
58
|
+
this.el.remove();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private hint: HTMLDivElement | null = null;
|
|
62
|
+
|
|
63
|
+
private showHintIfNeeded(shadowRoot: ShadowRoot): void {
|
|
64
|
+
try {
|
|
65
|
+
if (localStorage.getItem(SEEN_KEY)) return;
|
|
66
|
+
} catch {}
|
|
67
|
+
|
|
68
|
+
this.hint = document.createElement('div');
|
|
69
|
+
this.hint.style.cssText = `
|
|
70
|
+
position: fixed; bottom: 82px; right: 20px;
|
|
71
|
+
background: #1e293b; color: #e2e8f0;
|
|
72
|
+
padding: 10px 16px; border-radius: 10px;
|
|
73
|
+
font-size: 13px; pointer-events: auto;
|
|
74
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.35);
|
|
75
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
76
|
+
animation: vf-slide-in 0.3s ease-out;
|
|
77
|
+
max-width: 200px; line-height: 1.5;
|
|
78
|
+
`;
|
|
79
|
+
this.hint.textContent = t('hint.firstTime');
|
|
80
|
+
|
|
81
|
+
// Arrow pointing down to the button
|
|
82
|
+
const arrow = document.createElement('div');
|
|
83
|
+
arrow.style.cssText = `
|
|
84
|
+
position: absolute; bottom: -6px; right: 22px;
|
|
85
|
+
width: 12px; height: 12px; background: #1e293b;
|
|
86
|
+
transform: rotate(45deg);
|
|
87
|
+
`;
|
|
88
|
+
this.hint.appendChild(arrow);
|
|
89
|
+
|
|
90
|
+
shadowRoot.appendChild(this.hint);
|
|
91
|
+
|
|
92
|
+
// Auto-dismiss after 5s
|
|
93
|
+
setTimeout(() => this.dismissHint(), 5000);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private dismissHint(): void {
|
|
97
|
+
if (this.hint) {
|
|
98
|
+
this.hint.remove();
|
|
99
|
+
this.hint = null;
|
|
100
|
+
try { localStorage.setItem(SEEN_KEY, '1'); } catch {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function showToast(
|
|
2
|
+
shadowRoot: ShadowRoot,
|
|
3
|
+
message: string,
|
|
4
|
+
isError = false,
|
|
5
|
+
duration = 3000
|
|
6
|
+
): void {
|
|
7
|
+
const el = document.createElement('div');
|
|
8
|
+
el.className = `vf-toast${isError ? ' error' : ''}`;
|
|
9
|
+
el.textContent = message;
|
|
10
|
+
shadowRoot.appendChild(el);
|
|
11
|
+
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
el.style.opacity = '0';
|
|
14
|
+
el.style.transition = 'opacity 0.3s';
|
|
15
|
+
setTimeout(() => el.remove(), 300);
|
|
16
|
+
}, duration);
|
|
17
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { t } from '../core/i18n.js';
|
|
2
|
+
|
|
3
|
+
export class VerifyPanel {
|
|
4
|
+
private container: HTMLDivElement;
|
|
5
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
6
|
+
private baseUrl: string;
|
|
7
|
+
private shownIds = new Set<string>();
|
|
8
|
+
|
|
9
|
+
constructor(private shadowRoot: ShadowRoot) {
|
|
10
|
+
this.baseUrl = window.location.origin;
|
|
11
|
+
this.container = document.createElement('div');
|
|
12
|
+
this.container.style.cssText = `
|
|
13
|
+
position: fixed; bottom: 84px; left: 16px; width: 300px;
|
|
14
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
15
|
+
pointer-events: none; z-index: 2147483647;
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
17
|
+
`;
|
|
18
|
+
shadowRoot.appendChild(this.container);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
startPolling(): void {
|
|
22
|
+
this.pollTimer = setInterval(() => this.poll(), 3000);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
stopPolling(): void {
|
|
26
|
+
if (this.pollTimer) {
|
|
27
|
+
clearInterval(this.pollTimer);
|
|
28
|
+
this.pollTimer = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async poll(): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${this.baseUrl}/api/verify`);
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
for (const item of data.items || []) {
|
|
37
|
+
if (!this.shownIds.has(item.feedbackId)) {
|
|
38
|
+
this.shownIds.add(item.feedbackId);
|
|
39
|
+
this.showVerifyCard(item);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private showVerifyCard(item: any): void {
|
|
46
|
+
const card = document.createElement('div');
|
|
47
|
+
card.style.cssText = `
|
|
48
|
+
background: #1e293b; border-radius: 10px; padding: 14px 16px;
|
|
49
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.35); pointer-events: auto;
|
|
50
|
+
color: #e2e8f0; font-size: 13px;
|
|
51
|
+
border-left: 3px solid #f59e0b;
|
|
52
|
+
animation: vf-slide-in 0.3s ease-out;
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const title = document.createElement('div');
|
|
56
|
+
title.style.cssText = `font-weight:600;color:#f59e0b;margin-bottom:6px;font-size:12px;`;
|
|
57
|
+
title.textContent = t('verify.title');
|
|
58
|
+
card.appendChild(title);
|
|
59
|
+
|
|
60
|
+
const desc = document.createElement('div');
|
|
61
|
+
desc.style.cssText = `margin-bottom:10px;line-height:1.4;`;
|
|
62
|
+
desc.textContent = item.description || `${item.problemType} on ${item.element || 'page'}`;
|
|
63
|
+
card.appendChild(desc);
|
|
64
|
+
|
|
65
|
+
const btnRow = document.createElement('div');
|
|
66
|
+
btnRow.style.cssText = `display:flex;gap:8px;`;
|
|
67
|
+
|
|
68
|
+
const passBtn = document.createElement('button');
|
|
69
|
+
passBtn.style.cssText = `
|
|
70
|
+
flex:1; padding:6px; border:none; border-radius:6px;
|
|
71
|
+
background:#22c55e; color:white; font-size:12px; cursor:pointer; font-weight:500;
|
|
72
|
+
`;
|
|
73
|
+
passBtn.textContent = '\u2713 ' + t('verify.fixed');
|
|
74
|
+
passBtn.addEventListener('click', () => {
|
|
75
|
+
this.respond(item.feedbackId, true);
|
|
76
|
+
card.remove();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const failBtn = document.createElement('button');
|
|
80
|
+
failBtn.style.cssText = `
|
|
81
|
+
flex:1; padding:6px; border:none; border-radius:6px;
|
|
82
|
+
background:#ef4444; color:white; font-size:12px; cursor:pointer; font-weight:500;
|
|
83
|
+
`;
|
|
84
|
+
failBtn.textContent = '\u2717 ' + t('verify.broken');
|
|
85
|
+
failBtn.addEventListener('click', () => {
|
|
86
|
+
this.respond(item.feedbackId, false);
|
|
87
|
+
card.remove();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
btnRow.appendChild(passBtn);
|
|
91
|
+
btnRow.appendChild(failBtn);
|
|
92
|
+
card.appendChild(btnRow);
|
|
93
|
+
|
|
94
|
+
this.container.appendChild(card);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async respond(feedbackId: string, confirmed: boolean): Promise<void> {
|
|
98
|
+
try {
|
|
99
|
+
await fetch(`${this.baseUrl}/api/verify/${feedbackId}`, {
|
|
100
|
+
method: 'PATCH',
|
|
101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({ confirmed }),
|
|
103
|
+
});
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
destroy(): void {
|
|
108
|
+
this.stopPolling();
|
|
109
|
+
this.container.remove();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import net from 'net';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
interface DevServerInfo {
|
|
6
|
+
framework: string;
|
|
7
|
+
port: number;
|
|
8
|
+
command: string;
|
|
9
|
+
isRunning: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FRAMEWORK_PORTS: Record<string, { port: number; keywords: string[] }> = {
|
|
13
|
+
vite: { port: 5173, keywords: ['vite'] },
|
|
14
|
+
next: { port: 3000, keywords: ['next'] },
|
|
15
|
+
'create-react-app': { port: 3000, keywords: ['react-scripts'] },
|
|
16
|
+
webpack: { port: 8080, keywords: ['webpack serve', 'webpack-dev-server'] },
|
|
17
|
+
nuxt: { port: 3000, keywords: ['nuxt'] },
|
|
18
|
+
angular: { port: 4200, keywords: ['ng serve'] },
|
|
19
|
+
svelte: { port: 5173, keywords: ['svelte-kit', 'vite'] },
|
|
20
|
+
astro: { port: 4321, keywords: ['astro'] },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function detectDevServer(
|
|
24
|
+
cwd: string,
|
|
25
|
+
manualPort?: number
|
|
26
|
+
): Promise<DevServerInfo> {
|
|
27
|
+
// Read package.json
|
|
28
|
+
let devCommand = '';
|
|
29
|
+
let framework = 'unknown';
|
|
30
|
+
let port = manualPort || 0;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
34
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
35
|
+
const scripts = pkg.scripts || {};
|
|
36
|
+
|
|
37
|
+
// Check "dev" or "start" scripts
|
|
38
|
+
devCommand = scripts.dev || scripts.start || '';
|
|
39
|
+
|
|
40
|
+
// Match framework
|
|
41
|
+
for (const [name, info] of Object.entries(FRAMEWORK_PORTS)) {
|
|
42
|
+
if (info.keywords.some((kw) => devCommand.includes(kw))) {
|
|
43
|
+
framework = name;
|
|
44
|
+
if (!manualPort) port = info.port;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Also check dependencies for framework detection
|
|
50
|
+
if (framework === 'unknown') {
|
|
51
|
+
const allDeps = {
|
|
52
|
+
...pkg.dependencies,
|
|
53
|
+
...pkg.devDependencies,
|
|
54
|
+
};
|
|
55
|
+
for (const [name, info] of Object.entries(FRAMEWORK_PORTS)) {
|
|
56
|
+
if (info.keywords.some((kw) => kw in allDeps)) {
|
|
57
|
+
framework = name;
|
|
58
|
+
if (!manualPort) port = info.port;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// No package.json found
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Default port
|
|
68
|
+
if (!port) port = 3000;
|
|
69
|
+
|
|
70
|
+
// Check if port is already in use
|
|
71
|
+
const isRunning = await isPortInUse(port);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
framework,
|
|
75
|
+
port,
|
|
76
|
+
command: devCommand,
|
|
77
|
+
isRunning,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isPortInUse(port: number): Promise<boolean> {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const socket = new net.Socket();
|
|
84
|
+
socket.setTimeout(1000);
|
|
85
|
+
socket.on('connect', () => {
|
|
86
|
+
socket.destroy();
|
|
87
|
+
resolve(true);
|
|
88
|
+
});
|
|
89
|
+
socket.on('timeout', () => {
|
|
90
|
+
socket.destroy();
|
|
91
|
+
resolve(false);
|
|
92
|
+
});
|
|
93
|
+
socket.on('error', () => {
|
|
94
|
+
resolve(false);
|
|
95
|
+
});
|
|
96
|
+
socket.connect(port, '127.0.0.1');
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function waitForPort(
|
|
101
|
+
port: number,
|
|
102
|
+
timeout = 30000
|
|
103
|
+
): Promise<boolean> {
|
|
104
|
+
const start = Date.now();
|
|
105
|
+
while (Date.now() - start < timeout) {
|
|
106
|
+
if (await isPortInUse(port)) return true;
|
|
107
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
+
import { waitForPort } from './dev-server.js';
|
|
3
|
+
|
|
4
|
+
let devProcess: ChildProcess | null = null;
|
|
5
|
+
|
|
6
|
+
export async function spawnDevServer(
|
|
7
|
+
cwd: string,
|
|
8
|
+
command: string,
|
|
9
|
+
port: number
|
|
10
|
+
): Promise<boolean> {
|
|
11
|
+
const isWindows = process.platform === 'win32';
|
|
12
|
+
const npmCmd = isWindows ? 'npm.cmd' : 'npm';
|
|
13
|
+
|
|
14
|
+
// Use the detected script name (e.g., "dev" or "start")
|
|
15
|
+
const scriptName = command.includes('start') ? 'start' : 'dev';
|
|
16
|
+
|
|
17
|
+
devProcess = spawn(npmCmd, ['run', scriptName], {
|
|
18
|
+
cwd,
|
|
19
|
+
shell: isWindows,
|
|
20
|
+
stdio: 'pipe', // Don't inherit — we're an MCP server using stdio
|
|
21
|
+
env: { ...process.env },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
devProcess.stdout?.on('data', () => {
|
|
25
|
+
// Silently consume output — MCP server uses stdio
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
devProcess.stderr?.on('data', () => {
|
|
29
|
+
// Silently consume stderr
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
devProcess.on('error', () => {
|
|
33
|
+
// Silently handle — MCP server uses stdio, can't write to stderr
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Wait for the port to become available
|
|
37
|
+
const ready = await waitForPort(port, 30000);
|
|
38
|
+
return ready;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function killDevServer(): void {
|
|
42
|
+
if (devProcess) {
|
|
43
|
+
devProcess.kill();
|
|
44
|
+
devProcess = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isDevServerSpawned(): boolean {
|
|
49
|
+
return devProcess !== null && !devProcess.killed;
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface AITool {
|
|
5
|
+
name: string;
|
|
6
|
+
configPath: string;
|
|
7
|
+
detected: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
11
|
+
|
|
12
|
+
export async function detectAITools(): Promise<AITool[]> {
|
|
13
|
+
const tools: AITool[] = [];
|
|
14
|
+
|
|
15
|
+
// Claude Code
|
|
16
|
+
const claudeDir = path.join(HOME, '.claude');
|
|
17
|
+
tools.push({
|
|
18
|
+
name: 'Claude Code',
|
|
19
|
+
configPath: path.join(claudeDir, 'settings.json'),
|
|
20
|
+
detected: await dirExists(claudeDir),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Cursor
|
|
24
|
+
const cursorDir = path.join(HOME, '.cursor');
|
|
25
|
+
tools.push({
|
|
26
|
+
name: 'Cursor',
|
|
27
|
+
configPath: path.join(cursorDir, 'mcp.json'),
|
|
28
|
+
detected: await dirExists(cursorDir),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Windsurf
|
|
32
|
+
const windsurfDir = path.join(HOME, '.windsurf');
|
|
33
|
+
tools.push({
|
|
34
|
+
name: 'Windsurf',
|
|
35
|
+
configPath: path.join(windsurfDir, 'mcp.json'),
|
|
36
|
+
detected: await dirExists(windsurfDir),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return tools;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function dirExists(dir: string): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
const stat = await fs.stat(dir);
|
|
45
|
+
return stat.isDirectory();
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { AITool } from './detect-ai-tool.js';
|
|
4
|
+
|
|
5
|
+
const MCP_SERVER_CONFIG = {
|
|
6
|
+
command: 'npx',
|
|
7
|
+
args: ['yo-bug', 'mcp'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function writeMcpConfig(tool: AITool): Promise<{ success: boolean; message: string }> {
|
|
11
|
+
try {
|
|
12
|
+
// Ensure directory exists
|
|
13
|
+
await fs.mkdir(path.dirname(tool.configPath), { recursive: true });
|
|
14
|
+
|
|
15
|
+
// Read existing config
|
|
16
|
+
let config: any = {};
|
|
17
|
+
try {
|
|
18
|
+
const content = await fs.readFile(tool.configPath, 'utf-8');
|
|
19
|
+
config = JSON.parse(content);
|
|
20
|
+
} catch {
|
|
21
|
+
// File doesn't exist or invalid JSON — start fresh
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if already configured
|
|
25
|
+
if (tool.name === 'Claude Code') {
|
|
26
|
+
config.mcpServers = config.mcpServers || {};
|
|
27
|
+
if (config.mcpServers['yo-bug']) {
|
|
28
|
+
return { success: true, message: `${tool.name}: 已配置,跳过。` };
|
|
29
|
+
}
|
|
30
|
+
config.mcpServers['yo-bug'] = {
|
|
31
|
+
type: 'stdio',
|
|
32
|
+
...MCP_SERVER_CONFIG,
|
|
33
|
+
};
|
|
34
|
+
} else {
|
|
35
|
+
// Cursor / Windsurf format
|
|
36
|
+
config.mcpServers = config.mcpServers || {};
|
|
37
|
+
if (config.mcpServers['yo-bug']) {
|
|
38
|
+
return { success: true, message: `${tool.name}: 已配置,跳过。` };
|
|
39
|
+
}
|
|
40
|
+
config.mcpServers['yo-bug'] = MCP_SERVER_CONFIG;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Write config
|
|
44
|
+
await fs.writeFile(tool.configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
45
|
+
return { success: true, message: `${tool.name}: MCP 配置已写入 ${tool.configPath}` };
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
return { success: false, message: `${tool.name}: 写入失败 — ${err.message}` };
|
|
48
|
+
}
|
|
49
|
+
}
|