xenfra 0.4.4__py3-none-any.whl → 0.4.5__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/blueprints/__init__.py +55 -0
- xenfra/blueprints/base.py +85 -0
- xenfra/blueprints/cache.py +286 -0
- xenfra/blueprints/dry_run.py +251 -0
- xenfra/blueprints/e2b.py +101 -0
- xenfra/blueprints/factory.py +113 -0
- xenfra/blueprints/railpack.py +319 -0
- xenfra/blueprints/validation.py +182 -0
- xenfra/commands/deployments.py +303 -78
- xenfra/commands/intelligence.py +5 -5
- xenfra/main.py +4 -1
- {xenfra-0.4.4.dist-info → xenfra-0.4.5.dist-info}/METADATA +1 -1
- xenfra-0.4.5.dist-info/RECORD +29 -0
- {xenfra-0.4.4.dist-info → xenfra-0.4.5.dist-info}/WHEEL +1 -1
- xenfra-0.4.4.dist-info/RECORD +0 -21
- {xenfra-0.4.4.dist-info → xenfra-0.4.5.dist-info}/entry_points.txt +0 -0
xenfra/blueprints/e2b.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tier 3: E2B Sandbox integration via Xenfra API.
|
|
3
|
+
|
|
4
|
+
The sandbox runs on Xenfra's servers, not locally. Users don't need:
|
|
5
|
+
- E2B package installed
|
|
6
|
+
- E2B_API_KEY set
|
|
7
|
+
- Any E2B configuration
|
|
8
|
+
|
|
9
|
+
They just need a Xenfra account. We handle E2B on our backend.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from .base import BuildPlan
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class SandboxResult:
|
|
21
|
+
"""Result of sandbox build validation."""
|
|
22
|
+
success: bool
|
|
23
|
+
build_output: str = ""
|
|
24
|
+
error_message: Optional[str] = None
|
|
25
|
+
build_time_seconds: float = 0.0
|
|
26
|
+
|
|
27
|
+
def __bool__(self):
|
|
28
|
+
return self.success
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class E2BSandbox:
|
|
32
|
+
"""
|
|
33
|
+
E2B Sandbox client - calls Xenfra API to run sandbox.
|
|
34
|
+
|
|
35
|
+
The actual sandbox runs on Xenfra's servers using E2B.
|
|
36
|
+
This class just makes API calls to our backend via SDK.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, api_client=None):
|
|
40
|
+
self.api_client = api_client
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_available(self) -> bool:
|
|
44
|
+
"""Check if sandbox is available (has API client)."""
|
|
45
|
+
return self.api_client is not None
|
|
46
|
+
|
|
47
|
+
def validate_build(
|
|
48
|
+
self,
|
|
49
|
+
project_path: Path,
|
|
50
|
+
plan: BuildPlan,
|
|
51
|
+
file_manifest: list,
|
|
52
|
+
timeout: int = 300
|
|
53
|
+
) -> SandboxResult:
|
|
54
|
+
"""
|
|
55
|
+
Validate a build via Xenfra API sandbox.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
project_path: Path to the project directory (for reference)
|
|
59
|
+
plan: BuildPlan with build information
|
|
60
|
+
file_manifest: List of file info dicts for the project
|
|
61
|
+
timeout: Maximum time to wait
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
SandboxResult with build status
|
|
65
|
+
"""
|
|
66
|
+
if not self.api_client:
|
|
67
|
+
return SandboxResult(
|
|
68
|
+
success=False,
|
|
69
|
+
error_message="Not authenticated. Run 'xenfra auth login' first."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# Use SDK's build.validate method
|
|
74
|
+
result = self.api_client.build.validate(
|
|
75
|
+
file_manifest=file_manifest,
|
|
76
|
+
build_plan=plan.to_dict(),
|
|
77
|
+
timeout=timeout
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return SandboxResult(
|
|
81
|
+
success=result.get("success", False),
|
|
82
|
+
build_output=result.get("output", ""),
|
|
83
|
+
error_message=result.get("error"),
|
|
84
|
+
build_time_seconds=result.get("build_time", 0.0)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
return SandboxResult(
|
|
89
|
+
success=False,
|
|
90
|
+
error_message=f"Sandbox validation failed: {e}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_sandbox(api_client=None) -> E2BSandbox:
|
|
95
|
+
"""Get E2B sandbox client."""
|
|
96
|
+
return E2BSandbox(api_client)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_sandbox_available(api_client=None) -> bool:
|
|
100
|
+
"""Check if sandbox is available."""
|
|
101
|
+
return get_sandbox(api_client).is_available
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Blueprint factory - Railpack-first routing with E2B dry-run support.
|
|
3
|
+
|
|
4
|
+
Architecture:
|
|
5
|
+
- Railpack-first: Use Railway's buildpack for all project detection
|
|
6
|
+
- E2B sandbox: Optional dry-run builds in isolated environments
|
|
7
|
+
- Fallback chain: Railpack → Error (no other blueprints needed)
|
|
8
|
+
|
|
9
|
+
Environment Variables:
|
|
10
|
+
- E2B_API_KEY: API key for E2B sandbox (optional, for --dry-run)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from .base import Blueprint, BuildPlan
|
|
18
|
+
from .railpack import RailpackBlueprint
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BlueprintFactory:
|
|
22
|
+
"""
|
|
23
|
+
Factory for creating and selecting blueprints.
|
|
24
|
+
|
|
25
|
+
Implements Railpack-first routing:
|
|
26
|
+
1. RailpackBlueprint - Universal detection for all frameworks
|
|
27
|
+
2. No fallbacks needed - Railpack handles everything
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
# Single blueprint - Railpack handles all cases
|
|
32
|
+
self._blueprint = RailpackBlueprint()
|
|
33
|
+
|
|
34
|
+
def detect_framework(self, project_path: Path) -> tuple[str, BuildPlan]:
|
|
35
|
+
"""
|
|
36
|
+
Detect framework and generate build plan for a project.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
project_path: Path to the project directory
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Tuple of (framework_name, build_plan)
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
RuntimeError: If detection fails
|
|
46
|
+
"""
|
|
47
|
+
plan = self._blueprint.generate_plan(project_path)
|
|
48
|
+
return plan.framework, plan
|
|
49
|
+
|
|
50
|
+
def get_build_plan(self, project_path: Path) -> BuildPlan:
|
|
51
|
+
"""
|
|
52
|
+
Get the build plan for a project.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
project_path: Path to the project directory
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
BuildPlan with all build/deployment information
|
|
59
|
+
"""
|
|
60
|
+
return self._blueprint.generate_plan(project_path)
|
|
61
|
+
|
|
62
|
+
def generate_dockerfile(self, project_path: Path) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Generate a Dockerfile for the project.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
project_path: Path to the project directory
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Generated Dockerfile content
|
|
71
|
+
"""
|
|
72
|
+
return self._blueprint.generate_dockerfile(project_path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Global factory instance
|
|
76
|
+
_factory: Optional[BlueprintFactory] = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_factory() -> BlueprintFactory:
|
|
80
|
+
"""Get the global blueprint factory instance."""
|
|
81
|
+
global _factory
|
|
82
|
+
if _factory is None:
|
|
83
|
+
_factory = BlueprintFactory()
|
|
84
|
+
return _factory
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_blueprint_for_framework(framework: str) -> Blueprint:
|
|
88
|
+
"""
|
|
89
|
+
Get the appropriate blueprint for a framework.
|
|
90
|
+
|
|
91
|
+
Note: Since we use Railpack-first, this always returns RailpackBlueprint.
|
|
92
|
+
The framework parameter is kept for API compatibility.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
framework: Framework name (for compatibility, not used)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
RailpackBlueprint instance
|
|
99
|
+
"""
|
|
100
|
+
return RailpackBlueprint()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def detect_project(project_path: Path) -> tuple[str, BuildPlan]:
|
|
104
|
+
"""
|
|
105
|
+
Convenience function to detect framework and get build plan.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
project_path: Path to the project directory
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Tuple of (framework_name, build_plan)
|
|
112
|
+
"""
|
|
113
|
+
return get_factory().detect_framework(project_path)
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Railpack blueprint - Railway's buildpack for universal project detection.
|
|
3
|
+
|
|
4
|
+
Downloads and runs the Railpack binary to analyze projects and generate
|
|
5
|
+
Dockerfile-based build plans.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from .base import Blueprint, BuildPlan
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RailpackBlueprint(Blueprint):
|
|
20
|
+
"""
|
|
21
|
+
Railpack-based blueprint using Railway's buildpack.
|
|
22
|
+
|
|
23
|
+
Railpack is a universal buildpack that can detect and build:
|
|
24
|
+
- Python (FastAPI, Flask, Django, etc.)
|
|
25
|
+
- Node.js (Next.js, Express, NestJS, etc.)
|
|
26
|
+
- Go (Gin, Echo, etc.)
|
|
27
|
+
- Rust (Actix, Axum, etc.)
|
|
28
|
+
- Ruby, PHP, Java, and more
|
|
29
|
+
|
|
30
|
+
Download URL: https://github.com/railwayapp/railpack/releases
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name = "railpack"
|
|
34
|
+
languages = ["python", "node", "go", "rust", "ruby", "php", "java", "dotnet"]
|
|
35
|
+
|
|
36
|
+
# Version to download
|
|
37
|
+
RAILPACK_VERSION = "0.0.70"
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self._binary_path: Optional[Path] = None
|
|
41
|
+
self._cache_dir = Path.home() / ".xenfra" / "cache" / "railpack"
|
|
42
|
+
|
|
43
|
+
def _get_binary_name(self) -> str:
|
|
44
|
+
"""Get the appropriate binary name for the current platform."""
|
|
45
|
+
system = platform.system().lower()
|
|
46
|
+
machine = platform.machine().lower()
|
|
47
|
+
|
|
48
|
+
# Map machine arch
|
|
49
|
+
if machine in ("amd64", "x86_64"):
|
|
50
|
+
arch = "amd64"
|
|
51
|
+
elif machine in ("arm64", "aarch64"):
|
|
52
|
+
arch = "arm64"
|
|
53
|
+
else:
|
|
54
|
+
arch = "amd64" # fallback
|
|
55
|
+
|
|
56
|
+
# Map system
|
|
57
|
+
if system == "darwin":
|
|
58
|
+
os_name = "darwin"
|
|
59
|
+
elif system == "linux":
|
|
60
|
+
os_name = "linux"
|
|
61
|
+
elif system == "windows":
|
|
62
|
+
os_name = "windows"
|
|
63
|
+
else:
|
|
64
|
+
os_name = "linux" # fallback
|
|
65
|
+
|
|
66
|
+
ext = ".exe" if os_name == "windows" else ""
|
|
67
|
+
return f"railpack_{self.RAILPACK_VERSION}_{os_name}_{arch}{ext}"
|
|
68
|
+
|
|
69
|
+
def _get_download_url(self) -> str:
|
|
70
|
+
"""Get the download URL for the current platform."""
|
|
71
|
+
binary_name = self._get_binary_name()
|
|
72
|
+
return (
|
|
73
|
+
f"https://github.com/railwayapp/railpack/releases/download/"
|
|
74
|
+
f"v{self.RAILPACK_VERSION}/{binary_name}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _ensure_binary(self) -> Path:
|
|
78
|
+
"""
|
|
79
|
+
Ensure the Railpack binary is available, download if needed.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Path to the railpack binary
|
|
83
|
+
"""
|
|
84
|
+
if self._binary_path and self._binary_path.exists():
|
|
85
|
+
return self._binary_path
|
|
86
|
+
|
|
87
|
+
# Create cache directory
|
|
88
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
binary_name = self._get_binary_name()
|
|
91
|
+
binary_path = self._cache_dir / "railpack"
|
|
92
|
+
|
|
93
|
+
# Check if already cached
|
|
94
|
+
if binary_path.exists():
|
|
95
|
+
self._binary_path = binary_path
|
|
96
|
+
return binary_path
|
|
97
|
+
|
|
98
|
+
# Download binary
|
|
99
|
+
import urllib.request
|
|
100
|
+
|
|
101
|
+
url = self._get_download_url()
|
|
102
|
+
temp_path = self._cache_dir / binary_name
|
|
103
|
+
|
|
104
|
+
print(f"Downloading Railpack v{self.RAILPACK_VERSION}...")
|
|
105
|
+
print(f" URL: {url}")
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
urllib.request.urlretrieve(url, temp_path)
|
|
109
|
+
|
|
110
|
+
# Make executable (not needed on Windows)
|
|
111
|
+
if platform.system() != "Windows":
|
|
112
|
+
os.chmod(temp_path, 0o755)
|
|
113
|
+
|
|
114
|
+
# Rename to standard name
|
|
115
|
+
temp_path.rename(binary_path)
|
|
116
|
+
|
|
117
|
+
print(f" Saved to: {binary_path}")
|
|
118
|
+
|
|
119
|
+
self._binary_path = binary_path
|
|
120
|
+
return binary_path
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise RuntimeError(f"Failed to download Railpack: {e}")
|
|
124
|
+
|
|
125
|
+
def detect(self, project_path: Path) -> bool:
|
|
126
|
+
"""
|
|
127
|
+
Railpack can handle any project - it's universal.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
project_path: Path to the project directory
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Always True (fallback blueprint)
|
|
134
|
+
"""
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def generate_plan(self, project_path: Path) -> BuildPlan:
|
|
138
|
+
"""
|
|
139
|
+
Generate a build plan using Railpack analysis.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
project_path: Path to the project directory
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
BuildPlan from Railpack analysis
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
RuntimeError: If Railpack analysis fails
|
|
149
|
+
"""
|
|
150
|
+
binary = self._ensure_binary()
|
|
151
|
+
|
|
152
|
+
# Run railpack analyze
|
|
153
|
+
try:
|
|
154
|
+
result = subprocess.run(
|
|
155
|
+
[str(binary), "analyze", str(project_path)],
|
|
156
|
+
capture_output=True,
|
|
157
|
+
text=True,
|
|
158
|
+
timeout=60,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if result.returncode != 0:
|
|
162
|
+
raise RuntimeError(f"Railpack analyze failed: {result.stderr}")
|
|
163
|
+
|
|
164
|
+
# Parse the output as JSON
|
|
165
|
+
analysis = json.loads(result.stdout)
|
|
166
|
+
|
|
167
|
+
# Extract information from Railpack output
|
|
168
|
+
metadata = analysis.get("metadata", {})
|
|
169
|
+
|
|
170
|
+
# Detect framework from metadata
|
|
171
|
+
framework = self._detect_framework(metadata, project_path)
|
|
172
|
+
|
|
173
|
+
# Get package manager
|
|
174
|
+
pkg_manager = metadata.get("package_manager", "unknown")
|
|
175
|
+
|
|
176
|
+
# Get dependency file
|
|
177
|
+
dep_file = self._get_dependency_file(pkg_manager, project_path)
|
|
178
|
+
|
|
179
|
+
# Get port (common defaults)
|
|
180
|
+
port = self._detect_port(metadata, framework)
|
|
181
|
+
|
|
182
|
+
# Get start command from Railpack or infer
|
|
183
|
+
start_cmd = self._get_start_command(analysis, framework, port)
|
|
184
|
+
|
|
185
|
+
return BuildPlan(
|
|
186
|
+
framework=framework,
|
|
187
|
+
language=metadata.get("language", "unknown"),
|
|
188
|
+
package_manager=pkg_manager,
|
|
189
|
+
dependency_file=dep_file,
|
|
190
|
+
build_command=None, # Railpack handles this in the Dockerfile
|
|
191
|
+
start_command=start_cmd,
|
|
192
|
+
port=port,
|
|
193
|
+
dockerfile=None, # Generated by Railpack
|
|
194
|
+
detected_from="railpack",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
except json.JSONDecodeError as e:
|
|
198
|
+
raise RuntimeError(f"Failed to parse Railpack output: {e}")
|
|
199
|
+
except subprocess.TimeoutExpired:
|
|
200
|
+
raise RuntimeError("Railpack analysis timed out")
|
|
201
|
+
except Exception as e:
|
|
202
|
+
raise RuntimeError(f"Railpack analysis failed: {e}")
|
|
203
|
+
|
|
204
|
+
def generate_dockerfile(self, project_path: Path) -> str:
|
|
205
|
+
"""
|
|
206
|
+
Generate a Dockerfile using Railpack.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
project_path: Path to the project directory
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Generated Dockerfile content
|
|
213
|
+
"""
|
|
214
|
+
binary = self._ensure_binary()
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
result = subprocess.run(
|
|
218
|
+
[str(binary), "build", str(project_path)],
|
|
219
|
+
capture_output=True,
|
|
220
|
+
text=True,
|
|
221
|
+
timeout=120,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if result.returncode != 0:
|
|
225
|
+
raise RuntimeError(f"Railpack build failed: {result.stderr}")
|
|
226
|
+
|
|
227
|
+
return result.stdout
|
|
228
|
+
|
|
229
|
+
except subprocess.TimeoutExpired:
|
|
230
|
+
raise RuntimeError("Railpack build timed out")
|
|
231
|
+
except Exception as e:
|
|
232
|
+
raise RuntimeError(f"Railpack build failed: {e}")
|
|
233
|
+
|
|
234
|
+
def _detect_framework(self, metadata: dict, project_path: Path) -> str:
|
|
235
|
+
"""Detect framework from Railpack metadata and project files."""
|
|
236
|
+
# Check metadata hints
|
|
237
|
+
hints = metadata.get("hints", [])
|
|
238
|
+
|
|
239
|
+
for hint in hints:
|
|
240
|
+
hint_lower = hint.lower()
|
|
241
|
+
if "fastapi" in hint_lower:
|
|
242
|
+
return "fastapi"
|
|
243
|
+
elif "flask" in hint_lower:
|
|
244
|
+
return "flask"
|
|
245
|
+
elif "django" in hint_lower:
|
|
246
|
+
return "django"
|
|
247
|
+
elif "next" in hint_lower:
|
|
248
|
+
return "nextjs"
|
|
249
|
+
elif "express" in hint_lower:
|
|
250
|
+
return "express"
|
|
251
|
+
elif "nest" in hint_lower:
|
|
252
|
+
return "nestjs"
|
|
253
|
+
|
|
254
|
+
# Check for specific files
|
|
255
|
+
if (project_path / "next.config.js").exists() or (project_path / "next.config.ts").exists():
|
|
256
|
+
return "nextjs"
|
|
257
|
+
|
|
258
|
+
# Default to language
|
|
259
|
+
lang = metadata.get("language", "unknown")
|
|
260
|
+
return lang if lang != "unknown" else "generic"
|
|
261
|
+
|
|
262
|
+
def _get_dependency_file(self, package_manager: str, project_path: Path) -> str:
|
|
263
|
+
"""Get the dependency file name based on package manager."""
|
|
264
|
+
mapping = {
|
|
265
|
+
"pip": "requirements.txt",
|
|
266
|
+
"poetry": "pyproject.toml",
|
|
267
|
+
"pipenv": "Pipfile",
|
|
268
|
+
"uv": "pyproject.toml",
|
|
269
|
+
"npm": "package.json",
|
|
270
|
+
"yarn": "package.json",
|
|
271
|
+
"pnpm": "package.json",
|
|
272
|
+
"bun": "package.json",
|
|
273
|
+
"go": "go.mod",
|
|
274
|
+
"cargo": "Cargo.toml",
|
|
275
|
+
"bundler": "Gemfile",
|
|
276
|
+
"composer": "composer.json",
|
|
277
|
+
}
|
|
278
|
+
return mapping.get(package_manager, "requirements.txt")
|
|
279
|
+
|
|
280
|
+
def _detect_port(self, metadata: dict, framework: str) -> int:
|
|
281
|
+
"""Detect the port based on framework and metadata."""
|
|
282
|
+
# Check if port is specified in environment
|
|
283
|
+
for env in metadata.get("env", []):
|
|
284
|
+
if "PORT" in env:
|
|
285
|
+
try:
|
|
286
|
+
return int(env.split("=")[1])
|
|
287
|
+
except (IndexError, ValueError):
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
# Framework defaults
|
|
291
|
+
framework_ports = {
|
|
292
|
+
"fastapi": 8000,
|
|
293
|
+
"flask": 5000,
|
|
294
|
+
"django": 8000,
|
|
295
|
+
"nextjs": 3000,
|
|
296
|
+
"express": 3000,
|
|
297
|
+
"nestjs": 3000,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return framework_ports.get(framework, 8000)
|
|
301
|
+
|
|
302
|
+
def _get_start_command(self, analysis: dict, framework: str, port: int) -> Optional[str]:
|
|
303
|
+
"""Get the start command from Railpack analysis or infer it."""
|
|
304
|
+
# Check if Railpack provided a start command
|
|
305
|
+
commands = analysis.get("commands", {})
|
|
306
|
+
if "start" in commands:
|
|
307
|
+
return commands["start"]
|
|
308
|
+
|
|
309
|
+
# Infer based on framework
|
|
310
|
+
cmds = {
|
|
311
|
+
"fastapi": f"uvicorn main:app --host 0.0.0.0 --port {port}",
|
|
312
|
+
"flask": f"flask run --host=0.0.0.0 --port={port}",
|
|
313
|
+
"django": f"python manage.py runserver 0.0.0.0:{port}",
|
|
314
|
+
"nextjs": "npm start",
|
|
315
|
+
"express": "node server.js",
|
|
316
|
+
"nestjs": "npm run start:prod",
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return cmds.get(framework)
|