strictcli 0.13.0__tar.gz → 0.15.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 (140) hide show
  1. strictcli-0.15.0/.github/workflows/publish.yml +27 -0
  2. strictcli-0.15.0/.npmignore +34 -0
  3. strictcli-0.15.0/.rlsbl/bases/.github/workflows/publish.yml +27 -0
  4. strictcli-0.15.0/.rlsbl/bases/.npmignore +34 -0
  5. strictcli-0.15.0/.rlsbl/changes/.validated +1 -0
  6. strictcli-0.15.0/.rlsbl/changes/0.13.0.md +5 -0
  7. strictcli-0.15.0/.rlsbl/changes/0.14.0.jsonl +3 -0
  8. strictcli-0.15.0/.rlsbl/changes/0.14.0.md +5 -0
  9. strictcli-0.15.0/.rlsbl/changes/0.15.0.jsonl +6 -0
  10. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/hashes.json +3 -2
  11. strictcli-0.15.0/.rlsbl/releases/v0.15.0.toml +8 -0
  12. strictcli-0.15.0/.rlsbl/version +1 -0
  13. {strictcli-0.13.0 → strictcli-0.15.0}/CHANGELOG.md +25 -0
  14. {strictcli-0.13.0 → strictcli-0.15.0}/PKG-INFO +1 -1
  15. {strictcli-0.13.0 → strictcli-0.15.0}/package-lock.json +2 -2
  16. {strictcli-0.13.0 → strictcli-0.15.0}/package.json +1 -1
  17. {strictcli-0.13.0 → strictcli-0.15.0}/pyproject.toml +1 -1
  18. {strictcli-0.13.0 → strictcli-0.15.0}/strictcli/__init__.py +97 -42
  19. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_discovery.py +51 -0
  20. strictcli-0.15.0/tests/test_config_set_bugs.py +242 -0
  21. {strictcli-0.13.0 → strictcli-0.15.0}/uv.lock +1 -1
  22. strictcli-0.13.0/.github/workflows/publish.yml +0 -32
  23. strictcli-0.13.0/.rlsbl/bases/.github/workflows/publish.yml +0 -32
  24. strictcli-0.13.0/.rlsbl/changes/.validated +0 -1
  25. strictcli-0.13.0/.rlsbl/version +0 -1
  26. {strictcli-0.13.0 → strictcli-0.15.0}/.claude/settings.json +0 -0
  27. {strictcli-0.13.0 → strictcli-0.15.0}/.github/workflows/ci.yml +0 -0
  28. {strictcli-0.13.0 → strictcli-0.15.0}/.gitignore +0 -0
  29. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.claude/settings.json +0 -0
  30. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  31. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.gitignore +0 -0
  32. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  33. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  34. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  35. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  36. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  37. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  38. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  39. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/CLAUDE.md +0 -0
  40. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/bases/LICENSE +0 -0
  41. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
  42. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.10.0.md +0 -0
  43. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
  44. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.11.0.md +0 -0
  45. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.12.0.jsonl +0 -0
  46. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.12.0.md +0 -0
  47. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.13.0.jsonl +0 -0
  48. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  49. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.4.0.md +0 -0
  50. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  51. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.4.1.md +0 -0
  52. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
  53. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.5.0.md +0 -0
  54. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
  55. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.6.0.md +0 -0
  56. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
  57. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.6.1.md +0 -0
  58. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
  59. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.7.0.md +0 -0
  60. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
  61. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.7.1.md +0 -0
  62. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
  63. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.0.md +0 -0
  64. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
  65. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.1.md +0 -0
  66. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
  67. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.2.md +0 -0
  68. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
  69. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.3.md +0 -0
  70. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
  71. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.4.md +0 -0
  72. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
  73. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.5.md +0 -0
  74. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
  75. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.6.md +0 -0
  76. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
  77. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.8.7.md +0 -0
  78. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
  79. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.9.0.md +0 -0
  80. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
  81. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/0.9.1.md +0 -0
  82. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  83. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/config.json +0 -0
  84. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/hooks/post-release.sh +0 -0
  85. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/hooks/pre-checks.sh +0 -0
  86. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/hooks/pre-release.sh +0 -0
  87. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/lint/go.toml +0 -0
  88. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/lint/npm.toml +0 -0
  89. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/lint/python.toml +0 -0
  90. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/unreleased.toml +0 -0
  91. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.10.0.toml +0 -0
  92. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.11.0.toml +0 -0
  93. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.12.0.toml +0 -0
  94. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.13.0.toml +0 -0
  95. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.8.5.toml +0 -0
  96. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.8.6.toml +0 -0
  97. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.8.7.toml +0 -0
  98. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.9.0.toml +0 -0
  99. {strictcli-0.13.0 → strictcli-0.15.0}/.rlsbl/releases/v0.9.1.toml +0 -0
  100. {strictcli-0.13.0 → strictcli-0.15.0}/.strictcli/schema.json +0 -0
  101. {strictcli-0.13.0 → strictcli-0.15.0}/CLAUDE.md +0 -0
  102. {strictcli-0.13.0 → strictcli-0.15.0}/LICENSE +0 -0
  103. {strictcli-0.13.0 → strictcli-0.15.0}/README.md +0 -0
  104. {strictcli-0.13.0 → strictcli-0.15.0}/index.js +0 -0
  105. {strictcli-0.13.0 → strictcli-0.15.0}/postinstall.js +0 -0
  106. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_arg_default.py +0 -0
  107. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_at_prefix.py +0 -0
  108. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_auto_version.py +0 -0
  109. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_command.py +0 -0
  110. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_runner.py +0 -0
  111. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_schema.py +0 -0
  112. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_check_types.py +0 -0
  113. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_choices.py +0 -0
  114. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_command_help_suggestion.py +0 -0
  115. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_config.py +0 -0
  116. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_config_file_path.py +0 -0
  117. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_deep_nesting.py +0 -0
  118. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_dependencies.py +0 -0
  119. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_deprecated.py +0 -0
  120. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_dump_schema.py +0 -0
  121. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_e2e.py +0 -0
  122. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_env.py +0 -0
  123. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_exit_codes.py +0 -0
  124. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_float_type.py +0 -0
  125. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_global_flags.py +0 -0
  126. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_help.py +0 -0
  127. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_int_type.py +0 -0
  128. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_mutex.py +0 -0
  129. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_nesting.py +0 -0
  130. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_parser.py +0 -0
  131. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_passthrough.py +0 -0
  132. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_registration.py +0 -0
  133. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_repeatable.py +0 -0
  134. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_tagdsl.py +0 -0
  135. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_tags.py +0 -0
  136. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_toml_loading.py +0 -0
  137. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_validate.py +0 -0
  138. {strictcli-0.13.0 → strictcli-0.15.0}/tests/test_variadic.py +0 -0
  139. {strictcli-0.13.0 → strictcli-0.15.0}/todo/.defer/deferred.md +0 -0
  140. {strictcli-0.13.0 → strictcli-0.15.0}/todo/.done/original-idea.md +0 -0
