bingo-light 2.1.2 → 2.1.3

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/README.en.md CHANGED
@@ -10,7 +10,7 @@
10
10
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
11
11
  <a href="https://github.com/DanOps-1/bingo-light/releases"><img src="https://img.shields.io/github/v/release/DanOps-1/bingo-light?label=Release&color=orange" alt="Release"></a>
12
12
  <br>
13
- <a href="#for-ai-agents"><img src="https://img.shields.io/badge/MCP_Server-35_tools-blueviolet.svg" alt="MCP: 35 tools"></a>
13
+ <a href="#for-ai-agents"><img src="https://img.shields.io/badge/MCP_Server-49_tools-blueviolet.svg" alt="MCP: 49 tools"></a>
14
14
  <a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.8+-3776ab.svg" alt="Python 3.8+"></a>
15
15
  <img src="https://img.shields.io/badge/Dependencies-Zero-brightgreen.svg" alt="Zero deps">
16
16
  <a href="https://github.com/DanOps-1/bingo-light/stargazers"><img src="https://img.shields.io/github/stars/DanOps-1/bingo-light?style=social" alt="Stars"></a>
@@ -25,7 +25,7 @@ GitHub's "Sync fork" button breaks the moment you have customizations. `git reba
25
25
 
26
26
  Your patches live as a clean, named stack on top of upstream. Syncing is `bingo-light sync`. Conflicts get remembered so you never solve the same one twice. And if something goes sideways, `bingo-light undo` puts everything back in one second.
27
27
 
28
- Every command speaks JSON. The built-in MCP server gives AI agents 35 tools to manage your fork autonomously -- from init through conflict resolution. No human in the loop required.
28
+ Every command speaks JSON. The built-in MCP server gives AI agents 49 tools to manage your fork autonomously -- from init through conflict resolution. No human in the loop required.
29
29
 
30
30
  ---
31
31
 
@@ -69,6 +69,12 @@ That's it. Three commands and your fork stays in sync forever.
69
69
 
70
70
  > The AI calls `conflict-analyze --json`, reads the structured ours/theirs data, writes the merged file, and the rebase continues. No human needed.
71
71
 
72
+ During a rebase, `conflict-analyze` also returns:
73
+ - **`patch_intent`** — patch name, subject, full commit message, original SHA, original diff, metadata (reason/tags/upstream_pr/status/owner), and position in the patch stack.
74
+ - **`verify`** — configured `test.command` plus per-file syntax/parse commands by extension (`.py/.json/.yml/.yaml/.toml/.sh`).
75
+
76
+ `conflict-resolve --verify` (CLI) or `verify: true` (MCP) runs `test.command` after the final `git rebase --continue`; the result is attached as `verify_result`.
77
+
72
78
  ---
73
79
 
74
80
  ## Key Features
@@ -93,7 +99,7 @@ That's it. Three commands and your fork stays in sync forever.
93
99
 
94
100
  ### For AI Agents
95
101
 
96
- - :electric_plug: **MCP server (35 tools)** -- full fork management from init through conflict resolution.
102
+ - :electric_plug: **MCP server (49 tools)** -- full fork management from init through conflict resolution.
97
103
  - :bar_chart: **`--json` on everything** -- every command returns structured JSON. Parse, don't scrape.
98
104
  - :mute: **`--yes` flag** -- fully non-interactive. No TTY required. No prompts. Ever.
99
105
  - :gear: **Auto-detect non-TTY** -- pipes and subprocesses trigger non-interactive mode automatically.
@@ -200,7 +206,7 @@ make install && bingo-light setup
200
206
 
201
207
  ## For AI Agents
202
208
 
203
- bingo-light was designed from day one for AI agents. Every command speaks JSON. The MCP server exposes 35 tools covering the full lifecycle from `init` to `conflict-resolve`. Non-interactive mode is the default when stdin is not a TTY.
209
+ bingo-light was designed from day one for AI agents. Every command speaks JSON. The MCP server exposes 49 tools covering the full lifecycle from `init` to `conflict-resolve`. Non-interactive mode is the default when stdin is not a TTY.
204
210
 
205
211
  ### MCP setup -- Claude Code
206
212
 
@@ -234,7 +240,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
234
240
 
