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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devmux
3
- Version: 0.2.0
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. Publish version `0.2.0` to PyPI.
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. Publish version `0.2.0` to PyPI.
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
 
@@ -1,3 +1,3 @@
1
1
  """devmux - A tmux session manager for AI agent CLIs."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
@@ -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 Config, ConfigError, VALID_ROLES, render_preset
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(workspace_name: str, config: str, detach: bool, recreate: bool) -> None:
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 = _load_config(config)
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
- return self.server.sessions.get(session_name=workspace_name)
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=path.parent.resolve(),
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.0
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. Publish version `0.2.0` to PyPI.
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
 
@@ -15,4 +15,5 @@ devmux/utils/__init__.py
15
15
  devmux/utils/config.py
16
16
  tests/test_cli.py
17
17
  tests/test_config.py
18
- tests/test_manager.py
18
+ tests/test_manager.py
19
+ tests/test_version.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devmux"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "A tmux session manager purpose-built for AI agent CLIs"
9
9
  authors = [
10
10
  {name = "Ollayor", email = "ollayor@example.com"}
@@ -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