@@ -0,0 +1,27 @@
1
+ name: Publish
2
+ on:
3
+ release:
4
+ types: [published]
5
+ workflow_dispatch:
6
+ permissions:
7
+ contents: read
8
+ id-token: write
9
+ jobs:
10
+ pypi:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+ - uses: astral-sh/setup-uv@v7
15
+ - run: uv build
16
+ - uses: pypa/gh-action-pypi-publish@release/v1
17
+ npm:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+ - uses: actions/setup-node@v6
22
+ with:
23
+ node-version: 24
24
+ registry-url: https://registry.npmjs.org
25
+ - run: npm publish --provenance --access public
26
+ env:
27
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,34 @@
1
+ # Development and CI
2
+ tests/
3
+ test/
4
+ __tests__/
5
+ .github/
6
+ scripts/
7
+ docs/
8
+ todo/
9
+ e2e/
10
+
11
+ # rlsbl metadata
12
+ .rlsbl/
13
+ .rlsbl-monorepo/
14
+
15
+ # Python artifacts (for multi-target projects)
16
+ __pycache__/
17
+ *.pyc
18
+ *.py
19
+ *.toml
20
+ .venv/
21
+
22
+ # Build/cache
23
+ coverage/
24
+ .nyc_output/
25
+
26
+ # Editor/OS
27
+ .DS_Store
28
+ *.log
29
+ .env
30
+ .env.local
31
+
32
+ # Selfdoc
33
+ .selfdoc/
34
+ selfdoc.json
@@ -0,0 +1,27 @@
1
+ name: Publish
2
+ on:
3
+ release:
4
+ types: [published]
5
+ workflow_dispatch:
6
+ permissions:
7
+ contents: read
8
+ id-token: write
9
+ jobs:
10
+ pypi:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+ - uses: astral-sh/setup-uv@v7
15
+ - run: uv build
16
+ - uses: pypa/gh-action-pypi-publish@release/v1
17
+ npm:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+ - uses: actions/setup-node@v6
22
+ with:
23
+ node-version: 24
24
+ registry-url: https://registry.npmjs.org
25
+ - run: npm publish --provenance --access public
26
+ env:
27
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,34 @@
1
+ # Development and CI
2
+ tests/
3
+ test/
4
+ __tests__/
5
+ .github/
6
+ scripts/
7
+ docs/
8
+ todo/
9
+ e2e/
10
+
11
+ # rlsbl metadata
12
+ .rlsbl/
13
+ .rlsbl-monorepo/
14
+
15
+ # Python artifacts (for multi-target projects)
16
+ __pycache__/
17
+ *.pyc
18
+ *.py
19
+ *.toml
20
+ .venv/
21
+
22
+ # Build/cache
23
+ coverage/
24
+ .nyc_output/
25
+
26
+ # Editor/OS
27
+ .DS_Store
28
+ *.log
29
+ .env
30
+ .env.local
31
+
32
+ # Selfdoc
33
+ .selfdoc/
34
+ selfdoc.json
@@ -0,0 +1 @@
1
+ a6dff298b7a2dad7fa928b088eeb4f3f9c0b2cf5
@@ -0,0 +1,5 @@
1
+ ## 0.13.0
2
+
3
+ ### Features
4
+
5
+ - **New feature.** Schema dump (--dump-schema) now includes a project_id field read from pyproject.toml, providing provenance for schema validation.
@@ -0,0 +1,3 @@
1
+ {"commits":["f2f2de83edc0b7fe24f46744201e8be1407f28a3"],"user_facing":false}
2
+ {"commits":["a2e1d64fb5f695169fafa63e5d1b69fe5c4e043d"],"user_facing":true,"description":"**New feature.** `checks_embed` parameter accepts raw TOML bytes, enabling inline checks configuration without a file on disk.","type":"feature"}
3
+ {"commits":["09799755dc2ef93a088f739a46001102bcb2aa5e"],"user_facing":false}
@@ -0,0 +1,5 @@
1
+ ## 0.14.0
2
+
3
+ ### Features
4
+
5
+ - **New feature.** `checks_embed` parameter accepts raw TOML bytes, enabling inline checks configuration without a file on disk.
@@ -0,0 +1,6 @@
1
+ {"commits":["057edc5200703fe2a2d37d28f488833b35254a06"],"user_facing":false}
2
+ {"commits":["e6af4e228227d55585c09ec9012cca846eff2651"],"user_facing":false}
3
+ {"commits":["e5dc999d0d920ba0737d2c565be818af8465de1c"],"user_facing":true,"description":"**Breaking.** `config set` now validates keys against registered flags and rejects unknown keys.","type":"breaking"}
4
+ {"commits":["e5dc999d0d920ba0737d2c565be818af8465de1c"],"user_facing":true,"description":"**New feature.** `config set` coerces values to the flag's declared type (int, bool, float) before writing to config.","type":"feature"}
5
+ {"commits":["4cf2df20f76491a8d9b504307de8d78ca514e7ab"],"user_facing":false}
6
+ {"commits":["050a53f9af245a308f6d5895307beb7f479c9c27"],"user_facing":false}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  ".github/workflows/ci.yml": "b4e075171b131d25f9cce55a3e3a30732e95a2a794af00f354ee3200626d6a04",
3
- ".github/workflows/publish.yml": "8befe0164321eb68e9465c634aa15d6ccb30d46fa669f7d30bf3ed064ad258ba",
3
+ ".github/workflows/publish.yml": "e77dcd0b5d1c1abae5424e5f3b73862f44f06b5d46209cbe71bc04d6baa6b5be",
4
4
  "CHANGELOG.md": "7a6ec46141007c18090a0ff325e2dd68cd51588c4e0b5af7b580365e5eaaca85",
