bumblebee-cli 0.1.1 → 0.3.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/dist/index.js +4214 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -28
- package/templates/skills/bb-agent/SKILL.md +180 -0
- package/templates/skills/bb-agent/references/bb-commands.md +124 -0
- package/templates/skills/bb-agent/references/investigate-workflow.md +150 -0
- package/templates/skills/bb-agent/references/parallel-workflow.md +105 -0
- package/templates/skills/bb-agent/references/prompts.md +144 -0
- package/templates/skills/bb-agent/references/status-transitions.md +93 -0
- package/templates/skills/bb-agent/references/workflow.md +178 -0
- package/README.md +0 -47
- package/bin/bb.mjs +0 -132
- package/python/bb_cli/__init__.py +0 -0
- package/python/bb_cli/api_client.py +0 -38
- 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/requires.txt +0 -4
- package/python/bb_cli/bumblebee_cli.egg-info/top_level.txt +0 -5
- package/python/bb_cli/commands/__init__.py +0 -0
- 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
- package/python/bb_cli/commands/agent.py +0 -1030
- package/python/bb_cli/commands/auth.py +0 -79
- package/python/bb_cli/commands/board.py +0 -47
- package/python/bb_cli/commands/comment.py +0 -34
- package/python/bb_cli/commands/init.py +0 -62
- package/python/bb_cli/commands/item.py +0 -192
- package/python/bb_cli/commands/label.py +0 -43
- package/python/bb_cli/commands/project.py +0 -111
- package/python/bb_cli/commands/sprint.py +0 -84
- package/python/bb_cli/config.py +0 -136
- package/python/bb_cli/main.py +0 -44
- package/python/pyproject.toml +0 -18
- package/scripts/build.sh +0 -20
- package/scripts/postinstall.mjs +0 -146
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich import print as rprint
|
|
3
|
-
|
|
4
|
-
from ..api_client import api_get, api_post
|
|
5
|
-
from ..config import get_api_url, get_project_path, is_local_config_active, load_config, save_config
|
|
6
|
-
|
|
7
|
-
app = typer.Typer(help="Authentication commands")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@app.command()
|
|
11
|
-
def login(
|
|
12
|
-
email: str = typer.Option(..., prompt=True),
|
|
13
|
-
password: str = typer.Option(..., prompt=True, hide_input=True),
|
|
14
|
-
):
|
|
15
|
-
"""Log in to Bumblebee API."""
|
|
16
|
-
try:
|
|
17
|
-
data = api_post("/auth/login", json={"email": email, "password": password})
|
|
18
|
-
cfg = load_config()
|
|
19
|
-
cfg["token"] = data["access_token"]
|
|
20
|
-
save_config(cfg)
|
|
21
|
-
rprint("[green]Logged in successfully.[/green]")
|
|
22
|
-
except Exception as e:
|
|
23
|
-
rprint(f"[red]Login failed: {e}[/red]")
|
|
24
|
-
raise typer.Exit(1)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@app.command()
|
|
28
|
-
def register(
|
|
29
|
-
email: str = typer.Option(..., prompt=True),
|
|
30
|
-
username: str = typer.Option(..., prompt=True),
|
|
31
|
-
password: str = typer.Option(..., prompt=True, hide_input=True, confirmation_prompt=True),
|
|
32
|
-
):
|
|
33
|
-
"""Register a new Bumblebee account."""
|
|
34
|
-
try:
|
|
35
|
-
api_post("/auth/register", json={"email": email, "username": username, "password": password})
|
|
36
|
-
rprint("[green]Registered! You can now run [bold]bb login[/bold].[/green]")
|
|
37
|
-
except Exception as e:
|
|
38
|
-
rprint(f"[red]Registration failed: {e}[/red]")
|
|
39
|
-
raise typer.Exit(1)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@app.command()
|
|
43
|
-
def logout():
|
|
44
|
-
"""Clear saved credentials."""
|
|
45
|
-
cfg = load_config()
|
|
46
|
-
cfg.pop("token", None)
|
|
47
|
-
save_config(cfg)
|
|
48
|
-
rprint("[yellow]Logged out.[/yellow]")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@app.command()
|
|
52
|
-
def whoami():
|
|
53
|
-
"""Show current user info."""
|
|
54
|
-
try:
|
|
55
|
-
user = api_get("/auth/me")
|
|
56
|
-
rprint(f"[bold]{user['username']}[/bold] ({user['email']})")
|
|
57
|
-
except Exception:
|
|
58
|
-
rprint("[red]Not logged in.[/red]")
|
|
59
|
-
raise typer.Exit(1)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@app.command(name="config")
|
|
63
|
-
def show_config():
|
|
64
|
-
"""Show current CLI configuration."""
|
|
65
|
-
cfg = load_config()
|
|
66
|
-
slug = cfg.get("current_project")
|
|
67
|
-
config_source = "local (.bumblebee/)" if is_local_config_active() else "global (~/.bumblebee/)"
|
|
68
|
-
rprint(f"Config source: {config_source}")
|
|
69
|
-
rprint(f"API URL: {get_api_url()}")
|
|
70
|
-
rprint(f"Logged in: {'Yes' if cfg.get('token') else 'No'}")
|
|
71
|
-
rprint(f"Current project: {slug or 'None'}")
|
|
72
|
-
if slug:
|
|
73
|
-
path = get_project_path(slug)
|
|
74
|
-
rprint(f"Source path: {path or '[dim]not linked[/dim]'}")
|
|
75
|
-
projects = cfg.get("projects", {})
|
|
76
|
-
if projects:
|
|
77
|
-
rprint("\n[bold]Project paths:[/bold]")
|
|
78
|
-
for name, info in projects.items():
|
|
79
|
-
rprint(f" {name} -> {info.get('path', '?')}")
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich import print as rprint
|
|
3
|
-
from rich.columns import Columns
|
|
4
|
-
from rich.panel import Panel
|
|
5
|
-
|
|
6
|
-
from ..api_client import api_get
|
|
7
|
-
from ..config import get_current_project
|
|
8
|
-
|
|
9
|
-
app = typer.Typer(help="Kanban board view")
|
|
10
|
-
|
|
11
|
-
COLUMNS = ["open", "in_progress", "in_review", "resolved", "closed"]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@app.callback(invoke_without_command=True)
|
|
15
|
-
def board(
|
|
16
|
-
type: str = typer.Option(None, "-t", "--type", help="Filter by type (story, task, etc.)"),
|
|
17
|
-
):
|
|
18
|
-
"""Show kanban board for current project work items."""
|
|
19
|
-
slug = get_current_project()
|
|
20
|
-
if not slug:
|
|
21
|
-
rprint("[red]No project selected.[/red]")
|
|
22
|
-
raise typer.Exit(1)
|
|
23
|
-
|
|
24
|
-
params = {}
|
|
25
|
-
if type:
|
|
26
|
-
params["type"] = type
|
|
27
|
-
items = api_get(f"/api/projects/{slug}/work-items", params=params)
|
|
28
|
-
|
|
29
|
-
panels = []
|
|
30
|
-
for col in COLUMNS:
|
|
31
|
-
col_items = [i for i in items if i["status"] == col]
|
|
32
|
-
content = ""
|
|
33
|
-
for i in col_items:
|
|
34
|
-
key = i.get("key") or f"#{i['number']}"
|
|
35
|
-
prio = i.get("priority", "")
|
|
36
|
-
assignee = i.get("assignee") or ""
|
|
37
|
-
content += f"[bold]{key}[/bold] [{i['type']}] {i['title']}\n"
|
|
38
|
-
if prio or assignee:
|
|
39
|
-
content += f" [dim]{prio}[/dim]"
|
|
40
|
-
if assignee:
|
|
41
|
-
content += f" -> {assignee}"
|
|
42
|
-
content += "\n"
|
|
43
|
-
if not content:
|
|
44
|
-
content = "[dim]Empty[/dim]"
|
|
45
|
-
panels.append(Panel(content.strip(), title=col.replace("_", " ").title(), expand=True))
|
|
46
|
-
|
|
47
|
-
rprint(Columns(panels, equal=True, expand=True))
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich import print as rprint
|
|
3
|
-
|
|
4
|
-
from ..api_client import api_get, api_post
|
|
5
|
-
from .item import _require_project, _resolve_item
|
|
6
|
-
|
|
7
|
-
app = typer.Typer(help="Comment management")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@app.command(name="list")
|
|
11
|
-
def list_comments(id_or_number: str = typer.Argument(..., help="Work item UUID, number, or KEY-number")):
|
|
12
|
-
"""List comments on a work item."""
|
|
13
|
-
slug = _require_project()
|
|
14
|
-
item = _resolve_item(slug, id_or_number)
|
|
15
|
-
comments = api_get(f"/api/work-items/{item['id']}/comments")
|
|
16
|
-
if not comments:
|
|
17
|
-
rprint("[dim]No comments yet.[/dim]")
|
|
18
|
-
return
|
|
19
|
-
for c in comments:
|
|
20
|
-
type_tag = f" [{c['type']}]" if c.get("type") and c["type"] != "discussion" else ""
|
|
21
|
-
rprint(f"[bold]{c['author']}[/bold]{type_tag}: {c['body']}")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@app.command()
|
|
25
|
-
def add(
|
|
26
|
-
id_or_number: str = typer.Argument(..., help="Work item UUID, number, or KEY-number"),
|
|
27
|
-
body: str = typer.Argument(...),
|
|
28
|
-
type: str = typer.Option("discussion", "-t", "--type", help="Comment type: discussion, investigation, proposal, review, agent_output"),
|
|
29
|
-
):
|
|
30
|
-
"""Add a comment to a work item."""
|
|
31
|
-
slug = _require_project()
|
|
32
|
-
item = _resolve_item(slug, id_or_number)
|
|
33
|
-
api_post(f"/api/work-items/{item['id']}/comments", json={"body": body, "type": type})
|
|
34
|
-
rprint("[green]Comment added.[/green]")
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
import toml
|
|
4
|
-
import typer
|
|
5
|
-
from rich import print as rprint
|
|
6
|
-
|
|
7
|
-
from ..config import LOCAL_CONFIG_NAME, LOCAL_DIR_NAME, find_project_root
|
|
8
|
-
|
|
9
|
-
app = typer.Typer(help="Initialize project-local config")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@app.command()
|
|
13
|
-
def init(
|
|
14
|
-
project: str = typer.Option(None, "--project", "-p", help="Set current_project in local config"),
|
|
15
|
-
api_url: str = typer.Option(None, "--api-url", help="Set api_url in local config"),
|
|
16
|
-
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
|
|
17
|
-
):
|
|
18
|
-
"""Initialize a .bumblebee/ config directory in the project root.
|
|
19
|
-
|
|
20
|
-
Looks for the project root by walking up from cwd to find .git/.
|
|
21
|
-
Falls back to cwd if no .git/ is found.
|
|
22
|
-
"""
|
|
23
|
-
root = find_project_root()
|
|
24
|
-
if root is None:
|
|
25
|
-
root = Path.cwd().resolve()
|
|
26
|
-
|
|
27
|
-
bb_dir = root / LOCAL_DIR_NAME
|
|
28
|
-
config_file = bb_dir / LOCAL_CONFIG_NAME
|
|
29
|
-
gitignore_file = bb_dir / ".gitignore"
|
|
30
|
-
|
|
31
|
-
if config_file.exists() and not force:
|
|
32
|
-
rprint(f"[yellow].bumblebee/ already exists at {root}[/yellow]")
|
|
33
|
-
rprint("[dim]Use --force to overwrite.[/dim]")
|
|
34
|
-
raise typer.Exit(1)
|
|
35
|
-
|
|
36
|
-
# Create directory
|
|
37
|
-
bb_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
-
|
|
39
|
-
# Build config
|
|
40
|
-
cfg: dict = {}
|
|
41
|
-
if project:
|
|
42
|
-
cfg["current_project"] = project
|
|
43
|
-
if api_url:
|
|
44
|
-
cfg["api_url"] = api_url
|
|
45
|
-
|
|
46
|
-
# Write config.toml
|
|
47
|
-
with open(config_file, "w") as f:
|
|
48
|
-
if cfg:
|
|
49
|
-
toml.dump(cfg, f)
|
|
50
|
-
else:
|
|
51
|
-
f.write("# Bumblebee project-local config\n# Settings here override ~/.bumblebee/config.toml\n")
|
|
52
|
-
|
|
53
|
-
# Write .gitignore to exclude local-only files
|
|
54
|
-
if not gitignore_file.exists() or force:
|
|
55
|
-
gitignore_file.write_text("*.local.toml\n")
|
|
56
|
-
|
|
57
|
-
rprint(f"[green]Initialized .bumblebee/ at {root}[/green]")
|
|
58
|
-
if project:
|
|
59
|
-
rprint(f" current_project = {project}")
|
|
60
|
-
if api_url:
|
|
61
|
-
rprint(f" api_url = {api_url}")
|
|
62
|
-
rprint("[dim]Tip: Add project-specific settings to .bumblebee/config.toml[/dim]")
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
import typer
|
|
4
|
-
from rich import print as rprint
|
|
5
|
-
from rich.table import Table
|
|
6
|
-
|
|
7
|
-
from ..api_client import api_get, api_post, api_put
|
|
8
|
-
from ..config import get_current_project
|
|
9
|
-
|
|
10
|
-
app = typer.Typer(help="Work item management")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _require_project() -> str:
|
|
14
|
-
slug = get_current_project()
|
|
15
|
-
if not slug:
|
|
16
|
-
rprint("[red]No project selected. Run [bold]bb project switch <slug>[/bold] first.[/red]")
|
|
17
|
-
raise typer.Exit(1)
|
|
18
|
-
return slug
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _resolve_item(slug: str, id_or_number: str) -> dict:
|
|
22
|
-
"""Resolve a work item by UUID, number, or KEY-number format."""
|
|
23
|
-
# UUID format
|
|
24
|
-
if len(id_or_number) > 8 and "-" in id_or_number:
|
|
25
|
-
return api_get(f"/api/work-items/{id_or_number}")
|
|
26
|
-
|
|
27
|
-
# KEY-number format (e.g. BB-42) or plain integer
|
|
28
|
-
match = re.match(r"^(?:[A-Za-z]+-)?(\d+)$", id_or_number)
|
|
29
|
-
if match:
|
|
30
|
-
number = int(match.group(1))
|
|
31
|
-
return api_get(f"/api/projects/{slug}/work-items/by-number/{number}")
|
|
32
|
-
|
|
33
|
-
rprint(f"[red]Invalid identifier: {id_or_number}. Use UUID, number, or KEY-number (e.g. BB-42).[/red]")
|
|
34
|
-
raise typer.Exit(1)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@app.command(name="list")
|
|
38
|
-
def list_items(
|
|
39
|
-
type: str = typer.Option(None, "-t", "--type", help="Filter by type (story, task, epic, bug...)"),
|
|
40
|
-
status: str = typer.Option(None, "-s", "--status", help="Filter by status"),
|
|
41
|
-
parent: str = typer.Option(None, "--parent", help="Filter by parent ID"),
|
|
42
|
-
assignee: str = typer.Option(None, "-a", "--assignee", help="Filter by assignee"),
|
|
43
|
-
):
|
|
44
|
-
"""List work items in the current project."""
|
|
45
|
-
slug = _require_project()
|
|
46
|
-
params = {}
|
|
47
|
-
if type:
|
|
48
|
-
params["type"] = type
|
|
49
|
-
if status:
|
|
50
|
-
params["status"] = status
|
|
51
|
-
if parent:
|
|
52
|
-
params["parent_id"] = parent
|
|
53
|
-
if assignee:
|
|
54
|
-
params["assignee"] = assignee
|
|
55
|
-
items = api_get(f"/api/projects/{slug}/work-items", params=params)
|
|
56
|
-
table = Table(title=f"Work Items - {slug}")
|
|
57
|
-
table.add_column("#", style="cyan", justify="right")
|
|
58
|
-
table.add_column("Type", style="magenta")
|
|
59
|
-
table.add_column("Title")
|
|
60
|
-
table.add_column("Status", style="green")
|
|
61
|
-
table.add_column("Priority")
|
|
62
|
-
table.add_column("Assignee")
|
|
63
|
-
for item in items:
|
|
64
|
-
key = item.get("key") or str(item["number"])
|
|
65
|
-
table.add_row(
|
|
66
|
-
key,
|
|
67
|
-
item["type"],
|
|
68
|
-
item["title"],
|
|
69
|
-
item["status"],
|
|
70
|
-
item["priority"],
|
|
71
|
-
item.get("assignee") or "-",
|
|
72
|
-
)
|
|
73
|
-
rprint(table)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@app.command()
|
|
77
|
-
def create(
|
|
78
|
-
title: str = typer.Argument(...),
|
|
79
|
-
type: str = typer.Option("story", "-t", "--type", help="Item type: story, task, epic, bug, feature, chore, spike"),
|
|
80
|
-
description: str = typer.Option(None, "-d", "--description"),
|
|
81
|
-
priority: str = typer.Option("medium", "-p", "--priority"),
|
|
82
|
-
parent: str = typer.Option(None, "--parent", help="Parent item ID or number"),
|
|
83
|
-
):
|
|
84
|
-
"""Create a new work item."""
|
|
85
|
-
slug = _require_project()
|
|
86
|
-
data: dict = {"title": title, "type": type, "priority": priority}
|
|
87
|
-
if description:
|
|
88
|
-
data["description"] = description
|
|
89
|
-
if parent:
|
|
90
|
-
# Resolve parent to UUID
|
|
91
|
-
parent_item = _resolve_item(slug, parent)
|
|
92
|
-
data["parent_id"] = parent_item["id"]
|
|
93
|
-
item = api_post(f"/api/projects/{slug}/work-items", json=data)
|
|
94
|
-
key = item.get("key") or f"#{item['number']}"
|
|
95
|
-
rprint(f"[green]Created {item['type']} {key}: {item['title']}[/green]")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@app.command()
|
|
99
|
-
def show(id_or_number: str = typer.Argument(..., help="UUID, number, or KEY-number (e.g. BB-42)")):
|
|
100
|
-
"""Show work item details."""
|
|
101
|
-
slug = _require_project()
|
|
102
|
-
item = _resolve_item(slug, id_or_number)
|
|
103
|
-
key = item.get("key") or f"#{item['number']}"
|
|
104
|
-
rprint(f"[bold]{key}[/bold] [{item['type']}] {item['title']}")
|
|
105
|
-
rprint(f"Status: {item['status']} | Priority: {item['priority']}")
|
|
106
|
-
if item.get("assignee"):
|
|
107
|
-
rprint(f"Assignee: {item['assignee']}")
|
|
108
|
-
if item.get("parent_id"):
|
|
109
|
-
rprint(f"Parent: {item['parent_id']}")
|
|
110
|
-
if item.get("sprint_id"):
|
|
111
|
-
rprint(f"Sprint: {item['sprint_id']}")
|
|
112
|
-
if item.get("story_points"):
|
|
113
|
-
rprint(f"Points: {item['story_points']}")
|
|
114
|
-
if item.get("description"):
|
|
115
|
-
rprint(f"\n{item['description']}")
|
|
116
|
-
if item.get("acceptance_criteria"):
|
|
117
|
-
rprint(f"\n[bold]Acceptance Criteria:[/bold]\n{item['acceptance_criteria']}")
|
|
118
|
-
if item.get("plan"):
|
|
119
|
-
rprint(f"\n[bold]Plan:[/bold]\n{item['plan']}")
|
|
120
|
-
if item.get("ai_summary"):
|
|
121
|
-
rprint(f"\n[bold]AI Summary:[/bold]\n{item['ai_summary']}")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@app.command()
|
|
125
|
-
def update(
|
|
126
|
-
id_or_number: str = typer.Argument(..., help="UUID, number, or KEY-number"),
|
|
127
|
-
status: str = typer.Option(None, "-s", "--status"),
|
|
128
|
-
assignee: str = typer.Option(None, "-a", "--assignee"),
|
|
129
|
-
priority: str = typer.Option(None, "-p", "--priority"),
|
|
130
|
-
title: str = typer.Option(None, "--title"),
|
|
131
|
-
type: str = typer.Option(None, "-t", "--type"),
|
|
132
|
-
):
|
|
133
|
-
"""Update a work item."""
|
|
134
|
-
slug = _require_project()
|
|
135
|
-
item = _resolve_item(slug, id_or_number)
|
|
136
|
-
data = {}
|
|
137
|
-
if status:
|
|
138
|
-
data["status"] = status
|
|
139
|
-
if assignee:
|
|
140
|
-
data["assignee"] = assignee
|
|
141
|
-
if priority:
|
|
142
|
-
data["priority"] = priority
|
|
143
|
-
if title:
|
|
144
|
-
data["title"] = title
|
|
145
|
-
if type:
|
|
146
|
-
data["type"] = type
|
|
147
|
-
if not data:
|
|
148
|
-
rprint("[yellow]Nothing to update.[/yellow]")
|
|
149
|
-
raise typer.Exit()
|
|
150
|
-
updated = api_put(f"/api/work-items/{item['id']}", json=data)
|
|
151
|
-
key = updated.get("key") or f"#{updated['number']}"
|
|
152
|
-
rprint(f"[green]Updated {key}[/green]")
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@app.command()
|
|
156
|
-
def assign(
|
|
157
|
-
id_or_number: str = typer.Argument(..., help="UUID, number, or KEY-number"),
|
|
158
|
-
assignee: str = typer.Argument(...),
|
|
159
|
-
):
|
|
160
|
-
"""Assign a work item to someone."""
|
|
161
|
-
slug = _require_project()
|
|
162
|
-
item = _resolve_item(slug, id_or_number)
|
|
163
|
-
updated = api_put(f"/api/work-items/{item['id']}", json={"assignee": assignee})
|
|
164
|
-
key = updated.get("key") or f"#{updated['number']}"
|
|
165
|
-
rprint(f"[green]{key} assigned to {assignee}[/green]")
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
@app.command()
|
|
169
|
-
def children(id_or_number: str = typer.Argument(..., help="UUID, number, or KEY-number")):
|
|
170
|
-
"""List children of a work item."""
|
|
171
|
-
slug = _require_project()
|
|
172
|
-
item = _resolve_item(slug, id_or_number)
|
|
173
|
-
kids = api_get(f"/api/work-items/{item['id']}/children")
|
|
174
|
-
if not kids:
|
|
175
|
-
rprint("[dim]No children.[/dim]")
|
|
176
|
-
return
|
|
177
|
-
table = Table(title=f"Children of {item.get('key') or '#' + str(item['number'])}")
|
|
178
|
-
table.add_column("#", style="cyan", justify="right")
|
|
179
|
-
table.add_column("Type", style="magenta")
|
|
180
|
-
table.add_column("Title")
|
|
181
|
-
table.add_column("Status", style="green")
|
|
182
|
-
table.add_column("Assignee")
|
|
183
|
-
for kid in kids:
|
|
184
|
-
key = kid.get("key") or str(kid["number"])
|
|
185
|
-
table.add_row(
|
|
186
|
-
key,
|
|
187
|
-
kid["type"],
|
|
188
|
-
kid["title"],
|
|
189
|
-
kid["status"],
|
|
190
|
-
kid.get("assignee") or "-",
|
|
191
|
-
)
|
|
192
|
-
rprint(table)
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich import print as rprint
|
|
3
|
-
from rich.table import Table
|
|
4
|
-
|
|
5
|
-
from ..api_client import api_get, api_post
|
|
6
|
-
from ..config import get_current_project
|
|
7
|
-
|
|
8
|
-
app = typer.Typer(help="Label management")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _require_project() -> str:
|
|
12
|
-
slug = get_current_project()
|
|
13
|
-
if not slug:
|
|
14
|
-
rprint("[red]No project selected.[/red]")
|
|
15
|
-
raise typer.Exit(1)
|
|
16
|
-
return slug
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@app.command(name="list")
|
|
20
|
-
def list_labels():
|
|
21
|
-
"""List labels in the current project."""
|
|
22
|
-
slug = _require_project()
|
|
23
|
-
labels = api_get(f"/api/projects/{slug}/labels")
|
|
24
|
-
table = Table(title=f"Labels - {slug}")
|
|
25
|
-
table.add_column("Name", style="cyan")
|
|
26
|
-
table.add_column("Color")
|
|
27
|
-
for l in labels:
|
|
28
|
-
table.add_row(l["name"], l.get("color") or "-")
|
|
29
|
-
rprint(table)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@app.command()
|
|
33
|
-
def create(
|
|
34
|
-
name: str = typer.Argument(...),
|
|
35
|
-
color: str = typer.Option(None, "-c", "--color", help="Hex color e.g. #ff0000"),
|
|
36
|
-
):
|
|
37
|
-
"""Create a label."""
|
|
38
|
-
slug = _require_project()
|
|
39
|
-
data = {"name": name}
|
|
40
|
-
if color:
|
|
41
|
-
data["color"] = color
|
|
42
|
-
label = api_post(f"/api/projects/{slug}/labels", json=data)
|
|
43
|
-
rprint(f"[green]Created label: {label['name']}[/green]")
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
import typer
|
|
4
|
-
from rich import print as rprint
|
|
5
|
-
from rich.table import Table
|
|
6
|
-
|
|
7
|
-
from ..api_client import api_get, api_post
|
|
8
|
-
from ..config import (
|
|
9
|
-
get_project_path,
|
|
10
|
-
is_local_config_active,
|
|
11
|
-
load_config,
|
|
12
|
-
load_local_config,
|
|
13
|
-
save_config,
|
|
14
|
-
save_local_config,
|
|
15
|
-
set_project_path,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
app = typer.Typer(help="Project management")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@app.command(name="list")
|
|
22
|
-
def list_projects():
|
|
23
|
-
"""List all projects."""
|
|
24
|
-
projects = api_get("/api/projects")
|
|
25
|
-
table = Table(title="Projects")
|
|
26
|
-
table.add_column("Slug", style="cyan")
|
|
27
|
-
table.add_column("Name")
|
|
28
|
-
table.add_column("Local Path")
|
|
29
|
-
table.add_column("Repo")
|
|
30
|
-
for p in projects:
|
|
31
|
-
local = get_project_path(p["slug"]) or "-"
|
|
32
|
-
table.add_row(p["slug"], p["name"], local, p.get("repo_url") or "-")
|
|
33
|
-
rprint(table)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@app.command()
|
|
37
|
-
def create(
|
|
38
|
-
name: str = typer.Argument(...),
|
|
39
|
-
slug: str = typer.Option(None, help="Project slug (defaults to name kebab-cased)"),
|
|
40
|
-
repo_url: str = typer.Option(None, help="Repository URL"),
|
|
41
|
-
):
|
|
42
|
-
"""Create a new project."""
|
|
43
|
-
if not slug:
|
|
44
|
-
slug = name.lower().replace(" ", "-")
|
|
45
|
-
project = api_post("/api/projects", json={"name": name, "slug": slug, "repo_url": repo_url})
|
|
46
|
-
rprint(f"[green]Created project [bold]{project['slug']}[/bold][/green]")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@app.command()
|
|
50
|
-
def switch(
|
|
51
|
-
slug: str = typer.Argument(...),
|
|
52
|
-
path: str = typer.Option(None, "-p", "--path", help="Local source code directory"),
|
|
53
|
-
):
|
|
54
|
-
"""Switch to a project as the active context."""
|
|
55
|
-
# Verify project exists
|
|
56
|
-
api_get(f"/api/projects/{slug}")
|
|
57
|
-
# Write to local config if .bumblebee/ exists, otherwise global
|
|
58
|
-
if is_local_config_active():
|
|
59
|
-
local_cfg = load_local_config()
|
|
60
|
-
local_cfg["current_project"] = slug
|
|
61
|
-
save_local_config(local_cfg)
|
|
62
|
-
else:
|
|
63
|
-
cfg = load_config()
|
|
64
|
-
cfg["current_project"] = slug
|
|
65
|
-
save_config(cfg)
|
|
66
|
-
if path:
|
|
67
|
-
resolved = str(Path(path).resolve())
|
|
68
|
-
set_project_path(slug, resolved)
|
|
69
|
-
rprint(f"[green]Switched to [bold]{slug}[/bold] -> {resolved}[/green]")
|
|
70
|
-
else:
|
|
71
|
-
existing = get_project_path(slug)
|
|
72
|
-
rprint(f"[green]Switched to project [bold]{slug}[/bold][/green]")
|
|
73
|
-
if not existing:
|
|
74
|
-
rprint("[dim]Tip: use [bold]bb project link <path>[/bold] to set the source code directory.[/dim]")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@app.command()
|
|
78
|
-
def link(
|
|
79
|
-
path: str = typer.Argument(".", help="Path to the source code directory (default: current dir)"),
|
|
80
|
-
):
|
|
81
|
-
"""Link current project to a local source code directory."""
|
|
82
|
-
cfg = load_config()
|
|
83
|
-
slug = cfg.get("current_project")
|
|
84
|
-
if not slug:
|
|
85
|
-
rprint("[red]No project selected. Run [bold]bb project switch <slug>[/bold] first.[/red]")
|
|
86
|
-
raise typer.Exit(1)
|
|
87
|
-
resolved = str(Path(path).resolve())
|
|
88
|
-
if not Path(resolved).is_dir():
|
|
89
|
-
rprint(f"[red]Directory not found: {resolved}[/red]")
|
|
90
|
-
raise typer.Exit(1)
|
|
91
|
-
set_project_path(slug, resolved)
|
|
92
|
-
rprint(f"[green]Linked [bold]{slug}[/bold] -> {resolved}[/green]")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@app.command()
|
|
96
|
-
def current():
|
|
97
|
-
"""Show the current active project."""
|
|
98
|
-
cfg = load_config()
|
|
99
|
-
slug = cfg.get("current_project")
|
|
100
|
-
if not slug:
|
|
101
|
-
rprint("[yellow]No project selected. Use [bold]bb project switch <slug>[/bold].[/yellow]")
|
|
102
|
-
raise typer.Exit(1)
|
|
103
|
-
project = api_get(f"/api/projects/{slug}")
|
|
104
|
-
rprint(f"[bold]{project['name']}[/bold] ({project['slug']})")
|
|
105
|
-
local = get_project_path(slug)
|
|
106
|
-
if local:
|
|
107
|
-
rprint(f"Path: {local}")
|
|
108
|
-
else:
|
|
109
|
-
rprint("[dim]Path: not linked - use [bold]bb project link <path>[/bold][/dim]")
|
|
110
|
-
if project.get("repo_url"):
|
|
111
|
-
rprint(f"Repo: {project['repo_url']}")
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich import print as rprint
|
|
3
|
-
from rich.table import Table
|
|
4
|
-
|
|
5
|
-
from ..api_client import api_get, api_post, api_put
|
|
6
|
-
from ..config import get_current_project
|
|
7
|
-
|
|
8
|
-
app = typer.Typer(help="Sprint management")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _require_project() -> str:
|
|
12
|
-
slug = get_current_project()
|
|
13
|
-
if not slug:
|
|
14
|
-
rprint("[red]No project selected. Run [bold]bb project switch <slug>[/bold] first.[/red]")
|
|
15
|
-
raise typer.Exit(1)
|
|
16
|
-
return slug
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@app.command(name="list")
|
|
20
|
-
def list_sprints():
|
|
21
|
-
"""List sprints in the current project."""
|
|
22
|
-
slug = _require_project()
|
|
23
|
-
sprints = api_get(f"/api/projects/{slug}/sprints")
|
|
24
|
-
table = Table(title=f"Sprints - {slug}")
|
|
25
|
-
table.add_column("ID", style="cyan")
|
|
26
|
-
table.add_column("Name")
|
|
27
|
-
table.add_column("Status", style="green")
|
|
28
|
-
table.add_column("Start")
|
|
29
|
-
table.add_column("End")
|
|
30
|
-
for s in sprints:
|
|
31
|
-
table.add_row(
|
|
32
|
-
s["id"][:8],
|
|
33
|
-
s["name"],
|
|
34
|
-
s["status"],
|
|
35
|
-
s.get("start_date") or "-",
|
|
36
|
-
s.get("end_date") or "-",
|
|
37
|
-
)
|
|
38
|
-
rprint(table)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@app.command()
|
|
42
|
-
def create(
|
|
43
|
-
name: str = typer.Argument(...),
|
|
44
|
-
goal: str = typer.Option(None, "-g", "--goal"),
|
|
45
|
-
):
|
|
46
|
-
"""Create a new sprint."""
|
|
47
|
-
slug = _require_project()
|
|
48
|
-
data = {"name": name}
|
|
49
|
-
if goal:
|
|
50
|
-
data["goal"] = goal
|
|
51
|
-
sprint = api_post(f"/api/projects/{slug}/sprints", json=data)
|
|
52
|
-
rprint(f"[green]Created sprint: {sprint['name']}[/green]")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@app.command()
|
|
56
|
-
def start(sprint_id: str = typer.Argument(...)):
|
|
57
|
-
"""Start a sprint (set status to active)."""
|
|
58
|
-
slug = _require_project()
|
|
59
|
-
sprint = api_post(f"/api/projects/{slug}/sprints/{sprint_id}/start")
|
|
60
|
-
rprint(f"[green]Sprint [bold]{sprint['name']}[/bold] is now active.[/green]")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@app.command()
|
|
64
|
-
def close(sprint_id: str = typer.Argument(...)):
|
|
65
|
-
"""Close an active sprint."""
|
|
66
|
-
slug = _require_project()
|
|
67
|
-
sprint = api_post(f"/api/projects/{slug}/sprints/{sprint_id}/close")
|
|
68
|
-
rprint(f"[green]Sprint [bold]{sprint['name']}[/bold] completed.[/green]")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@app.command()
|
|
72
|
-
def current():
|
|
73
|
-
"""Show the currently active sprint."""
|
|
74
|
-
slug = _require_project()
|
|
75
|
-
sprints = api_get(f"/api/projects/{slug}/sprints")
|
|
76
|
-
active = [s for s in sprints if s["status"] == "active"]
|
|
77
|
-
if not active:
|
|
78
|
-
rprint("[yellow]No active sprint.[/yellow]")
|
|
79
|
-
raise typer.Exit()
|
|
80
|
-
s = active[0]
|
|
81
|
-
rprint(f"[bold]{s['name']}[/bold] ({s['status']})")
|
|
82
|
-
if s.get("goal"):
|
|
83
|
-
rprint(f"Goal: {s['goal']}")
|
|
84
|
-
rprint(f"Period: {s.get('start_date', '?')} -> {s.get('end_date', '?')}")
|