apcore-cli 0.3.1__tar.gz → 0.4.1__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.
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/CHANGELOG.md +20 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/PKG-INFO +11 -4
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/README.md +10 -3
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/pyproject.toml +1 -1
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/__main__.py +22 -2
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/cli.py +48 -7
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/shell.py +185 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_cli.py +53 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_shell.py +104 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/.github/CODEOWNERS +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/.github/copilot-ignore +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/.github/workflows/ci.yml +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/.gitignore +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/.gitmessage +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/.pre-commit-config.yaml +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/CLAUDE.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/commands/ops.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/examples/extensions/math/add.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/examples/extensions/math/multiply.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/examples/extensions/sysutil/disk.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/examples/extensions/sysutil/env.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/examples/extensions/sysutil/info.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/examples/extensions/text/reverse.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/examples/extensions/text/upper.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/examples/extensions/text/wordcount.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/examples/run_examples.sh +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/approval-gate.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/config-resolver.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/core-dispatcher.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/discovery.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/grouped-commands.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/output-formatter.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/overview.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/schema-parser.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/security-manager.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/shell-integration.md +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/planning/state.json +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/__init__.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/_sandbox_runner.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/approval.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/config.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/discovery.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/display_helpers.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/init_cmd.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/output.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/ref_resolver.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/schema_parser.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/security/__init__.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/security/audit.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/security/auth.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/security/config_encryptor.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/src/apcore_cli/security/sandbox.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/__init__.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/conftest.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_approval.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_bugfixes.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_config.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_discovery.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_e2e.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_init_cmd.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_integration.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_output.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_ref_resolver.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_schema_parser.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_security/__init__.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_security/test_audit.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_security/test_auth.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_security/test_config_encryptor.py +0 -0
- {apcore_cli-0.3.1 → apcore_cli-0.4.1}/tests/test_security/test_sandbox.py +0 -0
|
@@ -5,6 +5,26 @@ All notable changes to apcore-cli (Python SDK) will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
|
|
9
|
+
## [0.4.1] - 2026-03-30
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- prevent click parameter mismatch by setting expose_value=False for the --man option
|
|
13
|
+
|
|
14
|
+
## [0.4.0] - 2026-03-29
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Verbose help mode** — Built-in apcore options (`--input`, `--yes`, `--large-input`, `--format`, `--sandbox`) are now hidden from `--help` output by default. Pass `--help --verbose` to display the full option list including built-in options.
|
|
18
|
+
- **Universal man page generation** — `build_program_man_page()` generates a complete roff man page covering all registered commands. `configure_man_help()` adds `--help --man` support to any Click CLI, enabling downstream projects to get man pages for free.
|
|
19
|
+
- **Documentation URL support** — `set_docs_url()` sets a base URL for online docs. Per-command help shows `Docs: {url}/commands/{name}`, man page SEE ALSO includes `Full documentation at {url}`. No default — disabled when not set.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `build_module_command()` respects the global verbose help flag to control built-in option visibility.
|
|
23
|
+
- `--sandbox` is now always hidden from help (not yet implemented). Only four built-in options (`--input`, `--yes`, `--large-input`, `--format`) toggle with `--verbose`.
|
|
24
|
+
- Improved built-in option descriptions for clarity.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
8
28
|
## [0.3.1] - 2026-03-27
|
|
9
29
|
|
|
10
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apcore-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Terminal adapter for apcore — execute AI-Perceivable modules from the command line
|
|
5
5
|
Project-URL: Homepage, https://aiperceivable.com
|
|
6
6
|
Project-URL: Repository, https://github.com/aiperceivable/apcore-cli-python
|
|
@@ -241,6 +241,8 @@ apcore-cli [OPTIONS] COMMAND [ARGS]
|
|
|
241
241
|
| `--log-level` | `WARNING` | Logging: `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
|
242
242
|
| `--version` | | Show version and exit |
|
|
243
243
|
| `--help` | | Show help and exit |
|
|
244
|
+
| `--verbose` | | Show hidden built-in options in `--help` output |
|
|
245
|
+
| `--man` | | Print man page to stdout (use with `--help`) |
|
|
244
246
|
|
|
245
247
|
### Built-in Commands
|
|
246
248
|
|
|
@@ -253,7 +255,7 @@ apcore-cli [OPTIONS] COMMAND [ARGS]
|
|
|
253
255
|
|
|
254
256
|
### Module Execution Options
|
|
255
257
|
|
|
256
|
-
When executing a module (e.g. `apcore-cli math.add`), these built-in options are
|
|
258
|
+
When executing a module (e.g. `apcore-cli math.add`), these built-in options are available (hidden by default; pass `--help --verbose` to display them):
|
|
257
259
|
|
|
258
260
|
| Option | Description |
|
|
259
261
|
|--------|-------------|
|
|
@@ -261,7 +263,7 @@ When executing a module (e.g. `apcore-cli math.add`), these built-in options are
|
|
|
261
263
|
| `--yes` / `-y` | Bypass approval prompts |
|
|
262
264
|
| `--large-input` | Allow STDIN input larger than 10MB |
|
|
263
265
|
| `--format` | Output format: `json` or `table` |
|
|
264
|
-
| `--sandbox` | Run module in subprocess sandbox |
|
|
266
|
+
| `--sandbox` | Run module in subprocess sandbox *(not yet implemented)* |
|
|
265
267
|
|
|
266
268
|
Schema-generated flags (e.g. `--a`, `--b`) are added automatically from the module's `input_schema`.
|
|
267
269
|
|
|
@@ -328,7 +330,8 @@ cli:
|
|
|
328
330
|
- **Schema validation** -- inputs validated against JSON Schema before execution, with `$ref`/`allOf`/`anyOf`/`oneOf` resolution
|
|
329
331
|
- **Security** -- API key auth (keyring + AES-256-GCM), append-only audit logging, subprocess sandboxing
|
|
330
332
|
- **Shell completions** -- `apcore-cli completion bash|zsh|fish` generates completion scripts with dynamic module ID completion
|
|
331
|
-
- **Man pages** -- `apcore-cli man <command>` generates
|
|
333
|
+
- **Man pages** -- `apcore-cli man <command>` generates per-command man pages; `--help --man` prints a full-program man page via `configure_man_help()`
|
|
334
|
+
- **Documentation URL** -- `set_docs_url()` sets a base URL; per-command help shows `Docs: {url}/commands/{name}`, man page SEE ALSO links to the full docs site
|
|
332
335
|
- **Audit logging** -- all executions logged to `~/.apcore-cli/audit.jsonl` with SHA-256 input hashing
|
|
333
336
|
|
|
334
337
|
## How It Works
|
|
@@ -353,6 +356,10 @@ apcore-cli (the adapter)
|
|
|
353
356
|
|
|
|
354
357
|
+-- ConfigResolver 4-tier config precedence
|
|
355
358
|
+-- LazyModuleGroup Dynamic Click command generation
|
|
359
|
+
+-- set_verbose_help Toggle built-in option visibility
|
|
360
|
+
+-- set_docs_url Set base URL for online docs
|
|
361
|
+
+-- build_program_man_page Full-program roff man page
|
|
362
|
+
+-- configure_man_help Add --help --man support to any CLI
|
|
356
363
|
+-- schema_parser JSON Schema -> Click options
|
|
357
364
|
+-- ref_resolver $ref / allOf / anyOf / oneOf
|
|
358
365
|
+-- approval TTY-aware HITL approval
|
|
@@ -204,6 +204,8 @@ apcore-cli [OPTIONS] COMMAND [ARGS]
|
|
|
204
204
|
| `--log-level` | `WARNING` | Logging: `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
|
205
205
|
| `--version` | | Show version and exit |
|
|
206
206
|
| `--help` | | Show help and exit |
|
|
207
|
+
| `--verbose` | | Show hidden built-in options in `--help` output |
|
|
208
|
+
| `--man` | | Print man page to stdout (use with `--help`) |
|
|
207
209
|
|
|
208
210
|
### Built-in Commands
|
|
209
211
|
|
|
@@ -216,7 +218,7 @@ apcore-cli [OPTIONS] COMMAND [ARGS]
|
|
|
216
218
|
|
|
217
219
|
### Module Execution Options
|
|
218
220
|
|
|
219
|
-
When executing a module (e.g. `apcore-cli math.add`), these built-in options are
|
|
221
|
+
When executing a module (e.g. `apcore-cli math.add`), these built-in options are available (hidden by default; pass `--help --verbose` to display them):
|
|
220
222
|
|
|
221
223
|
| Option | Description |
|
|
222
224
|
|--------|-------------|
|
|
@@ -224,7 +226,7 @@ When executing a module (e.g. `apcore-cli math.add`), these built-in options are
|
|
|
224
226
|
| `--yes` / `-y` | Bypass approval prompts |
|
|
225
227
|
| `--large-input` | Allow STDIN input larger than 10MB |
|
|
226
228
|
| `--format` | Output format: `json` or `table` |
|
|
227
|
-
| `--sandbox` | Run module in subprocess sandbox |
|
|
229
|
+
| `--sandbox` | Run module in subprocess sandbox *(not yet implemented)* |
|
|
228
230
|
|
|
229
231
|
Schema-generated flags (e.g. `--a`, `--b`) are added automatically from the module's `input_schema`.
|
|
230
232
|
|
|
@@ -291,7 +293,8 @@ cli:
|
|
|
291
293
|
- **Schema validation** -- inputs validated against JSON Schema before execution, with `$ref`/`allOf`/`anyOf`/`oneOf` resolution
|
|
292
294
|
- **Security** -- API key auth (keyring + AES-256-GCM), append-only audit logging, subprocess sandboxing
|
|
293
295
|
- **Shell completions** -- `apcore-cli completion bash|zsh|fish` generates completion scripts with dynamic module ID completion
|
|
294
|
-
- **Man pages** -- `apcore-cli man <command>` generates
|
|
296
|
+
- **Man pages** -- `apcore-cli man <command>` generates per-command man pages; `--help --man` prints a full-program man page via `configure_man_help()`
|
|
297
|
+
- **Documentation URL** -- `set_docs_url()` sets a base URL; per-command help shows `Docs: {url}/commands/{name}`, man page SEE ALSO links to the full docs site
|
|
295
298
|
- **Audit logging** -- all executions logged to `~/.apcore-cli/audit.jsonl` with SHA-256 input hashing
|
|
296
299
|
|
|
297
300
|
## How It Works
|
|
@@ -316,6 +319,10 @@ apcore-cli (the adapter)
|
|
|
316
319
|
|
|
|
317
320
|
+-- ConfigResolver 4-tier config precedence
|
|
318
321
|
+-- LazyModuleGroup Dynamic Click command generation
|
|
322
|
+
+-- set_verbose_help Toggle built-in option visibility
|
|
323
|
+
+-- set_docs_url Set base URL for online docs
|
|
324
|
+
+-- build_program_man_page Full-program roff man page
|
|
325
|
+
+-- configure_man_help Add --help --man support to any CLI
|
|
319
326
|
+-- schema_parser JSON Schema -> Click options
|
|
320
327
|
+-- ref_resolver $ref / allOf / anyOf / oneOf
|
|
321
328
|
+-- approval TTY-aware HITL approval
|
|
@@ -9,7 +9,7 @@ import sys
|
|
|
9
9
|
import click
|
|
10
10
|
|
|
11
11
|
from apcore_cli import __version__
|
|
12
|
-
from apcore_cli.cli import GroupedModuleGroup, set_audit_logger
|
|
12
|
+
from apcore_cli.cli import GroupedModuleGroup, set_audit_logger, set_verbose_help
|
|
13
13
|
from apcore_cli.config import ConfigResolver
|
|
14
14
|
from apcore_cli.discovery import register_discovery_commands
|
|
15
15
|
from apcore_cli.security.audit import AuditLogger
|
|
@@ -50,6 +50,12 @@ def _extract_binding_path(argv: list[str] | None = None) -> str | None:
|
|
|
50
50
|
return _extract_argv_option(argv, "--binding")
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def _has_verbose_flag(argv: list[str] | None = None) -> bool:
|
|
54
|
+
"""Check if --verbose is present in argv (pre-parse, before Click)."""
|
|
55
|
+
args = argv if argv is not None else sys.argv[1:]
|
|
56
|
+
return "--verbose" in args
|
|
57
|
+
|
|
58
|
+
|
|
53
59
|
def create_cli(
|
|
54
60
|
extensions_dir: str | None = None,
|
|
55
61
|
prog_name: str | None = None,
|
|
@@ -75,6 +81,11 @@ def create_cli(
|
|
|
75
81
|
if prog_name is None:
|
|
76
82
|
prog_name = os.path.basename(sys.argv[0]) or "apcore-cli"
|
|
77
83
|
|
|
84
|
+
# Pre-parse --verbose before Click runs so build_module_command knows
|
|
85
|
+
# whether to hide built-in options.
|
|
86
|
+
verbose = _has_verbose_flag()
|
|
87
|
+
set_verbose_help(verbose)
|
|
88
|
+
|
|
78
89
|
# Resolve CLI log level (3-tier precedence, evaluated before Click runs):
|
|
79
90
|
# APCORE_CLI_LOGGING_LEVEL (CLI-specific) > APCORE_LOGGING_LEVEL (global) > WARNING
|
|
80
91
|
# The --log-level flag (parsed later) can further override at runtime.
|
|
@@ -115,7 +126,7 @@ def create_cli(
|
|
|
115
126
|
|
|
116
127
|
if ext_dir_missing:
|
|
117
128
|
click.echo(
|
|
118
|
-
f"Error: Extensions directory not found: '{ext_dir}'.
|
|
129
|
+
f"Error: Extensions directory not found: '{ext_dir}'. Set APCORE_EXTENSIONS_ROOT or verify the path.",
|
|
119
130
|
err=True,
|
|
120
131
|
)
|
|
121
132
|
sys.exit(EXIT_CONFIG_NOT_FOUND)
|
|
@@ -212,6 +223,13 @@ def create_cli(
|
|
|
212
223
|
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False),
|
|
213
224
|
help="Log verbosity. Overrides APCORE_CLI_LOGGING_LEVEL and APCORE_LOGGING_LEVEL env vars.",
|
|
214
225
|
)
|
|
226
|
+
@click.option(
|
|
227
|
+
"--verbose",
|
|
228
|
+
"verbose_help",
|
|
229
|
+
is_flag=True,
|
|
230
|
+
default=False,
|
|
231
|
+
help="Show all options in help output (including built-in apcore options).",
|
|
232
|
+
)
|
|
215
233
|
@click.pass_context
|
|
216
234
|
def cli(
|
|
217
235
|
ctx: click.Context,
|
|
@@ -219,6 +237,7 @@ def create_cli(
|
|
|
219
237
|
commands_dir_opt: str | None = None,
|
|
220
238
|
binding_opt: str | None = None,
|
|
221
239
|
log_level: str | None = None,
|
|
240
|
+
verbose_help: bool = False,
|
|
222
241
|
) -> None:
|
|
223
242
|
if log_level is not None:
|
|
224
243
|
# basicConfig() is a no-op once handlers exist; set level on the root logger directly.
|
|
@@ -229,6 +248,7 @@ def create_cli(
|
|
|
229
248
|
logging.getLogger("apcore").setLevel(apcore_level)
|
|
230
249
|
ctx.ensure_object(dict)
|
|
231
250
|
ctx.obj["extensions_dir"] = ext_dir
|
|
251
|
+
ctx.obj["verbose_help"] = verbose_help
|
|
232
252
|
|
|
233
253
|
# Register discovery commands
|
|
234
254
|
register_discovery_commands(cli, registry)
|
|
@@ -32,6 +32,33 @@ BUILTIN_COMMANDS = ["completion", "describe", "exec", "init", "list", "man"]
|
|
|
32
32
|
# Module-level audit logger, set during CLI init
|
|
33
33
|
_audit_logger: AuditLogger | None = None
|
|
34
34
|
|
|
35
|
+
# Module-level verbose help flag, set during CLI init
|
|
36
|
+
_verbose_help: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def set_verbose_help(verbose: bool) -> None:
|
|
40
|
+
"""Set the verbose help flag. When False, built-in options are hidden."""
|
|
41
|
+
global _verbose_help
|
|
42
|
+
_verbose_help = verbose
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Module-level docs URL, set by downstream projects
|
|
46
|
+
_docs_url: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_docs_url(url: str | None) -> None:
|
|
50
|
+
"""Set the base URL for online documentation links in help and man pages.
|
|
51
|
+
|
|
52
|
+
Pass None to disable. Command-level help appends ``/commands/{name}``
|
|
53
|
+
automatically.
|
|
54
|
+
|
|
55
|
+
Example::
|
|
56
|
+
|
|
57
|
+
set_docs_url("https://docs.apcore.dev/cli")
|
|
58
|
+
"""
|
|
59
|
+
global _docs_url
|
|
60
|
+
_docs_url = url
|
|
61
|
+
|
|
35
62
|
|
|
36
63
|
def set_audit_logger(audit_logger: AuditLogger | None) -> None:
|
|
37
64
|
"""Set the global audit logger instance. Pass None to clear."""
|
|
@@ -452,18 +479,27 @@ def build_module_command(
|
|
|
452
479
|
sys.exit(exit_code)
|
|
453
480
|
|
|
454
481
|
# Build the command with schema-generated options + built-in options
|
|
482
|
+
_epilog_parts: list[str] = []
|
|
483
|
+
if not _verbose_help:
|
|
484
|
+
_epilog_parts.append("Use --verbose to show all options (including built-in apcore options).")
|
|
485
|
+
if _docs_url:
|
|
486
|
+
_epilog_parts.append(f"Docs: {_docs_url}/commands/{effective_cmd_name}")
|
|
487
|
+
_epilog = "\n".join(_epilog_parts) if _epilog_parts else None
|
|
455
488
|
cmd = click.Command(
|
|
456
489
|
name=effective_cmd_name,
|
|
457
490
|
help=cmd_help,
|
|
458
491
|
callback=callback,
|
|
492
|
+
epilog=_epilog,
|
|
459
493
|
)
|
|
460
494
|
|
|
461
|
-
# Add built-in options
|
|
495
|
+
# Add built-in options (hidden unless --verbose is passed with --help)
|
|
496
|
+
_hide = not _verbose_help
|
|
462
497
|
cmd.params.append(
|
|
463
498
|
click.Option(
|
|
464
499
|
["--input"],
|
|
465
500
|
default=None,
|
|
466
|
-
help="Read input from file or
|
|
501
|
+
help="Read JSON input from a file path, or use '-' to read from stdin pipe.",
|
|
502
|
+
hidden=_hide,
|
|
467
503
|
)
|
|
468
504
|
)
|
|
469
505
|
cmd.params.append(
|
|
@@ -471,7 +507,8 @@ def build_module_command(
|
|
|
471
507
|
["--yes", "-y"],
|
|
472
508
|
is_flag=True,
|
|
473
509
|
default=False,
|
|
474
|
-
help="
|
|
510
|
+
help="Skip interactive approval prompts (for scripts and CI).",
|
|
511
|
+
hidden=_hide,
|
|
475
512
|
)
|
|
476
513
|
)
|
|
477
514
|
cmd.params.append(
|
|
@@ -479,7 +516,8 @@ def build_module_command(
|
|
|
479
516
|
["--large-input"],
|
|
480
517
|
is_flag=True,
|
|
481
518
|
default=False,
|
|
482
|
-
help="Allow
|
|
519
|
+
help="Allow stdin input larger than 10MB (default limit protects against accidental pipes).",
|
|
520
|
+
hidden=_hide,
|
|
483
521
|
)
|
|
484
522
|
)
|
|
485
523
|
cmd.params.append(
|
|
@@ -487,20 +525,23 @@ def build_module_command(
|
|
|
487
525
|
["--format"],
|
|
488
526
|
type=click.Choice(["json", "table"]),
|
|
489
527
|
default=None,
|
|
490
|
-
help="
|
|
528
|
+
help="Set output format: 'json' for machine-readable, 'table' for human-readable.",
|
|
529
|
+
hidden=_hide,
|
|
491
530
|
)
|
|
492
531
|
)
|
|
532
|
+
# --sandbox is always hidden (not yet implemented)
|
|
493
533
|
cmd.params.append(
|
|
494
534
|
click.Option(
|
|
495
535
|
["--sandbox"],
|
|
496
536
|
is_flag=True,
|
|
497
537
|
default=False,
|
|
498
|
-
help="Run module in subprocess
|
|
538
|
+
help="Run module in an isolated subprocess with restricted filesystem and env access.",
|
|
539
|
+
hidden=True,
|
|
499
540
|
)
|
|
500
541
|
)
|
|
501
542
|
|
|
502
543
|
# Guard: schema property names must not collide with built-in option names.
|
|
503
|
-
_reserved = {"input", "yes", "large_input", "format", "sandbox"}
|
|
544
|
+
_reserved = {"input", "yes", "large_input", "format", "sandbox", "verbose"}
|
|
504
545
|
for opt in schema_options:
|
|
505
546
|
if opt.name in _reserved:
|
|
506
547
|
click.echo(
|
|
@@ -339,6 +339,191 @@ def _generate_man_page(command_name: str, command: click.Command | None, prog_na
|
|
|
339
339
|
return "\n".join(sections)
|
|
340
340
|
|
|
341
341
|
|
|
342
|
+
def _roff_escape(s: str) -> str:
|
|
343
|
+
"""Escape a string for roff output."""
|
|
344
|
+
return s.replace("\\", "\\\\").replace("-", "\\-").replace("'", "\\(aq")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def build_program_man_page(
|
|
348
|
+
cli: click.Group,
|
|
349
|
+
prog_name: str,
|
|
350
|
+
version: str,
|
|
351
|
+
description: str | None = None,
|
|
352
|
+
docs_url: str | None = None,
|
|
353
|
+
) -> str:
|
|
354
|
+
"""Build a complete roff man page for the entire CLI program.
|
|
355
|
+
|
|
356
|
+
Covers all registered commands including downstream business commands
|
|
357
|
+
injected via GroupedModuleGroup.
|
|
358
|
+
"""
|
|
359
|
+
today = date.today().isoformat()
|
|
360
|
+
desc = description or cli.help or f"{prog_name} CLI"
|
|
361
|
+
s: list[str] = []
|
|
362
|
+
|
|
363
|
+
s.append(f'.TH "{prog_name.upper()}" "1" "{today}" "{prog_name} {version}" "{prog_name} Manual"')
|
|
364
|
+
|
|
365
|
+
s.append(".SH NAME")
|
|
366
|
+
s.append(f"{prog_name} \\- {_roff_escape(desc)}")
|
|
367
|
+
|
|
368
|
+
s.append(".SH SYNOPSIS")
|
|
369
|
+
s.append(f"\\fB{prog_name}\\fR [\\fIglobal\\-options\\fR] \\fIcommand\\fR [\\fIcommand\\-options\\fR]")
|
|
370
|
+
|
|
371
|
+
s.append(".SH DESCRIPTION")
|
|
372
|
+
s.append(_roff_escape(desc))
|
|
373
|
+
|
|
374
|
+
# Global options
|
|
375
|
+
ctx = click.Context(cli, info_name=prog_name)
|
|
376
|
+
params = cli.get_params(ctx)
|
|
377
|
+
visible_params = [p for p in params if not getattr(p, "hidden", False) and p.name not in ("help", "version", "man")]
|
|
378
|
+
if visible_params:
|
|
379
|
+
s.append(".SH GLOBAL OPTIONS")
|
|
380
|
+
for p in visible_params:
|
|
381
|
+
record = p.get_help_record(ctx)
|
|
382
|
+
if record:
|
|
383
|
+
s.append(".TP")
|
|
384
|
+
s.append(f"\\fB{_roff_escape(record[0])}\\fR")
|
|
385
|
+
s.append(_roff_escape(record[1]))
|
|
386
|
+
|
|
387
|
+
# Commands
|
|
388
|
+
cmd_names = cli.list_commands(ctx)
|
|
389
|
+
if cmd_names:
|
|
390
|
+
s.append(".SH COMMANDS")
|
|
391
|
+
for name in sorted(cmd_names):
|
|
392
|
+
if name == "help":
|
|
393
|
+
continue
|
|
394
|
+
cmd = cli.get_command(ctx, name)
|
|
395
|
+
if cmd is None:
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
cmd_desc = cmd.get_short_help_str() if cmd else ""
|
|
399
|
+
s.append(".TP")
|
|
400
|
+
s.append(f"\\fB{prog_name} {_roff_escape(name)}\\fR")
|
|
401
|
+
if cmd_desc:
|
|
402
|
+
s.append(_roff_escape(cmd_desc))
|
|
403
|
+
|
|
404
|
+
# Command options
|
|
405
|
+
sub_ctx = click.Context(cmd, info_name=name, parent=ctx)
|
|
406
|
+
sub_params = [
|
|
407
|
+
p for p in cmd.get_params(sub_ctx) if not getattr(p, "hidden", False) and p.name not in ("help",)
|
|
408
|
+
]
|
|
409
|
+
for p in sub_params:
|
|
410
|
+
record = p.get_help_record(sub_ctx)
|
|
411
|
+
if record:
|
|
412
|
+
s.append(".RS")
|
|
413
|
+
s.append(".TP")
|
|
414
|
+
s.append(f"\\fB{_roff_escape(record[0])}\\fR")
|
|
415
|
+
s.append(_roff_escape(record[1]))
|
|
416
|
+
s.append(".RE")
|
|
417
|
+
|
|
418
|
+
# Nested subcommands (groups)
|
|
419
|
+
if isinstance(cmd, click.Group):
|
|
420
|
+
sub_names = cmd.list_commands(sub_ctx)
|
|
421
|
+
for sub_name in sorted(sub_names):
|
|
422
|
+
if sub_name == "help":
|
|
423
|
+
continue
|
|
424
|
+
sub_cmd = cmd.get_command(sub_ctx, sub_name)
|
|
425
|
+
if sub_cmd is None:
|
|
426
|
+
continue
|
|
427
|
+
sub_desc = sub_cmd.get_short_help_str() if sub_cmd else ""
|
|
428
|
+
s.append(".TP")
|
|
429
|
+
s.append(f"\\fB{prog_name} {_roff_escape(name)} {_roff_escape(sub_name)}\\fR")
|
|
430
|
+
if sub_desc:
|
|
431
|
+
s.append(_roff_escape(sub_desc))
|
|
432
|
+
nested_ctx = click.Context(sub_cmd, info_name=sub_name, parent=sub_ctx)
|
|
433
|
+
nested_params = [
|
|
434
|
+
p
|
|
435
|
+
for p in sub_cmd.get_params(nested_ctx)
|
|
436
|
+
if not getattr(p, "hidden", False) and p.name not in ("help",)
|
|
437
|
+
]
|
|
438
|
+
for p in nested_params:
|
|
439
|
+
record = p.get_help_record(nested_ctx)
|
|
440
|
+
if record:
|
|
441
|
+
s.append(".RS")
|
|
442
|
+
s.append(".TP")
|
|
443
|
+
s.append(f"\\fB{_roff_escape(record[0])}\\fR")
|
|
444
|
+
s.append(_roff_escape(record[1]))
|
|
445
|
+
s.append(".RE")
|
|
446
|
+
|
|
447
|
+
# Environment
|
|
448
|
+
s.append(".SH ENVIRONMENT")
|
|
449
|
+
s.append(".TP")
|
|
450
|
+
s.append("\\fBAPCORE_EXTENSIONS_ROOT\\fR")
|
|
451
|
+
s.append("Path to the apcore extensions directory.")
|
|
452
|
+
s.append(".TP")
|
|
453
|
+
s.append("\\fBAPCORE_CLI_AUTO_APPROVE\\fR")
|
|
454
|
+
s.append("Set to \\fB1\\fR to bypass approval prompts.")
|
|
455
|
+
s.append(".TP")
|
|
456
|
+
s.append("\\fBAPCORE_CLI_LOGGING_LEVEL\\fR")
|
|
457
|
+
s.append("CLI\\-specific logging verbosity (DEBUG|INFO|WARNING|ERROR).")
|
|
458
|
+
|
|
459
|
+
# Exit codes
|
|
460
|
+
s.append(".SH EXIT CODES")
|
|
461
|
+
codes = [
|
|
462
|
+
("0", "Success."),
|
|
463
|
+
("1", "Module execution error."),
|
|
464
|
+
("2", "Invalid input."),
|
|
465
|
+
("44", "Module not found."),
|
|
466
|
+
("45", "Schema validation error."),
|
|
467
|
+
("46", "Approval denied or timed out."),
|
|
468
|
+
("47", "Configuration error."),
|
|
469
|
+
("77", "ACL denied."),
|
|
470
|
+
("130", "Cancelled by user (SIGINT)."),
|
|
471
|
+
]
|
|
472
|
+
for code, meaning in codes:
|
|
473
|
+
s.append(f".TP\n\\fB{code}\\fR\n{meaning}")
|
|
474
|
+
|
|
475
|
+
s.append(".SH SEE ALSO")
|
|
476
|
+
s.append(f"\\fB{prog_name} \\-\\-help \\-\\-verbose\\fR for full option list.")
|
|
477
|
+
if docs_url:
|
|
478
|
+
s.append(f".PP\nFull documentation at \\fI{_roff_escape(docs_url)}\\fR")
|
|
479
|
+
|
|
480
|
+
return "\n".join(s)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def configure_man_help(
|
|
484
|
+
cli: click.Group,
|
|
485
|
+
prog_name: str,
|
|
486
|
+
version: str,
|
|
487
|
+
description: str | None = None,
|
|
488
|
+
docs_url: str | None = None,
|
|
489
|
+
) -> None:
|
|
490
|
+
"""Configure --help --man support on a Click CLI group.
|
|
491
|
+
|
|
492
|
+
When --man is passed with --help, outputs a complete roff man page
|
|
493
|
+
covering all registered commands. Downstream projects call this once
|
|
494
|
+
to get man page generation for free.
|
|
495
|
+
|
|
496
|
+
.. note::
|
|
497
|
+
Call this **after** all commands are registered on ``cli``.
|
|
498
|
+
The argv pre-parse triggers immediate man page generation, so
|
|
499
|
+
commands added later will not appear in the output.
|
|
500
|
+
|
|
501
|
+
Usage:
|
|
502
|
+
configure_man_help(cli, "reach", "0.2.0", "ReachForge", "https://reachforge.dev/docs")
|
|
503
|
+
"""
|
|
504
|
+
# Add --man as a hidden Click option.
|
|
505
|
+
# expose_value=False: Click must not pass this to the group callback,
|
|
506
|
+
# which has no 'man' parameter. The value is read directly from sys.argv
|
|
507
|
+
# via the pre-parse below.
|
|
508
|
+
cli.params.append(
|
|
509
|
+
click.Option(
|
|
510
|
+
["--man"],
|
|
511
|
+
is_flag=True,
|
|
512
|
+
default=False,
|
|
513
|
+
hidden=True,
|
|
514
|
+
expose_value=False,
|
|
515
|
+
help="Output man page in roff format (use with --help).",
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Pre-parse: if both --help and --man in argv, generate man page and exit
|
|
520
|
+
args = sys.argv[1:]
|
|
521
|
+
if "--man" in args and ("--help" in args or "-h" in args):
|
|
522
|
+
roff = build_program_man_page(cli, prog_name, version, description, docs_url)
|
|
523
|
+
click.echo(roff)
|
|
524
|
+
sys.exit(0)
|
|
525
|
+
|
|
526
|
+
|
|
342
527
|
def register_shell_commands(cli: click.Group, prog_name: str = "apcore-cli") -> None:
|
|
343
528
|
"""Register completion and man commands."""
|
|
344
529
|
|
|
@@ -880,3 +880,56 @@ class TestGroupedE2E:
|
|
|
880
880
|
group = self._make_e2e_group()
|
|
881
881
|
result = CliRunner().invoke(group, ["product", "nonexistent"])
|
|
882
882
|
assert result.exit_code == 2
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
class TestVerboseHelp:
|
|
886
|
+
"""Tests for --verbose help flag controlling built-in option visibility."""
|
|
887
|
+
|
|
888
|
+
def test_builtin_options_hidden_by_default(self):
|
|
889
|
+
"""Built-in options are hidden from help by default."""
|
|
890
|
+
from apcore_cli import cli as cli_mod
|
|
891
|
+
|
|
892
|
+
cli_mod._verbose_help = False
|
|
893
|
+
try:
|
|
894
|
+
module_def = _make_mock_module_def()
|
|
895
|
+
cmd = build_module_command(module_def, _make_mock_executor())
|
|
896
|
+
hidden_names = [p.name for p in cmd.params if getattr(p, "hidden", False)]
|
|
897
|
+
assert "input" in hidden_names
|
|
898
|
+
assert "yes" in hidden_names
|
|
899
|
+
assert "large_input" in hidden_names
|
|
900
|
+
assert "format" in hidden_names
|
|
901
|
+
assert "sandbox" in hidden_names
|
|
902
|
+
finally:
|
|
903
|
+
cli_mod._verbose_help = False
|
|
904
|
+
|
|
905
|
+
def test_builtin_options_shown_when_verbose(self):
|
|
906
|
+
"""Built-in options are visible when verbose help is enabled."""
|
|
907
|
+
from apcore_cli import cli as cli_mod
|
|
908
|
+
|
|
909
|
+
cli_mod._verbose_help = True
|
|
910
|
+
try:
|
|
911
|
+
module_def = _make_mock_module_def()
|
|
912
|
+
cmd = build_module_command(module_def, _make_mock_executor())
|
|
913
|
+
hidden_names = [p.name for p in cmd.params if getattr(p, "hidden", False)]
|
|
914
|
+
assert "input" not in hidden_names
|
|
915
|
+
assert "yes" not in hidden_names
|
|
916
|
+
assert "large_input" not in hidden_names
|
|
917
|
+
assert "format" not in hidden_names
|
|
918
|
+
# sandbox is always hidden (not yet implemented)
|
|
919
|
+
assert "sandbox" in hidden_names
|
|
920
|
+
finally:
|
|
921
|
+
cli_mod._verbose_help = False
|
|
922
|
+
|
|
923
|
+
def test_set_verbose_help_function(self):
|
|
924
|
+
"""set_verbose_help correctly sets the module-level flag."""
|
|
925
|
+
from apcore_cli import cli as cli_mod
|
|
926
|
+
from apcore_cli.cli import set_verbose_help
|
|
927
|
+
|
|
928
|
+
original = cli_mod._verbose_help
|
|
929
|
+
try:
|
|
930
|
+
set_verbose_help(True)
|
|
931
|
+
assert cli_mod._verbose_help is True
|
|
932
|
+
set_verbose_help(False)
|
|
933
|
+
assert cli_mod._verbose_help is False
|
|
934
|
+
finally:
|
|
935
|
+
cli_mod._verbose_help = original
|
|
@@ -7,6 +7,8 @@ from apcore_cli.shell import (
|
|
|
7
7
|
_generate_bash_completion,
|
|
8
8
|
_generate_fish_completion,
|
|
9
9
|
_generate_zsh_completion,
|
|
10
|
+
build_program_man_page,
|
|
11
|
+
configure_man_help,
|
|
10
12
|
register_shell_commands,
|
|
11
13
|
)
|
|
12
14
|
|
|
@@ -231,3 +233,105 @@ class TestManCommand:
|
|
|
231
233
|
assert "SYNOPSIS" in result.output
|
|
232
234
|
# Should reflect the actual --tag option, not generic [ARGUMENTS]
|
|
233
235
|
assert "--tag" in result.output
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class TestBuildProgramManPage:
|
|
239
|
+
def test_generates_roff_with_th_header(self):
|
|
240
|
+
@click.group()
|
|
241
|
+
def cli():
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
@cli.command()
|
|
245
|
+
@click.option("--name", help="Your name")
|
|
246
|
+
def hello(name):
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
roff = build_program_man_page(cli, "test-cli", "1.0.0")
|
|
250
|
+
assert '.TH "TEST-CLI"' in roff
|
|
251
|
+
assert ".SH COMMANDS" in roff
|
|
252
|
+
assert "hello" in roff
|
|
253
|
+
|
|
254
|
+
def test_includes_nested_subcommands(self):
|
|
255
|
+
@click.group()
|
|
256
|
+
def cli():
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
@cli.group()
|
|
260
|
+
def grp():
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
@grp.command()
|
|
264
|
+
@click.option("--flag", is_flag=True, help="A flag")
|
|
265
|
+
def sub(flag):
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
roff = build_program_man_page(cli, "mycli", "1.0.0")
|
|
269
|
+
assert "mycli grp sub" in roff
|
|
270
|
+
|
|
271
|
+
def test_includes_standard_sections(self):
|
|
272
|
+
@click.group()
|
|
273
|
+
def cli():
|
|
274
|
+
"""My test CLI."""
|
|
275
|
+
|
|
276
|
+
roff = build_program_man_page(cli, "test-cli", "1.0.0")
|
|
277
|
+
assert ".SH NAME" in roff
|
|
278
|
+
assert ".SH SYNOPSIS" in roff
|
|
279
|
+
assert ".SH DESCRIPTION" in roff
|
|
280
|
+
assert ".SH ENVIRONMENT" in roff
|
|
281
|
+
assert ".SH EXIT CODES" in roff
|
|
282
|
+
assert ".SH SEE ALSO" in roff
|
|
283
|
+
|
|
284
|
+
def test_uses_custom_description(self):
|
|
285
|
+
@click.group()
|
|
286
|
+
def cli():
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
roff = build_program_man_page(cli, "test-cli", "1.0.0", description="Custom desc")
|
|
290
|
+
assert "Custom desc" in roff
|
|
291
|
+
|
|
292
|
+
def test_includes_command_options(self):
|
|
293
|
+
@click.group()
|
|
294
|
+
def cli():
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
@cli.command()
|
|
298
|
+
@click.option("--output", "-o", help="Output file")
|
|
299
|
+
def build(output):
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
roff = build_program_man_page(cli, "test-cli", "1.0.0")
|
|
303
|
+
assert "Output file" in roff
|
|
304
|
+
|
|
305
|
+
def test_skips_help_command(self):
|
|
306
|
+
@click.group()
|
|
307
|
+
def cli():
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
@cli.command()
|
|
311
|
+
def help():
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
roff = build_program_man_page(cli, "test-cli", "1.0.0")
|
|
315
|
+
# "help" should not appear in the COMMANDS section as a listed command
|
|
316
|
+
assert "test\\-cli help" not in roff
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class TestConfigureManHelp:
|
|
320
|
+
def test_adds_hidden_man_option(self):
|
|
321
|
+
@click.group()
|
|
322
|
+
def cli():
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
configure_man_help(cli, "test-cli", "1.0.0")
|
|
326
|
+
man_params = [p for p in cli.params if p.name == "man"]
|
|
327
|
+
assert len(man_params) == 1
|
|
328
|
+
assert man_params[0].hidden is True
|
|
329
|
+
|
|
330
|
+
def test_man_option_is_flag(self):
|
|
331
|
+
@click.group()
|
|
332
|
+
def cli():
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
configure_man_help(cli, "test-cli", "1.0.0")
|
|
336
|
+
man_params = [p for p in cli.params if p.name == "man"]
|
|
337
|
+
assert man_params[0].is_flag is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|