xenfra-sdk 0.2.5__py3-none-any.whl → 0.2.7__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.
Files changed (42) hide show
  1. xenfra_sdk/__init__.py +46 -2
  2. xenfra_sdk/blueprints/base.py +150 -0
  3. xenfra_sdk/blueprints/factory.py +99 -0
  4. xenfra_sdk/blueprints/node.py +219 -0
  5. xenfra_sdk/blueprints/python.py +57 -0
  6. xenfra_sdk/blueprints/railpack.py +99 -0
  7. xenfra_sdk/blueprints/schema.py +70 -0
  8. xenfra_sdk/cli/main.py +175 -49
  9. xenfra_sdk/client.py +6 -2
  10. xenfra_sdk/constants.py +26 -0
  11. xenfra_sdk/db/session.py +8 -3
  12. xenfra_sdk/detection.py +262 -191
  13. xenfra_sdk/dockerizer.py +76 -120
  14. xenfra_sdk/engine.py +767 -172
  15. xenfra_sdk/events.py +254 -0
  16. xenfra_sdk/exceptions.py +9 -0
  17. xenfra_sdk/governance.py +150 -0
  18. xenfra_sdk/manifest.py +93 -138
  19. xenfra_sdk/mcp_client.py +7 -5
  20. xenfra_sdk/{models.py → models/__init__.py} +17 -1
  21. xenfra_sdk/models/context.py +61 -0
  22. xenfra_sdk/orchestrator.py +223 -99
  23. xenfra_sdk/privacy.py +11 -0
  24. xenfra_sdk/protocol.py +38 -0
  25. xenfra_sdk/railpack_adapter.py +357 -0
  26. xenfra_sdk/railpack_detector.py +587 -0
  27. xenfra_sdk/railpack_manager.py +312 -0
  28. xenfra_sdk/recipes.py +152 -19
  29. xenfra_sdk/resources/activity.py +45 -0
  30. xenfra_sdk/resources/build.py +157 -0
  31. xenfra_sdk/resources/deployments.py +22 -2
  32. xenfra_sdk/resources/intelligence.py +25 -0
  33. xenfra_sdk-0.2.7.dist-info/METADATA +118 -0
  34. xenfra_sdk-0.2.7.dist-info/RECORD +49 -0
  35. {xenfra_sdk-0.2.5.dist-info → xenfra_sdk-0.2.7.dist-info}/WHEEL +1 -1
  36. xenfra_sdk/templates/Caddyfile.j2 +0 -14
  37. xenfra_sdk/templates/Dockerfile.j2 +0 -41
  38. xenfra_sdk/templates/cloud-init.sh.j2 +0 -90
  39. xenfra_sdk/templates/docker-compose-multi.yml.j2 +0 -29
  40. xenfra_sdk/templates/docker-compose.yml.j2 +0 -30
  41. xenfra_sdk-0.2.5.dist-info/METADATA +0 -116
  42. xenfra_sdk-0.2.5.dist-info/RECORD +0 -38
