bash-classify 0.5.0__tar.gz → 0.6.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 (197) hide show
  1. {bash_classify-0.5.0 → bash_classify-0.6.0}/.claude-plugin/marketplace.json +1 -1
  2. {bash_classify-0.5.0 → bash_classify-0.6.0}/PKG-INFO +9 -5
  3. {bash_classify-0.5.0 → bash_classify-0.6.0}/README.md +8 -4
  4. {bash_classify-0.5.0 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/.claude-plugin/plugin.json +1 -1
  5. {bash_classify-0.5.0 → bash_classify-0.6.0}/schemas/command.schema.json +26 -0
  6. bash_classify-0.6.0/src/bash_classify/commands/gws.yaml +211 -0
  7. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/database.py +24 -0
  8. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/matcher.py +11 -0
  9. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/models.py +1 -0
  10. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_classifier.py +57 -0
  11. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_database.py +39 -0
  12. {bash_classify-0.5.0 → bash_classify-0.6.0}/.claude/rules/command-database.md +0 -0
  13. {bash_classify-0.5.0 → bash_classify-0.6.0}/.editorconfig +0 -0
  14. {bash_classify-0.5.0 → bash_classify-0.6.0}/.github/workflows/ci.yml +0 -0
  15. {bash_classify-0.5.0 → bash_classify-0.6.0}/.github/workflows/publish.yml +0 -0
  16. {bash_classify-0.5.0 → bash_classify-0.6.0}/.gitignore +0 -0
  17. {bash_classify-0.5.0 → bash_classify-0.6.0}/.python-version +0 -0
  18. {bash_classify-0.5.0 → bash_classify-0.6.0}/CLAUDE.md +0 -0
  19. {bash_classify-0.5.0 → bash_classify-0.6.0}/LICENSE +0 -0
  20. {bash_classify-0.5.0 → bash_classify-0.6.0}/SPEC.md +0 -0
  21. {bash_classify-0.5.0 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/classify-bash.sh +0 -0
  22. {bash_classify-0.5.0 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/hooks.json +0 -0
  23. {bash_classify-0.5.0 → bash_classify-0.6.0}/docs/classification-guidance.md +0 -0
  24. {bash_classify-0.5.0 → bash_classify-0.6.0}/pyproject.toml +0 -0
  25. {bash_classify-0.5.0 → bash_classify-0.6.0}/ruff.toml +0 -0
  26. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/__init__.py +0 -0
  27. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/__main__.py +0 -0
  28. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/classifier.py +0 -0
  29. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/cli.py +0 -0
  30. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/agent-browser.yaml +0 -0
  31. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/apt.yaml +0 -0
  32. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/awk.yaml +0 -0
  33. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/basename.yaml +0 -0
  34. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/bash-classify.yaml +0 -0
  35. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/bash.yaml +0 -0
  36. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/black.yaml +0 -0
  37. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/brew.yaml +0 -0
  38. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/builtin.yaml +0 -0
  39. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/cargo.yaml +0 -0
  40. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/cat.yaml +0 -0
  41. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/chgrp.yaml +0 -0
  42. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/chmod.yaml +0 -0
  43. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/chown.yaml +0 -0
  44. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/column.yaml +0 -0
  45. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/comm.yaml +0 -0
  46. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/command.yaml +0 -0
  47. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/cp.yaml +0 -0
  48. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/curl.yaml +0 -0
  49. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/cut.yaml +0 -0
  50. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/date.yaml +0 -0
  51. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/df.yaml +0 -0
  52. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/diff.yaml +0 -0
  53. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/dig.yaml +0 -0
  54. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/dirname.yaml +0 -0
  55. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/docker.yaml +0 -0
  56. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/du.yaml +0 -0
  57. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/echo.yaml +0 -0
  58. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/env.yaml +0 -0
  59. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/eslint.yaml +0 -0
  60. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/expand.yaml +0 -0
  61. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/false.yaml +0 -0
  62. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/file.yaml +0 -0
  63. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/find.yaml +0 -0
  64. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/fmt.yaml +0 -0
  65. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/fold.yaml +0 -0
  66. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/free.yaml +0 -0
  67. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gawk.yaml +0 -0
  68. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gcloud.yaml +0 -0
  69. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gh.yaml +0 -0
  70. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/git.yaml +0 -0
  71. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/glab-discussion.yaml +0 -0
  72. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/glab.yaml +0 -0
  73. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/go.yaml +0 -0
  74. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gradle.yaml +0 -0
  75. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gradlew.yaml +0 -0
  76. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/grep.yaml +0 -0
  77. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/groups.yaml +0 -0
  78. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gunzip.yaml +0 -0
  79. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gzip.yaml +0 -0
  80. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/hadolint.yaml +0 -0
  81. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/head.yaml +0 -0
  82. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/helm.yaml +0 -0
  83. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/hostname.yaml +0 -0
  84. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/id.yaml +0 -0
  85. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/install.yaml +0 -0
  86. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/isort.yaml +0 -0
  87. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/java.yaml +0 -0
  88. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/javac.yaml +0 -0
  89. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/join.yaml +0 -0
  90. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/journalctl.yaml +0 -0
  91. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/jq.yaml +0 -0
  92. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/kill.yaml +0 -0
  93. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/killall.yaml +0 -0
  94. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/kubectl.yaml +0 -0
  95. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/less.yaml +0 -0
  96. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ln.yaml +0 -0
  97. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ls.yaml +0 -0
  98. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/make.yaml +0 -0
  99. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/md5sum.yaml +0 -0
  100. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mkdir.yaml +0 -0
  101. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/more.yaml +0 -0
  102. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mv.yaml +0 -0
  103. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mvn.yaml +0 -0
  104. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mvnw.yaml +0 -0
  105. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mypy.yaml +0 -0
  106. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/netstat.yaml +0 -0
  107. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/nice.yaml +0 -0
  108. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/nl.yaml +0 -0
  109. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/node.yaml +0 -0
  110. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/nohup.yaml +0 -0
  111. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/npm.yaml +0 -0
  112. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/npx.yaml +0 -0
  113. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/nslookup.yaml +0 -0
  114. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/paste.yaml +0 -0
  115. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ping.yaml +0 -0
  116. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pip.yaml +0 -0
  117. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pip3.yaml +0 -0
  118. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pipx.yaml +0 -0
  119. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pkill.yaml +0 -0
  120. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/poetry.yaml +0 -0
  121. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/prettier.yaml +0 -0
  122. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/printenv.yaml +0 -0
  123. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/printf.yaml +0 -0
  124. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ps.yaml +0 -0
  125. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pup.yaml +0 -0
  126. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pwd.yaml +0 -0
  127. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pytest.yaml +0 -0
  128. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/python.yaml +0 -0
  129. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/python3.yaml +0 -0
  130. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/read.yaml +0 -0
  131. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/readlink.yaml +0 -0
  132. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/realpath.yaml +0 -0
  133. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/rev.yaml +0 -0
  134. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/rg.yaml +0 -0
  135. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/rm.yaml +0 -0
  136. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/rsync.yaml +0 -0
  137. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ruff.yaml +0 -0
  138. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/scp.yaml +0 -0
  139. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sed.yaml +0 -0
  140. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/seq.yaml +0 -0
  141. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sh.yaml +0 -0
  142. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sha1sum.yaml +0 -0
  143. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sha256sum.yaml +0 -0
  144. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/shellcheck.yaml +0 -0
  145. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sort.yaml +0 -0
  146. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ss.yaml +0 -0
  147. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ssh.yaml +0 -0
  148. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/stat.yaml +0 -0
  149. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/strace.yaml +0 -0
  150. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sudo.yaml +0 -0
  151. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/systemctl.yaml +0 -0
  152. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tac.yaml +0 -0
  153. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tail.yaml +0 -0
  154. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tar.yaml +0 -0
  155. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tee.yaml +0 -0
  156. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/terraform.yaml +0 -0
  157. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/test.yaml +0 -0
  158. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/time.yaml +0 -0
  159. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/timeout.yaml +0 -0
  160. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/top.yaml +0 -0
  161. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/touch.yaml +0 -0
  162. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tr.yaml +0 -0
  163. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/traceroute.yaml +0 -0
  164. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/true.yaml +0 -0
  165. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tsc.yaml +0 -0
  166. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/type.yaml +0 -0
  167. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/uname.yaml +0 -0
  168. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/unexpand.yaml +0 -0
  169. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/uniq.yaml +0 -0
  170. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/unzip.yaml +0 -0
  171. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/uptime.yaml +0 -0
  172. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/uv.yaml +0 -0
  173. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/wc.yaml +0 -0
  174. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/wget.yaml +0 -0
  175. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/whereis.yaml +0 -0
  176. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/which.yaml +0 -0
  177. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/whoami.yaml +0 -0
  178. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/xargs.yaml +0 -0
  179. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/xmllint.yaml +0 -0
  180. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/yarn.yaml +0 -0
  181. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/yes.yaml +0 -0
  182. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/yq.yaml +0 -0
  183. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/zip.yaml +0 -0
  184. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/zsh.yaml +0 -0
  185. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/parser.py +0 -0
  186. {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/py.typed +0 -0
  187. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/__init__.py +0 -0
  188. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/conftest.py +0 -0
  189. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_cli.py +0 -0
  190. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_cli_serialization.py +0 -0
  191. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_matcher.py +0 -0
  192. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_models.py +0 -0
  193. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_parser.py +0 -0
  194. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_real_world.py +0 -0
  195. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_schema.py +0 -0
  196. {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_smoke.py +0 -0
  197. {bash_classify-0.5.0 → bash_classify-0.6.0}/uv.lock +0 -0
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "bash-classify-hook",
10
10
  "description": "Auto-allows low-risk bash commands in Claude Code using bash-classify",
11
- "version": "0.5.0",
11
+ "version": "0.6.0",
12
12
  "author": {
13
13
  "name": "Filip Procházka",
14
14
  "url": "https://github.com/fprochazka"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bash-classify
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Classify bash commands as READONLY, WRITE, DANGEROUS, or UNKNOWN
5
5
  Project-URL: Homepage, https://github.com/fprochazka/bash-classify
6
6
  Project-URL: Repository, https://github.com/fprochazka/bash-classify
@@ -33,7 +33,7 @@ Classify bash commands by their side-effect risk level.
33
33
 
34
34
  ## What it does
35
35
 
36
- bash-classify parses bash expressions using tree-sitter, classifies each command against a database of 120+ known commands, and outputs a structured JSON verdict. Commands are classified along two axes: **classification** (`READONLY`, `LOCAL_EFFECTS`, `EXTERNAL_EFFECTS`, `DANGEROUS`, `UNKNOWN`) describing what kind of effects a command has, and **risk** (`LOW`, `MEDIUM`, `HIGH`) describing how worried you should be.
36
+ bash-classify parses bash expressions using tree-sitter, classifies each command against a database of 150+ known commands, and outputs a structured JSON verdict. Commands are classified along two axes: **classification** (`READONLY`, `LOCAL_EFFECTS`, `EXTERNAL_EFFECTS`, `DANGEROUS`, `UNKNOWN`) describing what kind of effects a command has, and **risk** (`LOW`, `MEDIUM`, `HIGH`) describing how worried you should be.
37
37
 
38
38
  Designed primarily as a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) hook to automatically allow low-risk commands while flagging risky ones for human review.
39
39
 
@@ -86,10 +86,12 @@ Once installed, any Bash tool call with `risk: LOW` is auto-approved — no perm
86
86
 
87
87
  ## Command database
88
88
 
89
- The classification database includes 120+ command definitions covering common Unix utilities, package managers, container tools, cloud CLIs, and more.
89
+ bash-classify loads command definitions from two locations:
90
90
 
91
- - See [docs/classification-guidance.md](docs/classification-guidance.md) for how to add new commands
92
- - YAML definitions are validated against a JSON Schema for IDE autocomplete and CI checks
91
+ - **Built-in database** 150+ command definitions bundled with the package, covering common Unix utilities, package managers, container tools, cloud CLIs, and more. Lives in `src/bash_classify/commands/*.yaml`.
92
+ - **User database** — your own command definitions at `~/.config/bash-classify/commands/*.yaml` (override the location with the `BASH_CLASSIFY_CONFIG_DIR` env var, which resolves to `$BASH_CLASSIFY_CONFIG_DIR/commands/`). User files with the same name as a built-in override it completely, so you can customize classifications for internal tools, company-specific wrappers, or personal CLIs without forking the repo.
93
+
94
+ Both directories use the same YAML format. See [docs/classification-guidance.md](docs/classification-guidance.md) for how to add new commands. YAML definitions are validated against a [JSON Schema](schemas/command.schema.json) for IDE autocomplete and CI checks.
93
95
 
94
96
  ## Classification levels
95
97
 
@@ -118,7 +120,9 @@ Risk defaults are derived from classification (`READONLY`→LOW, `LOCAL_EFFECTS`
118
120
  - **Tree-sitter parsing** -- bash expressions are parsed into an AST for accurate command extraction, handling pipes, subshells, and command substitution
119
121
  - **YAML command database** -- each command has classification rules with subcommand and option matching
120
122
  - **Subcommand matching** -- `kubectl get` and `kubectl delete` can have different classifications
123
+ - **Multi-goal build tools** -- `subcommand_mode: match_all` handles commands like `mvn clean install` and `gradle clean build test` where multiple goals can be combined in any order
121
124
  - **Delegation for wrappers** -- commands like `xargs`, `sudo`, and `env` delegate classification to the inner command
125
+ - **File path detection** -- redirect operators (`>`, `>>`, `<`) are parsed into `write_paths`/`read_paths` in the output; writes to `/tmp` and `/var/tmp` stay at LOW risk
122
126
 
123
127
  ## Python API
124
128
 
@@ -4,7 +4,7 @@ Classify bash commands by their side-effect risk level.
4
4
 
5
5
  ## What it does
6
6
 
7
- bash-classify parses bash expressions using tree-sitter, classifies each command against a database of 120+ known commands, and outputs a structured JSON verdict. Commands are classified along two axes: **classification** (`READONLY`, `LOCAL_EFFECTS`, `EXTERNAL_EFFECTS`, `DANGEROUS`, `UNKNOWN`) describing what kind of effects a command has, and **risk** (`LOW`, `MEDIUM`, `HIGH`) describing how worried you should be.
7
+ bash-classify parses bash expressions using tree-sitter, classifies each command against a database of 150+ known commands, and outputs a structured JSON verdict. Commands are classified along two axes: **classification** (`READONLY`, `LOCAL_EFFECTS`, `EXTERNAL_EFFECTS`, `DANGEROUS`, `UNKNOWN`) describing what kind of effects a command has, and **risk** (`LOW`, `MEDIUM`, `HIGH`) describing how worried you should be.
8
8
 
9
9
  Designed primarily as a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) hook to automatically allow low-risk commands while flagging risky ones for human review.
10
10
 
@@ -57,10 +57,12 @@ Once installed, any Bash tool call with `risk: LOW` is auto-approved — no perm
57
57
 
58
58
  ## Command database
59
59
 
60
- The classification database includes 120+ command definitions covering common Unix utilities, package managers, container tools, cloud CLIs, and more.
60
+ bash-classify loads command definitions from two locations:
61
61
 
62
- - See [docs/classification-guidance.md](docs/classification-guidance.md) for how to add new commands
63
- - YAML definitions are validated against a JSON Schema for IDE autocomplete and CI checks
62
+ - **Built-in database** 150+ command definitions bundled with the package, covering common Unix utilities, package managers, container tools, cloud CLIs, and more. Lives in `src/bash_classify/commands/*.yaml`.
63
+ - **User database** — your own command definitions at `~/.config/bash-classify/commands/*.yaml` (override the location with the `BASH_CLASSIFY_CONFIG_DIR` env var, which resolves to `$BASH_CLASSIFY_CONFIG_DIR/commands/`). User files with the same name as a built-in override it completely, so you can customize classifications for internal tools, company-specific wrappers, or personal CLIs without forking the repo.
64
+
65
+ Both directories use the same YAML format. See [docs/classification-guidance.md](docs/classification-guidance.md) for how to add new commands. YAML definitions are validated against a [JSON Schema](schemas/command.schema.json) for IDE autocomplete and CI checks.
64
66
 
65
67
  ## Classification levels
66
68
 
@@ -89,7 +91,9 @@ Risk defaults are derived from classification (`READONLY`→LOW, `LOCAL_EFFECTS`
89
91
  - **Tree-sitter parsing** -- bash expressions are parsed into an AST for accurate command extraction, handling pipes, subshells, and command substitution
90
92
  - **YAML command database** -- each command has classification rules with subcommand and option matching
91
93
  - **Subcommand matching** -- `kubectl get` and `kubectl delete` can have different classifications
94
+ - **Multi-goal build tools** -- `subcommand_mode: match_all` handles commands like `mvn clean install` and `gradle clean build test` where multiple goals can be combined in any order
92
95
  - **Delegation for wrappers** -- commands like `xargs`, `sudo`, and `env` delegate classification to the inner command
96
+ - **File path detection** -- redirect operators (`>`, `>>`, `<`) are parsed into `write_paths`/`read_paths` in the output; writes to `/tmp` and `/var/tmp` stay at LOW risk
93
97
 
94
98
  ## Python API
95
99
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bash-classify-hook",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Auto-allows low-risk bash commands in Claude Code using bash-classify",
5
5
  "author": {
6
6
  "name": "Filip Prochazka"
@@ -6,6 +6,28 @@
6
6
  "type": "object",
7
7
  "required": ["command"],
8
8
  "additionalProperties": false,
9
+ "oneOf": [
10
+ {
11
+ "description": "Alias file: points at another command, mutually exclusive with classification-bearing fields",
12
+ "required": ["alias_of"],
13
+ "not": {
14
+ "anyOf": [
15
+ {"required": ["classification"]},
16
+ {"required": ["risk"]},
17
+ {"required": ["subcommands"]},
18
+ {"required": ["options"]},
19
+ {"required": ["global_options"]},
20
+ {"required": ["subcommand_mode"]},
21
+ {"required": ["delegates_to"]},
22
+ {"required": ["strict"]}
23
+ ]
24
+ }
25
+ },
26
+ {
27
+ "description": "Regular command definition",
28
+ "not": {"required": ["alias_of"]}
29
+ }
30
+ ],
9
31
  "properties": {
10
32
  "command": {
11
33
  "type": "string",
@@ -15,6 +37,10 @@
15
37
  "type": "string",
16
38
  "description": "Short description of the tool"
17
39
  },
40
+ "alias_of": {
41
+ "type": "string",
42
+ "description": "Name of another command in the database whose definition should be used verbatim. Mutually exclusive with classification/risk/subcommands/options/global_options/subcommand_mode/delegates_to/strict."
43
+ },
18
44
  "classification": {
19
45
  "$ref": "#/$defs/Classification"
20
46
  },
@@ -0,0 +1,211 @@
1
+ # $schema: ../../../schemas/command.schema.json
2
+ command: gws
3
+ description: "Google Workspace CLI — manages Drive, Sheets, Gmail, Calendar, Docs, and more via Google APIs"
4
+ classification: READONLY
5
+ strict: false
6
+
7
+ # Dispatch model:
8
+ #
9
+ # `gws <service> <resource> [sub-resource] <method>` is a 3-4 level hierarchy.
10
+ # bash-classify only honors `subcommand_mode: match_all` on the top-level
11
+ # command, not on nested subcommands. To avoid enumerating every
12
+ # service × resource × method triple, we run the whole thing in match_all
13
+ # mode at the top. Every positional token is matched independently against
14
+ # the flat verb dictionary below; unknown tokens (service names like
15
+ # "drive", resource names like "files", IDs, etc.) fall back to the base
16
+ # READONLY classification. The final classification is the max severity
17
+ # across all matched tokens.
18
+ #
19
+ # This means:
20
+ # gws drive files list -> list (READONLY) -> READONLY
21
+ # gws drive files delete -> delete (DANGEROUS) -> DANGEROUS
22
+ # gws drive files create -> create (EXTERNAL_EFFECTS) -> EXTERNAL
23
+ # gws gmail users messages send -> send (EXTERNAL_EFFECTS) -> EXTERNAL
24
+ #
25
+ # The verb `export` is used both for "download as format" (drive/sheets/docs
26
+ # export = READONLY) and for `auth export`, which by default prints *masked*
27
+ # credentials (also READONLY). Only `auth export --unmasked` prints plaintext
28
+ # OAuth tokens and client secrets — that specific case is flagged via the
29
+ # `--unmasked` global option override below.
30
+ subcommand_mode: match_all
31
+
32
+ global_options:
33
+ --help: {overrides: READONLY}
34
+ -h: {overrides: READONLY}
35
+ --format: {takes_value: true}
36
+ --params: {takes_value: true}
37
+ --json: {takes_value: true}
38
+ --upload: {takes_value: true}
39
+ --upload-content-type: {takes_value: true}
40
+ --output: {takes_value: true}
41
+ --api-version: {takes_value: true}
42
+ --page-all: {}
43
+ --page-limit: {takes_value: true}
44
+ --page-delay: {takes_value: true}
45
+ --sanitize: {takes_value: true}
46
+ --dry-run: {overrides: READONLY, risk: LOW}
47
+ # `auth export --unmasked` prints plaintext OAuth tokens and client secrets.
48
+ --unmasked: {overrides: DANGEROUS, risk: HIGH}
49
+
50
+ subcommands:
51
+ # ── service names (explicit READONLY so they don't pollute risk) ──
52
+ drive: {classification: READONLY}
53
+ sheets: {classification: READONLY}
54
+ gmail: {classification: READONLY}
55
+ calendar: {classification: READONLY}
56
+ docs: {classification: READONLY}
57
+ slides: {classification: READONLY}
58
+ tasks: {classification: READONLY}
59
+ people: {classification: READONLY}
60
+ chat: {classification: READONLY}
61
+ classroom: {classification: READONLY}
62
+ forms: {classification: READONLY}
63
+ keep: {classification: READONLY}
64
+ meet: {classification: READONLY}
65
+ events: {classification: READONLY}
66
+ workflow: {classification: READONLY}
67
+ wf: {classification: READONLY}
68
+ script: {classification: READONLY}
69
+ admin: {classification: READONLY}
70
+ admin-reports: {classification: READONLY}
71
+ reports: {classification: READONLY}
72
+ modelarmor: {classification: READONLY}
73
+ schema: {classification: READONLY}
74
+
75
+ # ── auth: a flat "verb" since match_all treats every token equally ─
76
+ # `auth` itself maps to READONLY; the method tokens below elevate.
77
+ # `auth export` defaults to READONLY (masked); `--unmasked` is flagged
78
+ # separately via the global option override above.
79
+ auth: {classification: READONLY}
80
+ status: {classification: READONLY}
81
+ # `auth login` performs an OAuth2 handshake with Google — creates/rotates
82
+ # a credential server-side, so it has external effects.
83
+ login: {classification: EXTERNAL_EFFECTS}
84
+ logout:
85
+ classification: LOCAL_EFFECTS
86
+ risk: LOW
87
+ # `auth setup` can shell out to `gcloud` and create GCP OAuth clients/projects.
88
+ setup: {classification: EXTERNAL_EFFECTS}
89
+
90
+ # ── READONLY methods ──────────────────────────────────────────────
91
+ get: {classification: READONLY}
92
+ getProfile: {classification: READONLY}
93
+ getContent: {classification: READONLY}
94
+ getMetrics: {classification: READONLY}
95
+ getThumbnail: {classification: READONLY}
96
+ getByDataFilter: {classification: READONLY}
97
+ getBatchGet: {classification: READONLY}
98
+ batchGet: {classification: READONLY}
99
+ list: {classification: READONLY}
100
+ listLabels: {classification: READONLY}
101
+ listContactGroups: {classification: READONLY}
102
+ listDirectoryPeople: {classification: READONLY}
103
+ listScriptProcesses: {classification: READONLY}
104
+ listUserProcesses: {classification: READONLY}
105
+ searchContacts: {classification: READONLY}
106
+ searchDirectoryPeople: {classification: READONLY}
107
+ search: {classification: READONLY}
108
+ query: {classification: READONLY}
109
+ findDirectMessage: {classification: READONLY}
110
+ freebusy: {classification: READONLY}
111
+ instances: {classification: READONLY}
112
+ about: {classification: READONLY}
113
+ download: {classification: READONLY}
114
+ export: {classification: READONLY} # drive/sheets/docs export; NOTE: auth export also matches
115
+ generateIds: {classification: READONLY}
116
+ generateCseToken: {classification: READONLY}
117
+
118
+ # ── READONLY helpers ('+' prefix) ─────────────────────────────────
119
+ "+read": {classification: READONLY}
120
+ "+triage": {classification: READONLY}
121
+ "+agenda": {classification: READONLY}
122
+ "+standup-report": {classification: READONLY}
123
+ "+meeting-prep": {classification: READONLY}
124
+ "+weekly-digest": {classification: READONLY}
125
+ "+subscribe": {classification: READONLY}
126
+
127
+ # ── EXTERNAL_EFFECTS methods ──────────────────────────────────────
128
+ create: {classification: EXTERNAL_EFFECTS}
129
+ createContact: {classification: EXTERNAL_EFFECTS}
130
+ update: {classification: EXTERNAL_EFFECTS}
131
+ updateContent: {classification: EXTERNAL_EFFECTS}
132
+ updateContact: {classification: EXTERNAL_EFFECTS}
133
+ updateContactPhoto: {classification: EXTERNAL_EFFECTS}
134
+ patch: {classification: EXTERNAL_EFFECTS}
135
+ insert: {classification: EXTERNAL_EFFECTS}
136
+ import: {classification: EXTERNAL_EFFECTS}
137
+ send: {classification: EXTERNAL_EFFECTS}
138
+ copy: {classification: EXTERNAL_EFFECTS}
139
+ copyTo: {classification: EXTERNAL_EFFECTS}
140
+ copyOtherContactToMyContactsGroup: {classification: EXTERNAL_EFFECTS}
141
+ move: {classification: EXTERNAL_EFFECTS}
142
+ modify: {classification: EXTERNAL_EFFECTS}
143
+ modifyLabels: {classification: EXTERNAL_EFFECTS}
144
+ modifyAttachments: {classification: EXTERNAL_EFFECTS}
145
+ modifyAssignment: {classification: EXTERNAL_EFFECTS}
146
+ modifyIndividualStudentsOptions: {classification: EXTERNAL_EFFECTS}
147
+ batchUpdate: {classification: EXTERNAL_EFFECTS}
148
+ batchUpdateByDataFilter: {classification: EXTERNAL_EFFECTS}
149
+ batchModify: {classification: EXTERNAL_EFFECTS}
150
+ batchCreateContacts: {classification: EXTERNAL_EFFECTS}
151
+ batchUpdateContacts: {classification: EXTERNAL_EFFECTS}
152
+ append: {classification: EXTERNAL_EFFECTS}
153
+ upload: {classification: EXTERNAL_EFFECTS}
154
+ watch: {classification: EXTERNAL_EFFECTS}
155
+ stop: {classification: EXTERNAL_EFFECTS}
156
+ untrash: {classification: EXTERNAL_EFFECTS}
157
+ hide: {classification: EXTERNAL_EFFECTS}
158
+ unhide: {classification: EXTERNAL_EFFECTS}
159
+ enable: {classification: EXTERNAL_EFFECTS}
160
+ disable: {classification: EXTERNAL_EFFECTS}
161
+ verify: {classification: EXTERNAL_EFFECTS}
162
+ accept: {classification: EXTERNAL_EFFECTS}
163
+ reclaim: {classification: EXTERNAL_EFFECTS}
164
+ return: {classification: EXTERNAL_EFFECTS}
165
+ turnIn: {classification: EXTERNAL_EFFECTS}
166
+ quickAdd: {classification: EXTERNAL_EFFECTS}
167
+ setUp: {classification: EXTERNAL_EFFECTS}
168
+ setPublishSettings: {classification: EXTERNAL_EFFECTS}
169
+ completeImport: {classification: EXTERNAL_EFFECTS}
170
+ reactivate: {classification: EXTERNAL_EFFECTS}
171
+ publish: {classification: EXTERNAL_EFFECTS}
172
+ endActiveConference: {classification: EXTERNAL_EFFECTS}
173
+ # Model Armor sanitize* methods POST user-controlled text to the remote
174
+ # modelarmor.{region}.rep.googleapis.com endpoint.
175
+ sanitizeUserPrompt: {classification: EXTERNAL_EFFECTS}
176
+ sanitizeModelResponse: {classification: EXTERNAL_EFFECTS}
177
+
178
+ # ── EXTERNAL_EFFECTS helpers ──────────────────────────────────────
179
+ "+upload": {classification: EXTERNAL_EFFECTS}
180
+ "+push": {classification: EXTERNAL_EFFECTS}
181
+ "+send": {classification: EXTERNAL_EFFECTS}
182
+ # `gmail +watch` creates Pub/Sub topic+subscription and calls users.watch —
183
+ # leaves persistent server-side GCP resources unless --cleanup is passed.
184
+ "+watch": {classification: EXTERNAL_EFFECTS}
185
+ "+sanitize-prompt": {classification: EXTERNAL_EFFECTS}
186
+ "+sanitize-response": {classification: EXTERNAL_EFFECTS}
187
+ "+reply": {classification: EXTERNAL_EFFECTS}
188
+ "+reply-all": {classification: EXTERNAL_EFFECTS}
189
+ "+forward": {classification: EXTERNAL_EFFECTS}
190
+ "+insert": {classification: EXTERNAL_EFFECTS}
191
+ "+write": {classification: EXTERNAL_EFFECTS}
192
+ "+append": {classification: EXTERNAL_EFFECTS}
193
+ "+email-to-task": {classification: EXTERNAL_EFFECTS}
194
+ "+file-announce": {classification: EXTERNAL_EFFECTS}
195
+ "+renew": {classification: EXTERNAL_EFFECTS}
196
+ "+create-template": {classification: EXTERNAL_EFFECTS}
197
+
198
+ # ── DANGEROUS methods ─────────────────────────────────────────────
199
+ delete: {classification: DANGEROUS}
200
+ deleteContact: {classification: DANGEROUS}
201
+ deleteContactPhoto: {classification: DANGEROUS}
202
+ batchDelete: {classification: DANGEROUS}
203
+ batchDeleteContacts: {classification: DANGEROUS}
204
+ batchClear: {classification: DANGEROUS}
205
+ batchClearByDataFilter: {classification: DANGEROUS}
206
+ clear: {classification: DANGEROUS}
207
+ trash: {classification: DANGEROUS}
208
+ emptyTrash: {classification: DANGEROUS}
209
+ remove: {classification: DANGEROUS}
210
+ # Running an Apps Script project executes arbitrary code.
211
+ run: {classification: DANGEROUS}
@@ -167,8 +167,32 @@ def _yaml_str(value: object) -> str:
167
167
  return str(value)
168
168
 
169
169
 
170
+ _ALIAS_FORBIDDEN_KEYS = frozenset(
171
+ {
172
+ "classification",
173
+ "risk",
174
+ "subcommands",
175
+ "options",
176
+ "global_options",
177
+ "subcommand_mode",
178
+ "delegates_to",
179
+ "strict",
180
+ }
181
+ )
182
+
183
+
170
184
  def _parse_command_def(data: dict, command_name: str) -> CommandDef:
171
185
  """Parse a raw YAML dict into a CommandDef structure."""
186
+ alias_of = data.get("alias_of")
187
+ if alias_of is not None:
188
+ conflicting = sorted(_ALIAS_FORBIDDEN_KEYS & data.keys())
189
+ if conflicting:
190
+ raise ValueError(
191
+ f"alias file for '{command_name}' has alias_of='{alias_of}' "
192
+ f"combined with forbidden field(s): {', '.join(conflicting)}"
193
+ )
194
+ return CommandDef(command=command_name, alias_of=str(alias_of))
195
+
172
196
  classification = _parse_classification(data.get("classification"))
173
197
  risk = _parse_risk(data.get("risk"))
174
198
 
@@ -94,6 +94,17 @@ def match_command(
94
94
  classification_reason="command not in database",
95
95
  )
96
96
 
97
+ # Step 1b: Resolve alias_of (max depth 1). The user-facing binary stays
98
+ # as typed; only command_def is swapped to the target.
99
+ if command_def.alias_of is not None:
100
+ target_name = command_def.alias_of
101
+ target_def = database.get(target_name)
102
+ if target_def is None:
103
+ raise ValueError(f"{binary} aliases '{target_name}' which is not in the database")
104
+ if target_def.alias_of is not None:
105
+ raise ValueError(f"alias chain too deep: {binary} -> {target_name} -> {target_def.alias_of}")
106
+ command_def = target_def
107
+
97
108
  remaining = argv[1:]
98
109
 
99
110
  # Step 2: Strip global options
@@ -135,6 +135,7 @@ class CommandDef:
135
135
  """Definition of a command (or subcommand) from the database."""
136
136
 
137
137
  command: str
138
+ alias_of: str | None = None
138
139
  classification: Classification | None = None
139
140
  risk: Risk | None = None
140
141
  global_options: dict[str, OptionDef] = field(default_factory=dict)
@@ -687,3 +687,60 @@ class TestFilePathDetection:
687
687
  result = classify_expression("cat < /home/user/data.txt | tee /tmp/out.txt > /tmp/log.txt", database=database)
688
688
  assert "/home/user/data.txt" in result.read_paths
689
689
  assert "/tmp/log.txt" in result.write_paths
690
+
691
+
692
+ class TestAliasOfClassification:
693
+ """Tests for the alias_of feature — an alias file points at another command def."""
694
+
695
+ def _make_db(self, tmp_path):
696
+ from bash_classify.database import load_database
697
+
698
+ (tmp_path / "target.yaml").write_text(
699
+ "command: target\n"
700
+ "classification: READONLY\n"
701
+ "strict: false\n"
702
+ "subcommands:\n"
703
+ " sub1: {classification: READONLY}\n"
704
+ " sub2: {classification: DANGEROUS}\n"
705
+ )
706
+ (tmp_path / "myalias.yaml").write_text("command: myalias\ndescription: pointer\nalias_of: target\n")
707
+ return load_database(tmp_path)
708
+
709
+ def test_alias_resolves_to_target(self, tmp_path) -> None:
710
+ db = self._make_db(tmp_path)
711
+ result = classify_expression("myalias sub1", database=db)
712
+ target_result = classify_expression("target sub1", database=db)
713
+ assert result.classification == target_result.classification
714
+ assert result.risk == target_result.risk
715
+ cmd = result.commands[0]
716
+ # matched_rule and command reflect the alias as typed
717
+ assert cmd.command[0] == "myalias"
718
+ assert cmd.matched_rule == "myalias.sub1"
719
+
720
+ def test_alias_resolves_dangerous_subcommand(self, tmp_path) -> None:
721
+ db = self._make_db(tmp_path)
722
+ result = classify_expression("myalias sub2", database=db)
723
+ assert result.classification == Classification.DANGEROUS
724
+ assert result.commands[0].matched_rule == "myalias.sub2"
725
+
726
+ def test_dangling_alias_raises(self, tmp_path) -> None:
727
+ import pytest
728
+
729
+ (tmp_path / "myalias.yaml").write_text("command: myalias\nalias_of: nonexistent\n")
730
+ from bash_classify.database import load_database
731
+
732
+ db = load_database(tmp_path)
733
+ with pytest.raises(ValueError, match="nonexistent"):
734
+ classify_expression("myalias foo", database=db)
735
+
736
+ def test_alias_chain_too_deep_raises(self, tmp_path) -> None:
737
+ import pytest
738
+
739
+ (tmp_path / "target.yaml").write_text("command: target\nclassification: READONLY\n")
740
+ (tmp_path / "mid.yaml").write_text("command: mid\nalias_of: target\n")
741
+ (tmp_path / "myalias.yaml").write_text("command: myalias\nalias_of: mid\n")
742
+ from bash_classify.database import load_database
743
+
744
+ db = load_database(tmp_path)
745
+ with pytest.raises(ValueError, match="alias chain too deep"):
746
+ classify_expression("myalias foo", database=db)
@@ -199,6 +199,45 @@ class TestStrictDefault:
199
199
  assert grep.strict is False
200
200
 
201
201
 
202
+ class TestAliasOf:
203
+ def test_alias_file_parses(self, tmp_path: Path) -> None:
204
+ """An alias file produces a CommandDef with alias_of set."""
205
+ target = tmp_path / "target.yaml"
206
+ target.write_text("command: target\nclassification: READONLY\n")
207
+ alias = tmp_path / "myalias.yaml"
208
+ alias.write_text("command: myalias\ndescription: pointer\nalias_of: target\n")
209
+
210
+ db = load_database(tmp_path)
211
+ alias_def = db["myalias"]
212
+ assert alias_def.alias_of == "target"
213
+ assert alias_def.command == "myalias"
214
+ assert alias_def.classification is None
215
+ assert alias_def.subcommands == {}
216
+
217
+ def test_alias_file_with_subcommands_raises(self, tmp_path: Path) -> None:
218
+ """An alias file combined with classification-bearing fields is rejected."""
219
+ alias = tmp_path / "bad.yaml"
220
+ alias.write_text("command: bad\nalias_of: target\nsubcommands:\n sub: {classification: READONLY}\n")
221
+ db = load_database(tmp_path)
222
+ with pytest.raises(ValueError, match="subcommands"):
223
+ db["bad"]
224
+
225
+ def test_alias_file_with_classification_raises(self, tmp_path: Path) -> None:
226
+ alias = tmp_path / "bad.yaml"
227
+ alias.write_text("command: bad\nalias_of: target\nclassification: READONLY\n")
228
+ db = load_database(tmp_path)
229
+ with pytest.raises(ValueError, match="classification"):
230
+ db["bad"]
231
+
232
+ def test_alias_file_only_command_description_alias_of_is_valid(self, tmp_path: Path) -> None:
233
+ target = tmp_path / "target.yaml"
234
+ target.write_text("command: target\nclassification: READONLY\n")
235
+ alias = tmp_path / "myalias.yaml"
236
+ alias.write_text('command: myalias\ndescription: "some desc"\nalias_of: target\n')
237
+ db = load_database(tmp_path)
238
+ assert db["myalias"].alias_of == "target"
239
+
240
+
202
241
  class TestUserCommandsDir:
203
242
  def test_user_override_replaces_builtin(self, tmp_path: Path) -> None:
204
243
  """User YAML overrides built-in command definition."""
File without changes
File without changes
File without changes
File without changes
File without changes