235
241
  **Any MCP client** (VS Code Copilot, Cursor, custom agents): connect via stdio to `python3 mcp-server.py`.
236
242
 
237
- ### 35 MCP Tools
243
+ ### 49 MCP Tools
238
244
 
239
245
  | Tool | Purpose |
240
246
  |------|---------|
@@ -424,7 +430,7 @@ StGit (649 stars) manages patch stacks but has no AI integration, no MCP server,
424
430
  | Handles customizations | **Yes** | **No** | Manual | Manual | Manual |
425
431
  | Conflict memory (rerere) | **Auto** | No | Manual | No | No |
426
432
  | Conflict prediction | **Yes** | No | No | No | No |
427
- | AI / MCP integration | **35 tools** | No | No | No | No |
433
+ | AI / MCP integration | **49 tools** | No | No | No | No |
428
434
  | JSON output | **All commands** | No | No | No | No |
429
435
  | Non-interactive mode | **Native** | No | Partial | Partial | Partial |
430
436
  | Undo sync | **One command** | No | git reflog | Manual | Manual |
@@ -490,7 +496,7 @@ Yes. bingo-light uses standard git operations (fetch, rebase, push). It works wi
490
496
  ```
491
497
  bingo-light CLI tool (Python 3, zero deps)
492
498
  bingo_core/ Core library package (all business logic)
493
- mcp-server.py MCP server (zero-dep Python 3, 35 tools, JSON-RPC 2.0)
499
+ mcp-server.py MCP server (zero-dep Python 3, 49 tools, JSON-RPC 2.0)
494
500
  contrib/agent.py Advisor agent (monitors drift, auto-syncs when safe)
495
501
  contrib/tui.py Terminal dashboard (curses TUI, real-time monitoring)
496
502
  install.sh Installer (--yes for CI, --help for options)
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  <a href="https://github.com/DanOps-1/bingo-light/actions"><img src="https://github.com/DanOps-1/bingo-light/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
10
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
11
11
  <a href="https://github.com/DanOps-1/bingo-light/releases"><img src="https://img.shields.io/github/v/release/DanOps-1/bingo-light?label=Release&color=orange" alt="Release"></a>
12
- <a href="#mcp-服务器"><img src="https://img.shields.io/badge/MCP_Server-35_tools-blueviolet.svg" alt="MCP: 35 tools"></a>
12
+ <a href="#mcp-服务器"><img src="https://img.shields.io/badge/MCP_Server-49_tools-blueviolet.svg" alt="MCP: 49 tools"></a>
13
13
  <a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.8+-3776ab.svg" alt="Python 3.8+"></a>
14
14
  <img src="https://img.shields.io/badge/Dependencies-Zero-brightgreen.svg" alt="Zero deps">
15
15
  <a href="https://github.com/DanOps-1/bingo-light/stargazers"><img src="https://img.shields.io/github/stars/DanOps-1/bingo-light?style=social" alt="Stars"></a>
@@ -168,10 +168,27 @@ cd bingo-light && make install && bingo-light setup
168
168
  "theirs": "... 你的补丁版本 ...",
169
169
  "hint": "上游重构了调度器核心;补丁需要适配新结构。"
170
170
  }
171
- ]
171
+ ],
172
+ "patch_intent": {
173
+ "name": "custom-scheduler",
174
+ "subject": "...",
175
+ "message": "完整 commit 消息",
176
+ "original_sha": "a1b2c3d...",
177
+ "original_diff": "diff --git ...",
178
+ "meta": {"reason": "...", "tags": [], "status": "permanent"},
179
+ "stack_position": {"index": 3, "total": 7}
180
+ },
181
+ "verify": {
182
+ "test_command": "make test",
183
+ "file_hints": [
184
+ {"file": "kernel/sched/core.c", "command": "bash -n ...", "kind": "syntax"}
185
+ ]
186
+ }
172
187
  }
173
188
  ```
174
189
 
190
+ `conflict-analyze` 在 rebase 中额外附带 `patch_intent`(补丁意图:原始 commit、diff、metadata、栈位置)与 `verify`(配置的 `test.command` + 按扩展名的逐文件校验命令)。`conflict-resolve --verify` 在最终 `git rebase --continue` 完成后自动跑 `test.command`,结果挂在 `verify_result` 字段。
191
+
175
192
  </details>