5
5
  ".gitignore": "c89d76c6c43e7f0897b3472a15b7c0103e0be5b3a04c2b44e585fa48bdcda2bb",
6
6
  "LICENSE": "e958892abc1dd5d991fca6d528e695d335f25773d373d2e0be5f4a9507d8f8d5",
@@ -11,5 +11,6 @@
11
11
  ".rlsbl/hooks/pre-checks.sh": "60c57d4563263124530e6f33aca77550ea2161ad3b33b3ba6ad1c03fea9eb6e2",
12
12
  ".rlsbl/lint/python.toml": "9cfbcef2e010d5fad243437bf2fd8df54a1fae70ee0762f9d11da70fe207501c",
13
13
  ".rlsbl/lint/go.toml": "807d5cf760baad6d571a5bfd43cb163efe356f7462ba9f1918d72de45f62339f",
14
- ".rlsbl/lint/npm.toml": "baa36014e65923d8cc5c8b83c77fa651da79351627570476402da66840027645"
14
+ ".rlsbl/lint/npm.toml": "baa36014e65923d8cc5c8b83c77fa651da79351627570476402da66840027645",
15
+ ".npmignore": "7c0eea9785c0318aeb39bf8b670ce8384f4a5b812fde69ce23d7d018057325a3"
15
16
  }
