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.
Files changed (44) hide show
  1. opun8/__init__.py +9 -0
  2. opun8/assets/__init__.py +0 -0
  3. opun8/auth/__init__.py +51 -0
  4. opun8/auth/github_oauth.py +363 -0
  5. opun8/cli.py +260 -0
  6. opun8/commands/__init__.py +9 -0
  7. opun8/commands/badges.py +199 -0
  8. opun8/commands/deploy.py +851 -0
  9. opun8/commands/detect.py +524 -0
  10. opun8/commands/doctor.py +73 -0
  11. opun8/commands/history.py +855 -0
  12. opun8/config/__init__.py +9 -0
  13. opun8/config/settings.py +18 -0
  14. opun8/core/__init__.py +8 -0
  15. opun8/core/detector.py +127 -0
  16. opun8/core/environment.py +204 -0
  17. opun8/core/templates.py +299 -0
  18. opun8/detector/__init__.py +0 -0
  19. opun8/exceptions/__init__.py +0 -0
  20. opun8/main.py +13 -0
  21. opun8/models/__init__.py +0 -0
  22. opun8/providers/__init__.py +0 -0
  23. opun8/providers/github/__init__.py +0 -0
  24. opun8/providers/render/__init__.py +0 -0
  25. opun8/providers/vercel/__init__.py +11 -0
  26. opun8/providers/vercel/auth.py +962 -0
  27. opun8/providers/vercel/deploy.py +1030 -0
  28. opun8/scanner/__init__.py +0 -0
  29. opun8/services/__init__.py +7 -0
  30. opun8/services/deployment_history.py +533 -0
  31. opun8/services/git_service.py +713 -0
  32. opun8/services/navigation.py +81 -0
  33. opun8/services/recent_projects.py +51 -0
  34. opun8/ui/__init__.py +5 -0
  35. opun8/ui/console.py +9 -0
  36. opun8/ui/messages.py +341 -0
  37. opun8/utils/__init__.py +0 -0
  38. opun8/version.py +9 -0
  39. opun8-0.1.0.dist-info/METADATA +17 -0
  40. opun8-0.1.0.dist-info/RECORD +44 -0
  41. opun8-0.1.0.dist-info/WHEEL +5 -0
  42. opun8-0.1.0.dist-info/entry_points.txt +2 -0
  43. opun8-0.1.0.dist-info/licenses/LICENSE +0 -0
  44. opun8-0.1.0.dist-info/top_level.txt +1 -0
opun8/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ Opun8 - Universal Deployment Platform.
3
+
4
+ Build. Deploy. Scale.
5
+ """
6
+
7
+ from opun8.version import __version__
8
+
9
+ __all__ = ["__version__"]
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()
@@ -0,0 +1,9 @@
1
+ """
2
+ Opun8 Commands - Available CLI commands.
3
+ """
4
+
5
+ from opun8.commands.doctor import doctor
6
+ from opun8.commands.detect import detect
7
+ from opun8.commands.deploy import deploy
8
+
9
+ __all__ = ["doctor", "detect", "deploy"]