@jobshimo/browser-link 0.7.8 → 0.7.12
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/dist/bridge/ipc-client.d.ts +50 -0
- package/dist/bridge/ipc-client.js +247 -0
- package/dist/bridge/ipc-client.js.map +1 -0
- package/dist/bridge/{client.d.ts → proxy.d.ts} +1 -50
- package/dist/bridge/proxy.js +231 -0
- package/dist/bridge/proxy.js.map +1 -0
- package/dist/bridge/ws-bridge.d.ts +26 -0
- package/dist/bridge/ws-bridge.js +213 -0
- package/dist/bridge/ws-bridge.js.map +1 -0
- package/dist/commands/about.d.ts +2 -0
- package/dist/commands/about.js +5 -0
- package/dist/commands/about.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +4 -207
- package/dist/server.js.map +1 -1
- package/dist/ui/app.js +1 -1
- package/dist/ui/app.js.map +1 -1
- package/dist/ui/screens/about.d.ts +6 -0
- package/dist/ui/screens/about.js +17 -0
- package/dist/ui/screens/about.js.map +1 -0
- package/dist/ui/screens/client-picker.d.ts +8 -0
- package/dist/ui/screens/client-picker.js +43 -0
- package/dist/ui/screens/client-picker.js.map +1 -0
- package/dist/ui/screens/doctor.d.ts +6 -0
- package/dist/ui/screens/doctor.js +43 -0
- package/dist/ui/screens/doctor.js.map +1 -0
- package/dist/ui/screens/extension.d.ts +6 -0
- package/dist/ui/screens/extension.js +32 -0
- package/dist/ui/screens/extension.js.map +1 -0
- package/dist/ui/screens/free-port.d.ts +6 -0
- package/dist/ui/screens/free-port.js +83 -0
- package/dist/ui/screens/free-port.js.map +1 -0
- package/dist/ui/screens/index.d.ts +12 -0
- package/dist/ui/screens/index.js +13 -0
- package/dist/ui/screens/index.js.map +1 -0
- package/dist/ui/screens/install-result.d.ts +8 -0
- package/dist/ui/screens/install-result.js +27 -0
- package/dist/ui/screens/install-result.js.map +1 -0
- package/dist/ui/screens/language.d.ts +8 -0
- package/dist/ui/screens/language.js +42 -0
- package/dist/ui/screens/language.js.map +1 -0
- package/dist/ui/screens/menu.d.ts +9 -0
- package/dist/ui/screens/menu.js +81 -0
- package/dist/ui/screens/menu.js.map +1 -0
- package/dist/ui/screens/multi-agent.d.ts +6 -0
- package/dist/ui/screens/multi-agent.js +131 -0
- package/dist/ui/screens/multi-agent.js.map +1 -0
- package/dist/ui/screens/permissions.d.ts +6 -0
- package/dist/ui/screens/permissions.js +135 -0
- package/dist/ui/screens/permissions.js.map +1 -0
- package/dist/ui/screens/types.d.ts +4 -0
- package/dist/ui/screens/types.js +2 -0
- package/dist/ui/screens/types.js.map +1 -0
- package/dist/ui/screens/updates.d.ts +6 -0
- package/dist/ui/screens/updates.js +113 -0
- package/dist/ui/screens/updates.js.map +1 -0
- package/dist/ui/screens/welcome.d.ts +10 -0
- package/dist/ui/screens/welcome.js +44 -0
- package/dist/ui/screens/welcome.js.map +1 -0
- package/package.json +1 -1
- package/dist/bridge/client.js +0 -470
- package/dist/bridge/client.js.map +0 -1
- package/dist/ui/screens.d.ts +0 -65
- package/dist/ui/screens.js +0 -743
- package/dist/ui/screens.js.map +0 -1
package/dist/ui/screens.js
DELETED
|
@@ -1,743 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import { Frame, Menu } from './components.js';
|
|
5
|
-
import { I18N_WELCOME } from '../commands/welcome.js';
|
|
6
|
-
import { I18N_ABOUT } from '../commands/about.js';
|
|
7
|
-
import { INSTALLERS } from '../installers/index.js';
|
|
8
|
-
import { runDoctor, formatDoctor } from '../commands/doctor.js';
|
|
9
|
-
import { getExtensionInfo } from '../commands/extension.js';
|
|
10
|
-
import { checkUpdates } from '../commands/updates.js';
|
|
11
|
-
import { runSelfUpdate, } from '../commands/self-update.js';
|
|
12
|
-
import { runFreePort } from '../commands/free-port.js';
|
|
13
|
-
import { PACKAGE_NAME } from '../version.js';
|
|
14
|
-
import { loadConfig, saveConfig } from '../config.js';
|
|
15
|
-
import { PRESETS, TOOL_CATALOGUE, sanitizeDisabledTools, } from '../permissions.js';
|
|
16
|
-
export function WelcomeScreen({ language, hideDismiss, onAccept, onDismiss, onSwapLang, onQuit, }) {
|
|
17
|
-
const t = I18N_WELCOME[language];
|
|
18
|
-
const items = [
|
|
19
|
-
{ value: 'accept', label: t.options.accept },
|
|
20
|
-
];
|
|
21
|
-
if (!hideDismiss)
|
|
22
|
-
items.push({ value: 'dismiss', label: t.options.dismiss });
|
|
23
|
-
items.push({ value: 'swap', label: t.options.swap }, { value: 'quit', label: t.options.quit });
|
|
24
|
-
const [idx, setIdx] = useState(0);
|
|
25
|
-
useInput((input, key) => {
|
|
26
|
-
if (key.upArrow)
|
|
27
|
-
setIdx((i) => (i - 1 + items.length) % items.length);
|
|
28
|
-
else if (key.downArrow)
|
|
29
|
-
setIdx((i) => (i + 1) % items.length);
|
|
30
|
-
else if (key.return) {
|
|
31
|
-
const v = items[idx].value;
|
|
32
|
-
if (v === 'accept')
|
|
33
|
-
onAccept();
|
|
34
|
-
else if (v === 'dismiss')
|
|
35
|
-
onDismiss();
|
|
36
|
-
else if (v === 'swap')
|
|
37
|
-
onSwapLang();
|
|
38
|
-
else
|
|
39
|
-
onQuit();
|
|
40
|
-
}
|
|
41
|
-
else if (input === 'q' || key.escape)
|
|
42
|
-
onQuit();
|
|
43
|
-
else if (input === 'l')
|
|
44
|
-
onSwapLang();
|
|
45
|
-
});
|
|
46
|
-
const footer = language === 'es'
|
|
47
|
-
? '↑↓ moverse · ↵ elegir · l idioma · q salir'
|
|
48
|
-
: '↑↓ navigate · ↵ select · l language · q quit';
|
|
49
|
-
return (_jsxs(Frame, { title: t.title, footer: footer, children: [_jsx(Section, { title: t.aboutTitle, body: t.about }), _jsx(Section, { title: t.capabilitiesTitle, body: t.capabilities }), _jsx(Section, { title: t.warningTitle, body: t.warning, warn: true }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", italic: true, children: t.responsibility }), _jsx(Text, { color: "gray", italic: true, children: t.extensionNote })] }), _jsx(Text, { color: "white", bold: true, children: t.prompt }), _jsx(Box, { marginTop: 1, children: _jsx(Menu, { items: items, selectedIndex: idx }) })] }));
|
|
50
|
-
}
|
|
51
|
-
function Section({ title, body, warn }) {
|
|
52
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: warn ? 'yellow' : 'cyan', bold: true, children: [warn ? '⚠ ' : '', title] }), _jsx(Text, { children: body })] }));
|
|
53
|
-
}
|
|
54
|
-
const MENU_I18N = {
|
|
55
|
-
en: {
|
|
56
|
-
title: 'browser-link — setup',
|
|
57
|
-
prompt: 'Pick an action',
|
|
58
|
-
footer: '↑↓ navigate · ↵ select · l language · q quit',
|
|
59
|
-
options: {
|
|
60
|
-
register: 'Register browser-link with an MCP client',
|
|
61
|
-
permissions: 'Permissions — pick which MCP tools to expose',
|
|
62
|
-
multiAgent: 'Multi-agent — let multiple MCP clients share one bridge',
|
|
63
|
-
extension: 'Show Chrome extension install steps',
|
|
64
|
-
doctor: 'Run doctor (diagnose current setup)',
|
|
65
|
-
updates: 'Check for updates on npm',
|
|
66
|
-
freePort: 'Free port — stop a stuck browser-link holding 17529',
|
|
67
|
-
language: 'Language — switch between English and Español',
|
|
68
|
-
welcome: 'Show the welcome screen',
|
|
69
|
-
about: 'About / Help — what is this and how it works',
|
|
70
|
-
repo: 'Open the GitHub repository',
|
|
71
|
-
quit: 'Quit',
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
es: {
|
|
75
|
-
title: 'browser-link — configuración',
|
|
76
|
-
prompt: 'Elegí una acción',
|
|
77
|
-
footer: '↑↓ moverse · ↵ elegir · l idioma · q salir',
|
|
78
|
-
options: {
|
|
79
|
-
register: 'Registrar browser-link en un cliente MCP',
|
|
80
|
-
permissions: 'Permisos — elegí qué tools del MCP se exponen',
|
|
81
|
-
multiAgent: 'Multi-agente — varios clientes MCP comparten el mismo puente',
|
|
82
|
-
extension: 'Ver pasos para instalar la extensión de Chrome',
|
|
83
|
-
doctor: 'Diagnóstico (estado actual de la instalación)',
|
|
84
|
-
updates: 'Buscar actualizaciones en npm',
|
|
85
|
-
freePort: 'Liberar puerto — matar el browser-link colgado en 17529',
|
|
86
|
-
language: 'Idioma — cambiar entre English y Español',
|
|
87
|
-
welcome: 'Mostrar la pantalla de bienvenida',
|
|
88
|
-
about: 'Información / ayuda — qué es esto y cómo funciona',
|
|
89
|
-
repo: 'Abrir el repositorio en GitHub',
|
|
90
|
-
quit: 'Salir',
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
export function MainMenu({ language, onSelect, onSwapLang, onQuit }) {
|
|
95
|
-
const t = MENU_I18N[language];
|
|
96
|
-
const items = [
|
|
97
|
-
'register',
|
|
98
|
-
'permissions',
|
|
99
|
-
'multiAgent',
|
|
100
|
-
'extension',
|
|
101
|
-
'doctor',
|
|
102
|
-
'updates',
|
|
103
|
-
'freePort',
|
|
104
|
-
'language',
|
|
105
|
-
'welcome',
|
|
106
|
-
'about',
|
|
107
|
-
'repo',
|
|
108
|
-
'quit',
|
|
109
|
-
].map((a) => ({ value: a, label: t.options[a] }));
|
|
110
|
-
const [idx, setIdx] = useState(0);
|
|
111
|
-
useInput((input, key) => {
|
|
112
|
-
if (key.upArrow)
|
|
113
|
-
setIdx((i) => (i - 1 + items.length) % items.length);
|
|
114
|
-
else if (key.downArrow)
|
|
115
|
-
setIdx((i) => (i + 1) % items.length);
|
|
116
|
-
else if (key.return) {
|
|
117
|
-
const v = items[idx].value;
|
|
118
|
-
if (v === 'quit')
|
|
119
|
-
onQuit();
|
|
120
|
-
else
|
|
121
|
-
onSelect(v);
|
|
122
|
-
}
|
|
123
|
-
else if (input === 'q' || key.escape)
|
|
124
|
-
onQuit();
|
|
125
|
-
else if (input === 'l')
|
|
126
|
-
onSwapLang();
|
|
127
|
-
});
|
|
128
|
-
return (_jsxs(Frame, { title: t.title, footer: t.footer, children: [_jsx(Text, { color: "white", bold: true, children: t.prompt }), _jsx(Box, { marginTop: 1, children: _jsx(Menu, { items: items, selectedIndex: idx }) })] }));
|
|
129
|
-
}
|
|
130
|
-
const PICKER_I18N = {
|
|
131
|
-
en: {
|
|
132
|
-
title: 'Register browser-link in…',
|
|
133
|
-
prompt: 'Which MCP client?',
|
|
134
|
-
footer: '↑↓ navigate · ↵ register · Esc back',
|
|
135
|
-
},
|
|
136
|
-
es: {
|
|
137
|
-
title: 'Registrar browser-link en…',
|
|
138
|
-
prompt: '¿Qué cliente MCP?',
|
|
139
|
-
footer: '↑↓ moverse · ↵ registrar · Esc volver',
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
export function ClientPicker({ language, onPick, onBack }) {
|
|
143
|
-
const t = PICKER_I18N[language];
|
|
144
|
-
const STATUS = {
|
|
145
|
-
en: { registered: 'registered', notRegistered: 'not registered', notDetected: 'not detected' },
|
|
146
|
-
es: { registered: 'registrado', notRegistered: 'no registrado', notDetected: 'no detectado' },
|
|
147
|
-
};
|
|
148
|
-
const s = STATUS[language];
|
|
149
|
-
const items = INSTALLERS.map((inst) => {
|
|
150
|
-
const d = inst.detect();
|
|
151
|
-
const hint = !d.installed ? s.notDetected : d.registered ? s.registered : s.notRegistered;
|
|
152
|
-
return { value: inst.id, label: inst.displayName, hint };
|
|
153
|
-
});
|
|
154
|
-
const [idx, setIdx] = useState(0);
|
|
155
|
-
useInput((input, key) => {
|
|
156
|
-
if (key.upArrow)
|
|
157
|
-
setIdx((i) => (i - 1 + items.length) % items.length);
|
|
158
|
-
else if (key.downArrow)
|
|
159
|
-
setIdx((i) => (i + 1) % items.length);
|
|
160
|
-
else if (key.return)
|
|
161
|
-
onPick(items[idx].value);
|
|
162
|
-
else if (input === 'q' || key.escape)
|
|
163
|
-
onBack();
|
|
164
|
-
});
|
|
165
|
-
return (_jsxs(Frame, { title: t.title, footer: t.footer, children: [_jsx(Text, { color: "white", bold: true, children: t.prompt }), _jsx(Box, { marginTop: 1, children: _jsx(Menu, { items: items, selectedIndex: idx }) })] }));
|
|
166
|
-
}
|
|
167
|
-
const RESULT_I18N = {
|
|
168
|
-
en: {
|
|
169
|
-
ok: 'Registered',
|
|
170
|
-
warn: 'Not registered',
|
|
171
|
-
hint: 'Restart the MCP client so it picks up the new entry.',
|
|
172
|
-
footer: '↵ / Esc back to menu',
|
|
173
|
-
},
|
|
174
|
-
es: {
|
|
175
|
-
ok: 'Registrado',
|
|
176
|
-
warn: 'No registrado',
|
|
177
|
-
hint: 'Reiniciá el cliente MCP para que tome la nueva entrada.',
|
|
178
|
-
footer: '↵ / Esc volver al menú',
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
export function InstallResultView({ language, report, onBack }) {
|
|
182
|
-
const t = RESULT_I18N[language];
|
|
183
|
-
useInput((_input, key) => {
|
|
184
|
-
if (key.return || key.escape)
|
|
185
|
-
onBack();
|
|
186
|
-
});
|
|
187
|
-
const ok = report.installedClient;
|
|
188
|
-
return (_jsxs(Frame, { title: `${report.displayName} — ${ok ? t.ok : t.warn}`, footer: t.footer, children: [_jsxs(Text, { color: ok ? 'green' : 'yellow', children: [ok ? '✓ ' : '⚠ ', report.message] }), ok && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2192 ", t.hint] }) }))] }));
|
|
189
|
-
}
|
|
190
|
-
/* ── Extension info ────────────────────────────────────────────────────── */
|
|
191
|
-
const EXT_I18N = {
|
|
192
|
-
en: {
|
|
193
|
-
title: 'Chrome extension — install steps',
|
|
194
|
-
pathLabel: 'Extension assets are at:',
|
|
195
|
-
stepsLabel: 'Install steps:',
|
|
196
|
-
afterLoading: 'After loading, open the extension popup on any tab and click "Conectar" to bridge it.',
|
|
197
|
-
notFound: 'Extension assets not found. Run `npm run build:extension` (dev) or reinstall the package.',
|
|
198
|
-
footer: '↵ / Esc back to menu',
|
|
199
|
-
},
|
|
200
|
-
es: {
|
|
201
|
-
title: 'Extensión de Chrome — pasos de instalación',
|
|
202
|
-
pathLabel: 'Los assets de la extensión están en:',
|
|
203
|
-
stepsLabel: 'Pasos:',
|
|
204
|
-
afterLoading: 'Después de cargarla, abrí el popup de la extensión en cualquier pestaña y hacé click en "Conectar" para puentearla.',
|
|
205
|
-
notFound: 'No se encontraron los assets. Corré `npm run build:extension` (dev) o reinstalá el paquete.',
|
|
206
|
-
footer: '↵ / Esc volver al menú',
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
export function ExtensionView({ language, onBack }) {
|
|
210
|
-
const t = EXT_I18N[language];
|
|
211
|
-
const info = getExtensionInfo();
|
|
212
|
-
useInput((_input, key) => {
|
|
213
|
-
if (key.return || key.escape)
|
|
214
|
-
onBack();
|
|
215
|
-
});
|
|
216
|
-
return (_jsx(Frame, { title: t.title, footer: t.footer, children: info.path ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: t.pathLabel }), _jsx(Text, { color: "cyan", children: info.path }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: t.stepsLabel }), _jsx(Text, { children: info.hints })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", italic: true, children: t.afterLoading }) })] })) : (_jsx(Text, { color: "yellow", children: t.notFound })) }));
|
|
217
|
-
}
|
|
218
|
-
/* ── Doctor ────────────────────────────────────────────────────────────── */
|
|
219
|
-
const DOCTOR_I18N = {
|
|
220
|
-
en: {
|
|
221
|
-
title: 'Doctor — diagnose current setup',
|
|
222
|
-
loading: 'Diagnosing…',
|
|
223
|
-
refresh: 'r refresh · ↵ / Esc back to menu',
|
|
224
|
-
footer: '↵ / Esc back to menu',
|
|
225
|
-
},
|
|
226
|
-
es: {
|
|
227
|
-
title: 'Diagnóstico — estado actual de la instalación',
|
|
228
|
-
loading: 'Diagnosticando…',
|
|
229
|
-
refresh: 'r refrescar · ↵ / Esc volver al menú',
|
|
230
|
-
footer: '↵ / Esc volver al menú',
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
export function DoctorView({ language, onBack }) {
|
|
234
|
-
const t = DOCTOR_I18N[language];
|
|
235
|
-
const [output, setOutput] = useState(null);
|
|
236
|
-
const [refreshKey, setRefreshKey] = useState(0);
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
let cancelled = false;
|
|
239
|
-
setOutput(null);
|
|
240
|
-
void runDoctor().then((r) => {
|
|
241
|
-
if (!cancelled)
|
|
242
|
-
setOutput(formatDoctor(r));
|
|
243
|
-
});
|
|
244
|
-
return () => {
|
|
245
|
-
cancelled = true;
|
|
246
|
-
};
|
|
247
|
-
}, [refreshKey]);
|
|
248
|
-
useInput((input, key) => {
|
|
249
|
-
if (key.return || key.escape)
|
|
250
|
-
onBack();
|
|
251
|
-
else if (input === 'r')
|
|
252
|
-
setRefreshKey((k) => k + 1);
|
|
253
|
-
});
|
|
254
|
-
return (_jsx(Frame, { title: t.title, footer: t.refresh, children: output === null ? _jsx(Text, { color: "gray", children: t.loading }) : _jsx(Text, { children: output }) }));
|
|
255
|
-
}
|
|
256
|
-
export function AboutView({ language, onBack }) {
|
|
257
|
-
const t = I18N_ABOUT[language];
|
|
258
|
-
useInput((_input, key) => {
|
|
259
|
-
if (key.return || key.escape)
|
|
260
|
-
onBack();
|
|
261
|
-
});
|
|
262
|
-
const footer = language === 'es' ? '↵ / Esc volver al menú' : '↵ / Esc back to menu';
|
|
263
|
-
return (_jsxs(Frame, { title: t.title, footer: footer, children: [_jsx(AboutSection, { title: t.whatTitle, body: t.what }), _jsx(AboutSection, { title: t.howTitle, body: t.how }), _jsx(AboutSection, { title: t.bindingTitle, body: t.binding }), _jsx(AboutSection, { title: t.bridgeToolsTitle, body: t.bridgeTools }), _jsx(AboutSection, { title: t.mapToolsTitle, body: t.mapTools }), _jsx(AboutSection, { title: t.privacyTitle, body: t.privacy }), _jsx(AboutSection, { title: t.helpTitle, body: t.help })] }));
|
|
264
|
-
}
|
|
265
|
-
function AboutSection({ title, body }) {
|
|
266
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: title }), _jsx(Text, { children: body })] }));
|
|
267
|
-
}
|
|
268
|
-
/* ── Permissions ───────────────────────────────────────────────────────── */
|
|
269
|
-
const PERM_I18N = {
|
|
270
|
-
en: {
|
|
271
|
-
title: 'Permissions — pick which MCP tools to expose',
|
|
272
|
-
intro: 'Apply a preset (↵) or toggle individual tools (Space). Press s to save.',
|
|
273
|
-
presetsHeader: 'Presets',
|
|
274
|
-
bridgeHeader: 'Browser bridge',
|
|
275
|
-
mapHeader: 'Persistent UI map',
|
|
276
|
-
applyPrefix: 'Apply: ',
|
|
277
|
-
unsaved: '* Unsaved changes — press s to save',
|
|
278
|
-
saved: '✓ Saved.',
|
|
279
|
-
restart: 'Changes take effect the next time your MCP client starts the server.',
|
|
280
|
-
footer: '↑↓ navigate · Space toggle · ↵ apply preset · s save · Esc back',
|
|
281
|
-
},
|
|
282
|
-
es: {
|
|
283
|
-
title: 'Permisos — elegí qué tools del MCP se exponen',
|
|
284
|
-
intro: 'Aplicá un preset (↵) o cambiá tools individuales (Espacio). Apretá s para guardar.',
|
|
285
|
-
presetsHeader: 'Presets',
|
|
286
|
-
bridgeHeader: 'Bridge del browser',
|
|
287
|
-
mapHeader: 'Mapa persistente',
|
|
288
|
-
applyPrefix: 'Aplicar: ',
|
|
289
|
-
unsaved: '* Cambios sin guardar — apretá s para guardar',
|
|
290
|
-
saved: '✓ Guardado.',
|
|
291
|
-
restart: 'Los cambios tienen efecto la próxima vez que el cliente MCP arranque el servidor.',
|
|
292
|
-
footer: '↑↓ moverse · Espacio cambiar · ↵ aplicar preset · s guardar · Esc volver',
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
function setsDiffer(a, b) {
|
|
296
|
-
if (a.size !== b.size)
|
|
297
|
-
return true;
|
|
298
|
-
for (const x of a)
|
|
299
|
-
if (!b.has(x))
|
|
300
|
-
return true;
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
303
|
-
export function PermissionsView({ language, onBack }) {
|
|
304
|
-
const t = PERM_I18N[language];
|
|
305
|
-
const rows = useMemo(() => {
|
|
306
|
-
const r = [];
|
|
307
|
-
for (const preset of PRESETS)
|
|
308
|
-
r.push({ kind: 'preset', preset });
|
|
309
|
-
r.push({ kind: 'header', family: 'bridge' });
|
|
310
|
-
for (const tool of TOOL_CATALOGUE) {
|
|
311
|
-
if (tool.family === 'bridge')
|
|
312
|
-
r.push({ kind: 'tool', tool });
|
|
313
|
-
}
|
|
314
|
-
r.push({ kind: 'header', family: 'map' });
|
|
315
|
-
for (const tool of TOOL_CATALOGUE) {
|
|
316
|
-
if (tool.family === 'map')
|
|
317
|
-
r.push({ kind: 'tool', tool });
|
|
318
|
-
}
|
|
319
|
-
return r;
|
|
320
|
-
}, []);
|
|
321
|
-
const initial = useMemo(() => new Set(loadConfig().disabledTools ?? []), []);
|
|
322
|
-
const [savedDisabled, setSavedDisabled] = useState(initial);
|
|
323
|
-
const [disabled, setDisabled] = useState(initial);
|
|
324
|
-
const firstNonHeader = rows.findIndex((r) => r.kind !== 'header');
|
|
325
|
-
const [cursor, setCursor] = useState(firstNonHeader);
|
|
326
|
-
const [justSaved, setJustSaved] = useState(false);
|
|
327
|
-
const move = (dir) => {
|
|
328
|
-
setCursor((idx) => {
|
|
329
|
-
let i = idx;
|
|
330
|
-
for (let n = 0; n < rows.length; n++) {
|
|
331
|
-
i = (i + dir + rows.length) % rows.length;
|
|
332
|
-
if (rows[i].kind !== 'header')
|
|
333
|
-
return i;
|
|
334
|
-
}
|
|
335
|
-
return idx;
|
|
336
|
-
});
|
|
337
|
-
};
|
|
338
|
-
const toggleTool = (name) => {
|
|
339
|
-
setDisabled((prev) => {
|
|
340
|
-
const next = new Set(prev);
|
|
341
|
-
if (next.has(name))
|
|
342
|
-
next.delete(name);
|
|
343
|
-
else
|
|
344
|
-
next.add(name);
|
|
345
|
-
return next;
|
|
346
|
-
});
|
|
347
|
-
setJustSaved(false);
|
|
348
|
-
};
|
|
349
|
-
const applyPreset = (preset) => {
|
|
350
|
-
setDisabled(new Set(preset.disabled));
|
|
351
|
-
setJustSaved(false);
|
|
352
|
-
};
|
|
353
|
-
const save = () => {
|
|
354
|
-
const list = sanitizeDisabledTools([...disabled]);
|
|
355
|
-
saveConfig({ disabledTools: list });
|
|
356
|
-
setSavedDisabled(new Set(list));
|
|
357
|
-
setJustSaved(true);
|
|
358
|
-
};
|
|
359
|
-
useInput((input, key) => {
|
|
360
|
-
if (key.upArrow)
|
|
361
|
-
move(-1);
|
|
362
|
-
else if (key.downArrow)
|
|
363
|
-
move(1);
|
|
364
|
-
else if (input === ' ') {
|
|
365
|
-
const row = rows[cursor];
|
|
366
|
-
if (row.kind === 'tool')
|
|
367
|
-
toggleTool(row.tool.name);
|
|
368
|
-
}
|
|
369
|
-
else if (key.return) {
|
|
370
|
-
const row = rows[cursor];
|
|
371
|
-
if (row.kind === 'preset')
|
|
372
|
-
applyPreset(row.preset);
|
|
373
|
-
}
|
|
374
|
-
else if (input === 's' || input === 'S') {
|
|
375
|
-
save();
|
|
376
|
-
}
|
|
377
|
-
else if (input === 'q' || key.escape)
|
|
378
|
-
onBack();
|
|
379
|
-
});
|
|
380
|
-
const unsaved = setsDiffer(disabled, savedDisabled);
|
|
381
|
-
return (_jsxs(Frame, { title: t.title, footer: t.footer, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "white", children: t.intro }) }), _jsx(Box, { flexDirection: "column", children: rows.map((row, i) => {
|
|
382
|
-
if (row.kind === 'header') {
|
|
383
|
-
const label = row.family === 'bridge' ? t.bridgeHeader : t.mapHeader;
|
|
384
|
-
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", bold: true, children: [label, ":"] }) }, `h-${i}`));
|
|
385
|
-
}
|
|
386
|
-
const isCursor = i === cursor;
|
|
387
|
-
const cursorMark = isCursor ? '❯' : ' ';
|
|
388
|
-
if (row.kind === 'preset') {
|
|
389
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: isCursor ? 'cyan' : 'gray', children: [cursorMark, " "] }), _jsxs(Text, { color: isCursor ? 'white' : 'gray', bold: isCursor, children: [t.applyPrefix, row.preset.label] })] }, `p-${row.preset.id}`));
|
|
390
|
-
}
|
|
391
|
-
const isDisabled = disabled.has(row.tool.name);
|
|
392
|
-
const checkbox = isDisabled ? '[ ]' : '[x]';
|
|
393
|
-
const checkboxColor = isDisabled ? 'red' : 'green';
|
|
394
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: isCursor ? 'cyan' : 'gray', children: [cursorMark, " "] }), _jsxs(Text, { color: checkboxColor, children: [checkbox, " "] }), _jsx(Text, { color: isCursor ? 'white' : isDisabled ? 'gray' : 'white', bold: isCursor, children: row.tool.name.padEnd(28) }), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', row.tool.summary] })] }, `t-${row.tool.name}`));
|
|
395
|
-
}) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [unsaved ? (_jsx(Text, { color: "yellow", children: t.unsaved })) : justSaved ? (_jsx(Text, { color: "green", children: t.saved })) : null, _jsx(Text, { color: "gray", italic: true, children: t.restart })] })] }));
|
|
396
|
-
}
|
|
397
|
-
/* ── Updates ───────────────────────────────────────────────────────────── */
|
|
398
|
-
const UPDATES_I18N = {
|
|
399
|
-
en: {
|
|
400
|
-
title: 'Check for updates',
|
|
401
|
-
checking: 'Checking the npm registry…',
|
|
402
|
-
current: 'Current version',
|
|
403
|
-
latest: 'Latest on npm ',
|
|
404
|
-
upToDate: '✓ You are on the latest published version.',
|
|
405
|
-
available: '⚠ A newer version is available.',
|
|
406
|
-
upgradeCmd: 'Or, to upgrade manually, run:',
|
|
407
|
-
updateKeyHint: 'Press u to update now (stops any running primary, then installs).',
|
|
408
|
-
updateRunning: 'Updating…',
|
|
409
|
-
error: 'Could not check the registry',
|
|
410
|
-
footerIdle: 'r retry · ↵ / Esc back to menu',
|
|
411
|
-
footerWithUpdate: 'u update · r retry · ↵ / Esc back to menu',
|
|
412
|
-
footerUpdating: 'updating — please wait…',
|
|
413
|
-
footerDone: '↵ / Esc back to menu — restart your MCP client to pick up the new version',
|
|
414
|
-
footerFailed: 'r retry check · ↵ / Esc back to menu',
|
|
415
|
-
},
|
|
416
|
-
es: {
|
|
417
|
-
title: 'Buscar actualizaciones',
|
|
418
|
-
checking: 'Consultando el registry de npm…',
|
|
419
|
-
current: 'Versión instalada',
|
|
420
|
-
latest: 'Última en npm ',
|
|
421
|
-
upToDate: '✓ Estás en la última versión publicada.',
|
|
422
|
-
available: '⚠ Hay una versión más nueva disponible.',
|
|
423
|
-
upgradeCmd: 'O, para actualizar a mano, corré:',
|
|
424
|
-
updateKeyHint: 'Tocá u para actualizar ahora (corta el primary en uso e instala).',
|
|
425
|
-
updateRunning: 'Actualizando…',
|
|
426
|
-
error: 'No se pudo consultar el registry',
|
|
427
|
-
footerIdle: 'r reintentar · ↵ / Esc volver al menú',
|
|
428
|
-
footerWithUpdate: 'u actualizar · r reintentar · ↵ / Esc volver al menú',
|
|
429
|
-
footerUpdating: 'actualizando — esperá…',
|
|
430
|
-
footerDone: '↵ / Esc volver al menú — reiniciá tu cliente MCP para que tome la nueva versión',
|
|
431
|
-
footerFailed: 'r reintentar el chequeo · ↵ / Esc volver al menú',
|
|
432
|
-
},
|
|
433
|
-
};
|
|
434
|
-
export function UpdatesView({ language, onBack }) {
|
|
435
|
-
const t = UPDATES_I18N[language];
|
|
436
|
-
const [info, setInfo] = useState(null);
|
|
437
|
-
const [refreshKey, setRefreshKey] = useState(0);
|
|
438
|
-
const [update, setUpdate] = useState({ kind: 'idle' });
|
|
439
|
-
useEffect(() => {
|
|
440
|
-
let cancelled = false;
|
|
441
|
-
setInfo(null);
|
|
442
|
-
setUpdate({ kind: 'idle' });
|
|
443
|
-
void checkUpdates().then((r) => {
|
|
444
|
-
if (!cancelled)
|
|
445
|
-
setInfo(r);
|
|
446
|
-
});
|
|
447
|
-
return () => {
|
|
448
|
-
cancelled = true;
|
|
449
|
-
};
|
|
450
|
-
}, [refreshKey]);
|
|
451
|
-
const isUpdating = update.kind === 'running';
|
|
452
|
-
const canUpdate = info !== null && info.newer === true && info.latest !== null && !isUpdating;
|
|
453
|
-
const startUpdate = () => {
|
|
454
|
-
// Guard against the type-system view (info: UpdateInfo | null,
|
|
455
|
-
// info.latest: string | null) — these are real runtime checks even
|
|
456
|
-
// though `canUpdate` already implies both. Without them TS won't
|
|
457
|
-
// narrow `info.latest` to `string` in the closure scope.
|
|
458
|
-
if (info === null)
|
|
459
|
-
return;
|
|
460
|
-
const target = info.latest;
|
|
461
|
-
if (target === null)
|
|
462
|
-
return;
|
|
463
|
-
if (!canUpdate)
|
|
464
|
-
return;
|
|
465
|
-
setUpdate({ kind: 'running', stage: 'preflight', message: t.updateRunning });
|
|
466
|
-
runSelfUpdate(target, language, (event) => {
|
|
467
|
-
setUpdate({ kind: 'running', stage: event.stage, message: event.message });
|
|
468
|
-
})
|
|
469
|
-
.then((result) => {
|
|
470
|
-
setUpdate({
|
|
471
|
-
kind: result.ok ? 'done' : 'failed',
|
|
472
|
-
message: result.message,
|
|
473
|
-
});
|
|
474
|
-
})
|
|
475
|
-
.catch((err) => {
|
|
476
|
-
setUpdate({
|
|
477
|
-
kind: 'failed',
|
|
478
|
-
message: err instanceof Error ? err.message : String(err),
|
|
479
|
-
});
|
|
480
|
-
});
|
|
481
|
-
};
|
|
482
|
-
useInput((input, key) => {
|
|
483
|
-
if (isUpdating)
|
|
484
|
-
return; // ignore keys while the install is in flight
|
|
485
|
-
if (key.return || key.escape)
|
|
486
|
-
onBack();
|
|
487
|
-
else if (input === 'r' && update.kind !== 'done')
|
|
488
|
-
setRefreshKey((k) => k + 1);
|
|
489
|
-
else if (input === 'u')
|
|
490
|
-
startUpdate();
|
|
491
|
-
});
|
|
492
|
-
const footer = update.kind === 'running'
|
|
493
|
-
? t.footerUpdating
|
|
494
|
-
: update.kind === 'done'
|
|
495
|
-
? t.footerDone
|
|
496
|
-
: update.kind === 'failed'
|
|
497
|
-
? t.footerFailed
|
|
498
|
-
: canUpdate
|
|
499
|
-
? t.footerWithUpdate
|
|
500
|
-
: t.footerIdle;
|
|
501
|
-
return (_jsx(Frame, { title: t.title, footer: footer, children: info === null ? (_jsx(Text, { color: "gray", children: t.checking })) : info.error || info.latest === null ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsxs(Text, { color: "white", children: [t.current, ": "] }), _jsx(Text, { bold: true, children: info.current })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: [t.error, ": ", info.error ?? 'unknown error'] }) })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsxs(Text, { color: "white", children: [t.current, ": "] }), _jsx(Text, { bold: true, children: info.current })] }), _jsxs(Text, { children: [_jsxs(Text, { color: "white", children: [t.latest, ": "] }), _jsx(Text, { bold: true, color: info.newer ? 'yellow' : 'green', children: info.latest })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: info.newer ? 'yellow' : 'green', children: info.newer ? t.available : t.upToDate }) }), info.newer && update.kind === 'idle' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", children: t.updateKeyHint }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: t.upgradeCmd }), _jsx(Text, { color: "cyan", children: `npm install -g ${PACKAGE_NAME}@latest` })] })] })), update.kind === 'running' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", children: t.updateRunning }), _jsx(Text, { color: "gray", children: update.message })] })), update.kind === 'done' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: update.message }) })), update.kind === 'failed' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: update.message }) }))] })) }));
|
|
502
|
-
}
|
|
503
|
-
/* ── Language ──────────────────────────────────────────────────────────── */
|
|
504
|
-
const LANG_I18N = {
|
|
505
|
-
en: {
|
|
506
|
-
title: 'Language — switch between English and Español',
|
|
507
|
-
prompt: 'Pick a language',
|
|
508
|
-
saved: '✓ Saved. All output will now use the selected language.',
|
|
509
|
-
footer: '↑↓ navigate · ↵ pick · Esc back',
|
|
510
|
-
},
|
|
511
|
-
es: {
|
|
512
|
-
title: 'Idioma — cambiar entre English y Español',
|
|
513
|
-
prompt: 'Elegí un idioma',
|
|
514
|
-
saved: '✓ Guardado. Todo el output va a usar el idioma elegido.',
|
|
515
|
-
footer: '↑↓ moverse · ↵ elegir · Esc volver',
|
|
516
|
-
},
|
|
517
|
-
};
|
|
518
|
-
export function LanguageView({ language, onPick, onBack }) {
|
|
519
|
-
const t = LANG_I18N[language];
|
|
520
|
-
const items = [
|
|
521
|
-
{ value: 'en', label: 'English' },
|
|
522
|
-
{ value: 'es', label: 'Español' },
|
|
523
|
-
];
|
|
524
|
-
const [idx, setIdx] = useState(language === 'es' ? 1 : 0);
|
|
525
|
-
const [saved, setSaved] = useState(false);
|
|
526
|
-
useInput((_input, key) => {
|
|
527
|
-
if (key.upArrow)
|
|
528
|
-
setIdx((i) => (i - 1 + items.length) % items.length);
|
|
529
|
-
else if (key.downArrow)
|
|
530
|
-
setIdx((i) => (i + 1) % items.length);
|
|
531
|
-
else if (key.return) {
|
|
532
|
-
const next = items[idx].value;
|
|
533
|
-
onPick(next);
|
|
534
|
-
setSaved(true);
|
|
535
|
-
}
|
|
536
|
-
else if (key.escape)
|
|
537
|
-
onBack();
|
|
538
|
-
});
|
|
539
|
-
return (_jsxs(Frame, { title: t.title, footer: t.footer, children: [_jsx(Text, { color: "white", bold: true, children: t.prompt }), _jsx(Box, { marginTop: 1, children: _jsx(Menu, { items: items, selectedIndex: idx }) }), saved && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: t.saved }) }))] }));
|
|
540
|
-
}
|
|
541
|
-
const MULTI_AGENT_I18N = {
|
|
542
|
-
en: {
|
|
543
|
-
title: 'Multi-agent — let multiple MCP clients share one browser-link',
|
|
544
|
-
multiHeader: 'Multi-agent mode',
|
|
545
|
-
multiBody: [
|
|
546
|
-
'browser-link normally binds 127.0.0.1:17529 from the first MCP',
|
|
547
|
-
'client that starts it. Other clients (Claude + Copilot + OpenCode at',
|
|
548
|
-
'the same time) crash with "port in use".',
|
|
549
|
-
'',
|
|
550
|
-
'When multi-agent is ON, the second instance becomes a proxy that',
|
|
551
|
-
'forwards MCP requests to the first one over an internal port',
|
|
552
|
-
'(127.0.0.1:17530, kernel-validated like the main one). All agents',
|
|
553
|
-
'end up sharing the same connected Chrome tabs and the same',
|
|
554
|
-
'persistent UI map.',
|
|
555
|
-
].join('\n'),
|
|
556
|
-
multiToggle: 'Enable multi-agent mode',
|
|
557
|
-
reelectHeader: 'Auto-reelect on primary close (advanced)',
|
|
558
|
-
reelectBody: [
|
|
559
|
-
'When the primary client closes, secondary clients lose the bridge.',
|
|
560
|
-
'With auto-reelect ON, one of them takes over the primary role',
|
|
561
|
-
'automatically (race on bind). With it OFF, you relaunch your MCP',
|
|
562
|
-
'client manually to reconnect.',
|
|
563
|
-
].join('\n'),
|
|
564
|
-
reelectToggle: 'Auto-reelect when primary closes',
|
|
565
|
-
reelectDisabled: '(enable multi-agent first)',
|
|
566
|
-
unsaved: '* Unsaved changes — press s to save',
|
|
567
|
-
saved: '✓ Saved.',
|
|
568
|
-
restart: 'Restart every MCP client for these changes to take effect.',
|
|
569
|
-
footer: '↑↓ navigate · Space toggle · s save · Esc back',
|
|
570
|
-
},
|
|
571
|
-
es: {
|
|
572
|
-
title: 'Multi-agente — varios clientes MCP comparten el mismo puente',
|
|
573
|
-
multiHeader: 'Modo multi-agente',
|
|
574
|
-
multiBody: [
|
|
575
|
-
'browser-link normalmente bindea 127.0.0.1:17529 desde el primer',
|
|
576
|
-
'cliente MCP que lo arranca. Otros clientes (Claude + Copilot +',
|
|
577
|
-
'OpenCode al mismo tiempo) caen con "port in use".',
|
|
578
|
-
'',
|
|
579
|
-
'Con multi-agente activado, el segundo proceso se vuelve un proxy',
|
|
580
|
-
'que reenvía los pedidos MCP al primero por un puerto interno',
|
|
581
|
-
'(127.0.0.1:17530, validado por kernel igual que el principal).',
|
|
582
|
-
'Todos los agentes terminan viendo las mismas pestañas conectadas',
|
|
583
|
-
'y el mismo mapa persistente.',
|
|
584
|
-
].join('\n'),
|
|
585
|
-
multiToggle: 'Activar modo multi-agente',
|
|
586
|
-
reelectHeader: 'Re-elección automática al cerrar el primary (avanzado)',
|
|
587
|
-
reelectBody: [
|
|
588
|
-
'Cuando el cliente primary cierra, los secundarios pierden el',
|
|
589
|
-
'puente. Con re-elección automática activada, uno de ellos toma el',
|
|
590
|
-
'rol de primary automáticamente (race en el bind). Con ella',
|
|
591
|
-
'desactivada, tenés que relanzar el cliente MCP a mano para',
|
|
592
|
-
'reconectar.',
|
|
593
|
-
].join('\n'),
|
|
594
|
-
reelectToggle: 'Re-elegir automáticamente al cerrar el primary',
|
|
595
|
-
reelectDisabled: '(activá primero multi-agente)',
|
|
596
|
-
unsaved: '* Cambios sin guardar — apretá s para guardar',
|
|
597
|
-
saved: '✓ Guardado.',
|
|
598
|
-
restart: 'Reiniciá cada cliente MCP para que los cambios tengan efecto.',
|
|
599
|
-
footer: '↑↓ moverse · Espacio cambiar · s guardar · Esc volver',
|
|
600
|
-
},
|
|
601
|
-
};
|
|
602
|
-
export function MultiAgentView({ language, onBack }) {
|
|
603
|
-
const t = MULTI_AGENT_I18N[language];
|
|
604
|
-
const initialCfg = useMemo(() => loadConfig(), []);
|
|
605
|
-
const [savedMulti, setSavedMulti] = useState(initialCfg.multiAgent === true);
|
|
606
|
-
const [savedReelect, setSavedReelect] = useState(initialCfg.autoReelect === true);
|
|
607
|
-
const [multi, setMulti] = useState(savedMulti);
|
|
608
|
-
const [reelect, setReelect] = useState(savedReelect);
|
|
609
|
-
const [justSaved, setJustSaved] = useState(false);
|
|
610
|
-
// Only the two toggles are navigable.
|
|
611
|
-
const [cursor, setCursor] = useState('multi');
|
|
612
|
-
const move = (dir) => {
|
|
613
|
-
setCursor((c) => {
|
|
614
|
-
if (dir === 1)
|
|
615
|
-
return c === 'multi' ? 'reelect' : 'multi';
|
|
616
|
-
return c === 'reelect' ? 'multi' : 'reelect';
|
|
617
|
-
});
|
|
618
|
-
};
|
|
619
|
-
const toggle = () => {
|
|
620
|
-
if (cursor === 'multi') {
|
|
621
|
-
setMulti((m) => {
|
|
622
|
-
const next = !m;
|
|
623
|
-
// Turning multi off also clears the working reelect state, matching
|
|
624
|
-
// the config normalisation rule (autoReelect implies multiAgent).
|
|
625
|
-
if (!next)
|
|
626
|
-
setReelect(false);
|
|
627
|
-
return next;
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
// Auto-reelect can only be turned on when multi is on.
|
|
632
|
-
if (!multi)
|
|
633
|
-
return;
|
|
634
|
-
setReelect((r) => !r);
|
|
635
|
-
}
|
|
636
|
-
setJustSaved(false);
|
|
637
|
-
};
|
|
638
|
-
const save = () => {
|
|
639
|
-
saveConfig({ multiAgent: multi, autoReelect: multi ? reelect : false });
|
|
640
|
-
setSavedMulti(multi);
|
|
641
|
-
setSavedReelect(multi ? reelect : false);
|
|
642
|
-
setJustSaved(true);
|
|
643
|
-
};
|
|
644
|
-
useInput((input, key) => {
|
|
645
|
-
if (key.upArrow)
|
|
646
|
-
move(-1);
|
|
647
|
-
else if (key.downArrow)
|
|
648
|
-
move(1);
|
|
649
|
-
else if (input === ' ')
|
|
650
|
-
toggle();
|
|
651
|
-
else if (input === 's' || input === 'S')
|
|
652
|
-
save();
|
|
653
|
-
else if (input === 'q' || key.escape)
|
|
654
|
-
onBack();
|
|
655
|
-
});
|
|
656
|
-
const unsaved = multi !== savedMulti || reelect !== savedReelect;
|
|
657
|
-
const checkbox = (on, dim) => {
|
|
658
|
-
if (dim)
|
|
659
|
-
return { mark: '[ ]', color: 'gray' };
|
|
660
|
-
return on ? { mark: '[x]', color: 'green' } : { mark: '[ ]', color: 'red' };
|
|
661
|
-
};
|
|
662
|
-
const multiBox = checkbox(multi, false);
|
|
663
|
-
const reelectBox = checkbox(multi ? reelect : false, !multi);
|
|
664
|
-
return (_jsxs(Frame, { title: t.title, footer: t.footer, children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: t.multiHeader }), _jsx(Text, { children: t.multiBody })] }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: cursor === 'multi' ? 'cyan' : 'gray', children: cursor === 'multi' ? '❯ ' : ' ' }), _jsxs(Text, { color: multiBox.color, children: [multiBox.mark, " "] }), _jsx(Text, { color: cursor === 'multi' ? 'white' : 'gray', bold: cursor === 'multi', children: t.multiToggle })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: t.reelectHeader }), _jsx(Text, { children: t.reelectBody })] }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: cursor === 'reelect' ? 'cyan' : 'gray', children: cursor === 'reelect' ? '❯ ' : ' ' }), _jsxs(Text, { color: reelectBox.color, children: [reelectBox.mark, " "] }), _jsxs(Text, { color: !multi ? 'gray' : cursor === 'reelect' ? 'white' : 'gray', bold: cursor === 'reelect' && multi, children: [t.reelectToggle, !multi && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', t.reelectDisabled] }))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [unsaved ? (_jsx(Text, { color: "yellow", children: t.unsaved })) : justSaved ? (_jsx(Text, { color: "green", children: t.saved })) : null, _jsx(Text, { color: "gray", italic: true, children: t.restart })] })] }));
|
|
665
|
-
}
|
|
666
|
-
const FREE_PORT_I18N = {
|
|
667
|
-
en: {
|
|
668
|
-
title: 'Free port 17529',
|
|
669
|
-
intro: [
|
|
670
|
-
'If an MCP client (Claude / OpenCode / Copilot) was closed without',
|
|
671
|
-
'shutting down browser-link cleanly, the next client crashes with',
|
|
672
|
-
'"port already in use". This screen finds the process holding',
|
|
673
|
-
'127.0.0.1:17529 and stops it.',
|
|
674
|
-
'',
|
|
675
|
-
'Safety: only processes whose image starts with "node" are killed.',
|
|
676
|
-
'Anything else is left alone with an explanation.',
|
|
677
|
-
].join('\n'),
|
|
678
|
-
confirmPrompt: 'Stop the process holding port 17529?',
|
|
679
|
-
confirmYes: 'Yes — kill it',
|
|
680
|
-
confirmNo: 'No — back to menu',
|
|
681
|
-
running: 'Stopping…',
|
|
682
|
-
footer: '↑↓ navigate · ↵ select · Esc back',
|
|
683
|
-
doneFooter: '↵ / Esc back to menu',
|
|
684
|
-
},
|
|
685
|
-
es: {
|
|
686
|
-
title: 'Liberar puerto 17529',
|
|
687
|
-
intro: [
|
|
688
|
-
'Si un cliente MCP (Claude / OpenCode / Copilot) cerró sin bajar',
|
|
689
|
-
'browser-link de forma limpia, el próximo cliente crashea con',
|
|
690
|
-
'"port already in use". Esta pantalla busca el proceso que tiene',
|
|
691
|
-
'127.0.0.1:17529 y lo para.',
|
|
692
|
-
'',
|
|
693
|
-
'Seguridad: sólo se matan procesos cuyo nombre empieza con "node".',
|
|
694
|
-
'Cualquier otro se deja vivo con una explicación.',
|
|
695
|
-
].join('\n'),
|
|
696
|
-
confirmPrompt: '¿Parar el proceso que tiene el puerto 17529?',
|
|
697
|
-
confirmYes: 'Sí — matalo',
|
|
698
|
-
confirmNo: 'No — volver al menú',
|
|
699
|
-
running: 'Parando…',
|
|
700
|
-
footer: '↑↓ moverse · ↵ elegir · Esc volver',
|
|
701
|
-
doneFooter: '↵ / Esc volver al menú',
|
|
702
|
-
},
|
|
703
|
-
};
|
|
704
|
-
export function FreePortView({ language, onBack }) {
|
|
705
|
-
const t = FREE_PORT_I18N[language];
|
|
706
|
-
const [phase, setPhase] = useState('confirm');
|
|
707
|
-
const [result, setResult] = useState(null);
|
|
708
|
-
const items = [
|
|
709
|
-
{ value: 'yes', label: t.confirmYes },
|
|
710
|
-
{ value: 'no', label: t.confirmNo },
|
|
711
|
-
];
|
|
712
|
-
const [idx, setIdx] = useState(1);
|
|
713
|
-
useInput((_input, key) => {
|
|
714
|
-
if (phase === 'confirm') {
|
|
715
|
-
if (key.upArrow)
|
|
716
|
-
setIdx((i) => (i - 1 + items.length) % items.length);
|
|
717
|
-
else if (key.downArrow)
|
|
718
|
-
setIdx((i) => (i + 1) % items.length);
|
|
719
|
-
else if (key.return) {
|
|
720
|
-
const v = items[idx].value;
|
|
721
|
-
if (v === 'no')
|
|
722
|
-
onBack();
|
|
723
|
-
else {
|
|
724
|
-
setPhase('running');
|
|
725
|
-
// Defer the synchronous kill so Ink renders the "running" state.
|
|
726
|
-
setTimeout(() => {
|
|
727
|
-
const r = runFreePort(language);
|
|
728
|
-
setResult(r);
|
|
729
|
-
setPhase('done');
|
|
730
|
-
}, 0);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
else if (key.escape)
|
|
734
|
-
onBack();
|
|
735
|
-
}
|
|
736
|
-
else if (phase === 'done') {
|
|
737
|
-
if (key.return || key.escape)
|
|
738
|
-
onBack();
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
return (_jsxs(Frame, { title: t.title, footer: phase === 'done' ? t.doneFooter : t.footer, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: t.intro }) }), phase === 'confirm' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", bold: true, children: t.confirmPrompt }), _jsx(Box, { marginTop: 1, children: _jsx(Menu, { items: items, selectedIndex: idx }) })] })), phase === 'running' && _jsx(Text, { color: "gray", children: t.running }), phase === 'done' && result && (_jsx(Text, { color: result.ok ? 'green' : 'yellow', children: result.message }))] }));
|
|
742
|
-
}
|
|
743
|
-
//# sourceMappingURL=screens.js.map
|