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.
Files changed (55) hide show
  1. opsmith/__init__.py +0 -0
  2. opsmith/agent.py +243 -0
  3. opsmith/cloud_providers/__init__.py +10 -0
  4. opsmith/cloud_providers/aws.py +46 -0
  5. opsmith/cloud_providers/base.py +57 -0
  6. opsmith/cloud_providers/gcp.py +67 -0
  7. opsmith/constants.py +175 -0
  8. opsmith/deployer.py +236 -0
  9. opsmith/git_repo.py +66 -0
  10. opsmith/main.py +269 -0
  11. opsmith/prompts.py +76 -0
  12. opsmith/queries/tree-sitter-languages/README.md +7 -0
  13. opsmith/queries/tree-sitter-languages/arduino-tags.scm +5 -0
  14. opsmith/queries/tree-sitter-languages/c-tags.scm +9 -0
  15. opsmith/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  16. opsmith/queries/tree-sitter-languages/chatito-tags.scm +16 -0
  17. opsmith/queries/tree-sitter-languages/commonlisp-tags.scm +122 -0
  18. opsmith/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  19. opsmith/queries/tree-sitter-languages/csharp-tags.scm +26 -0
  20. opsmith/queries/tree-sitter-languages/d-tags.scm +26 -0
  21. opsmith/queries/tree-sitter-languages/dart-tags.scm +92 -0
  22. opsmith/queries/tree-sitter-languages/elisp-tags.scm +5 -0
  23. opsmith/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  24. opsmith/queries/tree-sitter-languages/elm-tags.scm +19 -0
  25. opsmith/queries/tree-sitter-languages/gleam-tags.scm +41 -0
  26. opsmith/queries/tree-sitter-languages/go-tags.scm +42 -0
  27. opsmith/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  28. opsmith/queries/tree-sitter-languages/java-tags.scm +20 -0
  29. opsmith/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  30. opsmith/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  31. opsmith/queries/tree-sitter-languages/lua-tags.scm +34 -0
  32. opsmith/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  33. opsmith/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  34. opsmith/queries/tree-sitter-languages/php-tags.scm +26 -0
  35. opsmith/queries/tree-sitter-languages/pony-tags.scm +39 -0
  36. opsmith/queries/tree-sitter-languages/properties-tags.scm +5 -0
  37. opsmith/queries/tree-sitter-languages/python-tags.scm +14 -0
  38. opsmith/queries/tree-sitter-languages/ql-tags.scm +26 -0
  39. opsmith/queries/tree-sitter-languages/r-tags.scm +21 -0
  40. opsmith/queries/tree-sitter-languages/racket-tags.scm +12 -0
  41. opsmith/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  42. opsmith/queries/tree-sitter-languages/rust-tags.scm +60 -0
  43. opsmith/queries/tree-sitter-languages/scala-tags.scm +65 -0
  44. opsmith/queries/tree-sitter-languages/solidity-tags.scm +43 -0
  45. opsmith/queries/tree-sitter-languages/swift-tags.scm +51 -0
  46. opsmith/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  47. opsmith/queries/tree-sitter-languages/udev-tags.scm +20 -0
  48. opsmith/repo_map.py +572 -0
  49. opsmith/settings.py +26 -0
  50. opsmith/spinner.py +221 -0
  51. opsmith_cli-0.1.2a0.dist-info/METADATA +31 -0
  52. opsmith_cli-0.1.2a0.dist-info/RECORD +55 -0
  53. opsmith_cli-0.1.2a0.dist-info/WHEEL +4 -0
  54. opsmith_cli-0.1.2a0.dist-info/entry_points.txt +2 -0
  55. 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,5 @@
1
+ (function_declarator
2
+ declarator: (identifier) @name.definition.function) @definition.function
3
+
4
+ (call_expression
5
+ function: (identifier) @name.reference.call) @reference.call
@@ -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