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.
Files changed (166) hide show
  1. {devstuff-2.0.0 → devstuff-2.2.0}/CHANGELOG.md +12 -0
  2. {devstuff-2.0.0 → devstuff-2.2.0}/CLAUDE.md +28 -1
  3. {devstuff-2.0.0 → devstuff-2.2.0}/PKG-INFO +54 -1
  4. {devstuff-2.0.0 → devstuff-2.2.0}/README.md +53 -0
  5. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/README.md +1 -0
  6. devstuff-2.2.0/docs/specs/verbose-mode/development-plan.md +50 -0
  7. devstuff-2.2.0/docs/specs/verbose-mode/specifications.md +96 -0
  8. devstuff-2.2.0/docs/specs/verbose-mode/stack-decisions.md +91 -0
  9. {devstuff-2.0.0 → devstuff-2.2.0}/pyproject.toml +1 -1
  10. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/cli.py +28 -1
  11. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/install_cmd.py +1 -5
  12. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/remove_cmd.py +1 -5
  13. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/run_cmd.py +10 -1
  14. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/update_cmd.py +1 -5
  15. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/function_runner.py +27 -6
  16. devstuff-2.2.0/src/dev_setup/functions.yaml +240 -0
  17. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/generic.py +86 -69
  18. devstuff-2.2.0/src/dev_setup/verbose.py +148 -0
  19. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_functions.py +125 -0
  20. devstuff-2.2.0/tests/test_verbose.py +266 -0
  21. {devstuff-2.0.0 → devstuff-2.2.0}/uv.lock +1 -1
  22. devstuff-2.0.0/src/dev_setup/functions.yaml +0 -111
  23. {devstuff-2.0.0 → devstuff-2.2.0}/.claude/skills/run-stuff/SKILL.md +0 -0
  24. {devstuff-2.0.0 → devstuff-2.2.0}/.claude/skills/run-stuff/smoke.sh +0 -0
  25. {devstuff-2.0.0 → devstuff-2.2.0}/.github/workflows/bump.yml +0 -0
  26. {devstuff-2.0.0 → devstuff-2.2.0}/.github/workflows/ci.yml +0 -0
  27. {devstuff-2.0.0 → devstuff-2.2.0}/.github/workflows/publish.yml +0 -0
  28. {devstuff-2.0.0 → devstuff-2.2.0}/.github/workflows/test-installs.yml +0 -0
  29. {devstuff-2.0.0 → devstuff-2.2.0}/.gitignore +0 -0
  30. {devstuff-2.0.0 → devstuff-2.2.0}/dev/Dockerfile +0 -0
  31. {devstuff-2.0.0 → devstuff-2.2.0}/dev/Dockerfile.ci +0 -0
  32. {devstuff-2.0.0 → devstuff-2.2.0}/dev/Makefile +0 -0
  33. {devstuff-2.0.0 → devstuff-2.2.0}/dev/docker-compose.yml +0 -0
  34. {devstuff-2.0.0 → devstuff-2.2.0}/devstuff +0 -0
  35. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/agent/development-plan.md +0 -0
  36. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/agent/specifications.md +0 -0
  37. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/agent/stack-decisions.md +0 -0
  38. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/ansible-config/development-plan.md +0 -0
  39. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/ansible-config/specifications.md +0 -0
  40. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/ansible-config/stack-decisions.md +0 -0
  41. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/bat-config/development-plan.md +0 -0
  42. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/bat-config/specifications.md +0 -0
  43. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/bat-config/stack-decisions.md +0 -0
  44. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/commitizen-config/development-plan.md +0 -0
  45. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/commitizen-config/specifications.md +0 -0
  46. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/commitizen-config/stack-decisions.md +0 -0
  47. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/docker-config/development-plan.md +0 -0
  48. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/docker-config/specifications.md +0 -0
  49. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/docker-config/stack-decisions.md +0 -0
  50. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/lazygit-config/development-plan.md +0 -0
  51. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/lazygit-config/specifications.md +0 -0
  52. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/lazygit-config/stack-decisions.md +0 -0
  53. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/precommit-config/development-plan.md +0 -0
  54. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/precommit-config/specifications.md +0 -0
  55. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/precommit-config/stack-decisions.md +0 -0
  56. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/starship-config/development-plan.md +0 -0
  57. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/starship-config/specifications.md +0 -0
  58. {devstuff-2.0.0 → devstuff-2.2.0}/docs/specs/starship-config/stack-decisions.md +0 -0
  59. {devstuff-2.0.0 → devstuff-2.2.0}/install.sh +0 -0
  60. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/__init__.py +0 -0
  61. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/__main__.py +0 -0
  62. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/__init__.py +0 -0
  63. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/approval.py +0 -0
  64. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/bridges.py +0 -0
  65. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/catalog.py +0 -0
  66. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/completion.py +0 -0
  67. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/config.py +0 -0
  68. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/loop.py +0 -0
  69. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/ollama.py +0 -0
  70. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/preflight.py +0 -0
  71. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/primitives.py +0 -0
  72. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/registry.py +0 -0
  73. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/sandbox.py +0 -0
  74. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/session.py +0 -0
  75. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/transcript.py +0 -0
  76. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent/wizard.py +0 -0
  77. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent_tools.schema.json +0 -0
  78. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/agent_tools.yaml +0 -0
  79. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/base.py +0 -0
  80. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/catalog.py +0 -0
  81. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/__init__.py +0 -0
  82. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/add_cmd.py +0 -0
  83. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/agent_cmd.py +0 -0
  84. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/catalog_cmd.py +0 -0
  85. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/configure_cmd.py +0 -0
  86. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/delete_cmd.py +0 -0
  87. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/docs_cmd.py +0 -0
  88. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/doctor_cmd.py +0 -0
  89. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/functions_cmd.py +0 -0
  90. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/help_cmd.py +0 -0
  91. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/list_cmd.py +0 -0
  92. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/commands/skills_cmd.py +0 -0
  93. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/__init__.py +0 -0
  94. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/__init__.py +0 -0
  95. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/detect.py +0 -0
  96. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/model.py +0 -0
  97. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/render.py +0 -0
  98. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/validate.py +0 -0
  99. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/ansible/wizard.py +0 -0
  100. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/__init__.py +0 -0
  101. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/detect.py +0 -0
  102. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/model.py +0 -0
  103. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/preview.py +0 -0
  104. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/render.py +0 -0
  105. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/bat/wizard.py +0 -0
  106. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/__init__.py +0 -0
  107. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/detect.py +0 -0
  108. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/model.py +0 -0
  109. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/render.py +0 -0
  110. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/validate.py +0 -0
  111. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/commitizen/wizard.py +0 -0
  112. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/__init__.py +0 -0
  113. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/detect.py +0 -0
  114. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/model.py +0 -0
  115. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/render.py +0 -0
  116. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/validate.py +0 -0
  117. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/docker/wizard.py +0 -0
  118. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/__init__.py +0 -0
  119. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/detect.py +0 -0
  120. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/model.py +0 -0
  121. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/render.py +0 -0
  122. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/validate.py +0 -0
  123. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/lazygit/wizard.py +0 -0
  124. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/__init__.py +0 -0
  125. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/detect.py +0 -0
  126. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/model.py +0 -0
  127. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/render.py +0 -0
  128. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/validate.py +0 -0
  129. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/precommit/wizard.py +0 -0
  130. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/__init__.py +0 -0
  131. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/fonts.py +0 -0
  132. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/model.py +0 -0
  133. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/preview.py +0 -0
  134. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/render.py +0 -0
  135. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/configure/starship/wizard.py +0 -0
  136. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/doctor.py +0 -0
  137. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/functions.schema.json +0 -0
  138. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/functions_catalog.py +0 -0
  139. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/functions_registry.py +0 -0
  140. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/registry.py +0 -0
  141. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/tools.yaml +0 -0
  142. {devstuff-2.0.0 → devstuff-2.2.0}/src/dev_setup/ui.py +0 -0
  143. {devstuff-2.0.0 → devstuff-2.2.0}/tests/__init__.py +0 -0
  144. {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/__init__.py +0 -0
  145. {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/conftest.py +0 -0
  146. {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/test_agent_smoke.py +0 -0
  147. {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/test_agent_tui.py +0 -0
  148. {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/test_commitizen_config.py +0 -0
  149. {devstuff-2.0.0 → devstuff-2.2.0}/tests/integration/test_tools.py +0 -0
  150. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent.py +0 -0
  151. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_completion.py +0 -0
  152. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_loop.py +0 -0
  153. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_sandbox.py +0 -0
  154. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_tools.py +0 -0
  155. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_agent_wizard.py +0 -0
  156. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_catalog.py +0 -0
  157. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_ansible.py +0 -0
  158. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_bat.py +0 -0
  159. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_commitizen.py +0 -0
  160. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_docker.py +0 -0
  161. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_lazygit.py +0 -0
  162. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_precommit.py +0 -0
  163. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_configure_starship.py +0 -0
  164. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_doctor.py +0 -0
  165. {devstuff-2.0.0 → devstuff-2.2.0}/tests/test_generic.py +0 -0
  166. {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.0.0
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "devstuff"
3
- version = "2.0.0"
3
+ version = "2.2.0"
4
4
  description = "Development environment setup CLI for Linux"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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, ...], verbose: bool) -> None:
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, ...], verbose: bool) -> None:
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, ...], verbose: bool, target_version: str | None) -> None:
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.")