Skip to content

Commit 0446c77

Browse files
committed
Add command to show and switch global runtime setting
1 parent a392324 commit 0446c77

8 files changed

Lines changed: 175 additions & 7 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ This repository contains an initial v1 implementation with:
8282
- `vp list`
8383
- `vp config init`
8484
- `vp config show`
85+
- `vp config runtime`
8586
- `vp config path`
8687
- `vp version`
8788

docs/configuration.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ VibePod merges configuration from four sources in order, with each layer overrid
77
3. **Project config**`.vibepod/config.yaml` in the current directory
88
4. **Environment variables** — override specific keys at runtime
99

10-
Run `vp config path` to print the exact paths in use, and `vp config show` to print the fully merged result.
10+
Run `vp config path` to print the exact paths in use, `vp config show` to print the fully merged result, and `vp config runtime` to view or change the saved global container runtime.
1111

1212
## Full reference
1313

@@ -160,6 +160,24 @@ VP_IMAGE_NAMESPACE=myorg vp run claude
160160

161161
For end-to-end examples (extending a base image and assigning a brand-new image to an agent), see [Agents > Image customization workflows](agents/index.md#image-customization-workflows).
162162

163+
## Runtime preference
164+
165+
Use `vp config runtime` to inspect the saved global runtime preference:
166+
167+
```bash
168+
vp config runtime
169+
```
170+
171+
Set it explicitly without editing YAML by hand:
172+
173+
```bash
174+
vp config runtime podman
175+
vp config runtime docker
176+
vp config runtime auto
177+
```
178+
179+
This updates `container_runtime` in the global config file (`~/.config/vibepod/config.yaml` by default).
180+
163181
## Project-level config
164182

165183
Use `vp config init` in your repository to create `.vibepod/config.yaml` automatically when it does not already exist:

docs/podman.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ This is useful in CI or shell profiles where you always want a specific runtime.
4646

4747
### 3. Global config
4848

49-
Add `container_runtime` to `~/.config/vibepod/config.yaml`:
49+
Set the saved global runtime with the CLI:
50+
51+
```bash
52+
vp config runtime podman
53+
```
54+
55+
This writes `container_runtime` to `~/.config/vibepod/config.yaml`:
5056

5157
```yaml
5258
container_runtime: podman
@@ -56,6 +62,10 @@ When VibePod prompts you to choose a runtime interactively, it saves your answer
5662
5763
Set it back to `auto` to re-enable detection:
5864

65+
```bash
66+
vp config runtime auto
67+
```
68+
5969
```yaml
6070
container_runtime: auto
6171
```

src/vibepod/commands/config.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
import typer
1111
import yaml
1212

13-
from vibepod.constants import SUPPORTED_AGENTS
13+
from vibepod.constants import RUNTIME_AUTO, SUPPORTED_AGENTS, SUPPORTED_RUNTIMES
1414
from vibepod.core.allowed_dirs import (
1515
add_allowed_dir,
1616
is_protected_dir,
1717
load_allowed_dirs,
1818
remove_allowed_dir,
1919
)
2020
from vibepod.core.config import get_config, get_global_config_path, get_project_config_path
21+
from vibepod.core.runtime import get_saved_runtime_preference, save_runtime_preference
2122
from vibepod.utils.console import console, error, success
2223

2324
app = typer.Typer(help="Manage configuration")
@@ -116,6 +117,48 @@ def show(
116117
console.print(dumped)
117118

118119

120+
@app.command("runtime")
121+
def runtime(
122+
value: Annotated[
123+
str | None,
124+
typer.Argument(help="Default global runtime: auto, docker, or podman"),
125+
] = None,
126+
) -> None:
127+
"""Show or set the saved default container runtime."""
128+
if value is None:
129+
try:
130+
print(get_saved_runtime_preference())
131+
except (OSError, ValueError) as exc:
132+
error(str(exc))
133+
raise typer.Exit(1) from exc
134+
except yaml.YAMLError as exc:
135+
error(f"Failed to parse global config at {get_global_config_path()}: {exc}")
136+
raise typer.Exit(1) from exc
137+
return
138+
139+
allowed = (RUNTIME_AUTO, *SUPPORTED_RUNTIMES)
140+
normalized = value.strip().lower()
141+
if normalized not in allowed:
142+
error(f"Unknown runtime '{value}'. Supported: {', '.join(allowed)}")
143+
raise typer.Exit(1)
144+
145+
try:
146+
save_runtime_preference(normalized)
147+
except OSError as exc:
148+
error(f"Failed to update global config at {get_global_config_path()}: {exc}")
149+
raise typer.Exit(1) from exc
150+
except ValueError as exc:
151+
error(str(exc))
152+
raise typer.Exit(1) from exc
153+
except yaml.YAMLError as exc:
154+
error(f"Failed to parse global config at {get_global_config_path()}: {exc}")
155+
raise typer.Exit(1) from exc
156+
157+
success(
158+
f"Set default container runtime to '{normalized}' in {get_global_config_path()}"
159+
)
160+
161+
119162
@app.command("path")
120163
def path(
121164
global_only: Annotated[

src/vibepod/core/docker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def get_manager(
5959

6060
try:
6161
runtime_name, socket_url = resolve_runtime(override=runtime_override, config=config)
62-
except RuntimeError as exc:
62+
except (RuntimeError, ValueError) as exc:
6363
raise DockerClientError(str(exc)) from exc
6464

6565
return DockerManager(base_url=socket_url, runtime=runtime_name)

src/vibepod/core/runtime.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
DEFAULT_RUNTIME_PROBE_TIMEOUT = 10.0
2323

2424

25+
def _allowed_runtime_preferences() -> tuple[str, ...]:
26+
return (RUNTIME_AUTO, *SUPPORTED_RUNTIMES)
27+
28+
2529
def _socket_candidates() -> list[tuple[str, str]]:
2630
"""Return (runtime_name, socket_url) pairs to probe.
2731
@@ -96,8 +100,41 @@ def detect_available_runtimes() -> dict[str, str]:
96100
return found
97101

98102

103+
def get_saved_runtime_preference() -> str:
104+
"""Return the saved global ``container_runtime`` preference."""
105+
config_path = get_config_root() / "config.yaml"
106+
if not config_path.exists():
107+
return RUNTIME_AUTO
108+
109+
content = config_path.read_text(encoding="utf-8")
110+
if not content.strip():
111+
return RUNTIME_AUTO
112+
113+
loaded = yaml.safe_load(content)
114+
if loaded is None:
115+
return RUNTIME_AUTO
116+
if not isinstance(loaded, dict):
117+
raise ValueError(f"Global config must contain a YAML mapping: {config_path}")
118+
119+
saved = loaded.get("container_runtime", RUNTIME_AUTO)
120+
runtime = str(saved).strip().lower() or RUNTIME_AUTO
121+
if runtime not in _allowed_runtime_preferences():
122+
raise ValueError(
123+
f"Unknown container runtime '{runtime}'. "
124+
f"Supported: {', '.join(_allowed_runtime_preferences())}"
125+
)
126+
return runtime
127+
128+
99129
def save_runtime_preference(runtime: str) -> None:
100130
"""Persist *runtime* as ``container_runtime`` in the global config file."""
131+
normalized = runtime.strip().lower()
132+
if normalized not in _allowed_runtime_preferences():
133+
raise ValueError(
134+
f"Unknown container runtime '{normalized}'. "
135+
f"Supported: {', '.join(_allowed_runtime_preferences())}"
136+
)
137+
101138
config_path = get_config_root() / "config.yaml"
102139
config_path.parent.mkdir(parents=True, exist_ok=True)
103140

@@ -106,10 +143,13 @@ def save_runtime_preference(runtime: str) -> None:
106143
content = config_path.read_text(encoding="utf-8")
107144
if content.strip():
108145
loaded = yaml.safe_load(content)
109-
if isinstance(loaded, dict):
110-
existing = loaded
146+
if loaded is None:
147+
loaded = {}
148+
if not isinstance(loaded, dict):
149+
raise ValueError(f"Global config must contain a YAML mapping: {config_path}")
150+
existing = loaded
111151

112-
existing["container_runtime"] = runtime
152+
existing["container_runtime"] = normalized
113153
config_path.write_text(yaml.safe_dump(existing, sort_keys=False), encoding="utf-8")
114154

115155

tests/test_config.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,43 @@ def test_list_allowed_dirs_empty(monkeypatch, tmp_path: Path) -> None:
264264
result = runner.invoke(app, ["config", "list-allowed-dirs"])
265265
assert result.exit_code == 0
266266
assert "No directories" in result.stdout
267+
268+
269+
# ---------------------------------------------------------------------------
270+
# runtime subcommand tests
271+
# ---------------------------------------------------------------------------
272+
273+
274+
def test_config_runtime_shows_saved_global_runtime(monkeypatch, tmp_path: Path) -> None:
275+
monkeypatch.setenv("VP_CONFIG_DIR", str(tmp_path))
276+
277+
result = runner.invoke(app, ["config", "runtime"])
278+
279+
assert result.exit_code == 0
280+
assert result.stdout.strip() == "auto"
281+
282+
283+
def test_config_runtime_updates_global_config(monkeypatch, tmp_path: Path) -> None:
284+
monkeypatch.setenv("VP_CONFIG_DIR", str(tmp_path))
285+
global_config = tmp_path / "config.yaml"
286+
global_config.write_text("default_agent: codex\n", encoding="utf-8")
287+
288+
result = runner.invoke(app, ["config", "runtime", "podman"])
289+
290+
assert result.exit_code == 0
291+
loaded = yaml.safe_load(global_config.read_text(encoding="utf-8"))
292+
assert loaded == {
293+
"default_agent": "codex",
294+
"container_runtime": "podman",
295+
}
296+
assert "Set default container runtime to 'podman'" in result.stdout
297+
298+
299+
def test_config_runtime_rejects_unknown_value(monkeypatch, tmp_path: Path) -> None:
300+
monkeypatch.setenv("VP_CONFIG_DIR", str(tmp_path))
301+
302+
result = runner.invoke(app, ["config", "runtime", "nerdctl"])
303+
304+
assert result.exit_code == 1
305+
assert "Supported: auto, docker, podman" in result.stdout
306+
assert not (tmp_path / "config.yaml").exists()

tests/test_runtime.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99
from rich.prompt import Prompt
1010

11+
from vibepod.core import docker as docker_core
1112
from vibepod.core import runtime
1213

1314

@@ -104,3 +105,18 @@ def test_resolve_runtime_rejects_unavailable_prompt_choice(monkeypatch) -> None:
104105

105106
with pytest.raises(RuntimeError, match="not available"):
106107
runtime.resolve_runtime()
108+
109+
110+
def test_get_manager_wraps_runtime_preference_errors(monkeypatch) -> None:
111+
def _raise_runtime_preference_error(**kwargs) -> tuple[str, str]:
112+
del kwargs
113+
raise ValueError("Global config must contain a YAML mapping: /tmp/config.yaml")
114+
115+
monkeypatch.setattr(
116+
runtime,
117+
"resolve_runtime",
118+
_raise_runtime_preference_error,
119+
)
120+
121+
with pytest.raises(docker_core.DockerClientError, match="Global config must contain"):
122+
docker_core.get_manager()

0 commit comments

Comments
 (0)