From a9cf93aaa3202bfa557aab6003f18dc2e63cbd9c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 8 Jun 2026 11:31:10 +0200 Subject: [PATCH] Improve handling of configuration file updates * Make SerenaConfig.save private, because it is not a method that should ever be called on a runtime instance; it is purely an internal operation * Refactor the CLI init command appropriately (which used the save method) * Improve comments to make usage limitations clear * Improve the changelog entry to make error conditions more concrete --- CHANGELOG.md | 3 ++- src/serena/cli.py | 4 +--- src/serena/config/serena_config.py | 32 +++++++++++++++++++++++------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c040e0..3ba64b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ Status of the `main` branch. Changes prior to the next official version change w or a `.git`, including worktree/submodule pointer files), instead of preferring an ancestor's `.serena/project.yml` over a closer `.git`. This previously bound CLI agents (Claude Code, Codex, Gemini) launched from inside a worktree to the parent repo, causing stale reads and misdirected edits. - - Fix: some CLI flags on start-mcp-server modified the global config (notably, language-backend). + - Fix: CLI flags on `start-mcp-server` could incorrectly be saved to the global configuration file if the + list of projects was modified (triggering a save of the configuration with transient overrides applied) * Language Servers: - `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`. diff --git a/src/serena/cli.py b/src/serena/cli.py index 3b9ba581..98b8ed66 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -184,9 +184,7 @@ class TopLevelCommands(AutoRegisteringGroup): ) def init(language_backend: Literal["LSP", "JetBrains"] = "LSP") -> None: click.echo(f"\nSerena version: {serena_version()}\n") - serena_config = SerenaConfig.from_config_file() - serena_config.language_backend = LanguageBackend(language_backend) - serena_config.save() + serena_config = SerenaConfig.init(language_backend=LanguageBackend(language_backend)) click.echo(f"Configuration file: {serena_config.config_file_path}") click.echo(f"Language backend: {language_backend}") diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 92b2060d..15499fe3 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -957,7 +957,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): # re-save the configuration file if any migrations were performed if num_migrations > 0: log.info("Legacy configuration was migrated; re-saving configuration file") - instance.save() + instance._save() return instance @@ -985,6 +985,20 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): log.error(f"Error migrating configuration file: {e}") return None + @classmethod + def init(cls, language_backend: LanguageBackend) -> "SerenaConfig": + """ + Supports the config initialisation CLI command, allowing the user to configure fundamental settings before + the first launch. + + :param language_backend: the language backend to use + :return: the created SerenaConfig instance + """ + config = cls.from_config_file() + config.language_backend = language_backend + config._save() + return config + @cached_property def project_paths(self) -> list[str]: return sorted(str(project.project_root) for project in self.projects) @@ -1035,14 +1049,14 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): def add_registered_project(self, registered_project: RegisteredProject) -> None: """ - Adds a registered project, persisting only the updated project list (see :meth:`_persist_projects`). + Adds a registered project, persisting the updated project list """ self.projects.append(registered_project) self._persist_projects() def add_project_from_path(self, project_root: Path | str) -> "Project": """ - Add a new project to the Serena configuration from a given path, auto-generating the project + Adds a new project to the Serena configuration from a given path, auto-generating the project with defaults if it does not exist. Will raise a FileExistsError if a project already exists at the path. @@ -1087,7 +1101,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): def _persist_projects(self) -> None: """ - Persist ONLY the registered-projects list, leaving every other setting at its on-disk value. + Persists ONLY the registered-projects list, leaving every other setting at its on-disk value. Project (de)registration can happen while a session is running with transient runtime overrides applied to this in-memory instance (e.g. ``start-mcp-server --language-backend`` / ``--log-level``). A full @@ -1099,11 +1113,15 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): return persisted = SerenaConfig.from_config_file() persisted.projects = list(self.projects) - persisted.save() + persisted._save() - def save(self) -> None: + def _save(self) -> None: """ - Saves the configuration to the file from which it was loaded (if any) + Saves the full configuration to the file from which it was loaded (if any) + + NOTE: This method is private, because it is not usually safe to save a configuration instance used + at runtime, because it often contains transient overrides (e.g. specified through the CLI) + that should never be persisted back to the configuration file. """ if self.config_file_path is None: return