devstuff 1.15.1__tar.gz → 1.17.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.
- {devstuff-1.15.1 → devstuff-1.17.0}/.github/workflows/test-installs.yml +1 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/CHANGELOG.md +22 -0
- devstuff-1.17.0/CLAUDE.md +350 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/PKG-INFO +284 -3
- {devstuff-1.15.1 → devstuff-1.17.0}/README.md +283 -2
- devstuff-1.17.0/dev/Makefile +60 -0
- devstuff-1.17.0/docs/specs/README.md +42 -0
- devstuff-1.17.0/docs/specs/agent/development-plan.md +194 -0
- devstuff-1.17.0/docs/specs/agent/specifications.md +204 -0
- devstuff-1.17.0/docs/specs/agent/stack-decisions.md +177 -0
- devstuff-1.17.0/docs/specs/starship-config/development-plan.md +74 -0
- devstuff-1.17.0/docs/specs/starship-config/specifications.md +164 -0
- devstuff-1.17.0/docs/specs/starship-config/stack-decisions.md +187 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/pyproject.toml +1 -1
- devstuff-1.17.0/src/dev_setup/agent/__init__.py +6 -0
- devstuff-1.17.0/src/dev_setup/agent/approval.py +102 -0
- devstuff-1.17.0/src/dev_setup/agent/bridges.py +125 -0
- devstuff-1.17.0/src/dev_setup/agent/catalog.py +158 -0
- devstuff-1.17.0/src/dev_setup/agent/completion.py +50 -0
- devstuff-1.17.0/src/dev_setup/agent/config.py +170 -0
- devstuff-1.17.0/src/dev_setup/agent/loop.py +129 -0
- devstuff-1.17.0/src/dev_setup/agent/ollama.py +243 -0
- devstuff-1.17.0/src/dev_setup/agent/preflight.py +98 -0
- devstuff-1.17.0/src/dev_setup/agent/primitives.py +101 -0
- devstuff-1.17.0/src/dev_setup/agent/registry.py +156 -0
- devstuff-1.17.0/src/dev_setup/agent/sandbox.py +286 -0
- devstuff-1.17.0/src/dev_setup/agent/session.py +334 -0
- devstuff-1.17.0/src/dev_setup/agent/transcript.py +52 -0
- devstuff-1.17.0/src/dev_setup/agent/wizard.py +104 -0
- devstuff-1.17.0/src/dev_setup/agent_tools.schema.json +112 -0
- devstuff-1.17.0/src/dev_setup/agent_tools.yaml +145 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/cli.py +5 -0
- devstuff-1.17.0/src/dev_setup/commands/agent_cmd.py +143 -0
- devstuff-1.17.0/src/dev_setup/commands/configure_cmd.py +123 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/help_cmd.py +6 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/install_cmd.py +19 -1
- devstuff-1.17.0/src/dev_setup/configure/__init__.py +56 -0
- devstuff-1.17.0/src/dev_setup/configure/starship/__init__.py +1 -0
- devstuff-1.17.0/src/dev_setup/configure/starship/fonts.py +47 -0
- devstuff-1.17.0/src/dev_setup/configure/starship/model.py +850 -0
- devstuff-1.17.0/src/dev_setup/configure/starship/preview.py +157 -0
- devstuff-1.17.0/src/dev_setup/configure/starship/render.py +268 -0
- devstuff-1.17.0/src/dev_setup/configure/starship/wizard.py +396 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/function_runner.py +18 -2
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/tools.yaml +48 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/ui.py +4 -2
- devstuff-1.17.0/tests/integration/test_agent_smoke.py +107 -0
- devstuff-1.17.0/tests/integration/test_agent_tui.py +92 -0
- devstuff-1.17.0/tests/test_agent.py +379 -0
- devstuff-1.17.0/tests/test_agent_completion.py +197 -0
- devstuff-1.17.0/tests/test_agent_loop.py +368 -0
- devstuff-1.17.0/tests/test_agent_sandbox.py +348 -0
- devstuff-1.17.0/tests/test_agent_tools.py +417 -0
- devstuff-1.17.0/tests/test_agent_wizard.py +205 -0
- devstuff-1.17.0/tests/test_configure_starship.py +682 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/tests/test_functions.py +29 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/uv.lock +1 -1
- devstuff-1.15.1/CLAUDE.md +0 -185
- devstuff-1.15.1/dev/Makefile +0 -39
- {devstuff-1.15.1 → devstuff-1.17.0}/.claude/skills/run-stuff/SKILL.md +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/.claude/skills/run-stuff/smoke.sh +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/.github/workflows/bump.yml +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/.github/workflows/ci.yml +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/.github/workflows/publish.yml +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/.gitignore +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/dev/Dockerfile +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/dev/Dockerfile.ci +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/dev/docker-compose.yml +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/dev-setup +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/install.sh +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/__init__.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/__main__.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/base.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/catalog.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/__init__.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/add_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/catalog_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/delete_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/docs_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/functions_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/list_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/remove_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/run_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/skills_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/commands/update_cmd.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/functions.schema.json +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/functions.yaml +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/functions_catalog.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/functions_registry.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/generic.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/src/dev_setup/registry.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/tests/__init__.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/tests/integration/__init__.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/tests/integration/conftest.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/tests/integration/test_tools.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/tests/test_catalog.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/tests/test_generic.py +0 -0
- {devstuff-1.15.1 → devstuff-1.17.0}/tests/test_registry.py +0 -0
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## v1.17.0 (2026-07-31)
|
|
2
|
+
|
|
3
|
+
### Feat
|
|
4
|
+
|
|
5
|
+
- starship config font validation and install
|
|
6
|
+
- **configure**: add a starship prompt setup wizard with live preview
|
|
7
|
+
|
|
8
|
+
## v1.16.0 (2026-07-24)
|
|
9
|
+
|
|
10
|
+
### Feat
|
|
11
|
+
|
|
12
|
+
- **agent**: first-run setup wizard for agent configuration
|
|
13
|
+
- **agent**: multi-line input and slash completion in the REPL
|
|
14
|
+
- **agent**: milestone 4 — transcripts, /history, docs, schema
|
|
15
|
+
- **agent**: complete milestone 2 — tools, sandbox denylist, approval loop
|
|
16
|
+
- **agent**: add workspace sandbox, launch guard, and smoke suite
|
|
17
|
+
- **agent**: add ollama-backed agent scaffold (milestone 1)
|
|
18
|
+
|
|
19
|
+
### Fix
|
|
20
|
+
|
|
21
|
+
- **agent**: correct argument mapping and output capture for function tools
|
|
22
|
+
|
|
1
23
|
## v1.15.1 (2026-07-20)
|
|
2
24
|
|
|
3
25
|
### Fix
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`dev-setup` is a Python CLI (Click + Rich + questionary) that installs, removes, and tracks
|
|
8
|
+
developer tools on Linux. There is no per-tool Python code — every tool (built-in or
|
|
9
|
+
user-added) is a data record in a YAML catalog, executed by one generic engine
|
|
10
|
+
(`GenericTool` in `src/dev_setup/generic.py`). Adding a tool is a YAML edit, not a code change;
|
|
11
|
+
adding a new *install mechanism* (a "type") is a code change touched in ~5 places (see below).
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv run dev-setup <cmd> # run from source (repo root)
|
|
17
|
+
./dev-setup <cmd> # bash wrapper — bootstraps .venv on first run, then execs Python
|
|
18
|
+
uv run pytest # unit tests only (integration tests skipped by default, ~0.3s)
|
|
19
|
+
uv run pytest -m integration # real installs — requires sudo + network, run inside Docker (see below)
|
|
20
|
+
uv run pytest tests/test_catalog.py::test_user_catalog_overrides_bundled_tool_in_place # single test
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`pip` is not available in this dev environment — `uv pip install -e .` / `uv run` are the way in.
|
|
24
|
+
Interactive commands (`install`, `add`, `remove`, `delete`, `configure`) open questionary/click
|
|
25
|
+
prompts and hang in non-TTY shells; don't try to drive them from a piped/non-interactive shell.
|
|
26
|
+
To exercise one end-to-end anyway, fork a pty (`pty.fork()`) and write keystrokes to the fd —
|
|
27
|
+
that is how the starship wizard's full flow was verified.
|
|
28
|
+
|
|
29
|
+
### Integration tests (real installs, Docker-isolated)
|
|
30
|
+
|
|
31
|
+
Integration tests actually install each builtin tool and assert `is_installed()` afterward. Run
|
|
32
|
+
via the Makefile in `dev/`, which builds the wheel and a throwaway CI image so nothing touches
|
|
33
|
+
the host:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd dev
|
|
37
|
+
make run-tests # all builtin tools
|
|
38
|
+
make run-tests TOOL=uv # just one tool's install test
|
|
39
|
+
make run-tests PYTEST_ARGS="-x --tb=long"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`tests/integration/test_tools.py` auto-parametrizes over every `builtin` registry entry except
|
|
43
|
+
those listed in its `_SKIP` dict (currently `docker`, `ollama` — can't run inside a container).
|
|
44
|
+
The `.github/workflows/test-installs.yml` weekly canary runs the same suite per-tool as a matrix
|
|
45
|
+
in fresh containers and files/updates a GitHub issue on failure.
|
|
46
|
+
|
|
47
|
+
### Releases
|
|
48
|
+
|
|
49
|
+
Versioning is Commitizen-driven (`[tool.commitizen]` in `pyproject.toml`, conventional commits,
|
|
50
|
+
`version_provider = "pep621"`). `.github/workflows/bump.yml` bumps the version and changelog on
|
|
51
|
+
merge to master; `publish.yml` ships to PyPI. Don't hand-edit the version in `pyproject.toml`.
|
|
52
|
+
|
|
53
|
+
## Architecture
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
src/dev_setup/
|
|
57
|
+
├── __main__.py # python -m dev_setup entry point
|
|
58
|
+
├── cli.py # Click group, command registration (see _register_commands)
|
|
59
|
+
├── base.py # Tool ABC (is_installed/install/remove), WhichTool, bashrc patch helpers
|
|
60
|
+
├── catalog.py # YAML load/validate/merge/import/export — the schema is enforced here
|
|
61
|
+
├── registry.py # Loads the effective catalog into a live in-memory Tool registry
|
|
62
|
+
├── generic.py # GenericTool — the ONE engine that implements every install type
|
|
63
|
+
├── tools.yaml # Bundled built-in catalog (core/tools/languages categories)
|
|
64
|
+
├── ui.py # Rich console + questionary wrappers (spinners, prompts, styled output)
|
|
65
|
+
├── configure/ # Per-tool setup wizards (see "Configurators" below)
|
|
66
|
+
└── commands/ # One Click command per file: list, install, remove, add, delete, docs, catalog
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Catalog precedence** (`catalog.load_effective_catalog`): bundled `tools.yaml` loads first →
|
|
70
|
+
user YAML at `~/.config/dev-setup/tools.yaml` overrides matching keys in place and appends
|
|
71
|
+
new ones. `registry.py` turns that merged dict into `GenericTool` instances; a tool is
|
|
72
|
+
`builtin` only if it came from bundled and has no user override.
|
|
73
|
+
|
|
74
|
+
**Execution model**: `GenericTool` is a dataclass; `install()`/`remove()`/`is_installed()`/
|
|
75
|
+
`update()` each look up `self.install_type` (`npm`, `pip`, `uvx`, `apt`, `git`, `script`,
|
|
76
|
+
`bash`) in a strategy-dispatch dict (`_INSTALLERS`/`_REMOVERS`/`_CHECKERS`/`_UPDATERS` in
|
|
77
|
+
`generic.py`) rather than an if/elif chain, and shell out via `subprocess`. `install()` raises
|
|
78
|
+
`RuntimeError`/`CalledProcessError` on failure — there's no result enum, command handlers just
|
|
79
|
+
catch and report. `bash`-type scripts are written to a temp file and run with `bash <file>` for
|
|
80
|
+
full parsing fidelity (not `bash -c "<string>"`). `dev-setup update` reuses the same dispatch
|
|
81
|
+
pattern for upgrading an already-installed tool (latest or a pinned version); for `script`/
|
|
82
|
+
`bash` types "update" is a full reinstall, since there's no narrower mechanism, so the command
|
|
83
|
+
layer confirms before re-running it.
|
|
84
|
+
|
|
85
|
+
**Two ways a tool gets defined**: built-in (an entry added directly to `src/dev_setup/tools.yaml`,
|
|
86
|
+
`builtin=True`) or custom (created via the `dev-setup add` wizard, `dev-setup catalog import`,
|
|
87
|
+
or hand-edited YAML, landing in the user catalog). Both use the identical schema — the only
|
|
88
|
+
difference is which file the key lives in and `category`.
|
|
89
|
+
|
|
90
|
+
## Adding a new built-in tool
|
|
91
|
+
|
|
92
|
+
Add an entry to `src/dev_setup/tools.yaml` using an existing `type` (`npm`, `pip`, `uvx`, `apt`,
|
|
93
|
+
`git`, `script`, `bash`) — see README.md "Custom packages → YAML schema" for the full field list
|
|
94
|
+
and per-type examples. Then:
|
|
95
|
+
- Add the key to `.github/workflows/test-installs.yml`'s matrix (or to `_SKIP` in
|
|
96
|
+
`tests/integration/test_tools.py` with a reason, if it can't run in CI).
|
|
97
|
+
- Add it to the relevant table in README.md ("Built-in packages").
|
|
98
|
+
- No Python code changes needed — `GenericTool` already knows how to run every existing type.
|
|
99
|
+
|
|
100
|
+
## Adding a new tool *type* (e.g. a `composer`/PHP-package type)
|
|
101
|
+
|
|
102
|
+
`php` itself (the PHP runtime) is already a built-in `bash`-type tool — what's *not* supported is
|
|
103
|
+
installing PHP packages via Composer as their own first-class type (analogous to how `npm` and
|
|
104
|
+
`uvx`/`pip` are first-class today). Adding a type like `composer` touches every layer:
|
|
105
|
+
|
|
106
|
+
1. **`catalog.py`** — add any new field names (e.g. `composer_name`) to `SUPPORTED_FIELDS`, and
|
|
107
|
+
if the type implies an auto-`requires` (like `npm` → `["nvm"]`, `pip`/`uvx` → `["uv"]`), add
|
|
108
|
+
that inference in both `validate_catalog()` and `GenericTool.__init__`/`to_dict()` (`generic.py`)
|
|
109
|
+
— these two must stay in sync or `to_dict()`'s "don't persist auto-inferred requires" logic
|
|
110
|
+
will drift from validation.
|
|
111
|
+
2. **`generic.py` `GenericTool`** — add the new field(s) to `__init__`, `from_dict`, `to_dict`;
|
|
112
|
+
add a branch in `is_installed()`, `install()`, `remove()`, and (if version detection needs
|
|
113
|
+
type-specific logic) `_type_cmd()`.
|
|
114
|
+
3. **`commands/add_cmd.py`** — add the type to the wizard's type list (`install_type = ui.select(...)`)
|
|
115
|
+
and add the type-specific prompt branch (mirrors the existing `npm`/`apt`/`git` branches).
|
|
116
|
+
4. **README.md** — add the type to the type table and the YAML schema field table, plus a
|
|
117
|
+
worked example under "Custom packages → Examples".
|
|
118
|
+
5. **Tests** — extend `tests/test_catalog.py` for schema validation of the new fields; if you add
|
|
119
|
+
a built-in tool using the new type, it's auto-picked-up by
|
|
120
|
+
`tests/integration/test_tools.py`'s parametrization (add to CI matrix / `_SKIP` as above).
|
|
121
|
+
|
|
122
|
+
## Functions/scripts (a separate subsystem from tools)
|
|
123
|
+
|
|
124
|
+
`src/dev_setup/functions.yaml` + `functions_catalog.py` + `functions_registry.py` +
|
|
125
|
+
`function_runner.py` are a parallel, independent catalog/registry from tools — functions
|
|
126
|
+
aren't installed/removed, they're invoked (`dev-setup run <key>`), so they get their own
|
|
127
|
+
schema instead of overloading `GenericTool`. Some duplication with `catalog.py`/`registry.py`
|
|
128
|
+
is deliberate (see "Key design decisions" below).
|
|
129
|
+
|
|
130
|
+
`src/dev_setup/functions.schema.json` is a hand-maintained JSON Schema documenting every
|
|
131
|
+
field for editor tooling (YAML language server autocomplete/validation) — it is **not**
|
|
132
|
+
loaded or enforced at runtime (no `jsonschema` dependency), so if you add/change a field or
|
|
133
|
+
a constraint in `functions_catalog.py`'s `validate_catalog()`, update the schema file too or
|
|
134
|
+
they'll silently drift apart.
|
|
135
|
+
|
|
136
|
+
**Why two function `type`s exist**: a `dev-setup` command is its own child process, so
|
|
137
|
+
anything it does with `subprocess` (env vars, `cd`, aliases) is invisible to the shell that
|
|
138
|
+
invoked it the moment the process exits. `type: script` is for functions that don't need to
|
|
139
|
+
mutate the calling shell (runs as a subprocess, like a tool's `install_script`). `type:
|
|
140
|
+
shell-eval` is for functions that must (`ssh-agent`, `nvm use`-style tools) — it has two
|
|
141
|
+
`register` modes:
|
|
142
|
+
- `register: bashrc` (default) — `dev-setup functions enable <key>` patches a real shell
|
|
143
|
+
function into `~/.bashrc` via `base.patch_bashrc`; the user calls it directly by name in a
|
|
144
|
+
new shell afterward. `dev-setup run` refuses to run these directly (there's nothing it
|
|
145
|
+
*can* do) and points at `functions enable` instead.
|
|
146
|
+
- `register: eval` — `dev-setup run <key>` prints resolved shell code to stdout for
|
|
147
|
+
`eval "$(dev-setup run key args)"`. This path must never print anything else to stdout
|
|
148
|
+
(no `ui.*` calls, no prompts) since it would corrupt what gets `eval`'d — missing required
|
|
149
|
+
params are reported on stderr and exit non-zero instead of being prompted for.
|
|
150
|
+
|
|
151
|
+
**Named params, not positional**: catalog `params` entries become named shell vars in the
|
|
152
|
+
script body (`"$key_path"`, not `$1`). `function_runner.py` injects a prelude mapping real
|
|
153
|
+
argv positions to those names for `script`/bashrc-registered functions (`key_path="$1"`); for
|
|
154
|
+
`register: eval`, which has no argv channel of its own once `eval`'d, it instead bakes the
|
|
155
|
+
already-resolved values in as shell-quoted literals (`key_path='/path/with spaces'`).
|
|
156
|
+
|
|
157
|
+
**Gotcha if you touch `render_bashrc_function`**: it must strip blank lines from the function
|
|
158
|
+
body. `remove_bashrc_block` (shared with tool bashrc patches) treats the first blank line
|
|
159
|
+
after its marker as the end of the block, so a blank line inside the rendered function would
|
|
160
|
+
make `functions disable` orphan everything after it — closing brace included.
|
|
161
|
+
|
|
162
|
+
Functions have a `category` field (defaults to `custom`, freeform — not an enum) that
|
|
163
|
+
`functions list` groups/sorts by, mirroring tools. A `script`-type function that shells out to
|
|
164
|
+
another CLI should guard on `command -v <tool>` and point at `dev-setup install <tool>` in the
|
|
165
|
+
error rather than let a raw "command not found" surface — see `validate-yaml`/`aws-saml-reauth`
|
|
166
|
+
in `functions.yaml`. If that CLI is only reachable via nvm (like `pi`), source
|
|
167
|
+
`"$HOME/.nvm/nvm.sh"` first (see `acc-check`) — `script`-type functions run via a non-login,
|
|
168
|
+
non-interactive `bash <tmpfile>`, so `~/.bashrc`/nvm's shell init never runs on their own.
|
|
169
|
+
|
|
170
|
+
Not yet built: an `add` wizard and `catalog import`/`export` for functions, analogous to the
|
|
171
|
+
ones tools already have.
|
|
172
|
+
|
|
173
|
+
## Configurators (`configure/`) — tool-specific wizards, deliberately *not* catalog-driven
|
|
174
|
+
|
|
175
|
+
`src/dev_setup/configure/` holds per-tool setup wizards (`devstuff configure <tool>`), registered
|
|
176
|
+
in a `CONFIGURATORS` dict keyed by catalog tool key. `starship` is the first one:
|
|
177
|
+
`configure/starship/{model,render,preview,wizard}.py`.
|
|
178
|
+
|
|
179
|
+
**Why this one breaks the YAML-catalog rule.** Installation generalises into ~7 mechanisms, which
|
|
180
|
+
is what makes `GenericTool` possible. Configuration does not: starship's config is a TOML file of
|
|
181
|
+
format strings, palette tables and powerline transition glyphs; another tool's would be shell
|
|
182
|
+
exports or `git config` calls. Expressing this wizard in YAML would need conditional questions, an
|
|
183
|
+
ordered section model with colour roles, and a template language — a programming language spelled
|
|
184
|
+
in YAML. So configurators are Python, dispatched from a dict, the same shape as `_INSTALLERS` in
|
|
185
|
+
`generic.py`. Full reasoning in `docs/specs/starship-config/stack-decisions.md` (SD-1).
|
|
186
|
+
|
|
187
|
+
A configurator that finds a missing *prerequisite* should offer to install it through
|
|
188
|
+
`install_cmd.install_by_key(key)` rather than telling the user to run a second command — that is
|
|
189
|
+
what the starship wizard does for `nerd-font`.
|
|
190
|
+
|
|
191
|
+
**Adding a configurator** — a new module plus one dict entry. The module must expose
|
|
192
|
+
`run(*, target: Path | None = None)` (returns `None` if the user cancelled; writes nothing until
|
|
193
|
+
they confirm) and `config_path() -> Path`. Everything generic — the picker, install-state check,
|
|
194
|
+
`--list`/`--path`/`--output`, and the post-install offer in `install_cmd.py` — reads the registry,
|
|
195
|
+
so none of it needs touching. There is deliberately **no `configure:` field in `tools.yaml`**: user
|
|
196
|
+
catalogs are strictly validated, so a catalog naming a nonexistent configurator would add a
|
|
197
|
+
load-time failure mode for nothing.
|
|
198
|
+
|
|
199
|
+
**Within the starship configurator, `model.py` is the data and everything else reads it.**
|
|
200
|
+
`SECTIONS` (ordered — declaration order *is* prompt order), `PALETTES` and `PRESETS` drive both the
|
|
201
|
+
TOML emitter and the offline preview, which is what stops those two from drifting. Adding a section
|
|
202
|
+
or palette is one entry; no other file changes.
|
|
203
|
+
|
|
204
|
+
**Things learned from the real binary — don't "simplify" these away:**
|
|
205
|
+
- `symbol` is emitted for every section whose body contains `$symbol`, *including as an empty
|
|
206
|
+
string*. Omit it and starship's built-in Nerd Font glyph leaks into the `plain` preset. The
|
|
207
|
+
converse is unit-tested: a section carrying an icon its body can't render is a bug (that's why
|
|
208
|
+
`directory`, which has no `symbol` key at all, has no icon).
|
|
209
|
+
- Not every module spells the colour key `style` — hence `Section.style_key` (`username` wants
|
|
210
|
+
`style_user`). `battery` is deliberately absent: it accepts neither `style` nor `symbol` (both
|
|
211
|
+
live in its `[[battery.display]]` array), so it would cost three quirk fields for one section.
|
|
212
|
+
- `kubernetes`, `time`, `azure`, `status` and `shlvl` ship *disabled*; listing them in `format`
|
|
213
|
+
isn't enough, they need `disabled = false`. `os` and `git_metrics` are absent for the same
|
|
214
|
+
reason `battery` is: `os` takes its symbol from an `[os.symbols]` distro table, `git_metrics`
|
|
215
|
+
has `added_style`/`deleted_style` instead of `style`.
|
|
216
|
+
- Format strings are emitted as TOML **literal** strings (`'…'`) so starship's `$`, `[`, and `\[`
|
|
217
|
+
grammar needs no escaping. The multi-line top-level `format` is the one basic string, and it
|
|
218
|
+
contains no backslashes of its own beyond the line continuations.
|
|
219
|
+
- Powerline transitions are emitted per **run of consecutive sections sharing a palette role**, not
|
|
220
|
+
per section — otherwise two adjacent language segments draw an arrow between two identical
|
|
221
|
+
backgrounds. The three powerline presets differ *only* in the four glyphs in their `Powerline`
|
|
222
|
+
record (`POWERLINES` in `model.py`); nothing in the emitter knows which variant it is drawing.
|
|
223
|
+
- Any literal text the emitter writes into a format string is escaped with `_escape_format()`.
|
|
224
|
+
This is not theoretical: `success_symbol = '[$](bold fg:ok)'` made starship read `$` as the start
|
|
225
|
+
of a variable name, so the `plain` preset shipped with *no prompt symbol at all* until it was
|
|
226
|
+
caught by sweeping every combination and asserting empty stderr.
|
|
227
|
+
- A custom module (`custom.compose`) is a normal `Section`, but its dotted key has to be written
|
|
228
|
+
`${custom.compose}` in `format` — hence `Section.ref`. It also runs a shell command on **every**
|
|
229
|
+
prompt, so it carries a `when` guard, and anything needing the network is disqualified.
|
|
230
|
+
- Symbols for new sections come from `starship print-config --default` — the module's own shipped
|
|
231
|
+
glyph — rather than being picked from a Nerd Font chart that can't be verified without the font.
|
|
232
|
+
- The sample project must never gain a `bun.lock`: starship's `nodejs` module lists it as a
|
|
233
|
+
*negative* detector, so it silently switches the Node section off in previews.
|
|
234
|
+
- The live preview runs `starship prompt` with `STARSHIP_SHELL=nu`, not `bash`: for bash, starship
|
|
235
|
+
wraps escapes in readline's `\[`/`\]` markers, which are invisible inside a `PS1` and print
|
|
236
|
+
literally anywhere else. It also overrides `PWD` (starship prefers it over the real cwd) and
|
|
237
|
+
makes a second `--right` call, since `right_format` is not part of the left prompt.
|
|
238
|
+
- Every preview failure path returns `None` and degrades to the offline renderer. A preview must
|
|
239
|
+
never be able to end the wizard.
|
|
240
|
+
- The Nerd Font gate (`configure/starship/fonts.py`) is allowed to answer "don't know": without
|
|
241
|
+
fontconfig there is nothing to enumerate, and `None` means *stay silent* rather than warn. It
|
|
242
|
+
also refuses to install over SSH — the glyphs are drawn by the client's terminal — and always
|
|
243
|
+
says that installing a font does not repoint the terminal at it. The install itself goes through
|
|
244
|
+
the ordinary catalog path (`nerd-font` in `tools.yaml` → `install_cmd.install_by_key`), never a
|
|
245
|
+
private download (SD-10).
|
|
246
|
+
|
|
247
|
+
Not yet built: configurators for anything other than starship, and round-tripping an existing
|
|
248
|
+
hand-edited config back into wizard state (the timestamped backup is the safety net instead).
|
|
249
|
+
|
|
250
|
+
## Specs (`docs/specs/`)
|
|
251
|
+
|
|
252
|
+
Design documents live in `docs/specs/<feature>/` — `specifications.md` (numbered, testable
|
|
253
|
+
requirements), `stack-decisions.md` (choices *and* rejected alternatives with reasons), and
|
|
254
|
+
`development-plan.md` (milestones, testing strategy, risks). See `docs/specs/README.md` for the
|
|
255
|
+
conventions.
|
|
256
|
+
|
|
257
|
+
When working on a feature that has a spec, read it first and **keep it current in the same PR** —
|
|
258
|
+
a spec that no longer matches the code is worse than no spec. Record resolved open questions with
|
|
259
|
+
a date and the answer rather than deleting them, and when a live finding contradicts a
|
|
260
|
+
requirement, update the requirement and note what was learned. New features of any size should
|
|
261
|
+
get a spec directory before implementation starts.
|
|
262
|
+
|
|
263
|
+
## The agent (`devstuff agent`) — a third catalog subsystem
|
|
264
|
+
|
|
265
|
+
`src/dev_setup/agent/` + `agent_tools.yaml` is an interactive session where a local Ollama model
|
|
266
|
+
calls devstuff's tools plus a workspace-scoped filesystem/shell kit. It follows the same
|
|
267
|
+
catalog-driven shape as tools and functions: `agent/catalog.py` validates `agent_tools.yaml`
|
|
268
|
+
(bundled → user override), `agent/registry.py` turns it into `AgentTool` objects, and
|
|
269
|
+
`agent/primitives.py` dispatches by `impl` through a `_PRIMITIVES` dict — the same
|
|
270
|
+
strategy-dispatch pattern as `_INSTALLERS` in `generic.py`.
|
|
271
|
+
|
|
272
|
+
**Adding an agent tool**: if it bridges to something that already exists (a catalog tool, a
|
|
273
|
+
`functions.yaml` entry), it is a pure YAML edit — `impl: catalog` or `impl: function` plus a
|
|
274
|
+
`target`. Only a genuinely new mechanism needs a new `impl: primitive` callable registered in
|
|
275
|
+
`_PRIMITIVES`. `type: script` functions are auto-exposed as `fn_<key>` tools with no edit at all.
|
|
276
|
+
|
|
277
|
+
**Security invariants — do not weaken these without deliberate thought:**
|
|
278
|
+
- `Workspace.resolve()` in `agent/sandbox.py` is the *only* thing standing between the model and
|
|
279
|
+
the filesystem. It resolves symlinks and `..` **before** the containment check; reordering that
|
|
280
|
+
reintroduces a symlink escape. Prompt instructions are not a control and never will be.
|
|
281
|
+
- The command denylist (`check_command`) runs **before** any confirmation prompt and is
|
|
282
|
+
deliberately not disabled by `--yolo`. The prompt is a human attention filter; attention
|
|
283
|
+
degrades over a session, the denylist does not.
|
|
284
|
+
- Credential dirs are blocked for **read** as well as write — exfiltrating an SSH key into a model
|
|
285
|
+
context is as bad as overwriting one. `~/.config/dev-setup` is readable but not writable, so
|
|
286
|
+
the agent cannot author catalogs (FR-14a).
|
|
287
|
+
- `assess()` (the launch guard) is advisory UX, not a control. Keep that distinction in comments;
|
|
288
|
+
the risk is a future reader mistaking a warning for enforcement.
|
|
289
|
+
|
|
290
|
+
**Everything in the loop returns errors to the model rather than raising.** Unknown tool, bad
|
|
291
|
+
arguments, sandbox refusal, a crashing tool — all become `role: tool` messages so the agent can
|
|
292
|
+
re-plan. A malformed tool call must never end a session. `max_iterations` is what stops a runaway.
|
|
293
|
+
|
|
294
|
+
**`cd` is a tool, not a shell command**, for the same reason `shell-eval` functions exist: each
|
|
295
|
+
`run_command` is its own subprocess, so a shell `cd` evaporates on exit. And `shell-eval`
|
|
296
|
+
functions are excluded from the toolbox entirely — they exist to mutate the calling shell, which
|
|
297
|
+
an agent subprocess has no way to do (mirroring the guard in `run_cmd.py`).
|
|
298
|
+
|
|
299
|
+
**Ollama response-shape handling all lives in `ollama.parse_message()`.** Builds differ on
|
|
300
|
+
whether reasoning arrives in `message.thinking` or as inline `<think>` tags in `content`, and
|
|
301
|
+
whether tool calls arrive in `tool_calls` or as JSON inside `content`. Think-stripping must stay
|
|
302
|
+
*ahead* of the content-JSON fallback, or a reasoning preamble hides the tool call. Keep new
|
|
303
|
+
quirks in that one function.
|
|
304
|
+
|
|
305
|
+
**Model choice is measured, not assumed.** `ollama show` reports a `capabilities` array;
|
|
306
|
+
preflight requires `tools` in it. The default (`gemma4:latest`) was picked by running the same
|
|
307
|
+
scaffolding prompt across local models — lfm2.5 had `write_file` available and still shelled out
|
|
308
|
+
to `echo >`, corrupting the content through shell quoting. If you change the default, re-run that
|
|
309
|
+
comparison rather than reasoning from parameter counts.
|
|
310
|
+
|
|
311
|
+
`agent_tools.schema.json` is hand-maintained for editor tooling and **not** enforced at runtime —
|
|
312
|
+
same arrangement, and same drift hazard, as `functions.schema.json`.
|
|
313
|
+
|
|
314
|
+
First-run UX: `agent/wizard.py` builds `agent.yaml` on the first interactive run (host → a
|
|
315
|
+
pick-list of tool-capable models → reasoning visibility), re-runnable via `devstuff agent
|
|
316
|
+
--setup`. It configures only those three fields deliberately — a first-run wizard asking about
|
|
317
|
+
`num_ctx` and timeouts would be worse than one asking nothing; everything else keeps its default
|
|
318
|
+
and is hand-editable.
|
|
319
|
+
|
|
320
|
+
Not yet built: an `add` wizard for agent tools, and `catalog import`/`export` for them.
|
|
321
|
+
|
|
322
|
+
## Key design decisions (don't relitigate these)
|
|
323
|
+
|
|
324
|
+
- **uv owns Python provisioning.** The bash wrapper only guarantees `uv` is present; Python
|
|
325
|
+
version and virtualenv management is delegated entirely to `uv run`.
|
|
326
|
+
- **Catalogs are the source of truth, not Python classes.** There is deliberately no per-tool
|
|
327
|
+
subclass — everything is `GenericTool` driven by YAML data, so adding a tool is a data change.
|
|
328
|
+
- **`install()`/`remove()` raise, they don't return status codes.** No `InstallResult` enum;
|
|
329
|
+
callers catch `RuntimeError`/`CalledProcessError`.
|
|
330
|
+
- **Invalid catalogs fail loudly at load time** — malformed YAML, unsupported version, unknown
|
|
331
|
+
fields, bad `requires` all raise `CatalogError` immediately rather than silently degrading.
|
|
332
|
+
- **Custom install/remove scripts are plain strings**, written to a temp file at run time, so
|
|
333
|
+
`bash` gets full script-parsing fidelity instead of `bash -c "..."` string quoting problems.
|
|
334
|
+
- **The agent's safety boundary is the workspace root, enforced in code.** Not a sandbox
|
|
335
|
+
technology (bubblewrap/firejail) and not model instructions — `Path.resolve()` containment plus
|
|
336
|
+
a command denylist, both unit-tested. Chosen so it works with zero new dependencies and fails
|
|
337
|
+
closed; if you want stronger isolation, add it *around* this, not instead of it.
|
|
338
|
+
- **No new runtime dependencies for the agent.** The Ollama transport is stdlib `urllib` against
|
|
339
|
+
`/api/chat`; the REPL uses `prompt_toolkit`, already vendored via questionary. devstuff is a
|
|
340
|
+
globally installed CLI, so every dependency is a cost paid by users who never run `agent`.
|
|
341
|
+
- **Configuration is Python, installation is YAML.** The no-per-tool-code rule covers *install
|
|
342
|
+
mechanisms*, which generalise; per-tool config formats don't. `configure/` is a registry of
|
|
343
|
+
Python wizards on purpose — see the section above before trying to fold it into `tools.yaml`.
|
|
344
|
+
- **A config wizard previews with the real binary, not an approximation.** `starship prompt`
|
|
345
|
+
against a temp config in a throwaway project is the source of truth; the offline renderer is a
|
|
346
|
+
labelled fallback for when the tool isn't installed (and is what unit tests exercise).
|
|
347
|
+
- **Functions get a parallel catalog/registry instead of extending `GenericTool`.** The
|
|
348
|
+
schemas diverge enough (no `requires` inference, a `params` list, no install/remove
|
|
349
|
+
lifecycle) that folding them into the tool catalog would be lossy; some duplication with
|
|
350
|
+
`catalog.py`/`registry.py` is an accepted tradeoff over a forced shared abstraction.
|