devstuff 1.18.0__tar.gz → 2.0.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 (162) hide show
  1. {devstuff-1.18.0 → devstuff-2.0.0}/.claude/skills/run-stuff/SKILL.md +13 -13
  2. devstuff-2.0.0/.claude/skills/run-stuff/smoke.sh +55 -0
  3. {devstuff-1.18.0 → devstuff-2.0.0}/.github/workflows/test-installs.yml +8 -8
  4. {devstuff-1.18.0 → devstuff-2.0.0}/CHANGELOG.md +19 -0
  5. {devstuff-1.18.0 → devstuff-2.0.0}/CLAUDE.md +186 -20
  6. {devstuff-1.18.0 → devstuff-2.0.0}/PKG-INFO +378 -27
  7. {devstuff-1.18.0 → devstuff-2.0.0}/README.md +377 -26
  8. {devstuff-1.18.0 → devstuff-2.0.0}/dev/Dockerfile +8 -6
  9. {devstuff-1.18.0 → devstuff-2.0.0}/dev/Dockerfile.ci +5 -5
  10. {devstuff-1.18.0 → devstuff-2.0.0}/dev/Makefile +3 -3
  11. devstuff-2.0.0/dev/docker-compose.yml +14 -0
  12. devstuff-1.18.0/dev-setup → devstuff-2.0.0/devstuff +4 -4
  13. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/README.md +5 -0
  14. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/agent/development-plan.md +2 -2
  15. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/agent/specifications.md +5 -5
  16. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/agent/stack-decisions.md +3 -3
  17. devstuff-2.0.0/docs/specs/ansible-config/development-plan.md +84 -0
  18. devstuff-2.0.0/docs/specs/ansible-config/specifications.md +183 -0
  19. devstuff-2.0.0/docs/specs/ansible-config/stack-decisions.md +139 -0
  20. devstuff-2.0.0/docs/specs/bat-config/development-plan.md +86 -0
  21. devstuff-2.0.0/docs/specs/bat-config/specifications.md +179 -0
  22. devstuff-2.0.0/docs/specs/bat-config/stack-decisions.md +141 -0
  23. devstuff-2.0.0/docs/specs/docker-config/development-plan.md +92 -0
  24. devstuff-2.0.0/docs/specs/docker-config/specifications.md +198 -0
  25. devstuff-2.0.0/docs/specs/docker-config/stack-decisions.md +146 -0
  26. devstuff-2.0.0/docs/specs/lazygit-config/development-plan.md +82 -0
  27. devstuff-2.0.0/docs/specs/lazygit-config/specifications.md +165 -0
  28. devstuff-2.0.0/docs/specs/lazygit-config/stack-decisions.md +130 -0
  29. devstuff-2.0.0/docs/specs/precommit-config/development-plan.md +84 -0
  30. devstuff-2.0.0/docs/specs/precommit-config/specifications.md +198 -0
  31. devstuff-2.0.0/docs/specs/precommit-config/stack-decisions.md +173 -0
  32. {devstuff-1.18.0 → devstuff-2.0.0}/install.sh +1 -1
  33. {devstuff-1.18.0 → devstuff-2.0.0}/pyproject.toml +1 -2
  34. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/session.py +1 -1
  35. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/transcript.py +1 -1
  36. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent_tools.schema.json +1 -1
  37. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent_tools.yaml +1 -1
  38. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/catalog.py +1 -1
  39. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/cli.py +2 -0
  40. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/catalog_cmd.py +1 -1
  41. devstuff-2.0.0/src/dev_setup/commands/doctor_cmd.py +93 -0
  42. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/help_cmd.py +3 -0
  43. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/run_cmd.py +2 -2
  44. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/__init__.py +31 -0
  45. devstuff-2.0.0/src/dev_setup/configure/ansible/__init__.py +7 -0
  46. devstuff-2.0.0/src/dev_setup/configure/ansible/detect.py +292 -0
  47. devstuff-2.0.0/src/dev_setup/configure/ansible/model.py +646 -0
  48. devstuff-2.0.0/src/dev_setup/configure/ansible/render.py +201 -0
  49. devstuff-2.0.0/src/dev_setup/configure/ansible/validate.py +313 -0
  50. devstuff-2.0.0/src/dev_setup/configure/ansible/wizard.py +467 -0
  51. devstuff-2.0.0/src/dev_setup/configure/bat/__init__.py +6 -0
  52. devstuff-2.0.0/src/dev_setup/configure/bat/detect.py +237 -0
  53. devstuff-2.0.0/src/dev_setup/configure/bat/model.py +406 -0
  54. devstuff-2.0.0/src/dev_setup/configure/bat/preview.py +229 -0
  55. devstuff-2.0.0/src/dev_setup/configure/bat/render.py +197 -0
  56. devstuff-2.0.0/src/dev_setup/configure/bat/wizard.py +436 -0
  57. devstuff-2.0.0/src/dev_setup/configure/docker/__init__.py +6 -0
  58. devstuff-2.0.0/src/dev_setup/configure/docker/detect.py +352 -0
  59. devstuff-2.0.0/src/dev_setup/configure/docker/model.py +627 -0
  60. devstuff-2.0.0/src/dev_setup/configure/docker/render.py +202 -0
  61. devstuff-2.0.0/src/dev_setup/configure/docker/validate.py +404 -0
  62. devstuff-2.0.0/src/dev_setup/configure/docker/wizard.py +535 -0
  63. devstuff-2.0.0/src/dev_setup/configure/lazygit/__init__.py +7 -0
  64. devstuff-2.0.0/src/dev_setup/configure/lazygit/detect.py +282 -0
  65. devstuff-2.0.0/src/dev_setup/configure/lazygit/model.py +546 -0
  66. devstuff-2.0.0/src/dev_setup/configure/lazygit/render.py +242 -0
  67. devstuff-2.0.0/src/dev_setup/configure/lazygit/validate.py +230 -0
  68. devstuff-2.0.0/src/dev_setup/configure/lazygit/wizard.py +422 -0
  69. devstuff-2.0.0/src/dev_setup/configure/precommit/__init__.py +5 -0
  70. devstuff-2.0.0/src/dev_setup/configure/precommit/detect.py +292 -0
  71. devstuff-2.0.0/src/dev_setup/configure/precommit/model.py +845 -0
  72. devstuff-2.0.0/src/dev_setup/configure/precommit/render.py +296 -0
  73. devstuff-2.0.0/src/dev_setup/configure/precommit/validate.py +289 -0
  74. devstuff-2.0.0/src/dev_setup/configure/precommit/wizard.py +506 -0
  75. devstuff-2.0.0/src/dev_setup/doctor.py +447 -0
  76. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/function_runner.py +5 -5
  77. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/functions.schema.json +1 -1
  78. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/generic.py +1 -1
  79. {devstuff-1.18.0 → devstuff-2.0.0}/tests/integration/test_tools.py +1 -1
  80. devstuff-2.0.0/tests/test_configure_ansible.py +483 -0
  81. devstuff-2.0.0/tests/test_configure_bat.py +457 -0
  82. devstuff-2.0.0/tests/test_configure_docker.py +544 -0
  83. devstuff-2.0.0/tests/test_configure_lazygit.py +420 -0
  84. devstuff-2.0.0/tests/test_configure_precommit.py +449 -0
  85. devstuff-2.0.0/tests/test_doctor.py +285 -0
  86. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_functions.py +1 -1
  87. {devstuff-1.18.0 → devstuff-2.0.0}/uv.lock +1 -1
  88. devstuff-1.18.0/.claude/skills/run-stuff/smoke.sh +0 -55
  89. devstuff-1.18.0/dev/docker-compose.yml +0 -14
  90. {devstuff-1.18.0 → devstuff-2.0.0}/.github/workflows/bump.yml +0 -0
  91. {devstuff-1.18.0 → devstuff-2.0.0}/.github/workflows/ci.yml +0 -0
  92. {devstuff-1.18.0 → devstuff-2.0.0}/.github/workflows/publish.yml +0 -0
  93. {devstuff-1.18.0 → devstuff-2.0.0}/.gitignore +0 -0
  94. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/commitizen-config/development-plan.md +0 -0
  95. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/commitizen-config/specifications.md +0 -0
  96. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/commitizen-config/stack-decisions.md +0 -0
  97. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/starship-config/development-plan.md +0 -0
  98. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/starship-config/specifications.md +0 -0
  99. {devstuff-1.18.0 → devstuff-2.0.0}/docs/specs/starship-config/stack-decisions.md +0 -0
  100. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/__init__.py +0 -0
  101. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/__main__.py +0 -0
  102. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/__init__.py +0 -0
  103. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/approval.py +0 -0
  104. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/bridges.py +0 -0
  105. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/catalog.py +0 -0
  106. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/completion.py +0 -0
  107. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/config.py +0 -0
  108. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/loop.py +0 -0
  109. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/ollama.py +0 -0
  110. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/preflight.py +0 -0
  111. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/primitives.py +0 -0
  112. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/registry.py +0 -0
  113. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/sandbox.py +0 -0
  114. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/agent/wizard.py +0 -0
  115. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/base.py +0 -0
  116. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/__init__.py +0 -0
  117. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/add_cmd.py +0 -0
  118. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/agent_cmd.py +0 -0
  119. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/configure_cmd.py +0 -0
  120. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/delete_cmd.py +0 -0
  121. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/docs_cmd.py +0 -0
  122. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/functions_cmd.py +0 -0
  123. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/install_cmd.py +0 -0
  124. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/list_cmd.py +0 -0
  125. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/remove_cmd.py +0 -0
  126. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/skills_cmd.py +0 -0
  127. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/commands/update_cmd.py +0 -0
  128. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/commitizen/__init__.py +0 -0
  129. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/commitizen/detect.py +0 -0
  130. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/commitizen/model.py +0 -0
  131. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/commitizen/render.py +0 -0
  132. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/commitizen/validate.py +0 -0
  133. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/commitizen/wizard.py +0 -0
  134. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/starship/__init__.py +0 -0
  135. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/starship/fonts.py +0 -0
  136. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/starship/model.py +0 -0
  137. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/starship/preview.py +0 -0
  138. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/starship/render.py +0 -0
  139. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/configure/starship/wizard.py +0 -0
  140. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/functions.yaml +0 -0
  141. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/functions_catalog.py +0 -0
  142. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/functions_registry.py +0 -0
  143. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/registry.py +0 -0
  144. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/tools.yaml +0 -0
  145. {devstuff-1.18.0 → devstuff-2.0.0}/src/dev_setup/ui.py +0 -0
  146. {devstuff-1.18.0 → devstuff-2.0.0}/tests/__init__.py +0 -0
  147. {devstuff-1.18.0 → devstuff-2.0.0}/tests/integration/__init__.py +0 -0
  148. {devstuff-1.18.0 → devstuff-2.0.0}/tests/integration/conftest.py +0 -0
  149. {devstuff-1.18.0 → devstuff-2.0.0}/tests/integration/test_agent_smoke.py +0 -0
  150. {devstuff-1.18.0 → devstuff-2.0.0}/tests/integration/test_agent_tui.py +0 -0
  151. {devstuff-1.18.0 → devstuff-2.0.0}/tests/integration/test_commitizen_config.py +0 -0
  152. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_agent.py +0 -0
  153. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_agent_completion.py +0 -0
  154. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_agent_loop.py +0 -0
  155. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_agent_sandbox.py +0 -0
  156. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_agent_tools.py +0 -0
  157. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_agent_wizard.py +0 -0
  158. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_catalog.py +0 -0
  159. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_configure_commitizen.py +0 -0
  160. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_configure_starship.py +0 -0
  161. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_generic.py +0 -0
  162. {devstuff-1.18.0 → devstuff-2.0.0}/tests/test_registry.py +0 -0
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: run-stuff
3
- description: Run, build, test, and smoke-test the dev-setup CLI; exercise list/catalog/install commands
3
+ description: Run, build, test, and smoke-test the devstuff CLI; exercise list/catalog/install commands
4
4
  ---
