opsmith-cli 0.1.2a0__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.
- opsmith/__init__.py +0 -0
- opsmith/agent.py +243 -0
- opsmith/cloud_providers/__init__.py +10 -0
- opsmith/cloud_providers/aws.py +46 -0
- opsmith/cloud_providers/base.py +57 -0
- opsmith/cloud_providers/gcp.py +67 -0
- opsmith/constants.py +175 -0
- opsmith/deployer.py +236 -0
- opsmith/git_repo.py +66 -0
- opsmith/main.py +269 -0
- opsmith/prompts.py +76 -0
- opsmith/queries/tree-sitter-languages/README.md +7 -0
- opsmith/queries/tree-sitter-languages/arduino-tags.scm +5 -0
- opsmith/queries/tree-sitter-languages/c-tags.scm +9 -0
- opsmith/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- opsmith/queries/tree-sitter-languages/chatito-tags.scm +16 -0
- opsmith/queries/tree-sitter-languages/commonlisp-tags.scm +122 -0
- opsmith/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- opsmith/queries/tree-sitter-languages/csharp-tags.scm +26 -0
- opsmith/queries/tree-sitter-languages/d-tags.scm +26 -0
- opsmith/queries/tree-sitter-languages/dart-tags.scm +92 -0
- opsmith/queries/tree-sitter-languages/elisp-tags.scm +5 -0
- opsmith/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- opsmith/queries/tree-sitter-languages/elm-tags.scm +19 -0
- opsmith/queries/tree-sitter-languages/gleam-tags.scm +41 -0
- opsmith/queries/tree-sitter-languages/go-tags.scm +42 -0
- opsmith/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- opsmith/queries/tree-sitter-languages/java-tags.scm +20 -0
- opsmith/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- opsmith/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- opsmith/queries/tree-sitter-languages/lua-tags.scm +34 -0
- opsmith/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- opsmith/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- opsmith/queries/tree-sitter-languages/php-tags.scm +26 -0
- opsmith/queries/tree-sitter-languages/pony-tags.scm +39 -0
- opsmith/queries/tree-sitter-languages/properties-tags.scm +5 -0
- opsmith/queries/tree-sitter-languages/python-tags.scm +14 -0
- opsmith/queries/tree-sitter-languages/ql-tags.scm +26 -0
- opsmith/queries/tree-sitter-languages/r-tags.scm +21 -0
- opsmith/queries/tree-sitter-languages/racket-tags.scm +12 -0
- opsmith/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- opsmith/queries/tree-sitter-languages/rust-tags.scm +60 -0
- opsmith/queries/tree-sitter-languages/scala-tags.scm +65 -0
- opsmith/queries/tree-sitter-languages/solidity-tags.scm +43 -0
- opsmith/queries/tree-sitter-languages/swift-tags.scm +51 -0
- opsmith/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- opsmith/queries/tree-sitter-languages/udev-tags.scm +20 -0
- opsmith/repo_map.py +572 -0
- opsmith/settings.py +26 -0
- opsmith/spinner.py +221 -0
- opsmith_cli-0.1.2a0.dist-info/METADATA +31 -0
- opsmith_cli-0.1.2a0.dist-info/RECORD +55 -0
- opsmith_cli-0.1.2a0.dist-info/WHEEL +4 -0
- opsmith_cli-0.1.2a0.dist-info/entry_points.txt +2 -0
- opsmith_cli-0.1.2a0.dist-info/licenses/LICENSE.txt +674 -0
opsmith/deployer.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
from rich import print
|
|
8
|
+
|
|
9
|
+
from opsmith.agent import AgentDeps, ModelConfig, build_agent
|
|
10
|
+
from opsmith.cloud_providers.base import CloudProviderDetail
|
|
11
|
+
from opsmith.prompts import (
|
|
12
|
+
DOCKERFILE_GENERATION_PROMPT_TEMPLATE,
|
|
13
|
+
REPO_ANALYSIS_PROMPT_TEMPLATE,
|
|
14
|
+
)
|
|
15
|
+
from opsmith.repo_map import RepoMap
|
|
16
|
+
from opsmith.settings import settings
|
|
17
|
+
from opsmith.spinner import WaitingSpinner
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ServiceTypeEnum(str, Enum):
|
|
21
|
+
"""Enum for the different types of services that can be deployed."""
|
|
22
|
+
|
|
23
|
+
BACKEND_API = "BACKEND_API"
|
|
24
|
+
FRONTEND = "FRONTEND"
|
|
25
|
+
FULL_STACK = "FULL_STACK"
|
|
26
|
+
BACKEND_WORKER = "BACKEND_WORKER"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DependencyTypeEnum(str, Enum):
|
|
30
|
+
"""Enum for the different types of infrastructure dependencies."""
|
|
31
|
+
|
|
32
|
+
DATABASE = "DATABASE"
|
|
33
|
+
CACHE = "CACHE"
|
|
34
|
+
MESSAGE_QUEUE = "MESSAGE_QUEUE"
|
|
35
|
+
SEARCH_ENGINE = "SEARCH_ENGINE"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InfrastructureDependency(BaseModel):
|
|
39
|
+
"""Describes an infrastructure dependency for a service."""
|
|
40
|
+
|
|
41
|
+
dependency_type: DependencyTypeEnum = Field(
|
|
42
|
+
..., description="The type of the infrastructure dependency."
|
|
43
|
+
)
|
|
44
|
+
provider: str = Field(
|
|
45
|
+
...,
|
|
46
|
+
description=(
|
|
47
|
+
"The specific provider of the dependency (e.g., 'postgresql', 'redis', 'rabbitmq',"
|
|
48
|
+
" 'elasticsearch')."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
version: Optional[str] = Field(
|
|
52
|
+
None, description="The version of the infrastructure dependency, if identifiable."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class EnvVarConfig(BaseModel):
|
|
57
|
+
"""Describes an environment variable configuration for a service."""
|
|
58
|
+
|
|
59
|
+
key: str = Field(..., description="The name of the environment variable.")
|
|
60
|
+
is_secret: bool = Field(
|
|
61
|
+
..., description="Whether the environment variable should be treated as a secret."
|
|
62
|
+
)
|
|
63
|
+
default_value: Optional[str] = Field(
|
|
64
|
+
None,
|
|
65
|
+
description="The default value of the environment variable, if present in the code.",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ServiceInfo(BaseModel):
|
|
70
|
+
"""Describes a single service to be deployed."""
|
|
71
|
+
|
|
72
|
+
language: str = Field(..., description="The primary programming language of the service.")
|
|
73
|
+
language_version: Optional[str] = Field(
|
|
74
|
+
None, description="The specific version of the language, if identifiable."
|
|
75
|
+
)
|
|
76
|
+
service_type: ServiceTypeEnum = Field(..., description="The type of the service.")
|
|
77
|
+
framework: Optional[str] = Field(
|
|
78
|
+
None, description="The primary framework or library used, if any."
|
|
79
|
+
)
|
|
80
|
+
build_tool: Optional[str] = Field(
|
|
81
|
+
None,
|
|
82
|
+
description=(
|
|
83
|
+
"The build tool used for the service, if identifiable (e.g., 'maven', 'gradle', 'npm',"
|
|
84
|
+
" 'webpack')."
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
infra_deps: List[InfrastructureDependency] = Field(
|
|
88
|
+
default_factory=list,
|
|
89
|
+
description="A list of infrastructure dependencies required by the services.",
|
|
90
|
+
)
|
|
91
|
+
env_vars: List[EnvVarConfig] = Field(
|
|
92
|
+
default_factory=list,
|
|
93
|
+
description="A list of environment variable configurations required by the service.",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ServiceList(BaseModel):
|
|
98
|
+
"""List of services discovered within the repository."""
|
|
99
|
+
|
|
100
|
+
services: List[ServiceInfo] = Field(
|
|
101
|
+
..., description="A list of services identified in the repository."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DeploymentConfig(ServiceList):
|
|
106
|
+
"""Describes the deployment config for the repository, listing all services."""
|
|
107
|
+
|
|
108
|
+
app_name: str = Field(..., description="The name of the application.")
|
|
109
|
+
cloud_provider: CloudProviderDetail = Field(..., discriminator="name")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class DockerfileContent(BaseModel):
|
|
113
|
+
"""Describes the dockerfile response from the agent, including the generated Dockerfile content and reasoning for the selection."""
|
|
114
|
+
|
|
115
|
+
content: str = Field(
|
|
116
|
+
...,
|
|
117
|
+
description="The final generated Dockerfile content.",
|
|
118
|
+
)
|
|
119
|
+
reason: Optional[str] = Field(
|
|
120
|
+
None, description="The reasoning for the selection of the final Dockerfile content."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Deployer:
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
src_dir: str,
|
|
128
|
+
model_config: ModelConfig,
|
|
129
|
+
verbose: bool = False,
|
|
130
|
+
instrument: bool = False,
|
|
131
|
+
):
|
|
132
|
+
self.deployments_path = Path(src_dir).joinpath(settings.deployments_dir)
|
|
133
|
+
self.config_file_name = "config.yml"
|
|
134
|
+
self.config_file_path = self.deployments_path / self.config_file_name
|
|
135
|
+
self.agent_deps = AgentDeps(src_dir=Path(src_dir))
|
|
136
|
+
self.agent = build_agent(model_config=model_config, instrument=instrument)
|
|
137
|
+
self.repo_map = RepoMap(
|
|
138
|
+
src_dir=src_dir,
|
|
139
|
+
verbose=verbose,
|
|
140
|
+
)
|
|
141
|
+
self.verbose = verbose
|
|
142
|
+
|
|
143
|
+
def save_deployment_config(self, deployment_config: DeploymentConfig):
|
|
144
|
+
"""Saves the deployment configuration to a YAML file."""
|
|
145
|
+
self.deployments_path.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
with open(self.config_file_path, "w") as f:
|
|
147
|
+
yaml.dump(deployment_config.model_dump(mode="json"), f, indent=2)
|
|
148
|
+
print(
|
|
149
|
+
f"\n[bold blue]Deployment configuration saved to: {self.config_file_path}[/bold blue]"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def get_deployment_config(self) -> Optional[DeploymentConfig]:
|
|
153
|
+
"""Loads the deployment configuration from a YAML file."""
|
|
154
|
+
if not self.config_file_path.exists():
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
with open(self.config_file_path, "r") as f:
|
|
158
|
+
config_data = yaml.safe_load(f)
|
|
159
|
+
|
|
160
|
+
if config_data:
|
|
161
|
+
return DeploymentConfig(**config_data)
|
|
162
|
+
else:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def generate_dockerfiles(
|
|
166
|
+
self,
|
|
167
|
+
):
|
|
168
|
+
"""Generates Dockerfiles for each service in the deployment configuration."""
|
|
169
|
+
print("\n[bold blue]Starting Dockerfile generation...[/bold blue]")
|
|
170
|
+
deployment_config = self.get_deployment_config()
|
|
171
|
+
|
|
172
|
+
buildable_service_types = [
|
|
173
|
+
ServiceTypeEnum.BACKEND_API,
|
|
174
|
+
ServiceTypeEnum.FULL_STACK,
|
|
175
|
+
ServiceTypeEnum.BACKEND_WORKER,
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
for idx, service in enumerate(deployment_config.services):
|
|
179
|
+
if service.service_type not in buildable_service_types:
|
|
180
|
+
print(
|
|
181
|
+
f"\n[bold yellow]Dockerfile not needed for service {service.service_type},"
|
|
182
|
+
" skipping.[/bold yellow]"
|
|
183
|
+
)
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
service_name_slug = f"{service.language}_{service.service_type.value}".replace(
|
|
187
|
+
" ", "_"
|
|
188
|
+
).lower()
|
|
189
|
+
service_dir_name = "images"
|
|
190
|
+
service_dir_path = self.deployments_path / service_dir_name / service_name_slug
|
|
191
|
+
service_dir_path.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
dockerfile_path_abs = service_dir_path / "Dockerfile"
|
|
193
|
+
print(
|
|
194
|
+
f"\n[bold]Generating Dockerfile for service {idx + 1}:"
|
|
195
|
+
f" {service.language} ({service.service_type})...[/bold]"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
service_info_yaml = yaml.dump(service.model_dump(mode="json"), indent=2)
|
|
199
|
+
prompt = DOCKERFILE_GENERATION_PROMPT_TEMPLATE.format(
|
|
200
|
+
service_info_yaml=service_info_yaml,
|
|
201
|
+
repo_map_str=self.repo_map.get_repo_map(),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
response = self.agent.run_sync(
|
|
205
|
+
prompt, deps=self.agent_deps, output_type=DockerfileContent
|
|
206
|
+
)
|
|
207
|
+
dockerfile_content = response.output
|
|
208
|
+
|
|
209
|
+
# Write ensures the dockerfile content received from the agent to the deployments_dir.
|
|
210
|
+
with open(dockerfile_path_abs, "w", encoding="utf-8") as f:
|
|
211
|
+
f.write(dockerfile_content.content)
|
|
212
|
+
print(f"[green]Dockerfile saved to: {dockerfile_path_abs}[/green]")
|
|
213
|
+
|
|
214
|
+
print("\n[bold blue]Dockerfile generation complete.[/bold blue]")
|
|
215
|
+
|
|
216
|
+
def detect_services(self) -> ServiceList:
|
|
217
|
+
"""
|
|
218
|
+
Scans the repository to determine the services to be deployed, using the AI agent.
|
|
219
|
+
|
|
220
|
+
Generates a repository map, then uses an AI agent with a file reading tool
|
|
221
|
+
to identify services and their characteristics.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
A ServiceList object detailing the services to be deployed.
|
|
225
|
+
"""
|
|
226
|
+
repo_map_str = self.repo_map.get_repo_map()
|
|
227
|
+
if self.verbose:
|
|
228
|
+
print("Repo map generated:")
|
|
229
|
+
|
|
230
|
+
prompt = REPO_ANALYSIS_PROMPT_TEMPLATE.format(repo_map_str=repo_map_str)
|
|
231
|
+
|
|
232
|
+
print("Calling AI agent to analyse the repo and determine the services...")
|
|
233
|
+
with WaitingSpinner(text="Waiting for the LLM", delay=0.1):
|
|
234
|
+
run_result = self.agent.run_sync(prompt, output_type=ServiceList, deps=self.agent_deps)
|
|
235
|
+
|
|
236
|
+
return run_result.output
|
opsmith/git_repo.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
import git
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GitRepo:
|
|
10
|
+
def __init__(self, root_dir: Path):
|
|
11
|
+
"""
|
|
12
|
+
Initialize a Git repository object for the specified directory, or its parent directories
|
|
13
|
+
if it is a subdirectory, ensuring it belongs to an actual Git repository. If the provided
|
|
14
|
+
directory is not within a Git repository, it exits the program with an error.
|
|
15
|
+
|
|
16
|
+
:param root_dir: The root directory or subdirectory intended for Git repository initialization.
|
|
17
|
+
:type root_dir: Path
|
|
18
|
+
|
|
19
|
+
:raises git.exc.InvalidGitRepositoryError: If the specified directory or its parent directories
|
|
20
|
+
do not contain a valid Git repository.
|
|
21
|
+
:raises typer.Exit: Exits the program if the directory is not part of a valid Git repository.
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
# Initialize repo object, searching upwards from root_dir if it's a subdirectory
|
|
25
|
+
self.repo = git.Repo(str(root_dir), search_parent_directories=True)
|
|
26
|
+
|
|
27
|
+
except git.exc.InvalidGitRepositoryError:
|
|
28
|
+
print("[bold red]Not a git repository or git is not found in PATH[/bold red].")
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
def get_git_tracked_files(self, src_dirs: List[str]) -> List[Path]:
|
|
32
|
+
"""
|
|
33
|
+
Retrieves a list of absolute file paths that are tracked by Git within the
|
|
34
|
+
specified source directories.
|
|
35
|
+
|
|
36
|
+
The function searches for files tracked by Git within the provided directories,
|
|
37
|
+
respecting the .gitignore file. If there are no tracked files, an empty list
|
|
38
|
+
is returned. The file paths returned are converted into absolute paths relative
|
|
39
|
+
to the Git repository's root directory.
|
|
40
|
+
|
|
41
|
+
:param src_dirs: List of source directories (as strings) to scan for Git-tracked
|
|
42
|
+
files.
|
|
43
|
+
:type src_dirs: List[str]
|
|
44
|
+
|
|
45
|
+
:return: A list of absolute file paths for files tracked by Git within the
|
|
46
|
+
specified source directories.
|
|
47
|
+
:rtype: List[Path]
|
|
48
|
+
"""
|
|
49
|
+
# Searching upwards from root_dir if it's a subdirectory
|
|
50
|
+
git_root = Path(self.repo.working_dir)
|
|
51
|
+
|
|
52
|
+
# List tracked, cached, and other files (respecting .gitignore)
|
|
53
|
+
# The paths are relative to the git_root.
|
|
54
|
+
ls_files_args = ["-c", "--exclude-standard"]
|
|
55
|
+
ls_files_args.extend(src_dirs)
|
|
56
|
+
|
|
57
|
+
tracked_files_str = self.repo.git.ls_files(*ls_files_args)
|
|
58
|
+
|
|
59
|
+
if not tracked_files_str: # Handle case where there are no tracked files
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
relative_paths = tracked_files_str.strip().split("\n")
|
|
63
|
+
|
|
64
|
+
# Construct absolute paths and filter out potential empty strings from split
|
|
65
|
+
absolute_paths = [git_root / p for p in relative_paths if p]
|
|
66
|
+
return absolute_paths
|
opsmith/main.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Annotated, Optional
|
|
3
|
+
|
|
4
|
+
import inquirer
|
|
5
|
+
import logfire
|
|
6
|
+
import typer
|
|
7
|
+
import yaml
|
|
8
|
+
from rich import print
|
|
9
|
+
|
|
10
|
+
from opsmith.agent import AVAILABLE_MODELS_XREF, ModelConfig
|
|
11
|
+
from opsmith.cloud_providers import CLOUD_PROVIDER_REGISTRY
|
|
12
|
+
from opsmith.cloud_providers.base import CloudCredentialsError, CloudProviderEnum
|
|
13
|
+
from opsmith.deployer import Deployer, DeploymentConfig # Import the new Deployer class
|
|
14
|
+
from opsmith.repo_map import RepoMap
|
|
15
|
+
|
|
16
|
+
app = typer.Typer()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_model_arg(model: str) -> ModelConfig:
|
|
20
|
+
"""
|
|
21
|
+
Fetches the configuration corresponding to the given model name.
|
|
22
|
+
|
|
23
|
+
Attempts to retrieve the configuration for the provided model from
|
|
24
|
+
the mapping of available models. If the model name is not found in
|
|
25
|
+
the mapping, an error is raised indicating that the model is unsupported.
|
|
26
|
+
|
|
27
|
+
:param model: The name of the model for which the configuration is required.
|
|
28
|
+
:type model: str
|
|
29
|
+
|
|
30
|
+
:return: The configuration corresponding to the provided model name.
|
|
31
|
+
:rtype: ModelConfig
|
|
32
|
+
|
|
33
|
+
:raises KeyError: If the given model name is not found in the available models mapping.
|
|
34
|
+
:raises typer.BadParameter: If the provided model name is unsupported.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
return AVAILABLE_MODELS_XREF[model]
|
|
38
|
+
except KeyError:
|
|
39
|
+
raise typer.BadParameter(
|
|
40
|
+
f"Unsupported model name: {model}, must be one of: {AVAILABLE_MODELS_XREF.keys()}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def api_key_callback(ctx: typer.Context, value: str):
|
|
45
|
+
"""
|
|
46
|
+
This function serves as a callback for validating and processing an API key when
|
|
47
|
+
used in conjunction with a command-line interface. The function checks whether
|
|
48
|
+
the mandatory `--model` option is set before associating it with the provided
|
|
49
|
+
API key. If validation passes, it ensures the API key authentication process
|
|
50
|
+
is triggered for the specified model configuration.
|
|
51
|
+
|
|
52
|
+
Raises a BadParameter error if `--model` was not supplied before `--api-key`.
|
|
53
|
+
|
|
54
|
+
:param ctx: The Typer context object that contains information about the
|
|
55
|
+
current command execution context, including provided options and other
|
|
56
|
+
runtime parameters.
|
|
57
|
+
:type ctx: typer.Context
|
|
58
|
+
:param value: The API key provided by the user via the `--api-key` option
|
|
59
|
+
during the command-line execution.
|
|
60
|
+
:type value: str
|
|
61
|
+
:return: The validated API key after ensuring it is associated with the
|
|
62
|
+
specified model configuration.
|
|
63
|
+
:rtype: str
|
|
64
|
+
"""
|
|
65
|
+
if "model" not in ctx.params:
|
|
66
|
+
raise typer.BadParameter("The --model option must be specified before --api-key.")
|
|
67
|
+
model_config = ctx.params["model"]
|
|
68
|
+
model_config.ensure_auth(value)
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.callback()
|
|
73
|
+
def main(
|
|
74
|
+
ctx: typer.Context,
|
|
75
|
+
model: Annotated[
|
|
76
|
+
ModelConfig,
|
|
77
|
+
typer.Option(
|
|
78
|
+
parser=parse_model_arg,
|
|
79
|
+
help="The LLM model to be used for by the AI Agent.",
|
|
80
|
+
),
|
|
81
|
+
],
|
|
82
|
+
api_key: Annotated[
|
|
83
|
+
str,
|
|
84
|
+
typer.Option(
|
|
85
|
+
callback=api_key_callback,
|
|
86
|
+
help=(
|
|
87
|
+
"The API KEY to be used for by the AI Agent. This is the API key for the specified"
|
|
88
|
+
" model."
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
],
|
|
92
|
+
logfire_token: Optional[str] = typer.Option(
|
|
93
|
+
default=None,
|
|
94
|
+
help=(
|
|
95
|
+
"Logfire token to be used for logging. If not provided, logs will not be sent to"
|
|
96
|
+
" Logfire."
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
src_dir: Optional[str] = typer.Option(
|
|
100
|
+
default=None,
|
|
101
|
+
help="Source directory to be used by the command. Defaults to current working directory.",
|
|
102
|
+
),
|
|
103
|
+
verbose: bool = typer.Option(
|
|
104
|
+
False, "--verbose", "-v", help="Enable verbose output for repo map generation."
|
|
105
|
+
),
|
|
106
|
+
):
|
|
107
|
+
"""
|
|
108
|
+
AI Devops engineer in your terminal.
|
|
109
|
+
"""
|
|
110
|
+
if logfire_token:
|
|
111
|
+
logfire.configure(token=logfire_token, scrubbing=False)
|
|
112
|
+
ctx.obj = {
|
|
113
|
+
"deployer": Deployer(
|
|
114
|
+
src_dir=src_dir or os.getcwd(),
|
|
115
|
+
model_config=model,
|
|
116
|
+
verbose=verbose,
|
|
117
|
+
instrument=bool(logfire_token),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.command()
|
|
123
|
+
def setup(ctx: typer.Context):
|
|
124
|
+
"""
|
|
125
|
+
Setup the deployment configuration for the repository.
|
|
126
|
+
Identifies services, their languages, types, and frameworks.
|
|
127
|
+
"""
|
|
128
|
+
deployer = ctx.obj["deployer"]
|
|
129
|
+
deployment_config = deployer.get_deployment_config()
|
|
130
|
+
|
|
131
|
+
cloud_details = None
|
|
132
|
+
current_provider_name = None
|
|
133
|
+
services = []
|
|
134
|
+
services_updated = False
|
|
135
|
+
is_update = bool(deployment_config)
|
|
136
|
+
app_name = None
|
|
137
|
+
|
|
138
|
+
if is_update:
|
|
139
|
+
print("\n[bold yellow]Existing deployment configuration found.[/bold yellow]")
|
|
140
|
+
print("\n[bold green]Current Deployment Configuration:[/bold green]")
|
|
141
|
+
print(yaml.dump(deployment_config.model_dump(mode="json")))
|
|
142
|
+
|
|
143
|
+
update_answers = inquirer.prompt(
|
|
144
|
+
[
|
|
145
|
+
inquirer.Confirm(
|
|
146
|
+
"update", message="Do you want to update the configuration?", default=True
|
|
147
|
+
)
|
|
148
|
+
]
|
|
149
|
+
)
|
|
150
|
+
if not update_answers or not update_answers.get("update"):
|
|
151
|
+
print("Skipping configuration update.")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# Pre-fill with existing data
|
|
155
|
+
app_name = deployment_config.app_name
|
|
156
|
+
cloud_details = deployment_config.cloud_provider
|
|
157
|
+
current_provider_name = deployment_config.cloud_provider.name
|
|
158
|
+
services = deployment_config.services
|
|
159
|
+
else:
|
|
160
|
+
print("No existing deployment configuration found. Starting analysis...")
|
|
161
|
+
|
|
162
|
+
app_name_questions = [
|
|
163
|
+
inquirer.Text("app_name", message="Enter the application name", default=app_name),
|
|
164
|
+
]
|
|
165
|
+
app_name_answers = inquirer.prompt(app_name_questions)
|
|
166
|
+
if not app_name_answers or not app_name_answers.get("app_name"):
|
|
167
|
+
print("[bold red]Application name is required. Aborting.[/bold red]")
|
|
168
|
+
raise typer.Exit(code=1)
|
|
169
|
+
app_name = app_name_answers["app_name"]
|
|
170
|
+
|
|
171
|
+
# Cloud Provider Selection
|
|
172
|
+
provider_questions = [
|
|
173
|
+
inquirer.List(
|
|
174
|
+
"cloud_provider",
|
|
175
|
+
message="Select the cloud provider for deployment",
|
|
176
|
+
choices=[provider.value for provider in CloudProviderEnum],
|
|
177
|
+
default=current_provider_name,
|
|
178
|
+
),
|
|
179
|
+
]
|
|
180
|
+
provider_answers = inquirer.prompt(provider_questions)
|
|
181
|
+
if not provider_answers:
|
|
182
|
+
print("[bold red]Cloud provider selection is required. Aborting.[/bold red]")
|
|
183
|
+
raise typer.Exit(code=1)
|
|
184
|
+
|
|
185
|
+
selected_provider_value = provider_answers["cloud_provider"]
|
|
186
|
+
selected_provider_enum = CloudProviderEnum(selected_provider_value)
|
|
187
|
+
|
|
188
|
+
# Get cloud details if new or changed
|
|
189
|
+
if not is_update or selected_provider_enum.name != current_provider_name:
|
|
190
|
+
print(f"Initializing {selected_provider_enum.name} provider...")
|
|
191
|
+
provider_class = CLOUD_PROVIDER_REGISTRY[selected_provider_enum]
|
|
192
|
+
try:
|
|
193
|
+
provider_instance = provider_class()
|
|
194
|
+
cloud_details = provider_instance.get_account_details()
|
|
195
|
+
except CloudCredentialsError as e:
|
|
196
|
+
print(f"[bold red]Cloud provider authentication/configuration error:\n{e}[/bold red]")
|
|
197
|
+
raise typer.Exit(code=1)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(
|
|
200
|
+
"[bold red]An unexpected error occurred while initializing cloud provider or"
|
|
201
|
+
f" fetching details: {e}. Aborting.[/bold red]"
|
|
202
|
+
)
|
|
203
|
+
raise typer.Exit(code=1)
|
|
204
|
+
|
|
205
|
+
# Service Detection
|
|
206
|
+
if is_update:
|
|
207
|
+
service_questions = [
|
|
208
|
+
inquirer.Confirm(
|
|
209
|
+
"update_services", message="Do you want to re-scan for services?", default=False
|
|
210
|
+
)
|
|
211
|
+
]
|
|
212
|
+
service_answers = inquirer.prompt(service_questions)
|
|
213
|
+
if service_answers and service_answers.get("update_services"):
|
|
214
|
+
services_updated = True
|
|
215
|
+
else: # New config, so we must scan
|
|
216
|
+
services_updated = True
|
|
217
|
+
|
|
218
|
+
if services_updated:
|
|
219
|
+
print("Scanning your codebase now to detect services, frameworks, and languages...")
|
|
220
|
+
service_list_obj = deployer.detect_services()
|
|
221
|
+
services = service_list_obj.services
|
|
222
|
+
|
|
223
|
+
# Create/Update and Save Configuration
|
|
224
|
+
final_deployment_config = DeploymentConfig(
|
|
225
|
+
app_name=app_name,
|
|
226
|
+
cloud_provider=cloud_details,
|
|
227
|
+
services=services,
|
|
228
|
+
)
|
|
229
|
+
deployer.save_deployment_config(final_deployment_config)
|
|
230
|
+
|
|
231
|
+
if is_update:
|
|
232
|
+
print("\n[bold green]Deployment configuration updated:[/bold green]")
|
|
233
|
+
else:
|
|
234
|
+
print("\n[bold green]Created Deployment Configuration:[/bold green]")
|
|
235
|
+
print(yaml.dump(final_deployment_config.model_dump(mode="json")))
|
|
236
|
+
|
|
237
|
+
# Generate Dockerfiles if services were changed
|
|
238
|
+
if services_updated:
|
|
239
|
+
print("\n[bold blue]Services were updated. Generating Dockerfiles...[/bold blue]")
|
|
240
|
+
deployer.generate_dockerfiles()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@app.command()
|
|
244
|
+
def deploy(ctx: typer.Context):
|
|
245
|
+
""""""
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@app.command()
|
|
250
|
+
def repomap(ctx: typer.Context):
|
|
251
|
+
"""
|
|
252
|
+
Generates a map of the repository, showing important files and code elements.
|
|
253
|
+
"""
|
|
254
|
+
print("Generating repo map now...")
|
|
255
|
+
if ctx.parent.params["src_dir"]:
|
|
256
|
+
current_dir_str = ctx.parent.params["src_dir"]
|
|
257
|
+
else:
|
|
258
|
+
current_dir_str = os.getcwd()
|
|
259
|
+
|
|
260
|
+
repo_mapper = RepoMap(
|
|
261
|
+
src_dir=current_dir_str,
|
|
262
|
+
verbose=ctx.parent.params["verbose"],
|
|
263
|
+
)
|
|
264
|
+
repo_map_str = repo_mapper.get_repo_map()
|
|
265
|
+
|
|
266
|
+
if repo_map_str:
|
|
267
|
+
typer.echo(repo_map_str)
|
|
268
|
+
else:
|
|
269
|
+
typer.echo("No git-tracked files found in this repository or failed to generate map.")
|
opsmith/prompts.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
REPO_ANALYSIS_PROMPT_TEMPLATE = """
|
|
2
|
+
You are an expert DevOps engineer. Your task is to analyze the provided repository
|
|
3
|
+
and identify all distinct services that need to be deployed.
|
|
4
|
+
|
|
5
|
+
First, review the repository map provided below to get an overview of the project structure,
|
|
6
|
+
key files, and important symbols.
|
|
7
|
+
|
|
8
|
+
{repo_map_str}
|
|
9
|
+
|
|
10
|
+
Based on this map and the content of relevant files (which you can request using the
|
|
11
|
+
'read_file_content' tool), identify the services. For each service, determine:
|
|
12
|
+
1. `language`: The primary programming language of the service (e.g., "python", "javascript", "java").
|
|
13
|
+
2. `language_version`: The specific version of the language, if identifiable (e.g., "3.9", "17", "ES2020").
|
|
14
|
+
3. `service_type`: The type of the service. Must be one of: "backend_api", "backend_worker", "frontend", "full_stack".
|
|
15
|
+
4. `framework`: The primary framework or library used, if any (e.g., "django", "react", "spring boot", "celery").
|
|
16
|
+
5. `build_tool`: The build tool used for the service, if identifiable (e.g., "maven", "gradle", "npm", "webpack", "pip", "poetry").
|
|
17
|
+
6. `infra_deps`: A list of infrastructure dependencies required by the service. For each dependency, specify:
|
|
18
|
+
* `dependency_type`: The type of the dependency. Must be one of: "database", "cache", "message_queue", "search_engine".
|
|
19
|
+
* `provider`: The specific provider of the dependency (e.g., "postgresql", "redis", "rabbitmq", "elasticsearch").
|
|
20
|
+
* `version`: The version of the dependency, if identifiable.
|
|
21
|
+
7. `env_vars`: A list of environment variable configurations required by the service. For each variable, specify:
|
|
22
|
+
* `key`: The name of the environment variable.
|
|
23
|
+
* `is_secret`: A boolean indicating if the variable should be treated as a secret (e.g., contains API keys, passwords, or other sensitive information).
|
|
24
|
+
* `default_value`: The default value for the variable, if one is provided in the code.
|
|
25
|
+
|
|
26
|
+
Return the information as a list of services.
|
|
27
|
+
* Read the dependencies list from files like `requirements.txt`, `package.json`, `pom.xml`, `build.gradle`, etc., to get an idea of potential frameworks and infrastructure dependencies.
|
|
28
|
+
* Do not rely on the repository map and dependency information alone; read relevant files such as entry points to figure out the services.
|
|
29
|
+
* Look for configuration files or code that initializes connections to databases, caches, message queues, or search engines.
|
|
30
|
+
* Scan the code for environment variable usage (e.g., `os.environ.get` in Python, `process.env` in Node.js or settings files) to identify required configurations. Keywords like 'SECRET', 'KEY', 'TOKEN', 'PASSWORD' in the variable name often indicate a secret.
|
|
31
|
+
* Read as many files as needed until you are sure about the service and infrastructure dependency details.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
DOCKERFILE_GENERATION_PROMPT_TEMPLATE = """
|
|
35
|
+
You are an expert DevOps engineer. Your task is to generate a Dockerfile for the
|
|
36
|
+
service described below.
|
|
37
|
+
|
|
38
|
+
Service Details:
|
|
39
|
+
{service_info_yaml}
|
|
40
|
+
|
|
41
|
+
Repository Map:
|
|
42
|
+
{repo_map_str}
|
|
43
|
+
|
|
44
|
+
Your task is to generate an optimized and production-ready Dockerfile for this service.
|
|
45
|
+
After generating the Dockerfile content, you MUST validate it using the `build_dockerfile` tool.
|
|
46
|
+
The `build_dockerfile` tool will first attempt to build the image. If the build is successful,
|
|
47
|
+
it will then attempt to run the image.
|
|
48
|
+
|
|
49
|
+
Analyze the output from `build_dockerfile`:
|
|
50
|
+
1. If the **build fails** (indicated by errors in the build output), revise the Dockerfile
|
|
51
|
+
content based on the error messages and try building again by calling `build_dockerfile`
|
|
52
|
+
with the updated content.
|
|
53
|
+
2. If the **build succeeds but running the image fails**, examine the run output.
|
|
54
|
+
- If the failure is due to issues like **missing packages, command not found, file not found
|
|
55
|
+
within the container, or incorrect entrypoint/cmd**, revise the Dockerfile to fix these
|
|
56
|
+
issues and call `build_dockerfile` again.
|
|
57
|
+
- If the failure is due to **missing environment variables, inability to connect to external
|
|
58
|
+
services (like databases or other APIs), port conflicts, or similar runtime configuration
|
|
59
|
+
issues that are not part of the Dockerfile's direct responsibility for package installation
|
|
60
|
+
or command execution**, you can consider the Dockerfile itself valid for this stage.
|
|
61
|
+
Repeat this process until the Dockerfile builds successfully and, if it runs, does not fail due to
|
|
62
|
+
fixable Dockerfile issues (like missing packages or commands).
|
|
63
|
+
|
|
64
|
+
Ensure the final Dockerfile:
|
|
65
|
+
- Uses an appropriate base image.
|
|
66
|
+
- Copies only necessary files.
|
|
67
|
+
- Sets up the correct working directory.
|
|
68
|
+
- Installs dependencies efficiently.
|
|
69
|
+
- Exposes the correct port (if applicable for the service type, e.g., backend-api, frontend, full_stack).
|
|
70
|
+
- Defines the correct entrypoint or command.
|
|
71
|
+
- Follows Docker best practices (e.g., multi-stage builds if beneficial, non-root user).
|
|
72
|
+
|
|
73
|
+
Once the Dockerfile is successfully validated (built and, if run, passed the runtime checks for fixable errors)
|
|
74
|
+
with the `build_dockerfile` tool, return only the final, validated Dockerfile content.
|
|
75
|
+
If more information is required at any stage, use the `read_file_content` tool.
|
|
76
|
+
"""
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
These scm files are all adapted from the github repositories listed here:
|
|
2
|
+
|
|
3
|
+
https://github.com/Goldziher/tree-sitter-language-pack/blob/main/sources/language_definitions.json
|
|
4
|
+
|
|
5
|
+
See this URL for information on the licenses of each repo:
|
|
6
|
+
|
|
7
|
+
https://github.com/Goldziher/tree-sitter-language-pack/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
(struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class
|
|
2
|
+
|
|
3
|
+
(declaration type: (union_specifier name: (type_identifier) @name.definition.class)) @definition.class
|
|
4
|
+
|
|
5
|
+
(function_declarator declarator: (identifier) @name.definition.function) @definition.function
|
|
6
|
+
|
|
7
|
+
(type_definition declarator: (type_identifier) @name.definition.type) @definition.type
|
|
8
|
+
|
|
9
|
+
(enum_specifier name: (type_identifier) @name.definition.type) @definition.type
|