dotagents-cli 0.8.0__tar.gz → 0.8.1__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.
Files changed (120) hide show
  1. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/CHANGELOG.md +21 -1
  2. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/PKG-INFO +2 -1
  3. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/authoring.md +35 -6
  4. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/overlays.md +5 -2
  5. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/pyproject.toml +2 -0
  6. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/AGENTS.md +59 -7
  7. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/__init__.py +1 -1
  8. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_env.py +25 -1
  9. dotagents_cli-0.8.1/src/dotagents/_launchers.py +241 -0
  10. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlays.py +95 -4
  11. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_pyvenv.py +32 -2
  12. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/build_pyz.py +5 -0
  13. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_build_pyz.py +2 -1
  14. dotagents_cli-0.8.1/tests/test_launchers.py +322 -0
  15. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_pyvenv_core.py +10 -2
  16. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/.gitignore +0 -0
  17. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/LICENSE +0 -0
  18. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/README.md +0 -0
  19. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/agents.md +0 -0
  20. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/cli.md +0 -0
  21. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/commands.md +0 -0
  22. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/context.md +0 -0
  23. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/env.md +0 -0
  24. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/fs.md +0 -0
  25. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/hooks.md +0 -0
  26. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/merge.md +0 -0
  27. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/overlays.md +0 -0
  28. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/reference.md +0 -0
  29. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/resources.md +0 -0
  30. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/scope.md +0 -0
  31. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/skills.md +0 -0
  32. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/sources.md +0 -0
  33. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/wrappers.md +0 -0
  34. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/changelog.md +0 -0
  35. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/commands.md +0 -0
  36. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/install.md +0 -0
  37. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/private-sync.md +0 -0
  38. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/index.md +0 -0
  39. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/install.py +0 -0
  40. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/mkdocs.yml +0 -0
  41. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/__main__.py +0 -0
  42. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_agents.py +0 -0
  43. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_context.py +0 -0
  44. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_fs.py +0 -0
  45. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_hooks.py +0 -0
  46. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_ignore.py +0 -0
  47. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_merge.py +0 -0
  48. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
  49. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
  50. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
  51. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
  52. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
  53. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
  54. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
  55. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_pipwheel.py +0 -0
  56. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_resources.py +0 -0
  57. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_scope.py +0 -0
  58. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_skills.py +0 -0
  59. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_sources.py +0 -0
  60. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_wrappers.py +0 -0
  61. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/__init__.py +0 -0
  62. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/_common.py +0 -0
  63. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/about.py +0 -0
  64. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/context.py +0 -0
  65. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/env.py +0 -0
  66. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/init.py +0 -0
  67. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/overlays.py +0 -0
  68. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/path.py +0 -0
  69. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/py.typed +0 -0
  70. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/_helpers.py +0 -0
  71. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/_shell.py +0 -0
  72. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/conftest.py +0 -0
  73. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_about.py +0 -0
  74. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_agents.py +0 -0
  75. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_api_header.py +0 -0
  76. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_audit_leak.py +0 -0
  77. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_cli_main.py +0 -0
  78. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_cloud_setup.py +0 -0
  79. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_cmds_discovery.py +0 -0
  80. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_context.py +0 -0
  81. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env.py +0 -0
  82. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_cli_scope.py +0 -0
  83. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_format.py +0 -0
  84. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_into.py +0 -0
  85. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_regressions.py +0 -0
  86. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_safety.py +0 -0
  87. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_findings_cmd.py +0 -0
  88. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_adapters.py +0 -0
  89. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_cli.py +0 -0
  90. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_env.py +0 -0
  91. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_hooks.py +0 -0
  92. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_init.py +0 -0
  93. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_overlays.py +0 -0
  94. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_packaging.py +0 -0
  95. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_scope_context.py +0 -0
  96. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_skills_adapters.py +0 -0
  97. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_git_ssl_revoke.py +0 -0
  98. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_hooks.py +0 -0
  99. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_ignore.py +0 -0
  100. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_init_base.py +0 -0
  101. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_last_mile.py +0 -0
  102. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_launch_cmd.py +0 -0
  103. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_local_additions.py +0 -0
  104. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_mcp.py +0 -0
  105. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_no_internal_labels.py +0 -0
  106. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_overlay_libs.py +0 -0
  107. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_overlays_cli.py +0 -0
  108. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_overlays_command.py +0 -0
  109. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_path_timeout_frontmatter.py +0 -0
  110. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_pipwheel.py +0 -0
  111. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_powershell_env_hook.py +0 -0
  112. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_quoting_injection_safety.py +0 -0
  113. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_scope.py +0 -0
  114. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_sources.py +0 -0
  115. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_sources_uri.py +0 -0
  116. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_wire_hooks.py +0 -0
  117. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_wrappers.py +0 -0
  118. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tools/audit.py +0 -0
  119. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tools/cloud-setup.sh +0 -0
  120. {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tools/pyz_smoke.sh +0 -0
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.1] - 2026-10-21
11
+
12
+ ### Added
13
+
14
+ - Overlay commands get a launcher on `PATH`: each top-level `bin/<name>.py`, and
15
+ each `[scripts]` entry in `overlay.toml` (`name = "path"` adds or replaces one,
16
+ `name = false` opts a `bin/<name>.py` out). The launchers live in the agents'
17
+ venv, in `<venv>/overlays/`: `<name>.exe` on Windows, made with distlib's
18
+ `ScriptMaker` so no command interpreter parses the arguments, and a `/bin/sh`
19
+ script elsewhere. They run the script through `$<NAME>_OVERLAY_ROOT` when they
20
+ run, so only a new interpreter needs them made again. `init` and `overlays add` /
21
+ `sync` / `remove` keep them current and delete those no overlay has; two
22
+ overlays with one script name is an error naming both.
23
+ - `dotagents env` puts the launcher directory of the venv `AGENTS_PYTHON` names
24
+ first on `PATH`, ahead of the levels' `bin` dirs, and takes any other venv's
25
+ off it.
26
+ - A project gets its own venv when one of its overlays has scripts.
27
+ - New dependency: `distlib>=0.4.0,<0.5`, vendored in the `.pyz`.
28
+
10
29
  ## [0.8.0] - 2026-10-16
