bingo-light 2.1.1 → 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.
@@ -151,39 +151,85 @@ def _get_tools() -> List[AITool]:
151
151
  # ─── MCP Server Path Detection ──────────────────────────────────────────────
152
152
 
153
153
 
154
+ def _find_python_for_mcp() -> str:
155
+ """Find the correct python3 for running the MCP server.
156
+
157
+ When installed via pipx, system python3 can't import bingo_core.
158
+ We need to use the same python that runs bingo-light itself.
159
+ """
160
+ # Use the same python interpreter that's running this code
161
+ return sys.executable
162
+
163
+
154
164
  def find_mcp_server() -> Tuple[str, List[str]]:
155
165
  """Find the MCP server command and args.
156
166
 
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
167
+ Returns (command, args) tuple.
161
168
  """
162
- # 1. Installed binary on PATH
169
+ python = _find_python_for_mcp()
170
+
171
+ # 1. Installed binary on PATH (npm installs bingo-light-mcp)
163
172
  for name in ("bingo-light-mcp", "mcp-server.py"):
164
173
  mcp_bin = shutil.which(name)
165
174
  if mcp_bin:
166
- return ("python3", [mcp_bin])
175
+ return (python, [mcp_bin])
167
176
 
168
177
  # 2. Relative to running script
169
178
  script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
170
179
  candidate = os.path.join(script_dir, "mcp-server.py")
171
180
  if os.path.isfile(candidate):
172
- return ("python3", [candidate])
181
+ return (python, [candidate])
173
182
 
174
183
  # 3. Relative to bingo_core package
175
184
  pkg_dir = os.path.dirname(os.path.abspath(__file__))
176
185
  candidate = os.path.join(os.path.dirname(pkg_dir), "mcp-server.py")
177
186
  if os.path.isfile(candidate):
178
- return ("python3", [candidate])
187
+ return (python, [candidate])
179
188
 
180
189
  # Fallback
181
- return ("python3", ["bingo-light-mcp"])
190
+ return (python, ["bingo-light-mcp"])
182
191
 
183
192
 
184
193
  # ─── Config Writer ───────────────────────────────────────────────────────────
185
194
 
186
195
 