@@ -0,0 +1,99 @@
1
+ """
2
+ Railpack Blueprint - Auto-detects and builds any language using Railpack.
3
+
4
+ Railpack is Railway's open-source buildpack that replaces manual Dockerfile
5
+ templates with intelligent language detection.
6
+ """
7
+
8
+ import os
9
+ import tempfile
10
+ from typing import Optional
11
+
12
+ from xenfra_sdk.blueprints.base import BaseBlueprint
13
+ from xenfra_sdk.blueprints.schema import DeploymentBlueprintManifest
14
+ from xenfra_sdk.railpack_manager import get_railpack_manager
15
+ from xenfra_sdk.railpack_adapter import RailpackAdapter
16
+
17
+
18
+ class RailpackBlueprint(BaseBlueprint):
19
+ """
20
+ The Universal Blueprint: Uses Railpack for language-agnostic builds.
21
+
22
+ Railpack auto-detects the project type (Python, Node, Go, Rust, etc.)
23
+ and generates optimized container configurations.
24
+ """
25
+
26
+ def generate_manifest(self) -> DeploymentBlueprintManifest:
27
+ """
28
+ Generate deployment manifest using Railpack.
29
+
30
+ 1. Run Railpack prepare on source code
31
+ 2. Convert Railpack plan to Xenfra manifest
32
+ 3. Return standardized assets
33
+
34
+ Raises:
35
+ RuntimeError: If Railpack fails to generate valid plan
36
+ """
37
+ source_path = self._get_source_path()
38
+
39
+ # Get Railpack manager (auto-downloads if needed)
40
+ railpack = get_railpack_manager()
41
+
42
+ # Create temporary plan file
43
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
44
+ plan_path = f.name
45
+
46
+ try:
47
+ # Run Railpack prepare
48
+ result = railpack.run(
49
+ 'prepare', source_path,
50
+ '--plan-out', plan_path,
51
+ cwd=source_path, # CRITICAL: Railpack needs CWD to be project root for proper detection
52
+ capture_output=True,
53
+ text=True,
54
+ check=True
55
+ )
56
+
57
+ # Load the plan
58
+ import json
59
+ with open(plan_path) as f:
60
+ plan = json.load(f)
61
+
62
+ # DEBUG: Log the full plan for transparency
63
+ print(f"[RailpackBlueprint] Full JSON Plan:\n{json.dumps(plan, indent=2)}")
64
+
65
+ # Convert to Xenfra manifest
66
+ adapter = RailpackAdapter(plan, context=self.context)
67
+ manifest = adapter.convert()
68
+
69
+ # Store detected language in context for later use
70
+ detected_lang = adapter.get_detected_language()
71
+ if detected_lang:
72
+ self.context['detected_language'] = detected_lang
73
+
74
+ return manifest
75
+
76
+ except Exception as e:
77
+ raise RuntimeError(f"Railpack failed to generate manifest: {e}")
78
+ finally:
79
+ # Cleanup temp file
80
+ if os.path.exists(plan_path):
81
+ os.unlink(plan_path)
82
+
83
+ def _get_source_path(self) -> str:
84
+ """Get source code path from context."""
85
+ # Try explicit source path or repo path
86
+ source_path = self.context.get('source_path') or self.context.get('repo_path')
87
+ if source_path:
88
+ return source_path
89
+
90
+ # Try file manifest to infer path
91
+ file_manifest = self.context.get('file_manifest', [])
92
+ if file_manifest:
93
+ # Assume first file's directory is project root
94
+ first_file = file_manifest[0].get('path', '')
95
+ if first_file:
96
+ return os.path.dirname(first_file) or '.'
97
+
98
+ # Default to current directory
99
+ return '.'
@@ -0,0 +1,70 @@
1
+ from typing import List, Dict, Optional, Union, Any
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ # --- ZEN GAP FIX: Resource Governance Models ---
6
+ class ResourceLimitsModel(BaseModel):
7
+ """Docker cgroups resource limits (deterministic, no AI)."""
8
+ memory: str = "512m"
9
+ cpus: Union[str, float] = "0.5"
10
+
11
+
12
+ class ResourceReservationsModel(BaseModel):
13
+ """Docker resource reservations."""
14
+ memory: str = "128m"
15
+ cpus: Union[str, float] = "0.25"
16
+
17
+
18
+ class DeployResourcesModel(BaseModel):
19
+ """Docker Compose deploy.resources configuration."""
20
+ limits: ResourceLimitsModel = Field(default_factory=ResourceLimitsModel)
21
+ reservations: ResourceReservationsModel = Field(default_factory=ResourceReservationsModel)
22
+
23
+
24
+ class DeployModel(BaseModel):
25
+ """Docker Compose deploy configuration for resource governance."""
26
+ resources: DeployResourcesModel = Field(default_factory=DeployResourcesModel)
27
+
28
+
29
+ class DockerfileModel(BaseModel):
30
+ """Structured representation of a Dockerfile."""
31
+ base_image: str = Field(..., description="The base image (e.g., python:3.11-slim)")
32
+ env_vars: Dict[str, str] = Field(default_factory=dict, description="Environment variables for the build")
33
+ workdir: str = "/app"
34
+ system_packages: List[str] = Field(default_factory=list, description="APT packages to install")
35
+ copy_dirs: List[str] = Field(default_factory=list, description="Directories to copy into the image")
36
+ run_commands: List[str] = Field(default_factory=list, description="Custom RUN commands")
37
+ args: List[str] = Field(default_factory=list, description="ARG instructions for build-time variables")
38
+ expose_port: Optional[int] = None
39
+ entrypoint: Optional[Union[str, List[str]]] = None
40
+ command: Optional[Union[str, List[str]]] = None
41
+
42
+ class ServiceDetail(BaseModel):
43
+ """Detail for a single docker-compose service."""
44
+ image: Optional[str] = None
45
+ build_context: Optional[Union[str, Dict[str, Any]]] = Field(None, alias="build")
46
+ ports: List[str] = Field(default_factory=list)
47
+ environment: Dict[str, str] = Field(default_factory=dict)
48
+ env_file: List[str] = Field(default_factory=list)
49
+ restart: str = "unless-stopped"
50
+ depends_on: List[str] = Field(default_factory=list)
51
+ volumes: List[str] = Field(default_factory=list)
52
+ command: Optional[Union[str, List[str]]] = None
53
+ # --- ZEN GAP FIX: Resource Governance ---
54
+ deploy: Optional[DeployModel] = Field(default_factory=DeployModel)
55
+ oom_kill_disable: bool = False # Let OOM killer work to protect host
56
+
57
+ class ComposeModel(BaseModel):
58
+ """Structured representation of a docker-compose.yml file."""
59
+ version: str = "3.8"
60
+ services: Dict[str, ServiceDetail]
61
+ volumes: Dict[str, dict] = Field(default_factory=dict)
62
+ networks: Dict[str, dict] = Field(default_factory=dict)
63
+
64
+ class DeploymentBlueprintManifest(BaseModel):
65
+ """The complete set of manifests generated by a blueprint."""
66
+ dockerfile: DockerfileModel
67
+ compose: ComposeModel
68
+ env_file: Dict[str, str] = Field(default_factory=dict)
69
+ caddyfile: Optional[str] = None
70
+ railpack_plan: Optional[str] = None # Raw JSON plan from Railpack CLI
xenfra_sdk/cli/main.py CHANGED
@@ -2,9 +2,12 @@ import click
2
2
  import yaml
