treebox 0.6.0__tar.gz → 0.7.0__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.
- {treebox-0.6.0 → treebox-0.7.0}/.agents/skills/treebox/SKILL.md +2 -2
- {treebox-0.6.0 → treebox-0.7.0}/.github/workflows/ci.yml +3 -2
- {treebox-0.6.0 → treebox-0.7.0}/CLAUDE.md +105 -6
- {treebox-0.6.0 → treebox-0.7.0}/CONTRIBUTING.md +17 -0
- {treebox-0.6.0 → treebox-0.7.0}/PKG-INFO +6 -5
- {treebox-0.6.0 → treebox-0.7.0}/README.md +5 -4
- {treebox-0.6.0 → treebox-0.7.0}/ROADMAP.md +2 -2
- {treebox-0.6.0 → treebox-0.7.0}/docs/configuration.md +11 -3
- {treebox-0.6.0 → treebox-0.7.0}/docs/how-it-works.md +18 -2
- {treebox-0.6.0 → treebox-0.7.0}/docs/index.md +10 -7
- {treebox-0.6.0 → treebox-0.7.0}/docs/install.md +18 -13
- {treebox-0.6.0 → treebox-0.7.0}/docs/usage.md +6 -1
- treebox-0.7.0/scripts/golden-diff.sh +168 -0
- {treebox-0.6.0 → treebox-0.7.0}/scripts/validate.sh +6 -2
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/cli.py +168 -155
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/config.py +12 -8
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/git.py +4 -2
- treebox-0.7.0/src/treebox/harnesses.py +140 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/models.py +8 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/output.py +13 -10
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/provision.py +19 -11
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/resolve.py +2 -2
- treebox-0.7.0/src/treebox/runners/__init__.py +60 -0
- treebox-0.7.0/src/treebox/runners/base.py +111 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/runners/docker.py +146 -129
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/runners/host.py +28 -25
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/state.py +3 -1
- treebox-0.7.0/src/treebox/system.py +29 -0
- treebox-0.7.0/tests/golden/README.md +19 -0
- treebox-0.7.0/tests/golden/create-docker-json.txt +24 -0
- treebox-0.7.0/tests/golden/create-docker-print.txt +22 -0
- treebox-0.7.0/tests/golden/create-dryrun-docker-json.txt +22 -0
- treebox-0.7.0/tests/golden/create-dryrun-docker.txt +21 -0
- treebox-0.7.0/tests/golden/create-dryrun-host-json.txt +18 -0
- treebox-0.7.0/tests/golden/create-dryrun-host.txt +17 -0
- treebox-0.7.0/tests/golden/create-host-json.txt +17 -0
- treebox-0.7.0/tests/golden/create-host-print.txt +17 -0
- treebox-0.7.0/tests/golden/doctor-docker-json.txt +52 -0
- treebox-0.7.0/tests/golden/doctor-docker.txt +19 -0
- treebox-0.7.0/tests/golden/doctor-host-json.txt +52 -0
- treebox-0.7.0/tests/golden/doctor-host.txt +18 -0
- treebox-0.7.0/tests/golden/doctor-no-login.txt +18 -0
- treebox-0.7.0/tests/golden/enter-docker-json.txt +24 -0
- treebox-0.7.0/tests/golden/enter-docker-print.txt +14 -0
- treebox-0.7.0/tests/golden/enter-host-json.txt +17 -0
- treebox-0.7.0/tests/golden/enter-host-print.txt +12 -0
- {treebox-0.6.0 → treebox-0.7.0}/tests/test_integration.py +13 -7
- {treebox-0.6.0 → treebox-0.7.0}/tests/test_units.py +286 -170
- treebox-0.6.0/src/treebox/runners/__init__.py +0 -19
- treebox-0.6.0/src/treebox/runners/base.py +0 -81
- treebox-0.6.0/src/treebox/system.py +0 -44
- {treebox-0.6.0 → treebox-0.7.0}/.github/workflows/autofix.yml +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/.github/workflows/claude.yml +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/.github/workflows/docs.yml +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/.github/workflows/release.yml +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/.gitignore +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/.pre-commit-config.yaml +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/AGENTS.md +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/CHANGELOG.md +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/LICENSE +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/assets/treebox-demo.gif +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/assets/treebox-logo.png +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/docs/agents.md +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/docs/assets/treebox-logo.png +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/docs/javascripts/treebox.js +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/docs/stylesheets/extra.css +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/hooks/copy_page.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/install.sh +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/mkdocs.yml +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/pyproject.toml +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/__init__.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/assets/container/Dockerfile +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/assets/container/allowed-domains.sh +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/assets/container/container.json +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/assets/container/firewall.json +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/assets/container/init-firewall.sh +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/assets/container/post-create.sh +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/assets/pre-push +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/assets.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/ecosystems.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/forge.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/locking.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/names.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/py.typed +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/src/treebox/status.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/tests/conftest.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/tests/test_contract.py +0 -0
- {treebox-0.6.0 → treebox-0.7.0}/uv.lock +0 -0
|
@@ -26,7 +26,7 @@ Provisioning (fetch → resolve branch → worktree add → copy submodules →
|
|
|
26
26
|
## Command reference
|
|
27
27
|
|
|
28
28
|
- `create [NAME]` — provision and launch. Flags: `--base <branch>` `--checkout <existing-branch>` `--isolation host|docker` `--harness claude|codex` `--template <name>` `--cold` `--no-fetch` `--firewall/--no-firewall` `--dry-run` `--print` `--json` `--repo <path>` `--root <dir>` `--quiet` `--verbose`.
|
|
29
|
-
- `enter REF [-- AGENT_ARGS…]` — re-launch in an existing worktree; refreshes `.env`, re-syncs deps only if the lockfile changed. Flags: `--isolation` `--harness` `--template <name>` `--cold` `--print` `--json` `--repo` `--root` `--quiet` `--verbose`.
|
|
29
|
+
- `enter REF [-- AGENT_ARGS…]` — re-launch in an existing worktree; refreshes `.env`, reuses recorded isolation/firewall/harness/template defaults, and re-syncs deps only if the lockfile changed. Flags: `--isolation` `--harness` `--template <name>` `--cold` `--print` `--json` `--repo` `--root` `--quiet` `--verbose`.
|
|
30
30
|
- `list` (alias `ls`) — table of worktrees (branch · isolation · deps freshness · `.env` presence · directory). Flags: `--repo` `--root` `--json`.
|
|
31
31
|
- `teardown [REF...]` (alias `rm`) — remove selected worktrees; caches are left intact. Flags: `--force` `--delete-branch` `--remove-volumes` `--skip-container` `--json` `--isolation` `--repo` `--root` `--quiet` `--verbose`.
|
|
32
32
|
- `doctor` — check git, login credentials, UID/GID, and isolation deps. Flags: `--isolation` `--repo` `--json`.
|
|
@@ -81,7 +81,7 @@ With `--json`, a failure prints **one object on stderr**: `{"schemaVersion":3,"e
|
|
|
81
81
|
- `create` launches the agent (Claude by default) after provisioning and exits with the agent process's exit code. Use `--harness codex`, or `--print` / `--json` / `--dry-run` to avoid launching.
|
|
82
82
|
- Re-running `create` on a fully provisioned same-name worktree is a `SLUG_CONFLICT` (exit `5`), not an idempotent enter. Use `enter` to re-launch. If the dir exists but setup never finished (a prior run died mid-provision), `create` resumes setup instead of launching into a half-built tree.
|
|
83
83
|
- **Freshness is enforced.** `create` *requires* a successful `git fetch origin` and branches from the freshly-fetched `origin/<base>` (preferred over a stale local branch). If the fetch fails it exits `4` and does **not** fall back to stale refs. In a terminal, git/ssh will prompt for credentials (no pre-loaded ssh-agent needed); headless/`--json` runs need working non-interactive auth. Pass `--no-fetch` only when the user explicitly accepts possibly-stale local refs (e.g. offline). A repo with no `origin` skips the fetch. Run `doctor` to check origin reachability up front.
|
|
84
|
-
- `enter` recomputes the dependency-manifest hash and re-syncs only when deps changed; it always refreshes `.env`. Pass extra agent args after `--`. `--cold` bypasses the shared cache.
|
|
84
|
+
- `enter` recomputes the dependency-manifest hash and re-syncs only when deps changed; it always refreshes `.env`. It reuses the worktree's recorded isolation/firewall/harness/template defaults, so config changes do not silently change an existing worktree. A mismatched explicit `--isolation` is a conflict; explicit `--harness` and `--template` are per-session overrides. Pass extra agent args after `--`. `--cold` bypasses the shared cache.
|
|
85
85
|
- `--cold` is the escape hatch for a corrupted cache: from-source dependency resolution.
|
|
86
86
|
- `teardown` confirms interactively. Without a TTY (scripts/agents) or with `--json` it refuses unless `--force` (exit `5`); `--force` is also required to remove a worktree with uncommitted changes. `--json` prints a record of what was removed to stdout. `--skip-container` leaves containers/images alone; cleanup failures are best-effort (`container: "failed"`) and do not undo worktree removal. With host isolation there are no containers/volumes to remove.
|
|
87
87
|
- Only one `create`/`enter`/`teardown` may operate on a given worktree name at a time (a per-name lock, exit `5` if held). Wait rather than retrying immediately.
|
|
@@ -50,7 +50,8 @@ jobs:
|
|
|
50
50
|
with:
|
|
51
51
|
enable-cache: true
|
|
52
52
|
|
|
53
|
-
# Full reproducible validation: lint,
|
|
54
|
-
#
|
|
53
|
+
# Full reproducible validation: lint, type checks, shell asset checks,
|
|
54
|
+
# tests, golden CLI snapshots, and a live `treebox create` against a
|
|
55
|
+
# throwaway local git remote.
|
|
55
56
|
- name: validate.sh
|
|
56
57
|
run: ./scripts/validate.sh
|
|
@@ -25,19 +25,34 @@ uv run --extra dev ruff check src tests # lint
|
|
|
25
25
|
uv run --extra dev ruff format --check src tests # format check (drop --check to apply)
|
|
26
26
|
uv run --extra dev mypy # strict type check (config in pyproject.toml)
|
|
27
27
|
uv run --extra dev pre-commit install # install lint/format/strict type-check hooks (see CONTRIBUTING.md)
|
|
28
|
-
|
|
28
|
+
scripts/golden-diff.sh # diff CLI output against tests/golden snapshots
|
|
29
|
+
./scripts/validate.sh # full gate: lint + typecheck + shell assets + tests + snapshots + smoke
|
|
29
30
|
uv pip install -e ".[dev]" # editable dev environment
|
|
30
31
|
uv run --extra docs mkdocs serve # docs site (docs/ + mkdocs.yml), live-reloading
|
|
31
32
|
uv run --extra docs mkdocs build --strict # build docs to site/ (gitignored)
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
Run `validate.sh` before changes that affect provisioning, runners, git
|
|
35
|
-
handling, or CLI output;
|
|
36
|
+
handling, shell assets, or CLI output; it includes the golden CLI-output
|
|
37
|
+
snapshots and shell asset checks. Use `scripts/golden-diff.sh --update` only
|
|
38
|
+
after an intentional output change has been agreed. `pytest` is enough for
|
|
39
|
+
smaller changes.
|
|
36
40
|
|
|
37
41
|
## Architecture
|
|
38
42
|
|
|
39
43
|
The whole tool is organized around one seam: **provision (always host-side) vs.
|
|
40
|
-
run (pluggable)
|
|
44
|
+
run (pluggable)** — with three registries that define everything swappable.
|
|
45
|
+
|
|
46
|
+
**The three-seam glossary** (each term is one axis, one module, one registry):
|
|
47
|
+
|
|
48
|
+
- **Harness** — *which agent CLI launches* (`claude`, `codex`).
|
|
49
|
+
`harnesses.py`: the `Harness` dataclass + `HARNESSES` registry.
|
|
50
|
+
- **Runner / isolation** — *where it executes* (`host`, `docker`).
|
|
51
|
+
`runners/`: the `Runner` protocol + `RUNNERS` registry.
|
|
52
|
+
- **Ecosystem** — *what setup runs* (uv, npm, pnpm, go, cargo).
|
|
53
|
+
`ecosystems.py`: the `Ecosystem` dataclass + `ECOSYSTEMS` registry.
|
|
54
|
+
|
|
55
|
+
Module map:
|
|
41
56
|
|
|
42
57
|
- **`provision.py`** owns the host-side half, identical for every runner:
|
|
43
58
|
`fetch → resolve branch → git worktree add → install pre-push guard
|
|
@@ -46,17 +61,32 @@ run (pluggable)**.
|
|
|
46
61
|
`treebox/<name>` placeholder made un-pushable by a per-worktree pre-push
|
|
47
62
|
hook (`extensions.worktreeConfig` + `core.hooksPath` into the private git
|
|
48
63
|
dir); `create --checkout <existing-branch>` is the only path that skips it.
|
|
64
|
+
- **`harnesses.py`** is the one place agent-CLI wiring lives: each `Harness`
|
|
65
|
+
hides its autonomous launch argv, host login dir, staged login files, and
|
|
66
|
+
login advice behind a small method interface; `VALID_HARNESSES` and the
|
|
67
|
+
doctor login rows derive from `HARNESSES`. Boundary values (CLI/TOML/state)
|
|
68
|
+
stay `str` and are resolved to the object once, in `cli.py`.
|
|
49
69
|
- **`runners/base.py`** defines the `Runner` protocol — the *only* thing that
|
|
50
70
|
differs between modes — implemented by **`runners/host.py`** (setup + agent in
|
|
51
71
|
the worktree shell) and **`runners/docker.py`** (plain `docker build/run`,
|
|
52
72
|
setup via a baked-in `post-create.sh`, agent via `docker exec`; the worktree
|
|
53
73
|
and its git common dir are bind-mounted at their host paths so in-container
|
|
54
|
-
git just works). `
|
|
55
|
-
|
|
74
|
+
git just works). `runners/__init__.py` holds the `RUNNERS` registry;
|
|
75
|
+
`get_runner` and `VALID_ISOLATION` derive from it. Doctor-facing vocabulary
|
|
76
|
+
(preflight detail, whether a login is a hard gate) lives in `RunnerFacts`,
|
|
77
|
+
not in the run methods; teardown options (docker's `remove_volumes`) arrive
|
|
78
|
+
at the runner's constructor, never through the protocol.
|
|
56
79
|
- **`cli.py`** (Typer) is the entry point: `create [NAME] / enter <ref> /
|
|
57
80
|
list / teardown <ref>... / doctor / version` (`ls`/`rm` are hidden aliases
|
|
58
81
|
of list/teardown). `enter`/`teardown` resolve a ref as
|
|
59
82
|
name → live branch → unique substring (`resolve.py`); ambiguity exits 2.
|
|
83
|
+
`create` and `enter` share `_run_session` for runner preflight, the
|
|
84
|
+
per-name lock, provision error classification, and the final
|
|
85
|
+
`--json`/`--print`/launch fork. `_reconcile_with_state` folds recorded
|
|
86
|
+
creation-time choices into existing-worktree sessions before the runner and
|
|
87
|
+
harness objects are resolved: recorded isolation conflicts with a mismatched
|
|
88
|
+
explicit `--isolation`, while recorded firewall/harness/template protect an
|
|
89
|
+
existing worktree from config-default drift.
|
|
60
90
|
It enforces **stable exit codes** (`0` ok · `1` runtime/doctor hard-check ·
|
|
61
91
|
`2` usage · `3` not-found · `4` auth/fetch · `5` conflict) and **`--json`**
|
|
62
92
|
output carrying a `schemaVersion` that only gains fields within a version —
|
|
@@ -68,7 +98,9 @@ run (pluggable)**.
|
|
|
68
98
|
- **`state.py`** stores per-worktree state (lockfile hash + provisioning
|
|
69
99
|
choices) inside the worktree's private git dir (`.git/worktrees/<id>/`), so it
|
|
70
100
|
never appears in `git status` and is pruned with the worktree. The lockfile
|
|
71
|
-
hash is what lets `enter` re-sync only when deps changed
|
|
101
|
+
hash is what lets `enter` re-sync only when deps changed; the recorded
|
|
102
|
+
choices are what let `enter`/`teardown` recover the worktree's created-time
|
|
103
|
+
isolation, firewall, harness, and template defaults.
|
|
72
104
|
- **`models.py`** holds the `Worktree` value object and the name-as-identity
|
|
73
105
|
rule: the *name* is the directory leaf and lock key, never renamed; the
|
|
74
106
|
*branch* is a mutable attribute read live from git (the agent renames it
|
|
@@ -84,6 +116,73 @@ run (pluggable)**.
|
|
|
84
116
|
and rendered into a host-side dir *beside* the worktree, never inside the
|
|
85
117
|
mount, so a boxed agent cannot edit the config that defines its own sandbox.
|
|
86
118
|
|
|
119
|
+
## Extending treebox
|
|
120
|
+
|
|
121
|
+
**Adding a harness** (a new agent CLI):
|
|
122
|
+
|
|
123
|
+
1. One `Harness` entry in `harnesses.py`'s `HARNESSES` registry — name,
|
|
124
|
+
autonomous argv, host login dir, staged login files. If the new CLI's
|
|
125
|
+
credentials are *behavior* rather than plain files (generated settings, a
|
|
126
|
+
non-file auth store), override the relevant methods instead of growing
|
|
127
|
+
branches in callers.
|
|
128
|
+
2. Add the harness name to `config.py`'s hard-coded `Harness` Literal alias.
|
|
129
|
+
The drift test `test_registry_vocabularies_cannot_drift` must keep passing:
|
|
130
|
+
the alias, `HARNESSES`, and `VALID_HARNESSES` must name the same set.
|
|
131
|
+
3. One operator-template stanza — a `Dockerfile` install line and a
|
|
132
|
+
config-dir env line in `container.json`. This half is deliberately
|
|
133
|
+
template territory (see the `assets.py` security model): treebox never
|
|
134
|
+
derives sandbox contents from code paths the target repo can influence.
|
|
135
|
+
|
|
136
|
+
Runtime config validation, `--harness` help, doctor login rows, the no-login
|
|
137
|
+
advice, credential staging, and sandbox mounts derive from the registry.
|
|
138
|
+
|
|
139
|
+
**Adding a runner** (a new isolation backend):
|
|
140
|
+
|
|
141
|
+
1. One `Runner` implementation (see the protocol in `runners/base.py` and
|
|
142
|
+
the contract below).
|
|
143
|
+
2. One entry in `runners/__init__.py`'s `RUNNERS` registry — the factory
|
|
144
|
+
declares which generic options the adapter consumes.
|
|
145
|
+
3. Add the isolation name to `config.py`'s hard-coded `Isolation` Literal
|
|
146
|
+
alias. The drift test `test_registry_vocabularies_cannot_drift` must keep
|
|
147
|
+
passing: the alias, `RUNNERS`, and `VALID_ISOLATION` must name the same set.
|
|
148
|
+
|
|
149
|
+
Runtime config validation and the `--isolation` help derive from
|
|
150
|
+
`VALID_ISOLATION`, which derives from the registry.
|
|
151
|
+
|
|
152
|
+
### The isolation contract
|
|
153
|
+
|
|
154
|
+
The `Runner` seam abstracts **where the agent process runs** — it does *not*
|
|
155
|
+
abstract **where the workspace lives**. Host locality binds every backend;
|
|
156
|
+
the security invariants bind sandboxed backends. `HostRunner` is the deliberate
|
|
157
|
+
non-sandbox exception: it launches directly on the host with live
|
|
158
|
+
`~/.claude` / `~/.codex` login dirs and normal host repo access.
|
|
159
|
+
|
|
160
|
+
What a sandboxed backend must **guarantee** (the security invariants):
|
|
161
|
+
|
|
162
|
+
- Staged credential *copies* only — the live `~/.claude` / `~/.codex` are
|
|
163
|
+
never exposed to the sandbox (they hold host-executed config).
|
|
164
|
+
- The sandbox-defining config is rendered outside the mount, so a boxed
|
|
165
|
+
agent cannot edit the definition of its own box.
|
|
166
|
+
- The shared `.git/hooks` is presented read-only (host git executes it).
|
|
167
|
+
- Egress lockdown, when enabled, exists before any workspace-derived code
|
|
168
|
+
runs (firewall-before-post-create).
|
|
169
|
+
- Only user-level treebox config is ever read — never the target repo's.
|
|
170
|
+
|
|
171
|
+
What a backend may **assume** (host locality):
|
|
172
|
+
|
|
173
|
+
- Provisioning already happened host-side (`provision.py` writes the
|
|
174
|
+
worktree, guard, submodules, `.env` before the runner sees it).
|
|
175
|
+
- The host filesystem is visible *at identical absolute paths*: docker
|
|
176
|
+
bind-mounts the worktree and its git common dir 1:1, state lives in the
|
|
177
|
+
host-side private git dir, the lockfile hash stats host files, and the
|
|
178
|
+
name-is-permanent rule in `models.py` presumes the absolute-path mount.
|
|
179
|
+
|
|
180
|
+
Consequence: OCI-compatible engines (podman, etc.) and bind-mount sandboxes
|
|
181
|
+
(bubblewrap/nsjail-style) fit this seam as-is. SSH-remote, VM, or cloud
|
|
182
|
+
backends — anywhere the agent's filesystem ≠ the host's — would first need a
|
|
183
|
+
filesystem-transport seam that deliberately does not exist yet (no adapter
|
|
184
|
+
needs path translation today, so building it would be indirection).
|
|
185
|
+
|
|
87
186
|
## Invariants to preserve
|
|
88
187
|
|
|
89
188
|
- **Code is never silently stale.** `create` requires a successful
|
|
@@ -44,6 +44,23 @@ uv run --extra dev ruff format --check src tests
|
|
|
44
44
|
uv run --extra dev mypy
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
For changes that touch provisioning, runners, git handling, or CLI output, run
|
|
48
|
+
the full validation gate: lint, type checks, shell asset checks, tests, the
|
|
49
|
+
golden CLI-output snapshots in `tests/golden/`, and a live host-runner smoke
|
|
50
|
+
test against a throwaway local remote:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
./scripts/validate.sh
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For issue #141 behavior-preserving refactors, `scripts/golden-diff.sh` is the
|
|
57
|
+
focused snapshot-only gate. Refresh snapshots only after the issue explicitly
|
|
58
|
+
accepts a behavior change.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
scripts/golden-diff.sh
|
|
62
|
+
```
|
|
63
|
+
|
|
47
64
|
For docs-only changes, this is enough:
|
|
48
65
|
|
|
49
66
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: treebox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Isolated, ready-to-run git worktrees for AI coding agents — host-native or docker-sandboxed.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Seth-Peters/treebox
|
|
6
6
|
Project-URL: Documentation, https://seth-peters.github.io/treebox/
|
|
@@ -97,9 +97,10 @@ your back. Or install directly:
|
|
|
97
97
|
uv tool install treebox
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
Host isolation needs
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
Host isolation needs `git` plus the selected agent CLI on PATH and logged in.
|
|
101
|
+
Docker isolation additionally needs `docker`; the default template supplies the
|
|
102
|
+
agent CLI, and treebox copies scoped login files from your `~/.claude` /
|
|
103
|
+
`~/.codex` subscription login. See the
|
|
103
104
|
[install guide](https://seth-peters.github.io/treebox/install/) for
|
|
104
105
|
requirements and installer overrides.
|
|
105
106
|
|
|
@@ -279,7 +280,7 @@ uv run treebox ... # run the CLI from the working tree
|
|
|
279
280
|
uv run --extra dev pre-commit install # lint/format/strict type-check hooks (see CONTRIBUTING.md)
|
|
280
281
|
uv run --extra dev python -m pytest # unit + integration suite
|
|
281
282
|
uv run --extra dev mypy # strict type check
|
|
282
|
-
./scripts/validate.sh # lint +
|
|
283
|
+
./scripts/validate.sh # full gate: lint + typecheck + shell assets + tests + snapshots + smoke
|
|
283
284
|
```
|
|
284
285
|
|
|
285
286
|
## Contributing
|
|
@@ -60,9 +60,10 @@ your back. Or install directly:
|
|
|
60
60
|
uv tool install treebox
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Host isolation needs
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
Host isolation needs `git` plus the selected agent CLI on PATH and logged in.
|
|
64
|
+
Docker isolation additionally needs `docker`; the default template supplies the
|
|
65
|
+
agent CLI, and treebox copies scoped login files from your `~/.claude` /
|
|
66
|
+
`~/.codex` subscription login. See the
|
|
66
67
|
[install guide](https://seth-peters.github.io/treebox/install/) for
|
|
67
68
|
requirements and installer overrides.
|
|
68
69
|
|
|
@@ -242,7 +243,7 @@ uv run treebox ... # run the CLI from the working tree
|
|
|
242
243
|
uv run --extra dev pre-commit install # lint/format/strict type-check hooks (see CONTRIBUTING.md)
|
|
243
244
|
uv run --extra dev python -m pytest # unit + integration suite
|
|
244
245
|
uv run --extra dev mypy # strict type check
|
|
245
|
-
./scripts/validate.sh # lint +
|
|
246
|
+
./scripts/validate.sh # full gate: lint + typecheck + shell assets + tests + snapshots + smoke
|
|
246
247
|
```
|
|
247
248
|
|
|
248
249
|
## Contributing
|
|
@@ -4,8 +4,8 @@ treebox is intentionally small: isolated git worktrees, cache-backed setup, and
|
|
|
4
4
|
host or Docker runners for AI coding agents. A few directions we'd like to grow in,
|
|
5
5
|
roughly in priority order:
|
|
6
6
|
|
|
7
|
-
1. **Support more coding-agent CLIs.**
|
|
8
|
-
`claude`/`codex` so more agents can launch in a provisioned worktree.
|
|
7
|
+
1. **Support more coding-agent CLIs.** Extend the `harnesses.py` registry
|
|
8
|
+
beyond `claude`/`codex` so more agents can launch in a provisioned worktree.
|
|
9
9
|
2. **Support more isolation environments.** Add runners beyond host and Docker —
|
|
10
10
|
Podman first, as a rootless, daemonless drop-in.
|
|
11
11
|
3. **Wider support and testing for different git services.** Exercise and harden
|
|
@@ -38,12 +38,19 @@ uv = "/mnt/fast/cache/uv"
|
|
|
38
38
|
npm = "/mnt/fast/cache/npm"
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
For a new `create`, precedence is what you'd expect:
|
|
42
42
|
|
|
43
43
|
```text
|
|
44
44
|
command-line flag > config.toml > built-in default
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
For an existing worktree, `enter` and `teardown` also read the worktree's
|
|
48
|
+
recorded creation-time state. Recorded isolation wins over the config default,
|
|
49
|
+
and a conflicting explicit `--isolation` exits `5`; `enter` always reuses the
|
|
50
|
+
recorded firewall, while recorded harness and template beat config defaults
|
|
51
|
+
unless `--harness` or `--template` is passed. `teardown` uses the recorded
|
|
52
|
+
template for docker volume cleanup because it has no `--template` flag.
|
|
53
|
+
|
|
47
54
|
| Key | Default | What it controls |
|
|
48
55
|
| ---------- | -------------------- | --------------------------------------------------------- |
|
|
49
56
|
| `isolation`| `host` | Where agents run: the worktree shell, or a docker sandbox. |
|
|
@@ -156,5 +163,6 @@ container config in the target repo itself is deliberately ignored — see
|
|
|
156
163
|
| `XDG_CACHE_HOME` | Standard XDG base for the shared package caches treebox mounts. |
|
|
157
164
|
|
|
158
165
|
Secrets stay in files: treebox copies your repo's `.env` (or the configured
|
|
159
|
-
`env_file`) into each worktree and mounts it into containers.
|
|
160
|
-
|
|
166
|
+
`env_file`) into each worktree and mounts it into containers. Host isolation
|
|
167
|
+
uses your live subscription login; docker isolation mounts scoped copies of the
|
|
168
|
+
login files each harness declares and refreshes them on every entry.
|
|
@@ -4,6 +4,12 @@ treebox is orchestration glue — it shells out to `git` and `docker` rather
|
|
|
4
4
|
than reimplementing them. The whole tool is organized around one seam:
|
|
5
5
|
**provisioning always happens on the host; running is pluggable.**
|
|
6
6
|
|
|
7
|
+
Three closed vocabularies sit on top of that seam:
|
|
8
|
+
|
|
9
|
+
- **Harness** — which agent CLI launches (`claude`, `codex`).
|
|
10
|
+
- **Runner / isolation** — where it executes (`host`, `docker`).
|
|
11
|
+
- **Ecosystem** — what setup runs (uv, npm, pnpm, go, cargo).
|
|
12
|
+
|
|
7
13
|
## The provisioning pipeline
|
|
8
14
|
|
|
9
15
|
Every `create` walks the same host-side pipeline, regardless of isolation mode:
|
|
@@ -66,6 +72,14 @@ is pruned together with the worktree, and is what lets `enter` re-sync
|
|
|
66
72
|
dependencies only when they actually changed — and `list` show `fresh` /
|
|
67
73
|
`stale` at a glance.
|
|
68
74
|
|
|
75
|
+
The same private state records the worktree's creation-time choices. For an
|
|
76
|
+
existing worktree, `enter` and `teardown` recover the recorded isolation and
|
|
77
|
+
template instead of drifting to today's config defaults; `enter` also reuses the
|
|
78
|
+
recorded firewall, and it reuses the recorded harness unless a per-session
|
|
79
|
+
harness override is passed.
|
|
80
|
+
An explicit `--isolation` that disagrees with the recorded mode is a conflict,
|
|
81
|
+
not an override.
|
|
82
|
+
|
|
69
83
|
## Warmth lives in the cache, not the tree
|
|
70
84
|
|
|
71
85
|
treebox detects the package manager from the repo — **uv, npm, pnpm, go, or
|
|
@@ -98,8 +112,10 @@ agent's own in-container `git` remains normal, but config it writes into the
|
|
|
98
112
|
shared git dir cannot make the next host-side treebox call execute
|
|
99
113
|
repo-controlled hooks or monitors.
|
|
100
114
|
|
|
101
|
-
Your `.env
|
|
102
|
-
your
|
|
115
|
+
Your `.env`, shared caches, and scoped credential copies are mounted in. Host
|
|
116
|
+
isolation uses your live `~/.claude` / `~/.codex` login dirs; docker isolation
|
|
117
|
+
never mounts those live dirs. Instead, each harness declares which login files
|
|
118
|
+
to copy into a per-worktree credentials dir, refreshed on every `enter`.
|
|
103
119
|
|
|
104
120
|
## Built to be scripted
|
|
105
121
|
|
|
@@ -41,7 +41,8 @@ Docker sandbox.
|
|
|
41
41
|
<div class="t-line" style="--d:3.2s;--run:.5s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> worktree <span class="c-dim">.treebox/worktrees/brave-otter</span></div>
|
|
42
42
|
<div class="t-line" style="--d:3.7s;--run:.3s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> secrets <span class="c-dim">copied</span></div>
|
|
43
43
|
<div class="t-line" style="--d:4.0s;--run:.4s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> sandbox <span class="c-dim">templates written (outside worktree)</span></div>
|
|
44
|
-
<div class="t-line" style="--d:4.4s;--run
|
|
44
|
+
<div class="t-line" style="--d:4.4s;--run:.3s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> credentials <span class="c-dim">scoped copies staged</span></div>
|
|
45
|
+
<div class="t-line" style="--d:4.8s;--run:1.5s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> image <span class="c-dim">building container image…</span></div>
|
|
45
46
|
<div class="t-line" style="--d:6.3s;--run:.5s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> container <span class="c-dim">started · agent execs via docker exec</span></div>
|
|
46
47
|
<div class="t-line" style="--d:6.9s"> </div>
|
|
47
48
|
<div class="t-line" style="--d:6.9s"> <span class="c-ok">Ready</span> <span class="c-dim">in 22.6s — launching</span> claude</div>
|
|
@@ -54,9 +55,10 @@ Docker sandbox.
|
|
|
54
55
|
<div class="t-line" style="--d:1.9s"> <span class="c-dim">isolation</span> docker <span class="c-dim">→</span> codex</div>
|
|
55
56
|
<div class="t-line" style="--d:2.1s"> </div>
|
|
56
57
|
<div class="t-line" style="--d:2.2s;--run:.3s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> secrets <span class="c-dim">copied</span></div>
|
|
57
|
-
<div class="t-line" style="--d:2.
|
|
58
|
-
<div class="t-line" style="--d:
|
|
59
|
-
<div class="t-line" style="--d:3.
|
|
58
|
+
<div class="t-line" style="--d:2.6s;--run:.3s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> credentials <span class="c-dim">scoped copies refreshed</span></div>
|
|
59
|
+
<div class="t-line" style="--d:3.0s"> <span class="c-dim">· deps unchanged · skipping setup</span></div>
|
|
60
|
+
<div class="t-line" style="--d:3.2s"> </div>
|
|
61
|
+
<div class="t-line" style="--d:3.3s"> <span class="c-ok">Ready</span> <span class="c-dim">in 1.4s — launching</span> codex</div>
|
|
60
62
|
<div class="t-line" style="--d:4.4s"> </div>
|
|
61
63
|
<div class="t-line" style="--d:4.5s">^C</div>
|
|
62
64
|
</div>
|
|
@@ -68,9 +70,10 @@ Docker sandbox.
|
|
|
68
70
|
<div class="t-line" style="--d:2.0s"> <span class="c-dim">isolation</span> docker <span class="c-dim">→</span> claude</div>
|
|
69
71
|
<div class="t-line" style="--d:2.2s"> </div>
|
|
70
72
|
<div class="t-line" style="--d:2.3s;--run:.3s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> secrets <span class="c-dim">copied</span></div>
|
|
71
|
-
<div class="t-line" style="--d:2.
|
|
72
|
-
<div class="t-line" style="--d:3.
|
|
73
|
-
<div class="t-line" style="--d:3.
|
|
73
|
+
<div class="t-line" style="--d:2.7s;--run:.3s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> credentials <span class="c-dim">scoped copies refreshed</span></div>
|
|
74
|
+
<div class="t-line" style="--d:3.1s"> <span class="c-dim">· deps unchanged · skipping setup</span></div>
|
|
75
|
+
<div class="t-line" style="--d:3.3s"> </div>
|
|
76
|
+
<div class="t-line" style="--d:3.4s"> <span class="c-ok">Ready</span> <span class="c-dim">in 1.2s — launching</span> claude</div>
|
|
74
77
|
</div>
|
|
75
78
|
<div class="t-scene" data-dur="4800">
|
|
76
79
|
<div class="t-line" style="--d:0s;--n:10"><span class="c-ok">$ </span><span class="t-type">treebox ls</span></div>
|
|
@@ -39,20 +39,25 @@ curl -fsSL https://raw.githubusercontent.com/Seth-Peters/treebox/main/install.sh
|
|
|
39
39
|
|
|
40
40
|
## What each isolation mode needs
|
|
41
41
|
|
|
42
|
-
| Requirement
|
|
43
|
-
|
|
|
44
|
-
| `git`
|
|
45
|
-
|
|
|
46
|
-
| `
|
|
42
|
+
| Requirement | `host` | `docker` |
|
|
43
|
+
| ------------------------------------------------ | :-----------: | :-------------: |
|
|
44
|
+
| `git` | ✓ | ✓ |
|
|
45
|
+
| Subscription login (`~/.claude` / `~/.codex`) | ✓ | ✓ |
|
|
46
|
+
| Host agent CLI on PATH (`claude` / `codex`) | ✓ | — (template-provided) |
|
|
47
|
+
| `docker` | — | ✓ |
|
|
47
48
|
|
|
48
49
|
That's the whole table: docker isolation speaks plain `docker build` /
|
|
49
|
-
`docker run` / `docker exec`, so there is no Node.js and no
|
|
50
|
-
install. The
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
`docker run` / `docker exec`, so there is no Node.js and no dev-container CLI
|
|
51
|
+
to install. The default template installs the agent CLIs in the image; the host
|
|
52
|
+
supplies scoped subscription-login copies. The worktree and its git dir are
|
|
53
|
+
mounted at their host paths, so in-container `git` just works — with no special
|
|
54
|
+
git version requirement.
|
|
55
|
+
|
|
56
|
+
Host isolation launches against your live `~/.claude` / `~/.codex`
|
|
57
|
+
**subscription login**. Docker isolation stages scoped copies of those login
|
|
58
|
+
files and never mounts the live dirs. The sandbox template docker isolation
|
|
59
|
+
provisions is pinned to CPython 3.14.6, independent of whatever runs treebox
|
|
60
|
+
itself.
|
|
56
61
|
|
|
57
62
|
## Verify
|
|
58
63
|
|
|
@@ -120,5 +125,5 @@ uv run treebox ... # run the CLI from the working tree
|
|
|
120
125
|
uv run --extra dev pre-commit install # lint/format/strict type-check hooks (see CONTRIBUTING.md)
|
|
121
126
|
uv run --extra dev python -m pytest # unit + integration suite
|
|
122
127
|
uv run --extra dev mypy # strict type check
|
|
123
|
-
./scripts/validate.sh # lint +
|
|
128
|
+
./scripts/validate.sh # full gate: lint + typecheck + shell assets + tests + snapshots + smoke
|
|
124
129
|
```
|
|
@@ -138,7 +138,12 @@ harness the worktree was **provisioned with** (`create -H codex` then plain
|
|
|
138
138
|
per-session override that launches that agent this time without changing what's
|
|
139
139
|
recorded on disk. The sandbox **template** is reused the same way: a worktree
|
|
140
140
|
created with `--template node` re-renders that template on `enter`, not the
|
|
141
|
-
config default, and an explicit `--template` is a per-session override.
|
|
141
|
+
config default, and an explicit `--template` is a per-session override. The
|
|
142
|
+
recorded **isolation** mode also wins over the config default; a conflicting
|
|
143
|
+
explicit `--isolation` exits `5` instead of entering the wrong kind of
|
|
144
|
+
worktree. For docker worktrees, the recorded firewall choice is reused too, so
|
|
145
|
+
`create --no-firewall` keeps entering cleanly even under a `firewall = true`
|
|
146
|
+
config.
|
|
142
147
|
Dependencies re-sync **only if the lockfile changed** since the last setup
|
|
143
148
|
(treebox stores the hash in the worktree's private git dir, so it never shows
|
|
144
149
|
up in `git status`); a re-sync preserves the recorded harness and template
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Golden output snapshots for the CLI's observable surface (issue #141, PR 0).
|
|
3
|
+
#
|
|
4
|
+
# Runs a fixed matrix of commands — doctor, create --print/--dry-run,
|
|
5
|
+
# enter --print, and their --json forms, for both isolations — against a
|
|
6
|
+
# hermetic throwaway repo, normalizes the machine-specific bits (paths,
|
|
7
|
+
# uid/gid, git version, container-name digests), and diffs the result against
|
|
8
|
+
# the committed snapshots in tests/golden/. The refactor PRs under issue #141
|
|
9
|
+
# must leave every snapshot byte-identical: the "output identical" gate is
|
|
10
|
+
# mechanical, not eyeballed.
|
|
11
|
+
#
|
|
12
|
+
# scripts/golden-diff.sh # compare against tests/golden/ (CI gate)
|
|
13
|
+
# scripts/golden-diff.sh --update # regenerate the snapshots (review the diff!)
|
|
14
|
+
#
|
|
15
|
+
# Docker is replaced by a no-op shim on PATH so the docker-isolation cases are
|
|
16
|
+
# deterministic and run on hosts without a daemon: `docker ps -aq` reports no
|
|
17
|
+
# containers and every other subcommand succeeds silently, which drives the
|
|
18
|
+
# fresh-create path (render config -> stage credentials -> build -> run ->
|
|
19
|
+
# post-create) end to end without a real container.
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
|
22
|
+
|
|
23
|
+
MODE="diff"
|
|
24
|
+
[[ "${1:-}" == "--update" ]] && MODE="update"
|
|
25
|
+
|
|
26
|
+
GOLDEN_DIR="tests/golden"
|
|
27
|
+
ROOT_PARENT="${TREEBOX_GOLDEN_ROOT:-/tmp}"
|
|
28
|
+
ROOT_PARENT="${ROOT_PARENT%/}"
|
|
29
|
+
[[ -n "$ROOT_PARENT" ]] || { echo "FATAL: TREEBOX_GOLDEN_ROOT must not be /" >&2; exit 1; }
|
|
30
|
+
mkdir -p "$ROOT_PARENT"
|
|
31
|
+
ROOT="$(mktemp -d "$ROOT_PARENT/treebox-golden.XXXXXX")"
|
|
32
|
+
ROOT="$(cd "$ROOT" && pwd -P)"
|
|
33
|
+
WORK="$ROOT/out"
|
|
34
|
+
trap 'rm -rf "$ROOT"' EXIT
|
|
35
|
+
|
|
36
|
+
TREEBOX="$PWD/.venv/bin/treebox"
|
|
37
|
+
[[ -x "$TREEBOX" ]] || uv sync --quiet
|
|
38
|
+
[[ -x "$TREEBOX" ]] || { echo "FATAL: $TREEBOX not found after uv sync" >&2; exit 1; }
|
|
39
|
+
|
|
40
|
+
mkdir -p "$WORK" "$ROOT/tbhome" "$ROOT/home-empty"
|
|
41
|
+
|
|
42
|
+
# --- fake HOME with both subscription logins staged --------------------------
|
|
43
|
+
HOME_FAKE="$ROOT/home"
|
|
44
|
+
mkdir -p "$HOME_FAKE/.claude" "$HOME_FAKE/.codex"
|
|
45
|
+
printf '{"token": "golden"}\n' > "$HOME_FAKE/.claude/.credentials.json"
|
|
46
|
+
printf '{}\n' > "$HOME_FAKE/.claude/settings.json"
|
|
47
|
+
printf '{}\n' > "$HOME_FAKE/.codex/auth.json"
|
|
48
|
+
printf '\n' > "$HOME_FAKE/.codex/config.toml"
|
|
49
|
+
|
|
50
|
+
# --- docker shim: no containers exist; every subcommand succeeds -------------
|
|
51
|
+
SHIM="$ROOT/shim"
|
|
52
|
+
mkdir -p "$SHIM"
|
|
53
|
+
printf '#!/bin/sh\nexit 0\n' > "$SHIM/docker"
|
|
54
|
+
chmod +x "$SHIM/docker"
|
|
55
|
+
|
|
56
|
+
# --- hermetic config: setup is a marker-writing hook, never a real installer -
|
|
57
|
+
CFG="$ROOT/config.toml"
|
|
58
|
+
printf 'setup_hook = ["echo ran >> setup.log"]\n' > "$CFG"
|
|
59
|
+
|
|
60
|
+
# --- fixture repo: a working clone of a local bare origin --------------------
|
|
61
|
+
git_q() { git -c init.defaultBranch=main "$@" >/dev/null; }
|
|
62
|
+
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@e GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@e
|
|
63
|
+
git_q init --bare -b main "$ROOT/origin.git"
|
|
64
|
+
git_q clone "$ROOT/origin.git" "$ROOT/repo" 2>/dev/null
|
|
65
|
+
REPO="$ROOT/repo"
|
|
66
|
+
printf '[project]\nname="golden"\nversion="0"\n' > "$REPO/pyproject.toml"
|
|
67
|
+
printf 'version = 1\n' > "$REPO/uv.lock"
|
|
68
|
+
printf 'SECRET=golden\n' > "$REPO/.env"
|
|
69
|
+
git_q -C "$REPO" add -A
|
|
70
|
+
git_q -C "$REPO" commit -qm init
|
|
71
|
+
git_q -C "$REPO" push -q origin main
|
|
72
|
+
WTS="$ROOT/wts"
|
|
73
|
+
|
|
74
|
+
GITVER="$(git --version | awk '{print $3}')"
|
|
75
|
+
GITVER_PARSED="$(git --version | perl -lane 'my $s = $F[-1]; my @parts = split /\./, $s; my @nums; for my $p (@parts[0..2]) { $p //= ""; $p =~ s/\D//g; push @nums, length($p) ? $p : 0; } push @nums, 0 while @nums < 3; print join ".", @nums[0..2]')"
|
|
76
|
+
export GD_ROOT="$ROOT" GD_PRIVROOT="/private$ROOT" GD_GITVER="$GITVER" GD_GITVER_PARSED="$GITVER_PARSED"
|
|
77
|
+
# Literal (not pattern) uid/gid substitutions, so a regression that changed
|
|
78
|
+
# one number into another is a snapshot diff, not silently normalized away.
|
|
79
|
+
GD_UID="$(id -u)"
|
|
80
|
+
GD_GID="$(id -g)"
|
|
81
|
+
export GD_UID GD_GID
|
|
82
|
+
|
|
83
|
+
normalize() {
|
|
84
|
+
perl -pe '
|
|
85
|
+
s/\Q$ENV{GD_PRIVROOT}\E/__ROOT__/g;
|
|
86
|
+
s/\Q$ENV{GD_ROOT}\E/__ROOT__/g;
|
|
87
|
+
s/\Q$ENV{GD_GITVER_PARSED}\E/__GITVER__/g;
|
|
88
|
+
s/\Q$ENV{GD_GITVER}\E/__GITVER__/g;
|
|
89
|
+
s/\bUSER_UID=\Q$ENV{GD_UID}\E\b/USER_UID=__UID__/g;
|
|
90
|
+
s/\bUSER_GID=\Q$ENV{GD_GID}\E\b/USER_GID=__GID__/g;
|
|
91
|
+
s/\b\Q$ENV{GD_UID}\E:\Q$ENV{GD_GID}\E\b/__UID__:__GID__/g;
|
|
92
|
+
s/\btreebox-([a-z0-9._-]+)-[0-9a-f]{10}\b/treebox-$1-__HASH__/g;
|
|
93
|
+
'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
FAILED=0
|
|
97
|
+
|
|
98
|
+
# run_case <name> <home-dir> <treebox args...>
|
|
99
|
+
run_case() {
|
|
100
|
+
local name="$1" home="$2"
|
|
101
|
+
shift 2
|
|
102
|
+
local rc=0
|
|
103
|
+
set +e
|
|
104
|
+
env -u TZ -u XDG_CACHE_HOME -u PNPM_HOME -u UV_CACHE_DIR -u npm_config_cache \
|
|
105
|
+
-u npm_config_store_dir -u GOMODCACHE -u CARGO_HOME -u TREEBOX_TEMPLATE_DIR \
|
|
106
|
+
-u NO_COLOR -u FORCE_COLOR -u CLICOLOR_FORCE \
|
|
107
|
+
HOME="$home" TREEBOX_CONFIG="$CFG" TREEBOX_HOME="$ROOT/tbhome" \
|
|
108
|
+
PATH="$SHIM:$PATH" COLUMNS=80 \
|
|
109
|
+
"$TREEBOX" "$@" >"$WORK/$name.stdout" 2>"$WORK/$name.stderr"
|
|
110
|
+
rc=$?
|
|
111
|
+
set -e
|
|
112
|
+
{
|
|
113
|
+
echo "# treebox $*" | normalize
|
|
114
|
+
echo "# exit: $rc"
|
|
115
|
+
echo "# --- stdout ---"
|
|
116
|
+
normalize < "$WORK/$name.stdout"
|
|
117
|
+
echo "# --- stderr ---"
|
|
118
|
+
normalize < "$WORK/$name.stderr"
|
|
119
|
+
} > "$WORK/$name.txt"
|
|
120
|
+
rm -f "$WORK/$name.stdout" "$WORK/$name.stderr"
|
|
121
|
+
|
|
122
|
+
if [[ "$MODE" == "update" ]]; then
|
|
123
|
+
cp "$WORK/$name.txt" "$GOLDEN_DIR/$name.txt"
|
|
124
|
+
echo "updated $name"
|
|
125
|
+
elif [[ ! -f "$GOLDEN_DIR/$name.txt" ]]; then
|
|
126
|
+
echo "MISSING snapshot: $GOLDEN_DIR/$name.txt (run with --update on main first)" >&2
|
|
127
|
+
FAILED=1
|
|
128
|
+
elif ! diff -u "$GOLDEN_DIR/$name.txt" "$WORK/$name.txt"; then
|
|
129
|
+
echo "DIFF $name" >&2
|
|
130
|
+
FAILED=1
|
|
131
|
+
else
|
|
132
|
+
echo "ok $name"
|
|
133
|
+
fi
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
mkdir -p "$GOLDEN_DIR"
|
|
137
|
+
R=(--repo "$REPO")
|
|
138
|
+
RW=(--repo "$REPO" --root "$WTS")
|
|
139
|
+
|
|
140
|
+
# --- host isolation -----------------------------------------------------------
|
|
141
|
+
run_case doctor-host "$HOME_FAKE" doctor "${R[@]}"
|
|
142
|
+
run_case doctor-host-json "$HOME_FAKE" doctor "${R[@]}" --json
|
|
143
|
+
run_case doctor-no-login "$ROOT/home-empty" doctor "${R[@]}"
|
|
144
|
+
run_case create-dryrun-host "$HOME_FAKE" create golden-host "${RW[@]}" --dry-run
|
|
145
|
+
run_case create-dryrun-host-json "$HOME_FAKE" create golden-host "${RW[@]}" --dry-run --json
|
|
146
|
+
run_case create-host-print "$HOME_FAKE" create golden-host "${RW[@]}" --print
|
|
147
|
+
run_case create-host-json "$HOME_FAKE" create golden-host-json "${RW[@]}" --json
|
|
148
|
+
run_case enter-host-print "$HOME_FAKE" enter golden-host "${RW[@]}" --print
|
|
149
|
+
run_case enter-host-json "$HOME_FAKE" enter golden-host "${RW[@]}" --json
|
|
150
|
+
|
|
151
|
+
# --- docker isolation (through the shim) ---------------------------------------
|
|
152
|
+
run_case doctor-docker "$HOME_FAKE" doctor "${R[@]}" --isolation docker
|
|
153
|
+
run_case doctor-docker-json "$HOME_FAKE" doctor "${R[@]}" --isolation docker --json
|
|
154
|
+
run_case create-dryrun-docker "$HOME_FAKE" create golden-docker "${RW[@]}" --isolation docker --dry-run
|
|
155
|
+
run_case create-dryrun-docker-json "$HOME_FAKE" create golden-docker "${RW[@]}" --isolation docker --dry-run --json
|
|
156
|
+
run_case create-docker-print "$HOME_FAKE" create golden-docker "${RW[@]}" --isolation docker --print
|
|
157
|
+
run_case create-docker-json "$HOME_FAKE" create golden-docker-json "${RW[@]}" --isolation docker --json
|
|
158
|
+
run_case enter-docker-print "$HOME_FAKE" enter golden-docker "${RW[@]}" --print
|
|
159
|
+
run_case enter-docker-json "$HOME_FAKE" enter golden-docker "${RW[@]}" --json
|
|
160
|
+
|
|
161
|
+
if [[ "$MODE" == "diff" && "$FAILED" -ne 0 ]]; then
|
|
162
|
+
echo >&2
|
|
163
|
+
echo "golden-diff: OUTPUT CHANGED — the stability contract (issue #141) requires" >&2
|
|
164
|
+
echo "byte-identical CLI output. If the change is intentional, discuss on the" >&2
|
|
165
|
+
echo "issue first; only regenerate with --update after that decision." >&2
|
|
166
|
+
exit 1
|
|
167
|
+
fi
|
|
168
|
+
echo "golden-diff: all snapshots match"
|