xenfra 0.3.4__py3-none-any.whl → 0.3.5__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.
@@ -167,13 +167,11 @@ def deploy(project_name, git_repo, branch, framework, no_heal):
167
167
  console.print(f"[bold red]Invalid git repository URL: {error_msg}[/bold red]")
168
168
  raise click.Abort()
169
169
  console.print(f"[cyan]Deploying {project_name} from git repository...[/cyan]")
170
+ console.print(f"[dim]Repository: {git_repo} (branch: {branch})[/dim]")
170
171
  else:
171
172
  console.print(f"[cyan]Deploying {project_name} from local directory...[/cyan]")
172
- console.print(
173
- "[yellow]Local deployment requires code upload (not yet fully implemented).[/yellow]"
174
- )
175
- console.print("[dim]Please use --git-repo for now.[/dim]")
176
- return
173
+ console.print("[dim]Code will be uploaded to the droplet[/dim]")
174
+ # Note: SDK's InfraEngine handles local upload via fabric.transfer.Upload()
177
175
 
178
176
  # Validate branch name
179
177
  is_valid, error_msg = validate_branch_name(branch)
@@ -215,12 +213,26 @@ def deploy(project_name, git_repo, branch, framework, no_heal):
215
213
 
216
214
  # Create deployment
217
215
  try:
218
- deployment = client.deployments.create(
219
- project_name=project_name,
220
- git_repo=git_repo,
221
- branch=branch,
222
- framework=framework,
223
- )
216
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
217
+
218
+ with Progress(
219
+ SpinnerColumn(),
220
+ TextColumn("[bold blue]{task.description}"),
221
+ BarColumn(),
222
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
223
+ console=console,
224
+ ) as progress:
225
+ # Track deployment phases
226
+ task = progress.add_task("Creating deployment...", total=100)
227
+
228
+ deployment = client.deployments.create(
229
+ project_name=project_name,
230
+ git_repo=git_repo,
231
+ branch=branch,
232
+ framework=framework,
233
+ )
234
+
235
+ progress.update(task, advance=100, description="Deployment created!")
224
236
 
225
237
  deployment_id = deployment["deployment_id"]
