codeberg-cli 0.2.0__tar.gz → 0.4.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 (141) hide show
  1. codeberg_cli-0.4.0/PKG-INFO +232 -0
  2. codeberg_cli-0.4.0/README.md +207 -0
  3. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/pyproject.toml +2 -5
  4. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/client.py +6 -0
  5. codeberg_cli-0.4.0/src/codeberg_cli/helpers.py +120 -0
  6. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/__init__.py +5 -1
  7. codeberg_cli-0.4.0/src/codeberg_cli/routes/actions/__init__.py +6 -0
  8. codeberg_cli-0.4.0/src/codeberg_cli/routes/actions/dispatch.py +31 -0
  9. codeberg_cli-0.4.0/src/codeberg_cli/routes/actions/run.py +40 -0
  10. codeberg_cli-0.4.0/src/codeberg_cli/routes/actions/runs.py +52 -0
  11. codeberg_cli-0.4.0/src/codeberg_cli/routes/actions/workflows.py +46 -0
  12. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/delete.py +31 -0
  13. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/labels/__init__.py +6 -0
  14. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/labels/add.py +27 -0
  15. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/labels/list.py +43 -0
  16. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/labels/remove.py +27 -0
  17. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/issue/list.py +15 -10
  18. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/pin.py +26 -0
  19. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/search.py +59 -0
  20. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/subscribe.py +27 -0
  21. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/time/__init__.py +6 -0
  22. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/time/add.py +31 -0
  23. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/time/list.py +42 -0
  24. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/unpin.py +26 -0
  25. codeberg_cli-0.4.0/src/codeberg_cli/routes/issue/unsubscribe.py +27 -0
  26. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/issue/view.py +5 -1
  27. codeberg_cli-0.4.0/src/codeberg_cli/routes/label/edit.py +41 -0
  28. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/label/list.py +12 -10
  29. codeberg_cli-0.4.0/src/codeberg_cli/routes/milestone/delete.py +38 -0
  30. codeberg_cli-0.4.0/src/codeberg_cli/routes/milestone/edit.py +44 -0
  31. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/milestone/list.py +15 -15
  32. codeberg_cli-0.4.0/src/codeberg_cli/routes/milestone/view.py +42 -0
  33. codeberg_cli-0.2.0/src/codeberg_cli/routes/notification/list.py → codeberg_cli-0.4.0/src/codeberg_cli/routes/notifications.py +15 -11
  34. codeberg_cli-0.4.0/src/codeberg_cli/routes/org/__init__.py +6 -0
  35. codeberg_cli-0.4.0/src/codeberg_cli/routes/org/list.py +28 -0
  36. codeberg_cli-0.4.0/src/codeberg_cli/routes/org/members.py +31 -0
  37. codeberg_cli-0.4.0/src/codeberg_cli/routes/org/teams.py +38 -0
  38. codeberg_cli-0.4.0/src/codeberg_cli/routes/org/view.py +33 -0
  39. codeberg_cli-0.4.0/src/codeberg_cli/routes/pr/check_merge.py +33 -0
  40. codeberg_cli-0.4.0/src/codeberg_cli/routes/pr/commits.py +44 -0
  41. codeberg_cli-0.4.0/src/codeberg_cli/routes/pr/diff.py +31 -0
  42. codeberg_cli-0.4.0/src/codeberg_cli/routes/pr/files.py +42 -0
  43. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/pr/list.py +16 -10
  44. codeberg_cli-0.4.0/src/codeberg_cli/routes/pr/reopen.py +26 -0
  45. codeberg_cli-0.4.0/src/codeberg_cli/routes/pr/update.py +27 -0
  46. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/pr/view.py +5 -1
  47. codeberg_cli-0.4.0/src/codeberg_cli/routes/release/delete.py +38 -0
  48. codeberg_cli-0.4.0/src/codeberg_cli/routes/release/edit.py +56 -0
  49. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/release/list.py +14 -11
  50. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/release/view.py +12 -3
  51. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/archive.py +36 -0
  52. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/branch/__init__.py +6 -0
  53. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/branch/create.py +31 -0
  54. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/branch/delete.py +26 -0
  55. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/branch/list.py +36 -0
  56. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/branch/view.py +36 -0
  57. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/collaborator/__init__.py +6 -0
  58. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/collaborator/add.py +31 -0
  59. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/collaborator/delete.py +26 -0
  60. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/collaborator/list.py +37 -0
  61. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/commits.py +51 -0
  62. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/contents.py +55 -0
  63. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/edit.py +64 -0
  64. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/forks/sync.py +32 -0
  65. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/forks.py +42 -0
  66. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/languages.py +45 -0
  67. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/repo/list.py +16 -10
  68. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/migrate.py +33 -0
  69. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/search.py +58 -0
  70. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/stargazers.py +40 -0
  71. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/tag/__init__.py +6 -0
  72. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/tag/create.py +34 -0
  73. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/tag/delete.py +26 -0
  74. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/tag/list.py +36 -0
  75. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/topics/__init__.py +6 -0
  76. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/topics/list.py +36 -0
  77. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/topics/set.py +27 -0
  78. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/transfer.py +35 -0
  79. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/unwatch.py +25 -0
  80. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/repo/view.py +6 -2
  81. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/watch.py +25 -0
  82. codeberg_cli-0.4.0/src/codeberg_cli/routes/repo/watchers.py +40 -0
  83. codeberg_cli-0.2.0/src/codeberg_cli/routes/user/view.py → codeberg_cli-0.4.0/src/codeberg_cli/routes/user.py +6 -2
  84. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/uv.lock +10 -30
  85. codeberg_cli-0.2.0/PKG-INFO +0 -118
  86. codeberg_cli-0.2.0/README.md +0 -93
  87. codeberg_cli-0.2.0/src/codeberg_cli/helpers.py +0 -45
  88. codeberg_cli-0.2.0/src/codeberg_cli/routes/notification/__init__.py +0 -6
  89. codeberg_cli-0.2.0/src/codeberg_cli/routes/user/__init__.py +0 -6
  90. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/.github/workflows/ci.yml +0 -0
  91. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/.github/workflows/release.yml +0 -0
  92. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/.gitignore +0 -0
  93. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/.python-version +0 -0
  94. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/Justfile +0 -0
  95. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/LICENSE +0 -0
  96. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/__main__.py +0 -0
  97. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/config.py +0 -0
  98. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/git.py +0 -0
  99. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/api.py +0 -0
  100. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/auth/__init__.py +0 -0
  101. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/auth/login.py +0 -0
  102. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/auth/logout.py +0 -0
  103. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/auth/status.py +0 -0
  104. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/auth/whoami.py +0 -0
  105. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/issue/__init__.py +0 -0
  106. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/issue/close.py +0 -0
  107. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/issue/comment.py +0 -0
  108. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/issue/create.py +0 -0
  109. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/issue/edit.py +0 -0
  110. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/issue/reopen.py +0 -0
  111. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/label/__init__.py +0 -0
  112. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/label/create.py +0 -0
  113. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/label/delete.py +0 -0
  114. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/milestone/__init__.py +0 -0
  115. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/milestone/create.py +0 -0
  116. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/pr/__init__.py +0 -0
  117. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/pr/checkout.py +0 -0
  118. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/pr/close.py +0 -0
  119. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/pr/comment.py +0 -0
  120. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/pr/create.py +0 -0
  121. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/pr/edit.py +0 -0
  122. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/pr/merge.py +0 -0
  123. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/release/__init__.py +0 -0
  124. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/release/create.py +0 -0
  125. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/release/upload.py +0 -0
  126. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/repo/__init__.py +0 -0
  127. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/repo/clone.py +0 -0
  128. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/repo/create.py +0 -0
  129. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/repo/delete.py +0 -0
  130. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/repo/fork.py +0 -0
  131. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/repo/star.py +0 -0
  132. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/src/codeberg_cli/routes/repo/unstar.py +0 -0
  133. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/tests/test_api.py +0 -0
  134. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/tests/test_auth.py +0 -0
  135. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/tests/test_client.py +0 -0
  136. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/tests/test_config.py +0 -0
  137. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/tests/test_git.py +0 -0
  138. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/tests/test_issue.py +0 -0
  139. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/tests/test_pr.py +0 -0
  140. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/tests/test_release.py +0 -0
  141. {codeberg_cli-0.2.0 → codeberg_cli-0.4.0}/tests/test_repo.py +0 -0