176
193
 
177
194
  <details>
@@ -197,7 +214,7 @@ Claude Code:
197
214
  ```console
198
215
  $ bingo-light setup
199
216
 
200
- ◆ bingo-light setup v2.1.1
217
+ ◆ bingo-light setup v2.x.x
201
218
 
202
219
  ◆ MCP Server
203
220
  │ Connect bingo-light tools to your AI coding assistants
@@ -434,7 +451,7 @@ bingo-light help 打印帮助
434
451
 
435
452
  | 集成方式 | 适用场景 | 示例 |
436
453
  |---------|---------|------|
437
- | **MCP** (35 tools) | Claude Code / Cursor / Windsurf 等 | `bingo-light setup` 自动配 |
454
+ | **MCP** (49 tools) | Claude Code / Cursor / Windsurf 等 | `bingo-light setup` 自动配 |
438
455
  | **CLI `--json`** | 任何能跑 shell 的 AI | `bingo-light sync --json --yes` |
439
456
  | **Skill** | Claude Code / Continue / Gemini 等 | `/bingo` 教 AI 用法 |
440
457
 
package/bingo-light CHANGED
@@ -234,6 +234,7 @@ def _format_conflict_analyze(result: dict) -> str:
234
234
  conflicts = result.get("conflicts", [])
235
235
  if not conflicts:
236
236
  return f"{_c(GREEN, 'OK')} No conflicts detected."
237
+
237
238
  lines = [f"{_c(YELLOW, '!')} {len(conflicts)} conflicting file(s):"]
238
239
  for c in conflicts:
239
240
  f = c.get("file", "?")
@@ -241,6 +242,33 @@ def _format_conflict_analyze(result: dict) -> str:
241
242
  lines.append(f" {_c(RED, f)}")
242
243
  if hint:
243
244
  lines.append(f" hint: {hint}")
245
+
246
+ intent = result.get("patch_intent")
247
+ if intent and intent.get("name"):
248
+ lines.append("")
249
+ lines.append(
250
+ f"{_c(BOLD, 'Patch intent:')} "
251
+ f"{intent['name']} \u2014 {intent.get('subject', '')}"
252
+ )
253
+ meta = intent.get("meta") or {}
254
+ if meta.get("reason"):
255
+ lines.append(f" reason: {meta['reason']}")
256
+ if meta.get("tags"):
257
+ lines.append(f" tags: {', '.join(meta['tags'])}")
258
+ sp = intent.get("stack_position")
259
+ if sp:
260
+ lines.append(f" position: {sp['index']}/{sp['total']}")
261
+
262
+ verify = result.get("verify")
263
+ if verify:
264
+ lines.append("")
265
+ lines.append(f"{_c(BOLD, 'Verify suggestions:')}")
266
+ tc = verify.get("test_command")
267
+ if tc:
268
+ lines.append(f" test.command: {tc}")
269
+ for h in verify.get("file_hints", []):
270
+ lines.append(f" [{h['kind']}] {h['command']}")
271
+
244
272
  return "\n".join(lines)
245
273
 
246
274
 
@@ -261,10 +289,18 @@ def _format_conflict_resolve(result: dict) -> str:
261
289
  )
262
290
 
263
291
  if result.get("sync_complete"):
264
- return (
265
- f"{_c(GREEN, 'OK')} Resolved {resolved} "
266
- f"\u2014 sync complete!"
267
- )
292
+ out = f"{_c(GREEN, 'OK')} Resolved {resolved} \u2014 sync complete!"
293
+ vr = result.get("verify_result")
294
+ if vr:
295
+ if vr.get("skipped"):
296
+ out += f"\n verify: skipped ({vr.get('reason', '')})"
297
+ elif vr.get("test") == "pass":
298
+ out += f"\n {_c(GREEN, 'verify: PASS')} ({vr.get('command', '')})"
299
+ elif vr.get("test") == "fail":
300
+ out += f"\n {_c(RED, 'verify: FAIL')} ({vr.get('command', '')})"
301
+ else:
302
+ out += f"\n verify: {vr.get('test', '?')}"
303
+ return out
268
304
 
269
305
  if result.get("conflict"):
270
306
  patch = result.get("current_patch", "")
@@ -284,10 +320,18 @@ def _format_conflict_resolve(result: dict) -> str:
284
320
  return "\n".join(lines)
285
321
 
286
322
  if result.get("rebase_continued"):
287
- return (
288
- f"{_c(GREEN, 'OK')} Resolved {resolved} "
289
- f"\u2014 rebase continued."
290
- )
323
+ out = f"{_c(GREEN, 'OK')} Resolved {resolved} \u2014 rebase continued."
324
+ vr = result.get("verify_result")
325
+ if vr:
326
+ if vr.get("skipped"):
327
+ out += f"\n verify: skipped ({vr.get('reason', '')})"
328
+ elif vr.get("test") == "pass":
329
+ out += f"\n {_c(GREEN, 'verify: PASS')} ({vr.get('command', '')})"
330
+ elif vr.get("test") == "fail":
331
+ out += f"\n {_c(RED, 'verify: FAIL')} ({vr.get('command', '')})"
332
+ else:
333
+ out += f"\n verify: {vr.get('test', '?')}"
334
+ return out
291
335
 
292
336
  return f"{_c(GREEN, 'OK')} Resolved {resolved}."
293
337
 
@@ -500,6 +544,118 @@ def _format_patch_meta(result: dict) -> str:
500
544
  return f" {k}: {v}" if k else f"{_c(GREEN, 'OK')} Done."
501
545
 
502
546
 
547
+ def _format_patch_lock(result: dict) -> str:
548
+ """Format patch lock/unlock result."""
549
+ if result.get("ok") is False:
550
+ return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
551
+ patch = result.get("patch", "")
552
+ owner = result.get("owner", "")
553
+ if result.get("was_locked") is False:
554
+ return f"{_c(GREEN, 'OK')} {patch} was not locked."
555
+ if "locked_at" in result:
556
+ reason = result.get("reason", "")
557
+ msg = f"{_c(GREEN, 'OK')} Locked {_c(BOLD, patch)} by {owner}"
558
+ if reason:
559
+ msg += f" ({reason})"
560
+ return msg
561
+ prev = result.get("previous_owner", "")
562
+ if prev and prev != owner:
563
+ return f"{_c(GREEN, 'OK')} Unlocked {_c(BOLD, patch)} (was locked by {prev})"
564
+ return f"{_c(GREEN, 'OK')} Unlocked {_c(BOLD, patch)}"
565
+
566
+
567
+ def _format_patch_check(result: dict) -> str:
568
+ """Format patch check (obsolescence detection) result."""
569
+ if result.get("ok") is False:
570
+ return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
571
+ patches = result.get("patches", [])
572
+ if not patches:
573
+ return "No patches to check."
574
+ lines = []
575
+ for p in patches:
576
+ status = p.get("status", "unknown")
577
+ name = p.get("name", "?")
578
+ reason = p.get("reason", "")
579
+ if status == "obsolete":
580
+ lines.append(f" {_c(YELLOW, 'OBSOLETE')} {_c(BOLD, name)} — {reason}")
581
+ elif status == "active":
582
+ lines.append(f" {_c(GREEN, 'ACTIVE')} {_c(BOLD, name)} — {reason}")
583
+ else:
584
+ lines.append(f" {_c(DIM, 'UNKNOWN')} {name} — {reason}")
585
+ header = f"Checked {len(patches)} patch(es):"
586
+ return header + "\n" + "\n".join(lines)
587
+
588
+
589
+ def _format_patch_upstream(result: dict) -> str:
590
+ """Format patch upstream (PR-ready export) result."""
591
+ if result.get("ok") is False:
592
+ return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
593
+ name = result.get("patch", "")
594
+ desc = result.get("description", "")
595
+ files = result.get("files", [])
596
+ stats = result.get("stats", "")
597
+ diff = result.get("diff", "")
598
+ lines = [
599
+ f"{_c(BOLD, 'PR-ready export:')} {name}",
600
+ f" Description: {desc}",
601
+ f" Files: {', '.join(files)}",
602
+ ]
603
+ if stats:
604
+ lines.append(f" Stats: {stats}")
605
+ lines.append("")
606
+ lines.append(diff)
607
+ return "\n".join(lines)
608
+
609
+
610
+ def _format_patch_expire(result: dict) -> str:
611
+ """Format patch expire result."""
612
+ if result.get("ok") is False:
613
+ return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
614
+ expired = result.get("expired", [])
615
+ expiring = result.get("expiring_soon", [])
616
+ if not expired and not expiring:
617
+ return f"{_c(GREEN, 'OK')} No expired or expiring patches."
618
+ lines = []
619
+ if expired:
620
+ lines.append(f"{_c(RED, 'Expired')} ({len(expired)}):")
621
+ for e in expired:
622
+ lines.append(f" {_c(RED, 'x')} {_c(BOLD, e['name'])} — expired {e['expires']} ({abs(e['days_left'])}d ago)")
623
+ if expiring:
624
+ lines.append(f"{_c(YELLOW, 'Expiring soon')} ({len(expiring)}):")
625
+ for e in expiring:
626
+ lines.append(f" {_c(YELLOW, '!')} {_c(BOLD, e['name'])} — expires {e['expires']} ({e['days_left']}d left)")
627
+ return "\n".join(lines)
628
+
629
+
630
+ def _format_patch_stats(result: dict) -> str:
631
+ """Format patch stats result."""
632
+ if result.get("ok") is False:
633
+ return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
634
+ patches = result.get("patches", [])
635
+ if not patches:
636
+ return "No patches."
637
+ lines = [f"{'#':>3} {'Name':<20} {'Age':>6} {'Size':>10} {'Syncs':>6} {'Status':<12} {'Owner':<15}"]
638
+ lines.append("─" * 80)
639
+ for i, p in enumerate(patches, 1):
640
+ age = f"{p['age_days']}d" if p.get("age_days", -1) >= 0 else "?"
641
+ size = f"+{p.get('insertions', 0)}/-{p.get('deletions', 0)}"
642
+ syncs = str(p.get("syncs_survived", 0))
643
+ status = p.get("status", "")
644
+ owner = p.get("owner", "") or p.get("locked_by", "") or ""
645
+ lines.append(f"{i:>3} {p['name']:<20} {age:>6} {size:>10} {syncs:>6} {status:<12} {owner:<15}")
646
+ return "\n".join(lines)
647
+
648
+
649
+ def _format_report(result: dict) -> str:
650
+ """Format fork health report."""
651
+ if result.get("ok") is False:
652
+ return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
653
+ report = result.get("report", "")
654
+ if report:
655
+ return report
656
+ return f"{_c(GREEN, 'OK')} Report generated."
657
+
658
+
503
659
  def _format_undo(result: dict) -> str:
504
660
  """Format undo result."""
505
661
  if result.get("ok") is False:
@@ -740,7 +896,11 @@ def build_parser() -> argparse.ArgumentParser:
740
896
  sub.add_parser("diff", aliases=["d"], add_help=False)
741
897
 
742
898
  # doctor
743
- sub.add_parser("doctor", add_help=False)
899
+ p_doctor = sub.add_parser("doctor", add_help=False)
900
+ p_doctor.add_argument("--report", action="store_true")
901
+
902
+ # report
903
+ sub.add_parser("report", add_help=False)
744
904
 
745
905
  # log
746
906
  sub.add_parser("log", add_help=False)
@@ -755,6 +915,8 @@ def build_parser() -> argparse.ArgumentParser:
755
915
  cr = sub.add_parser("conflict-resolve", add_help=False)
756
916
  cr.add_argument("resolve_file", nargs="?", default="")
757
917
  cr.add_argument("--content-stdin", action="store_true")
918
+ cr.add_argument("--verify", action="store_true",
919
+ help="Run test.command after the final rebase continues")
758
920
 
759
921
  # session
760
922
  p_session = sub.add_parser("session", add_help=False)
@@ -811,6 +973,23 @@ def build_parser() -> argparse.ArgumentParser:
811
973
  pp_meta.add_argument("meta_key", nargs="?", default="")
812
974
  pp_meta.add_argument("meta_value", nargs="?", default="")
813
975
 
976
+ pp_lock = patch_sub.add_parser("lock", add_help=False)
977
+ pp_lock.add_argument("target")
978
+ pp_lock.add_argument("--reason", default="")
979
+
980
+ pp_unlock = patch_sub.add_parser("unlock", add_help=False)
981
+ pp_unlock.add_argument("target")
982
+ pp_unlock.add_argument("--force", action="store_true")
983
+
984
+ pp_check = patch_sub.add_parser("check", add_help=False)
985
+ pp_check.add_argument("target", nargs="?", default="")
986
+
987
+ pp_upstream = patch_sub.add_parser("upstream", add_help=False)
988
+ pp_upstream.add_argument("target")
989
+
990
+ patch_sub.add_parser("expire", add_help=False)
991
+ patch_sub.add_parser("stats", add_help=False)
992
+
814
993
  # workspace
815
994
  p_ws = sub.add_parser("workspace", aliases=["ws"], add_help=False)
816
995
  ws_sub = p_ws.add_subparsers(dest="ws_command")
@@ -853,6 +1032,26 @@ def build_parser() -> argparse.ArgumentParser:
853
1032
  dp_drop.add_argument("dep_package")
854
1033
  dp_drop.add_argument("dep_patch_name", nargs="?", default="")
855
1034
 
1035
+ # dep override subcommands
1036
+ dp_override = dep_sub.add_parser("override", add_help=False)
1037
+ override_sub = dp_override.add_subparsers(dest="override_command")
1038
+ override_sub.add_parser("list", add_help=False)
1039
+ override_sub.add_parser("check", add_help=False)
1040
+ ov_add = override_sub.add_parser("add", add_help=False)
1041
+ ov_add.add_argument("override_package")
1042
+ ov_add.add_argument("override_version")
1043
+ ov_add.add_argument("--reason", dest="override_reason", default="")
1044
+ ov_drop = override_sub.add_parser("drop", add_help=False)
1045
+ ov_drop.add_argument("override_package")
1046
+
1047
+ # dep fork subcommands
1048
+ dp_fork = dep_sub.add_parser("fork", add_help=False)
1049
+ fork_sub = dp_fork.add_subparsers(dest="fork_command")
1050
+ fork_sub.add_parser("list", add_help=False)
1051
+ fork_sub.add_parser("check", add_help=False)
1052
+ fk_sync = fork_sub.add_parser("sync", add_help=False)
1053
+ fk_sync.add_argument("fork_package")
1054
+
856
1055
  # help
857
1056
  sub.add_parser("help", add_help=False)
858
1057
 
@@ -884,6 +1083,31 @@ def dispatch(args: argparse.Namespace, json_mode: bool) -> Optional[dict]:
884
1083
  return dm.sync()
885
1084
  if dcmd == "drop":
886
1085
  return dm.drop(args.dep_package, getattr(args, "dep_patch_name", ""))
1086
+ if dcmd == "override":
1087
+ ocmd = getattr(args, "override_command", "")
1088
+ if ocmd == "list":
1089
+ return dm.override_list()
1090
+ if ocmd == "check":
1091
+ return dm.override_check()
1092
+ if ocmd == "add":
1093
+ return dm.override_add(
1094
+ args.override_package, args.override_version,
1095
+ getattr(args, "override_reason", ""),
1096
+ )
1097
+ if ocmd == "drop":
1098
+ return dm.override_drop(args.override_package)
1099
+ return {"ok": False, "error": f"Unknown override subcommand: {ocmd}"}
1100
+ if dcmd == "fork":
1101
+ from bingo_core.dep_fork import ForkTracker
1102
+ ft = ForkTracker()
1103
+ fcmd = getattr(args, "fork_command", "")
1104
+ if fcmd == "list":
1105
+ return ft.fork_list()
1106
+ if fcmd == "check":
1107
+ return ft.fork_check()
1108
+ if fcmd == "sync":
1109
+ return ft.fork_sync(args.fork_package)
1110
+ return {"ok": False, "error": f"Unknown fork subcommand: {fcmd}"}
887
1111
  return {"ok": False, "error": f"Unknown dep subcommand: {dcmd}"}
888
1112
 
889
1113
  # setup doesn't need a Repo (works outside git repos)
@@ -919,7 +1143,10 @@ def dispatch(args: argparse.Namespace, json_mode: bool) -> Optional[dict]:
919
1143
  return repo.diff()
920
1144
 
921
1145
  if cmd == "doctor":
922
- return repo.doctor()
1146
+ return repo.doctor(report=getattr(args, "report", False))
1147
+
1148
+ if cmd == "report":
1149
+ return repo.report()
923
1150
 
924
1151
  if cmd == "log":
925
1152
  return repo.history()
@@ -935,7 +1162,9 @@ def dispatch(args: argparse.Namespace, json_mode: bool) -> Optional[dict]:
935
1162
  if args.content_stdin:
936
1163
  import sys as _sys
937
1164
  content = _sys.stdin.read()
938
- return repo.conflict_resolve(args.resolve_file, content)
1165
+ return repo.conflict_resolve(
1166
+ args.resolve_file, content, verify=args.verify
1167
+ )
939
1168
 
940
1169
  if cmd == "session":
941
1170
  update = (args.session_action == "update")
@@ -1018,6 +1247,24 @@ def _dispatch_patch(args: argparse.Namespace, repo: Repo) -> dict:
1018
1247
  value=meta_value,
1019
1248
  )
1020
1249
 
1250
+ if pcmd == "lock":
1251
+ return repo.patch_lock(args.target, reason=getattr(args, "reason", ""))
1252
+
1253
+ if pcmd == "unlock":
1254
+ return repo.patch_unlock(args.target, force=getattr(args, "force", False))
1255
+
1256
+ if pcmd == "check":
1257
+ return repo.patch_check(getattr(args, "target", ""))
1258
+
1259
+ if pcmd == "upstream":
1260
+ return repo.patch_upstream(args.target)
1261
+
1262
+ if pcmd == "expire":
1263
+ return repo.patch_expire()
1264
+
1265
+ if pcmd == "stats":
1266
+ return repo.patch_stats()
1267
+
1021
1268
  raise BingoError(f"Unknown patch subcommand: {pcmd}")
1022
1269
 
1023
1270
 
@@ -1075,6 +1322,7 @@ _FORMATTERS: Dict[str, Any] = {
1075
1322
  "smart-sync": _format_smart_sync,
1076
1323
  "setup": _format_setup,
1077
1324
  "dep": _format_dep,
1325
+ "report": _format_report,
1078
1326
  }
1079
1327
 
1080
1328
 
@@ -1099,6 +1347,16 @@ def _get_formatter(args: argparse.Namespace):
1099
1347
  return _format_patch_import
1100
1348
  if pcmd == "meta":
1101
1349
  return _format_patch_meta
1350
+ if pcmd in ("lock", "unlock"):
1351
+ return _format_patch_lock
1352
+ if pcmd == "check":
1353
+ return _format_patch_check
1354
+ if pcmd == "upstream":
1355
+ return _format_patch_upstream
1356
+ if pcmd == "expire":
1357
+ return _format_patch_expire
1358
+ if pcmd == "stats":
1359
+ return _format_patch_stats
1102
1360
  return _format_generic
1103
1361
 
1104
1362
  # workspace subcommands
@@ -14,7 +14,7 @@ import re
14
14
 
15
15
  # --- Constants ---
16
16
 
17
- VERSION = "2.1.2"
17
+ VERSION = "2.1.3"
18
18
  PATCH_PREFIX = "[bl]"
19
19
  CONFIG_FILE = ".bingolight"
20
20
  BINGO_DIR = ".bingo"
@@ -42,6 +42,7 @@ from bingo_core.models import PatchInfo, ConflictInfo # noqa: E402
42
42
  from bingo_core.git import Git # noqa: E402
43
43
  from bingo_core.config import Config # noqa: E402
44
44
  from bingo_core.state import State # noqa: E402
45
+ from bingo_core.team import TeamState # noqa: E402
45
46
  from bingo_core.repo import Repo # noqa: E402
46
47
 
47
48
  __all__ = [
@@ -73,5 +74,6 @@ __all__ = [
73
74
  "Git",
74
75
  "Config",
75
76
  "State",
77
+ "TeamState",
76
78
  "Repo",
77
79
  ]