@@ -0,0 +1,8 @@
1
+ # Version bump type: patch, minor, or major
2
+ bump = "minor"
3
+ # Short description of this release (required)
4
+ description = "Config set type coercion and key validation"
5
+ # Optional context explaining why these changes were made
6
+ context = "config set now validates keys against registered flags and coerces string values to the flag's declared type before writing. This is a breaking change: unknown keys are rejected."
7
+ include = ["pypi", "npm"]
8
+ exclude = []
@@ -0,0 +1 @@
1
+ 0.60.1
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## 0.15.0
6
+
7
+ Config set type coercion and key validation
8
+
9
+ <details>
10
+ <summary>Context</summary>
11
+
12
+ config set now validates keys against registered flags and coerces string values to the flag's declared type before writing. This is a breaking change: unknown keys are rejected.
13
+
14
+ </details>
15
+
16
+ ### Breaking
17
+
18
+ - **Breaking.** `config set` now validates keys against registered flags and rejects unknown keys.
19
+
20
+ ### Features
21
+
22
+ - **New feature.** `config set` coerces values to the flag's declared type (int, bool, float) before writing to config.
23
+
24
+ ## 0.14.0
25
+
26
+ ### Features
27
+
28
+ - **New feature.** `checks_embed` parameter accepts raw TOML bytes, enabling inline checks configuration without a file on disk.
29
+
5
30
  ## 0.13.0
6
31
 
7
32
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.13.0
3
+ Version: 0.15.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
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "strictcli",
9
- "version": "0.13.0",
9
+ "version": "0.15.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.13.0",
3
+ "version": "0.15.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.13.0"
7
+ version = "0.15.0"
8
8
  description = "A strict, zero-dependency CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.13.0"
5
+ __version__ = "0.15.0"
6
6
 