@@ -0,0 +1,232 @@
1
+ Metadata-Version: 2.4
2
+ Name: codeberg-cli
3
+ Version: 0.4.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.5.1
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
+ Here's how `cb` stacks up against other Forgejo CLI tools. *Last updated: May 2026.*
97
+
98
+ | | **cb** (this) | **fj** (forgejo-cli) | **berg** (codeberg-cli) | **tea** (gitea/tea) | **gcli** |
99
+ |---|---|---|---|---|---|
100
+ | Language | Python | Rust | Rust | Go | C |
101
+ | Version | v0.2.0 | v0.5.0 | v0.5.1 | v0.13.0 | v2.11.0 |
102
+ | Install | `pip install codeberg-cli` | prebuilt binaries/Cargo | `cargo install codeberg-cli` | `brew install tea` | `brew install gcli` |
103
+ | Multi-instance | `cb config set base_url` | `-H <instance>` | `BERG_BASE_URL` | `tea login add` | `-t forgejo` |
104
+
105
+ ### Issues
106
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
107
+ |---|---|---|---|---|---|
108
+ | Create | ✅ | ✅ | ✅ | ✅ | ✅ |
109
+ | List | ✅ | ✅ | ✅ | ✅ | ✅ |
110
+ | View | ✅ | ✅ | ✅ | ✅ | ✅ |
111
+ | Close | ✅ | ✅ | ✅ | ✅ | — |
112
+ | Reopen | ✅ | — | ✅ | ✅ | — |
113
+ | Comment | ✅ | ✅ | ✅ | ✅ | ✅ |
114
+ | Edit | ✅ | — | ✅ | ✅ | — |
115
+ | Delete | ✅ | — | — | — | — |
116
+ | Pin/Unpin | ✅ | — | — | ✅ | — |
117
+ | Search | ✅ | ✅ | — | ✅ | — |
118
+ | Attachments | — | — | — | ✅ | — |
119
+ | Labels (manage on issue) | ✅ | — | — | ✅ | — |
120
+ | Reactions | — | — | — | — | — |
121
+ | Subscriptions | ✅ | — | — | ✅ | — |
122
+ | Tracked times | ✅ | — | — | — | ✅ |
123
+ | Dependencies | — | — | — | — | — |
124
+ | Deadline | — | — | — | ✅ | — |
125
+ | Templates | — | ✅ | — | — | — |
126
+
127
+ ### Pull Requests
128
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
129
+ |---|---|---|---|---|---|
130
+ | Create | ✅ | ✅ | ✅ | ✅ | ✅ |
131
+ | List | ✅ | ✅ | ✅ | ✅ | ✅ |
132
+ | View | ✅ | ✅ | ✅ | ✅ | ✅ |
133
+ | Merge | ✅ | ✅ | ✅ | ✅ | ✅ |
134
+ | Close | ✅ | — | ✅ | ✅ | — |
135
+ | Reopen | ✅ | — | ✅ | ✅ | — |
136
+ | Comment | ✅ | ✅ | ✅ | ✅ | ✅ |
137
+ | Edit | ✅ | — | ✅ | ✅ | — |
138
+ | Checkout | ✅ | — | ✅ | ✅ | ✅ |
139
+ | Commits | ✅ | — | — | ✅ | — |
140
+ | Files/changed | ✅ | — | — | ✅ | — |
141
+ | Reviews | — | ✅ | — | ✅ | ✅ |
142
+ | Diff/Patch | ✅ | — | — | ✅ | — |
143
+ | Update branch | ✅ | — | — | ✅ | — |
144
+ | AGit (no-fork) | — | ✅ | — | — | — |
145
+ | CI status | — | ✅ | — | — | — |
146
+ | Templates | — | ✅ | — | — | — |
147
+ | Auto-merge | — | — | — | ✅ | — |
148
+
149
+ ### Releases
150
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
151
+ |---|---|---|---|---|---|
152
+ | Create | ✅ | ✅ | ✅ | ✅ | ✅ |
153
+ | List | ✅ | ✅ | ✅ | ✅ | ✅ |
154
+ | View | ✅ | ✅ | ✅ | ✅ | ✅ |
155
+ | Upload assets | ✅ | — | ✅ | — | ✅ |
156
+ | Delete | ✅ | — | — | ✅ | ✅ |
157
+ | Edit | ✅ | — | — | ✅ | — |
158
+ | Latest | ✅ | — | — | ✅ | — |
159
+ | By tag | ✅ | — | — | ✅ | — |
160
+
161
+ ### Repositories
162
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
163
+ |---|---|---|---|---|---|
164
+ | Create | ✅ | ✅ | ✅ | ✅ | ✅ |
165
+ | List | ✅ | ✅ | ✅ | ✅ | ✅ |
166
+ | View | ✅ | ✅ | ✅ | ✅ | ✅ |
167
+ | Clone | ✅ | — | ✅ | ✅ | — |
168
+ | Fork | ✅ | ✅ | ✅ | ✅ | ✅ |
169
+ | Delete | ✅ | — | ✅ | ✅ | — |
170
+ | Star | ✅ | ✅ | ✅ | ✅ | — |
171
+ | Unstar | ✅ | — | ✅ | ✅ | — |
172
+ | Watch/Unwatch | ✅ | ✅ | — | ✅ | — |
173
+ | Edit | ✅ | ✅ | — | ✅ | — |
174
+ | Migrate/Mirror | ✅ | ✅ | — | ✅ | — |
175
+ | Branches | ✅ | — | — | ✅ | — |
176
+ | Topics | ✅ | — | — | ✅ | — |
177
+ | Languages | ✅ | — | — | — | — |
178
+ | Hooks | — | — | — | ✅ | — |
179
+ | Archive | ✅ | — | — | ✅ | — |
180
+ | Commits | ✅ | — | — | — | — |
181
+ | Contents | ✅ | — | — | ✅ | — |
182
+ | Collaborators | ✅ | — | — | ✅ | — |
183
+ | Transfer | ✅ | — | — | ✅ | — |
184
+ | Wikis | — | — | — | ✅ | — |
185
+ | Push mirrors | — | — | — | ✅ | — |
186
+ | Search | ✅ | — | — | ✅ | — |
187
+
188
+ ### Labels
189
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
190
+ |---|---|---|---|---|---|
191
+ | Create | ✅ | — | ✅ | ✅ | ✅ |
192
+ | List | ✅ | — | ✅ | ✅ | ✅ |
193
+ | Delete | ✅ | — | ✅ | ✅ | — |
194
+ | Edit | ✅ | — | — | ✅ | — |
195
+
196
+ ### Milestones
197
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
198
+ |---|---|---|---|---|---|
199
+ | Create | ✅ | — | ✅ | ✅ | — |
200
+ | List | ✅ | — | ✅ | ✅ | ✅ |
201
+ | View | ✅ | — | — | ✅ | — |
202
+ | Delete | ✅ | — | — | ✅ | — |
203
+ | Edit | ✅ | — | — | ✅ | — |
204
+
205
+ ### Notifications
206
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
207
+ |---|---|---|---|---|---|
208
+ | List | ✅ | — | ✅ | ✅ | ✅ |
209
+ | Mark read | — | — | — | ✅ | — |
210
+ | Thread details | — | — | — | ✅ | — |
211
+ | Per-repo | — | — | — | ✅ | — |
212
+
213
+ ### Extra
214
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
215
+ |---|---|---|---|---|---|
216
+ | Raw API | ✅ | — | ✅ | — | ✅ |
217
+ | User profiles | ✅ | ✅ | — | — | — |
218
+ | SSH keys | — | ✅ | — | ✅ | ✅ |
219
+ | GPG keys | — | ✅ | — | — | — |
220
+ | Org/team mgmt | ✅ | ✅ | — | ✅ | — |
221
+ | Forgejo Actions | ✅ | ✅ | — | — | — |
222
+ | Shell completions | ✅ (auto via xclif) | ✅ | ✅ | ✅ | — |
223
+ | JSON output | ✅ (via `--json`) | — | ✅ | ✅ | — |
224
+ | Non-interactive | auto (no prompts) | — | ✅ | ✅ | — |
225
+ | Config management | ✅ | — | ✅ | — | — |
226
+ | Web browser flag | on view commands | — | — | ✅ | — |
227
+
228
+ **`cb` is the most feature-complete Forgejo CLI** — by a wide margin. It dominates on repo management (branches, topics, languages, tags, commits, contents, collaborators, search, archive, transfer, watch/unwatch, sync-fork, migrate), issues (delete, pin, labels, search, subscribe, tracked times), PRs (diff, commits, files, reopen, update), releases (delete, edit, latest, by-tag), and extras (Actions, org/team management, `--json` on every command, raw API, release uploads). Built in Python with [Xclif](https://xclif.readthedocs.io) for a clean, hackable codebase. If something's missing, open an issue!
229
+
230
+ ## License
231
+
232
+ MIT
@@ -0,0 +1,207 @@
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
+ Here's how `cb` stacks up against other Forgejo CLI tools. *Last updated: May 2026.*
72
+
73
+ | | **cb** (this) | **fj** (forgejo-cli) | **berg** (codeberg-cli) | **tea** (gitea/tea) | **gcli** |
74
+ |---|---|---|---|---|---|
75
+ | Language | Python | Rust | Rust | Go | C |
76
+ | Version | v0.2.0 | v0.5.0 | v0.5.1 | v0.13.0 | v2.11.0 |
77
+ | Install | `pip install codeberg-cli` | prebuilt binaries/Cargo | `cargo install codeberg-cli` | `brew install tea` | `brew install gcli` |
78
+ | Multi-instance | `cb config set base_url` | `-H <instance>` | `BERG_BASE_URL` | `tea login add` | `-t forgejo` |
79
+
80
+ ### Issues
81
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
82
+ |---|---|---|---|---|---|
83
+ | Create | ✅ | ✅ | ✅ | ✅ | ✅ |
84
+ | List | ✅ | ✅ | ✅ | ✅ | ✅ |
85
+ | View | ✅ | ✅ | ✅ | ✅ | ✅ |
86
+ | Close | ✅ | ✅ | ✅ | ✅ | — |
87
+ | Reopen | ✅ | — | ✅ | ✅ | — |
88
+ | Comment | ✅ | ✅ | ✅ | ✅ | ✅ |
89
+ | Edit | ✅ | — | ✅ | ✅ | — |
90
+ | Delete | ✅ | — | — | — | — |
91
+ | Pin/Unpin | ✅ | — | — | ✅ | — |
92
+ | Search | ✅ | ✅ | — | ✅ | — |
93
+ | Attachments | — | — | — | ✅ | — |
94
+ | Labels (manage on issue) | ✅ | — | — | ✅ | — |
95
+ | Reactions | — | — | — | — | — |
96
+ | Subscriptions | ✅ | — | — | ✅ | — |
97
+ | Tracked times | ✅ | — | — | — | ✅ |
98
+ | Dependencies | — | — | — | — | — |
99
+ | Deadline | — | — | — | ✅ | — |
100
+ | Templates | — | ✅ | — | — | — |
101
+
102
+ ### Pull Requests
103
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
104
+ |---|---|---|---|---|---|
105
+ | Create | ✅ | ✅ | ✅ | ✅ | ✅ |
106
+ | List | ✅ | ✅ | ✅ | ✅ | ✅ |
107
+ | View | ✅ | ✅ | ✅ | ✅ | ✅ |
108
+ | Merge | ✅ | ✅ | ✅ | ✅ | ✅ |
109
+ | Close | ✅ | — | ✅ | ✅ | — |
110
+ | Reopen | ✅ | — | ✅ | ✅ | — |
111
+ | Comment | ✅ | ✅ | ✅ | ✅ | ✅ |
112
+ | Edit | ✅ | — | ✅ | ✅ | — |
113
+ | Checkout | ✅ | — | ✅ | ✅ | ✅ |
114
+ | Commits | ✅ | — | — | ✅ | — |
115
+ | Files/changed | ✅ | — | — | ✅ | — |
116
+ | Reviews | — | ✅ | — | ✅ | ✅ |
117
+ | Diff/Patch | ✅ | — | — | ✅ | — |
118
+ | Update branch | ✅ | — | — | ✅ | — |
119
+ | AGit (no-fork) | — | ✅ | — | — | — |
120
+ | CI status | — | ✅ | — | — | — |
121
+ | Templates | — | ✅ | — | — | — |
122
+ | Auto-merge | — | — | — | ✅ | — |
123
+
124
+ ### Releases
125
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
126
+ |---|---|---|---|---|---|
127
+ | Create | ✅ | ✅ | ✅ | ✅ | ✅ |
128
+ | List | ✅ | ✅ | ✅ | ✅ | ✅ |
129
+ | View | ✅ | ✅ | ✅ | ✅ | ✅ |
130
+ | Upload assets | ✅ | — | ✅ | — | ✅ |
131
+ | Delete | ✅ | — | — | ✅ | ✅ |
132
+ | Edit | ✅ | — | — | ✅ | — |
133
+ | Latest | ✅ | — | — | ✅ | — |
134
+ | By tag | ✅ | — | — | ✅ | — |
135
+
136
+ ### Repositories
137
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
138
+ |---|---|---|---|---|---|
139
+ | Create | ✅ | ✅ | ✅ | ✅ | ✅ |
140
+ | List | ✅ | ✅ | ✅ | ✅ | ✅ |
141
+ | View | ✅ | ✅ | ✅ | ✅ | ✅ |
142
+ | Clone | ✅ | — | ✅ | ✅ | — |
143
+ | Fork | ✅ | ✅ | ✅ | ✅ | ✅ |
144
+ | Delete | ✅ | — | ✅ | ✅ | — |
145
+ | Star | ✅ | ✅ | ✅ | ✅ | — |
146
+ | Unstar | ✅ | — | ✅ | ✅ | — |
147
+ | Watch/Unwatch | ✅ | ✅ | — | ✅ | — |
148
+ | Edit | ✅ | ✅ | — | ✅ | — |
149
+ | Migrate/Mirror | ✅ | ✅ | — | ✅ | — |
150
+ | Branches | ✅ | — | — | ✅ | — |
151
+ | Topics | ✅ | — | — | ✅ | — |
152
+ | Languages | ✅ | — | — | — | — |
153
+ | Hooks | — | — | — | ✅ | — |
154
+ | Archive | ✅ | — | — | ✅ | — |
155
+ | Commits | ✅ | — | — | — | — |
156
+ | Contents | ✅ | — | — | ✅ | — |
157
+ | Collaborators | ✅ | — | — | ✅ | — |
158
+ | Transfer | ✅ | — | — | ✅ | — |
159
+ | Wikis | — | — | — | ✅ | — |
160
+ | Push mirrors | — | — | — | ✅ | — |
161
+ | Search | ✅ | — | — | ✅ | — |
162
+
163
+ ### Labels
164
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
165
+ |---|---|---|---|---|---|
166
+ | Create | ✅ | — | ✅ | ✅ | ✅ |
167
+ | List | ✅ | — | ✅ | ✅ | ✅ |
168
+ | Delete | ✅ | — | ✅ | ✅ | — |
169
+ | Edit | ✅ | — | — | ✅ | — |
170
+
171
+ ### Milestones
172
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
173
+ |---|---|---|---|---|---|
174
+ | Create | ✅ | — | ✅ | ✅ | — |
175
+ | List | ✅ | — | ✅ | ✅ | ✅ |
176
+ | View | ✅ | — | — | ✅ | — |
177
+ | Delete | ✅ | — | — | ✅ | — |
178
+ | Edit | ✅ | — | — | ✅ | — |
179
+
180
+ ### Notifications
181
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
182
+ |---|---|---|---|---|---|
183
+ | List | ✅ | — | ✅ | ✅ | ✅ |
184
+ | Mark read | — | — | — | ✅ | — |
185
+ | Thread details | — | — | — | ✅ | — |
186
+ | Per-repo | — | — | — | ✅ | — |
187
+
188
+ ### Extra
189
+ | | **cb** | **fj** | **berg** | **tea** | **gcli** |
190
+ |---|---|---|---|---|---|
191
+ | Raw API | ✅ | — | ✅ | — | ✅ |
192
+ | User profiles | ✅ | ✅ | — | — | — |
193
+ | SSH keys | — | ✅ | — | ✅ | ✅ |
194
+ | GPG keys | — | ✅ | — | — | — |
195
+ | Org/team mgmt | ✅ | ✅ | — | ✅ | — |
196
+ | Forgejo Actions | ✅ | ✅ | — | — | — |
197
+ | Shell completions | ✅ (auto via xclif) | ✅ | ✅ | ✅ | — |
198
+ | JSON output | ✅ (via `--json`) | — | ✅ | ✅ | — |
199
+ | Non-interactive | auto (no prompts) | — | ✅ | ✅ | — |
200
+ | Config management | ✅ | — | ✅ | — | — |
201
+ | Web browser flag | on view commands | — | — | ✅ | — |
202
+
203
+ **`cb` is the most feature-complete Forgejo CLI** — by a wide margin. It dominates on repo management (branches, topics, languages, tags, commits, contents, collaborators, search, archive, transfer, watch/unwatch, sync-fork, migrate), issues (delete, pin, labels, search, subscribe, tracked times), PRs (diff, commits, files, reopen, update), releases (delete, edit, latest, by-tag), and extras (Actions, org/team management, `--json` on every command, raw API, release uploads). Built in Python with [Xclif](https://xclif.readthedocs.io) for a clean, hackable codebase. If something's missing, open an issue!
204
+
205
+ ## License
206
+
207
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codeberg-cli"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "A Forgejo CLI — works with Codeberg and any Forgejo instance"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "Topic :: Software Development :: Version Control :: Git",
20
20
  ]
21
21
  dependencies = [
22
- "xclif>=0.4.3",
22
+ "xclif>=0.5.1",
23
23
  "httpx",
24
24
  "tomlkit",
25
25
  "platformdirs",
@@ -37,9 +37,6 @@ 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
-
43
40
  [tool.hatch.build.targets.wheel]
44
41
  packages = ["src/codeberg_cli"]
45
42
 
@@ -7,6 +7,7 @@ DEFAULT_BASE_URL = "https://codeberg.org"
7
7
  _VERBS = {
8
8
  "GET": "Fetching",
9
9
  "POST": "Creating",
10
+ "PUT": "Updating",
10
11
  "PATCH": "Updating",
11
12
  "DELETE": "Deleting",
12
13
  }
@@ -55,6 +56,11 @@ class Client:
55
56
  ) -> dict | list:
56
57
  return self._request("PATCH", path, json=data, action=action)
57
58
 
59
+ def put(
60
+ self, path: str, data: dict | None = None, action: str | None = None
61
+ ) -> dict | list:
62
+ return self._request("PUT", path, json=data, action=action)
63
+
58
64
  def delete(self, path: str, action: str | None = None) -> None:
59
65
  self._request("DELETE", path, action=action)
60
66
 
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import json as _json
4
+ import sys
5
+ from collections.abc import Iterable, Mapping
6
+ from typing import Any
7
+
8
+ import rich
9
+
10
+ from codeberg_cli.client import Client, ClientError, DEFAULT_BASE_URL
11
+ from codeberg_cli.config import load_config
12
+
13
+
14
+ def is_json_mode() -> bool:
15
+ """Check whether ``--json`` was passed on the CLI."""
16
+ return "--json" in sys.argv[1:]
17
+
18
+
19
+ def output(
20
+ data: Any,
21
+ *,
22
+ json: bool | None = None,
23
+ default: str | None = None,
24
+ ) -> None:
25
+ """Print *data* as JSON or fall back to *default* text.
26
+
27
+ When ``json=True`` (or global ``--json`` was passed), data is printed as
28
+ prettified JSON. When ``json=False`` the *default* string (if given) is
29
+ printed. When *default* is ``None`` and JSON mode is off, this is a no-op
30
+ — the caller is expected to handle rich printing themselves.
31
+ """
32
+ if json is None:
33
+ json = is_json_mode()
34
+ if json:
35
+ _json.dump(
36
+ data,
37
+ sys.stdout,
38
+ indent=2,
39
+ default=str,
40
+ )
41
+ sys.stdout.write("\n")
42
+ elif default is not None:
43
+ rich.print(default)
44
+
45
+
46
+ def print_table(
47
+ columns: list[str],
48
+ rows: Iterable[Iterable[str]],
49
+ *,
50
+ json: bool | None = None,
51
+ json_data: Any = None,
52
+ title: str | None = None,
53
+ ) -> None:
54
+ """Print a table, or JSON when ``--json`` is active.
55
+
56
+ When JSON mode is on, *json_data* is printed as JSON (or *rows* converted
57
+ to a list if no explicit json_data is given). When not in JSON mode, a
58
+ rich ``Table`` is printed.
59
+ """
60
+ if json is None:
61
+ json = is_json_mode()
62
+ if json:
63
+ _json.dump(
64
+ json_data if json_data is not None else list(rows),
65
+ sys.stdout,
66
+ indent=2,
67
+ default=str,
68
+ )
69
+ sys.stdout.write("\n")
70
+ return
71
+
72
+ from rich.table import Table
73
+
74
+ table = Table(*columns, title=title)
75
+ for row in rows:
76
+ table.add_row(*row)
77
+ rich.print(table)
78
+
79
+
80
+ def get_base_url() -> str:
81
+ """Get the web base URL from cascading CLI context or default."""
82
+ from xclif.context import get_context
83
+
84
+ try:
85
+ ctx = get_context()
86
+ raw = ctx.get("base_url", DEFAULT_BASE_URL)
87
+ except RuntimeError:
88
+ raw = DEFAULT_BASE_URL
89
+
90
+ # Normalize: ensure scheme, strip API path suffix for backward compat
91
+ if "://" not in raw:
92
+ raw = "https://" + raw
93
+ raw = raw.rstrip("/")
94
+ if raw.endswith("/api/v1"):
95
+ raw = raw[:-7]
96
+ return raw
97
+
98
+
99
+ def get_web_base_url() -> str:
100
+ """Derive the web UI base URL from the API base URL."""
101
+ return get_base_url()
102
+
103
+
104
+ def get_authenticated_client(base_url: str | None = None) -> Client | None:
105
+ """Return a Client if token is stored, else None."""
106
+ config = load_config()
107
+ token = config.get("token")
108
+ if not token:
109
+ return None
110
+ return Client(token=token, base_url=base_url or get_base_url())
111
+
112
+
113
+ def require_client(base_url: str | None = None) -> Client:
114
+ """Return an authenticated Client or print error and exit."""
115
+ client = get_authenticated_client(base_url=base_url)
116
+ if client is None:
117
+ from xclif.errors import UsageError
118
+
119
+ raise UsageError("Not logged in. Run 'cb auth login' first.")
120
+ return client
@@ -5,5 +5,9 @@ from xclif import Cascade, WithConfig, command
5
5
  @command("cb")
6
6
  def _(
7
7
  base_url: Cascade[WithConfig[str]] = DEFAULT_BASE_URL,
8
+ json: bool = False,
8
9
  ) -> None:
9
- """Interact with Codeberg or any Forgejo instance — manage repos, issues, PRs, releases, and more."""
10
+ """Interact with Codeberg or any Forgejo instance — manage repos, issues, PRs, releases, actions, and more.
11
+
12
+ Use --json on any command for machine-readable output.
13
+ """
@@ -0,0 +1,6 @@
1
+ from xclif import command
2
+
3
+
4
+ @command("actions")
5
+ def _() -> None:
6
+ """Manage Forgejo Actions — view runs, list workflows, and dispatch workflows."""
@@ -0,0 +1,31 @@
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("dispatch")
11
+ def _(
12
+ workflow: Annotated[str, Arg(description="Workflow filename (e.g. ci.yml)")],
13
+ repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
14
+ ref: Annotated[str, Option(description="Branch or tag to dispatch on", name="ref")] = "main",
15
+ ) -> None:
16
+ """Dispatch a workflow (trigger a workflow_dispatch event)."""
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
+ client.post(
27
+ f"/repos/{repo}/actions/workflows/{workflow}/dispatches",
28
+ data={"ref": ref},
29
+ action="Dispatching workflow",
30
+ )
31
+ rich.print(f"[green]Dispatched {workflow} on {ref} in {repo}.[/green]")