strictcli 0.17.0__tar.gz → 0.19.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 (161) hide show
  1. strictcli-0.19.0/.rlsbl/changes/.validated +1 -0
  2. strictcli-0.19.0/.rlsbl/changes/0.17.0.md +9 -0
  3. strictcli-0.19.0/.rlsbl/changes/0.18.0.jsonl +4 -0
  4. strictcli-0.19.0/.rlsbl/changes/0.18.0.md +5 -0
  5. strictcli-0.19.0/.rlsbl/changes/0.19.0.jsonl +2 -0
  6. strictcli-0.19.0/.rlsbl/changes/0.19.0.md +5 -0
  7. strictcli-0.19.0/.rlsbl/releases/v0.18.0.toml +8 -0
  8. strictcli-0.19.0/.rlsbl/releases/v0.19.0.toml +0 -0
  9. strictcli-0.19.0/.rlsbl/version +1 -0
  10. {strictcli-0.17.0 → strictcli-0.19.0}/CHANGELOG.md +21 -2
  11. {strictcli-0.17.0 → strictcli-0.19.0}/PKG-INFO +7 -7
  12. {strictcli-0.17.0 → strictcli-0.19.0}/README.md +6 -6
  13. {strictcli-0.17.0 → strictcli-0.19.0}/package-lock.json +2 -2
  14. {strictcli-0.17.0 → strictcli-0.19.0}/package.json +1 -1
  15. {strictcli-0.17.0 → strictcli-0.19.0}/pyproject.toml +1 -1
  16. {strictcli-0.17.0 → strictcli-0.19.0}/strictcli/__init__.py +101 -52
  17. strictcli-0.19.0/tests/test_check_public_api.py +435 -0
  18. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_runner.py +9 -9
  19. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_e2e.py +8 -8
  20. strictcli-0.17.0/tests/test_tags.py → strictcli-0.19.0/tests/test_flag_sets.py +22 -22
  21. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_passthrough.py +5 -5
  22. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_registration.py +4 -4
  23. strictcli-0.19.0/todo/.done/public-check-runner-api.md +36 -0
  24. {strictcli-0.17.0 → strictcli-0.19.0}/uv.lock +1 -1
  25. strictcli-0.17.0/.rlsbl/bases/.rlsbl/lint/go.toml +0 -17
  26. strictcli-0.17.0/.rlsbl/changes/.validated +0 -1
  27. strictcli-0.17.0/.rlsbl/lint/go.toml +0 -17
  28. strictcli-0.17.0/.rlsbl/version +0 -1
  29. {strictcli-0.17.0 → strictcli-0.19.0}/.claude/settings.json +0 -0
  30. {strictcli-0.17.0 → strictcli-0.19.0}/.github/workflows/ci.yml +0 -0
  31. {strictcli-0.17.0 → strictcli-0.19.0}/.github/workflows/publish.yml +0 -0
  32. {strictcli-0.17.0 → strictcli-0.19.0}/.gitignore +0 -0
  33. {strictcli-0.17.0 → strictcli-0.19.0}/.npmignore +0 -0
  34. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.claude/settings.json +0 -0
  35. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  36. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  37. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.gitignore +0 -0
  38. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.npmignore +0 -0
  39. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  40. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  41. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  42. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  43. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  44. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  45. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/CLAUDE.md +0 -0
  46. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/bases/LICENSE +0 -0
  47. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
  48. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.10.0.md +0 -0
  49. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
  50. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.11.0.md +0 -0
  51. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.12.0.jsonl +0 -0
  52. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.12.0.md +0 -0
  53. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.13.0.jsonl +0 -0
  54. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.13.0.md +0 -0
  55. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.14.0.jsonl +0 -0
  56. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.14.0.md +0 -0
  57. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.15.0.jsonl +0 -0
  58. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.15.0.md +0 -0
  59. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.15.1.jsonl +0 -0
  60. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.15.1.md +0 -0
  61. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.16.0.jsonl +0 -0
  62. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.16.0.md +0 -0
  63. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.16.1.jsonl +0 -0
  64. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.16.1.md +0 -0
  65. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.17.0.jsonl +0 -0
  66. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  67. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.4.0.md +0 -0
  68. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  69. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.4.1.md +0 -0
  70. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
  71. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.5.0.md +0 -0
  72. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
  73. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.6.0.md +0 -0
  74. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
  75. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.6.1.md +0 -0
  76. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
  77. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.7.0.md +0 -0
  78. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
  79. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.7.1.md +0 -0
  80. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
  81. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.0.md +0 -0
  82. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
  83. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.1.md +0 -0
  84. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
  85. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.2.md +0 -0
  86. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
  87. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.3.md +0 -0
  88. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
  89. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.4.md +0 -0
  90. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
  91. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.5.md +0 -0
  92. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
  93. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.6.md +0 -0
  94. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
  95. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.8.7.md +0 -0
  96. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
  97. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.9.0.md +0 -0
  98. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
  99. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/0.9.1.md +0 -0
  100. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  101. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/config.json +0 -0
  102. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/hashes.json +0 -0
  103. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/hooks/post-release.sh +0 -0
  104. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/hooks/pre-checks.sh +0 -0
  105. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/hooks/pre-release.sh +0 -0
  106. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/lint/npm.toml +0 -0
  107. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/lint/python.toml +0 -0
  108. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/unreleased.toml +0 -0
  109. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.10.0.toml +0 -0
  110. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.11.0.toml +0 -0
  111. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.12.0.toml +0 -0
  112. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.13.0.toml +0 -0
  113. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.15.0.toml +0 -0
  114. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.15.1.toml +0 -0
  115. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.16.0.toml +0 -0
  116. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.16.1.toml +0 -0
  117. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.17.0.toml +0 -0
  118. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.8.5.toml +0 -0
  119. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.8.6.toml +0 -0
  120. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.8.7.toml +0 -0
  121. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.9.0.toml +0 -0
  122. {strictcli-0.17.0 → strictcli-0.19.0}/.rlsbl/releases/v0.9.1.toml +0 -0
  123. {strictcli-0.17.0 → strictcli-0.19.0}/.strictcli/schema.json +0 -0
  124. {strictcli-0.17.0 → strictcli-0.19.0}/CLAUDE.md +0 -0
  125. {strictcli-0.17.0 → strictcli-0.19.0}/LICENSE +0 -0
  126. {strictcli-0.17.0 → strictcli-0.19.0}/index.js +0 -0
  127. {strictcli-0.17.0 → strictcli-0.19.0}/postinstall.js +0 -0
  128. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_arg_default.py +0 -0
  129. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_at_prefix.py +0 -0
  130. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_auto_version.py +0 -0
  131. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_command.py +0 -0
  132. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_discovery.py +0 -0
  133. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_schema.py +0 -0
  134. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_check_types.py +0 -0
  135. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_choices.py +0 -0
  136. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_command_help_suggestion.py +0 -0
  137. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_config.py +0 -0
  138. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_config_file_path.py +0 -0
  139. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_config_set_bugs.py +0 -0
  140. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_deep_nesting.py +0 -0
  141. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_dependencies.py +0 -0
  142. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_deprecated.py +0 -0
  143. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_dump_schema.py +0 -0
  144. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_env.py +0 -0
  145. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_exit_codes.py +0 -0
  146. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_float_type.py +0 -0
  147. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_global_flags.py +0 -0
  148. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_help.py +0 -0
  149. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_int_type.py +0 -0
  150. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_mutex.py +0 -0
  151. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_nesting.py +0 -0
  152. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_parser.py +0 -0
  153. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_repeatable.py +0 -0
  154. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_tagdsl.py +0 -0
  155. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_toml_loading.py +0 -0
  156. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_unique.py +0 -0
  157. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_utilities.py +0 -0
  158. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_validate.py +0 -0
  159. {strictcli-0.17.0 → strictcli-0.19.0}/tests/test_variadic.py +0 -0
  160. {strictcli-0.17.0 → strictcli-0.19.0}/todo/.defer/deferred.md +0 -0
  161. {strictcli-0.17.0 → strictcli-0.19.0}/todo/.done/original-idea.md +0 -0
