treebox 0.3.0__tar.gz → 0.4.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.4.0/.github/workflows/autofix.yml +69 -0
- {treebox-0.3.0 → treebox-0.4.0}/.github/workflows/ci.yml +3 -0
- treebox-0.4.0/.pre-commit-config.yaml +25 -0
- {treebox-0.3.0 → treebox-0.4.0}/CLAUDE.md +4 -2
- treebox-0.4.0/CONTRIBUTING.md +52 -0
- {treebox-0.3.0 → treebox-0.4.0}/PKG-INFO +47 -9
- {treebox-0.3.0 → treebox-0.4.0}/README.md +44 -8
- {treebox-0.3.0 → treebox-0.4.0}/docs/configuration.md +45 -3
- {treebox-0.3.0 → treebox-0.4.0}/docs/how-it-works.md +4 -2
- {treebox-0.3.0 → treebox-0.4.0}/docs/index.md +56 -3
- {treebox-0.3.0 → treebox-0.4.0}/docs/install.md +2 -1
- {treebox-0.3.0 → treebox-0.4.0}/docs/javascripts/treebox.js +20 -13
- {treebox-0.3.0 → treebox-0.4.0}/docs/stylesheets/extra.css +59 -8
- {treebox-0.3.0 → treebox-0.4.0}/docs/usage.md +19 -8
- {treebox-0.3.0 → treebox-0.4.0}/pyproject.toml +4 -2
- {treebox-0.3.0 → treebox-0.4.0}/scripts/validate.sh +1 -0
- {treebox-0.3.0 → treebox-0.4.0}/skills/treebox/SKILL.md +3 -3
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets.py +20 -2
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/cli.py +111 -25
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/config.py +10 -2
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/git.py +10 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/provision.py +128 -26
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/runners/base.py +6 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/runners/docker.py +65 -12
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/runners/host.py +17 -4
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/state.py +8 -0
- {treebox-0.3.0 → treebox-0.4.0}/tests/test_integration.py +373 -0
- {treebox-0.3.0 → treebox-0.4.0}/tests/test_units.py +235 -11
- {treebox-0.3.0 → treebox-0.4.0}/uv.lock +93 -0
- treebox-0.3.0/CONTRIBUTING.md +0 -31
- {treebox-0.3.0 → treebox-0.4.0}/.agents/skills/no-mistakes/SKILL.md +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/.github/workflows/claude.yml +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/.github/workflows/docs.yml +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/.github/workflows/release.yml +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/.gitignore +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/AGENTS.md +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/LICENSE +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/ROADMAP.md +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/assets/treebox-logo.png +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/docs/agents.md +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/docs/assets/treebox-logo.png +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/hooks/copy_page.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/install.sh +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/mkdocs.yml +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/__init__.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/Dockerfile +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/allowed-domains.sh +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/container.json +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/firewall.json +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/init-firewall.sh +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/post-create.sh +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/pre-push +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/ecosystems.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/forge.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/locking.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/models.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/names.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/output.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/py.typed +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/resolve.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/runners/__init__.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/status.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/src/treebox/system.py +0 -0
- {treebox-0.3.0 → treebox-0.4.0}/tests/conftest.py +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Autofix
|
|
2
|
+
|
|
3
|
+
# Applies ruff's auto-fixes (`ruff check --fix` + `ruff format`) to pull requests
|
|
4
|
+
# and commits the result back to the PR branch. This is the CI counterpart to the
|
|
5
|
+
# pre-commit hooks: if a change lands unformatted (hooks skipped), the branch is
|
|
6
|
+
# fixed in place instead of just failing the CI gate. Skips fork PRs, where the
|
|
7
|
+
# workflow token cannot push, and its own commits, to avoid loops.
|
|
8
|
+
#
|
|
9
|
+
# Gotcha: pushes made with the default GITHUB_TOKEN (from actions/checkout) do
|
|
10
|
+
# NOT trigger new workflow runs. So the auto-fixed commit this job pushes will
|
|
11
|
+
# not re-run ci.yml. Two implications:
|
|
12
|
+
# - If branch protection REQUIRES the CI status checks, the auto-fixed SHA will
|
|
13
|
+
# have no reported checks and the PR will stall. The remedy is to check out
|
|
14
|
+
# and push using a PAT or GitHub App token stored as a repo secret (e.g.
|
|
15
|
+
# AUTOFIX_TOKEN) instead of GITHUB_TOKEN, so the push re-triggers CI.
|
|
16
|
+
# - To close the "merged with a commit CI never validated" hole regardless,
|
|
17
|
+
# this job re-runs the ruff + mypy checks on the fixed tree below and fails
|
|
18
|
+
# rather than pushing if they don't pass, so the exact pushed commit is
|
|
19
|
+
# validated within this run.
|
|
20
|
+
|
|
21
|
+
on:
|
|
22
|
+
pull_request:
|
|
23
|
+
|
|
24
|
+
concurrency:
|
|
25
|
+
group: autofix-${{ github.ref }}
|
|
26
|
+
cancel-in-progress: true
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: write
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
autofix:
|
|
33
|
+
name: ruff --fix
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
if: >-
|
|
36
|
+
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
37
|
+
github.actor != 'github-actions[bot]'
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
with:
|
|
41
|
+
ref: ${{ github.head_ref }}
|
|
42
|
+
|
|
43
|
+
- name: Install uv
|
|
44
|
+
uses: astral-sh/setup-uv@v8.2.0
|
|
45
|
+
with:
|
|
46
|
+
enable-cache: true
|
|
47
|
+
|
|
48
|
+
- name: ruff check --fix
|
|
49
|
+
run: uv run --extra dev ruff check --fix src tests
|
|
50
|
+
|
|
51
|
+
- name: ruff format
|
|
52
|
+
run: uv run --extra dev ruff format src tests
|
|
53
|
+
|
|
54
|
+
- name: Validate fixed tree
|
|
55
|
+
run: |
|
|
56
|
+
uv run --extra dev ruff check src tests
|
|
57
|
+
uv run --extra dev ruff format --check src tests
|
|
58
|
+
uv run --extra dev mypy
|
|
59
|
+
|
|
60
|
+
- name: Commit fixes
|
|
61
|
+
run: |
|
|
62
|
+
if git diff --quiet; then
|
|
63
|
+
echo "No formatting or lint fixes needed."
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
git config user.name "github-actions[bot]"
|
|
67
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
68
|
+
git commit -am "style: apply ruff --fix and format"
|
|
69
|
+
git push
|
|
@@ -29,6 +29,9 @@ jobs:
|
|
|
29
29
|
- name: Lint
|
|
30
30
|
run: uv run -p ${{ matrix.python-version }} --extra dev ruff check src tests
|
|
31
31
|
|
|
32
|
+
- name: Format
|
|
33
|
+
run: uv run -p ${{ matrix.python-version }} --extra dev ruff format --check src tests
|
|
34
|
+
|
|
32
35
|
- name: Type check
|
|
33
36
|
run: uv run -p ${{ matrix.python-version }} --extra dev mypy
|
|
34
37
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Pre-commit hooks: keep every commit lint-clean, formatted, and type-checked.
|
|
2
|
+
# Install once with `uv run --extra dev pre-commit install` (or `pre-commit install`).
|
|
3
|
+
# Run against everything with `uv run --extra dev pre-commit run --all-files`.
|
|
4
|
+
#
|
|
5
|
+
# Keep the ruff rev in sync with the `ruff>=…` pin in pyproject.toml's [dev] extra.
|
|
6
|
+
repos:
|
|
7
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
8
|
+
rev: v0.15.20
|
|
9
|
+
hooks:
|
|
10
|
+
# Lint and auto-fix (import sorting, pyupgrade, simplify, …), then format.
|
|
11
|
+
- id: ruff-check
|
|
12
|
+
args: [--fix]
|
|
13
|
+
- id: ruff-format
|
|
14
|
+
|
|
15
|
+
# mypy runs from the project's own dev environment so it sees typer/questionary
|
|
16
|
+
# stubs and the [tool.mypy] config (files = ["src"]). pass_filenames is off
|
|
17
|
+
# because that config already selects the files to check.
|
|
18
|
+
- repo: local
|
|
19
|
+
hooks:
|
|
20
|
+
- id: mypy
|
|
21
|
+
name: mypy
|
|
22
|
+
entry: uv run --extra dev mypy
|
|
23
|
+
language: system
|
|
24
|
+
types: [python]
|
|
25
|
+
pass_filenames: false
|
|
@@ -19,9 +19,11 @@ template it provisions is separately pinned to CPython 3.14.6).
|
|
|
19
19
|
uv run treebox ... # run the CLI from the working tree
|
|
20
20
|
uv run --extra dev python -m pytest # full unit + integration suite
|
|
21
21
|
uv run --extra dev python -m pytest tests/test_units.py::test_name # single test (or -k <pattern>)
|
|
22
|
-
ruff check src tests
|
|
22
|
+
uv run --extra dev ruff check src tests # lint
|
|
23
|
+
uv run --extra dev ruff format --check src tests # format check (drop --check to apply)
|
|
23
24
|
uv run --extra dev mypy # type check (config in pyproject.toml)
|
|
24
|
-
|
|
25
|
+
uv run --extra dev pre-commit install # install lint/format/type hooks (see CONTRIBUTING.md)
|
|
26
|
+
./scripts/validate.sh # lint + format + tests + live host-runner smoke (real uv, throwaway local remote)
|
|
25
27
|
uv pip install -e ".[dev]" # editable dev environment
|
|
26
28
|
uv run --extra docs mkdocs serve # docs site (docs/ + mkdocs.yml), live-reloading
|
|
27
29
|
uv run --extra docs mkdocs build --strict # build docs to site/ (gitignored)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
treebox is a small, one-maintainer project. Contributions are welcome, but the scope is intentionally modest.
|
|
4
|
+
|
|
5
|
+
## Good contributions
|
|
6
|
+
|
|
7
|
+
- Bug fixes with a clear reproduction.
|
|
8
|
+
- Documentation fixes that make install, usage, or runner behavior clearer.
|
|
9
|
+
- Small compatibility fixes that preserve the existing CLI and scripting behavior.
|
|
10
|
+
|
|
11
|
+
For larger feature ideas, open an issue first so we can decide whether they fit the project.
|
|
12
|
+
|
|
13
|
+
## Local checks
|
|
14
|
+
|
|
15
|
+
Install the pre-commit hooks once so every commit is auto-formatted, lint-fixed,
|
|
16
|
+
and type-checked (`ruff format`, `ruff check --fix`, and `mypy`):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv run --extra dev pre-commit install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Note: `pre-commit install` has no effect inside a treebox worktree, because
|
|
23
|
+
treebox sets `core.hooksPath` for its own pre-push guard, so git ignores the
|
|
24
|
+
hook pre-commit writes to `.git/hooks`. Run the hooks manually there with
|
|
25
|
+
`uv run --extra dev pre-commit run --all-files`.
|
|
26
|
+
|
|
27
|
+
If the hooks are skipped, the Autofix CI workflow applies the same `ruff --fix`
|
|
28
|
+
and `ruff format` to same-repo PRs and pushes the result back. Note that pushes
|
|
29
|
+
made with the default `GITHUB_TOKEN` do not re-trigger CI, so if this repo ever
|
|
30
|
+
requires the CI status checks under branch protection, the auto-fixed commit
|
|
31
|
+
will have no reported checks and the PR will stall; the remedy is to push from
|
|
32
|
+
the workflow using a PAT or GitHub App token stored as a repo secret (e.g.
|
|
33
|
+
`AUTOFIX_TOKEN`) instead of `GITHUB_TOKEN`.
|
|
34
|
+
|
|
35
|
+
For code changes, run the relevant checks before opening a PR:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv run --extra dev python -m pytest
|
|
39
|
+
uv run --extra dev ruff check src tests
|
|
40
|
+
uv run --extra dev ruff format --check src tests
|
|
41
|
+
uv run --extra dev mypy
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For docs-only changes, this is enough:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv run --extra docs mkdocs build --strict
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Boundaries
|
|
51
|
+
|
|
52
|
+
Please preserve the basics: fresh refs by default, no trust in target-repo sandbox config, stable CLI output for scripts, and subscription-based agent auth.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: treebox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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/
|
|
@@ -23,9 +23,11 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
23
23
|
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
24
24
|
Requires-Python: >=3.11
|
|
25
25
|
Requires-Dist: questionary<3,>=2
|
|
26
|
+
Requires-Dist: rich<16,>=13
|
|
26
27
|
Requires-Dist: typer<1.0,>=0.12
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: mypy>=1.14; extra == 'dev'
|
|
30
|
+
Requires-Dist: pre-commit>=4; extra == 'dev'
|
|
29
31
|
Requires-Dist: pytest-cov>=6; extra == 'dev'
|
|
30
32
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
33
|
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
@@ -149,7 +151,7 @@ the branch starts as a `treebox/<name>` placeholder that a per-worktree
|
|
|
149
151
|
pre-push guard keeps **un-pushable** — rename it conventionally
|
|
150
152
|
(`git branch -m feature/user-auth`, `fix/login-race`, `chore/bump-deps`, …)
|
|
151
153
|
when the work has a shape, then push. So a machine-generated name can never
|
|
152
|
-
become a PR title.
|
|
154
|
+
become a PR title. `--checkout` is the one path that skips the placeholder: it checks out an
|
|
153
155
|
existing branch exactly.
|
|
154
156
|
|
|
155
157
|
`--base` takes any branch, not just `main` — branch off `dev`, or stack a new
|
|
@@ -157,10 +159,11 @@ worktree on top of an existing PR's branch, even while that branch is checked
|
|
|
157
159
|
out in another worktree. It resolves as the freshly fetched `origin/<base>`,
|
|
158
160
|
so push the base first if its latest commits only exist locally.
|
|
159
161
|
|
|
160
|
-
**Enter.** Come back to an existing worktree
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
**Enter.** Come back to an existing worktree. By default it reuses the harness
|
|
163
|
+
the worktree was created with; an explicit `--harness` overrides it for that
|
|
164
|
+
session only, without changing what's recorded on disk. The ref is the name,
|
|
165
|
+
the *current* branch (renames are followed live), or a unique substring of
|
|
166
|
+
either. Dependencies re-sync only if the lockfile changed since last time:
|
|
164
167
|
|
|
165
168
|
```bash
|
|
166
169
|
treebox enter fix-auth --harness claude
|
|
@@ -207,8 +210,9 @@ conflict). Full reference in the
|
|
|
207
210
|
hooks are ignored.
|
|
208
211
|
- **Credentials go in as scoped copies.** Only the agents' login files are
|
|
209
212
|
copied into a throwaway per-worktree dir — never the live `~/.claude` /
|
|
210
|
-
`~/.codex` —
|
|
211
|
-
|
|
213
|
+
`~/.codex` — refreshed on every entry so a host logout or a fresh login
|
|
214
|
+
reaches the sandbox next time, and treebox uses your subscription login,
|
|
215
|
+
never `ANTHROPIC_API_KEY`.
|
|
212
216
|
|
|
213
217
|
More in [how it works](https://seth-peters.github.io/treebox/how-it-works/).
|
|
214
218
|
|
|
@@ -227,13 +231,47 @@ All keys, shared-cache overrides, setup hooks, and sandbox templates are
|
|
|
227
231
|
covered in the
|
|
228
232
|
[configuration guide](https://seth-peters.github.io/treebox/configuration/).
|
|
229
233
|
|
|
234
|
+
## Customizing isolation
|
|
235
|
+
|
|
236
|
+
`docker` isolation is defined by an **operator-owned template** — a
|
|
237
|
+
`Dockerfile` + `container.json` you copy out and edit, kept beside the worktree
|
|
238
|
+
so a boxed agent can't touch its own cage. The shipped image bundles Node 22,
|
|
239
|
+
uv, `gh`, ripgrep, and the agent CLIs, so a Node project often needs no image
|
|
240
|
+
change. For a genuinely separate, non-Python box, copy the template and edit
|
|
241
|
+
two things — global tooling in the `Dockerfile`, and `postCreate` to install
|
|
242
|
+
your repo's deps (in docker, setup runs `postCreate`, not the host-mode
|
|
243
|
+
ecosystem auto-detect):
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
cp -R "$(python -c 'import treebox.assets, pathlib; print(pathlib.Path(treebox.assets.__file__).parent)')/container" \
|
|
247
|
+
~/.config/treebox/templates/node
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```dockerfile
|
|
251
|
+
# ~/.config/treebox/templates/node/Dockerfile — Node 22 is already baked in
|
|
252
|
+
USER root
|
|
253
|
+
RUN npm install -g pnpm@9 typescript tsx
|
|
254
|
+
USER ${USERNAME}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
// ~/.config/treebox/templates/node/container.json
|
|
259
|
+
"postCreate": "if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else npm install; fi"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Then select it per run (`--isolation docker --template node`) or set
|
|
263
|
+
`template = "node"` in your config — a `node` box and a `python` box coexist,
|
|
264
|
+
one per stack. Full walkthrough in the
|
|
265
|
+
[configuration guide](https://seth-peters.github.io/treebox/configuration/#worked-example-a-node-sandbox-not-python).
|
|
266
|
+
|
|
230
267
|
## Development
|
|
231
268
|
|
|
232
269
|
```bash
|
|
233
270
|
git clone https://github.com/Seth-Peters/treebox && cd treebox
|
|
234
271
|
uv run treebox ... # run the CLI from the working tree
|
|
272
|
+
uv run --extra dev pre-commit install # lint/format/type hooks (see CONTRIBUTING.md)
|
|
235
273
|
uv run --extra dev python -m pytest # unit + integration suite
|
|
236
|
-
./scripts/validate.sh # lint + tests + live host-runner smoke
|
|
274
|
+
./scripts/validate.sh # lint + format + tests + live host-runner smoke
|
|
237
275
|
```
|
|
238
276
|
|
|
239
277
|
## Contributing
|
|
@@ -114,7 +114,7 @@ the branch starts as a `treebox/<name>` placeholder that a per-worktree
|
|
|
114
114
|
pre-push guard keeps **un-pushable** — rename it conventionally
|
|
115
115
|
(`git branch -m feature/user-auth`, `fix/login-race`, `chore/bump-deps`, …)
|
|
116
116
|
when the work has a shape, then push. So a machine-generated name can never
|
|
117
|
-
become a PR title.
|
|
117
|
+
become a PR title. `--checkout` is the one path that skips the placeholder: it checks out an
|
|
118
118
|
existing branch exactly.
|
|
119
119
|
|
|
120
120
|
`--base` takes any branch, not just `main` — branch off `dev`, or stack a new
|
|
@@ -122,10 +122,11 @@ worktree on top of an existing PR's branch, even while that branch is checked
|
|
|
122
122
|
out in another worktree. It resolves as the freshly fetched `origin/<base>`,
|
|
123
123
|
so push the base first if its latest commits only exist locally.
|
|
124
124
|
|
|
125
|
-
**Enter.** Come back to an existing worktree
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
**Enter.** Come back to an existing worktree. By default it reuses the harness
|
|
126
|
+
the worktree was created with; an explicit `--harness` overrides it for that
|
|
127
|
+
session only, without changing what's recorded on disk. The ref is the name,
|
|
128
|
+
the *current* branch (renames are followed live), or a unique substring of
|
|
129
|
+
either. Dependencies re-sync only if the lockfile changed since last time:
|
|
129
130
|
|
|
130
131
|
```bash
|
|
131
132
|
treebox enter fix-auth --harness claude
|
|
@@ -172,8 +173,9 @@ conflict). Full reference in the
|
|
|
172
173
|
hooks are ignored.
|
|
173
174
|
- **Credentials go in as scoped copies.** Only the agents' login files are
|
|
174
175
|
copied into a throwaway per-worktree dir — never the live `~/.claude` /
|
|
175
|
-
`~/.codex` —
|
|
176
|
-
|
|
176
|
+
`~/.codex` — refreshed on every entry so a host logout or a fresh login
|
|
177
|
+
reaches the sandbox next time, and treebox uses your subscription login,
|
|
178
|
+
never `ANTHROPIC_API_KEY`.
|
|
177
179
|
|
|
178
180
|
More in [how it works](https://seth-peters.github.io/treebox/how-it-works/).
|
|
179
181
|
|
|
@@ -192,13 +194,47 @@ All keys, shared-cache overrides, setup hooks, and sandbox templates are
|
|
|
192
194
|
covered in the
|
|
193
195
|
[configuration guide](https://seth-peters.github.io/treebox/configuration/).
|
|
194
196
|
|
|
197
|
+
## Customizing isolation
|
|
198
|
+
|
|
199
|
+
`docker` isolation is defined by an **operator-owned template** — a
|
|
200
|
+
`Dockerfile` + `container.json` you copy out and edit, kept beside the worktree
|
|
201
|
+
so a boxed agent can't touch its own cage. The shipped image bundles Node 22,
|
|
202
|
+
uv, `gh`, ripgrep, and the agent CLIs, so a Node project often needs no image
|
|
203
|
+
change. For a genuinely separate, non-Python box, copy the template and edit
|
|
204
|
+
two things — global tooling in the `Dockerfile`, and `postCreate` to install
|
|
205
|
+
your repo's deps (in docker, setup runs `postCreate`, not the host-mode
|
|
206
|
+
ecosystem auto-detect):
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
cp -R "$(python -c 'import treebox.assets, pathlib; print(pathlib.Path(treebox.assets.__file__).parent)')/container" \
|
|
210
|
+
~/.config/treebox/templates/node
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```dockerfile
|
|
214
|
+
# ~/.config/treebox/templates/node/Dockerfile — Node 22 is already baked in
|
|
215
|
+
USER root
|
|
216
|
+
RUN npm install -g pnpm@9 typescript tsx
|
|
217
|
+
USER ${USERNAME}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
// ~/.config/treebox/templates/node/container.json
|
|
222
|
+
"postCreate": "if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else npm install; fi"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Then select it per run (`--isolation docker --template node`) or set
|
|
226
|
+
`template = "node"` in your config — a `node` box and a `python` box coexist,
|
|
227
|
+
one per stack. Full walkthrough in the
|
|
228
|
+
[configuration guide](https://seth-peters.github.io/treebox/configuration/#worked-example-a-node-sandbox-not-python).
|
|
229
|
+
|
|
195
230
|
## Development
|
|
196
231
|
|
|
197
232
|
```bash
|
|
198
233
|
git clone https://github.com/Seth-Peters/treebox && cd treebox
|
|
199
234
|
uv run treebox ... # run the CLI from the working tree
|
|
235
|
+
uv run --extra dev pre-commit install # lint/format/type hooks (see CONTRIBUTING.md)
|
|
200
236
|
uv run --extra dev python -m pytest # unit + integration suite
|
|
201
|
-
./scripts/validate.sh # lint + tests + live host-runner smoke
|
|
237
|
+
./scripts/validate.sh # lint + format + tests + live host-runner smoke
|
|
202
238
|
```
|
|
203
239
|
|
|
204
240
|
## Contributing
|
|
@@ -40,7 +40,7 @@ command-line flag > config.toml > built-in default
|
|
|
40
40
|
| Key | Default | What it controls |
|
|
41
41
|
| ---------- | -------------------- | --------------------------------------------------------- |
|
|
42
42
|
| `isolation`| `host` | Where agents run: the worktree shell, or a docker sandbox. |
|
|
43
|
-
| `harness` | `claude` | Which agent `create
|
|
43
|
+
| `harness` | `claude` | Which agent `create` launches by default; `enter` reuses the harness the worktree was created with unless `--harness` overrides it. |
|
|
44
44
|
| `base` | `main` | Base branch for new branches (resolved as `origin/<base>`). |
|
|
45
45
|
| `root` | `.treebox/worktrees` | Where worktree directories are created, relative to the repo. |
|
|
46
46
|
| `env_file` | `.env` | The secrets file copied into every new worktree. |
|
|
@@ -66,7 +66,9 @@ cp -R "$(python -c 'import treebox.assets, pathlib; print(pathlib.Path(treebox.a
|
|
|
66
66
|
syntax, with `${workspaceName}` substituted per worktree), and `runArgs` feed
|
|
67
67
|
`docker run`; `postCreate` is the setup command exec'd in the workspace after
|
|
68
68
|
the container starts. The firewall overlay in `firewall.json` deep-merges on
|
|
69
|
-
top when
|
|
69
|
+
top when the firewall is enabled (`firewall = true` in the config, or
|
|
70
|
+
`--firewall` per run; `--no-firewall` opts a single run out of a config
|
|
71
|
+
default).
|
|
70
72
|
|
|
71
73
|
Templates in `~/.config/treebox/templates/<name>` are picked by the
|
|
72
74
|
`template` config key or per-invocation with `--template <name>`. The
|
|
@@ -75,11 +77,51 @@ mount — so a sandboxed agent can't edit it. Any container config in the
|
|
|
75
77
|
target repo itself is deliberately ignored; see
|
|
76
78
|
[how it works](how-it-works.md#the-sandbox-config-lives-outside-the-box).
|
|
77
79
|
|
|
80
|
+
### Worked example: a Node sandbox, not Python
|
|
81
|
+
|
|
82
|
+
Say your project is Node and you want a box with nothing to do with the
|
|
83
|
+
default Python one. Keep a separate named template and select it per run —
|
|
84
|
+
a `node` box and a `python` box coexist, one per stack. Copy the template out:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cp -R "$(python -c 'import treebox.assets, pathlib; print(pathlib.Path(treebox.assets.__file__).parent)')/container" \
|
|
88
|
+
~/.config/treebox/templates/node
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The shipped image already bundles Node 22 (plus uv, `gh`, ripgrep, and the
|
|
92
|
+
agent CLIs), so a Node project often needs no image change at all. Add any
|
|
93
|
+
**global** tooling to `~/.config/treebox/templates/node/Dockerfile`:
|
|
94
|
+
|
|
95
|
+
```dockerfile
|
|
96
|
+
USER root
|
|
97
|
+
RUN npm install -g pnpm@9 typescript tsx
|
|
98
|
+
USER ${USERNAME}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then the one docker-specific step: point `postCreate` in `container.json` at
|
|
102
|
+
your project's install command. In docker isolation, dependency setup is
|
|
103
|
+
driven by the template's `postCreate` — not the host-mode ecosystem
|
|
104
|
+
auto-detect — so a non-Python project must wire its own install here (the
|
|
105
|
+
npm/pnpm cache is mounted, so it stays warm across worktrees):
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
"postCreate": "if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else npm install; fi"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Now provision with it, per-invocation or as your default (`template = "node"`):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
treebox create my-feature --isolation docker --template node
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
First run builds your image (cached after that), provisions the worktree, runs
|
|
118
|
+
`postCreate` to install deps, and launches the agent inside.
|
|
119
|
+
|
|
78
120
|
## Environment variables
|
|
79
121
|
|
|
80
122
|
| Variable | Effect |
|
|
81
123
|
| ----------------- | ------------------------------------------------------------- |
|
|
82
|
-
| `TREEBOX_CONFIG` | Explicit path to the config file (overrides the XDG lookup).
|
|
124
|
+
| `TREEBOX_CONFIG` | Explicit path to the config file (overrides the XDG lookup). Setting it asserts the file exists — a missing file here is a loud error (exit `2`), not a silent fall-back to defaults. |
|
|
83
125
|
| `XDG_CONFIG_HOME` | Standard XDG base for `treebox/config.toml` and templates. |
|
|
84
126
|
| `XDG_CACHE_HOME` | Standard XDG base for the shared package caches treebox mounts. |
|
|
85
127
|
|
|
@@ -104,5 +104,7 @@ treebox assumes the caller is often another program:
|
|
|
104
104
|
- **`--json` with a `schemaVersion`** that only gains fields within a version
|
|
105
105
|
(git-porcelain discipline), plus `--print` and `--dry-run` for scripts that
|
|
106
106
|
want the commands, not the side effects.
|
|
107
|
-
- **Per-worktree locking**,
|
|
108
|
-
|
|
107
|
+
- **Per-worktree locking**, held by `create`, `enter`, and `teardown` alike,
|
|
108
|
+
so racing operations on one name — two `create fix-auth` calls, or a
|
|
109
|
+
`teardown fix-auth` against a concurrent provision — conflict cleanly (`5`)
|
|
110
|
+
instead of corrupting or half-removing a worktree.
|
|
@@ -28,13 +28,14 @@ sandbox.
|
|
|
28
28
|
<div class="tx-terminal">
|
|
29
29
|
<div class="tx-terminal__bar"><span class="tx-dot tx-dot--r"></span><span class="tx-dot tx-dot--y"></span><span class="tx-dot tx-dot--g"></span><button class="tx-copy" type="button" data-copy="treebox create --isolation docker" aria-label="Copy command"><svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path fill="currentColor" d="M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12z"/></svg><span class="tx-copy__done">✓</span></button></div>
|
|
30
30
|
<div class="tx-screen">
|
|
31
|
-
<div class="t-
|
|
31
|
+
<div class="t-scene" data-dur="8600">
|
|
32
|
+
<div class="t-line" style="--d:0s;--n:33"><span class="c-ok">$ </span><span class="t-type">treebox create --isolation docker</span></div>
|
|
32
33
|
<div class="t-line" style="--d:1.8s"> </div>
|
|
33
34
|
<div class="t-line" style="--d:1.8s"> <span class="c-acc">●</span> create <b>brave-otter</b></div>
|
|
34
35
|
<div class="t-line" style="--d:2.0s"> </div>
|
|
35
36
|
<div class="t-line" style="--d:2.0s"> <span class="c-dim">branch</span> treebox/brave-otter <span class="c-dim">· placeholder — rename before push</span></div>
|
|
36
37
|
<div class="t-line" style="--d:2.1s"> <span class="c-dim">base</span> main</div>
|
|
37
|
-
<div class="t-line" style="--d:2.2s"> <span class="c-dim">isolation</span>
|
|
38
|
+
<div class="t-line" style="--d:2.2s"> <span class="c-dim">isolation</span> docker <span class="c-dim">→</span> claude</div>
|
|
38
39
|
<div class="t-line" style="--d:2.4s"> </div>
|
|
39
40
|
<div class="t-line" style="--d:2.4s;--run:.8s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> fetch <span class="c-dim">origin up to date</span></div>
|
|
40
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>
|
|
@@ -45,6 +46,58 @@ sandbox.
|
|
|
45
46
|
<div class="t-line" style="--d:6.9s"> </div>
|
|
46
47
|
<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>
|
|
47
48
|
</div>
|
|
49
|
+
<div class="t-scene" data-dur="5800">
|
|
50
|
+
<div class="t-line" style="--d:0s;--n:34"><span class="c-ok">$ </span><span class="t-type">treebox enter brave-otter -H codex</span></div>
|
|
51
|
+
<div class="t-line" style="--d:1.7s"> </div>
|
|
52
|
+
<div class="t-line" style="--d:1.7s"> <span class="c-acc">●</span> enter <b>brave-otter</b> <span class="c-dim">· feature/user-auth</span></div>
|
|
53
|
+
<div class="t-line" style="--d:1.9s"> </div>
|
|
54
|
+
<div class="t-line" style="--d:1.9s"> <span class="c-dim">isolation</span> docker <span class="c-dim">→</span> codex</div>
|
|
55
|
+
<div class="t-line" style="--d:2.1s"> </div>
|
|
56
|
+
<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.7s"> <span class="c-dim">· deps unchanged · skipping setup</span></div>
|
|
58
|
+
<div class="t-line" style="--d:2.9s"> </div>
|
|
59
|
+
<div class="t-line" style="--d:3.0s"> <span class="c-ok">Ready</span> <span class="c-dim">in 1.4s — launching</span> codex</div>
|
|
60
|
+
<div class="t-line" style="--d:4.4s"> </div>
|
|
61
|
+
<div class="t-line" style="--d:4.5s">^C</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="t-scene" data-dur="4800">
|
|
64
|
+
<div class="t-line" style="--d:0s;--n:35"><span class="c-ok">$ </span><span class="t-type">treebox enter brave-otter -H claude</span></div>
|
|
65
|
+
<div class="t-line" style="--d:1.8s"> </div>
|
|
66
|
+
<div class="t-line" style="--d:1.8s"> <span class="c-acc">●</span> enter <b>brave-otter</b> <span class="c-dim">· feature/user-auth</span></div>
|
|
67
|
+
<div class="t-line" style="--d:2.0s"> </div>
|
|
68
|
+
<div class="t-line" style="--d:2.0s"> <span class="c-dim">isolation</span> docker <span class="c-dim">→</span> claude</div>
|
|
69
|
+
<div class="t-line" style="--d:2.2s"> </div>
|
|
70
|
+
<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.8s"> <span class="c-dim">· deps unchanged · skipping setup</span></div>
|
|
72
|
+
<div class="t-line" style="--d:3.0s"> </div>
|
|
73
|
+
<div class="t-line" style="--d:3.1s"> <span class="c-ok">Ready</span> <span class="c-dim">in 1.2s — launching</span> claude</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="t-scene" data-dur="4800">
|
|
76
|
+
<div class="t-line" style="--d:0s;--n:10"><span class="c-ok">$ </span><span class="t-type">treebox ls</span></div>
|
|
77
|
+
<div class="t-line" style="--d:1.0s"> </div>
|
|
78
|
+
<div class="t-line" style="--d:1.1s"> <span class="c-dim">NAME BRANCH LAST COMMIT AGE DEPS ENV</span></div>
|
|
79
|
+
<div class="t-line" style="--d:1.1s"> <span class="c-dim">─────────────────────────────────────────────────────────────────────────────</span></div>
|
|
80
|
+
<div class="t-line" style="--d:1.3s"> brave-otter feature/user-auth <span class="c-dim">feat: add signup flow 5m</span> <span class="c-ok">● fresh</span> <span class="c-ok">● present</span></div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="t-scene" data-dur="6600">
|
|
83
|
+
<div class="t-line" style="--d:0s;--n:22"><span class="c-ok">$ </span><span class="t-type">treebox rm brave-otter</span></div>
|
|
84
|
+
<div class="t-line" style="--d:1.4s;--td:2s">Remove worktree brave-otter? [y/N]: <span class="t-in">y</span></div>
|
|
85
|
+
<div class="t-line" style="--d:2.5s"> </div>
|
|
86
|
+
<div class="t-line" style="--d:2.5s"> <span class="c-acc">●</span> teardown <b>brave-otter</b></div>
|
|
87
|
+
<div class="t-line" style="--d:2.7s"> </div>
|
|
88
|
+
<div class="t-line" style="--d:2.8s;--run:.7s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> container <span class="c-dim">removed treebox-brave-otter</span></div>
|
|
89
|
+
<div class="t-line" style="--d:3.7s;--run:.25s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> sandbox <span class="c-dim">removed</span></div>
|
|
90
|
+
<div class="t-line" style="--d:4.1s;--run:.35s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> worktree <span class="c-dim">removed .treebox/worktrees/brave-otter</span></div>
|
|
91
|
+
<div class="t-line" style="--d:4.7s"> <span class="c-dim">· branch kept feature/user-auth</span></div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="t-scene" data-dur="5200">
|
|
94
|
+
<div class="t-line" style="--d:0s;--n:10"><span class="c-ok">$ </span><span class="t-type">treebox ls</span></div>
|
|
95
|
+
<div class="t-line" style="--d:0.9s"> </div>
|
|
96
|
+
<div class="t-line" style="--d:1.0s"> <span class="c-dim">no worktrees yet — create one with</span> <span class="c-acc">treebox create</span></div>
|
|
97
|
+
<div class="t-line" style="--d:1.8s"> </div>
|
|
98
|
+
<div class="t-line" style="--d:2.1s"><span class="c-ok">$ </span><span class="t-cur"></span></div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
48
101
|
</div>
|
|
49
102
|
|
|
50
103
|
No branch name needed up front: each worktree gets its own directory and a
|
|
@@ -140,7 +193,7 @@ Docker isolation needs exactly one extra thing: Docker — see
|
|
|
140
193
|
```bash
|
|
141
194
|
treebox doctor # verify the host is ready
|
|
142
195
|
treebox create # provision + launch claude (name generated)
|
|
143
|
-
treebox enter brave-otter --harness codex # re-enter later
|
|
196
|
+
treebox enter brave-otter --harness codex # re-enter later; -H overrides the recorded harness for this session
|
|
144
197
|
treebox list # what exists, what's stale
|
|
145
198
|
treebox teardown brave-otter --delete-branch
|
|
146
199
|
```
|
|
@@ -118,6 +118,7 @@ The `install.sh` script reads a few environment variables:
|
|
|
118
118
|
```bash
|
|
119
119
|
git clone https://github.com/Seth-Peters/treebox && cd treebox
|
|
120
120
|
uv run treebox ... # run the CLI from the working tree
|
|
121
|
+
uv run --extra dev pre-commit install # lint/format/type hooks (see CONTRIBUTING.md)
|
|
121
122
|
uv run --extra dev python -m pytest # unit + integration suite
|
|
122
|
-
./scripts/validate.sh # lint + tests + live host-runner smoke
|
|
123
|
+
./scripts/validate.sh # lint + format + tests + live host-runner smoke
|
|
123
124
|
```
|
|
@@ -4,30 +4,36 @@
|
|
|
4
4
|
* 2. Console code blocks: the Material copy button copies only the
|
|
5
5
|
* `$ `-prefixed commands, never the output printed under them.
|
|
6
6
|
* 3. ✓ / ✗ marks in console output get their CLI colors.
|
|
7
|
-
* 4. The hero terminal
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* 4. The hero terminal plays the full treebox lifecycle in a loop — create,
|
|
8
|
+
* enter (codex), ^C, enter (claude), ls, rm, done — one .t-scene per
|
|
9
|
+
* command, each shown for its data-dur, starting when the terminal
|
|
10
|
+
* scrolls into view. Without JS or with reduced motion, the first
|
|
11
|
+
* scene shows as a static frame instead.
|
|
10
12
|
* 5. A "Copy page" button at the top of every page copies that page's raw
|
|
11
13
|
* Markdown source (embedded by hooks/copy_page.py) — an agent-ready copy,
|
|
12
14
|
* entirely offline.
|
|
13
15
|
*/
|
|
14
16
|
(function () {
|
|
15
|
-
var PLAY_MS = 7000; // last line lands at ~6.9s
|
|
16
|
-
var HOLD_MS = 3800; // linger on the final frame before replaying
|
|
17
|
-
|
|
18
17
|
function animateTerminals() {
|
|
19
18
|
document.querySelectorAll(".tx-terminal").forEach(function (term) {
|
|
20
19
|
if (!term.querySelector(".tx-screen") || term.dataset.anim) return;
|
|
21
20
|
term.dataset.anim = "1";
|
|
22
21
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
22
|
+
var scenes = term.querySelectorAll(".t-scene");
|
|
23
|
+
if (!scenes.length) return;
|
|
23
24
|
term.classList.add("tx-anim");
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
-
if (!term.isConnected) return;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
function show(i) {
|
|
27
|
+
if (!term.isConnected) return; // page swapped out — stop the loop
|
|
28
|
+
scenes.forEach(function (s) {
|
|
29
|
+
s.classList.remove("is-on");
|
|
30
|
+
});
|
|
31
|
+
void term.offsetWidth; // reflush so the scene's animations restart from 0
|
|
32
|
+
scenes[i].classList.add("is-on");
|
|
33
|
+
var dur = parseInt(scenes[i].getAttribute("data-dur"), 10) || 5000;
|
|
34
|
+
setTimeout(function () {
|
|
35
|
+
show((i + 1) % scenes.length);
|
|
36
|
+
}, dur);
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
var io = new IntersectionObserver(
|
|
@@ -35,7 +41,8 @@
|
|
|
35
41
|
entries.forEach(function (e) {
|
|
36
42
|
if (e.isIntersecting) {
|
|
37
43
|
io.disconnect();
|
|
38
|
-
|
|
44
|
+
term.classList.add("is-playing");
|
|
45
|
+
show(0);
|
|
39
46
|
}
|
|
40
47
|
});
|
|
41
48
|
},
|