5
5
 
6
- `dev-setup` is a Python CLI that manages a Linux dev environment. It is driven directly via `uv run dev-setup <command>` — no GUI, no server, no browser. The smoke script at `.claude/skills/run-stuff/smoke.sh` is the primary agent harness.
6
+ `devstuff` is a Python CLI that manages a Linux dev environment. It is driven directly via `uv run devstuff <command>` — no GUI, no server, no browser. The smoke script at `.claude/skills/run-stuff/smoke.sh` is the primary agent harness.
7
7
 
8
8
  ## Prerequisites
9
9
 
@@ -29,14 +29,14 @@ It exercises `version`, `list` (all variants), `catalog path`, `catalog export`,
29
29
  To exercise a specific command directly:
30
30
 
31
31
  ```bash
32
- uv run dev-setup version
33
- uv run dev-setup list
34
- uv run dev-setup list core
35
- uv run dev-setup list --installed
36
- uv run dev-setup list --available
37
- uv run dev-setup catalog path
38
- uv run dev-setup catalog export /tmp/export.yaml
39
- uv run dev-setup --help
32
+ uv run devstuff version
33
+ uv run devstuff list
34
+ uv run devstuff list core
35
+ uv run devstuff list --installed
36
+ uv run devstuff list --available
37
+ uv run devstuff catalog path
38
+ uv run devstuff catalog export /tmp/export.yaml
39
+ uv run devstuff --help
40
40
  ```
