Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ Set your API key in the environment:

```bash
export CELESTO_API_KEY="your-key"
export CELESTO_PROJECT_NAME="your-project-name"
```

## CLI

```bash
celesto deploy
celesto deploy --project "My Project"
celesto ls
celesto a2a get-card --agent http://localhost:8000
```
Expand Down
14 changes: 13 additions & 1 deletion src/celesto/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ def deploy(
"-e",
help='Environment variables as comma-separated key=value pairs (e.g., "API_KEY=xyz,DEBUG=true")',
),
project_name: Optional[str] = typer.Option(
None,
"--project",
"-p",
help="Celesto project name (optional; defaults to first project)",
),
api_key: Optional[str] = typer.Option(
None,
"--api-key",
Expand All @@ -127,6 +133,8 @@ def deploy(
# Get API key
final_api_key = _get_api_key(api_key, ignore_env_file, "CELESTO_API_KEY")

resolved_project_name = project_name or os.environ.get("CELESTO_PROJECT_NAME")

# Validate folder path
folder_path = Path(folder).resolve()
if not folder_path.exists():
Expand All @@ -151,7 +159,11 @@ def deploy(

client = CelestoSDK(final_api_key)
result = client.deployment.deploy(
folder=folder_path, name=name, description=description, envs=env_dict
folder=folder_path,
name=name,
description=description,
envs=env_dict,
project_name=resolved_project_name,
)
console.print("✅ [bold green]Deployment successful![/bold green]")

Expand Down
76 changes: 71 additions & 5 deletions src/celesto/sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,16 +241,69 @@ class Deployment(_BaseClient):
folder=Path("./my-agent"),
name="my-agent",
description="My AI assistant",
envs={"OPENAI_API_KEY": "sk-..."}
envs={"OPENAI_API_KEY": "sk-..."},
project_name="My Project"
)
print(f"Deployment ID: {result['id']}")

# List all deployments
deployments = client.deployment.list()
"""

def _resolve_project_id(self, project_name: str) -> str:
"""Resolve a project ID from a project name."""
skip = 0
limit = 100
while True:
response = self._request(
"GET",
"/projects",
params={"skip": skip, "limit": limit},
)
projects = response.get("data") or []
for project in projects:
if project.get("name") == project_name:
project_id = project.get("id")
if not project_id:
raise CelestoValidationError(
f"Project '{project_name}' missing id in response."
)
return project_id
total = response.get("total")
if total is None:
break
skip += limit
if skip >= total:
break

raise CelestoValidationError(f"Project '{project_name}' not found.")

def _resolve_first_project_id(self) -> str:
"""Resolve the first available project ID."""
response = self._request(
"GET",
"/projects",
params={"skip": 0, "limit": 1},
)
projects = response.get("data") or []
if not projects:
raise CelestoValidationError(
"No projects found. Create a project or specify project_name."
)
project_id = projects[0].get("id")
if not project_id:
raise CelestoValidationError(
"First project missing id in response."
)
return project_id

def _create_deployment(
self, bundle: Path, name: str, description: str, envs: dict[str, str]
self,
bundle: Path,
name: str,
description: str,
envs: dict[str, str],
project_id: str,
) -> dict:
"""Internal method to upload and create a deployment."""
if bundle.exists() and not bundle.is_file():
Expand All @@ -263,6 +316,7 @@ def _create_deployment(
form_data = {
"name": name,
"description": description,
"project_id": project_id,
"config": json.dumps(config),
}

Expand All @@ -277,6 +331,7 @@ def deploy(
name: str,
description: Optional[str] = None,
envs: Optional[dict[str, str]] = None,
project_name: Optional[str] = None,
) -> dict:
"""Deploy an agent from a local folder.

Expand All @@ -289,6 +344,7 @@ def deploy(
name: Unique name for the deployment
description: Human-readable description (optional)
envs: Environment variables to inject (optional)
project_name: Project name to scope the deployment (optional; defaults to first project)

Returns:
Deployment result with 'id', 'status', and other metadata
Expand All @@ -301,7 +357,8 @@ def deploy(
folder=Path("./my-agent"),
name="weather-bot",
description="A bot that provides weather information",
envs={"API_KEY": "secret123"}
envs={"API_KEY": "secret123"},
project_name="My Project"
)
print(f"Status: {result['status']}") # "READY" or "BUILDING"
"""
Expand All @@ -310,6 +367,12 @@ def deploy(
if not folder.is_dir():
raise CelestoValidationError(f"Folder {folder} is not a directory")

resolved_project_name = project_name or os.environ.get("CELESTO_PROJECT_NAME")
if resolved_project_name:
resolved_project_id = self._resolve_project_id(resolved_project_name)
else:
resolved_project_id = self._resolve_first_project_id()

# Create tar.gz archive (Nixpacks expects tar.gz format)
with tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz") as temp_file:
with tarfile.open(temp_file.name, "w:gz") as tar:
Expand All @@ -318,7 +381,9 @@ def deploy(
bundle = Path(temp_file.name)

try:
return self._create_deployment(bundle, name, description, envs)
return self._create_deployment(
bundle, name, description, envs, resolved_project_id
)
finally:
bundle.unlink()

Expand Down Expand Up @@ -657,7 +722,8 @@ class CelestoSDK(_BaseConnection):
# Deploy an agent
result = client.deployment.deploy(
folder=Path("./my-app"),
name="My App"
name="My App",
project_name="My Project"
)

# Manage delegated access
Expand Down