codexmgr 0.1.0__py3-none-any.whl

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.
codexmgr/toml_io.py ADDED
@@ -0,0 +1,213 @@
1
+ """Small TOML IO helpers for codexmgr-managed configuration files."""
2
+
3
+ import json
4
+ import re
5
+ import tomllib
6
+ from collections.abc import Mapping
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .errors import CommandError
11
+
12
+ BARE_KEY = re.compile(r"^[A-Za-z0-9_-]+$")
13
+
14
+
15
+ def load_toml_file(path: Path) -> dict[str, Any]:
16
+ """Load a TOML file and convert parse failures into CommandError.
17
+
18
+ Args:
19
+ path: TOML file path to read.
20
+
21
+ Returns:
22
+ Parsed TOML document.
23
+ """
24
+ try:
25
+ return tomllib.loads(path.read_text(encoding="utf-8"))
26
+ except tomllib.TOMLDecodeError as exc:
27
+ raise CommandError(f"Invalid TOML in {path}: {exc}") from exc
28
+
29
+
30
+ def load_optional_toml_file(path: Path) -> dict[str, Any]:
31
+ """Load a TOML file or return an empty document when it is missing.
32
+
33
+ Args:
34
+ path: Optional TOML file path to read.
35
+
36
+ Returns:
37
+ Parsed TOML document, or an empty dictionary.
38
+ """
39
+ if not path.exists():
40
+ return {}
41
+ return load_toml_file(path)
42
+
43
+
44
+ def write_toml_file(path: Path, data: Mapping[str, Any]) -> None:
45
+ """Write TOML data with UTF-8 encoding.
46
+
47
+ Args:
48
+ path: TOML file path to write.
49
+ data: Mapping to serialize.
50
+
51
+ Returns:
52
+ None. The file is replaced atomically by pathlib's write_text behavior.
53
+ """
54
+ path.write_text(dump_toml(data), encoding="utf-8")
55
+
56
+
57
+ def format_toml_value(value: Any) -> str:
58
+ """Format a Python value as a TOML literal.
59
+
60
+ Args:
61
+ value: Supported TOML scalar, list, or mapping value.
62
+
63
+ Returns:
64
+ TOML literal string.
65
+ """
66
+ return _format_value(value)
67
+
68
+
69
+ def dump_toml(data: Mapping[str, Any]) -> str:
70
+ """Serialize supported TOML data into a deterministic TOML document.
71
+
72
+ Args:
73
+ data: Parsed TOML-style mapping to serialize.
74
+
75
+ Returns:
76
+ TOML document text ending with a newline when non-empty.
77
+ """
78
+ tables = list(_iter_tables(data))
79
+ chunks = [_format_table(path, values, is_array) for is_array, path, values in tables]
80
+ return "\n\n".join(chunks) + ("\n" if chunks else "")
81
+
82
+
83
+ def _iter_tables(
84
+ data: Mapping[str, Any],
85
+ prefix: tuple[str, ...] = (),
86
+ ) -> list[tuple[bool, tuple[str, ...], dict[str, Any]]]:
87
+ """Flatten nested TOML tables into renderable table chunks.
88
+
89
+ Args:
90
+ data: Mapping for the current TOML table.
91
+ prefix: TOML path for the current table.
92
+
93
+ Returns:
94
+ Table chunks as ``(is_array, path, values)`` tuples.
95
+ """
96
+ scalars: dict[str, Any] = {}
97
+ children: list[tuple[str, Mapping[str, Any]]] = []
98
+ arrays: list[tuple[str, list[Mapping[str, Any]]]] = []
99
+
100
+ for key, value in data.items():
101
+ if _is_array_of_tables(value):
102
+ arrays.append((key, value))
103
+ elif isinstance(value, Mapping):
104
+ children.append((key, value))
105
+ else:
106
+ scalars[key] = value
107
+
108
+ tables: list[tuple[bool, tuple[str, ...], dict[str, Any]]] = []
109
+ if scalars:
110
+ tables.append((False, prefix, scalars))
111
+
112
+ for key, child in children:
113
+ tables.extend(_iter_tables(child, (*prefix, key)))
114
+
115
+ for key, items in arrays:
116
+ tables.extend(_iter_array_tables(items, (*prefix, key)))
117
+
118
+ return tables
119
+
120
+
121
+ def _iter_array_tables(
122
+ items: list[Mapping[str, Any]],
123
+ prefix: tuple[str, ...],
124
+ ) -> list[tuple[bool, tuple[str, ...], dict[str, Any]]]:
125
+ """Flatten TOML array-of-table items into renderable chunks.
126
+
127
+ Args:
128
+ items: Array items that are all mappings.
129
+ prefix: TOML path for the array-of-table.
130
+
131
+ Returns:
132
+ Array table chunks as ``(True, path, values)`` tuples.
133
+ """
134
+ tables: list[tuple[bool, tuple[str, ...], dict[str, Any]]] = []
135
+ for item in items:
136
+ for key, value in item.items():
137
+ if isinstance(value, Mapping):
138
+ path = ".".join((*prefix, key))
139
+ raise CommandError(
140
+ f"Unsupported nested table in TOML array item: {path}"
141
+ )
142
+ scalars = dict(item)
143
+ tables.append((True, prefix, scalars))
144
+ return tables
145
+
146
+
147
+ def _format_table(path: tuple[str, ...], values: Mapping[str, Any], is_array: bool) -> str:
148
+ """Format one TOML table chunk.
149
+
150
+ Args:
151
+ path: TOML table path.
152
+ values: Scalar values to write in the table.
153
+ is_array: Whether the table is an array-of-tables item.
154
+
155
+ Returns:
156
+ TOML text for the table chunk.
157
+ """
158
+ formatted_path = ".".join(_format_key(part) for part in path)
159
+ lines = []
160
+ if path:
161
+ heading = f"[[{formatted_path}]]" if is_array else f"[{formatted_path}]"
162
+ lines.append(heading)
163
+ lines.extend(f"{_format_key(key)} = {_format_value(value)}" for key, value in values.items())
164
+ return "\n".join(lines)
165
+
166
+
167
+ def _is_array_of_tables(value: Any) -> bool:
168
+ """Return whether a value should be emitted as TOML array-of-tables.
169
+
170
+ Args:
171
+ value: Candidate value from a TOML mapping.
172
+
173
+ Returns:
174
+ True when the value is a non-empty list containing only mappings.
175
+ """
176
+ return isinstance(value, list) and bool(value) and all(isinstance(item, Mapping) for item in value)
177
+
178
+
179
+ def _format_key(key: str) -> str:
180
+ """Format a TOML key, quoting keys that are not bare-key compatible.
181
+
182
+ Args:
183
+ key: Raw key name.
184
+
185
+ Returns:
186
+ TOML key text.
187
+ """
188
+ if BARE_KEY.match(key):
189
+ return key
190
+ return json.dumps(key)
191
+
192
+
193
+ def _format_value(value: Any) -> str:
194
+ """Format a supported Python value as TOML.
195
+
196
+ Args:
197
+ value: Value to serialize.
198
+
199
+ Returns:
200
+ TOML literal or inline table text.
201
+ """
202
+ if isinstance(value, str):
203
+ return json.dumps(value)
204
+ if isinstance(value, list):
205
+ return f"[{', '.join(_format_value(item) for item in value)}]"
206
+ if isinstance(value, Mapping):
207
+ items = (f"{_format_key(key)}={_format_value(item)}" for key, item in value.items())
208
+ return f"{{{', '.join(items)}}}"
209
+ if isinstance(value, bool):
210
+ return "true" if value else "false"
211
+ if isinstance(value, int | float):
212
+ return str(value)
213
+ raise CommandError(f"Unsupported TOML value type: {type(value).__name__}")
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: codexmgr
3
+ Version: 0.1.0
4
+ Summary: Manage project-local Codex configuration from reusable templates
5
+ Keywords: agents,cli,codex,configuration,skills
6
+ Classifier: Development Status :: 4 - Beta
7
+ Classifier: Environment :: Console
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Software Development
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.11
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=9.0.3; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # codexmgr
21
+
22
+ `codexmgr` manages project-local Codex configuration from reusable templates.
23
+ It keeps hand-written project instructions in `AGENTS.md` and generated Codex
24
+ configuration in `.codex/` synchronized from a small declarative
25
+ `.codex/codexmgr.toml` file.
26
+
27
+ The tool is intentionally narrow:
28
+
29
+ - compose reusable AGENTS.md instruction fragments
30
+ - enable or disable Codex skills per project
31
+ - write reproducible lock data for the resolved project configuration
32
+ - run `codex` with project `.codex/config.toml` values translated into `-c`
33
+ overrides
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.11 or newer
38
+ - `uv` for local development
39
+ - `codex` on `PATH` only when using `codexmgr codex ...`
40
+
41
+ ## Installation
42
+
43
+ From a checkout:
44
+
45
+ ```bash
46
+ uv sync --group dev
47
+ uv run codexmgr --help
48
+ ```
49
+
50
+ For local command-line use from this repository:
51
+
52
+ ```bash
53
+ uv tool install .
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ Create the project `.codex/` directory:
59
+
60
+ ```bash
61
+ codexmgr setup
62
+ ```
63
+
64
+ Create or install a named AGENTS.md template under
65
+ `$CODEXMGR_HOME/agentsmd/<name>.toml`. If `CODEXMGR_HOME` is unset,
66
+ `~/.codexmgr` is used.
67
+
68
+ ```toml
69
+ [coding]
70
+ text = """
71
+ - Keep source files focused and small.
72
+ - Add tests for behavior changes before implementation.
73
+ """
74
+
75
+ [coding.debugging]
76
+ text = "Prefer lasting regression tests over temporary scripts."
77
+ ```
78
+
79
+ Add the template to the current project:
80
+
81
+ ```bash
82
+ codexmgr agentsmd add coding
83
+ ```
84
+
85
+ This updates `.codex/codexmgr.toml`, runs `apply`, writes
86
+ `.codex/codexmgr.lock`, and refreshes the managed block in `AGENTS.md`.
87
+
88
+ ## Managed Files
89
+
90
+ `codexmgr` reads and writes these project files:
91
+
92
+ - `.codex/codexmgr.toml`: source configuration edited by CLI commands or by
93
+ hand
94
+ - `.codex/codexmgr.lock`: resolved template and skill state written by `apply`
95
+ - `.codex/config.toml`: Codex config updated with `[[skills.config]]` entries
96
+ - `AGENTS.md`: project instructions, with only the managed block replaced
97
+
98
+ The managed AGENTS.md block is:
99
+
100
+ ```markdown
101
+ <!-- BEGIN CODEXMGR GENERATED -->
102
+ <!-- END CODEXMGR GENERATED -->
103
+ ```
104
+
105
+ Manual content outside this block is preserved. If the block is missing,
106
+ `codexmgr` appends it. If `AGENTS.md` is missing, `codexmgr` creates it.
107
+
108
+ ## Project Configuration
109
+
110
+ `.codex/codexmgr.toml` supports AGENTS.md templates and skill state:
111
+
112
+ ```toml
113
+ [agents_md]
114
+ src = ["coding", "/absolute/or/project-relative/template.toml"]
115
+
116
+ [skills]
117
+ enabled = ["review-helper"]
118
+ disabled = ["experimental-skill", "skills/local-disabled"]
119
+ ```
120
+
121
+ Named AGENTS.md templates resolve from `$CODEXMGR_HOME/agentsmd/<name>.toml`.
122
+ Path-like template values resolve relative to the project unless they are
123
+ absolute paths.
124
+
125
+ Named skills resolve from `$CODEX_HOME/skills/<name>/SKILL.md`. If `CODEX_HOME`
126
+ is unset, `~/.codex` is used. Path-like skill values resolve to either a
127
+ `SKILL.md` file or a directory containing `SKILL.md`. Missing skills are written
128
+ as name-based entries so Codex can resolve them later.
129
+
130
+ Mutating commands run `apply` automatically unless `--no-sync` is passed.
131
+ Project guidelines require `apply` whenever `.codex/codexmgr.toml` changes,
132
+ unless `--no-sync` was explicitly requested.
133
+
134
+ ## Template Format
135
+
136
+ Template files are TOML documents. Each top-level key must be a table and
137
+ becomes an AGENTS.md heading. A `text` value inside a table becomes the body
138
+ under that heading. Nested tables become nested headings.
139
+
140
+ ```toml
141
+ [coding]
142
+ text = "Top-level guidance."
143
+
144
+ [coding.tests]
145
+ text = "Test behavior, not implementation details."
146
+ ```
147
+
148
+ renders as:
149
+
150
+ ```markdown
151
+ # coding
152
+ Top-level guidance.
153
+
154
+ ## tests
155
+ Test behavior, not implementation details.
156
+ ```
157
+
158
+ Unsupported scalar entries fail loudly instead of being silently ignored. This
159
+ keeps template mistakes visible during `apply`.
160
+
161
+ ## Commands
162
+
163
+ ```bash
164
+ codexmgr setup
165
+ codexmgr apply
166
+ codexmgr agentsmd add [--no-sync] <name-or-template-path>
167
+ codexmgr agentsmd remove [--no-sync] <name-or-template-path>
168
+ codexmgr skill enable [--no-sync] <name-or-skill-path>
169
+ codexmgr skill disable [--no-sync] <name-or-skill-path>
170
+ codexmgr codex <args...>
171
+ ```
172
+
173
+ `setup` creates `.codex/` in the current project.
174
+
175
+ `apply` reads `.codex/codexmgr.toml`, resolves configured sources, writes
176
+ `.codex/codexmgr.lock`, updates `.codex/config.toml` skill entries when a
177
+ `[skills]` table is configured, and refreshes the generated `AGENTS.md` block
178
+ when `[agents_md]` is configured.
179
+
180
+ `agentsmd add` validates that the template exists before writing config.
181
+ Repeated adds keep one source entry.
182
+
183
+ `agentsmd remove` removes a configured template source and fails if the source
184
+ is not present.
185
+
186
+ `skill enable` and `skill disable` keep enabled and disabled lists mutually
187
+ exclusive. Repeated commands keep one entry.
188
+
189
+ `codex` forwards arguments to the real `codex` command. Values from
190
+ `.codex/config.toml` are flattened into `-c key=value` overrides. User-provided
191
+ `-c` or `--config` overrides are merged after project config: scalar values
192
+ replace earlier values, while list values append.
193
+
194
+ ## Development
195
+
196
+ Install dependencies:
197
+
198
+ ```bash
199
+ uv sync --group dev
200
+ ```
201
+
202
+ Run tests:
203
+
204
+ ```bash
205
+ uv run pytest
206
+ ```
207
+
208
+ Build distributions:
209
+
210
+ ```bash
211
+ uv build
212
+ ```
213
+
214
+ The package is typed (`py.typed`) and the test suite covers CLI behavior,
215
+ template rendering, TOML writing, skill resolution, Codex command generation,
216
+ home-directory resolution, and package metadata.
217
+
218
+ ## Release Notes
219
+
220
+ The GitHub workflow runs the test matrix on Python 3.11, 3.12, and 3.13 across
221
+ Linux, Windows, and macOS. The publish workflow builds and publishes to PyPI
222
+ when the version in `pyproject.toml` differs from the latest published version.
@@ -0,0 +1,17 @@
1
+ codexmgr/__init__.py,sha256=sAP_YLKd-Zt6dty5Rp0ipIcZEBbCrL-PNqdCUqJ-TxA,91
2
+ codexmgr/agents_file.py,sha256=HsRjqYs4QMkgk64J7dBc6sn1QifEup-m_6s3vJbpHNU,2561
3
+ codexmgr/agentsmd.py,sha256=G8_X0H5HB79j-0DyGeW1WgzAM6EZowEI8qBIX-ygWEA,3382
4
+ codexmgr/cli.py,sha256=kRsEYBwbLA74BMM10p0nKU-jS3TGx7S3hH9188hDW6o,7806
5
+ codexmgr/codex.py,sha256=dy837exLosUAcu2_ExGZ9kZ2nfWy7rUGH4_tPDIwjDQ,6441
6
+ codexmgr/errors.py,sha256=9xne9BtVD2pzaMVRgPhJvJRCudIePQqsc13JHspG9Sk,169
7
+ codexmgr/paths.py,sha256=DgDpgdYkce5x8n7X9g6ylcm8yQNFUrpz2Vo8iklqgYM,3380
8
+ codexmgr/project.py,sha256=KdZyQwU_jnnnC4BW1q6TOy4X7_b0YLB8DuPYOkiYB-4,2537
9
+ codexmgr/project_config.py,sha256=JUtvZ00v_kTbWhl5BxxYWHLUVKF75lUHkl_sE5zypIQ,2204
10
+ codexmgr/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
+ codexmgr/renderer.py,sha256=ocdZsj_jWRqt0QYF-V7ODOYpICNFz5qe9Hofgmx3jTU,4189
12
+ codexmgr/skills.py,sha256=9ssCQJ2JXt5Tci3G2QMnYwWlDstTj3o1LJlfxtN4L24,8911
13
+ codexmgr/toml_io.py,sha256=YyvCGopi5rSkDd6nPp4mmLESwjBENtDxeD7vq7qVTi8,6153
14
+ codexmgr-0.1.0.dist-info/METADATA,sha256=Z6PkPtRwR7WfRkdghZEgrwmskTlhyiO2OiAp5R4ATiA,6219
15
+ codexmgr-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
16
+ codexmgr-0.1.0.dist-info/entry_points.txt,sha256=k0bRhfoAzWhclwvkJNKhYXc21amhujQ_EGy5G36j_Wc,53
17
+ codexmgr-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codexmgr = codexmgr.cli:entrypoint