strictcli 0.9.1__tar.gz → 0.11.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.
- {strictcli-0.9.1/.rlsbl/bases → strictcli-0.11.0}/.github/workflows/ci.yml +1 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.github/workflows/publish.yml +2 -0
- {strictcli-0.9.1 → strictcli-0.11.0/.rlsbl/bases}/.github/workflows/ci.yml +1 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/.github/workflows/publish.yml +3 -1
- strictcli-0.11.0/.rlsbl/changes/.validated +1 -0
- strictcli-0.11.0/.rlsbl/changes/0.10.0.jsonl +6 -0
- strictcli-0.11.0/.rlsbl/changes/0.10.0.md +6 -0
- strictcli-0.11.0/.rlsbl/changes/0.11.0.jsonl +7 -0
- strictcli-0.11.0/.rlsbl/changes/0.11.0.md +5 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/config.json +2 -1
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/hashes.json +2 -2
- strictcli-0.11.0/.rlsbl/hooks/pre-checks.sh +10 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/hooks/pre-release.sh +5 -0
- strictcli-0.11.0/.rlsbl/releases/v0.11.0.toml +3 -0
- strictcli-0.11.0/.rlsbl/releases/v0.9.0.toml +3 -0
- strictcli-0.11.0/.rlsbl/version +1 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/CHANGELOG.md +13 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/PKG-INFO +1 -1
- {strictcli-0.9.1 → strictcli-0.11.0}/package-lock.json +2 -2
- {strictcli-0.9.1 → strictcli-0.11.0}/package.json +1 -1
- {strictcli-0.9.1 → strictcli-0.11.0}/pyproject.toml +1 -1
- {strictcli-0.9.1 → strictcli-0.11.0}/strictcli/__init__.py +218 -41
- strictcli-0.11.0/tests/test_at_prefix.py +342 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_check_discovery.py +67 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_dump_schema.py +232 -13
- {strictcli-0.9.1 → strictcli-0.11.0}/uv.lock +1 -1
- strictcli-0.9.1/.rlsbl/changes/.validated +0 -1
- strictcli-0.9.1/.rlsbl/hooks/pre-checks.sh +0 -5
- strictcli-0.9.1/.rlsbl/version +0 -1
- {strictcli-0.9.1 → strictcli-0.11.0}/.claude/settings.json +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.gitignore +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/.gitignore +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/bases/LICENSE +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.4.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.4.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.5.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.6.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.6.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.7.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.7.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.2.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.3.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.4.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.5.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.6.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.8.7.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.9.0.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/0.9.1.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/hooks/post-release.sh +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/lint/go.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/lint/npm.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/releases/unreleased.toml +0 -0
- /strictcli-0.9.1/.rlsbl/releases/v0.9.0.toml → /strictcli-0.11.0/.rlsbl/releases/v0.10.0.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/releases/v0.8.5.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/releases/v0.8.6.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/releases/v0.8.7.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.rlsbl/releases/v0.9.1.toml +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/.strictcli/schema.json +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/CLAUDE.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/LICENSE +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/README.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/index.js +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/postinstall.js +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_check_command.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_check_types.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_choices.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_config.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_e2e.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_env.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_float_type.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_help.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_int_type.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_mutex.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_nesting.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_parser.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_registration.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_tags.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_validate.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/tests/test_variadic.py +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.9.1 → strictcli-0.11.0}/todo/.done/original-idea.md +0 -0
|
@@ -3,6 +3,8 @@ name: Publish
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
permissions:
|
|
8
10
|
contents: read
|
|
@@ -24,7 +26,7 @@ jobs:
|
|
|
24
26
|
- uses: actions/setup-node@v6
|
|
25
27
|
with:
|
|
26
28
|
node-version: 24
|
|
27
|
-
registry-url:
|
|
29
|
+
registry-url: https://registry.npmjs.org
|
|
28
30
|
- run: npm publish --provenance --access public
|
|
29
31
|
env:
|
|
30
32
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
228f7f6e4e51965e7f3cc19ea1d112e915b585c8
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"commits":["faa8f348f03793084361365f15bd9f8be06046ff"],"user_facing":true,"description":"**New feature.** `--dump-schema` omits fields matching defaults and includes a top-level `defaults` object documenting what missing fields mean.","type":"feature"}
|
|
2
|
+
{"commits":["ef852d540829f5fb248c380b649e8c15e56b849b"],"user_facing":true,"description":"**New feature.** `@`-prefix for string flag values: `@path` reads from file, `@-` reads from stdin, `@@` escapes. 1 MB size limit, trailing whitespace stripped.","type":"feature"}
|
|
3
|
+
{"commits":["4294effc300d83eeba90c3cb90c6429fc443c929","e7e5bc79004fe8c0883fdc5c34a1f36eb8d97e75","6bf56e841883195ea6045386b21f93a4d1c8ee2d"],"user_facing":false}
|
|
4
|
+
{"commits":["05d896e80e8df10ee737f50ae735e99695bbc213","1e354f1681539c39d71e4df7d57090d6b32d3f59","5f8b3d49ab264aa26dbe8c6c1d1f1bebb955d73b"],"user_facing":false}
|
|
5
|
+
{"commits":["33dfbe42738b308dc01df908c8286b48eb97e12f"],"user_facing":false}
|
|
6
|
+
{"commits":["f2b61b9ca7eb86207a0631d01cb672c39dcb89a0"],"user_facing":false}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
## 0.10.0
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **New feature.** `--dump-schema` omits fields matching defaults and includes a top-level `defaults` object documenting what missing fields mean.
|
|
6
|
+
- **New feature.** `@`-prefix for string flag values: `@path` reads from file, `@-` reads from stdin, `@@` escapes. 1 MB size limit, trailing whitespace stripped.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{"commits":["feae73fae6f1e08f1355bc7c1b9beae842397517"],"user_facing":false}
|
|
2
|
+
{"commits":["087bd6cb358bf6ebf99e2635e93a2fdc59ac5517"],"user_facing":false}
|
|
3
|
+
{"commits":["f7e07e1d5ce69895df1a95da9c51ab686300e0b3"],"user_facing":false}
|
|
4
|
+
{"commits":["f96d157204a50c577f78b99667be81472e23bfe7"],"user_facing":false}
|
|
5
|
+
{"commits":["c7ee8c1401de38d9e086c38284388497c54cca0a"],"user_facing":true,"description":"**New feature.** `checks_path` parameter on `App` for explicit checks.toml location, replacing CWD-based discovery.","type":"feature"}
|
|
6
|
+
{"commits":["dd7a103769db5bccfaabb2636fce0ef5082e2159","df35fbde862721ef8b01a483abf6b33e5387afa8"],"user_facing":false}
|
|
7
|
+
{"commits":["f94f50bad6401756ca75ee82b31fbfc7e85edc83"],"user_facing":false}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
".github/workflows/ci.yml": "
|
|
3
|
-
".github/workflows/publish.yml": "
|
|
2
|
+
".github/workflows/ci.yml": "b4e075171b131d25f9cce55a3e3a30732e95a2a794af00f354ee3200626d6a04",
|
|
3
|
+
".github/workflows/publish.yml": "8befe0164321eb68e9465c634aa15d6ccb30d46fa669f7d30bf3ed064ad258ba",
|
|
4
4
|
"CHANGELOG.md": "7a6ec46141007c18090a0ff325e2dd68cd51588c4e0b5af7b580365e5eaaca85",
|
|
5
5
|
".gitignore": "c89d76c6c43e7f0897b3472a15b7c0103e0be5b3a04c2b44e585fa48bdcda2bb",
|
|
6
6
|
"LICENSE": "e958892abc1dd5d991fca6d528e695d335f25773d373d2e0be5f4a9507d8f8d5",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# This hook runs BEFORE built-in pre-release checks (tests, lint).
|
|
4
|
+
# Use it for setup tasks: starting services, setting env vars, etc.
|
|
5
|
+
# Built-in checks run after this hook. Custom validation goes in pre-release.sh.
|
|
6
|
+
|
|
7
|
+
# Ensure strictcli and conformance tool are installed (editable)
|
|
8
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
9
|
+
pip install -e "$REPO_ROOT/python" --break-system-packages --quiet 2>/dev/null || true
|
|
10
|
+
pip install -e "$REPO_ROOT/conformance" --break-system-packages --quiet 2>/dev/null || true
|
|
@@ -6,3 +6,8 @@ set -euo pipefail
|
|
|
6
6
|
# - Check for uncommitted documentation
|
|
7
7
|
# - Verify external service connectivity
|
|
8
8
|
# - Run integration tests not covered by the test suite
|
|
9
|
+
|
|
10
|
+
# Run conformance checks before release
|
|
11
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
12
|
+
cd "$REPO_ROOT/conformance"
|
|
13
|
+
conformance check --tag pre-release
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.45.0
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## 0.11.0
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **New feature.** `checks_path` parameter on `App` for explicit checks.toml location, replacing CWD-based discovery.
|
|
10
|
+
|
|
11
|
+
## 0.10.0
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- **New feature.** `--dump-schema` omits fields matching defaults and includes a top-level `defaults` object documenting what missing fields mean.
|
|
16
|
+
- **New feature.** `@`-prefix for string flag values: `@path` reads from file, `@-` reads from stdin, `@@` escapes. 1 MB size limit, trailing whitespace stripped.
|
|
17
|
+
|
|
5
18
|
## 0.9.1
|
|
6
19
|
|
|
7
20
|
### Features
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strictcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "strictcli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.11.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT"
|
|
12
12
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.11.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -316,6 +316,50 @@ def _float_parse_error(
|
|
|
316
316
|
return _ParseError(f"--{flag_name}: expected float, got {raw!r}{suffix}")
|
|
317
317
|
|
|
318
318
|
|
|
319
|
+
_AT_PREFIX_MAX_SIZE = 1024 * 1024 # 1 MB
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _resolve_at_prefix(
|
|
323
|
+
flag_name: str, raw: str, stdin_consumed_by: str | None,
|
|
324
|
+
) -> tuple[str, str | None]:
|
|
325
|
+
"""Resolve @-prefix for string flag values.
|
|
326
|
+
|
|
327
|
+
Returns (resolved_value, updated_stdin_consumed_by).
|
|
328
|
+
"""
|
|
329
|
+
if not raw.startswith("@"):
|
|
330
|
+
return raw, stdin_consumed_by
|
|
331
|
+
if raw.startswith("@@"):
|
|
332
|
+
return raw[1:], stdin_consumed_by
|
|
333
|
+
if raw == "@-":
|
|
334
|
+
if stdin_consumed_by is not None:
|
|
335
|
+
raise _ParseError(
|
|
336
|
+
f"--{flag_name}: stdin (@-) can only be used once per invocation"
|
|
337
|
+
)
|
|
338
|
+
try:
|
|
339
|
+
data = sys.stdin.read(_AT_PREFIX_MAX_SIZE + 1)
|
|
340
|
+
if len(data) > _AT_PREFIX_MAX_SIZE:
|
|
341
|
+
raise _ParseError(f"--{flag_name}: file exceeds 1 MB limit")
|
|
342
|
+
return data.rstrip(), flag_name
|
|
343
|
+
except _ParseError:
|
|
344
|
+
raise
|
|
345
|
+
except Exception:
|
|
346
|
+
raise _ParseError(f"--{flag_name}: cannot read stdin")
|
|
347
|
+
# @path -- read file
|
|
348
|
+
path = raw[1:]
|
|
349
|
+
if not os.path.exists(path):
|
|
350
|
+
raise _ParseError(f"--{flag_name}: file not found: {path}")
|
|
351
|
+
try:
|
|
352
|
+
with open(path, "r") as f:
|
|
353
|
+
data = f.read(_AT_PREFIX_MAX_SIZE + 1)
|
|
354
|
+
if len(data) > _AT_PREFIX_MAX_SIZE:
|
|
355
|
+
raise _ParseError(f"--{flag_name}: file exceeds 1 MB limit")
|
|
356
|
+
return data.rstrip(), stdin_consumed_by
|
|
357
|
+
except _ParseError:
|
|
358
|
+
raise
|
|
359
|
+
except Exception:
|
|
360
|
+
raise _ParseError(f"--{flag_name}: cannot read file: {path}")
|
|
361
|
+
|
|
362
|
+
|
|
319
363
|
def _require_non_empty_str(value: str, field_name: str, class_name: str) -> None:
|
|
320
364
|
if not isinstance(value, str) or not value.strip():
|
|
321
365
|
raise ValueError(f"{class_name}.{field_name} must be a non-empty string")
|
|
@@ -623,6 +667,7 @@ class App:
|
|
|
623
667
|
config: bool = False
|
|
624
668
|
config_path: str | None = None
|
|
625
669
|
config_format: str = "json"
|
|
670
|
+
checks_path: str | Path | None = None
|
|
626
671
|
flags: list[Flag] = field(default_factory=list)
|
|
627
672
|
_commands: dict[str, Command] = field(default_factory=dict)
|
|
628
673
|
_groups: dict[str, Group] = field(default_factory=dict)
|
|
@@ -660,7 +705,12 @@ class App:
|
|
|
660
705
|
self._register_config_group()
|
|
661
706
|
# Discover checks TOML
|
|
662
707
|
self._check_context_factory: Callable | None = None
|
|
663
|
-
|
|
708
|
+
if self.checks_path is not None:
|
|
709
|
+
checks_toml_path = Path(self.checks_path).resolve()
|
|
710
|
+
if not checks_toml_path.is_file():
|
|
711
|
+
raise ValueError(f"checks_path does not exist: {self.checks_path}")
|
|
712
|
+
else:
|
|
713
|
+
checks_toml_path = Path.cwd() / ".strictcli" / "checks.toml"
|
|
664
714
|
if checks_toml_path.is_file():
|
|
665
715
|
self._check_defs: dict[str, _CheckDef] = _load_checks_toml(checks_toml_path)
|
|
666
716
|
self._checks_enabled = True
|
|
@@ -1024,6 +1074,7 @@ class App:
|
|
|
1024
1074
|
raise _DumpSchemaRequested()
|
|
1025
1075
|
|
|
1026
1076
|
# Step 1.5: parse global flags before command routing
|
|
1077
|
+
self._stdin_consumed_by: str | None = None
|
|
1027
1078
|
global_values, remaining = self._parse_global_flags(argv)
|
|
1028
1079
|
self._last_global_values = global_values
|
|
1029
1080
|
|
|
@@ -1092,9 +1143,13 @@ class App:
|
|
|
1092
1143
|
return cmd, rest
|
|
1093
1144
|
|
|
1094
1145
|
# Step 3: parse remaining tokens for the resolved command
|
|
1146
|
+
# Pass stdin_consumed_by as a mutable single-element list so
|
|
1147
|
+
# _parse_command can update the shared state.
|
|
1148
|
+
stdin_state: list[str | None] = [self._stdin_consumed_by]
|
|
1095
1149
|
try:
|
|
1096
1150
|
cmd, kwargs, post_global = _parse_command(
|
|
1097
1151
|
cmd, rest, self._global_flags, config_data=self._config_data,
|
|
1152
|
+
stdin_consumed_by=stdin_state,
|
|
1098
1153
|
)
|
|
1099
1154
|
except _ParseError as e:
|
|
1100
1155
|
prefix_parts = [self.name] + path + [cmd.name]
|
|
@@ -1181,7 +1236,10 @@ class App:
|
|
|
1181
1236
|
except ValueError as e:
|
|
1182
1237
|
raise _float_parse_error(f.name, value_part, e)
|
|
1183
1238
|
else:
|
|
1184
|
-
|
|
1239
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1240
|
+
f.name, value_part, self._stdin_consumed_by,
|
|
1241
|
+
)
|
|
1242
|
+
_store_value(f, resolved)
|
|
1185
1243
|
i += 1
|
|
1186
1244
|
continue
|
|
1187
1245
|
elif flag_part in negation_lookup:
|
|
@@ -1222,7 +1280,10 @@ class App:
|
|
|
1222
1280
|
except ValueError as e:
|
|
1223
1281
|
raise _float_parse_error(f.name, raw, e)
|
|
1224
1282
|
else:
|
|
1225
|
-
|
|
1283
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1284
|
+
f.name, raw, self._stdin_consumed_by,
|
|
1285
|
+
)
|
|
1286
|
+
_store_value(f, resolved)
|
|
1226
1287
|
i += 2
|
|
1227
1288
|
else:
|
|
1228
1289
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1250,7 +1311,10 @@ class App:
|
|
|
1250
1311
|
except ValueError as e:
|
|
1251
1312
|
raise _float_parse_error(f.name, raw, e)
|
|
1252
1313
|
else:
|
|
1253
|
-
|
|
1314
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1315
|
+
f.name, raw, self._stdin_consumed_by,
|
|
1316
|
+
)
|
|
1317
|
+
_store_value(f, resolved)
|
|
1254
1318
|
i += 2
|
|
1255
1319
|
else:
|
|
1256
1320
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1297,7 +1361,10 @@ class App:
|
|
|
1297
1361
|
raise _float_parse_error(f.name, env_val, e, env=f.env)
|
|
1298
1362
|
cli_set[f.name] = [coerced] if f.repeatable else coerced
|
|
1299
1363
|
else:
|
|
1300
|
-
|
|
1364
|
+
resolved, self._stdin_consumed_by = _resolve_at_prefix(
|
|
1365
|
+
f.name, env_val, self._stdin_consumed_by,
|
|
1366
|
+
)
|
|
1367
|
+
cli_set[f.name] = [resolved] if f.repeatable else resolved
|
|
1301
1368
|
|
|
1302
1369
|
# Resolve config values for global flags not set by CLI or env
|
|
1303
1370
|
if self._config_data:
|
|
@@ -1471,12 +1538,18 @@ def _parse_command(
|
|
|
1471
1538
|
tokens: list[str],
|
|
1472
1539
|
global_flags: list[Flag] | None = None,
|
|
1473
1540
|
config_data: dict | None = None,
|
|
1541
|
+
stdin_consumed_by: list[str | None] | None = None,
|
|
1474
1542
|
) -> tuple[Command, dict[str, object], dict[str, object]]:
|
|
1475
1543
|
"""Parse tokens against a resolved command's flags and args.
|
|
1476
1544
|
|
|
1477
1545
|
Returns (cmd, kwargs, global_cli_set) where global_cli_set contains
|
|
1478
1546
|
any global flag values parsed from tokens appearing after the command name.
|
|
1547
|
+
|
|
1548
|
+
stdin_consumed_by is a mutable single-element list tracking which flag
|
|
1549
|
+
has already consumed stdin via @-. Updated in-place.
|
|
1479
1550
|
"""
|
|
1551
|
+
if stdin_consumed_by is None:
|
|
1552
|
+
stdin_consumed_by = [None]
|
|
1480
1553
|
|
|
1481
1554
|
# Build flag lookup dicts
|
|
1482
1555
|
long_lookup: dict[str, Flag] = {} # --flag-name -> Flag
|
|
@@ -1554,7 +1627,10 @@ def _parse_command(
|
|
|
1554
1627
|
except ValueError as e:
|
|
1555
1628
|
raise _float_parse_error(f.name, value_part, e)
|
|
1556
1629
|
else:
|
|
1557
|
-
|
|
1630
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1631
|
+
f.name, value_part, stdin_consumed_by[0],
|
|
1632
|
+
)
|
|
1633
|
+
_store_value(f, resolved)
|
|
1558
1634
|
elif flag_part in negation_lookup:
|
|
1559
1635
|
raise _ParseError(
|
|
1560
1636
|
f"flag '{flag_part}' is a boolean negation and does not take a value"
|
|
@@ -1593,7 +1669,10 @@ def _parse_command(
|
|
|
1593
1669
|
except ValueError as e:
|
|
1594
1670
|
raise _float_parse_error(f.name, raw, e)
|
|
1595
1671
|
else:
|
|
1596
|
-
|
|
1672
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1673
|
+
f.name, raw, stdin_consumed_by[0],
|
|
1674
|
+
)
|
|
1675
|
+
_store_value(f, resolved)
|
|
1597
1676
|
i += 2
|
|
1598
1677
|
else:
|
|
1599
1678
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1623,7 +1702,10 @@ def _parse_command(
|
|
|
1623
1702
|
except ValueError as e:
|
|
1624
1703
|
raise _float_parse_error(f.name, raw, e)
|
|
1625
1704
|
else:
|
|
1626
|
-
|
|
1705
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1706
|
+
f.name, raw, stdin_consumed_by[0],
|
|
1707
|
+
)
|
|
1708
|
+
_store_value(f, resolved)
|
|
1627
1709
|
i += 2
|
|
1628
1710
|
else:
|
|
1629
1711
|
raise _ParseError(f"flag '{tok}' requires a value")
|
|
@@ -1668,7 +1750,10 @@ def _parse_command(
|
|
|
1668
1750
|
raise _float_parse_error(f.name, env_val, e, env=f.env)
|
|
1669
1751
|
cli_set[f.name] = [coerced] if f.repeatable else coerced
|
|
1670
1752
|
else:
|
|
1671
|
-
|
|
1753
|
+
resolved, stdin_consumed_by[0] = _resolve_at_prefix(
|
|
1754
|
+
f.name, env_val, stdin_consumed_by[0],
|
|
1755
|
+
)
|
|
1756
|
+
cli_set[f.name] = [resolved] if f.repeatable else resolved
|
|
1672
1757
|
|
|
1673
1758
|
# Step 4.2: resolve config values for flags not set by CLI or env
|
|
1674
1759
|
if config_data:
|
|
@@ -2870,66 +2955,158 @@ _TYPE_NAMES = {str: "str", bool: "bool", int: "int", float: "float"}
|
|
|
2870
2955
|
|
|
2871
2956
|
|
|
2872
2957
|
def _serialize_flag(f: Flag) -> dict:
|
|
2873
|
-
"""Serialize a Flag to a JSON-serializable dict.
|
|
2874
|
-
|
|
2958
|
+
"""Serialize a Flag to a JSON-serializable dict.
|
|
2959
|
+
|
|
2960
|
+
Identity fields (name, type, help) are always included.
|
|
2961
|
+
Other fields are omitted when they match the schema defaults.
|
|
2962
|
+
"""
|
|
2963
|
+
d: dict = {
|
|
2875
2964
|
"name": f.name,
|
|
2876
2965
|
"type": _TYPE_NAMES[f.type],
|
|
2877
2966
|
"help": f.help,
|
|
2878
|
-
"short": f.short,
|
|
2879
|
-
"default": f.default,
|
|
2880
|
-
"env": f.env,
|
|
2881
|
-
"choices": f.choices,
|
|
2882
|
-
"repeatable": f.repeatable,
|
|
2883
|
-
"negatable": f.negatable if f.type is bool else None,
|
|
2884
|
-
"hidden": False,
|
|
2885
2967
|
}
|
|
2968
|
+
if f.short is not None:
|
|
2969
|
+
d["short"] = f.short
|
|
2970
|
+
if f.default is not None:
|
|
2971
|
+
d["default"] = f.default
|
|
2972
|
+
if f.env is not None:
|
|
2973
|
+
d["env"] = f.env
|
|
2974
|
+
if f.choices is not None:
|
|
2975
|
+
d["choices"] = f.choices
|
|
2976
|
+
if f.repeatable:
|
|
2977
|
+
d["repeatable"] = f.repeatable
|
|
2978
|
+
negatable = f.negatable if f.type is bool else None
|
|
2979
|
+
if negatable is not None:
|
|
2980
|
+
d["negatable"] = negatable
|
|
2981
|
+
# hidden is currently always False, so always omitted
|
|
2982
|
+
return d
|
|
2886
2983
|
|
|
2887
2984
|
|
|
2888
2985
|
def _serialize_arg(a: Arg) -> dict:
|
|
2889
|
-
"""Serialize an Arg to a JSON-serializable dict.
|
|
2890
|
-
|
|
2986
|
+
"""Serialize an Arg to a JSON-serializable dict.
|
|
2987
|
+
|
|
2988
|
+
Identity fields (name, help) are always included.
|
|
2989
|
+
Other fields are omitted when they match the schema defaults.
|
|
2990
|
+
"""
|
|
2991
|
+
d: dict = {
|
|
2891
2992
|
"name": a.name,
|
|
2892
2993
|
"help": a.help,
|
|
2893
|
-
"required": a.required,
|
|
2894
|
-
"variadic": a.variadic,
|
|
2895
2994
|
}
|
|
2995
|
+
if not a.required:
|
|
2996
|
+
d["required"] = a.required
|
|
2997
|
+
if a.variadic:
|
|
2998
|
+
d["variadic"] = a.variadic
|
|
2999
|
+
return d
|
|
2896
3000
|
|
|
2897
3001
|
|
|
2898
3002
|
def _serialize_command(cmd: Command) -> dict:
|
|
2899
|
-
"""Serialize a Command to a JSON-serializable dict.
|
|
2900
|
-
|
|
3003
|
+
"""Serialize a Command to a JSON-serializable dict.
|
|
3004
|
+
|
|
3005
|
+
Identity fields (name, help) are always included.
|
|
3006
|
+
Other fields are omitted when they match the schema defaults.
|
|
3007
|
+
"""
|
|
3008
|
+
d: dict = {
|
|
2901
3009
|
"name": cmd.name,
|
|
2902
3010
|
"help": cmd.help,
|
|
2903
|
-
"flags": [_serialize_flag(f) for f in cmd.flags],
|
|
2904
|
-
"args": [_serialize_arg(a) for a in cmd.args],
|
|
2905
|
-
"passthrough": cmd.passthrough is not None,
|
|
2906
3011
|
}
|
|
3012
|
+
if cmd.passthrough is not None:
|
|
3013
|
+
d["passthrough"] = True
|
|
3014
|
+
flags = [_serialize_flag(f) for f in cmd.flags]
|
|
3015
|
+
if flags:
|
|
3016
|
+
d["flags"] = flags
|
|
3017
|
+
args = [_serialize_arg(a) for a in cmd.args]
|
|
3018
|
+
if args:
|
|
3019
|
+
d["args"] = args
|
|
3020
|
+
return d
|
|
2907
3021
|
|
|
2908
3022
|
|
|
2909
3023
|
def _serialize_group(group: Group) -> dict:
|
|
2910
|
-
"""Serialize a Group to a JSON-serializable dict (recursive).
|
|
2911
|
-
|
|
3024
|
+
"""Serialize a Group to a JSON-serializable dict (recursive).
|
|
3025
|
+
|
|
3026
|
+
Identity fields (name, help) are always included.
|
|
3027
|
+
Other fields are omitted when they match the schema defaults.
|
|
3028
|
+
"""
|
|
3029
|
+
d: dict = {
|
|
2912
3030
|
"name": group.name,
|
|
2913
3031
|
"help": group.help,
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
3032
|
+
}
|
|
3033
|
+
commands = {name: _serialize_command(cmd) for name, cmd in group.commands.items()}
|
|
3034
|
+
if commands:
|
|
3035
|
+
d["commands"] = commands
|
|
3036
|
+
groups = {name: _serialize_group(g) for name, g in group._groups.items()}
|
|
3037
|
+
if groups:
|
|
3038
|
+
d["groups"] = groups
|
|
3039
|
+
deprecated = {name: dep.message for name, dep in group.deprecated.items()}
|
|
3040
|
+
if deprecated:
|
|
3041
|
+
d["deprecated"] = deprecated
|
|
3042
|
+
return d
|
|
3043
|
+
|
|
3044
|
+
|
|
3045
|
+
def _build_schema_defaults() -> dict:
|
|
3046
|
+
"""Return the defaults object documenting what 'missing' means in the schema."""
|
|
3047
|
+
return {
|
|
3048
|
+
"app": {
|
|
3049
|
+
"env_prefix": None,
|
|
3050
|
+
"config": False,
|
|
3051
|
+
"global_flags": [],
|
|
3052
|
+
"commands": {},
|
|
3053
|
+
"groups": {},
|
|
3054
|
+
"deprecated": {},
|
|
3055
|
+
},
|
|
3056
|
+
"flag": {
|
|
3057
|
+
"short": None,
|
|
3058
|
+
"default": None,
|
|
3059
|
+
"env": None,
|
|
3060
|
+
"choices": None,
|
|
3061
|
+
"repeatable": False,
|
|
3062
|
+
"negatable": None,
|
|
3063
|
+
"hidden": False,
|
|
3064
|
+
},
|
|
3065
|
+
"arg": {
|
|
3066
|
+
"required": True,
|
|
3067
|
+
"variadic": False,
|
|
3068
|
+
},
|
|
3069
|
+
"command": {
|
|
3070
|
+
"passthrough": False,
|
|
3071
|
+
"flags": [],
|
|
3072
|
+
"args": [],
|
|
3073
|
+
},
|
|
3074
|
+
"group": {
|
|
3075
|
+
"commands": {},
|
|
3076
|
+
"groups": {},
|
|
3077
|
+
"deprecated": {},
|
|
3078
|
+
},
|
|
2917
3079
|
}
|
|
2918
3080
|
|
|
2919
3081
|
|
|
2920
3082
|
def _dump_schema(app: App) -> dict:
|
|
2921
|
-
"""Produce a JSON-serializable dict representing the app's command tree.
|
|
2922
|
-
|
|
3083
|
+
"""Produce a JSON-serializable dict representing the app's command tree.
|
|
3084
|
+
|
|
3085
|
+
Fields whose values match the schema defaults are omitted.
|
|
3086
|
+
The top-level ``defaults`` key documents what each missing field means.
|
|
3087
|
+
"""
|
|
3088
|
+
schema: dict = {
|
|
3089
|
+
"defaults": _build_schema_defaults(),
|
|
2923
3090
|
"name": app.name,
|
|
2924
3091
|
"version": app.version,
|
|
2925
3092
|
"help": app.help,
|
|
2926
|
-
"env_prefix": app.env_prefix,
|
|
2927
|
-
"config": app.config,
|
|
2928
|
-
"global_flags": [_serialize_flag(f) for f in app._global_flags],
|
|
2929
|
-
"commands": {name: _serialize_command(cmd) for name, cmd in app._commands.items()},
|
|
2930
|
-
"groups": {name: _serialize_group(grp) for name, grp in app._groups.items()},
|
|
2931
|
-
"deprecated": {name: dep.message for name, dep in app._deprecated.items()},
|
|
2932
3093
|
}
|
|
3094
|
+
if app.env_prefix is not None:
|
|
3095
|
+
schema["env_prefix"] = app.env_prefix
|
|
3096
|
+
if app.config:
|
|
3097
|
+
schema["config"] = app.config
|
|
3098
|
+
global_flags = [_serialize_flag(f) for f in app._global_flags]
|
|
3099
|
+
if global_flags:
|
|
3100
|
+
schema["global_flags"] = global_flags
|
|
3101
|
+
commands = {name: _serialize_command(cmd) for name, cmd in app._commands.items()}
|
|
3102
|
+
if commands:
|
|
3103
|
+
schema["commands"] = commands
|
|
3104
|
+
groups = {name: _serialize_group(grp) for name, grp in app._groups.items()}
|
|
3105
|
+
if groups:
|
|
3106
|
+
schema["groups"] = groups
|
|
3107
|
+
deprecated = {name: dep.message for name, dep in app._deprecated.items()}
|
|
3108
|
+
if deprecated:
|
|
3109
|
+
schema["deprecated"] = deprecated
|
|
2933
3110
|
if app._checks_enabled:
|
|
2934
3111
|
schema["checks"] = {
|
|
2935
3112
|
name: {
|