3
3
  from rich.console import Console
4
4
  from rich.table import Table
5
+ from rich.panel import Panel
6
+ from rich.tree import Tree
5
7
  from xenfra_sdk import dockerizer
6
8
  from xenfra_sdk.db.session import create_db_and_tables
7
9
  from xenfra_sdk.engine import DeploymentError, InfraEngine
10
+ from xenfra_sdk.manifest import XenfraConfig, InfrastructureService, load_xenfra_config
8
11
 
9
12
  console = Console()
10
13
 
@@ -31,88 +34,211 @@ def main(ctx):
31
34
  @click.pass_context
32
35
  def init(ctx):
33
36
  """Initializes a project by creating a xenfra.yaml configuration file."""
34
- console.print("\n[bold blue]🔎 INITIALIZING PROJECT[/bold blue]")
35
-
36
- framework, _, _ = dockerizer.detect_framework()
37
- if not framework:
38
- console.print("[yellow] Warning: No recognizable web framework detected.[/yellow]")
39
-
40
- console.print(f" - Detected [cyan]{framework or 'unknown'}[/cyan] project.")
37
+ console.print("\n[bold blue]🔎 INITIALIZING PROJECT (UNIFIED BRAIN)[/bold blue]")
38
+
39
+ from xenfra_sdk.detection import auto_detect_services
40
+
41
+ with console.status("[bold green]Analyzing project with Unified Brain...[/bold green]"):
42
+ detected = auto_detect_services(".")
43
+
44
+ if not detected:
45
+ console.print("[yellow] Warning: No services detected in this repository.[/yellow]")
46
+ if not click.confirm(" Do you want to continue with a blank configuration?"):
47
+ return
48
+ services = []
49
+ infrastructure = []
50
+ else:
51
+ # Construct config from detected services
52
+ from xenfra_sdk.manifest import create_services_from_detected
53
+ services = create_services_from_detected(detected)
54
+
55
+ # Simple infrastructure inference (Migrated from discovery.py logic)
56
+ infrastructure = []
57
+ found_infra = set()
58
+ for svc in detected:
59
+ if svc.get("is_infrastructure"):
60
+ found_infra.add(svc["framework"]) # Basic mapping
61
+
62
+ # Display detected services
63
+ table = Table(title="Detected Services", show_header=True, header_style="bold cyan")
64
+ table.add_column("Service Name", style="bold")
65
+ table.add_column("Tier", style="dim")
66
+ table.add_column("Framework", style="green")
67
+ table.add_column("Port", style="magenta")
68
+
69
+ for svc in services:
70
+ table.add_row(svc.name, svc.tier, svc.framework, str(svc.port))
71
+
72
+ console.print(table)
41
73
 
