rlsbl 0.8.2 → 0.9.0
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.
- package/package.json +3 -5
- package/rlsbl/__init__.py +0 -247
- package/rlsbl/__main__.py +0 -4
- package/rlsbl/commands/__init__.py +0 -0
- package/rlsbl/commands/check.py +0 -229
- package/rlsbl/commands/config.py +0 -67
- package/rlsbl/commands/discover.py +0 -198
- package/rlsbl/commands/init_cmd.py +0 -518
- package/rlsbl/commands/pre_push_check.py +0 -46
- package/rlsbl/commands/record_gif.py +0 -92
- package/rlsbl/commands/release.py +0 -287
- package/rlsbl/commands/status.py +0 -76
- package/rlsbl/commands/undo.py +0 -74
- package/rlsbl/commands/watch.py +0 -122
- package/rlsbl/config.py +0 -57
- package/rlsbl/registries/__init__.py +0 -5
- package/rlsbl/registries/go.py +0 -123
- package/rlsbl/registries/npm.py +0 -119
- package/rlsbl/registries/pypi.py +0 -171
- package/rlsbl/tagging.py +0 -207
- package/rlsbl/templates/go/VERSION.tpl +0 -1
- package/rlsbl/templates/go/ci.yml.tpl +0 -21
- package/rlsbl/templates/go/goreleaser.yml.tpl +0 -25
- package/rlsbl/templates/go/publish.yml.tpl +0 -25
- package/rlsbl/templates/merged/publish.yml.tpl +0 -30
- package/rlsbl/templates/npm/ci.yml.tpl +0 -22
- package/rlsbl/templates/npm/publish.yml.tpl +0 -22
- package/rlsbl/templates/pypi/ci.yml.tpl +0 -20
- package/rlsbl/templates/pypi/publish.yml.tpl +0 -18
- package/rlsbl/templates/shared/CHANGELOG.md.tpl +0 -5
- package/rlsbl/templates/shared/CLAUDE.md.tpl +0 -20
- package/rlsbl/templates/shared/LICENSE.tpl +0 -21
- package/rlsbl/templates/shared/claude-settings.json.tpl +0 -3
- package/rlsbl/templates/shared/gitignore.tpl +0 -14
- package/rlsbl/templates/shared/hooks/post-release.sh.tpl +0 -8
- package/rlsbl/templates/shared/hooks/pre-release.sh.tpl +0 -31
- package/rlsbl/utils.py +0 -131
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
"""Discover command: list projects in the rlsbl ecosystem."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
|
-
import urllib.error
|
|
8
|
-
import urllib.request
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
SEARCH_URL = "https://api.github.com/search/repositories?q=topic:rlsbl&sort=updated&per_page=100"
|
|
12
|
-
MAX_RESULTS = 1000
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _get_github_token():
|
|
16
|
-
"""Get a GitHub token from GITHUB_TOKEN env or `gh auth token`."""
|
|
17
|
-
token = os.environ.get("GITHUB_TOKEN")
|
|
18
|
-
if token:
|
|
19
|
-
return token
|
|
20
|
-
try:
|
|
21
|
-
result = subprocess.run(
|
|
22
|
-
["gh", "auth", "token"],
|
|
23
|
-
capture_output=True, text=True, check=True, timeout=10,
|
|
24
|
-
)
|
|
25
|
-
return result.stdout.strip() or None
|
|
26
|
-
except Exception:
|
|
27
|
-
return None
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _make_request(url, token):
|
|
31
|
-
"""Make a GET request to the GitHub API, return parsed JSON and response headers."""
|
|
32
|
-
req = urllib.request.Request(url, method="GET")
|
|
33
|
-
req.add_header("Accept", "application/vnd.github+json")
|
|
34
|
-
req.add_header("User-Agent", "rlsbl-cli")
|
|
35
|
-
if token:
|
|
36
|
-
req.add_header("Authorization", f"token {token}")
|
|
37
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
38
|
-
data = json.loads(resp.read().decode("utf-8"))
|
|
39
|
-
headers = dict(resp.headers)
|
|
40
|
-
return data, headers
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _parse_next_link(headers):
|
|
44
|
-
"""Extract the 'next' URL from the Link header, or None."""
|
|
45
|
-
link = headers.get("Link") or headers.get("link")
|
|
46
|
-
if not link:
|
|
47
|
-
return None
|
|
48
|
-
for part in link.split(","):
|
|
49
|
-
if 'rel="next"' in part:
|
|
50
|
-
start = part.index("<") + 1
|
|
51
|
-
end = part.index(">")
|
|
52
|
-
url = part[start:end]
|
|
53
|
-
if not url.startswith("https://api.github.com/"):
|
|
54
|
-
return None
|
|
55
|
-
return url
|
|
56
|
-
return None
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _relative_time(iso_timestamp):
|
|
60
|
-
"""Convert an ISO 8601 timestamp to a relative time string like '2d ago'."""
|
|
61
|
-
from datetime import datetime, timezone
|
|
62
|
-
|
|
63
|
-
if not iso_timestamp:
|
|
64
|
-
return ""
|
|
65
|
-
|
|
66
|
-
# Parse ISO timestamp (GitHub uses Z suffix)
|
|
67
|
-
ts = iso_timestamp.replace("Z", "+00:00")
|
|
68
|
-
dt = datetime.fromisoformat(ts)
|
|
69
|
-
now = datetime.now(timezone.utc)
|
|
70
|
-
delta = now - dt
|
|
71
|
-
|
|
72
|
-
seconds = int(delta.total_seconds())
|
|
73
|
-
if seconds < 60:
|
|
74
|
-
return "just now"
|
|
75
|
-
minutes = seconds // 60
|
|
76
|
-
if minutes < 60:
|
|
77
|
-
return f"{minutes}m ago"
|
|
78
|
-
hours = minutes // 60
|
|
79
|
-
if hours < 24:
|
|
80
|
-
return f"{hours}h ago"
|
|
81
|
-
days = hours // 24
|
|
82
|
-
if days < 7:
|
|
83
|
-
return f"{days}d ago"
|
|
84
|
-
weeks = days // 7
|
|
85
|
-
if weeks < 5:
|
|
86
|
-
return f"{weeks}w ago"
|
|
87
|
-
months = days // 30
|
|
88
|
-
if months < 12:
|
|
89
|
-
return f"{months}mo ago"
|
|
90
|
-
years = days // 365
|
|
91
|
-
return f"{years}y ago"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _get_authenticated_user(token):
|
|
95
|
-
"""Get the authenticated user's login name."""
|
|
96
|
-
if not token:
|
|
97
|
-
return None
|
|
98
|
-
try:
|
|
99
|
-
req = urllib.request.Request("https://api.github.com/user", method="GET")
|
|
100
|
-
req.add_header("Accept", "application/vnd.github+json")
|
|
101
|
-
req.add_header("User-Agent", "rlsbl-cli")
|
|
102
|
-
req.add_header("Authorization", f"token {token}")
|
|
103
|
-
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
104
|
-
data = json.loads(resp.read().decode("utf-8"))
|
|
105
|
-
return data.get("login")
|
|
106
|
-
except Exception:
|
|
107
|
-
return None
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _fetch_all_repos(token):
|
|
111
|
-
"""Fetch all repos with the rlsbl topic, handling pagination."""
|
|
112
|
-
repos = []
|
|
113
|
-
url = SEARCH_URL
|
|
114
|
-
|
|
115
|
-
while url and len(repos) < MAX_RESULTS:
|
|
116
|
-
data, headers = _make_request(url, token)
|
|
117
|
-
items = data.get("items", [])
|
|
118
|
-
repos.extend(items)
|
|
119
|
-
url = _parse_next_link(headers)
|
|
120
|
-
|
|
121
|
-
return repos
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def run_cmd(registry, args, flags):
|
|
125
|
-
"""Discover command: list projects in the rlsbl ecosystem."""
|
|
126
|
-
token = _get_github_token()
|
|
127
|
-
mine_only = flags.get("mine", False)
|
|
128
|
-
|
|
129
|
-
if mine_only and not token:
|
|
130
|
-
print("Error: --mine requires authentication (set GITHUB_TOKEN or install gh CLI).", file=sys.stderr)
|
|
131
|
-
sys.exit(1)
|
|
132
|
-
|
|
133
|
-
# Fetch repos
|
|
134
|
-
try:
|
|
135
|
-
repos = _fetch_all_repos(token)
|
|
136
|
-
except urllib.error.HTTPError as e:
|
|
137
|
-
print(f"Error: GitHub API returned {e.code}: {e.reason}", file=sys.stderr)
|
|
138
|
-
if e.code == 403:
|
|
139
|
-
print("Hint: run 'gh auth login' to increase API rate limits (60/hr unauthenticated → 5000/hr).", file=sys.stderr)
|
|
140
|
-
sys.exit(1)
|
|
141
|
-
except urllib.error.URLError as e:
|
|
142
|
-
print(f"Error: could not reach GitHub API: {e.reason}", file=sys.stderr)
|
|
143
|
-
sys.exit(1)
|
|
144
|
-
except Exception as e:
|
|
145
|
-
print(f"Error: {e}", file=sys.stderr)
|
|
146
|
-
sys.exit(1)
|
|
147
|
-
|
|
148
|
-
# Filter to --mine if requested
|
|
149
|
-
if mine_only:
|
|
150
|
-
username = _get_authenticated_user(token)
|
|
151
|
-
if not username:
|
|
152
|
-
print("Error: could not determine authenticated user.", file=sys.stderr)
|
|
153
|
-
sys.exit(1)
|
|
154
|
-
repos = [r for r in repos if r.get("owner", {}).get("login") == username]
|
|
155
|
-
|
|
156
|
-
if not repos:
|
|
157
|
-
if mine_only:
|
|
158
|
-
print("No rlsbl-tagged repositories found for your account.")
|
|
159
|
-
else:
|
|
160
|
-
print("No rlsbl-tagged repositories found.")
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
# Build table rows
|
|
164
|
-
rows = []
|
|
165
|
-
for repo in repos:
|
|
166
|
-
full_name = repo.get("full_name", "")
|
|
167
|
-
description = repo.get("description") or ""
|
|
168
|
-
updated = _relative_time(repo.get("updated_at", ""))
|
|
169
|
-
rows.append((full_name, description, updated))
|
|
170
|
-
|
|
171
|
-
# Calculate column widths
|
|
172
|
-
name_width = max(len(r[0]) for r in rows)
|
|
173
|
-
desc_width = max(len(r[1]) for r in rows)
|
|
174
|
-
time_width = max(len(r[2]) for r in rows)
|
|
175
|
-
|
|
176
|
-
# Cap description width to keep output readable
|
|
177
|
-
max_desc = 40
|
|
178
|
-
if desc_width > max_desc:
|
|
179
|
-
desc_width = max_desc
|
|
180
|
-
|
|
181
|
-
# Ensure minimum widths match headers
|
|
182
|
-
name_width = max(name_width, len("owner/repo"))
|
|
183
|
-
desc_width = max(desc_width, len("description"))
|
|
184
|
-
time_width = max(time_width, len("updated"))
|
|
185
|
-
|
|
186
|
-
# Print header
|
|
187
|
-
print(f"\nrlsbl ecosystem ({len(repos)} projects)\n")
|
|
188
|
-
header = f" {'owner/repo':<{name_width}} {'description':<{desc_width}} {'updated':<{time_width}}"
|
|
189
|
-
print(header)
|
|
190
|
-
separator_len = name_width + desc_width + time_width + 6
|
|
191
|
-
print(f" {'─' * separator_len}")
|
|
192
|
-
|
|
193
|
-
# Print rows
|
|
194
|
-
for full_name, description, updated in rows:
|
|
195
|
-
# Truncate long descriptions
|
|
196
|
-
if len(description) > max_desc:
|
|
197
|
-
description = description[:max_desc - 1] + "…"
|
|
198
|
-
print(f" {full_name:<{name_width}} {description:<{desc_width}} {updated}")
|