41
41
 
42
42
  Interactive commands (`install`, `remove`, `add`, `delete`) require a TTY and are not smoke-testable without mocking. To test them, run the app directly in a terminal.
@@ -44,8 +44,8 @@ Interactive commands (`install`, `remove`, `add`, `delete`) require a TTY and ar
44
44
  ## Run (human path)
45
45
 
46
46
  ```bash
47
- ./dev-setup list # bash wrapper — bootstraps .venv on first run
48
- ./dev-setup install # interactive multi-select picker
47
+ ./devstuff list # bash wrapper — bootstraps .venv on first run
48
+ ./devstuff install # interactive multi-select picker
49
49
  ```
50
50
 
51
51
  ## Test suite
@@ -59,7 +59,7 @@ All 6 unit tests pass in ~0.3s.
59
59
 
60
60
  ## Gotchas
61
61
 
62
- - `dev-setup` is not on `$PATH` directly in the dev environment — `uv run dev-setup` is the reliable invocation path. The `./dev-setup` bash wrapper also works from repo root and bootstraps its own `.venv`.
62
+ - `devstuff` is not on `$PATH` directly in the dev environment — `uv run devstuff` is the reliable invocation path. The `./devstuff` bash wrapper also works from repo root and bootstraps its own `.venv`.
63
63
  - `pip` is not available in this environment; `uv pip install` is the substitute.
64
64
  - Interactive commands (`install`, `add`) open questionary/click prompts that hang in non-TTY shells. Pipe input only if you know the exact prompt sequence.