@@ -0,0 +1 @@
1
+ 436f8a9ce1b048614cb66e929969dfbcb0791689
@@ -0,0 +1,9 @@
1
+ ## 0.17.0
2
+
3
+ ### Breaking
4
+
5
+ - **Breaking.** Repeatable flag defaults are now validated at registration: must be a list, `default=[]` is rejected as redundant, and element types must match the flag type.
6
+
7
+ ### Features
8
+
9
+ - **New.** Help text now displays non-empty repeatable flag defaults as `[default: a, b, c]`.
@@ -0,0 +1,4 @@
1
+ {"commits":["ab63f2e29d95b834aef6e3307d23b53e3f845afe","fc7ae2927aa12b7627c3119c818d9a7e89209e39","db1924cbd143285c857324a8fcf2110718080bfa"],"user_facing":true,"description":"**New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.","type":"feature"}
2
+ {"commits":["6407ea33c3a16a83dcd7c362cda8e98980074a73","edfcf45b0eb1ba4896fcff1d31ea9993c9b363a1","fcaae9bb7895805308a6b9299158ada7f6d45f3b","b6720d2dd1063bff0b49f6d5624608672c0cac33"],"user_facing":false}
3
+ {"commits":["9cc04830fa5b7eef0d87b249e57248415fd769a5","4e89101a5fdb8c0a6f46b62829f22e09b79ec2d1"],"user_facing":false}
4
+ {"commits":["67ee39ff3cdf4d09a9a29c0bdcb9853999878ee9"],"user_facing":false}
@@ -0,0 +1,5 @@
1
+ ## 0.18.0
2
+
3
+ ### Features
4
+
5
+ - **New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.
@@ -0,0 +1,2 @@
1
+ {"commits":["03d0cd60c4d207b568a19bf6916f58aacdac1450","5a1368de51cbe0abe465f21f3a38f78ba1746fef"],"user_facing":true,"description":"**Breaking.** Renamed `Tag` to `FlagSet` and `tags=` parameter to `flag_sets=` across all API surfaces.","type":"breaking"}
2
+ {"commits":["03782821de6b48073259aa3edb66b141ab70eb3b","232a6e47db207159abfdb81a5e1b93c1cfce82ea","fa29d925446a00a2bddec93cb949f91f13682704"],"user_facing":false}
@@ -0,0 +1,5 @@
1
+ ## 0.19.0
2
+
3
+ ### Breaking
4
+
5
+ - **Breaking.** Renamed `Tag` to `FlagSet` and `tags=` parameter to `flag_sets=` across all API surfaces.
@@ -0,0 +1,8 @@
1
+ # Version bump type: patch, minor, or major
2
+ bump = "minor"
3
+ # Short description of this release (required)
4
+ description = "Public API for running checks programmatically"
5
+ # Optional context explaining why these changes were made
6
+ context = "Adds App.run_checks(), format_check_results(), format_check_results_json(), and CheckRunResult — a stable public API replacing the private _filter_checks/_resolve_check_order/_run_checks/_check_format_human functions that consumers like rlsbl were forced to use."
7
+ include = ["pypi", "npm"]
8
+ exclude = []
File without changes
@@ -0,0 +1 @@
1
+ 0.69.2
@@ -2,9 +2,28 @@
2
2
 
