codeberg-cli 0.1.0__tar.gz → 0.2.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.
Files changed (84) hide show
  1. codeberg_cli-0.2.0/PKG-INFO +118 -0
  2. codeberg_cli-0.2.0/README.md +93 -0
  3. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/pyproject.toml +5 -2
  4. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/__main__.py +1 -1
  5. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/client.py +20 -5
  6. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/git.py +18 -7
  7. codeberg_cli-0.2.0/src/codeberg_cli/helpers.py +45 -0
  8. codeberg_cli-0.2.0/src/codeberg_cli/routes/__init__.py +9 -0
  9. codeberg_cli-0.2.0/src/codeberg_cli/routes/auth/__init__.py +6 -0
  10. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/auth/login.py +0 -2
  11. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/auth/status.py +0 -2
  12. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/auth/whoami.py +0 -2
  13. codeberg_cli-0.2.0/src/codeberg_cli/routes/issue/__init__.py +6 -0
  14. codeberg_cli-0.2.0/src/codeberg_cli/routes/issue/comment.py +41 -0
  15. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/issue/create.py +2 -2
  16. codeberg_cli-0.2.0/src/codeberg_cli/routes/issue/edit.py +38 -0
  17. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/issue/view.py +2 -2
  18. codeberg_cli-0.2.0/src/codeberg_cli/routes/label/__init__.py +6 -0
  19. codeberg_cli-0.2.0/src/codeberg_cli/routes/milestone/__init__.py +6 -0
  20. codeberg_cli-0.2.0/src/codeberg_cli/routes/notification/__init__.py +6 -0
  21. codeberg_cli-0.2.0/src/codeberg_cli/routes/notification/list.py +38 -0
  22. codeberg_cli-0.2.0/src/codeberg_cli/routes/pr/__init__.py +6 -0
  23. codeberg_cli-0.2.0/src/codeberg_cli/routes/pr/comment.py +41 -0
  24. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/pr/create.py +2 -2
  25. codeberg_cli-0.2.0/src/codeberg_cli/routes/pr/edit.py +38 -0
  26. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/pr/view.py +2 -2
  27. codeberg_cli-0.2.0/src/codeberg_cli/routes/release/__init__.py +6 -0
  28. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/release/create.py +2 -2
  29. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/release/upload.py +1 -1
  30. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/release/view.py +2 -2
  31. codeberg_cli-0.2.0/src/codeberg_cli/routes/repo/__init__.py +6 -0
  32. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/repo/clone.py +3 -2
  33. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/repo/create.py +5 -4
  34. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/repo/fork.py +1 -1
  35. codeberg_cli-0.2.0/src/codeberg_cli/routes/repo/star.py +25 -0
  36. codeberg_cli-0.2.0/src/codeberg_cli/routes/repo/unstar.py +25 -0
  37. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/repo/view.py +4 -3
  38. codeberg_cli-0.2.0/src/codeberg_cli/routes/user/__init__.py +6 -0
  39. codeberg_cli-0.2.0/src/codeberg_cli/routes/user/view.py +26 -0
  40. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/tests/test_issue.py +3 -1
  41. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/tests/test_pr.py +3 -1
  42. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/tests/test_repo.py +3 -1
  43. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/uv.lock +27 -7
  44. codeberg_cli-0.1.0/PKG-INFO +0 -182
  45. codeberg_cli-0.1.0/README.md +0 -157
  46. codeberg_cli-0.1.0/src/codeberg_cli/helpers.py +0 -20
  47. codeberg_cli-0.1.0/src/codeberg_cli/routes/__init__.py +0 -6
  48. codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/__init__.py +0 -1
  49. codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/__init__.py +0 -1
  50. codeberg_cli-0.1.0/src/codeberg_cli/routes/label/__init__.py +0 -1
  51. codeberg_cli-0.1.0/src/codeberg_cli/routes/milestone/__init__.py +0 -1
  52. codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/__init__.py +0 -1
  53. codeberg_cli-0.1.0/src/codeberg_cli/routes/release/__init__.py +0 -1
  54. codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/__init__.py +0 -1
  55. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/.github/workflows/ci.yml +0 -0
  56. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/.github/workflows/release.yml +0 -0
  57. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/.gitignore +0 -0
  58. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/.python-version +0 -0
  59. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/Justfile +0 -0
  60. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/LICENSE +0 -0
  61. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/config.py +0 -0
  62. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/api.py +0 -0
  63. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/auth/logout.py +0 -0
  64. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/issue/close.py +0 -0
  65. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/issue/list.py +0 -0
  66. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/issue/reopen.py +0 -0
  67. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/label/create.py +0 -0
  68. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/label/delete.py +0 -0
  69. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/label/list.py +0 -0
  70. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/milestone/create.py +0 -0
  71. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/milestone/list.py +0 -0
  72. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/pr/checkout.py +0 -0
  73. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/pr/close.py +0 -0
  74. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/pr/list.py +0 -0
  75. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/pr/merge.py +0 -0
  76. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/release/list.py +0 -0
  77. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/repo/delete.py +0 -0
  78. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/src/codeberg_cli/routes/repo/list.py +0 -0
  79. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/tests/test_api.py +0 -0
  80. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/tests/test_auth.py +0 -0
  81. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/tests/test_client.py +0 -0
  82. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/tests/test_config.py +0 -0
  83. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/tests/test_git.py +0 -0
  84. {codeberg_cli-0.1.0 → codeberg_cli-0.2.0}/tests/test_release.py +0 -0
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: codeberg-cli
3
+ Version: 0.2.0
4
+ Summary: A Forgejo CLI — works with Codeberg and any Forgejo instance
5
+ Project-URL: Homepage, https://codeberg.org/ThatXliner/codeberg-cli
6
+ Project-URL: Repository, https://codeberg.org/ThatXliner/codeberg-cli
7
+ Project-URL: Issues, https://codeberg.org/ThatXliner/codeberg-cli/issues
8
+ Author-email: Bryan Hu <thatxliner@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,codeberg,forgejo
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Version Control :: Git
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: httpx
21
+ Requires-Dist: platformdirs
22
+ Requires-Dist: tomlkit
23
+ Requires-Dist: xclif>=0.4.3
24
+ Description-Content-Type: text/markdown
25
+
26
+ # cb — A Codeberg CLI
27
+
28
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/codeberg-cli)](https://pypi.org/project/codeberg-cli)
29
+ [![PyPI](https://img.shields.io/pypi/v/codeberg-cli)](https://pypi.org/project/codeberg-cli)
30
+ [![PyPI - License](https://img.shields.io/pypi/l/codeberg-cli)](#license)
31
+
32
+ `cb` is a CLI for [Codeberg](https://codeberg.org) (a [Forgejo](https://forgejo.org) instance) — think `gh` for Codeberg. It also works with any Forgejo instance. Built with [Xclif](https://xclif.readthedocs.io).
33
+
34
+ ```text
35
+ # One-time setup
36
+ cb auth login
37
+
38
+ # Work with repos, issues, PRs, releases
39
+ cb repo list
40
+ cb issue create --title "Fix the thing"
41
+ cb pr create --base main --head fix
42
+ cb release create v0.2.0
43
+ ```
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install codeberg-cli # or: uv tool install codeberg-cli
49
+ ```
50
+
51
+ Or from source:
52
+
53
+ ```bash
54
+ git clone https://codeberg.org/ThatXliner/codeberg-cli.git
55
+ cd cb
56
+ uv tool install .
57
+ ```
58
+
59
+ ## Quickstart
60
+
61
+ ```bash
62
+ # Authenticate (tokens at https://codeberg.org/user/settings/applications)
63
+ cb auth login
64
+
65
+ # Who am I?
66
+ cb auth whoami
67
+
68
+ # List your repos
69
+ cb repo list
70
+
71
+ # Clone one
72
+ cb repo clone ThatXliner/cb
73
+
74
+ # Open an issue
75
+ cb issue create --title "suggestion" --body "what about..."
76
+
77
+ # See everything you can do
78
+ cb --help
79
+ ```
80
+
81
+ ## Config
82
+
83
+ Token stored in `$XDG_CONFIG_HOME/codeberg-cli/config.toml` (managed by `cb auth login` / `cb auth logout`).
84
+
85
+ View or change config:
86
+
87
+ ```bash
88
+ cb config path # Show config file location
89
+ cb config get # Print all config values
90
+ cb config set base_url "https://codeberg.org/api/v1" # Codeberg (default)
91
+ cb config set base_url "https://git.example.com/api/v1" # Self-hosted Forgejo
92
+ ```
93
+
94
+ ## Comparison
95
+
96
+ | | **cb** | **fj** (forgejo-cli) | **berg** (codeberg-cli) |
97
+ |---|---|---|---|
98
+ | Language | Python | Rust | Rust |
99
+ | Multi-instance | Yes (configurable via `cb config set base_url`) | Any Forgejo instance | Yes |
100
+ | Issues | create, list, view, close, reopen, comment, edit | open, edit, comment, close | yes |
101
+ | Pull requests | create, list, view, merge, checkout, close, comment, edit | create, merge | yes |
102
+ | Releases | create, list, view, upload | publish | — |
103
+ | Repos | create, list, clone, view, fork, delete, star, unstar | create, edit, star, watch | yes |
104
+ | Labels | create, list, delete | — | yes |
105
+ | Milestones | create, list | — | yes |
106
+ | Notifications | list | — | yes |
107
+ | User profiles | view | search, keys | — |
108
+ | Raw API | `cb api GET /path` | — | — |
109
+ | Config management | `cb config get/set/path` | — | `berg config` |
110
+ | AGit PRs (no-fork) | — | yes | — |
111
+ | Org/team mgmt | — | yes | — |
112
+ | Install | `pip install codeberg-cli` | prebuilt binaries | `cargo install codeberg-cli` |
113
+
114
+ **Choose `fj`** if you self-host Forgejo or need org/team management or Actions workflows. **Choose `berg`** if you want your tools to be written in Rust. **Choose `cb`** if you want a minimal, readable Python CLI — it's the only one that uploads release assets, the only one with a raw API command, and the most fully-featured for everyday issue/PR/repo workflows. We will be actively working on making ours the most feature complete, just open an issue!
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,93 @@
1
+ # cb — A Codeberg CLI
2
+
3
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/codeberg-cli)](https://pypi.org/project/codeberg-cli)
4
+ [![PyPI](https://img.shields.io/pypi/v/codeberg-cli)](https://pypi.org/project/codeberg-cli)
5
+ [![PyPI - License](https://img.shields.io/pypi/l/codeberg-cli)](#license)
6
+
7
+ `cb` is a CLI for [Codeberg](https://codeberg.org) (a [Forgejo](https://forgejo.org) instance) — think `gh` for Codeberg. It also works with any Forgejo instance. Built with [Xclif](https://xclif.readthedocs.io).
8
+
9
+ ```text
10
+ # One-time setup
11
+ cb auth login
12
+
13
+ # Work with repos, issues, PRs, releases
14
+ cb repo list
15
+ cb issue create --title "Fix the thing"
16
+ cb pr create --base main --head fix
17
+ cb release create v0.2.0
18
+ ```
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install codeberg-cli # or: uv tool install codeberg-cli
24
+ ```
25
+
26
+ Or from source:
27
+
28
+ ```bash
29
+ git clone https://codeberg.org/ThatXliner/codeberg-cli.git
30
+ cd cb
31
+ uv tool install .
32
+ ```
33
+
34
+ ## Quickstart
35
+
36
+ ```bash
37
+ # Authenticate (tokens at https://codeberg.org/user/settings/applications)
38
+ cb auth login
39
+
40
+ # Who am I?
41
+ cb auth whoami
42
+
43
+ # List your repos
44
+ cb repo list
45
+
46
+ # Clone one
47
+ cb repo clone ThatXliner/cb
48
+
49
+ # Open an issue
50
+ cb issue create --title "suggestion" --body "what about..."
51
+
52
+ # See everything you can do
53
+ cb --help
54
+ ```
55
+
56
+ ## Config
57
+
58
+ Token stored in `$XDG_CONFIG_HOME/codeberg-cli/config.toml` (managed by `cb auth login` / `cb auth logout`).
59
+
60
+ View or change config:
61
+
62
+ ```bash
63
+ cb config path # Show config file location
64
+ cb config get # Print all config values
65
+ cb config set base_url "https://codeberg.org/api/v1" # Codeberg (default)
66
+ cb config set base_url "https://git.example.com/api/v1" # Self-hosted Forgejo
67
+ ```
68
+
69
+ ## Comparison
70
+
71
+ | | **cb** | **fj** (forgejo-cli) | **berg** (codeberg-cli) |
72
+ |---|---|---|---|
73
+ | Language | Python | Rust | Rust |
74
+ | Multi-instance | Yes (configurable via `cb config set base_url`) | Any Forgejo instance | Yes |
75
+ | Issues | create, list, view, close, reopen, comment, edit | open, edit, comment, close | yes |
76
+ | Pull requests | create, list, view, merge, checkout, close, comment, edit | create, merge | yes |
77
+ | Releases | create, list, view, upload | publish | — |
78
+ | Repos | create, list, clone, view, fork, delete, star, unstar | create, edit, star, watch | yes |
79
+ | Labels | create, list, delete | — | yes |
80
+ | Milestones | create, list | — | yes |
81
+ | Notifications | list | — | yes |
82
+ | User profiles | view | search, keys | — |
83
+ | Raw API | `cb api GET /path` | — | — |
84
+ | Config management | `cb config get/set/path` | — | `berg config` |
85
+ | AGit PRs (no-fork) | — | yes | — |
86
+ | Org/team mgmt | — | yes | — |
87
+ | Install | `pip install codeberg-cli` | prebuilt binaries | `cargo install codeberg-cli` |
88
+
89
+ **Choose `fj`** if you self-host Forgejo or need org/team management or Actions workflows. **Choose `berg`** if you want your tools to be written in Rust. **Choose `cb`** if you want a minimal, readable Python CLI — it's the only one that uploads release assets, the only one with a raw API command, and the most fully-featured for everyday issue/PR/repo workflows. We will be actively working on making ours the most feature complete, just open an issue!
90
+
91
+ ## License
92
+
93
+ MIT
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "codeberg-cli"
3
- version = "0.1.0"
4
- description = "A Codeberg CLI"
3
+ version = "0.2.0"
4
+ description = "A Forgejo CLI — works with Codeberg and any Forgejo instance"
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  requires-python = ">=3.12"
@@ -37,6 +37,9 @@ cb = "codeberg_cli.__main__:cli"
37
37
  requires = ["hatchling"]
38
38
  build-backend = "hatchling.build"
39
39
 
40
+ [tool.uv.sources]
41
+ xclif = { path = "../xclif" }
42
+
40
43
  [tool.hatch.build.targets.wheel]
41
44
  packages = ["src/codeberg_cli"]
42
45
 
@@ -2,7 +2,7 @@ from xclif import Cli
2
2
 
3
3
  from . import routes
4
4
 
5
- cli = Cli.from_routes(routes)
5
+ cli = Cli.from_routes(routes, config_name="codeberg-cli")
6
6
 
7
7
  if __name__ == "__main__":
8
8
  cli()
@@ -2,7 +2,7 @@ import httpx
2
2
  from rich.status import Status
3
3
  from xclif.context import get_context
4
4
 
5
- BASE_URL = "https://codeberg.org/api/v1"
5
+ DEFAULT_BASE_URL = "https://codeberg.org"
6
6
 
7
7
  _VERBS = {
8
8
  "GET": "Fetching",
@@ -19,7 +19,7 @@ class ClientError(RuntimeError):
19
19
  class Client:
20
20
  """HTTP client for the Codeberg API."""
21
21
 
22
- def __init__(self, token: str | None = None) -> None:
22
+ def __init__(self, token: str | None = None, base_url: str | None = None) -> None:
23
23
  headers = {
24
24
  "Accept": "application/json",
25
25
  "User-Agent": "codeberg-cli/0.1.0",
@@ -27,7 +27,18 @@ class Client:
27
27
  }
28
28
  if token:
29
29
  headers["Authorization"] = f"Bearer {token}"
30
- self._client = httpx.Client(base_url=BASE_URL, headers=headers, timeout=30.0)
30
+ raw = base_url or DEFAULT_BASE_URL
31
+ if "://" not in raw:
32
+ raw = "https://" + raw
33
+ raw = raw.rstrip("/")
34
+ if not raw.endswith("/api/v1"):
35
+ raw += "/api/v1"
36
+ self._client = httpx.Client(base_url=raw, headers=headers, timeout=30.0)
37
+
38
+ @property
39
+ def base_url(self) -> str:
40
+ """The API base URL used by this client."""
41
+ return str(self._client.base_url)
31
42
 
32
43
  def get(
33
44
  self, path: str, params: dict | None = None, action: str | None = None
@@ -50,10 +61,14 @@ class Client:
50
61
  def _request(
51
62
  self, method: str, path: str, action: str | None = None, **kwargs
52
63
  ) -> dict | list | None:
53
- ctx = get_context()
64
+ try:
65
+ ctx = get_context()
66
+ verbosity = ctx.verbosity
67
+ except RuntimeError:
68
+ verbosity = 0
54
69
  label = action or (
55
70
  f"{method} {path}"
56
- if ctx.verbosity >= 2
71
+ if verbosity >= 2
57
72
  else f"{_VERBS.get(method, method)} {path.split('/')[-1]}"
58
73
  )
59
74
  with Status(f"{label}..."):
@@ -1,16 +1,27 @@
1
- import re
2
1
  import subprocess
3
2
  from pathlib import Path
3
+ from urllib.parse import urlparse
4
4
 
5
5
 
6
6
  def parse_repo_from_remote(remote_url: str) -> str:
7
7
  """Extract owner/repo from a git remote URL."""
8
- # HTTPS: https://codeberg.org/owner/repo.git
9
- # SSH: git@codeberg.org:owner/repo.git
10
- m = re.search(r"codeberg\.org[:/](.+?)(?:\.git)?/?$", remote_url)
11
- if m:
12
- return m.group(1)
13
- raise ValueError(f"Could not parse repo from remote: {remote_url}")
8
+ # HTTPS: https://hostname/owner/repo.git
9
+ parsed = urlparse(remote_url)
10
+ if parsed.scheme in ("http", "https"):
11
+ path = parsed.path
12
+ elif "@" in remote_url:
13
+ # SSH: git@hostname:owner/repo.git
14
+ path = remote_url.split(":", 1)[1]
15
+ else:
16
+ raise ValueError(f"Could not parse repo from remote: {remote_url}")
17
+
18
+ path = path.rstrip("/")
19
+ if path.endswith(".git"):
20
+ path = path[:-4]
21
+ owner_repo = path.removeprefix("/")
22
+ if "/" not in owner_repo:
23
+ raise ValueError(f"Could not parse repo from remote: {remote_url}")
24
+ return owner_repo
14
25
 
15
26
 
16
27
  def get_default_branch(cwd: Path) -> str:
@@ -0,0 +1,45 @@
1
+ from codeberg_cli.client import Client, ClientError, DEFAULT_BASE_URL
2
+ from codeberg_cli.config import load_config
3
+
4
+
5
+ def get_base_url() -> str:
6
+ """Get the web base URL from cascading CLI context or default."""
7
+ from xclif.context import get_context
8
+
9
+ try:
10
+ ctx = get_context()
11
+ raw = ctx.get("base_url", DEFAULT_BASE_URL)
12
+ except RuntimeError:
13
+ raw = DEFAULT_BASE_URL
14
+
15
+ # Normalize: ensure scheme, strip API path suffix for backward compat
16
+ if "://" not in raw:
17
+ raw = "https://" + raw
18
+ raw = raw.rstrip("/")
19
+ if raw.endswith("/api/v1"):
20
+ raw = raw[:-7]
21
+ return raw
22
+
23
+
24
+ def get_web_base_url() -> str:
25
+ """Derive the web UI base URL from the API base URL."""
26
+ return get_base_url()
27
+
28
+
29
+ def get_authenticated_client(base_url: str | None = None) -> Client | None:
30
+ """Return a Client if token is stored, else None."""
31
+ config = load_config()
32
+ token = config.get("token")
33
+ if not token:
34
+ return None
35
+ return Client(token=token, base_url=base_url or get_base_url())
36
+
37
+
38
+ def require_client(base_url: str | None = None) -> Client:
39
+ """Return an authenticated Client or print error and exit."""
40
+ client = get_authenticated_client(base_url=base_url)
41
+ if client is None:
42
+ from xclif.errors import UsageError
43
+
44
+ raise UsageError("Not logged in. Run 'cb auth login' first.")
45
+ return client
@@ -0,0 +1,9 @@
1
+ from codeberg_cli.client import DEFAULT_BASE_URL
2
+ from xclif import Cascade, WithConfig, command
3
+
4
+
5
+ @command("cb")
6
+ def _(
7
+ base_url: Cascade[WithConfig[str]] = DEFAULT_BASE_URL,
8
+ ) -> None:
9
+ """Interact with Codeberg or any Forgejo instance — manage repos, issues, PRs, releases, and more."""
@@ -0,0 +1,6 @@
1
+ from xclif import command
2
+
3
+
4
+ @command("auth")
5
+ def _() -> None:
6
+ """Manage authentication — login, logout, check status, and whoami."""
@@ -19,5 +19,3 @@ def _(
19
19
  user = client.get("/user", action="Verifying token")
20
20
  save_config({"token": token})
21
21
  rich.print(f"[bold green]Logged in as[/bold green] [bold]{user['login']}[/bold]")
22
-
23
-
@@ -17,5 +17,3 @@ def _() -> None:
17
17
  rich.print(f"Logged in to [cyan]Codeberg[/cyan] as [bold]{user['login']}[/bold]")
18
18
  rich.print(f" User ID: {user['id']}")
19
19
  rich.print(f" Full Name: {user.get('full_name', '(not set)')}")
20
-
21
-
@@ -10,5 +10,3 @@ def _() -> None:
10
10
  client = require_client()
11
11
  user = client.get("/user")
12
12
  print(user["login"])
13
-
14
-
@@ -0,0 +1,6 @@
1
+ from xclif import command
2
+
3
+
4
+ @command("issue")
5
+ def _() -> None:
6
+ """Manage issues — create, view, close, and more."""
@@ -0,0 +1,41 @@
1
+ from typing import Annotated
2
+
3
+ import rich
4
+
5
+ from codeberg_cli.git import infer_repo
6
+ from codeberg_cli.helpers import require_client
7
+ from xclif import Arg, Option, command
8
+
9
+
10
+ @command("comment")
11
+ def _(
12
+ id: Annotated[int, Arg(description="Issue number")],
13
+ message: Annotated[str, Option(description="Comment text", name="message")] = "",
14
+ repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
15
+ ) -> None:
16
+ """Comment on an issue."""
17
+ client = require_client()
18
+
19
+ if not repo:
20
+ inferred = infer_repo()
21
+ if not inferred:
22
+ rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
23
+ return 1
24
+ repo = inferred
25
+
26
+ if not message:
27
+ rich.print("[dim]Enter comment (Ctrl+D to finish):[/dim]")
28
+ try:
29
+ lines = []
30
+ while True:
31
+ line = input()
32
+ lines.append(line)
33
+ except (EOFError, KeyboardInterrupt):
34
+ pass
35
+ message = "\n".join(lines).strip()
36
+ if not message:
37
+ rich.print("[bold red]Error:[/bold red] Comment cannot be empty")
38
+ return 1
39
+
40
+ client.post(f"/repos/{repo}/issues/{id}/comments", data={"body": message})
41
+ rich.print(f"[bold green]Commented[/bold green] on issue #{id} in {repo}")
@@ -3,7 +3,7 @@ from typing import Annotated
3
3
  import rich
4
4
 
5
5
  from codeberg_cli.git import infer_repo
6
- from codeberg_cli.helpers import require_client
6
+ from codeberg_cli.helpers import get_web_base_url, require_client
7
7
  from xclif import Option, command
8
8
 
9
9
 
@@ -50,6 +50,6 @@ def _(
50
50
  result = client.post(f"/repos/{repo}/issues", data=data)
51
51
 
52
52
  rich.print(f"[bold green]Created issue[/bold green] #{result['number']}: {result['title']}")
53
- rich.print(f"[dim]https://codeberg.org/{repo}/issues/{result['number']}[/dim]")
53
+ rich.print(f"[dim]{get_web_base_url()}/{repo}/issues/{result['number']}[/dim]")
54
54
 
55
55
 
@@ -0,0 +1,38 @@
1
+ from typing import Annotated
2
+
3
+ import rich
4
+
5
+ from codeberg_cli.git import infer_repo
6
+ from codeberg_cli.helpers import require_client
7
+ from xclif import Arg, Option, command
8
+
9
+
10
+ @command("edit")
11
+ def _(
12
+ id: Annotated[int, Arg(description="Issue number")],
13
+ title: Annotated[str, Option(description="New title", name="title")] = "",
14
+ body: Annotated[str, Option(description="New body", name="body")] = "",
15
+ repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
16
+ ) -> None:
17
+ """Edit an issue's title or body."""
18
+ client = require_client()
19
+
20
+ if not repo:
21
+ inferred = infer_repo()
22
+ if not inferred:
23
+ rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
24
+ return 1
25
+ repo = inferred
26
+
27
+ data: dict = {}
28
+ if title:
29
+ data["title"] = title
30
+ if body:
31
+ data["body"] = body
32
+
33
+ if not data:
34
+ rich.print("[bold yellow]Nothing to update.[/bold yellow] Pass --title and/or --body.")
35
+ return 1
36
+
37
+ client.patch(f"/repos/{repo}/issues/{id}", data=data)
38
+ rich.print(f"[bold green]Updated[/bold green] issue #{id} in {repo}")
@@ -4,7 +4,7 @@ from typing import Annotated
4
4
  import rich
5
5
 
6
6
  from codeberg_cli.git import infer_repo
7
- from codeberg_cli.helpers import require_client
7
+ from codeberg_cli.helpers import get_web_base_url, require_client
8
8
  from xclif import Arg, Option, command
9
9
 
10
10
 
@@ -25,7 +25,7 @@ def _(
25
25
  repo = inferred
26
26
 
27
27
  if web:
28
- webbrowser.open(f"https://codeberg.org/{repo}/issues/{id}")
28
+ webbrowser.open(f"{get_web_base_url()}/{repo}/issues/{id}")
29
29
  return
30
30
 
31
31
  issue = client.get(f"/repos/{repo}/issues/{id}")
@@ -0,0 +1,6 @@
1
+ from xclif import command
2
+
3
+
4
+ @command("label")
5
+ def _() -> None:
6
+ """Manage repository labels — create, list, and delete."""
@@ -0,0 +1,6 @@
1
+ from xclif import command
2
+
3
+
4
+ @command("milestone")
5
+ def _() -> None:
6
+ """Manage milestones — create and list."""
@@ -0,0 +1,6 @@
1
+ from xclif import command
2
+
3
+
4
+ @command("notification")
5
+ def _() -> None:
6
+ """View notifications from your repositories."""
@@ -0,0 +1,38 @@
1
+ from typing import Annotated
2
+
3
+ import rich
4
+ from rich.table import Table
5
+
6
+ from codeberg_cli.helpers import require_client
7
+ from xclif import Option, command
8
+
9
+
10
+ @command("list", "ls")
11
+ def _(
12
+ limit: Annotated[int, Option(description="Maximum notifications to show", name="limit")] = 30,
13
+ all: Annotated[bool, Option(description="Show all notifications, not just unread", name="all")] = False,
14
+ ) -> None:
15
+ """List notifications."""
16
+ client = require_client()
17
+
18
+ params: dict = {"limit": limit, "page": 1}
19
+ if not all:
20
+ params["status-types"] = "unread"
21
+
22
+ notifications = client.get("/notifications", params=params)
23
+
24
+ if not notifications:
25
+ rich.print("[dim]No notifications.[/dim]")
26
+ return
27
+
28
+ table = Table("ID", "Type", "Subject", "Repo")
29
+ for n in notifications:
30
+ subject = n.get("subject", {})
31
+ repo = n.get("repository", {})
32
+ table.add_row(
33
+ str(n["id"]),
34
+ subject.get("type", ""),
35
+ subject.get("title", ""),
36
+ repo.get("full_name", ""),
37
+ )
38
+ rich.print(table)
@@ -0,0 +1,6 @@
1
+ from xclif import command
2
+
3
+
4
+ @command("pr")
5
+ def _() -> None:
6
+ """Manage pull requests — create, list, comment, and more."""
@@ -0,0 +1,41 @@
1
+ from typing import Annotated
2
+
3
+ import rich
4
+
5
+ from codeberg_cli.git import infer_repo
6
+ from codeberg_cli.helpers import require_client
7
+ from xclif import Arg, Option, command
8
+
9
+
10
+ @command("comment")
11
+ def _(
12
+ id: Annotated[int, Arg(description="PR number")],
13
+ message: Annotated[str, Option(description="Comment text", name="message")] = "",
14
+ repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
15
+ ) -> None:
16
+ """Comment on a pull request."""
17
+ client = require_client()
18
+
19
+ if not repo:
20
+ inferred = infer_repo()
21
+ if not inferred:
22
+ rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
23
+ return 1
24
+ repo = inferred
25
+
26
+ if not message:
27
+ rich.print("[dim]Enter comment (Ctrl+D to finish):[/dim]")
28
+ try:
29
+ lines = []
30
+ while True:
31
+ line = input()
32
+ lines.append(line)
33
+ except (EOFError, KeyboardInterrupt):
34
+ pass
35
+ message = "\n".join(lines).strip()
36
+ if not message:
37
+ rich.print("[bold red]Error:[/bold red] Comment cannot be empty")
38
+ return 1
39
+
40
+ client.post(f"/repos/{repo}/issues/{id}/comments", data={"body": message})
41
+ rich.print(f"[bold green]Commented[/bold green] on PR #{id} in {repo}")
@@ -5,7 +5,7 @@ from typing import Annotated
5
5
  import rich
6
6
 
7
7
  from codeberg_cli.git import get_default_branch, infer_repo
8
- from codeberg_cli.helpers import require_client
8
+ from codeberg_cli.helpers import get_web_base_url, require_client
9
9
  from xclif import Option, command
10
10
 
11
11
 
@@ -63,6 +63,6 @@ def _(
63
63
 
64
64
  result = client.post(f"/repos/{repo}/pulls", data=data)
65
65
  rich.print(f"[bold green]Created PR[/bold green] #{result['number']}: {result['title']}")
66
- rich.print(f"[dim]https://codeberg.org/{repo}/pulls/{result['number']}[/dim]")
66
+ rich.print(f"[dim]{get_web_base_url()}/{repo}/pulls/{result['number']}[/dim]")
67
67
 
68
68