dhub-cli 0.1.0__tar.gz
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.
- dhub_cli-0.1.0/.gitignore +22 -0
- dhub_cli-0.1.0/PKG-INFO +78 -0
- dhub_cli-0.1.0/README.md +52 -0
- dhub_cli-0.1.0/pyproject.toml +50 -0
- dhub_cli-0.1.0/src/dhub/__init__.py +1 -0
- dhub_cli-0.1.0/src/dhub/cli/__init__.py +1 -0
- dhub_cli-0.1.0/src/dhub/cli/app.py +35 -0
- dhub_cli-0.1.0/src/dhub/cli/auth.py +93 -0
- dhub_cli-0.1.0/src/dhub/cli/config.py +74 -0
- dhub_cli-0.1.0/src/dhub/cli/keys.py +96 -0
- dhub_cli-0.1.0/src/dhub/cli/org.py +125 -0
- dhub_cli-0.1.0/src/dhub/cli/registry.py +254 -0
- dhub_cli-0.1.0/src/dhub/cli/runtime.py +87 -0
- dhub_cli-0.1.0/src/dhub/cli/search.py +36 -0
- dhub_cli-0.1.0/src/dhub/core/__init__.py +1 -0
- dhub_cli-0.1.0/src/dhub/core/install.py +159 -0
- dhub_cli-0.1.0/src/dhub/core/manifest.py +221 -0
- dhub_cli-0.1.0/src/dhub/core/runtime.py +84 -0
- dhub_cli-0.1.0/src/dhub/core/validation.py +50 -0
- dhub_cli-0.1.0/src/dhub/models.py +38 -0
- dhub_cli-0.1.0/tests/__init__.py +1 -0
- dhub_cli-0.1.0/tests/conftest.py +1 -0
- dhub_cli-0.1.0/tests/test_cli/__init__.py +1 -0
- dhub_cli-0.1.0/tests/test_cli/test_auth_cli.py +98 -0
- dhub_cli-0.1.0/tests/test_cli/test_keys_cli.py +109 -0
- dhub_cli-0.1.0/tests/test_cli/test_org_cli.py +215 -0
- dhub_cli-0.1.0/tests/test_cli/test_registry_cli.py +468 -0
- dhub_cli-0.1.0/tests/test_cli/test_runtime_cli.py +181 -0
- dhub_cli-0.1.0/tests/test_cli/test_search_cli.py +90 -0
- dhub_cli-0.1.0/tests/test_core/__init__.py +1 -0
- dhub_cli-0.1.0/tests/test_core/test_docx_integration.py +271 -0
- dhub_cli-0.1.0/tests/test_core/test_install.py +52 -0
- dhub_cli-0.1.0/tests/test_core/test_install_symlinks.py +168 -0
- dhub_cli-0.1.0/tests/test_core/test_manifest.py +353 -0
- dhub_cli-0.1.0/tests/test_core/test_runtime.py +174 -0
- dhub_cli-0.1.0/tests/test_core/test_validation.py +74 -0
dhub_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dhub-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The CLI package manager for AI agent skills
|
|
5
|
+
Project-URL: Homepage, https://github.com/lfiaschi/decision-hub
|
|
6
|
+
Project-URL: Repository, https://github.com/lfiaschi/decision-hub
|
|
7
|
+
Project-URL: Issues, https://github.com/lfiaschi/decision-hub/issues
|
|
8
|
+
Author-email: Luca Fiaschi <luca.fiaschi@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
21
|
+
Requires-Dist: rich>=13.0.0
|
|
22
|
+
Requires-Dist: typer[all]>=0.12.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# dhub
|
|
28
|
+
|
|
29
|
+
The CLI package manager for AI agent skills.
|
|
30
|
+
|
|
31
|
+
`dhub` lets you publish, discover, and install **Skills** — modular capabilities (code + prompts) that agents like Claude, Cursor, and Gemini can use.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Via uv
|
|
37
|
+
uv tool install dhub
|
|
38
|
+
|
|
39
|
+
# Via pipx
|
|
40
|
+
pipx install dhub
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Authenticate via GitHub
|
|
47
|
+
dhub login
|
|
48
|
+
|
|
49
|
+
# Publish a skill
|
|
50
|
+
dhub publish --org my-org --name my-skill --version 1.0.0
|
|
51
|
+
|
|
52
|
+
# Install a skill
|
|
53
|
+
dhub install my-org/my-skill
|
|
54
|
+
|
|
55
|
+
# Search for skills
|
|
56
|
+
dhub ask "analyze A/B test results"
|
|
57
|
+
|
|
58
|
+
# Run a skill locally
|
|
59
|
+
dhub run my-org/my-skill
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---------|-------------|
|
|
66
|
+
| `dhub login` | Authenticate via GitHub Device Flow |
|
|
67
|
+
| `dhub publish` | Publish a skill to the registry |
|
|
68
|
+
| `dhub install` | Install a skill |
|
|
69
|
+
| `dhub list` | List installed skills |
|
|
70
|
+
| `dhub delete` | Delete a skill from the registry |
|
|
71
|
+
| `dhub run` | Run a locally installed skill |
|
|
72
|
+
| `dhub ask` | Natural language skill search |
|
|
73
|
+
| `dhub org` | Manage organizations |
|
|
74
|
+
| `dhub keys` | Manage API keys for evaluations |
|
|
75
|
+
|
|
76
|
+
## Documentation
|
|
77
|
+
|
|
78
|
+
See the [main repository](https://github.com/lfiaschi/decision-hub) for full documentation.
|
dhub_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# dhub
|
|
2
|
+
|
|
3
|
+
The CLI package manager for AI agent skills.
|
|
4
|
+
|
|
5
|
+
`dhub` lets you publish, discover, and install **Skills** — modular capabilities (code + prompts) that agents like Claude, Cursor, and Gemini can use.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Via uv
|
|
11
|
+
uv tool install dhub
|
|
12
|
+
|
|
13
|
+
# Via pipx
|
|
14
|
+
pipx install dhub
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Authenticate via GitHub
|
|
21
|
+
dhub login
|
|
22
|
+
|
|
23
|
+
# Publish a skill
|
|
24
|
+
dhub publish --org my-org --name my-skill --version 1.0.0
|
|
25
|
+
|
|
26
|
+
# Install a skill
|
|
27
|
+
dhub install my-org/my-skill
|
|
28
|
+
|
|
29
|
+
# Search for skills
|
|
30
|
+
dhub ask "analyze A/B test results"
|
|
31
|
+
|
|
32
|
+
# Run a skill locally
|
|
33
|
+
dhub run my-org/my-skill
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
| Command | Description |
|
|
39
|
+
|---------|-------------|
|
|
40
|
+
| `dhub login` | Authenticate via GitHub Device Flow |
|
|
41
|
+
| `dhub publish` | Publish a skill to the registry |
|
|
42
|
+
| `dhub install` | Install a skill |
|
|
43
|
+
| `dhub list` | List installed skills |
|
|
44
|
+
| `dhub delete` | Delete a skill from the registry |
|
|
45
|
+
| `dhub run` | Run a locally installed skill |
|
|
46
|
+
| `dhub ask` | Natural language skill search |
|
|
47
|
+
| `dhub org` | Manage organizations |
|
|
48
|
+
| `dhub keys` | Manage API keys for evaluations |
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
See the [main repository](https://github.com/lfiaschi/decision-hub) for full documentation.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dhub-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "The CLI package manager for AI agent skills"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Luca Fiaschi", email = "luca.fiaschi@gmail.com" },
|
|
9
|
+
]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"typer[all]>=0.12.0",
|
|
23
|
+
"rich>=13.0.0",
|
|
24
|
+
"httpx>=0.27.0",
|
|
25
|
+
"pyyaml>=6.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
dhub = "dhub.cli.app:run"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/dhub"]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/lfiaschi/decision-hub"
|
|
45
|
+
Repository = "https://github.com/lfiaschi/decision-hub"
|
|
46
|
+
Issues = "https://github.com/lfiaschi/decision-hub/issues"
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Main Typer app with subcommand registration."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(
|
|
6
|
+
name="dhub",
|
|
7
|
+
help="Decision Hub - The package manager for AI agent skills",
|
|
8
|
+
no_args_is_help=True,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Register top-level commands
|
|
12
|
+
from dhub.cli.auth import login_command # noqa: E402
|
|
13
|
+
from dhub.cli.registry import delete_command, install_command, list_command, publish_command # noqa: E402
|
|
14
|
+
from dhub.cli.runtime import run_command # noqa: E402
|
|
15
|
+
from dhub.cli.search import ask_command # noqa: E402
|
|
16
|
+
|
|
17
|
+
app.command("login")(login_command)
|
|
18
|
+
app.command("publish")(publish_command)
|
|
19
|
+
app.command("install")(install_command)
|
|
20
|
+
app.command("list")(list_command)
|
|
21
|
+
app.command("delete")(delete_command)
|
|
22
|
+
app.command("run")(run_command)
|
|
23
|
+
app.command("ask")(ask_command)
|
|
24
|
+
|
|
25
|
+
# Register subcommand groups
|
|
26
|
+
from dhub.cli.keys import keys_app # noqa: E402
|
|
27
|
+
from dhub.cli.org import org_app # noqa: E402
|
|
28
|
+
|
|
29
|
+
app.add_typer(org_app, name="org")
|
|
30
|
+
app.add_typer(keys_app, name="keys")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run() -> None:
|
|
34
|
+
"""Entry point for the dhub CLI."""
|
|
35
|
+
app()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Login via GitHub Device Flow."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def login_command(
|
|
14
|
+
api_url: str = typer.Option(None, "--api-url", help="API URL override"),
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Authenticate with Decision Hub via GitHub."""
|
|
17
|
+
from dhub.cli.config import CliConfig, get_api_url, save_config
|
|
18
|
+
|
|
19
|
+
base_url = api_url or get_api_url()
|
|
20
|
+
|
|
21
|
+
# Step 1: Request a device code from the API
|
|
22
|
+
with httpx.Client() as client:
|
|
23
|
+
resp = client.post(f"{base_url}/auth/github/code")
|
|
24
|
+
resp.raise_for_status()
|
|
25
|
+
data = resp.json()
|
|
26
|
+
|
|
27
|
+
device_code: str = data["device_code"]
|
|
28
|
+
user_code: str = data["user_code"]
|
|
29
|
+
verification_uri: str = data["verification_uri"]
|
|
30
|
+
poll_interval: int = data.get("interval", 5)
|
|
31
|
+
|
|
32
|
+
# Step 2: Show the user code and URL
|
|
33
|
+
console.print(
|
|
34
|
+
Panel(
|
|
35
|
+
f"Open [bold blue]{verification_uri}[/] and enter code: "
|
|
36
|
+
f"[bold green]{user_code}[/]",
|
|
37
|
+
title="GitHub Login",
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
console.print("Waiting for authorization...")
|
|
41
|
+
|
|
42
|
+
# Step 3: Poll for the token until the user completes the flow
|
|
43
|
+
token_data = _poll_for_token(base_url, device_code, poll_interval)
|
|
44
|
+
|
|
45
|
+
# Step 4: Persist the token
|
|
46
|
+
new_config = CliConfig(api_url=base_url, token=token_data["access_token"])
|
|
47
|
+
save_config(new_config)
|
|
48
|
+
|
|
49
|
+
console.print(f"[green]Authenticated as @{token_data['username']}[/]")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _poll_for_token(
|
|
53
|
+
base_url: str,
|
|
54
|
+
device_code: str,
|
|
55
|
+
interval: int,
|
|
56
|
+
timeout_seconds: int = 300,
|
|
57
|
+
) -> dict:
|
|
58
|
+
"""Poll the token endpoint until authorization succeeds or times out.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
base_url: API base URL.
|
|
62
|
+
device_code: The device code returned from the code request.
|
|
63
|
+
interval: Seconds to wait between poll attempts.
|
|
64
|
+
timeout_seconds: Maximum total seconds to wait before giving up.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Parsed JSON response containing 'access_token' and 'username'.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
typer.Exit: If the flow times out or the server rejects the request.
|
|
71
|
+
"""
|
|
72
|
+
deadline = time.monotonic() + timeout_seconds
|
|
73
|
+
|
|
74
|
+
with httpx.Client(timeout=30) as client:
|
|
75
|
+
while time.monotonic() < deadline:
|
|
76
|
+
resp = client.post(
|
|
77
|
+
f"{base_url}/auth/github/token",
|
|
78
|
+
json={"device_code": device_code},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if resp.status_code == 200:
|
|
82
|
+
return resp.json()
|
|
83
|
+
|
|
84
|
+
# 428 means "authorization_pending" -- keep polling
|
|
85
|
+
if resp.status_code == 428:
|
|
86
|
+
time.sleep(interval)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# Any other error is fatal
|
|
90
|
+
resp.raise_for_status()
|
|
91
|
+
|
|
92
|
+
console.print("[red]Error: Login timed out. Please try again.[/]")
|
|
93
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""CLI configuration file management for ~/.dhub/config.json."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
CONFIG_DIR = Path.home() / ".dhub"
|
|
11
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
12
|
+
DEFAULT_API_URL = "https://decision-hub--api.modal.run"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class CliConfig:
|
|
17
|
+
"""Immutable CLI configuration."""
|
|
18
|
+
|
|
19
|
+
api_url: str = DEFAULT_API_URL
|
|
20
|
+
token: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_config() -> CliConfig:
|
|
24
|
+
"""Load CLI config from ~/.dhub/config.json.
|
|
25
|
+
|
|
26
|
+
Returns defaults if the file does not exist or contains
|
|
27
|
+
incomplete data.
|
|
28
|
+
"""
|
|
29
|
+
if not CONFIG_FILE.exists():
|
|
30
|
+
return CliConfig()
|
|
31
|
+
|
|
32
|
+
raw = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
33
|
+
return CliConfig(
|
|
34
|
+
api_url=raw.get("api_url", DEFAULT_API_URL),
|
|
35
|
+
token=raw.get("token"),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def save_config(config: CliConfig) -> None:
|
|
40
|
+
"""Save CLI config to ~/.dhub/config.json.
|
|
41
|
+
|
|
42
|
+
Creates the ~/.dhub directory if it does not already exist.
|
|
43
|
+
"""
|
|
44
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
CONFIG_FILE.write_text(
|
|
46
|
+
json.dumps(asdict(config), indent=2) + "\n",
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_api_url() -> str:
|
|
52
|
+
"""Get API URL from the DHUB_API_URL env var, falling back to saved config."""
|
|
53
|
+
env_url = os.environ.get("DHUB_API_URL")
|
|
54
|
+
if env_url:
|
|
55
|
+
return env_url
|
|
56
|
+
return load_config().api_url
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_token() -> str:
|
|
60
|
+
"""Get the stored auth token.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
typer.Exit: If no token is stored (user not logged in).
|
|
64
|
+
"""
|
|
65
|
+
from rich.console import Console
|
|
66
|
+
|
|
67
|
+
token = load_config().token
|
|
68
|
+
if not token:
|
|
69
|
+
console = Console(stderr=True)
|
|
70
|
+
console.print(
|
|
71
|
+
"[red]Error: Not logged in. Run [bold]dhub login[/bold] first.[/]"
|
|
72
|
+
)
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
return token
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""API key management commands."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
keys_app = typer.Typer(help="Manage API keys for agent evals", no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _headers() -> dict[str, str]:
|
|
13
|
+
"""Build authorization headers using the stored token."""
|
|
14
|
+
from dhub.cli.config import get_token
|
|
15
|
+
|
|
16
|
+
return {"Authorization": f"Bearer {get_token()}"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _api_url() -> str:
|
|
20
|
+
"""Retrieve the configured API URL."""
|
|
21
|
+
from dhub.cli.config import get_api_url
|
|
22
|
+
|
|
23
|
+
return get_api_url()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@keys_app.command("add")
|
|
27
|
+
def add_key(
|
|
28
|
+
key_name: str = typer.Argument(help="Name for the API key"),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Add an API key (prompts for the value securely)."""
|
|
31
|
+
key_value = typer.prompt("Enter API key value", hide_input=True)
|
|
32
|
+
|
|
33
|
+
if not key_value.strip():
|
|
34
|
+
console.print("[red]Error: Key value cannot be empty.[/]")
|
|
35
|
+
raise typer.Exit(1)
|
|
36
|
+
|
|
37
|
+
with httpx.Client() as client:
|
|
38
|
+
resp = client.post(
|
|
39
|
+
f"{_api_url()}/v1/keys",
|
|
40
|
+
headers=_headers(),
|
|
41
|
+
json={"key_name": key_name, "value": key_value},
|
|
42
|
+
)
|
|
43
|
+
if resp.status_code == 409:
|
|
44
|
+
console.print(
|
|
45
|
+
f"[red]Error: Key '{key_name}' already exists. "
|
|
46
|
+
"Remove it first with [bold]dhub keys remove[/bold].[/]"
|
|
47
|
+
)
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
resp.raise_for_status()
|
|
50
|
+
|
|
51
|
+
console.print(f"[green]Added key: {key_name}[/]")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@keys_app.command("list")
|
|
55
|
+
def list_keys() -> None:
|
|
56
|
+
"""List stored API key names."""
|
|
57
|
+
with httpx.Client() as client:
|
|
58
|
+
resp = client.get(
|
|
59
|
+
f"{_api_url()}/v1/keys",
|
|
60
|
+
headers=_headers(),
|
|
61
|
+
)
|
|
62
|
+
resp.raise_for_status()
|
|
63
|
+
keys = resp.json()
|
|
64
|
+
|
|
65
|
+
if not keys:
|
|
66
|
+
console.print("No API keys stored.")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
table = Table(title="API Keys")
|
|
70
|
+
table.add_column("Name", style="cyan")
|
|
71
|
+
table.add_column("Created", style="dim")
|
|
72
|
+
|
|
73
|
+
for key in keys:
|
|
74
|
+
table.add_row(key.get("key_name", ""), key.get("created_at", ""))
|
|
75
|
+
|
|
76
|
+
console.print(table)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@keys_app.command("remove")
|
|
80
|
+
def remove_key(
|
|
81
|
+
key_name: str = typer.Argument(help="Name of the API key to remove"),
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Remove a stored API key."""
|
|
84
|
+
with httpx.Client() as client:
|
|
85
|
+
resp = client.delete(
|
|
86
|
+
f"{_api_url()}/v1/keys/{key_name}",
|
|
87
|
+
headers=_headers(),
|
|
88
|
+
)
|
|
89
|
+
if resp.status_code == 404:
|
|
90
|
+
console.print(
|
|
91
|
+
f"[red]Error: Key '{key_name}' not found.[/]"
|
|
92
|
+
)
|
|
93
|
+
raise typer.Exit(1)
|
|
94
|
+
resp.raise_for_status()
|
|
95
|
+
|
|
96
|
+
console.print(f"[green]Removed key: {key_name}[/]")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Organization management commands."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
org_app = typer.Typer(help="Manage organizations", no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _headers() -> dict[str, str]:
|
|
13
|
+
"""Build authorization headers using the stored token."""
|
|
14
|
+
from dhub.cli.config import get_token
|
|
15
|
+
|
|
16
|
+
return {"Authorization": f"Bearer {get_token()}"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _api_url() -> str:
|
|
20
|
+
"""Retrieve the configured API URL."""
|
|
21
|
+
from dhub.cli.config import get_api_url
|
|
22
|
+
|
|
23
|
+
return get_api_url()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@org_app.command("create")
|
|
27
|
+
def create_org(slug: str = typer.Argument(help="Organization slug")) -> None:
|
|
28
|
+
"""Create a new organization."""
|
|
29
|
+
with httpx.Client() as client:
|
|
30
|
+
resp = client.post(
|
|
31
|
+
f"{_api_url()}/v1/orgs",
|
|
32
|
+
headers=_headers(),
|
|
33
|
+
json={"slug": slug},
|
|
34
|
+
)
|
|
35
|
+
if resp.status_code == 409:
|
|
36
|
+
console.print(
|
|
37
|
+
f"[red]Error: Organization '{slug}' already exists.[/]"
|
|
38
|
+
)
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
resp.raise_for_status()
|
|
41
|
+
data = resp.json()
|
|
42
|
+
|
|
43
|
+
console.print(f"[green]Created organization: {data['slug']}[/]")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@org_app.command("list")
|
|
47
|
+
def list_orgs() -> None:
|
|
48
|
+
"""List organizations you belong to."""
|
|
49
|
+
with httpx.Client() as client:
|
|
50
|
+
resp = client.get(
|
|
51
|
+
f"{_api_url()}/v1/orgs",
|
|
52
|
+
headers=_headers(),
|
|
53
|
+
)
|
|
54
|
+
resp.raise_for_status()
|
|
55
|
+
orgs = resp.json()
|
|
56
|
+
|
|
57
|
+
if not orgs:
|
|
58
|
+
console.print("You are not a member of any organizations.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
table = Table(title="Organizations")
|
|
62
|
+
table.add_column("Slug", style="cyan")
|
|
63
|
+
table.add_column("Role", style="green")
|
|
64
|
+
|
|
65
|
+
for org in orgs:
|
|
66
|
+
table.add_row(org.get("slug", ""), org.get("role", ""))
|
|
67
|
+
|
|
68
|
+
console.print(table)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@org_app.command("invite")
|
|
72
|
+
def invite_member(
|
|
73
|
+
org: str = typer.Argument(help="Organization slug"),
|
|
74
|
+
user: str = typer.Option(..., "--user", help="GitHub username to invite"),
|
|
75
|
+
role: str = typer.Option(
|
|
76
|
+
"member", "--role", help="Role: owner, admin, or member"
|
|
77
|
+
),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Invite a user to an organization."""
|
|
80
|
+
with httpx.Client() as client:
|
|
81
|
+
resp = client.post(
|
|
82
|
+
f"{_api_url()}/v1/orgs/{org}/invites",
|
|
83
|
+
headers=_headers(),
|
|
84
|
+
json={"invitee_github_username": user, "role": role},
|
|
85
|
+
)
|
|
86
|
+
if resp.status_code == 404:
|
|
87
|
+
console.print(
|
|
88
|
+
f"[red]Error: Organization '{org}' not found.[/]"
|
|
89
|
+
)
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
if resp.status_code == 403:
|
|
92
|
+
console.print(
|
|
93
|
+
"[red]Error: You do not have permission to invite members.[/]"
|
|
94
|
+
)
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
resp.raise_for_status()
|
|
97
|
+
data = resp.json()
|
|
98
|
+
|
|
99
|
+
console.print(
|
|
100
|
+
f"[green]Invited @{user} to '{org}' as {role}. "
|
|
101
|
+
f"Invite ID: {data['id']}[/]"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@org_app.command("accept")
|
|
106
|
+
def accept_invite(
|
|
107
|
+
invite_id: str = typer.Argument(help="Invite ID to accept"),
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Accept an organization invite."""
|
|
110
|
+
with httpx.Client() as client:
|
|
111
|
+
resp = client.post(
|
|
112
|
+
f"{_api_url()}/v1/invites/{invite_id}/accept",
|
|
113
|
+
headers=_headers(),
|
|
114
|
+
)
|
|
115
|
+
if resp.status_code == 404:
|
|
116
|
+
console.print(
|
|
117
|
+
f"[red]Error: Invite '{invite_id}' not found.[/]"
|
|
118
|
+
)
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
resp.raise_for_status()
|
|
121
|
+
data = resp.json()
|
|
122
|
+
|
|
123
|
+
console.print(
|
|
124
|
+
f"[green]Accepted invite. You are now a member of '{data['org_slug']}'.[/]"
|
|
125
|
+
)
|