65
65
  - `catalog export` writes to a path argument; without an argument it prints to stdout. Check the README for the exact signature.
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # Smoke test for devstuff CLI. Run from repo root.
3
+ set -euo pipefail
4
+
5
+ PASS=0
6
+ FAIL=0
7
+
8
+ check() {
9
+ local label="$1"; shift
10
+ if output=$("$@" 2>&1); then
11
+ echo " ✔ $label"
12
+ PASS=$((PASS+1))
13
+ else
14
+ echo " ✘ $label"
15
+ echo " Output: $output"
16
+ FAIL=$((FAIL+1))
17
+ fi
18
+ }
19
+
20
+ check_output() {
21
+ local label="$1"; local expected="$2"; shift 2
22
+ output=$("$@" 2>&1)
23
+ if echo "$output" | grep -q "$expected"; then
24
+ echo " ✔ $label"
25
+ PASS=$((PASS+1))
26
+ else
27
+ echo " ✘ $label (expected '$expected' in output)"
28
+ echo " Got: $output"
29
+ FAIL=$((FAIL+1))
30
+ fi
31
+ }
32
+
33
+ echo ""
34
+ echo "=== devstuff smoke tests ==="
35
+ echo ""
36
+
37
+ check "version" uv run devstuff version
38
+ check_output "version number" "1\." uv run devstuff version
39
+ check "list (all)" uv run devstuff list
40
+ check "list core" uv run devstuff list core
41
+ check "list tools" uv run devstuff list tools
42
+ check "list --installed" uv run devstuff list --installed
43
+ check "list --available" uv run devstuff list --available
44
+ check "catalog path" uv run devstuff catalog path
45
+ check_output "catalog path output" ".config/devstuff" uv run devstuff catalog path
46
+ check "catalog export" uv run devstuff catalog export /tmp/devstuff-smoke-export.yaml
47
+ check_output "help flag" "Commands:" uv run devstuff --help
48
+
49
+ echo ""
50
+ if [ "$FAIL" -eq 0 ]; then
51
+ echo "All $PASS checks passed."
52
+ else
53
+ echo "$FAIL/$((PASS+FAIL)) checks failed."
54
+ exit 1
55
+ fi
@@ -24,22 +24,22 @@ jobs:
24
24
  run: uv build
25
25
 
26
26
  # Two-step docker build matching the local dev workflow:
27
- # dev/Dockerfile → dev-setup-sandbox (minimal system + wheel installed)
28
- # dev/Dockerfile.ci → dev-setup-ci (+ pytest + test files baked in)
27
+ # dev/Dockerfile → devstuff-sandbox (minimal system + wheel installed)
28
+ # dev/Dockerfile.ci → devstuff-ci (+ pytest + test files baked in)
29
29
  - name: Build sandbox image
30
- run: docker build -f dev/Dockerfile -t dev-setup-sandbox .
30
+ run: docker build -f dev/Dockerfile -t devstuff-sandbox .
31
31
 
32
32
  - name: Build CI test image
33
- run: docker build -f dev/Dockerfile.ci -t dev-setup-ci .
33
+ run: docker build -f dev/Dockerfile.ci -t devstuff-ci .
34
34
 
35
35
  - name: Export image archive
36
- run: docker save dev-setup-ci | gzip > dev-setup-ci.tar.gz
36
+ run: docker save devstuff-ci | gzip > devstuff-ci.tar.gz
37
37
 
38
38
  - name: Upload image artifact
39
39
  uses: actions/upload-artifact@v4
40
40
  with:
41
41
  name: docker-image
42
- path: dev-setup-ci.tar.gz
42
+ path: devstuff-ci.tar.gz
43
43
  retention-days: 1
44
44
 
45
45
  # ─── Stage 2: one isolated container per tool, all run in parallel ───────────
@@ -81,12 +81,12 @@ jobs:
81
81
  name: docker-image
82
82
 
83
83
  - name: Load image
84
- run: docker load < dev-setup-ci.tar.gz
84
+ run: docker load < devstuff-ci.tar.gz
85
85
 
86
86
  - name: Test ${{ matrix.tool }} installs
87
87
  run: |
88
88
  set +e
89
- docker run --rm dev-setup-ci \
89
+ docker run --rm devstuff-ci \
90
90
  pytest "tests/integration/test_tools.py::test_install[${{ matrix.tool }}]" \
91
91
  -v --tb=short 2>&1 | tee test-output.txt
92
92
  RESULT=${PIPESTATUS[0]}
@@ -1,3 +1,22 @@
1
+ ## v2.0.0 (2026-08-04)
2
+
3
+ ### BREAKING CHANGE
4
+
5
+ - the `dev-setup` command alias is removed. Config/data moved
6
+ from ~/.config/dev-setup and ~/.local/share/dev-setup to the devstuff
7
+ equivalents. bashrc block markers renamed (dev-setup: bat -> devstuff: bat,
8
+ dev-setup-fn:<key> -> devstuff-fn:<key>). Existing users run `devstuff
9
+ doctor --fix` to migrate.
10
+
11
+ ### Features
12
+
13
+ - drop dev-setup alias, rename paths to devstuff, add doctor command
14
+ - **configure**: add pre-commit, docker, bat, ansible and lazygit wizards
15
+
16
+ ### Bug Fixes
17
+
18
+ - **dev**: copy the devstuff wheel, not the stale dev_setup name
19
+
1
20
  ## v1.18.0 (2026-08-01)
2
21
 
3
22
  ### Features
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## What this is
6
6
 
7
- `dev-setup` is a Python CLI (Click + Rich + questionary) that installs, removes, and tracks
7
+ `devstuff` is a Python CLI (Click + Rich + questionary) that installs, removes, and tracks
8
8
  developer tools on Linux. There is no per-tool Python code — every tool (built-in or
9
9
  user-added) is a data record in a YAML catalog, executed by one generic engine
10
10
  (`GenericTool` in `src/dev_setup/generic.py`). Adding a tool is a YAML edit, not a code change;
@@ -13,8 +13,8 @@ adding a new *install mechanism* (a "type") is a code change touched in ~5 place
13
13
  ## Commands
14
14
 