7
7
  __all__ = [
8
8
  "App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
@@ -130,37 +130,32 @@ _CHECK_REQUIRED_FIELDS = {"tags", "severity", "fast", "pure", "needs_network", "
130
130
  _CHECK_VALID_SEVERITIES = {"error", "warn"}
131
131
 
132
132
 
133
- def _load_checks_toml(path: str | Path) -> tuple[str, dict[str, _CheckDef]]:
134
- """Parse and validate a checks.toml file, returning (app_name, check_defs).
133
+ def _parse_checks_toml(data: bytes) -> tuple[str, dict[str, _CheckDef]]:
134
+ """Parse and validate checks TOML data, returning (app_name, check_defs).
135
135
 
136
- Raises ValueError on any schema violation, file error, or invalid TOML.
136
+ Raises ValueError on any schema violation or invalid TOML.
137
137
  """
138
- path = Path(path)
139
138
  try:
140
- raw = path.read_bytes()
141
- except OSError as exc:
142
- raise ValueError(f"checks.toml: {exc}") from exc
143
- try:
144
- data = tomllib.loads(raw.decode())
139
+ parsed = tomllib.loads(data.decode())
145
140
  except (tomllib.TOMLDecodeError, UnicodeDecodeError) as exc:
146
141
  raise ValueError(f"checks.toml: {exc}") from exc
147
142
 
148
143
  # Only "app" and [checks] are allowed at the top level
149
- for key in data:
144
+ for key in parsed:
150
145
  if key not in ("app", "checks"):
151
146
  raise ValueError(f'checks.toml: unknown top-level key "{key}"')
152
147
 
153
148
  # Validate required "app" field
154
- if "app" not in data:
149
+ if "app" not in parsed:
155
150
  raise ValueError('checks.toml: missing required top-level key "app"')
156
- if not isinstance(data["app"], str) or not data["app"]:
151
+ if not isinstance(parsed["app"], str) or not parsed["app"]:
157
152
  raise ValueError('checks.toml: "app" must be a non-empty string')
158
- app_name = data["app"]
153
+ app_name = parsed["app"]
159
154
 
160
- if "checks" not in data:
155
+ if "checks" not in parsed:
161
156
  return (app_name, {})
162
157
 
163
- checks_section = data["checks"]
158
+ checks_section = parsed["checks"]
164
159
  if not isinstance(checks_section, dict):
165
160
  raise ValueError("checks.toml: [checks] must be a table")
166
161
 
@@ -253,6 +248,19 @@ def _load_checks_toml(path: str | Path) -> tuple[str, dict[str, _CheckDef]]:
253
248
  return (app_name, result)
254
249
 
255
250
 
251
+ def _load_checks_toml(path: str | Path) -> tuple[str, dict[str, _CheckDef]]:
252
+ """Read and parse a checks.toml file, returning (app_name, check_defs).
253
+
254
+ Raises ValueError on any file error, schema violation, or invalid TOML.
255
+ """
256
+ path = Path(path)
257
+ try:
258
+ raw = path.read_bytes()
259
+ except OSError as exc:
260
+ raise ValueError(f"checks.toml: {exc}") from exc
261
+ return _parse_checks_toml(raw)
262
+
263
+
256
264
  class _HelpRequested(Exception):
257
265
  """Raised when --help or -h is encountered."""
258
266
 
@@ -277,6 +285,21 @@ class _ParseError(Exception):
277
285
  self.command_prefix = command_prefix
278
286
 
279
287
 
288
+ def _strict_bool(s: str) -> bool:
289
+ """Parse a boolean string strictly.
290
+
291
+ Accepts: 1, true, yes (case-insensitive) -> True
292
+ Accepts: 0, false, no (case-insensitive) -> False
293
+ Everything else raises ValueError.
294
+ """
295
+ lower = s.lower()
296
+ if lower in ("1", "true", "yes"):
297
+ return True
298
+ if lower in ("0", "false", "no"):
299
+ return False
300
+ raise ValueError(f"expected boolean, got '{s}'")
301
+
302
+
280
303
  def _strict_int(s: str) -> int:
281
304
  """Parse an integer string strictly -- no leading/trailing whitespace allowed.
282
305
 
@@ -675,6 +698,7 @@ class App:
675
698
  config_path: str | None = None
676
699
  config_format: str = "json"
677
700
  checks_path: str | Path | None = None
701
+ checks_embed: bytes | None = None
678
702
  flags: list[Flag] = field(default_factory=list)
679
703
  _commands: dict[str, Command] = field(default_factory=dict)
680
704
  _groups: dict[str, Group] = field(default_factory=dict)
@@ -712,6 +736,8 @@ class App:
712
736
  self._register_config_group()
713
737
  # Discover checks TOML
714
738
  self._check_context_factory: Callable | None = None
739
+ if self.checks_path is not None and self.checks_embed is not None:
740
+ raise ValueError("cannot use both checks_path and checks_embed")
715
741
  if self.checks_path is not None:
716
742
  checks_toml_path = Path(self.checks_path).resolve()
717
743
  if not checks_toml_path.is_file():
@@ -723,6 +749,14 @@ class App:
723
749
  )
724
750
  self._checks_enabled = True
725
751
  self._register_check_command()
752
+ elif self.checks_embed is not None:
753
+ app_name, self._check_defs = _parse_checks_toml(self.checks_embed)
754
+ if app_name != self.name:
755
+ raise ValueError(
756
+ f'checks.toml: app "{app_name}" does not match app name "{self.name}"'
757
+ )
758
+ self._checks_enabled = True
759
+ self._register_check_command()
726
760
  else:
727
761
  self._check_defs: dict[str, _CheckDef] = {}
728
762
  self._checks_enabled = False
@@ -995,7 +1029,7 @@ class App:
995
1029
  )
