bumblebee-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -47
- package/bin/bb.mjs +132 -102
- package/package.json +28 -28
- package/python/bb_cli/__main__.py +3 -0
- package/python/bb_cli/api_client.py +7 -0
- package/python/bb_cli/commands/agent.py +2287 -1030
- package/python/bb_cli/commands/auth.py +79 -79
- package/python/bb_cli/commands/board.py +47 -47
- package/python/bb_cli/commands/comment.py +34 -34
- package/python/bb_cli/commands/daemon.py +153 -0
- package/python/bb_cli/commands/init.py +83 -62
- package/python/bb_cli/commands/item.py +192 -192
- package/python/bb_cli/commands/project.py +175 -111
- package/python/bb_cli/config.py +136 -136
- package/python/bb_cli/main.py +44 -44
- package/python/bb_cli/progress.py +117 -0
- package/python/bb_cli/streaming.py +168 -0
- package/python/pyproject.toml +1 -1
- package/python/{bb_cli/bumblebee_cli.egg-info/requires.txt → requirements.txt} +4 -4
- package/scripts/build.sh +20 -20
- package/scripts/postinstall.mjs +146 -121
- package/python/bb_cli/bumblebee_cli.egg-info/PKG-INFO +0 -9
- package/python/bb_cli/bumblebee_cli.egg-info/SOURCES.txt +0 -21
- package/python/bb_cli/bumblebee_cli.egg-info/dependency_links.txt +0 -1
- package/python/bb_cli/bumblebee_cli.egg-info/entry_points.txt +0 -2
- package/python/bb_cli/bumblebee_cli.egg-info/top_level.txt +0 -5
- package/python/bb_cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/agent.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/auth.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/board.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/comment.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/init.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/item.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/label.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/project.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/sprint.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/story.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/task.cpython-313.pyc +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Rich Live progress tracker for batch agent operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
from rich.live import Live
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentProgressTracker:
|
|
14
|
+
"""Thread-safe progress tracker with Rich Live table display.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
with AgentProgressTracker() as tracker:
|
|
18
|
+
# From worker threads:
|
|
19
|
+
tracker.update("BB-42", "execute", "running", "Modifying auth.py")
|
|
20
|
+
tracker.complete("BB-42", True, "Done")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._data: dict[str, dict] = {}
|
|
25
|
+
self._lock = threading.Lock()
|
|
26
|
+
self._start_time = time.monotonic()
|
|
27
|
+
self._completed = 0
|
|
28
|
+
self._total = 0
|
|
29
|
+
self._live: Live | None = None
|
|
30
|
+
|
|
31
|
+
def register(self, item_key: str):
|
|
32
|
+
"""Register an item before work starts."""
|
|
33
|
+
with self._lock:
|
|
34
|
+
self._data[item_key] = {
|
|
35
|
+
"phase": "pending",
|
|
36
|
+
"status": "waiting",
|
|
37
|
+
"last_line": "",
|
|
38
|
+
"start": time.monotonic(),
|
|
39
|
+
}
|
|
40
|
+
self._total += 1
|
|
41
|
+
|
|
42
|
+
def update(self, item_key: str, phase: str, status: str, last_line: str):
|
|
43
|
+
"""Update progress for an item (called from worker threads)."""
|
|
44
|
+
with self._lock:
|
|
45
|
+
if item_key not in self._data:
|
|
46
|
+
self._data[item_key] = {"start": time.monotonic()}
|
|
47
|
+
self._data[item_key].update({
|
|
48
|
+
"phase": phase,
|
|
49
|
+
"status": status,
|
|
50
|
+
"last_line": last_line,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
def complete(self, item_key: str, success: bool, message: str = ""):
|
|
54
|
+
"""Mark an item as completed."""
|
|
55
|
+
with self._lock:
|
|
56
|
+
status = "done" if success else "failed"
|
|
57
|
+
if item_key in self._data:
|
|
58
|
+
self._data[item_key].update({
|
|
59
|
+
"status": status,
|
|
60
|
+
"last_line": message[:60],
|
|
61
|
+
})
|
|
62
|
+
self._completed += 1
|
|
63
|
+
|
|
64
|
+
def _build_table(self) -> Table:
|
|
65
|
+
"""Build the Rich table (called on each Live refresh)."""
|
|
66
|
+
table = Table(show_header=True, header_style="bold cyan", expand=True)
|
|
67
|
+
table.add_column("Item", style="bold", width=12)
|
|
68
|
+
table.add_column("Phase", width=14)
|
|
69
|
+
table.add_column("Status", width=10)
|
|
70
|
+
table.add_column("Last Output", ratio=1)
|
|
71
|
+
|
|
72
|
+
with self._lock:
|
|
73
|
+
for key, info in self._data.items():
|
|
74
|
+
status = info.get("status", "?")
|
|
75
|
+
phase = info.get("phase", "?")
|
|
76
|
+
last_line = info.get("last_line", "")
|
|
77
|
+
|
|
78
|
+
# Color status
|
|
79
|
+
if status == "running":
|
|
80
|
+
status_text = Text(status, style="yellow")
|
|
81
|
+
elif status == "done":
|
|
82
|
+
status_text = Text(status, style="green")
|
|
83
|
+
elif status == "failed":
|
|
84
|
+
status_text = Text(status, style="red")
|
|
85
|
+
else:
|
|
86
|
+
status_text = Text(status, style="dim")
|
|
87
|
+
|
|
88
|
+
table.add_row(key, phase, status_text, Text(last_line, overflow="ellipsis"))
|
|
89
|
+
|
|
90
|
+
elapsed = time.monotonic() - self._start_time
|
|
91
|
+
mins, secs = divmod(int(elapsed), 60)
|
|
92
|
+
|
|
93
|
+
table.caption = f"Elapsed: {mins}m {secs:02d}s | {self._completed}/{self._total} complete | Ctrl+C to abort"
|
|
94
|
+
return table
|
|
95
|
+
|
|
96
|
+
def __enter__(self):
|
|
97
|
+
self._live = Live(self._build_table(), refresh_per_second=2)
|
|
98
|
+
self._live.__enter__()
|
|
99
|
+
# Start refresh loop
|
|
100
|
+
self._refresh_thread = threading.Thread(target=self._refresh_loop, daemon=True)
|
|
101
|
+
self._stop_refresh = threading.Event()
|
|
102
|
+
self._refresh_thread.start()
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, *args):
|
|
106
|
+
self._stop_refresh.set()
|
|
107
|
+
self._refresh_thread.join(timeout=2)
|
|
108
|
+
if self._live:
|
|
109
|
+
self._live.update(self._build_table())
|
|
110
|
+
self._live.__exit__(*args)
|
|
111
|
+
|
|
112
|
+
def _refresh_loop(self):
|
|
113
|
+
"""Refresh the Live display periodically."""
|
|
114
|
+
while not self._stop_refresh.is_set():
|
|
115
|
+
if self._live:
|
|
116
|
+
self._live.update(self._build_table())
|
|
117
|
+
self._stop_refresh.wait(0.5)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Buffered agent output streaming with local log fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import queue
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .api_client import api_post
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _logs_dir() -> Path:
|
|
15
|
+
d = Path.home() / ".bumblebee" / "logs"
|
|
16
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
return d
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentStreamer:
|
|
21
|
+
"""Thread-safe buffered relay for Claude CLI stream-json output.
|
|
22
|
+
|
|
23
|
+
- Reads JSON lines via feed()
|
|
24
|
+
- Writes each line to a local log file immediately (safety net)
|
|
25
|
+
- Batches events and POSTs to /relay-batch every FLUSH_INTERVAL_S or BATCH_SIZE events
|
|
26
|
+
- Extracts text blocks for summary
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
BATCH_SIZE = 20
|
|
30
|
+
FLUSH_INTERVAL_S = 0.5
|
|
31
|
+
|
|
32
|
+
def __init__(self, session_id: int | str):
|
|
33
|
+
self._session_id = session_id
|
|
34
|
+
self._queue: queue.Queue[dict] = queue.Queue()
|
|
35
|
+
self._done = threading.Event()
|
|
36
|
+
self._text_blocks: list[str] = []
|
|
37
|
+
self._log_path = _logs_dir() / f"session-{session_id}.jsonl"
|
|
38
|
+
self._log_file = open(self._log_path, "a", encoding="utf-8")
|
|
39
|
+
self._sender_thread: threading.Thread | None = None
|
|
40
|
+
self._callback: callable | None = None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def text_blocks(self) -> list[str]:
|
|
44
|
+
return self._text_blocks
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def log_path(self) -> Path:
|
|
48
|
+
return self._log_path
|
|
49
|
+
|
|
50
|
+
def set_callback(self, cb: callable):
|
|
51
|
+
"""Set a callback(payload: dict, text: str|None) for each event."""
|
|
52
|
+
self._callback = cb
|
|
53
|
+
|
|
54
|
+
def start(self):
|
|
55
|
+
"""Start the background sender thread."""
|
|
56
|
+
self._sender_thread = threading.Thread(
|
|
57
|
+
target=self._sender_loop, daemon=True, name=f"streamer-{self._session_id}"
|
|
58
|
+
)
|
|
59
|
+
self._sender_thread.start()
|
|
60
|
+
|
|
61
|
+
def feed(self, line: str):
|
|
62
|
+
"""Feed a raw line from Claude CLI stdout."""
|
|
63
|
+
line = line.strip()
|
|
64
|
+
if not line:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
payload = json.loads(line)
|
|
69
|
+
except json.JSONDecodeError:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Write to log file immediately
|
|
73
|
+
self._log_file.write(line + "\n")
|
|
74
|
+
self._log_file.flush()
|
|
75
|
+
|
|
76
|
+
# Extract text blocks
|
|
77
|
+
text = None
|
|
78
|
+
if payload.get("type") == "assistant":
|
|
79
|
+
for block in payload.get("content", []):
|
|
80
|
+
if block.get("type") == "text":
|
|
81
|
+
text = block["text"]
|
|
82
|
+
self._text_blocks.append(text)
|
|
83
|
+
|
|
84
|
+
# Notify callback (for terminal display, progress tracker, etc.)
|
|
85
|
+
if self._callback:
|
|
86
|
+
try:
|
|
87
|
+
self._callback(payload, text)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
# Enqueue for batch relay
|
|
92
|
+
self._queue.put(payload)
|
|
93
|
+
|
|
94
|
+
def stop(self) -> list[str]:
|
|
95
|
+
"""Signal completion, flush remaining events, return text blocks."""
|
|
96
|
+
self._done.set()
|
|
97
|
+
if self._sender_thread:
|
|
98
|
+
self._sender_thread.join(timeout=10)
|
|
99
|
+
self._log_file.close()
|
|
100
|
+
return self._text_blocks
|
|
101
|
+
|
|
102
|
+
def _sender_loop(self):
|
|
103
|
+
"""Background thread: batch events and POST to API."""
|
|
104
|
+
buffer: list[dict] = []
|
|
105
|
+
last_flush = time.monotonic()
|
|
106
|
+
|
|
107
|
+
while not self._done.is_set() or not self._queue.empty():
|
|
108
|
+
# Drain queue
|
|
109
|
+
try:
|
|
110
|
+
item = self._queue.get(timeout=0.25)
|
|
111
|
+
buffer.append(item)
|
|
112
|
+
except queue.Empty:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
# Flush conditions
|
|
116
|
+
now = time.monotonic()
|
|
117
|
+
should_flush = (
|
|
118
|
+
len(buffer) >= self.BATCH_SIZE
|
|
119
|
+
or (buffer and now - last_flush >= self.FLUSH_INTERVAL_S)
|
|
120
|
+
or (buffer and self._done.is_set())
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if should_flush:
|
|
124
|
+
self._flush(buffer)
|
|
125
|
+
buffer = []
|
|
126
|
+
last_flush = now
|
|
127
|
+
|
|
128
|
+
def _flush(self, events: list[dict]):
|
|
129
|
+
"""POST batch to API. Swallow errors — log file is the safety net."""
|
|
130
|
+
if not events:
|
|
131
|
+
return
|
|
132
|
+
try:
|
|
133
|
+
api_post(
|
|
134
|
+
f"/api/agent-sessions/{self._session_id}/relay-batch",
|
|
135
|
+
json={"events": events},
|
|
136
|
+
)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass # Data preserved in log file
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def api_patch(path: str, json: dict) -> dict:
|
|
142
|
+
"""PATCH request helper (not in api_client.py yet)."""
|
|
143
|
+
from .api_client import get_client
|
|
144
|
+
|
|
145
|
+
with get_client() as client:
|
|
146
|
+
resp = client.patch(path, json=json)
|
|
147
|
+
resp.raise_for_status()
|
|
148
|
+
return resp.json()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def update_phase(session_id: int | str, phase: str, **kwargs):
|
|
152
|
+
"""Update agent session phase via API."""
|
|
153
|
+
body: dict = {"phase": phase}
|
|
154
|
+
body.update(kwargs)
|
|
155
|
+
try:
|
|
156
|
+
api_patch(f"/api/agent-sessions/{session_id}/phase", json=body)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def complete_session(session_id: int | str, status: str, **kwargs):
|
|
162
|
+
"""Mark agent session as completed/failed via API."""
|
|
163
|
+
body: dict = {"status": status}
|
|
164
|
+
body.update(kwargs)
|
|
165
|
+
try:
|
|
166
|
+
api_post(f"/api/agent-sessions/{session_id}/complete", json=body)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
package/python/pyproject.toml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
typer>=0.15.0
|
|
2
|
-
httpx>=0.28.0
|
|
3
|
-
rich>=13.9.0
|
|
4
|
-
toml>=0.10.2
|
|
1
|
+
typer>=0.15.0
|
|
2
|
+
httpx>=0.28.0
|
|
3
|
+
rich>=13.9.0
|
|
4
|
+
toml>=0.10.2
|
package/scripts/build.sh
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
# Build script: copies the Python CLI source into the npm package for publishing.
|
|
5
|
-
# Run from repo root: bash packages/npm-cli/scripts/build.sh
|
|
6
|
-
|
|
7
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
-
PKG_DIR="$(dirname "$SCRIPT_DIR")"
|
|
9
|
-
REPO_ROOT="$(dirname "$(dirname "$PKG_DIR")")"
|
|
10
|
-
|
|
11
|
-
DEST="$PKG_DIR/python"
|
|
12
|
-
|
|
13
|
-
echo "Copying CLI source to $DEST..."
|
|
14
|
-
rm -rf "$DEST"
|
|
15
|
-
cp -r "$REPO_ROOT/cli" "$DEST"
|
|
16
|
-
|
|
17
|
-
# Clean up build artifacts
|
|
18
|
-
rm -rf "$DEST"/__pycache__ "$DEST"/**/__pycache__ "$DEST"/*.egg-info "$DEST"/dist "$DEST"/build
|
|
19
|
-
|
|
20
|
-
echo "Build complete. Ready to publish with: cd $PKG_DIR && npm publish"
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Build script: copies the Python CLI source into the npm package for publishing.
|
|
5
|
+
# Run from repo root: bash packages/npm-cli/scripts/build.sh
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
PKG_DIR="$(dirname "$SCRIPT_DIR")"
|
|
9
|
+
REPO_ROOT="$(dirname "$(dirname "$PKG_DIR")")"
|
|
10
|
+
|
|
11
|
+
DEST="$PKG_DIR/python"
|
|
12
|
+
|
|
13
|
+
echo "Copying CLI source to $DEST..."
|
|
14
|
+
rm -rf "$DEST"
|
|
15
|
+
cp -r "$REPO_ROOT/cli" "$DEST"
|
|
16
|
+
|
|
17
|
+
# Clean up build artifacts
|
|
18
|
+
rm -rf "$DEST"/__pycache__ "$DEST"/**/__pycache__ "$DEST"/*.egg-info "$DEST"/dist "$DEST"/build
|
|
19
|
+
|
|
20
|
+
echo "Build complete. Ready to publish with: cd $PKG_DIR && npm publish"
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,121 +1,146 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Postinstall script for bumblebee-cli
|
|
5
|
-
*
|
|
6
|
-
* - Checks for Python 3.12+
|
|
7
|
-
* - Skips if `bb` is already on PATH
|
|
8
|
-
* - Creates a local .venv and installs the Python CLI
|
|
9
|
-
* - NEVER fails npm install — warns instead
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { execSync } from "node:child_process";
|
|
13
|
-
import { existsSync } from "node:fs";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
|
-
|
|
17
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const pkgRoot = join(__dirname, "..");
|
|
19
|
-
const pythonDir = join(pkgRoot, "python");
|
|
20
|
-
|
|
21
|
-
function log(msg) {
|
|
22
|
-
console.log(`[bumblebee-cli] ${msg}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function warn(msg) {
|
|
26
|
-
console.warn(`[bumblebee-cli] WARNING: ${msg}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Check if bb is already available
|
|
30
|
-
function
|
|
31
|
-
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script for bumblebee-cli
|
|
5
|
+
*
|
|
6
|
+
* - Checks for Python 3.12+
|
|
7
|
+
* - Skips if a real (pip-installed) `bb` is already on PATH
|
|
8
|
+
* - Creates a local .venv and installs the Python CLI
|
|
9
|
+
* - NEVER fails npm install — warns instead
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const pkgRoot = join(__dirname, "..");
|
|
19
|
+
const pythonDir = join(pkgRoot, "python");
|
|
20
|
+
|
|
21
|
+
function log(msg) {
|
|
22
|
+
console.log(`[bumblebee-cli] ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function warn(msg) {
|
|
26
|
+
console.warn(`[bumblebee-cli] WARNING: ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if a real (non-npm-wrapper) bb is already available
|
|
30
|
+
function isPipBbInstalled() {
|
|
31
|
+
try {
|
|
32
|
+
// Try running bb --help and check if it's the Python CLI (not our wrapper)
|
|
33
|
+
const which = process.platform === "win32" ? "where" : "which";
|
|
34
|
+
const result = execSync(`${which} bb`, { encoding: "utf8", stdio: "pipe" });
|
|
35
|
+
const paths = result.trim().split(/\r?\n/);
|
|
36
|
+
|
|
37
|
+
// Get npm global prefix to filter out our own wrapper
|
|
38
|
+
let npmPrefix = null;
|
|
39
|
+
try {
|
|
40
|
+
npmPrefix = execSync("npm prefix -g", { encoding: "utf8", stdio: "pipe" }).trim();
|
|
41
|
+
} catch {}
|
|
42
|
+
|
|
43
|
+
for (const bbPath of paths) {
|
|
44
|
+
const p = bbPath.trim();
|
|
45
|
+
if (!p) continue;
|
|
46
|
+
const normalized = resolve(p).toLowerCase();
|
|
47
|
+
// Skip if it's inside npm global prefix (that's our own wrapper)
|
|
48
|
+
if (npmPrefix && normalized.startsWith(resolve(npmPrefix).toLowerCase())) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Skip if it's inside node_modules
|
|
52
|
+
if (normalized.includes("node_modules") && normalized.includes("bumblebee-cli")) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Found a real bb (pip-installed)
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// not found
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Find python and check version
|
|
65
|
+
function findPython() {
|
|
66
|
+
const candidates =
|
|
67
|
+
process.platform === "win32" ? ["python", "python3"] : ["python3", "python"];
|
|
68
|
+
for (const cmd of candidates) {
|
|
69
|
+
try {
|
|
70
|
+
const version = execSync(`${cmd} --version`, {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
73
|
+
}).trim();
|
|
74
|
+
// Parse "Python 3.12.x"
|
|
75
|
+
const match = version.match(/Python (\d+)\.(\d+)/);
|
|
76
|
+
if (match) {
|
|
77
|
+
const major = parseInt(match[1], 10);
|
|
78
|
+
const minor = parseInt(match[2], 10);
|
|
79
|
+
if (major === 3 && minor >= 12) {
|
|
80
|
+
return { cmd, version };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// try next
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main() {
|
|
91
|
+
// Skip if bb already installed via pip
|
|
92
|
+
if (isPipBbInstalled()) {
|
|
93
|
+
log("'bb' command found on PATH (pip) — skipping Python setup.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find suitable Python
|
|
98
|
+
const python = findPython();
|
|
99
|
+
if (!python) {
|
|
100
|
+
warn(
|
|
101
|
+
"Python 3.12+ not found. The 'bb' command won't work until you:\n" +
|
|
102
|
+
" 1. Install Python 3.12+ (https://python.org)\n" +
|
|
103
|
+
" 2. Run: pip install bumblebee-cli"
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
log(`Found ${python.version} (${python.cmd})`);
|
|
109
|
+
|
|
110
|
+
// Create .venv and install
|
|
111
|
+
const venvDir = join(pkgRoot, ".venv");
|
|
112
|
+
const isWin = process.platform === "win32";
|
|
113
|
+
const pip = isWin
|
|
114
|
+
? join(venvDir, "Scripts", "pip.exe")
|
|
115
|
+
: join(venvDir, "bin", "pip");
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (!existsSync(venvDir)) {
|
|
119
|
+
log("Creating virtual environment...");
|
|
120
|
+
execSync(`${python.cmd} -m venv "${venvDir}"`, { stdio: "pipe" });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Install from bundled python/ dir if available, otherwise from PyPI
|
|
124
|
+
if (existsSync(pythonDir)) {
|
|
125
|
+
log("Installing Bumblebee CLI from bundled source...");
|
|
126
|
+
execSync(`"${pip}" install "${pythonDir}"`, { stdio: "pipe" });
|
|
127
|
+
} else {
|
|
128
|
+
log("Installing Bumblebee CLI from PyPI...");
|
|
129
|
+
execSync(`"${pip}" install bumblebee-cli`, { stdio: "pipe" });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
log("Setup complete! Run 'bb --help' to get started.");
|
|
133
|
+
} catch (e) {
|
|
134
|
+
warn(
|
|
135
|
+
`Python setup failed: ${e.message}\n` +
|
|
136
|
+
"You can still install manually: pip install bumblebee-cli"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
main();
|
|
143
|
+
} catch {
|
|
144
|
+
// Never fail npm install
|
|
145
|
+
warn("Postinstall encountered an error, but npm install will continue.");
|
|
146
|
+
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: bumblebee-cli
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Bumblebee CLI — bb command for dev task management
|
|
5
|
-
Requires-Python: >=3.12
|
|
6
|
-
Requires-Dist: typer>=0.15.0
|
|
7
|
-
Requires-Dist: httpx>=0.28.0
|
|
8
|
-
Requires-Dist: rich>=13.9.0
|
|
9
|
-
Requires-Dist: toml>=0.10.2
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
pyproject.toml
|
|
2
|
-
src/__init__.py
|
|
3
|
-
src/api_client.py
|
|
4
|
-
src/config.py
|
|
5
|
-
src/main.py
|
|
6
|
-
src/bumblebee_cli.egg-info/PKG-INFO
|
|
7
|
-
src/bumblebee_cli.egg-info/SOURCES.txt
|
|
8
|
-
src/bumblebee_cli.egg-info/dependency_links.txt
|
|
9
|
-
src/bumblebee_cli.egg-info/entry_points.txt
|
|
10
|
-
src/bumblebee_cli.egg-info/requires.txt
|
|
11
|
-
src/bumblebee_cli.egg-info/top_level.txt
|
|
12
|
-
src/commands/__init__.py
|
|
13
|
-
src/commands/agent.py
|
|
14
|
-
src/commands/auth.py
|
|
15
|
-
src/commands/board.py
|
|
16
|
-
src/commands/comment.py
|
|
17
|
-
src/commands/label.py
|
|
18
|
-
src/commands/project.py
|
|
19
|
-
src/commands/sprint.py
|
|
20
|
-
src/commands/story.py
|
|
21
|
-
src/commands/task.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|