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.
- xenfra_sdk/__init__.py +46 -2
- xenfra_sdk/blueprints/base.py +150 -0
- xenfra_sdk/blueprints/factory.py +99 -0
- xenfra_sdk/blueprints/node.py +219 -0
- xenfra_sdk/blueprints/python.py +57 -0
- xenfra_sdk/blueprints/railpack.py +99 -0
- xenfra_sdk/blueprints/schema.py +70 -0
- xenfra_sdk/cli/main.py +175 -49
- xenfra_sdk/client.py +6 -2
- xenfra_sdk/constants.py +26 -0
- xenfra_sdk/db/session.py +8 -3
- xenfra_sdk/detection.py +262 -191
- xenfra_sdk/dockerizer.py +76 -120
- xenfra_sdk/engine.py +767 -172
- xenfra_sdk/events.py +254 -0
- xenfra_sdk/exceptions.py +9 -0
- xenfra_sdk/governance.py +150 -0
- xenfra_sdk/manifest.py +93 -138
- xenfra_sdk/mcp_client.py +7 -5
- xenfra_sdk/{models.py → models/__init__.py} +17 -1
- xenfra_sdk/models/context.py +61 -0
- xenfra_sdk/orchestrator.py +223 -99
- xenfra_sdk/privacy.py +11 -0
- xenfra_sdk/protocol.py +38 -0
- xenfra_sdk/railpack_adapter.py +357 -0
- xenfra_sdk/railpack_detector.py +587 -0
- xenfra_sdk/railpack_manager.py +312 -0
- xenfra_sdk/recipes.py +152 -19
- xenfra_sdk/resources/activity.py +45 -0
- xenfra_sdk/resources/build.py +157 -0
- xenfra_sdk/resources/deployments.py +22 -2
- xenfra_sdk/resources/intelligence.py +25 -0
- xenfra_sdk-0.2.7.dist-info/METADATA +118 -0
- xenfra_sdk-0.2.7.dist-info/RECORD +49 -0
- {xenfra_sdk-0.2.5.dist-info → xenfra_sdk-0.2.7.dist-info}/WHEEL +1 -1
- xenfra_sdk/templates/Caddyfile.j2 +0 -14
- xenfra_sdk/templates/Dockerfile.j2 +0 -41
- xenfra_sdk/templates/cloud-init.sh.j2 +0 -90
- xenfra_sdk/templates/docker-compose-multi.yml.j2 +0 -29
- xenfra_sdk/templates/docker-compose.yml.j2 +0 -30
- xenfra_sdk-0.2.5.dist-info/METADATA +0 -116
- 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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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.
|
|
87
|
-
do_config = config.
|
|
88
|
-
region = do_config.
|
|
89
|
-
size = do_config.
|
|
90
|
-
image = do_config.
|
|
91
|
-
|
|
92
|
-
#
|
|
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
|
|
104
|
-
|
|
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,
|
|
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
|
-
|
|
115
|
-
|
|
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."""
|
xenfra_sdk/constants.py
ADDED
|
@@ -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
|
-
|
|
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(
|
|
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():
|