xenfra 0.3.1__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.
@@ -0,0 +1,343 @@
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
+ return XenfraClient(token=token, api_url=API_BASE_URL)
38
+
39
+
40
+ @click.command()
41
+ @click.option("--manual", is_flag=True, help="Skip AI detection, use interactive mode")
42
+ @click.option("--accept-all", is_flag=True, help="Accept AI suggestions without confirmation")
43
+ def init(manual, accept_all):
44
+ """
45
+ Initialize Xenfra configuration (AI-powered by default).
46
+
47
+ Scans your codebase, detects framework and dependencies,
48
+ and generates xenfra.yaml automatically.
49
+
50
+ Use --manual to skip AI and configure interactively.
51
+ Set XENFRA_NO_AI=1 environment variable to force manual mode globally.
52
+ """
53
+ # Check if config already exists
54
+ if has_xenfra_config():
55
+ console.print("[yellow]xenfra.yaml already exists.[/yellow]")
56
+ if not Confirm.ask("Overwrite existing configuration?"):
57
+ console.print("[dim]Cancelled.[/dim]")
58
+ return
59
+
60
+ # Check for XENFRA_NO_AI environment variable
61
+ no_ai = os.environ.get("XENFRA_NO_AI", "0") == "1"
62
+ if no_ai and not manual:
63
+ console.print("[yellow]XENFRA_NO_AI is set. Using manual mode.[/yellow]")
64
+ manual = True
65
+
66
+ # Manual mode - interactive prompts
67
+ if manual:
68
+ console.print("[cyan]Manual configuration mode[/cyan]\n")
69
+ try:
70
+ manual_prompt_for_config()
71
+ console.print("\n[bold green]✓ xenfra.yaml created successfully![/bold green]")
72
+ console.print("[dim]Run 'xenfra deploy' to deploy your project.[/dim]")
73
+ except KeyboardInterrupt:
74
+ console.print("\n[dim]Cancelled.[/dim]")
75
+ except Exception as e:
76
+ console.print(f"[bold red]Error: {e}[/bold red]")
77
+ return
78
+
79
+ # AI-powered detection (default)
80
+ try:
81
+ # Use context manager for SDK client
82
+ with get_client() as client:
83
+ # Scan codebase
84
+ console.print("[cyan]Analyzing your codebase...[/cyan]")
85
+ code_snippets = scan_codebase()
86
+
87
+ if not code_snippets:
88
+ console.print("[bold red]No code files found to analyze.[/bold red]")
89
+ console.print("[dim]Make sure you're in a Python project directory.[/dim]")
90
+ return
91
+
92
+ console.print(f"[dim]Found {len(code_snippets)} files to analyze[/dim]")
93
+
94
+ # Call Intelligence Service
95
+ analysis = client.intelligence.analyze_codebase(code_snippets)
96
+
97
+ # Display results
98
+ console.print("\n[bold green]Analysis Complete![/bold green]\n")
99
+
100
+ # Handle package manager conflict
101
+ selected_package_manager = analysis.package_manager
102
+ selected_dependency_file = analysis.dependency_file
103
+
104
+ if analysis.has_conflict and analysis.detected_package_managers:
105
+ console.print("[yellow]Multiple package managers detected![/yellow]\n")
106
+
107
+ # Show options
108
+ for i, option in enumerate(analysis.detected_package_managers, 1):
109
+ console.print(f" {i}. [cyan]{option.manager}[/cyan] ({option.file})")
110
+
111
+ console.print(f"\n[dim]Recommended: {analysis.package_manager} (most modern)[/dim]")
112
+
113
+ # Prompt user to select
114
+ choice = Prompt.ask(
115
+ "\nWhich package manager do you want to use?",
116
+ choices=[str(i) for i in range(1, len(analysis.detected_package_managers) + 1)],
117
+ default="1",
118
+ )
119
+
120
+ # Update selection based on user choice
121
+ selected_option = analysis.detected_package_managers[int(choice) - 1]
122
+ selected_package_manager = selected_option.manager
123
+ selected_dependency_file = selected_option.file
124
+
125
+ console.print(
126
+ f"\n[green]Using {selected_package_manager} ({selected_dependency_file})[/green]\n"
127
+ )
128
+
129
+ table = Table(show_header=False, box=None)
130
+ table.add_column("Property", style="cyan")
131
+ table.add_column("Value", style="white")
132
+
133
+ table.add_row("Framework", analysis.framework)
134
+ table.add_row("Port", str(analysis.port))
135
+ table.add_row("Database", analysis.database)
136
+ if analysis.cache:
137
+ table.add_row("Cache", analysis.cache)
138
+ if analysis.workers:
139
+ table.add_row("Workers", ", ".join(analysis.workers))
140
+ table.add_row("Package Manager", selected_package_manager)
141
+ table.add_row("Dependency File", selected_dependency_file)
142
+ table.add_row("Instance Size", analysis.instance_size)
143
+ table.add_row("Estimated Cost", f"${analysis.estimated_cost_monthly:.2f}/month")
144
+ table.add_row("Confidence", f"{analysis.confidence:.0%}")
145
+
146
+ console.print(Panel(table, title="[bold]Detected Configuration[/bold]"))
147
+
148
+ if analysis.notes:
149
+ console.print(f"\n[dim]{analysis.notes}[/dim]")
150
+
151
+ # Confirm or edit
152
+ if accept_all:
153
+ confirmed = True
154
+ else:
155
+ confirmed = Confirm.ask("\nCreate xenfra.yaml with this configuration?", default=True)
156
+
157
+ if confirmed:
158
+ generate_xenfra_yaml(analysis)
159
+ console.print("[bold green]xenfra.yaml created successfully![/bold green]")
160
+ console.print("[dim]Run 'xenfra deploy' to deploy your project.[/dim]")
161
+ else:
162
+ console.print("[yellow]Configuration cancelled.[/yellow]")
163
+
164
+ except XenfraAPIError as e:
165
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
166
+ except XenfraError as e:
167
+ console.print(f"[bold red]Error: {e}[/bold red]")
168
+ except click.Abort:
169
+ pass
170
+ except Exception as e:
171
+ console.print(f"[bold red]Unexpected error: {e}[/bold red]")
172
+
173
+
174
+ @click.command()
175
+ @click.argument("deployment-id", required=False)
176
+ @click.option("--apply", is_flag=True, help="Auto-apply suggested patch (with confirmation)")
177
+ @click.option("--logs", type=click.File("r"), help="Diagnose from log file instead of deployment")
178
+ def diagnose(deployment_id, apply, logs):
179
+ """
180
+ Diagnose deployment failures using AI.
181
+
182
+ Analyzes logs and provides diagnosis, suggestions, and optionally
183
+ an automatic patch to fix the issue.
184
+ """
185
+ try:
186
+ # Use context manager for all SDK operations
187
+ with get_client() as client:
188
+ # Get logs
189
+ if logs:
190
+ log_content = logs.read()
191
+ console.print("[cyan]Analyzing logs from file...[/cyan]")
192
+ elif deployment_id:
193
+ # Validate deployment ID
194
+ is_valid, error_msg = validate_deployment_id(deployment_id)
195
+ if not is_valid:
196
+ console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
197
+ return
198
+
199
+ console.print(f"[cyan]Fetching logs for deployment {deployment_id}...[/cyan]")
200
+ log_content = client.deployments.get_logs(deployment_id)
201
+
202
+ if not log_content:
203
+ console.print("[yellow]No logs found for this deployment.[/yellow]")
204
+ return
205
+ else:
206
+ console.print(
207
+ "[bold red]Please specify a deployment ID or use --logs <file>[/bold red]"
208
+ )
209
+ console.print(
210
+ "[dim]Usage: xenfra diagnose <deployment-id> or xenfra diagnose --logs error.log[/dim]"
211
+ )
212
+ return
213
+
214
+ # Scrub sensitive data
215
+ scrubbed_logs = scrub_logs(log_content)
216
+
217
+ # Try to read package manager context from xenfra.yaml
218
+ package_manager = None
219
+ dependency_file = None
220
+ try:
221
+ config = read_xenfra_yaml()
222
+ package_manager = config.get("package_manager")
223
+ dependency_file = config.get("dependency_file")
224
+
225
+ if package_manager and dependency_file:
226
+ console.print(
227
+ f"[dim]Using context: {package_manager} ({dependency_file})[/dim]"
228
+ )
229
+ except FileNotFoundError:
230
+ # No config file - diagnosis will infer from logs
231
+ console.print(
232
+ "[dim]No xenfra.yaml found - inferring package manager from logs[/dim]"
233
+ )
234
+
235
+ # Diagnose with context
236
+ console.print("[cyan]Analyzing failure...[/cyan]")
237
+ result = client.intelligence.diagnose(
238
+ logs=scrubbed_logs, package_manager=package_manager, dependency_file=dependency_file
239
+ )
240
+
241
+ # Display diagnosis
242
+ console.print("\n")
243
+ console.print(
244
+ Panel(result.diagnosis, title="[bold red]Diagnosis[/bold red]", border_style="red")
245
+ )
246
+ console.print(
247
+ Panel(
248
+ result.suggestion,
249
+ title="[bold yellow]Suggestion[/bold yellow]",
250
+ border_style="yellow",
251
+ )
252
+ )
253
+
254
+ # Handle patch
255
+ if result.patch and result.patch.file:
256
+ console.print("\n[bold green]Automatic fix available![/bold green]")
257
+ console.print(f" File: [cyan]{result.patch.file}[/cyan]")
258
+ console.print(f" Operation: [yellow]{result.patch.operation}[/yellow]")
259
+ console.print(f" Value: [white]{result.patch.value}[/white]")
260
+
261
+ if apply or Confirm.ask("\nApply this patch?", default=False):
262
+ try:
263
+ apply_patch(result.patch.model_dump())
264
+ console.print("[bold green]Patch applied successfully![/bold green]")
265
+ console.print("[cyan]Run 'xenfra deploy' to retry deployment.[/cyan]")
266
+ except FileNotFoundError as e:
267
+ console.print(f"[bold red]Error: {e}[/bold red]")
268
+ except Exception as e:
269
+ console.print(f"[bold red]Failed to apply patch: {e}[/bold red]")
270
+ else:
271
+ console.print("[dim]Patch not applied. Follow manual steps above.[/dim]")
272
+ else:
273
+ console.print("\n[yellow]No automatic fix available.[/yellow]")
274
+ console.print("[dim]Please follow the manual steps in the suggestion above.[/dim]")
275
+
276
+ except XenfraAPIError as e:
277
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
278
+ except XenfraError as e:
279
+ console.print(f"[bold red]Error: {e}[/bold red]")
280
+ except click.Abort:
281
+ pass
282
+ except Exception as e:
283
+ console.print(f"[bold red]Unexpected error: {e}[/bold red]")
284
+
285
+
286
+ @click.command()
287
+ def analyze():
288
+ """
289
+ Analyze codebase without creating configuration.
290
+
291
+ Shows what AI would detect, useful for previewing before running init.
292
+ """
293
+ try:
294
+ # Use context manager for SDK client
295
+ with get_client() as client:
296
+ # Scan codebase
297
+ console.print("[cyan]Analyzing your codebase...[/cyan]")
298
+ code_snippets = scan_codebase()
299
+
300
+ if not code_snippets:
301
+ console.print("[bold red]No code files found to analyze.[/bold red]")
302
+ return
303
+
304
+ # Call Intelligence Service
305
+ analysis = client.intelligence.analyze_codebase(code_snippets)
306
+
307
+ # Display results
308
+ console.print("\n[bold green]Analysis Results:[/bold green]\n")
309
+
310
+ table = Table(show_header=False, box=None)
311
+ table.add_column("Property", style="cyan")
312
+ table.add_column("Value", style="white")
313
+
314
+ table.add_row("Framework", analysis.framework)
315
+ table.add_row("Port", str(analysis.port))
316
+ table.add_row("Database", analysis.database)
317
+ if analysis.cache:
318
+ table.add_row("Cache", analysis.cache)
319
+ if analysis.workers:
320
+ table.add_row("Workers", ", ".join(analysis.workers))
321
+ if analysis.env_vars:
322
+ table.add_row("Environment Variables", ", ".join(analysis.env_vars))
323
+ table.add_row("Instance Size", analysis.instance_size)
324
+ table.add_row("Estimated Cost", f"${analysis.estimated_cost_monthly:.2f}/month")
325
+ table.add_row("Confidence", f"{analysis.confidence:.0%}")
326
+
327
+ console.print(table)
328
+
329
+ if analysis.notes:
330
+ console.print(f"\n[dim]Notes: {analysis.notes}[/dim]")
331
+
332
+ console.print(
333
+ "\n[dim]Run 'xenfra init' to create xenfra.yaml with this configuration.[/dim]"
334
+ )
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]")
@@ -0,0 +1,204 @@
1
+ """
2
+ Project management commands for Xenfra CLI.
3
+ """
4
+
5
+ import click
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+ from xenfra_sdk import XenfraClient
9
+ from xenfra_sdk.exceptions import XenfraAPIError, XenfraError
10
+
11
+ from ..utils.auth import API_BASE_URL, get_auth_token
12
+ from ..utils.validation import (
13
+ validate_project_id,
14
+ validate_project_name,
15
+ validate_region,
16
+ validate_size_slug,
17
+ )
18
+
19
+ console = Console()
20
+
21
+
22
+ def get_client() -> XenfraClient:
23
+ """Get authenticated SDK client."""
24
+ token = get_auth_token()
25
+ if not token:
26
+ console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
27
+ raise click.Abort()
28
+
29
+ return XenfraClient(token=token, api_url=API_BASE_URL)
30
+
31
+
32
+ @click.group()
33
+ def projects():
34
+ """Manage projects."""
35
+ pass
36
+
37
+
38
+ @projects.command()
39
+ def list():
40
+ """List all projects."""
41
+ try:
42
+ # Use context manager for proper cleanup
43
+ with get_client() as client:
44
+ projects_list = client.projects.list()
45
+
46
+ if not projects_list:
47
+ console.print("[bold yellow]No projects found.[/bold yellow]")
48
+ return
49
+
50
+ # Create a rich table
51
+ table = Table(title="Projects")
52
+ table.add_column("ID", style="cyan")
53
+ table.add_column("Name", style="green")
54
+ table.add_column("Status", style="yellow")
55
+ table.add_column("Region", style="blue")
56
+ table.add_column("IP Address", style="magenta")
57
+ table.add_column("Cost/Month", style="red")
58
+
59
+ for project in projects_list:
60
+ cost = (
61
+ f"${project.estimated_monthly_cost:.2f}"
62
+ if project.estimated_monthly_cost
63
+ else "N/A"
64
+ )
65
+ table.add_row(
66
+ str(project.id),
67
+ project.name,
68
+ project.status,
69
+ project.region,
70
+ project.ip_address or "N/A",
71
+ cost,
72
+ )
73
+
74
+ console.print(table)
75
+
76
+ except XenfraAPIError as e:
77
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
78
+ except XenfraError as e:
79
+ console.print(f"[bold red]Error: {e}[/bold red]")
80
+ except click.Abort:
81
+ pass
82
+
83
+
84
+ @projects.command()
85
+ @click.argument("project_id", type=int)
86
+ def show(project_id):
87
+ """Show details for a specific project."""
88
+ # Validate project ID
89
+ is_valid, error_msg = validate_project_id(project_id)
90
+ if not is_valid:
91
+ console.print(f"[bold red]Invalid project ID: {error_msg}[/bold red]")
92
+ raise click.Abort()
93
+
94
+ try:
95
+ with get_client() as client:
96
+ project = client.projects.show(project_id)
97
+
98
+ # Create detailed panel
99
+ from rich.panel import Panel
100
+
101
+ details = f"""[cyan]Name:[/cyan] {project.name}
102
+ [cyan]Status:[/cyan] {project.status}
103
+ [cyan]Region:[/cyan] {project.region}
104
+ [cyan]IP Address:[/cyan] {project.ip_address or 'N/A'}
105
+ [cyan]Size:[/cyan] {project.size_slug}
106
+ [cyan]Cost/Month:[/cyan] ${project.estimated_monthly_cost:.2f} USD
107
+ [cyan]Created:[/cyan] {project.created_at}"""
108
+
109
+ panel = Panel(
110
+ details,
111
+ title=f"[bold green]Project {project.id}[/bold green]",
112
+ border_style="green",
113
+ )
114
+ console.print(panel)
115
+
116
+ except XenfraAPIError as e:
117
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
118
+ except XenfraError as e:
119
+ console.print(f"[bold red]Error: {e}[/bold red]")
120
+ except click.Abort:
121
+ pass
122
+
123
+
124
+ @projects.command()
125
+ @click.argument("project_id", type=int)
126
+ @click.confirmation_option(prompt="Are you sure you want to delete this project?")
127
+ def delete(project_id):
128
+ """Delete a project."""
129
+ # Validate project ID
130
+ is_valid, error_msg = validate_project_id(project_id)
131
+ if not is_valid:
132
+ console.print(f"[bold red]Invalid project ID: {error_msg}[/bold red]")
133
+ raise click.Abort()
134
+
135
+ try:
136
+ with get_client() as client:
137
+ client.projects.delete(str(project_id))
138
+ console.print(f"[bold green]Project {project_id} deletion initiated.[/bold green]")
139
+
140
+ except XenfraAPIError as e:
141
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
142
+ except XenfraError as e:
143
+ console.print(f"[bold red]Error: {e}[/bold red]")
144
+ except click.Abort:
145
+ pass
146
+
147
+
148
+ @projects.command()
149
+ @click.argument("name")
150
+ @click.option("--region", default="nyc3", help="DigitalOcean region (default: nyc3)")
151
+ @click.option(
152
+ "--size", "size_slug", default="s-1vcpu-1gb", help="Droplet size (default: s-1vcpu-1gb)"
153
+ )
154
+ def create(name, region, size_slug):
155
+ """Create a new project."""
156
+ # Validate project name
157
+ is_valid, error_msg = validate_project_name(name)
158
+ if not is_valid:
159
+ console.print(f"[bold red]Invalid project name: {error_msg}[/bold red]")
160
+ raise click.Abort()
161
+
162
+ # Validate region
163
+ is_valid, error_msg = validate_region(region)
164
+ if not is_valid:
165
+ console.print(f"[bold red]Invalid region: {error_msg}[/bold red]")
166
+ raise click.Abort()
167
+
168
+ # Validate size slug
169
+ is_valid, error_msg = validate_size_slug(size_slug)
170
+ if not is_valid:
171
+ console.print(f"[bold red]Invalid size slug: {error_msg}[/bold red]")
172
+ raise click.Abort()
173
+
174
+ try:
175
+ with get_client() as client:
176
+ console.print(f"[cyan]Creating project '{name}'...[/cyan]")
177
+
178
+ # Create project
179
+ project = client.projects.create(name=name, region=region, size_slug=size_slug)
180
+
181
+ # Display success message
182
+ console.print("[bold green]✓[/bold green] Project created successfully!")
183
+
184
+ # Show project details
185
+ from rich.panel import Panel
186
+
187
+ details = f"""[cyan]ID:[/cyan] {project.id}
188
+ [cyan]Name:[/cyan] {project.name}
189
+ [cyan]Status:[/cyan] {project.status}
190
+ [cyan]Region:[/cyan] {project.region}
191
+ [cyan]Size:[/cyan] {project.size_slug}
192
+ [cyan]Estimated Cost:[/cyan] ${project.estimated_monthly_cost:.2f}/month"""
193
+
194
+ panel = Panel(
195
+ details, title="[bold green]New Project[/bold green]", border_style="green"
196
+ )
197
+ console.print(panel)
198
+
199
+ except XenfraAPIError as e:
200
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
201
+ except XenfraError as e:
202
+ console.print(f"[bold red]Error: {e}[/bold red]")
203
+ except click.Abort:
204
+ pass