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.
@@ -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
@@ -14,7 +14,7 @@ import re
14
14
 
15
15
  # --- Constants ---
16
16
 
17
- VERSION = "2.0.0"
17
+ VERSION = "2.1.0"
18
18
  PATCH_PREFIX = "[bl]"
19
19
  CONFIG_FILE = ".bingolight"
20
20
  BINGO_DIR = ".bingo"
@@ -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 arrow keys and spacebar.
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
- 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", "")
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
- if i == cursor:
327
- label = f"\033[1m{label}\033[0m"
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
- 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"
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
- out.write(f"{prefix} {check} {label}{hint_str}\n")
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
- # Hide cursor
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
- 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")
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
- # ── Header ──
672
+ # ═════════════════════════════════════════════════════════════════════════
673
+ # Header
674
+ # ═════════════════════════════════════════════════════════════════════════
463
675
  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")
676
+ _ui_header(out, VERSION)
677
+ _ui_info(out, f"MCP server: {command} {' '.join(args)}")
467
678
 
468
- # ── Detect tools ──
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
- # ── Select tools ──
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" \033[32m[x]\033[0m {tools[i].name}\n")
489
- out.write("\n")
701
+ out.write(f" {BAR} {_G}■{_R} {tools[i].name}\n")
490
702
  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")
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.write(" No tools selected.\n\n")
503
- return {"ok": True, "configured": [], "skipped": True}
709
+ _ui_skip(out, "No tools selected")
504
710
 
505
- # ── Configure each selected tool ──
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
- path = _tildify(result["config_path"])
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.write(f" \033[31mx\033[0m {tool.name}: {result.get('error', 'unknown error')}\n")
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
- # ── Shell completions ──
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 = script_dir if os.path.isdir(os.path.join(script_dir, "completions")) else os.path.dirname(pkg_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.write(f" \033[32m✓\033[0m {shell} completions installed\n")
792
+ _ui_ok(out, f"{shell} {_tildify(comp_result['path'])}")
531
793
  else:
532
- out.write(f" \033[2m⊘ {shell} completions: {comp_result.get('error', 'skipped')}\033[0m\n")
794
+ _ui_skip(out, f"{shell}: {comp_result.get('error', 'skipped')}")
533
795
 
534
- # ── Summary ──
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
- 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")
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": len(failed) == 0,
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
@@ -754,7 +754,7 @@ def main():
754
754
  "capabilities": {"tools": {}},
755
755
  "serverInfo": {
756
756
  "name": "bingo-light",
757
- "version": "2.0.0",
757
+ "version": "2.1.0",
758
758
  },
759
759
  }))
760
760
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingo-light",
3
- "version": "2.0.1",
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
  ],