15
15
  ```bash
16
- uv run dev-setup <cmd> # run from source (repo root)
17
- ./dev-setup <cmd> # bash wrapper — bootstraps .venv on first run, then execs Python
16
+ uv run devstuff <cmd> # run from source (repo root)
17
+ ./devstuff <cmd> # bash wrapper — bootstraps .venv on first run, then execs Python
18
18
  uv run pytest # unit tests only (integration tests skipped by default, ~0.3s)
19
19
  uv run pytest -m integration # real installs — requires sudo + network, run inside Docker (see below)
20
20
  uv run pytest tests/test_catalog.py::test_user_catalog_overrides_bundled_tool_in_place # single test
@@ -67,7 +67,7 @@ src/dev_setup/
67
67
  ```
68
68
 
69
69
  **Catalog precedence** (`catalog.load_effective_catalog`): bundled `tools.yaml` loads first →
70
- user YAML at `~/.config/dev-setup/tools.yaml` overrides matching keys in place and appends
70
+ user YAML at `~/.config/devstuff/tools.yaml` overrides matching keys in place and appends
71
71
  new ones. `registry.py` turns that merged dict into `GenericTool` instances; a tool is
72
72
  `builtin` only if it came from bundled and has no user override.
73
73
 
@@ -77,13 +77,13 @@ new ones. `registry.py` turns that merged dict into `GenericTool` instances; a t
77
77
  `generic.py`) rather than an if/elif chain, and shell out via `subprocess`. `install()` raises
78
78
  `RuntimeError`/`CalledProcessError` on failure — there's no result enum, command handlers just
79
79
  catch and report. `bash`-type scripts are written to a temp file and run with `bash <file>` for
80
- full parsing fidelity (not `bash -c "<string>"`). `dev-setup update` reuses the same dispatch
80
+ full parsing fidelity (not `bash -c "<string>"`). `devstuff update` reuses the same dispatch
81
81
  pattern for upgrading an already-installed tool (latest or a pinned version); for `script`/
82
82
  `bash` types "update" is a full reinstall, since there's no narrower mechanism, so the command
83
83
  layer confirms before re-running it.
84
84
 
85
85
  **Two ways a tool gets defined**: built-in (an entry added directly to `src/dev_setup/tools.yaml`,
86
- `builtin=True`) or custom (created via the `dev-setup add` wizard, `dev-setup catalog import`,
86
+ `builtin=True`) or custom (created via the `devstuff add` wizard, `devstuff catalog import`,
87
87
  or hand-edited YAML, landing in the user catalog). Both use the identical schema — the only
88
88
  difference is which file the key lives in and `category`.
89
89
 
