xenfra 0.3.8__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.
xenfra-0.3.8/PKG-INFO ADDED
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.3
2
+ Name: xenfra
3
+ Version: 0.3.8
4
+ Summary: A 'Zen Mode' infrastructure engine for Python developers.
5
+ Author: xenfra-cloud
6
+ Author-email: xenfra-cloud <xenfracloud@gmail.com>
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Classifier: Topic :: System :: Systems Administration
14
+ Requires-Dist: click>=8.1.7
15
+ Requires-Dist: rich>=14.2.0
16
+ Requires-Dist: sqlmodel>=0.0.16
17
+ Requires-Dist: python-digitalocean>=1.17.0
18
+ Requires-Dist: python-dotenv>=1.2.1
19
+ Requires-Dist: pyyaml>=6.0.1
20
+ Requires-Dist: fabric>=3.2.2
21
+ Requires-Dist: xenfra-sdk
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: keyring>=25.7.0
24
+ Requires-Dist: keyrings-alt>=5.0.2
25
+ Requires-Dist: tenacity>=8.2.3
26
+ Requires-Dist: cryptography>=43.0.0
27
+ Requires-Dist: toml>=0.10.2
28
+ Requires-Dist: pytest>=8.0.0 ; extra == 'test'
29
+ Requires-Dist: pytest-mock>=3.12.0 ; extra == 'test'
30
+ Requires-Python: >=3.13
31
+ Project-URL: Homepage, https://github.com/xenfra-cloud/xenfra
32
+ Project-URL: Issues, https://github.com/xenfra-cloud/xenfra/issues
33
+ Provides-Extra: test
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Xenfra CLI
37
+
38
+ ## Xenfra CLI: Deploy Python Apps with Zen Mode
39
+
40
+ The Xenfra CLI is a powerful and intuitive command-line interface designed to streamline the deployment of Python applications to DigitalOcean. Built with a "Zen Mode" philosophy, it automates complex infrastructure tasks, allowing developers to focus on writing code.
41
+
42
+ ### ✨ Key Features
43
+
44
+ - **Zero-Configuration Deployment:** Automatically detects your project's framework and dependencies.
45
+ - **AI-Powered Auto-Healing:** Diagnoses common deployment failures and suggests, or even applies, fixes automatically.
46
+ - **Real-time Monitoring:** View deployment status and stream live application logs directly from your terminal.
47
+ - **Integrated Project Management:** Easily list, view, and destroy your deployed projects.
48
+ - **Secure Authentication:** Uses OAuth2 PKCE flow for secure, token-based authentication.
49
+
50
+ ### 🚀 Quickstart
51
+
52
+ #### 1. Installation
53
+
54
+ Install the Xenfra CLI using `uv` (recommended) or `pip`:
55
+
56
+ ```bash
57
+ uv pip install xenfra-cli
58
+ # or
59
+ pip install xenfra-cli
60
+ ```
61
+
62
+ #### 2. Authentication
63
+
64
+ Log in to your Xenfra account. This will open your web browser to complete the OAuth2 flow.
65
+
66
+ ```bash
67
+ xenfra auth login
68
+ ```
69
+
70
+ #### 3. Initialize Your Project
71
+
72
+ Navigate to your Python project's root directory and run `init`. The CLI will scan your codebase, detect its characteristics, and generate a `xenfra.yaml` configuration file.
73
+
74
+ ```bash
75
+ cd your-python-project/
76
+ xenfra init
77
+ ```
78
+
79
+ #### 4. Deploy Your Application
80
+
81
+ Once `xenfra.yaml` is configured, deploy your application. The CLI will handle provisioning a DigitalOcean Droplet, setting up Docker, and deploying your code.
82
+
83
+ ```bash
84
+ xenfra deploy
85
+ ```
86
+
87
+ ### 📋 Usage Examples
88
+
89
+ - **Monitor Deployment Status:**
90
+ ```bash
91
+ xenfra status <deployment-id>
92
+ ```
93
+ - **Stream Application Logs:**
94
+ ```bash
95
+ xenfra logs <deployment-id>
96
+ ```
97
+ - **List Deployed Projects:**
98
+ ```bash
99
+ xenfra projects list
100
+ ```
101
+ - **Diagnose a Failed Deployment (AI-Powered):**
102
+ ```bash
103
+ xenfra diagnose <deployment-id>
104
+ # Or to diagnose from a log file:
105
+ xenfra diagnose --logs error.log
106
+ ```
107
+
108
+ ### 📚 Documentation
109
+
110
+ For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.tech/cli) (Link will be updated upon final deployment).
111
+
112
+ ### 🤝 Contributing
113
+
114
+ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more details.
115
+
116
+ ### 📄 License
117
+
118
+ This project is licensed under the [MIT License](LICENSE).
xenfra-0.3.8/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Xenfra CLI
2
+
3
+ ## Xenfra CLI: Deploy Python Apps with Zen Mode
4
+
5
+ The Xenfra CLI is a powerful and intuitive command-line interface designed to streamline the deployment of Python applications to DigitalOcean. Built with a "Zen Mode" philosophy, it automates complex infrastructure tasks, allowing developers to focus on writing code.
6
+
7
+ ### ✨ Key Features
8
+
9
+ - **Zero-Configuration Deployment:** Automatically detects your project's framework and dependencies.
10
+ - **AI-Powered Auto-Healing:** Diagnoses common deployment failures and suggests, or even applies, fixes automatically.
11
+ - **Real-time Monitoring:** View deployment status and stream live application logs directly from your terminal.
12
+ - **Integrated Project Management:** Easily list, view, and destroy your deployed projects.
13
+ - **Secure Authentication:** Uses OAuth2 PKCE flow for secure, token-based authentication.
14
+
15
+ ### 🚀 Quickstart
16
+
17
+ #### 1. Installation
18
+
19
+ Install the Xenfra CLI using `uv` (recommended) or `pip`:
20
+
21
+ ```bash
22
+ uv pip install xenfra-cli
23
+ # or
24
+ pip install xenfra-cli
25
+ ```
26
+
27
+ #### 2. Authentication
28
+
29
+ Log in to your Xenfra account. This will open your web browser to complete the OAuth2 flow.
30
+
31
+ ```bash
32
+ xenfra auth login
33
+ ```
34
+
35
+ #### 3. Initialize Your Project
36
+
37
+ Navigate to your Python project's root directory and run `init`. The CLI will scan your codebase, detect its characteristics, and generate a `xenfra.yaml` configuration file.
38
+
39
+ ```bash
40
+ cd your-python-project/
41
+ xenfra init
42
+ ```
43
+
44
+ #### 4. Deploy Your Application
45
+
46
+ Once `xenfra.yaml` is configured, deploy your application. The CLI will handle provisioning a DigitalOcean Droplet, setting up Docker, and deploying your code.
47
+
48
+ ```bash
49
+ xenfra deploy
50
+ ```
51
+
52
+ ### 📋 Usage Examples
53
+
54
+ - **Monitor Deployment Status:**
55
+ ```bash
56
+ xenfra status <deployment-id>
57
+ ```
58
+ - **Stream Application Logs:**
59
+ ```bash
60
+ xenfra logs <deployment-id>
61
+ ```
62
+ - **List Deployed Projects:**
63
+ ```bash
64
+ xenfra projects list
65
+ ```
66
+ - **Diagnose a Failed Deployment (AI-Powered):**
67
+ ```bash
68
+ xenfra diagnose <deployment-id>
69
+ # Or to diagnose from a log file:
70
+ xenfra diagnose --logs error.log
71
+ ```
72
+
73
+ ### 📚 Documentation
74
+
75
+ For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.tech/cli) (Link will be updated upon final deployment).
76
+
77
+ ### 🤝 Contributing
78
+
79
+ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more details.
80
+
81
+ ### 📄 License
82
+
83
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "xenfra"
3
+ version = "0.3.8"
4
+ description = "A 'Zen Mode' infrastructure engine for Python developers."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "xenfra-cloud", email = "xenfracloud@gmail.com" }
8
+ ]
9
+
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Topic :: Software Development :: Build Tools",
17
+ "Topic :: System :: Systems Administration",
18
+ ]
19
+
20
+ dependencies = [
21
+ "click>=8.1.7",
22
+ "rich>=14.2.0",
23
+ "sqlmodel>=0.0.16",
24
+ "python-digitalocean>=1.17.0",
25
+ "python-dotenv>=1.2.1",
26
+ "pyyaml>=6.0.1",
27
+ "fabric>=3.2.2",
28
+ "xenfra-sdk",
29
+ "httpx>=0.27.0",
30
+ "keyring>=25.7.0",
31
+ "keyrings.alt>=5.0.2",
32
+ "tenacity>=8.2.3", # For retry logic
33
+ "cryptography>=43.0.0", # For encrypted file-based token storage
34
+ "toml>=0.10.2",
35
+ ]
36
+ requires-python = ">=3.13"
37
+
38
+ [tool.uv.sources]
39
+ xenfra-sdk = { workspace = true }
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/xenfra-cloud/xenfra"
43
+ Issues = "https://github.com/xenfra-cloud/xenfra/issues"
44
+
45
+ [project.optional-dependencies]
46
+ test = [
47
+ "pytest>=8.0.0",
48
+ "pytest-mock>=3.12.0",
49
+ ]
50
+
51
+ [project.scripts]
52
+ xenfra = "xenfra.main:main"
53
+
54
+ [build-system]
55
+ requires = ["uv_build>=0.9.18,<0.10.0"]
56
+ build-backend = "uv_build"
File without changes
@@ -0,0 +1,3 @@
1
+ """
2
+ CLI command modules for Xenfra.
3
+ """
@@ -0,0 +1,144 @@
1
+ """
2
+ Authentication commands for Xenfra CLI.
3
+ """
4
+
5
+ import base64
6
+ import hashlib
7
+ import secrets
8
+ import urllib.parse
9
+ import webbrowser
10
+ from http.server import HTTPServer
11
+
12
+ import click
13
+ import httpx
14
+ import keyring
15
+ from rich.console import Console
16
+ from tenacity import (
17
+ retry,
18
+ retry_if_exception_type,
19
+ stop_after_attempt,
20
+ wait_exponential,
21
+ )
22
+
23
+ from ..utils.auth import (
24
+ API_BASE_URL,
25
+ CLI_CLIENT_ID,
26
+ CLI_LOCAL_SERVER_END_PORT,
27
+ CLI_LOCAL_SERVER_START_PORT,
28
+ CLI_REDIRECT_PATH,
29
+ SERVICE_ID,
30
+ AuthCallbackHandler,
31
+ clear_tokens,
32
+ get_auth_token,
33
+ )
34
+
35
+ console = Console()
36
+
37
+ # HTTP request timeout (30 seconds)
38
+ HTTP_TIMEOUT = 30.0
39
+
40
+
41
+ @click.group()
42
+ def auth():
43
+ """Authentication commands (login, logout, whoami)."""
44
+ pass
45
+
46
+
47
+ @retry(
48
+ stop=stop_after_attempt(3),
49
+ wait=wait_exponential(multiplier=1, min=2, max=10),
50
+ retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
51
+ reraise=True,
52
+ )
53
+ def _exchange_code_for_tokens_with_retry(code: str, code_verifier: str, redirect_uri: str) -> dict:
54
+ """
55
+ Exchange authorization code for tokens with retry logic.
56
+
57
+ Returns token data dictionary.
58
+ """
59
+ with httpx.Client(timeout=HTTP_TIMEOUT) as client:
60
+ response = client.post(
61
+ f"{API_BASE_URL}/auth/token",
62
+ data={
63
+ "grant_type": "authorization_code",
64
+ "client_id": CLI_CLIENT_ID,
65
+ "code": code,
66
+ "code_verifier": code_verifier,
67
+ "redirect_uri": redirect_uri,
68
+ },
69
+ headers={"Accept": "application/json"},
70
+ )
71
+ response.raise_for_status()
72
+
73
+ # Safe JSON parsing with content-type check
74
+ content_type = response.headers.get("content-type", "")
75
+ if "application/json" not in content_type:
76
+ raise ValueError(f"Expected JSON response, got {content_type}")
77
+
78
+ try:
79
+ token_data = response.json()
80
+ except (ValueError, TypeError) as e:
81
+ raise ValueError(f"Failed to parse JSON response: {e}")
82
+
83
+ return token_data
84
+
85
+
86
+ @auth.command()
87
+ def login():
88
+ """Login to Xenfra using Device Authorization Flow (like GitHub CLI, Claude Code)."""
89
+ from .auth_device import device_login
90
+ device_login()
91
+
92
+ # Removed old PKCE flow - now using Device Authorization Flow
93
+
94
+
95
+ @auth.command()
96
+ def logout():
97
+ """Logout and clear stored tokens."""
98
+ try:
99
+ clear_tokens()
100
+ console.print("[bold green]Logged out successfully.[/bold green]")
101
+ except Exception as e:
102
+ console.print(f"[yellow]Warning: Error during logout: {e}[/yellow]")
103
+ console.print("[dim]Tokens may still be stored in keyring.[/dim]")
104
+
105
+
106
+ @auth.command()
107
+ @click.option("--token", is_flag=True, help="Show access token")
108
+ def whoami(token):
109
+ """Show current authenticated user."""
110
+ access_token = get_auth_token()
111
+
112
+ if not access_token:
113
+ console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
114
+ return
115
+
116
+ try:
117
+ import base64
118
+ import json
119
+
120
+ # Manually decode JWT payload without verification
121
+ # JWT format: header.payload.signature
122
+ parts = access_token.split(".")
123
+ if len(parts) != 3:
124
+ console.print("[bold red]Invalid token format[/bold red]")
125
+ return
126
+
127
+ # Decode payload (second part)
128
+ payload_b64 = parts[1]
129
+ # Add padding if needed
130
+ padding = 4 - len(payload_b64) % 4
131
+ if padding != 4:
132
+ payload_b64 += "=" * padding
133
+
134
+ payload_bytes = base64.urlsafe_b64decode(payload_b64)
135
+ claims = json.loads(payload_bytes)
136
+
137
+ console.print("[bold green]Logged in as:[/bold green]")
138
+ console.print(f" Email: {claims.get('sub', 'N/A')}")
139
+ console.print(f" User ID: {claims.get('user_id', 'N/A')}")
140
+
141
+ if token:
142
+ console.print(f"\n[dim]Access Token:[/dim]\n{access_token}")
143
+ except Exception as e:
144
+ console.print(f"[bold red]Failed to decode token: {e}[/bold red]")
@@ -0,0 +1,164 @@
1
+ """
2
+ Device Authorization Flow for Xenfra CLI.
3
+ Modern OAuth flow used by GitHub CLI, AWS CLI, Claude Code, etc.
4
+ """
5
+
6
+ import time
7
+ import webbrowser
8
+ from urllib.parse import urlencode
9
+
10
+ import click
11
+ import httpx
12
+ import keyring
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+
16
+ from ..utils.auth import API_BASE_URL, CLI_CLIENT_ID, HTTP_TIMEOUT, SERVICE_ID
17
+
18
+ console = Console()
19
+
20
+
21
+ def device_login():
22
+ """
23
+ Device Authorization Flow (OAuth 2.0 Device Grant).
24
+
25
+ Flow:
26
+ 1. CLI calls /auth/device/authorize to get device_code and user_code
27
+ 2. User visits https://www.xenfra.tech/activate and enters user_code
28
+ 3. CLI polls /auth/device/token until user authorizes
29
+ 4. CLI receives access_token and stores it
30
+ """
31
+ try:
32
+ # Step 1: Request device code
33
+ console.print("[cyan]Initiating device authorization...[/cyan]")
34
+
35
+ with httpx.Client(timeout=HTTP_TIMEOUT) as client:
36
+ response = client.post(
37
+ f"{API_BASE_URL}/auth/device/authorize",
38
+ data={
39
+ "client_id": CLI_CLIENT_ID,
40
+ "scope": "openid profile",
41
+ },
42
+ )
43
+ response.raise_for_status()
44
+ device_data = response.json()
45
+
46
+ device_code = device_data["device_code"]
47
+ user_code = device_data["user_code"]
48
+ verification_uri = device_data["verification_uri"]
49
+ verification_uri_complete = device_data.get("verification_uri_complete")
50
+ expires_in = device_data["expires_in"]
51
+ interval = device_data.get("interval", 5)
52
+
53
+ # Step 2: Show user code and open browser
54
+ console.print()
55
+ console.print(
56
+ Panel.fit(
57
+ f"[bold white]{user_code}[/bold white]",
58
+ title="[bold green]Your Activation Code[/bold green]",
59
+ border_style="green",
60
+ )
61
+ )
62
+ console.print()
63
+ console.print(f"[bold]Visit:[/bold] [link]{verification_uri}[/link]")
64
+ console.print(f"[bold]Enter code:[/bold] [cyan]{user_code}[/cyan]")
65
+ console.print()
66
+
67
+ # Open browser automatically
68
+ try:
69
+ url_to_open = verification_uri_complete or verification_uri
70
+ webbrowser.open(url_to_open)
71
+ console.print("[dim]Opening browser...[/dim]")
72
+ except Exception:
73
+ console.print("[yellow]Could not open browser automatically. Please visit the URL above.[/yellow]")
74
+
75
+ # Step 3: Poll for authorization
76
+ console.print()
77
+ console.print("[cyan]Waiting for authorization...[/cyan]")
78
+ console.print("[dim](Press Ctrl+C to cancel)[/dim]")
79
+ console.print()
80
+
81
+ start_time = time.time()
82
+ poll_count = 0
83
+
84
+ with httpx.Client(timeout=HTTP_TIMEOUT) as client:
85
+ while True:
86
+ # Check timeout
87
+ if time.time() - start_time > expires_in:
88
+ console.print("[bold red]✗ Authorization timed out. Please try again.[/bold red]")
89
+ return False
90
+
91
+ # Poll the token endpoint
92
+ try:
93
+ response = client.post(
94
+ f"{API_BASE_URL}/auth/device/token",
95
+ data={
96
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
97
+ "device_code": device_code,
98
+ "client_id": CLI_CLIENT_ID,
99
+ },
100
+ )
101
+
102
+ if response.status_code == 200:
103
+ # Success! User authorized
104
+ token_data = response.json()
105
+ access_token = token_data["access_token"]
106
+ refresh_token = token_data.get("refresh_token")
107
+
108
+ # Store tokens (keyring or file fallback)
109
+ try:
110
+ keyring.set_password(SERVICE_ID, "access_token", access_token)
111
+ if refresh_token:
112
+ keyring.set_password(SERVICE_ID, "refresh_token", refresh_token)
113
+ except keyring.errors.KeyringError as e:
114
+ console.print(f"[dim]Keyring unavailable, using file storage: {e}[/dim]")
115
+ # Fallback to file storage
116
+ from ..utils.auth import _set_token_to_file
117
+ _set_token_to_file("access_token", access_token)
118
+ if refresh_token:
119
+ _set_token_to_file("refresh_token", refresh_token)
120
+
121
+ console.print()
122
+ console.print("[bold green]✓ Successfully authenticated![/bold green]")
123
+ console.print()
124
+ return True
125
+
126
+ elif response.status_code == 400:
127
+ error_data = response.json()
128
+ error = error_data.get("error", "unknown_error")
129
+
130
+ if error == "authorization_pending":
131
+ # Still waiting for user to authorize
132
+ poll_count += 1
133
+ if poll_count % 6 == 0: # Every 30 seconds
134
+ console.print("[dim]Still waiting...[/dim]")
135
+ time.sleep(interval)
136
+ continue
137
+
138
+ elif error == "slow_down":
139
+ # We're polling too fast
140
+ interval += 5
141
+ time.sleep(interval)
142
+ continue
143
+
144
+ else:
145
+ # Other error
146
+ error_desc = error_data.get("error_description", error)
147
+ console.print(f"[bold red]✗ Authorization failed: {error_desc}[/bold red]")
148
+ return False
149
+
150
+ else:
151
+ console.print(f"[bold red]✗ Unexpected response: {response.status_code}[/bold red]")
152
+ return False
153
+
154
+ except httpx.HTTPError as e:
155
+ console.print(f"[bold red]✗ Network error: {e}[/bold red]")
156
+ return False
157
+
158
+ except KeyboardInterrupt:
159
+ console.print()
160
+ console.print("[yellow]Authorization cancelled.[/yellow]")
161
+ return False
162
+ except Exception as e:
163
+ console.print(f"[bold red]✗ Error: {e}[/bold red]")
164
+ return False