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 )