xenfra-sdk 0.2.5__tar.gz → 0.2.6__tar.gz

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 (62) hide show
  1. xenfra_sdk-0.2.6/PKG-INFO +118 -0
  2. xenfra_sdk-0.2.6/README.md +91 -0
  3. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/pyproject.toml +2 -19
  4. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/__init__.py +46 -2
  5. xenfra_sdk-0.2.6/src/xenfra_sdk/blueprints/base.py +150 -0
  6. xenfra_sdk-0.2.6/src/xenfra_sdk/blueprints/factory.py +99 -0
  7. xenfra_sdk-0.2.6/src/xenfra_sdk/blueprints/node.py +219 -0
  8. xenfra_sdk-0.2.6/src/xenfra_sdk/blueprints/python.py +57 -0
  9. xenfra_sdk-0.2.6/src/xenfra_sdk/blueprints/railpack.py +99 -0
  10. xenfra_sdk-0.2.6/src/xenfra_sdk/blueprints/schema.py +70 -0
  11. xenfra_sdk-0.2.6/src/xenfra_sdk/cli/main.py +352 -0
  12. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/client.py +6 -2
  13. xenfra_sdk-0.2.6/src/xenfra_sdk/constants.py +26 -0
  14. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/db/session.py +8 -3
  15. xenfra_sdk-0.2.6/src/xenfra_sdk/detection.py +467 -0
  16. xenfra_sdk-0.2.6/src/xenfra_sdk/dockerizer.py +151 -0
  17. xenfra_sdk-0.2.6/src/xenfra_sdk/engine.py +1343 -0
  18. xenfra_sdk-0.2.6/src/xenfra_sdk/events.py +254 -0
  19. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/exceptions.py +9 -0
  20. xenfra_sdk-0.2.6/src/xenfra_sdk/governance.py +150 -0
  21. xenfra_sdk-0.2.6/src/xenfra_sdk/manifest.py +167 -0
  22. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/mcp_client.py +7 -5
  23. xenfra_sdk-0.2.5/src/xenfra_sdk/models.py → xenfra_sdk-0.2.6/src/xenfra_sdk/models/__init__.py +17 -1
  24. xenfra_sdk-0.2.6/src/xenfra_sdk/models/context.py +61 -0
  25. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/orchestrator.py +223 -99
  26. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/privacy.py +11 -0
  27. xenfra_sdk-0.2.6/src/xenfra_sdk/protocol.py +38 -0
  28. xenfra_sdk-0.2.6/src/xenfra_sdk/railpack_adapter.py +357 -0
  29. xenfra_sdk-0.2.6/src/xenfra_sdk/railpack_detector.py +587 -0
  30. xenfra_sdk-0.2.6/src/xenfra_sdk/railpack_manager.py +312 -0
  31. xenfra_sdk-0.2.6/src/xenfra_sdk/recipes.py +159 -0
  32. xenfra_sdk-0.2.6/src/xenfra_sdk/resources/activity.py +45 -0
  33. xenfra_sdk-0.2.6/src/xenfra_sdk/resources/build.py +157 -0
  34. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/resources/deployments.py +22 -2
  35. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/resources/intelligence.py +25 -0
  36. xenfra_sdk-0.2.5/PKG-INFO +0 -116
  37. xenfra_sdk-0.2.5/README.md +0 -82
  38. xenfra_sdk-0.2.5/src/xenfra_sdk/cli/main.py +0 -226
  39. xenfra_sdk-0.2.5/src/xenfra_sdk/detection.py +0 -396
  40. xenfra_sdk-0.2.5/src/xenfra_sdk/dockerizer.py +0 -195
  41. xenfra_sdk-0.2.5/src/xenfra_sdk/engine.py +0 -757
  42. xenfra_sdk-0.2.5/src/xenfra_sdk/manifest.py +0 -212
  43. xenfra_sdk-0.2.5/src/xenfra_sdk/recipes.py +0 -26
  44. xenfra_sdk-0.2.5/src/xenfra_sdk/templates/Caddyfile.j2 +0 -14
  45. xenfra_sdk-0.2.5/src/xenfra_sdk/templates/Dockerfile.j2 +0 -41
  46. xenfra_sdk-0.2.5/src/xenfra_sdk/templates/cloud-init.sh.j2 +0 -90
  47. xenfra_sdk-0.2.5/src/xenfra_sdk/templates/docker-compose-multi.yml.j2 +0 -29
  48. xenfra_sdk-0.2.5/src/xenfra_sdk/templates/docker-compose.yml.j2 +0 -30
  49. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/cli/__init__.py +0 -0
  50. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/client_with_hooks.py +0 -0
  51. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/config.py +0 -0
  52. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/db/__init__.py +0 -0
  53. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/db/models.py +0 -0
  54. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/dependencies.py +0 -0
  55. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/patterns.json +0 -0
  56. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/resources/__init__.py +0 -0
  57. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/resources/base.py +0 -0
  58. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/resources/files.py +0 -0
  59. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/resources/projects.py +0 -0
  60. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/security.py +0 -0
  61. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/security_scanner.py +0 -0
  62. {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.6}/src/xenfra_sdk/utils.py +0 -0
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.3
2
+ Name: xenfra-sdk
3
+ Version: 0.2.6
4
+ Summary: Xenfra SDK: Core engine and utilities for the Xenfra platform.
5
+ Author: xenfra-cloud
6
+ Author-email: xenfra-cloud <xenfracloud@gmail.com>
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Classifier: Topic :: System :: Systems Administration
14
+ Requires-Dist: fabric>=3.2.2
15
+ Requires-Dist: python-digitalocean>=1.17.0
16
+ Requires-Dist: python-dotenv>=1.2.1
17
+ Requires-Dist: rich>=14.2.0
18
+ Requires-Dist: sqlmodel>=0.0.16
19
+ Requires-Dist: pyyaml>=6.0.1
20
+ Requires-Dist: httpx>=0.27.0
21
+ Requires-Dist: jinja2>=3.1.3
22
+ Requires-Dist: python-jose[cryptography]>=3.3.0
23
+ Requires-Dist: passlib>=1.7.4
24
+ Requires-Dist: cryptography>=41.0.0
25
+ Requires-Python: >=3.13
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Xenfra Python SDK
29
+
30
+ The official, open-source Python SDK for interacting with the Xenfra API.
31
+
32
+ This SDK provides a simple and Pythonic interface for developers and AI Agents to programmatically manage infrastructure, deployments, and other platform resources.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install xenfra-sdk
38
+ ```
39
+
40
+ ## Basic Usage
41
+
42
+ Initialize the client with your API token (or ensure the `XENFRA_TOKEN` environment variable is set).
43
+
44
+ ```python
45
+ import os
46
+ from xenfra_sdk import XenfraClient
47
+ from xenfra_sdk.exceptions import XenfraAPIError
48
+
49
+ client = XenfraClient(token=os.getenv("XENFRA_TOKEN"))
50
+
51
+ try:
52
+ projects = client.projects.list()
53
+ for p in projects:
54
+ print(f"Found project: {p.name} (Status: {p.status})")
55
+ except XenfraAPIError as e:
56
+ print(f"API Error: {e.detail}")
57
+ ```
58
+
59
+ ## Usage for Agentic Workflows
60
+
61
+ The Xenfra SDK is designed to be used as a "tool" by AI Agents (e.g., OpenAI Assistants). The Pydantic models are compatible with function-calling schemas, allowing an agent to easily call these methods.
62
+
63
+ Here is a conceptual example of how an agent might use the SDK to fulfill a user's request.
64
+
65
+ ```python
66
+ # This is a conceptual representation of an agent's internal logic.
67
+ # The agent would be configured with functions that call these SDK methods.
68
+
69
+ def list_all_projects():
70
+ """Lists all available projects in the Xenfra account."""
71
+ return client.projects.list()
72
+
73
+ def create_new_deployment(project_name: str, git_repo: str, branch: str = "main"):
74
+ """
75
+ Creates a new deployment for a project.
76
+
77
+ Args:
78
+ project_name: The name for the new deployment.
79
+ git_repo: The URL of the git repository to deploy.
80
+ branch: The branch to deploy (defaults to 'main').
81
+ """
82
+ return client.deployments.create(
83
+ project_name=project_name,
84
+ git_repo=git_repo,
85
+ branch=branch,
86
+ framework="fastapi" # Framework detection would be part of a more complex agent
87
+ )
88
+
89
+ # --- Agent Execution Flow ---
90
+
91
+ # User prompt: "Deploy my new app from github.com/user/my-app"
92
+
93
+ # 1. Agent decides which tool to use: `create_new_deployment`
94
+ # 2. Agent extracts parameters:
95
+ # - project_name = "my-app" (inferred)
96
+ # - git_repo = "https://github.com/user/my-app"
97
+ # 3. Agent calls the tool:
98
+ # create_new_deployment(
99
+ # project_name="my-app",
100
+ # git_repo="https://github.com/user/my-app"
101
+ # )
102
+ ```
103
+
104
+ ## Error Handling
105
+
106
+ The SDK uses custom exceptions for clear error handling. All API-related errors will raise a `XenfraAPIError`, which contains the `status_code` and a `detail` message from the API response.
107
+
108
+ ```python
109
+ from xenfra_sdk.exceptions import XenfraAPIError, AuthenticationError
110
+
111
+ try:
112
+ # Make an API call
113
+ ...
114
+ except AuthenticationError as e:
115
+ print("Authentication failed. Please check your token.")
116
+ except XenfraAPIError as e:
117
+ print(f"An API error occurred with status {e.status_code}: {e.detail}")
118
+ ```
@@ -0,0 +1,91 @@
1
+ # Xenfra Python SDK
2
+
3
+ The official, open-source Python SDK for interacting with the Xenfra API.
4
+
5
+ This SDK provides a simple and Pythonic interface for developers and AI Agents to programmatically manage infrastructure, deployments, and other platform resources.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install xenfra-sdk
11
+ ```
12
+
13
+ ## Basic Usage
14
+
15
+ Initialize the client with your API token (or ensure the `XENFRA_TOKEN` environment variable is set).
16
+
17
+ ```python
18
+ import os
19
+ from xenfra_sdk import XenfraClient
20
+ from xenfra_sdk.exceptions import XenfraAPIError
21
+
22
+ client = XenfraClient(token=os.getenv("XENFRA_TOKEN"))
23
+
24
+ try:
25
+ projects = client.projects.list()
26
+ for p in projects:
27
+ print(f"Found project: {p.name} (Status: {p.status})")
28
+ except XenfraAPIError as e:
29
+ print(f"API Error: {e.detail}")
30
+ ```
31
+
32
+ ## Usage for Agentic Workflows
33
+
34
+ The Xenfra SDK is designed to be used as a "tool" by AI Agents (e.g., OpenAI Assistants). The Pydantic models are compatible with function-calling schemas, allowing an agent to easily call these methods.
35
+
36
+ Here is a conceptual example of how an agent might use the SDK to fulfill a user's request.
37
+
38
+ ```python
39
+ # This is a conceptual representation of an agent's internal logic.
40
+ # The agent would be configured with functions that call these SDK methods.
41
+
42
+ def list_all_projects():
43
+ """Lists all available projects in the Xenfra account."""
44
+ return client.projects.list()
45
+
46
+ def create_new_deployment(project_name: str, git_repo: str, branch: str = "main"):
47
+ """
48
+ Creates a new deployment for a project.
49
+
50
+ Args:
51
+ project_name: The name for the new deployment.
52
+ git_repo: The URL of the git repository to deploy.
53
+ branch: The branch to deploy (defaults to 'main').
54
+ """
55
+ return client.deployments.create(
56
+ project_name=project_name,
57
+ git_repo=git_repo,
58
+ branch=branch,
59
+ framework="fastapi" # Framework detection would be part of a more complex agent
60
+ )
61
+
62
+ # --- Agent Execution Flow ---
63
+
64
+ # User prompt: "Deploy my new app from github.com/user/my-app"
65
+
66
+ # 1. Agent decides which tool to use: `create_new_deployment`
67
+ # 2. Agent extracts parameters:
68
+ # - project_name = "my-app" (inferred)
69
+ # - git_repo = "https://github.com/user/my-app"
70
+ # 3. Agent calls the tool:
71
+ # create_new_deployment(
72
+ # project_name="my-app",
73
+ # git_repo="https://github.com/user/my-app"
74
+ # )
75
+ ```
76
+
77
+ ## Error Handling
78
+
79
+ The SDK uses custom exceptions for clear error handling. All API-related errors will raise a `XenfraAPIError`, which contains the `status_code` and a `detail` message from the API response.
80
+
81
+ ```python
82
+ from xenfra_sdk.exceptions import XenfraAPIError, AuthenticationError
83
+
84
+ try:
85
+ # Make an API call
86
+ ...
87
+ except AuthenticationError as e:
88
+ print("Authentication failed. Please check your token.")
89
+ except XenfraAPIError as e:
90
+ print(f"An API error occurred with status {e.status_code}: {e.detail}")
91
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xenfra-sdk"
3
- version = "0.2.5"
3
+ version = "0.2.6"
4
4
  description = "Xenfra SDK: Core engine and utilities for the Xenfra platform."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -30,24 +30,7 @@ dependencies = [
30
30
  "passlib>=1.7.4",
31
31
  "cryptography>=41.0.0",
32
32
  ]
33
- requires-python = ">=3.11"
34
-
35
- [project.urls]
36
- Homepage = "https://github.com/xenfra-cloud/xenfra-sdk"
37
- Issues = "https://github.com/xenfra-cloud/xenfra-sdk/issues"
38
-
39
- [project.optional-dependencies]
40
- dev = [
41
- "pytest>=8.0.0",
42
- "pytest-mock>=3.12.0",
43
- "pytest-cov>=4.0.0",
44
- "pytest-asyncio>=0.21.0"
45
- ]
46
-
47
- [tool.pytest.ini_options]
48
- asyncio_mode = "auto"
49
- testpaths = ["tests"]
50
- python_files = ["test_*.py"]
33
+ requires-python = ">=3.13"
51
34
 
52
35
  [build-system]
53
36
  requires = ["uv_build>=0.9.18,<0.10.0"]
@@ -1,16 +1,23 @@
1
1
  # This file makes src/xenfra_sdk a Python package.
2
2
 
3
3
  from .client import XenfraClient
4
- from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
4
+ from .engine import InfraEngine
5
+ from .events import BuildEvent, DeploymentPhase, EventEmitter, EventStatus
6
+ from .exceptions import AuthenticationError, DeploymentError, XenfraAPIError, XenfraError
7
+ from .protocol import ProtocolRegistry
5
8
  from .models import (
9
+ BalanceRead,
6
10
  CodebaseAnalysisResponse,
7
11
  DiagnosisResponse,
12
+ DropletCostRead,
8
13
  PatchObject,
9
14
  ProjectRead,
10
15
  )
11
16
 
12
- # Microservices support
17
+ # Microservices & Config support
13
18
  from .manifest import (
19
+ XenfraConfig,
20
+ load_xenfra_config,
14
21
  ServiceDefinition,
15
22
  load_services_from_xenfra_yaml,
16
23
  is_microservices_project,
@@ -37,6 +44,22 @@ from .security_scanner import (
37
44
  Severity,
38
45
  )
39
46
 
47
+ # Railpack Integration
48
+ from .railpack_detector import (
49
+ RailpackDetector,
50
+ RailpackDetectionResult,
51
+ EnvVariable,
52
+ get_railpack_detector,
53
+ )
54
+ from .railpack_manager import (
55
+ RailpackManager,
56
+ get_railpack_manager,
57
+ )
58
+ from .railpack_adapter import (
59
+ RailpackAdapter,
60
+ RailpackPlan,
61
+ )
62
+
40
63
  __all__ = [
41
64
  "XenfraClient",
42
65
  "XenfraError",
@@ -46,6 +69,11 @@ __all__ = [
46
69
  "CodebaseAnalysisResponse",
47
70
  "PatchObject",
48
71
  "ProjectRead",
72
+ "BalanceRead",
73
+ "DropletCostRead",
74
+ # Config
75
+ "XenfraConfig",
76
+ "load_xenfra_config",
49
77
  # Microservices
50
78
  "ServiceDefinition",
51
79
  "load_services_from_xenfra_yaml",
@@ -58,4 +86,20 @@ __all__ = [
58
86
  "detect_pyproject_services",
59
87
  "ServiceOrchestrator",
60
88
  "get_orchestrator_for_project",
89
+ "InfraEngine",
90
+ "DeploymentError",
91
+ "ProtocolRegistry",
92
+ "EventEmitter",
93
+ "BuildEvent",
94
+ "DeploymentPhase",
95
+ "EventStatus",
96
+ # Railpack Integration
97
+ "RailpackDetector",
98
+ "RailpackDetectionResult",
99
+ "EnvVariable",
100
+ "get_railpack_detector",
101
+ "RailpackManager",
102
+ "get_railpack_manager",
103
+ "RailpackAdapter",
104
+ "RailpackPlan",
61
105
  ]
@@ -0,0 +1,150 @@
1
+ import yaml
2
+ from abc import ABC, abstractmethod
3
+ from typing import Dict, List, Optional
4
+ from pathlib import Path
5
+
6
+ from xenfra_sdk.blueprints.schema import (
7
+ DeploymentBlueprintManifest, DockerfileModel, ComposeModel, ServiceDetail,
8
+ DeployModel, DeployResourcesModel, ResourceLimitsModel, ResourceReservationsModel
9
+ )
10
+
11
+ from xenfra_sdk.constants import DEFAULT_PORT_RANGE_START
12
+
13
+ class BaseBlueprint(ABC):
14
+ """
15
+ Abstract base class for all Xenfra Blueprints (Build Packs).
16
+
17
+ A Blueprint is responsible for:
18
+ 1. Analyzing code to detect requirements.
19
+ 2. Building a valid Pydantic manifest.
20
+ 3. Rendering that manifest to files (Dockerfile, compose, env).
21
+ """
22
+
23
+ def __init__(self, context: dict):
24
+ self.context = context
25
+ self.framework = context.get("framework")
26
+ self.port = context.get("port") or DEFAULT_PORT_RANGE_START
27
+ self.file_manifest = context.get("file_manifest", [])
28
+ self.resource_limits = context.get("resource_limits", {})
29
+
30
+ def _generate_deploy_model(self) -> DeployModel:
31
+ """Centralized helper for resource governance."""
32
+ return DeployModel(
33
+ resources=DeployResourcesModel(
34
+ limits=ResourceLimitsModel(
35
+ memory=self.resource_limits.get("memory", "512m"),
36
+ cpus=self.resource_limits.get("cpus", "0.5"),
37
+ ),
38
+ reservations=ResourceReservationsModel(
39
+ memory=self.resource_limits.get("memory_reserved", "128m"),
40
+ cpus=self.resource_limits.get("cpus_reserved", "0.25"),
41
+ ),
42
+ )
43
+ )
44
+
45
+ @abstractmethod
46
+ def generate_manifest(self) -> DeploymentBlueprintManifest:
47
+ """Analyze context and files to build the Pydantic manifest."""
48
+ pass
49
+
50
+ def render(self) -> Dict[str, str]:
51
+ """Renders the generated manifest into final deployment strings."""
52
+ manifest = self.generate_manifest()
53
+ result = {}
54
+
55
+ # 1. Render Dockerfile
56
+ result["Dockerfile"] = self._render_dockerfile(manifest.dockerfile)
57
+
58
+ # 2. Render docker-compose.yml
59
+ result["docker-compose.yml"] = self._render_compose(manifest.compose)
60
+
61
+ # 3. Render .env
62
+ if manifest.env_file:
63
+ result[".env"] = "\n".join([f'{k}="{v}"' for k, v in manifest.env_file.items()])
64
+
65
+ # 4. Render Caddyfile (if provided)
66
+ if manifest.caddyfile:
67
+ result["Caddyfile"] = manifest.caddyfile
68
+
69
+ # 5. Render Railpack Plan (if provided)
70
+ if hasattr(manifest, "railpack_plan") and manifest.railpack_plan:
71
+ result["railpack-plan.json"] = manifest.railpack_plan
72
+
73
+ return result
74
+
75
+ def _render_dockerfile(self, model: DockerfileModel) -> str:
76
+ """Converts DockerfileModel to a string."""
77
+ lines = [f"FROM {model.base_image}"]
78
+
79
+ # Inject ARG instructions (Build-time variables)
80
+ # Must come after FROM (usually) or before depending on if they affect FROM
81
+ # Standard practice: FROM -> ARG -> WORKDIR -> COPY -> RUN
82
+ if model.args:
83
+ for arg in model.args:
84
+ lines.append(f"ARG {arg}")
85
+
86
+ # APT packages
87
+ if model.system_packages:
88
+ packages = " ".join(model.system_packages)
89
+ lines.append("RUN apt-get update && apt-get install -y " + packages + " && rm -rf /var/lib/apt/lists/*")
90
+
91
+ lines.append(f"WORKDIR {model.workdir}")
92
+
93
+ # Env vars (Non-secret defaults only)
94
+ # We rely on .env + docker-compose for actual deployment values.
95
+ # ARGs are already available during build.
96
+ arg_keys = set(model.args)
97
+ for k, v in model.env_vars.items():
98
+ if k in arg_keys:
99
+ # Skip ENV if it's already an ARG, to keep secrets out of Dockerfile and fix syntax errors
100
+ continue
101
+ # Quote values for safety
102
+ lines.append(f'ENV {k}="{v}"')
103
+
104
+ # Copy commands
105
+ if model.copy_dirs:
106
+ for d in model.copy_dirs:
107
+ lines.append(f"COPY {d} .")
108
+ else:
109
+ lines.append("COPY . .")
110
+
111
+ # Custom RUN commands
112
+ for cmd in model.run_commands:
113
+ lines.append(f"RUN {cmd}")
114
+
115
+ if model.expose_port:
116
+ lines.append(f"EXPOSE {model.expose_port}")
117
+
118
+ if model.entrypoint:
119
+ if isinstance(model.entrypoint, list):
120
+ ep = ", ".join([f'"{x}"' for x in model.entrypoint])
121
+ lines.append(f"ENTRYPOINT [{ep}]")
122
+ else:
123
+ lines.append(f"ENTRYPOINT {model.entrypoint}")
124
+
125
+ if model.command:
126
+ if isinstance(model.command, list):
127
+ cmd = ", ".join([f'"{x}"' for x in model.command])
128
+ lines.append(f"CMD [{cmd}]")
129
+ else:
130
+ lines.append(f"CMD {model.command}")
131
+
132
+ return "\n".join(lines)
133
+
134
+ def _render_compose(self, model: ComposeModel) -> str:
135
+ """Converts ComposeModel to a YAML string."""
136
+ # We use a custom dumper to ensure services come first etc if needed
137
+ # But standard yaml works for start
138
+ data = model.model_dump(by_alias=True, exclude_none=True)
139
+ return yaml.dump(data, sort_keys=False, default_flow_style=False)
140
+
141
+ def has_file(self, filename: str) -> bool:
142
+ """Helper to check if a file exists in the manifest."""
143
+ return any(f.get("path") == filename for f in self.file_manifest)
144
+
145
+ def get_file_content(self, filename: str) -> Optional[str]:
146
+ """Helper to get file content from manifest."""
147
+ for f in self.file_manifest:
148
+ if f.get("path") == filename:
149
+ return f.get("content")
150
+ return None
@@ -0,0 +1,99 @@
1
+ from typing import Type, Dict, List
2
+ from xenfra_sdk.blueprints.base import BaseBlueprint
3
+ from xenfra_sdk.blueprints.python import PythonBlueprint
4
+ from xenfra_sdk.blueprints.node import NodeBlueprint
5
+ from xenfra_sdk.blueprints.railpack import RailpackBlueprint
6
+ from xenfra_sdk.governance import get_resource_limits, ResourceLimits
7
+
8
+
9
+ def resolve_blueprint_class(context: dict) -> Type[BaseBlueprint]:
10
+ """
11
+ The 'Check-In Desk' for manifest generation.
12
+ Decides between Sovereign (Lean) and Specialist (Railpack) paths.
13
+
14
+ Priority:
15
+ 1. Explicit framework selection (user-specified)
16
+ 2. File-based detection fallback
17
+ """
18
+ framework = str(context.get("framework", "")).lower().strip()
19
+ file_manifest = context.get("file_manifest", [])
20
+ file_names = {f.get("path") for f in file_manifest}
21
+
22
+ # 1. EXPLICIT FRAMEWORK SELECTION (takes priority)
23
+ # If user explicitly selected a framework, respect that choice
24
+ SOVEREIGN_PYTHON = ("python", "fastapi", "flask", "django")
25
+
26
+ # Modern Node.js frameworks that need railpack (auto-detect + build)
27
+ # These have complex build pipelines that railpack/nixpacks handles better
28
+ RAILPACK_NODE = ("next", "nextjs", "nuxt", "vite", "nestjs", "nest")
29
+
30
+ # Simple Node.js that can use our lean NodeBlueprint
31
+ # Only for Express-style apps without complex build steps
32
+ SOVEREIGN_NODE = ("express",)
33
+
34
+ if framework in SOVEREIGN_PYTHON:
35
+ # Check for complex package managers (uv, poetry, pipenv) -> Use Railpack
36
+ if any(f in file_names for f in ("uv.lock", "poetry.lock", "Pipfile.lock")):
37
+ return RailpackBlueprint
38
+ return PythonBlueprint
39
+
40
+ # Route modern frameworks to RailpackBlueprint
41
+ if framework in RAILPACK_NODE:
42
+ return RailpackBlueprint
43
+
44
+ # Generic "node" or "nodejs" → check file_manifest for framework hints
45
+ if framework in ("node", "nodejs"):
46
+ # Check for Next.js, Nuxt, etc. config files
47
+ nextjs_configs = {"next.config.js", "next.config.ts", "next.config.mjs"}
48
+ nuxt_configs = {"nuxt.config.js", "nuxt.config.ts"}
49
+ vite_configs = {"vite.config.js", "vite.config.ts"}
50
+
51
+ if nextjs_configs & file_names or nuxt_configs & file_names or vite_configs & file_names:
52
+ return RailpackBlueprint
53
+
54
+ # Default to RailpackBlueprint for generic Node.js (safer, handles more cases)
55
+ return RailpackBlueprint
56
+
57
+ if framework in SOVEREIGN_NODE:
58
+ return NodeBlueprint
59
+
60
+ # 2. FILE-BASED DETECTION FALLBACK (only if no explicit framework)
61
+ # Check for Python Sovereign Path (Strict Golden Path)
62
+ if "requirements.txt" in file_names and "pyproject.toml" not in file_names:
63
+ return PythonBlueprint
64
+
65
+ # Check for Node Sovereign Path (Strict Golden Path)
66
+ # Only for simple Express apps with package-lock.json
67
+ if "package-lock.json" in file_names and "package.json" in file_names:
68
+ # If has modern framework config files, use Railpack
69
+ nextjs_configs = {"next.config.js", "next.config.ts", "next.config.mjs"}
70
+ if nextjs_configs & file_names:
71
+ return RailpackBlueprint
72
+ # Otherwise, still prefer RailpackBlueprint for safety
73
+ return RailpackBlueprint
74
+
75
+ # 3. Specialist Path (The Delegate)
76
+ # Handles EVERYTHING else: Rust, Go, Bun, UV, Poetry, etc.
77
+ return RailpackBlueprint
78
+
79
+
80
+ def render_blueprint(context: dict) -> Dict[str, str]:
81
+ """
82
+ Main entry point for manifest generation.
83
+ Resolves the blueprint and renders it to strings.
84
+
85
+ ZEN GAP FIX: Automatically injects tier-based resource limits.
86
+ """
87
+ # Inject resource limits if tier is provided but resource_limits isn't
88
+ if "resource_limits" not in context and "tier" in context:
89
+ limits = get_resource_limits(context["tier"])
90
+ context["resource_limits"] = {
91
+ "memory": limits.memory,
92
+ "cpus": limits.cpus,
93
+ "memory_reserved": limits.memory_reserved,
94
+ "cpus_reserved": limits.cpus_reserved,
95
+ }
96
+
97
+ blueprint_class = resolve_blueprint_class(context)
98
+ blueprint = blueprint_class(context)
99
+ return blueprint.render()