bingo-light 2.0.1 → 2.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/.claude/commands/bingo.md +116 -0
- package/bingo_core/__init__.py +1 -1
- package/bingo_core/setup.py +343 -55
- package/mcp-server.py +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bingo
|
|
3
|
+
description: Manage forked git repos with bingo-light — sync upstream, resolve conflicts, manage patch stack
|
|
4
|
+
allowed-tools: Bash(bingo-light *) Bash(git add *) Bash(git rebase --continue) Read Edit
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are managing a forked git repository using bingo-light. Your patches live as a clean stack on top of upstream. Your job: keep the fork in sync, resolve conflicts, and manage patches — autonomously.
|
|
8
|
+
|
|
9
|
+
## Decision Loop
|
|
10
|
+
|
|
11
|
+
Always start with status. The `recommended_action` field tells you what to do:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bingo-light status --json --yes
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Response includes `recommended_action`:
|
|
18
|
+
- `"up_to_date"` → Nothing to do. Tell the user.
|
|
19
|
+
- `"sync_safe"` → Run sync directly (no conflict risk).
|
|
20
|
+
- `"sync_risky"` → Dry-run first, then sync if clean, resolve if conflicts.
|
|
21
|
+
- `"resolve_conflict"` → Already mid-rebase. Analyze and resolve.
|
|
22
|
+
- `"unknown"` → Run doctor for diagnostics.
|
|
23
|
+
|
|
24
|
+
## Sync Flow
|
|
25
|
+
|
|
26
|
+
### Use smart-sync (preferred — one call does everything)
|
|
27
|
+
```bash
|
|
28
|
+
bingo-light smart-sync --json --yes
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Responses:
|
|
32
|
+
- `{"ok":true, "action":"none"}` → Already up to date
|
|
33
|
+
- `{"ok":true, "action":"synced", "conflicts_resolved":0}` → Clean sync
|
|
34
|
+
- `{"ok":true, "action":"synced_with_rerere", "conflicts_auto_resolved":N}` → Conflicts auto-resolved by rerere
|
|
35
|
+
- `{"ok":false, "action":"needs_human", "remaining_conflicts":[...]}` → Needs manual resolution (see below)
|
|
36
|
+
|
|
37
|
+
### When smart-sync returns needs_human
|
|
38
|
+
|
|
39
|
+
The response includes `remaining_conflicts` with full context:
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"remaining_conflicts": [
|
|
43
|
+
{"file": "app.py", "ours": "upstream code", "theirs": "your code", "merge_hint": "Keep both"}
|
|
44
|
+
],
|
|
45
|
+
"resolution_steps": ["1. Read ours/theirs", "2. Write merged", "3. git add", "4. git rebase --continue"]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For each conflict:
|
|
50
|
+
1. Read the `merge_hint` — it tells you the strategy
|
|
51
|
+
2. Read the actual file (has <<<<<<< ======= >>>>>>> markers)
|
|
52
|
+
3. Write the merged version (usually: keep BOTH changes)
|
|
53
|
+
4. `git add <file>`
|
|
54
|
+
5. `git rebase --continue`
|
|
55
|
+
6. Run `bingo-light status --json` to verify
|
|
56
|
+
|
|
57
|
+
### Fallback: manual sync (for fine-grained control)
|
|
58
|
+
```bash
|
|
59
|
+
bingo-light sync --dry-run --json --yes # Preview
|
|
60
|
+
bingo-light sync --json --yes # Execute
|
|
61
|
+
bingo-light conflict-analyze --json # If conflict
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Patch Management
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Create a patch (always set BINGO_DESCRIPTION)
|
|
68
|
+
BINGO_DESCRIPTION="what this patch does" bingo-light patch new <name> --json --yes
|
|
69
|
+
|
|
70
|
+
# List patches
|
|
71
|
+
bingo-light patch list --json --yes
|
|
72
|
+
|
|
73
|
+
# Show a specific patch diff
|
|
74
|
+
bingo-light patch show <name-or-index> --json --yes
|
|
75
|
+
|
|
76
|
+
# Remove a patch
|
|
77
|
+
bingo-light patch drop <name-or-index> --json --yes
|
|
78
|
+
|
|
79
|
+
# Reorder patches (provide ALL indices)
|
|
80
|
+
bingo-light patch reorder --order "3,1,2" --json --yes
|
|
81
|
+
|
|
82
|
+
# Merge two adjacent patches
|
|
83
|
+
bingo-light patch squash <idx1> <idx2> --json --yes
|
|
84
|
+
|
|
85
|
+
# Edit a patch (stage changes first, then edit)
|
|
86
|
+
git add <files>
|
|
87
|
+
bingo-light patch edit <name-or-index> --json --yes
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Diagnostics
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bingo-light doctor --json --yes # Full health check
|
|
94
|
+
bingo-light diff --json --yes # All changes vs upstream
|
|
95
|
+
bingo-light history --json --yes # Sync history with hashes
|
|
96
|
+
bingo-light undo --json --yes # Revert last sync
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
bingo-light config set test.command "make test" # Run tests after sync
|
|
103
|
+
bingo-light config set sync.auto-test true # Auto-test on sync
|
|
104
|
+
bingo-light test --json --yes # Run tests manually
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Key Rules
|
|
108
|
+
|
|
109
|
+
1. **Always use `--json --yes`** when calling via Bash
|
|
110
|
+
2. **Always check `recommended_action`** from status before deciding what to do
|
|
111
|
+
3. **Read `merge_hint`** from conflict-analyze — it tells you the resolution strategy
|
|
112
|
+
4. **After resolving conflicts**: `git add` then `git rebase --continue`, NOT `bingo-light sync`
|
|
113
|
+
5. **BINGO_DESCRIPTION** env var sets patch description (required for `patch new`)
|
|
114
|
+
6. Patch names: alphanumeric + hyphens + underscores only
|
|
115
|
+
7. `bingo-light undo` reverts the last sync — use it if sync went wrong
|
|
116
|
+
8. rerere remembers conflict resolutions — same conflict auto-resolves on next sync
|
package/bingo_core/__init__.py
CHANGED
package/bingo_core/setup.py
CHANGED
|
@@ -299,48 +299,114 @@ def _read_key_tty() -> str:
|
|
|
299
299
|
os.close(fd)
|
|
300
300
|
|
|
301
301
|
|
|
302
|
+
# ─── UI Primitives (clack-style tree-line) ───────────────────────────────────
|
|
303
|
+
|
|
304
|
+
# ANSI helpers
|
|
305
|
+
_B = "\033[1m" # bold
|
|
306
|
+
_D = "\033[2m" # dim
|
|
307
|
+
_R = "\033[0m" # reset
|
|
308
|
+
_G = "\033[32m" # green
|
|
309
|
+
_C = "\033[36m" # cyan
|
|
310
|
+
_Y = "\033[33m" # yellow
|
|
311
|
+
_RD = "\033[31m" # red
|
|
312
|
+
_BG = "\033[38;5;75m" # branded blue
|
|
313
|
+
|
|
314
|
+
BAR = f"{_D}│{_R}" # vertical connector
|
|
315
|
+
END = f"{_D}└{_R}" # end connector
|
|
316
|
+
DOT = f"{_C}◆{_R}" # active section marker
|
|
317
|
+
CHK = f"{_G}◇{_R}" # completed section marker
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _ui_header(out, version: str) -> None:
|
|
321
|
+
out.write(f"\n {_BG}{_B}◆ bingo-light setup{_R} {_D}v{version}{_R}\n")
|
|
322
|
+
out.write(f" {BAR}\n")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _ui_section(out, title: str, subtitle: str = "") -> None:
|
|
326
|
+
out.write(f" {BAR}\n")
|
|
327
|
+
out.write(f" {DOT} {_B}{title}{_R}\n")
|
|
328
|
+
if subtitle:
|
|
329
|
+
out.write(f" {BAR} {_D}{subtitle}{_R}\n")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _ui_info(out, text: str) -> None:
|
|
333
|
+
out.write(f" {BAR} {_D}{text}{_R}\n")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _ui_ok(out, text: str) -> None:
|
|
337
|
+
out.write(f" {BAR} {_G}✓{_R} {text}\n")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _ui_fail(out, text: str) -> None:
|
|
341
|
+
out.write(f" {BAR} {_RD}✗{_R} {text}\n")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _ui_skip(out, text: str) -> None:
|
|
345
|
+
out.write(f" {BAR} {_D}⊘ {text}{_R}\n")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _ui_done(out, text: str) -> None:
|
|
349
|
+
out.write(f" {BAR}\n")
|
|
350
|
+
out.write(f" {END} {_G}{_B}{text}{_R}\n\n")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _ui_bar(out) -> None:
|
|
354
|
+
out.write(f" {BAR}\n")
|
|
355
|
+
|
|
356
|
+
|
|
302
357
|
def multiselect(
|
|
358
|
+
out,
|
|
303
359
|
items: List[Dict[str, Any]],
|
|
304
360
|
pre_selected: Optional[List[int]] = None,
|
|
305
361
|
) -> List[int]:
|
|
306
|
-
"""Interactive multi-select with
|
|
362
|
+
"""Interactive multi-select with clack-style rendering.
|
|
307
363
|
|
|
308
364
|
items: list of {"label": str, "hint": str, "detected": bool}
|
|
309
|
-
pre_selected: indices that start checked
|
|
310
|
-
|
|
311
365
|
Returns list of selected indices.
|
|
312
366
|
"""
|
|
313
|
-
out = sys.stderr
|
|
314
367
|
selected = set(pre_selected or [])
|
|
315
368
|
cursor = 0
|
|
316
369
|
n = len(items)
|
|
317
370
|
|
|
318
371
|
def render():
|
|
319
372
|
for i, item in enumerate(items):
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
label = item["label"]
|
|
323
|
-
hint = item.get("hint", "")
|
|
373
|
+
is_cur = (i == cursor)
|
|
374
|
+
is_sel = (i in selected)
|
|
324
375
|
detected = item.get("detected", False)
|
|
376
|
+
hint = item.get("hint", "")
|
|
325
377
|
|
|
326
|
-
|
|
327
|
-
|
|
378
|
+
# Checkbox
|
|
379
|
+
if is_sel:
|
|
380
|
+
check = f"{_G}■{_R}"
|
|
381
|
+
elif is_cur:
|
|
382
|
+
check = f"{_C}□{_R}"
|
|
383
|
+
else:
|
|
384
|
+
check = f"{_D}□{_R}"
|
|
328
385
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
386
|
+
# Label
|
|
387
|
+
label = item["label"]
|
|
388
|
+
if is_cur:
|
|
389
|
+
label = f"{_B}{label}{_R}"
|
|
390
|
+
|
|
391
|
+
# Hint
|
|
392
|
+
if hint and detected:
|
|
393
|
+
hint_str = f" {_D}{hint}{_R}"
|
|
394
|
+
elif hint:
|
|
395
|
+
hint_str = f" {_D}(not detected){_R}"
|
|
334
396
|
else:
|
|
335
397
|
hint_str = ""
|
|
336
398
|
|
|
337
|
-
|
|
399
|
+
# Cursor indicator
|
|
400
|
+
cur = f"{_C}›{_R}" if is_cur else " "
|
|
401
|
+
|
|
402
|
+
out.write(f" {BAR} {cur} {check} {label}{hint_str}\n")
|
|
338
403
|
|
|
339
404
|
def clear():
|
|
340
405
|
for _ in range(n):
|
|
341
406
|
out.write("\033[1A\033[2K")
|
|
342
407
|
|
|
343
|
-
|
|
408
|
+
out.write(f" {BAR} {_D}↑/↓ navigate · space select · a all · enter confirm{_R}\n")
|
|
409
|
+
out.write(f" {BAR}\n")
|
|
344
410
|
out.write("\033[?25l")
|
|
345
411
|
out.flush()
|
|
346
412
|
|
|
@@ -367,11 +433,11 @@ def multiselect(
|
|
|
367
433
|
selected = set(range(n))
|
|
368
434
|
elif key in ("\r", "\n"): # Enter = confirm
|
|
369
435
|
clear()
|
|
370
|
-
# Show final state (non-interactive)
|
|
371
436
|
for i, item in enumerate(items):
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
437
|
+
if i in selected:
|
|
438
|
+
out.write(f" {BAR} {_G}■{_R} {item['label']}\n")
|
|
439
|
+
else:
|
|
440
|
+
out.write(f" {BAR} {_D}□ {item['label']}{_R}\n")
|
|
375
441
|
out.flush()
|
|
376
442
|
return sorted(selected)
|
|
377
443
|
elif key in ("\x03", "\x04"): # Ctrl-C / Ctrl-D
|
|
@@ -441,6 +507,148 @@ def install_completions(shell: str, source_dir: str) -> Dict[str, Any]:
|
|
|
441
507
|
return {"ok": True, "shell": shell, "path": dst}
|
|
442
508
|
|
|
443
509
|
|
|
510
|
+
# ─── Skill / Custom Instructions ─────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
SKILL_MARKER = "<!-- bingo-light-skill -->"
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@dataclass
|
|
516
|
+
class SkillTarget:
|
|
517
|
+
"""Describes where to install the bingo-light skill/rules for one AI tool."""
|
|
518
|
+
|
|
519
|
+
id: str
|
|
520
|
+
name: str
|
|
521
|
+
dest_path: str # ~ expanded at runtime
|
|
522
|
+
mode: str # "copy" = drop file, "append" = append to existing file
|
|
523
|
+
detect_dirs: List[str] = field(default_factory=list)
|
|
524
|
+
detect_cmds: List[str] = field(default_factory=list)
|
|
525
|
+
note: str = ""
|
|
526
|
+
|
|
527
|
+
def expand_dest(self) -> str:
|
|
528
|
+
return os.path.expandvars(os.path.expanduser(self.dest_path))
|
|
529
|
+
|
|
530
|
+
def is_detected(self) -> bool:
|
|
531
|
+
for d in self.detect_dirs:
|
|
532
|
+
if os.path.isdir(os.path.expanduser(d)):
|
|
533
|
+
return True
|
|
534
|
+
for c in self.detect_cmds:
|
|
535
|
+
if shutil.which(c):
|
|
536
|
+
return True
|
|
537
|
+
return False
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _get_skill_targets() -> List[SkillTarget]:
|
|
541
|
+
"""Return all supported skill/rules targets."""
|
|
542
|
+
cline_rules = "~/Documents/Cline/Rules"
|
|
543
|
+
|
|
544
|
+
return [
|
|
545
|
+
SkillTarget(
|
|
546
|
+
id="claude-code",
|
|
547
|
+
name="Claude Code",
|
|
548
|
+
dest_path="~/.claude/commands/bingo.md",
|
|
549
|
+
mode="copy",
|
|
550
|
+
detect_dirs=["~/.claude"],
|
|
551
|
+
detect_cmds=["claude"],
|
|
552
|
+
note="/bingo slash command",
|
|
553
|
+
),
|
|
554
|
+
SkillTarget(
|
|
555
|
+
id="windsurf",
|
|
556
|
+
name="Windsurf",
|
|
557
|
+
dest_path="~/.codeium/windsurf/memories/global_rules.md",
|
|
558
|
+
mode="append",
|
|
559
|
+
detect_dirs=["~/.codeium/windsurf"],
|
|
560
|
+
note="Appends to global rules (6000 char limit)",
|
|
561
|
+
),
|
|
562
|
+
SkillTarget(
|
|
563
|
+
id="continue",
|
|
564
|
+
name="Continue",
|
|
565
|
+
dest_path="~/.continue/rules/bingo.md",
|
|
566
|
+
mode="copy",
|
|
567
|
+
detect_dirs=["~/.continue"],
|
|
568
|
+
),
|
|
569
|
+
SkillTarget(
|
|
570
|
+
id="cline",
|
|
571
|
+
name="Cline",
|
|
572
|
+
dest_path=cline_rules + "/bingo.md",
|
|
573
|
+
mode="copy",
|
|
574
|
+
detect_dirs=[cline_rules],
|
|
575
|
+
),
|
|
576
|
+
SkillTarget(
|
|
577
|
+
id="roo-code",
|
|
578
|
+
name="Roo Code",
|
|
579
|
+
dest_path="~/.roo/rules/bingo.md",
|
|
580
|
+
mode="copy",
|
|
581
|
+
detect_dirs=["~/.roo"],
|
|
582
|
+
),
|
|
583
|
+
SkillTarget(
|
|
584
|
+
id="gemini-cli",
|
|
585
|
+
name="Gemini CLI",
|
|
586
|
+
dest_path="~/.gemini/GEMINI.md",
|
|
587
|
+
mode="append",
|
|
588
|
+
detect_dirs=["~/.gemini"],
|
|
589
|
+
detect_cmds=["gemini"],
|
|
590
|
+
note="Appends to GEMINI.md",
|
|
591
|
+
),
|
|
592
|
+
]
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def find_skill_file() -> Optional[str]:
|
|
596
|
+
"""Find the bingo.md skill file in common locations."""
|
|
597
|
+
candidates = []
|
|
598
|
+
|
|
599
|
+
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
600
|
+
candidates.append(os.path.join(script_dir, ".claude", "commands", "bingo.md"))
|
|
601
|
+
|
|
602
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
603
|
+
repo_dir = os.path.dirname(pkg_dir)
|
|
604
|
+
candidates.append(os.path.join(repo_dir, ".claude", "commands", "bingo.md"))
|
|
605
|
+
|
|
606
|
+
# npm package layout
|
|
607
|
+
candidates.append(os.path.join(script_dir, "..", ".claude", "commands", "bingo.md"))
|
|
608
|
+
|
|
609
|
+
for c in candidates:
|
|
610
|
+
if os.path.isfile(c):
|
|
611
|
+
return os.path.abspath(c)
|
|
612
|
+
return None
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def install_skill_to(target: SkillTarget, content: str) -> Dict[str, Any]:
|
|
616
|
+
"""Install skill content to one target. Returns result dict."""
|
|
617
|
+
dest = target.expand_dest()
|
|
618
|
+
dest_dir = os.path.dirname(dest)
|
|
619
|
+
|
|
620
|
+
try:
|
|
621
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
622
|
+
except OSError as e:
|
|
623
|
+
return {"ok": False, "id": target.id, "error": str(e)}
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
if target.mode == "copy":
|
|
627
|
+
with open(dest, "w") as f:
|
|
628
|
+
f.write(content)
|
|
629
|
+
return {"ok": True, "id": target.id, "path": dest, "action": "created"}
|
|
630
|
+
|
|
631
|
+
elif target.mode == "append":
|
|
632
|
+
# Check if already present
|
|
633
|
+
existing = ""
|
|
634
|
+
if os.path.isfile(dest):
|
|
635
|
+
with open(dest) as f:
|
|
636
|
+
existing = f.read()
|
|
637
|
+
if SKILL_MARKER in existing:
|
|
638
|
+
return {"ok": True, "id": target.id, "path": dest, "action": "unchanged"}
|
|
639
|
+
# Append with marker
|
|
640
|
+
with open(dest, "a") as f:
|
|
641
|
+
if existing and not existing.endswith("\n"):
|
|
642
|
+
f.write("\n")
|
|
643
|
+
f.write(f"\n{SKILL_MARKER}\n{content}\n")
|
|
644
|
+
return {"ok": True, "id": target.id, "path": dest, "action": "appended"}
|
|
645
|
+
|
|
646
|
+
except OSError as e:
|
|
647
|
+
return {"ok": False, "id": target.id, "error": str(e)}
|
|
648
|
+
|
|
649
|
+
return {"ok": False, "id": target.id, "error": "Unknown mode"}
|
|
650
|
+
|
|
651
|
+
|
|
444
652
|
# ─── Main Setup Flow ────────────────────────────────────────────────────────
|
|
445
653
|
|
|
446
654
|
|
|
@@ -453,19 +661,24 @@ def run_setup(
|
|
|
453
661
|
|
|
454
662
|
Returns a result dict summarizing what was configured.
|
|
455
663
|
"""
|
|
664
|
+
from bingo_core import VERSION
|
|
665
|
+
|
|
456
666
|
out = sys.stderr
|
|
457
667
|
tools = _get_tools()
|
|
458
668
|
command, args = find_mcp_server()
|
|
459
669
|
results: List[Dict[str, Any]] = []
|
|
460
670
|
is_tty = hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
|
|
461
671
|
|
|
462
|
-
#
|
|
672
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
673
|
+
# Header
|
|
674
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
463
675
|
if not json_mode and is_tty:
|
|
464
|
-
out
|
|
465
|
-
out
|
|
466
|
-
out.write(f" MCP server: \033[2m{command} {' '.join(args)}\033[0m\n\n")
|
|
676
|
+
_ui_header(out, VERSION)
|
|
677
|
+
_ui_info(out, f"MCP server: {command} {' '.join(args)}")
|
|
467
678
|
|
|
468
|
-
#
|
|
679
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
680
|
+
# Step 1: MCP Server Configuration
|
|
681
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
469
682
|
items = []
|
|
470
683
|
detected_indices = []
|
|
471
684
|
for i, tool in enumerate(tools):
|
|
@@ -478,72 +691,147 @@ def run_setup(
|
|
|
478
691
|
if detected:
|
|
479
692
|
detected_indices.append(i)
|
|
480
693
|
|
|
481
|
-
|
|
694
|
+
if not json_mode and is_tty:
|
|
695
|
+
_ui_section(out, "MCP Server", "Connect bingo-light tools to your AI coding assistants")
|
|
696
|
+
|
|
482
697
|
if yes:
|
|
483
|
-
# Non-interactive: configure all detected tools
|
|
484
698
|
selected = detected_indices
|
|
485
699
|
if not json_mode and is_tty:
|
|
486
|
-
out.write(" Auto-configuring detected tools:\n")
|
|
487
700
|
for i in selected:
|
|
488
|
-
out.write(f"
|
|
489
|
-
out.write("\n")
|
|
701
|
+
out.write(f" {BAR} {_G}■{_R} {tools[i].name}\n")
|
|
490
702
|
elif is_tty:
|
|
491
|
-
|
|
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")
|
|
703
|
+
selected = multiselect(out, items, pre_selected=detected_indices)
|
|
496
704
|
else:
|
|
497
|
-
# Non-TTY fallback: configure detected tools
|
|
498
705
|
selected = detected_indices
|
|
499
706
|
|
|
500
707
|
if not selected:
|
|
501
708
|
if not json_mode and is_tty:
|
|
502
|
-
out
|
|
503
|
-
return {"ok": True, "configured": [], "skipped": True}
|
|
709
|
+
_ui_skip(out, "No tools selected")
|
|
504
710
|
|
|
505
|
-
#
|
|
711
|
+
# Configure each selected tool
|
|
506
712
|
for idx in selected:
|
|
507
713
|
tool = tools[idx]
|
|
508
714
|
result = write_mcp_config(tool, command, args)
|
|
509
715
|
results.append(result)
|
|
510
|
-
|
|
511
716
|
if not json_mode and is_tty:
|
|
512
717
|
if result["ok"]:
|
|
513
|
-
|
|
514
|
-
out.write(f" \033[32m✓\033[0m {tool.name} → {path}\n")
|
|
718
|
+
_ui_ok(out, f"{tool.name} → {_tildify(result['config_path'])}")
|
|
515
719
|
else:
|
|
516
|
-
out
|
|
720
|
+
_ui_fail(out, f"{tool.name}: {result.get('error', '?')}")
|
|
721
|
+
|
|
722
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
723
|
+
# Step 2: Skills / Custom Instructions
|
|
724
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
725
|
+
skill_results: List[Dict[str, Any]] = []
|
|
726
|
+
skill_src = find_skill_file()
|
|
727
|
+
|
|
728
|
+
if skill_src:
|
|
729
|
+
with open(skill_src) as f:
|
|
730
|
+
skill_content = f.read()
|
|
517
731
|
|
|
518
|
-
|
|
732
|
+
skill_targets = _get_skill_targets()
|
|
733
|
+
|
|
734
|
+
if not json_mode and is_tty:
|
|
735
|
+
_ui_section(out, "Skills / Custom Instructions", "Teach your AI how to use bingo-light")
|
|
736
|
+
|
|
737
|
+
skill_items = []
|
|
738
|
+
skill_detected = []
|
|
739
|
+
for i, st in enumerate(skill_targets):
|
|
740
|
+
detected = st.is_detected()
|
|
741
|
+
hint = _tildify(st.expand_dest())
|
|
742
|
+
if st.note:
|
|
743
|
+
hint = f"{hint} ({st.note})"
|
|
744
|
+
skill_items.append({
|
|
745
|
+
"label": st.name,
|
|
746
|
+
"hint": hint,
|
|
747
|
+
"detected": detected,
|
|
748
|
+
})
|
|
749
|
+
if detected:
|
|
750
|
+
skill_detected.append(i)
|
|
751
|
+
|
|
752
|
+
if yes:
|
|
753
|
+
skill_selected = skill_detected
|
|
754
|
+
if not json_mode and is_tty:
|
|
755
|
+
for i in skill_selected:
|
|
756
|
+
out.write(f" {BAR} {_G}■{_R} {skill_targets[i].name}\n")
|
|
757
|
+
elif is_tty:
|
|
758
|
+
skill_selected = multiselect(out, skill_items, pre_selected=skill_detected)
|
|
759
|
+
else:
|
|
760
|
+
skill_selected = skill_detected
|
|
761
|
+
|
|
762
|
+
for idx in skill_selected:
|
|
763
|
+
st = skill_targets[idx]
|
|
764
|
+
sr = install_skill_to(st, skill_content)
|
|
765
|
+
skill_results.append(sr)
|
|
766
|
+
if not json_mode and is_tty:
|
|
767
|
+
if sr["ok"]:
|
|
768
|
+
action = sr.get("action", "")
|
|
769
|
+
suffix = f" {_D}({action}){_R}" if action == "unchanged" else ""
|
|
770
|
+
_ui_ok(out, f"{st.name} → {_tildify(sr['path'])}{suffix}")
|
|
771
|
+
else:
|
|
772
|
+
_ui_fail(out, f"{st.name}: {sr.get('error', '?')}")
|
|
773
|
+
|
|
774
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
775
|
+
# Step 3: Shell Completions
|
|
776
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
519
777
|
if not no_completions:
|
|
520
778
|
shell = detect_shell()
|
|
521
|
-
# Find source dir (completions/ alongside bingo-light script)
|
|
522
779
|
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
523
780
|
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
524
|
-
source_dir =
|
|
781
|
+
source_dir = (
|
|
782
|
+
script_dir if os.path.isdir(os.path.join(script_dir, "completions"))
|
|
783
|
+
else os.path.dirname(pkg_dir)
|
|
784
|
+
)
|
|
525
785
|
|
|
526
786
|
if os.path.isdir(os.path.join(source_dir, "completions")):
|
|
787
|
+
if not json_mode and is_tty:
|
|
788
|
+
_ui_section(out, "Shell Completions")
|
|
527
789
|
comp_result = install_completions(shell, source_dir)
|
|
528
790
|
if not json_mode and is_tty:
|
|
529
791
|
if comp_result["ok"]:
|
|
530
|
-
out
|
|
792
|
+
_ui_ok(out, f"{shell} → {_tildify(comp_result['path'])}")
|
|
531
793
|
else:
|
|
532
|
-
out
|
|
794
|
+
_ui_skip(out, f"{shell}: {comp_result.get('error', 'skipped')}")
|
|
533
795
|
|
|
534
|
-
#
|
|
796
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
797
|
+
# Summary
|
|
798
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
535
799
|
configured = [r for r in results if r["ok"]]
|
|
536
800
|
failed = [r for r in results if not r["ok"]]
|
|
801
|
+
skills_ok = [r for r in skill_results if r["ok"]]
|
|
802
|
+
skills_fail = [r for r in skill_results if not r["ok"]]
|
|
803
|
+
all_fail = len(failed) + len(skills_fail)
|
|
537
804
|
|
|
538
805
|
if not json_mode and is_tty:
|
|
539
|
-
|
|
540
|
-
if
|
|
541
|
-
|
|
542
|
-
|
|
806
|
+
parts = []
|
|
807
|
+
if configured:
|
|
808
|
+
parts.append(f"{len(configured)} MCP")
|
|
809
|
+
if skills_ok:
|
|
810
|
+
parts.append(f"{len(skills_ok)} skill(s)")
|
|
811
|
+
summary = " + ".join(parts) if parts else "0"
|
|
812
|
+
|
|
813
|
+
if all_fail:
|
|
814
|
+
_ui_done(out, f"{summary} configured, {all_fail} failed")
|
|
815
|
+
else:
|
|
816
|
+
_ui_done(out, f"{summary} configured — ready to go!")
|
|
817
|
+
|
|
818
|
+
# Next steps box
|
|
819
|
+
out.write(f" {_D}┌─────────────────────────────────────────────────┐{_R}\n")
|
|
820
|
+
out.write(f" {_D}│{_R} {_B}Next steps:{_R} {_D}│{_R}\n")
|
|
821
|
+
out.write(f" {_D}│{_R} {_D}│{_R}\n")
|
|
822
|
+
out.write(f" {_D}│{_R} cd your-forked-project {_D}│{_R}\n")
|
|
823
|
+
out.write(f" {_D}│{_R} {_C}bingo-light init{_R} <upstream-url> {_D}│{_R}\n")
|
|
824
|
+
out.write(f" {_D}│{_R} {_C}bingo-light sync{_R} {_D}│{_R}\n")
|
|
825
|
+
out.write(f" {_D}│{_R} {_D}│{_R}\n")
|
|
826
|
+
out.write(f" {_D}│{_R} Re-run anytime: {_C}bingo-light setup{_R} {_D}│{_R}\n")
|
|
827
|
+
out.write(f" {_D}└─────────────────────────────────────────────────┘{_R}\n")
|
|
828
|
+
out.write("\n")
|
|
543
829
|
|
|
544
830
|
return {
|
|
545
|
-
"ok":
|
|
831
|
+
"ok": all_fail == 0,
|
|
546
832
|
"configured": [r["tool"] for r in configured],
|
|
547
833
|
"failed": [r["tool"] for r in failed],
|
|
834
|
+
"skills": [r["id"] for r in skills_ok],
|
|
548
835
|
"results": results,
|
|
836
|
+
"skill_results": skill_results,
|
|
549
837
|
}
|
package/mcp-server.py
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bingo-light",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "AI-native fork maintenance — manage customizations as a clean patch stack on top of upstream",
|
|
5
5
|
"keywords": ["git", "fork", "patch", "mcp", "ai", "maintenance", "upstream", "rebase"],
|
|
6
6
|
"homepage": "https://github.com/DanOps-1/bingo-light",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"mcp-server.py",
|
|
24
24
|
"bingo_core/*.py",
|
|
25
25
|
"completions/",
|
|
26
|
+
".claude/commands/bingo.md",
|
|
26
27
|
"LICENSE",
|
|
27
28
|
"README.md"
|
|
28
29
|
],
|