feat(settings): add explicit settings dir override for sandboxed runs

Add LORA_MANAGER_SETTINGS_DIR env var and standalone --settings-path to pin
the settings location (settings.json, cache/, wildcards/, backups/, logs/,
stats/) to an arbitrary directory. The override takes precedence over
portable mode and the platform user config dir, and skips legacy migration,
so sandboxed dev/E2E runs no longer need to write settings.json in the repo
root or collide with the real instance.

standalone.py pre-scans argv for --settings-path at import time because the
settings location is resolved before main() parses arguments. SettingsManager
portable-switch migration is a no-op while the directory is pinned.

Update the lora-manager-e2e skill (prefer --settings-path sandboxing;
start_server.py passes it through) and the lora-manager-runtime-context
skill (document precedence; inspect script honors the override).
This commit is contained in:
Will Miao
2026-08-27 00:03:50 +08:00
parent 1d3bcdfe47
commit 574dfbbe55
10 changed files with 383 additions and 30 deletions
+15 -1
View File
@@ -34,6 +34,8 @@ from ..utils.settings_paths import (
APP_NAME,
ensure_settings_file,
get_legacy_settings_path,
get_settings_dir_override,
is_settings_dir_pinned,
)
from ..utils.tag_priorities import (
PriorityTagEntry,
@@ -156,7 +158,10 @@ class SettingsManager:
self._check_environment_variables()
self._collect_configuration_warnings()
if os.environ.get("LORA_MANAGER_PORTABLE", "0") == "1":
if (
os.environ.get("LORA_MANAGER_PORTABLE", "0") == "1"
and not is_settings_dir_pinned()
):
if not self.settings.get("use_portable_settings"):
self.settings["use_portable_settings"] = True
self._save_settings()
@@ -1641,6 +1646,15 @@ class SettingsManager:
def _prepare_portable_switch(self, use_portable: bool) -> None:
"""Prepare switching the settings storage location."""
if is_settings_dir_pinned():
logger.info(
"Portable-mode switch ignored: settings directory is pinned via "
"%s/--settings-path (%s)",
"LORA_MANAGER_SETTINGS_DIR",
get_settings_dir_override(),
)
return
legacy_path = get_legacy_settings_path()
user_dir = self._get_user_config_directory()
user_settings_path = os.path.join(user_dir, "settings.json")
+81 -7
View File
@@ -13,6 +13,15 @@ from platformdirs import user_config_dir
APP_NAME = "ComfyUI-LoRA-Manager"
_LM_PORTABLE_ENV = "LORA_MANAGER_PORTABLE"
# Explicit settings-directory override. Setting this (env var, or standalone's
# ``--settings-path`` which publishes it) pins the settings location: settings.json,
# cache/, wildcards/, backups/, logs/, stats/ all resolve under this directory,
# bypassing portable mode and the platform user config dir. Useful for sandboxed
# development/E2E runs that must not touch the real user data or the project root.
SETTINGS_DIR_ENV = "LORA_MANAGER_SETTINGS_DIR"
_settings_dir_override: Optional[str] = None
_LOGGER = logging.getLogger(__name__)
@@ -22,6 +31,51 @@ def get_project_root() -> str:
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
def _normalize_settings_dir(path: str) -> str:
"""Expand ``~`` and absolutize a user-supplied settings directory."""
return os.path.abspath(os.path.expanduser(path))
def set_settings_dir_override(path: Optional[str]) -> Optional[str]:
"""Set or clear the programmatic settings-directory override.
Args:
path: Absolute/relative directory to pin, or ``None`` to clear the
override. ``~`` is expanded and the path absolutized.
Returns:
The previous override value (``None`` when none was active).
"""
global _settings_dir_override
previous = _settings_dir_override
_settings_dir_override = (
_normalize_settings_dir(path) if path else None
)
return previous
def get_settings_dir_override() -> Optional[str]:
"""Return the active explicit settings-directory override, if any.
The ``LORA_MANAGER_SETTINGS_DIR`` environment variable takes precedence over
the programmatic override so that standalone's ``--settings-path`` (which
publishes itself through the environment) wins over embedded callers.
"""
env_path = os.environ.get(SETTINGS_DIR_ENV)
if env_path:
return _normalize_settings_dir(env_path)
return _settings_dir_override
def is_settings_dir_pinned() -> bool:
"""Return ``True`` when an explicit settings-directory override is active."""
return get_settings_dir_override() is not None
def get_legacy_settings_path() -> str:
"""Return the legacy location of ``settings.json`` within the project tree."""
@@ -31,6 +85,11 @@ def get_legacy_settings_path() -> str:
def get_settings_dir(create: bool = True) -> str:
"""Return the user configuration directory for the application.
An explicit override (``LORA_MANAGER_SETTINGS_DIR`` or
:func:`set_settings_dir_override`) takes precedence. Otherwise the portable
project-root ``settings.json`` is used when enabled, falling back to the
platform-specific user configuration directory.
Args:
create: Whether to create the directory if it does not already exist.
@@ -38,11 +97,15 @@ def get_settings_dir(create: bool = True) -> str:
The absolute path to the user configuration directory.
"""
legacy_path = get_legacy_settings_path()
if _should_use_portable_settings(legacy_path, _LOGGER):
config_dir = os.path.dirname(legacy_path)
override = get_settings_dir_override()
if override:
config_dir = override
else:
config_dir = user_config_dir(APP_NAME, appauthor=False)
legacy_path = get_legacy_settings_path()
if _should_use_portable_settings(legacy_path, _LOGGER):
config_dir = os.path.dirname(legacy_path)
else:
config_dir = user_config_dir(APP_NAME, appauthor=False)
if create and config_dir:
os.makedirs(config_dir, exist_ok=True)
@@ -58,9 +121,14 @@ def get_settings_file_path(create_dir: bool = True) -> str:
def ensure_settings_file(logger: Optional[logging.Logger] = None) -> str:
"""Ensure the settings file resides in the user configuration directory.
If a legacy ``settings.json`` is detected in the project root it is migrated to
the platform-specific user configuration folder. The caller receives the path
to the settings file irrespective of whether a migration was needed.
An explicit override (``LORA_MANAGER_SETTINGS_DIR`` or
:func:`set_settings_dir_override`) pins the settings file to
``<override>/settings.json`` and skips legacy migration entirely.
Otherwise, if a legacy ``settings.json`` is detected in the project root it is
migrated to the platform-specific user configuration folder. The caller
receives the path to the settings file irrespective of whether a migration was
needed.
Args:
logger: Optional logger used for migration messages. Falls back to a
@@ -71,6 +139,12 @@ def ensure_settings_file(logger: Optional[logging.Logger] = None) -> str:
"""
logger = logger or _LOGGER
override = get_settings_dir_override()
if override:
os.makedirs(override, exist_ok=True)
return os.path.join(override, "settings.json")
legacy_path = get_legacy_settings_path()
if _should_use_portable_settings(legacy_path, logger):