996
1030
 
997
1031
  # config set
998
- def _config_set_handler(key, value, **_kw) -> None:
1032
+ def _config_set_handler(key, value, **_kw) -> int:
999
1033
  path = _config_path(
1000
1034
  app_ref.name,
1001
1035
  override=app_ref.config_path,
@@ -1004,26 +1038,53 @@ class App:
1004
1038
  dir_path = os.path.dirname(path)
1005
1039
  os.makedirs(dir_path, exist_ok=True)
1006
1040
  # Read existing config
1007
- existing: dict = {}
1008
- if os.path.isfile(path):
1009
- if app_ref.config_format == "toml":
1041
+ existing = _load_config(
1042
+ app_ref.name,
1043
+ config_path_override=app_ref.config_path,
1044
+ config_format=app_ref.config_format,
1045
+ )
1046
+
1047
+ # Look up the key against registered flags
1048
+ all_flags = app_ref._collect_all_flags()
1049
+ matched_flag = None
1050
+ for f in all_flags:
1051
+ if _flag_param_name(f.name) == key:
1052
+ matched_flag = f
1053
+ break
1054
+ if matched_flag is None:
1055
+ print(f"config set: unknown key '{key}'", file=sys.stderr)
1056
+ return 1
1057
+
1058
+ # Coerce the string value to the flag's type
1059
+ try:
1060
+ if matched_flag.type == bool:
1061
+ typed_value = _strict_bool(value)
1062
+ elif matched_flag.type == int:
1010
1063
  try:
1011
- with open(path, "rb") as fh:
1012
- existing = tomllib.load(fh)
1013
- except (tomllib.TOMLDecodeError, UnicodeDecodeError):
1014
- existing = {}
1015
- else:
1064
+ typed_value = _strict_int(value)
1065
+ except ValueError:
1066
+ raise ValueError(f"expected integer, got '{value}'")
1067
+ elif matched_flag.type == float:
1016
1068
  try:
1017
- with open(path) as fh:
1018
- existing = json.loads(fh.read())
1019
- except (json.JSONDecodeError, ValueError):
1020
- existing = {}
1021
- existing[key] = value
1069
+ typed_value = _strict_float(value)
1070
+ except ValueError as fe:
1071
+ msg = str(fe)
1072
+ if msg in ("NaN is not allowed", "Inf is not allowed"):
1073
+ raise
1074
+ raise ValueError(f"expected float, got '{value}'") from fe
1075
+ else: # str
1076
+ typed_value = value
1077
+ except ValueError as e:
1078
+ print(f"config set: key '{key}': {e}", file=sys.stderr)
1079
+ return 1
1080
+
1081
+ existing[key] = typed_value
1022
1082
  if app_ref.config_format == "toml":
1023
1083
  _write_toml_flat(existing, path)
1024
1084
  else:
1025
1085
  with open(path, "w") as fh:
1026
1086
  fh.write(json.dumps(existing, indent=2) + "\n")
1087
+ return 0
1027
1088
 
1028
1089
  config_grp.commands["set"] = Command(
1029
1090
  name="set",
@@ -1343,12 +1404,9 @@ class App:
1343
1404
  env_val = os.environ.get(f.env)
1344
1405
  if env_val is not None:
1345
1406
  if f.type is bool:
1346
- lower = env_val.lower()
1347
- if lower in ("1", "true", "yes"):
1348
- cli_set[f.name] = True
1349
- elif lower in ("0", "false", "no"):
1350
- cli_set[f.name] = False
1351
- else:
1407
+ try:
1408
+ cli_set[f.name] = _strict_bool(env_val)
1409
+ except ValueError:
1352
1410
  raise _ParseError(
1353
1411
  f"invalid boolean value {env_val!r} for env var "
1354
1412
  f"'{f.env}' (flag '--{f.name}')"
@@ -1741,12 +1799,9 @@ def _parse_command(
1741
1799
  env_val = os.environ.get(f.env)
1742
1800
  if env_val is not None:
1743
1801
  if f.type is bool:
1744
- lower = env_val.lower()
1745
- if lower in ("1", "true", "yes"):
1746
- cli_set[f.name] = True
1747
- elif lower in ("0", "false", "no"):
1748
- cli_set[f.name] = False
1749
- else:
1802
+ try:
1803
+ cli_set[f.name] = _strict_bool(env_val)
1804
+ except ValueError:
1750
1805
  raise _ParseError(
1751
1806
  f"invalid boolean value {env_val!r} for env var "
1752
1807
  f"'{f.env}' (flag '--{f.name}')"
@@ -293,3 +293,54 @@ depends_on = []
293
293
  name="testapp", version="1.0.0", help="test app",
294
294
  checks_path=str(toml_file),
295
295
  )
296
+
297
+
298
+ class TestChecksEmbed:
299
+ def test_checks_embed_enables_checks(self, tmp_path, monkeypatch):
300
+ """checks_embed with valid TOML bytes enables checks."""
301
+ monkeypatch.chdir(tmp_path)
302
+
303
+ app = strictcli.App(
304
+ name="testapp", version="1.0.0", help="test app",
305
+ checks_embed=VALID_TOML.encode(),
306
+ )
307
+ assert app._checks_enabled is True
308
+ assert "lint-code" in app._check_defs
309
+ assert "check-deps" in app._check_defs
310
+
311
+ def test_checks_embed_and_checks_path_raises(self, tmp_path, monkeypatch):
312
+ """Using both checks_path and checks_embed raises ValueError."""
313
+ monkeypatch.chdir(tmp_path)
314
+ toml_file = tmp_path / "checks.toml"
315
+ toml_file.write_text(VALID_TOML)
316
+
317
+ with pytest.raises(ValueError, match="cannot use both checks_path and checks_embed"):
318
+ strictcli.App(
319
+ name="testapp", version="1.0.0", help="test app",
320
+ checks_path=str(toml_file),
321
+ checks_embed=VALID_TOML.encode(),
322
+ )
323
+
324
+ def test_checks_embed_invalid_toml(self, tmp_path, monkeypatch):
325
+ """checks_embed with garbage bytes raises ValueError."""
326
+ monkeypatch.chdir(tmp_path)
327
+
328
+ with pytest.raises(ValueError, match="checks.toml:"):
329
+ strictcli.App(
330
+ name="testapp", version="1.0.0", help="test app",
331
+ checks_embed=b"\x80\x81 not valid toml {{{{",
332
+ )
333
+
334
+ def test_checks_embed_wrong_app_name(self, tmp_path, monkeypatch):
335
+ """checks_embed with mismatched app name raises ValueError."""
336
+ monkeypatch.chdir(tmp_path)
337
+ wrong_toml = 'app = "wrong"\n'
338
+
339
+ with pytest.raises(
340
+ ValueError,
341
+ match=r'checks.toml: app "wrong" does not match app name "testapp"',
342
+ ):
343
+ strictcli.App(
344
+ name="testapp", version="1.0.0", help="test app",
345
+ checks_embed=wrong_toml.encode(),
346
+ )