opun8 0.1.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.
- opun8/__init__.py +9 -0
- opun8/assets/__init__.py +0 -0
- opun8/auth/__init__.py +51 -0
- opun8/auth/github_oauth.py +363 -0
- opun8/cli.py +260 -0
- opun8/commands/__init__.py +9 -0
- opun8/commands/badges.py +199 -0
- opun8/commands/deploy.py +851 -0
- opun8/commands/detect.py +524 -0
- opun8/commands/doctor.py +73 -0
- opun8/commands/history.py +855 -0
- opun8/config/__init__.py +9 -0
- opun8/config/settings.py +18 -0
- opun8/core/__init__.py +8 -0
- opun8/core/detector.py +127 -0
- opun8/core/environment.py +204 -0
- opun8/core/templates.py +299 -0
- opun8/detector/__init__.py +0 -0
- opun8/exceptions/__init__.py +0 -0
- opun8/main.py +13 -0
- opun8/models/__init__.py +0 -0
- opun8/providers/__init__.py +0 -0
- opun8/providers/github/__init__.py +0 -0
- opun8/providers/render/__init__.py +0 -0
- opun8/providers/vercel/__init__.py +11 -0
- opun8/providers/vercel/auth.py +962 -0
- opun8/providers/vercel/deploy.py +1030 -0
- opun8/scanner/__init__.py +0 -0
- opun8/services/__init__.py +7 -0
- opun8/services/deployment_history.py +533 -0
- opun8/services/git_service.py +713 -0
- opun8/services/navigation.py +81 -0
- opun8/services/recent_projects.py +51 -0
- opun8/ui/__init__.py +5 -0
- opun8/ui/console.py +9 -0
- opun8/ui/messages.py +341 -0
- opun8/utils/__init__.py +0 -0
- opun8/version.py +9 -0
- opun8-0.1.0.dist-info/METADATA +17 -0
- opun8-0.1.0.dist-info/RECORD +44 -0
- opun8-0.1.0.dist-info/WHEEL +5 -0
- opun8-0.1.0.dist-info/entry_points.txt +2 -0
- opun8-0.1.0.dist-info/licenses/LICENSE +0 -0
- opun8-0.1.0.dist-info/top_level.txt +1 -0
opun8/__init__.py
ADDED
opun8/assets/__init__.py
ADDED
|
File without changes
|
opun8/auth/__init__.py
ADDED
|
@@ -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
|
opun8/cli.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Opun8 CLI - Command Line Interface for the Universal Deployment Platform.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.prompt import Prompt
|
|
9
|
+
|
|
10
|
+
from opun8 import __version__
|
|
11
|
+
from opun8.ui.messages import show_welcome
|
|
12
|
+
from opun8.auth import (
|
|
13
|
+
login_to_github,
|
|
14
|
+
logout as logout_github,
|
|
15
|
+
is_authenticated,
|
|
16
|
+
get_authenticated_user,
|
|
17
|
+
list_github_repos,
|
|
18
|
+
)
|
|
19
|
+
from opun8.providers.vercel.auth import (
|
|
20
|
+
login_to_vercel,
|
|
21
|
+
is_vercel_authenticated,
|
|
22
|
+
logout_vercel,
|
|
23
|
+
show_vercel_projects,
|
|
24
|
+
switch_vercel_team,
|
|
25
|
+
set_deploy_callback,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(
|
|
29
|
+
name="opun8",
|
|
30
|
+
help="Developer-first deployment platform.",
|
|
31
|
+
add_completion=False,
|
|
32
|
+
no_args_is_help=False,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.callback(invoke_without_command=True)
|
|
39
|
+
def main(
|
|
40
|
+
ctx: typer.Context,
|
|
41
|
+
version: bool = typer.Option(
|
|
42
|
+
False,
|
|
43
|
+
"--version",
|
|
44
|
+
"-v",
|
|
45
|
+
help="Show Opun8 version.",
|
|
46
|
+
),
|
|
47
|
+
):
|
|
48
|
+
if version:
|
|
49
|
+
console.print(f"Opun8 v{__version__}")
|
|
50
|
+
raise typer.Exit()
|
|
51
|
+
|
|
52
|
+
if ctx.invoked_subcommand is None:
|
|
53
|
+
show_welcome()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ──────────────────────────────────────────────────────────────
|
|
57
|
+
# COMMANDS
|
|
58
|
+
# ──────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
@app.command()
|
|
61
|
+
def doctor():
|
|
62
|
+
"""Check your environment and project."""
|
|
63
|
+
from opun8.commands.doctor import doctor as doctor_cmd
|
|
64
|
+
doctor_cmd()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command()
|
|
68
|
+
def detect():
|
|
69
|
+
"""Detect your project type and stack."""
|
|
70
|
+
from opun8.commands.detect import detect as detect_cmd
|
|
71
|
+
detect_cmd()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command()
|
|
75
|
+
def deploy(
|
|
76
|
+
platform: Optional[str] = typer.Argument(
|
|
77
|
+
None,
|
|
78
|
+
help="Platform to deploy to (vercel, netlify, render). Optional.",
|
|
79
|
+
),
|
|
80
|
+
):
|
|
81
|
+
"""Deploy your project to the cloud."""
|
|
82
|
+
from opun8.commands.deploy import deploy as deploy_cmd
|
|
83
|
+
deploy_cmd(platform_arg=platform)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
def history():
|
|
88
|
+
"""View and manage your deployment history."""
|
|
89
|
+
from opun8.commands.history import history as history_cmd
|
|
90
|
+
history_cmd()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command()
|
|
94
|
+
def badges():
|
|
95
|
+
"""Show your badge progress and achievements."""
|
|
96
|
+
from opun8.commands.badges import badges as badges_cmd
|
|
97
|
+
badges_cmd()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ──────────────────────────────────────────────────────────────
|
|
101
|
+
# GITHUB COMMANDS
|
|
102
|
+
# ──────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
@app.command()
|
|
105
|
+
def github(
|
|
106
|
+
logout_flag: bool = typer.Option(
|
|
107
|
+
False,
|
|
108
|
+
"--logout",
|
|
109
|
+
"-l",
|
|
110
|
+
help="Logout from GitHub.",
|
|
111
|
+
),
|
|
112
|
+
):
|
|
113
|
+
"""Connect to GitHub account."""
|
|
114
|
+
|
|
115
|
+
if logout_flag:
|
|
116
|
+
logout_github()
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if is_authenticated():
|
|
120
|
+
user = get_authenticated_user()
|
|
121
|
+
console.print(f"[green]✅ Already connected as: {user}[/green]")
|
|
122
|
+
console.print("[dim]To disconnect, run: opun8 github --logout[/dim]")
|
|
123
|
+
console.print()
|
|
124
|
+
|
|
125
|
+
console.print("[bold]📁 Your GitHub Repositories:[/bold]")
|
|
126
|
+
console.print()
|
|
127
|
+
repos = list_github_repos()
|
|
128
|
+
if repos:
|
|
129
|
+
for i, repo in enumerate(repos[:10], 1):
|
|
130
|
+
private_tag = "[dim](private)[/dim]" if repo.get("private") else ""
|
|
131
|
+
console.print(f" [bold cyan]{i}[/] [white]{repo['name']}[/white] {private_tag}")
|
|
132
|
+
if len(repos) > 10:
|
|
133
|
+
console.print(f" [dim]... and {len(repos) - 10} more[/dim]")
|
|
134
|
+
else:
|
|
135
|
+
console.print(" [dim]No repositories found[/dim]")
|
|
136
|
+
console.print()
|
|
137
|
+
console.print("[bold]What would you like to do?[/bold]")
|
|
138
|
+
console.print()
|
|
139
|
+
console.print(" [bold cyan]1[/] 🚀 [white]Deploy a repository[/white]")
|
|
140
|
+
console.print(" [bold cyan]2[/] 📁 [white]Detect my current project[/white]")
|
|
141
|
+
console.print(" [bold cyan]3[/] 🔄 [white]Go back[/white]")
|
|
142
|
+
console.print()
|
|
143
|
+
|
|
144
|
+
choice = Prompt.ask(
|
|
145
|
+
"[bold cyan]➜[/] Select an option",
|
|
146
|
+
choices=["1", "2", "3"],
|
|
147
|
+
default="3",
|
|
148
|
+
show_choices=False,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if choice == "1":
|
|
152
|
+
console.print("[yellow]🚀 Deploy repository coming soon![/yellow]")
|
|
153
|
+
elif choice == "2":
|
|
154
|
+
from opun8.commands.detect import detect as detect_cmd
|
|
155
|
+
detect_cmd()
|
|
156
|
+
else:
|
|
157
|
+
show_welcome()
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
console.print()
|
|
161
|
+
console.print("[bold cyan]🔐 Connect to GitHub[/bold cyan]")
|
|
162
|
+
console.print("[dim]This will allow Opun8 to create repositories and push code on your behalf.[/dim]")
|
|
163
|
+
console.print()
|
|
164
|
+
|
|
165
|
+
token = login_to_github()
|
|
166
|
+
|
|
167
|
+
if token:
|
|
168
|
+
console.print("[green]✅ Connected successfully![/green]")
|
|
169
|
+
console.print("[dim]Run [cyan]opun8 github[/cyan] again to see your repositories.[/dim]")
|
|
170
|
+
else:
|
|
171
|
+
console.print("[red]❌ Connection failed.[/red]")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ──────────────────────────────────────────────────────────────
|
|
175
|
+
# VERCEL COMMANDS
|
|
176
|
+
# ──────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
@app.command()
|
|
179
|
+
def vercel(
|
|
180
|
+
logout_flag: bool = typer.Option(
|
|
181
|
+
False,
|
|
182
|
+
"--logout",
|
|
183
|
+
"-l",
|
|
184
|
+
help="Logout from Vercel.",
|
|
185
|
+
),
|
|
186
|
+
switch_flag: bool = typer.Option(
|
|
187
|
+
False,
|
|
188
|
+
"--switch",
|
|
189
|
+
"-s",
|
|
190
|
+
help="Switch Vercel team/scope.",
|
|
191
|
+
),
|
|
192
|
+
show_flag: bool = typer.Option(
|
|
193
|
+
False,
|
|
194
|
+
"--show",
|
|
195
|
+
help="Show projects without re-authenticating.",
|
|
196
|
+
),
|
|
197
|
+
):
|
|
198
|
+
"""Connect to Vercel account."""
|
|
199
|
+
|
|
200
|
+
if logout_flag:
|
|
201
|
+
logout_vercel()
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
if switch_flag:
|
|
205
|
+
switch_vercel_team()
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if show_flag:
|
|
209
|
+
from opun8.commands.deploy import deploy as deploy_cmd
|
|
210
|
+
set_deploy_callback(deploy_cmd)
|
|
211
|
+
show_vercel_projects()
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if is_vercel_authenticated():
|
|
215
|
+
console.print("[green]✅ Already connected to Vercel.[/green]")
|
|
216
|
+
console.print("[dim]To disconnect, run: opun8 vercel --logout[/dim]")
|
|
217
|
+
console.print("[dim]To switch teams, run: opun8 vercel --switch[/dim]")
|
|
218
|
+
console.print()
|
|
219
|
+
from opun8.commands.deploy import deploy as deploy_cmd
|
|
220
|
+
set_deploy_callback(deploy_cmd)
|
|
221
|
+
show_vercel_projects()
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
console.print()
|
|
225
|
+
console.print("[bold cyan]▲ Connect to Vercel[/bold cyan]")
|
|
226
|
+
console.print("[dim]This will allow Opun8 to deploy projects to Vercel.[/dim]")
|
|
227
|
+
console.print()
|
|
228
|
+
|
|
229
|
+
from opun8.commands.deploy import deploy as deploy_cmd
|
|
230
|
+
set_deploy_callback(deploy_cmd)
|
|
231
|
+
|
|
232
|
+
token = login_to_vercel()
|
|
233
|
+
|
|
234
|
+
if token:
|
|
235
|
+
console.print("[green]✅ Connected to Vercel successfully![/green]")
|
|
236
|
+
else:
|
|
237
|
+
console.print("[red]❌ Connection failed.[/red]")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ──────────────────────────────────────────────────────────────
|
|
241
|
+
# LOGOUT
|
|
242
|
+
# ──────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
@app.command(name="logout")
|
|
245
|
+
def logout_all():
|
|
246
|
+
"""Logout from all services."""
|
|
247
|
+
logout_github()
|
|
248
|
+
logout_vercel()
|
|
249
|
+
console.print("[green]✅ Logged out from all services.[/green]")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ──────────────────────────────────────────────────────────────
|
|
253
|
+
# HELP
|
|
254
|
+
# ──────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
@app.command()
|
|
257
|
+
def help():
|
|
258
|
+
"""Show all available commands."""
|
|
259
|
+
from opun8.ui.messages import show_help
|
|
260
|
+
show_help()
|