diff --git a/dimos/cli/dimos.py b/dimos/cli/dimos.py index 4dd72d3110..2566476faf 100644 --- a/dimos/cli/dimos.py +++ b/dimos/cli/dimos.py @@ -82,6 +82,20 @@ SIMULATORS = ("mujoco", "dimsim") +DEFAULT_CONFIG_PATH = CONFIG_DIR / "dimos" / "config" + + +def _reject_legacy_config() -> None: + """~/.config/dimos used to BE the config file; it is now a directory.""" + legacy = CONFIG_DIR / "dimos" + if legacy.is_file(): + typer.echo( + f"config found at old path {legacy}, which is now a directory; move it:\n" + f" mv {legacy} {legacy}.tmp && mkdir {legacy} && mv {legacy}.tmp {legacy}/config", + err=True, + ) + raise typer.Exit(2) + def _normalize_simulation_argv(argv: list[str]) -> list[str]: """Keep `--simulation` backwards compatible. @@ -207,7 +221,7 @@ def run( daemon: bool = typer.Option(False, "--daemon", "-d", help="Run in background"), disable: list[str] = typer.Option([], "--disable", help="Module names to disable"), config_path: Path = typer.Option( - CONFIG_DIR / "dimos", "--config", "-c", help="Path to config file" + DEFAULT_CONFIG_PATH, "--config", "-c", help="Path to config file" ), local_relay: bool | None = typer.Option( None, @@ -220,6 +234,8 @@ def run( show_help: bool = typer.Option(False, "--help"), ) -> None: """Start a robot blueprint""" + if config_path == DEFAULT_CONFIG_PATH: + _reject_legacy_config() from dimos.core.coordination.blueprint_config.errors import BlueprintConfigError from dimos.core.coordination.blueprint_config.parser import ( BlueprintConfigParser, diff --git a/dimos/cli/test_cloud.py b/dimos/cli/test_cloud.py index ff0554a1cc..470c3d061c 100644 --- a/dimos/cli/test_cloud.py +++ b/dimos/cli/test_cloud.py @@ -47,7 +47,8 @@ def delete_password(self, service: str, user: str) -> None: def filestore(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: """Force the headless path: no keyring backend, credentials in a temp file.""" monkeypatch.setattr(cloud, "_keyring", lambda: None) - cred = tmp_path / "dimos-credentials" + cred = tmp_path / "dimos" / "credentials" + cred.parent.mkdir() monkeypatch.setattr(cloud, "CREDENTIALS_PATH", cred) monkeypatch.setattr(global_config, "dimos_api_key", None) return cred diff --git a/dimos/cli/test_dimos_config.py b/dimos/cli/test_dimos_config.py new file mode 100644 index 0000000000..9ead110bcb --- /dev/null +++ b/dimos/cli/test_dimos_config.py @@ -0,0 +1,43 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest +import typer + +from dimos.cli import dimos as cli + + +@pytest.fixture +def config_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: + monkeypatch.setattr(cli, "CONFIG_DIR", tmp_path) + return tmp_path + + +def test_legacy_flat_file_is_refused_with_instructions( + config_home: Path, capsys: pytest.CaptureFixture[str] +) -> None: + (config_home / "dimos").write_text('{"viewer": "rerun"}') + with pytest.raises(typer.Exit): + cli._reject_legacy_config() + assert "mv " in capsys.readouterr().err + assert (config_home / "dimos").read_text() == '{"viewer": "rerun"}' + + +def test_no_legacy_file_passes(config_home: Path) -> None: + cli._reject_legacy_config() # nothing exists + + (config_home / "dimos").mkdir() # already a directory + cli._reject_legacy_config() diff --git a/dimos/constants.py b/dimos/constants.py index e744ca057a..9336ef5c4a 100644 --- a/dimos/constants.py +++ b/dimos/constants.py @@ -38,7 +38,8 @@ LOG_DIR = STATE_DIR / "logs" RECORDINGS_DIR = STATE_DIR / "recordings" -CREDENTIALS_PATH = CONFIG_DIR / "dimos-credentials" +CREDENTIALS_PATH = CONFIG_DIR / "dimos" / "credentials" + """ Constants for shared memory diff --git a/dimos/core/coordination/blueprint_config/sources.py b/dimos/core/coordination/blueprint_config/sources.py index f32c56a205..ed0f803f33 100644 --- a/dimos/core/coordination/blueprint_config/sources.py +++ b/dimos/core/coordination/blueprint_config/sources.py @@ -66,7 +66,7 @@ def validate_global_values(values: Mapping[str, Any]) -> dict[str, Any]: def read_config_file(path: Path) -> Mapping[str, Any]: try: raw = path.read_text() - except FileNotFoundError: + except (FileNotFoundError, IsADirectoryError): return {} except OSError as error: raise BlueprintConfigError(f"Could not read config file {path}: {error}") from error diff --git a/dimos/core/coordination/blueprint_config/test_sources.py b/dimos/core/coordination/blueprint_config/test_sources.py index 7a60b7a2f6..b911ee1f19 100644 --- a/dimos/core/coordination/blueprint_config/test_sources.py +++ b/dimos/core/coordination/blueprint_config/test_sources.py @@ -18,6 +18,7 @@ from dimos.core.coordination.blueprint_config.errors import BlueprintConfigError from dimos.core.coordination.blueprint_config.parser import BlueprintConfigParser +from dimos.core.coordination.blueprint_config.sources import read_config_file from dimos.core.global_config import global_config as process_global_config from dimos.core.module import Module, ModuleConfig @@ -80,3 +81,7 @@ def test_config_file_errors_are_clear_but_missing_file_is_optional(tmp_path: Pat not_an_object.write_text("[]") with pytest.raises(BlueprintConfigError, match="must contain a JSON object"): parser.parse(config_path=not_an_object, environ={}) + + +def test_config_path_that_is_a_directory_reads_as_absent(tmp_path: Path) -> None: + assert read_config_file(tmp_path) == {}