feat: add hot-reload, shorthand commands, and plugin help
- Plugin registry: add unload_plugin(), reload_plugin(), path tracking - Bot: add load_plugin(), reload_plugin(), unload_plugin() public API - Core plugin: add !load, !reload, !unload, !plugins commands - Command dispatch: support unambiguous prefix matching (!h -> !help) - Help: support !help <plugin> to show plugin description and commands - Tests: 17 new tests covering hot-reload, prefix matching Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
"""Core plugin: ping, help, version."""
|
||||
"""Core plugin: ping, help, version, plugin management."""
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from derp import __version__
|
||||
from derp.plugin import command
|
||||
@@ -10,22 +12,37 @@ async def cmd_ping(bot, message):
|
||||
await bot.reply(message, "pong")
|
||||
|
||||
|
||||
@command("help", help="List commands or show command help")
|
||||
@command("help", help="List commands or show command/plugin help")
|
||||
async def cmd_help(bot, message):
|
||||
"""Show available commands or help for a specific command.
|
||||
"""Show available commands, or help for a specific command or plugin.
|
||||
|
||||
Usage: !help [command]
|
||||
Usage: !help [command|plugin]
|
||||
"""
|
||||
parts = message.text.split(None, 2)
|
||||
if len(parts) > 1:
|
||||
# Help for a specific command
|
||||
cmd_name = parts[1].lower().lstrip(bot.prefix)
|
||||
handler = bot.registry.commands.get(cmd_name)
|
||||
name = parts[1].lower().lstrip(bot.prefix)
|
||||
|
||||
# Check command first
|
||||
handler = bot.registry.commands.get(name)
|
||||
if handler:
|
||||
help_text = handler.help or "No help available."
|
||||
await bot.reply(message, f"{bot.prefix}{cmd_name} -- {help_text}")
|
||||
else:
|
||||
await bot.reply(message, f"Unknown command: {cmd_name}")
|
||||
await bot.reply(message, f"{bot.prefix}{name} -- {help_text}")
|
||||
return
|
||||
|
||||
# Check plugin
|
||||
module = bot.registry._modules.get(name)
|
||||
if module:
|
||||
desc = (getattr(module, "__doc__", "") or "").split("\n")[0].strip()
|
||||
cmds = sorted(
|
||||
k for k, v in bot.registry.commands.items() if v.plugin == name
|
||||
)
|
||||
lines = [f"{name} -- {desc}" if desc else name]
|
||||
if cmds:
|
||||
lines.append(f"Commands: {', '.join(bot.prefix + c for c in cmds)}")
|
||||
await bot.reply(message, " | ".join(lines))
|
||||
return
|
||||
|
||||
await bot.reply(message, f"Unknown command or plugin: {name}")
|
||||
return
|
||||
|
||||
# List all commands
|
||||
@@ -37,3 +54,61 @@ async def cmd_help(bot, message):
|
||||
async def cmd_version(bot, message):
|
||||
"""Report the running version."""
|
||||
await bot.reply(message, f"derp {__version__}")
|
||||
|
||||
|
||||
@command("load", help="Hot-load a plugin: !load <name>")
|
||||
async def cmd_load(bot, message):
|
||||
"""Load a new plugin from the plugins directory."""
|
||||
parts = message.text.split(None, 2)
|
||||
if len(parts) < 2:
|
||||
await bot.reply(message, "Usage: !load <plugin>")
|
||||
return
|
||||
name = parts[1].lower()
|
||||
ok, reason = bot.load_plugin(name)
|
||||
if ok:
|
||||
await bot.reply(message, f"Loaded plugin: {name} ({reason})")
|
||||
else:
|
||||
await bot.reply(message, f"Failed to load plugin: {reason}")
|
||||
|
||||
|
||||
@command("reload", help="Reload a plugin: !reload <name>")
|
||||
async def cmd_reload(bot, message):
|
||||
"""Unload and reload a plugin, picking up file changes."""
|
||||
parts = message.text.split(None, 2)
|
||||
if len(parts) < 2:
|
||||
await bot.reply(message, "Usage: !reload <plugin>")
|
||||
return
|
||||
name = parts[1].lower()
|
||||
ok, reason = bot.reload_plugin(name)
|
||||
if ok:
|
||||
await bot.reply(message, f"Reloaded plugin: {name}")
|
||||
else:
|
||||
await bot.reply(message, f"Failed to reload plugin: {reason}")
|
||||
|
||||
|
||||
@command("unload", help="Unload a plugin: !unload <name>")
|
||||
async def cmd_unload(bot, message):
|
||||
"""Unload a plugin, removing all its handlers."""
|
||||
parts = message.text.split(None, 2)
|
||||
if len(parts) < 2:
|
||||
await bot.reply(message, "Usage: !unload <plugin>")
|
||||
return
|
||||
name = parts[1].lower()
|
||||
ok, reason = bot.unload_plugin(name)
|
||||
if ok:
|
||||
await bot.reply(message, f"Unloaded plugin: {name}")
|
||||
else:
|
||||
await bot.reply(message, f"Failed to unload plugin: {reason}")
|
||||
|
||||
|
||||
@command("plugins", help="List loaded plugins")
|
||||
async def cmd_plugins(bot, message):
|
||||
"""List all loaded plugins with handler counts."""
|
||||
counts: Counter[str] = Counter()
|
||||
for handler in bot.registry.commands.values():
|
||||
counts[handler.plugin] += 1
|
||||
for handlers in bot.registry.events.values():
|
||||
for handler in handlers:
|
||||
counts[handler.plugin] += 1
|
||||
parts = [f"{name} ({counts[name]})" for name in sorted(counts)]
|
||||
await bot.reply(message, f"Plugins: {', '.join(parts)}")
|
||||
|
||||
Reference in New Issue
Block a user