xenfra 0.2.8__py3-none-any.whl → 0.3.0__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,484 +1,666 @@
1
- """
2
- Deployment commands for Xenfra CLI.
3
- """
4
-
5
- import os
6
-
7
- import click
8
- from rich.console import Console
9
- from rich.panel import Panel
10
- from rich.table import Table
11
- from xenfra_sdk import XenfraClient
12
- from xenfra_sdk.exceptions import XenfraAPIError, XenfraError
13
- from xenfra_sdk.privacy import scrub_logs
14
-
15
- from ..utils.auth import API_BASE_URL, get_auth_token
16
- from ..utils.codebase import has_xenfra_config
17
- from ..utils.config import apply_patch
18
- from ..utils.validation import (
19
- validate_branch_name,
20
- validate_deployment_id,
21
- validate_framework,
22
- validate_git_repo_url,
23
- validate_project_name,
24
- )
25
-
26
- console = Console()
27
-
28
- # Maximum number of retry attempts for auto-healing
29
- MAX_RETRY_ATTEMPTS = 3
30
-
31
-
32
- def get_client() -> XenfraClient:
33
- """Get authenticated SDK client."""
34
- token = get_auth_token()
35
- if not token:
36
- console.print("[bold red]Not logged in. Run 'xenfra auth login' first.[/bold red]")
37
- raise click.Abort()
38
-
39
- return XenfraClient(token=token, api_url=API_BASE_URL)
40
-
41
-
42
- def show_diagnosis_panel(diagnosis: str, suggestion: str):
43
- """Display diagnosis and suggestion in formatted panels."""
44
- console.print()
45
- console.print(Panel(diagnosis, title="[bold red]🔍 Diagnosis[/bold red]", border_style="red"))
46
- console.print()
47
- console.print(
48
- Panel(suggestion, title="[bold yellow]💡 Suggestion[/bold yellow]", border_style="yellow")
49
- )
50
-
51
-
52
- def show_patch_preview(patch_data: dict):
53
- """Show a preview of the patch that will be applied."""
54
- console.print()
55
- console.print("[bold green]🔧 Automatic Fix Available[/bold green]")
56
- console.print(f" [cyan]File:[/cyan] {patch_data.get('file')}")
57
- console.print(f" [cyan]Operation:[/cyan] {patch_data.get('operation')}")
58
- console.print(f" [cyan]Value:[/cyan] {patch_data.get('value')}")
59
- console.print()
60
-
61
-
62
- def zen_nod_workflow(logs: str, client: XenfraClient, attempt: int) -> bool:
63
- """
64
- Execute the Zen Nod auto-healing workflow.
65
-
66
- Args:
67
- logs: Deployment error logs
68
- client: Authenticated SDK client
69
- attempt: Current attempt number
70
-
71
- Returns:
72
- True if patch was applied and user wants to retry, False otherwise
73
- """
74
- console.print()
75
- console.print(f"[cyan]🤖 Analyzing failure (attempt {attempt}/{MAX_RETRY_ATTEMPTS})...[/cyan]")
76
-
77
- # Scrub sensitive data from logs
78
- scrubbed_logs = scrub_logs(logs)
79
-
80
- # Diagnose with AI
81
- try:
82
- diagnosis_result = client.intelligence.diagnose(scrubbed_logs)
83
- except Exception as e:
84
- console.print(f"[yellow]Could not diagnose failure: {e}[/yellow]")
85
- return False
86
-
87
- # Show diagnosis
88
- show_diagnosis_panel(diagnosis_result.diagnosis, diagnosis_result.suggestion)
89
-
90
- # Check if there's an automatic patch
91
- if diagnosis_result.patch and diagnosis_result.patch.file:
92
- show_patch_preview(diagnosis_result.patch.model_dump())
93
-
94
- # Zen Nod confirmation
95
- if click.confirm("Apply this fix and retry deployment?", default=True):
96
- try:
97
- # Apply patch (with automatic backup)
98
- backup_path = apply_patch(diagnosis_result.patch.model_dump())
99
- console.print("[bold green]✓ Patch applied[/bold green]")
100
- if backup_path:
101
- console.print(f"[dim]Backup saved: {backup_path}[/dim]")
102
- return True # Signal to retry
103
- except Exception as e:
104
- console.print(f"[bold red]Failed to apply patch: {e}[/bold red]")
105
- return False
106
- else:
107
- console.print()
108
- console.print("[yellow]❌ Patch declined. Follow the manual steps above.[/yellow]")
109
- return False
110
- else:
111
- console.print()
112
- console.print(
113
- "[yellow]No automatic fix available. Please follow the manual steps above.[/yellow]"
114
- )
115
- return False
116
-
117
-
118
- @click.command()
119
- @click.option("--project-name", help="Project name (defaults to current directory name)")
120
- @click.option("--git-repo", help="Git repository URL (if deploying from git)")
121
- @click.option("--branch", default="main", help="Git branch (default: main)")
122
- @click.option("--framework", help="Framework override (fastapi, flask, django)")
123
- @click.option("--no-heal", is_flag=True, help="Disable auto-healing on failure")
124
- def deploy(project_name, git_repo, branch, framework, no_heal):
125
- """
126
- Deploy current project to DigitalOcean with auto-healing.
127
-
128
- Deploys your application with zero configuration. The CLI will:
129
- 1. Check for xenfra.yaml (or run init if missing)
130
- 2. Create a deployment
131
- 3. Auto-diagnose and fix failures (unless --no-heal is set or XENFRA_NO_AI=1)
132
-
133
- Set XENFRA_NO_AI=1 environment variable to disable all AI features.
134
- """
135
- # Check XENFRA_NO_AI environment variable
136
- no_ai = os.environ.get("XENFRA_NO_AI", "0") == "1"
137
- if no_ai:
138
- console.print("[yellow]XENFRA_NO_AI is set. Auto-healing disabled.[/yellow]")
139
- no_heal = True
140
-
141
- # Check for xenfra.yaml
142
- if not has_xenfra_config():
143
- console.print("[yellow]No xenfra.yaml found.[/yellow]")
144
- if click.confirm("Run 'xenfra init' to create configuration?", default=True):
145
- from .intelligence import init
146
-
147
- ctx = click.get_current_context()
148
- ctx.invoke(init, manual=no_ai, accept_all=False)
149
- else:
150
- console.print("[dim]Deployment cancelled.[/dim]")
151
- return
152
-
153
- # Default project name to current directory
154
- if not project_name:
155
- project_name = os.path.basename(os.getcwd())
156
-
157
- # Validate project name
158
- is_valid, error_msg = validate_project_name(project_name)
159
- if not is_valid:
160
- console.print(f"[bold red]Invalid project name: {error_msg}[/bold red]")
161
- raise click.Abort()
162
-
163
- # Validate git repo if provided
164
- if git_repo:
165
- is_valid, error_msg = validate_git_repo_url(git_repo)
166
- if not is_valid:
167
- console.print(f"[bold red]Invalid git repository URL: {error_msg}[/bold red]")
168
- raise click.Abort()
169
- console.print(f"[cyan]Deploying {project_name} from git repository...[/cyan]")
170
- else:
171
- 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
177
-
178
- # Validate branch name
179
- is_valid, error_msg = validate_branch_name(branch)
180
- if not is_valid:
181
- console.print(f"[bold red]Invalid branch name: {error_msg}[/bold red]")
182
- raise click.Abort()
183
-
184
- # Validate framework if provided
185
- if framework:
186
- is_valid, error_msg = validate_framework(framework)
187
- if not is_valid:
188
- console.print(f"[bold red]Invalid framework: {error_msg}[/bold red]")
189
- raise click.Abort()
190
-
191
- # Retry loop for auto-healing
192
- attempt = 0
193
- deployment_id = None
194
-
195
- try:
196
- with get_client() as client:
197
- while attempt < MAX_RETRY_ATTEMPTS:
198
- # Safety check to prevent infinite loops
199
- if attempt > MAX_RETRY_ATTEMPTS:
200
- raise RuntimeError("Safety break: Retry loop exceeded MAX_RETRY_ATTEMPTS.")
201
-
202
- attempt += 1
203
-
204
- if attempt > 1:
205
- console.print(
206
- f"\n[cyan]🔄 Retrying deployment (attempt {attempt}/{MAX_RETRY_ATTEMPTS})...[/cyan]"
207
- )
208
- else:
209
- console.print("[cyan]Creating deployment...[/cyan]")
210
-
211
- # Detect framework if not provided
212
- if not framework:
213
- console.print("[dim]Auto-detecting framework...[/dim]")
214
- framework = "fastapi" # Default for now
215
-
216
- # Create deployment
217
- try:
218
- deployment = client.deployments.create(
219
- project_name=project_name,
220
- git_repo=git_repo,
221
- branch=branch,
222
- framework=framework,
223
- )
224
-
225
- deployment_id = deployment["deployment_id"]
226
- console.print(
227
- f"[bold green]✓[/bold green] Deployment created: [cyan]{deployment_id}[/cyan]"
228
- )
229
-
230
- # Show deployment details
231
- details_table = Table(show_header=False, box=None)
232
- details_table.add_column("Property", style="cyan")
233
- details_table.add_column("Value", style="white")
234
-
235
- details_table.add_row("Deployment ID", str(deployment_id))
236
- details_table.add_row("Project", project_name)
237
- if git_repo:
238
- details_table.add_row("Repository", git_repo)
239
- details_table.add_row("Branch", branch)
240
- details_table.add_row("Framework", framework)
241
- details_table.add_row("Status", deployment.get("status", "PENDING"))
242
-
243
- panel = Panel(
244
- details_table,
245
- title="[bold green]Deployment Started[/bold green]",
246
- border_style="green",
247
- )
248
- console.print(panel)
249
-
250
- # Show next steps
251
- console.print("\n[bold]Next steps:[/bold]")
252
- console.print(f" • Monitor status: [cyan]xenfra status {deployment_id}[/cyan]")
253
- console.print(f" • View logs: [cyan]xenfra logs {deployment_id}[/cyan]")
254
- if not no_heal:
255
- console.print(
256
- f" • Diagnose issues: [cyan]xenfra diagnose {deployment_id}[/cyan]"
257
- )
258
-
259
- # Success - break out of retry loop
260
- break
261
-
262
- except XenfraAPIError as e:
263
- # Deployment failed
264
- console.print(f"[bold red]✗ Deployment failed: {e.detail}[/bold red]")
265
-
266
- # Check if we should auto-heal
267
- if no_heal or attempt >= MAX_RETRY_ATTEMPTS:
268
- # No auto-healing or max retries reached
269
- if attempt >= MAX_RETRY_ATTEMPTS:
270
- console.print(
271
- f"\n[bold red]❌ Maximum retry attempts ({MAX_RETRY_ATTEMPTS}) reached.[/bold red]"
272
- )
273
- console.print(
274
- "[yellow]Unable to auto-fix the issue. Please review the errors above.[/yellow]"
275
- )
276
- raise
277
- else:
278
- # Try to get logs for diagnosis
279
- error_logs = str(e.detail)
280
- try:
281
- if deployment_id:
282
- # This should be a method in the SDK that returns a string
283
- logs_response = client.deployments.get_logs(deployment_id)
284
- if isinstance(logs_response, dict):
285
- error_logs = logs_response.get("logs", str(e.detail))
286
- else:
287
- error_logs = str(logs_response) # Assuming it can be a string
288
- except Exception as log_err:
289
- console.print(
290
- f"[yellow]Warning: Could not fetch detailed logs for diagnosis: {log_err}[/yellow]"
291
- )
292
- # Fallback to the initial error detail
293
- pass
294
-
295
- # Run Zen Nod workflow
296
- should_retry = zen_nod_workflow(error_logs, client, attempt)
297
-
298
- if not should_retry:
299
- # User declined patch or no patch available
300
- console.print("\n[dim]Deployment cancelled.[/dim]")
301
- raise click.Abort()
302
-
303
- # Continue to next iteration (retry)
304
- continue
305
-
306
- except XenfraAPIError as e:
307
- console.print(f"[bold red]API Error: {e.detail}[/bold red]")
308
- except XenfraError as e:
309
- console.print(f"[bold red]Error: {e}[/bold red]")
310
- except click.Abort:
311
- pass
312
- except Exception as e:
313
- console.print(f"[bold red]Unexpected error: {e}[/bold red]")
314
-
315
-
316
- @click.command()
317
- @click.argument("deployment-id")
318
- @click.option("--follow", "-f", is_flag=True, help="Follow log output (stream)")
319
- @click.option("--tail", type=int, help="Show last N lines")
320
- def logs(deployment_id, follow, tail):
321
- # Validate deployment ID
322
- is_valid, error_msg = validate_deployment_id(deployment_id)
323
- if not is_valid:
324
- console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
325
- raise click.Abort()
326
- """
327
- Stream deployment logs.
328
-
329
- Shows logs for a specific deployment. Use --follow to stream logs in real-time.
330
- """
331
- try:
332
- with get_client() as client:
333
- console.print(f"[cyan]Fetching logs for deployment {deployment_id}...[/cyan]")
334
-
335
- log_content = client.deployments.get_logs(deployment_id)
336
-
337
- if not log_content:
338
- console.print("[yellow]No logs available yet.[/yellow]")
339
- console.print("[dim]The deployment may still be starting up.[/dim]")
340
- return
341
-
342
- # Process logs
343
- log_lines = log_content.strip().split("\n")
344
-
345
- # Apply tail if specified
346
- if tail:
347
- log_lines = log_lines[-tail:]
348
-
349
- # Display logs with syntax highlighting
350
- console.print(f"\n[bold]Logs for deployment {deployment_id}:[/bold]\n")
351
-
352
- if follow:
353
- console.print(
354
- "[yellow]Note: --follow flag not yet implemented (showing static logs)[/yellow]\n"
355
- )
356
-
357
- # Display logs
358
- for line in log_lines:
359
- # Color-code based on log level
360
- if "ERROR" in line or "FAILED" in line:
361
- console.print(f"[red]{line}[/red]")
362
- elif "WARN" in line or "WARNING" in line:
363
- console.print(f"[yellow]{line}[/yellow]")
364
- elif "SUCCESS" in line or "COMPLETED" in line:
365
- console.print(f"[green]{line}[/green]")
366
- elif "INFO" in line:
367
- console.print(f"[cyan]{line}[/cyan]")
368
- else:
369
- console.print(line)
370
-
371
- except XenfraAPIError as e:
372
- console.print(f"[bold red]API Error: {e.detail}[/bold red]")
373
- except XenfraError as e:
374
- console.print(f"[bold red]Error: {e}[/bold red]")
375
- except click.Abort:
376
- pass
377
-
378
-
379
- @click.command()
380
- @click.argument("deployment-id", required=False)
381
- @click.option("--watch", "-w", is_flag=True, help="Watch status updates")
382
- def status(deployment_id, watch):
383
- """
384
- Show deployment status.
385
-
386
- Displays current status, progress, and details for a deployment.
387
- Use --watch to monitor status in real-time.
388
- """
389
- try:
390
- if not deployment_id:
391
- console.print("[yellow]No deployment ID provided.[/yellow]")
392
- console.print("[dim]Usage: xenfra status <deployment-id>[/dim]")
393
- return
394
-
395
- # Validate deployment ID
396
- is_valid, error_msg = validate_deployment_id(deployment_id)
397
- if not is_valid:
398
- console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
399
- raise click.Abort()
400
-
401
- with get_client() as client:
402
- console.print(f"[cyan]Fetching status for deployment {deployment_id}...[/cyan]")
403
-
404
- deployment_status = client.deployments.get_status(deployment_id)
405
-
406
- if watch:
407
- console.print(
408
- "[yellow]Note: --watch flag not yet implemented (showing current status)[/yellow]\n"
409
- )
410
-
411
- # Display status
412
- status_value = deployment_status.get("status", "UNKNOWN")
413
- state = deployment_status.get("state", "unknown")
414
- progress = deployment_status.get("progress", 0)
415
-
416
- # Status panel
417
- status_color = {
418
- "PENDING": "yellow",
419
- "IN_PROGRESS": "cyan",
420
- "SUCCESS": "green",
421
- "FAILED": "red",
422
- "CANCELLED": "dim",
423
- }.get(status_value, "white")
424
-
425
- # Create status table
426
- table = Table(show_header=False, box=None)
427
- table.add_column("Property", style="cyan")
428
- table.add_column("Value")
429
-
430
- table.add_row("Deployment ID", str(deployment_id))
431
- table.add_row("Status", f"[{status_color}]{status_value}[/{status_color}]")
432
- table.add_row("State", state)
433
-
434
- if progress > 0:
435
- table.add_row("Progress", f"{progress}%")
436
-
437
- if "project_name" in deployment_status:
438
- table.add_row("Project", deployment_status["project_name"])
439
-
440
- if "created_at" in deployment_status:
441
- table.add_row("Created", deployment_status["created_at"])
442
-
443
- if "finished_at" in deployment_status:
444
- table.add_row("Finished", deployment_status["finished_at"])
445
-
446
- if "url" in deployment_status:
447
- table.add_row("URL", f"[link]{deployment_status['url']}[/link]")
448
-
449
- if "ip_address" in deployment_status:
450
- table.add_row("IP Address", deployment_status["ip_address"])
451
-
452
- panel = Panel(table, title="[bold]Deployment Status[/bold]", border_style=status_color)
453
- console.print(panel)
454
-
455
- # Show error if failed
456
- if status_value == "FAILED" and "error" in deployment_status:
457
- error_panel = Panel(
458
- deployment_status["error"],
459
- title="[bold red]Error[/bold red]",
460
- border_style="red",
461
- )
462
- console.print("\n", error_panel)
463
-
464
- console.print("\n[bold]Troubleshooting:[/bold]")
465
- console.print(f" • View logs: [cyan]xenfra logs {deployment_id}[/cyan]")
466
- console.print(f" • Diagnose: [cyan]xenfra diagnose {deployment_id}[/cyan]")
467
-
468
- # Show next steps based on status
469
- elif status_value == "SUCCESS":
470
- console.print("\n[bold green]Deployment successful! 🎉[/bold green]")
471
- if "url" in deployment_status:
472
- console.print(f" • Visit: [link]{deployment_status['url']}[/link]")
473
-
474
- elif status_value in ["PENDING", "IN_PROGRESS"]:
475
- console.print("\n[bold]Deployment in progress...[/bold]")
476
- console.print(f" • View logs: [cyan]xenfra logs {deployment_id}[/cyan]")
477
- console.print(f" • Check again: [cyan]xenfra status {deployment_id}[/cyan]")
478
-
479
- except XenfraAPIError as e:
480
- console.print(f"[bold red]API Error: {e.detail}[/bold red]")
481
- except XenfraError as e:
482
- console.print(f"[bold red]Error: {e}[/bold red]")
483
- except click.Abort:
484
- pass
1
+ """
2
+ Deployment commands for Xenfra CLI.
3
+ """
4
+
5
+ import os
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+ from xenfra_sdk import XenfraClient
12
+ from xenfra_sdk.exceptions import XenfraAPIError, XenfraError
13
+ from xenfra_sdk.privacy import scrub_logs
14
+
15
+ from ..utils.auth import API_BASE_URL, get_auth_token
16
+ from ..utils.codebase import has_xenfra_config
17
+ from ..utils.config import apply_patch
18
+ from ..utils.validation import (
19
+ validate_branch_name,
20
+ validate_deployment_id,
21
+ validate_framework,
22
+ validate_git_repo_url,
23
+ validate_project_name,
24
+ )
25
+
26
+ console = Console()
27
+
28
+ # Maximum number of retry attempts for auto-healing
29
+ MAX_RETRY_ATTEMPTS = 3
30
+
31
+
32
+ def get_client() -> XenfraClient:
33
+ """Get authenticated SDK client."""
34
+ token = get_auth_token()
35
+ if not token:
36
+ console.print("[bold red]Not logged in. Run 'xenfra auth login' first.[/bold red]")
37
+ raise click.Abort()
38
+
39
+ return XenfraClient(token=token, api_url=API_BASE_URL)
40
+
41
+
42
+ def show_diagnosis_panel(diagnosis: str, suggestion: str):
43
+ """Display diagnosis and suggestion in formatted panels."""
44
+ console.print()
45
+ console.print(Panel(diagnosis, title="[bold red]🔍 Diagnosis[/bold red]", border_style="red"))
46
+ console.print()
47
+ console.print(
48
+ Panel(suggestion, title="[bold yellow]💡 Suggestion[/bold yellow]", border_style="yellow")
49
+ )
50
+
51
+
52
+ def show_patch_preview(patch_data: dict):
53
+ """Show a preview of the patch that will be applied."""
54
+ console.print()
55
+ console.print("[bold green]🔧 Automatic Fix Available[/bold green]")
56
+ console.print(f" [cyan]File:[/cyan] {patch_data.get('file')}")
57
+ console.print(f" [cyan]Operation:[/cyan] {patch_data.get('operation')}")
58
+ console.print(f" [cyan]Value:[/cyan] {patch_data.get('value')}")
59
+ console.print()
60
+
61
+
62
+ def zen_nod_workflow(logs: str, client: XenfraClient, attempt: int) -> bool:
63
+ """
64
+ Execute the Zen Nod auto-healing workflow.
65
+
66
+ Args:
67
+ logs: Deployment error logs
68
+ client: Authenticated SDK client
69
+ attempt: Current attempt number
70
+
71
+ Returns:
72
+ True if patch was applied and user wants to retry, False otherwise
73
+ """
74
+ console.print()
75
+ console.print(f"[cyan]🤖 Analyzing failure (attempt {attempt}/{MAX_RETRY_ATTEMPTS})...[/cyan]")
76
+
77
+ # Scrub sensitive data from logs
78
+ scrubbed_logs = scrub_logs(logs)
79
+
80
+ # Diagnose with AI
81
+ try:
82
+ diagnosis_result = client.intelligence.diagnose(scrubbed_logs)
83
+ except Exception as e:
84
+ console.print(f"[yellow]Could not diagnose failure: {e}[/yellow]")
85
+ return False
86
+
87
+ # Show diagnosis
88
+ show_diagnosis_panel(diagnosis_result.diagnosis, diagnosis_result.suggestion)
89
+
90
+ # Check if there's an automatic patch
91
+ if diagnosis_result.patch and diagnosis_result.patch.file:
92
+ show_patch_preview(diagnosis_result.patch.model_dump())
93
+
94
+ # Zen Nod confirmation
95
+ if click.confirm("Apply this fix and retry deployment?", default=True):
96
+ try:
97
+ # Apply patch (with automatic backup)
98
+ backup_path = apply_patch(diagnosis_result.patch.model_dump())
99
+ console.print("[bold green]✓ Patch applied[/bold green]")
100
+ if backup_path:
101
+ console.print(f"[dim]Backup saved: {backup_path}[/dim]")
102
+ return True # Signal to retry
103
+ except Exception as e:
104
+ console.print(f"[bold red]Failed to apply patch: {e}[/bold red]")
105
+ return False
106
+ else:
107
+ console.print()
108
+ console.print("[yellow]❌ Patch declined. Follow the manual steps above.[/yellow]")
109
+ return False
110
+ else:
111
+ console.print()
112
+ console.print(
113
+ "[yellow]No automatic fix available. Please follow the manual steps above.[/yellow]"
114
+ )
115
+ return False
116
+
117
+
118
+ @click.command()
119
+ @click.option("--project-name", help="Project name (defaults to current directory name)")
120
+ @click.option("--git-repo", help="Git repository URL (if deploying from git)")
121
+ @click.option("--branch", default="main", help="Git branch (default: main)")
122
+ @click.option("--framework", help="Framework override (fastapi, flask, django)")
123
+ @click.option("--no-heal", is_flag=True, help="Disable auto-healing on failure")
124
+ def deploy(project_name, git_repo, branch, framework, no_heal):
125
+ """
126
+ Deploy current project to DigitalOcean with auto-healing.
127
+
128
+ Deploys your application with zero configuration. The CLI will:
129
+ 1. Check for xenfra.yaml (or run init if missing)
130
+ 2. Create a deployment
131
+ 3. Auto-diagnose and fix failures (unless --no-heal is set or XENFRA_NO_AI=1)
132
+
133
+ Set XENFRA_NO_AI=1 environment variable to disable all AI features.
134
+ """
135
+ # Check XENFRA_NO_AI environment variable
136
+ no_ai = os.environ.get("XENFRA_NO_AI", "0") == "1"
137
+ if no_ai:
138
+ console.print("[yellow]XENFRA_NO_AI is set. Auto-healing disabled.[/yellow]")
139
+ no_heal = True
140
+
141
+ # Check for xenfra.yaml
142
+ if not has_xenfra_config():
143
+ console.print("[yellow]No xenfra.yaml found.[/yellow]")
144
+ if click.confirm("Run 'xenfra init' to create configuration?", default=True):
145
+ from .intelligence import init
146
+
147
+ ctx = click.get_current_context()
148
+ ctx.invoke(init, manual=no_ai, accept_all=False)
149
+ else:
150
+ console.print("[dim]Deployment cancelled.[/dim]")
151
+ return
152
+
153
+ # Default project name to current directory
154
+ if not project_name:
155
+ project_name = os.path.basename(os.getcwd())
156
+
157
+ # Validate project name
158
+ is_valid, error_msg = validate_project_name(project_name)
159
+ if not is_valid:
160
+ console.print(f"[bold red]Invalid project name: {error_msg}[/bold red]")
161
+ raise click.Abort()
162
+
163
+ # Validate git repo if provided
164
+ if git_repo:
165
+ is_valid, error_msg = validate_git_repo_url(git_repo)
166
+ if not is_valid:
167
+ console.print(f"[bold red]Invalid git repository URL: {error_msg}[/bold red]")
168
+ raise click.Abort()
169
+ console.print(f"[cyan]Deploying {project_name} from git repository...[/cyan]")
170
+ else:
171
+ 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
177
+
178
+ # Validate branch name
179
+ is_valid, error_msg = validate_branch_name(branch)
180
+ if not is_valid:
181
+ console.print(f"[bold red]Invalid branch name: {error_msg}[/bold red]")
182
+ raise click.Abort()
183
+
184
+ # Validate framework if provided
185
+ if framework:
186
+ is_valid, error_msg = validate_framework(framework)
187
+ if not is_valid:
188
+ console.print(f"[bold red]Invalid framework: {error_msg}[/bold red]")
189
+ raise click.Abort()
190
+
191
+ # Retry loop for auto-healing
192
+ attempt = 0
193
+ deployment_id = None
194
+
195
+ try:
196
+ with get_client() as client:
197
+ while attempt < MAX_RETRY_ATTEMPTS:
198
+ # Safety check to prevent infinite loops
199
+ if attempt > MAX_RETRY_ATTEMPTS:
200
+ raise RuntimeError("Safety break: Retry loop exceeded MAX_RETRY_ATTEMPTS.")
201
+
202
+ attempt += 1
203
+
204
+ if attempt > 1:
205
+ console.print(
206
+ f"\n[cyan]🔄 Retrying deployment (attempt {attempt}/{MAX_RETRY_ATTEMPTS})...[/cyan]"
207
+ )
208
+ else:
209
+ console.print("[cyan]Creating deployment...[/cyan]")
210
+
211
+ # Detect framework if not provided
212
+ if not framework:
213
+ console.print("[dim]Auto-detecting framework...[/dim]")
214
+ framework = "fastapi" # Default for now
215
+
216
+ # Create deployment
217
+ try:
218
+ deployment = client.deployments.create(
219
+ project_name=project_name,
220
+ git_repo=git_repo,
221
+ branch=branch,
222
+ framework=framework,
223
+ )
224
+
225
+ deployment_id = deployment["deployment_id"]
226
+ console.print(
227
+ f"[bold green]✓[/bold green] Deployment created: [cyan]{deployment_id}[/cyan]"
228
+ )
229
+
230
+ # Show deployment details
231
+ details_table = Table(show_header=False, box=None)
232
+ details_table.add_column("Property", style="cyan")
233
+ details_table.add_column("Value", style="white")
234
+
235
+ details_table.add_row("Deployment ID", str(deployment_id))
236
+ details_table.add_row("Project", project_name)
237
+ if git_repo:
238
+ details_table.add_row("Repository", git_repo)
239
+ details_table.add_row("Branch", branch)
240
+ details_table.add_row("Framework", framework)
241
+ details_table.add_row("Status", deployment.get("status", "PENDING"))
242
+
243
+ panel = Panel(
244
+ details_table,
245
+ title="[bold green]Deployment Started[/bold green]",
246
+ border_style="green",
247
+ )
248
+ console.print(panel)
249
+
250
+ # Show next steps
251
+ console.print("\n[bold]Next steps:[/bold]")
252
+ console.print(f" • Monitor status: [cyan]xenfra status {deployment_id}[/cyan]")
253
+ console.print(f" • View logs: [cyan]xenfra logs {deployment_id}[/cyan]")
254
+ if not no_heal:
255
+ console.print(
256
+ f" • Diagnose issues: [cyan]xenfra diagnose {deployment_id}[/cyan]"
257
+ )
258
+
259
+ # Success - break out of retry loop
260
+ break
261
+
262
+ except XenfraAPIError as e:
263
+ # Deployment failed
264
+ console.print(f"[bold red]✗ Deployment failed: {e.detail}[/bold red]")
265
+
266
+ # Check if we should auto-heal
267
+ if no_heal or attempt >= MAX_RETRY_ATTEMPTS:
268
+ # No auto-healing or max retries reached
269
+ if attempt >= MAX_RETRY_ATTEMPTS:
270
+ console.print(
271
+ f"\n[bold red]❌ Maximum retry attempts ({MAX_RETRY_ATTEMPTS}) reached.[/bold red]"
272
+ )
273
+ console.print(
274
+ "[yellow]Unable to auto-fix the issue. Please review the errors above.[/yellow]"
275
+ )
276
+ raise
277
+ else:
278
+ # Try to get logs for diagnosis
279
+ error_logs = str(e.detail)
280
+ try:
281
+ if deployment_id:
282
+ # This should be a method in the SDK that returns a string
283
+ logs_response = client.deployments.get_logs(deployment_id)
284
+ if isinstance(logs_response, dict):
285
+ error_logs = logs_response.get("logs", str(e.detail))
286
+ else:
287
+ error_logs = str(logs_response) # Assuming it can be a string
288
+ except Exception as log_err:
289
+ console.print(
290
+ f"[yellow]Warning: Could not fetch detailed logs for diagnosis: {log_err}[/yellow]"
291
+ )
292
+ # Fallback to the initial error detail
293
+ pass
294
+
295
+ # Run Zen Nod workflow
296
+ should_retry = zen_nod_workflow(error_logs, client, attempt)
297
+
298
+ if not should_retry:
299
+ # User declined patch or no patch available
300
+ console.print("\n[dim]Deployment cancelled.[/dim]")
301
+ raise click.Abort()
302
+
303
+ # Continue to next iteration (retry)
304
+ continue
305
+
306
+ except XenfraAPIError as e:
307
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
308
+ except XenfraError as e:
309
+ console.print(f"[bold red]Error: {e}[/bold red]")
310
+ except click.Abort:
311
+ pass
312
+ except Exception as e:
313
+ console.print(f"[bold red]Unexpected error: {e}[/bold red]")
314
+
315
+
316
+ @click.command()
317
+ @click.argument("deployment-id")
318
+ @click.option("--follow", "-f", is_flag=True, help="Follow log output (stream)")
319
+ @click.option("--tail", type=int, help="Show last N lines")
320
+ def logs(deployment_id, follow, tail):
321
+ # Validate deployment ID
322
+ is_valid, error_msg = validate_deployment_id(deployment_id)
323
+ if not is_valid:
324
+ console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
325
+ raise click.Abort()
326
+ """
327
+ Stream deployment logs.
328
+
329
+ Shows logs for a specific deployment. Use --follow to stream logs in real-time.
330
+ """
331
+ try:
332
+ with get_client() as client:
333
+ console.print(f"[cyan]Fetching logs for deployment {deployment_id}...[/cyan]")
334
+
335
+ log_content = client.deployments.get_logs(deployment_id)
336
+
337
+ if not log_content:
338
+ console.print("[yellow]No logs available yet.[/yellow]")
339
+ console.print("[dim]The deployment may still be starting up.[/dim]")
340
+ return
341
+
342
+ # Process logs
343
+ log_lines = log_content.strip().split("\n")
344
+
345
+ # Apply tail if specified
346
+ if tail:
347
+ log_lines = log_lines[-tail:]
348
+
349
+ # Display logs with syntax highlighting
350
+ console.print(f"\n[bold]Logs for deployment {deployment_id}:[/bold]\n")
351
+
352
+ if follow:
353
+ console.print(
354
+ "[yellow]Note: --follow flag not yet implemented (showing static logs)[/yellow]\n"
355
+ )
356
+
357
+ # Display logs
358
+ for line in log_lines:
359
+ # Color-code based on log level
360
+ if "ERROR" in line or "FAILED" in line:
361
+ console.print(f"[red]{line}[/red]")
362
+ elif "WARN" in line or "WARNING" in line:
363
+ console.print(f"[yellow]{line}[/yellow]")
364
+ elif "SUCCESS" in line or "COMPLETED" in line:
365
+ console.print(f"[green]{line}[/green]")
366
+ elif "INFO" in line:
367
+ console.print(f"[cyan]{line}[/cyan]")
368
+ else:
369
+ console.print(line)
370
+
371
+ except XenfraAPIError as e:
372
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
373
+ except XenfraError as e:
374
+ console.print(f"[bold red]Error: {e}[/bold red]")
375
+ except click.Abort:
376
+ pass
377
+
378
+
379
+ @click.command()
380
+ @click.argument("deployment-id", required=False)
381
+ @click.option("--watch", "-w", is_flag=True, help="Watch status updates")
382
+ def status(deployment_id, watch):
383
+ """
384
+ Show deployment status.
385
+
386
+ Displays current status, progress, and details for a deployment.
387
+ Use --watch to monitor status in real-time.
388
+ """
389
+ try:
390
+ if not deployment_id:
391
+ console.print("[yellow]No deployment ID provided.[/yellow]")
392
+ console.print("[dim]Usage: xenfra status <deployment-id>[/dim]")
393
+ return
394
+
395
+ # Validate deployment ID
396
+ is_valid, error_msg = validate_deployment_id(deployment_id)
397
+ if not is_valid:
398
+ console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
399
+ raise click.Abort()
400
+
401
+ with get_client() as client:
402
+ console.print(f"[cyan]Fetching status for deployment {deployment_id}...[/cyan]")
403
+
404
+ deployment_status = client.deployments.get_status(deployment_id)
405
+
406
+ if watch:
407
+ console.print(
408
+ "[yellow]Note: --watch flag not yet implemented (showing current status)[/yellow]\n"
409
+ )
410
+
411
+ # Display status
412
+ status_value = deployment_status.get("status", "UNKNOWN")
413
+ state = deployment_status.get("state", "unknown")
414
+ progress = deployment_status.get("progress", 0)
415
+
416
+ # Status panel
417
+ status_color = {
418
+ "PENDING": "yellow",
419
+ "IN_PROGRESS": "cyan",
420
+ "SUCCESS": "green",
421
+ "FAILED": "red",
422
+ "CANCELLED": "dim",
423
+ }.get(status_value, "white")
424
+
425
+ # Create status table
426
+ table = Table(show_header=False, box=None)
427
+ table.add_column("Property", style="cyan")
428
+ table.add_column("Value")
429
+
430
+ table.add_row("Deployment ID", str(deployment_id))
431
+ table.add_row("Status", f"[{status_color}]{status_value}[/{status_color}]")
432
+ table.add_row("State", state)
433
+
434
+ if progress > 0:
435
+ table.add_row("Progress", f"{progress}%")
436
+
437
+ if "project_name" in deployment_status:
438
+ table.add_row("Project", deployment_status["project_name"])
439
+
440
+ if "created_at" in deployment_status:
441
+ table.add_row("Created", deployment_status["created_at"])
442
+
443
+ if "finished_at" in deployment_status:
444
+ table.add_row("Finished", deployment_status["finished_at"])
445
+
446
+ if "url" in deployment_status:
447
+ table.add_row("URL", f"[link]{deployment_status['url']}[/link]")
448
+
449
+ if "ip_address" in deployment_status:
450
+ table.add_row("IP Address", deployment_status["ip_address"])
451
+
452
+ panel = Panel(table, title="[bold]Deployment Status[/bold]", border_style=status_color)
453
+ console.print(panel)
454
+
455
+ # Show error if failed
456
+ if status_value == "FAILED" and "error" in deployment_status:
457
+ error_panel = Panel(
458
+ deployment_status["error"],
459
+ title="[bold red]Error[/bold red]",
460
+ border_style="red",
461
+ )
462
+ console.print("\n", error_panel)
463
+
464
+ console.print("\n[bold]Troubleshooting:[/bold]")
465
+ console.print(f" • View logs: [cyan]xenfra logs {deployment_id}[/cyan]")
466
+ console.print(f" • Diagnose: [cyan]xenfra diagnose {deployment_id}[/cyan]")
467
+
468
+ # Show next steps based on status
469
+ elif status_value == "SUCCESS":
470
+ console.print("\n[bold green]Deployment successful! 🎉[/bold green]")
471
+ if "url" in deployment_status:
472
+ console.print(f" • Visit: [link]{deployment_status['url']}[/link]")
473
+
474
+ elif status_value in ["PENDING", "IN_PROGRESS"]:
475
+ console.print("\n[bold]Deployment in progress...[/bold]")
476
+ console.print(f" • View logs: [cyan]xenfra logs {deployment_id}[/cyan]")
477
+ console.print(f" • Check again: [cyan]xenfra status {deployment_id}[/cyan]")
478
+
479
+ except XenfraAPIError as e:
480
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
481
+ except XenfraError as e:
482
+ console.print(f"[bold red]Error: {e}[/bold red]")
483
+ except click.Abort:
484
+ pass
485
+
486
+
487
+ @click.command()
488
+ @click.argument("deployment-id")
489
+ @click.option("--format", "output_format", type=click.Choice(["detailed", "summary"], case_sensitive=False), default="detailed", help="Report format (detailed or summary)")
490
+ def report(deployment_id, output_format):
491
+ """
492
+ Generate deployment report with self-healing events.
493
+
494
+ Shows comprehensive deployment information including:
495
+ - Deployment status and timeline
496
+ - Self-healing attempts and outcomes
497
+ - Patches applied during healing
498
+ - Statistics and metrics
499
+ """
500
+ try:
501
+ # Validate deployment ID
502
+ is_valid, error_msg = validate_deployment_id(deployment_id)
503
+ if not is_valid:
504
+ console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
505
+ raise click.Abort()
506
+
507
+ with get_client() as client:
508
+ console.print(f"[cyan]Generating report for deployment {deployment_id}...[/cyan]\n")
509
+
510
+ # Get deployment status
511
+ try:
512
+ deployment_status = client.deployments.get_status(deployment_id)
513
+ except XenfraAPIError as e:
514
+ console.print(f"[bold red]Error fetching deployment status: {e.detail}[/bold red]")
515
+ raise click.Abort()
516
+
517
+ # Get deployment logs
518
+ try:
519
+ logs = client.deployments.get_logs(deployment_id)
520
+ except XenfraAPIError:
521
+ logs = None
522
+
523
+ # Parse status
524
+ status_value = deployment_status.get("status", "UNKNOWN")
525
+ state = deployment_status.get("state", "unknown")
526
+ progress = deployment_status.get("progress", 0)
527
+
528
+ # Status color mapping
529
+ status_color = {
530
+ "PENDING": "yellow",
531
+ "IN_PROGRESS": "cyan",
532
+ "SUCCESS": "green",
533
+ "FAILED": "red",
534
+ "CANCELLED": "dim",
535
+ }.get(status_value, "white")
536
+
537
+ # Calculate statistics from logs
538
+ heal_attempts = logs.count("🤖 Analyzing failure") if logs else 0
539
+ patches_applied = logs.count("✓ Patch applied") if logs else 0
540
+ diagnoses = logs.count("🔍 Diagnosis") if logs else 0
541
+
542
+ # Create main report table
543
+ report_table = Table(show_header=True, box=None)
544
+ report_table.add_column("Property", style="cyan", width=25)
545
+ report_table.add_column("Value", style="white")
546
+
547
+ report_table.add_row("Deployment ID", str(deployment_id))
548
+ report_table.add_row("Status", f"[{status_color}]{status_value}[/{status_color}]")
549
+ report_table.add_row("State", state)
550
+
551
+ if progress > 0:
552
+ report_table.add_row("Progress", f"{progress}%")
553
+
554
+ if "project_name" in deployment_status:
555
+ report_table.add_row("Project", deployment_status["project_name"])
556
+
557
+ if "created_at" in deployment_status:
558
+ report_table.add_row("Created", deployment_status["created_at"])
559
+
560
+ if "finished_at" in deployment_status:
561
+ report_table.add_row("Finished", deployment_status["finished_at"])
562
+
563
+ if "url" in deployment_status:
564
+ report_table.add_row("URL", f"[link]{deployment_status['url']}[/link]")
565
+
566
+ if "ip_address" in deployment_status:
567
+ report_table.add_row("IP Address", deployment_status["ip_address"])
568
+
569
+ # Self-healing statistics
570
+ report_table.add_row("", "") # Separator
571
+ report_table.add_row("[bold]Self-Healing Stats[/bold]", "")
572
+ report_table.add_row("Healing Attempts", str(heal_attempts))
573
+ report_table.add_row("Patches Applied", str(patches_applied))
574
+ report_table.add_row("Diagnoses Performed", str(diagnoses))
575
+
576
+ if heal_attempts > 0:
577
+ success_rate = (patches_applied / heal_attempts * 100) if heal_attempts > 0 else 0
578
+ report_table.add_row("Healing Success Rate", f"{success_rate:.1f}%")
579
+
580
+ # Display main report
581
+ console.print(Panel(report_table, title="[bold]Deployment Report[/bold]", border_style=status_color))
582
+
583
+ # Detailed format includes timeline and healing events
584
+ if output_format == "detailed" and logs:
585
+ console.print("\n[bold]Self-Healing Timeline[/bold]\n")
586
+
587
+ # Extract healing events from logs
588
+ log_lines = logs.split("\n")
589
+ timeline_entries = []
590
+
591
+ for i, line in enumerate(log_lines):
592
+ if "🤖 Analyzing failure" in line:
593
+ attempt_match = None
594
+ # Try to find attempt number in surrounding lines
595
+ for j in range(max(0, i-5), min(len(log_lines), i+10)):
596
+ if "attempt" in log_lines[j].lower():
597
+ timeline_entries.append(("Healing Attempt", log_lines[j].strip()))
598
+ break
599
+ elif "🔍 Diagnosis" in line or "Diagnosis" in line:
600
+ # Extract diagnosis text from next few lines
601
+ diagnosis_text = line.strip()
602
+ if i+1 < len(log_lines) and log_lines[i+1].strip():
603
+ diagnosis_text += "\n " + log_lines[i+1].strip()[:100]
604
+ timeline_entries.append(("Diagnosis", diagnosis_text))
605
+ elif "✓ Patch applied" in line or "Patch applied" in line:
606
+ timeline_entries.append(("Patch Applied", line.strip()))
607
+ elif "🔄 Retrying deployment" in line:
608
+ timeline_entries.append(("Retry", line.strip()))
609
+
610
+ if timeline_entries:
611
+ timeline_table = Table(show_header=True, box=None)
612
+ timeline_table.add_column("Event", style="cyan", width=20)
613
+ timeline_table.add_column("Details", style="white")
614
+
615
+ for event_type, details in timeline_entries[:20]: # Limit to 20 entries
616
+ timeline_table.add_row(event_type, details)
617
+
618
+ console.print(timeline_table)
619
+ else:
620
+ console.print("[dim]No self-healing events detected in logs.[/dim]")
621
+
622
+ # Show error if failed
623
+ if status_value == "FAILED":
624
+ console.print("\n[bold red]⚠ Deployment Failed[/bold red]")
625
+ if "error" in deployment_status:
626
+ error_panel = Panel(
627
+ deployment_status["error"],
628
+ title="[bold red]Error Details[/bold red]",
629
+ border_style="red",
630
+ )
631
+ console.print("\n", error_panel)
632
+
633
+ console.print("\n[bold]Troubleshooting:[/bold]")
634
+ console.print(f" • View logs: [cyan]xenfra logs {deployment_id}[/cyan]")
635
+ console.print(f" • Diagnose: [cyan]xenfra diagnose {deployment_id}[/cyan]")
636
+ console.print(f" • View status: [cyan]xenfra status {deployment_id}[/cyan]")
637
+
638
+ # Success summary
639
+ elif status_value == "SUCCESS":
640
+ console.print("\n[bold green]✓ Deployment Successful[/bold green]")
641
+ if heal_attempts > 0:
642
+ console.print(f"[dim]Deployment succeeded after {heal_attempts} self-healing attempt(s).[/dim]")
643
+ if "url" in deployment_status:
644
+ console.print(f" • Visit: [link]{deployment_status['url']}[/link]")
645
+
646
+ # Export summary format (JSON-like structure for programmatic use)
647
+ if output_format == "summary":
648
+ console.print("\n[bold]Summary Format:[/bold]")
649
+ import json
650
+ summary = {
651
+ "deployment_id": deployment_id,
652
+ "status": status_value,
653
+ "healing_attempts": heal_attempts,
654
+ "patches_applied": patches_applied,
655
+ "success": status_value == "SUCCESS",
656
+ }
657
+ console.print(f"[dim]{json.dumps(summary, indent=2)}[/dim]")
658
+
659
+ except XenfraAPIError as e:
660
+ console.print(f"[bold red]API Error: {e.detail}[/bold red]")
661
+ except XenfraError as e:
662
+ console.print(f"[bold red]Error: {e}[/bold red]")
663
+ except click.Abort:
664
+ pass
665
+ except Exception as e:
666
+ console.print(f"[bold red]Unexpected error: {e}[/bold red]")