feat(cli,wizard): auto-offer telegram_bot setup after install; integrate into pyra setup

cli.py — plugin_install() now asks "Configure now?" after a successful install,
runs the plugin's setup wizard, and offers to enable inline. Failing to install
short-circuits before the prompt is shown.

wizard.py — _offer_telegram_setup_if_selected() runs install + wizard + enable
automatically at the end of pyra setup when the user selected "Communication bots".
Adds load_config import (was missing alongside save_config).

Tests: test_plugin_install_decline_setup, test_plugin_install_error_does_not_prompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-19 23:15:29 +02:00
parent f59aa1a758
commit aba28293b7
3 changed files with 154 additions and 3 deletions
+46 -2
View File
@@ -165,6 +165,7 @@ def plugin_list() -> None:
@click.argument("name")
def plugin_install(name: str) -> None:
"""Install a bundled plugin to ~/.pyra/plugins/."""
import questionary
from pyra.plugins.install import get_bundled_plugins_dir, install_bundled_plugin
from pyra.utils.paths import pyra_home
@@ -173,12 +174,55 @@ def plugin_install(name: str) -> None:
try:
install_bundled_plugin(name, bundled_dir, plugins_dir)
console.print(f"[green]Installed:[/green] {name}")
console.print(f" Enable: [dim]pyra plugin enable {name}[/dim]")
console.print(f" Configure: [dim]pyra plugin setup {name}[/dim]")
except FileNotFoundError as exc:
console.print(f"[red]Error:[/red] {exc}")
return
except Exception as exc:
console.print(f"[red]Install failed:[/red] {exc}")
return
try:
configure_now = questionary.confirm(f"Configure {name} now?", default=True).ask()
except (KeyboardInterrupt, EOFError):
return
if not configure_now:
console.print(f" Configure later: [dim]pyra plugin setup {name}[/dim]")
console.print(f" Enable: [dim]pyra plugin enable {name}[/dim]")
return
from pyra.plugins.loader import load_plugin_by_name
from pyra.vault.writer import set_key as _set_key
p = load_plugin_by_name(name, plugins_dir)
if p is None:
console.print(f"[red]Could not load {name} for setup.[/red]")
return
try:
p.setup(console, _set_key)
except (KeyboardInterrupt, EOFError):
console.print("[dim]Setup cancelled.[/dim]")
return
except Exception as exc:
console.print(f"[red]Setup error:[/red] {exc}")
return
try:
enable_now = questionary.confirm(f"Enable {name} now?", default=True).ask()
except (KeyboardInterrupt, EOFError):
enable_now = False
if enable_now:
from pyra.config.manager import load_config, save_config
try:
cfg = load_config()
if name not in cfg.plugins.enabled:
cfg.plugins.enabled.append(name)
save_config(cfg)
console.print(f"[green]Enabled:[/green] {name}")
except Exception as exc:
console.print(f"[yellow]Could not enable automatically:[/yellow] {exc}")
console.print(f" Enable manually: [dim]pyra plugin enable {name}[/dim]")
console.print(f"[dim]Run [bold]pyra daemon start[/bold] to bring {name} online.[/dim]")
@plugin.command("enable")
+64 -1
View File
@@ -7,7 +7,7 @@ from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from pyra.config.manager import save_config
from pyra.config.manager import load_config, save_config
from pyra.config.schema import GeneralConfig, ProviderConfig, PyraConfig
from pyra.setup.providers import PROVIDERS, Provider, get_provider
from pyra.utils.paths import pyra_home, safe_chmod
@@ -167,6 +167,7 @@ def run_setup() -> None:
_delete_draft()
_suggest_plugins(use_cases)
_offer_telegram_setup_if_selected(use_cases)
console.print()
console.print(Panel(
@@ -238,6 +239,68 @@ def _suggest_plugins(use_cases: list[str]) -> None:
))
def _offer_telegram_setup_if_selected(use_cases: list[str]) -> None:
"""If the user chose 'Communication bots', offer to install and configure telegram_bot."""
relevant = any(
"telegram_bot" in _USE_CASE_PLUGINS.get(uc, [])
for uc in use_cases
)
if not relevant:
return
console.print()
try:
answer = questionary.confirm(
"Set up the Telegram bot for remote access to Pyra?", default=True
).ask()
except (KeyboardInterrupt, EOFError):
return
if not answer:
console.print(
" [dim]You can do this later: pyra plugin install telegram_bot[/dim]"
)
return
from pyra.plugins.install import get_bundled_plugins_dir, install_bundled_plugin
from pyra.plugins.loader import load_plugin_by_name
from pyra.utils.paths import pyra_home
from pyra.vault.writer import set_key
bundled_dir = get_bundled_plugins_dir()
plugins_dir = pyra_home() / "plugins"
try:
install_bundled_plugin("telegram_bot", bundled_dir, plugins_dir)
except Exception as exc:
console.print(f"[red]Could not install telegram_bot:[/red] {exc}")
return
p = load_plugin_by_name("telegram_bot", plugins_dir)
if p is None:
console.print("[red]Could not load telegram_bot for setup.[/red]")
return
try:
p.setup(console, set_key)
except (KeyboardInterrupt, EOFError):
console.print("[dim]Telegram setup skipped.[/dim]")
return
except Exception as exc:
console.print(f"[red]Telegram setup error:[/red] {exc}")
return
try:
cfg = load_config()
if "telegram_bot" not in cfg.plugins.enabled:
cfg.plugins.enabled.append("telegram_bot")
save_config(cfg)
console.print("[green]Telegram bot enabled.[/green]")
except Exception:
console.print(
" [dim]Enable manually: pyra plugin enable telegram_bot[/dim]"
)
def _choose_provider() -> Provider:
local = [p for p in PROVIDERS if p.group == "Local"]
cloud = [p for p in PROVIDERS if p.group == "Cloud"]
+44
View File
@@ -137,3 +137,47 @@ def test_main_skips_setup_when_config_exists(tmp_pyra_home, monkeypatch):
def test_config_slash_command_registered():
from pyra.chat.session import _STATIC_COMMANDS
assert "/config" in _STATIC_COMMANDS
# ── plugin install ────────────────────────────────────────────────────────────
def test_plugin_install_decline_setup(tmp_pyra_home, monkeypatch):
"""Declining 'Configure now?' shows manual instructions and exits cleanly."""
from unittest.mock import MagicMock
import questionary
monkeypatch.setattr(
"pyra.plugins.install.install_bundled_plugin", lambda *a, **kw: None
)
monkeypatch.setattr(
questionary, "confirm",
lambda *a, **kw: MagicMock(ask=lambda: False),
)
runner = CliRunner()
result = runner.invoke(main, ["plugin", "install", "telegram_bot"])
assert result.exit_code == 0
assert "Installed" in result.output
assert "Configure later" in result.output
def test_plugin_install_error_does_not_prompt(tmp_pyra_home, monkeypatch):
"""If install fails, the configure prompt is never shown."""
from unittest.mock import MagicMock
import questionary
monkeypatch.setattr(
"pyra.plugins.install.install_bundled_plugin",
lambda *a, **kw: (_ for _ in ()).throw(FileNotFoundError("not found")),
)
confirm_calls = []
monkeypatch.setattr(
questionary, "confirm",
lambda *a, **kw: confirm_calls.append(1) or MagicMock(ask=lambda: False),
)
runner = CliRunner()
result = runner.invoke(main, ["plugin", "install", "telegram_bot"])
assert result.exit_code == 0
assert "Error" in result.output
assert len(confirm_calls) == 0 # prompt never reached