@@ -123,7 +123,7 @@ installing PHP packages via Composer as their own first-class type (analogous to
123
123
 
124
124
  `src/dev_setup/functions.yaml` + `functions_catalog.py` + `functions_registry.py` +
125
125
  `function_runner.py` are a parallel, independent catalog/registry from tools — functions
126
- aren't installed/removed, they're invoked (`dev-setup run <key>`), so they get their own
126
+ aren't installed/removed, they're invoked (`devstuff run <key>`), so they get their own
127
127
  schema instead of overloading `GenericTool`. Some duplication with `catalog.py`/`registry.py`
128
128
  is deliberate (see "Key design decisions" below).
129
129
 
@@ -133,18 +133,18 @@ loaded or enforced at runtime (no `jsonschema` dependency), so if you add/change
133
133
  a constraint in `functions_catalog.py`'s `validate_catalog()`, update the schema file too or
134
134
  they'll silently drift apart.
135
135
 
136
- **Why two function `type`s exist**: a `dev-setup` command is its own child process, so
136
+ **Why two function `type`s exist**: a `devstuff` command is its own child process, so
137
137
  anything it does with `subprocess` (env vars, `cd`, aliases) is invisible to the shell that
138
138
  invoked it the moment the process exits. `type: script` is for functions that don't need to
139
139
  mutate the calling shell (runs as a subprocess, like a tool's `install_script`). `type:
140
140
  shell-eval` is for functions that must (`ssh-agent`, `nvm use`-style tools) — it has two
141
141
  `register` modes:
142
- - `register: bashrc` (default) — `dev-setup functions enable <key>` patches a real shell
142
+ - `register: bashrc` (default) — `devstuff functions enable <key>` patches a real shell
143
143
  function into `~/.bashrc` via `base.patch_bashrc`; the user calls it directly by name in a
144
- new shell afterward. `dev-setup run` refuses to run these directly (there's nothing it
144
+ new shell afterward. `devstuff run` refuses to run these directly (there's nothing it
145
145
  *can* do) and points at `functions enable` instead.
146
- - `register: eval` — `dev-setup run <key>` prints resolved shell code to stdout for
147
- `eval "$(dev-setup run key args)"`. This path must never print anything else to stdout
146
+ - `register: eval` — `devstuff run <key>` prints resolved shell code to stdout for
147
+ `eval "$(devstuff run key args)"`. This path must never print anything else to stdout
148
148
  (no `ui.*` calls, no prompts) since it would corrupt what gets `eval`'d — missing required
149
149
  params are reported on stderr and exit non-zero instead of being prompted for.
150
150
 
@@ -161,7 +161,7 @@ make `functions disable` orphan everything after it — closing brace included.
161
161
 
162
162
  Functions have a `category` field (defaults to `custom`, freeform — not an enum) that
163
163
  `functions list` groups/sorts by, mirroring tools. A `script`-type function that shells out to
164
- another CLI should guard on `command -v <tool>` and point at `dev-setup install <tool>` in the
164
+ another CLI should guard on `command -v <tool>` and point at `devstuff install <tool>` in the
165
165
  error rather than let a raw "command not found" surface — see `validate-yaml`/`aws-saml-reauth`
166
166
  in `functions.yaml`. If that CLI is only reachable via nvm (like `pi`), source
167
167
  `"$HOME/.nvm/nvm.sh"` first (see `acc-check`) — `script`-type functions run via a non-login,
@@ -173,9 +173,27 @@ ones tools already have.
173
173
  ## Configurators (`configure/`) — tool-specific wizards, deliberately *not* catalog-driven
174
174
 
175
175
  `src/dev_setup/configure/` holds per-tool setup wizards (`devstuff configure <tool>`), registered
176
- in a `CONFIGURATORS` dict keyed by catalog tool key. There are two:
177
- `configure/starship/{model,render,preview,wizard}.py` and
178
- `configure/commitizen/{model,render,detect,validate,wizard}.py`.
176
+ in a `CONFIGURATORS` dict keyed by catalog tool key. There are seven:
177
+ `configure/starship/{model,render,preview,fonts,wizard}.py`,
178
+ `configure/commitizen/{model,render,detect,validate,wizard}.py`,
179
+ `configure/precommit/{model,render,detect,validate,wizard}.py` (the package can't be named
180
+ `pre-commit`, so the hyphenated catalog key maps to the `precommit` package in `CONFIGURATORS`),
181
+ `configure/docker/{model,render,detect,validate,wizard}.py`,
182
+ `configure/bat/{model,render,detect,preview,wizard}.py`,
183
+ `configure/ansible/{model,render,detect,validate,wizard}.py` and
184
+ `configure/lazygit/{model,render,detect,validate,wizard}.py`.
185
+
186
+ **The pattern they all share, and the one thing that matters most when adding another:**
187
+ *measure the tool, don't recall it.* Every configurator's `model.py` was built by interrogating
188
+ the installed binary, and in every single case that turned up something a plausible-sounding
189
+ memory would have got wrong — see each one's "things learned" block below. If you are adding a
190
+ configurator and you have not run the tool, you are guessing.
191
+
192
+ **The second shared pattern: work out what the tool's own validator does *not* check.** All four
193
+ of the newer wizards found their reason to exist there. `dockerd --validate`, `pre-commit
194
+ validate-config`, `ansible-config validate` and lazygit's startup check each accept configs that
195
+ are silently broken, and each in a different way. Establishing that boundary — by writing a
196
+ deliberately broken config and seeing what happens — is the first thing to do, not the last.
179
197
 
180
198
  **Why this one breaks the YAML-catalog rule.** Installation generalises into ~7 mechanisms, which
181
199
  is what makes `GenericTool` possible. Configuration does not: starship's config is a TOML file of
@@ -282,9 +300,157 @@ other edit. Full reasoning in `docs/specs/commitizen-config/`.
282
300
  time — not on every redraw (~3s, unlike starship's millisecond preview) — and a disagreement
283
301
  *warns*, it never vetoes a save. Keep that distinction in the comments.
284
302
 
285
- Not yet built: configurators for anything other than starship and commitizen, and round-tripping an
286
- existing hand-edited config back into wizard state (the timestamped backup is the safety net
287
- instead).
303
+ **Within the pre-commit configurator, the object being configured is a curated catalog of hooks,
304
+ not arbitrary YAML.** `REPOS`/`HOOKS`/`PRESETS` in `model.py` are ordered and everything else reads
305
+ them — adding a hook is one `Hook` record and it reaches the picker, any preset naming it, the
306
+ language detector, the emitter and the preview with no other edit. Full reasoning in
307
+ `docs/specs/precommit-config/`.
308
+
309
+ **Things learned from the real binary — don't "simplify" these away:**
310
+ - **`default_install_hook_types` is derived from the selected hooks' stages, never asked.**
311
+ `pre-commit install` installs *only* the `pre-commit` git hook unless given `--install-hook-types`,
312
+ so a `commit-msg` hook (commitizen's) without this yields a config that validates, an install
313
+ that reports success, and a hook that never fires. `install_hook_types()` feeds three places: the
314
+ emitted key, the `pre-commit install --hook-type` flags, and a check in `verify()`. Verified end
315
+ to end — with it, `git commit -m "oops no convention"` is rejected.
316
+ - **`pre-commit validate-config` does not check hook ids.** It reads the file's shape and never
317
+ opens a repository, so a mistyped id passes and fails on the user's next commit. That is why
318
+ `validate.resolve()` (`pre-commit install-hooks`, network, minutes) exists as a separate level
319
+ from `verify()` (offline, milliseconds, runs before every save). Don't collapse them.
320
+ - **Quoting asks the YAML parser rather than a rules table** (`render._str`): a scalar is written
321
+ bare only if `yaml.safe_load(value) == value`. This is what keeps shfmt's indent width `'2'` a
322
+ string, which pre-commit's schema requires, and a rev like `1.0` from becoming a float.
323
+ - **`repos:` with nothing under it parses as null, not `[]`,** and pre-commit rejects it — the
324
+ emitter special-cases `repos: []`. Found by the round-trip assertion, not by reasoning.
325
+ - **The emitter writes YAML by hand for the comments, and `render.matches()` parses it back and
326
+ compares against `render.data()`.** Those two representations must stay in sync; the round-trip
327
+ test over every preset is what enforces it. Don't swap in `yaml.safe_dump` (loses the comments)
328
+ or ruamel (a new runtime dependency for one wizard).
329
+ - **`pre-commit autoupdate` can move a pin to a prerelease** (observed: isort `8.0.1` → `9.0.0b1`),
330
+ so "refresh the revs" shows a diff and confirms rather than applying silently. The GitHub tags
331
+ API is *not* version-ordered and disagreed with autoupdate in both directions — every rev in
332
+ `REPOS` is what autoupdate itself resolved.
333
+ - **`ruff-check` carries `--exit-non-zero-on-fix` deliberately.** Without it the hook exits 0
334
+ having rewritten files, and the fixes silently miss the commit that triggered them.
335
+ - **`Hook.needs` is load-bearing, not documentation.** It surfaces prerequisites on the review
336
+ screen *and* derives the pre-commit.ci `skip` list — pre-commit.ci has no Docker daemon, so
337
+ every `language: docker` hook must be skipped there or the whole CI run fails.
338
+ - **Language detection goes through `git ls-files`** so gitignored content can't decide the
339
+ answer, and needs two files before a language counts — one stray `.sh` in a Python repo must not
340
+ pull in two Docker-backed hooks.
341
+ - **The commitizen hook is only suggested when a commitizen config exists.** It runs `cz check`,
342
+ which fails *every* commit without one.
343
+ - Conflicting hook pairs (`ruff-format` + `black`) are **warned about, not refused** — but no
344
+ shipped preset may contain one, which is a test.
345
+
346
+ **Within the docker configurator, the object being configured is a daemon that has to be
347
+ restarted before anything takes effect.** Full reasoning in `docs/specs/docker-config/`.
348
+
349
+ **Things learned from the real binary — don't "simplify" these away:**
350
+ - **`dockerd --validate` accepts five configs that break every container.** Measured: an
351
+ unloadable log driver, an unknown log option for the chosen driver, `compress: "true"` with
352
+ `max-file: "1"`, an address pool `size` below its base prefix, and `hosts` alongside a systemd
353
+ unit passing `-H`. The daemon starts healthy and every `docker run` then fails with an error
354
+ that never mentions daemon.json. `validate.py` runs `--validate` *and* those checks; do not
355
+ collapse it into "the tool already validates this".
356
+ - **Per-driver `log-opts` are measured, and two contradict the obvious guess.** `local` *does*
357
+ take `tag`/`labels`/`env`; `journald` does *not* take `max-size`/`max-file`/`compress`. So
358
+ `log_opts()` filters by driver rather than emitting whatever was set.
359
+ - **`none` is not in `docker info`'s `Plugins.Log`** — it is built into the daemon. The plugin
360
+ list is "drivers that certainly exist", not "all valid drivers", hence `BUILTIN_DRIVERS`.
361
+ - **Only non-default values are written.** A config restating a default freezes it across
362
+ daemon upgrades. This makes `Setting.default` and the `DockerConfig` field default a pair that
363
+ must agree — which is a test, not a convention.
364
+ - **daemon.json has no comment syntax**, so it cannot carry a "generated by" marker (an unknown
365
+ key is rejected — measured). The overwrite path shows a unified diff instead, which answers a
366
+ better question anyway.
367
+ - **The restart is a separate confirmation and reads the daemon's *current* live-restore state**,
368
+ not the new config's: turning live-restore on protects the *next* restart, not the one applying
369
+ it.
370
+ - Writes go through `sudo install` staged via a temp file, never a redirect — a truncating
371
+ redirect that fails leaves the daemon with half a config.
372
+
373
+ **Within the bat configurator, the preview is the deliverable** — it is the closest sibling to
374
+ starship's. Full reasoning in `docs/specs/bat-config/`.
375
+
376
+ **Things learned from the real binary — don't "simplify" these away:**
377
+ - **A bad theme is a warning and exit 0.** bat prints `[bat warning]: Unknown theme` to stderr —
378
+ which a pager swallows — and carries on with the default forever. Every theme named is checked
379
+ against `bat --list-themes`. A bad `--style` component, by contrast, is a hard error and needs
380
+ no help; the checks are asymmetric on purpose.
381
+ - **The theme list is read at run time, not tabled.** `bat cache --build` lets users add their
382
+ own. The shipped table is the offline fallback plus the light/dark metadata bat does not
383
+ expose — and a test asserts every shipped name still exists, so the fallback can't rot.
384
+ - **Classifying themes by foreground luminance is wrong for Solarized.** Both variants share one
385
+ palette by design and measure identically; three more (`ansi`, `base16`, `base16-256`) emit no
386
+ true colour at all. Hence three modes, hand-corrected against the measurement.
387
+ - **Unquoted values containing spaces are split into arguments** — `--theme=Solarized (dark)`
388
+ makes bat try to open a file called `(dark)`. Every value is quoted.
389
+ - **`rule` is a subset of `grid`** and bat says so. Found when the live check rejected a
390
+ "turn everything on" preset; all 21 component pairs were then swept and this is the only one.
391
+ - The preview clears every `BAT_*` variable, because they beat the config file — otherwise it
392
+ would render the environment's theme while claiming to show the candidate's.
393
+
394
+ **Within the ansible configurator, the check that matters is "did ansible *read* this",
395
+ not "is this valid".** Full reasoning in `docs/specs/ansible-config/`.
396
+
397
+ **Things learned from the real binary — don't "simplify" these away:**
398
+ - **`[ssh_connection]` is not a section ansible-core reads.** `pipelining` moved to `[defaults]`.
399
+ Verified both ways: under `[ssh_connection]` nothing appears in `ansible-config dump
400
+ --only-changed`; under `[defaults]` it appears as `ANSIBLE_PIPELINING`. That stanza is the most
401
+ copy-pasted three lines in Ansible and it is inert.
402
+ - **So the load-bearing check is the dump comparison**, not `ansible-config validate`. Every
403
+ setting written must appear in `ansible-config dump --only-changed` sourced from our file. A
404
+ test proves the check can *fail* by moving `pipelining` back into `[ssh_connection]`.
405
+ - **The `yaml` stdout callback was removed** — superseded by `callback_result_format`. Setting
406
+ `stdout_callback = yaml` passes every check and produces JSON. The first draft of the `project`
407
+ preset did exactly this.
408
+ - **`ansible-config validate` rejects a working setting.** `callback_result_format` is a
409
+ callback-*plugin* option and the validator only knows core settings. So it is unreliable in
410
+ both directions. Hence `Setting.dump_type`, the discounted complaint (stated in the check's own
411
+ detail, never silent), and the warning telling the user what they will see.
412
+ - **Plugin options dump under `-t <type>` and in lowercase**, unlike core settings' SHOUTING_CASE.
413
+ - **Values are never quoted.** `inventory = "./inv"` becomes a path containing quotes, and
414
+ `pipelining = "True"` is read as **False** — quoting a boolean inverts it. The reader coerces
415
+ the way ansible does, not the way `configparser` would.
416
+ - **A retired section is preserved and reported, never migrated.** The wizard cannot know whether
417
+ an older ansible elsewhere still reads it.
418
+ - **ansible ignores `./ansible.cfg` in a world-writable directory** — warned about before the
419
+ questions and again after the save.
420
+
421
+ **Within the lazygit configurator, the tool provides no usable way to check a config's keys.**
422
+ Full reasoning in `docs/specs/lazygit-config/`.
423
+
424
+ **Things learned from the real binary — don't "simplify" these away:**
425
+ - **lazygit rejects wrong *types* and silently ignores unknown *keys* and invalid enum values.**
426
+ Everything in the package follows from that split. Two live tests assert both halves, so a
427
+ future lazygit changing either is caught.
428
+ - **`lazygit --config` is not a key list.** It omits every setting with no default, including
429
+ `git.paging.pager` — the delta integration everyone wants. Treating it as authoritative would
430
+ have made the wizard refuse to write the most-wanted setting there is.
431
+ - **Keys were verified with a type probe**: set the key to a value of obviously the wrong type
432
+ and start lazygit; a real key errors, an unknown one is ignored. The method is recorded in
433
+ `model.py` so the table can be re-derived. It is what found `git.paging.useConfig` dead.
434
+ - **The defaults dump *is* used for values**, and `default_drift()` compares it against the
435
+ model — which caught `gui.sidePanelWidth` being modelled as a string when it is a float. A
436
+ setting absent from the dump is explicitly *not* drift.
437
+ - **PyYAML cannot load lazygit's own default config.** `expandAll: =` hits YAML's
438
+ `tag:yaml.org,2002:value` and `safe_load` raises. All parsing goes through `render.load()`, and
439
+ `=` is quoted on the way out.
440
+ - **A list of mappings must not be hand-emitted.** The first emitter stringified
441
+ `customCommands` — the user data this wizard most promises not to damage. Carried-over
442
+ structures go through `yaml.safe_dump` and are re-indented; only settings the wizard authors
443
+ are hand-emitted, because only those need comments.
444
+ - **Icons reuse `configure/starship/fonts.detect()`**, including its `None` — "cannot tell" is
445
+ not "no", and inverting that would nag every user without fontconfig.
446
+
447
+ Not yet built: configurators for anything other than starship, commitizen, pre-commit, docker,
448
+ bat, ansible and lazygit; `repo: local` pre-commit hook authoring and hooks outside the curated
449
+ catalog; lazygit keybindings and `customCommands` authoring (both preserved, never edited);
450
+ ansible fact-caching backends; and — for pre-commit and commitizen only — round-tripping an
451
+ existing hand-edited config back into wizard state. The four newer configurators *do* round-trip,
452
+ because JSON, INI, bat's flag list and lazygit's YAML are all flat enough to preserve faithfully;
453
+ pre-commit's is not, and that asymmetry is deliberate (see each spec's stack-decisions).
288
454
 
289
455
  ## Specs (`docs/specs/`)
290
456
 
@@ -321,7 +487,7 @@ strategy-dispatch pattern as `_INSTALLERS` in `generic.py`.
321
487
  deliberately not disabled by `--yolo`. The prompt is a human attention filter; attention
322
488
  degrades over a session, the denylist does not.
323
489
  - Credential dirs are blocked for **read** as well as write — exfiltrating an SSH key into a model
324
- context is as bad as overwriting one. `~/.config/dev-setup` is readable but not writable, so
490
+ context is as bad as overwriting one. `~/.config/devstuff` is readable but not writable, so
325
491
  the agent cannot author catalogs (FR-14a).
326
492
  - `assess()` (the launch guard) is advisory UX, not a control. Keep that distinction in comments;
327
493
  the risk is a future reader mistaking a warning for enforcement.