opun8 0.1.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.
- opun8-0.1.0/LICENSE +0 -0
- opun8-0.1.0/PKG-INFO +17 -0
- opun8-0.1.0/README.md +0 -0
- opun8-0.1.0/pyproject.toml +32 -0
- opun8-0.1.0/setup.cfg +4 -0
- opun8-0.1.0/src/opun8/__init__.py +9 -0
- opun8-0.1.0/src/opun8/assets/__init__.py +0 -0
- opun8-0.1.0/src/opun8/auth/__init__.py +51 -0
- opun8-0.1.0/src/opun8/auth/github_oauth.py +363 -0
- opun8-0.1.0/src/opun8/cli.py +260 -0
- opun8-0.1.0/src/opun8/commands/__init__.py +9 -0
- opun8-0.1.0/src/opun8/commands/badges.py +199 -0
- opun8-0.1.0/src/opun8/commands/deploy.py +851 -0
- opun8-0.1.0/src/opun8/commands/detect.py +524 -0
- opun8-0.1.0/src/opun8/commands/doctor.py +73 -0
- opun8-0.1.0/src/opun8/commands/history.py +855 -0
- opun8-0.1.0/src/opun8/config/__init__.py +9 -0
- opun8-0.1.0/src/opun8/config/settings.py +18 -0
- opun8-0.1.0/src/opun8/core/__init__.py +8 -0
- opun8-0.1.0/src/opun8/core/detector.py +127 -0
- opun8-0.1.0/src/opun8/core/environment.py +204 -0
- opun8-0.1.0/src/opun8/core/templates.py +299 -0
- opun8-0.1.0/src/opun8/detector/__init__.py +0 -0
- opun8-0.1.0/src/opun8/exceptions/__init__.py +0 -0
- opun8-0.1.0/src/opun8/main.py +13 -0
- opun8-0.1.0/src/opun8/models/__init__.py +0 -0
- opun8-0.1.0/src/opun8/providers/__init__.py +0 -0
- opun8-0.1.0/src/opun8/providers/github/__init__.py +0 -0
- opun8-0.1.0/src/opun8/providers/render/__init__.py +0 -0
- opun8-0.1.0/src/opun8/providers/vercel/__init__.py +11 -0
- opun8-0.1.0/src/opun8/providers/vercel/auth.py +962 -0
- opun8-0.1.0/src/opun8/providers/vercel/deploy.py +1030 -0
- opun8-0.1.0/src/opun8/scanner/__init__.py +0 -0
- opun8-0.1.0/src/opun8/services/__init__.py +7 -0
- opun8-0.1.0/src/opun8/services/deployment_history.py +533 -0
- opun8-0.1.0/src/opun8/services/git_service.py +713 -0
- opun8-0.1.0/src/opun8/services/navigation.py +81 -0
- opun8-0.1.0/src/opun8/services/recent_projects.py +51 -0
- opun8-0.1.0/src/opun8/ui/__init__.py +5 -0
- opun8-0.1.0/src/opun8/ui/console.py +9 -0
- opun8-0.1.0/src/opun8/ui/messages.py +341 -0
- opun8-0.1.0/src/opun8/utils/__init__.py +0 -0
- opun8-0.1.0/src/opun8/version.py +9 -0
- opun8-0.1.0/src/opun8.egg-info/PKG-INFO +17 -0
- opun8-0.1.0/src/opun8.egg-info/SOURCES.txt +48 -0
- opun8-0.1.0/src/opun8.egg-info/dependency_links.txt +1 -0
- opun8-0.1.0/src/opun8.egg-info/entry_points.txt +2 -0
- opun8-0.1.0/src/opun8.egg-info/requires.txt +7 -0
- opun8-0.1.0/src/opun8.egg-info/top_level.txt +1 -0
- opun8-0.1.0/tests/test_deploy_skip_github_contract.py +62 -0
opun8-0.1.0/LICENSE
ADDED
|
File without changes
|
opun8-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opun8
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal Deployment Platform for developers.
|
|
5
|
+
Author-email: Kakes David <codewithkakes@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: typer
|
|
11
|
+
Requires-Dist: rich
|
|
12
|
+
Requires-Dist: httpx
|
|
13
|
+
Requires-Dist: python-dotenv
|
|
14
|
+
Requires-Dist: pydantic
|
|
15
|
+
Requires-Dist: gitpython
|
|
16
|
+
Requires-Dist: keyring
|
|
17
|
+
Dynamic: license-file
|
opun8-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=70.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "opun8"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Universal Deployment Platform for developers."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Kakes David", email = "codewithkakes@gmail.com" }
|
|
13
|
+
]
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
dependencies = [
|
|
16
|
+
"typer",
|
|
17
|
+
"rich",
|
|
18
|
+
"httpx",
|
|
19
|
+
"python-dotenv",
|
|
20
|
+
"pydantic",
|
|
21
|
+
"gitpython",
|
|
22
|
+
"keyring"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
package-dir = {"" = "src"}
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
opun8 = "opun8.cli:app"
|
opun8-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication module for Opun8.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from opun8.auth.github_oauth import (
|
|
6
|
+
login_to_github,
|
|
7
|
+
get_github_token,
|
|
8
|
+
is_authenticated,
|
|
9
|
+
logout,
|
|
10
|
+
save_github_token,
|
|
11
|
+
get_authenticated_user,
|
|
12
|
+
get_github_user,
|
|
13
|
+
list_github_repos,
|
|
14
|
+
create_github_repo,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Re-export Vercel auth functions from the providers module
|
|
18
|
+
from opun8.providers.vercel.auth import (
|
|
19
|
+
login_to_vercel,
|
|
20
|
+
get_vercel_token,
|
|
21
|
+
is_vercel_authenticated,
|
|
22
|
+
logout_vercel,
|
|
23
|
+
show_vercel_projects,
|
|
24
|
+
switch_vercel_team,
|
|
25
|
+
set_deploy_callback,
|
|
26
|
+
get_vercel_user,
|
|
27
|
+
get_vercel_scope,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# GitHub
|
|
32
|
+
"login_to_github",
|
|
33
|
+
"get_github_token",
|
|
34
|
+
"is_authenticated",
|
|
35
|
+
"logout",
|
|
36
|
+
"save_github_token",
|
|
37
|
+
"get_authenticated_user",
|
|
38
|
+
"get_github_user",
|
|
39
|
+
"list_github_repos",
|
|
40
|
+
"create_github_repo",
|
|
41
|
+
# Vercel
|
|
42
|
+
"login_to_vercel",
|
|
43
|
+
"get_vercel_token",
|
|
44
|
+
"is_vercel_authenticated",
|
|
45
|
+
"logout_vercel",
|
|
46
|
+
"show_vercel_projects",
|
|
47
|
+
"switch_vercel_team",
|
|
48
|
+
"set_deploy_callback",
|
|
49
|
+
"get_vercel_user",
|
|
50
|
+
"get_vercel_scope",
|
|
51
|
+
]
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub OAuth authentication for Opun8.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import webbrowser
|
|
7
|
+
import requests
|
|
8
|
+
import json
|
|
9
|
+
import threading
|
|
10
|
+
import secrets
|
|
11
|
+
import urllib.parse
|
|
12
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Dict, List
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.prompt import Prompt
|
|
18
|
+
|
|
19
|
+
from dotenv import load_dotenv
|
|
20
|
+
|
|
21
|
+
load_dotenv()
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------------------
|
|
26
|
+
# OAuth Configuration — READ FROM .env ONLY — NO HARDCODED VALUES
|
|
27
|
+
# ------------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID")
|
|
30
|
+
CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET")
|
|
31
|
+
|
|
32
|
+
# Actually honor GITHUB_REDIRECT_URI from .env instead of silently ignoring it.
|
|
33
|
+
REDIRECT_URI = os.environ.get("GITHUB_REDIRECT_URI", "http://localhost:8080/callback")
|
|
34
|
+
|
|
35
|
+
_parsed_redirect = urllib.parse.urlparse(REDIRECT_URI)
|
|
36
|
+
CALLBACK_HOST = _parsed_redirect.hostname or "localhost"
|
|
37
|
+
CALLBACK_PORT = _parsed_redirect.port or 8080
|
|
38
|
+
CALLBACK_PATH = _parsed_redirect.path or "/callback"
|
|
39
|
+
|
|
40
|
+
SCOPES = "repo,workflow" # GitHub accepts comma-delimited scopes.
|
|
41
|
+
|
|
42
|
+
AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize"
|
|
43
|
+
TOKEN_ENDPOINT = "https://github.com/login/oauth/access_token"
|
|
44
|
+
|
|
45
|
+
if not CLIENT_ID or not CLIENT_SECRET:
|
|
46
|
+
console.print("[red]❌ GitHub credentials not found in .env file.[/red]")
|
|
47
|
+
console.print("[dim]Please ensure GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET are set in .env[/dim]")
|
|
48
|
+
|
|
49
|
+
TOKEN_FILE = Path.home() / ".opun8" / "github_token.json"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _build_authorize_url(state: str) -> str:
|
|
53
|
+
params = {
|
|
54
|
+
"client_id": CLIENT_ID,
|
|
55
|
+
"redirect_uri": REDIRECT_URI,
|
|
56
|
+
"scope": SCOPES,
|
|
57
|
+
"state": state,
|
|
58
|
+
}
|
|
59
|
+
return f"{AUTHORIZATION_ENDPOINT}?{urllib.parse.urlencode(params)}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ------------------------------------------------------------------------------
|
|
63
|
+
# Local callback server — catches the redirect instead of asking the user
|
|
64
|
+
# to copy/paste a code out of a dead browser tab.
|
|
65
|
+
# ------------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
class _CallbackResult:
|
|
68
|
+
code: Optional[str] = None
|
|
69
|
+
state: Optional[str] = None
|
|
70
|
+
error: Optional[str] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _make_handler(result: _CallbackResult, done_event: threading.Event):
|
|
74
|
+
class Handler(BaseHTTPRequestHandler):
|
|
75
|
+
def do_GET(self):
|
|
76
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
77
|
+
if parsed.path != CALLBACK_PATH:
|
|
78
|
+
self.send_response(404)
|
|
79
|
+
self.end_headers()
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
params = urllib.parse.parse_qs(parsed.query)
|
|
83
|
+
result.code = params.get("code", [None])[0]
|
|
84
|
+
result.state = params.get("state", [None])[0]
|
|
85
|
+
result.error = params.get("error_description", params.get("error", [None]))[0]
|
|
86
|
+
|
|
87
|
+
self.send_response(200)
|
|
88
|
+
self.send_header("Content-Type", "text/html")
|
|
89
|
+
self.end_headers()
|
|
90
|
+
if result.code:
|
|
91
|
+
self.wfile.write(
|
|
92
|
+
b"<html><body><h2>GitHub authorization complete.</h2>"
|
|
93
|
+
b"<p>You can close this tab and return to the terminal.</p></body></html>"
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
self.wfile.write(
|
|
97
|
+
b"<html><body><h2>Authorization failed.</h2>"
|
|
98
|
+
b"<p>You can close this tab and return to the terminal.</p></body></html>"
|
|
99
|
+
)
|
|
100
|
+
done_event.set()
|
|
101
|
+
|
|
102
|
+
def log_message(self, format, *args):
|
|
103
|
+
pass # silence default HTTP server logging
|
|
104
|
+
|
|
105
|
+
return Handler
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _wait_for_callback(timeout: int = 180) -> _CallbackResult:
|
|
109
|
+
"""Start a local server, wait for GitHub's redirect, then shut down."""
|
|
110
|
+
result = _CallbackResult()
|
|
111
|
+
done_event = threading.Event()
|
|
112
|
+
handler = _make_handler(result, done_event)
|
|
113
|
+
|
|
114
|
+
server = HTTPServer((CALLBACK_HOST, CALLBACK_PORT), handler)
|
|
115
|
+
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
116
|
+
server_thread.start()
|
|
117
|
+
|
|
118
|
+
got_it = done_event.wait(timeout=timeout)
|
|
119
|
+
server.shutdown()
|
|
120
|
+
server_thread.join()
|
|
121
|
+
|
|
122
|
+
if not got_it:
|
|
123
|
+
result.error = "timed out waiting for GitHub to redirect back"
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------------------------
|
|
129
|
+
# Token Storage
|
|
130
|
+
# ------------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def get_github_token() -> Optional[str]:
|
|
133
|
+
if TOKEN_FILE.exists():
|
|
134
|
+
try:
|
|
135
|
+
with open(TOKEN_FILE, "r") as f:
|
|
136
|
+
return json.load(f).get("access_token")
|
|
137
|
+
except Exception:
|
|
138
|
+
return None
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_github_user() -> Optional[Dict]:
|
|
143
|
+
if TOKEN_FILE.exists():
|
|
144
|
+
try:
|
|
145
|
+
with open(TOKEN_FILE, "r") as f:
|
|
146
|
+
return json.load(f).get("user")
|
|
147
|
+
except Exception:
|
|
148
|
+
return None
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def save_github_token(token: str, user_info: Dict) -> None:
|
|
153
|
+
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
with open(TOKEN_FILE, "w") as f:
|
|
155
|
+
json.dump({"access_token": token, "user": user_info}, f, indent=2)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------------------
|
|
159
|
+
# Login Flow
|
|
160
|
+
# ------------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def login_to_github() -> Optional[str]:
|
|
163
|
+
console.print()
|
|
164
|
+
console.print(Panel(
|
|
165
|
+
"[bold cyan]🔐 GitHub Authentication[/bold cyan]\n\n"
|
|
166
|
+
"Opun8 needs access to GitHub to:\n"
|
|
167
|
+
" • Create repositories\n"
|
|
168
|
+
" • Push your code\n"
|
|
169
|
+
" • Enable auto-deploy\n\n"
|
|
170
|
+
"[dim]Your browser will open for authorization.[/dim]",
|
|
171
|
+
border_style="cyan",
|
|
172
|
+
padding=(1, 2),
|
|
173
|
+
width=60,
|
|
174
|
+
))
|
|
175
|
+
console.print()
|
|
176
|
+
console.print("[bold]1[/] 🔑 [white]Login with GitHub[/white] [dim](opens browser)[/dim]")
|
|
177
|
+
console.print("[bold]2[/] ⏭️ [white]Skip[/white] [dim](deploy without GitHub)[/dim]")
|
|
178
|
+
console.print()
|
|
179
|
+
|
|
180
|
+
choice = Prompt.ask(
|
|
181
|
+
"[bold cyan]➜[/] Select an option",
|
|
182
|
+
choices=["1", "2"],
|
|
183
|
+
default="1",
|
|
184
|
+
show_choices=False,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if choice == "2":
|
|
188
|
+
console.print("\n[yellow]Skipping GitHub authentication.[/yellow]")
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
if not CLIENT_ID or not CLIENT_SECRET:
|
|
192
|
+
console.print("[red]❌ Missing GitHub credentials in .env file.[/red]")
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
state = secrets.token_urlsafe(32)
|
|
196
|
+
authorize_url = _build_authorize_url(state)
|
|
197
|
+
|
|
198
|
+
console.print()
|
|
199
|
+
console.print("[dim]🌐 Opening browser for GitHub authorization...[/dim]")
|
|
200
|
+
console.print(f"[dim]Waiting on {REDIRECT_URI} for the redirect...[/dim]")
|
|
201
|
+
console.print()
|
|
202
|
+
|
|
203
|
+
webbrowser.open(authorize_url)
|
|
204
|
+
|
|
205
|
+
console.print("[bold]Waiting for GitHub to redirect back...[/bold]")
|
|
206
|
+
console.print("[dim]This happens automatically — no need to paste anything.[/dim]")
|
|
207
|
+
console.print()
|
|
208
|
+
|
|
209
|
+
result = _wait_for_callback()
|
|
210
|
+
|
|
211
|
+
if result.error and not result.code:
|
|
212
|
+
console.print(f"[red]❌ Authorization failed: {result.error}[/red]")
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
if result.state != state:
|
|
216
|
+
console.print("[red]❌ State mismatch — possible CSRF, aborting.[/red]")
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
token = exchange_github_code_for_token(result.code)
|
|
220
|
+
if not token:
|
|
221
|
+
console.print("[red]❌ Failed to exchange code for a token.[/red]")
|
|
222
|
+
return token
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def exchange_github_code_for_token(code: str) -> Optional[str]:
|
|
226
|
+
try:
|
|
227
|
+
if not code:
|
|
228
|
+
console.print("[red]❌ No authorization code received.[/red]")
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
if not CLIENT_SECRET:
|
|
232
|
+
console.print("[red]❌ GITHUB_CLIENT_SECRET not found in .env file.[/red]")
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
response = requests.post(
|
|
236
|
+
TOKEN_ENDPOINT,
|
|
237
|
+
headers={"Accept": "application/json"},
|
|
238
|
+
data={
|
|
239
|
+
"client_id": CLIENT_ID,
|
|
240
|
+
"client_secret": CLIENT_SECRET,
|
|
241
|
+
"code": code,
|
|
242
|
+
"redirect_uri": REDIRECT_URI,
|
|
243
|
+
},
|
|
244
|
+
timeout=30,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
data = response.json()
|
|
248
|
+
|
|
249
|
+
if "access_token" in data:
|
|
250
|
+
token = data["access_token"]
|
|
251
|
+
|
|
252
|
+
user = get_github_user_info(token)
|
|
253
|
+
if user:
|
|
254
|
+
save_github_token(token, user)
|
|
255
|
+
console.print()
|
|
256
|
+
console.print(f"[bold green]✅ Connected as: {user.get('login', 'Unknown')}[/bold green]")
|
|
257
|
+
console.print("[dim]Token saved securely for future use.[/dim]")
|
|
258
|
+
else:
|
|
259
|
+
save_github_token(token, {"login": "Unknown"})
|
|
260
|
+
|
|
261
|
+
return token
|
|
262
|
+
else:
|
|
263
|
+
error_msg = data.get("error_description", data.get("error", "Unknown error"))
|
|
264
|
+
console.print(f"[red]Failed to get token: {error_msg}[/red]")
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
console.print(f"[red]Error exchanging code: {e}[/red]")
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_github_user_info(token: str) -> Optional[Dict]:
|
|
273
|
+
try:
|
|
274
|
+
response = requests.get(
|
|
275
|
+
"https://api.github.com/user",
|
|
276
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
277
|
+
timeout=10,
|
|
278
|
+
)
|
|
279
|
+
return response.json() if response.status_code == 200 else None
|
|
280
|
+
except Exception:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def is_authenticated() -> bool:
|
|
285
|
+
return get_github_token() is not None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def logout() -> None:
|
|
289
|
+
if TOKEN_FILE.exists():
|
|
290
|
+
TOKEN_FILE.unlink()
|
|
291
|
+
console.print("[green]✅ Logged out of GitHub.[/green]")
|
|
292
|
+
else:
|
|
293
|
+
console.print("[yellow]Not logged in.[/yellow]")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_authenticated_user() -> Optional[str]:
|
|
297
|
+
user = get_github_user()
|
|
298
|
+
if user:
|
|
299
|
+
return user.get("login")
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def list_github_repos(token: Optional[str] = None) -> List[Dict]:
|
|
304
|
+
if token is None:
|
|
305
|
+
token = get_github_token()
|
|
306
|
+
|
|
307
|
+
if not token:
|
|
308
|
+
return []
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
response = requests.get(
|
|
312
|
+
"https://api.github.com/user/repos",
|
|
313
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
314
|
+
params={"per_page": 50, "sort": "updated"},
|
|
315
|
+
timeout=10,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if response.status_code == 200:
|
|
319
|
+
repos = response.json()
|
|
320
|
+
return [
|
|
321
|
+
{
|
|
322
|
+
"name": repo["name"],
|
|
323
|
+
"full_name": repo["full_name"],
|
|
324
|
+
"private": repo["private"],
|
|
325
|
+
"url": repo["html_url"],
|
|
326
|
+
"description": repo.get("description", ""),
|
|
327
|
+
"updated_at": repo.get("updated_at", ""),
|
|
328
|
+
}
|
|
329
|
+
for repo in repos
|
|
330
|
+
]
|
|
331
|
+
return []
|
|
332
|
+
except Exception:
|
|
333
|
+
return []
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def create_github_repo(token: Optional[str], name: str, description: str = "", private: bool = False) -> Optional[Dict]:
|
|
337
|
+
if token is None:
|
|
338
|
+
token = get_github_token()
|
|
339
|
+
|
|
340
|
+
if not token:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
response = requests.post(
|
|
345
|
+
"https://api.github.com/user/repos",
|
|
346
|
+
headers={
|
|
347
|
+
"Authorization": f"Bearer {token}",
|
|
348
|
+
"Content-Type": "application/json",
|
|
349
|
+
},
|
|
350
|
+
json={
|
|
351
|
+
"name": name,
|
|
352
|
+
"description": description,
|
|
353
|
+
"private": private,
|
|
354
|
+
"auto_init": True,
|
|
355
|
+
},
|
|
356
|
+
timeout=30,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if response.status_code == 201:
|
|
360
|
+
return response.json()
|
|
361
|
+
return None
|
|
362
|
+
except Exception:
|
|
363
|
+
return None
|