solace-agent-mesh 1.4.0__py3-none-any.whl → 1.4.1__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.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/ae4415af.16cc58d3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{main.1de3da6a.js → main.9bc1a102.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/{runtime~main.3188e049.js → runtime~main.f2b4ea70.js} +1 -1
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +20 -5
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
- solace_agent_mesh/assets/docs/lunr-index-1758036158289.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1758036158289.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/plugin_cmd/__init__.py +2 -0
- solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +10 -245
- solace_agent_mesh/cli/commands/plugin_cmd/install_cmd.py +283 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{main-Dq4AJNvn.js → main-B6BpuH9K.js} +2 -2
- solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
- solace_agent_mesh/common/services/identity_service.py +2 -1
- solace_agent_mesh/common/services/providers/local_file_identity_service.py +1 -1
- solace_agent_mesh/common/utils/pydantic_utils.py +4 -0
- solace_agent_mesh/gateway/base/app.py +12 -1
- solace_agent_mesh/gateway/http_sse/main.py +337 -375
- solace_agent_mesh/templates/webui.yaml +1 -1
- {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/METADATA +7 -1
- {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/RECORD +64 -63
- solace_agent_mesh/assets/docs/assets/js/ae4415af.24cdc514.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1757991496554.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1757991496554.json +0 -1
- /solace_agent_mesh/assets/docs/assets/js/{main.1de3da6a.js.LICENSE.txt → main.9bc1a102.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.4.
|
|
1
|
+
__version__ = "1.4.1"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
3
|
from .create_cmd import create_plugin_cmd
|
|
4
|
+
from .install_cmd import install_plugin_cmd
|
|
4
5
|
from .add_cmd import add_plugin_component_cmd
|
|
5
6
|
from .build_cmd import build_plugin_cmd
|
|
6
7
|
from .catalog_cmd import catalog
|
|
@@ -13,6 +14,7 @@ def plugin():
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
plugin.add_command(create_plugin_cmd, name="create")
|
|
17
|
+
plugin.add_command(install_plugin_cmd, name="install")
|
|
16
18
|
plugin.add_command(add_plugin_component_cmd, name="add")
|
|
17
19
|
plugin.add_command(build_plugin_cmd, name="build")
|
|
18
20
|
plugin.add_command(catalog, name="catalog")
|
|
@@ -1,55 +1,15 @@
|
|
|
1
1
|
import click
|
|
2
2
|
import pathlib
|
|
3
|
-
import subprocess
|
|
4
|
-
import tempfile
|
|
5
|
-
import shutil
|
|
6
|
-
import os
|
|
7
|
-
import re
|
|
8
3
|
import toml
|
|
9
4
|
|
|
10
|
-
from cli.utils import get_formatted_names,
|
|
11
|
-
from .
|
|
5
|
+
from cli.utils import get_formatted_names, error_exit
|
|
6
|
+
from .install_cmd import install_plugin
|
|
12
7
|
|
|
13
8
|
|
|
14
9
|
def ensure_directory_exists(path: pathlib.Path):
|
|
15
10
|
"""Creates a directory if it doesn't exist."""
|
|
16
11
|
path.mkdir(parents=True, exist_ok=True)
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
def _check_command_exists(command: str) -> bool:
|
|
20
|
-
"""Checks if a command exists on the system."""
|
|
21
|
-
return shutil.which(command) is not None
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _get_plugin_name_from_source_pyproject(source_path: pathlib.Path) -> str | None:
|
|
25
|
-
"""Reads pyproject.toml from source_path and returns the project name."""
|
|
26
|
-
pyproject_path = source_path / "pyproject.toml"
|
|
27
|
-
if not pyproject_path.is_file():
|
|
28
|
-
click.echo(
|
|
29
|
-
click.style(
|
|
30
|
-
f"Warning: pyproject.toml not found at {pyproject_path}. Cannot determine module name automatically.",
|
|
31
|
-
fg="yellow",
|
|
32
|
-
)
|
|
33
|
-
)
|
|
34
|
-
return None
|
|
35
|
-
try:
|
|
36
|
-
with open(pyproject_path, "r", encoding="utf-8") as f:
|
|
37
|
-
data = toml.load(f)
|
|
38
|
-
project_name = data.get("project", {}).get("name")
|
|
39
|
-
if project_name:
|
|
40
|
-
return project_name.strip().replace("-", "_") # Normalize to snake_case
|
|
41
|
-
click.echo(
|
|
42
|
-
click.style(
|
|
43
|
-
f"Warning: Could not find 'project.name' in {pyproject_path}.",
|
|
44
|
-
fg="yellow",
|
|
45
|
-
)
|
|
46
|
-
)
|
|
47
|
-
return None
|
|
48
|
-
except Exception as e:
|
|
49
|
-
click.echo(click.style(f"Error parsing {pyproject_path}: {e}", fg="red"))
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
|
|
53
13
|
def _get_plugin_type_from_pyproject(source_path: pathlib.Path) -> str | None:
|
|
54
14
|
"""Reads pyproject.toml from source_path and returns the plugin type."""
|
|
55
15
|
pyproject_path = source_path / "pyproject.toml"
|
|
@@ -82,37 +42,6 @@ def _get_plugin_type_from_pyproject(source_path: pathlib.Path) -> str | None:
|
|
|
82
42
|
return None
|
|
83
43
|
|
|
84
44
|
|
|
85
|
-
def _run_install(
|
|
86
|
-
install_command, install_target: str | pathlib.Path, operation_desc: str
|
|
87
|
-
) -> str | None:
|
|
88
|
-
"""Runs install for the given target."""
|
|
89
|
-
click.echo(
|
|
90
|
-
f"Attempting to install plugin using {install_command} from {operation_desc}..."
|
|
91
|
-
)
|
|
92
|
-
try:
|
|
93
|
-
process = subprocess.run(
|
|
94
|
-
install_command.format(package=str(install_target)).split(),
|
|
95
|
-
capture_output=True,
|
|
96
|
-
text=True,
|
|
97
|
-
check=False,
|
|
98
|
-
)
|
|
99
|
-
if process.returncode == 0:
|
|
100
|
-
click.echo(
|
|
101
|
-
click.style(
|
|
102
|
-
f"Plugin successfully installed via from {operation_desc}.",
|
|
103
|
-
fg="green",
|
|
104
|
-
)
|
|
105
|
-
)
|
|
106
|
-
if process.stdout:
|
|
107
|
-
click.echo(f"install output:\n{process.stdout}")
|
|
108
|
-
return None
|
|
109
|
-
else:
|
|
110
|
-
return f"Error: 'install {install_target}' failed.\nstdout:\n{process.stdout}\nstderr:\n{process.stderr}"
|
|
111
|
-
except FileNotFoundError:
|
|
112
|
-
return "Error: 'python' or command not found. Ensure Python and command are installed and in your PATH."
|
|
113
|
-
except Exception as e:
|
|
114
|
-
return f"An unexpected error occurred during install: {e}"
|
|
115
|
-
|
|
116
45
|
|
|
117
46
|
@click.command("add")
|
|
118
47
|
@click.argument("component_name")
|
|
@@ -130,189 +59,25 @@ def _run_install(
|
|
|
130
59
|
def add_plugin_component_cmd(
|
|
131
60
|
component_name: str, plugin_source: str, installer_command: str | None = None
|
|
132
61
|
):
|
|
133
|
-
"""
|
|
62
|
+
"""Installs the plugin and creates a new component instance from a specified plugin source."""
|
|
134
63
|
|
|
135
64
|
click.echo(
|
|
136
65
|
f"Attempting to add component '{component_name}' using plugin source '{plugin_source}'..."
|
|
137
66
|
)
|
|
138
|
-
if not installer_command:
|
|
139
|
-
installer_command = os.environ.get(
|
|
140
|
-
"SAM_PLUGIN_INSTALL_COMMAND", "pip3 install {package}"
|
|
141
|
-
)
|
|
142
|
-
try:
|
|
143
|
-
installer_command.format(package="dummy") # Test if the command is valid
|
|
144
|
-
except (KeyError, ValueError):
|
|
145
|
-
return error_exit(
|
|
146
|
-
"Error: The installer command must contain a placeholder '{package}' to be replaced with the actual package name."
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
official_plugin_url = get_official_plugin_url(plugin_source)
|
|
150
|
-
if official_plugin_url:
|
|
151
|
-
click.echo(f"Found official plugin '{plugin_source}' at: {official_plugin_url}")
|
|
152
|
-
plugin_source = official_plugin_url
|
|
153
|
-
|
|
154
|
-
install_type = None # "module", "local", "git"
|
|
155
|
-
module_name_to_load = None
|
|
156
|
-
install_target = None
|
|
157
|
-
source_path_for_name_extraction = None
|
|
158
|
-
|
|
159
|
-
if plugin_source.startswith(("http://", "https://")) and plugin_source.endswith(
|
|
160
|
-
".git"
|
|
161
|
-
):
|
|
162
|
-
install_type = "repository"
|
|
163
|
-
install_target = plugin_source
|
|
164
|
-
elif plugin_source.startswith(("git+")):
|
|
165
|
-
install_type = "git"
|
|
166
|
-
install_target = plugin_source
|
|
167
|
-
elif os.path.exists(plugin_source):
|
|
168
|
-
local_path = pathlib.Path(plugin_source).resolve()
|
|
169
|
-
if local_path.is_dir():
|
|
170
|
-
install_type = "local"
|
|
171
|
-
install_target = str(local_path)
|
|
172
|
-
source_path_for_name_extraction = local_path
|
|
173
|
-
elif local_path.is_file() and local_path.suffix in [".whl", ".tar.gz"]:
|
|
174
|
-
install_type = "wheel"
|
|
175
|
-
install_target = str(local_path)
|
|
176
|
-
else:
|
|
177
|
-
return error_exit(
|
|
178
|
-
f"Error: Local path '{plugin_source}' exists but is not a directory or wheel."
|
|
179
|
-
)
|
|
180
|
-
elif not re.search(r"[/\\]", plugin_source):
|
|
181
|
-
install_type = "module"
|
|
182
|
-
module_name_to_load = plugin_source.strip().replace("-", "_")
|
|
183
|
-
else:
|
|
184
|
-
return error_exit(
|
|
185
|
-
f"Error: Invalid plugin source '{plugin_source}'. Not a recognized module name, local path, or Git URL."
|
|
186
|
-
)
|
|
187
67
|
|
|
188
|
-
|
|
189
|
-
if install_type == "repository":
|
|
190
|
-
if not _check_command_exists("git"):
|
|
191
|
-
return error_exit(
|
|
192
|
-
"Error: 'git' command not found. Please install Git or install the plugin manually."
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
196
|
-
temp_dir_path = pathlib.Path(temp_dir)
|
|
197
|
-
cloned_repo_path = temp_dir_path / "plugin_repo"
|
|
198
|
-
click.echo(
|
|
199
|
-
f"Cloning Git repository '{plugin_source}' to temporary directory {cloned_repo_path}..."
|
|
200
|
-
)
|
|
201
|
-
try:
|
|
202
|
-
subprocess.run(
|
|
203
|
-
["git", "clone", plugin_source, str(cloned_repo_path)],
|
|
204
|
-
capture_output=True,
|
|
205
|
-
text=True,
|
|
206
|
-
check=True,
|
|
207
|
-
)
|
|
208
|
-
source_path_for_name_extraction = cloned_repo_path
|
|
209
|
-
except subprocess.CalledProcessError as e:
|
|
210
|
-
return error_exit(f"Error cloning Git repository: {e.stderr}")
|
|
211
|
-
except FileNotFoundError:
|
|
212
|
-
return error_exit("Error: 'git' command not found during clone.")
|
|
213
|
-
|
|
214
|
-
module_name_from_pyproject = _get_plugin_name_from_source_pyproject(
|
|
215
|
-
source_path_for_name_extraction
|
|
216
|
-
)
|
|
217
|
-
if not module_name_from_pyproject:
|
|
218
|
-
return error_exit(
|
|
219
|
-
"Could not determine module name from pyproject.toml in the Git repo. Aborting."
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
err = _run_install(
|
|
223
|
-
installer_command, install_target, f"Git URL ({plugin_source})"
|
|
224
|
-
)
|
|
225
|
-
if err:
|
|
226
|
-
return error_exit(err)
|
|
227
|
-
module_name_to_load = module_name_from_pyproject
|
|
228
|
-
|
|
229
|
-
elif install_type == "git":
|
|
230
|
-
module_name_from_url = (
|
|
231
|
-
plugin_source.split("#")[0]
|
|
232
|
-
.split("?")[0]
|
|
233
|
-
.split("/")[-1]
|
|
234
|
-
.replace(".git", "")
|
|
235
|
-
.replace("-", "_")
|
|
236
|
-
)
|
|
237
|
-
if "#subdirectory=" in plugin_source:
|
|
238
|
-
module_name_from_url = (
|
|
239
|
-
plugin_source.split("#subdirectory=")[-1]
|
|
240
|
-
.split("?")[0]
|
|
241
|
-
.replace(".git", "")
|
|
242
|
-
.replace("-", "_")
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
if not module_name_from_url:
|
|
246
|
-
return error_exit(
|
|
247
|
-
f"Could not determine module name from the Git URL {plugin_source}. Aborting."
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
err = _run_install(
|
|
251
|
-
installer_command, install_target, f"Git URL ({plugin_source})"
|
|
252
|
-
)
|
|
253
|
-
if err:
|
|
254
|
-
return error_exit(err)
|
|
255
|
-
module_name_to_load = module_name_from_url
|
|
256
|
-
|
|
257
|
-
elif install_type == "local":
|
|
258
|
-
module_name_from_pyproject = _get_plugin_name_from_source_pyproject(
|
|
259
|
-
source_path_for_name_extraction
|
|
260
|
-
)
|
|
261
|
-
if not module_name_from_pyproject:
|
|
262
|
-
return error_exit(
|
|
263
|
-
f"Could not determine module name from pyproject.toml at {source_path_for_name_extraction}. Aborting."
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
err = _run_install(
|
|
267
|
-
installer_command, install_target, f"local path ({install_target})"
|
|
268
|
-
)
|
|
269
|
-
if err:
|
|
270
|
-
return error_exit(err)
|
|
271
|
-
module_name_to_load = module_name_from_pyproject
|
|
272
|
-
|
|
273
|
-
elif install_type == "wheel":
|
|
274
|
-
module_name_from_wheel = (
|
|
275
|
-
pathlib.Path(install_target).stem.split("-")[0]
|
|
276
|
-
)
|
|
277
|
-
if not module_name_from_wheel:
|
|
278
|
-
return error_exit(
|
|
279
|
-
f"Could not determine module name from the wheel file {install_target}. Aborting."
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
err = _run_install(
|
|
283
|
-
installer_command, install_target, f"wheel file ({install_target})"
|
|
284
|
-
)
|
|
285
|
-
if err:
|
|
286
|
-
return error_exit(err)
|
|
287
|
-
module_name_to_load = module_name_from_wheel
|
|
288
|
-
|
|
289
|
-
if not module_name_to_load:
|
|
290
|
-
return error_exit("Error: Could not determine the plugin module name to load.")
|
|
291
|
-
|
|
292
|
-
click.echo(f"Proceeding to load plugin module '{module_name_to_load}'...")
|
|
293
|
-
try:
|
|
294
|
-
plugin_root_path = pathlib.Path(get_module_path(module_name_to_load))
|
|
295
|
-
except ImportError:
|
|
296
|
-
return error_exit(
|
|
297
|
-
f"Error: Plugin module '{module_name_to_load}' not found after potential installation. Please check installation logs or install manually."
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
if not plugin_root_path or not plugin_root_path.exists():
|
|
301
|
-
return error_exit(
|
|
302
|
-
f"Error: Could not determine a valid root path for plugin module '{module_name_to_load}'. Path: {plugin_root_path}"
|
|
303
|
-
)
|
|
68
|
+
module_name, plugin_path = install_plugin(plugin_source, installer_command)
|
|
304
69
|
|
|
305
|
-
plugin_config_path =
|
|
306
|
-
plugin_pyproject_path =
|
|
70
|
+
plugin_config_path = plugin_path / "config.yaml"
|
|
71
|
+
plugin_pyproject_path = plugin_path / "pyproject.toml"
|
|
307
72
|
|
|
308
73
|
if not plugin_pyproject_path.is_file():
|
|
309
74
|
return error_exit(
|
|
310
|
-
f"Error: pyproject.toml not found in plugin '{
|
|
75
|
+
f"Error: pyproject.toml not found in plugin '{module_name}' at expected path {plugin_pyproject_path}"
|
|
311
76
|
)
|
|
312
77
|
|
|
313
78
|
if not plugin_config_path.is_file():
|
|
314
79
|
return error_exit(
|
|
315
|
-
f"Error: config.yaml not found in plugin '{
|
|
80
|
+
f"Error: config.yaml not found in plugin '{module_name}' at expected path {plugin_config_path}"
|
|
316
81
|
)
|
|
317
82
|
|
|
318
83
|
try:
|
|
@@ -341,7 +106,7 @@ def add_plugin_component_cmd(
|
|
|
341
106
|
for placeholder, value in component_replacements.items():
|
|
342
107
|
processed_config_content = processed_config_content.replace(placeholder, value)
|
|
343
108
|
|
|
344
|
-
plugin_type = _get_plugin_type_from_pyproject(
|
|
109
|
+
plugin_type = _get_plugin_type_from_pyproject(plugin_path)
|
|
345
110
|
if plugin_type == "agent":
|
|
346
111
|
target_dir = pathlib.Path("configs/agents")
|
|
347
112
|
elif plugin_type == "gateway":
|
|
@@ -362,7 +127,7 @@ def add_plugin_component_cmd(
|
|
|
362
127
|
click.echo(f" Created component configuration: {target_path}")
|
|
363
128
|
click.echo(
|
|
364
129
|
click.style(
|
|
365
|
-
f"Component '{component_name}' created successfully from plugin '{
|
|
130
|
+
f"Component '{component_name}' created successfully from plugin '{module_name}'.",
|
|
366
131
|
fg="green",
|
|
367
132
|
)
|
|
368
133
|
)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import pathlib
|
|
6
|
+
import click
|
|
7
|
+
import shutil
|
|
8
|
+
import toml
|
|
9
|
+
|
|
10
|
+
from cli.utils import get_module_path, error_exit
|
|
11
|
+
from .official_registry import get_official_plugin_url
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _check_command_exists(command: str) -> bool:
|
|
15
|
+
"""Checks if a command exists on the system."""
|
|
16
|
+
return shutil.which(command) is not None
|
|
17
|
+
|
|
18
|
+
def _get_plugin_name_from_source_pyproject(source_path: pathlib.Path) -> str | None:
|
|
19
|
+
"""Reads pyproject.toml from source_path and returns the project name."""
|
|
20
|
+
pyproject_path = source_path / "pyproject.toml"
|
|
21
|
+
if not pyproject_path.is_file():
|
|
22
|
+
click.echo(
|
|
23
|
+
click.style(
|
|
24
|
+
f"Warning: pyproject.toml not found at {pyproject_path}. Cannot determine module name automatically.",
|
|
25
|
+
fg="yellow",
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
with open(pyproject_path, "r", encoding="utf-8") as f:
|
|
31
|
+
data = toml.load(f)
|
|
32
|
+
project_name = data.get("project", {}).get("name")
|
|
33
|
+
if project_name:
|
|
34
|
+
return project_name.strip().replace("-", "_") # Normalize to snake_case
|
|
35
|
+
click.echo(
|
|
36
|
+
click.style(
|
|
37
|
+
f"Warning: Could not find 'project.name' in {pyproject_path}.",
|
|
38
|
+
fg="yellow",
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
return None
|
|
42
|
+
except Exception as e:
|
|
43
|
+
click.echo(click.style(f"Error parsing {pyproject_path}: {e}", fg="red"))
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def _run_install(
|
|
47
|
+
install_command, install_target: str | pathlib.Path, operation_desc: str
|
|
48
|
+
) -> str | None:
|
|
49
|
+
"""Runs install for the given target."""
|
|
50
|
+
click.echo(
|
|
51
|
+
f"Attempting to install plugin using {install_command} from {operation_desc}..."
|
|
52
|
+
)
|
|
53
|
+
try:
|
|
54
|
+
process = subprocess.run(
|
|
55
|
+
install_command.format(package=str(install_target)).split(),
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
check=False,
|
|
59
|
+
)
|
|
60
|
+
if process.returncode == 0:
|
|
61
|
+
click.echo(
|
|
62
|
+
click.style(
|
|
63
|
+
f"Plugin successfully installed via from {operation_desc}.",
|
|
64
|
+
fg="green",
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
if process.stdout:
|
|
68
|
+
click.echo(f"install output:\n{process.stdout}")
|
|
69
|
+
return None
|
|
70
|
+
else:
|
|
71
|
+
return f"Error: 'install {install_target}' failed.\nstdout:\n{process.stdout}\nstderr:\n{process.stderr}"
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
return "Error: 'python' or command not found. Ensure Python and command are installed and in your PATH."
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return f"An unexpected error occurred during install: {e}"
|
|
76
|
+
|
|
77
|
+
def install_plugin(plugin_source: str, installer_command: str | None = None) -> tuple[str | None, pathlib.Path | None]:
|
|
78
|
+
"""Installs a plugin from the specified source.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
plugin_source: Source of the plugin (module name, local path, or Git URL)
|
|
82
|
+
installer_command: Command to install the plugin, with '{package}' placeholder
|
|
83
|
+
Returns:
|
|
84
|
+
Tuple of (module_name, plugin_path) if successful, or (None, None) on failure
|
|
85
|
+
"""
|
|
86
|
+
if not installer_command:
|
|
87
|
+
installer_command = os.environ.get(
|
|
88
|
+
"SAM_PLUGIN_INSTALL_COMMAND", "pip3 install {package}"
|
|
89
|
+
)
|
|
90
|
+
try:
|
|
91
|
+
installer_command.format(package="dummy") # Test if the command is valid
|
|
92
|
+
except (KeyError, ValueError):
|
|
93
|
+
return error_exit(
|
|
94
|
+
"Error: The installer command must contain a placeholder '{package}' to be replaced with the actual package name."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
official_plugin_url = get_official_plugin_url(plugin_source)
|
|
98
|
+
if official_plugin_url:
|
|
99
|
+
click.echo(f"Found official plugin '{plugin_source}' at: {official_plugin_url}")
|
|
100
|
+
plugin_source = official_plugin_url
|
|
101
|
+
|
|
102
|
+
install_type = None # "module", "local", "git"
|
|
103
|
+
module_name = None
|
|
104
|
+
install_target = None
|
|
105
|
+
source_path_for_name_extraction = None
|
|
106
|
+
|
|
107
|
+
if plugin_source.startswith(("http://", "https://")) and plugin_source.endswith(
|
|
108
|
+
".git"
|
|
109
|
+
):
|
|
110
|
+
install_type = "repository"
|
|
111
|
+
install_target = plugin_source
|
|
112
|
+
elif plugin_source.startswith(("git+")):
|
|
113
|
+
install_type = "git"
|
|
114
|
+
install_target = plugin_source
|
|
115
|
+
elif os.path.exists(plugin_source):
|
|
116
|
+
local_path = pathlib.Path(plugin_source).resolve()
|
|
117
|
+
if local_path.is_dir():
|
|
118
|
+
install_type = "local"
|
|
119
|
+
install_target = str(local_path)
|
|
120
|
+
source_path_for_name_extraction = local_path
|
|
121
|
+
elif local_path.is_file() and local_path.suffix in [".whl", ".tar.gz"]:
|
|
122
|
+
install_type = "wheel"
|
|
123
|
+
install_target = str(local_path)
|
|
124
|
+
else:
|
|
125
|
+
return error_exit(
|
|
126
|
+
f"Error: Local path '{plugin_source}' exists but is not a directory or wheel."
|
|
127
|
+
)
|
|
128
|
+
elif not re.search(r"[/\\]", plugin_source):
|
|
129
|
+
install_type = "module"
|
|
130
|
+
module_name = plugin_source.strip().replace("-", "_")
|
|
131
|
+
else:
|
|
132
|
+
return error_exit(
|
|
133
|
+
f"Error: Invalid plugin source '{plugin_source}'. Not a recognized module name, local path, or Git URL."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if install_type in ["local", "git", "repository", "wheel"]:
|
|
137
|
+
if install_type == "repository":
|
|
138
|
+
if not _check_command_exists("git"):
|
|
139
|
+
return error_exit(
|
|
140
|
+
"Error: 'git' command not found. Please install Git or install the plugin manually."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
144
|
+
temp_dir_path = pathlib.Path(temp_dir)
|
|
145
|
+
cloned_repo_path = temp_dir_path / "plugin_repo"
|
|
146
|
+
click.echo(
|
|
147
|
+
f"Cloning Git repository '{plugin_source}' to temporary directory {cloned_repo_path}..."
|
|
148
|
+
)
|
|
149
|
+
try:
|
|
150
|
+
subprocess.run(
|
|
151
|
+
["git", "clone", plugin_source, str(cloned_repo_path)],
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
check=True,
|
|
155
|
+
)
|
|
156
|
+
source_path_for_name_extraction = cloned_repo_path
|
|
157
|
+
except subprocess.CalledProcessError as e:
|
|
158
|
+
return error_exit(f"Error cloning Git repository: {e.stderr}")
|
|
159
|
+
except FileNotFoundError:
|
|
160
|
+
return error_exit("Error: 'git' command not found during clone.")
|
|
161
|
+
|
|
162
|
+
module_name_from_pyproject = _get_plugin_name_from_source_pyproject(
|
|
163
|
+
source_path_for_name_extraction
|
|
164
|
+
)
|
|
165
|
+
if not module_name_from_pyproject:
|
|
166
|
+
return error_exit(
|
|
167
|
+
"Could not determine module name from pyproject.toml in the Git repo. Aborting."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
err = _run_install(
|
|
171
|
+
installer_command, install_target, f"Git URL ({plugin_source})"
|
|
172
|
+
)
|
|
173
|
+
if err:
|
|
174
|
+
return error_exit(err)
|
|
175
|
+
module_name = module_name_from_pyproject
|
|
176
|
+
|
|
177
|
+
elif install_type == "git":
|
|
178
|
+
module_name_from_url = (
|
|
179
|
+
plugin_source.split("#")[0]
|
|
180
|
+
.split("?")[0]
|
|
181
|
+
.split("/")[-1]
|
|
182
|
+
.replace(".git", "")
|
|
183
|
+
.replace("-", "_")
|
|
184
|
+
)
|
|
185
|
+
if "#subdirectory=" in plugin_source:
|
|
186
|
+
module_name_from_url = (
|
|
187
|
+
plugin_source.split("#subdirectory=")[-1]
|
|
188
|
+
.split("?")[0]
|
|
189
|
+
.replace(".git", "")
|
|
190
|
+
.replace("-", "_")
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if not module_name_from_url:
|
|
194
|
+
return error_exit(
|
|
195
|
+
f"Could not determine module name from the Git URL {plugin_source}. Aborting."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
err = _run_install(
|
|
199
|
+
installer_command, install_target, f"Git URL ({plugin_source})"
|
|
200
|
+
)
|
|
201
|
+
if err:
|
|
202
|
+
return error_exit(err)
|
|
203
|
+
module_name = module_name_from_url
|
|
204
|
+
|
|
205
|
+
elif install_type == "local":
|
|
206
|
+
module_name_from_pyproject = _get_plugin_name_from_source_pyproject(
|
|
207
|
+
source_path_for_name_extraction
|
|
208
|
+
)
|
|
209
|
+
if not module_name_from_pyproject:
|
|
210
|
+
return error_exit(
|
|
211
|
+
f"Could not determine module name from pyproject.toml at {source_path_for_name_extraction}. Aborting."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
err = _run_install(
|
|
215
|
+
installer_command, install_target, f"local path ({install_target})"
|
|
216
|
+
)
|
|
217
|
+
if err:
|
|
218
|
+
return error_exit(err)
|
|
219
|
+
module_name = module_name_from_pyproject
|
|
220
|
+
|
|
221
|
+
elif install_type == "wheel":
|
|
222
|
+
module_name_from_wheel = (
|
|
223
|
+
pathlib.Path(install_target).stem.split("-")[0]
|
|
224
|
+
)
|
|
225
|
+
if not module_name_from_wheel:
|
|
226
|
+
return error_exit(
|
|
227
|
+
f"Could not determine module name from the wheel file {install_target}. Aborting."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
err = _run_install(
|
|
231
|
+
installer_command, install_target, f"wheel file ({install_target})"
|
|
232
|
+
)
|
|
233
|
+
if err:
|
|
234
|
+
return error_exit(err)
|
|
235
|
+
module_name = module_name_from_wheel
|
|
236
|
+
|
|
237
|
+
if not module_name:
|
|
238
|
+
return error_exit("Error: Could not determine the plugin module name to load.")
|
|
239
|
+
|
|
240
|
+
click.echo(f"Proceeding to load plugin module '{module_name}'...")
|
|
241
|
+
try:
|
|
242
|
+
plugin_path = pathlib.Path(get_module_path(module_name))
|
|
243
|
+
except ImportError:
|
|
244
|
+
return error_exit(
|
|
245
|
+
f"Error: Plugin module '{module_name}' not found after potential installation. Please check installation logs or install manually."
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if not plugin_path or not plugin_path.exists():
|
|
249
|
+
return error_exit(
|
|
250
|
+
f"Error: Could not determine a valid root path for plugin module '{module_name}'. Path: {plugin_path}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return module_name, plugin_path
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@click.command("install")
|
|
257
|
+
@click.argument("plugin_source")
|
|
258
|
+
@click.option(
|
|
259
|
+
"--install-command",
|
|
260
|
+
"installer_command",
|
|
261
|
+
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.",
|
|
262
|
+
)
|
|
263
|
+
def install_plugin_cmd(
|
|
264
|
+
plugin_source: str,
|
|
265
|
+
installer_command: str | None = None
|
|
266
|
+
):
|
|
267
|
+
"""
|
|
268
|
+
Installs a plugin from the specified source.
|
|
269
|
+
|
|
270
|
+
PLUGIN_SOURCE can be:
|
|
271
|
+
- A local path to a directory (e.g., '/path/to/plugin')
|
|
272
|
+
- A local path to a wheel file (e.g., '/path/to/plugin.whl')
|
|
273
|
+
- A Git URL (e.g., 'https://github.com/user/repo.git')
|
|
274
|
+
- The name of the plugin from https://github.com/SolaceLabs/solace-agent-mesh-core-plugins
|
|
275
|
+
"""
|
|
276
|
+
module_name, plugin_path = install_plugin(plugin_source, installer_command)
|
|
277
|
+
if module_name and plugin_path:
|
|
278
|
+
click.echo(
|
|
279
|
+
click.style(
|
|
280
|
+
f"Plugin '{module_name}' installed and available at {plugin_path}.",
|
|
281
|
+
fg="green",
|
|
282
|
+
)
|
|
283
|
+
)
|