bingo-light 2.0.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 +522 -0
- package/README.zh-CN.md +534 -0
- package/bin/cli.js +46 -0
- package/bin/mcp.js +45 -0
- package/bingo-light +1094 -0
- package/bingo_core/__init__.py +77 -0
- package/bingo_core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/bingo_core/__pycache__/_entry.cpython-313.pyc +0 -0
- package/bingo_core/__pycache__/config.cpython-313.pyc +0 -0
- package/bingo_core/__pycache__/exceptions.cpython-313.pyc +0 -0
- package/bingo_core/__pycache__/git.cpython-313.pyc +0 -0
- package/bingo_core/__pycache__/models.cpython-313.pyc +0 -0
- package/bingo_core/__pycache__/repo.cpython-313.pyc +0 -0
- package/bingo_core/__pycache__/setup.cpython-313.pyc +0 -0
- package/bingo_core/__pycache__/state.cpython-313.pyc +0 -0
- package/bingo_core/config.py +110 -0
- package/bingo_core/exceptions.py +48 -0
- package/bingo_core/git.py +194 -0
- package/bingo_core/models.py +37 -0
- package/bingo_core/repo.py +2376 -0
- package/bingo_core/setup.py +549 -0
- package/bingo_core/state.py +306 -0
- package/completions/bingo-light.bash +118 -0
- package/completions/bingo-light.fish +197 -0
- package/completions/bingo-light.zsh +169 -0
- package/mcp-server.py +788 -0
- package/package.json +34 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bingo_core.setup — Interactive setup wizard for configuring MCP across AI tools.
|
|
3
|
+
|
|
4
|
+
Detects installed AI coding tools, presents a multi-select menu, and writes
|
|
5
|
+
the correct MCP server configuration for each selected tool.
|
|
6
|
+
|
|
7
|
+
Python 3.8+ stdlib only. Uses termios for raw input on Unix.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import shutil
|
|
16
|
+
import sys
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ─── AI Tool Definitions ────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AITool:
|
|
26
|
+
"""Describes one AI coding tool that supports MCP."""
|
|
27
|
+
|
|
28
|
+
id: str
|
|
29
|
+
name: str
|
|
30
|
+
config_path: str # with ~ or env vars, expanded at runtime
|
|
31
|
+
top_key: str # JSON key wrapping the server entry
|
|
32
|
+
detect_dirs: List[str] = field(default_factory=list)
|
|
33
|
+
detect_cmds: List[str] = field(default_factory=list)
|
|
34
|
+
extra_fields: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
note: str = ""
|
|
36
|
+
|
|
37
|
+
def expand_path(self) -> str:
|
|
38
|
+
return os.path.expandvars(os.path.expanduser(self.config_path))
|
|
39
|
+
|
|
40
|
+
def is_detected(self) -> bool:
|
|
41
|
+
for d in self.detect_dirs:
|
|
42
|
+
if os.path.isdir(os.path.expanduser(d)):
|
|
43
|
+
return True
|
|
44
|
+
for c in self.detect_cmds:
|
|
45
|
+
if shutil.which(c):
|
|
46
|
+
return True
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_tools() -> List[AITool]:
|
|
51
|
+
"""Return the list of all supported AI tools."""
|
|
52
|
+
is_mac = platform.system() == "Darwin"
|
|
53
|
+
is_win = platform.system() == "Windows"
|
|
54
|
+
|
|
55
|
+
tools = [
|
|
56
|
+
AITool(
|
|
57
|
+
id="claude-code",
|
|
58
|
+
name="Claude Code",
|
|
59
|
+
config_path="~/.claude/settings.json",
|
|
60
|
+
top_key="mcpServers",
|
|
61
|
+
detect_dirs=["~/.claude"],
|
|
62
|
+
detect_cmds=["claude"],
|
|
63
|
+
),
|
|
64
|
+
AITool(
|
|
65
|
+
id="claude-desktop",
|
|
66
|
+
name="Claude Desktop",
|
|
67
|
+
config_path=(
|
|
68
|
+
"~/Library/Application Support/Claude/claude_desktop_config.json"
|
|
69
|
+
if is_mac
|
|
70
|
+
else "%APPDATA%\\Claude\\claude_desktop_config.json"
|
|
71
|
+
if is_win
|
|
72
|
+
else ""
|
|
73
|
+
),
|
|
74
|
+
top_key="mcpServers",
|
|
75
|
+
detect_dirs=(
|
|
76
|
+
["~/Library/Application Support/Claude"] if is_mac
|
|
77
|
+
else ["%APPDATA%\\Claude"] if is_win
|
|
78
|
+
else []
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
AITool(
|
|
82
|
+
id="cursor",
|
|
83
|
+
name="Cursor",
|
|
84
|
+
config_path="~/.cursor/mcp.json",
|
|
85
|
+
top_key="mcpServers",
|
|
86
|
+
detect_dirs=["~/.cursor"],
|
|
87
|
+
detect_cmds=["cursor"],
|
|
88
|
+
),
|
|
89
|
+
AITool(
|
|
90
|
+
id="windsurf",
|
|
91
|
+
name="Windsurf",
|
|
92
|
+
config_path="~/.codeium/windsurf/mcp_config.json",
|
|
93
|
+
top_key="mcpServers",
|
|
94
|
+
detect_dirs=["~/.codeium/windsurf"],
|
|
95
|
+
),
|
|
96
|
+
AITool(
|
|
97
|
+
id="vscode-copilot",
|
|
98
|
+
name="VS Code / Copilot",
|
|
99
|
+
config_path="~/.vscode/mcp.json",
|
|
100
|
+
top_key="servers",
|
|
101
|
+
detect_dirs=["~/.vscode"],
|
|
102
|
+
detect_cmds=["code"],
|
|
103
|
+
extra_fields={"type": "stdio"},
|
|
104
|
+
note="Also supports .vscode/mcp.json per project",
|
|
105
|
+
),
|
|
106
|
+
AITool(
|
|
107
|
+
id="cline",
|
|
108
|
+
name="Cline",
|
|
109
|
+
config_path="~/.vscode/cline_mcp_settings.json",
|
|
110
|
+
top_key="mcpServers",
|
|
111
|
+
detect_dirs=["~/.vscode"],
|
|
112
|
+
note="Managed by Cline extension; edit via MCP Servers panel",
|
|
113
|
+
),
|
|
114
|
+
AITool(
|
|
115
|
+
id="roo-code",
|
|
116
|
+
name="Roo Code",
|
|
117
|
+
config_path="~/.vscode/roo_mcp_settings.json",
|
|
118
|
+
top_key="mcpServers",
|
|
119
|
+
detect_dirs=["~/.vscode"],
|
|
120
|
+
note="Managed by Roo Code extension; edit via settings",
|
|
121
|
+
),
|
|
122
|
+
AITool(
|
|
123
|
+
id="zed",
|
|
124
|
+
name="Zed",
|
|
125
|
+
config_path="~/.config/zed/settings.json",
|
|
126
|
+
top_key="context_servers",
|
|
127
|
+
detect_dirs=["~/.config/zed"],
|
|
128
|
+
detect_cmds=["zed"],
|
|
129
|
+
),
|
|
130
|
+
AITool(
|
|
131
|
+
id="gemini-cli",
|
|
132
|
+
name="Gemini CLI",
|
|
133
|
+
config_path="~/.gemini/settings.json",
|
|
134
|
+
top_key="mcpServers",
|
|
135
|
+
detect_dirs=["~/.gemini"],
|
|
136
|
+
detect_cmds=["gemini"],
|
|
137
|
+
),
|
|
138
|
+
AITool(
|
|
139
|
+
id="amazon-q",
|
|
140
|
+
name="Amazon Q Developer",
|
|
141
|
+
config_path="~/.aws/amazonq/mcp.json",
|
|
142
|
+
top_key="mcpServers",
|
|
143
|
+
detect_dirs=["~/.aws/amazonq"],
|
|
144
|
+
),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# Filter out tools with empty config paths (e.g. Claude Desktop on Linux)
|
|
148
|
+
return [t for t in tools if t.config_path]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ─── MCP Server Path Detection ──────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def find_mcp_server() -> Tuple[str, List[str]]:
|
|
155
|
+
"""Find the MCP server command and args.
|
|
156
|
+
|
|
157
|
+
Returns (command, args) tuple. Tries:
|
|
158
|
+
1. Installed `bingo-light-mcp` on PATH
|
|
159
|
+
2. mcp-server.py next to the running script
|
|
160
|
+
3. mcp-server.py next to bingo_core package
|
|
161
|
+
"""
|
|
162
|
+
# 1. Installed binary on PATH
|
|
163
|
+
for name in ("bingo-light-mcp", "mcp-server.py"):
|
|
164
|
+
mcp_bin = shutil.which(name)
|
|
165
|
+
if mcp_bin:
|
|
166
|
+
return ("python3", [mcp_bin])
|
|
167
|
+
|
|
168
|
+
# 2. Relative to running script
|
|
169
|
+
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
170
|
+
candidate = os.path.join(script_dir, "mcp-server.py")
|
|
171
|
+
if os.path.isfile(candidate):
|
|
172
|
+
return ("python3", [candidate])
|
|
173
|
+
|
|
174
|
+
# 3. Relative to bingo_core package
|
|
175
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
176
|
+
candidate = os.path.join(os.path.dirname(pkg_dir), "mcp-server.py")
|
|
177
|
+
if os.path.isfile(candidate):
|
|
178
|
+
return ("python3", [candidate])
|
|
179
|
+
|
|
180
|
+
# Fallback
|
|
181
|
+
return ("python3", ["bingo-light-mcp"])
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ─── Config Writer ───────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def write_mcp_config(
|
|
188
|
+
tool: AITool,
|
|
189
|
+
command: str,
|
|
190
|
+
args: List[str],
|
|
191
|
+
server_name: str = "bingo-light",
|
|
192
|
+
) -> Dict[str, Any]:
|
|
193
|
+
"""Write MCP server config for one AI tool.
|
|
194
|
+
|
|
195
|
+
Returns dict with ok, tool, config_path, action (created|updated|error).
|
|
196
|
+
"""
|
|
197
|
+
config_path = tool.expand_path()
|
|
198
|
+
config_dir = os.path.dirname(config_path)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
202
|
+
except OSError as e:
|
|
203
|
+
return {"ok": False, "tool": tool.id, "error": str(e)}
|
|
204
|
+
|
|
205
|
+
# Read existing config
|
|
206
|
+
data: Dict[str, Any] = {}
|
|
207
|
+
if os.path.isfile(config_path):
|
|
208
|
+
try:
|
|
209
|
+
with open(config_path) as f:
|
|
210
|
+
data = json.load(f)
|
|
211
|
+
action = "updated"
|
|
212
|
+
except (json.JSONDecodeError, OSError):
|
|
213
|
+
data = {}
|
|
214
|
+
action = "created"
|
|
215
|
+
else:
|
|
216
|
+
action = "created"
|
|
217
|
+
|
|
218
|
+
# Build the server entry
|
|
219
|
+
entry: Dict[str, Any] = {"command": command, "args": args}
|
|
220
|
+
entry.update(tool.extra_fields)
|
|
221
|
+
|
|
222
|
+
# Write under the correct top-level key
|
|
223
|
+
servers = data.setdefault(tool.top_key, {})
|
|
224
|
+
already = server_name in servers
|
|
225
|
+
servers[server_name] = entry
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
with open(config_path, "w") as f:
|
|
229
|
+
json.dump(data, f, indent=2)
|
|
230
|
+
f.write("\n")
|
|
231
|
+
except OSError as e:
|
|
232
|
+
return {"ok": False, "tool": tool.id, "error": str(e)}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"ok": True,
|
|
236
|
+
"tool": tool.id,
|
|
237
|
+
"name": tool.name,
|
|
238
|
+
"config_path": config_path,
|
|
239
|
+
"action": "unchanged" if already else action,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ─── Interactive Multi-Select ────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _tildify(path: str) -> str:
|
|
247
|
+
home = os.path.expanduser("~")
|
|
248
|
+
if path.startswith(home + os.sep):
|
|
249
|
+
return "~" + path[len(home):]
|
|
250
|
+
if path == home:
|
|
251
|
+
return "~"
|
|
252
|
+
return path
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _read_key() -> str:
|
|
256
|
+
"""Read a single keypress (including escape sequences) from /dev/tty."""
|
|
257
|
+
import termios
|
|
258
|
+
import tty
|
|
259
|
+
|
|
260
|
+
fd = sys.stdin.fileno()
|
|
261
|
+
old = termios.tcgetattr(fd)
|
|
262
|
+
try:
|
|
263
|
+
tty.setraw(fd)
|
|
264
|
+
ch = sys.stdin.read(1)
|
|
265
|
+
if ch == "\x1b":
|
|
266
|
+
ch2 = sys.stdin.read(1)
|
|
267
|
+
if ch2 == "[":
|
|
268
|
+
ch3 = sys.stdin.read(1)
|
|
269
|
+
return f"\x1b[{ch3}"
|
|
270
|
+
return ch + ch2
|
|
271
|
+
return ch
|
|
272
|
+
finally:
|
|
273
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _read_key_tty() -> str:
|
|
277
|
+
"""Read a keypress from /dev/tty (works even when stdin is piped)."""
|
|
278
|
+
import termios
|
|
279
|
+
import tty
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
fd = os.open("/dev/tty", os.O_RDONLY)
|
|
283
|
+
except OSError:
|
|
284
|
+
return _read_key()
|
|
285
|
+
|
|
286
|
+
old = termios.tcgetattr(fd)
|
|
287
|
+
try:
|
|
288
|
+
tty.setraw(fd)
|
|
289
|
+
ch = os.read(fd, 1).decode("utf-8", errors="replace")
|
|
290
|
+
if ch == "\x1b":
|
|
291
|
+
ch2 = os.read(fd, 1).decode("utf-8", errors="replace")
|
|
292
|
+
if ch2 == "[":
|
|
293
|
+
ch3 = os.read(fd, 1).decode("utf-8", errors="replace")
|
|
294
|
+
return f"\x1b[{ch3}"
|
|
295
|
+
return ch + ch2
|
|
296
|
+
return ch
|
|
297
|
+
finally:
|
|
298
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
299
|
+
os.close(fd)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def multiselect(
|
|
303
|
+
items: List[Dict[str, Any]],
|
|
304
|
+
pre_selected: Optional[List[int]] = None,
|
|
305
|
+
) -> List[int]:
|
|
306
|
+
"""Interactive multi-select with arrow keys and spacebar.
|
|
307
|
+
|
|
308
|
+
items: list of {"label": str, "hint": str, "detected": bool}
|
|
309
|
+
pre_selected: indices that start checked
|
|
310
|
+
|
|
311
|
+
Returns list of selected indices.
|
|
312
|
+
"""
|
|
313
|
+
out = sys.stderr
|
|
314
|
+
selected = set(pre_selected or [])
|
|
315
|
+
cursor = 0
|
|
316
|
+
n = len(items)
|
|
317
|
+
|
|
318
|
+
def render():
|
|
319
|
+
for i, item in enumerate(items):
|
|
320
|
+
prefix = " >" if i == cursor else " "
|
|
321
|
+
check = "\033[32m[x]\033[0m" if i in selected else "[ ]"
|
|
322
|
+
label = item["label"]
|
|
323
|
+
hint = item.get("hint", "")
|
|
324
|
+
detected = item.get("detected", False)
|
|
325
|
+
|
|
326
|
+
if i == cursor:
|
|
327
|
+
label = f"\033[1m{label}\033[0m"
|
|
328
|
+
|
|
329
|
+
if hint:
|
|
330
|
+
if detected:
|
|
331
|
+
hint_str = f" \033[2m{hint}\033[0m"
|
|
332
|
+
else:
|
|
333
|
+
hint_str = " \033[2m(not detected)\033[0m"
|
|
334
|
+
else:
|
|
335
|
+
hint_str = ""
|
|
336
|
+
|
|
337
|
+
out.write(f"{prefix} {check} {label}{hint_str}\n")
|
|
338
|
+
|
|
339
|
+
def clear():
|
|
340
|
+
for _ in range(n):
|
|
341
|
+
out.write("\033[1A\033[2K")
|
|
342
|
+
|
|
343
|
+
# Hide cursor
|
|
344
|
+
out.write("\033[?25l")
|
|
345
|
+
out.flush()
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
render()
|
|
349
|
+
out.flush()
|
|
350
|
+
|
|
351
|
+
while True:
|
|
352
|
+
key = _read_key_tty()
|
|
353
|
+
|
|
354
|
+
if key == "\x1b[A": # Up
|
|
355
|
+
cursor = (cursor - 1) % n
|
|
356
|
+
elif key == "\x1b[B": # Down
|
|
357
|
+
cursor = (cursor + 1) % n
|
|
358
|
+
elif key == " ": # Space = toggle
|
|
359
|
+
if cursor in selected:
|
|
360
|
+
selected.discard(cursor)
|
|
361
|
+
else:
|
|
362
|
+
selected.add(cursor)
|
|
363
|
+
elif key == "a": # Toggle all
|
|
364
|
+
if len(selected) == n:
|
|
365
|
+
selected.clear()
|
|
366
|
+
else:
|
|
367
|
+
selected = set(range(n))
|
|
368
|
+
elif key in ("\r", "\n"): # Enter = confirm
|
|
369
|
+
clear()
|
|
370
|
+
# Show final state (non-interactive)
|
|
371
|
+
for i, item in enumerate(items):
|
|
372
|
+
check = "\033[32m[x]\033[0m" if i in selected else "\033[2m[ ]\033[0m"
|
|
373
|
+
label = item["label"]
|
|
374
|
+
out.write(f" {check} {label}\n")
|
|
375
|
+
out.flush()
|
|
376
|
+
return sorted(selected)
|
|
377
|
+
elif key in ("\x03", "\x04"): # Ctrl-C / Ctrl-D
|
|
378
|
+
clear()
|
|
379
|
+
out.write("\033[?25h")
|
|
380
|
+
out.flush()
|
|
381
|
+
raise KeyboardInterrupt
|
|
382
|
+
|
|
383
|
+
clear()
|
|
384
|
+
render()
|
|
385
|
+
out.flush()
|
|
386
|
+
finally:
|
|
387
|
+
out.write("\033[?25h")
|
|
388
|
+
out.flush()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ─── Shell Completions ───────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def detect_shell() -> str:
|
|
395
|
+
"""Detect the user's shell."""
|
|
396
|
+
return os.path.basename(os.environ.get("SHELL", "sh"))
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def install_completions(shell: str, source_dir: str) -> Dict[str, Any]:
|
|
400
|
+
"""Install shell completions. Returns result dict."""
|
|
401
|
+
home = os.path.expanduser("~")
|
|
402
|
+
|
|
403
|
+
if shell == "bash":
|
|
404
|
+
xdg = os.environ.get("XDG_DATA_HOME", os.path.join(home, ".local", "share"))
|
|
405
|
+
comp_dir = os.environ.get(
|
|
406
|
+
"BASH_COMPLETION_USER_DIR",
|
|
407
|
+
os.path.join(xdg, "bash-completion", "completions"),
|
|
408
|
+
)
|
|
409
|
+
src = os.path.join(source_dir, "completions", "bingo-light.bash")
|
|
410
|
+
dst = os.path.join(comp_dir, "bingo-light")
|
|
411
|
+
elif shell == "zsh":
|
|
412
|
+
comp_dir = os.path.join(home, ".zfunc")
|
|
413
|
+
src = os.path.join(source_dir, "completions", "bingo-light.zsh")
|
|
414
|
+
dst = os.path.join(comp_dir, "_bingo-light")
|
|
415
|
+
elif shell == "fish":
|
|
416
|
+
comp_dir = os.path.join(home, ".config", "fish", "completions")
|
|
417
|
+
src = os.path.join(source_dir, "completions", "bingo-light.fish")
|
|
418
|
+
dst = os.path.join(comp_dir, "bingo-light.fish")
|
|
419
|
+
else:
|
|
420
|
+
return {"ok": False, "shell": shell, "error": f"Unsupported shell: {shell}"}
|
|
421
|
+
|
|
422
|
+
if not os.path.isfile(src):
|
|
423
|
+
return {"ok": False, "shell": shell, "error": f"Completion file not found: {src}"}
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
os.makedirs(comp_dir, exist_ok=True)
|
|
427
|
+
shutil.copy2(src, dst)
|
|
428
|
+
except OSError as e:
|
|
429
|
+
return {"ok": False, "shell": shell, "error": str(e)}
|
|
430
|
+
|
|
431
|
+
# For zsh, ensure fpath is set in .zshrc
|
|
432
|
+
if shell == "zsh":
|
|
433
|
+
zshrc = os.path.join(home, ".zshrc")
|
|
434
|
+
if os.path.isfile(zshrc):
|
|
435
|
+
with open(zshrc) as f:
|
|
436
|
+
content = f.read()
|
|
437
|
+
if ".zfunc" not in content:
|
|
438
|
+
with open(zshrc, "a") as f:
|
|
439
|
+
f.write("\nfpath=(~/.zfunc $fpath)\nautoload -Uz compinit && compinit\n")
|
|
440
|
+
|
|
441
|
+
return {"ok": True, "shell": shell, "path": dst}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ─── Main Setup Flow ────────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def run_setup(
|
|
448
|
+
yes: bool = False,
|
|
449
|
+
json_mode: bool = False,
|
|
450
|
+
no_completions: bool = False,
|
|
451
|
+
) -> Dict[str, Any]:
|
|
452
|
+
"""Run the interactive setup wizard.
|
|
453
|
+
|
|
454
|
+
Returns a result dict summarizing what was configured.
|
|
455
|
+
"""
|
|
456
|
+
out = sys.stderr
|
|
457
|
+
tools = _get_tools()
|
|
458
|
+
command, args = find_mcp_server()
|
|
459
|
+
results: List[Dict[str, Any]] = []
|
|
460
|
+
is_tty = hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
|
|
461
|
+
|
|
462
|
+
# ── Header ──
|
|
463
|
+
if not json_mode and is_tty:
|
|
464
|
+
out.write("\n \033[1mbingo-light setup\033[0m\n")
|
|
465
|
+
out.write(" \033[2mConfigure MCP server for your AI tools\033[0m\n\n")
|
|
466
|
+
out.write(f" MCP server: \033[2m{command} {' '.join(args)}\033[0m\n\n")
|
|
467
|
+
|
|
468
|
+
# ── Detect tools ──
|
|
469
|
+
items = []
|
|
470
|
+
detected_indices = []
|
|
471
|
+
for i, tool in enumerate(tools):
|
|
472
|
+
detected = tool.is_detected()
|
|
473
|
+
items.append({
|
|
474
|
+
"label": tool.name,
|
|
475
|
+
"hint": _tildify(tool.expand_path()),
|
|
476
|
+
"detected": detected,
|
|
477
|
+
})
|
|
478
|
+
if detected:
|
|
479
|
+
detected_indices.append(i)
|
|
480
|
+
|
|
481
|
+
# ── Select tools ──
|
|
482
|
+
if yes:
|
|
483
|
+
# Non-interactive: configure all detected tools
|
|
484
|
+
selected = detected_indices
|
|
485
|
+
if not json_mode and is_tty:
|
|
486
|
+
out.write(" Auto-configuring detected tools:\n")
|
|
487
|
+
for i in selected:
|
|
488
|
+
out.write(f" \033[32m[x]\033[0m {tools[i].name}\n")
|
|
489
|
+
out.write("\n")
|
|
490
|
+
elif is_tty:
|
|
491
|
+
# Interactive multi-select
|
|
492
|
+
out.write(" Select tools to configure:\n")
|
|
493
|
+
out.write(" \033[2m↑/↓ navigate SPACE select a toggle all ENTER confirm\033[0m\n\n")
|
|
494
|
+
selected = multiselect(items, pre_selected=detected_indices)
|
|
495
|
+
out.write("\n")
|
|
496
|
+
else:
|
|
497
|
+
# Non-TTY fallback: configure detected tools
|
|
498
|
+
selected = detected_indices
|
|
499
|
+
|
|
500
|
+
if not selected:
|
|
501
|
+
if not json_mode and is_tty:
|
|
502
|
+
out.write(" No tools selected.\n\n")
|
|
503
|
+
return {"ok": True, "configured": [], "skipped": True}
|
|
504
|
+
|
|
505
|
+
# ── Configure each selected tool ──
|
|
506
|
+
for idx in selected:
|
|
507
|
+
tool = tools[idx]
|
|
508
|
+
result = write_mcp_config(tool, command, args)
|
|
509
|
+
results.append(result)
|
|
510
|
+
|
|
511
|
+
if not json_mode and is_tty:
|
|
512
|
+
if result["ok"]:
|
|
513
|
+
path = _tildify(result["config_path"])
|
|
514
|
+
out.write(f" \033[32m✓\033[0m {tool.name} → {path}\n")
|
|
515
|
+
else:
|
|
516
|
+
out.write(f" \033[31mx\033[0m {tool.name}: {result.get('error', 'unknown error')}\n")
|
|
517
|
+
|
|
518
|
+
# ── Shell completions ──
|
|
519
|
+
if not no_completions:
|
|
520
|
+
shell = detect_shell()
|
|
521
|
+
# Find source dir (completions/ alongside bingo-light script)
|
|
522
|
+
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
523
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
524
|
+
source_dir = script_dir if os.path.isdir(os.path.join(script_dir, "completions")) else os.path.dirname(pkg_dir)
|
|
525
|
+
|
|
526
|
+
if os.path.isdir(os.path.join(source_dir, "completions")):
|
|
527
|
+
comp_result = install_completions(shell, source_dir)
|
|
528
|
+
if not json_mode and is_tty:
|
|
529
|
+
if comp_result["ok"]:
|
|
530
|
+
out.write(f" \033[32m✓\033[0m {shell} completions installed\n")
|
|
531
|
+
else:
|
|
532
|
+
out.write(f" \033[2m⊘ {shell} completions: {comp_result.get('error', 'skipped')}\033[0m\n")
|
|
533
|
+
|
|
534
|
+
# ── Summary ──
|
|
535
|
+
configured = [r for r in results if r["ok"]]
|
|
536
|
+
failed = [r for r in results if not r["ok"]]
|
|
537
|
+
|
|
538
|
+
if not json_mode and is_tty:
|
|
539
|
+
out.write(f"\n \033[1m\033[32m✓ {len(configured)} tool(s) configured\033[0m")
|
|
540
|
+
if failed:
|
|
541
|
+
out.write(f", \033[31m{len(failed)} failed\033[0m")
|
|
542
|
+
out.write("\n\n")
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
"ok": len(failed) == 0,
|
|
546
|
+
"configured": [r["tool"] for r in configured],
|
|
547
|
+
"failed": [r["tool"] for r in failed],
|
|
548
|
+
"results": results,
|
|
549
|
+
}
|