3
3
  # Changelog
4
4
 
5
- ## 0.17.0
5
+ ## 0.19.0
6
+
7
+ Renamed Tag to FlagSet
8
+
9
+ <details>
10
+ <summary>Context</summary>
11
+
12
+ Breaking: Tag class renamed to FlagSet, tags= parameter renamed to flag_sets= across all API surfaces. The Tag name was misleading — it implied labeling/annotation, but the type is actually a reusable bundle of flags.
13
+
14
+ </details>
15
+
16
+ ### Breaking
6
17
 
7
- Repeatable flag default validation and help text display
18
+ - **Breaking.** Renamed `Tag` to `FlagSet` and `tags=` parameter to `flag_sets=` across all API surfaces.
19
+
20
+ ## 0.18.0
21
+
22
+ ### Features
23
+
24
+ - **New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.
25
+
26
+ ## 0.17.0
8
27
 
9
28
  ### Breaking
10
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.17.0
3
+ Version: 0.19.0
4
4
  Summary: A strict, zero-dependency CLI framework for Python
5
5
  Project-URL: Homepage, https://github.com/smm-h/strictcli
6
6
  Project-URL: Repository, https://github.com/smm-h/strictcli
@@ -222,12 +222,12 @@ Values resolve in this order: CLI argument > environment variable > default. If
222
222
 
223
223
  Bool flags from env vars accept `1`, `true`, `yes` (case-insensitive) for `True` and `0`, `false`, `no` for `False`. Any other value is an error.
224
224
 
225
- ## Tags
225
+ ## FlagSets
226
226
 
227
- Tags are reusable bundles of flags that can be applied to multiple commands:
227
+ FlagSets are reusable bundles of flags that can be applied to multiple commands:
228
228
 
