codeberg-cli 0.4.2__tar.gz → 0.5.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 (187) hide show
  1. codeberg_cli-0.5.0/.claude/workflows/forgejo-surface.mjs +488 -0
  2. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/PKG-INFO +15 -15
  3. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/README.md +14 -14
  4. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/pyproject.toml +1 -1
  5. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/client.py +6 -2
  6. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/assets/__init__.py +6 -0
  7. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/assets/delete.py +27 -0
  8. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/assets/list.py +47 -0
  9. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/assets/upload.py +50 -0
  10. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/deps/__init__.py +6 -0
  11. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/deps/add.py +27 -0
  12. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/deps/list.py +41 -0
  13. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/deps/remove.py +36 -0
  14. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/react.py +40 -0
  15. codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/reactions.py +39 -0
  16. codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/__init__.py +6 -0
  17. codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/gpg/__init__.py +6 -0
  18. codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/gpg/add.py +31 -0
  19. codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/gpg/delete.py +17 -0
  20. codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/gpg/list.py +29 -0
  21. codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/ssh/__init__.py +6 -0
  22. codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/ssh/add.py +31 -0
  23. codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/ssh/delete.py +17 -0
  24. codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/ssh/list.py +29 -0
  25. codeberg_cli-0.5.0/src/codeberg_cli/routes/notify/__init__.py +6 -0
  26. codeberg_cli-0.4.2/src/codeberg_cli/routes/notifications.py → codeberg_cli-0.5.0/src/codeberg_cli/routes/notify/list.py +4 -2
  27. codeberg_cli-0.5.0/src/codeberg_cli/routes/notify/read.py +23 -0
  28. codeberg_cli-0.5.0/src/codeberg_cli/routes/notify/thread.py +31 -0
  29. codeberg_cli-0.5.0/src/codeberg_cli/routes/pr/review.py +44 -0
  30. codeberg_cli-0.5.0/src/codeberg_cli/routes/pr/reviews.py +48 -0
  31. codeberg_cli-0.5.0/src/codeberg_cli/routes/pr/status.py +51 -0
  32. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/__init__.py +6 -0
  33. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/create.py +46 -0
  34. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/delete.py +26 -0
  35. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/list.py +42 -0
  36. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/test.py +26 -0
  37. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/view.py +35 -0
  38. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/__init__.py +6 -0
  39. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/create.py +47 -0
  40. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/delete.py +26 -0
  41. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/list.py +47 -0
  42. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/sync.py +25 -0
  43. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/__init__.py +6 -0
  44. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/create.py +34 -0
  45. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/delete.py +27 -0
  46. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/edit.py +39 -0
  47. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/list.py +42 -0
  48. codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/view.py +35 -0
  49. codeberg_cli-0.5.0/tests/test_issue.py +82 -0
  50. codeberg_cli-0.5.0/tests/test_keys.py +35 -0
  51. codeberg_cli-0.5.0/tests/test_notifications.py +26 -0
  52. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_pr.py +24 -0
  53. codeberg_cli-0.5.0/tests/test_push_mirror.py +38 -0
  54. codeberg_cli-0.5.0/tests/test_repo_hook.py +44 -0
  55. codeberg_cli-0.5.0/tests/test_repo_wiki.py +44 -0
  56. codeberg_cli-0.4.2/tests/test_issue.py +0 -24
  57. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/.github/workflows/ci.yml +0 -0
  58. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/.github/workflows/release.yml +0 -0
  59. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/.gitignore +0 -0
  60. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/.python-version +0 -0
  61. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/Justfile +0 -0
  62. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/LICENSE +0 -0
  63. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/__main__.py +0 -0
  64. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/config.py +0 -0
  65. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/git.py +0 -0
  66. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/helpers.py +0 -0
  67. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/__init__.py +0 -0
  68. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/__init__.py +0 -0
  69. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/_format.py +0 -0
  70. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/dispatch.py +0 -0
  71. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/run.py +0 -0
  72. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/runs.py +0 -0
  73. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/workflows.py +0 -0
  74. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/api.py +0 -0
  75. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/__init__.py +0 -0
  76. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/login.py +0 -0
  77. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/logout.py +0 -0
  78. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/status.py +0 -0
  79. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/whoami.py +0 -0
  80. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/__init__.py +0 -0
  81. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/close.py +0 -0
  82. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/comment.py +0 -0
  83. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/create.py +0 -0
  84. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/delete.py +0 -0
  85. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/edit.py +0 -0
  86. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/labels/__init__.py +0 -0
  87. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/labels/add.py +0 -0
  88. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/labels/list.py +0 -0
  89. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/labels/remove.py +0 -0
  90. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/list.py +0 -0
  91. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/pin.py +0 -0
  92. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/reopen.py +0 -0
  93. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/search.py +0 -0
  94. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/subscribe.py +0 -0
  95. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/time/__init__.py +0 -0
  96. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/time/add.py +0 -0
  97. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/time/list.py +0 -0
  98. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/unpin.py +0 -0
  99. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/unsubscribe.py +0 -0
  100. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/view.py +0 -0
  101. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/__init__.py +0 -0
  102. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/create.py +0 -0
  103. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/delete.py +0 -0
  104. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/edit.py +0 -0
  105. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/list.py +0 -0
  106. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/__init__.py +0 -0
  107. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/create.py +0 -0
  108. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/delete.py +0 -0
  109. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/edit.py +0 -0
  110. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/list.py +0 -0
  111. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/view.py +0 -0
  112. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/__init__.py +0 -0
  113. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/list.py +0 -0
  114. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/members.py +0 -0
  115. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/teams.py +0 -0
  116. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/view.py +0 -0
  117. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/__init__.py +0 -0
  118. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/check_merge.py +0 -0
  119. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/checkout.py +0 -0
  120. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/close.py +0 -0
  121. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/comment.py +0 -0
  122. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/commits.py +0 -0
  123. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/create.py +0 -0
  124. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/diff.py +0 -0
  125. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/edit.py +0 -0
  126. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/files.py +0 -0
  127. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/list.py +0 -0
  128. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/merge.py +0 -0
  129. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/reopen.py +0 -0
  130. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/update.py +0 -0
  131. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/view.py +0 -0
  132. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/__init__.py +0 -0
  133. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/create.py +0 -0
  134. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/delete.py +0 -0
  135. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/edit.py +0 -0
  136. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/list.py +0 -0
  137. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/upload.py +0 -0
  138. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/view.py +0 -0
  139. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/__init__.py +0 -0
  140. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/archive.py +0 -0
  141. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/__init__.py +0 -0
  142. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/create.py +0 -0
  143. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/delete.py +0 -0
  144. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/list.py +0 -0
  145. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/view.py +0 -0
  146. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/clone.py +0 -0
  147. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/collaborator/__init__.py +0 -0
  148. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/collaborator/add.py +0 -0
  149. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/collaborator/delete.py +0 -0
  150. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/collaborator/list.py +0 -0
  151. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/commits.py +0 -0
  152. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/contents.py +0 -0
  153. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/create.py +0 -0
  154. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/delete.py +0 -0
  155. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/edit.py +0 -0
  156. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/fork.py +0 -0
  157. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/forks/sync.py +0 -0
  158. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/forks.py +0 -0
  159. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/languages.py +0 -0
  160. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/list.py +0 -0
  161. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/migrate.py +0 -0
  162. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/search.py +0 -0
  163. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/star.py +0 -0
  164. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/stargazers.py +0 -0
  165. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/tag/__init__.py +0 -0
  166. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/tag/create.py +0 -0
  167. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/tag/delete.py +0 -0
  168. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/tag/list.py +0 -0
  169. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/topics/__init__.py +0 -0
  170. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/topics/list.py +0 -0
  171. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/topics/set.py +0 -0
  172. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/transfer.py +0 -0
  173. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/unstar.py +0 -0
  174. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/unwatch.py +0 -0
  175. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/view.py +0 -0
  176. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/watch.py +0 -0
  177. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/watchers.py +0 -0
  178. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/user.py +0 -0
  179. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_actions.py +0 -0
  180. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_api.py +0 -0
  181. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_auth.py +0 -0
  182. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_client.py +0 -0
  183. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_config.py +0 -0
  184. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_git.py +0 -0
  185. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_release.py +0 -0
  186. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_repo.py +0 -0
  187. {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/uv.lock +0 -0
@@ -0,0 +1,488 @@
1
+ export const meta = {
2
+ name: 'forgejo-surface',
3
+ description: 'Implement the remaining Forgejo API surface in cb (7 groups), each adversarially reviewed and tested',
4
+ phases: [
5
+ { title: 'Implement', detail: 'one agent per surface group writes routes + tests' },
6
+ { title: 'Review', detail: 'code-reviewer audits each group against spec + conventions' },
7
+ { title: 'Fix', detail: 'apply confirmed defects per group' },
8
+ ],
9
+ }
10
+
11
+ // ---- Shared brief: conventions every implementer + reviewer must honor ----
12
+ const CONVENTIONS = `
13
+ PROJECT: codeberg-cli ("cb"), a Python CLI built on the xclif framework. Repo root:
14
+ /Users/bryanhu/Developer/current/XClif/codeberg-cli
15
+ Source lives under src/codeberg_cli/routes/. Tests under tests/.
16
+
17
+ VERIFIED ROUTING RULES (do not deviate — confirmed from xclif source):
18
+ - One file per command. The function is ALWAYS named \`_\` and decorated
19
+ @command("name") or @command("name", "alias").
20
+ - A subcommand GROUP is a directory containing __init__.py with @command("group")
21
+ and a one-line docstring (no body). Example routes/repo/branch/__init__.py:
22
+ from xclif import command
23
+ @command("branch")
24
+ def _() -> None:
25
+ """Manage branches — list, create, view, and delete."""
26
+ - CRITICAL: the DIRECTORY NAME MUST EXACTLY EQUAL the group's @command token.
27
+ xclif derives intermediate namespace tokens from the directory name but mounts
28
+ the __init__ command by its decorator name; if they differ the tree breaks.
29
+ => Directory names must be valid Python identifiers AND the CLI token. NO HYPHENS
30
+ in directory names. (e.g. use dir "mirror/" with @command("mirror"), never
31
+ "push-mirror".)
32
+ - xclif autodiscovers the route tree by walking the package. NO central registration.
33
+ - Args: name: Annotated[int, Arg(description="...")]
34
+ - Options: name: Annotated[str, Option(description="...", name="cli-name")] = default
35
+ (option CLI names MAY contain hyphens; only DIRECTORY names may not.)
36
+ - Imports come from: from xclif import Arg, Option, command (and Command in tests).
37
+
38
+ HELPERS (from codeberg_cli.helpers):
39
+ - client = require_client() # authenticated httpx wrapper or exits
40
+ - is_json_mode() # True if --json passed
41
+ - output(obj) # prints obj as JSON when in json mode
42
+ - print_table(cols, rows, json_data=...) # table, or JSON when --json
43
+ - get_web_base_url() # web UI base url for links
44
+ Client verbs: client.get(path, params=...), client.post(path, data=...),
45
+ client.patch(path, data=...), client.put(path), client.delete(path).
46
+ DELETE returns None. GET/POST/PATCH/PUT return parsed JSON (dict or list).
47
+
48
+ REPO INFERENCE (copy VERBATIM into every repo-scoped command; skip for user-scoped):
49
+ from codeberg_cli.git import infer_repo
50
+ ...
51
+ if not repo:
52
+ inferred = infer_repo()
53
+ if not inferred:
54
+ rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
55
+ return 1
56
+ repo = inferred
57
+ The repo option is always:
58
+ repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = ""
59
+
60
+ OUTPUT / ERROR CONVENTIONS:
61
+ - List commands: build rows + json_rows, then print_table([...], rows, json_data=json_rows).
62
+ Handle empty: rich.print("[dim]No ...[/dim]"); return
63
+ - Detail commands: if is_json_mode(): output(obj); return — then rich.print human view.
64
+ - Success: rich.print("[bold green]<Verb>[/bold green] ...")
65
+ - Errors: rich.print("[bold red]Error:[/bold red] ...") then return 1
66
+ - import rich at top for rich.print.
67
+
68
+ MULTIPART UPLOAD (only for attachments) — mirror routes/release/upload.py EXACTLY:
69
+ token = client._client.headers.get("Authorization", "").replace("Bearer ", "")
70
+ headers = {"Authorization": f"Bearer {token}"}
71
+ response = httpx.post(f"{client.base_url}/<path>", headers=headers,
72
+ files={"attachment": (file_path.name, content)})
73
+
74
+ TESTS (tests/test_<area>.py) — must NOT hit the network. Pattern:
75
+ from xclif import Command
76
+ def test_<area>_commands_are_commands():
77
+ from codeberg_cli.routes.<...>.create import _ as create_cmd
78
+ ...
79
+ for cmd in [create_cmd, ...]:
80
+ assert isinstance(cmd, Command)
81
+ def test_<area>_create_name():
82
+ from codeberg_cli.routes.<...>.create import _ as cmd
83
+ assert cmd.name == "create"
84
+ Add an alias assertion where an alias exists (e.g. assert "ls" in cmd.aliases).
85
+
86
+ REFERENCE FILES TO MIRROR (read them before writing):
87
+ - src/codeberg_cli/routes/issue/list.py (list/table + repo inference + --json)
88
+ - src/codeberg_cli/routes/issue/subscribe.py (arg + put + simple success)
89
+ - src/codeberg_cli/routes/pr/view.py (detail view + is_json_mode + web flag)
90
+ - src/codeberg_cli/routes/release/upload.py (multipart upload)
91
+ - src/codeberg_cli/routes/repo/branch/__init__.py (group __init__.py shape)
92
+ - tests/test_issue.py (test shape)
93
+
94
+ Endpoint spec ground truth: /tmp/forgejo_swagger.json (Forgejo OpenAPI). When unsure
95
+ of a request/response body shape, read the relevant path's schema from that file with
96
+ python -c / a small script — do NOT guess field names.
97
+ `
98
+
99
+ // ---- Per-group implementation briefs ----
100
+ const GROUPS = [
101
+ {
102
+ key: 'keys',
103
+ label: 'keys (ssh/gpg)',
104
+ files: [
105
+ 'src/codeberg_cli/routes/keys/__init__.py',
106
+ 'src/codeberg_cli/routes/keys/ssh/__init__.py',
107
+ 'src/codeberg_cli/routes/keys/ssh/list.py',
108
+ 'src/codeberg_cli/routes/keys/ssh/add.py',
109
+ 'src/codeberg_cli/routes/keys/ssh/delete.py',
110
+ 'src/codeberg_cli/routes/keys/gpg/__init__.py',
111
+ 'src/codeberg_cli/routes/keys/gpg/list.py',
112
+ 'src/codeberg_cli/routes/keys/gpg/add.py',
113
+ 'src/codeberg_cli/routes/keys/gpg/delete.py',
114
+ 'tests/test_keys.py',
115
+ ],
116
+ brief: `
117
+ Implement \`cb keys\` — user SSH and GPG key management. USER-SCOPED: NO repo inference.
118
+
119
+ Group __init__.py files:
120
+ - routes/keys/__init__.py @command("keys") docstring "Manage your SSH and GPG keys."
121
+ - routes/keys/ssh/__init__.py @command("ssh") docstring "Manage SSH keys."
122
+ - routes/keys/gpg/__init__.py @command("gpg") docstring "Manage GPG keys."
123
+
124
+ cb keys ssh list (alias "ls"): GET /user/keys → table [ID, Title, Fingerprint] (+json_data
125
+ with id,title,fingerprint,created_at). Empty → "[dim]No SSH keys.[/dim]".
126
+ cb keys ssh add: args title (Arg), key (Arg, the public key string). Also accept
127
+ --key-file option to read the key from a file path (if given, read file, strip).
128
+ POST /user/keys data {"title": title, "key": key}. Success: green "Added SSH key" + id.
129
+ cb keys ssh delete: arg id (int). DELETE /user/keys/{id}. Success "Deleted SSH key #<id>".
130
+
131
+ cb keys gpg list (alias "ls"): GET /user/gpg_keys → table [ID, Key ID, Can Sign] (+json_data
132
+ id, key_id, can_sign, created_at). Empty → "[dim]No GPG keys.[/dim]".
133
+ cb keys gpg add: arg armored_key (Arg) OR --key-file option (read file). POST /user/gpg_keys
134
+ data {"armored_public_key": <value>}. Success green "Added GPG key" + id.
135
+ cb keys gpg delete: arg id (int). DELETE /user/gpg_keys/{id}. Success "Deleted GPG key #<id>".
136
+
137
+ tests/test_keys.py: assert each leaf is a Command; assert names; assert "ls" alias on both list cmds.
138
+ `,
139
+ },
140
+ {
141
+ key: 'pr',
142
+ label: 'pr reviews + status',
143
+ files: [
144
+ 'src/codeberg_cli/routes/pr/review.py',
145
+ 'src/codeberg_cli/routes/pr/reviews.py',
146
+ 'src/codeberg_cli/routes/pr/status.py',
147
+ 'tests/test_pr.py (APPEND new tests; do not delete existing)',
148
+ ],
149
+ brief: `
150
+ Extend the existing \`cb pr\` group (routes/pr/) with three new leaf commands. Do NOT
151
+ create a subdirectory — these are siblings of routes/pr/view.py etc. Read routes/pr/view.py
152
+ first for the exact repo-inference + json + arg pattern.
153
+
154
+ cb pr review <id>: arg id (int, "PR number"). Options:
155
+ --approve (bool), --request-changes (bool), --comment (bool), --body (str, default "").
156
+ Map to event: approve→"APPROVED", request-changes→"REQUEST_CHANGES", else "COMMENT".
157
+ If more than one of the three flags is set → error "[bold red]Error:[/bold red] Choose at
158
+ most one of --approve/--request-changes/--comment" return 1.
159
+ POST /repos/{repo}/pulls/{id}/reviews data {"event": <event>, "body": body}.
160
+ (For REQUEST_CHANGES/COMMENT a body is recommended; do not hard-require it.)
161
+ Success: green "Submitted <event> review on PR #<id>".
162
+
163
+ cb pr reviews <id> (alias none): arg id (int). GET /repos/{repo}/pulls/{id}/reviews →
164
+ table [ID, Reviewer, State, Submitted] from each review's id, user.login, state,
165
+ submitted_at. json_data list of those fields. Empty → "[dim]No reviews on PR #<id>.[/dim]".
166
+
167
+ cb pr status <id>: arg id (int). First GET /repos/{repo}/pulls/{id} to get head sha
168
+ (pr["head"]["sha"]). Then GET /repos/{repo}/commits/{sha}/status → combined status object
169
+ with keys "state" and "statuses" (list of {context, state, target_url, description}).
170
+ if is_json_mode(): output(that object); return.
171
+ Human: rich.print combined state colored (success→green, pending→yellow, error/failure→red),
172
+ then one line per status context: "<state> <context>". If no statuses: "[dim]No CI status[/dim]".
173
+
174
+ APPEND to tests/test_pr.py (keep existing tests intact): assert review/reviews/status are
175
+ Commands and have the right .name.
176
+
177
+ Verify exact field names against /tmp/forgejo_swagger.json (PullReview, CombinedStatus, CommitStatus).
178
+ `,
179
+ },
180
+ {
181
+ key: 'hook',
182
+ label: 'repo hooks',
183
+ files: [
184
+ 'src/codeberg_cli/routes/repo/hook/__init__.py',
185
+ 'src/codeberg_cli/routes/repo/hook/list.py',
186
+ 'src/codeberg_cli/routes/repo/hook/view.py',
187
+ 'src/codeberg_cli/routes/repo/hook/create.py',
188
+ 'src/codeberg_cli/routes/repo/hook/delete.py',
189
+ 'src/codeberg_cli/routes/repo/hook/test.py',
190
+ 'tests/test_repo_hook.py',
191
+ ],
192
+ brief: `
193
+ Implement \`cb repo hook\` — repository webhooks. Directory routes/repo/hook/ (dir name "hook"
194
+ == @command("hook")). All commands are repo-scoped (use the verbatim repo-inference block).
195
+
196
+ routes/repo/hook/__init__.py: @command("hook") docstring "Manage repository webhooks."
197
+
198
+ cb repo hook list (alias "ls"): GET /repos/{repo}/hooks → table [ID, Type, URL, Active]
199
+ (url from h["config"]["url"]). json_data id,type,active,events,config. Empty → "[dim]No webhooks.[/dim]".
200
+ cb repo hook view <id>: arg id (int). GET /repos/{repo}/hooks/{id}; if is_json_mode(): output; return.
201
+ Human: print id, type, active, events (comma-joined), config url + content_type.
202
+ cb repo hook create: options --type (str, default "forgejo"), --url (str, REQUIRED — if empty
203
+ error+return 1), --events (str, comma-separated, default "push"), --content-type (str,
204
+ name="content-type", default "json"), --secret (str default ""), --active (bool default True).
205
+ POST /repos/{repo}/hooks data {"type": type, "active": active,
206
+ "events": [e.strip() for e in events.split(",") if e.strip()],
207
+ "config": {"url": url, "content_type": content_type, **({"secret": secret} if secret else {})}}.
208
+ Success green "Created webhook #<id>".
209
+ cb repo hook delete: arg id (int). DELETE /repos/{repo}/hooks/{id}. Success "Deleted webhook #<id>".
210
+ cb repo hook test: arg id (int). POST /repos/{repo}/hooks/{id}/tests (data None).
211
+ Success green "Triggered test delivery for webhook #<id>".
212
+
213
+ tests/test_repo_hook.py: Commands + names + "ls" alias on list. Verify Hook/CreateHookOption
214
+ shapes against /tmp/forgejo_swagger.json.
215
+ `,
216
+ },
217
+ {
218
+ key: 'wiki',
219
+ label: 'repo wiki',
220
+ files: [
221
+ 'src/codeberg_cli/routes/repo/wiki/__init__.py',
222
+ 'src/codeberg_cli/routes/repo/wiki/list.py',
223
+ 'src/codeberg_cli/routes/repo/wiki/view.py',
224
+ 'src/codeberg_cli/routes/repo/wiki/create.py',
225
+ 'src/codeberg_cli/routes/repo/wiki/edit.py',
226
+ 'src/codeberg_cli/routes/repo/wiki/delete.py',
227
+ 'tests/test_repo_wiki.py',
228
+ ],
229
+ brief: `
230
+ Implement \`cb repo wiki\` — repository wiki pages. Directory routes/repo/wiki/. Repo-scoped.
231
+
232
+ routes/repo/wiki/__init__.py: @command("wiki") docstring "Manage wiki pages."
233
+
234
+ cb repo wiki list (alias "ls"): GET /repos/{repo}/wiki/pages → table [Title, Last Commit]
235
+ (title, last_commit.sha[:8] if present else ""). json_data title, sub_url, last_commit.
236
+ Empty → "[dim]No wiki pages.[/dim]".
237
+ cb repo wiki view <page>: arg page (str, "Page name"). GET /repos/{repo}/wiki/page/{page}
238
+ (URL-encode the page name with urllib.parse.quote, safe=""). Response has content_base64.
239
+ if is_json_mode(): output(resp); return. Human: print title bold, then decode:
240
+ import base64; text = base64.b64decode(resp["content_base64"]).decode("utf-8", "replace");
241
+ rich.print(text).
242
+ cb repo wiki create: options --title (str REQUIRED → error+return 1 if empty),
243
+ --content (str, default ""), --message (str default "", commit message).
244
+ POST /repos/{repo}/wiki/new data {"title": title, "content_base64":
245
+ base64.b64encode(content.encode()).decode(), **({"message": message} if message else {})}.
246
+ Success green "Created wiki page '<title>'".
247
+ cb repo wiki edit <page>: arg page (str). options --title (str default "" → keep), --content (str
248
+ default "" — only send if provided), --message. Build data with content_base64 from content,
249
+ title if given. PATCH /repos/{repo}/wiki/page/{page}. Success "Updated wiki page '<page>'".
250
+ cb repo wiki delete <page>: arg page. DELETE /repos/{repo}/wiki/page/{page}. Success "Deleted wiki page '<page>'".
251
+
252
+ tests/test_repo_wiki.py: Commands + names + "ls" alias on list. Verify WikiPage /
253
+ CreateWikiPageOptions field names (content_base64, message, title) against /tmp/forgejo_swagger.json.
254
+ `,
255
+ },
256
+ {
257
+ key: 'mirror',
258
+ label: 'push mirrors',
259
+ files: [
260
+ 'src/codeberg_cli/routes/repo/mirror/__init__.py',
261
+ 'src/codeberg_cli/routes/repo/mirror/list.py',
262
+ 'src/codeberg_cli/routes/repo/mirror/create.py',
263
+ 'src/codeberg_cli/routes/repo/mirror/delete.py',
264
+ 'src/codeberg_cli/routes/repo/mirror/sync.py',
265
+ 'tests/test_push_mirror.py',
266
+ ],
267
+ brief: `
268
+ Implement \`cb repo mirror\` — push mirrors. Directory routes/repo/mirror/ (token "mirror";
269
+ DO NOT use a hyphenated dir name). Repo-scoped.
270
+
271
+ routes/repo/mirror/__init__.py: @command("mirror") docstring "Manage push mirrors."
272
+
273
+ cb repo mirror list (alias "ls"): GET /repos/{repo}/push_mirrors → table
274
+ [Name, Remote, Interval, Last Update] (remote_name, remote_address, interval, last_update).
275
+ json_data those fields + sync_on_commit. Empty → "[dim]No push mirrors.[/dim]".
276
+ cb repo mirror create: options --remote (str REQUIRED → error+return 1 if empty, the remote
277
+ repo address/URL), --interval (str default "8h0m0s"), --sync-on-commit (bool name="sync-on-commit"
278
+ default False), --username (str default ""), --password (str default "").
279
+ POST /repos/{repo}/push_mirrors data {"remote_address": remote, "interval": interval,
280
+ "sync_on_commit": sync_on_commit, **({"remote_username": username} if username else {}),
281
+ **({"remote_password": password} if password else {})}.
282
+ Success green "Created push mirror to <remote>".
283
+ cb repo mirror delete <name>: arg name (str, the remote_name). DELETE /repos/{repo}/push_mirrors/{name}.
284
+ Success "Deleted push mirror '<name>'".
285
+ cb repo mirror sync: POST /repos/{repo}/push_mirrors-sync (data None). Success green
286
+ "Triggered push mirror sync for <repo>".
287
+
288
+ tests/test_push_mirror.py: Commands + names + "ls" alias on list. Verify
289
+ CreatePushMirrorOption field names against /tmp/forgejo_swagger.json.
290
+ `,
291
+ },
292
+ {
293
+ key: 'notify',
294
+ label: 'notifications (migrate to group)',
295
+ files: [
296
+ 'src/codeberg_cli/routes/notifications.py (DELETE this file)',
297
+ 'src/codeberg_cli/routes/notify/__init__.py',
298
+ 'src/codeberg_cli/routes/notify/list.py',
299
+ 'src/codeberg_cli/routes/notify/read.py',
300
+ 'src/codeberg_cli/routes/notify/thread.py',
301
+ 'tests/test_notifications.py (REWRITE for new module paths)',
302
+ ],
303
+ brief: `
304
+ Migrate notifications into a \`cb notify\` group. Read the EXISTING file first:
305
+ src/codeberg_cli/routes/notifications.py — it has the current listing logic.
306
+
307
+ STEPS:
308
+ 1. DELETE src/codeberg_cli/routes/notifications.py (it becomes the group's list command).
309
+ 2. routes/notify/__init__.py: @command("notify") docstring "View and manage notifications."
310
+ 3. routes/notify/list.py: move the EXISTING listing logic here. Decorate
311
+ @command("list", "notifications") so \`cb notify list\` works and \`cb notify notifications\`
312
+ is an alias. (Note: this is now nested under \`notify\`; the old top-level \`cb notifications\`
313
+ becomes \`cb notify notifications\` / \`cb notify list\` — that's the intended migration.)
314
+ Keep the same options (--limit, --all) and table output exactly.
315
+ 4. routes/notify/read.py: @command("read"). Options --all (bool default False),
316
+ --repo (str default "", the repo-inference block is OPTIONAL here — only infer if --repo
317
+ given OR a last-read approach). Behavior:
318
+ - if repo (explicitly given): PUT /repos/{repo}/notifications (mark that repo's read)
319
+ - else: PUT /notifications (mark ALL read)
320
+ Both accept query param "all=true" semantics via Forgejo's "status-types"; simplest: call
321
+ client.put with no body. Success green "Marked notifications as read".
322
+ (Use client.put(path) — it sends no body which is fine.)
323
+ 5. routes/notify/thread.py: @command("thread"). arg id (int, "Thread ID"). option --read (bool
324
+ default False). If --read: PATCH /notifications/threads/{id} → success "Marked thread #<id> read".
325
+ Else: GET /notifications/threads/{id}; if is_json_mode(): output; return; human-print subject
326
+ type/title and repository full_name.
327
+
328
+ REWRITE tests/test_notifications.py: import from codeberg_cli.routes.notify.list / read / thread.
329
+ Assert each is a Command, assert list_cmd.name == "list" and "notifications" in list_cmd.aliases,
330
+ assert read/thread names.
331
+
332
+ IMPORTANT: after deleting notifications.py, make sure nothing else imports it. grep the repo
333
+ for "routes.notifications" and "import notifications" and fix/none. The old test file referenced
334
+ the old module — that's why you rewrite it.
335
+ `,
336
+ },
337
+ {
338
+ key: 'issue-extras',
339
+ label: 'issue reactions/deps/attachments',
340
+ files: [
341
+ 'src/codeberg_cli/routes/issue/react.py',
342
+ 'src/codeberg_cli/routes/issue/reactions.py',
343
+ 'src/codeberg_cli/routes/issue/deps/__init__.py',
344
+ 'src/codeberg_cli/routes/issue/deps/list.py',
345
+ 'src/codeberg_cli/routes/issue/deps/add.py',
346
+ 'src/codeberg_cli/routes/issue/deps/remove.py',
347
+ 'src/codeberg_cli/routes/issue/assets/__init__.py',
348
+ 'src/codeberg_cli/routes/issue/assets/list.py',
349
+ 'src/codeberg_cli/routes/issue/assets/upload.py',
350
+ 'src/codeberg_cli/routes/issue/assets/delete.py',
351
+ 'tests/test_issue.py (APPEND; keep existing tests)',
352
+ ],
353
+ brief: `
354
+ Extend \`cb issue\` with reactions, dependencies, attachments. Read routes/issue/list.py,
355
+ routes/issue/subscribe.py, routes/release/upload.py first. All repo-scoped.
356
+
357
+ REACTIONS (sibling leaf files, NOT a subgroup):
358
+ - routes/issue/react.py @command("react"): args index (int "Issue number"), content (str
359
+ "Reaction emoji, e.g. +1, -1, heart, laugh"). option --remove (bool default False).
360
+ if remove: DELETE /repos/{repo}/issues/{index}/reactions with the body — BUT client.delete
361
+ takes no body. So for remove use client._request? NO — instead call client.post for add and
362
+ for remove use the framework: the API needs a JSON body {"content": content} on DELETE.
363
+ Use httpx via the client's underlying request for DELETE-with-body:
364
+ client._client.request("DELETE", f"/repos/{repo}/issues/{index}/reactions",
365
+ json={"content": content})
366
+ (check resp; on non-2xx rich red error + return 1). For add: client.post(
367
+ f"/repos/{repo}/issues/{index}/reactions", data={"content": content}).
368
+ Success: "Added reaction :<content>:" / "Removed reaction :<content>:".
369
+ - routes/issue/reactions.py @command("reactions"): arg index (int). GET
370
+ /repos/{repo}/issues/{index}/reactions → table [User, Reaction] (user.login, content).
371
+ json_data list. Empty → "[dim]No reactions.[/dim]".
372
+
373
+ DEPENDENCIES — group routes/issue/deps/ (dir "deps"):
374
+ - deps/__init__.py @command("deps") docstring "Manage issue dependencies (blocked-by)."
375
+ - deps/list.py @command("list","ls"): arg index (int). GET /repos/{repo}/issues/{index}/dependencies
376
+ → table [#, Title, State] (number,title,state). json_data. Empty → "[dim]No dependencies.[/dim]".
377
+ - deps/add.py @command("add"): args index (int "Issue number"), depends_on (int "Issue number
378
+ this one depends on"). POST /repos/{repo}/issues/{index}/dependencies data
379
+ {"index": depends_on}. (Same-repo dependency.) Success "Issue #<index> now depends on #<depends_on>".
380
+ - deps/remove.py @command("remove"): args index, depends_on. DELETE-with-body like reactions:
381
+ client._client.request("DELETE", ".../dependencies", json={"index": depends_on}); check resp.
382
+ Success "Removed dependency #<depends_on> from issue #<index>".
383
+
384
+ ATTACHMENTS — group routes/issue/assets/ (dir "assets"):
385
+ - assets/__init__.py @command("assets") docstring "Manage issue attachments."
386
+ - assets/list.py @command("list","ls"): arg index (int). GET /repos/{repo}/issues/{index}/assets
387
+ → table [ID, Name, Size, Download] (id,name,size,browser_download_url). json_data. Empty msg.
388
+ - assets/upload.py @command("upload"): args index (int), file (str). Mirror release/upload.py
389
+ multipart EXACTLY but POST to f"{client.base_url}/repos/{repo}/issues/{index}/assets" with
390
+ files={"attachment": (file_path.name, content)}. Check file exists first (red error+return 1).
391
+ Success green "Uploaded <name> to issue #<index>".
392
+ - assets/delete.py @command("delete"): args index (int), attachment_id (int). DELETE
393
+ /repos/{repo}/issues/{index}/assets/{attachment_id}. Success "Deleted attachment #<attachment_id>".
394
+
395
+ APPEND to tests/test_issue.py (DO NOT remove existing tests): import the new commands, assert
396
+ Commands, assert names, assert "ls" alias on deps/list and assets/list.
397
+
398
+ Verify Reaction / IssueDependency / Attachment shapes against /tmp/forgejo_swagger.json.
399
+ `,
400
+ },
401
+ ]
402
+
403
+ phase('Implement')
404
+ log(`Implementing ${GROUPS.length} Forgejo surface groups in parallel, each reviewed + fixed independently.`)
405
+
406
+ const REVIEW_SCHEMA = {
407
+ type: 'object',
408
+ additionalProperties: false,
409
+ properties: {
410
+ defects: {
411
+ type: 'array',
412
+ items: {
413
+ type: 'object',
414
+ additionalProperties: false,
415
+ properties: {
416
+ file: { type: 'string' },
417
+ severity: { type: 'string', enum: ['critical', 'major', 'minor'] },
418
+ issue: { type: 'string' },
419
+ fix: { type: 'string' },
420
+ },
421
+ required: ['file', 'severity', 'issue', 'fix'],
422
+ },
423
+ },
424
+ summary: { type: 'string' },
425
+ },
426
+ required: ['defects', 'summary'],
427
+ }
428
+
429
+ const results = await pipeline(
430
+ GROUPS,
431
+ // Stage 1: implement
432
+ (g) =>
433
+ agent(
434
+ `${CONVENTIONS}\n\n=== YOUR TASK: implement group "${g.label}" ===\n${g.brief}\n\n` +
435
+ `Write every file listed. Create __init__.py for any new directory. Run NO build/test ` +
436
+ `commands yourself beyond reading files and the swagger spec. When done, return a short ` +
437
+ `confirmation listing the files you wrote.`,
438
+ { label: `impl:${g.key}`, phase: 'Implement' }
439
+ ).then((res) => ({ g, implReport: res })),
440
+ // Stage 2: adversarial review
441
+ ({ g }) =>
442
+ agent(
443
+ `${CONVENTIONS}\n\n=== ADVERSARIAL REVIEW of group "${g.label}" ===\n` +
444
+ `These files were just written:\n${g.files.map((f) => ' - ' + f).join('\n')}\n\n` +
445
+ `Read each one. Check, with skepticism:\n` +
446
+ `1. ROUTING: every group directory name EXACTLY equals its @command token (no hyphens in dir names). ` +
447
+ `Leaf functions named \`_\`, decorated correctly.\n` +
448
+ `2. API CORRECTNESS: paths and request/response field names match the Forgejo spec at ` +
449
+ `/tmp/forgejo_swagger.json (read the relevant schemas — do not assume). Flag wrong paths, ` +
450
+ `wrong field names, wrong HTTP verbs, missing required body fields.\n` +
451
+ `3. CONVENTIONS: repo-inference block present & verbatim for repo-scoped cmds (absent for ` +
452
+ `user-scoped); require_client() used; --json handled via is_json_mode()/output()/print_table; ` +
453
+ `errors use the red-error + return 1 pattern; imports correct (Arg/Option/command/rich/httpx).\n` +
454
+ `4. DELETE-with-body and multipart cases use the documented workaround correctly.\n` +
455
+ `5. TESTS exist, assert Command-ness + names + aliases, and do NOT hit the network.\n` +
456
+ `6. For the notify group: notifications.py is deleted and nothing imports it.\n` +
457
+ `Report ONLY real defects with a concrete fix each. If clean, return empty defects.`,
458
+ { label: `review:${g.key}`, phase: 'Review', schema: REVIEW_SCHEMA, agentType: 'feature-dev:code-reviewer' }
459
+ ).then((review) => ({ g, review })),
460
+ // Stage 3: fix confirmed defects
461
+ ({ g, review }) => {
462
+ const real = (review?.defects || []).filter((d) => d.severity !== 'minor')
463
+ if (!real.length) {
464
+ return Promise.resolve({ g, review, fixReport: 'no actionable defects' })
465
+ }
466
+ return agent(
467
+ `${CONVENTIONS}\n\n=== APPLY FIXES to group "${g.label}" ===\n` +
468
+ `A reviewer found these defects. Fix each in the listed files ONLY (do not touch other ` +
469
+ `groups). After fixing, re-read to confirm. Defects:\n` +
470
+ real
471
+ .map((d, i) => `${i + 1}. [${d.severity}] ${d.file}: ${d.issue}\n FIX: ${d.fix}`)
472
+ .join('\n') +
473
+ `\n\nReturn a short confirmation of what you changed.`,
474
+ { label: `fix:${g.key}`, phase: 'Fix' }
475
+ ).then((fixReport) => ({ g, review, fixReport }))
476
+ }
477
+ )
478
+
479
+ const clean = results.filter(Boolean)
480
+ return {
481
+ groups: clean.map((r) => ({
482
+ group: r.g.label,
483
+ reviewSummary: r.review?.summary,
484
+ defectCount: (r.review?.defects || []).length,
485
+ actionableFixed: (r.review?.defects || []).filter((d) => d.severity !== 'minor').length,
486
+ fix: r.fixReport,
487
+ })),
488
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeberg-cli
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: A Forgejo CLI — works with Codeberg and any Forgejo instance
5
5
  Project-URL: Homepage, https://codeberg.org/ThatXliner/codeberg-cli
6
6
  Project-URL: Repository, https://codeberg.org/ThatXliner/codeberg-cli
@@ -93,7 +93,7 @@ cb config set base_url "https://git.example.com/api/v1" # Self-hosted Forgejo
93
93
 
94
94
  ## Comparison
95
95
 
96
- Here's how `cb` stacks up against other Forgejo CLI tools. *Last updated: May 2026.*
96
+ Here's how `cb` stacks up against other Forgejo CLI tools. *Last updated: June 2026.*
97
97
 
98
98
  TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
99
99
 
@@ -116,12 +116,12 @@ TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
116
116
  | Delete | ✅ | — | — | — | — |
117
117
  | Pin/Unpin | ✅ | — | — | ✅ | — |
118
118
  | Search | ✅ | ✅ | — | ✅ | — |
119
- | Attachments | | — | — | ✅ | — |
119
+ | Attachments | | — | — | ✅ | — |
120
120
  | Labels (manage on issue) | ✅ | — | — | ✅ | ✅ |
121
- | Reactions | | — | — | — | — |
121
+ | Reactions | | — | — | — | — |
122
122
  | Subscriptions | ✅ | — | — | ✅ | — |
123
123
  | Tracked times | ✅ | — | — | ✅ | ✅ |
124
- | Dependencies | | — | — | — | — |
124
+ | Dependencies | | — | — | — | — |
125
125
  | Deadline | — | — | — | ✅ | — |
126
126
  | Templates | — | ✅ | — | — | — |
127
127
 
@@ -139,11 +139,11 @@ TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
139
139
  | Checkout | ✅ | ✅ | ✅ | ✅ | ✅ |
140
140
  | Commits | ✅ | ✅ | — | — | ✅ |
141
141
  | Files/changed | ✅ | ✅ | — | — | — |
142
- | Reviews | | ✅ | — | ✅ | ✅ |
142
+ | Reviews | | ✅ | — | ✅ | ✅ |
143
143
  | Diff/Patch | ✅ | ✅ | — | — | ✅ |
144
144
  | Update branch | ✅ | — | — | — | — |
145
145
  | AGit (no-fork) | — | ✅ | — | — | — |
146
- | CI status | | ✅ | — | — | — |
146
+ | CI status | | ✅ | — | — | — |
147
147
  | Templates | — | ✅ | — | — | — |
148
148
  | Auto-merge | — | — | — | ✅ | — |
149
149
 
@@ -176,14 +176,14 @@ TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
176
176
  | Branches | ✅ | — | — | ✅ | — |
177
177
  | Topics | ✅ | — | — | ✅ | — |
178
178
  | Languages | ✅ | — | — | — | — |
179
- | Hooks | | — | — | ✅ | — |
179
+ | Hooks | | — | — | ✅ | — |
180
180
  | Archive | ✅ | — | — | ✅ | — |
181
181
  | Commits | ✅ | — | — | — | — |
182
182
  | Contents | ✅ | — | — | ✅ | — |
183
183
  | Collaborators | ✅ | — | — | ✅ | — |
184
184
  | Transfer | ✅ | — | — | ✅ | — |
185
- | Wikis | | — | — | ✅ | — |
186
- | Push mirrors | | — | — | ✅ | — |
185
+ | Wikis | | — | — | ✅ | — |
186
+ | Push mirrors | | — | — | ✅ | — |
187
187
  | Search | ✅ | — | — | ✅ | — |
188
188
 
189
189
  ### Labels
@@ -207,17 +207,17 @@ TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
207
207
  | | **cb** | **fj** | **berg** | **tea** | **gcli** |
208
208
  |---|---|---|---|---|---|
209
209
  | List | ✅ | — | ✅ | ✅ | ✅ |
210
- | Mark read | | — | — | ✅ | — |
211
- | Thread details | | — | — | — | — |
212
- | Per-repo | | — | — | ✅ | — |
210
+ | Mark read | | — | — | ✅ | — |
211
+ | Thread details | | — | — | — | — |
212
+ | Per-repo | | — | — | ✅ | — |
213
213
 
214
214
  ### Extra
215
215
  | | **cb** | **fj** | **berg** | **tea** | **gcli** |
216
216
  |---|---|---|---|---|---|
217
217
  | Raw API | ✅ | — | ✅ | — | ✅ |
218
218
  | User profiles | ✅ | ✅ | — | — | — |
219
- | SSH keys | | ✅ | — | ✅ | ✅ |
220
- | GPG keys | | ✅ | — | — | — |
219
+ | SSH keys | | ✅ | — | ✅ | ✅ |
220
+ | GPG keys | | ✅ | — | — | — |
221
221
  | Org/team mgmt | ✅ | ✅ | — | ✅ | — |
222
222
  | Forgejo Actions | ✅ | ✅ | — | — | — |
223
223
  | Shell completions | ✅ (auto via xclif) | ✅ | ✅ | ✅ | — |
@@ -68,7 +68,7 @@ cb config set base_url "https://git.example.com/api/v1" # Self-hosted Forgejo
68
68
 
69
69
  ## Comparison
70
70
 
71
- Here's how `cb` stacks up against other Forgejo CLI tools. *Last updated: May 2026.*
71
+ Here's how `cb` stacks up against other Forgejo CLI tools. *Last updated: June 2026.*
72
72
 
73
73
  TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
74
74
 
@@ -91,12 +91,12 @@ TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
91
91
  | Delete | ✅ | — | — | — | — |
92
92
  | Pin/Unpin | ✅ | — | — | ✅ | — |
93
93
  | Search | ✅ | ✅ | — | ✅ | — |
94
- | Attachments | | — | — | ✅ | — |
94
+ | Attachments | | — | — | ✅ | — |
95
95
  | Labels (manage on issue) | ✅ | — | — | ✅ | ✅ |
96
- | Reactions | | — | — | — | — |
96
+ | Reactions | | — | — | — | — |
97
97
  | Subscriptions | ✅ | — | — | ✅ | — |
98
98
  | Tracked times | ✅ | — | — | ✅ | ✅ |
99
- | Dependencies | | — | — | — | — |
99
+ | Dependencies | | — | — | — | — |
100
100
  | Deadline | — | — | — | ✅ | — |
101
101
  | Templates | — | ✅ | — | — | — |
102
102
 
@@ -114,11 +114,11 @@ TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
114
114
  | Checkout | ✅ | ✅ | ✅ | ✅ | ✅ |
115
115
  | Commits | ✅ | ✅ | — | — | ✅ |
116
116
  | Files/changed | ✅ | ✅ | — | — | — |
117
- | Reviews | | ✅ | — | ✅ | ✅ |
117
+ | Reviews | | ✅ | — | ✅ | ✅ |
118
118
  | Diff/Patch | ✅ | ✅ | — | — | ✅ |
119
119
  | Update branch | ✅ | — | — | — | — |
120
120
  | AGit (no-fork) | — | ✅ | — | — | — |
121
- | CI status | | ✅ | — | — | — |
121
+ | CI status | | ✅ | — | — | — |
122
122
  | Templates | — | ✅ | — | — | — |
123
123
  | Auto-merge | — | — | — | ✅ | — |
124
124
 
@@ -151,14 +151,14 @@ TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
151
151
  | Branches | ✅ | — | — | ✅ | — |
152
152
  | Topics | ✅ | — | — | ✅ | — |
153
153
  | Languages | ✅ | — | — | — | — |
154
- | Hooks | | — | — | ✅ | — |
154
+ | Hooks | | — | — | ✅ | — |
155
155
  | Archive | ✅ | — | — | ✅ | — |
156
156
  | Commits | ✅ | — | — | — | — |
157
157
  | Contents | ✅ | — | — | ✅ | — |
158
158
  | Collaborators | ✅ | — | — | ✅ | — |
159
159
  | Transfer | ✅ | — | — | ✅ | — |
160
- | Wikis | | — | — | ✅ | — |
161
- | Push mirrors | | — | — | ✅ | — |
160
+ | Wikis | | — | — | ✅ | — |
161
+ | Push mirrors | | — | — | ✅ | — |
162
162
  | Search | ✅ | — | — | ✅ | — |
163
163
 
164
164
  ### Labels
@@ -182,17 +182,17 @@ TL;DR: **`cb` is the most feature-complete Forgejo CLI** — by a wide margin.
182
182
  | | **cb** | **fj** | **berg** | **tea** | **gcli** |
183
183
  |---|---|---|---|---|---|
184
184
  | List | ✅ | — | ✅ | ✅ | ✅ |
185
- | Mark read | | — | — | ✅ | — |
186
- | Thread details | | — | — | — | — |
187
- | Per-repo | | — | — | ✅ | — |
185
+ | Mark read | | — | — | ✅ | — |
186
+ | Thread details | | — | — | — | — |
187
+ | Per-repo | | — | — | ✅ | — |
188
188
 
189
189
  ### Extra
190
190
  | | **cb** | **fj** | **berg** | **tea** | **gcli** |
191
191
  |---|---|---|---|---|---|
192
192
  | Raw API | ✅ | — | ✅ | — | ✅ |
193
193
  | User profiles | ✅ | ✅ | — | — | — |
194
- | SSH keys | | ✅ | — | ✅ | ✅ |
195
- | GPG keys | | ✅ | — | — | — |
194
+ | SSH keys | | ✅ | — | ✅ | ✅ |
195
+ | GPG keys | | ✅ | — | — | — |
196
196
  | Org/team mgmt | ✅ | ✅ | — | ✅ | — |
197
197
  | Forgejo Actions | ✅ | ✅ | — | — | — |
198
198
  | Shell completions | ✅ (auto via xclif) | ✅ | ✅ | ✅ | — |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codeberg-cli"
3
- version = "0.4.2"
3
+ version = "0.5.0"
4
4
  description = "A Forgejo CLI — works with Codeberg and any Forgejo instance"
5
5
  readme = "README.md"
6
6
  license = "MIT"