devstuff 2.0.0__tar.gz → 2.2.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-2.0.0 → devstuff-2.2.0}/CHANGELOG.md +12 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/CLAUDE.md +28 -1
- {devstuff-2.0.0 → devstuff-2.2.0}/PKG-INFO +54 -1
- {devstuff-2.0.0 → devstuff-2.2.0}/README.md +53 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/README.md +1 -0
- devstuff-2.2.0/docs/specs/verbose-mode/development-plan.md +50 -0
- devstuff-2.2.0/docs/specs/verbose-mode/specifications.md +96 -0
- devstuff-2.2.0/docs/specs/verbose-mode/stack-decisions.md +91 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/pyproject.toml +1 -1
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/cli.py +28 -1
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/install_cmd.py +1 -5
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/remove_cmd.py +1 -5
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/run_cmd.py +10 -1
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/update_cmd.py +1 -5
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/function_runner.py +27 -6
- devstuff-2.2.0/src/dev_setup/functions.yaml +240 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/generic.py +86 -69
- devstuff-2.2.0/src/dev_setup/verbose.py +148 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_functions.py +125 -0
- devstuff-2.2.0/tests/test_verbose.py +266 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/uv.lock +1 -1
- devstuff-2.0.0/src/dev_setup/functions.yaml +0 -111
- {devstuff-2.0.0 → devstuff-2.2.0}/.claude/skills/run-stuff/SKILL.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/.claude/skills/run-stuff/smoke.sh +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/.github/workflows/bump.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/.github/workflows/ci.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/.github/workflows/publish.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/.github/workflows/test-installs.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/.gitignore +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/dev/Dockerfile +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/dev/Dockerfile.ci +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/dev/Makefile +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/dev/docker-compose.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/devstuff +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/agent/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/agent/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/agent/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/ansible-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/ansible-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/ansible-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/bat-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/bat-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/bat-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/commitizen-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/commitizen-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/commitizen-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/docker-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/docker-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/docker-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/lazygit-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/lazygit-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/lazygit-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/precommit-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/precommit-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/precommit-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/starship-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/starship-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/starship-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/install.sh +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/__main__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/approval.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/bridges.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/catalog.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/completion.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/config.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/loop.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/ollama.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/preflight.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/primitives.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/registry.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/sandbox.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/session.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/transcript.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent_tools.schema.json +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent_tools.yaml +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/base.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/catalog.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/add_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/agent_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/catalog_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/configure_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/delete_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/docs_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/doctor_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/functions_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/help_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/list_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/skills_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/preview.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/fonts.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/preview.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/doctor.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/functions.schema.json +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/functions_catalog.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/functions_registry.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/registry.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/tools.yaml +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/ui.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/conftest.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/test_agent_smoke.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/test_agent_tui.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/test_commitizen_config.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/test_tools.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_completion.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_loop.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_sandbox.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_tools.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_catalog.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_ansible.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_bat.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_commitizen.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_docker.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_lazygit.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_precommit.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_starship.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_doctor.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_generic.py +0 -0
- {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_registry.py +0 -0
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## v2.2.0 (2026-08-05)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **cli**: add -v/-vv verbose mode across every command
|
|
6
|
+
|
|
7
|
+
## v2.1.0 (2026-08-04)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **functions**: add whats-on-port to find what is holding a port
|
|
12
|
+
|
|
1
13
|
## v2.0.0 (2026-08-04)
|
|
2
14
|
|
|
3
15
|
### BREAKING CHANGE
|
|
@@ -62,6 +62,7 @@ src/dev_setup/
|
|
|
62
62
|
├── generic.py # GenericTool — the ONE engine that implements every install type
|
|
63
63
|
├── tools.yaml # Bundled built-in catalog (core/tools/languages categories)
|
|
64
64
|
├── ui.py # Rich console + questionary wrappers (spinners, prompts, styled output)
|
|
65
|
+
├── verbose.py # Process-wide -v/-vv level + the stderr logger built on it
|
|
65
66
|
├── configure/ # Per-tool setup wizards (see "Configurators" below)
|
|
66
67
|
└── commands/ # One Click command per file: list, install, remove, add, delete, docs, catalog
|
|
67
68
|
```
|
|
@@ -82,6 +83,25 @@ pattern for upgrading an already-installed tool (latest or a pinned version); fo
|
|
|
82
83
|
`bash` types "update" is a full reinstall, since there's no narrower mechanism, so the command
|
|
83
84
|
layer confirms before re-running it.
|
|
84
85
|
|
|
86
|
+
**Verbosity** (`verbose.py`, spec in `docs/specs/verbose-mode/`): one process-wide level —
|
|
87
|
+
`0` / `-v` / `-vv` — set by a Click callback and read by the subprocess helpers, never threaded
|
|
88
|
+
through call signatures. Three things about it are load-bearing:
|
|
89
|
+
- **Every line goes to stderr.** `devstuff run` for a `register: eval` function puts shell code
|
|
90
|
+
on stdout for `eval "$(...)"`; a verbose line there is *code that gets executed*. The logger
|
|
91
|
+
has no stdout path at all rather than a check at each call site.
|
|
92
|
+
- **`cli._add_verbose_option` applies `-v` to every command and subcommand**, walking the group
|
|
93
|
+
tree, so `devstuff -v install x`, `devstuff install -v x` and `devstuff functions -v enable k`
|
|
94
|
+
all work. Don't go back to decorating commands individually — the first draft did, and `list`
|
|
95
|
+
was immediately missing it.
|
|
96
|
+
- **`generic.py` has exactly two subprocess helpers**: `_run` (state-changing — streams when
|
|
97
|
+
verbose, raises `RuntimeError`) and `_probe` (captures, never raises, logs at `-vv` only
|
|
98
|
+
because probes fire once per tool on every `list`). Anything new that shells out belongs in
|
|
99
|
+
one of them; a direct `subprocess.run` is a hole in the flag's coverage, which is exactly what
|
|
100
|
+
the old boolean `-v` was — it never reached npm/uvx/git installs at all. `verbose.step` is the
|
|
101
|
+
spinner-or-line swap, since a spinner repaints its line and can't share a terminal with
|
|
102
|
+
streaming output. At `-vv` script bodies run under `bash -x`, *except* eval-mode function
|
|
103
|
+
scripts — `set -x` there would persist in the caller's interactive shell.
|
|
104
|
+
|
|
85
105
|
**Two ways a tool gets defined**: built-in (an entry added directly to `src/dev_setup/tools.yaml`,
|
|
86
106
|
`builtin=True`) or custom (created via the `devstuff add` wizard, `devstuff catalog import`,
|
|
87
107
|
or hand-edited YAML, landing in the user catalog). Both use the identical schema — the only
|
|
@@ -165,7 +185,14 @@ another CLI should guard on `command -v <tool>` and point at `devstuff install <
|
|
|
165
185
|
error rather than let a raw "command not found" surface — see `validate-yaml`/`aws-saml-reauth`
|
|
166
186
|
in `functions.yaml`. If that CLI is only reachable via nvm (like `pi`), source
|
|
167
187
|
`"$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.
|
|
188
|
+
non-interactive `bash <tmpfile>`, so `~/.bashrc`/nvm's shell init never runs on their own. When
|
|
189
|
+
the CLI isn't a catalog tool at all, name the distro package instead (`whats-on-port` points at
|
|
190
|
+
`apt-get install iproute2`) — pointing at `devstuff install ss` would be a lie.
|
|
191
|
+
|
|
192
|
+
**`run_cmd` flattens every non-zero exit to 1**, so a `script` function cannot signal a *result*
|
|
193
|
+
through its exit code — only whether it ran. A "found nothing" answer should therefore exit 0 and
|
|
194
|
+
say so, or the user gets a red "command failed" banner under a correct result; keep non-zero for
|
|
195
|
+
"could not perform the lookup" (see `whats-on-port`'s comment on this).
|
|
169
196
|
|
|
170
197
|
Not yet built: an `add` wizard and `catalog import`/`export` for functions, analogous to the
|
|
171
198
|
ones tools already have.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devstuff
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Development environment setup CLI for Linux
|
|
5
5
|
Project-URL: Repository, https://github.com/thesawdawg/dev-setup-py
|
|
6
6
|
Author-email: Sawyer <sawyerksu@gmail.com>
|
|
@@ -193,6 +193,30 @@ The bash `./devstuff` script in the repo is a convenience runner for the git-clo
|
|
|
193
193
|
|
|
194
194
|
---
|
|
195
195
|
|
|
196
|
+
## Verbose output (`-v` / `-vv`)
|
|
197
|
+
|
|
198
|
+
Every command accepts `-v`, before or after the subcommand. devstuff is a thin layer over other
|
|
199
|
+
people's commands; this is how you see them.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
devstuff install lazygit -v # log each command and stream its output live
|
|
203
|
+
devstuff -vv run whats-on-port 8080 # + probes, exit codes, and bash -x line tracing
|
|
204
|
+
DEVSTUFF_VERBOSE=1 devstuff update # same as -v, for scripts and CI
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
| Level | What you get |
|
|
208
|
+
|-------|--------------|
|
|
209
|
+
| *(default)* | Normal output. Command output is captured and shown only if the command fails. |
|
|
210
|
+
| `-v` | Every command devstuff runs, printed as a line you can paste back into a shell, with its output streaming live. Spinners are replaced by plain lines so they don't fight with that output. |
|
|
211
|
+
| `-vv` | Plus read-only probes (version checks, install-state checks) with their exit codes and output, the body of any script before it runs, resolved function parameters, and `bash -x` tracing of every line inside function and installer scripts. |
|
|
212
|
+
|
|
213
|
+
`-vvv` and above are the same as `-vv`.
|
|
214
|
+
|
|
215
|
+
All of it goes to **stderr**, so it never contaminates anything you pipe or capture — including
|
|
216
|
+
`eval "$(devstuff run <shell-eval function>)"`, whose stdout has to be shell code and nothing else.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
196
220
|
## Commands
|
|
197
221
|
|
|
198
222
|
### `list`
|
|
@@ -1105,9 +1129,38 @@ ones tools already have — for now, custom functions are hand-edited YAML at
|
|
|
1105
1129
|
| `ssh-agent-key` | auth | shell-eval (bashrc) | Start ssh-agent in the current shell and add a key to it | `key_path` |
|
|
1106
1130
|
| `validate-docker-compose` | validation | script | Validate a docker-compose.yml file in the current directory | — |
|
|
1107
1131
|
| `validate-yaml` | validation | script | Validate a YAML file's syntax using `yq` | `file` |
|
|
1132
|
+
| `whats-on-port` | network | script | Find which process is listening on a port | `port`, `protocol` (optional: `tcp`/`udp`/`all`) |
|
|
1108
1133
|
| `acc-check` | web-dev | script | Run the pi coding agent's `/dogfood` skill against a web URL | `url`, `instruction` (optional) |
|
|
1109
1134
|
| `aws-saml-reauth` | web-dev | script | Reauthorize the AWS CLI via `saml2aws login --force` | `profile` (optional) |
|
|
1110
1135
|
|
|
1136
|
+
#### `whats-on-port`
|
|
1137
|
+
|
|
1138
|
+
```bash
|
|
1139
|
+
devstuff run whats-on-port 8080 # tcp and udp
|
|
1140
|
+
devstuff run whats-on-port 5432 tcp # one protocol
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
Prints the matching sockets, then the PID, user, elapsed time and **untruncated** command
|
|
1144
|
+
line of every process holding them — plural because `SO_REUSEPORT` lets several processes
|
|
1145
|
+
share one port, and "which of these five node processes" is usually the actual question.
|
|
1146
|
+
|
|
1147
|
+
It uses `ss` rather than `lsof` or `fuser` for one measured reason: **run unprivileged
|
|
1148
|
+
against another user's socket, `lsof` and `fuser` print nothing and exit as though the port
|
|
1149
|
+
were free.** `ss` still lists the socket, just without the `users:((...))` field — so the
|
|
1150
|
+
function can tell "nothing there" apart from "something there I'm not allowed to name", and
|
|
1151
|
+
says which. When the process is hidden it retries under `sudo -n`, and only if that needs no
|
|
1152
|
+
password; a diagnostic shouldn't stop to prompt for one. If sudo isn't available it says
|
|
1153
|
+
what to re-run.
|
|
1154
|
+
|
|
1155
|
+
"Nothing is listening" is reported as a success, not a failure, along with the two reasons a
|
|
1156
|
+
bind can still fail afterwards: a container port published with `userland-proxy: false` is
|
|
1157
|
+
NAT-forwarded with no host socket to find at all, and sockets in another network namespace
|
|
1158
|
+
are invisible. If Docker is running, published container ports are matched and named too —
|
|
1159
|
+
`docker-proxy`'s own command line doesn't say which container it belongs to.
|
|
1160
|
+
|
|
1161
|
+
`ss` comes from `iproute2`, which isn't a devstuff package and isn't on every minimal image,
|
|
1162
|
+
so a missing `ss` names the apt package instead of surfacing "command not found".
|
|
1163
|
+
|
|
1111
1164
|
---
|
|
1112
1165
|
|
|
1113
1166
|
## Built-in packages
|
|
@@ -166,6 +166,30 @@ The bash `./devstuff` script in the repo is a convenience runner for the git-clo
|
|
|
166
166
|
|
|
167
167
|
---
|
|
168
168
|
|
|
169
|
+
## Verbose output (`-v` / `-vv`)
|
|
170
|
+
|
|
171
|
+
Every command accepts `-v`, before or after the subcommand. devstuff is a thin layer over other
|
|
172
|
+
people's commands; this is how you see them.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
devstuff install lazygit -v # log each command and stream its output live
|
|
176
|
+
devstuff -vv run whats-on-port 8080 # + probes, exit codes, and bash -x line tracing
|
|
177
|
+
DEVSTUFF_VERBOSE=1 devstuff update # same as -v, for scripts and CI
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| Level | What you get |
|
|
181
|
+
|-------|--------------|
|
|
182
|
+
| *(default)* | Normal output. Command output is captured and shown only if the command fails. |
|
|
183
|
+
| `-v` | Every command devstuff runs, printed as a line you can paste back into a shell, with its output streaming live. Spinners are replaced by plain lines so they don't fight with that output. |
|
|
184
|
+
| `-vv` | Plus read-only probes (version checks, install-state checks) with their exit codes and output, the body of any script before it runs, resolved function parameters, and `bash -x` tracing of every line inside function and installer scripts. |
|
|
185
|
+
|
|
186
|
+
`-vvv` and above are the same as `-vv`.
|
|
187
|
+
|
|
188
|
+
All of it goes to **stderr**, so it never contaminates anything you pipe or capture — including
|
|
189
|
+
`eval "$(devstuff run <shell-eval function>)"`, whose stdout has to be shell code and nothing else.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
169
193
|
## Commands
|
|
170
194
|
|
|
171
195
|
### `list`
|
|
@@ -1078,9 +1102,38 @@ ones tools already have — for now, custom functions are hand-edited YAML at
|
|
|
1078
1102
|
| `ssh-agent-key` | auth | shell-eval (bashrc) | Start ssh-agent in the current shell and add a key to it | `key_path` |
|
|
1079
1103
|
| `validate-docker-compose` | validation | script | Validate a docker-compose.yml file in the current directory | — |
|
|
1080
1104
|
| `validate-yaml` | validation | script | Validate a YAML file's syntax using `yq` | `file` |
|
|
1105
|
+
| `whats-on-port` | network | script | Find which process is listening on a port | `port`, `protocol` (optional: `tcp`/`udp`/`all`) |
|
|
1081
1106
|
| `acc-check` | web-dev | script | Run the pi coding agent's `/dogfood` skill against a web URL | `url`, `instruction` (optional) |
|
|
1082
1107
|
| `aws-saml-reauth` | web-dev | script | Reauthorize the AWS CLI via `saml2aws login --force` | `profile` (optional) |
|
|
1083
1108
|
|
|
1109
|
+
#### `whats-on-port`
|
|
1110
|
+
|
|
1111
|
+
```bash
|
|
1112
|
+
devstuff run whats-on-port 8080 # tcp and udp
|
|
1113
|
+
devstuff run whats-on-port 5432 tcp # one protocol
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
Prints the matching sockets, then the PID, user, elapsed time and **untruncated** command
|
|
1117
|
+
line of every process holding them — plural because `SO_REUSEPORT` lets several processes
|
|
1118
|
+
share one port, and "which of these five node processes" is usually the actual question.
|
|
1119
|
+
|
|
1120
|
+
It uses `ss` rather than `lsof` or `fuser` for one measured reason: **run unprivileged
|
|
1121
|
+
against another user's socket, `lsof` and `fuser` print nothing and exit as though the port
|
|
1122
|
+
were free.** `ss` still lists the socket, just without the `users:((...))` field — so the
|
|
1123
|
+
function can tell "nothing there" apart from "something there I'm not allowed to name", and
|
|
1124
|
+
says which. When the process is hidden it retries under `sudo -n`, and only if that needs no
|
|
1125
|
+
password; a diagnostic shouldn't stop to prompt for one. If sudo isn't available it says
|
|
1126
|
+
what to re-run.
|
|
1127
|
+
|
|
1128
|
+
"Nothing is listening" is reported as a success, not a failure, along with the two reasons a
|
|
1129
|
+
bind can still fail afterwards: a container port published with `userland-proxy: false` is
|
|
1130
|
+
NAT-forwarded with no host socket to find at all, and sockets in another network namespace
|
|
1131
|
+
are invisible. If Docker is running, published container ports are matched and named too —
|
|
1132
|
+
`docker-proxy`'s own command line doesn't say which container it belongs to.
|
|
1133
|
+
|
|
1134
|
+
`ss` comes from `iproute2`, which isn't a devstuff package and isn't on every minimal image,
|
|
1135
|
+
so a missing `ss` names the apt package instead of surfacing "command not found".
|
|
1136
|
+
|
|
1084
1137
|
---
|
|
1085
1138
|
|
|
1086
1139
|
## Built-in packages
|
|
@@ -46,3 +46,4 @@ They are most useful when they capture:
|
|
|
46
46
|
| [`bat-config/`](bat-config/) — `devstuff configure bat`, the theme and decoration wizard | Complete (v1) |
|
|
47
47
|
| [`ansible-config/`](ansible-config/) — `devstuff configure ansible`, the ansible.cfg wizard | Complete (v1) |
|
|
48
48
|
| [`lazygit-config/`](lazygit-config/) — `devstuff configure lazygit`, the icons/pager/panels wizard | Complete (v1) |
|
|
49
|
+
| [`verbose-mode/`](verbose-mode/) — `-v`/`-vv`, the process-wide verbosity level | Complete (v1) |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Development plan: verbose mode
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-04
|
|
4
|
+
**Status:** Complete (v1)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Milestones
|
|
9
|
+
|
|
10
|
+
| # | Milestone | State |
|
|
11
|
+
|---|-----------|-------|
|
|
12
|
+
| 1 | `verbose.py`: level, env var, stderr printers, `step`, Click option factory | Done |
|
|
13
|
+
| 2 | `generic.py`: `_run` / `_probe` split; route every `subprocess.run` through one of them | Done |
|
|
14
|
+
| 3 | `generic.py`: swap spinner-plus-capture call sites (npm/uvx/git) onto `_run` + `verbose.step` | Done |
|
|
15
|
+
| 4 | `function_runner.py`: log resolved params, script body, `bash -x` at `-vv` | Done |
|
|
16
|
+
| 5 | `cli.py`: apply the option to the group and every command/subcommand recursively | Done |
|
|
17
|
+
| 6 | Tests (`tests/test_verbose.py`), README, CLAUDE.md | Done |
|
|
18
|
+
|
|
19
|
+
## Testing strategy
|
|
20
|
+
|
|
21
|
+
`tests/test_verbose.py`, 33 tests. The level is process-wide state, so an autouse fixture
|
|
22
|
+
resets it before and after every test — a leaked level would make unrelated tests print.
|
|
23
|
+
|
|
24
|
+
What is actually asserted, beyond the obvious:
|
|
25
|
+
|
|
26
|
+
- **stdout stays empty at `-vv`.** Two tests: one over every printer in `verbose.py`, one over
|
|
27
|
+
`render_eval_script`. This is the invariant in SD-3 and the only one whose failure would
|
|
28
|
+
execute arbitrary text in a user's shell.
|
|
29
|
+
- **`_run` streams rather than captures when verbose** — asserted on the `subprocess.run`
|
|
30
|
+
kwargs, because "did output reach the terminal" is not otherwise observable in-process.
|
|
31
|
+
- **`_probe` is silent at `-v` and reports exit code + output at `-vv`**, and never raises.
|
|
32
|
+
- **`bash -x` appears at `-vv` and not below**, for both functions and install scripts, plus one
|
|
33
|
+
end-to-end test (`capfd`) that a real function's output and its `+ echo …` trace both arrive.
|
|
34
|
+
- **The CLI accepts `-v` before and after the subcommand**, parametrized over both positions and
|
|
35
|
+
both levels — this is the case that broke in development (SD-2).
|
|
36
|
+
- **The spinner is not used when verbose** (SD-6), asserted by patching `ui.spinner`.
|
|
37
|
+
|
|
38
|
+
Manual verification against the real CLI, with a throwaway `bash`-type tool in an isolated
|
|
39
|
+
`HOME`: quiet / `-v` / `-vv` / `DEVSTUFF_VERBOSE=1` on both a succeeding and a failing install,
|
|
40
|
+
and `--help` on all fourteen commands confirming each advertises `-v`.
|
|
41
|
+
|
|
42
|
+
## Risks
|
|
43
|
+
|
|
44
|
+
- **Verbose output can contain secrets** a user passed as a function parameter. Not mitigated in
|
|
45
|
+
v1 and stated in the spec rather than half-solved (spec §3).
|
|
46
|
+
- **`bash -x` changes a script's stderr**, so a function whose stderr is parsed by something
|
|
47
|
+
downstream would see the trace. Only reachable at `-vv`, which is an interactive debugging
|
|
48
|
+
flag; `capture=True` (the agent path) also gets the trace, and the agent reads it as text.
|
|
49
|
+
- **The env var is read at import time.** Changing `DEVSTUFF_VERBOSE` mid-process has no effect;
|
|
50
|
+
tests call `_from_env()` directly rather than relying on re-import.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Specification: verbose mode (`-v` / `-vv`)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-04
|
|
4
|
+
**Status:** Implemented (v1)
|
|
5
|
+
**Authors:** Sawyer + Claude
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Problem Statement & Goals
|
|
10
|
+
|
|
11
|
+
devstuff is a thin layer over other people's commands. When one of them misbehaves, the
|
|
12
|
+
user is looking at devstuff's summary of it rather than the thing itself:
|
|
13
|
+
|
|
14
|
+
- **Install output was captured and thrown away on success.** `-v` existed on `install`,
|
|
15
|
+
`remove` and `update` as a boolean, but it only reached `generic._run` — which is used
|
|
16
|
+
by the `apt`, `script` and `bash` types. Every `npm`, `uvx`, `git` install called
|
|
17
|
+
`subprocess.run(..., capture_output=True)` directly, so `devstuff install -v <npm tool>`
|
|
18
|
+
streamed nothing at all. The flag silently did nothing for a third of the catalog.
|
|
19
|
+
- **A failing function was a black box.** `devstuff run` streams a function's stdout, but
|
|
20
|
+
`run_cmd` flattens every non-zero exit to 1 and a `script`-type function is an opaque
|
|
21
|
+
blob of someone else's shell. There was no way to see *which line* failed.
|
|
22
|
+
- **Nothing ever showed the command being run.** The most useful thing a tool can tell you
|
|
23
|
+
when it fails is the exact command it ran, so you can run it yourself.
|
|
24
|
+
|
|
25
|
+
**Goal:** one process-wide verbosity level, set the same way everywhere, that makes every
|
|
26
|
+
command devstuff shells out to visible — and at `-vv`, traceable line by line.
|
|
27
|
+
|
|
28
|
+
**Non-goals:** a logging framework or log files (`logging`, handlers, rotation); per-command
|
|
29
|
+
verbosity; a `--quiet` level below the default; structured/JSON output.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Functional Requirements
|
|
34
|
+
|
|
35
|
+
### Levels
|
|
36
|
+
|
|
37
|
+
- **FR-1** Exactly three levels: `0` quiet (default), `1` (`-v`), `2` (`-vv`). Values above
|
|
38
|
+
2 clamp to 2 rather than erroring — `-vvv` is a reasonable thing to type.
|
|
39
|
+
- **FR-2** The level is process-wide state in `verbose.py`, not a parameter threaded through
|
|
40
|
+
call signatures. Set once during Click option parsing, read by the subprocess helpers.
|
|
41
|
+
- **FR-3** `-v` is accepted **before or after** the subcommand (`devstuff -v install x` and
|
|
42
|
+
`devstuff install -v x`), and on subcommands of groups (`devstuff functions -v enable k`).
|
|
43
|
+
When given in both positions the higher wins; they can never cancel out.
|
|
44
|
+
- **FR-4** Every registered command accepts `-v`, applied centrally in `cli._add_verbose_option`
|
|
45
|
+
rather than per-command, so no command can be forgotten. A command already defining
|
|
46
|
+
`--verbose` is left alone.
|
|
47
|
+
- **FR-5** `DEVSTUFF_VERBOSE=1|2` (or `true`) sets the starting level, so a function invoked
|
|
48
|
+
from a script can be verbose without the caller threading a flag. An explicit `-v` can only
|
|
49
|
+
raise the level, never lower it.
|
|
50
|
+
|
|
51
|
+
### What each level shows
|
|
52
|
+
|
|
53
|
+
- **FR-6** At `-v`: every state-changing command is logged as a pasteable shell line
|
|
54
|
+
(`$ bash -lc 'npm install -g x'`) and its output **streams live** instead of being captured.
|
|
55
|
+
- **FR-7** At `-v`: the spinner is replaced by a plain logged line (`verbose.step`). A spinner
|
|
56
|
+
repaints its own line and cannot share a terminal with streaming output.
|
|
57
|
+
- **FR-8** At `-vv`, additionally: read-only probes (version checks, `dpkg` queries, install-state
|
|
58
|
+
checks) with their exit code and captured output; the body of any script about to run; the
|
|
59
|
+
resolved parameter values of a function; the sha256 and size of a downloaded install script.
|
|
60
|
+
- **FR-9** At `-vv`, script bodies run under `bash -x`, so each expanded command is traced. This
|
|
61
|
+
covers both function scripts (`function_runner`) and tool install/remove scripts (`generic`).
|
|
62
|
+
- **FR-10** Probes are silent at `-v`. `devstuff list` alone fires one per tool; at `-v` they
|
|
63
|
+
would bury the actual work. The few *best-effort actions* that share the probe helper
|
|
64
|
+
(`apt-get update`, a git tool's remove command) opt in to `-v` logging via `log_at`.
|
|
65
|
+
|
|
66
|
+
### Output routing
|
|
67
|
+
|
|
68
|
+
- **NFR-1** **Every verbose line goes to stderr, never stdout.** `devstuff run <key>` for a
|
|
69
|
+
`register: eval` function prints shell code to stdout for `eval "$(...)"`; a verbose line on
|
|
70
|
+
stdout would be executed in the user's shell. Enforced by the logger having no stdout path
|
|
71
|
+
at all, rather than by per-call-site checks.
|
|
72
|
+
- **NFR-2** Verbose lines are not word-wrapped (`soft_wrap`). A wrapped command line cannot be
|
|
73
|
+
pasted back into a shell, which is the main thing users do with one.
|
|
74
|
+
- **NFR-3** Commands are rendered with `shlex.join`, so a logged line is a valid shell command.
|
|
75
|
+
- **FR-11** An `eval`-mode function's script is **logged but never traced with `set -x`** — it
|
|
76
|
+
is evaluated by the caller's interactive shell, where xtrace would persist after it returns.
|
|
77
|
+
|
|
78
|
+
### Error reporting
|
|
79
|
+
|
|
80
|
+
- **FR-12** Quiet mode keeps its existing behaviour: output is captured and the captured stderr
|
|
81
|
+
becomes the `RuntimeError` message on failure. That is the only reason quiet mode can say
|
|
82
|
+
anything useful about a failure at all.
|
|
83
|
+
- **FR-13** Verbose mode raises `RuntimeError("exit code N")` instead, since the real output has
|
|
84
|
+
already streamed past and repeating the argv in the message adds nothing.
|
|
85
|
+
- **FR-14** At `-v`, a failing `devstuff run` reports the script's real exit code, which is
|
|
86
|
+
otherwise invisible — `run_cmd` always exits 1 regardless.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 3. Out of scope
|
|
91
|
+
|
|
92
|
+
- Verbose output from the `agent` subsystem's tool calls (it has its own transcript).
|
|
93
|
+
- Verbose output from configurators (`configure/`), which already preview what they do.
|
|
94
|
+
- Redaction. Verbose output can contain whatever the user passed as a function parameter;
|
|
95
|
+
nothing in devstuff handles secrets, and pretending otherwise would be worse than the
|
|
96
|
+
current honest behaviour.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Stack decisions: verbose mode
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-04
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## SD-1 — A module-level level, not Python's `logging`
|
|
8
|
+
|
|
9
|
+
**Chosen:** a tiny `verbose.py` holding an int and five printers on a Rich stderr console.
|
|
10
|
+
|
|
11
|
+
**Rejected:** `logging` with a `StreamHandler` and `setLevel`. It is the obvious answer and it
|
|
12
|
+
buys nothing here: there are no log files, no per-module filtering, no third-party libraries
|
|
13
|
+
whose loggers we want to reconfigure, and no format that survives `logging`'s own formatter
|
|
14
|
+
better than an f-string. What it *would* add is a global side effect on `logging.root` from a
|
|
15
|
+
CLI that other tools may import, plus a second styling system alongside Rich.
|
|
16
|
+
|
|
17
|
+
**Rejected:** threading a `verbose: int` parameter through `install()` / `remove()` /
|
|
18
|
+
`is_installed()`. The subprocess calls that need it are four layers below the command handler,
|
|
19
|
+
across `GenericTool`, `function_runner` and the strategy dicts — every one of those signatures
|
|
20
|
+
would grow a parameter that only two lines in each ever read. `Tool` is an ABC with third-party
|
|
21
|
+
implementations in principle; changing its contract for logging is the wrong trade.
|
|
22
|
+
|
|
23
|
+
## SD-2 — Set by a Click callback with `expose_value=False`
|
|
24
|
+
|
|
25
|
+
The option is applied to every command by `cli._add_verbose_option`, walking the group tree.
|
|
26
|
+
|
|
27
|
+
**Rejected:** decorating each command with `@verbose.option`. That is what the first draft did,
|
|
28
|
+
and it immediately produced the bug it was always going to: `devstuff list -v` errored with
|
|
29
|
+
"No such option" because `list` hadn't been decorated. Users type `-v` on whatever command is
|
|
30
|
+
in front of them; a flag that works on four commands out of fourteen is worse than none.
|
|
31
|
+
|
|
32
|
+
**Rejected:** a group-level option only. `devstuff -v install x` is the unusual spelling; every
|
|
33
|
+
other CLI accepts `install -v` and users will type it first.
|
|
34
|
+
|
|
35
|
+
`expose_value=False` keeps the level out of command signatures, which is the point of SD-1.
|
|
36
|
+
|
|
37
|
+
## SD-3 — Everything to stderr, no exceptions
|
|
38
|
+
|
|
39
|
+
This is a correctness constraint, not a style choice. `devstuff run <key>` for a `register: eval`
|
|
40
|
+
function prints shell code to stdout precisely so `eval "$(devstuff run key)"` works. A verbose
|
|
41
|
+
line on that channel is not noise — it is **code the user's shell will execute**.
|
|
42
|
+
|
|
43
|
+
The logger therefore has no stdout path at all. A per-call-site "don't log in eval mode" check
|
|
44
|
+
would work today and be forgotten the first time someone adds a log line.
|
|
45
|
+
|
|
46
|
+
## SD-4 — `bash -x` for extra-verbose, not a custom tracer
|
|
47
|
+
|
|
48
|
+
`-vv` runs script bodies under `bash -x`. Bash already prints each expanded command with its
|
|
49
|
+
variables substituted, which is exactly what "which line failed" needs and is not reproducible
|
|
50
|
+
by logging the script text alone.
|
|
51
|
+
|
|
52
|
+
**Not applied to `register: eval` scripts.** `set -x` inside evaluated code persists in the
|
|
53
|
+
caller's interactive shell after the function returns — the trace would follow them around until
|
|
54
|
+
they typed `set +x`. Those scripts are logged instead.
|
|
55
|
+
|
|
56
|
+
## SD-5 — Two subprocess helpers, split by "does this change anything"
|
|
57
|
+
|
|
58
|
+
`generic.py` now has `_run` (state-changing: streams when verbose, raises `RuntimeError`) and
|
|
59
|
+
`_probe` (captures, never raises, logs at `-vv`). Everything that shells out goes through one of
|
|
60
|
+
them, which is what makes the flag's coverage complete rather than a list of places someone
|
|
61
|
+
remembered.
|
|
62
|
+
|
|
63
|
+
The split is by **consequence**, not by whether output is captured: a probe's output is captured
|
|
64
|
+
because the caller parses it, so verbosity can only add logging there. The `log_at` parameter
|
|
65
|
+
exists for the two commands that are best-effort *actions* (`apt-get update`, a git tool's
|
|
66
|
+
`git_remove_cmd`) — captured and non-fatal like a probe, but work the user should see at `-v`.
|
|
67
|
+
|
|
68
|
+
**Rejected:** logging probes at `-v`. Measured: `devstuff -vv list` emits ~60 probe lines before
|
|
69
|
+
any output. At `-v` that would hide the one line the user asked for.
|
|
70
|
+
|
|
71
|
+
## SD-6 — The spinner is swapped out, not left running
|
|
72
|
+
|
|
73
|
+
`verbose.step` is a spinner when quiet and a printed line when verbose. Rich's `console.status`
|
|
74
|
+
repaints its own line on a timer; with a subprocess writing to the same terminal, the two
|
|
75
|
+
interleave and the result is unreadable. This also fixed the pre-existing gap where npm/uvx/git
|
|
76
|
+
installs used a spinner *and* captured output, so the old `-v` flag did nothing for them.
|
|
77
|
+
|
|
78
|
+
## SD-7 — `DEVSTUFF_VERBOSE` can only be raised by `-v`, never lowered
|
|
79
|
+
|
|
80
|
+
`set_level(max(level(), value))` in the callback. The env var is for a context that can't pass
|
|
81
|
+
flags (a wrapper script, CI); an explicit `-v` on top of `DEVSTUFF_VERBOSE=2` shouldn't quieten
|
|
82
|
+
anything down, and `-v` meaning "less verbose" would be indefensible.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Open questions
|
|
87
|
+
|
|
88
|
+
| Question | Status |
|
|
89
|
+
|----------|--------|
|
|
90
|
+
| Should `-vv` redact function parameter values that look like secrets? | Open. Deliberately not done in v1 (see spec §3) — a partial redactor implies a guarantee devstuff can't make. |
|
|
91
|
+
| Should the agent's tool calls honour the level? | Open. The agent prints its own transcript; a second channel may just duplicate it. |
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
|
-
from dev_setup import __version__
|
|
3
|
+
from dev_setup import __version__, verbose
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
@click.group(
|
|
7
7
|
invoke_without_command=True,
|
|
8
8
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
9
9
|
)
|
|
10
|
+
@verbose.option
|
|
10
11
|
@click.pass_context
|
|
11
12
|
def cli(ctx: click.Context) -> None:
|
|
12
13
|
if ctx.invoked_subcommand is None:
|
|
@@ -54,5 +55,31 @@ def _register_commands() -> None:
|
|
|
54
55
|
cli.add_command(skills_cmd, "skills")
|
|
55
56
|
cli.add_command(agent_cmd, "agent")
|
|
56
57
|
|
|
58
|
+
_add_verbose_option(cli)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _add_verbose_option(group: click.Group) -> None:
|
|
62
|
+
"""Give every command and subcommand `-v`/`-vv`, recursively.
|
|
63
|
+
|
|
64
|
+
Done centrally rather than by decorating each command so that the flag is accepted
|
|
65
|
+
everywhere it could plausibly be typed — `devstuff -v install x`, `devstuff install
|
|
66
|
+
-v x`, `devstuff functions -v enable k` all work, and no command can be forgotten.
|
|
67
|
+
Aliases (`remove`/`uninstall`) share one Command object, hence the seen-set.
|
|
68
|
+
"""
|
|
69
|
+
seen: set[int] = set()
|
|
70
|
+
|
|
71
|
+
def walk(cmd: click.Command) -> None:
|
|
72
|
+
if id(cmd) in seen:
|
|
73
|
+
return
|
|
74
|
+
seen.add(id(cmd))
|
|
75
|
+
if not any("--verbose" in p.opts for p in cmd.params):
|
|
76
|
+
verbose.option(cmd)
|
|
77
|
+
if isinstance(cmd, click.Group):
|
|
78
|
+
for sub in cmd.commands.values():
|
|
79
|
+
walk(sub)
|
|
80
|
+
|
|
81
|
+
for command in group.commands.values():
|
|
82
|
+
walk(command)
|
|
83
|
+
|
|
57
84
|
|
|
58
85
|
_register_commands()
|
|
@@ -11,13 +11,9 @@ from dev_setup.base import Tool
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@click.command("install")
|
|
14
|
-
@click.option("--verbose", "-v", is_flag=True, help="Stream install output to the terminal.")
|
|
15
14
|
@click.argument("packages", nargs=-1)
|
|
16
|
-
def install_cmd(packages: tuple[str, ...]
|
|
15
|
+
def install_cmd(packages: tuple[str, ...]) -> None:
|
|
17
16
|
"""Install packages. Interactive picker when called with no arguments."""
|
|
18
|
-
from dev_setup import generic
|
|
19
|
-
generic._verbose = verbose
|
|
20
|
-
|
|
21
17
|
if not packages:
|
|
22
18
|
_install_interactive()
|
|
23
19
|
else:
|
|
@@ -9,13 +9,9 @@ from dev_setup.base import Tool
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@click.command("remove")
|
|
12
|
-
@click.option("--verbose", "-v", is_flag=True, help="Stream removal output to the terminal.")
|
|
13
12
|
@click.argument("packages", nargs=-1)
|
|
14
|
-
def remove_cmd(packages: tuple[str, ...]
|
|
13
|
+
def remove_cmd(packages: tuple[str, ...]) -> None:
|
|
15
14
|
"""Uninstall installed packages."""
|
|
16
|
-
from dev_setup import generic
|
|
17
|
-
generic._verbose = verbose
|
|
18
|
-
|
|
19
15
|
if not packages:
|
|
20
16
|
ui.error("Specify at least one package key. See: devstuff list --installed")
|
|
21
17
|
sys.exit(1)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import subprocess
|
|
3
4
|
import sys
|
|
4
5
|
|
|
5
6
|
import click
|
|
6
7
|
|
|
7
8
|
from dev_setup import function_runner as runner
|
|
8
|
-
from dev_setup import functions_registry, ui
|
|
9
|
+
from dev_setup import functions_registry, ui, verbose
|
|
9
10
|
from dev_setup.function_runner import ParamResolutionError
|
|
10
11
|
from dev_setup.functions_registry import FunctionDef, FunctionParam
|
|
11
12
|
|
|
@@ -20,6 +21,8 @@ def run_cmd(key: str, args: tuple[str, ...]) -> None:
|
|
|
20
21
|
ui.error(f"Unknown function: '{key}'")
|
|
21
22
|
sys.exit(1)
|
|
22
23
|
|
|
24
|
+
verbose.trace(f"function '{fn.key}': type={fn.type} register={fn.register} args={list(args)}")
|
|
25
|
+
|
|
23
26
|
if fn.type == "script":
|
|
24
27
|
_run_script(fn, args)
|
|
25
28
|
elif fn.type == "shell-eval" and fn.register == "eval":
|
|
@@ -49,6 +52,12 @@ def _run_script(fn: FunctionDef, args: tuple[str, ...]) -> None:
|
|
|
49
52
|
except ParamResolutionError as exc:
|
|
50
53
|
ui.error(str(exc))
|
|
51
54
|
sys.exit(1)
|
|
55
|
+
except subprocess.CalledProcessError as exc:
|
|
56
|
+
# devstuff's own exit code is always 1 here (a function can't signal a result
|
|
57
|
+
# through it), so the script's real exit code is only visible under -v.
|
|
58
|
+
ui.error(f"'{fn.name}' failed: {exc}")
|
|
59
|
+
verbose.log(f"'{fn.key}' exited {exc.returncode}")
|
|
60
|
+
sys.exit(1)
|
|
52
61
|
except Exception as exc:
|
|
53
62
|
ui.error(f"'{fn.name}' failed: {exc}")
|
|
54
63
|
sys.exit(1)
|
|
@@ -12,17 +12,13 @@ from dev_setup.generic import UpdateStatus
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@click.command("update")
|
|
15
|
-
@click.option("--verbose", "-v", is_flag=True, help="Stream update output to the terminal.")
|
|
16
15
|
@click.option(
|
|
17
16
|
"--version", "target_version", default=None,
|
|
18
17
|
help="Update to a specific version instead of latest. Only valid with a single package.",
|
|
19
18
|
)
|
|
20
19
|
@click.argument("packages", nargs=-1)
|
|
21
|
-
def update_cmd(packages: tuple[str, ...],
|
|
20
|
+
def update_cmd(packages: tuple[str, ...], target_version: str | None) -> None:
|
|
22
21
|
"""Update packages. Interactive picker with recommended updates when called with no arguments."""
|
|
23
|
-
from dev_setup import generic
|
|
24
|
-
generic._verbose = verbose
|
|
25
|
-
|
|
26
22
|
if not packages:
|
|
27
23
|
if target_version:
|
|
28
24
|
ui.error("--version requires a package key.")
|