xcorecli 1.0.0__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.
- xcli/__init__.py +0 -0
- xcli/_run.py +14 -0
- xcli/_xcore.py +56 -0
- xcli/init/__init__.py +0 -0
- xcli/init/cli.py +11 -0
- xcli/init/manager.py +141 -0
- xcli/init/upgrade.py +198 -0
- xcli/main.py +64 -0
- xcli/manager/__init__.py +0 -0
- xcli/manager/cli.py +537 -0
- xcli/marketplace/__init__.py +0 -0
- xcli/marketplace/cli.py +44 -0
- xcli/migrations/__init__.py +3 -0
- xcli/migrations/cli.py +292 -0
- xcli/migrations/runtime.py +198 -0
- xcli/plugin/__init__.py +0 -0
- xcli/plugin/cli.py +538 -0
- xcli/plugin/scaffold.py +213 -0
- xcli/sandbox/__init__.py +0 -0
- xcli/sandbox/cli.py +124 -0
- xcli/worker/__init__.py +0 -0
- xcli/worker/cli.py +569 -0
- xcli/worker/worker.py +565 -0
- xcorecli-1.0.0.dist-info/METADATA +11 -0
- xcorecli-1.0.0.dist-info/RECORD +27 -0
- xcorecli-1.0.0.dist-info/WHEEL +4 -0
- xcorecli-1.0.0.dist-info/entry_points.txt +3 -0
xcli/plugin/cli.py
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
import yaml
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.prompt import Confirm, Prompt
|
|
10
|
+
from rich.tree import Tree
|
|
11
|
+
from typer import Typer
|
|
12
|
+
|
|
13
|
+
from xcli._run import ns, run
|
|
14
|
+
from .scaffold import scaffold
|
|
15
|
+
|
|
16
|
+
_CTX = {"help_option_names": ["-h", "--help"]}
|
|
17
|
+
app = Typer(help="Plugin management.", context_settings=_CTX)
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
21
|
+
_CONFIG_CANDIDATES = [
|
|
22
|
+
"integration.yaml", "integration.json",
|
|
23
|
+
"config/integration.yaml", "config/integration.json",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _plugins_dir() -> Path:
|
|
28
|
+
for candidate in _CONFIG_CANDIDATES:
|
|
29
|
+
path = Path(candidate)
|
|
30
|
+
if not path.exists():
|
|
31
|
+
continue
|
|
32
|
+
try:
|
|
33
|
+
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
34
|
+
d = raw.get("plugins", {}).get("directory")
|
|
35
|
+
if d:
|
|
36
|
+
return Path(d)
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
console.print("[yellow]⚠[/yellow] No integration.yaml — run [cyan]xcli init[/cyan] first.")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _print_tree(root: Path, created: list[Path]) -> None:
|
|
44
|
+
tree = Tree(f"[bold green]{root.name}/[/bold green]")
|
|
45
|
+
dirs: dict = {}
|
|
46
|
+
for path in sorted(created):
|
|
47
|
+
rel = path.relative_to(root)
|
|
48
|
+
parts = rel.parts
|
|
49
|
+
current = tree
|
|
50
|
+
for i, part in enumerate(parts[:-1]):
|
|
51
|
+
key = "/".join(parts[: i + 1])
|
|
52
|
+
if key not in dirs:
|
|
53
|
+
dirs[key] = current.add(f"[cyan]{part}/[/cyan]")
|
|
54
|
+
current = dirs[key]
|
|
55
|
+
current.add(f"[white]{parts[-1]}[/white]")
|
|
56
|
+
console.print(tree)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── new ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
@app.command("new")
|
|
62
|
+
def new() -> None:
|
|
63
|
+
"""Scaffold a new plugin in the directory from integration.yaml."""
|
|
64
|
+
console.print("\n[bold]xcli plugin new[/bold]\n")
|
|
65
|
+
plugins_root = _plugins_dir()
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
name = Prompt.ask("Plugin name [dim](lowercase, underscores)[/dim]")
|
|
69
|
+
if _NAME_RE.match(name):
|
|
70
|
+
break
|
|
71
|
+
console.print("[red]Lowercase letters, digits and underscores only.[/red]")
|
|
72
|
+
|
|
73
|
+
mode = Prompt.ask(
|
|
74
|
+
"Execution mode", choices=["trusted", "sandboxed", "legacy"], default="trusted"
|
|
75
|
+
)
|
|
76
|
+
target = plugins_root / name
|
|
77
|
+
|
|
78
|
+
if target.exists() and not Confirm.ask(f"[yellow]{target}[/yellow] exists. Continue?", default=False):
|
|
79
|
+
sys.exit(0)
|
|
80
|
+
|
|
81
|
+
created = scaffold({"name": name, "execution_mode": mode}, target)
|
|
82
|
+
console.print()
|
|
83
|
+
_print_tree(target, created)
|
|
84
|
+
console.print(f"\n[green]✓[/green] [cyan]{target}[/cyan] ready.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── list ──────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
@app.command("list")
|
|
90
|
+
def list_plugins() -> None:
|
|
91
|
+
"""List installed plugins."""
|
|
92
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
93
|
+
run(handle_plugin(ns(subcommand="list")))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── health ────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
@app.command("health")
|
|
99
|
+
def health() -> None:
|
|
100
|
+
"""Health-check all installed plugins."""
|
|
101
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
102
|
+
run(handle_plugin(ns(subcommand="health")))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── install ───────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def _resolve_marketplace_version(name: str, version: str) -> tuple[str, str]:
|
|
108
|
+
"""
|
|
109
|
+
Returns (download_url, source_type) for the requested version.
|
|
110
|
+
Talks to the marketplace configured in integration.yaml.
|
|
111
|
+
"""
|
|
112
|
+
import asyncio
|
|
113
|
+
from xcore.configurations.loader import ConfigLoader
|
|
114
|
+
from xcore.marketplace import MarketplaceClient
|
|
115
|
+
|
|
116
|
+
cfg = ConfigLoader.load(None)
|
|
117
|
+
client = MarketplaceClient(cfg)
|
|
118
|
+
|
|
119
|
+
async def _fetch():
|
|
120
|
+
if version == "latest":
|
|
121
|
+
data = await client.get_plugin(name)
|
|
122
|
+
if not data:
|
|
123
|
+
console.print(f"[red]Plugin '[cyan]{name}[/cyan]' not found on marketplace.[/red]")
|
|
124
|
+
raise typer.Exit(1)
|
|
125
|
+
return data.get("download_url"), data.get("source_type", "zip")
|
|
126
|
+
|
|
127
|
+
versions = await client.get_versions(name)
|
|
128
|
+
match = next((v for v in versions if v.get("version") == version), None)
|
|
129
|
+
if not match:
|
|
130
|
+
available = ", ".join(v.get("version", "?") for v in versions) or "none"
|
|
131
|
+
console.print(f"[red]Version [cyan]{version}[/cyan] not found for [cyan]{name}[/cyan].[/red]")
|
|
132
|
+
console.print(f"Available: {available}")
|
|
133
|
+
console.print(f"Tip: [dim]xcli plugin versions {name}[/dim]")
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
return match.get("download_url"), match.get("source_type", "zip")
|
|
136
|
+
|
|
137
|
+
return asyncio.run(_fetch())
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("install")
|
|
141
|
+
def install(
|
|
142
|
+
plugin_spec: str = typer.Argument(..., help="Plugin to install: name, name@latest, or name@1.2.3"),
|
|
143
|
+
source: str = typer.Option("marketplace", "--source", "-s", help="marketplace | git | zip"),
|
|
144
|
+
url: Optional[str] = typer.Option(None, "--url", "-u", help="URL for git/zip source"),
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Install a plugin — supports name@version syntax for marketplace."""
|
|
147
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
148
|
+
|
|
149
|
+
# Parse name@version
|
|
150
|
+
if "@" in plugin_spec:
|
|
151
|
+
name, version = plugin_spec.split("@", 1)
|
|
152
|
+
else:
|
|
153
|
+
name, version = plugin_spec, None
|
|
154
|
+
|
|
155
|
+
if version and source == "marketplace":
|
|
156
|
+
console.print(f"Resolving [cyan]{name}@{version}[/cyan] on marketplace...")
|
|
157
|
+
resolved_url, resolved_source = _resolve_marketplace_version(name, version)
|
|
158
|
+
if not resolved_url:
|
|
159
|
+
console.print(f"[red]No download URL found for {name}@{version}.[/red]")
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
console.print(f" → [dim]{resolved_url}[/dim]")
|
|
162
|
+
run(handle_plugin(ns(subcommand="install", name=name, source=resolved_source, url=resolved_url)))
|
|
163
|
+
else:
|
|
164
|
+
run(handle_plugin(ns(subcommand="install", name=name, source=source, url=url)))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── versions ──────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
@app.command("versions")
|
|
170
|
+
def versions(name: str) -> None:
|
|
171
|
+
"""List available versions of a marketplace plugin."""
|
|
172
|
+
async def _run():
|
|
173
|
+
from rich.table import Table
|
|
174
|
+
from xcore.configurations.loader import ConfigLoader
|
|
175
|
+
from xcore.marketplace import MarketplaceClient
|
|
176
|
+
|
|
177
|
+
cfg = ConfigLoader.load(None)
|
|
178
|
+
client = MarketplaceClient(cfg)
|
|
179
|
+
|
|
180
|
+
with console.status(f"Fetching versions for [cyan]{name}[/cyan]..."):
|
|
181
|
+
data = await client.get_versions(name)
|
|
182
|
+
|
|
183
|
+
if not data:
|
|
184
|
+
console.print(f"[yellow]No versions found for '[cyan]{name}[/cyan]'.[/yellow]")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
table = Table(title=f"Versions — {name}")
|
|
188
|
+
table.add_column("Version", style="cyan")
|
|
189
|
+
table.add_column("Released", style="dim")
|
|
190
|
+
table.add_column("Source", style="green")
|
|
191
|
+
table.add_column("Download URL", style="dim", no_wrap=False)
|
|
192
|
+
for v in data:
|
|
193
|
+
table.add_row(
|
|
194
|
+
v.get("version", "?"),
|
|
195
|
+
v.get("released_at", "—"),
|
|
196
|
+
v.get("source_type", "zip"),
|
|
197
|
+
v.get("download_url", "—"),
|
|
198
|
+
)
|
|
199
|
+
console.print(table)
|
|
200
|
+
console.print(f"\n[dim]Install: xcli plugin install {name}@<version>[/dim]")
|
|
201
|
+
|
|
202
|
+
run(_run())
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ── update / update-all ───────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
def _installed_version(name: str) -> str | None:
|
|
208
|
+
"""Return the version string from the installed plugin.yaml, or None."""
|
|
209
|
+
plugin_dir = _plugins_dir() / name
|
|
210
|
+
manifest_path = plugin_dir / "plugin.yaml"
|
|
211
|
+
if not manifest_path.exists():
|
|
212
|
+
return None
|
|
213
|
+
try:
|
|
214
|
+
raw = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) or {}
|
|
215
|
+
return str(raw.get("version", "?"))
|
|
216
|
+
except Exception:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _do_update(name: str, target_version: str | None) -> None:
|
|
221
|
+
"""Core update logic: resolve version, remove, reinstall."""
|
|
222
|
+
import asyncio
|
|
223
|
+
import shutil
|
|
224
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
225
|
+
from xcore.configurations.loader import ConfigLoader
|
|
226
|
+
from xcore.marketplace import MarketplaceClient
|
|
227
|
+
|
|
228
|
+
current = _installed_version(name)
|
|
229
|
+
cfg = ConfigLoader.load(None)
|
|
230
|
+
client = MarketplaceClient(cfg)
|
|
231
|
+
|
|
232
|
+
async def _resolve():
|
|
233
|
+
if target_version and target_version != "latest":
|
|
234
|
+
versions = await client.get_versions(name)
|
|
235
|
+
match = next((v for v in versions if v.get("version") == target_version), None)
|
|
236
|
+
if not match:
|
|
237
|
+
available = ", ".join(v.get("version", "?") for v in versions)
|
|
238
|
+
console.print(f"[red]Version [cyan]{target_version}[/cyan] not found.[/red]")
|
|
239
|
+
console.print(f"Available: {available}")
|
|
240
|
+
raise typer.Exit(1)
|
|
241
|
+
return match.get("download_url"), match.get("source_type", "zip"), target_version
|
|
242
|
+
else:
|
|
243
|
+
data = await client.get_plugin(name)
|
|
244
|
+
if not data:
|
|
245
|
+
console.print(f"[red]Plugin '[cyan]{name}[/cyan]' not found on marketplace.[/red]")
|
|
246
|
+
raise typer.Exit(1)
|
|
247
|
+
return data.get("download_url"), data.get("source_type", "zip"), data.get("version", "latest")
|
|
248
|
+
|
|
249
|
+
url, source_type, new_version = asyncio.run(_resolve())
|
|
250
|
+
|
|
251
|
+
if current and current == new_version:
|
|
252
|
+
console.print(f"[green]✓[/green] [cyan]{name}[/cyan] is already at version [magenta]{current}[/magenta].")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
console.print(f"Updating [cyan]{name}[/cyan]: [dim]{current or '?'}[/dim] → [magenta]{new_version}[/magenta]")
|
|
256
|
+
if not url:
|
|
257
|
+
console.print(f"[red]No download URL for {name}@{new_version}.[/red]")
|
|
258
|
+
raise typer.Exit(1)
|
|
259
|
+
|
|
260
|
+
plugin_path = _plugins_dir() / name
|
|
261
|
+
if plugin_path.exists():
|
|
262
|
+
shutil.rmtree(plugin_path)
|
|
263
|
+
|
|
264
|
+
run(handle_plugin(ns(subcommand="install", name=name, source=source_type, url=url)))
|
|
265
|
+
console.print(f"[green]✓[/green] [cyan]{name}[/cyan] updated to [magenta]{new_version}[/magenta].")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@app.command("update")
|
|
269
|
+
def update(
|
|
270
|
+
plugin_spec: str = typer.Argument(..., help="Plugin to update: name or name@version"),
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Update an installed plugin to a newer version from the marketplace."""
|
|
273
|
+
if "@" in plugin_spec:
|
|
274
|
+
name, version = plugin_spec.split("@", 1)
|
|
275
|
+
else:
|
|
276
|
+
name, version = plugin_spec, None
|
|
277
|
+
_do_update(name, version)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@app.command("update-all")
|
|
281
|
+
def update_all(
|
|
282
|
+
check: bool = typer.Option(False, "--check", "-c", help="Only show available updates, do not install"),
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Check for and apply updates for all installed plugins."""
|
|
285
|
+
import asyncio
|
|
286
|
+
from rich.table import Table
|
|
287
|
+
from xcore.configurations.loader import ConfigLoader
|
|
288
|
+
from xcore.marketplace import MarketplaceClient
|
|
289
|
+
|
|
290
|
+
names = [p.name for p in _plugins_dir().iterdir() if p.is_dir() and not p.name.startswith("_")]
|
|
291
|
+
if not names:
|
|
292
|
+
console.print("[yellow]No installed plugins found.[/yellow]")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
cfg = ConfigLoader.load(None)
|
|
296
|
+
client = MarketplaceClient(cfg)
|
|
297
|
+
|
|
298
|
+
async def _fetch_all():
|
|
299
|
+
results = []
|
|
300
|
+
for name in names:
|
|
301
|
+
current = _installed_version(name)
|
|
302
|
+
try:
|
|
303
|
+
data = await client.get_plugin(name)
|
|
304
|
+
latest = data.get("version", "?") if data else None
|
|
305
|
+
url = data.get("download_url") if data else None
|
|
306
|
+
src = data.get("source_type", "zip") if data else None
|
|
307
|
+
except Exception:
|
|
308
|
+
latest = url = src = None
|
|
309
|
+
results.append((name, current, latest, url, src))
|
|
310
|
+
return results
|
|
311
|
+
|
|
312
|
+
with console.status("Checking marketplace for updates..."):
|
|
313
|
+
results = asyncio.run(_fetch_all())
|
|
314
|
+
|
|
315
|
+
table = Table(title="Plugin Update Status")
|
|
316
|
+
table.add_column("Plugin", style="cyan")
|
|
317
|
+
table.add_column("Installed", style="dim")
|
|
318
|
+
table.add_column("Latest", style="magenta")
|
|
319
|
+
table.add_column("Status", justify="center")
|
|
320
|
+
|
|
321
|
+
upgradable = []
|
|
322
|
+
for name, current, latest, url, src in results:
|
|
323
|
+
if latest is None:
|
|
324
|
+
status = "[dim]not on marketplace[/dim]"
|
|
325
|
+
elif current == latest:
|
|
326
|
+
status = "[green]up to date[/green]"
|
|
327
|
+
else:
|
|
328
|
+
status = "[yellow]update available[/yellow]"
|
|
329
|
+
upgradable.append((name, current, latest, url, src))
|
|
330
|
+
table.add_row(name, current or "?", latest or "—", status)
|
|
331
|
+
|
|
332
|
+
console.print(table)
|
|
333
|
+
|
|
334
|
+
if not upgradable:
|
|
335
|
+
console.print("\n[green]All plugins are up to date.[/green]")
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
if check:
|
|
339
|
+
console.print(f"\n[yellow]{len(upgradable)} update(s) available.[/yellow] Run without [dim]--check[/dim] to apply.")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
import shutil
|
|
343
|
+
from rich.prompt import Confirm
|
|
344
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
345
|
+
|
|
346
|
+
names_str = ", ".join(f"[cyan]{n}[/cyan]" for n, *_ in upgradable)
|
|
347
|
+
if not Confirm.ask(f"\nUpdate {names_str}?", default=True):
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
for name, current, latest, url, src in upgradable:
|
|
351
|
+
console.print(f"\nUpdating [cyan]{name}[/cyan]: [dim]{current}[/dim] → [magenta]{latest}[/magenta]")
|
|
352
|
+
plugin_path = _plugins_dir() / name
|
|
353
|
+
if plugin_path.exists():
|
|
354
|
+
shutil.rmtree(plugin_path)
|
|
355
|
+
run(handle_plugin(ns(subcommand="install", name=name, source=src, url=url)))
|
|
356
|
+
console.print(f"[green]✓[/green] {name} updated.")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ── remove ────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
@app.command("remove")
|
|
362
|
+
def remove(name: str) -> None:
|
|
363
|
+
"""Remove an installed plugin."""
|
|
364
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
365
|
+
run(handle_plugin(ns(subcommand="remove", name=name)))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ── info ──────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
@app.command("info")
|
|
371
|
+
def info(name: str) -> None:
|
|
372
|
+
"""Show details of an installed plugin."""
|
|
373
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
374
|
+
run(handle_plugin(ns(subcommand="info", name=name)))
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ── load / reload / unload / status / call ────────────────────
|
|
378
|
+
|
|
379
|
+
@app.command("load")
|
|
380
|
+
def load(name: str) -> None:
|
|
381
|
+
"""Load a plugin directly (boots xcore standalone)."""
|
|
382
|
+
async def _run():
|
|
383
|
+
from xcli._xcore import boot
|
|
384
|
+
xcore = await boot()
|
|
385
|
+
try:
|
|
386
|
+
await xcore.plugins.load(name)
|
|
387
|
+
console.print(f"[green]✓[/green] Plugin [cyan]{name}[/cyan] loaded.")
|
|
388
|
+
finally:
|
|
389
|
+
await xcore.plugins.shutdown()
|
|
390
|
+
run(_run())
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@app.command("reload")
|
|
394
|
+
def reload(name: str) -> None:
|
|
395
|
+
"""Reload a plugin directly (boots xcore standalone)."""
|
|
396
|
+
async def _run():
|
|
397
|
+
from xcli._xcore import boot
|
|
398
|
+
xcore = await boot()
|
|
399
|
+
try:
|
|
400
|
+
await xcore.plugins.reload(name)
|
|
401
|
+
console.print(f"[green]✓[/green] Plugin [cyan]{name}[/cyan] reloaded.")
|
|
402
|
+
finally:
|
|
403
|
+
await xcore.plugins.shutdown()
|
|
404
|
+
run(_run())
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@app.command("unload")
|
|
408
|
+
def unload(name: str) -> None:
|
|
409
|
+
"""Unload a plugin directly (boots xcore standalone)."""
|
|
410
|
+
async def _run():
|
|
411
|
+
from xcli._xcore import boot
|
|
412
|
+
xcore = await boot()
|
|
413
|
+
try:
|
|
414
|
+
await xcore.plugins.unload(name)
|
|
415
|
+
console.print(f"[green]✓[/green] Plugin [cyan]{name}[/cyan] unloaded.")
|
|
416
|
+
finally:
|
|
417
|
+
await xcore.plugins.shutdown()
|
|
418
|
+
run(_run())
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@app.command("status")
|
|
422
|
+
def status() -> None:
|
|
423
|
+
"""Show runtime status of all loaded plugins."""
|
|
424
|
+
async def _run():
|
|
425
|
+
from xcli._xcore import boot
|
|
426
|
+
from rich.table import Table
|
|
427
|
+
xcore = await boot()
|
|
428
|
+
try:
|
|
429
|
+
data = xcore.plugins.status()
|
|
430
|
+
table = Table(title=f"Plugin Runtime Status ({data['count']} plugins)")
|
|
431
|
+
table.add_column("Name", style="cyan")
|
|
432
|
+
table.add_column("State", justify="center")
|
|
433
|
+
table.add_column("Mode", style="green")
|
|
434
|
+
for p in data["plugins"]:
|
|
435
|
+
state = p.get("state", "?")
|
|
436
|
+
color = "green" if state == "running" else "yellow"
|
|
437
|
+
table.add_row(
|
|
438
|
+
p.get("name", "?"),
|
|
439
|
+
f"[{color}]{state}[/]",
|
|
440
|
+
p.get("mode", "?"),
|
|
441
|
+
)
|
|
442
|
+
console.print(table)
|
|
443
|
+
finally:
|
|
444
|
+
await xcore.plugins.shutdown()
|
|
445
|
+
run(_run())
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@app.command("call")
|
|
449
|
+
def call(
|
|
450
|
+
name: str,
|
|
451
|
+
action: str,
|
|
452
|
+
payload: str = typer.Option("{}", "--payload", "-p", help="JSON payload"),
|
|
453
|
+
) -> None:
|
|
454
|
+
"""Call a plugin action directly (boots xcore standalone)."""
|
|
455
|
+
import json
|
|
456
|
+
async def _run():
|
|
457
|
+
from xcli._xcore import boot
|
|
458
|
+
try:
|
|
459
|
+
data = json.loads(payload)
|
|
460
|
+
except json.JSONDecodeError as e:
|
|
461
|
+
console.print(f"[red]Invalid JSON payload:[/] {e}")
|
|
462
|
+
raise typer.Exit(1)
|
|
463
|
+
xcore = await boot()
|
|
464
|
+
try:
|
|
465
|
+
result = await xcore.plugins.call(name, action, data)
|
|
466
|
+
console.print(result)
|
|
467
|
+
finally:
|
|
468
|
+
await xcore.plugins.shutdown()
|
|
469
|
+
run(_run())
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ── marketplace ───────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
@app.command("browse")
|
|
475
|
+
def browse() -> None:
|
|
476
|
+
"""List all plugins available on the marketplace."""
|
|
477
|
+
from xcore.cli.marketplace_cmd import handle_marketplace
|
|
478
|
+
run(handle_marketplace(ns(subcommand="list")))
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@app.command("trending")
|
|
482
|
+
def trending() -> None:
|
|
483
|
+
"""Show trending plugins on the marketplace."""
|
|
484
|
+
from xcore.cli.marketplace_cmd import handle_marketplace
|
|
485
|
+
run(handle_marketplace(ns(subcommand="trending")))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@app.command("search")
|
|
489
|
+
def search(query: str) -> None:
|
|
490
|
+
"""Search the marketplace for a plugin."""
|
|
491
|
+
from xcore.cli.marketplace_cmd import handle_marketplace
|
|
492
|
+
run(handle_marketplace(ns(subcommand="search", query=query)))
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@app.command("show")
|
|
496
|
+
def show(name: str) -> None:
|
|
497
|
+
"""Show marketplace details for a plugin (use info for installed plugins)."""
|
|
498
|
+
from xcore.cli.marketplace_cmd import handle_marketplace
|
|
499
|
+
run(handle_marketplace(ns(subcommand="show", name=name)))
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@app.command("rate")
|
|
503
|
+
def rate(
|
|
504
|
+
name: str,
|
|
505
|
+
score: int = typer.Option(..., min=1, max=5, help="Score 1–5"),
|
|
506
|
+
) -> None:
|
|
507
|
+
"""Rate a marketplace plugin."""
|
|
508
|
+
from xcore.cli.marketplace_cmd import handle_marketplace
|
|
509
|
+
run(handle_marketplace(ns(subcommand="rate", name=name, score=score)))
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# ── sign / verify / validate ──────────────────────────────────
|
|
513
|
+
|
|
514
|
+
@app.command("sign")
|
|
515
|
+
def sign(
|
|
516
|
+
path: str,
|
|
517
|
+
key: Optional[str] = typer.Option(None, "--key", "-k", help="HMAC signing key (reads from config if omitted)"),
|
|
518
|
+
) -> None:
|
|
519
|
+
"""Sign a plugin with its HMAC key."""
|
|
520
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
521
|
+
run(handle_plugin(ns(subcommand="sign", path=path, key=key)))
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@app.command("verify")
|
|
525
|
+
def verify(
|
|
526
|
+
path: str,
|
|
527
|
+
key: Optional[str] = typer.Option(None, "--key", "-k", help="HMAC signing key"),
|
|
528
|
+
) -> None:
|
|
529
|
+
"""Verify a plugin signature."""
|
|
530
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
531
|
+
run(handle_plugin(ns(subcommand="verify", path=path, key=key)))
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@app.command("validate")
|
|
535
|
+
def validate(path: str) -> None:
|
|
536
|
+
"""Validate a plugin manifest."""
|
|
537
|
+
from xcore.cli.plugin_cmd import handle_plugin
|
|
538
|
+
run(handle_plugin(ns(subcommand="validate", path=path)))
|