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.
- codeberg_cli-0.5.0/.claude/workflows/forgejo-surface.mjs +488 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/PKG-INFO +15 -15
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/README.md +14 -14
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/pyproject.toml +1 -1
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/client.py +6 -2
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/assets/__init__.py +6 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/assets/delete.py +27 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/assets/list.py +47 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/assets/upload.py +50 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/deps/__init__.py +6 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/deps/add.py +27 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/deps/list.py +41 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/deps/remove.py +36 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/react.py +40 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/issue/reactions.py +39 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/__init__.py +6 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/gpg/__init__.py +6 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/gpg/add.py +31 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/gpg/delete.py +17 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/gpg/list.py +29 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/ssh/__init__.py +6 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/ssh/add.py +31 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/ssh/delete.py +17 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/keys/ssh/list.py +29 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/notify/__init__.py +6 -0
- 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
- codeberg_cli-0.5.0/src/codeberg_cli/routes/notify/read.py +23 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/notify/thread.py +31 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/pr/review.py +44 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/pr/reviews.py +48 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/pr/status.py +51 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/__init__.py +6 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/create.py +46 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/delete.py +26 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/list.py +42 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/test.py +26 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/hook/view.py +35 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/__init__.py +6 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/create.py +47 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/delete.py +26 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/list.py +47 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/mirror/sync.py +25 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/__init__.py +6 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/create.py +34 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/delete.py +27 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/edit.py +39 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/list.py +42 -0
- codeberg_cli-0.5.0/src/codeberg_cli/routes/repo/wiki/view.py +35 -0
- codeberg_cli-0.5.0/tests/test_issue.py +82 -0
- codeberg_cli-0.5.0/tests/test_keys.py +35 -0
- codeberg_cli-0.5.0/tests/test_notifications.py +26 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_pr.py +24 -0
- codeberg_cli-0.5.0/tests/test_push_mirror.py +38 -0
- codeberg_cli-0.5.0/tests/test_repo_hook.py +44 -0
- codeberg_cli-0.5.0/tests/test_repo_wiki.py +44 -0
- codeberg_cli-0.4.2/tests/test_issue.py +0 -24
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/.github/workflows/ci.yml +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/.github/workflows/release.yml +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/.gitignore +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/.python-version +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/Justfile +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/LICENSE +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/__main__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/config.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/git.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/helpers.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/_format.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/dispatch.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/run.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/runs.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/actions/workflows.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/api.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/login.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/logout.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/status.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/auth/whoami.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/close.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/comment.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/create.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/delete.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/edit.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/labels/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/labels/add.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/labels/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/labels/remove.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/pin.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/reopen.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/search.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/subscribe.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/time/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/time/add.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/time/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/unpin.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/unsubscribe.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/issue/view.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/create.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/delete.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/edit.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/label/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/create.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/delete.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/edit.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/milestone/view.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/members.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/teams.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/org/view.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/check_merge.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/checkout.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/close.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/comment.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/commits.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/create.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/diff.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/edit.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/files.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/merge.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/reopen.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/update.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/pr/view.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/create.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/delete.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/edit.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/upload.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/release/view.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/archive.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/create.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/delete.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/branch/view.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/clone.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/collaborator/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/collaborator/add.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/collaborator/delete.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/collaborator/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/commits.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/contents.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/create.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/delete.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/edit.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/fork.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/forks/sync.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/forks.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/languages.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/migrate.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/search.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/star.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/stargazers.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/tag/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/tag/create.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/tag/delete.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/tag/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/topics/__init__.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/topics/list.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/topics/set.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/transfer.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/unstar.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/unwatch.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/view.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/watch.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/repo/watchers.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/src/codeberg_cli/routes/user.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_actions.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_api.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_auth.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_client.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_config.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_git.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_release.py +0 -0
- {codeberg_cli-0.4.2 → codeberg_cli-0.5.0}/tests/test_repo.py +0 -0
- {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.
|
|
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:
|
|
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:
|
|
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) | ✅ | ✅ | ✅ | — |
|