devmux 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devmux-0.2.0 → devmux-0.2.2}/PKG-INFO +11 -3
- {devmux-0.2.0 → devmux-0.2.2}/README.md +10 -2
- {devmux-0.2.0 → devmux-0.2.2}/devmux/__init__.py +1 -1
- {devmux-0.2.0 → devmux-0.2.2}/devmux/cli/main.py +48 -4
- {devmux-0.2.0 → devmux-0.2.2}/devmux/core/manager.py +4 -1
- {devmux-0.2.0 → devmux-0.2.2}/devmux/utils/config.py +24 -1
- {devmux-0.2.0 → devmux-0.2.2}/devmux.egg-info/PKG-INFO +11 -3
- {devmux-0.2.0 → devmux-0.2.2}/devmux.egg-info/SOURCES.txt +2 -1
- {devmux-0.2.0 → devmux-0.2.2}/pyproject.toml +1 -1
- {devmux-0.2.0 → devmux-0.2.2}/tests/test_cli.py +76 -0
- {devmux-0.2.0 → devmux-0.2.2}/tests/test_config.py +10 -1
- devmux-0.2.2/tests/test_version.py +13 -0
- {devmux-0.2.0 → devmux-0.2.2}/LICENSE +0 -0
- {devmux-0.2.0 → devmux-0.2.2}/devmux/core/__init__.py +0 -0
- {devmux-0.2.0 → devmux-0.2.2}/devmux/utils/__init__.py +0 -0
- {devmux-0.2.0 → devmux-0.2.2}/devmux.egg-info/dependency_links.txt +0 -0
- {devmux-0.2.0 → devmux-0.2.2}/devmux.egg-info/entry_points.txt +0 -0
- {devmux-0.2.0 → devmux-0.2.2}/devmux.egg-info/requires.txt +0 -0
- {devmux-0.2.0 → devmux-0.2.2}/devmux.egg-info/top_level.txt +0 -0
- {devmux-0.2.0 → devmux-0.2.2}/setup.cfg +0 -0
- {devmux-0.2.0 → devmux-0.2.2}/tests/test_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devmux
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A tmux session manager purpose-built for AI agent CLIs
|
|
5
5
|
Author-email: Ollayor <ollayor@example.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/olllayor/devmux
|
|
@@ -179,7 +179,7 @@ Legacy `agents` configs are still accepted and mapped to `role: agent`.
|
|
|
179
179
|
|
|
180
180
|
```bash
|
|
181
181
|
devmux init [--preset minimal|backend|full-stack] [--config PATH] [--force]
|
|
182
|
-
devmux start <workspace> [--config PATH] [--detach] [--recreate]
|
|
182
|
+
devmux start [<workspace>] [--config PATH] [--preset minimal|backend|full-stack] [--detach] [--recreate]
|
|
183
183
|
devmux attach <workspace>
|
|
184
184
|
devmux resume <workspace>
|
|
185
185
|
devmux ls
|
|
@@ -187,6 +187,14 @@ devmux kill <workspace>
|
|
|
187
187
|
devmux send "<prompt>" [--session NAME] [--all | --role ROLE | --pane NAME ...]
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
+
You can also skip editing `devmux.yaml` for a quick start:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
devmux start my-project --preset backend
|
|
194
|
+
# or, from inside a project directory:
|
|
195
|
+
devmux start --preset backend
|
|
196
|
+
```
|
|
197
|
+
|
|
190
198
|
## Typical workflows
|
|
191
199
|
|
|
192
200
|
### Two-agent coding setup
|
|
@@ -256,7 +264,7 @@ Use two distribution paths:
|
|
|
256
264
|
Recommended rollout order:
|
|
257
265
|
|
|
258
266
|
1. Push this repo to GitHub with a clean README, license, and tagged release.
|
|
259
|
-
2.
|
|
267
|
+
2. Push a version tag like `vX.Y.Z`; GitHub Actions creates the release and publishes to PyPI.
|
|
260
268
|
3. Share a short demo GIF/video showing `devmux init`, `devmux start`, and `devmux send`.
|
|
261
269
|
4. Post it in places where AI-heavy CLI devs already are: X, Reddit, Hacker News, tmux/devtools communities, Discord/Slack groups, and your own network.
|
|
262
270
|
|
|
@@ -160,7 +160,7 @@ Legacy `agents` configs are still accepted and mapped to `role: agent`.
|
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
162
|
devmux init [--preset minimal|backend|full-stack] [--config PATH] [--force]
|
|
163
|
-
devmux start <workspace> [--config PATH] [--detach] [--recreate]
|
|
163
|
+
devmux start [<workspace>] [--config PATH] [--preset minimal|backend|full-stack] [--detach] [--recreate]
|
|
164
164
|
devmux attach <workspace>
|
|
165
165
|
devmux resume <workspace>
|
|
166
166
|
devmux ls
|
|
@@ -168,6 +168,14 @@ devmux kill <workspace>
|
|
|
168
168
|
devmux send "<prompt>" [--session NAME] [--all | --role ROLE | --pane NAME ...]
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
+
You can also skip editing `devmux.yaml` for a quick start:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
devmux start my-project --preset backend
|
|
175
|
+
# or, from inside a project directory:
|
|
176
|
+
devmux start --preset backend
|
|
177
|
+
```
|
|
178
|
+
|
|
171
179
|
## Typical workflows
|
|
172
180
|
|
|
173
181
|
### Two-agent coding setup
|
|
@@ -237,7 +245,7 @@ Use two distribution paths:
|
|
|
237
245
|
Recommended rollout order:
|
|
238
246
|
|
|
239
247
|
1. Push this repo to GitHub with a clean README, license, and tagged release.
|
|
240
|
-
2.
|
|
248
|
+
2. Push a version tag like `vX.Y.Z`; GitHub Actions creates the release and publishes to PyPI.
|
|
241
249
|
3. Share a short demo GIF/video showing `devmux init`, `devmux start`, and `devmux send`.
|
|
242
250
|
4. Post it in places where AI-heavy CLI devs already are: X, Reddit, Hacker News, tmux/devtools communities, Discord/Slack groups, and your own network.
|
|
243
251
|
|
|
@@ -10,7 +10,13 @@ import click
|
|
|
10
10
|
|
|
11
11
|
from devmux import __version__
|
|
12
12
|
from devmux.core.manager import SessionManager, SessionManagerError
|
|
13
|
-
from devmux.utils.config import
|
|
13
|
+
from devmux.utils.config import (
|
|
14
|
+
Config,
|
|
15
|
+
ConfigError,
|
|
16
|
+
VALID_ROLES,
|
|
17
|
+
load_preset,
|
|
18
|
+
render_preset,
|
|
19
|
+
)
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
def _load_config(config_path: str) -> Config:
|
|
@@ -25,6 +31,33 @@ def _fail(exc: Exception) -> None:
|
|
|
25
31
|
raise SystemExit(1) from exc
|
|
26
32
|
|
|
27
33
|
|
|
34
|
+
def _resolve_start_request(
|
|
35
|
+
workspace_name: str | None, config_path: str, preset: str | None
|
|
36
|
+
) -> tuple[str, Config]:
|
|
37
|
+
if preset:
|
|
38
|
+
resolved_name = workspace_name or Path.cwd().resolve().name
|
|
39
|
+
return resolved_name, load_preset(
|
|
40
|
+
preset, workspace_name=resolved_name, base_dir=Path.cwd().resolve()
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if not workspace_name:
|
|
44
|
+
raise click.UsageError(
|
|
45
|
+
"Missing argument 'WORKSPACE_NAME'. Or pass --preset to start from a built-in template."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
cfg = _load_config(config_path)
|
|
49
|
+
if workspace_name in cfg.workspaces:
|
|
50
|
+
return workspace_name, cfg
|
|
51
|
+
|
|
52
|
+
available = ", ".join(sorted(cfg.workspaces)) or "(none)"
|
|
53
|
+
raise ConfigError(
|
|
54
|
+
f"Workspace '{workspace_name}' not found in config '{config_path}'. "
|
|
55
|
+
f"Available workspaces: {available}. "
|
|
56
|
+
f"Start one of those, add '{workspace_name}' under 'workspaces', "
|
|
57
|
+
f"or run 'devmux start {workspace_name} --preset backend'."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
28
61
|
@click.group()
|
|
29
62
|
@click.version_option(version=__version__)
|
|
30
63
|
def cli() -> None:
|
|
@@ -68,7 +101,7 @@ def init(preset: str, config: str, force: bool) -> None:
|
|
|
68
101
|
|
|
69
102
|
|
|
70
103
|
@cli.command()
|
|
71
|
-
@click.argument("workspace_name")
|
|
104
|
+
@click.argument("workspace_name", required=False)
|
|
72
105
|
@click.option(
|
|
73
106
|
"--config",
|
|
74
107
|
"-c",
|
|
@@ -76,6 +109,11 @@ def init(preset: str, config: str, force: bool) -> None:
|
|
|
76
109
|
show_default=True,
|
|
77
110
|
help="Path to config file.",
|
|
78
111
|
)
|
|
112
|
+
@click.option(
|
|
113
|
+
"--preset",
|
|
114
|
+
type=click.Choice(["minimal", "backend", "full-stack"]),
|
|
115
|
+
help="Use a built-in preset without reading devmux.yaml. If WORKSPACE_NAME is omitted, the current directory name is used.",
|
|
116
|
+
)
|
|
79
117
|
@click.option(
|
|
80
118
|
"--detach",
|
|
81
119
|
is_flag=True,
|
|
@@ -86,10 +124,16 @@ def init(preset: str, config: str, force: bool) -> None:
|
|
|
86
124
|
is_flag=True,
|
|
87
125
|
help="Kill any existing session with the same name before creating it again.",
|
|
88
126
|
)
|
|
89
|
-
def start(
|
|
127
|
+
def start(
|
|
128
|
+
workspace_name: str | None,
|
|
129
|
+
config: str,
|
|
130
|
+
preset: str | None,
|
|
131
|
+
detach: bool,
|
|
132
|
+
recreate: bool,
|
|
133
|
+
) -> None:
|
|
90
134
|
"""Create or resume a named workspace."""
|
|
91
135
|
try:
|
|
92
|
-
cfg =
|
|
136
|
+
workspace_name, cfg = _resolve_start_request(workspace_name, config, preset)
|
|
93
137
|
manager = SessionManager()
|
|
94
138
|
result = manager.start_workspace(workspace_name, cfg, recreate=recreate)
|
|
95
139
|
|
|
@@ -212,7 +212,10 @@ class SessionManager:
|
|
|
212
212
|
return workspace
|
|
213
213
|
|
|
214
214
|
def _find_session(self, workspace_name: str) -> Session | None:
|
|
215
|
-
|
|
215
|
+
try:
|
|
216
|
+
return self.server.sessions.get(session_name=workspace_name)
|
|
217
|
+
except Exception:
|
|
218
|
+
return None
|
|
216
219
|
|
|
217
220
|
def _current_session_name(self) -> str | None:
|
|
218
221
|
pane_id = os.environ.get("TMUX_PANE")
|
|
@@ -99,7 +99,17 @@ class Config:
|
|
|
99
99
|
def load(cls, path: Path) -> "Config":
|
|
100
100
|
with path.open("r", encoding="utf-8") as handle:
|
|
101
101
|
data = yaml.safe_load(handle) or {}
|
|
102
|
+
return cls.from_mapping(data, base_dir=path.parent.resolve())
|
|
102
103
|
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_text(cls, raw: str, base_dir: Path | None = None) -> "Config":
|
|
106
|
+
data = yaml.safe_load(raw) or {}
|
|
107
|
+
return cls.from_mapping(data, base_dir=base_dir)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_mapping(
|
|
111
|
+
cls, data: dict[str, Any], base_dir: Path | None = None
|
|
112
|
+
) -> "Config":
|
|
103
113
|
if not isinstance(data, dict):
|
|
104
114
|
raise ConfigError("The config file must contain a top-level mapping.")
|
|
105
115
|
|
|
@@ -122,7 +132,7 @@ class Config:
|
|
|
122
132
|
layout=layout,
|
|
123
133
|
cwd=workspace_cwd,
|
|
124
134
|
panes=panes,
|
|
125
|
-
base_dir=
|
|
135
|
+
base_dir=(base_dir or Path.cwd()).resolve(),
|
|
126
136
|
)
|
|
127
137
|
workspace.validate(workspace_name)
|
|
128
138
|
workspaces[workspace_name] = workspace
|
|
@@ -228,3 +238,16 @@ workspaces:
|
|
|
228
238
|
f"Unknown preset '{preset}'. Choose one of: minimal, backend, full-stack."
|
|
229
239
|
)
|
|
230
240
|
return presets[preset]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def load_preset(
|
|
244
|
+
preset: str, workspace_name: str, base_dir: Path | None = None
|
|
245
|
+
) -> Config:
|
|
246
|
+
data = yaml.safe_load(render_preset(preset)) or {}
|
|
247
|
+
workspaces_data = data.get("workspaces", {})
|
|
248
|
+
if not isinstance(workspaces_data, dict) or not workspaces_data:
|
|
249
|
+
raise ConfigError(f"Preset '{preset}' did not define any workspaces.")
|
|
250
|
+
|
|
251
|
+
_, workspace_template = next(iter(workspaces_data.items()))
|
|
252
|
+
data["workspaces"] = {workspace_name: workspace_template}
|
|
253
|
+
return Config.from_mapping(data, base_dir=base_dir)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devmux
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A tmux session manager purpose-built for AI agent CLIs
|
|
5
5
|
Author-email: Ollayor <ollayor@example.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/olllayor/devmux
|
|
@@ -179,7 +179,7 @@ Legacy `agents` configs are still accepted and mapped to `role: agent`.
|
|
|
179
179
|
|
|
180
180
|
```bash
|
|
181
181
|
devmux init [--preset minimal|backend|full-stack] [--config PATH] [--force]
|
|
182
|
-
devmux start <workspace> [--config PATH] [--detach] [--recreate]
|
|
182
|
+
devmux start [<workspace>] [--config PATH] [--preset minimal|backend|full-stack] [--detach] [--recreate]
|
|
183
183
|
devmux attach <workspace>
|
|
184
184
|
devmux resume <workspace>
|
|
185
185
|
devmux ls
|
|
@@ -187,6 +187,14 @@ devmux kill <workspace>
|
|
|
187
187
|
devmux send "<prompt>" [--session NAME] [--all | --role ROLE | --pane NAME ...]
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
+
You can also skip editing `devmux.yaml` for a quick start:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
devmux start my-project --preset backend
|
|
194
|
+
# or, from inside a project directory:
|
|
195
|
+
devmux start --preset backend
|
|
196
|
+
```
|
|
197
|
+
|
|
190
198
|
## Typical workflows
|
|
191
199
|
|
|
192
200
|
### Two-agent coding setup
|
|
@@ -256,7 +264,7 @@ Use two distribution paths:
|
|
|
256
264
|
Recommended rollout order:
|
|
257
265
|
|
|
258
266
|
1. Push this repo to GitHub with a clean README, license, and tagged release.
|
|
259
|
-
2.
|
|
267
|
+
2. Push a version tag like `vX.Y.Z`; GitHub Actions creates the release and publishes to PyPI.
|
|
260
268
|
3. Share a short demo GIF/video showing `devmux init`, `devmux start`, and `devmux send`.
|
|
261
269
|
4. Post it in places where AI-heavy CLI devs already are: X, Reddit, Hacker News, tmux/devtools communities, Discord/Slack groups, and your own network.
|
|
262
270
|
|
|
@@ -54,6 +54,21 @@ def test_resume_calls_attach(monkeypatch) -> None:
|
|
|
54
54
|
assert fake_manager.attached == ["backend"]
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def test_attach_missing_session_shows_real_error(monkeypatch) -> None:
|
|
58
|
+
runner = CliRunner()
|
|
59
|
+
|
|
60
|
+
class MissingManager:
|
|
61
|
+
def attach_session(self, workspace_name):
|
|
62
|
+
raise RuntimeError(f"Session '{workspace_name}' does not exist.")
|
|
63
|
+
|
|
64
|
+
monkeypatch.setattr("devmux.cli.main.SessionManager", lambda: MissingManager())
|
|
65
|
+
|
|
66
|
+
result = runner.invoke(cli, ["attach", "codex"])
|
|
67
|
+
|
|
68
|
+
assert result.exit_code == 1
|
|
69
|
+
assert "Session 'codex' does not exist." in result.output
|
|
70
|
+
|
|
71
|
+
|
|
57
72
|
def test_start_with_detach_uses_idempotent_manager(monkeypatch, tmp_path: Path) -> None:
|
|
58
73
|
runner = CliRunner()
|
|
59
74
|
fake_manager = FakeManager()
|
|
@@ -86,6 +101,67 @@ workspaces:
|
|
|
86
101
|
assert "detached session 'backend'" in result.output
|
|
87
102
|
|
|
88
103
|
|
|
104
|
+
def test_start_with_preset_bootstraps_named_workspace(monkeypatch) -> None:
|
|
105
|
+
runner = CliRunner()
|
|
106
|
+
fake_manager = FakeManager()
|
|
107
|
+
monkeypatch.setattr("devmux.cli.main.SessionManager", lambda: fake_manager)
|
|
108
|
+
|
|
109
|
+
result = runner.invoke(cli, ["start", "agenix", "--preset", "backend", "--detach"])
|
|
110
|
+
|
|
111
|
+
assert result.exit_code == 0
|
|
112
|
+
assert fake_manager.started == [("agenix", False, ["agenix"])]
|
|
113
|
+
assert "detached session 'agenix'" in result.output
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_start_with_preset_uses_current_directory_name(monkeypatch, tmp_path: Path) -> None:
|
|
117
|
+
runner = CliRunner()
|
|
118
|
+
fake_manager = FakeManager()
|
|
119
|
+
project_dir = tmp_path / "agenix"
|
|
120
|
+
project_dir.mkdir()
|
|
121
|
+
monkeypatch.chdir(project_dir)
|
|
122
|
+
monkeypatch.setattr("devmux.cli.main.SessionManager", lambda: fake_manager)
|
|
123
|
+
|
|
124
|
+
result = runner.invoke(cli, ["start", "--preset", "backend", "--detach"])
|
|
125
|
+
|
|
126
|
+
assert result.exit_code == 0
|
|
127
|
+
assert fake_manager.started == [("agenix", False, ["agenix"])]
|
|
128
|
+
assert "detached session 'agenix'" in result.output
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_start_unknown_workspace_shows_available_workspaces(monkeypatch, tmp_path: Path) -> None:
|
|
132
|
+
runner = CliRunner()
|
|
133
|
+
config_path = tmp_path / "devmux.yaml"
|
|
134
|
+
config_path.write_text(
|
|
135
|
+
"""
|
|
136
|
+
workspaces:
|
|
137
|
+
backend:
|
|
138
|
+
layout: duo
|
|
139
|
+
cwd: .
|
|
140
|
+
panes:
|
|
141
|
+
- name: planner
|
|
142
|
+
role: agent
|
|
143
|
+
command: "claude"
|
|
144
|
+
- name: builder
|
|
145
|
+
role: agent
|
|
146
|
+
command: "codex --approval auto"
|
|
147
|
+
""",
|
|
148
|
+
encoding="utf-8",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
class UnusedManager:
|
|
152
|
+
def start_workspace(self, workspace_name, config, recreate=False): # pragma: no cover
|
|
153
|
+
raise AssertionError("manager should not be called when the workspace is missing")
|
|
154
|
+
|
|
155
|
+
monkeypatch.setattr("devmux.cli.main.SessionManager", lambda: UnusedManager())
|
|
156
|
+
|
|
157
|
+
result = runner.invoke(cli, ["start", "agenix", "--config", str(config_path)])
|
|
158
|
+
|
|
159
|
+
assert result.exit_code == 1
|
|
160
|
+
assert "Workspace 'agenix' not found" in result.output
|
|
161
|
+
assert "Available workspaces: backend" in result.output
|
|
162
|
+
assert "devmux start agenix --preset backend" in result.output
|
|
163
|
+
|
|
164
|
+
|
|
89
165
|
def test_send_enforces_safe_flag_combinations(monkeypatch) -> None:
|
|
90
166
|
runner = CliRunner()
|
|
91
167
|
fake_manager = FakeManager()
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
-
from devmux.utils.config import Config, ConfigError
|
|
5
|
+
from devmux.utils.config import Config, ConfigError, load_preset
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def test_loads_new_panes_schema(write_config) -> None:
|
|
@@ -58,6 +58,15 @@ def test_loads_legacy_agents_schema_as_agent_panes(write_config) -> None:
|
|
|
58
58
|
assert workspace.panes[1].command == "codex --approval auto"
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def test_load_preset_rekeys_workspace_name(tmp_path) -> None:
|
|
62
|
+
config = load_preset("backend", workspace_name="agenix", base_dir=tmp_path)
|
|
63
|
+
|
|
64
|
+
assert sorted(config.workspaces) == ["agenix"]
|
|
65
|
+
workspace = config.workspaces["agenix"]
|
|
66
|
+
assert workspace.layout == "trio"
|
|
67
|
+
assert workspace.base_dir == tmp_path.resolve()
|
|
68
|
+
|
|
69
|
+
|
|
61
70
|
@pytest.mark.parametrize(
|
|
62
71
|
("contents", "message"),
|
|
63
72
|
[
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import tomllib
|
|
5
|
+
|
|
6
|
+
from devmux import __version__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_package_version_matches_pyproject() -> None:
|
|
10
|
+
with Path("pyproject.toml").open("rb") as handle:
|
|
11
|
+
pyproject = tomllib.load(handle)
|
|
12
|
+
|
|
13
|
+
assert __version__ == pyproject["project"]["version"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|