Add support for local plugins configuration and directory handling

- Updated `config.ini.example` and `config.ini.minimal-example` to include optional `local_dir_path` for specifying a custom local plugins directory.
- Enhanced `install-service.sh` to preserve the `local/` directory during installation and create its structure if it doesn't exist.
- Modified `CommandManager` and `MeshCoreBot` to resolve the local commands and services directory based on the new configuration.
- Added validation in `config_validation.py` to check the existence and readability of the specified local plugins path.
- Improved documentation in `local-plugins.md` to clarify the usage of local plugins and the new configuration options.
This commit is contained in:
agessaman committed 2026-03-07 12:31:54 -08:00
1 parent e9f17ec48e
commit f86fe0b140
7 files changed
+74 -6

No files matched your search

+6
View File
@@ -130,6 +130,12 @@ auto_manage_contacts = bot
# Default: meshcore_bot.db
db_path = meshcore_bot.db
# Local plugins directory (optional)
# Directory containing commands/, service_plugins/, and optional config.ini.
# Relative paths are resolved from the bot root; absolute paths used as-is.
# Default: local (i.e. bot_root/local). Changing this requires a bot restart.
#local_dir_path = local
# Seconds to wait after a failed service restart before retrying (default: 300)
service_restart_backoff_seconds = 300
+3
View File
@@ -143,6 +143,9 @@ auto_manage_contacts = false
# Default: meshcore_bot.db
db_path = meshcore_bot.db
# Local plugins directory (optional). Default: local. See config.ini.example.
#local_dir_path = local
[Localization]
# Language code for bot responses (en, es, es-MX, es-ES, fr, de, ja, etc.)
# Default: en (English)
+3 -1
View File
@@ -1,6 +1,6 @@
# Local plugins and services
You can add your own **command plugins** and **service plugins** without modifying the bot’s code by placing them in the **`local/`** directory. Their configuration can live in **`local/config.ini`** so it stays separate from the main `config.ini`.
You can add your own **command plugins** and **service plugins** without modifying the bot’s code by placing them in a **local** directory. By default that directory is **`local/`** next to your main config.ini; you can change it with **`[Bot]` `local_dir_path`** (see below). Their configuration can live in **`local/config.ini`** so it stays separate from the main `config.ini`.
## Directories
@@ -12,6 +12,8 @@ You can add your own **command plugins** and **service plugins** without modifyi
Local plugins are **additive**: they are loaded after built-in (and alternative) plugins. If a local plugin or service has the same logical **name** as one already loaded, it is **skipped** and a warning is logged. There is no override-by-name for local code.
**Custom local path:** In `config.ini`, under `[Bot]`, you can set **`local_dir_path`** to a directory that contains `commands/`, `service_plugins/`, and optional `config.ini`. Use a relative path (resolved from the bot root) or an absolute path so the local folder can live outside the install directory (e.g. `/home/user/meshcore-local`). Changing `local_dir_path` requires a **bot restart**.
## Minimal command plugin
Create a file in **local/commands/** (e.g. `local/commands/hello_local.py`):
+27
View File
@@ -308,6 +308,12 @@ copy_files_smart() {
print_info "Preserving existing alternative commands (only updating if source is newer)"
fi
# Preserve install dir's local/ entirely (user custom commands and service plugins)
# Never overwrite or delete anything under $dest_dir/local/
if [ -d "$dest_dir/local" ]; then
print_info "Preserving existing local/ directory (not overwriting)"
fi
# Exclude patterns
rsync -a --update --exclude='.git' \
--exclude='__pycache__' \
@@ -320,10 +326,20 @@ copy_files_smart() {
--exclude='*.db-wal' \
--exclude='*.log' \
--exclude='backups' \
--exclude='local/' \
$preserve_config \
"$source_dir/" "$dest_dir/" 2>/dev/null || {
print_warning "rsync had some issues, falling back to manual copy"
}
# If install dir has no local/, create minimal structure from source so service has valid layout
if [ ! -d "$dest_dir/local" ]; then
print_info "Creating local/ directory structure (first-time install)"
mkdir -p "$dest_dir/local/commands" "$dest_dir/local/service_plugins"
[ -f "$source_dir/local/README.md" ] && cp "$source_dir/local/README.md" "$dest_dir/local/" || true
[ -f "$source_dir/local/__init__.py" ] && cp "$source_dir/local/__init__.py" "$dest_dir/local/" || true
[ -f "$source_dir/local/commands/.gitkeep" ] && cp "$source_dir/local/commands/.gitkeep" "$dest_dir/local/commands/" || true
[ -f "$source_dir/local/service_plugins/.gitkeep" ] && cp "$source_dir/local/service_plugins/.gitkeep" "$dest_dir/local/service_plugins/" || true
fi
print_success "Files synchronized using rsync"
return 0
fi
@@ -336,6 +352,11 @@ copy_files_smart() {
print_info "Preserving existing alternative commands (not overwriting)"
fi
# Preserve install dir's local/ entirely - never overwrite when it exists
if [ -d "$dest_dir/local" ]; then
print_info "Preserving existing local/ directory (not overwriting)"
fi
# Copy files, preserving config.ini if it exists
while IFS= read -r file; do
local rel_path="${file#$source_dir/}"
@@ -355,6 +376,12 @@ copy_files_smart() {
[[ "$rel_path" == *".log" ]] && continue
[[ "$rel_path" == *"/backups/"* ]] && continue
# Preserve install dir's local/ entirely - skip all files under local/ when dest has local/
if [[ "$rel_path" == "local/"* ]] && [ -d "$dest_dir/local" ]; then
files_skipped=$((files_skipped + 1))
continue
fi
# Preserve alternatives directory - only update if source file is newer
if [[ "$rel_path" == "modules/commands/alternatives/"* ]] && [ -d "$dest_dir/modules/commands/alternatives" ]; then
if [ -f "$dest_file" ]; then
+5 -2
View File
@@ -89,9 +89,12 @@ class CommandManager:
self.command_prefix = self.load_command_prefix()
# Initialize plugin loader and load all plugins
self.plugin_loader = PluginLoader(
bot, local_commands_dir=str(bot.bot_root / "local" / "commands")
local_commands_dir = (
str(bot._local_root / "commands")
if getattr(bot, "_local_root", None) is not None
else str(bot.bot_root / "local" / "commands")
)
self.plugin_loader = PluginLoader(bot, local_commands_dir=local_commands_dir)
self.commands = self.plugin_loader.load_all_plugins()
# Cache for internet connectivity status to avoid checking on every command
+20
View File
@@ -198,6 +198,26 @@ def validate_config(config_path: str) -> List[Tuple[str, str]]:
msg = _check_path_writable(db_path, bot_root, "Database path")
if msg:
results.append((SEVERITY_WARNING, msg))
# Optional: local_dir_path should exist and be readable when set
local_dir = config.get("Bot", "local_dir_path", fallback="").strip()
if local_dir:
try:
resolved = _resolve_path(local_dir, bot_root)
if not resolved.exists():
results.append((
SEVERITY_WARNING,
f"Local plugins path '{resolved}' does not exist; local commands/plugins will not load.",
))
elif not os.access(str(resolved), os.R_OK):
results.append((
SEVERITY_WARNING,
f"Local plugins path '{resolved}' is not readable.",
))
except (OSError, RuntimeError):
results.append((
SEVERITY_WARNING,
f"Local plugins path: cannot resolve '{local_dir}'.",
))
if config.has_section("Logging"):
log_file = config.get("Logging", "log_file", fallback="").strip()
if log_file:
+10 -3
View File
@@ -214,7 +214,7 @@ class MeshCoreBot:
self.logger.info("Initializing service plugin loader")
try:
self.service_loader = ServicePluginLoader(
self, local_services_dir=str(self.bot_root / "local" / "service_plugins")
self, local_services_dir=str(self._local_root / "service_plugins")
)
self.services = self.service_loader.load_all_services()
self.logger.info(f"Service plugin loader initialized with {len(self.services)} service(s)")
@@ -269,8 +269,15 @@ class MeshCoreBot:
# Force UTF-8 so emoji and non-ASCII characters in config.ini parse on Windows.
self.config.read(self.config_file, encoding="utf-8")
# Resolve local plugins directory from [Bot] local_dir_path (default: local)
self._local_root = Path(
resolve_path(
self.config.get("Bot", "local_dir_path", fallback="local"),
self.bot_root,
)
)
# Merge local config if present (user plugins/services settings)
local_config = self.bot_root / "local" / "config.ini"
local_config = self._local_root / "config.ini"
if local_config.exists():
self.config.read(local_config, encoding="utf-8")
@@ -335,7 +342,7 @@ class MeshCoreBot:
# Reload the config
self.config.read(self.config_file, encoding="utf-8")
local_config = self.bot_root / "local" / "config.ini"
local_config = self._local_root / "config.ini"
if local_config.exists():
self.config.read(local_config, encoding="utf-8")