229
229
  ```python
230
- auth_tag = strictcli.Tag(
230
+ auth_flags = strictcli.FlagSet(
231
231
  name="auth",
232
232
  flags=[
233
233
  strictcli.Flag(name="token", type=str, help="auth token", env="MYAPP_TOKEN", default=""),
@@ -235,16 +235,16 @@ auth_tag = strictcli.Tag(
235
235
  ],
236
236
  )
237
237
 
238
- @app.command("deploy", help="deploy the app", tags=[auth_tag])
238
+ @app.command("deploy", help="deploy the app", flag_sets=[auth_flags])
239
239
  def deploy(token, insecure):
240
240
  print(f"token={'set' if token else 'unset'}, insecure={insecure}")
241
241
 
242
- @app.command("status", help="check status", tags=[auth_tag])
242
+ @app.command("status", help="check status", flag_sets=[auth_flags])
243
243
  def status(token, insecure):
244
244
  print(f"checking status")
245
245
  ```
246
246
 
247
- Both commands now have `--token` and `--insecure` flags. Tag flags appear in help output and are parsed like any other flag.
247
+ Both commands now have `--token` and `--insecure` flags. FlagSet flags appear in help output and are parsed like any other flag.
248
248
 
249
249
  ## Mutex Groups
250
250
 
@@ -204,12 +204,12 @@ Values resolve in this order: CLI argument > environment variable > default. If
204
204
 
205
205
  Bool flags from env vars accept `1`, `true`, `yes` (case-insensitive) for `True` and `0`, `false`, `no` for `False`. Any other value is an error.
206
206
 
207
- ## Tags
207
+ ## FlagSets
208
208
 
209
- Tags are reusable bundles of flags that can be applied to multiple commands:
209
+ FlagSets are reusable bundles of flags that can be applied to multiple commands:
210
210
 
211
211
  ```python
212
- auth_tag = strictcli.Tag(
212
+ auth_flags = strictcli.FlagSet(
213
213
  name="auth",
214
214
  flags=[
215
215
  strictcli.Flag(name="token", type=str, help="auth token", env="MYAPP_TOKEN", default=""),
@@ -217,16 +217,16 @@ auth_tag = strictcli.Tag(
217
217
  ],
218
218
  )
219
219
 
220
- @app.command("deploy", help="deploy the app", tags=[auth_tag])
220
+ @app.command("deploy", help="deploy the app", flag_sets=[auth_flags])
221
221
  def deploy(token, insecure):
222
222
  print(f"token={'set' if token else 'unset'}, insecure={insecure}")
223
223
 
224
- @app.command("status", help="check status", tags=[auth_tag])
224
+ @app.command("status", help="check status", flag_sets=[auth_flags])
225
225
  def status(token, insecure):
226
226
  print(f"checking status")
227
227
  ```
228
228
 
229
- Both commands now have `--token` and `--insecure` flags. Tag flags appear in help output and are parsed like any other flag.
229
+ Both commands now have `--token` and `--insecure` flags. FlagSet flags appear in help output and are parsed like any other flag.
230
230
 
231
231
  ## Mutex Groups
232
232
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "strictcli",
9
- "version": "0.17.0",
9
+ "version": "0.19.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "A strict, zero-dependency CLI framework for Python (npm wrapper)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "strictcli"
7
- version = "0.17.0"
7
+ version = "0.19.0"
8
8
  description = "A strict, zero-dependency CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,12 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.17.0"
5
+ __version__ = "0.19.0"
6
6
 
7
7
  __all__ = [
8
- "App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
8
+ "App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
9
9
  "Implies", "Passthrough", "DeprecatedCommand", "Result", "flag", "arg",
10
- "CheckResult", "CheckContext",
10
+ "CheckResult", "CheckContext", "CheckRunResult",
11
+ "format_check_results", "format_check_results_json",
11
12
  ]
12
13
 
13
14
  import contextlib
@@ -676,7 +677,7 @@ class Arg:
676
677
 
677
678
 
678
679
  @dataclass
679
- class Tag:
680
+ class FlagSet:
680
681
  """A reusable bundle of flags."""
681
682
 
682
683
  name: str
@@ -739,7 +740,7 @@ class Command:
739
740
  handler: Callable | None
740
741
  flags: list[Flag] = field(default_factory=list)
741
742
  args: list[Arg] = field(default_factory=list)
