deepctl-cmd-plugin 0.1.10__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.
- deepctl_cmd_plugin/__init__.py +5 -0
- deepctl_cmd_plugin/command.py +1107 -0
- deepctl_cmd_plugin/models.py +75 -0
- deepctl_cmd_plugin-0.1.10.dist-info/METADATA +85 -0
- deepctl_cmd_plugin-0.1.10.dist-info/RECORD +7 -0
- deepctl_cmd_plugin-0.1.10.dist-info/WHEEL +4 -0
- deepctl_cmd_plugin-0.1.10.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
"""Plugin management command implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from deepctl_cmd_update.installation import InstallationDetector, InstallMethod
|
|
11
|
+
from deepctl_core.auth import AuthManager
|
|
12
|
+
from deepctl_core.base_group_command import BaseGroupCommand
|
|
13
|
+
from deepctl_core.client import DeepgramClient
|
|
14
|
+
from deepctl_core.config import Config
|
|
15
|
+
from deepctl_core.output import print_error, print_info, print_success
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from .models import (
|
|
20
|
+
PluginAction,
|
|
21
|
+
PluginInstallOptions,
|
|
22
|
+
PluginOperationResult,
|
|
23
|
+
PluginPackage,
|
|
24
|
+
PluginRegistryEntry,
|
|
25
|
+
PluginSearchResult,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PluginCommand(BaseGroupCommand):
|
|
32
|
+
"""Plugin management command."""
|
|
33
|
+
|
|
34
|
+
name = "plugin"
|
|
35
|
+
help = "Manage deepctl plugins"
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
"""Initialize plugin command."""
|
|
39
|
+
super().__init__()
|
|
40
|
+
self.detector = InstallationDetector()
|
|
41
|
+
self._python_executable = sys.executable
|
|
42
|
+
|
|
43
|
+
# Plugin environment paths
|
|
44
|
+
self._plugin_dir = Path.home() / ".deepctl" / "plugins"
|
|
45
|
+
self._plugin_venv = self._plugin_dir / "venv"
|
|
46
|
+
self._plugin_state_file = self._plugin_dir / "plugins.json"
|
|
47
|
+
|
|
48
|
+
def execute(self, ctx: click.Context, **kwargs: Any) -> None:
|
|
49
|
+
"""Execute plugin group command.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
ctx: Click context
|
|
53
|
+
**kwargs: Additional arguments
|
|
54
|
+
"""
|
|
55
|
+
# Get config from context
|
|
56
|
+
config = ctx.obj.get("config") if ctx.obj else None
|
|
57
|
+
if not config:
|
|
58
|
+
# If no config in context, create one
|
|
59
|
+
config = Config()
|
|
60
|
+
|
|
61
|
+
# Create auth manager and client
|
|
62
|
+
auth_manager = AuthManager(config)
|
|
63
|
+
client = DeepgramClient(config, auth_manager)
|
|
64
|
+
|
|
65
|
+
# Store in context for subcommands
|
|
66
|
+
ctx.obj = ctx.obj or {}
|
|
67
|
+
ctx.obj["config"] = config
|
|
68
|
+
ctx.obj["auth_manager"] = auth_manager
|
|
69
|
+
ctx.obj["client"] = client
|
|
70
|
+
|
|
71
|
+
# Continue with normal group execution
|
|
72
|
+
super().execute(ctx, **kwargs)
|
|
73
|
+
|
|
74
|
+
def setup_commands(self) -> list[click.Command]:
|
|
75
|
+
"""Set up plugin management commands.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of subcommands
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
# Create a wrapper for subcommands to handle context
|
|
82
|
+
def context_wrapper(func: Any) -> Any:
|
|
83
|
+
"""Wrap subcommand to provide config and auth."""
|
|
84
|
+
|
|
85
|
+
@click.pass_context
|
|
86
|
+
def wrapper(ctx: click.Context, /, **kwargs: Any) -> Any:
|
|
87
|
+
# Try to get from parent context first
|
|
88
|
+
if ctx.parent and ctx.parent.obj:
|
|
89
|
+
config = ctx.parent.obj.get("config")
|
|
90
|
+
auth_manager = ctx.parent.obj.get("auth_manager")
|
|
91
|
+
client = ctx.parent.obj.get("client")
|
|
92
|
+
if config and auth_manager and client:
|
|
93
|
+
return func(config, auth_manager, client, **kwargs)
|
|
94
|
+
|
|
95
|
+
# Fallback - look for deepctl_context
|
|
96
|
+
current_ctx: click.Context | None = ctx
|
|
97
|
+
while current_ctx:
|
|
98
|
+
if hasattr(current_ctx, "deepctl_context"):
|
|
99
|
+
config = current_ctx.deepctl_context.get("config")
|
|
100
|
+
if config:
|
|
101
|
+
auth_manager = AuthManager(config)
|
|
102
|
+
client = DeepgramClient(config, auth_manager)
|
|
103
|
+
return func(config, auth_manager, client, **kwargs)
|
|
104
|
+
current_ctx = current_ctx.parent
|
|
105
|
+
|
|
106
|
+
# Final fallback
|
|
107
|
+
config = Config()
|
|
108
|
+
auth_manager = AuthManager(config)
|
|
109
|
+
client = DeepgramClient(config, auth_manager)
|
|
110
|
+
return func(config, auth_manager, client, **kwargs)
|
|
111
|
+
|
|
112
|
+
wrapper.__name__ = func.__name__
|
|
113
|
+
wrapper.__doc__ = func.__doc__
|
|
114
|
+
return wrapper
|
|
115
|
+
|
|
116
|
+
# Define all subcommands
|
|
117
|
+
return [
|
|
118
|
+
self._create_install_command(context_wrapper),
|
|
119
|
+
self._create_list_command(context_wrapper),
|
|
120
|
+
self._create_update_command(context_wrapper),
|
|
121
|
+
self._create_remove_command(context_wrapper),
|
|
122
|
+
self._create_search_command(context_wrapper),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
def _ensure_plugin_environment(self) -> tuple[bool, str]:
|
|
126
|
+
"""Ensure plugin environment exists.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Tuple of (success, python_executable_path)
|
|
130
|
+
"""
|
|
131
|
+
# Create plugin directory if it doesn't exist
|
|
132
|
+
self._plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
|
|
134
|
+
# Check if venv exists
|
|
135
|
+
if not self._plugin_venv.exists():
|
|
136
|
+
print_info("Creating plugin environment...")
|
|
137
|
+
try:
|
|
138
|
+
# Create virtual environment
|
|
139
|
+
subprocess.run(
|
|
140
|
+
[sys.executable, "-m", "venv", str(self._plugin_venv)],
|
|
141
|
+
check=True,
|
|
142
|
+
capture_output=True,
|
|
143
|
+
text=True,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Get the python executable in the venv
|
|
147
|
+
if sys.platform == "win32":
|
|
148
|
+
venv_python = self._plugin_venv / "Scripts" / "python.exe"
|
|
149
|
+
else:
|
|
150
|
+
venv_python = self._plugin_venv / "bin" / "python"
|
|
151
|
+
|
|
152
|
+
# Ensure pip is installed and up to date
|
|
153
|
+
subprocess.run(
|
|
154
|
+
[
|
|
155
|
+
str(venv_python),
|
|
156
|
+
"-m",
|
|
157
|
+
"pip",
|
|
158
|
+
"install",
|
|
159
|
+
"--upgrade",
|
|
160
|
+
"pip",
|
|
161
|
+
],
|
|
162
|
+
check=True,
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
print_success("Plugin environment created successfully")
|
|
168
|
+
return True, str(venv_python)
|
|
169
|
+
|
|
170
|
+
except subprocess.CalledProcessError as e:
|
|
171
|
+
print_error(f"Failed to create plugin environment: {e}")
|
|
172
|
+
return False, ""
|
|
173
|
+
|
|
174
|
+
# Venv exists, return python executable
|
|
175
|
+
if sys.platform == "win32":
|
|
176
|
+
venv_python = self._plugin_venv / "Scripts" / "python.exe"
|
|
177
|
+
else:
|
|
178
|
+
venv_python = self._plugin_venv / "bin" / "python"
|
|
179
|
+
|
|
180
|
+
return True, str(venv_python)
|
|
181
|
+
|
|
182
|
+
def _get_plugin_state(self) -> dict[str, Any]:
|
|
183
|
+
"""Get plugin state from file.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Plugin state dictionary
|
|
187
|
+
"""
|
|
188
|
+
if self._plugin_state_file.exists():
|
|
189
|
+
try:
|
|
190
|
+
result: dict[str, Any] = json.loads(
|
|
191
|
+
self._plugin_state_file.read_text()
|
|
192
|
+
)
|
|
193
|
+
return result
|
|
194
|
+
except Exception:
|
|
195
|
+
return {"plugins": {}}
|
|
196
|
+
return {"plugins": {}}
|
|
197
|
+
|
|
198
|
+
def _save_plugin_state(self, state: dict[str, Any]) -> None:
|
|
199
|
+
"""Save plugin state to file.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
state: Plugin state to save
|
|
203
|
+
"""
|
|
204
|
+
self._plugin_state_file.write_text(json.dumps(state, indent=2))
|
|
205
|
+
|
|
206
|
+
def _create_install_command(self, context_wrapper: Any) -> click.Command:
|
|
207
|
+
"""Create the install subcommand."""
|
|
208
|
+
|
|
209
|
+
@click.command(name="install", help="Install a plugin")
|
|
210
|
+
@click.argument("package")
|
|
211
|
+
@click.option(
|
|
212
|
+
"--version",
|
|
213
|
+
"-v",
|
|
214
|
+
help="Specific version to install",
|
|
215
|
+
)
|
|
216
|
+
@click.option(
|
|
217
|
+
"--upgrade",
|
|
218
|
+
"-U",
|
|
219
|
+
is_flag=True,
|
|
220
|
+
help="Upgrade if already installed",
|
|
221
|
+
)
|
|
222
|
+
@click.option(
|
|
223
|
+
"--pre",
|
|
224
|
+
is_flag=True,
|
|
225
|
+
help="Include pre-release versions",
|
|
226
|
+
)
|
|
227
|
+
@click.option(
|
|
228
|
+
"--force-reinstall",
|
|
229
|
+
is_flag=True,
|
|
230
|
+
help="Force reinstallation",
|
|
231
|
+
)
|
|
232
|
+
@click.option(
|
|
233
|
+
"--index-url",
|
|
234
|
+
help="Base URL of Python package index",
|
|
235
|
+
)
|
|
236
|
+
@click.option(
|
|
237
|
+
"--extra-index-url",
|
|
238
|
+
help="Extra URLs of package indexes",
|
|
239
|
+
)
|
|
240
|
+
@click.option(
|
|
241
|
+
"--git",
|
|
242
|
+
"git_url",
|
|
243
|
+
help="Install from git repository URL",
|
|
244
|
+
)
|
|
245
|
+
@click.option(
|
|
246
|
+
"--editable",
|
|
247
|
+
"-e",
|
|
248
|
+
is_flag=True,
|
|
249
|
+
help="Install in editable mode",
|
|
250
|
+
)
|
|
251
|
+
def install_cmd(**kwargs: Any) -> None:
|
|
252
|
+
"""Install a plugin handler."""
|
|
253
|
+
pass # Handler is wrapped by context_wrapper
|
|
254
|
+
|
|
255
|
+
# Apply the context wrapper
|
|
256
|
+
install_cmd.callback = context_wrapper(
|
|
257
|
+
lambda config, auth_manager, client, **kwargs: self._handle_install(
|
|
258
|
+
config, auth_manager, client, **kwargs
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return install_cmd
|
|
263
|
+
|
|
264
|
+
def _handle_install(
|
|
265
|
+
self,
|
|
266
|
+
config: Config,
|
|
267
|
+
auth_manager: AuthManager,
|
|
268
|
+
client: DeepgramClient,
|
|
269
|
+
**kwargs: Any,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Handle install command."""
|
|
272
|
+
# Extract package argument
|
|
273
|
+
package = kwargs.pop("package", "")
|
|
274
|
+
|
|
275
|
+
# Check if package is a git URL
|
|
276
|
+
if package.startswith("git+") or (
|
|
277
|
+
(package.startswith("http://") or package.startswith("https://"))
|
|
278
|
+
and ".git" in package
|
|
279
|
+
):
|
|
280
|
+
# This is a git URL
|
|
281
|
+
kwargs["git_url"] = package
|
|
282
|
+
# Extract package name from URL for tracking
|
|
283
|
+
# e.g., git+https://github.com/user/deepctl-plugin-foo.git -> deepctl-plugin-foo
|
|
284
|
+
if "/" in package:
|
|
285
|
+
potential_name = package.split("/")[-1]
|
|
286
|
+
if potential_name.endswith(".git"):
|
|
287
|
+
potential_name = potential_name[:-4]
|
|
288
|
+
kwargs["package"] = potential_name
|
|
289
|
+
else:
|
|
290
|
+
kwargs["package"] = package
|
|
291
|
+
else:
|
|
292
|
+
kwargs["package"] = package
|
|
293
|
+
|
|
294
|
+
options = PluginInstallOptions(**kwargs)
|
|
295
|
+
result = self.install_plugin(config, auth_manager, client, options)
|
|
296
|
+
|
|
297
|
+
if result.success:
|
|
298
|
+
print_success(result.message)
|
|
299
|
+
else:
|
|
300
|
+
print_error(result.error or result.message)
|
|
301
|
+
raise click.ClickException(result.error or result.message)
|
|
302
|
+
|
|
303
|
+
def _create_list_command(self, context_wrapper: Any) -> click.Command:
|
|
304
|
+
"""Create the list subcommand."""
|
|
305
|
+
|
|
306
|
+
@click.command(name="list", help="List installed plugins")
|
|
307
|
+
@click.option(
|
|
308
|
+
"--verbose",
|
|
309
|
+
"-v",
|
|
310
|
+
is_flag=True,
|
|
311
|
+
help="Show detailed information",
|
|
312
|
+
)
|
|
313
|
+
def list_cmd(**kwargs: Any) -> None:
|
|
314
|
+
"""List plugins handler."""
|
|
315
|
+
pass # Handler is wrapped by context_wrapper
|
|
316
|
+
|
|
317
|
+
# Apply the context wrapper
|
|
318
|
+
list_cmd.callback = context_wrapper(
|
|
319
|
+
lambda config, auth_manager, client, **kwargs: self.list_plugins(
|
|
320
|
+
config, auth_manager, client, **kwargs
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return list_cmd
|
|
325
|
+
|
|
326
|
+
def _create_update_command(self, context_wrapper: Any) -> click.Command:
|
|
327
|
+
"""Create the update subcommand."""
|
|
328
|
+
|
|
329
|
+
@click.command(name="update", help="Update a plugin")
|
|
330
|
+
@click.argument("package")
|
|
331
|
+
@click.option(
|
|
332
|
+
"--pre",
|
|
333
|
+
is_flag=True,
|
|
334
|
+
help="Include pre-release versions",
|
|
335
|
+
)
|
|
336
|
+
def update_cmd(**kwargs: Any) -> None:
|
|
337
|
+
"""Update plugin handler."""
|
|
338
|
+
pass # Handler is wrapped by context_wrapper
|
|
339
|
+
|
|
340
|
+
# Apply the context wrapper
|
|
341
|
+
update_cmd.callback = context_wrapper(
|
|
342
|
+
lambda config, auth_manager, client, **kwargs: self._handle_update(
|
|
343
|
+
config, auth_manager, client, **kwargs
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return update_cmd
|
|
348
|
+
|
|
349
|
+
def _handle_update(
|
|
350
|
+
self,
|
|
351
|
+
config: Config,
|
|
352
|
+
auth_manager: AuthManager,
|
|
353
|
+
client: DeepgramClient,
|
|
354
|
+
**kwargs: Any,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Handle update command."""
|
|
357
|
+
options = PluginInstallOptions(
|
|
358
|
+
package=kwargs["package"],
|
|
359
|
+
upgrade=True,
|
|
360
|
+
pre=kwargs.get("pre", False),
|
|
361
|
+
)
|
|
362
|
+
result = self.install_plugin(config, auth_manager, client, options)
|
|
363
|
+
|
|
364
|
+
if result.success:
|
|
365
|
+
print_success(result.message)
|
|
366
|
+
else:
|
|
367
|
+
print_error(result.error or result.message)
|
|
368
|
+
raise click.ClickException(result.error or result.message)
|
|
369
|
+
|
|
370
|
+
def _create_remove_command(self, context_wrapper: Any) -> click.Command:
|
|
371
|
+
"""Create the remove subcommand."""
|
|
372
|
+
|
|
373
|
+
@click.command(name="remove", help="Remove a plugin")
|
|
374
|
+
@click.argument("package")
|
|
375
|
+
@click.option(
|
|
376
|
+
"--yes",
|
|
377
|
+
"-y",
|
|
378
|
+
is_flag=True,
|
|
379
|
+
help="Skip confirmation",
|
|
380
|
+
)
|
|
381
|
+
def remove_cmd(**kwargs: Any) -> None:
|
|
382
|
+
"""Remove plugin handler."""
|
|
383
|
+
pass # Handler is wrapped by context_wrapper
|
|
384
|
+
|
|
385
|
+
# Apply the context wrapper
|
|
386
|
+
remove_cmd.callback = context_wrapper(
|
|
387
|
+
lambda config, auth_manager, client, **kwargs: self._handle_remove(
|
|
388
|
+
config, auth_manager, client, **kwargs
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return remove_cmd
|
|
393
|
+
|
|
394
|
+
def _handle_remove(
|
|
395
|
+
self,
|
|
396
|
+
config: Config,
|
|
397
|
+
auth_manager: AuthManager,
|
|
398
|
+
client: DeepgramClient,
|
|
399
|
+
**kwargs: Any,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Handle remove command."""
|
|
402
|
+
package = kwargs["package"]
|
|
403
|
+
yes = kwargs.get("yes", False)
|
|
404
|
+
|
|
405
|
+
if not yes and not click.confirm(
|
|
406
|
+
f"Are you sure you want to remove {package}?"
|
|
407
|
+
):
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
result = self.remove_plugin(config, auth_manager, client, package)
|
|
411
|
+
|
|
412
|
+
if result.success:
|
|
413
|
+
print_success(result.message)
|
|
414
|
+
else:
|
|
415
|
+
print_error(result.error or result.message)
|
|
416
|
+
raise click.ClickException(result.error or result.message)
|
|
417
|
+
|
|
418
|
+
def _create_search_command(self, context_wrapper: Any) -> click.Command:
|
|
419
|
+
"""Create the search subcommand."""
|
|
420
|
+
|
|
421
|
+
@click.command(name="search", help="Search for available plugins")
|
|
422
|
+
@click.argument("query", required=False)
|
|
423
|
+
@click.option(
|
|
424
|
+
"--installed",
|
|
425
|
+
is_flag=True,
|
|
426
|
+
help="Show only installed plugins",
|
|
427
|
+
)
|
|
428
|
+
@click.option(
|
|
429
|
+
"--available",
|
|
430
|
+
is_flag=True,
|
|
431
|
+
help="Show only available (not installed) plugins",
|
|
432
|
+
)
|
|
433
|
+
def search_cmd(**kwargs: Any) -> None:
|
|
434
|
+
"""Search plugins handler."""
|
|
435
|
+
pass # Handler is wrapped by context_wrapper
|
|
436
|
+
|
|
437
|
+
# Apply the context wrapper
|
|
438
|
+
search_cmd.callback = context_wrapper(
|
|
439
|
+
lambda config, auth_manager, client, **kwargs: self._handle_search(
|
|
440
|
+
config, auth_manager, client, **kwargs
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
return search_cmd
|
|
445
|
+
|
|
446
|
+
def _handle_search(
|
|
447
|
+
self,
|
|
448
|
+
config: Config,
|
|
449
|
+
auth_manager: AuthManager,
|
|
450
|
+
client: DeepgramClient,
|
|
451
|
+
**kwargs: Any,
|
|
452
|
+
) -> None:
|
|
453
|
+
"""Handle search command."""
|
|
454
|
+
query = kwargs.get("query", "")
|
|
455
|
+
show_installed = kwargs.get("installed", False)
|
|
456
|
+
show_available = kwargs.get("available", False)
|
|
457
|
+
|
|
458
|
+
# Get available plugins from registry
|
|
459
|
+
registry_plugins = self._get_plugin_registry()
|
|
460
|
+
|
|
461
|
+
# Get installed plugins
|
|
462
|
+
installed_plugins = {p.name: p for p in self._discover_plugins()}
|
|
463
|
+
|
|
464
|
+
# Filter plugins based on query
|
|
465
|
+
search_results = []
|
|
466
|
+
for plugin in registry_plugins:
|
|
467
|
+
# Check if plugin matches query
|
|
468
|
+
if query:
|
|
469
|
+
query_lower = query.lower()
|
|
470
|
+
if (
|
|
471
|
+
query_lower not in plugin.name.lower()
|
|
472
|
+
and query_lower not in plugin.description.lower()
|
|
473
|
+
and not any(
|
|
474
|
+
query_lower in kw.lower() for kw in plugin.keywords
|
|
475
|
+
)
|
|
476
|
+
):
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
# Check installed status
|
|
480
|
+
is_installed = (
|
|
481
|
+
plugin.install_name is not None
|
|
482
|
+
and plugin.install_name in installed_plugins
|
|
483
|
+
) or plugin.name in installed_plugins
|
|
484
|
+
installed_version = None
|
|
485
|
+
if is_installed:
|
|
486
|
+
installed_plugin = (
|
|
487
|
+
installed_plugins.get(plugin.install_name)
|
|
488
|
+
if plugin.install_name
|
|
489
|
+
else None
|
|
490
|
+
) or installed_plugins.get(plugin.name)
|
|
491
|
+
if installed_plugin:
|
|
492
|
+
installed_version = installed_plugin.version
|
|
493
|
+
|
|
494
|
+
# Apply filters
|
|
495
|
+
if show_installed and not is_installed:
|
|
496
|
+
continue
|
|
497
|
+
if show_available and is_installed:
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
search_results.append(
|
|
501
|
+
PluginSearchResult(
|
|
502
|
+
plugin=plugin,
|
|
503
|
+
installed=is_installed,
|
|
504
|
+
installed_version=installed_version,
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Display results
|
|
509
|
+
if not search_results:
|
|
510
|
+
if query:
|
|
511
|
+
print_info(f"No plugins found matching '{query}'")
|
|
512
|
+
else:
|
|
513
|
+
print_info("No plugins found")
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
# Create table
|
|
517
|
+
table = Table(title="Available Plugins")
|
|
518
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
519
|
+
table.add_column("Description", style="white")
|
|
520
|
+
table.add_column("Version", style="green")
|
|
521
|
+
table.add_column("Status", style="yellow")
|
|
522
|
+
|
|
523
|
+
for result in search_results:
|
|
524
|
+
status = "Installed" if result.installed else "Available"
|
|
525
|
+
if result.installed and result.installed_version:
|
|
526
|
+
status = f"Installed ({result.installed_version})"
|
|
527
|
+
|
|
528
|
+
table.add_row(
|
|
529
|
+
result.plugin.name,
|
|
530
|
+
result.plugin.description,
|
|
531
|
+
result.plugin.version,
|
|
532
|
+
status,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
console.print(table)
|
|
536
|
+
|
|
537
|
+
# Show install hint
|
|
538
|
+
available_count = sum(1 for r in search_results if not r.installed)
|
|
539
|
+
if available_count > 0:
|
|
540
|
+
print_info(
|
|
541
|
+
"\nTo install a plugin, use: deepctl plugin install <name>"
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def _get_plugin_registry(self) -> list[PluginRegistryEntry]:
|
|
545
|
+
"""Get the plugin registry.
|
|
546
|
+
|
|
547
|
+
For now, this returns a hardcoded list. In the future, this will
|
|
548
|
+
fetch from a .well-known URL or plugin registry service.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
List of available plugins
|
|
552
|
+
"""
|
|
553
|
+
# TODO: Fetch from https://deepgram.com/.well-known/deepctl-plugins.json
|
|
554
|
+
return [
|
|
555
|
+
PluginRegistryEntry(
|
|
556
|
+
name="deepctl-plugin-example",
|
|
557
|
+
description="Example plugin demonstrating the plugin system",
|
|
558
|
+
version="0.1.8",
|
|
559
|
+
author="Deepgram DevRel",
|
|
560
|
+
url="https://github.com/deepgram/deepctl",
|
|
561
|
+
keywords=["example", "demo", "plugin"],
|
|
562
|
+
install_name="deepctl-plugin-example",
|
|
563
|
+
),
|
|
564
|
+
PluginRegistryEntry(
|
|
565
|
+
name="deepctl-plugin-tts",
|
|
566
|
+
description="Text-to-speech functionality for Deepgram",
|
|
567
|
+
version="0.2.0",
|
|
568
|
+
author="Deepgram",
|
|
569
|
+
url="https://github.com/deepgram/deepctl-plugin-tts",
|
|
570
|
+
keywords=["tts", "text-to-speech", "audio", "synthesis"],
|
|
571
|
+
install_name="deepctl-plugin-tts",
|
|
572
|
+
),
|
|
573
|
+
PluginRegistryEntry(
|
|
574
|
+
name="deepctl-plugin-analyze",
|
|
575
|
+
description="Advanced audio analysis tools",
|
|
576
|
+
version="0.1.0",
|
|
577
|
+
author="Deepgram",
|
|
578
|
+
url="https://github.com/deepgram/deepctl-plugin-analyze",
|
|
579
|
+
keywords=["analyze", "audio", "analysis", "metrics"],
|
|
580
|
+
install_name="deepctl-plugin-analyze",
|
|
581
|
+
),
|
|
582
|
+
PluginRegistryEntry(
|
|
583
|
+
name="deepctl-plugin-batch",
|
|
584
|
+
description="Batch processing utilities for large-scale transcription",
|
|
585
|
+
version="0.3.1",
|
|
586
|
+
author="Deepgram",
|
|
587
|
+
url="https://github.com/deepgram/deepctl-plugin-batch",
|
|
588
|
+
keywords=["batch", "bulk", "processing", "scale"],
|
|
589
|
+
install_name="deepctl-plugin-batch",
|
|
590
|
+
),
|
|
591
|
+
PluginRegistryEntry(
|
|
592
|
+
name="deepctl-plugin-export",
|
|
593
|
+
description="Export transcriptions to various formats (SRT, VTT, JSON, etc.)",
|
|
594
|
+
version="0.2.5",
|
|
595
|
+
author="Deepgram Community",
|
|
596
|
+
url="https://github.com/deepgram-community/deepctl-plugin-export",
|
|
597
|
+
keywords=["export", "srt", "vtt", "subtitle", "captions"],
|
|
598
|
+
install_name="deepctl-plugin-export",
|
|
599
|
+
),
|
|
600
|
+
PluginRegistryEntry(
|
|
601
|
+
name="deepctl-plugin-realtime",
|
|
602
|
+
description="Enhanced real-time transcription with WebSocket support",
|
|
603
|
+
version="0.4.0",
|
|
604
|
+
author="Deepgram",
|
|
605
|
+
url="https://github.com/deepgram/deepctl-plugin-realtime",
|
|
606
|
+
keywords=["realtime", "websocket", "streaming", "live"],
|
|
607
|
+
install_name="deepctl-plugin-realtime",
|
|
608
|
+
),
|
|
609
|
+
PluginRegistryEntry(
|
|
610
|
+
name="deepctl-plugin-translate",
|
|
611
|
+
description="Translation capabilities for transcribed text",
|
|
612
|
+
version="0.1.2",
|
|
613
|
+
author="Deepgram Labs",
|
|
614
|
+
url="https://github.com/deepgram-labs/deepctl-plugin-translate",
|
|
615
|
+
keywords=["translate", "translation", "language", "i18n"],
|
|
616
|
+
install_name="deepctl-plugin-translate",
|
|
617
|
+
),
|
|
618
|
+
]
|
|
619
|
+
|
|
620
|
+
def install_plugin(
|
|
621
|
+
self,
|
|
622
|
+
config: Config,
|
|
623
|
+
auth_manager: AuthManager,
|
|
624
|
+
client: DeepgramClient,
|
|
625
|
+
options: PluginInstallOptions,
|
|
626
|
+
) -> PluginOperationResult:
|
|
627
|
+
"""Install a plugin.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
config: CLI configuration
|
|
631
|
+
auth_manager: Authentication manager
|
|
632
|
+
client: Deepgram client
|
|
633
|
+
options: Installation options
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Operation result
|
|
637
|
+
"""
|
|
638
|
+
# Detect installation method
|
|
639
|
+
install_info = self.detector.detect()
|
|
640
|
+
|
|
641
|
+
# Determine which Python executable to use
|
|
642
|
+
if install_info.method in [
|
|
643
|
+
InstallMethod.SYSTEM,
|
|
644
|
+
InstallMethod.UNKNOWN,
|
|
645
|
+
]:
|
|
646
|
+
# For system installations, use isolated plugin environment
|
|
647
|
+
print_info(
|
|
648
|
+
"System installation detected, using isolated plugin environment..."
|
|
649
|
+
)
|
|
650
|
+
success, python_exe = self._ensure_plugin_environment()
|
|
651
|
+
if not success:
|
|
652
|
+
return PluginOperationResult(
|
|
653
|
+
success=False,
|
|
654
|
+
action=PluginAction.INSTALL,
|
|
655
|
+
package=options.package,
|
|
656
|
+
message="Failed to create plugin environment",
|
|
657
|
+
error="Could not create isolated environment for plugins",
|
|
658
|
+
)
|
|
659
|
+
target_python = python_exe
|
|
660
|
+
using_plugin_env = True
|
|
661
|
+
else:
|
|
662
|
+
# For other installations, use the same environment
|
|
663
|
+
target_python = self._python_executable
|
|
664
|
+
using_plugin_env = False
|
|
665
|
+
|
|
666
|
+
# Build pip command
|
|
667
|
+
cmd = [target_python, "-m", "pip", "install"]
|
|
668
|
+
|
|
669
|
+
# Add options
|
|
670
|
+
if options.upgrade:
|
|
671
|
+
cmd.append("--upgrade")
|
|
672
|
+
if options.pre:
|
|
673
|
+
cmd.append("--pre")
|
|
674
|
+
if options.force_reinstall:
|
|
675
|
+
cmd.append("--force-reinstall")
|
|
676
|
+
if options.index_url:
|
|
677
|
+
cmd.extend(["--index-url", options.index_url])
|
|
678
|
+
if options.extra_index_url:
|
|
679
|
+
cmd.extend(["--extra-index-url", options.extra_index_url])
|
|
680
|
+
if options.editable:
|
|
681
|
+
cmd.append("--editable")
|
|
682
|
+
|
|
683
|
+
# Add package
|
|
684
|
+
if options.git_url:
|
|
685
|
+
package_spec = options.git_url
|
|
686
|
+
elif options.version:
|
|
687
|
+
package_spec = f"{options.package}=={options.version}"
|
|
688
|
+
else:
|
|
689
|
+
package_spec = options.package
|
|
690
|
+
|
|
691
|
+
cmd.append(package_spec)
|
|
692
|
+
|
|
693
|
+
# Execute installation
|
|
694
|
+
try:
|
|
695
|
+
print_info(f"Installing {options.package}...")
|
|
696
|
+
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
697
|
+
|
|
698
|
+
# Get installed version
|
|
699
|
+
installed_version = self._get_package_version(
|
|
700
|
+
options.package, target_python
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
# Update plugin state if using plugin environment
|
|
704
|
+
if using_plugin_env:
|
|
705
|
+
state = self._get_plugin_state()
|
|
706
|
+
state["plugins"][options.package] = {
|
|
707
|
+
"version": installed_version,
|
|
708
|
+
"source": "plugin_env",
|
|
709
|
+
}
|
|
710
|
+
self._save_plugin_state(state)
|
|
711
|
+
|
|
712
|
+
return PluginOperationResult(
|
|
713
|
+
success=True,
|
|
714
|
+
action=PluginAction.INSTALL,
|
|
715
|
+
package=options.package,
|
|
716
|
+
message=f"Successfully installed {options.package}",
|
|
717
|
+
installed_version=installed_version,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
except subprocess.CalledProcessError as e:
|
|
721
|
+
return PluginOperationResult(
|
|
722
|
+
success=False,
|
|
723
|
+
action=PluginAction.INSTALL,
|
|
724
|
+
package=options.package,
|
|
725
|
+
message=f"Failed to install {options.package}",
|
|
726
|
+
error=e.stderr or str(e),
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
def list_plugins(
|
|
730
|
+
self,
|
|
731
|
+
config: Config,
|
|
732
|
+
auth_manager: AuthManager,
|
|
733
|
+
client: DeepgramClient,
|
|
734
|
+
verbose: bool = False,
|
|
735
|
+
) -> None:
|
|
736
|
+
"""List installed plugins.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
config: CLI configuration
|
|
740
|
+
auth_manager: Authentication manager
|
|
741
|
+
client: Deepgram client
|
|
742
|
+
verbose: Show detailed information
|
|
743
|
+
"""
|
|
744
|
+
# Get all installed packages that provide deepctl plugins
|
|
745
|
+
plugins = self._discover_plugins()
|
|
746
|
+
|
|
747
|
+
if not plugins:
|
|
748
|
+
print_info("No plugins installed")
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
# Create table
|
|
752
|
+
table = Table(title="Installed Plugins")
|
|
753
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
754
|
+
table.add_column("Version", style="green")
|
|
755
|
+
table.add_column("Entry Point", style="dim")
|
|
756
|
+
|
|
757
|
+
if verbose:
|
|
758
|
+
table.add_column("Type", style="yellow")
|
|
759
|
+
table.add_column("Commands", style="magenta")
|
|
760
|
+
table.add_column("Environment", style="blue")
|
|
761
|
+
|
|
762
|
+
for plugin in plugins:
|
|
763
|
+
row = [
|
|
764
|
+
plugin.name,
|
|
765
|
+
plugin.version or "Unknown",
|
|
766
|
+
plugin.entry_point or "N/A",
|
|
767
|
+
]
|
|
768
|
+
|
|
769
|
+
if verbose:
|
|
770
|
+
plugin_type = "Built-in" if plugin.is_builtin else "External"
|
|
771
|
+
commands = self._get_plugin_commands(plugin.name)
|
|
772
|
+
|
|
773
|
+
# Check if this is from plugin environment
|
|
774
|
+
state = self._get_plugin_state()
|
|
775
|
+
env = (
|
|
776
|
+
"Plugin Env"
|
|
777
|
+
if plugin.name in state.get("plugins", {})
|
|
778
|
+
else "Main Env"
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
row.extend([plugin_type, commands, env])
|
|
782
|
+
|
|
783
|
+
table.add_row(*row)
|
|
784
|
+
|
|
785
|
+
console.print(table)
|
|
786
|
+
|
|
787
|
+
if verbose:
|
|
788
|
+
# Show installation info
|
|
789
|
+
install_info = self.detector.detect()
|
|
790
|
+
print_info(f"\nInstallation method: {install_info.method.value}")
|
|
791
|
+
if install_info.method == InstallMethod.SYSTEM:
|
|
792
|
+
print_info(
|
|
793
|
+
"Using isolated plugin environment at: ~/.deepctl/plugins/venv"
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
def update_plugin(
|
|
797
|
+
self,
|
|
798
|
+
config: Config,
|
|
799
|
+
auth_manager: AuthManager,
|
|
800
|
+
client: DeepgramClient,
|
|
801
|
+
package: str,
|
|
802
|
+
pre: bool = False,
|
|
803
|
+
) -> PluginOperationResult:
|
|
804
|
+
"""Update a plugin.
|
|
805
|
+
|
|
806
|
+
Args:
|
|
807
|
+
config: CLI configuration
|
|
808
|
+
auth_manager: Authentication manager
|
|
809
|
+
client: Deepgram client
|
|
810
|
+
package: Package name to update
|
|
811
|
+
pre: Include pre-release versions
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
Operation result
|
|
815
|
+
"""
|
|
816
|
+
options = PluginInstallOptions(
|
|
817
|
+
package=package,
|
|
818
|
+
upgrade=True,
|
|
819
|
+
pre=pre,
|
|
820
|
+
)
|
|
821
|
+
return self.install_plugin(config, auth_manager, client, options)
|
|
822
|
+
|
|
823
|
+
def remove_plugin(
|
|
824
|
+
self,
|
|
825
|
+
config: Config,
|
|
826
|
+
auth_manager: AuthManager,
|
|
827
|
+
client: DeepgramClient,
|
|
828
|
+
package: str,
|
|
829
|
+
) -> PluginOperationResult:
|
|
830
|
+
"""Remove a plugin.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
config: CLI configuration
|
|
834
|
+
auth_manager: Authentication manager
|
|
835
|
+
client: Deepgram client
|
|
836
|
+
package: Package name to remove
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
Operation result
|
|
840
|
+
"""
|
|
841
|
+
# Check if plugin is installed
|
|
842
|
+
plugins = self._discover_plugins()
|
|
843
|
+
plugin_found = any(
|
|
844
|
+
p.name == package for p in plugins if not p.is_builtin
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
if not plugin_found:
|
|
848
|
+
return PluginOperationResult(
|
|
849
|
+
success=False,
|
|
850
|
+
action=PluginAction.REMOVE,
|
|
851
|
+
package=package,
|
|
852
|
+
message=f"Plugin {package} is not installed",
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
# Detect installation method
|
|
856
|
+
install_info = self.detector.detect()
|
|
857
|
+
|
|
858
|
+
# Determine which Python executable to use
|
|
859
|
+
state = self._get_plugin_state()
|
|
860
|
+
if package in state.get("plugins", {}):
|
|
861
|
+
# Plugin is in isolated environment
|
|
862
|
+
_, python_exe = self._ensure_plugin_environment()
|
|
863
|
+
target_python = python_exe
|
|
864
|
+
using_plugin_env = True
|
|
865
|
+
elif install_info.method in [
|
|
866
|
+
InstallMethod.SYSTEM,
|
|
867
|
+
InstallMethod.UNKNOWN,
|
|
868
|
+
]:
|
|
869
|
+
# System installation but plugin not in plugin env - shouldn't happen
|
|
870
|
+
return PluginOperationResult(
|
|
871
|
+
success=False,
|
|
872
|
+
action=PluginAction.REMOVE,
|
|
873
|
+
package=package,
|
|
874
|
+
message="Cannot remove plugin from system installation",
|
|
875
|
+
error="This plugin was not installed via deepctl",
|
|
876
|
+
)
|
|
877
|
+
else:
|
|
878
|
+
# Use same environment
|
|
879
|
+
target_python = self._python_executable
|
|
880
|
+
using_plugin_env = False
|
|
881
|
+
|
|
882
|
+
# Build pip command
|
|
883
|
+
cmd = [target_python, "-m", "pip", "uninstall", "-y", package]
|
|
884
|
+
|
|
885
|
+
# Execute removal
|
|
886
|
+
try:
|
|
887
|
+
print_info(f"Removing {package}...")
|
|
888
|
+
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
889
|
+
|
|
890
|
+
# Update plugin state if using plugin environment
|
|
891
|
+
if using_plugin_env:
|
|
892
|
+
state = self._get_plugin_state()
|
|
893
|
+
state["plugins"].pop(package, None)
|
|
894
|
+
self._save_plugin_state(state)
|
|
895
|
+
|
|
896
|
+
return PluginOperationResult(
|
|
897
|
+
success=True,
|
|
898
|
+
action=PluginAction.REMOVE,
|
|
899
|
+
package=package,
|
|
900
|
+
message=f"Successfully removed {package}",
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
except subprocess.CalledProcessError as e:
|
|
904
|
+
return PluginOperationResult(
|
|
905
|
+
success=False,
|
|
906
|
+
action=PluginAction.REMOVE,
|
|
907
|
+
package=package,
|
|
908
|
+
message=f"Failed to remove {package}",
|
|
909
|
+
error=e.stderr or str(e),
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
def _discover_plugins(self) -> list[PluginPackage]:
|
|
913
|
+
"""Discover all installed plugins.
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
List of discovered plugins
|
|
917
|
+
"""
|
|
918
|
+
plugins = []
|
|
919
|
+
discovered_names = set()
|
|
920
|
+
|
|
921
|
+
# Discover from main environment
|
|
922
|
+
main_env_plugins = self._discover_from_environment(sys.executable)
|
|
923
|
+
for plugin in main_env_plugins:
|
|
924
|
+
# Use a combination of name and entry point to identify unique plugins
|
|
925
|
+
plugin_key = f"{plugin.name}:{plugin.entry_point}"
|
|
926
|
+
if plugin_key not in discovered_names:
|
|
927
|
+
discovered_names.add(plugin_key)
|
|
928
|
+
plugins.append(plugin)
|
|
929
|
+
|
|
930
|
+
# Also discover from plugin environment if it exists
|
|
931
|
+
if self._plugin_venv.exists():
|
|
932
|
+
_, plugin_python = self._ensure_plugin_environment()
|
|
933
|
+
plugin_env_plugins = self._discover_from_environment(plugin_python)
|
|
934
|
+
|
|
935
|
+
# Add only plugins not already discovered
|
|
936
|
+
for plugin in plugin_env_plugins:
|
|
937
|
+
plugin_key = f"{plugin.name}:{plugin.entry_point}"
|
|
938
|
+
if plugin_key not in discovered_names:
|
|
939
|
+
discovered_names.add(plugin_key)
|
|
940
|
+
plugins.append(plugin)
|
|
941
|
+
|
|
942
|
+
return plugins
|
|
943
|
+
|
|
944
|
+
def _discover_from_environment(
|
|
945
|
+
self, python_exe: str
|
|
946
|
+
) -> list[PluginPackage]:
|
|
947
|
+
"""Discover plugins from a specific Python environment.
|
|
948
|
+
|
|
949
|
+
Args:
|
|
950
|
+
python_exe: Path to Python executable
|
|
951
|
+
|
|
952
|
+
Returns:
|
|
953
|
+
List of discovered plugins
|
|
954
|
+
"""
|
|
955
|
+
plugins = []
|
|
956
|
+
|
|
957
|
+
# Get entry points from the specified environment
|
|
958
|
+
try:
|
|
959
|
+
# Use a simpler approach - run importlib.metadata directly
|
|
960
|
+
if python_exe == sys.executable:
|
|
961
|
+
# For the current environment, use importlib directly
|
|
962
|
+
import importlib.metadata as metadata
|
|
963
|
+
|
|
964
|
+
for dist in metadata.distributions():
|
|
965
|
+
if dist.metadata:
|
|
966
|
+
eps = dist.entry_points
|
|
967
|
+
if hasattr(eps, "select"):
|
|
968
|
+
# Newer versions
|
|
969
|
+
for ep in eps.select(group="deepctl.commands"):
|
|
970
|
+
plugins.append(
|
|
971
|
+
PluginPackage(
|
|
972
|
+
name=dist.name,
|
|
973
|
+
version=dist.version,
|
|
974
|
+
entry_point=f"{ep.name}={ep.value}",
|
|
975
|
+
is_builtin=dist.name.startswith(
|
|
976
|
+
"deepctl-cmd-"
|
|
977
|
+
),
|
|
978
|
+
)
|
|
979
|
+
)
|
|
980
|
+
# Also check for external plugins
|
|
981
|
+
for ep in eps.select(group="deepctl.plugins"):
|
|
982
|
+
plugins.append(
|
|
983
|
+
PluginPackage(
|
|
984
|
+
name=dist.name,
|
|
985
|
+
version=dist.version,
|
|
986
|
+
entry_point=f"{ep.name}={ep.value}",
|
|
987
|
+
is_builtin=False,
|
|
988
|
+
)
|
|
989
|
+
)
|
|
990
|
+
else:
|
|
991
|
+
# Older versions
|
|
992
|
+
if "deepctl.commands" in eps:
|
|
993
|
+
for ep in eps["deepctl.commands"]:
|
|
994
|
+
plugins.append(
|
|
995
|
+
PluginPackage(
|
|
996
|
+
name=dist.name,
|
|
997
|
+
version=dist.version,
|
|
998
|
+
entry_point=f"{ep.name}={ep.value}",
|
|
999
|
+
is_builtin=dist.name.startswith(
|
|
1000
|
+
"deepctl-cmd-"
|
|
1001
|
+
),
|
|
1002
|
+
)
|
|
1003
|
+
)
|
|
1004
|
+
# Also check for external plugins
|
|
1005
|
+
if "deepctl.plugins" in eps:
|
|
1006
|
+
for ep in eps["deepctl.plugins"]:
|
|
1007
|
+
plugins.append(
|
|
1008
|
+
PluginPackage(
|
|
1009
|
+
name=dist.name,
|
|
1010
|
+
version=dist.version,
|
|
1011
|
+
entry_point=f"{ep.name}={ep.value}",
|
|
1012
|
+
is_builtin=False,
|
|
1013
|
+
)
|
|
1014
|
+
)
|
|
1015
|
+
else:
|
|
1016
|
+
# For other environments, try subprocess
|
|
1017
|
+
code = """import json;from importlib import metadata;plugins=[];
|
|
1018
|
+
for dist in metadata.distributions():
|
|
1019
|
+
if dist.metadata:
|
|
1020
|
+
eps = dist.entry_points
|
|
1021
|
+
if hasattr(eps, 'select'):
|
|
1022
|
+
for group in ['deepctl.commands', 'deepctl.plugins']:
|
|
1023
|
+
for ep in eps.select(group=group):
|
|
1024
|
+
plugins.append({
|
|
1025
|
+
"name": dist.name,
|
|
1026
|
+
"version": dist.version,
|
|
1027
|
+
"entry_point": f"{ep.name}={ep.value}",
|
|
1028
|
+
"is_builtin": dist.name.startswith("deepctl-cmd-") if group == "deepctl.commands" else False
|
|
1029
|
+
})
|
|
1030
|
+
else:
|
|
1031
|
+
for group in ['deepctl.commands', 'deepctl.plugins']:
|
|
1032
|
+
if group in eps:
|
|
1033
|
+
for ep in eps[group]:
|
|
1034
|
+
plugins.append({
|
|
1035
|
+
"name": dist.name,
|
|
1036
|
+
"version": dist.version,
|
|
1037
|
+
"entry_point": f"{ep.name}={ep.value}",
|
|
1038
|
+
"is_builtin": dist.name.startswith("deepctl-cmd-") if group == "deepctl.commands" else False
|
|
1039
|
+
})
|
|
1040
|
+
print(json.dumps(plugins))"""
|
|
1041
|
+
|
|
1042
|
+
result = subprocess.run(
|
|
1043
|
+
[python_exe, "-c", code],
|
|
1044
|
+
capture_output=True,
|
|
1045
|
+
text=True,
|
|
1046
|
+
check=True,
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
plugin_data = json.loads(result.stdout)
|
|
1050
|
+
for data in plugin_data:
|
|
1051
|
+
plugins.append(PluginPackage(**data))
|
|
1052
|
+
|
|
1053
|
+
except Exception as e:
|
|
1054
|
+
# Silently ignore subprocess failures for plugin environments
|
|
1055
|
+
if python_exe != sys.executable:
|
|
1056
|
+
pass
|
|
1057
|
+
else:
|
|
1058
|
+
print_info(f"Warning: Could not discover plugins: {e}")
|
|
1059
|
+
|
|
1060
|
+
return plugins
|
|
1061
|
+
|
|
1062
|
+
def _get_package_version(
|
|
1063
|
+
self, package: str, python_exe: str | None = None
|
|
1064
|
+
) -> str | None:
|
|
1065
|
+
"""Get installed package version.
|
|
1066
|
+
|
|
1067
|
+
Args:
|
|
1068
|
+
package: Package name
|
|
1069
|
+
python_exe: Python executable to use (defaults to current)
|
|
1070
|
+
|
|
1071
|
+
Returns:
|
|
1072
|
+
Package version or None
|
|
1073
|
+
"""
|
|
1074
|
+
if python_exe is None:
|
|
1075
|
+
python_exe = self._python_executable
|
|
1076
|
+
|
|
1077
|
+
try:
|
|
1078
|
+
code = f"""
|
|
1079
|
+
import importlib.metadata
|
|
1080
|
+
try:
|
|
1081
|
+
print(importlib.metadata.version("{package}"))
|
|
1082
|
+
except:
|
|
1083
|
+
print("")
|
|
1084
|
+
"""
|
|
1085
|
+
result = subprocess.run(
|
|
1086
|
+
[python_exe, "-c", code],
|
|
1087
|
+
capture_output=True,
|
|
1088
|
+
text=True,
|
|
1089
|
+
check=True,
|
|
1090
|
+
)
|
|
1091
|
+
version = result.stdout.strip()
|
|
1092
|
+
return version if version else None
|
|
1093
|
+
except Exception:
|
|
1094
|
+
return None
|
|
1095
|
+
|
|
1096
|
+
def _get_plugin_commands(self, plugin_name: str) -> str:
|
|
1097
|
+
"""Get commands provided by a plugin.
|
|
1098
|
+
|
|
1099
|
+
Args:
|
|
1100
|
+
plugin_name: Plugin package name
|
|
1101
|
+
|
|
1102
|
+
Returns:
|
|
1103
|
+
Comma-separated list of commands
|
|
1104
|
+
"""
|
|
1105
|
+
# This is a simplified version - in reality you'd introspect
|
|
1106
|
+
# the entry points to find actual command names
|
|
1107
|
+
return "varies"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Models for plugin management command."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from deepctl_core.models import BaseModel
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PluginAction(str, Enum):
|
|
10
|
+
"""Plugin management actions."""
|
|
11
|
+
|
|
12
|
+
INSTALL = "install"
|
|
13
|
+
LIST = "list"
|
|
14
|
+
UPDATE = "update"
|
|
15
|
+
REMOVE = "remove"
|
|
16
|
+
SEARCH = "search"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PluginPackage(BaseModel):
|
|
20
|
+
"""Information about a plugin package."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
version: str | None = None
|
|
24
|
+
source: str | None = None # pypi, git, local
|
|
25
|
+
installed_version: str | None = None
|
|
26
|
+
available_version: str | None = None
|
|
27
|
+
entry_point: str | None = None
|
|
28
|
+
is_builtin: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PluginRegistryEntry(BaseModel):
|
|
32
|
+
"""Entry in the plugin registry."""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
description: str
|
|
36
|
+
version: str
|
|
37
|
+
author: str | None = None
|
|
38
|
+
url: str | None = None
|
|
39
|
+
keywords: list[str] = Field(default_factory=list)
|
|
40
|
+
# Package name for installation if different from name
|
|
41
|
+
install_name: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PluginSearchResult(BaseModel):
|
|
45
|
+
"""Result from plugin search."""
|
|
46
|
+
|
|
47
|
+
plugin: PluginRegistryEntry
|
|
48
|
+
installed: bool = False
|
|
49
|
+
installed_version: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PluginInstallOptions(BaseModel):
|
|
53
|
+
"""Options for plugin installation."""
|
|
54
|
+
|
|
55
|
+
package: str
|
|
56
|
+
version: str | None = None
|
|
57
|
+
upgrade: bool = False
|
|
58
|
+
pre: bool = False # Allow pre-release versions
|
|
59
|
+
force_reinstall: bool = False
|
|
60
|
+
index_url: str | None = None
|
|
61
|
+
extra_index_url: str | None = None
|
|
62
|
+
git_url: str | None = None
|
|
63
|
+
editable: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PluginOperationResult(BaseModel):
|
|
67
|
+
"""Result of a plugin operation."""
|
|
68
|
+
|
|
69
|
+
success: bool
|
|
70
|
+
action: str # One of PluginAction values
|
|
71
|
+
package: str
|
|
72
|
+
message: str
|
|
73
|
+
error: str | None = None
|
|
74
|
+
installed_version: str | None = None
|
|
75
|
+
previous_version: str | None = None
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepctl-cmd-plugin
|
|
3
|
+
Version: 0.1.10
|
|
4
|
+
Summary: Plugin management command for deepctl
|
|
5
|
+
Author-email: Deepgram DevRel <devrel@deepgram.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: deepctl-core>=0.1.10
|
|
8
|
+
Requires-Dist: packaging>=21.0
|
|
9
|
+
Requires-Dist: toml>=0.10.2
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# deepctl-cmd-plugin
|
|
13
|
+
|
|
14
|
+
Plugin management command for deepctl CLI.
|
|
15
|
+
|
|
16
|
+
## Description
|
|
17
|
+
|
|
18
|
+
This command provides plugin management functionality for deepctl, allowing users to:
|
|
19
|
+
|
|
20
|
+
- Search for available plugins in the registry
|
|
21
|
+
- Install plugins from PyPI, GitHub, or local paths
|
|
22
|
+
- List installed plugins
|
|
23
|
+
- Update plugins to newer versions
|
|
24
|
+
- Remove plugins
|
|
25
|
+
|
|
26
|
+
This solves the environment isolation issue that occurs when deepctl is installed via pipx, uv, brew, or other package managers that create isolated environments.
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
- `deepctl plugin search [query]` - Search for available plugins
|
|
31
|
+
- `deepctl plugin install <package>` - Install a plugin
|
|
32
|
+
- `deepctl plugin list` - List installed plugins
|
|
33
|
+
- `deepctl plugin update <package>` - Update a plugin
|
|
34
|
+
- `deepctl plugin remove <package>` - Remove a plugin
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
### Search for plugins
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Show all available plugins
|
|
42
|
+
deepctl plugin search
|
|
43
|
+
|
|
44
|
+
# Search for plugins by keyword
|
|
45
|
+
deepctl plugin search audio
|
|
46
|
+
|
|
47
|
+
# Show only installed plugins
|
|
48
|
+
deepctl plugin search --installed
|
|
49
|
+
|
|
50
|
+
# Show only available (not installed) plugins
|
|
51
|
+
deepctl plugin search --available
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Install plugins
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Install from PyPI
|
|
58
|
+
deepctl plugin install deepctl-plugin-example
|
|
59
|
+
|
|
60
|
+
# Install from GitHub
|
|
61
|
+
deepctl plugin install git+https://github.com/deepgram/deepctl-plugin-custom.git
|
|
62
|
+
|
|
63
|
+
# Install specific version
|
|
64
|
+
deepctl plugin install deepctl-plugin-example==1.0.0
|
|
65
|
+
|
|
66
|
+
# Install from local directory
|
|
67
|
+
deepctl plugin install -e /path/to/plugin
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## How it works
|
|
71
|
+
|
|
72
|
+
The plugin command automatically detects how deepctl was installed:
|
|
73
|
+
|
|
74
|
+
- **pip/pipx/uv**: Plugins install directly into the same environment
|
|
75
|
+
- **System installations (brew, apt, etc.)**: Plugins install into an isolated environment at `~/.deepctl/plugins/venv`
|
|
76
|
+
|
|
77
|
+
This approach ensures all installation methods support plugins while maintaining system package manager integrity.
|
|
78
|
+
|
|
79
|
+
## Plugin Registry
|
|
80
|
+
|
|
81
|
+
The `plugin search` command shows plugins from a curated registry. Currently, the registry is hardcoded but will eventually fetch from a `.well-known` URL or plugin registry service.
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
This is a built-in command that is part of the deepctl core distribution.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
deepctl_cmd_plugin/__init__.py,sha256=sSUnnSKmU3b4E0deVbDAfzj5K5YAWGjOnhdluMHNoUw,110
|
|
2
|
+
deepctl_cmd_plugin/command.py,sha256=O5bQS0y9iHG-9JVH5iPoaDP9_ORs4rwPDAIksiwUoGs,39181
|
|
3
|
+
deepctl_cmd_plugin/models.py,sha256=URxMP3EhmJ1IftNS9-t-t5uziSFG1jSklKPGTGT2NGs,1821
|
|
4
|
+
deepctl_cmd_plugin-0.1.10.dist-info/METADATA,sha256=TlHqgUnfIGiR0FuI5kNjylepiwTA_CoWJ_xWIAQy_D8,2478
|
|
5
|
+
deepctl_cmd_plugin-0.1.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
deepctl_cmd_plugin-0.1.10.dist-info/entry_points.txt,sha256=dB_y0wmCYzoVlrN3CugB4EwNjE6AxYuh07-kkgy3Mpk,69
|
|
7
|
+
deepctl_cmd_plugin-0.1.10.dist-info/RECORD,,
|