bingo-light 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,306 @@
1
+ """
2
+ bingo_core.state — State management (.bingo/ directory) for bingo-light.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import tempfile
11
+ from datetime import datetime, timezone
12
+ from typing import List, Optional, Tuple
13
+
14
+ from bingo_core import (
15
+ BINGO_DIR,
16
+ CIRCUIT_BREAKER_LIMIT,
17
+ SYNC_HISTORY_MAX,
18
+ )
19
+ from bingo_core.exceptions import BingoError
20
+
21
+
22
+ class State:
23
+ """Manages .bingo/ directory state: undo, circuit breaker, metadata, etc."""
24
+
25
+ def __init__(self, repo_dir: str):
26
+ self.repo_dir = repo_dir
27
+ self.bingo_dir = os.path.join(repo_dir, BINGO_DIR)
28
+ self.metadata_file = os.path.join(self.bingo_dir, "metadata.json")
29
+ self.sync_history_file = os.path.join(self.bingo_dir, "sync-history.json")
30
+ self.session_file = os.path.join(self.bingo_dir, "session.md")
31
+
32
+ def _ensure_dir(self) -> None:
33
+ """Create .bingo/ directory if it doesn't exist."""
34
+ os.makedirs(self.bingo_dir, exist_ok=True)
35
+
36
+ def acquire_lock(self) -> None:
37
+ """Acquire an exclusive operation lock. Raises BingoError if locked."""
38
+ self._ensure_dir()
39
+ lock_path = os.path.join(self.bingo_dir, ".lock")
40
+ if os.path.isfile(lock_path):
41
+ try:
42
+ with open(lock_path) as f:
43
+ pid = int(f.read().strip())
44
+ # Check if the locking process is still running
45
+ os.kill(pid, 0)
46
+ raise BingoError(
47
+ f"Another bingo-light operation is in progress (pid {pid}). "
48
+ "If this is stale, remove .bingo/.lock"
49
+ )
50
+ except (ValueError, OSError):
51
+ pass # Stale lock — process is gone
52
+ with open(lock_path, "w") as f:
53
+ f.write(str(os.getpid()))
54
+
55
+ def release_lock(self) -> None:
56
+ """Release the operation lock."""
57
+ lock_path = os.path.join(self.bingo_dir, ".lock")
58
+ try:
59
+ os.unlink(lock_path)
60
+ except OSError:
61
+ pass
62
+
63
+ # -- Undo --
64
+
65
+ def save_undo(self, head: str, tracking: str) -> None:
66
+ """Save undo state for rollback."""
67
+ self._ensure_dir()
68
+ self._write(os.path.join(self.bingo_dir, ".undo-head"), head)
69
+ self._write(os.path.join(self.bingo_dir, ".undo-tracking"), tracking)
70
+ # Clear undo marker -- new sync starts a new cycle
71
+ self._remove(os.path.join(self.bingo_dir, ".undo-active"))
72
+
73
+ def load_undo(self) -> Tuple[Optional[str], Optional[str]]:
74
+ """Load saved undo state. Returns (head, tracking) or (None, None)."""
75
+ head = self._read(os.path.join(self.bingo_dir, ".undo-head"))
76
+ tracking = self._read(os.path.join(self.bingo_dir, ".undo-tracking"))
77
+ return head, tracking
78
+
79
+ def mark_undo_active(self) -> None:
80
+ """Mark that undo was used -- prevents _fix_stale_tracking from auto-advancing."""
81
+ self._ensure_dir()
82
+ self._write(os.path.join(self.bingo_dir, ".undo-active"), "")
83
+
84
+ def is_undo_active(self) -> bool:
85
+ """Check if undo marker is set."""
86
+ return os.path.isfile(os.path.join(self.bingo_dir, ".undo-active"))
87
+
88
+ def clear_undo_tracking(self) -> None:
89
+ """Remove the undo tracking file after restoring."""
90
+ self._remove(os.path.join(self.bingo_dir, ".undo-tracking"))
91
+
92
+ # -- Circuit Breaker --
93
+
94
+ def check_circuit_breaker(self, upstream_target: str) -> bool:
95
+ """Check if circuit breaker is tripped (3+ failures on same commit).
96
+
97
+ Returns True if we should STOP (breaker is tripped).
98
+ """
99
+ path = os.path.join(self.bingo_dir, ".sync-failures")
100
+ if not os.path.isfile(path):
101
+ return False
102
+ try:
103
+ content = self._read(path)
104
+ if content is None:
105
+ return False
106
+ lines = content.strip().split("\n")
107
+ if len(lines) < 2:
108
+ return False
109
+ target = lines[0]
110
+ count = int(lines[1])
111
+ return target == upstream_target and count >= CIRCUIT_BREAKER_LIMIT
112
+ except (ValueError, IndexError):
113
+ return False
114
+
115
+ def record_circuit_breaker(self, upstream_target: str) -> None:
116
+ """Record a sync failure for circuit breaker."""
117
+ self._ensure_dir()
118
+ path = os.path.join(self.bingo_dir, ".sync-failures")
119
+ prev_count = 0
120
+ if os.path.isfile(path):
121
+ try:
122
+ content = self._read(path)
123
+ if content:
124
+ lines = content.strip().split("\n")
125
+ if len(lines) >= 2 and lines[0] == upstream_target:
126
+ prev_count = int(lines[1])
127
+ except (ValueError, IndexError):
128
+ pass
129
+ self._write(path, f"{upstream_target}\n{prev_count + 1}")
130
+
131
+ def clear_circuit_breaker(self) -> None:
132
+ """Reset circuit breaker on success."""
133
+ self._remove(os.path.join(self.bingo_dir, ".sync-failures"))
134
+
135
+ # -- Metadata --
136
+
137
+ def _load_metadata(self) -> dict:
138
+ """Load metadata.json, creating it if needed."""
139
+ self._ensure_dir()
140
+ if not os.path.isfile(self.metadata_file):
141
+ return {"patches": {}}
142
+ try:
143
+ with open(self.metadata_file) as f:
144
+ return json.load(f)
145
+ except (json.JSONDecodeError, IOError):
146
+ return {"patches": {}}
147
+
148
+ def _save_metadata(self, data: dict) -> None:
149
+ """Atomically write metadata.json."""
150
+ self._ensure_dir()
151
+ self._write_json(self.metadata_file, data)
152
+
153
+ def patch_meta_get(self, patch_name: str) -> dict:
154
+ """Get metadata for a patch."""
155
+ data = self._load_metadata()
156
+ return data.get("patches", {}).get(
157
+ patch_name,
158
+ {
159
+ "reason": "",
160
+ "tags": [],
161
+ "expires": None,
162
+ "upstream_pr": "",
163
+ "status": "permanent",
164
+ },
165
+ )
166
+
167
+ def patch_meta_set(self, patch_name: str, key: str, value: str) -> None:
168
+ """Set a metadata field for a patch."""
169
+ data = self._load_metadata()
170
+ patches = data.setdefault("patches", {})
171
+ p = patches.setdefault(
172
+ patch_name,
173
+ {
174
+ "reason": "",
175
+ "tags": [],
176
+ "expires": None,
177
+ "upstream_pr": "",
178
+ "status": "permanent",
179
+ "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
180
+ },
181
+ )
182
+ if key in ("tag", "tags"):
183
+ tags_list = p.setdefault("tags", [])
184
+ for t in (v.strip() for v in value.split(",")):
185
+ if t and t not in tags_list:
186
+ tags_list.append(t)
187
+ elif key in ("reason", "expires", "upstream_pr", "status"):
188
+ p[key] = value
189
+ self._save_metadata(data)
190
+
191
+ # -- Sync History --
192
+
193
+ def record_sync(
194
+ self,
195
+ behind: int,
196
+ upstream_before: str,
197
+ upstream_after: str,
198
+ patches: List[dict],
199
+ ) -> None:
200
+ """Record a sync event."""
201
+ self._ensure_dir()
202
+ data: dict = {"syncs": []}
203
+ if os.path.isfile(self.sync_history_file):
204
+ try:
205
+ with open(self.sync_history_file) as f:
206
+ data = json.load(f)
207
+ except (json.JSONDecodeError, IOError):
208
+ data = {"syncs": []}
209
+
210
+ data["syncs"].append(
211
+ {
212
+ "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
213
+ "upstream_before": upstream_before,
214
+ "upstream_after": upstream_after,
215
+ "upstream_commits_integrated": behind,
216
+ "patches": patches,
217
+ }
218
+ )
219
+ # Keep only the last N entries
220
+ data["syncs"] = data["syncs"][-SYNC_HISTORY_MAX:]
221
+ self._write_json(self.sync_history_file, data)
222
+
223
+ def get_sync_history(self) -> dict:
224
+ """Get sync history."""
225
+ if not os.path.isfile(self.sync_history_file):
226
+ return {"syncs": []}
227
+ try:
228
+ with open(self.sync_history_file) as f:
229
+ return json.load(f)
230
+ except (json.JSONDecodeError, IOError):
231
+ return {"syncs": []}
232
+
233
+ # -- Session --
234
+
235
+ def update_session(self, content: str) -> None:
236
+ """Write session notes."""
237
+ self._ensure_dir()
238
+ self._write(self.session_file, content)
239
+
240
+ def get_session(self) -> Optional[str]:
241
+ """Read session notes."""
242
+ return self._read(self.session_file)
243
+
244
+ # -- Hooks --
245
+
246
+ def run_hook(self, event: str, data: Optional[dict] = None) -> None:
247
+ """Run a hook script if it exists: .bingo/hooks/<event>."""
248
+ hook_path = os.path.join(self.bingo_dir, "hooks", event)
249
+ if os.path.isfile(hook_path) and os.access(hook_path, os.X_OK):
250
+ try:
251
+ json_data = json.dumps(data or {})
252
+ result = subprocess.run(
253
+ [hook_path],
254
+ cwd=self.repo_dir,
255
+ input=json_data,
256
+ capture_output=True,
257
+ text=True,
258
+ timeout=30,
259
+ )
260
+ if result.returncode != 0:
261
+ import sys
262
+ print(
263
+ f"warning: hook '{event}' exited {result.returncode}",
264
+ file=sys.stderr,
265
+ )
266
+ except subprocess.TimeoutExpired:
267
+ import sys
268
+ print(f"warning: hook '{event}' timed out", file=sys.stderr)
269
+ except OSError:
270
+ pass # Hook not executable or missing interpreter
271
+
272
+ # -- Internal helpers --
273
+
274
+ def _write(self, path: str, content: str) -> None:
275
+ with open(path, "w") as f:
276
+ f.write(content)
277
+
278
+ def _read(self, path: str) -> Optional[str]:
279
+ if not os.path.isfile(path):
280
+ return None
281
+ try:
282
+ with open(path) as f:
283
+ return f.read().strip()
284
+ except IOError:
285
+ return None
286
+
287
+ def _remove(self, path: str) -> None:
288
+ try:
289
+ os.remove(path)
290
+ except FileNotFoundError:
291
+ pass
292
+
293
+ def _write_json(self, path: str, data: dict) -> None:
294
+ """Atomically write JSON file using temp file + rename."""
295
+ dir_name = os.path.dirname(path) or "."
296
+ fd, tmp_path = tempfile.mkstemp(suffix=".tmp", dir=dir_name)
297
+ try:
298
+ with os.fdopen(fd, "w") as f:
299
+ json.dump(data, f, indent=2)
300
+ os.replace(tmp_path, path)
301
+ except Exception:
302
+ try:
303
+ os.unlink(tmp_path)
304
+ except FileNotFoundError:
305
+ pass
306
+ raise
@@ -0,0 +1,118 @@
1
+ # Bash completion for bingo-light
2
+ # Source this file or place it in /etc/bash_completion.d/
3
+ #
4
+ # Usage:
5
+ # source bingo-light.bash
6
+ # # or
7
+ # cp bingo-light.bash /etc/bash_completion.d/bingo-light
8
+
9
+ _bingo_light() {
10
+ local cur prev words cword
11
+ _init_completion || return
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"
14
+ local -r toplevel_aliases="p s st d ws"
15
+ local -r all_toplevel="${toplevel_commands} ${toplevel_aliases}"
16
+
17
+ local -r patch_subcommands="new list show edit drop export import reorder squash meta"
18
+ local -r patch_aliases="ls add create rm remove"
19
+ local -r all_patch="${patch_subcommands} ${patch_aliases}"
20
+
21
+ # Walk backward through the command line to find the active command context.
22
+ local cmd=""
23
+ local subcmd=""
24
+ local i
25
+ for (( i=1; i < cword; i++ )); do
26
+ case "${words[i]}" in
27
+ patch|p)
28
+ cmd="patch"
29
+ ;;
30
+ sync|s)
31
+ cmd="sync"
32
+ ;;
33
+ status|st)
34
+ cmd="status"
35
+ ;;
36
+ diff|d)
37
+ cmd="diff"
38
+ ;;
39
+ init|setup|doctor|auto-sync|log|undo|version|help|conflict-analyze|conflict-resolve|config|history|test|workspace|ws|smart-sync|session)
40
+ cmd="${words[i]}"
41
+ ;;
42
+ *)
43
+ # If we already have a command, this may be a subcommand.
44
+ if [[ -n "$cmd" && -z "$subcmd" ]]; then
45
+ subcmd="${words[i]}"
46
+ fi
47
+ ;;
48
+ esac
49
+ done
50
+
51
+ # ---- Completions based on context ----
52
+
53
+ # Inside "patch" (or alias "p") -- complete subcommands and flags
54
+ if [[ "$cmd" == "patch" ]]; then
55
+ # If a subcommand is already chosen, offer its flags
56
+ case "$subcmd" in
57
+ list|ls)
58
+ COMPREPLY=( $(compgen -W "-v --verbose --help -h" -- "$cur") )
59
+ return
60
+ ;;
61
+ new|add|create|show|edit|drop|rm|remove|export|import|reorder)
62
+ COMPREPLY=( $(compgen -W "--help -h" -- "$cur") )
63
+ return
64
+ ;;
65
+ esac
66
+
67
+ # No subcommand yet -- offer patch subcommands (including aliases)
68
+ if [[ "$cur" == -* ]]; then
69
+ COMPREPLY=( $(compgen -W "--help -h" -- "$cur") )
70
+ else
71
+ COMPREPLY=( $(compgen -W "${all_patch}" -- "$cur") )
72
+ fi
73
+ return
74
+ fi
75
+
76
+ # Inside "sync" (or alias "s") -- complete flags
77
+ if [[ "$cmd" == "sync" ]]; then
78
+ COMPREPLY=( $(compgen -W "--dry-run -n --force -f --help -h" -- "$cur") )
79
+ return
80
+ fi
81
+
82
+ # Inside "status" (or alias "st")
83
+ if [[ "$cmd" == "status" ]]; then
84
+ COMPREPLY=( $(compgen -W "--help -h" -- "$cur") )
85
+ return
86
+ fi
87
+
88
+ # Inside "diff" (or alias "d")
89
+ if [[ "$cmd" == "diff" ]]; then
90
+ COMPREPLY=( $(compgen -W "--help -h" -- "$cur") )
91
+ return
92
+ fi
93
+
94
+ # Inside "workspace" (or alias "ws") -- complete subcommands
95
+ if [[ "$cmd" == "workspace" || "$cmd" == "ws" ]]; then
96
+ if [[ -z "$subcmd" ]]; then
97
+ COMPREPLY=( $(compgen -W "init add remove list sync status" -- "$cur") )
98
+ else
99
+ COMPREPLY=( $(compgen -W "--help -h" -- "$cur") )
100
+ fi
101
+ return
102
+ fi
103
+
104
+ # Other known commands that take no further subcommands
105
+ if [[ -n "$cmd" ]]; then
106
+ COMPREPLY=( $(compgen -W "--help -h" -- "$cur") )
107
+ return
108
+ fi
109
+
110
+ # Top-level: no command selected yet
111
+ if [[ "$cur" == -* ]]; then
112
+ COMPREPLY=( $(compgen -W "--help -h --version --json --yes -y" -- "$cur") )
113
+ else
114
+ COMPREPLY=( $(compgen -W "${all_toplevel}" -- "$cur") )
115
+ fi
116
+ }
117
+
118
+ complete -F _bingo_light bingo-light
@@ -0,0 +1,197 @@
1
+ # Fish completion for bingo-light
2
+ # Place in ~/.config/fish/completions/ or source directly.
3
+ #
4
+ # Usage:
5
+ # cp bingo-light.fish ~/.config/fish/completions/
6
+
7
+ # ---- Helper: detect whether a specific command/subcommand is already on the line ----
8
+
9
+ # Returns true when no subcommand has been given yet (top-level completions).
10
+ function __bingo_light_needs_command
11
+ set -l cmd (commandline -opc)
12
+ if test (count $cmd) -eq 1
13
+ return 0
14
+ end
15
+ return 1
16
+ end
17
+
18
+ # Returns true when the first positional argument matches any of the supplied values.
19
+ function __bingo_light_using_command
20
+ set -l cmd (commandline -opc)
21
+ if test (count $cmd) -lt 2
22
+ return 1
23
+ end
24
+ for arg in $argv
25
+ if test "$cmd[2]" = "$arg"
26
+ return 0
27
+ end
28
+ end
29
+ return 1
30
+ end
31
+
32
+ # Returns true when we are inside "patch" (or "p") and need a patch subcommand.
33
+ function __bingo_light_patch_needs_subcommand
34
+ set -l cmd (commandline -opc)
35
+ if test (count $cmd) -lt 2
36
+ return 1
37
+ end
38
+ if test "$cmd[2]" != patch -a "$cmd[2]" != p
39
+ return 1
40
+ end
41
+ if test (count $cmd) -eq 2
42
+ return 0
43
+ end
44
+ return 1
45
+ end
46
+
47
+ # Returns true when the patch subcommand matches any of the supplied values.
48
+ function __bingo_light_patch_using_subcommand
49
+ set -l cmd (commandline -opc)
50
+ if test (count $cmd) -lt 3
51
+ return 1
52
+ end
53
+ if test "$cmd[2]" != patch -a "$cmd[2]" != p
54
+ return 1
55
+ end
56
+ for arg in $argv
57
+ if test "$cmd[3]" = "$arg"
58
+ return 0
59
+ end
60
+ end
61
+ return 1
62
+ end
63
+
64
+ # ---- Disable file completions by default ----
65
+ complete -c bingo-light -f
66
+
67
+ # ---- Top-level commands ----
68
+ complete -c bingo-light -n __bingo_light_needs_command -a init -d 'Initialize a new bingo-light project'
69
+ complete -c bingo-light -n __bingo_light_needs_command -a setup -d 'Configure MCP for AI tools (interactive)'
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 sync -d 'Synchronize changes with upstream'
72
+ complete -c bingo-light -n __bingo_light_needs_command -a status -d 'Show current status'
73
+ complete -c bingo-light -n __bingo_light_needs_command -a doctor -d 'Diagnose and fix common problems'
74
+ complete -c bingo-light -n __bingo_light_needs_command -a auto-sync -d 'Enable or configure automatic synchronization'
75
+ complete -c bingo-light -n __bingo_light_needs_command -a log -d 'Show change log'
76
+ complete -c bingo-light -n __bingo_light_needs_command -a undo -d 'Undo the last operation'
77
+ complete -c bingo-light -n __bingo_light_needs_command -a diff -d 'Show differences between states'
78
+ complete -c bingo-light -n __bingo_light_needs_command -a version -d 'Print version information'
79
+ complete -c bingo-light -n __bingo_light_needs_command -a help -d 'Show help for a command'
80
+ complete -c bingo-light -n __bingo_light_needs_command -a conflict-analyze -d 'Analyze conflicts during rebase'
81
+ complete -c bingo-light -n __bingo_light_needs_command -a conflict-resolve -d 'Resolve a conflict file and continue'
82
+ complete -c bingo-light -n __bingo_light_needs_command -a config -d 'Get/set/list configuration'
83
+ complete -c bingo-light -n __bingo_light_needs_command -a history -d 'Show sync history with hash mappings'
84
+ complete -c bingo-light -n __bingo_light_needs_command -a test -d 'Run configured test suite'
85
+ complete -c bingo-light -n __bingo_light_needs_command -a workspace -d 'Manage multiple forks'
86
+ complete -c bingo-light -n __bingo_light_needs_command -a smart-sync -d 'Smart sync with circuit breaker and partial state'
87
+ complete -c bingo-light -n __bingo_light_needs_command -a session -d 'Manage session memory'
88
+
89
+ # Short aliases
90
+ complete -c bingo-light -n __bingo_light_needs_command -a p -d 'Alias for patch'
91
+ complete -c bingo-light -n __bingo_light_needs_command -a s -d 'Alias for sync'
92
+ complete -c bingo-light -n __bingo_light_needs_command -a st -d 'Alias for status'
93
+ complete -c bingo-light -n __bingo_light_needs_command -a d -d 'Alias for diff'
94
+ complete -c bingo-light -n __bingo_light_needs_command -a ws -d 'Alias for workspace'
95
+
96
+ # Global flags
97
+ complete -c bingo-light -n __bingo_light_needs_command -s h -l help -d 'Show help'
98
+ complete -c bingo-light -n __bingo_light_needs_command -l version -d 'Show version'
99
+ complete -c bingo-light -l json -d 'Output structured JSON'
100
+ complete -c bingo-light -l yes -d 'Non-interactive mode, auto-confirm prompts'
101
+ complete -c bingo-light -s y -d 'Non-interactive mode (short for --yes)'
102
+
103
+ # ---- sync flags (also alias "s") ----
104
+ complete -c bingo-light -n '__bingo_light_using_command sync s' -s n -l dry-run -d 'Show what would be done without making changes'
105
+ complete -c bingo-light -n '__bingo_light_using_command sync s' -s f -l force -d 'Force sync, overwriting conflicts'
106
+ complete -c bingo-light -n '__bingo_light_using_command sync s' -s t -l test -d 'Run test suite after sync'
107
+ complete -c bingo-light -n '__bingo_light_using_command sync s' -s h -l help -d 'Show help'
108
+
109
+ # ---- status flags (also alias "st") ----
110
+ complete -c bingo-light -n '__bingo_light_using_command status st' -s h -l help -d 'Show help'
111
+
112
+ # ---- diff flags (also alias "d") ----
113
+ complete -c bingo-light -n '__bingo_light_using_command diff d' -s h -l help -d 'Show help'
114
+
115
+ # ---- Simple commands (no subcommands, just --help) ----
116
+ complete -c bingo-light -n '__bingo_light_using_command init' -s h -l help -d 'Show help'
117
+ complete -c bingo-light -n '__bingo_light_using_command doctor' -s h -l help -d 'Show help'
118
+ complete -c bingo-light -n '__bingo_light_using_command auto-sync' -s h -l help -d 'Show help'
119
+ complete -c bingo-light -n '__bingo_light_using_command log' -s h -l help -d 'Show help'
120
+ complete -c bingo-light -n '__bingo_light_using_command undo' -s h -l help -d 'Show help'
121
+ complete -c bingo-light -n '__bingo_light_using_command version' -s h -l help -d 'Show help'
122
+
123
+ # ---- conflict-resolve flags ----
124
+ complete -c bingo-light -n '__bingo_light_using_command conflict-resolve' -s h -l help -d 'Show help'
125
+
126
+ # ---- 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'
128
+
129
+ # ---- patch subcommands (also alias "p") ----
130
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a new -d 'Create a new patch'
131
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a list -d 'List all patches'
132
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a show -d 'Show details of a patch'
133
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a edit -d 'Edit an existing patch'
134
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a drop -d 'Remove a patch'
135
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a export -d 'Export patches to files'
136
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a import -d 'Import patches from files'
137
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a reorder -d 'Reorder the patch stack'
138
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a squash -d 'Squash two patches into one'
139
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a meta -d 'Get/set patch metadata'
140
+
141
+ # Patch short aliases
142
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a ls -d 'Alias for list'
143
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a add -d 'Alias for new'
144
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a create -d 'Alias for new'
145
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a rm -d 'Alias for drop'
146
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -a remove -d 'Alias for drop'
147
+
148
+ # Help flag for patch itself
149
+ complete -c bingo-light -n __bingo_light_patch_needs_subcommand -s h -l help -d 'Show help'
150
+
151
+ # ---- patch list / ls flags ----
152
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand list ls' -s v -l verbose -d 'Show detailed patch information'
153
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand list ls' -s h -l help -d 'Show help'
154
+
155
+ # ---- patch subcommands that only take --help ----
156
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand new add create' -s h -l help -d 'Show help'
157
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand show' -s h -l help -d 'Show help'
158
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand edit' -s h -l help -d 'Show help'
159
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand drop rm remove' -s h -l help -d 'Show help'
160
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand export' -s h -l help -d 'Show help'
161
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand import' -s h -l help -d 'Show help'
162
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand reorder' -s h -l help -d 'Show help'
163
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand squash' -s h -l help -d 'Show help'
164
+ complete -c bingo-light -n '__bingo_light_patch_using_subcommand meta' -s h -l help -d 'Show help'
165
+
166
+ # ---- workspace subcommands (also alias "ws") ----
167
+
168
+ # Returns true when we are inside "workspace" (or "ws") and need a workspace subcommand.
169
+ function __bingo_light_workspace_needs_subcommand
170
+ set -l cmd (commandline -opc)
171
+ if test (count $cmd) -lt 2
172
+ return 1
173
+ end
174
+ if test "$cmd[2]" != workspace -a "$cmd[2]" != ws
175
+ return 1
176
+ end
177
+ if test (count $cmd) -eq 2
178
+ return 0
179
+ end
180
+ return 1
181
+ end
182
+
183
+ complete -c bingo-light -n __bingo_light_workspace_needs_subcommand -a init -d 'Initialize workspace'
184
+ complete -c bingo-light -n __bingo_light_workspace_needs_subcommand -a add -d 'Add a repo to workspace'
185
+ complete -c bingo-light -n __bingo_light_workspace_needs_subcommand -a remove -d 'Remove a repo from workspace'
186
+ complete -c bingo-light -n __bingo_light_workspace_needs_subcommand -a list -d 'List workspace repos'
187
+ complete -c bingo-light -n __bingo_light_workspace_needs_subcommand -a sync -d 'Sync all workspace repos'
188
+ complete -c bingo-light -n __bingo_light_workspace_needs_subcommand -a status -d 'Show workspace status'
189
+
190
+ # ---- New top-level command flags ----
191
+ complete -c bingo-light -n '__bingo_light_using_command conflict-analyze' -s h -l help -d 'Show help'
192
+ complete -c bingo-light -n '__bingo_light_using_command config' -s h -l help -d 'Show help'
193
+ complete -c bingo-light -n '__bingo_light_using_command history' -s h -l help -d 'Show help'
194
+ complete -c bingo-light -n '__bingo_light_using_command test' -s h -l help -d 'Show help'
195
+ complete -c bingo-light -n '__bingo_light_using_command workspace ws' -s h -l help -d 'Show help'
196
+ complete -c bingo-light -n '__bingo_light_using_command smart-sync' -s h -l help -d 'Show help'
197
+ complete -c bingo-light -n '__bingo_light_using_command session' -s h -l help -d 'Show help'