dotagents-cli 0.6.2__tar.gz → 0.6.3__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.
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/CHANGELOG.md +28 -1
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/PKG-INFO +1 -1
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/guide/commands.md +12 -2
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/AGENTS.md +20 -3
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/__init__.py +1 -1
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_agents.py +11 -4
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_env.py +22 -2
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_resources.py +12 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/cli/env.py +77 -3
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_env.py +37 -0
- dotagents_cli-0.6.3/tests/test_env_into.py +89 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_wire_hooks.py +10 -7
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tools/pyz_smoke.sh +7 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/.gitignore +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/LICENSE +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/README.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/agents.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/cli.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/commands.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/context.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/env.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/fs.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/hooks.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/merge.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/overlays.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/reference.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/resources.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/scope.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/skills.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/sources.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/api/wrappers.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/changelog.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/guide/authoring.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/guide/install.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/guide/overlays.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/guide/private-sync.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/docs/index.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/install.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/mkdocs.yml +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/pyproject.toml +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/__main__.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_context.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_fs.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_hooks.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_merge.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlays.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_scope.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_skills.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_sources.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_wrappers.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/cli/__init__.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/cli/_common.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/cli/about.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/cli/build_pyz.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/cli/context.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/cli/init.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/cli/overlays.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/cli/path.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/py.typed +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/_helpers.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/_shell.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/conftest.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_about.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_agents.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_api_header.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_audit_leak.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_build_pyz.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_cli_main.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_cloud_setup.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_cmds_discovery.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_context.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_env_cli_scope.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_env_format.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_env_regressions.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_env_safety.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_findings_cmd.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_fix_adapters.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_fix_cli.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_fix_env.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_fix_hooks.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_fix_init.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_fix_overlays.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_fix_packaging.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_fix_scope_context.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_fix_skills_adapters.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_hooks.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_init_base.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_last_mile.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_launch_cmd.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_mcp.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_overlay_libs.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_overlays_cli.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_overlays_command.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_path_timeout_frontmatter.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_powershell_env_hook.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_review_security.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_scope.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_sources.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_sources_uri.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tests/test_wrappers.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tools/audit.py +0 -0
- {dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/tools/cloud-setup.sh +0 -0
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.3] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `env --into FILE` writes the output into FILE as the one block `env` owns
|
|
15
|
+
there, replacing the block an earlier run wrote and keeping every other line.
|
|
16
|
+
- With `--diff --format export`, a `PATH` that is the caller's own with entries
|
|
17
|
+
put in front is written as `export PATH='<entries>'"${PATH:+:$PATH}"`, so it
|
|
18
|
+
stays short and keeps what the sourcing shell has.
|
|
19
|
+
- When `dotagents` runs from a `.pyz`, `env` exports `AGENTS_PYLIB`, the path
|
|
20
|
+
of that archive. The libraries it bundles (duho, pathlib_next) import from
|
|
21
|
+
it, so an overlay built on duho works even when the interpreter in
|
|
22
|
+
`AGENTS_PYTHON` has no duho installed. An installed `dotagents` sets nothing:
|
|
23
|
+
its libraries are already importable from `AGENTS_PYTHON`. The archive is
|
|
24
|
+
never put on `PYTHONPATH`, where it would shadow a project's own copy; a
|
|
25
|
+
consumer adds it to `sys.path` itself when its import fails.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- The Claude Code SessionStart hook no longer appends the whole environment to
|
|
30
|
+
`$CLAUDE_ENV_FILE` every time it runs (startup, resume, compact, clear). Claude
|
|
31
|
+
Code inlines that file into every Bash command, and once it passed about 8 KB,
|
|
32
|
+
Git for Windows' bash truncated the command and every Bash call failed with an
|
|
33
|
+
unterminated quote. The hook now uses `env --into`, which keeps one block.
|
|
34
|
+
Re-run `dotagents init -g` to update an installed hook.
|
|
35
|
+
|
|
10
36
|
## [0.6.2] - 2026-09-28
|
|
11
37
|
|
|
12
38
|
### Added
|
|
@@ -1308,7 +1334,8 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
|
|
|
1308
1334
|
affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
|
|
1309
1335
|
before dispatch; a no-op for a plain install.
|
|
1310
1336
|
|
|
1311
|
-
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.6.
|
|
1337
|
+
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.6.3...HEAD
|
|
1338
|
+
[0.6.3]: https://github.com/jose-pr/dotagents/compare/v0.6.2...v0.6.3
|
|
1312
1339
|
[0.6.2]: https://github.com/jose-pr/dotagents/compare/v0.6.1...v0.6.2
|
|
1313
1340
|
[0.6.1]: https://github.com/jose-pr/dotagents/compare/v0.6.0...v0.6.1
|
|
1314
1341
|
[0.6.0]: https://github.com/jose-pr/dotagents/compare/v0.5.1...v0.6.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dotagents-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
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
|
|
@@ -87,7 +87,7 @@ the store's, else `dotagents` on `PATH`.
|
|
|
87
87
|
|
|
88
88
|
| Hook | Command | Why |
|
|
89
89
|
| --- | --- | --- |
|
|
90
|
-
| `SessionStart` |
|
|
90
|
+
| `SessionStart` | writes `dotagents env --diff --format export` into `$CLAUDE_ENV_FILE` (`--into`: one block, replaced on every run; `PATH` as a prefix on the shell's own), then runs `dotagents context` | Claude sources `$CLAUDE_ENV_FILE` before each Bash command, so the env layers reach every command in the session; and it injects the hook's **stdout into the session context**, which is how the assembled context reaches the model. |
|
|
91
91
|
| `CwdChanged` | when the new directory has a `.agents/`, appends `export AGENTS_PROJECT_ROOT=<it>` to `$CLAUDE_ENV_FILE`; then prints the directory's `AGENTS.md` if there is one | Re-pins the project root after a `cd` into another project, and surfaces that directory's `AGENTS.md`. |
|
|
92
92
|
|
|
93
93
|
On Windows each event gets a second, PowerShell-native handler (`shell:
|
|
@@ -272,6 +272,11 @@ dotagents env --diff --format json # only vars that differ from the caller's e
|
|
|
272
272
|
environment, the working directory, the stores, the overlays, every env and `lib`
|
|
273
273
|
file), for at most five minutes; the per-command env loaders use it. The cache is
|
|
274
274
|
owner-only, under `<user store>/.cache/env/`.
|
|
275
|
+
- `--into <file>` — write into `<file>` instead of stdout, as the one block this
|
|
276
|
+
command owns there (between `: dotagents-env-begin` / `: dotagents-env-end`):
|
|
277
|
+
an earlier block is replaced, anything else in the file is kept. The SessionStart
|
|
278
|
+
hook writes `$CLAUDE_ENV_FILE` this way, so the file stays one block however often
|
|
279
|
+
the hook runs. `export` format only.
|
|
275
280
|
- `-g` / `--global` — skip the project-level env files (the store is unaffected).
|
|
276
281
|
- `--agents-dir <dir>` — user store override for this run.
|
|
277
282
|
|
|
@@ -307,7 +312,12 @@ form and drop names that are not shell identifiers.
|
|
|
307
312
|
overlay, where `NAME` is the overlay's name upper-cased with `-` turned into `_`
|
|
308
313
|
(`my-ov` → `MY_OV_OVERLAY_ROOT`). That is the same name `context` expands as a
|
|
309
314
|
`<NAME_OVERLAY_ROOT>` placeholder, so an env file and a context file refer to an
|
|
310
|
-
overlay's install dir by one name.
|
|
315
|
+
overlay's install dir by one name. When `dotagents` runs from a `.pyz`,
|
|
316
|
+
`AGENTS_PYLIB` names that archive: the libraries it bundles (duho,
|
|
317
|
+
pathlib_next) import from it, so an overlay built on them appends it to
|
|
318
|
+
`sys.path` when its own import fails. An installed `dotagents` sets nothing,
|
|
319
|
+
since those libraries are already in `AGENTS_PYTHON`'s site-packages. It is
|
|
320
|
+
never put on `PYTHONPATH`, where it would shadow a project's own copy.
|
|
311
321
|
3. **`PATH`** — every level's `bin/` (each store's overlays, then the store) goes
|
|
312
322
|
to the front in that order, including ones that do not exist yet, so an `env.py`
|
|
313
323
|
and every subprocess can call an overlay's helpers by name. The order holds even
|
|
@@ -38,7 +38,7 @@ and an error saying a command is required, and exits 2.
|
|
|
38
38
|
| `overlays show NAME [-g] [--repo SPEC]... [--json]` | One overlay: the copy a session would use, else the source's; manifest, setup, skills, file count, recorded source. No name: exit 2. |
|
|
39
39
|
| `context [OUT] [-g] [--agents a,b] [--format markdown\|system-reminder\|json] [--write-agent] [--inline]` | Print (or write to `OUT`) the assembled context. `--write-agent` merges it into each agent's `context_target` under the project root (not with `-g`). Exits 2 when `--agents` names no known agent. |
|
|
40
40
|
| `path [-g] [--lib] [--format list\|native\|posix\|json]` | Print the bin dirs `env` puts on `PATH` (`--lib`: the existing lib dirs it puts on `PYTHONPATH`), highest precedence first; runs no env file. `posix` is `:`-joined, MSYS2 form on Windows. |
|
|
41
|
-
| `env [-g] [--format F] [--diff] [--cache]` | Print the assembled environment (`--diff`: only what differs from the caller's; `--cache`: replay the previous output while `env_cache_key` holds, up to `ENV_CACHE_TTL
|
|
41
|
+
| `env [-g] [--format F] [--diff] [--cache] [--into FILE]` | Print the assembled environment (`--diff`: only what differs from the caller's; `--cache`: replay the previous output while `env_cache_key` holds, up to `ENV_CACHE_TTL`; `--into FILE`: write it into FILE as the one block this command owns there) in format `F`: `auto` (default: the calling shell), `export`/`posix`/`sh`/`bash`, `dotenv`/`env`, `powershell`/`pwsh`/`ps`, `cmd`/`bat`/`batch`, `fish`, `json`, `ini`, `yaml`. |
|
|
42
42
|
| `launch [AGENT] [-g] [--command PROG] [--no-context] [--inline] [--write-agent] [--dry-run] [-- ARGS...]` | Run the agent's CLI with the env applied and the context handed over; the exit code is the harness's (`128+N` on signal N). |
|
|
43
43
|
| `findings add\|list\|show\|done\|reopen\|remove\|index\|path [-g] [--dir D]` | The findings queue at `<store>/findings/` (`--dir` overrides): one markdown file per finding, `done` moves it to `processed/` with a required resolution, `INDEX.md` regenerated on every change. |
|
|
44
44
|
| `about [--json]` | `dotagents-cli <version>`, then `<distribution> <version>` per bundled (`.pyz`) or installed package. |
|
|
@@ -108,6 +108,10 @@ The package's own data; library code, importing nothing from `dotagents.cli`.
|
|
|
108
108
|
store at `dest`: the project template for a project store when the base has one,
|
|
109
109
|
else the user template, else `<src>/AGENTS.md`; `{{AGENTS_MD}}` rendered as the
|
|
110
110
|
absolute POSIX path of `<dest>/AGENTS.md`.
|
|
111
|
+
- `pyz_archive() -> str | None` — the `.pyz` this dotagents is imported from (the
|
|
112
|
+
ancestor of the module's `__file__` that is a file), or `None` for a plain
|
|
113
|
+
install. The archive also holds the vendored duho and pathlib_next; `env`
|
|
114
|
+
exports it as `AGENTS_PYLIB`.
|
|
111
115
|
|
|
112
116
|
## Commands — `dotagents.cli.init`, `dotagents.cli.overlays`, `dotagents.cli.context`, `dotagents.cli.env`, `dotagents.cli.path`, `dotagents.cli.about`, `dotagents.cli.build_pyz`
|
|
113
117
|
|
|
@@ -119,6 +123,13 @@ returns the exit code.
|
|
|
119
123
|
("markdown", "system-reminder", "json")`); `env.Env`; `path.PathCmd`
|
|
120
124
|
(`PATH_FORMATS = ("list", "native", "posix", "json")`); `build_pyz.BuildPyz`;
|
|
121
125
|
`about.About`.
|
|
126
|
+
- `env`: `write_into(path, text, output_format)` — what `--into` does: the file
|
|
127
|
+
keeps ONE block between `INTO_BEGIN = ": dotagents-env-begin"` and
|
|
128
|
+
`INTO_END = ": dotagents-env-end"` (no-op commands, not comments, so a `; cmd`
|
|
129
|
+
appended to the last line still runs; earlier blocks removed, other lines kept,
|
|
130
|
+
the new block last), written atomically; `INTO_FORMATS = ("export",)`. With
|
|
131
|
+
`--diff --format export`, a `PATH` that is the caller's with entries in front is
|
|
132
|
+
written as `export PATH='<prefix>'"${PATH:+:$PATH}"`.
|
|
122
133
|
- `about`: `DISTRIBUTION = "dotagents-cli"`, `BUNDLE_FILE = "_bundle.json"` (written
|
|
123
134
|
by `build-pyz` into the package), `RUNTIME_PACKAGES = ("duho", "pathlib_next",
|
|
124
135
|
"uritools", "netimps", "requests")` (the only packages a plain install reports);
|
|
@@ -322,7 +333,11 @@ proxy model.
|
|
|
322
333
|
user-scope walk `AGENTS_PROJECT_ROOT` is set only when unset. Only when unset:
|
|
323
334
|
`AGENTS_PYTHON` (`sys.executable`) and one `<NAME>_OVERLAY_ROOT` per
|
|
324
335
|
`scope.overlays` (a value pinned to the user store's copy is re-pointed when a
|
|
325
|
-
project copy shadows it).
|
|
336
|
+
project copy shadows it). `AGENTS_PYLIB` (`_env.PYLIB_VAR`): the `.pyz` when
|
|
337
|
+
dotagents runs from one (`_resources.pyz_archive()`), else not set — where its
|
|
338
|
+
own libraries (duho, pathlib_next) import from by zipimport for an overlay
|
|
339
|
+
built on them. Never added to `PYTHONPATH`: a consumer appends it to
|
|
340
|
+
`sys.path` when its import fails.
|
|
326
341
|
- `PATH`: every level's `bin` (contract A, not project-root, missing dirs included)
|
|
327
342
|
at the front in that order, whatever the caller's `PATH` held; empty and
|
|
328
343
|
cwd-relative inherited entries are dropped.
|
|
@@ -631,6 +646,8 @@ Read:
|
|
|
631
646
|
- `AGENTS_HARNESS` — the running harness (registry name or harness id), for
|
|
632
647
|
identity. `AGENTS_PYTHON` — the interpreter for `env.py`. `AGENTS_PYTHONPATH` —
|
|
633
648
|
the overlay/store `lib` dirs `env` put on `PYTHONPATH`, highest precedence first.
|
|
649
|
+
`AGENTS_PYLIB` — the `.pyz` dotagents runs from (duho and pathlib_next import
|
|
650
|
+
from it), unset for an installed dotagents.
|
|
634
651
|
- Harness config dirs: `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `PI_CODING_AGENT_DIR`.
|
|
635
652
|
Harness markers: `CLAUDECODE`, `CLAUDE_CODE_ENTRYPOINT`, `GEMINI_CLI`,
|
|
636
653
|
`CODEX_SANDBOX*`, `CURSOR_AGENT`, `PI_CODING_AGENT`. Model vars: `ANTHROPIC_MODEL`,
|
|
@@ -644,7 +661,7 @@ Read:
|
|
|
644
661
|
|
|
645
662
|
Emitted by `env` (and applied by `launch`): `AGENTS_HARNESS`, `AGENTS_VENDOR`,
|
|
646
663
|
`AGENT`, `AGENTS_MODEL`, `AGENTS_HOME`, `AGENTS_PROJECT_ROOT`, `AGENTS_PYTHON`,
|
|
647
|
-
`<NAME>_OVERLAY_ROOT`, `PATH`, `AGENTS_PYTHONPATH`, `AGENTS_PROXY` and the mirrored
|
|
664
|
+
`AGENTS_PYLIB` (from a `.pyz` only), `<NAME>_OVERLAY_ROOT`, `PATH`, `AGENTS_PYTHONPATH`, `AGENTS_PROXY` and the mirrored
|
|
648
665
|
proxy vars, plus whatever the env files set. `AGENTS_AGENT` is never emitted.
|
|
649
666
|
`launch` also exports `AGENTS_CONTEXT_FILE`.
|
|
650
667
|
|
|
@@ -500,14 +500,21 @@ class ClaudeAgent(Agent):
|
|
|
500
500
|
# SessionStart does two things, in one `type: command` hook (SessionStart
|
|
501
501
|
# supports only command/mcp_tool hooks, and env must run before context):
|
|
502
502
|
#
|
|
503
|
-
# 1. `dotagents env --diff` output
|
|
503
|
+
# 1. `dotagents env --diff` output goes into $CLAUDE_ENV_FILE
|
|
504
504
|
# (code.claude.com/docs/en/env-vars + hooks.md "Persist environment
|
|
505
505
|
# variables"): Claude sources that file before each Bash command. It is
|
|
506
506
|
# set only inside SessionStart/Setup/CwdChanged/FileChanged hook
|
|
507
507
|
# processes, never in the session's own environment.
|
|
508
|
-
# *
|
|
508
|
+
# * `--into`, not `>>`: the block a previous run wrote is REPLACED
|
|
509
|
+
# and anything else in the file (another hook's lines) is kept.
|
|
510
|
+
# SessionStart re-runs on resume, compact and clear; appending
|
|
511
|
+
# grew the file by the whole env each time, and Claude Code inlines
|
|
512
|
+
# it into every Bash command's `bash -c` argument, which Git for
|
|
513
|
+
# Windows' bash truncates at about 8 KB -- every Bash call then
|
|
514
|
+
# failed with an unterminated quote. PATH is written relative to
|
|
515
|
+
# the shell's own ($PATH), which keeps the block small.
|
|
509
516
|
# * Guard on non-empty (`[ -n "$CLAUDE_ENV_FILE" ]`) -- when unset,
|
|
510
|
-
#
|
|
517
|
+
# `--into ""` has nowhere to write.
|
|
511
518
|
#
|
|
512
519
|
# 2. `dotagents context`: Claude injects a SessionStart hook's stdout into
|
|
513
520
|
# the session context.
|
|
@@ -534,7 +541,7 @@ class ClaudeAgent(Agent):
|
|
|
534
541
|
SESSION_START_COMMAND = (
|
|
535
542
|
_HOOK_DOTAGENTS
|
|
536
543
|
+ 'if [ -n "$CLAUDE_ENV_FILE" ]; then '
|
|
537
|
-
'"$d" env --diff --format export
|
|
544
|
+
'"$d" env --diff --format export --into "$CLAUDE_ENV_FILE"; '
|
|
538
545
|
"fi; "
|
|
539
546
|
'"$d" context'
|
|
540
547
|
)
|
|
@@ -59,7 +59,10 @@ The identity/proxy model is wired into the output around the file chain:
|
|
|
59
59
|
the same set :func:`get_overlay_roots` lists, named by
|
|
60
60
|
:attr:`_overlays.Overlay.root_var` --
|
|
61
61
|
the same name ``dotagents context`` expands as a placeholder), so an env
|
|
62
|
-
file can reference an overlay's install dir by name.
|
|
62
|
+
file can reference an overlay's install dir by name. ``AGENTS_PYLIB``
|
|
63
|
+
(:data:`PYLIB_VAR`) names the ``.pyz`` when dotagents runs from one --
|
|
64
|
+
where its own libraries (duho, pathlib_next) import from for an overlay
|
|
65
|
+
built on them; never put on ``PYTHONPATH``.
|
|
63
66
|
* **Proxy** is normalized AFTER the file chain: ``AGENTS_PROXY`` is seeded if
|
|
64
67
|
unset (from ``AGENTS_WEBFETCH_PROXY_URL`` else the global
|
|
65
68
|
HTTPS/HTTP/ALL_PROXY, either case); any proxy var that *already exists* is
|
|
@@ -94,6 +97,11 @@ from typing import Iterable, Optional
|
|
|
94
97
|
|
|
95
98
|
from dotagents._scope import Scope, project_root_default
|
|
96
99
|
|
|
100
|
+
#: Names the ``.pyz`` dotagents runs from (unset for an installed dotagents,
|
|
101
|
+
#: whose libraries are in ``AGENTS_PYTHON``'s site-packages): an overlay built
|
|
102
|
+
#: on duho appends it to ``sys.path`` when ``import duho`` fails.
|
|
103
|
+
PYLIB_VAR = "AGENTS_PYLIB"
|
|
104
|
+
|
|
97
105
|
|
|
98
106
|
class EnvChanges(dict):
|
|
99
107
|
"""A change set: the vars set (``dict[str, str]``, as before) plus
|
|
@@ -1047,6 +1055,17 @@ def get_environment(
|
|
|
1047
1055
|
# `sys.executable` is '' or None under an embedding host: then nothing is
|
|
1048
1056
|
# exported rather than an empty command name.
|
|
1049
1057
|
_seed("AGENTS_PYTHON", sys.executable)
|
|
1058
|
+
# --- AGENTS_PYLIB --- where dotagents' own libraries (duho, pathlib_next)
|
|
1059
|
+
# import from for an overlay that builds on them. Set only when dotagents
|
|
1060
|
+
# runs from a .pyz: the archive, importable by zipimport. An installed
|
|
1061
|
+
# dotagents has them in AGENTS_PYTHON's site-packages already. Never put on
|
|
1062
|
+
# PYTHONPATH, which would shadow a project's own copy: a consumer appends
|
|
1063
|
+
# it to sys.path when its import fails.
|
|
1064
|
+
from dotagents._resources import pyz_archive
|
|
1065
|
+
|
|
1066
|
+
pylib = pyz_archive()
|
|
1067
|
+
if pylib and osenv.get(PYLIB_VAR) != pylib:
|
|
1068
|
+
_apply({PYLIB_VAR: pylib})
|
|
1050
1069
|
|
|
1051
1070
|
# --- Overlay roots --- one `<NAME>_OVERLAY_ROOT` per installed overlay, the
|
|
1052
1071
|
# same name `dotagents context` expands as a `<NAME_OVERLAY_ROOT>` placeholder
|
|
@@ -1208,10 +1227,11 @@ def env_cache_key(scope: Scope, base_env: "dict[str, str]", *parts: str) -> str:
|
|
|
1208
1227
|
import hashlib
|
|
1209
1228
|
|
|
1210
1229
|
from dotagents import __version__
|
|
1230
|
+
from dotagents._resources import pyz_archive
|
|
1211
1231
|
|
|
1212
1232
|
scope = _absolute_scope(scope)
|
|
1213
1233
|
items = [
|
|
1214
|
-
__version__, sys.executable or "", os.getcwd(), scope.level,
|
|
1234
|
+
__version__, sys.executable or "", pyz_archive() or "", os.getcwd(), scope.level,
|
|
1215
1235
|
str(scope.user_root), str(scope.agents_root), str(scope.project_root),
|
|
1216
1236
|
str(scope.system_root), *parts,
|
|
1217
1237
|
]
|
|
@@ -37,6 +37,18 @@ def _scratch_dir() -> Path:
|
|
|
37
37
|
return _scratch
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def pyz_archive() -> "str | None":
|
|
41
|
+
"""The ``.pyz`` this dotagents is imported from, or None for a plain
|
|
42
|
+
install. Inside a zipapp a module's ``__file__`` is a path INTO the
|
|
43
|
+
archive (``.../dotagents.pyz/dotagents/_resources.py``), so the archive
|
|
44
|
+
is the ancestor that is a file. It also holds the vendored runtime
|
|
45
|
+
dependencies (duho, pathlib_next), importable from it by zipimport."""
|
|
46
|
+
for parent in Path(__file__).parents:
|
|
47
|
+
if parent.is_file():
|
|
48
|
+
return str(parent)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
40
52
|
def _package_data_dir(name: str) -> "Path | None":
|
|
41
53
|
"""Resolve a directory under the installed `dotagents` package (e.g.
|
|
42
54
|
`_overlay`) to a real filesystem Path, working whether the package is a
|
|
@@ -232,10 +232,63 @@ def _looks_like_posix_path_list(key: str, value: str) -> bool:
|
|
|
232
232
|
return any(_looks_like_posix_chunk(chunk) for chunk in value.split(";") if chunk)
|
|
233
233
|
|
|
234
234
|
|
|
235
|
+
#: The lines around the block `env --into` owns in a file. No-op COMMANDS,
|
|
236
|
+
#: not `#` comments: a harness that inlines the file and appends `; cmd` or
|
|
237
|
+
#: `&& cmd` to its last line would otherwise comment the command out.
|
|
238
|
+
INTO_BEGIN = ": dotagents-env-begin"
|
|
239
|
+
INTO_END = ": dotagents-env-end"
|
|
240
|
+
#: The formats `--into` writes: a POSIX shell's, where `:` is the no-op.
|
|
241
|
+
INTO_FORMATS = ("export",)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def write_into(path: Path, text: str, output_format: str) -> None:
|
|
245
|
+
"""Put ``text`` into ``path`` as the ONE block ``env --into`` owns there:
|
|
246
|
+
every earlier block (between :data:`INTO_BEGIN` / :data:`INTO_END`) is
|
|
247
|
+
removed, anything else in the file -- another hook's lines -- is kept in
|
|
248
|
+
place, and the new block goes at the end, where a sourced file's later
|
|
249
|
+
lines win (what an append did). Atomic, LF-only. A hook that re-runs
|
|
250
|
+
(resume, compact, clear) used to append its whole output each time, and
|
|
251
|
+
the file Claude Code inlines into every Bash command grew past what Git
|
|
252
|
+
for Windows' bash accepts as a ``-c`` argument (about 8 KB)."""
|
|
253
|
+
from dotagents._env import FORMAT_ALIASES
|
|
254
|
+
from dotagents._fs import write_text_lf
|
|
255
|
+
|
|
256
|
+
fmt = FORMAT_ALIASES.get(output_format, output_format)
|
|
257
|
+
if fmt not in INTO_FORMATS:
|
|
258
|
+
raise SystemExit("error: --into writes the %s format only, not %r" % ("/".join(INTO_FORMATS), fmt))
|
|
259
|
+
kept: "list[str]" = []
|
|
260
|
+
inside = False
|
|
261
|
+
try:
|
|
262
|
+
current = path.read_text(encoding="utf-8")
|
|
263
|
+
except FileNotFoundError:
|
|
264
|
+
current = ""
|
|
265
|
+
for line in current.splitlines():
|
|
266
|
+
if line.strip() == INTO_BEGIN:
|
|
267
|
+
inside = True
|
|
268
|
+
elif line.strip() == INTO_END and inside:
|
|
269
|
+
inside = False
|
|
270
|
+
elif not inside:
|
|
271
|
+
kept.append(line)
|
|
272
|
+
block = [INTO_BEGIN] + [ln for ln in text.splitlines() if ln] + [INTO_END]
|
|
273
|
+
write_text_lf(path, "\n".join(kept + block) + "\n", atomic=True)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _prefixed_path_line(new: str, old: str, sep: str) -> "Optional[str]":
|
|
277
|
+
"""``export PATH='<prefix>'"${PATH:+:$PATH}"`` when ``new`` is ``old``
|
|
278
|
+
with entries put in front, else ``None``. The sourcing shell's own PATH
|
|
279
|
+
completes it, so the line stays short however long PATH is (the env file
|
|
280
|
+
is inlined into every Bash command) and keeps what the shell adds."""
|
|
281
|
+
if not old or not new.endswith(sep + old) or len(new) == len(old) + 1:
|
|
282
|
+
return None
|
|
283
|
+
prefix = new[: -(len(old) + 1)]
|
|
284
|
+
return 'export PATH=%s"${PATH:+%s$PATH}"' % (_sh_quote(prefix), sep)
|
|
285
|
+
|
|
286
|
+
|
|
235
287
|
def _format_env(
|
|
236
288
|
env: "dict[str, str]",
|
|
237
289
|
output_format: str,
|
|
238
290
|
removed: "Optional[Iterable[str]]" = None,
|
|
291
|
+
base: "Optional[dict[str, str]]" = None,
|
|
239
292
|
) -> str:
|
|
240
293
|
"""Render the assembled env in the requested (canonical or aliased) format.
|
|
241
294
|
|
|
@@ -369,9 +422,17 @@ def _format_env(
|
|
|
369
422
|
for k in keys]
|
|
370
423
|
+ ["set -e %s" % k for k in gone if _POSIX_IDENTIFIER_RE.match(k)]
|
|
371
424
|
)
|
|
372
|
-
# default / "export"
|
|
425
|
+
# default / "export". With the caller's env (`--diff`), a PATH that is the
|
|
426
|
+
# caller's own with entries in front is written relative to the sourcing
|
|
427
|
+
# shell's $PATH rather than in full (see `_prefixed_path_line`).
|
|
428
|
+
path_line = None
|
|
429
|
+
if base is not None and "PATH" in env:
|
|
430
|
+
old = base.get("PATH") or ""
|
|
431
|
+
if windows_host and _looks_like_path_list("PATH", old):
|
|
432
|
+
old = _to_posix_path_list(old)
|
|
433
|
+
path_line = _prefixed_path_line(env["PATH"], old, ":")
|
|
373
434
|
return "\n".join(
|
|
374
|
-
["export %s=%s" % (k, _sh_quote(env[k])) for k in keys]
|
|
435
|
+
[path_line if k == "PATH" and path_line else "export %s=%s" % (k, _sh_quote(env[k])) for k in keys]
|
|
375
436
|
+ ["unset %s" % k for k in gone if _POSIX_IDENTIFIER_RE.match(k)]
|
|
376
437
|
)
|
|
377
438
|
|
|
@@ -568,6 +629,14 @@ class Env(DotAgentsArgs):
|
|
|
568
629
|
)
|
|
569
630
|
("--cache",)
|
|
570
631
|
|
|
632
|
+
into: "Optional[Path]" = None
|
|
633
|
+
(
|
|
634
|
+
"Write into this file instead of stdout (a hook's $CLAUDE_ENV_FILE), "
|
|
635
|
+
"replacing the block an earlier run wrote there and keeping the rest: "
|
|
636
|
+
"the file holds one current block however often the hook runs."
|
|
637
|
+
)
|
|
638
|
+
("--into",)
|
|
639
|
+
|
|
571
640
|
# Both flags come from `DotAgentsArgs`; only their HELP is restated here
|
|
572
641
|
# (same flags, defaults and types), because this command's `-g` is narrower
|
|
573
642
|
# than the base's and its store is always the user store.
|
|
@@ -631,8 +700,13 @@ class Env(DotAgentsArgs):
|
|
|
631
700
|
|
|
632
701
|
# UTF-8 straight to the buffer: a bare print() encodes with the console
|
|
633
702
|
# codepage and dies on the first non-Latin-1 character in any value.
|
|
634
|
-
|
|
703
|
+
# With --diff, PATH may be written relative to the caller's own PATH
|
|
704
|
+
# (see `_format_env`): the base is what the diff was taken against.
|
|
705
|
+
text = _format_env(env, output_format, removed, base=base if self.diff else None) + "\n"
|
|
635
706
|
if key is not None:
|
|
636
707
|
_env.write_env_cache(scope, key, text)
|
|
708
|
+
if self.into is not None:
|
|
709
|
+
write_into(Path(self.into), text, output_format)
|
|
710
|
+
return 0
|
|
637
711
|
_write_stdout(text)
|
|
638
712
|
return 0
|
|
@@ -489,6 +489,43 @@ def test_agents_python_is_not_exported_when_the_interpreter_is_unknown(tree, mon
|
|
|
489
489
|
assert "AGENTS_PYTHON" not in env
|
|
490
490
|
|
|
491
491
|
|
|
492
|
+
def test_agents_pylib_names_the_pyz_only_when_running_from_one(tree, tmp_path, monkeypatch):
|
|
493
|
+
"""An installed dotagents has its libraries in AGENTS_PYTHON's
|
|
494
|
+
site-packages: nothing to export. From a .pyz the archive is where they
|
|
495
|
+
import from; the walk's value replaces another, and it never reaches
|
|
496
|
+
PYTHONPATH (a project's own duho must not be shadowed)."""
|
|
497
|
+
from dotagents import _resources
|
|
498
|
+
|
|
499
|
+
agents_dir, project_root = tree
|
|
500
|
+
assert _resources.pyz_archive() is None, "the suite runs from the source tree"
|
|
501
|
+
env = _run(agents_dir, project_root, {"PATH": "/usr/bin"})
|
|
502
|
+
assert _env.PYLIB_VAR not in env
|
|
503
|
+
pyz = tmp_path / "dotagents.pyz"
|
|
504
|
+
pyz.write_bytes(b"PK")
|
|
505
|
+
monkeypatch.setattr(_resources, "pyz_archive", lambda: str(pyz))
|
|
506
|
+
env = _run(agents_dir, project_root, {"PATH": "/usr/bin"})
|
|
507
|
+
assert env[_env.PYLIB_VAR] == str(pyz)
|
|
508
|
+
assert str(pyz) not in env.get("PYTHONPATH", "")
|
|
509
|
+
env = _run(agents_dir, project_root, {"PATH": "/usr/bin", _env.PYLIB_VAR: str(pyz)})
|
|
510
|
+
assert _env.PYLIB_VAR not in env, "already right: no change"
|
|
511
|
+
env = _run(agents_dir, project_root, {"PATH": "/usr/bin", _env.PYLIB_VAR: "/old/dotagents.pyz"})
|
|
512
|
+
assert env[_env.PYLIB_VAR] == str(pyz), "another archive's value is replaced"
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def test_pyz_archive_finds_the_archive_a_module_is_imported_from(tmp_path):
|
|
516
|
+
"""Run from a real zipapp: the archive is the ancestor of __file__ that is a file."""
|
|
517
|
+
import subprocess
|
|
518
|
+
import zipfile
|
|
519
|
+
|
|
520
|
+
src = Path(_env.__file__).resolve().parents[1]
|
|
521
|
+
pyz = tmp_path / "t.pyz"
|
|
522
|
+
with zipfile.ZipFile(str(pyz), "w") as zf:
|
|
523
|
+
zf.write(str(src / "dotagents" / "_resources.py"), "probe_resources.py")
|
|
524
|
+
code = "import sys; sys.path.insert(0, sys.argv[1]); import probe_resources as r; print(r.pyz_archive())"
|
|
525
|
+
out = subprocess.run([sys.executable, "-c", code, str(pyz)], capture_output=True, text=True, check=True)
|
|
526
|
+
assert out.stdout.strip() == str(pyz)
|
|
527
|
+
|
|
528
|
+
|
|
492
529
|
def test_env_py_runs_under_a_pinned_agents_python(tree, tmp_path, monkeypatch):
|
|
493
530
|
"""The pin `env` exports for shims is honoured for dotagents' own
|
|
494
531
|
subprocesses too (only when it names an existing file)."""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""`env --into FILE` keeps ONE dotagents block in a hook's env file however
|
|
2
|
+
often the hook runs, leaves other hooks' lines alone, and `--diff --format
|
|
3
|
+
export` writes PATH relative to the sourcing shell's own -- so the file
|
|
4
|
+
Claude Code inlines into every Bash command stays small. (A SessionStart hook
|
|
5
|
+
that appended on every resume/compact grew it past Git for Windows' ~8 KB
|
|
6
|
+
`bash -c` limit, and every Bash call failed.)"""
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from dotagents.cli.env import INTO_BEGIN, INTO_END, _format_env, _prefixed_path_line, write_into
|
|
13
|
+
from _shell import real_bash
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_write_into_replaces_only_its_own_block(tmp_path):
|
|
17
|
+
env_file = tmp_path / "hook.sh"
|
|
18
|
+
env_file.write_text("export OTHER_HOOK='kept'\n", encoding="utf-8")
|
|
19
|
+
for value in ("one", "two", "three"):
|
|
20
|
+
write_into(env_file, "export A='%s'\nexport B='x'\n" % value, "export")
|
|
21
|
+
text = env_file.read_text(encoding="utf-8")
|
|
22
|
+
assert text.count(INTO_BEGIN) == 1 and text.count(INTO_END) == 1, "one block, however often"
|
|
23
|
+
assert "export OTHER_HOOK='kept'" in text, "another hook's line stays"
|
|
24
|
+
assert "export A='three'" in text and "one" not in text and "two" not in text
|
|
25
|
+
assert text.index("OTHER_HOOK") < text.index(INTO_BEGIN), "ours last: it wins, as an append did"
|
|
26
|
+
assert b"\r" not in env_file.read_bytes()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_write_into_creates_the_file_and_refuses_formats_without_comments(tmp_path):
|
|
30
|
+
env_file = tmp_path / "new.sh"
|
|
31
|
+
write_into(env_file, "export A='1'\n", "export")
|
|
32
|
+
assert env_file.read_text(encoding="utf-8") == "%s\nexport A='1'\n%s\n" % (INTO_BEGIN, INTO_END)
|
|
33
|
+
with pytest.raises(SystemExit):
|
|
34
|
+
write_into(tmp_path / "x.cmd", 'set "A=1"\n', "cmd")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_a_command_appended_to_the_block_still_runs(tmp_path):
|
|
38
|
+
"""The markers are no-op COMMANDS: a harness that inlines the file and
|
|
39
|
+
joins the command onto its last line (`; cmd`, `&& cmd`) must not have it
|
|
40
|
+
commented out."""
|
|
41
|
+
bash = real_bash()
|
|
42
|
+
if not bash:
|
|
43
|
+
pytest.skip("no bash")
|
|
44
|
+
env_file = tmp_path / "hook.sh"
|
|
45
|
+
write_into(env_file, "export A='1'\n", "export")
|
|
46
|
+
body = env_file.read_text(encoding="utf-8").rstrip("\n")
|
|
47
|
+
for joiner in ("; ", " && ", "\n"):
|
|
48
|
+
out = subprocess.run([bash, "-c", body + joiner + 'printf "%s" "$A"'], capture_output=True, text=True)
|
|
49
|
+
assert out.stdout == "1", (joiner, out.stderr)
|
|
50
|
+
with pytest.raises(SystemExit):
|
|
51
|
+
write_into(tmp_path / "x.env", "A=1\n", "dotenv")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_prefixed_path_line():
|
|
55
|
+
assert _prefixed_path_line("/a:/b:/usr/bin", "/usr/bin", ":") == 'export PATH=\'/a:/b\'"${PATH:+:$PATH}"'
|
|
56
|
+
assert _prefixed_path_line("/usr/bin", "/usr/bin", ":") is None, "nothing in front"
|
|
57
|
+
assert _prefixed_path_line("/a:/usr/local/bin", "/usr/bin", ":") is None, "not a prefix: full value"
|
|
58
|
+
assert _prefixed_path_line("/a", "", ":") is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.skipif(os.name == "nt", reason="a POSIX PATH; the Windows conversion is covered below")
|
|
62
|
+
def test_diff_export_writes_path_as_a_prefix_on_posix():
|
|
63
|
+
out = _format_env({"PATH": "/opt/x/bin:/usr/bin:/bin", "A": "1"}, "export", base={"PATH": "/usr/bin:/bin"})
|
|
64
|
+
assert "export PATH='/opt/x/bin'\"${PATH:+:$PATH}\"" in out
|
|
65
|
+
assert _format_env({"PATH": "/opt/x/bin:/usr/bin"}, "export") == "export PATH='/opt/x/bin:/usr/bin'", "no base: full"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_diff_export_prefix_survives_the_windows_conversion(monkeypatch):
|
|
69
|
+
from dotagents.cli import env as env_cli
|
|
70
|
+
|
|
71
|
+
monkeypatch.setattr(env_cli, "_host_is_windows", lambda: True)
|
|
72
|
+
base = {"PATH": r"C:\Windows\System32;C:\Windows"}
|
|
73
|
+
new = {"PATH": r"C:\Tools\dotagents\bin;C:\Windows\System32;C:\Windows"}
|
|
74
|
+
out = _format_env(new, "export", base=base)
|
|
75
|
+
assert out == "export PATH='/c/Tools/dotagents/bin'\"${PATH:+:$PATH}\""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_a_sourced_block_really_prepends(tmp_path):
|
|
79
|
+
"""Source the rendered line in a real POSIX shell: the prefix lands in
|
|
80
|
+
front of that shell's own PATH."""
|
|
81
|
+
bash = real_bash()
|
|
82
|
+
if not bash:
|
|
83
|
+
pytest.skip("no bash")
|
|
84
|
+
line = _prefixed_path_line("/opt/dotagents/bin:/usr/bin", "/usr/bin", ":")
|
|
85
|
+
script = tmp_path / "env.sh"
|
|
86
|
+
script.write_text(line + "\n", encoding="utf-8")
|
|
87
|
+
out = subprocess.run([bash, "-c", "PATH=/shell/own; . ./env.sh; printf %s \"$PATH\""],
|
|
88
|
+
cwd=str(tmp_path), capture_output=True, text=True)
|
|
89
|
+
assert out.stdout == "/opt/dotagents/bin:/shell/own", out.stderr
|
|
@@ -110,15 +110,18 @@ def test_hook_finds_dotagents_by_path_not_by_splicing_path():
|
|
|
110
110
|
assert "PATH=" not in cmd
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
def
|
|
114
|
-
"""
|
|
115
|
-
|
|
116
|
-
`>` would discard variables other hooks wrote to the same file;
|
|
117
|
-
|
|
113
|
+
def test_env_hook_replaces_its_own_block_and_is_guarded():
|
|
114
|
+
"""Three requirements, all load-bearing.
|
|
115
|
+
|
|
116
|
+
`>` would discard variables other hooks wrote to the same file; `>>`
|
|
117
|
+
grew it by the whole env on every SessionStart (resume, compact, clear)
|
|
118
|
+
until the file Claude Code inlines into each Bash command passed Git for
|
|
119
|
+
Windows' ~8 KB `bash -c` limit -- so `env --into` replaces only its own
|
|
120
|
+
block. An unguarded write with CLAUDE_ENV_FILE unset has nowhere to go.
|
|
118
121
|
"""
|
|
119
122
|
cmd = ClaudeAgent.SESSION_START_COMMAND
|
|
120
|
-
assert '
|
|
121
|
-
assert ">>" in cmd and not _has_truncating_redirect(cmd)
|
|
123
|
+
assert 'env --diff --format export --into "$CLAUDE_ENV_FILE"' in cmd
|
|
124
|
+
assert ">>" not in cmd and not _has_truncating_redirect(cmd), "no shell redirect into the env file"
|
|
122
125
|
assert '[ -n "$CLAUDE_ENV_FILE" ]' in cmd, "must guard against an unset var"
|
|
123
126
|
assert cmd.index('"$d" env') < cmd.index('"$d" context'), (
|
|
124
127
|
"env must be written before context runs"
|
|
@@ -181,6 +181,13 @@ has "$(pinned_env --format powershell)" "\${env:FROM_PINNED_STORE} = 'yes'"
|
|
|
181
181
|
has "$(pinned_env --format cmd)" 'set "FROM_PINNED_STORE=yes"'
|
|
182
182
|
has_re "$(pinned_env --format dotenv)" "^FROM_PINNED_STORE=yes$"
|
|
183
183
|
has "$(pinned_env)" "FROM_PINNED_STORE"
|
|
184
|
+
# Run from a .pyz, env names it in AGENTS_PYLIB, and the vendored duho imports
|
|
185
|
+
# from it by zipimport: what an overlay built on duho (the net overlay's curl
|
|
186
|
+
# fallback) relies on when AGENTS_PYTHON has no duho of its own.
|
|
187
|
+
has_re "$(pyz env --diff --format dotenv -g)" "^AGENTS_PYLIB=.*\.pyz"
|
|
188
|
+
# -S -E: no site-packages, no PYTHONPATH -- a runner that has duho installed
|
|
189
|
+
# must still prove the archive alone provides it.
|
|
190
|
+
"$PYTHON" -S -E -c "import sys; sys.path.insert(0, sys.argv[1]); import duho; assert '.pyz' in duho.__file__, duho.__file__" "$(native "$PYZ")"
|
|
184
191
|
|
|
185
192
|
# overlays add: positional and aliased flags.
|
|
186
193
|
h=$(pyz overlays add --help)
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/cmds/findings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/templates/AGENTS.md
RENAMED
|
File without changes
|
{dotagents_cli-0.6.2 → dotagents_cli-0.6.3}/src/dotagents/_overlay/dotagents/templates/PROJECT.md
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|