/ cli / commands / plugin_cmd / add_cmd.py
add_cmd.py
  1  import click
  2  import pathlib
  3  import toml
  4  
  5  from cli.utils import get_formatted_names, error_exit
  6  from .install_cmd import install_plugin
  7  
  8  
  9  def ensure_directory_exists(path: pathlib.Path):
 10      """Creates a directory if it doesn't exist."""
 11      path.mkdir(parents=True, exist_ok=True)
 12  
 13  def _get_plugin_type_from_pyproject(source_path: pathlib.Path) -> str | None:
 14      """Reads pyproject.toml from source_path and returns the plugin type."""
 15      pyproject_path = source_path / "pyproject.toml"
 16      if not pyproject_path.is_file():
 17          click.echo(
 18              click.style(
 19                  f"Warning: pyproject.toml not found at {pyproject_path}. Cannot determine plugin type automatically.",
 20                  fg="yellow",
 21              )
 22          )
 23          return None
 24      try:
 25          with open(pyproject_path, "r", encoding="utf-8") as f:
 26              data = toml.load(f)
 27          project_name = data.get("project", {}).get("name", "").strip().replace("-", "_")
 28          plugin_type = (
 29              data.get("tool", {}).get(project_name, {}).get("metadata", {}).get("type")
 30          )
 31          if plugin_type:
 32              return plugin_type.strip()
 33          click.echo(
 34              click.style(
 35                  f"Warning: Could not find plugin type for '{project_name}' in {pyproject_path}.",
 36                  fg="yellow",
 37              )
 38          )
 39          return None
 40      except Exception as e:
 41          click.echo(click.style(f"Error parsing {pyproject_path}: {e}", fg="red"))
 42          return None
 43  
 44  
 45  
 46  @click.command("add")
 47  @click.argument("component_name")
 48  @click.option(
 49      "--plugin",
 50      "plugin_source",
 51      required=True,
 52      help="Plugin source: installed module name, local path, or Git URL.",
 53  )
 54  @click.option(
 55      "--install-command",
 56      "installer_command",
 57      help="Command to use to install a python package. Must follow the format 'command {package} args', by default 'pip3 install {package}'. Can also be set through the environment variable SAM_PLUGIN_INSTALL_COMMAND.",
 58  )
 59  def add_plugin_component_cmd(
 60      component_name: str, plugin_source: str, installer_command: str | None = None
 61  ):
 62      """Installs the plugin and creates a new component instance from a specified plugin source."""
 63  
 64      click.echo(
 65          f"Attempting to add component '{component_name}' using plugin source '{plugin_source}'..."
 66      )
 67  
 68      module_name, plugin_path = install_plugin(plugin_source, installer_command)
 69  
 70      plugin_config_path = plugin_path / "config.yaml"
 71      plugin_pyproject_path = plugin_path / "pyproject.toml"
 72  
 73      if not plugin_pyproject_path.is_file():
 74          return error_exit(
 75              f"Error: pyproject.toml not found in plugin '{module_name}' at expected path {plugin_pyproject_path}"
 76          )
 77  
 78      if not plugin_config_path.is_file():
 79          return error_exit(
 80              f"Error: config.yaml not found in plugin '{module_name}' at expected path {plugin_config_path}"
 81          )
 82      try:
 83          plugin_config_content = plugin_config_path.read_text(encoding="utf-8")
 84      except Exception as e:
 85          return error_exit(
 86              f"Error reading plugin config.yaml from {plugin_config_path}: {e}"
 87          )
 88  
 89      component_formats = get_formatted_names(component_name)
 90  
 91      component_replacements = {
 92          "__COMPONENT_SNAKE_CASE_NAME__": component_formats["SNAKE_CASE_NAME"],
 93          "__COMPONENT_UPPER_SNAKE_CASE_NAME__": component_formats[
 94              "SNAKE_UPPER_CASE_NAME"
 95          ],
 96          "__COMPONENT_KEBAB_CASE_NAME__": component_formats["KEBAB_CASE_NAME"],
 97          "__COMPONENT_PASCAL_CASE_NAME__": component_formats["PASCAL_CASE_NAME"],
 98          "__COMPONENT_SPACED_NAME__": component_formats["SPACED_NAME"],
 99          "__COMPONENT_SPACED_CAPITALIZED_NAME__": component_formats[
100              "SPACED_CAPITALIZED_NAME"
101          ]
102      }
103  
104      processed_config_content = plugin_config_content
105      for placeholder, value in component_replacements.items():
106          processed_config_content = processed_config_content.replace(placeholder, value)
107  
108      plugin_type = _get_plugin_type_from_pyproject(plugin_path)
109      if plugin_type == "agent" or plugin_type == "tool":
110          target_dir = pathlib.Path("configs/agents")
111      elif plugin_type == "gateway":
112          target_dir = pathlib.Path("configs/gateways")
113      elif plugin_type == "workflow":
114          target_dir = pathlib.Path("configs/workflows")
115      else:
116          target_dir = pathlib.Path("configs/plugins")
117  
118      try:
119          ensure_directory_exists(target_dir)
120      except Exception as e:
121          return error_exit(f"Error creating target directory {target_dir}: {e}")
122  
123      target_filename = f"{component_formats['KEBAB_CASE_NAME']}.yaml"
124      target_path = target_dir / target_filename
125  
126      try:
127          with open(target_path, "w", encoding="utf-8") as f:
128              f.write(processed_config_content)
129          click.echo(f"  Created component configuration: {target_path}")
130          click.echo(
131              click.style(
132                  f"Component '{component_name}' created successfully from plugin '{module_name}'.",
133                  fg="green",
134              )
135          )
136      except IOError as e:
137          return error_exit(
138              f"Error writing component configuration file {target_path}: {e}"
139          )