742
- tags: list[Tag] = field(default_factory=list)
743
+ flag_sets: list[FlagSet] = field(default_factory=list)
743
744
  mutex: list[MutexGroup] = field(default_factory=list)
744
745
  dependencies: list[CoRequired | Requires | Implies] = field(default_factory=list)
745
746
  passthrough: Passthrough | None = None
@@ -804,7 +805,7 @@ class Group:
804
805
  *,
805
806
  help: str,
806
807
  args: list[Arg] | None = None,
807
- tags: list[Tag] | None = None,
808
+ flag_sets: list[FlagSet] | None = None,
808
809
  mutex: list[MutexGroup] | None = None,
809
810
  dependencies: list[CoRequired | Requires | Implies] | None = None,
810
811
  passthrough: Passthrough | None = None,
@@ -817,7 +818,7 @@ class Group:
817
818
  f'command "{name}" collides with an existing group'
818
819
  )
819
820
  cmd = _build_and_validate_command(
820
- name, help=help, handler=func, args=args, tags=tags, mutex=mutex,
821
+ name, help=help, handler=func, args=args, flag_sets=flag_sets, mutex=mutex,
821
822
  dependencies=dependencies,
822
823
  env_prefix=self.env_prefix,
823
824
  global_flags=self._global_flags,
@@ -856,6 +857,14 @@ class CheckResult:
856
857
  raise ValueError("CheckResult.message must be a non-empty string")
857
858
 
858
859
 
860
+ @dataclass(frozen=True)
861
+ class CheckRunResult:
862
+ """A named check result returned by App.run_checks()."""
863
+
864
+ name: str
865
+ result: CheckResult
866
+
867
+
859
868
  @runtime_checkable
860
869
  class CheckContext(Protocol):
861
870
  """Minimal interface that tool-specific check contexts must satisfy."""
@@ -1001,6 +1010,38 @@ class App:
1001
1010
  """
1002
1011
  self._check_context_factory = factory
1003
1012
 
1013
+ def run_checks(
1014
+ self,
1015
+ context: CheckContext,
1016
+ *,
1017
+ tag_expr: str | None = None,
1018
+ name_glob: str | None = None,
1019
+ run_all: bool = False,
1020
+ ignore_warnings: bool = False,
1021
+ ) -> tuple[list[CheckRunResult], int]:
1022
+ """Run checks programmatically with filtering and dependency resolution.
1023
+
1024
+ Returns (results, exit_code) where results is a list of CheckRunResult
1025
+ and exit_code is 0 if all pass (or all warn with ignore_warnings), else 1.
1026
+ """
1027
+ if not self._checks_enabled:
1028
+ raise ValueError("checks are not enabled on this App")
1029
+ err = self._validate_check_registrations()
1030
+ if err:
1031
+ raise ValueError(err)
1032
+ selected = _filter_checks(self._check_defs, tag_expr, name_glob, run_all)
1033
+ if not selected:
1034
+ return ([], 0)
1035
+ order = _resolve_check_order(self._check_defs, selected)
1036
+ raw_results, exit_code = _run_checks(
1037
+ self._check_defs, order, context, ignore_warnings,
1038
+ )
1039
+ results = [
1040
+ CheckRunResult(name=name, result=result)
1041
+ for name, result in raw_results
1042
+ ]
1043
+ return (results, exit_code)
1044
+
1004
1045
  def _register_check_command(self) -> None:
1005
1046
  """Register the auto-generated 'check' command when checks.toml exists."""
1006
1047
  app_ref = self # capture for closure
@@ -1048,14 +1089,19 @@ class App:
1048
1089
  )
1049
1090
  return 1
1050
1091
  context = app_ref._check_context_factory()
1051
- results, exit_code = _run_checks(
1052
- app_ref, order, context, ignore_warnings,
1092
+ raw_results, exit_code = _run_checks(
1093
+ app_ref._check_defs, order, context, ignore_warnings,
1053
1094
  )
1054
1095
 
1096
+ results_wrapped = [
1097
+ CheckRunResult(name=n, result=r) for n, r in raw_results
1098
+ ]
1055
1099
  if json:
1056
- _check_format_json(results)
1100
+ print(format_check_results_json(results_wrapped))
1057
1101
  else:
1058
- _check_format_human(results, verbose)
1102
+ output = format_check_results(results_wrapped, verbose)
1103
+ if output:
1104
+ print(output)
1059
1105
 
1060
1106
  return exit_code
1061
1107
 
@@ -1078,7 +1124,7 @@ class App:
1078
1124
  help="Run project checks",
1079
1125
  handler=_check_handler,
1080
1126
  args=None,
1081
- tags=None,
1127
+ flag_sets=None,
1082
1128
  mutex=None,
1083
1129
  dependencies=None,
1084
1130
  env_prefix=self.env_prefix,
@@ -1094,7 +1140,7 @@ class App:
1094
1140
  *,
1095
1141
  help: str,
1096
1142
  args: list[Arg] | None = None,
1097
- tags: list[Tag] | None = None,
1143
+ flag_sets: list[FlagSet] | None = None,
1098
1144
  mutex: list[MutexGroup] | None = None,
1099
1145
  dependencies: list[CoRequired | Requires | Implies] | None = None,
1100
1146
  passthrough: Passthrough | None = None,
@@ -1107,7 +1153,7 @@ class App:
1107
1153
  help=help,
1108
1154
  handler=func,
1109
1155
  args=args,
1110
- tags=tags,
1156
+ flag_sets=flag_sets,
1111
1157
  mutex=mutex,
1112
1158
  dependencies=dependencies,
1113
1159
  env_prefix=self.env_prefix,
@@ -2437,7 +2483,7 @@ def _build_and_validate_command(
2437
2483
  help: str,
2438
2484
  handler: Callable,
2439
2485
  args: list[Arg] | None,
2440
- tags: list[Tag] | None,
2486
+ flag_sets: list[FlagSet] | None,
2441
2487
  mutex: list[MutexGroup] | None,
2442
2488
  dependencies: list[CoRequired | Requires | Implies] | None = None,
2443
2489
  env_prefix: str | None,
@@ -2449,22 +2495,22 @@ def _build_and_validate_command(
2449
2495
  if not help or not help.strip():
2450
2496
  raise ValueError(f'command "{name}": missing help text')
2451
2497
 
2452
- # Passthrough commands must not have flags, args, tags, or mutex groups
2498
+ # Passthrough commands must not have flags, args, flag sets, or mutex groups
2453
2499
  if passthrough is not None:
2454
2500
  decorator_flags = list(getattr(handler, "_strictcli_flags", []))
2455
2501
  decorator_args = list(getattr(handler, "_strictcli_args", []))
2456
2502
  has_flags = bool(decorator_flags)
2457
2503
  has_args = bool(args) or bool(decorator_args)
2458
- has_tags = bool(tags)
2504
+ has_flag_sets = bool(flag_sets)
2459
2505
  has_mutex = bool(mutex)
2460
- if has_flags or has_args or has_tags or has_mutex:
2506
+ if has_flags or has_args or has_flag_sets or has_mutex:
2461
2507
  parts = []
2462
2508
  if has_flags:
2463
2509
  parts.append("flags")
2464
2510
  if has_args:
2465
2511
  parts.append("args")
2466
- if has_tags:
2467
- parts.append("tags")
2512
+ if has_flag_sets:
2513
+ parts.append("flag sets")
2468
2514
  if has_mutex:
2469
2515
  parts.append("mutex groups")
2470
2516
  raise ValueError(
@@ -2489,11 +2535,11 @@ def _build_and_validate_command(
2489
2535
  all_args = list(args) if args else []
2490
2536
  all_args.extend(decorator_args)
2491
2537
 
2492
- # Merge tags into flags
2493
- resolved_tags = list(tags) if tags else []
2494
- tag_flags: list[Flag] = []
2495
- for tag in resolved_tags:
2496
- tag_flags.extend(tag.flags)
2538
+ # Merge flag sets into flags
2539
+ resolved_flag_sets = list(flag_sets) if flag_sets else []
2540
+ flag_set_flags: list[Flag] = []
2541
+ for flag_set in resolved_flag_sets:
2542
+ flag_set_flags.extend(flag_set.flags)
2497
2543
 
2498
2544
  # Resolve mutex groups and merge their flags
2499
2545
  resolved_mutex = list(mutex) if mutex else []
@@ -2517,13 +2563,13 @@ def _build_and_validate_command(
2517
2563
  )
2518
2564
  mutex_flag_names.add(f.name)
2519
2565
 
2520
- # All flags: decorator flags + tag flags + mutex flags + extra flags
2521
- all_flags = decorator_flags + tag_flags + mutex_flags
2566
+ # All flags: decorator flags + flag set flags + mutex flags + extra flags
2567
+ all_flags = decorator_flags + flag_set_flags + mutex_flags
2522
2568
  if extra_flags:
2523
2569
  all_flags.extend(extra_flags)
2524
2570
 
2525
2571
  # Validate: no duplicate flag names (catches mutex flags overlapping with
2526
- # regular flags or tag flags)
2572
+ # regular flags or flag set flags)
2527
2573
  seen_flag_names: set[str] = set()
2528
2574
  for f in all_flags:
2529
2575
  if f.name in seen_flag_names:
@@ -2699,7 +2745,7 @@ def _build_and_validate_command(
2699
2745
  handler=handler,
2700
2746
  flags=all_flags,
2701
2747
  args=all_args,
2702
- tags=resolved_tags,
2748
+ flag_sets=resolved_flag_sets,
2703
2749
  mutex=resolved_mutex,
2704
2750
  dependencies=resolved_dependencies,
2705
2751
  )
@@ -3330,7 +3376,7 @@ def _find_cycle(
3330
3376
 
3331
3377
 
3332
3378
  def _run_checks(
3333
- app: "App",
3379
+ check_defs: dict,
3334
3380
  check_names: list[str],
3335
3381
  context: CheckContext,
3336
3382
  ignore_warnings: bool,
@@ -3340,7 +3386,6 @@ def _run_checks(
3340
3386
  Returns (results_list, exit_code). exit_code is 0 if all pass (or all
3341
3387
  warn with ignore_warnings=True), 1 otherwise.
3342
3388
  """
3343
- check_defs = app._check_defs
3344
3389
  results: list[tuple[str, CheckResult]] = []
3345
3390
  failed_checks: set[str] = set()
3346
3391
  exit_code = 0
@@ -3440,39 +3485,43 @@ def _check_dry_run_mode(
3440
3485
  print(f" {i}. {name}")
3441
3486
 
3442
3487
 
3443
- def _check_format_human(
3444
- results: list[tuple[str, CheckResult]], verbose: bool,
3445
- ) -> None:
3446
- """Print check results in human-readable aligned format."""
3488
+
3489
+ def format_check_results(
3490
+ results: list[CheckRunResult], verbose: bool = False,
3491
+ ) -> str:
3492
+ """Format check results as a human-readable aligned string."""
3447
3493
  if not results:
3448
- return
3494
+ return ""
3449
3495
 
3450
- name_width = max(len(name) for name, _ in results)
3496
+ name_width = max(len(r.name) for r in results)
3497
+ lines: list[str] = []
3451
3498
 
3452
- for name, result in results:
3453
- label = _CHECK_STATUS_LABELS[result.status]
3454
- print(f"{label} {name:<{name_width}} {result.message}")
3499
+ for r in results:
3500
+ label = _CHECK_STATUS_LABELS[r.result.status]
3501
+ lines.append(f"{label} {r.name:<{name_width}} {r.result.message}")
3455
3502
 
3456
- show_details = result.details and (
3457
- verbose or result.status in ("fail", "warn", "skip")
3503
+ show_details = r.result.details and (
3504
+ verbose or r.result.status in ("fail", "warn", "skip")
3458
3505
  )
3459
3506
  if show_details:
3460
- for detail in result.details:
3461
- print(f" {detail}")
3507
+ for detail in r.result.details:
3508
+ lines.append(f" {detail}")
3509
+
3510
+ return "\n".join(lines)
3462
3511
 
3463
3512
 
3464
- def _check_format_json(results: list[tuple[str, CheckResult]]) -> None:
3465
- """Print check results as JSON."""
3513
+ def format_check_results_json(results: list[CheckRunResult]) -> str:
3514
+ """Format check results as a JSON string."""
3466
3515
  items = [
3467
3516
  {
3468
- "name": name,
3469
- "status": result.status,
3470
- "message": result.message,
3471
- "details": result.details,
3517
+ "name": r.name,
3518
+ "status": r.result.status,
3519
+ "message": r.result.message,
3520
+ "details": r.result.details if r.result.details is not None else [],
3472
3521
  }
3473
- for name, result in results
3522
+ for r in results
3474
3523
  ]
3475
- print(json.dumps(items, separators=(",", ":")))
3524
+ return json.dumps(items, separators=(",", ":"))
3476
3525
 
3477
3526
 
3478
3527
  # ---------------------------------------------------------------------------