xenfra 0.4.3__py3-none-any.whl → 0.4.4__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.
- xenfra/commands/__init__.py +3 -3
- xenfra/commands/auth.py +144 -144
- xenfra/commands/auth_device.py +164 -164
- xenfra/commands/deployments.py +1133 -973
- xenfra/commands/intelligence.py +503 -412
- xenfra/commands/projects.py +204 -204
- xenfra/commands/security_cmd.py +233 -233
- xenfra/main.py +76 -75
- xenfra/utils/__init__.py +3 -3
- xenfra/utils/auth.py +374 -374
- xenfra/utils/codebase.py +169 -169
- xenfra/utils/config.py +459 -436
- xenfra/utils/errors.py +116 -116
- xenfra/utils/file_sync.py +286 -286
- xenfra/utils/security.py +336 -336
- xenfra/utils/validation.py +234 -234
- xenfra-0.4.4.dist-info/METADATA +113 -0
- xenfra-0.4.4.dist-info/RECORD +21 -0
- xenfra-0.4.3.dist-info/METADATA +0 -118
- xenfra-0.4.3.dist-info/RECORD +0 -21
- {xenfra-0.4.3.dist-info → xenfra-0.4.4.dist-info}/WHEEL +0 -0
- {xenfra-0.4.3.dist-info → xenfra-0.4.4.dist-info}/entry_points.txt +0 -0
xenfra/utils/validation.py
CHANGED
|
@@ -1,234 +1,234 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Validation utilities for CLI commands.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
import uuid
|
|
7
|
-
from typing import Optional
|
|
8
|
-
from urllib.parse import urlparse
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def validate_deployment_id(deployment_id: str) -> tuple[bool, Optional[str]]:
|
|
12
|
-
"""
|
|
13
|
-
Validate deployment ID format (UUID or positive integer).
|
|
14
|
-
|
|
15
|
-
Returns (is_valid, error_message).
|
|
16
|
-
"""
|
|
17
|
-
if not deployment_id or not isinstance(deployment_id, str):
|
|
18
|
-
return False, "Deployment ID cannot be empty"
|
|
19
|
-
|
|
20
|
-
deployment_id = deployment_id.strip()
|
|
21
|
-
|
|
22
|
-
# Try UUID format first
|
|
23
|
-
try:
|
|
24
|
-
uuid.UUID(deployment_id)
|
|
25
|
-
return True, None
|
|
26
|
-
except ValueError:
|
|
27
|
-
pass
|
|
28
|
-
|
|
29
|
-
# Try positive integer
|
|
30
|
-
try:
|
|
31
|
-
if int(deployment_id) > 0:
|
|
32
|
-
return True, None
|
|
33
|
-
except ValueError:
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
return False, "Deployment ID must be a valid UUID or positive integer"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def validate_project_id(project_id: int) -> tuple[bool, Optional[str]]:
|
|
40
|
-
"""
|
|
41
|
-
Validate project ID (must be positive integer).
|
|
42
|
-
|
|
43
|
-
Returns (is_valid, error_message).
|
|
44
|
-
"""
|
|
45
|
-
if not isinstance(project_id, int):
|
|
46
|
-
return False, "Project ID must be an integer"
|
|
47
|
-
|
|
48
|
-
if project_id <= 0:
|
|
49
|
-
return False, "Project ID must be a positive integer"
|
|
50
|
-
|
|
51
|
-
return True, None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def validate_git_repo_url(git_repo: str) -> tuple[bool, Optional[str]]:
|
|
55
|
-
"""
|
|
56
|
-
Validate git repository URL format.
|
|
57
|
-
|
|
58
|
-
Returns (is_valid, error_message).
|
|
59
|
-
"""
|
|
60
|
-
if not git_repo or not isinstance(git_repo, str):
|
|
61
|
-
return False, "Git repository URL cannot be empty"
|
|
62
|
-
|
|
63
|
-
git_repo = git_repo.strip()
|
|
64
|
-
|
|
65
|
-
if len(git_repo) > 2048:
|
|
66
|
-
return False, "Git repository URL is too long (max 2048 characters)"
|
|
67
|
-
|
|
68
|
-
try:
|
|
69
|
-
parsed = urlparse(git_repo)
|
|
70
|
-
|
|
71
|
-
# Must have scheme
|
|
72
|
-
if parsed.scheme not in ["http", "https", "git"]:
|
|
73
|
-
return False, "Git repository URL must use http, https, or git scheme"
|
|
74
|
-
|
|
75
|
-
# Must have hostname
|
|
76
|
-
if not parsed.hostname:
|
|
77
|
-
return False, "Git repository URL must have a hostname"
|
|
78
|
-
|
|
79
|
-
# Common git hosting patterns
|
|
80
|
-
if not any(
|
|
81
|
-
domain in parsed.hostname.lower()
|
|
82
|
-
for domain in ["github.com", "gitlab.com", "bitbucket.org", "gitea.com"]
|
|
83
|
-
):
|
|
84
|
-
# Allow custom domains but warn
|
|
85
|
-
pass
|
|
86
|
-
|
|
87
|
-
return True, None
|
|
88
|
-
|
|
89
|
-
except Exception as e:
|
|
90
|
-
return False, f"Invalid git repository URL format: {e}"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def validate_project_name(project_name: str) -> tuple[bool, Optional[str]]:
|
|
94
|
-
"""
|
|
95
|
-
Validate project name format.
|
|
96
|
-
|
|
97
|
-
Returns (is_valid, error_message).
|
|
98
|
-
"""
|
|
99
|
-
if not project_name or not isinstance(project_name, str):
|
|
100
|
-
return False, "Project name cannot be empty"
|
|
101
|
-
|
|
102
|
-
project_name = project_name.strip()
|
|
103
|
-
|
|
104
|
-
if len(project_name) > 100:
|
|
105
|
-
return False, "Project name is too long (max 100 characters)"
|
|
106
|
-
|
|
107
|
-
if len(project_name) < 1:
|
|
108
|
-
return False, "Project name is too short (min 1 character)"
|
|
109
|
-
|
|
110
|
-
# Alphanumeric, hyphens, underscores, dots
|
|
111
|
-
if not re.match(r"^[a-zA-Z0-9._-]+$", project_name):
|
|
112
|
-
return (
|
|
113
|
-
False,
|
|
114
|
-
"Project name can only contain alphanumeric characters, dots, hyphens, and underscores",
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
# Reserved names
|
|
118
|
-
reserved_names = ["admin", "api", "www", "root", "system", "xenfra"]
|
|
119
|
-
if project_name.lower() in reserved_names:
|
|
120
|
-
return False, f"Project name '{project_name}' is reserved"
|
|
121
|
-
|
|
122
|
-
return True, None
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def validate_branch_name(branch: str) -> tuple[bool, Optional[str]]:
|
|
126
|
-
"""
|
|
127
|
-
Validate git branch name format.
|
|
128
|
-
|
|
129
|
-
Returns (is_valid, error_message).
|
|
130
|
-
"""
|
|
131
|
-
if not branch or not isinstance(branch, str):
|
|
132
|
-
return False, "Branch name cannot be empty"
|
|
133
|
-
|
|
134
|
-
branch = branch.strip()
|
|
135
|
-
|
|
136
|
-
if len(branch) > 255:
|
|
137
|
-
return False, "Branch name is too long (max 255 characters)"
|
|
138
|
-
|
|
139
|
-
# Git branch name rules: no spaces, no special chars except /, -, _
|
|
140
|
-
if not re.match(r"^[a-zA-Z0-9/._-]+$", branch):
|
|
141
|
-
return (
|
|
142
|
-
False,
|
|
143
|
-
"Branch name can only contain alphanumeric characters, slashes, dots, hyphens, and underscores",
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# Cannot start with . or end with .lock
|
|
147
|
-
if branch.startswith(".") or branch.endswith(".lock"):
|
|
148
|
-
return False, "Branch name cannot start with '.' or end with '.lock'"
|
|
149
|
-
|
|
150
|
-
return True, None
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def validate_framework(framework: str) -> tuple[bool, Optional[str]]:
|
|
154
|
-
"""
|
|
155
|
-
Validate framework name.
|
|
156
|
-
|
|
157
|
-
Returns (is_valid, error_message).
|
|
158
|
-
"""
|
|
159
|
-
if not framework or not isinstance(framework, str):
|
|
160
|
-
return False, "Framework cannot be empty"
|
|
161
|
-
|
|
162
|
-
framework = framework.strip().lower()
|
|
163
|
-
|
|
164
|
-
allowed_frameworks = ["fastapi", "flask", "django", "other"]
|
|
165
|
-
if framework not in allowed_frameworks:
|
|
166
|
-
return False, f"Framework must be one of: {', '.join(allowed_frameworks)}"
|
|
167
|
-
|
|
168
|
-
return True, None
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def validate_port(port: int) -> tuple[bool, Optional[str]]:
|
|
172
|
-
"""
|
|
173
|
-
Validate port number.
|
|
174
|
-
|
|
175
|
-
Returns (is_valid, error_message).
|
|
176
|
-
"""
|
|
177
|
-
if not isinstance(port, int):
|
|
178
|
-
return False, "Port must be an integer"
|
|
179
|
-
|
|
180
|
-
if port < 1 or port > 65535:
|
|
181
|
-
return False, "Port must be between 1 and 65535"
|
|
182
|
-
|
|
183
|
-
return True, None
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def validate_region(region: str) -> tuple[bool, Optional[str]]:
|
|
187
|
-
"""
|
|
188
|
-
Validate DigitalOcean region.
|
|
189
|
-
|
|
190
|
-
Returns (is_valid, error_message).
|
|
191
|
-
"""
|
|
192
|
-
if not region or not isinstance(region, str):
|
|
193
|
-
return False, "Region cannot be empty"
|
|
194
|
-
|
|
195
|
-
region = region.strip().lower()
|
|
196
|
-
|
|
197
|
-
# Common DigitalOcean regions (not exhaustive, but validates format)
|
|
198
|
-
if not re.match(r"^[a-z]{3}[0-9]$", region):
|
|
199
|
-
return False, "Region must be in format 'xxx1' (e.g., 'nyc3', 'sfo3')"
|
|
200
|
-
|
|
201
|
-
return True, None
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def validate_size_slug(size_slug: str) -> tuple[bool, Optional[str]]:
|
|
205
|
-
"""
|
|
206
|
-
Validate DigitalOcean size slug.
|
|
207
|
-
|
|
208
|
-
Returns (is_valid, error_message).
|
|
209
|
-
"""
|
|
210
|
-
if not size_slug or not isinstance(size_slug, str):
|
|
211
|
-
return False, "Size slug cannot be empty"
|
|
212
|
-
|
|
213
|
-
size_slug = size_slug.strip().lower()
|
|
214
|
-
|
|
215
|
-
# DigitalOcean size slug format: s-{vcpu}vcpu-{ram}gb
|
|
216
|
-
if not re.match(r"^s-[0-9]+vcpu-[0-9]+gb$", size_slug):
|
|
217
|
-
return False, "Size slug must be in format 's-Xvcpu-Ygb' (e.g., 's-1vcpu-1gb')"
|
|
218
|
-
|
|
219
|
-
return True, None
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def validate_codebase_scan_limits(max_files: int, max_size: int) -> tuple[bool, Optional[str]]:
|
|
223
|
-
"""
|
|
224
|
-
Validate codebase scan limits.
|
|
225
|
-
|
|
226
|
-
Returns (is_valid, error_message).
|
|
227
|
-
"""
|
|
228
|
-
if not isinstance(max_files, int) or max_files < 1 or max_files > 100:
|
|
229
|
-
return False, "max_files must be between 1 and 100"
|
|
230
|
-
|
|
231
|
-
if not isinstance(max_size, int) or max_size < 1024 or max_size > 10 * 1024 * 1024:
|
|
232
|
-
return False, "max_size must be between 1KB and 10MB"
|
|
233
|
-
|
|
234
|
-
return True, None
|
|
1
|
+
"""
|
|
2
|
+
Validation utilities for CLI commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_deployment_id(deployment_id: str) -> tuple[bool, Optional[str]]:
|
|
12
|
+
"""
|
|
13
|
+
Validate deployment ID format (UUID or positive integer).
|
|
14
|
+
|
|
15
|
+
Returns (is_valid, error_message).
|
|
16
|
+
"""
|
|
17
|
+
if not deployment_id or not isinstance(deployment_id, str):
|
|
18
|
+
return False, "Deployment ID cannot be empty"
|
|
19
|
+
|
|
20
|
+
deployment_id = deployment_id.strip()
|
|
21
|
+
|
|
22
|
+
# Try UUID format first
|
|
23
|
+
try:
|
|
24
|
+
uuid.UUID(deployment_id)
|
|
25
|
+
return True, None
|
|
26
|
+
except ValueError:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
# Try positive integer
|
|
30
|
+
try:
|
|
31
|
+
if int(deployment_id) > 0:
|
|
32
|
+
return True, None
|
|
33
|
+
except ValueError:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
return False, "Deployment ID must be a valid UUID or positive integer"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_project_id(project_id: int) -> tuple[bool, Optional[str]]:
|
|
40
|
+
"""
|
|
41
|
+
Validate project ID (must be positive integer).
|
|
42
|
+
|
|
43
|
+
Returns (is_valid, error_message).
|
|
44
|
+
"""
|
|
45
|
+
if not isinstance(project_id, int):
|
|
46
|
+
return False, "Project ID must be an integer"
|
|
47
|
+
|
|
48
|
+
if project_id <= 0:
|
|
49
|
+
return False, "Project ID must be a positive integer"
|
|
50
|
+
|
|
51
|
+
return True, None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_git_repo_url(git_repo: str) -> tuple[bool, Optional[str]]:
|
|
55
|
+
"""
|
|
56
|
+
Validate git repository URL format.
|
|
57
|
+
|
|
58
|
+
Returns (is_valid, error_message).
|
|
59
|
+
"""
|
|
60
|
+
if not git_repo or not isinstance(git_repo, str):
|
|
61
|
+
return False, "Git repository URL cannot be empty"
|
|
62
|
+
|
|
63
|
+
git_repo = git_repo.strip()
|
|
64
|
+
|
|
65
|
+
if len(git_repo) > 2048:
|
|
66
|
+
return False, "Git repository URL is too long (max 2048 characters)"
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
parsed = urlparse(git_repo)
|
|
70
|
+
|
|
71
|
+
# Must have scheme
|
|
72
|
+
if parsed.scheme not in ["http", "https", "git"]:
|
|
73
|
+
return False, "Git repository URL must use http, https, or git scheme"
|
|
74
|
+
|
|
75
|
+
# Must have hostname
|
|
76
|
+
if not parsed.hostname:
|
|
77
|
+
return False, "Git repository URL must have a hostname"
|
|
78
|
+
|
|
79
|
+
# Common git hosting patterns
|
|
80
|
+
if not any(
|
|
81
|
+
domain in parsed.hostname.lower()
|
|
82
|
+
for domain in ["github.com", "gitlab.com", "bitbucket.org", "gitea.com"]
|
|
83
|
+
):
|
|
84
|
+
# Allow custom domains but warn
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
return True, None
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return False, f"Invalid git repository URL format: {e}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def validate_project_name(project_name: str) -> tuple[bool, Optional[str]]:
|
|
94
|
+
"""
|
|
95
|
+
Validate project name format.
|
|
96
|
+
|
|
97
|
+
Returns (is_valid, error_message).
|
|
98
|
+
"""
|
|
99
|
+
if not project_name or not isinstance(project_name, str):
|
|
100
|
+
return False, "Project name cannot be empty"
|
|
101
|
+
|
|
102
|
+
project_name = project_name.strip()
|
|
103
|
+
|
|
104
|
+
if len(project_name) > 100:
|
|
105
|
+
return False, "Project name is too long (max 100 characters)"
|
|
106
|
+
|
|
107
|
+
if len(project_name) < 1:
|
|
108
|
+
return False, "Project name is too short (min 1 character)"
|
|
109
|
+
|
|
110
|
+
# Alphanumeric, hyphens, underscores, dots
|
|
111
|
+
if not re.match(r"^[a-zA-Z0-9._-]+$", project_name):
|
|
112
|
+
return (
|
|
113
|
+
False,
|
|
114
|
+
"Project name can only contain alphanumeric characters, dots, hyphens, and underscores",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Reserved names
|
|
118
|
+
reserved_names = ["admin", "api", "www", "root", "system", "xenfra"]
|
|
119
|
+
if project_name.lower() in reserved_names:
|
|
120
|
+
return False, f"Project name '{project_name}' is reserved"
|
|
121
|
+
|
|
122
|
+
return True, None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_branch_name(branch: str) -> tuple[bool, Optional[str]]:
|
|
126
|
+
"""
|
|
127
|
+
Validate git branch name format.
|
|
128
|
+
|
|
129
|
+
Returns (is_valid, error_message).
|
|
130
|
+
"""
|
|
131
|
+
if not branch or not isinstance(branch, str):
|
|
132
|
+
return False, "Branch name cannot be empty"
|
|
133
|
+
|
|
134
|
+
branch = branch.strip()
|
|
135
|
+
|
|
136
|
+
if len(branch) > 255:
|
|
137
|
+
return False, "Branch name is too long (max 255 characters)"
|
|
138
|
+
|
|
139
|
+
# Git branch name rules: no spaces, no special chars except /, -, _
|
|
140
|
+
if not re.match(r"^[a-zA-Z0-9/._-]+$", branch):
|
|
141
|
+
return (
|
|
142
|
+
False,
|
|
143
|
+
"Branch name can only contain alphanumeric characters, slashes, dots, hyphens, and underscores",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Cannot start with . or end with .lock
|
|
147
|
+
if branch.startswith(".") or branch.endswith(".lock"):
|
|
148
|
+
return False, "Branch name cannot start with '.' or end with '.lock'"
|
|
149
|
+
|
|
150
|
+
return True, None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def validate_framework(framework: str) -> tuple[bool, Optional[str]]:
|
|
154
|
+
"""
|
|
155
|
+
Validate framework name.
|
|
156
|
+
|
|
157
|
+
Returns (is_valid, error_message).
|
|
158
|
+
"""
|
|
159
|
+
if not framework or not isinstance(framework, str):
|
|
160
|
+
return False, "Framework cannot be empty"
|
|
161
|
+
|
|
162
|
+
framework = framework.strip().lower()
|
|
163
|
+
|
|
164
|
+
allowed_frameworks = ["fastapi", "flask", "django", "other"]
|
|
165
|
+
if framework not in allowed_frameworks:
|
|
166
|
+
return False, f"Framework must be one of: {', '.join(allowed_frameworks)}"
|
|
167
|
+
|
|
168
|
+
return True, None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def validate_port(port: int) -> tuple[bool, Optional[str]]:
|
|
172
|
+
"""
|
|
173
|
+
Validate port number.
|
|
174
|
+
|
|
175
|
+
Returns (is_valid, error_message).
|
|
176
|
+
"""
|
|
177
|
+
if not isinstance(port, int):
|
|
178
|
+
return False, "Port must be an integer"
|
|
179
|
+
|
|
180
|
+
if port < 1 or port > 65535:
|
|
181
|
+
return False, "Port must be between 1 and 65535"
|
|
182
|
+
|
|
183
|
+
return True, None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def validate_region(region: str) -> tuple[bool, Optional[str]]:
|
|
187
|
+
"""
|
|
188
|
+
Validate DigitalOcean region.
|
|
189
|
+
|
|
190
|
+
Returns (is_valid, error_message).
|
|
191
|
+
"""
|
|
192
|
+
if not region or not isinstance(region, str):
|
|
193
|
+
return False, "Region cannot be empty"
|
|
194
|
+
|
|
195
|
+
region = region.strip().lower()
|
|
196
|
+
|
|
197
|
+
# Common DigitalOcean regions (not exhaustive, but validates format)
|
|
198
|
+
if not re.match(r"^[a-z]{3}[0-9]$", region):
|
|
199
|
+
return False, "Region must be in format 'xxx1' (e.g., 'nyc3', 'sfo3')"
|
|
200
|
+
|
|
201
|
+
return True, None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def validate_size_slug(size_slug: str) -> tuple[bool, Optional[str]]:
|
|
205
|
+
"""
|
|
206
|
+
Validate DigitalOcean size slug.
|
|
207
|
+
|
|
208
|
+
Returns (is_valid, error_message).
|
|
209
|
+
"""
|
|
210
|
+
if not size_slug or not isinstance(size_slug, str):
|
|
211
|
+
return False, "Size slug cannot be empty"
|
|
212
|
+
|
|
213
|
+
size_slug = size_slug.strip().lower()
|
|
214
|
+
|
|
215
|
+
# DigitalOcean size slug format: s-{vcpu}vcpu-{ram}gb
|
|
216
|
+
if not re.match(r"^s-[0-9]+vcpu-[0-9]+gb$", size_slug):
|
|
217
|
+
return False, "Size slug must be in format 's-Xvcpu-Ygb' (e.g., 's-1vcpu-1gb')"
|
|
218
|
+
|
|
219
|
+
return True, None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def validate_codebase_scan_limits(max_files: int, max_size: int) -> tuple[bool, Optional[str]]:
|
|
223
|
+
"""
|
|
224
|
+
Validate codebase scan limits.
|
|
225
|
+
|
|
226
|
+
Returns (is_valid, error_message).
|
|
227
|
+
"""
|
|
228
|
+
if not isinstance(max_files, int) or max_files < 1 or max_files > 100:
|
|
229
|
+
return False, "max_files must be between 1 and 100"
|
|
230
|
+
|
|
231
|
+
if not isinstance(max_size, int) or max_size < 1024 or max_size > 10 * 1024 * 1024:
|
|
232
|
+
return False, "max_size must be between 1KB and 10MB"
|
|
233
|
+
|
|
234
|
+
return True, None
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: xenfra
|
|
3
|
+
Version: 0.4.4
|
|
4
|
+
Summary: Xenfra CLI: Hands for AI to deploy on DigitalOcean.
|
|
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>=0.2.2
|
|
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.11
|
|
31
|
+
Project-URL: Homepage, https://github.com/xenfra-cloud/xenfra-cli
|
|
32
|
+
Project-URL: Issues, https://github.com/xenfra-cloud/xenfra-cli/issues
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Xenfra CLI (The Interface) 🖥️
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/xenfra/)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
|
|
41
|
+
The official command-line interface for **Xenfra** (The Sovereign Cloud OS). It empowers developers to deploy, monitor, and manage applications on their own infrastructure (DigitalOcean) with the ease of Heroku.
|
|
42
|
+
|
|
43
|
+
## 🚀 Features
|
|
44
|
+
|
|
45
|
+
- **Zero-Config Deployment**: `xenfra deploy` detects your stack (Python, Node.js) and ships it.
|
|
46
|
+
- **Sovereign Auth**: `xenfra auth login` connects securely to your cloud provider.
|
|
47
|
+
- **Live Logs**: `xenfra logs` streams colorized, PII-scrubbed logs from your servers.
|
|
48
|
+
- **Doctor**: `xenfra doctor` runs a battery of health checks on your deployment environment.
|
|
49
|
+
- **Zen Mode**: Automatically applies fix patches when deployments fail.
|
|
50
|
+
|
|
51
|
+
## 📦 Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Recommended: Install via uv
|
|
55
|
+
uv tool install xenfra
|
|
56
|
+
|
|
57
|
+
# Or via pip
|
|
58
|
+
pip install xenfra
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 🛠️ Quick Start
|
|
62
|
+
|
|
63
|
+
### 1. Login
|
|
64
|
+
|
|
65
|
+
Authenticate with your cloud provider (DigitalOcean via Xenfra Platform).
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
xenfra auth login
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Deploy Your App
|
|
72
|
+
|
|
73
|
+
Navigate to your project directory and blast off.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd ~/my-projects/awesome-api
|
|
77
|
+
xenfra deploy
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
_That's it._ Xenfra handles Dockerfile generation, server provisioning, SSL (Caddy), and database connections.
|
|
81
|
+
|
|
82
|
+
### 3. Check Status
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
xenfra status
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 🎛️ Command Reference
|
|
89
|
+
|
|
90
|
+
| Command | Description |
|
|
91
|
+
| :------------------ | :-------------------------------------- |
|
|
92
|
+
| `xenfra auth login` | Start the OAuth flow |
|
|
93
|
+
| `xenfra deploy` | Deploy current directory |
|
|
94
|
+
| `xenfra logs` | Tail logs (Ctrl+C to stop) |
|
|
95
|
+
| `xenfra status` | Show health metrics (CPU/RAM) |
|
|
96
|
+
| `xenfra list` | List all your projects |
|
|
97
|
+
| `xenfra init` | Generate config files without deploying |
|
|
98
|
+
|
|
99
|
+
## 🔗 The Xenfra Ecosystem
|
|
100
|
+
|
|
101
|
+
This CLI is the "Interface" of the Xenfra Open Core architecture:
|
|
102
|
+
|
|
103
|
+
- **[xenfra-sdk](https://github.com/xenfracloud/xenfra-sdk)**: The Core Engine (Used by this CLI).
|
|
104
|
+
- **[xenfra-mcp](https://github.com/xenfracloud/xenfra-mcp)**: The AI Agent Interface.
|
|
105
|
+
- **xenfra-platform**: The Private SaaS Backend.
|
|
106
|
+
|
|
107
|
+
## 🤝 Contributing
|
|
108
|
+
|
|
109
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
110
|
+
|
|
111
|
+
## 📄 License
|
|
112
|
+
|
|
113
|
+
MIT © [Xenfra Cloud](https://xenfra.tech)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
xenfra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
xenfra/commands/__init__.py,sha256=bdugTOErbWUhDvnwFl17KkGPPV7gtmDkSOzhF_NEHX0,40
|
|
3
|
+
xenfra/commands/auth.py,sha256=_KzQpdYx-9V1cd5gCC__UGgCOzCl7wjPZvzgkeuCqTw,4034
|
|
4
|
+
xenfra/commands/auth_device.py,sha256=Z9bCK6TzDD5Tzl14dl-oDtLy6Klrk-PAsmfww74XPXg,6546
|
|
5
|
+
xenfra/commands/deployments.py,sha256=2jfgYItrryR__y5BzFdlE9xBcFo9HXQKwzpJO0vVc58,49792
|
|
6
|
+
xenfra/commands/intelligence.py,sha256=9s2CgnPha7lbyLG3URE4EswL_b84nFxogueoxHoO41o,21684
|
|
7
|
+
xenfra/commands/projects.py,sha256=O2tG--iDWN5oCcHOv1jp88kl9bAK61oGRCLJ60M0b7E,6492
|
|
8
|
+
xenfra/commands/security_cmd.py,sha256=MJxbjQksKrtRn21FSAhTY3ESn_S_tUCGfdNRWL7kNsc,7094
|
|
9
|
+
xenfra/main.py,sha256=TWr0Ir6m9nlpkbJLh22g1KOfG_t6BQFnpWgeQK_Hkxs,1990
|
|
10
|
+
xenfra/utils/__init__.py,sha256=57o8j7Tibrhyid84zTFLHjFmRP5sCnNbtLEfpRqIpMk,42
|
|
11
|
+
xenfra/utils/auth.py,sha256=bMzjZt92eenVmZFSyURPe0KTu52kxnNc0ORlY0nuTwE,13395
|
|
12
|
+
xenfra/utils/codebase.py,sha256=s5oWg1uToQ_B8lzQ7heVhqzWWoPye5flwa2zAq8OvDo,5780
|
|
13
|
+
xenfra/utils/config.py,sha256=eirrFYYy9vRCftfGvODJI8hQp7xEvPHhHazKuOGS2us,16144
|
|
14
|
+
xenfra/utils/errors.py,sha256=x0DKEfx9BXhlt48RKk5zg4-OFmB-yzRpOnH0NJtTN6c,4095
|
|
15
|
+
xenfra/utils/file_sync.py,sha256=P4hCsndU10molrjhNfojE2FeFx3NT3G2_XoeKYmpvzc,7647
|
|
16
|
+
xenfra/utils/security.py,sha256=V0CqA47ZYt-8AesWb7FPRzzygqEY_g2WF1Duvs5BZ_Y,11143
|
|
17
|
+
xenfra/utils/validation.py,sha256=6mGC5CqAbx-CBp06omWLBpKjnEWXsEzlYWq71wjDeX8,6678
|
|
18
|
+
xenfra-0.4.4.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
19
|
+
xenfra-0.4.4.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
|
|
20
|
+
xenfra-0.4.4.dist-info/METADATA,sha256=Kfjav54itPcVq9gA5GlDvdjjFy2isDW0PzpmAOWVdK4,3811
|
|
21
|
+
xenfra-0.4.4.dist-info/RECORD,,
|