196
+ def _configure_claude_code_via_cli(
197
+ command: str,
198
+ args: List[str],
199
+ server_name: str = "bingo-light",
200
+ ) -> Optional[Dict[str, Any]]:
201
+ """Try to configure Claude Code using `claude mcp add` CLI.
202
+
203
+ Returns result dict if successful, None if claude CLI not available.
204
+ """
205
+ claude_bin = shutil.which("claude")
206
+ if not claude_bin:
207
+ return None
208
+
209
+ import subprocess
210
+ # Remove existing entry first (ignore errors if not present)
211
+ subprocess.run(
212
+ [claude_bin, "mcp", "remove", server_name],
213
+ capture_output=True, text=True,
214
+ )
215
+ # Add new entry: claude mcp add <name> -- <command> <args...>
216
+ cmd = [claude_bin, "mcp", "add", server_name, "--", command] + args
217
+ result = subprocess.run(cmd, capture_output=True, text=True)
218
+ if result.returncode == 0:
219
+ return {
220
+ "ok": True,
221
+ "tool": "claude-code",
222
+ "name": "Claude Code",
223
+ "config_path": "~/.claude.json (via claude mcp add)",
224
+ "action": "created",
225
+ }
226
+ return {
227
+ "ok": False,
228
+ "tool": "claude-code",
229
+ "error": result.stderr.strip() or "claude mcp add failed",
230
+ }
231
+
232
+
187
233
  def write_mcp_config(
188
234
  tool: AITool,
189
235
  command: str,
@@ -194,6 +240,13 @@ def write_mcp_config(
194
240
 
195
241
  Returns dict with ok, tool, config_path, action (created|updated|error).
196
242
  """
243
+ # Claude Code: prefer `claude mcp add` CLI (writes to correct location)
244
+ if tool.id == "claude-code":
245
+ cli_result = _configure_claude_code_via_cli(command, args, server_name)
246
+ if cli_result:
247
+ return cli_result
248
+ # Fall through to JSON file method if claude CLI not available
249
+
197
250
  config_path = tool.expand_path()
198
251
  config_dir = os.path.dirname(config_path)
199
252
 
@@ -594,17 +647,20 @@ def _get_skill_targets() -> List[SkillTarget]:
594
647
 
595
648
  def find_skill_file() -> Optional[str]:
596
649
  """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
650
  pkg_dir = os.path.dirname(os.path.abspath(__file__))
651
+ script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
603
652
  repo_dir = os.path.dirname(pkg_dir)
604
- candidates.append(os.path.join(repo_dir, ".claude", "commands", "bingo.md"))
605
653
 
606
- # npm package layout
607
- candidates.append(os.path.join(script_dir, "..", ".claude", "commands", "bingo.md"))
654
+ candidates = [
655
+ # Bundled inside bingo_core/ (pip/npm installs)
656
+ os.path.join(pkg_dir, "_skill.md"),
657
+ # Repo layout
658
+ os.path.join(repo_dir, ".claude", "commands", "bingo.md"),
659
+ # Script-relative (install.sh)
660
+ os.path.join(script_dir, ".claude", "commands", "bingo.md"),
661
+ # npm package layout
662
+ os.path.join(script_dir, "..", ".claude", "commands", "bingo.md"),
663
+ ]
608
664
 
609
665
  for c in candidates:
610
666
  if os.path.isfile(c):
@@ -184,7 +184,7 @@ class State:
184
184
  for t in (v.strip() for v in value.split(",")):
185
185
  if t and t not in tags_list:
186
186
  tags_list.append(t)
187
- elif key in ("reason", "expires", "upstream_pr", "status"):
187
+ elif key in ("reason", "expires", "upstream_pr", "status", "owner"):
188
188
  p[key] = value
189
189
  self._save_metadata(data)
190
190
 
@@ -0,0 +1,170 @@
1
+ """
2
+ bingo_core.team — Team collaboration state (.bingo/team.json).
3
+
4
+ Manages patch locks and team membership for multi-person fork maintenance.
5
+ Advisory locking: prevents accidental concurrent edits, not a security boundary.
6
+
7
+ Storage:
8
+ .bingo/team.json
9
+ {
10
+ "locks": {
11
+ "<patch_name>": {
12
+ "owner": "<user>",
13
+ "locked_at": "ISO8601",
14
+ "reason": ""
15
+ }
16
+ }
17
+ }
18
+
19
+ Python 3.8+ stdlib only.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import tempfile
27
+ from datetime import datetime, timezone
28
+ from typing import List, Optional
29
+
30
+ from bingo_core import BINGO_DIR
31
+ from bingo_core.exceptions import BingoError
32
+
33
+
34
+ class TeamState:
35
+ """Manages .bingo/team.json for patch locking and team coordination."""
36
+
37
+ def __init__(self, repo_dir: str, git=None):
38
+ self.repo_dir = repo_dir
39
+ self._git = git # optional Git instance for get_user()
40
+ self.bingo_dir = os.path.join(repo_dir, BINGO_DIR)
41
+ self.team_file = os.path.join(self.bingo_dir, "team.json")
42
+
43
+ def _load(self) -> dict:
44
+ """Load team.json, returning empty structure if missing."""
45
+ if not os.path.isfile(self.team_file):
46
+ return {"locks": {}}
47
+ try:
48
+ with open(self.team_file) as f:
49
+ data = json.load(f)
50
+ if "locks" not in data:
51
+ data["locks"] = {}
52
+ return data
53
+ except (json.JSONDecodeError, IOError):
54
+ return {"locks": {}}
55
+
56
+ def _save(self, data: dict) -> None:
57
+ """Atomically write team.json."""
58
+ os.makedirs(self.bingo_dir, exist_ok=True)
59
+ dir_name = os.path.dirname(self.team_file)
60
+ fd, tmp_path = tempfile.mkstemp(suffix=".tmp", dir=dir_name)
61
+ try:
62
+ with os.fdopen(fd, "w") as f:
63
+ json.dump(data, f, indent=2)
64
+ os.replace(tmp_path, self.team_file)
65
+ except Exception:
66
+ try:
67
+ os.unlink(tmp_path)
68
+ except FileNotFoundError:
69
+ pass
70
+ raise
71
+
72
+ def get_user(self) -> str:
73
+ """Detect current user from git config or environment."""
74
+ if self._git:
75
+ for key in ("user.name", "user.email"):
76
+ try:
77
+ val = self._git.run("config", key, check=False)
78
+ if val and val.strip():
79
+ return val.strip()
80
+ except Exception:
81
+ pass
82
+ return os.environ.get("USER", "unknown")
83
+
84
+ def lock(self, patch_name: str, owner: str = "", reason: str = "") -> dict:
85
+ """Lock a patch for exclusive editing.
86
+
87
+ Returns {"ok": True, "patch": ..., "owner": ..., "locked_at": ...}
88
+ Raises BingoError if already locked by another user.
89
+ """
90
+ if not owner:
91
+ owner = self.get_user()
92
+ data = self._load()
93
+ existing = data["locks"].get(patch_name)
94
+ if existing and existing["owner"] != owner:
95
+ raise BingoError(
96
+ f"Patch '{patch_name}' is locked by {existing['owner']} "
97
+ f"(since {existing['locked_at']}). "
98
+ f"They must unlock it first, or use --force."
99
+ )
100
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
101
+ data["locks"][patch_name] = {
102
+ "owner": owner,
103
+ "locked_at": now,
104
+ "reason": reason,
105
+ }
106
+ self._save(data)
107
+ return {
108
+ "ok": True,
109
+ "patch": patch_name,
110
+ "owner": owner,
111
+ "locked_at": now,
112
+ "reason": reason,
113
+ }
114
+
115
+ def unlock(self, patch_name: str, owner: str = "", force: bool = False) -> dict:
116
+ """Unlock a patch.
117
+
118
+ Returns {"ok": True, "patch": ..., "owner": ...}
119
+ Raises BingoError if locked by someone else (unless force=True).
120
+ """
121
+ if not owner:
122
+ owner = self.get_user()
123
+ data = self._load()
124
+ existing = data["locks"].get(patch_name)
125
+ if not existing:
126
+ return {"ok": True, "patch": patch_name, "owner": owner, "was_locked": False}
127
+ if existing["owner"] != owner and not force:
128
+ raise BingoError(
129
+ f"Patch '{patch_name}' is locked by {existing['owner']}. "
130
+ "Use --force to override."
131
+ )
132
+ del data["locks"][patch_name]
133
+ self._save(data)
134
+ return {
135
+ "ok": True,
136
+ "patch": patch_name,
137
+ "owner": owner,
138
+ "was_locked": True,
139
+ "previous_owner": existing["owner"],
140
+ }
141
+
142
+ def get_lock(self, patch_name: str) -> Optional[dict]:
143
+ """Get lock info for a patch, or None if unlocked."""
144
+ data = self._load()
145
+ return data["locks"].get(patch_name)
146
+
147
+ def is_locked_by_other(self, patch_name: str, current_user: str = "") -> bool:
148
+ """Check if a patch is locked by someone other than current_user."""
149
+ if not current_user:
150
+ current_user = self.get_user()
151
+ lock = self.get_lock(patch_name)
152
+ if not lock:
153
+ return False
154
+ return lock["owner"] != current_user
155
+
156
+ def list_locks(self) -> List[dict]:
157
+ """List all active locks.
158
+
159
+ Returns list of {"patch": ..., "owner": ..., "locked_at": ..., "reason": ...}
160
+ """
161
+ data = self._load()
162
+ result = []
163
+ for patch_name, info in data["locks"].items():
164
+ result.append({
165
+ "patch": patch_name,
166
+ "owner": info.get("owner", ""),
167
+ "locked_at": info.get("locked_at", ""),
168
+ "reason": info.get("reason", ""),
169
+ })
170
+ return result
@@ -10,11 +10,11 @@ _bingo_light() {
10
10
  local cur prev words cword
11
11
  _init_completion || return
12
12
 
13
- local -r toplevel_commands="init setup patch sync status doctor auto-sync log undo diff version help conflict-analyze conflict-resolve config history test workspace smart-sync session"
13
+ local -r toplevel_commands="init setup patch dep sync status doctor auto-sync log undo diff version help conflict-analyze conflict-resolve config history test workspace smart-sync session report"
14
14
  local -r toplevel_aliases="p s st d ws"
15
15
  local -r all_toplevel="${toplevel_commands} ${toplevel_aliases}"
16
16
 
17
- local -r patch_subcommands="new list show edit drop export import reorder squash meta"
17
+ local -r patch_subcommands="new list show edit drop export import reorder squash meta lock unlock check upstream expire stats"
18
18
  local -r patch_aliases="ls add create rm remove"
19
19
  local -r all_patch="${patch_subcommands} ${patch_aliases}"
20
20
 
@@ -36,7 +36,10 @@ _bingo_light() {
36
36
  diff|d)
37
37
  cmd="diff"
38
38
  ;;
39
- init|setup|doctor|auto-sync|log|undo|version|help|conflict-analyze|conflict-resolve|config|history|test|workspace|ws|smart-sync|session)
39
+ dep)
40
+ cmd="dep"
41
+ ;;
42
+ init|setup|doctor|auto-sync|log|undo|version|help|conflict-analyze|conflict-resolve|config|history|test|workspace|ws|smart-sync|session|report)
40
43
  cmd="${words[i]}"
41
44
  ;;
42
45
  *)
@@ -91,6 +94,26 @@ _bingo_light() {
91
94
  return
92
95
  fi
93
96
 
97
+ # Inside "conflict-resolve"
98
+ if [[ "$cmd" == "conflict-resolve" ]]; then
99
+ COMPREPLY=( $(compgen -W "--verify --content-stdin --help -h" -- "$cur") )
100
+ return
101
+ fi
102
+
103
+ # Inside "dep" -- complete subcommands
104
+ if [[ "$cmd" == "dep" ]]; then
105
+ if [[ -z "$subcmd" ]]; then
106
+ COMPREPLY=( $(compgen -W "patch apply sync status list drop override fork" -- "$cur") )
107
+ elif [[ "$subcmd" == "override" ]]; then
108
+ COMPREPLY=( $(compgen -W "list check add drop" -- "$cur") )
109
+ elif [[ "$subcmd" == "fork" ]]; then
110
+ COMPREPLY=( $(compgen -W "list check sync" -- "$cur") )
111
+ else
112
+ COMPREPLY=( $(compgen -W "--help -h" -- "$cur") )
113
+ fi
114
+ return
115
+ fi
116
+
94
117
  # Inside "workspace" (or alias "ws") -- complete subcommands
95
118
  if [[ "$cmd" == "workspace" || "$cmd" == "ws" ]]; then
96
119
  if [[ -z "$subcmd" ]]; then
@@ -68,6 +68,7 @@ complete -c bingo-light -f
68
68
  complete -c bingo-light -n __bingo_light_needs_command -a init -d 'Initialize a new bingo-light project'
69
69
  complete -c bingo-light -n __bingo_light_needs_command -a setup -d 'Configure MCP for AI tools (interactive)'
70
70
  complete -c bingo-light -n __bingo_light_needs_command -a patch -d 'Manage patches'
71
+ complete -c bingo-light -n __bingo_light_needs_command -a dep -d 'Patch npm/pip dependencies'
71
72
  complete -c bingo-light -n __bingo_light_needs_command -a sync -d 'Synchronize changes with upstream'
72
73
  complete -c bingo-light -n __bingo_light_needs_command -a status -d 'Show current status'
73
74
  complete -c bingo-light -n __bingo_light_needs_command -a doctor -d 'Diagnose and fix common problems'
@@ -85,6 +86,7 @@ complete -c bingo-light -n __bingo_light_needs_command -a test -d 'Run config
85
86
  complete -c bingo-light -n __bingo_light_needs_command -a workspace -d 'Manage multiple forks'
86
87
  complete -c bingo-light -n __bingo_light_needs_command -a smart-sync -d 'Smart sync with circuit breaker and partial state'
87
88
  complete -c bingo-light -n __bingo_light_needs_command -a session -d 'Manage session memory'
89
+ complete -c bingo-light -n __bingo_light_needs_command -a report -d 'Generate fork health report'
88
90
 
89
91
  # Short aliases
90
92
  complete -c bingo-light -n __bingo_light_needs_command -a p -d 'Alias for patch'
@@ -122,9 +124,11 @@ complete -c bingo-light -n '__bingo_light_using_command version' -s h -l help
122
124
 
123
125
  # ---- conflict-resolve flags ----
124
126
  complete -c bingo-light -n '__bingo_light_using_command conflict-resolve' -s h -l help -d 'Show help'
127
+ complete -c bingo-light -n '__bingo_light_using_command conflict-resolve' -l verify -d 'Run test.command after the final rebase continues'
128
+ complete -c bingo-light -n '__bingo_light_using_command conflict-resolve' -l content-stdin -d 'Read resolved content from stdin'
125
129
 
126
130
  # ---- help: complete with command names ----
127
- complete -c bingo-light -n '__bingo_light_using_command help' -a 'init setup patch sync status doctor auto-sync log undo diff version conflict-analyze conflict-resolve config history test workspace smart-sync session' -d 'Command'
131
+ complete -c bingo-light -n '__bingo_light_using_command help' -a 'init setup patch dep sync status doctor auto-sync log undo diff version conflict-analyze conflict-resolve config history test workspace smart-sync session report' -d 'Command'
128
132
 
129
133
  # ---- patch subcommands (also alias "p") ----
130
134
  complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a new -d 'Create a new patch'
@@ -137,6 +141,12 @@ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a import -d 'I
137
141
  complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a reorder -d 'Reorder the patch stack'
138
142
  complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a squash -d 'Squash two patches into one'
139
143
  complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a meta -d 'Get/set patch metadata'
144
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a lock -d 'Lock a patch for exclusive editing'
145
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a unlock -d 'Unlock a patch'
146
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a check -d 'Check if patches are still needed'
147
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a upstream -d 'Export patch as PR-ready diff'
148
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a expire -d 'List expired patches'
149
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a stats -d 'Show patch health metrics'
140
150
 
141
151
  # Patch short aliases
142
152
  complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a ls -d 'Alias for list'
@@ -162,6 +172,39 @@ complete -c bingo-light -n '__bingo_light_patch_using_subcommand import'
162
172
  complete -c bingo-light -n '__bingo_light_patch_using_subcommand reorder' -s h -l help -d 'Show help'
163
173
  complete -c bingo-light -n '__bingo_light_patch_using_subcommand squash' -s h -l help -d 'Show help'
164
174
  complete -c bingo-light -n '__bingo_light_patch_using_subcommand meta' -s h -l help -d 'Show help'
175
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand lock' -s h -l help -d 'Show help'
176
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand lock' -l reason -d 'Reason for locking'
177
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand unlock' -s h -l help -d 'Show help'
178
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand unlock' -l force -d 'Force unlock even if locked by someone else'
179
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand check' -s h -l help -d 'Show help'
180
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand upstream' -s h -l help -d 'Show help'
181
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand expire' -s h -l help -d 'Show help'
182
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand stats' -s h -l help -d 'Show help'
183
+
184
+ # ---- dep subcommands ----
185
+
186
+ function __bingo_light_dep_needs_subcommand
187
+ set -l cmd (commandline -opc)
188
+ if test (count $cmd) -lt 2
189
+ return 1
190
+ end
191
+ if test "$cmd[2]" != dep
192
+ return 1
193
+ end
194
+ if test (count $cmd) -eq 2
195
+ return 0
196
+ end
197
+ return 1
198
+ end
199
+
200
+ complete -c bingo-light -n __bingo_light_dep_needs_subcommand -a patch -d 'Patch a modified dependency'
201
+ complete -c bingo-light -n __bingo_light_dep_needs_subcommand -a apply -d 'Re-apply patches after install'
202
+ complete -c bingo-light -n __bingo_light_dep_needs_subcommand -a sync -d 'Re-apply after update, detect conflicts'
203
+ complete -c bingo-light -n __bingo_light_dep_needs_subcommand -a status -d 'Show patch health'
204
+ complete -c bingo-light -n __bingo_light_dep_needs_subcommand -a list -d 'List all dependency patches'
205
+ complete -c bingo-light -n __bingo_light_dep_needs_subcommand -a drop -d 'Remove a dependency patch'
206
+ complete -c bingo-light -n __bingo_light_dep_needs_subcommand -a override -d 'Manage npm overrides/resolutions'
207
+ complete -c bingo-light -n __bingo_light_dep_needs_subcommand -a fork -d 'Track fork-as-dependency drift'
165
208
 
166
209
  # ---- workspace subcommands (also alias "ws") ----
167
210
 
@@ -195,3 +238,5 @@ complete -c bingo-light -n '__bingo_light_using_command test' -s h -
195
238
  complete -c bingo-light -n '__bingo_light_using_command workspace ws' -s h -l help -d 'Show help'
196
239
  complete -c bingo-light -n '__bingo_light_using_command smart-sync' -s h -l help -d 'Show help'
197
240
  complete -c bingo-light -n '__bingo_light_using_command session' -s h -l help -d 'Show help'
241
+ complete -c bingo-light -n '__bingo_light_using_command report' -s h -l help -d 'Show help'
242
+ complete -c bingo-light -n '__bingo_light_using_command doctor' -l report -d 'Include team, expiry, and dep checks'
@@ -11,6 +11,7 @@ _bingo-light() {
11
11
  local -a toplevel_commands=(
12
12
  'init:Initialize a new bingo-light project'
13
13
  'patch:Manage patches'
14
+ 'dep:Patch npm/pip dependencies'
14
15
  'setup:Configure MCP for AI tools (interactive)'
15
16
  'sync:Synchronize changes with upstream'
16
17
  'status:Show current status'
@@ -29,6 +30,7 @@ _bingo-light() {
29
30
  'workspace:Manage multiple forks'
30
31
  'smart-sync:Smart sync with circuit breaker and partial state'
31
32
  'session:Manage session memory'
33
+ 'report:Generate fork health report'
32
34
  )
33
35
 
34
36
  local -a toplevel_aliases=(
@@ -50,6 +52,12 @@ _bingo-light() {
50
52
  'reorder:Reorder the patch stack'
51
53
  'squash:Squash two patches into one'
52
54
  'meta:Get/set patch metadata'
55
+ 'lock:Lock a patch for exclusive editing'
56
+ 'unlock:Unlock a patch'
57
+ 'check:Check if patches are still needed'
58
+ 'upstream:Export patch as PR-ready diff'
59
+ 'expire:List expired patches'
60
+ 'stats:Show patch health metrics'
53
61
  )
54
62
 
55
63
  local -a patch_aliases=(
@@ -120,7 +128,7 @@ _bingo-light() {
120
128
  list|ls)
121
129
  _arguments $patch_list_flags
122
130
  ;;
123
- new|add|create|show|edit|drop|rm|remove|export|import|reorder|squash|meta)
131
+ new|add|create|show|edit|drop|rm|remove|export|import|reorder|squash|meta|lock|unlock|check|upstream|expire|stats)
124
132
  _arguments $help_flag
125
133
  ;;
126
134
  esac
@@ -136,6 +144,27 @@ _bingo-light() {
136
144
  diff|d)
137
145
  _arguments $help_flag
138
146
  ;;
147
+ dep)
148
+ local -a dep_subcommands=(
149
+ 'patch:Patch a modified dependency'
150
+ 'apply:Re-apply patches after install'
151
+ 'sync:Re-apply after update, detect conflicts'
152
+ 'status:Show patch health'
153
+ 'list:List all dependency patches'
154
+ 'drop:Remove a dependency patch'
155
+ 'override:Manage npm overrides/resolutions'
156
+ 'fork:Track fork-as-dependency drift'
157
+ )
158
+ _arguments -C \
159
+ '(- *)'{-h,--help}'[Show help]' \
160
+ '1:subcommand:->dep_subcmd' && return
161
+
162
+ case $state in
163
+ dep_subcmd)
164
+ _describe -t subcommands 'dep subcommand' dep_subcommands
165
+ ;;
166
+ esac
167
+ ;;
139
168
  workspace|ws)
140
169
  local -a ws_subcommands=(
141
170
  'init:Initialize workspace'
@@ -155,7 +184,14 @@ _bingo-light() {
155
184
  ;;
156
185
  esac
157
186
  ;;
158
- init|setup|doctor|auto-sync|log|undo|version|conflict-analyze|conflict-resolve|config|history|test|smart-sync|session)
187
+ conflict-resolve)
188
+ _arguments \
189
+ '--verify[Run test.command after the final rebase continues]' \
190
+ '--content-stdin[Read resolved content from stdin]' \
191
+ $help_flag \
192
+ '*:file:_files'
193
+ ;;
194
+ init|setup|doctor|auto-sync|log|undo|version|conflict-analyze|config|history|test|smart-sync|session|report)
159
195
  _arguments $help_flag
160
196
  ;;
161
197
  help)