Skip to content
18 changes: 17 additions & 1 deletion dimos/cli/dimos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion dimos/cli/test_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions dimos/cli/test_dimos_config.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion dimos/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dimos/core/coordination/blueprint_config/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions dimos/core/coordination/blueprint_config/test_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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) == {}
Loading