42
- use_db = click.confirm(
43
- "\n Would you like to add a PostgreSQL database to your deployment?", default=False
74
+ # Project Name
75
+ config_name = Path(".").resolve().name
76
+ project_name = click.prompt("\n Project Name", default=config_name)
77
+
78
+ config = XenfraConfig(
79
+ name=project_name,
80
+ services=services,
81
+ infrastructure=infrastructure
44
82
  )
45
83
 
46
- config = {
47
- "name": "xenfra-app",
48
- "digitalocean": {"region": "nyc3", "size": "s-1vcpu-1gb", "image": "ubuntu-22-04-x64"},
49
- "app": {"framework": framework},
50
- }
51
-
52
- if use_db:
53
- config["database"] = {
54
- "type": "postgres",
55
- "user": "db_user",
56
- "password": "db_password", # In a real scenario, this should be handled more securely
57
- "name": "app_db",
58
- }
59
- console.print(" - Added [bold green]PostgreSQL[/bold green] to the configuration.")
60
-
61
- with open("xenfra.yaml", "w") as f:
62
- yaml.dump(config, f, default_flow_style=False, sort_keys=False)
84
+ # Commit to file
85
+ config.to_yaml(Path("xenfra.yaml"))
63
86
 
64
87
  console.print("\n[bold green]✅ SUCCESS![/bold green]")
65
- console.print(" - Created [cyan]xenfra.yaml[/cyan].")
88
+ console.print(" - Created [cyan]xenfra.yaml[/cyan] with Unified Brain discovery.")
66
89
  console.print("\n Next step: Review the configuration and run 'xenfra deploy'!")
67
90
 
68
91
 
69
92
  @main.command()
93
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON for scripting.")
94
+ def detect(output_json):
95
+ """Detects the framework and configuration of the current project."""
96
+ console.print("\n[bold blue]🔍 ANALYZING PROJECT[/bold blue]")
97
+
98
+ detector = get_railpack_detector()
99
+
100
+ with console.status("[bold green]Running Railpack detection...[/bold green]"):
101
+ result = detector.detect_from_path(".")
102
+
103
+ if output_json:
104
+ import json
105
+ click.echo(json.dumps({
106
+ "framework": result.framework,
107
+ "confidence": result.confidence,
108
+ "detected_from": result.detected_from,
109
+ "package_manager": result.package_manager,
110
+ "runtime_version": result.runtime_version,
111
+ "start_command": result.start_command,
112
+ "detected_port": result.detected_port,
113
+ "runtime_name": result.runtime_name,
114
+ "build_commands": result.build_commands
115
+ }, indent=2))
116
+ else:
117
+ if result.framework == "unknown":
118
+ console.print("[yellow]⚠️ Could not detect framework.[/yellow]")
119
+ console.print("[dim] Try running from a project root with recognizable files.[/dim]")
120
+ else:
121
+ tree = Tree(f"[bold green]{result.framework.upper()}[/bold green]")
122
+
123
+ if result.runtime_name:
124
+ tree.add(f"[blue]Runtime:[/blue] {result.runtime_name}")
125
+ if result.runtime_version:
126
+ tree.add(f"[blue]Version:[/blue] {result.runtime_version}")
127
+ if result.package_manager:
128
+ tree.add(f"[magenta]Package Manager:[/magenta] {result.package_manager}")
129
+ if result.detected_port:
130
+ tree.add(f"[yellow]Port:[/yellow] {result.detected_port}")
131
+ if result.start_command:
132
+ tree.add(f"[cyan]Start Command:[/cyan] {result.start_command}")
133
+ if result.build_commands:
134
+ build_tree = tree.add("[dim]Build Steps:[/dim]")
135
+ for cmd in result.build_commands:
136
+ build_tree.add(f"[dim]→ {cmd}[/dim]")
137
+
138
+ tree.add(f"[dim]Confidence:[/dim] {result.confidence}")
139
+ tree.add(f"[dim]Detected From:[/dim] {result.detected_from}")
140
+
141
+ console.print(tree)
142
+
143
+ console.print("\n[dim]Tip: Run 'xenfra init --railpack' to create config from this detection.[/dim]")
144
+
145
+
146
+ @main.command()
147
+ @click.option("--skip-local", is_flag=True, help="Skip the mandatory local mirroring check (Not recommended for Free Tier).")
148
+ @click.option("--dry-run", is_flag=True, help="Generate assets without deploying.")
149
+ @click.option("--env-file", type=click.Path(exists=True), help="Path to .env file to load environment variables.")
70
150
  @click.pass_context
71
- def deploy(ctx):
151
+ def deploy(ctx, skip_local, dry_run, env_file):
72
152
  """Deploys the project based on the xenfra.yaml configuration."""
73
153
  console.print("\n[bold green]🚀 INITIATING DEPLOYMENT FROM CONFIGURATION[/bold green]")
74
154
 
75
- try:
76
- with open("xenfra.yaml", "r") as f:
77
- config = yaml.safe_load(f)
78
- except FileNotFoundError:
155
+ config = load_xenfra_config(".")
156
+ if not config:
79
157
  raise click.ClickException(
80
158
  "No 'xenfra.yaml' found. Run 'xenfra init' to create a configuration file."
81
159
  )
82
160
 
83
161
  engine = ctx.obj["engine"]
84
162
 
85
- # Extract config values
86
- name = config.get("name", "xenfra-app")
87
- do_config = config.get("digitalocean", {})
88
- region = do_config.get("region", "nyc3")
89
- size = do_config.get("size", "s-1vcpu-1gb")
90
- image = do_config.get("image", "ubuntu-22-04-x64")
91
-
92
- # Build context for templates
163
+ # Extract config values from Pydantic model
164
+ name = config.name
165
+ do_config = config.digitalocean
166
+ region = do_config.region
167
+ size = do_config.size
168
+ image = do_config.image
169
+
170
+ # Load environment variables from file if provided
171
+ env_vars = {}
172
+ if env_file:
173
+ from xenfra_sdk.railpack_detector import RailpackDetector
174
+ detector = RailpackDetector()
175
+ with open(env_file) as f:
176
+ content = f.read()
177
+ parsed = detector.parse_env_content(content)
178
+ env_vars = {v.key: v.value for v in parsed}
179
+ console.print(f" - Loaded [cyan]{len(env_vars)}[/cyan] variables from [cyan]{env_file}[/cyan]")
180
+
181
+ # Build context for templates (handle infrastructure)
182
+ # Note: Modern engine uses InfrastructureService models
93
183
  template_context = {
94
- "database": config.get("database", {}).get("type"),
95
- "db_user": config.get("database", {}).get("user"),
96
- "db_password": config.get("database", {}).get("password"),
97
- "db_name": config.get("database", {}).get("name"),
98
184
  "email": ctx.obj["engine"].get_user_info().email,
185
+ "mode": config.mode,
186
+ "env_vars": env_vars
99
187
  }
188
+
189
+ # Map infrastructure for templates
190
+ for infra in config.infrastructure:
191
+ template_context[f"include_{infra.type}"] = True
192
+ # For backwards compatibility with original simple logic
193
+ if infra.type == "postgres":
194
+ template_context["database"] = "postgres"
195
+ template_context["db_user"] = infra.env_vars.get("user", "db_user")
196
+ template_context["db_password"] = infra.env_vars.get("password", "db_password")
197
+ template_context["db_name"] = infra.env_vars.get("name", "app_db")
198
+
199
+ # Handle services (microservices or single)
200
+ if config.services:
201
+ template_context["services"] = config.services
202
+ # Use first service framework as primary
203
+ primary_service = config.services[0]
204
+ template_context["framework"] = primary_service.framework
205
+ template_context["port"] = primary_service.port
206
+ console.print(f" - Framework: [cyan]{primary_service.framework}[/cyan]")
100
207
 
101
208
  console.print(f" - App Name: [cyan]{name}[/cyan]")
102
209
  console.print(f" - Region: [cyan]{region}[/cyan], Size: [cyan]{size}[/cyan]")
103
- if template_context.get("database"):
104
- console.print(f" - Including Database: [cyan]{template_context['database']}[/cyan]")
210
+ if config.infrastructure:
211
+ infra_names = [i.type for i in config.infrastructure]
212
+ console.print(f" - Including Infrastructure: [cyan]{', '.join(infra_names)}[/cyan]")
105
213
 
214
+ if dry_run:
215
+ console.print("\n[bold yellow]🧪 DRY RUN MODE - Generating assets only[/bold yellow]")
216
+
106
217
  if not click.confirm(f"\n Ready to deploy '{name}' from 'xenfra.yaml'?"):
107
218
  return
108
219
 
109
220
  with console.status("[bold green]Deployment in progress...[/bold green]"):
110
221
  result = engine.deploy_server(
111
- name=name, region=region, size=size, image=image, logger=console.log, **template_context
222
+ name=name,
223
+ region=region,
224
+ size=size,
225
+ image=image,
226
+ logger=console.log,
227
+ verify_local=not skip_local,
228
+ dry_run=dry_run,
229
+ **template_context
112
230
  )
113
231
 
114
- console.print("\n[bold green]✅ DEPLOYMENT COMPLETE![/bold green]")
115
- console.print(result)
232
+ if dry_run:
233
+ console.print("\n[bold yellow]🧪 DRY RUN COMPLETE[/bold yellow]")
234
+ if isinstance(result, dict) and "assets" in result:
235
+ console.print("\n[bold cyan]Generated Assets:[/bold cyan]")
236
+ for filename, content in result["assets"].items():
237
+ console.print(f"\n[bold]{filename}:[/bold]")
238
+ console.print(f"[dim]{content[:500]}{'...' if len(content) > 500 else ''}[/dim]")
239
+ else:
240
+ console.print("\n[bold green]✅ DEPLOYMENT COMPLETE![/bold green]")
241
+ console.print(result)
116
242
 
117
243
 
118
244
  @main.command(name="list")
xenfra_sdk/client.py CHANGED
@@ -3,11 +3,13 @@ import os
3
3
  import httpx
4
4
 
5
5
  from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
6
+ from .resources.activity import ActivityManager
7
+ from .resources.build import BuildManager
6
8
  from .resources.deployments import DeploymentsManager
7
9
  from .resources.files import FilesManager
8
10
  from .resources.intelligence import IntelligenceManager
9
11
  from .resources.projects import ProjectsManager
10
-
12
+ from .resources.sandbox import SandboxManager
11
13
 
12
14
  class XenfraClient:
13
15
  def __init__(self, token: str = None, api_url: str = None):
@@ -36,7 +38,9 @@ class XenfraClient:
36
38
  self.deployments = DeploymentsManager(self)
37
39
  self.intelligence = IntelligenceManager(self)
38
40
  self.files = FilesManager(self)
39
-
41
+ self.activity = ActivityManager(self)
42
+ self.build = BuildManager(self) # 3-tier dry-run validation
43
+ self.sandbox = SandboxManager(self)
40
44
 
41
45
  def _request(self, method: str, path: str, json: dict = None) -> httpx.Response:
42
46
  """Internal method to handle all HTTP requests."""
@@ -0,0 +1,26 @@
1
+ """
2
+ Centralized registry for Xenfra SDK defaults and constants.
3
+ Follows XENFRA_PROTOCOL.md by eliminating hardcoded strings in logic.
4
+ """
5
+
6
+ from typing import Final
7
+
8
+ # Infrastructure Defaults
9
+ DEFAULT_REGION: Final[str] = "nyc3"
10
+ DEFAULT_SIZE: Final[str] = "s-1vcpu-1gb"
11
+ DEFAULT_OS: Final[str] = "ubuntu-22-04-x64"
12
+ DEFAULT_SSH_KEY_PATH: Final[str] = "~/.ssh/id_rsa"
13
+ DEFAULT_SSH_PUB_KEY_PATH: Final[str] = "~/.ssh/id_rsa.pub"
14
+
15
+ # Tiered Droplet Sizing (Microservice Orchestration)
16
+ SIZE_LIGHT: Final[str] = "s-1vcpu-2gb" # 1-2 services
17
+ SIZE_MEDIUM: Final[str] = "s-2vcpu-4gb" # 3-5 services
18
+ SIZE_HEAVY: Final[str] = "s-4vcpu-8gb" # 6+ services
19
+
20
+ # Framework Defaults
21
+ DEFAULT_PYTHON_VERSION: Final[str] = "3.11-slim"
22
+ DEFAULT_NODE_VERSION: Final[str] = "18-slim"
23
+
24
+ # Protocol Limits
25
+ MAX_SERVICES_PER_PROJECT: Final[int] = 20
26
+ DEFAULT_PORT_RANGE_START: Final[int] = 8000
xenfra_sdk/db/session.py CHANGED
@@ -3,11 +3,11 @@
3
3
  import os
4
4
  from pathlib import Path
5
5
 
6
+ import click
6
7
  from sqlmodel import Session, SQLModel, create_engine
7
8
 
8
9
  # Get the app directory for the current user
9
- # Use ~/.xenfra for cross-platform simplicity and to avoid 'click' dependency in SDK
10
- app_dir = Path.home() / ".xenfra"
10
+ app_dir = Path(click.get_app_dir("xenfra"))
11
11
  app_dir.mkdir(parents=True, exist_ok=True)
12
12
  db_path = app_dir / "xenfra.db"
13
13
 
@@ -18,7 +18,12 @@ DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite:///{db_path}")
18
18
  # Only echo SQL in development (set SQL_ECHO=1 to enable)
19
19
  SQL_ECHO = os.getenv("SQL_ECHO", "0") == "1"
20
20
 
21
- engine = create_engine(DATABASE_URL, echo=SQL_ECHO)
21
+ engine = create_engine(
22
+ DATABASE_URL,
23
+ echo=SQL_ECHO,
24
+ pool_pre_ping=True,
25
+ pool_recycle=300
26
+ )
22
27
 
23
28
 
24
29
  def create_db_and_tables():