226
238
  console.print(
@@ -237,6 +249,8 @@ def deploy(project_name, git_repo, branch, framework, no_heal):
237
249
  if git_repo:
238
250
  details_table.add_row("Repository", git_repo)
239
251
  details_table.add_row("Branch", branch)
252
+ else:
253
+ details_table.add_row("Source", "Local directory")
240
254
  details_table.add_row("Framework", framework)
241
255
  details_table.add_row("Status", deployment.get("status", "PENDING"))
242
256
 
@@ -260,8 +274,17 @@ def deploy(project_name, git_repo, branch, framework, no_heal):
260
274
  break
261
275
 
262
276
  except XenfraAPIError as e:
263
- # Deployment failed
264
- console.print(f"[bold red]✗ Deployment failed: {e.detail}[/bold red]")
277
+ # Deployment failed - try to provide helpful error
278
+ from ..utils.errors import detect_error_type, show_error_with_solution
279
+
280
+ console.print(f"[bold red]✗ Deployment failed[/bold red]")
281
+
282
+ # Try to detect error type and show solution
283
+ error_type, error_kwargs = detect_error_type(str(e.detail))
284
+ if error_type:
285
+ show_error_with_solution(error_type, **error_kwargs)
286
+ else:
287
+ console.print(f"[red]{e.detail}[/red]")
265
288
 
266
289
  # Check if we should auto-heal
267
290
  if no_heal or attempt >= MAX_RETRY_ATTEMPTS:
@@ -131,6 +131,20 @@ def init(manual, accept_all):
131
131
  # Call Intelligence Service
132
132
  analysis = client.intelligence.analyze_codebase(code_snippets)
133
133
 
134
+ # Client-side conflict detection (ensures Zen Nod always triggers)
135
+ from ..utils.codebase import detect_package_manager_conflicts
136
+ has_conflict_local, detected_managers_local = detect_package_manager_conflicts(code_snippets)
137
+
138
+ if has_conflict_local and not analysis.has_conflict:
139
+ # AI missed the conflict - fix it client-side
140
+ console.print("[dim]Note: Enhanced conflict detection activated[/dim]\n")
141
+ analysis.has_conflict = True
142
+ # Convert dict to object for compatibility
143
+ from types import SimpleNamespace
144
+ analysis.detected_package_managers = [
145
+ SimpleNamespace(**pm) for pm in detected_managers_local
146
+ ]
147
+
134
148
  # Display results
135
149
  console.print("\n[bold green]Analysis Complete![/bold green]\n")
136
150
 
xenfra/utils/codebase.py CHANGED
@@ -120,6 +120,49 @@ def scan_codebase(max_files: int = 10, max_size: int = 50000) -> dict[str, str]:
120
120
 
121
121
  return code_snippets
122
122
 
123
+ def detect_package_manager_conflicts(code_snippets: dict[str, str]) -> tuple[bool, list[dict]]:
124
+ """
125
+ Deterministically detect package manager conflicts from scanned files.
126
+
127
+ This ensures Zen Nod (conflict resolution) always triggers when multiple
128
+ package managers are present, regardless of AI detection.
129
+
130
+ Args:
131
+ code_snippets: Dictionary of filename -> content from scan_codebase()
132
+
133
+ Returns:
134
+ (has_conflict, detected_managers) where detected_managers is a list of
135
+ {"manager": str, "file": str} dictionaries
136
+ """
137
+ detected = []
138
+
139
+ # Check for Python package managers
140
+ if "pyproject.toml" in code_snippets:
141
+ content = code_snippets["pyproject.toml"]
142
+ if "[tool.poetry]" in content or "poetry.lock" in code_snippets:
143
+ detected.append({"manager": "poetry", "file": "pyproject.toml"})
144
+ elif "[tool.uv]" in content or "uv.lock" in code_snippets or "[project]" in content:
145
+ detected.append({"manager": "uv", "file": "pyproject.toml"})
146
+
147
+ if "Pipfile" in code_snippets:
148
+ detected.append({"manager": "pipenv", "file": "Pipfile"})
149
+
150
+ if "requirements.txt" in code_snippets:
151
+ detected.append({"manager": "pip", "file": "requirements.txt"})
152
+
153
+ # Check for Node.js package managers (all independent checks)
154
+ if "pnpm-lock.yaml" in code_snippets:
155
+ detected.append({"manager": "pnpm", "file": "pnpm-lock.yaml"})
156
+
157
+ if "yarn.lock" in code_snippets:
158
+ detected.append({"manager": "yarn", "file": "yarn.lock"})
159
+
160
+ if "package-lock.json" in code_snippets:
161
+ detected.append({"manager": "npm", "file": "package-lock.json"})
162
+
163
+ has_conflict = len(detected) > 1
164
+ return has_conflict, detected
165
+
123
166
 
124
167
  def has_xenfra_config() -> bool:
125
168
  """Check if xenfra.yaml already exists."""
xenfra/utils/errors.py ADDED
@@ -0,0 +1,116 @@
1
+ """Human-friendly error messages with actionable solutions."""
2
+
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+
8
+ ERROR_SOLUTIONS = {
9
+ "port_in_use": {
10
+ "message": "Port {port} is already in use on the droplet",
11
+ "solution": "Change the port in xenfra.yaml or stop the conflicting service",
12
+ "command": "ssh root@{{ip}} 'lsof -i :{port}' # Find process using port",
13
+ },
14
+ "missing_dependency": {
15
+ "message": "Missing dependency: {package}",
16
+ "solution": "Add {package} to dependencies in {file}",
17
+ "command": "uv add {package} # OR: echo '{package}' >> requirements.txt",
18
+ },
19
+ "ssh_failure": {
20
+ "message": "Cannot connect to droplet via SSH",
21
+ "solution": "Check firewall rules, wait for droplet boot, or verify SSH keys",
22
+ "command": "ssh -v root@{ip} # Verbose SSH for debugging",
23
+ },
24
+ "docker_build_failed": {
25
+ "message": "Docker build failed",
26
+ "solution": "Check Dockerfile syntax and base image availability",
27
+ "command": "docker build . --no-cache # Test locally",
28
+ },
29
+ "health_check_failed": {
30
+ "message": "Application failed health check",
31
+ "solution": "Ensure your app responds on port {port} at /health or /",
32
+ "command": "curl http://{{ip}}:{port}/health",
33
+ },
34
+ "out_of_memory": {
35
+ "message": "Container out of memory",
36
+ "solution": "Upgrade to a larger instance size in xenfra.yaml",
37
+ "command": "docker stats # Check memory usage",
38
+ },
39
+ }
40
+
41
+
42
+ def show_error_with_solution(error_type: str, **kwargs) -> None:
43
+ """
44
+ Display error with actionable solution.
45
+
46
+ Args:
47
+ error_type: Key from ERROR_SOLUTIONS
48
+ **kwargs: Template variables (port, ip, package, file, etc.)
49
+ """
50
+ error = ERROR_SOLUTIONS.get(error_type)
51
+
52
+ if not error:
53
+ # Fallback for unknown errors
54
+ console.print(f"[red]❌ Error: {error_type}[/red]")
55
+ return
56
+
57
+ # Format message with provided kwargs
58
+ try:
59
+ message = error["message"].format(**kwargs)
60
+ solution = error["solution"].format(**kwargs)
61
+ command = error.get("command", "").format(**kwargs)
62
+ except KeyError as e:
63
+ console.print(f"[red]❌ Error formatting message: missing {e}[/red]")
64
+ return
65
+
66
+ console.print()
67
+ console.print(f"[red]❌ {message}[/red]")
68
+ console.print(f"[yellow]💡 Solution: {solution}[/yellow]")
69
+
70
+ if command:
71
+ console.print(f"[dim]Try: {command}[/dim]")
72
+ console.print()
73
+
74
+
75
+ def detect_error_type(error_message: str) -> tuple[str, dict]:
76
+ """
77
+ Attempt to detect error type from message.
78
+
79
+ Returns:
80
+ (error_type, kwargs) for show_error_with_solution()
81
+ """
82
+ error_lower = error_message.lower()
83
+
84
+ # Port detection
85
+ if "port" in error_lower and ("in use" in error_lower or "already" in error_lower):
86
+ # Try to extract port number
87
+ import re
88
+ port_match = re.search(r"port\s+(\d+)", error_lower)
89
+ port = port_match.group(1) if port_match else "8000"
90
+ return "port_in_use", {"port": port}
91
+
92
+ # SSH detection
93
+ if "ssh" in error_lower or "connection refused" in error_lower:
94
+ return "ssh_failure", {"ip": "DROPLET_IP"}
95
+
96
+ # Docker detection
97
+ if "docker" in error_lower and "build" in error_lower:
98
+ return "docker_build_failed", {}
99
+
100
+ # Health check detection
101
+ if "health" in error_lower and ("fail" in error_lower or "timeout" in error_lower):
102
+ return "health_check_failed", {"port": "8000"}
103
+
104
+ # Memory detection
105
+ if "memory" in error_lower or "oom" in error_lower:
106
+ return "out_of_memory", {}
107
+
108
+ # Module not found
109
+ if "modulenotfounderror" in error_lower or "no module named" in error_lower:
110
+ import re
111
+ module_match = re.search(r"no module named ['\"]([^'\"]+)['\"]", error_lower)
112
+ package = module_match.group(1) if module_match else "PACKAGE_NAME"
113
+ return "missing_dependency", {"package": package, "file": "pyproject.toml"}
114
+
115
+ # Unknown
116
+ return None, {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: xenfra
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: A 'Zen Mode' infrastructure engine for Python developers.
5
5
  Author: xenfra-cloud
6
6
  Author-email: xenfra-cloud <xenfracloud@gmail.com>
@@ -2,18 +2,19 @@ xenfra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  xenfra/commands/__init__.py,sha256=kTTwVnTvoxikyPUhQiyTAbnw4PYafktuE1----TqQoA,43
3
3
  xenfra/commands/auth.py,sha256=ecReVCGl7Ys2d77mv_e4mCbs4ug6FLIb3S9dl2FUhr4,4178
4
4
  xenfra/commands/auth_device.py,sha256=caD2UdveEZtAFjgjmnA-l5bjbbPONFjXJXgeJN7mhbk,6710
5
- xenfra/commands/deployments.py,sha256=bI6d9lVYPhCDoQE3nke7s80MUQ75ILPqiEGhcbXDExo,28609
6
- xenfra/commands/intelligence.py,sha256=U4nYh1aoJJ3Pxv_eliG3tmJUhCzX9iOiWg5yFALXtvE,15799
5
+ xenfra/commands/deployments.py,sha256=bVpQ9kuFDT5a9M7ADwXZzXIXYyPS92saZEuYmiI_lU8,30016
6
+ xenfra/commands/intelligence.py,sha256=jDQa2Bx4blTFoqtyutf1xsf6fHjIjRYEFW5G3mrz-Ks,16594
7
7
  xenfra/commands/projects.py,sha256=SAxF_pOr95K6uz35U-zENptKndKxJNZn6bcD45PHcpI,6696
8
8
  xenfra/commands/security_cmd.py,sha256=EI5sjX2lcMxgMH-LCFmPVkc9YqadOrcoSgTiKknkVRY,7327
9
9
  xenfra/main.py,sha256=2EPPuIdxjhW-I-e-Mc0i2ayeLaSJdmzddNThkXq7B7c,2033
10
10
  xenfra/utils/__init__.py,sha256=4ZRYkrb--vzoXjBHG8zRxz2jCXNGtAoKNtkyu2WRI2A,45
11
11
  xenfra/utils/auth.py,sha256=9JbFnv0-rdlJF-4hKD2uWd9h9ehqC1iIHee1O5e-3RM,13769
12
- xenfra/utils/codebase.py,sha256=57GthXOvOQnUHiDwIHqxK6hNUGWlWf6Nfs3T8647Wrc,4144
12
+ xenfra/utils/codebase.py,sha256=GMrqhOJWX8q5ZXSLI9P3hJZBpufXMQA3Z4fKh2XSTNo,5949
13
13
  xenfra/utils/config.py,sha256=F2zedd3JXP7TBdul0u8b4NVx-C1N6Hq4sH5szyWim6M,11947
14
+ xenfra/utils/errors.py,sha256=6G91YzzDDNkKHANTgfAMiOiMElEyi57wo6-FzRa4VuQ,4211
14
15
  xenfra/utils/security.py,sha256=EA8CIPLt8Y-QP5uZ7c5NuC6ZLRV1aZS8NapS9ix_vok,11479
15
16
  xenfra/utils/validation.py,sha256=cvuL_AEFJ2oCoP0abCqoOIABOwz79Gkf-jh_dcFIQlM,6912
16
- xenfra-0.3.4.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
17
- xenfra-0.3.4.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
18
- xenfra-0.3.4.dist-info/METADATA,sha256=0b3lQ-vh0J5Wyeaa7_UKA9HKfRRnw9CjkD068to65os,3870
19
- xenfra-0.3.4.dist-info/RECORD,,
17
+ xenfra-0.3.5.dist-info/WHEEL,sha256=KSLUh82mDPEPk0Bx0ScXlWL64bc8KmzIPNcpQZFV-6E,79
18
+ xenfra-0.3.5.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
19
+ xenfra-0.3.5.dist-info/METADATA,sha256=OrFNS_krWEq8-BglAdLn5A1Lpel4bxWcpDcqDcsmrRk,3870
20
+ xenfra-0.3.5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.21
2
+ Generator: uv 0.9.22
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any