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.
- xenfra/commands/__init__.py +3 -3
- xenfra/commands/auth.py +144 -144
- xenfra/commands/auth_device.py +164 -164
- xenfra/commands/deployments.py +1133 -912
- xenfra/commands/intelligence.py +503 -412
- xenfra/commands/projects.py +204 -204
- xenfra/commands/security_cmd.py +233 -233
- xenfra/main.py +76 -75
- xenfra/utils/__init__.py +3 -3
- xenfra/utils/auth.py +374 -374
- xenfra/utils/codebase.py +169 -169
- xenfra/utils/config.py +459 -432
- xenfra/utils/errors.py +116 -116
- xenfra/utils/file_sync.py +286 -0
- xenfra/utils/security.py +336 -336
- xenfra/utils/validation.py +234 -234
- xenfra-0.4.4.dist-info/METADATA +113 -0
- xenfra-0.4.4.dist-info/RECORD +21 -0
- {xenfra-0.4.2.dist-info → xenfra-0.4.4.dist-info}/WHEEL +2 -2
- xenfra-0.4.2.dist-info/METADATA +0 -118
- xenfra-0.4.2.dist-info/RECORD +0 -20
- {xenfra-0.4.2.dist-info → xenfra-0.4.4.dist-info}/entry_points.txt +0 -0
xenfra/commands/intelligence.py
CHANGED
|
@@ -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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
console.print(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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]")
|