11
30
 
12
31
  ### Added
@@ -1527,7 +1546,8 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
1527
1546
  affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
1528
1547
  before dispatch; a no-op for a plain install.
1529
1548
 
1530
- [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.8.0...HEAD
1549
+ [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.8.1...HEAD
1550
+ [0.8.1]: https://github.com/jose-pr/dotagents/compare/v0.8.0...v0.8.1
1531
1551
  [0.8.0]: https://github.com/jose-pr/dotagents/compare/v0.7.6...v0.8.0
1532
1552
  [0.7.6]: https://github.com/jose-pr/dotagents/compare/v0.7.5...v0.7.6
1533
1553
  [0.7.5]: https://github.com/jose-pr/dotagents/compare/v0.7.4...v0.7.5
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dotagents-cli
3
- Version: 0.8.0
3
+ Version: 0.8.1
4
4
  Summary: The dotagents CLI: install and manage a portable ~/.agents config for AI coding agents
5
5
  Project-URL: Homepage, https://github.com/jose-pr/dotagents/
6
6
  Project-URL: Repository, https://github.com/jose-pr/dotagents
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.14
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Classifier: Typing :: Typed
25
25
  Requires-Python: >=3.9
26
+ Requires-Dist: distlib<0.5,>=0.4.0
26
27
  Requires-Dist: duho<0.8,>=0.7.5
27
28
  Requires-Dist: pathapi<0.2,>=0.1.0
28
29
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
@@ -72,12 +72,41 @@ dotagents starts with the libs in place whatever shell it runs from. A `lib/` mo
72
72
  comes before site-packages and the standard library: do not reuse a package name you
73
73
  do not mean to replace.
74
74
 
75
- A `bin/` launcher can also be run outside a `dotagents env` session (from cmd.exe or
76
- a PowerShell prompt, say), so it sets `PYTHONPATH` itself before starting Python:
77
- its own `lib/`, then `$AGENTS_PYTHONPATH`, then the caller's, leaving empty parts
78
- out (an empty entry means the current directory). The net overlay's `bin/curl` and
79
- `bin/curl.cmd` are the pattern: POSIX sh with no external commands, and a `.cmd`
80
- that uses `setlocal` so the caller's cmd session is not changed.
75
+ ## Commands on PATH: `bin/*.py` and `[scripts]`
76
+
77
+ A Python command needs no launcher of your own. Every top-level `bin/<name>.py`
78
+ gets one named `<name>`: `<name>.exe` on Windows (distlib's `ScriptMaker`, the
79
+ launcher pip gives console scripts) and a `/bin/sh` script elsewhere, made in the
80
+ agents' venv, in `<venv>/overlays/`, whenever `overlays add` / `sync` / `remove` or
81
+ `init` brings the venv up to date. `dotagents env` puts that directory first on
82
+ `PATH` for the venv `$AGENTS_PYTHON` names, so `bin/curl.py` answers to `curl`
83
+ ahead of the system's, and drops any other venv's.
84
+
85
+ The launcher runs your script under the venv's interpreter, as `python
86
+ bin/<name>.py` would, found when it runs through `$<NAME>_OVERLAY_ROOT`: editing
87
+ the script or moving the store needs nothing regenerated, only a new interpreter
88
+ does. Unset, or with the script missing, it exits 2 and says so. A file whose name
89
+ starts with `_` is not a command. Name other scripts, rename one, or opt one out
90
+ in `overlay.toml`:
91
+
92
+ ```toml
93
+ [scripts]
94
+ tool = "lib/tool/main.py" # a launcher `tool` for another path
95
+ helper = false # none for bin/helper.py
96
+ ```
97
+
98
+ Two overlays with one script name is an error naming both. A launcher no overlay
99
+ has any more is deleted at the next sync.
100
+
101
+ **Never ship a `.cmd` or `.bat` that takes arguments.** cmd.exe parses its command
102
+ line before the first line runs, so an argument's `&`, `|`, `>` or `%VAR%` acts:
103
+ from PowerShell, `curl "http://h/?a=1|hostname"` would run `hostname`. The `.exe`
104
+ above hands the arguments over as passed.
105
+
106
+ Your script may run outside a `dotagents env` session too, so set `PYTHONPATH`
107
+ for what it starts, if anything needs the libs: its own `lib/`, then
108
+ `$AGENTS_PYTHONPATH`, then the caller's, leaving empty parts out (an empty entry
109
+ means the current directory). The net overlay's `bin/curl.py` does this.
81
110
 
82
111
  ## Setup scripts
83
112
 
@@ -48,7 +48,7 @@ or `--repo <checkout>/overlays` for a local checkout of the branch.
48
48
  | `python`, `node`, `rust` | Per-language `kb/` conventions + manifest templates + CI workflow templates. |
49
49
  | `release` | A host-agnostic release helper: an agent-driven commit-plan loop plus tag/CI monitoring across GitHub (`gh`) and GitLab. |
50
50
  | `private-sync` | The one-private-repo, per-project `.agents` model (`kb/PRIVATE_SYNC.md` + cloud hooks) — see [Private sync](private-sync.md). |
51
- | `net` | Dependency-free HTTP tooling (a drop-in `curl` shim, an OS-trust-store `certifi` shim, an `httplib` session toolkit reading `AGENTS_PROXY`). |
51
+ | `net` | HTTP tooling: a drop-in `curl` launcher (curlike), an OS-trust-store `certifi` shim, and syshttp (the system proxy and the OS verifier by default), both packages installed from the overlay's `requirements.txt`; settings are `AGENTS_*` (`AGENTS_PROXY` and kin). |
52
52
  | `recovery` | A config-recovery playbook for reconstructing a lost `~/.agents`. |
53
53
 
54
54
  ## Managing overlays
@@ -182,7 +182,9 @@ dotagents overlays sync -g # r
182
182
 
183
183
  Nothing in an overlay has to set up its own paths. For every installed overlay,
184
184
  `dotagents env` prepends `bin/` to `PATH` and an existing `lib/` to `PYTHONPATH`
185
- (listing the libs in `AGENTS_PYTHONPATH` too), and exports **`$<NAME>_OVERLAY_ROOT`** — the overlay's
185
+ (listing the libs in `AGENTS_PYTHONPATH` too), puts the launchers of its scripts
186
+ (`bin/*.py` and `[scripts]`, in `<venv>/overlays/` of the agents' venv) first on
187
+ `PATH`, and exports **`$<NAME>_OVERLAY_ROOT`** — the overlay's
186
188
  installed directory (`NAME` is the overlay's name upper-cased, `-` → `_`:
187
189
  `private-sync` → `PRIVATE_SYNC_OVERLAY_ROOT`).
188
190
  That variable is how overlay content refers to itself and to other overlays: a
@@ -214,6 +216,7 @@ small built-in reader that covers these keys. Keys:
214
216
  | `rules` | Overlay-relative markdown paths whose rule bullets append to "Always-on rules". |
215
217
  | `priority` | Merge order (lower sorts earlier; unprioritized default is 500). |
216
218
  | `setup_timeout` | Seconds the setup script may run before it is stopped (default 300; `0` = no limit). |
219
+ | `[scripts]` | Launchers on `PATH`: `name = "path/in/overlay.py"` adds or replaces one, `name = false` drops the one for a `bin/<name>.py`. See [Authoring → Commands on PATH](authoring.md#commands-on-path-binpy-and-scripts). |
217
220
 
218
221
  `overlay.toml` is the only manifest. A leading `---` frontmatter block in a
219
222
  `CONTEXT.md` (the `priority:` / `scope:` / `dependencies:` an older overlay format
@@ -37,6 +37,8 @@ classifiers = [
37
37
  dependencies = [
38
38
  "duho>=0.7.5,<0.8",
39
39
  "pathapi>=0.1.0,<0.2",
40
+ # The launchers of overlays' [scripts]: ScriptMaker writes the Windows .exe.
41
+ "distlib>=0.4.0,<0.5",
40
42
  # overlay.toml manifests and TOML registries; tomllib is stdlib from 3.11.
41
43
  'tomli>=1.1.0; python_version < "3.11"',
42
44
  ]
@@ -239,8 +239,10 @@ returns the exit code.
239
239
  otherwise; `0` from either means no limit.
240
240
  - `parse_manifest_text(text, origin="overlay.toml") -> Optional[dict]` — the TOML
241
241
  document via `tomllib` / `tomli`, else a built-in reader (top-level strings,
242
- string arrays including multi-line strings, an integer); `None` plus one warning
243
- when it is not valid TOML.
242
+ string arrays including multi-line strings, an integer, and the `[scripts]` table
243
+ of strings and booleans); `None` plus one warning when it is not valid TOML.
244
+ - `SCRIPT_NAME_RE` — a script (launcher) name: a letter or digit, then letters,
245
+ digits, `.`, `_`, `-`; never ending in `.exe` nor a Windows device name.
244
246
  - `class InstallResult` — what `install_to` did: `written`, `unchanged`,
245
247
  `replaced` (edited here, replaced by the source's file), `removed` (not in the
246
248
  source, or cleared by `prune`), `backed_up`, `lines`; property `skipped`
@@ -270,8 +272,16 @@ returns the exit code.
270
272
  (the later copy shadows it).
271
273
  - `Overlay.sort_by_priority(overlays) -> list[Overlay]` — sorted by `sort_key`.
272
274
  - `read_manifest() -> dict` — keys `name`, `description`, `routing`, `rules`,
273
- `requires` (valid names only, normalized, de-duplicated), `priority`; a missing
274
- or invalid manifest gives empty contributions.
275
+ `requires` (valid names only, normalized, de-duplicated), `priority`,
276
+ `setup_timeout`, `scripts` (the `[scripts]` table as written: name -> path
277
+ relative to the overlay, `/`-separated, plain file-name characters, no `..`,
278
+ or `false`; any other entry is dropped with a warning); a missing or invalid
279
+ manifest gives empty contributions.
280
+ - `scripts() -> dict[str, str]` — the scripts that get a launcher
281
+ (`dotagents._launchers`): each top-level `bin/<name>.py` whose stem is a
282
+ `SCRIPT_NAME_RE` name (so not `_private.py`, not `__init__.py`), then
283
+ `[scripts]`, which adds or replaces a path by name, or drops a detected one
284
+ with `false`.
275
285
  - `find_setup_script() -> Optional[Path]`.
276
286
  - `setup_timeout(override=None) -> int` — `override`, else the manifest's
277
287
  `setup_timeout` (a non-negative int; anything else is ignored with a warning),
@@ -307,6 +317,35 @@ returns the exit code.
307
317
  `sort_key` order; creates the file when absent; content outside the markers kept;
308
318
  returns whether it changed (`False` and a warning when the file has no block).
309
319
 
320
+ ## `dotagents._launchers`
321
+
322
+ Launchers for overlays' scripts (`Overlay.scripts()`), in `<venv>/overlays/` of a
323
+ dotagents venv; `_pyvenv.sync_venvs` brings them up to date on every sync, and
324
+ `_env.get_environment` puts that dir first on `PATH` for the venv `AGENTS_PYTHON`
325
+ names. A launcher runs `$<NAME>_OVERLAY_ROOT/<script>` under the venv's
326
+ interpreter, resolved when it runs (argv[0] the script, its dir first on
327
+ `sys.path`, run as `__main__`); with the variable unset or the script missing it
328
+ exits 2 naming both. Windows: `<name>.exe`, distlib's `ScriptMaker` launcher (no
329
+ command interpreter parses its arguments). POSIX: `<name>`, mode 0755, a `/bin/sh`
330
+ script that `exec`s the interpreter.
331
+
332
+ - `DIR = "overlays"`, `MANIFEST = ".dotagents-launchers.json"` (in that dir:
333
+ `{name: overlay name}` of the launchers dotagents wrote).
334
+ - `class LauncherError(SystemExit)` — two overlays with one script name; the
335
+ message names both.
336
+ - `launcher_dir(venv) -> Path`. `file_name(name) -> str` (`.exe` added on Windows).
337
+ `is_launcher_dir(path) -> bool` — the shape `<store>/.pyvenv/<triplet>/overlays`,
338
+ no filesystem access.
339
+ - `declared(overlays) -> dict[str, tuple[Overlay, str]]` — every script by name;
340
+ raises `LauncherError` on a name two overlays have.
341
+ - `render(name, var, script, python) -> bytes` — one launcher's bytes; equal
342
+ inputs give equal bytes (the `.exe`'s archive carries a fixed time).
343
+ - `sync(venv, python, overlays, *, dry_run=False, logger=None) -> list[str]` —
344
+ writes each launcher whose bytes differ (atomic replace), deletes those
345
+ `MANIFEST` lists that no overlay has any more (other files there are left
346
+ alone), rewrites `MANIFEST`; returns the names written or deleted. Raises
347
+ `LauncherError` before any write.
348
+
310
349
  ## `dotagents._sources`
311
350
 
312
351
  Spec grammar: `[git+]<location>[@<ref>][#<path>]`. A location ending `.git`, starting
@@ -383,7 +422,12 @@ a directory of overlays or a registry file (default: the root); for a **source**
383
422
 
384
423
  Assembly order (`get_environment`): identity (`stamp_identity`), then the roots,
385
424
  then `PATH`, then `PYTHONPATH` / `AGENTS_PYTHONPATH`, then the env files, then the
386
- proxy model.
425
+ proxy model. `PATH`: every level's `bin` prepended, then, first of all, the active
426
+ venv's launcher dir (`<venv>/overlays`, `_launchers`) when `AGENTS_PYTHON` is that
427
+ venv's interpreter and the dir exists; every other dotagents venv's launcher dir is
428
+ taken out. First even before the `bin` dirs: cmd.exe and PowerShell try every
429
+ `PATHEXT` extension in a directory before the next one, so a `bin/<name>.py` (with
430
+ `.PY` in `PATHEXT`) or a `.cmd` there would otherwise win.
387
431
 
388
432
  - Roots: `AGENTS_HOME` and `AGENTS_PROJECT_ROOT` name the stores walked, absolute —
389
433
  an inherited value naming the same directory holds, another is replaced; in a
@@ -706,10 +750,16 @@ code. `AGENTS_NO_PYVENV=1` (or `true`/`yes`/`on`) turns it all off.
706
750
  when the requirements' digest changed, in one `pip install -r ... -r ...` per
707
751
  venv, with `--find-links` from `AGENTS_PIP_SOURCE`. The `.pth` is rewritten when
708
752
  it differs: the store's own libs, then each lower store's venv site-packages and
709
- libs, then `runtime_entry()`. Raises `VenvError` (a `SystemExit`) on failure.
753
+ libs, then `runtime_entry()`. Each venv's launchers (`_launchers.sync`) serve its
754
+ store's overlays and every lower store's; a project venv a sync made that is no
755
+ longer wanted keeps its launchers current too. With `AGENTS_NO_PYVENV` it logs
756
+ which scripts get no launcher. Raises `VenvError` or `LauncherError` (both
757
+ `SystemExit`) on failure.
710
758
  - `wants_venv(scope, level, store, libs) -> bool` — always for `user`; for `project`
711
- when it has overlay requirements or libs; never for `system` (a venv an
759
+ when it has overlay requirements, libs or scripts; never for `system` (a venv an
712
760
  administrator made there is still layered).
761
+ - `store_overlays(scope, stores) -> list[Overlay]` — the overlays in play
762
+ installed in any of `stores`.
713
763
  - `ensure_venv(target, python=None, *, with_pip=None, dry_run=False, logger=None)
714
764
  -> Optional[Path]` — the venv's interpreter, created when missing; `with_pip=None`
715
765
  tries ensurepip and falls back to `--without-pip`, `False` never uses it.
@@ -802,6 +852,8 @@ proxy vars, plus whatever the env files set. `AGENTS_AGENT` is never emitted.
802
852
  Set for an overlay's `setup.py`: `AGENTS_HOME` (user store), `AGENTS_SCOPE_ROOT`,
803
853
  `AGENTS_SCOPE`, `AGENTS_OVERLAY_DIR`.
804
854
 
855
+ Read by a script launcher (`_launchers`): its overlay's `<NAME>_OVERLAY_ROOT`.
856
+
805
857
  ## Gotchas
806
858
 
807
859
  - **Python 3.9 floor.** Runtime-evaluated annotations need `from __future__ import
@@ -1,3 +1,3 @@
1
1
  """dotagents: installable CLI for the dotagents agent-config payload."""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.8.1"
@@ -4,7 +4,8 @@ Contract B, the exact sequence :func:`get_environment` performs:
4
4
 
5
5
  1. **Bins onto PATH FIRST**, before any env eval. Each level's ``bin`` dir
6
6
  (contract-A precedence order, *except* project-root) is prepended to
7
- ``PATH`` so env scripts can call overlay helpers by name. **Libs go on
7
+ ``PATH`` so env scripts can call overlay helpers by name, and ahead of
8
+ them the active venv's launcher dir (:func:`_launcher_path`). **Libs go on
8
9
  ``PYTHONPATH`` the same way**: each level's ``lib`` dir that EXISTS
9
10
  (:func:`get_lib_paths`) is prepended to ``PYTHONPATH`` and listed,
10
11
  ``os.pathsep``-joined, in ``AGENTS_PYTHONPATH`` -- so an ``env.py``, a
@@ -852,6 +853,25 @@ def _anchored(entry: str) -> bool:
852
853
  return bool(entry) and (os.path.isabs(entry) or entry[0] in "/\\")
853
854
 
854
855
 
856
+ def _launcher_path(path: str, venv: "Optional[Path]") -> "Optional[str]":
857
+ """``PATH`` with ``venv``'s launcher dir (``<venv>/overlays``, the
858
+ overlays' ``[scripts]``: :mod:`dotagents._launchers`) first when it
859
+ exists, and every other dotagents venv's taken out: a launcher names its
860
+ own venv's interpreter. ``None`` if nothing changes.
861
+
862
+ First, ahead of the levels' ``bin`` dirs too: cmd.exe and PowerShell try
863
+ every ``PATHEXT`` extension in one directory before the next, so a
864
+ ``bin`` holding the script itself (``curl.py``, with ``.PY`` in
865
+ ``PATHEXT``) or a ``.cmd`` of the same name would otherwise win."""
866
+ from dotagents import _launchers
867
+
868
+ entries = [e for e in path.split(os.pathsep) if not (e and _launchers.is_launcher_dir(e))] if path else []
869
+ if venv is not None and _launchers.launcher_dir(venv).is_dir():
870
+ entries.insert(0, str(_launchers.launcher_dir(venv)))
871
+ updated = os.pathsep.join(entries)
872
+ return updated if updated != path else None
873
+
874
+
855
875
  def _prepended_path_var(osenv: "dict[str, str]", var: str, entries: "list[str]") -> "Optional[str]":
856
876
  """The new value of an ``os.pathsep``-joined path var with ``entries``
857
877
  prepended (:func:`_prepend_missing` order), or ``None`` if nothing changes.
@@ -1106,6 +1126,10 @@ def get_environment(
1106
1126
  updated = _prepended_path_var(osenv, "PATH", bin_paths)
1107
1127
  if updated is not None:
1108
1128
  _apply({"PATH": updated})
1129
+ updated = _launcher_path(osenv.get("PATH", ""), venv if venv is not None
1130
+ and osenv.get("AGENTS_PYTHON") == str(_pyvenv.venv_python(venv)) else None)
1131
+ if updated is not None:
1132
+ _apply({"PATH": updated})
1109
1133
 
1110
1134
  # --- Libs: on PYTHONPATH, and published as AGENTS_PYTHONPATH, same moment.
1111
1135
  # Everything an overlay starts -- a skill script the agent runs, a `bin`
@@ -0,0 +1,241 @@
1
+ """Launchers for overlays' scripts: each top-level ``bin/<name>.py``, plus
2
+ what ``overlay.toml`` declares (``Overlay.scripts``)::
3
+
4
+ [scripts]
5
+ tool = "lib/tool/main.py" # another path, or another name
6
+ helper = false # no launcher for bin/helper.py
7
+
8
+ Each becomes an executable named after the key in ``<venv>/overlays/`` of a
9
+ dotagents venv (:mod:`dotagents._pyvenv`), which ``env`` puts first on
10
+ ``PATH`` for the venv ``AGENTS_PYTHON`` names. A launcher runs the script
11
+ under that venv's interpreter, found at RUN time through the overlay's
12
+ ``<NAME>_OVERLAY_ROOT``, so moving a store or syncing an overlay never
13
+ strands it; only a new interpreter needs a new launcher. With the variable
14
+ unset or the script missing it exits 2, saying so.
15
+
16
+ * Windows: ``<name>.exe``, made by distlib's ``ScriptMaker`` (pip's console
17
+ script launcher) around a short Python bootstrap. A native program: no
18
+ command interpreter parses its arguments, as cmd.exe would a ``.cmd``'s.
19
+ * POSIX: ``<name>``, a ``/bin/sh`` script that ``exec``s the interpreter.
20
+
21
+ :data:`MANIFEST` in the directory records which overlay made which launcher,
22
+ so one no overlay declares any more is deleted; files it does not list are
23
+ left alone. Writes are atomic and happen only when the bytes differ.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import os
30
+ import shutil
31
+ import sys
32
+ import tempfile
33
+ from pathlib import Path
34
+ from typing import Optional
35
+
36
+ #: ``<venv>/overlays``: the launchers, and nothing of the venv's own.
37
+ DIR = "overlays"
38
+ MANIFEST = ".dotagents-launchers.json"
39
+ #: The time stamped on the bootstrap in an ``.exe``'s archive (1980-01-01, the
40
+ #: earliest a ZIP can hold): fixed, so equal inputs give equal bytes.
41
+ _EPOCH = "315532800"
42
+
43
+
44
+ class LauncherError(SystemExit):
45
+ """Two overlays declare the same script name."""
46
+
47
+
48
+ def launcher_dir(venv: "str | os.PathLike[str]") -> Path:
49
+ return Path(venv) / DIR
50
+
51
+
52
+ def is_launcher_dir(path: str) -> bool:
53
+ """Whether ``path`` has the shape of any dotagents venv's launcher dir
54
+ (``<store>/.pyvenv/<triplet>/overlays``); the filesystem is not consulted."""
55
+ from dotagents._pyvenv import PYVENV_DIR
56
+
57
+ parts = Path(os.path.normpath(path)).parts
58
+ fold = (lambda s: s.lower()) if os.name == "nt" else (lambda s: s)
59
+ return len(parts) >= 3 and fold(parts[-1]) == fold(DIR) and fold(parts[-3]) == fold(PYVENV_DIR)
60
+
61
+
62
+ def file_name(name: str) -> str:
63
+ return name + ".exe" if os.name == "nt" else name
64
+
65
+
66
+ def declared(overlays) -> "dict[str, tuple]":
67
+ """``{name: (overlay, script)}`` over ``overlays``; a name two overlays
68
+ declare is a :class:`LauncherError` naming both."""
69
+ out: "dict[str, tuple]" = {}
70
+ seen: "dict[str, str]" = {}
71
+ for overlay in overlays:
72
+ for name, script in overlay.scripts().items():
73
+ key = name.lower() if os.name == "nt" else name
74
+ if key in seen and out[seen[key]][0].path != overlay.path:
75
+ raise LauncherError(
76
+ "error: overlays %s and %s both declare the script %r"
77
+ % (out[seen[key]][0].name, overlay.name, name)
78
+ )
79
+ seen[key] = name
80
+ out[name] = (overlay, script)
81
+ return out
82
+
83
+
84
+ def _bootstrap(name: str, var: str, script: str) -> str:
85
+ """The Python an ``.exe`` runs: the script as ``__main__``, as
86
+ ``python <script>`` would."""
87
+ return "\n".join([
88
+ "#!python",
89
+ "import os",
90
+ "import runpy",
91
+ "import sys",
92
+ "",
93
+ "root = os.environ.get(%r, '')" % var,
94
+ "script = os.path.join(root, *%r)" % (script.split("/"),),
95
+ "if not root or not os.path.isfile(script):",
96
+ " sys.stderr.write(%r %% root)" % (
97
+ "%s: %s=%%s has no %s: run it in a `dotagents env` session\n" % (name, var, script)),
98
+ " sys.exit(2)",
99
+ "sys.argv[0] = script",
100
+ "sys.path[0] = os.path.dirname(script)",
101
+ "runpy.run_path(script, run_name='__main__')",
102
+ "",
103
+ ])
104
+
105
+
106
+ def _sh_quote(text: str) -> str:
107
+ return "'%s'" % text.replace("'", "'\\''")
108
+
109
+
110
+ def _shell(name: str, var: str, script: str, python: str) -> bytes:
111
+ message = "%s: %s=${%s:-} has no %s: run it in a dotagents env session" % (name, var, var, script)
112
+ path = '"$%s"%s' % (var, _sh_quote("/" + script))
113
+ return "\n".join([
114
+ "#!/bin/sh",
115
+ "# Made by dotagents from overlay.toml [scripts]; regenerated on overlays sync.",
116
+ 'if [ -z "${%s:-}" ] || [ ! -f %s ]; then' % (var, path),
117
+ ' echo "%s" >&2' % message,
118
+ " exit 2",
119
+ "fi",
120
+ "exec %s %s \"$@\"" % (_sh_quote(python), path),
121
+ "",
122
+ ]).encode("utf-8")
123
+
124
+
125
+ def _exe(name: str, var: str, script: str, python: str, work: str) -> bytes:
126
+ from distlib.scripts import ScriptMaker
127
+
128
+ source = os.path.join(work, "source")
129
+ out = os.path.join(work, "out")
130
+ os.mkdir(source)
131
+ os.mkdir(out)
132
+ with open(os.path.join(source, name + ".py"), "w", encoding="utf-8", newline="\n") as handle:
133
+ handle.write(_bootstrap(name, var, script))
134
+ maker = ScriptMaker(source, out, add_launchers=True)
135
+ maker.executable = python
136
+ maker.variants = {""}
137
+ saved = os.environ.get("SOURCE_DATE_EPOCH")
138
+ os.environ["SOURCE_DATE_EPOCH"] = _EPOCH
139
+ try:
140
+ maker.make(name + ".py")
141
+ finally:
142
+ if saved is None:
143
+ del os.environ["SOURCE_DATE_EPOCH"]
144
+ else:
145
+ os.environ["SOURCE_DATE_EPOCH"] = saved
146
+ with open(os.path.join(out, name + ".exe"), "rb") as handle:
147
+ return handle.read()
148
+
149
+
150
+ def render(name: str, var: str, script: str, python: str) -> bytes:
151
+ """The bytes of launcher ``name`` for this platform."""
152
+ if os.name != "nt":
153
+ return _shell(name, var, script, python)
154
+ work = tempfile.mkdtemp(prefix="dotagents-launcher-")
155
+ try:
156
+ return _exe(name, var, script, python, work)
157
+ finally:
158
+ shutil.rmtree(work, ignore_errors=True)
159
+
160
+
161
+ def _read(path: Path) -> "Optional[bytes]":
162
+ try:
163
+ with open(str(path), "rb") as handle:
164
+ return handle.read()
165
+ except OSError:
166
+ return None
167
+
168
+
169
+ def _write(path: Path, data: bytes, mode: "Optional[int]") -> None:
170
+ partial = path.with_name(".%s.new" % path.name)
171
+ with open(str(partial), "wb") as handle:
172
+ handle.write(data)
173
+ if mode is not None:
174
+ os.chmod(str(partial), mode)
175
+ try:
176
+ os.replace(str(partial), str(path))
177
+ except OSError:
178
+ os.unlink(str(partial))
179
+ raise
180
+
181
+
182
+ def sync(venv: Path, python: Path, overlays, *, dry_run: bool = False, logger=None) -> "list[str]":
183
+ """Make ``<venv>/overlays`` hold exactly the launchers ``overlays``
184
+ declare (interpreter ``python``), deleting those :data:`MANIFEST` lists
185
+ that no overlay declares any more. Returns the launcher names written or
186
+ deleted. A name declared twice is a :class:`LauncherError`, before any
187
+ write."""
188
+ wanted = declared(overlays)
189
+ target = launcher_dir(venv)
190
+ manifest_path = target / MANIFEST
191
+ try:
192
+ recorded = json.loads((_read(manifest_path) or b"{}").decode("utf-8"))
193
+ except ValueError:
194
+ recorded = {}
195
+ if not isinstance(recorded, dict):
196
+ recorded = {}
197
+ changed: "list[str]" = []
198
+ for name in sorted(set(recorded) - set(wanted)):
199
+ path = target / file_name(name)
200
+ if path.exists():
201
+ _log(logger, "info", "launcher %s: removed (no overlay declares it)%s", path, _dry(dry_run))
202
+ if not dry_run:
203
+ path.unlink()
204
+ changed.append(name)
205
+ for name, (overlay, script) in sorted(wanted.items()):
206
+ if not (overlay.path / script).is_file():
207
+ _log(logger, "warning", "overlay %s: script %s (%s) not found; its launcher exits 2 until it is",
208
+ overlay.name, name, script)
209
+ data = render(name, overlay.root_var, script, str(python))
210
+ path = target / file_name(name)
211
+ if _read(path) == data:
212
+ continue
213
+ _log(logger, "info", "launcher %s: %s (%s)%s", path, "updated" if path.exists() else "created",
214
+ overlay.name, _dry(dry_run))
215
+ changed.append(name)
216
+ if not dry_run:
217
+ target.mkdir(parents=True, exist_ok=True)
218
+ _write(path, data, None if os.name == "nt" else 0o755)
219
+ record = {name: overlay.name for name, (overlay, _script) in wanted.items()}
220
+ if not dry_run and record != recorded:
221
+ if record:
222
+ target.mkdir(parents=True, exist_ok=True)
223
+ _write(manifest_path, (json.dumps(record, indent=1, sort_keys=True) + "\n").encode("utf-8"), None)
224
+ elif manifest_path.exists():
225
+ manifest_path.unlink()
226
+ try:
227
+ target.rmdir()
228
+ except OSError:
229
+ pass # something else lives there: left alone
230
+ return changed
231
+
232
+
233
+ def _dry(dry_run: bool) -> str:
234
+ return " [dry-run]" if dry_run else ""
235
+
236
+
237
+ def _log(logger, level: str, message: str, *args) -> None:
238
+ if logger is not None:
239
+ getattr(logger, level)(message, *args)
240
+ elif level == "warning":
241
+ sys.stderr.write("warning: " + (message % args) + "\n")