xenfra 0.4.2__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.
@@ -1,412 +1,503 @@
1
- """
2
- AI-powered intelligence commands for Xenfra CLI.
3
- Includes smart initialization, deployment diagnosis, and codebase analysis.
4
- """
5
-
6
- import os
7
-
8
- import click
9
- from rich.console import Console
10
- from rich.panel import Panel
11
- from rich.prompt import Confirm, Prompt
12
- from rich.table import Table
13
- from xenfra_sdk import XenfraClient
14
- from xenfra_sdk.exceptions import XenfraAPIError, XenfraError
15
- from xenfra_sdk.privacy import scrub_logs
16
-
17
- from ..utils.auth import API_BASE_URL, get_auth_token
18
- from ..utils.codebase import has_xenfra_config, scan_codebase
19
- from ..utils.config import (
20
- apply_patch,
21
- generate_xenfra_yaml,
22
- manual_prompt_for_config,
23
- read_xenfra_yaml,
24
- )
25
- from ..utils.validation import validate_deployment_id
26
-
27
- console = Console()
28
-
29
-
30
- def get_client() -> XenfraClient:
31
- """Get authenticated SDK client."""
32
- token = get_auth_token()
33
- if not token:
34
- console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
35
- raise click.Abort()
36
-
37
- # DEBUG: Only show token info if XENFRA_DEBUG environment variable is set
38
- import os
39
- if os.getenv("XENFRA_DEBUG") == "1":
40
- import base64
41
- import json
42
- try:
43
- parts = token.split(".")
44
- if len(parts) == 3:
45
- # Decode payload
46
- payload_b64 = parts[1]
47
- padding = 4 - len(payload_b64) % 4
48
- if padding != 4:
49
- payload_b64 += "=" * padding
50
- payload_bytes = base64.urlsafe_b64decode(payload_b64)
51
- claims = json.loads(payload_bytes)
52
-
53
- console.print("[dim]━━━ DEBUG: Token Info ━━━[/dim]")
54
- console.print(f"[dim] API URL: {API_BASE_URL}[/dim]")
55
- console.print(f"[dim] Token prefix: {token[:20]}...[/dim]")
56
- console.print(f"[dim] sub (email): {claims.get('sub', 'MISSING')}[/dim]")
57
- console.print(f"[dim] user_id: {claims.get('user_id', 'MISSING')}[/dim]")
58
- console.print(f"[dim] iss (issuer): {claims.get('iss', 'MISSING')}[/dim]")
59
- console.print(f"[dim] aud (audience): {claims.get('aud', 'MISSING')}[/dim]")
60
-
61
- # Check if token is expired
62
- exp = claims.get('exp')
63
- if exp:
64
- import time
65
- is_expired = time.time() >= exp
66
- from datetime import datetime, timezone
67
- exp_time = datetime.fromtimestamp(exp, tz=timezone.utc)
68
- console.print(f"[dim] expires_at: {exp_time}[/dim]")
69
- console.print(f"[dim] expired: {is_expired}[/dim]")
70
- console.print("[dim]━━━━━━━━━━━━━━━━━━━━━[/dim]\n")
71
- except Exception as e:
72
- console.print(f"[dim]DEBUG: Could not decode token: {e}[/dim]\n")
73
-
74
- return XenfraClient(token=token, api_url=API_BASE_URL)
75
-
76
-
77
- @click.command()
78
- @click.option("--manual", is_flag=True, help="Skip AI detection, use interactive mode")
79
- @click.option("--accept-all", is_flag=True, help="Accept AI suggestions without confirmation")
80
- def init(manual, accept_all):
81
- """
82
- Initialize Xenfra configuration (AI-powered by default).
83
-
84
- Scans your codebase, detects framework and dependencies,
85
- and generates xenfra.yaml automatically.
86
-
87
- Use --manual to skip AI and configure interactively.
88
- Set XENFRA_NO_AI=1 environment variable to force manual mode globally.
89
- """
90
- # Check if config already exists
91
- if has_xenfra_config():
92
- console.print("[yellow]xenfra.yaml already exists.[/yellow]")
93
- if not Confirm.ask("Overwrite existing configuration?"):
94
- console.print("[dim]Cancelled.[/dim]")
95
- return
96
-
97
- # Check for XENFRA_NO_AI environment variable
98
- no_ai = os.environ.get("XENFRA_NO_AI", "0") == "1"
99
- if no_ai and not manual:
100
- console.print("[yellow]XENFRA_NO_AI is set. Using manual mode.[/yellow]")
101
- manual = True
102
-
103
- # Manual mode - interactive prompts
104
- if manual:
105
- console.print("[cyan]Manual configuration mode[/cyan]\n")
106
- try:
107
- manual_prompt_for_config()
108
- console.print("\n[bold green]✓ xenfra.yaml created successfully![/bold green]")
109
- console.print("[dim]Run 'xenfra deploy' to deploy your project.[/dim]")
110
- except KeyboardInterrupt:
111
- console.print("\n[dim]Cancelled.[/dim]")
112
- except Exception as e:
113
- console.print(f"[bold red]Error: {e}[/bold red]")
114
- return
115
-
116
- # AI-powered detection (default)
117
- try:
118
- # Use context manager for SDK client
119
- with get_client() as client:
120
- # Scan codebase
121
- console.print("[cyan]Analyzing your codebase...[/cyan]")
122
- code_snippets = scan_codebase()
123
-
124
- if not code_snippets:
125
- console.print("[bold red]No code files found to analyze.[/bold red]")
126
- console.print("[dim]Make sure you're in a Python project directory.[/dim]")
127
- return
128
-
129
- console.print(f"[dim]Found {len(code_snippets)} files to analyze[/dim]")
130
-
131
- # Call Intelligence Service
132
- analysis = client.intelligence.analyze_codebase(code_snippets)
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
-
148
- # Display results
149
- console.print("\n[bold green]Analysis Complete![/bold green]\n")
150
-
151
- # Handle package manager conflict
152
- selected_package_manager = analysis.package_manager
153
- selected_dependency_file = analysis.dependency_file
154
-
155
- if analysis.has_conflict and analysis.detected_package_managers:
156
- console.print("[yellow]Multiple package managers detected![/yellow]\n")
157
-
158
- # Show options
159
- for i, option in enumerate(analysis.detected_package_managers, 1):
160
- console.print(f" {i}. [cyan]{option.manager}[/cyan] ({option.file})")
161
-
162
- console.print(f"\n[dim]Recommended: {analysis.package_manager} (most modern)[/dim]")
163
-
164
- # Prompt user to select
165
- choice = Prompt.ask(
166
- "\nWhich package manager do you want to use?",
167
- choices=[str(i) for i in range(1, len(analysis.detected_package_managers) + 1)],
168
- default="1",
169
- )
170
-
171
- # Update selection based on user choice
172
- selected_option = analysis.detected_package_managers[int(choice) - 1]
173
- selected_package_manager = selected_option.manager
174
- selected_dependency_file = selected_option.file
175
-
176
- console.print(
177
- f"\n[green]Using {selected_package_manager} ({selected_dependency_file})[/green]\n"
178
- )
179
-
180
- table = Table(show_header=False, box=None)
181
- table.add_column("Property", style="cyan")
182
- table.add_column("Value", style="white")
183
-
184
- table.add_row("Framework", analysis.framework)
185
- table.add_row("Port", str(analysis.port))
186
- table.add_row("Database", analysis.database)
187
- if analysis.cache:
188
- table.add_row("Cache", analysis.cache)
189
- if analysis.workers:
190
- table.add_row("Workers", ", ".join(analysis.workers))
191
- table.add_row("Package Manager", selected_package_manager)
192
- table.add_row("Dependency File", selected_dependency_file)
193
-
194
- # New: Infrastructure details in summary
195
- table.add_row("Region", "nyc3 (default)")
196
- table.add_row("Instance Size", analysis.instance_size)
197
-
198
- # Resource visualization
199
- cpu = 1 if analysis.instance_size == "basic" else (2 if analysis.instance_size == "standard" else 4)
200
- ram = "1GB" if analysis.instance_size == "basic" else ("4GB" if analysis.instance_size == "standard" else "8GB")
201
- table.add_row("Resources", f"{cpu} vCPU, {ram} RAM")
202
-
203
- table.add_row("Estimated Cost", f"${analysis.estimated_cost_monthly:.2f}/month")
204
- table.add_row("Confidence", f"{analysis.confidence:.0%}")
205
-
206
- console.print(Panel(table, title="[bold]Detected Configuration[/bold]"))
207
-
208
- if analysis.notes:
209
- console.print(f"\n[dim]{analysis.notes}[/dim]")
210
-
211
- # Confirm or edit
212
- if accept_all:
213
- confirmed = True
214
- else:
215
- confirmed = Confirm.ask("\nCreate xenfra.yaml with this configuration?", default=True)
216
-
217
- if confirmed:
218
- generate_xenfra_yaml(analysis, package_manager_override=selected_package_manager, dependency_file_override=selected_dependency_file)
219
- console.print("[bold green]xenfra.yaml created successfully![/bold green]")
220
- console.print("[dim]Run 'xenfra deploy' to deploy your project.[/dim]")
221
- else:
222
- console.print("[yellow]Configuration cancelled.[/yellow]")
223
-
224
- except XenfraAPIError as e:
225
- console.print(f"[bold red]API Error: {e.detail}[/bold red]")
226
- except XenfraError as e:
227
- console.print(f"[bold red]Error: {e}[/bold red]")
228
- except click.Abort:
229
- pass
230
- except Exception as e:
231
- console.print(f"[bold red]Unexpected error: {e}[/bold red]")
232
-
233
-
234
- @click.command()
235
- @click.argument("deployment-id", required=False)
236
- @click.option("--apply", is_flag=True, help="Auto-apply suggested patch (with confirmation)")
237
- @click.option("--logs", type=click.File("r"), help="Diagnose from log file instead of deployment")
238
- def diagnose(deployment_id, apply, logs):
239
- """
240
- Diagnose deployment failures using AI.
241
-
242
- Analyzes logs and provides diagnosis, suggestions, and optionally
243
- an automatic patch to fix the issue.
244
- """
245
- try:
246
- # Use context manager for all SDK operations
247
- with get_client() as client:
248
- # Get logs
249
- if logs:
250
- log_content = logs.read()
251
- console.print("[cyan]Analyzing logs from file...[/cyan]")
252
- elif deployment_id:
253
- # Validate deployment ID
254
- is_valid, error_msg = validate_deployment_id(deployment_id)
255
- if not is_valid:
256
- console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
257
- return
258
-
259
- console.print(f"[cyan]Fetching logs for deployment {deployment_id}...[/cyan]")
260
- log_content = client.deployments.get_logs(deployment_id)
261
-
262
- if not log_content:
263
- console.print("[yellow]No logs found for this deployment.[/yellow]")
264
- return
265
- else:
266
- console.print(
267
- "[bold red]Please specify a deployment ID or use --logs <file>[/bold red]"
268
- )
269
- console.print(
270
- "[dim]Usage: xenfra diagnose <deployment-id> or xenfra diagnose --logs error.log[/dim]"
271
- )
272
- return
273
-
274
- # Scrub sensitive data
275
- scrubbed_logs = scrub_logs(log_content)
276
-
277
- # Try to read package manager context from xenfra.yaml
278
- package_manager = None
279
- dependency_file = None
280
- try:
281
- config = read_xenfra_yaml()
282
- package_manager = config.get("package_manager")
283
- dependency_file = config.get("dependency_file")
284
-
285
- if package_manager and dependency_file:
286
- console.print(
287
- f"[dim]Using context: {package_manager} ({dependency_file})[/dim]"
288
- )
289
- except FileNotFoundError:
290
- # No config file - diagnosis will infer from logs
291
- console.print(
292
- "[dim]No xenfra.yaml found - inferring package manager from logs[/dim]"
293
- )
294
-
295
- # Diagnose with context
296
- console.print("[cyan]Analyzing failure...[/cyan]")
297
- result = client.intelligence.diagnose(
298
- logs=scrubbed_logs, package_manager=package_manager, dependency_file=dependency_file
299
- )
300
-
301
- # Display diagnosis
302
- console.print("\n")
303
- console.print(
304
- Panel(result.diagnosis, title="[bold red]Diagnosis[/bold red]", border_style="red")
305
- )
306
- console.print(
307
- Panel(
308
- result.suggestion,
309
- title="[bold yellow]Suggestion[/bold yellow]",
310
- border_style="yellow",
311
- )
312
- )
313
-
314
- # Handle patch
315
- if result.patch and result.patch.file:
316
- console.print("\n[bold green]Automatic fix available![/bold green]")
317
- console.print(f" File: [cyan]{result.patch.file}[/cyan]")
318
- console.print(f" Operation: [yellow]{result.patch.operation}[/yellow]")
319
- console.print(f" Value: [white]{result.patch.value}[/white]")
320
-
321
- if apply or Confirm.ask("\nApply this patch?", default=False):
322
- try:
323
- apply_patch(result.patch.model_dump())
324
- console.print("[bold green]Patch applied successfully![/bold green]")
325
- console.print("[cyan]Run 'xenfra deploy' to retry deployment.[/cyan]")
326
- except FileNotFoundError as e:
327
- console.print(f"[bold red]Error: {e}[/bold red]")
328
- except Exception as e:
329
- console.print(f"[bold red]Failed to apply patch: {e}[/bold red]")
330
- else:
331
- console.print("[dim]Patch not applied. Follow manual steps above.[/dim]")
332
- else:
333
- console.print("\n[yellow]No automatic fix available.[/yellow]")
334
- console.print("[dim]Please follow the manual steps in the suggestion above.[/dim]")
335
-
336
- except XenfraAPIError as e:
337
- console.print(f"[bold red]API Error: {e.detail}[/bold red]")
338
- except XenfraError as e:
339
- console.print(f"[bold red]Error: {e}[/bold red]")
340
- except click.Abort:
341
- pass
342
- except Exception as e:
343
- console.print(f"[bold red]Unexpected error: {e}[/bold red]")
344
-
345
-
346
- @click.command()
347
- def analyze():
348
- """
349
- Analyze codebase without creating configuration.
350
-
351
- Shows what AI would detect, useful for previewing before running init.
352
- """
353
- try:
354
- # Use context manager for SDK client
355
- with get_client() as client:
356
- # Scan codebase
357
- console.print("[cyan]Analyzing your codebase...[/cyan]")
358
- code_snippets = scan_codebase()
359
-
360
- if not code_snippets:
361
- console.print("[bold red]No code files found to analyze.[/bold red]")
362
- return
363
-
364
- # Call Intelligence Service
365
- analysis = client.intelligence.analyze_codebase(code_snippets)
366
-
367
- # Display results
368
- console.print("\n[bold green]Analysis Results:[/bold green]\n")
369
-
370
- table = Table(show_header=False, box=None)
371
- table.add_column("Property", style="cyan")
372
- table.add_column("Value", style="white")
373
-
374
- table.add_row("Framework", analysis.framework)
375
- table.add_row("Port", str(analysis.port))
376
- table.add_row("Database", analysis.database)
377
- if analysis.cache:
378
- table.add_row("Cache", analysis.cache)
379
- if analysis.workers:
380
- table.add_row("Workers", ", ".join(analysis.workers))
381
- if analysis.env_vars:
382
- table.add_row("Environment Variables", ", ".join(analysis.env_vars))
383
-
384
- # New: Infrastructure details in preview
385
- table.add_row("Region", "nyc3 (default)")
386
- table.add_row("Instance Size", analysis.instance_size)
387
-
388
- # Resource visualization
389
- cpu = 1 if analysis.instance_size == "basic" else (2 if analysis.instance_size == "standard" else 4)
390
- ram = "1GB" if analysis.instance_size == "basic" else ("4GB" if analysis.instance_size == "standard" else "8GB")
391
- table.add_row("Resources", f"{cpu} vCPU, {ram} RAM")
392
-
393
- table.add_row("Estimated Cost", f"${analysis.estimated_cost_monthly:.2f}/month")
394
- table.add_row("Confidence", f"{analysis.confidence:.0%}")
395
-
396
- console.print(table)
397
-
398
- if analysis.notes:
399
- console.print(f"\n[dim]Notes: {analysis.notes}[/dim]")
400
-
401
- console.print(
402
- "\n[dim]Run 'xenfra init' to create xenfra.yaml with this configuration.[/dim]"
403
- )
404
-
405
- except XenfraAPIError as e:
406
- console.print(f"[bold red]API Error: {e.detail}[/bold red]")
407
- except XenfraError as e:
408
- console.print(f"[bold red]Error: {e}[/bold red]")
409
- except click.Abort:
410
- pass
411
- except Exception as e:
412
- console.print(f"[bold red]Unexpected error: {e}[/bold red]")
1
+ """
2
+ AI-powered intelligence commands for Xenfra CLI.
3
+ Includes smart initialization, deployment diagnosis, and codebase analysis.
4
+ """
5
+
6
+ import os
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.prompt import Confirm, Prompt
12
+ from rich.table import Table
13
+ from xenfra_sdk import XenfraClient
14
+ from xenfra_sdk.exceptions import XenfraAPIError, XenfraError
15
+ from xenfra_sdk.privacy import scrub_logs
16
+
17
+ from ..utils.auth import API_BASE_URL, get_auth_token
18
+ from ..utils.codebase import has_xenfra_config, scan_codebase
19
+ from ..utils.config import (
20
+ apply_patch,
21
+ generate_xenfra_yaml,
22
+ manual_prompt_for_config,
23
+ read_xenfra_yaml,
24
+ )
25
+ from ..utils.validation import validate_deployment_id
26
+
27
+ console = Console()
28
+
29
+
30
+ def get_client() -> XenfraClient:
31
+ """Get authenticated SDK client."""
32
+ token = get_auth_token()
33
+ if not token:
34
+ console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
35
+ raise click.Abort()
36
+
37
+ # DEBUG: Only show token info if XENFRA_DEBUG environment variable is set
38
+ import os
39
+ if os.getenv("XENFRA_DEBUG") == "1":
40
+ import base64
41
+ import json
42
+ try:
43
+ parts = token.split(".")
44
+ if len(parts) == 3:
45
+ # Decode payload
46
+ payload_b64 = parts[1]
47
+ padding = 4 - len(payload_b64) % 4
48
+ if padding != 4:
49
+ payload_b64 += "=" * padding
50
+ payload_bytes = base64.urlsafe_b64decode(payload_b64)
51
+ claims = json.loads(payload_bytes)
52
+
53
+ console.print("[dim]━━━ DEBUG: Token Info ━━━[/dim]")
54
+ console.print(f"[dim] API URL: {API_BASE_URL}[/dim]")
55
+ console.print(f"[dim] Token prefix: {token[:20]}...[/dim]")
56
+ console.print(f"[dim] sub (email): {claims.get('sub', 'MISSING')}[/dim]")
57
+ console.print(f"[dim] user_id: {claims.get('user_id', 'MISSING')}[/dim]")
58
+ console.print(f"[dim] iss (issuer): {claims.get('iss', 'MISSING')}[/dim]")
59
+ console.print(f"[dim] aud (audience): {claims.get('aud', 'MISSING')}[/dim]")
60
+
61
+ # Check if token is expired
62
+ exp = claims.get('exp')
63
+ if exp:
64
+ import time
65
+ is_expired = time.time() >= exp
66
+ from datetime import datetime, timezone
67
+ exp_time = datetime.fromtimestamp(exp, tz=timezone.utc)
68
+ console.print(f"[dim] expires_at: {exp_time}[/dim]")
69
+ console.print(f"[dim] expired: {is_expired}[/dim]")
70
+ console.print("[dim]━━━━━━━━━━━━━━━━━━━━━[/dim]\n")
71
+ except Exception as e:
72
+ console.print(f"[dim]DEBUG: Could not decode token: {e}[/dim]\n")
73
+
74
+ return XenfraClient(token=token, api_url=API_BASE_URL)
75
+
76
+
77
+ @click.command()
78
+ @click.option("--manual", is_flag=True, help="Skip AI detection, use interactive mode")
79
+ @click.option("--accept-all", is_flag=True, help="Accept AI suggestions without confirmation")
80
+ def init(manual, accept_all):
81
+ """
82
+ Initialize Xenfra configuration (AI-powered by default).
83
+
84
+ Scans your codebase, detects framework and dependencies,
85
+ and generates xenfra.yaml automatically.
86
+
87
+ For microservices projects (multiple services), generates xenfra-services.yaml.
88
+
89
+ Use --manual to skip AI and configure interactively.
90
+ Set XENFRA_NO_AI=1 environment variable to force manual mode globally.
91
+ """
92
+ # Check if config already exists
93
+ if has_xenfra_config():
94
+ console.print("[yellow]xenfra.yaml already exists.[/yellow]")
95
+ if not Confirm.ask("Overwrite existing configuration?"):
96
+ console.print("[dim]Cancelled.[/dim]")
97
+ return
98
+
99
+ # Check if xenfra-services.yaml already exists
100
+ from pathlib import Path
101
+
102
+ # === MICROSERVICES AUTO-DETECTION ===
103
+ # Check for microservices project BEFORE AI analysis
104
+ try:
105
+ from xenfra_sdk import auto_detect_services, add_services_to_xenfra_yaml
106
+
107
+ detected_services = auto_detect_services(".")
108
+
109
+ if detected_services and len(detected_services) > 1:
110
+ console.print(f"\n[bold cyan]🔍 Detected microservices project ({len(detected_services)} services)[/bold cyan]\n")
111
+
112
+ # Display detected services
113
+ from rich.table import Table
114
+ svc_table = Table(show_header=True, header_style="bold cyan")
115
+ svc_table.add_column("Service", style="white")
116
+ svc_table.add_column("Path", style="dim")
117
+ svc_table.add_column("Port", style="green")
118
+ svc_table.add_column("Framework", style="yellow")
119
+ svc_table.add_column("Entrypoint", style="dim")
120
+
121
+ for svc in detected_services:
122
+ svc_table.add_row(
123
+ svc.get("name", "?"),
124
+ svc.get("path", "?"),
125
+ str(svc.get("port", "?")),
126
+ svc.get("framework", "?"),
127
+ svc.get("entrypoint", "-") or "-"
128
+ )
129
+
130
+ console.print(svc_table)
131
+ console.print()
132
+
133
+ if Confirm.ask("Add services to xenfra.yaml for microservices deployment?", default=True):
134
+ # Add services array to xenfra.yaml
135
+ add_services_to_xenfra_yaml(".", detected_services, mode="single-droplet")
136
+
137
+ console.print("\n[bold green]✓ Added services to xenfra.yaml![/bold green]")
138
+ console.print("[dim]Run 'xenfra deploy' to deploy all services.[/dim]")
139
+ console.print("[dim]Use 'xenfra deploy --mode=multi-droplet' for separate droplets per service.[/dim]")
140
+ return
141
+ else:
142
+ console.print("[dim]Continuing with single-service configuration...[/dim]\n")
143
+
144
+ except ImportError:
145
+ # SDK doesn't have microservices support yet
146
+ pass
147
+ except Exception as e:
148
+ console.print(f"[dim]Note: Microservices detection skipped: {e}[/dim]\n")
149
+
150
+
151
+ # Check for XENFRA_NO_AI environment variable
152
+ no_ai = os.environ.get("XENFRA_NO_AI", "0") == "1"
153
+ if no_ai and not manual:
154
+ console.print("[yellow]XENFRA_NO_AI is set. Using manual mode.[/yellow]")
155
+ manual = True
156
+
157
+ # Manual mode - interactive prompts
158
+ if manual:
159
+ console.print("[cyan]Manual configuration mode[/cyan]\n")
160
+ try:
161
+ manual_prompt_for_config()
162
+ console.print("\n[bold green] xenfra.yaml created successfully![/bold green]")
163
+ console.print("[dim]Run 'xenfra deploy' to deploy your project.[/dim]")
164
+ except KeyboardInterrupt:
165
+ console.print("\n[dim]Cancelled.[/dim]")
166
+ except Exception as e:
167
+ console.print(f"[bold red]Error: {e}[/bold red]")
168
+ return
169
+
170
+ # AI-powered detection (default)
171
+ try:
172
+ # Use context manager for SDK client
173
+ with get_client() as client:
174
+ # Scan codebase
175
+ console.print("[cyan]Analyzing your codebase...[/cyan]")
176
+ code_snippets = scan_codebase()
177
+
178
+ if not code_snippets:
179
+ console.print("[bold red]No code files found to analyze.[/bold red]")
180
+ console.print("[dim]Make sure you're in a Python project directory.[/dim]")
181
+ return
182
+
183
+ console.print(f"[dim]Found {len(code_snippets)} files to analyze[/dim]")
184
+
185
+ # Call Intelligence Service
186
+ analysis = client.intelligence.analyze_codebase(code_snippets)
187
+
188
+ # Client-side conflict detection (ensures Zen Nod always triggers)
189
+ from ..utils.codebase import detect_package_manager_conflicts
190
+ has_conflict_local, detected_managers_local = detect_package_manager_conflicts(code_snippets)
191
+
192
+ if has_conflict_local and not analysis.has_conflict:
193
+ # AI missed the conflict - fix it client-side
194
+ console.print("[dim]Note: Enhanced conflict detection activated[/dim]\n")
195
+ analysis.has_conflict = True
196
+ # Convert dict to object for compatibility
197
+ from types import SimpleNamespace
198
+ analysis.detected_package_managers = [
199
+ SimpleNamespace(**pm) for pm in detected_managers_local
200
+ ]
201
+
202
+ # Display results
203
+ console.print("\n[bold green]Analysis Complete![/bold green]\n")
204
+
205
+ # Handle package manager conflict
206
+ selected_package_manager = analysis.package_manager
207
+ selected_dependency_file = analysis.dependency_file
208
+
209
+ if analysis.has_conflict and analysis.detected_package_managers:
210
+ console.print("[yellow]Multiple package managers detected![/yellow]\n")
211
+
212
+ # Show options
213
+ for i, option in enumerate(analysis.detected_package_managers, 1):
214
+ console.print(f" {i}. [cyan]{option.manager}[/cyan] ({option.file})")
215
+
216
+ console.print(f"\n[dim]Recommended: {analysis.package_manager} (most modern)[/dim]")
217
+
218
+ # Prompt user to select
219
+ choice = Prompt.ask(
220
+ "\nWhich package manager do you want to use?",
221
+ choices=[str(i) for i in range(1, len(analysis.detected_package_managers) + 1)],
222
+ default="1",
223
+ )
224
+
225
+ # Update selection based on user choice
226
+ selected_option = analysis.detected_package_managers[int(choice) - 1]
227
+ selected_package_manager = selected_option.manager
228
+ selected_dependency_file = selected_option.file
229
+
230
+ console.print(
231
+ f"\n[green]Using {selected_package_manager} ({selected_dependency_file})[/green]\n"
232
+ )
233
+
234
+ table = Table(show_header=False, box=None)
235
+ table.add_column("Property", style="cyan")
236
+ table.add_column("Value", style="white")
237
+
238
+ table.add_row("Framework", analysis.framework)
239
+ table.add_row("Port", str(analysis.port))
240
+ table.add_row("Database", analysis.database)
241
+ if analysis.cache:
242
+ table.add_row("Cache", analysis.cache)
243
+ if analysis.workers:
244
+ table.add_row("Workers", ", ".join(analysis.workers))
245
+ table.add_row("Package Manager", selected_package_manager)
246
+ table.add_row("Dependency File", selected_dependency_file)
247
+
248
+ # New: Infrastructure details in summary
249
+ table.add_row("Region", "nyc3 (default)")
250
+ table.add_row("Instance Size", analysis.instance_size)
251
+
252
+ # Resource visualization
253
+ cpu = 1 if analysis.instance_size == "basic" else (2 if analysis.instance_size == "standard" else 4)
254
+ ram = "1GB" if analysis.instance_size == "basic" else ("4GB" if analysis.instance_size == "standard" else "8GB")
255
+ table.add_row("Resources", f"{cpu} vCPU, {ram} RAM")
256
+
257
+ table.add_row("Estimated Cost", f"${analysis.estimated_cost_monthly:.2f}/month")
258
+ table.add_row("Confidence", f"{analysis.confidence:.0%}")
259
+
260
+ console.print(Panel(table, title="[bold]Detected Configuration[/bold]"))
261
+
262
+ if analysis.notes:
263
+ console.print(f"\n[dim]{analysis.notes}[/dim]")
264
+
265
+ # Confirm or edit
266
+ if accept_all:
267
+ confirmed = True
268
+ else:
269
+ confirmed = Confirm.ask("\nCreate xenfra.yaml with this configuration?", default=True)
270
+
271
+ if confirmed:
272
+ generate_xenfra_yaml(analysis, package_manager_override=selected_package_manager, dependency_file_override=selected_dependency_file)
273
+ console.print("[bold green]xenfra.yaml created successfully![/bold green]")
274
+ console.print("[dim]Run 'xenfra deploy' to deploy your project.[/dim]")
275
+ else:
276
+ console.print("[yellow]Configuration cancelled.[/yellow]")
277
+
278
+ except XenfraAPIError as e:
279
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
280
+ except XenfraError as e:
281
+ console.print(f"[bold red]Error: {e}[/bold red]")
282
+ except click.Abort:
283
+ pass
284
+ except Exception as e:
285
+ console.print(f"[bold red]Unexpected error: {e}[/bold red]")
286
+
287
+
288
+ @click.command()
289
+ @click.argument("deployment-id", required=False)
290
+ @click.option("--apply", is_flag=True, help="Auto-apply suggested patch (with confirmation)")
291
+ @click.option("--logs", type=click.File("r"), help="Diagnose from log file instead of deployment")
292
+ def diagnose(deployment_id, apply, logs):
293
+ """
294
+ Diagnose deployment failures using AI.
295
+
296
+ Analyzes logs and provides diagnosis, suggestions, and optionally
297
+ an automatic patch to fix the issue.
298
+ """
299
+ try:
300
+ # Use context manager for all SDK operations
301
+ with get_client() as client:
302
+ # Get logs
303
+ if logs:
304
+ log_content = logs.read()
305
+ console.print("[cyan]Analyzing logs from file...[/cyan]")
306
+ elif deployment_id:
307
+ # Validate deployment ID
308
+ is_valid, error_msg = validate_deployment_id(deployment_id)
309
+ if not is_valid:
310
+ console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
311
+ return
312
+
313
+ console.print(f"[cyan]Fetching logs for deployment {deployment_id}...[/cyan]")
314
+ log_content = client.deployments.get_logs(deployment_id)
315
+
316
+ if not log_content:
317
+ console.print("[yellow]No logs found for this deployment.[/yellow]")
318
+ return
319
+ else:
320
+ console.print(
321
+ "[bold red]Please specify a deployment ID or use --logs <file>[/bold red]"
322
+ )
323
+ console.print(
324
+ "[dim]Usage: xenfra diagnose <deployment-id> or xenfra diagnose --logs error.log[/dim]"
325
+ )
326
+ return
327
+
328
+ # Scrub sensitive data
329
+ scrubbed_logs = scrub_logs(log_content)
330
+
331
+ # Try to read package manager context and collect code snippets
332
+ package_manager = None
333
+ dependency_file = None
334
+ code_snippets = []
335
+ services = None
336
+ try:
337
+ config = read_xenfra_yaml()
338
+ package_manager = config.get("package_manager")
339
+ dependency_file = config.get("dependency_file")
340
+ services = config.get("services")
341
+
342
+ if package_manager and dependency_file:
343
+ console.print(
344
+ f"[dim]Using context: {package_manager} ({dependency_file})[/dim]"
345
+ )
346
+ # Automatically collect the main dependency file
347
+ if os.path.exists(dependency_file):
348
+ with open(dependency_file, "r", encoding="utf-8", errors="ignore") as f:
349
+ code_snippets.append({
350
+ "file": dependency_file,
351
+ "content": f.read()
352
+ })
353
+
354
+ # If it's a multi-service project, also collect requirement files from sub-services
355
+ if services:
356
+ for svc in services:
357
+ svc_path = svc.get("path", ".")
358
+ # Look for common dependency files in service path
359
+ for common_file in ["requirements.txt", "pyproject.toml"]:
360
+ pfile = os.path.join(svc_path, common_file) if svc_path != "." else common_file
361
+ # Don't add if already added (e.g. root dependency file)
362
+ if os.path.exists(pfile) and not any(s["file"] == pfile for s in code_snippets):
363
+ with open(pfile, "r", encoding="utf-8", errors="ignore") as f:
364
+ code_snippets.append({
365
+ "file": pfile,
366
+ "content": f.read()
367
+ })
368
+
369
+ except FileNotFoundError:
370
+ console.print(
371
+ "[dim]No xenfra.yaml found - inferring context from files[/dim]"
372
+ )
373
+ # Fallback: scan root for dependency files
374
+ for common_file in ["requirements.txt", "pyproject.toml"]:
375
+ if os.path.exists(common_file):
376
+ with open(common_file, "r", encoding="utf-8", errors="ignore") as f:
377
+ code_snippets.append({
378
+ "file": common_file,
379
+ "content": f.read()
380
+ })
381
+
382
+ # Diagnose with context and snippets
383
+ console.print("[cyan]Analyzing failure...[/cyan]")
384
+ result = client.intelligence.diagnose(
385
+ logs=scrubbed_logs,
386
+ package_manager=package_manager,
387
+ dependency_file=dependency_file,
388
+ services=services,
389
+ code_snippets=code_snippets
390
+ )
391
+
392
+ # Display diagnosis
393
+ console.print("\n")
394
+ console.print(
395
+ Panel(result.diagnosis, title="[bold red]Diagnosis[/bold red]", border_style="red")
396
+ )
397
+ console.print(
398
+ Panel(
399
+ result.suggestion,
400
+ title="[bold yellow]Suggestion[/bold yellow]",
401
+ border_style="yellow",
402
+ )
403
+ )
404
+
405
+ # Handle patch
406
+ if result.patch and result.patch.file:
407
+ console.print("\n[bold green]Automatic fix available![/bold green]")
408
+ console.print(f" File: [cyan]{result.patch.file}[/cyan]")
409
+ console.print(f" Operation: [yellow]{result.patch.operation}[/yellow]")
410
+ console.print(f" Value: [white]{result.patch.value}[/white]")
411
+
412
+ if apply or Confirm.ask("\nApply this patch?", default=False):
413
+ try:
414
+ apply_patch(result.patch.model_dump())
415
+ console.print("[bold green]Patch applied successfully![/bold green]")
416
+ console.print("[cyan]Run 'xenfra deploy' to retry deployment.[/cyan]")
417
+ except FileNotFoundError as e:
418
+ console.print(f"[bold red]Error: {e}[/bold red]")
419
+ except Exception as e:
420
+ console.print(f"[bold red]Failed to apply patch: {e}[/bold red]")
421
+ else:
422
+ console.print("[dim]Patch not applied. Follow manual steps above.[/dim]")
423
+ else:
424
+ console.print("\n[yellow]No automatic fix available.[/yellow]")
425
+ console.print("[dim]Please follow the manual steps in the suggestion above.[/dim]")
426
+
427
+ except XenfraAPIError as e:
428
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
429
+ except XenfraError as e:
430
+ console.print(f"[bold red]Error: {e}[/bold red]")
431
+ except click.Abort:
432
+ pass
433
+ except Exception as e:
434
+ console.print(f"[bold red]Unexpected error: {e}[/bold red]")
435
+
436
+
437
+ @click.command()
438
+ def analyze():
439
+ """
440
+ Analyze codebase without creating configuration.
441
+
442
+ Shows what AI would detect, useful for previewing before running init.
443
+ """
444
+ try:
445
+ # Use context manager for SDK client
446
+ with get_client() as client:
447
+ # Scan codebase
448
+ console.print("[cyan]Analyzing your codebase...[/cyan]")
449
+ code_snippets = scan_codebase()
450
+
451
+ if not code_snippets:
452
+ console.print("[bold red]No code files found to analyze.[/bold red]")
453
+ return
454
+
455
+ # Call Intelligence Service
456
+ analysis = client.intelligence.analyze_codebase(code_snippets)
457
+
458
+ # Display results
459
+ console.print("\n[bold green]Analysis Results:[/bold green]\n")
460
+
461
+ table = Table(show_header=False, box=None)
462
+ table.add_column("Property", style="cyan")
463
+ table.add_column("Value", style="white")
464
+
465
+ table.add_row("Framework", analysis.framework)
466
+ table.add_row("Port", str(analysis.port))
467
+ table.add_row("Database", analysis.database)
468
+ if analysis.cache:
469
+ table.add_row("Cache", analysis.cache)
470
+ if analysis.workers:
471
+ table.add_row("Workers", ", ".join(analysis.workers))
472
+ if analysis.env_vars:
473
+ table.add_row("Environment Variables", ", ".join(analysis.env_vars))
474
+
475
+ # New: Infrastructure details in preview
476
+ table.add_row("Region", "nyc3 (default)")
477
+ table.add_row("Instance Size", analysis.instance_size)
478
+
479
+ # Resource visualization
480
+ cpu = 1 if analysis.instance_size == "basic" else (2 if analysis.instance_size == "standard" else 4)
481
+ ram = "1GB" if analysis.instance_size == "basic" else ("4GB" if analysis.instance_size == "standard" else "8GB")
482
+ table.add_row("Resources", f"{cpu} vCPU, {ram} RAM")
483
+
484
+ table.add_row("Estimated Cost", f"${analysis.estimated_cost_monthly:.2f}/month")
485
+ table.add_row("Confidence", f"{analysis.confidence:.0%}")
486
+
487
+ console.print(table)
488
+
489
+ if analysis.notes:
490
+ console.print(f"\n[dim]Notes: {analysis.notes}[/dim]")
491
+
492
+ console.print(
493
+ "\n[dim]Run 'xenfra init' to create xenfra.yaml with this configuration.[/dim]"
494
+ )
495
+
496
+ except XenfraAPIError as e:
497
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
498
+ except XenfraError as e:
499
+ console.print(f"[bold red]Error: {e}[/bold red]")
500
+ except click.Abort:
501
+ pass
502
+ except Exception as e:
503
+ console.print(f"[bold red]Unexpected error: {e}[/bold red]")