kctl-github 0.2.0__py3-none-any.whl
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.
- kctl_github/__init__.py +3 -0
- kctl_github/__main__.py +5 -0
- kctl_github/cli.py +133 -0
- kctl_github/commands/__init__.py +0 -0
- kctl_github/commands/billing.py +182 -0
- kctl_github/commands/ci.py +271 -0
- kctl_github/commands/config_cmd.py +196 -0
- kctl_github/commands/dashboard.py +89 -0
- kctl_github/commands/doctor_cmd.py +82 -0
- kctl_github/commands/health.py +63 -0
- kctl_github/commands/labels.py +131 -0
- kctl_github/commands/prs.py +161 -0
- kctl_github/commands/repos.py +179 -0
- kctl_github/commands/secrets.py +132 -0
- kctl_github/commands/skill_cmd.py +76 -0
- kctl_github/commands/stats.py +208 -0
- kctl_github/core/__init__.py +0 -0
- kctl_github/core/callbacks.py +40 -0
- kctl_github/core/client.py +124 -0
- kctl_github/core/config.py +55 -0
- kctl_github/core/exceptions.py +21 -0
- kctl_github/core/plugins.py +13 -0
- kctl_github-0.2.0.dist-info/METADATA +17 -0
- kctl_github-0.2.0.dist-info/RECORD +26 -0
- kctl_github-0.2.0.dist-info/WHEEL +4 -0
- kctl_github-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Repository statistics commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime, timedelta
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from kctl_github.core.callbacks import AppContext
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Repository statistics across kodemeio-* repos.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def overview(ctx: typer.Context) -> None:
|
|
17
|
+
"""Total repos, total stars, total issues, total PRs."""
|
|
18
|
+
actx: AppContext = ctx.obj
|
|
19
|
+
out = actx.output
|
|
20
|
+
client = actx.client
|
|
21
|
+
|
|
22
|
+
repos = client.get_repos()
|
|
23
|
+
total_stars = sum(r.get("stargazers_count", 0) for r in repos)
|
|
24
|
+
total_forks = sum(r.get("forks_count", 0) for r in repos)
|
|
25
|
+
total_issues = sum(r.get("open_issues_count", 0) for r in repos)
|
|
26
|
+
total_size = sum(r.get("size", 0) for r in repos)
|
|
27
|
+
|
|
28
|
+
if out.json_mode:
|
|
29
|
+
out.raw_json(
|
|
30
|
+
{
|
|
31
|
+
"repos": len(repos),
|
|
32
|
+
"stars": total_stars,
|
|
33
|
+
"forks": total_forks,
|
|
34
|
+
"open_issues": total_issues,
|
|
35
|
+
"total_size_kb": total_size,
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
sections = [
|
|
41
|
+
(
|
|
42
|
+
"Overview",
|
|
43
|
+
[
|
|
44
|
+
("Repositories", str(len(repos))),
|
|
45
|
+
("Total Stars", str(total_stars)),
|
|
46
|
+
("Total Forks", str(total_forks)),
|
|
47
|
+
("Open Issues", str(total_issues)),
|
|
48
|
+
("Total Size", f"{total_size:,} KB ({total_size // 1024} MB)"),
|
|
49
|
+
],
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
out.detail("GitHub Stats Overview", sections)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def activity(
|
|
57
|
+
ctx: typer.Context,
|
|
58
|
+
period: Annotated[str, typer.Option("--period", help="Time period (e.g., 7d, 30d)")] = "30d",
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Commit activity, PR merge rate, issue velocity."""
|
|
61
|
+
actx: AppContext = ctx.obj
|
|
62
|
+
out = actx.output
|
|
63
|
+
client = actx.client
|
|
64
|
+
|
|
65
|
+
days = int(period.rstrip("d"))
|
|
66
|
+
since = datetime.now(UTC) - timedelta(days=days)
|
|
67
|
+
since_str = since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
68
|
+
|
|
69
|
+
repos = client.get_repos()
|
|
70
|
+
rows = []
|
|
71
|
+
|
|
72
|
+
for repo in sorted(repos, key=lambda x: x["name"]):
|
|
73
|
+
name = repo["name"]
|
|
74
|
+
owner = client.organization
|
|
75
|
+
|
|
76
|
+
# Commits since period
|
|
77
|
+
try:
|
|
78
|
+
commits = client.get(
|
|
79
|
+
f"/repos/{owner}/{name}/commits",
|
|
80
|
+
params={"since": since_str, "per_page": 100},
|
|
81
|
+
)
|
|
82
|
+
commit_count = len(commits) if isinstance(commits, list) else 0
|
|
83
|
+
except Exception: # noqa: BLE001
|
|
84
|
+
commit_count = 0
|
|
85
|
+
|
|
86
|
+
# Merged PRs in period
|
|
87
|
+
try:
|
|
88
|
+
prs = client.get(
|
|
89
|
+
f"/repos/{owner}/{name}/pulls",
|
|
90
|
+
params={"state": "closed", "sort": "updated", "direction": "desc", "per_page": 50},
|
|
91
|
+
)
|
|
92
|
+
merged_count = 0
|
|
93
|
+
if isinstance(prs, list):
|
|
94
|
+
for pr in prs:
|
|
95
|
+
if pr.get("merged_at"):
|
|
96
|
+
try:
|
|
97
|
+
merged_dt = datetime.fromisoformat(pr["merged_at"].replace("Z", "+00:00"))
|
|
98
|
+
if merged_dt >= since:
|
|
99
|
+
merged_count += 1
|
|
100
|
+
except (ValueError, TypeError):
|
|
101
|
+
pass
|
|
102
|
+
except Exception: # noqa: BLE001
|
|
103
|
+
merged_count = 0
|
|
104
|
+
|
|
105
|
+
if commit_count > 0 or merged_count > 0:
|
|
106
|
+
rows.append([name, str(commit_count), str(merged_count)])
|
|
107
|
+
|
|
108
|
+
if out.json_mode:
|
|
109
|
+
out.raw_json(
|
|
110
|
+
[
|
|
111
|
+
{
|
|
112
|
+
"repo": r[0],
|
|
113
|
+
"commits": int(r[1]),
|
|
114
|
+
"merged_prs": int(r[2]),
|
|
115
|
+
}
|
|
116
|
+
for r in rows
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
out.table(
|
|
122
|
+
f"Activity (last {days} days)",
|
|
123
|
+
[("Repo", "cyan"), ("Commits", "green"), ("Merged PRs", "yellow")],
|
|
124
|
+
rows,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.command()
|
|
129
|
+
def languages(ctx: typer.Context) -> None:
|
|
130
|
+
"""Language breakdown across all repos."""
|
|
131
|
+
actx: AppContext = ctx.obj
|
|
132
|
+
out = actx.output
|
|
133
|
+
client = actx.client
|
|
134
|
+
|
|
135
|
+
repos = client.get_repos()
|
|
136
|
+
lang_totals: dict[str, int] = {}
|
|
137
|
+
|
|
138
|
+
for repo in repos:
|
|
139
|
+
name = repo["name"]
|
|
140
|
+
try:
|
|
141
|
+
langs = client.get(f"/repos/{client.organization}/{name}/languages")
|
|
142
|
+
if isinstance(langs, dict):
|
|
143
|
+
for lang, bytes_count in langs.items():
|
|
144
|
+
lang_totals[lang] = lang_totals.get(lang, 0) + bytes_count
|
|
145
|
+
except Exception: # noqa: BLE001
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
total = sum(lang_totals.values())
|
|
149
|
+
sorted_langs = sorted(lang_totals.items(), key=lambda x: x[1], reverse=True)
|
|
150
|
+
|
|
151
|
+
if out.json_mode:
|
|
152
|
+
out.raw_json(
|
|
153
|
+
{
|
|
154
|
+
lang: {"bytes": b, "percentage": f"{(b / total * 100):.1f}%"} if total else {"bytes": b}
|
|
155
|
+
for lang, b in sorted_langs
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
rows = []
|
|
161
|
+
for lang, bytes_count in sorted_langs[:20]:
|
|
162
|
+
pct = (bytes_count / total * 100) if total else 0
|
|
163
|
+
bar = "#" * int(pct / 2)
|
|
164
|
+
rows.append([lang, f"{bytes_count:,}", f"{pct:.1f}%", bar])
|
|
165
|
+
|
|
166
|
+
out.table(
|
|
167
|
+
"Languages (all repos)",
|
|
168
|
+
[("Language", "cyan"), ("Bytes", ""), ("%", "yellow"), ("Distribution", "green")],
|
|
169
|
+
rows,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command()
|
|
174
|
+
def contributors(ctx: typer.Context) -> None:
|
|
175
|
+
"""Contributor activity across all repos."""
|
|
176
|
+
actx: AppContext = ctx.obj
|
|
177
|
+
out = actx.output
|
|
178
|
+
client = actx.client
|
|
179
|
+
|
|
180
|
+
repos = client.get_repos()
|
|
181
|
+
contributor_totals: dict[str, int] = {}
|
|
182
|
+
|
|
183
|
+
for repo in repos:
|
|
184
|
+
name = repo["name"]
|
|
185
|
+
try:
|
|
186
|
+
contribs = client.get(
|
|
187
|
+
f"/repos/{client.organization}/{name}/contributors",
|
|
188
|
+
params={"per_page": 30},
|
|
189
|
+
)
|
|
190
|
+
if isinstance(contribs, list):
|
|
191
|
+
for c in contribs:
|
|
192
|
+
login = c.get("login", "unknown")
|
|
193
|
+
contributor_totals[login] = contributor_totals.get(login, 0) + c.get("contributions", 0)
|
|
194
|
+
except Exception: # noqa: BLE001
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
sorted_contribs = sorted(contributor_totals.items(), key=lambda x: x[1], reverse=True)
|
|
198
|
+
|
|
199
|
+
if out.json_mode:
|
|
200
|
+
out.raw_json([{"login": login, "total_contributions": count} for login, count in sorted_contribs])
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
rows = [[login, str(count)] for login, count in sorted_contribs[:20]]
|
|
204
|
+
out.table(
|
|
205
|
+
"Top Contributors (all repos)",
|
|
206
|
+
[("Login", "cyan"), ("Total Contributions", "yellow")],
|
|
207
|
+
rows,
|
|
208
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Application context for kctl-github."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from kctl_lib.callbacks import AppContextBase
|
|
8
|
+
|
|
9
|
+
from kctl_github.core.client import GitHubClient
|
|
10
|
+
from kctl_github.core.config import (
|
|
11
|
+
ServiceConfig,
|
|
12
|
+
get_service_config,
|
|
13
|
+
resolve_active_profile_name,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AppContext(AppContextBase):
|
|
19
|
+
"""kctl-github application context."""
|
|
20
|
+
|
|
21
|
+
_client: GitHubClient | None = field(default=None, repr=False)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def client(self) -> GitHubClient:
|
|
25
|
+
"""Lazy-initialize GitHub API client from active profile config."""
|
|
26
|
+
if self._client is None:
|
|
27
|
+
profile = resolve_active_profile_name(self.profile)
|
|
28
|
+
cfg = get_service_config(profile)
|
|
29
|
+
self._client = GitHubClient(
|
|
30
|
+
credential=cfg.token,
|
|
31
|
+
organization=cfg.organization,
|
|
32
|
+
repo_prefix=cfg.repo_prefix,
|
|
33
|
+
)
|
|
34
|
+
return self._client
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def config(self) -> ServiceConfig:
|
|
38
|
+
"""Get the resolved service config."""
|
|
39
|
+
profile = resolve_active_profile_name(self.profile)
|
|
40
|
+
return get_service_config(profile)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""GitHub API client and gh CLI helper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import parse_qs, urlparse
|
|
7
|
+
|
|
8
|
+
from kctl_lib.api_client import APIClient
|
|
9
|
+
from kctl_lib.runner import run
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GitHubClient(APIClient):
|
|
13
|
+
"""Synchronous GitHub REST API client.
|
|
14
|
+
|
|
15
|
+
Subclasses APIClient with GitHub-specific defaults.
|
|
16
|
+
Provides helpers for paginated listing and repo filtering.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
BASE_URL = "https://api.github.com"
|
|
20
|
+
AUTH_HEADER = "Authorization"
|
|
21
|
+
AUTH_PREFIX = "Bearer"
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
credential: str = "",
|
|
26
|
+
organization: str = "tgunawandev",
|
|
27
|
+
repo_prefix: str = "kodemeio-",
|
|
28
|
+
timeout: float = 30.0,
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(credential=credential, timeout=timeout, **kwargs)
|
|
32
|
+
self._organization = organization
|
|
33
|
+
self._repo_prefix = repo_prefix
|
|
34
|
+
# Add Accept header for GitHub API v3
|
|
35
|
+
self._client.headers["Accept"] = "application/vnd.github+json"
|
|
36
|
+
self._client.headers["X-GitHub-Api-Version"] = "2022-11-28"
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------------
|
|
39
|
+
# Pagination helper
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def get_paginated(
|
|
43
|
+
self,
|
|
44
|
+
endpoint: str,
|
|
45
|
+
params: dict[str, Any] | None = None,
|
|
46
|
+
max_pages: int = 10,
|
|
47
|
+
) -> list[dict[str, Any]]:
|
|
48
|
+
"""Fetch all pages from a paginated GitHub endpoint.
|
|
49
|
+
|
|
50
|
+
GitHub uses Link headers for pagination. This method follows
|
|
51
|
+
``next`` links up to *max_pages*.
|
|
52
|
+
"""
|
|
53
|
+
all_items: list[dict[str, Any]] = []
|
|
54
|
+
_params = dict(params or {})
|
|
55
|
+
_params.setdefault("per_page", 100)
|
|
56
|
+
|
|
57
|
+
for _ in range(max_pages):
|
|
58
|
+
response = self._request("GET", endpoint, params=_params)
|
|
59
|
+
data = response.json()
|
|
60
|
+
if isinstance(data, list):
|
|
61
|
+
all_items.extend(data)
|
|
62
|
+
else:
|
|
63
|
+
# Some endpoints return objects with items inside
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
# Check for next page via Link header
|
|
67
|
+
link = response.headers.get("link", "")
|
|
68
|
+
if 'rel="next"' not in link:
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
# Parse next URL from Link header
|
|
72
|
+
next_url = _parse_next_link(link)
|
|
73
|
+
if not next_url:
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
# Extract query params from the next URL for subsequent request
|
|
77
|
+
parsed = urlparse(next_url)
|
|
78
|
+
_params = {k: v[0] for k, v in parse_qs(parsed.query).items()}
|
|
79
|
+
|
|
80
|
+
return all_items
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# Repo helpers
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def get_repos(self) -> list[dict[str, Any]]:
|
|
87
|
+
"""Get all repos matching the configured prefix."""
|
|
88
|
+
repos = self.get_paginated(f"/users/{self._organization}/repos")
|
|
89
|
+
return [r for r in repos if r.get("name", "").startswith(self._repo_prefix)]
|
|
90
|
+
|
|
91
|
+
def get_repo(self, name: str) -> dict[str, Any]:
|
|
92
|
+
"""Get a single repo by name (short name, not full_name)."""
|
|
93
|
+
return self.get(f"/repos/{self._organization}/{name}")
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def organization(self) -> str:
|
|
97
|
+
return self._organization
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def repo_prefix(self) -> str:
|
|
101
|
+
return self._repo_prefix
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _parse_next_link(link_header: str) -> str | None:
|
|
105
|
+
"""Parse the 'next' URL from a GitHub Link header."""
|
|
106
|
+
for part in link_header.split(","):
|
|
107
|
+
if 'rel="next"' in part:
|
|
108
|
+
url = part.split(";")[0].strip().strip("<>")
|
|
109
|
+
return url
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def gh_run(args: list[str], check: bool = True) -> str:
|
|
114
|
+
"""Run a gh CLI command and return stdout.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
args: Arguments to pass to ``gh`` (e.g., ["pr", "view", "123"]).
|
|
118
|
+
check: If True, raise CommandError on non-zero exit.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The stdout output as a string.
|
|
122
|
+
"""
|
|
123
|
+
result = run(["gh", *args], check=check)
|
|
124
|
+
return result.stdout.strip()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Config management for kctl-github."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from kctl_lib.config import (
|
|
6
|
+
get_all_services_in_profile,
|
|
7
|
+
get_profile_names,
|
|
8
|
+
remove_profile,
|
|
9
|
+
set_default_profile,
|
|
10
|
+
)
|
|
11
|
+
from kctl_lib.config import get_service_config as _get_service_config
|
|
12
|
+
from kctl_lib.config import (
|
|
13
|
+
resolve_active_profile_name as _resolve_active_profile_name,
|
|
14
|
+
)
|
|
15
|
+
from kctl_lib.config import set_service_config as _set_service_config
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ServiceConfig",
|
|
20
|
+
"get_all_services_in_profile",
|
|
21
|
+
"get_profile_names",
|
|
22
|
+
"get_service_config",
|
|
23
|
+
"remove_profile",
|
|
24
|
+
"resolve_active_profile_name",
|
|
25
|
+
"set_default_profile",
|
|
26
|
+
"set_service_config",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
SERVICE_KEY = "github"
|
|
30
|
+
ENV_PREFIX = "KCTL_GITHUB"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ServiceConfig(BaseModel):
|
|
34
|
+
"""Service-specific config within a profile."""
|
|
35
|
+
|
|
36
|
+
token: str = "" # GitHub Personal Access Token
|
|
37
|
+
organization: str = "tgunawandev" # GitHub org or username
|
|
38
|
+
repo_prefix: str = "kodemeio-" # Filter repos by this prefix
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_service_config(profile_name: str) -> ServiceConfig:
|
|
42
|
+
"""Get the 'github' service config from a profile."""
|
|
43
|
+
data = _get_service_config(profile_name, SERVICE_KEY, list(ServiceConfig.model_fields.keys()))
|
|
44
|
+
return ServiceConfig(**data) if data else ServiceConfig()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def set_service_config(profile_name: str, svc_config: ServiceConfig) -> None:
|
|
48
|
+
"""Set the 'github' service config within a profile."""
|
|
49
|
+
cleaned = {k: v for k, v in svc_config.model_dump().items() if v}
|
|
50
|
+
_set_service_config(profile_name, SERVICE_KEY, cleaned)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def resolve_active_profile_name(profile_name: str | None = None) -> str:
|
|
54
|
+
"""Resolve active profile: explicit > env > default."""
|
|
55
|
+
return _resolve_active_profile_name(profile_name, ENV_PREFIX)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Exception hierarchy -- re-exported from kctl-lib."""
|
|
2
|
+
|
|
3
|
+
from kctl_lib.exceptions import (
|
|
4
|
+
APIError,
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
CommandError,
|
|
7
|
+
ConfigError,
|
|
8
|
+
KctlError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
)
|
|
11
|
+
from kctl_lib.exceptions import ConnectionError as KctlConnectionError
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"APIError",
|
|
15
|
+
"AuthenticationError",
|
|
16
|
+
"CommandError",
|
|
17
|
+
"ConfigError",
|
|
18
|
+
"KctlConnectionError",
|
|
19
|
+
"KctlError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Plugin discovery for kctl-github."""
|
|
2
|
+
|
|
3
|
+
from kctl_lib.plugins import KctlPlugin
|
|
4
|
+
from kctl_lib.plugins import discover_and_load_plugins as _discover
|
|
5
|
+
|
|
6
|
+
__all__ = ["KctlPlugin", "discover_and_load_plugins"]
|
|
7
|
+
|
|
8
|
+
ENTRY_POINT_GROUP = "kctl_github.plugins"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def discover_and_load_plugins(app): # noqa: ANN001, ANN201
|
|
12
|
+
"""Discover and load kctl-github plugins."""
|
|
13
|
+
return _discover(app, ENTRY_POINT_GROUP)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kctl-github
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Kodemeio GitHub CLI — cross-repo GitHub management
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: httpx>=0.28.0
|
|
7
|
+
Requires-Dist: kctl-lib>=0.4.0
|
|
8
|
+
Requires-Dist: pydantic>=2.10.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
10
|
+
Requires-Dist: rich>=13.9.0
|
|
11
|
+
Requires-Dist: typer>=0.15.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: mypy>=1.14.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-httpx>=0.35.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.9.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
kctl_github/__init__.py,sha256=ssQ8gwlDUUPmKmQR0Dgl3Cq9sAapzcRw7tLy-dOKfk4,73
|
|
2
|
+
kctl_github/__main__.py,sha256=zu0zAIML3odZ6Gfhdi8aZns3cMwzXTcNPd8OpE6PrU4,88
|
|
3
|
+
kctl_github/cli.py,sha256=EgSieOmkYLPZQP2R7U53_LSLR08sI95toYVtc78Q6qY,4506
|
|
4
|
+
kctl_github/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
kctl_github/commands/billing.py,sha256=7JP9xcMMRrcCDEKVcGMYtaTkbkE_se2shffNYr5J0Lw,5514
|
|
6
|
+
kctl_github/commands/ci.py,sha256=Z8evyvPI2m5AInQvoHwPZIOAw8BO0aDGX-qkS1XLxTA,8419
|
|
7
|
+
kctl_github/commands/config_cmd.py,sha256=-Q9Kl2SVctAPOLpsr0RnD3DvFO-GbDbJ7wOAX_ieTwU,6644
|
|
8
|
+
kctl_github/commands/dashboard.py,sha256=ZTsxtiw_WiLr-0oGRAPV7zHd4aFtWcdqWOD9Xg2S9mY,2318
|
|
9
|
+
kctl_github/commands/doctor_cmd.py,sha256=nKTOsNMr3wQMqdsSlbnzkd8HGEfwJu1du6ZteLzEBNQ,2667
|
|
10
|
+
kctl_github/commands/health.py,sha256=2DXKQvTBfGH9OdwGrin5b5uHOej2BESlA6mv_9mqTbU,1799
|
|
11
|
+
kctl_github/commands/labels.py,sha256=fNwPmdFdznEY2TzW20yb6WMPpTvj3V81bxzjvMl-kpE,4128
|
|
12
|
+
kctl_github/commands/prs.py,sha256=FjISEt6pfpypP1xJEnfHcsADffveC3UyTUcIi_gfP3k,4962
|
|
13
|
+
kctl_github/commands/repos.py,sha256=8X4N1XuRY9Z1GLRf2dcuI-IKrn-vlN1tigFimpSBHqs,5769
|
|
14
|
+
kctl_github/commands/secrets.py,sha256=9v7yDyHwyPVj3P9M8zdcTsrA_pPWaDrQwAJtG0FVA4k,4157
|
|
15
|
+
kctl_github/commands/skill_cmd.py,sha256=96_A2tOt7rlhA6FCNOp8bwRdLgveX6ON4xwD8vhrNJE,2434
|
|
16
|
+
kctl_github/commands/stats.py,sha256=uckK6hm5kv1JRxxY90RNRDmWLtYqw5EnqcW6lWJ02s4,6419
|
|
17
|
+
kctl_github/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
kctl_github/core/callbacks.py,sha256=C9KzkKnksZfJesCsVbLpkSSyURxPKvM-ZQJI8IvMp0s,1181
|
|
19
|
+
kctl_github/core/client.py,sha256=lDhRr8ThNtVJIRdEskVuNA8X7zP-4jhwCwm5CHuPQQg,4045
|
|
20
|
+
kctl_github/core/config.py,sha256=Sul8rP_hfTi-5o-x3kTbAeIi9YMYEDWQQeUE6XQ6kPQ,1783
|
|
21
|
+
kctl_github/core/exceptions.py,sha256=zt41Q5YfY01BdNJRLLwoTmlosJBceNioqwAWBIuzE8M,434
|
|
22
|
+
kctl_github/core/plugins.py,sha256=L-AJBFVVhlu1pxztAGnIGCzx0OY4x4Wnre0OEEgUqM4,403
|
|
23
|
+
kctl_github-0.2.0.dist-info/METADATA,sha256=-qmWuYp18TqN--5EKfsUlV4kbuoVlW0EwHeKm1EhiaY,574
|
|
24
|
+
kctl_github-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
25
|
+
kctl_github-0.2.0.dist-info/entry_points.txt,sha256=-5NGnCyq0nyeojwI-_lyg4ZRw3ZIEQdSJrRyarPrWSk,53
|
|
26
|
+
kctl_github-0.2.0.dist-info/RECORD,,
|