apcore-cli 0.3.0__tar.gz → 0.4.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.
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/CHANGELOG.md +34 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/PKG-INFO +13 -6
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/README.md +11 -4
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/pyproject.toml +2 -2
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/__main__.py +51 -5
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/cli.py +49 -8
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/init_cmd.py +5 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/shell.py +189 -2
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_cli.py +53 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_shell.py +104 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/.github/CODEOWNERS +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/.github/copilot-ignore +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/.gitignore +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/.gitmessage +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/.pre-commit-config.yaml +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/CLAUDE.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/commands/ops.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/examples/extensions/math/add.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/examples/extensions/math/multiply.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/examples/extensions/sysutil/disk.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/examples/extensions/sysutil/env.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/examples/extensions/sysutil/info.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/examples/extensions/text/reverse.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/examples/extensions/text/upper.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/examples/extensions/text/wordcount.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/examples/run_examples.sh +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/approval-gate.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/config-resolver.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/core-dispatcher.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/discovery.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/grouped-commands.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/output-formatter.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/overview.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/schema-parser.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/security-manager.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/shell-integration.md +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/planning/state.json +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/__init__.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/_sandbox_runner.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/approval.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/config.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/discovery.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/display_helpers.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/output.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/ref_resolver.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/schema_parser.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/security/__init__.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/security/audit.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/security/auth.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/security/config_encryptor.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/src/apcore_cli/security/sandbox.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/__init__.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/conftest.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_approval.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_bugfixes.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_config.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_discovery.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_e2e.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_init_cmd.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_integration.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_output.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_ref_resolver.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_schema_parser.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_security/__init__.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_security/test_audit.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_security/test_auth.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_security/test_config_encryptor.py +0 -0
- {apcore_cli-0.3.0 → apcore_cli-0.4.0}/tests/test_security/test_sandbox.py +0 -0
|
@@ -5,6 +5,40 @@ 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
|
+
## [0.4.0] - 2026-03-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **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.
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `build_module_command()` respects the global verbose help flag to control built-in option visibility.
|
|
17
|
+
- `--sandbox` is now always hidden from help (not yet implemented). Only four built-in options (`--input`, `--yes`, `--large-input`, `--format`) toggle with `--verbose`.
|
|
18
|
+
- Improved built-in option descriptions for clarity.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [0.3.1] - 2026-03-27
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **DisplayResolver integration** — `__main__.py` integrates `DisplayResolver` from `apcore-toolkit` (optional) when `--binding` option is provided; gracefully skipped when not installed.
|
|
27
|
+
- **`init` to `BUILTIN_COMMANDS`** — `init` subcommand is now registered in the builtin commands set.
|
|
28
|
+
- **`APCORE_AUTH_API_KEY` to man page** — environment variable documented in generated roff man page.
|
|
29
|
+
- **Grouped shell completion with `_APCORE_GRP`** — bash/zsh/fish completion scripts now support two-level group/command completion via the `_APCORE_GRP` environment variable (`shell.py`).
|
|
30
|
+
- **Path traversal validation for `--dir` in `init` command** — rejects paths containing `..` segments to prevent directory escape (`init_cmd.py`).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **`RegistryWriter` API call** — constructor now called without parameters; fixes `TypeError` introduced by upstream API change.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `apcore` dependency bumped to `>=0.14.0`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
8
42
|
## [0.3.0] - 2026-03-23
|
|
9
43
|
|
|
10
44
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apcore-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
20
20
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Requires-Python: >=3.11
|
|
23
|
-
Requires-Dist: apcore>=0.
|
|
23
|
+
Requires-Dist: apcore>=0.14.0
|
|
24
24
|
Requires-Dist: click>=8.1
|
|
25
25
|
Requires-Dist: cryptography>=41.0
|
|
26
26
|
Requires-Dist: jsonschema>=4.20
|
|
@@ -85,7 +85,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
|
|
|
85
85
|
pip install apcore-cli
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Requires Python 3.11+ and `apcore >= 0.
|
|
88
|
+
Requires Python 3.11+ and `apcore >= 0.14.0`.
|
|
89
89
|
|
|
90
90
|
## Quick Start
|
|
91
91
|
|
|
@@ -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
|
|
@@ -48,7 +48,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
|
|
|
48
48
|
pip install apcore-cli
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Requires Python 3.11+ and `apcore >= 0.
|
|
51
|
+
Requires Python 3.11+ and `apcore >= 0.14.0`.
|
|
52
52
|
|
|
53
53
|
## Quick Start
|
|
54
54
|
|
|
@@ -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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apcore-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Terminal adapter for apcore — execute AI-Perceivable modules from the command line"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Environment :: Console",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"apcore>=0.
|
|
29
|
+
"apcore>=0.14.0",
|
|
30
30
|
"click>=8.1",
|
|
31
31
|
"jsonschema>=4.20",
|
|
32
32
|
"rich>=13.0",
|
|
@@ -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
|
|
@@ -45,10 +45,22 @@ def _extract_commands_dir(argv: list[str] | None = None) -> str | None:
|
|
|
45
45
|
return _extract_argv_option(argv, "--commands-dir")
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _extract_binding_path(argv: list[str] | None = None) -> str | None:
|
|
49
|
+
"""Extract --binding value from argv before Click parses it."""
|
|
50
|
+
return _extract_argv_option(argv, "--binding")
|
|
51
|
+
|
|
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
|
+
|
|
48
59
|
def create_cli(
|
|
49
60
|
extensions_dir: str | None = None,
|
|
50
61
|
prog_name: str | None = None,
|
|
51
62
|
commands_dir: str | None = None,
|
|
63
|
+
binding_path: str | None = None,
|
|
52
64
|
) -> click.Group:
|
|
53
65
|
"""Create the CLI application.
|
|
54
66
|
|
|
@@ -62,10 +74,18 @@ def create_cli(
|
|
|
62
74
|
commands_dir: Directory containing convention-based modules.
|
|
63
75
|
When set, scans for plain-function modules and registers
|
|
64
76
|
them via ConventionScanner (requires apcore-toolkit).
|
|
77
|
+
binding_path: Path to binding.yaml file or directory for display resolution.
|
|
78
|
+
When set, applies DisplayResolver to convention-scanned modules
|
|
79
|
+
(requires apcore-toolkit).
|
|
65
80
|
"""
|
|
66
81
|
if prog_name is None:
|
|
67
82
|
prog_name = os.path.basename(sys.argv[0]) or "apcore-cli"
|
|
68
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
|
+
|
|
69
89
|
# Resolve CLI log level (3-tier precedence, evaluated before Click runs):
|
|
70
90
|
# APCORE_CLI_LOGGING_LEVEL (CLI-specific) > APCORE_LOGGING_LEVEL (global) > WARNING
|
|
71
91
|
# The --log-level flag (parsed later) can further override at runtime.
|
|
@@ -106,7 +126,7 @@ def create_cli(
|
|
|
106
126
|
|
|
107
127
|
if ext_dir_missing:
|
|
108
128
|
click.echo(
|
|
109
|
-
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.",
|
|
110
130
|
err=True,
|
|
111
131
|
)
|
|
112
132
|
sys.exit(EXIT_CONFIG_NOT_FOUND)
|
|
@@ -138,8 +158,17 @@ def create_cli(
|
|
|
138
158
|
conv_scanner = ConventionScanner()
|
|
139
159
|
conv_modules = conv_scanner.scan(commands_dir)
|
|
140
160
|
if conv_modules:
|
|
141
|
-
|
|
142
|
-
|
|
161
|
+
if binding_path is not None:
|
|
162
|
+
try:
|
|
163
|
+
from apcore_toolkit import DisplayResolver
|
|
164
|
+
|
|
165
|
+
display_resolver = DisplayResolver()
|
|
166
|
+
conv_modules = display_resolver.resolve(conv_modules, binding_path=binding_path)
|
|
167
|
+
logger.info("DisplayResolver: applied binding from %s", binding_path)
|
|
168
|
+
except ImportError:
|
|
169
|
+
logger.warning("DisplayResolver not available in apcore-toolkit")
|
|
170
|
+
writer = RegistryWriter()
|
|
171
|
+
writer.write(conv_modules, registry)
|
|
143
172
|
logger.info("Convention scanner: registered %d modules from %s", len(conv_modules), commands_dir)
|
|
144
173
|
except ImportError:
|
|
145
174
|
logger.warning("apcore-toolkit not installed — convention module scanning unavailable")
|
|
@@ -182,18 +211,33 @@ def create_cli(
|
|
|
182
211
|
default=None,
|
|
183
212
|
help="Path to convention-based commands directory.",
|
|
184
213
|
)
|
|
214
|
+
@click.option(
|
|
215
|
+
"--binding",
|
|
216
|
+
"binding_opt",
|
|
217
|
+
default=None,
|
|
218
|
+
help="Path to binding.yaml file or directory for display resolution.",
|
|
219
|
+
)
|
|
185
220
|
@click.option(
|
|
186
221
|
"--log-level",
|
|
187
222
|
default=None,
|
|
188
223
|
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False),
|
|
189
224
|
help="Log verbosity. Overrides APCORE_CLI_LOGGING_LEVEL and APCORE_LOGGING_LEVEL env vars.",
|
|
190
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
|
+
)
|
|
191
233
|
@click.pass_context
|
|
192
234
|
def cli(
|
|
193
235
|
ctx: click.Context,
|
|
194
236
|
extensions_dir_opt: str | None = None,
|
|
195
237
|
commands_dir_opt: str | None = None,
|
|
238
|
+
binding_opt: str | None = None,
|
|
196
239
|
log_level: str | None = None,
|
|
240
|
+
verbose_help: bool = False,
|
|
197
241
|
) -> None:
|
|
198
242
|
if log_level is not None:
|
|
199
243
|
# basicConfig() is a no-op once handlers exist; set level on the root logger directly.
|
|
@@ -204,6 +248,7 @@ def create_cli(
|
|
|
204
248
|
logging.getLogger("apcore").setLevel(apcore_level)
|
|
205
249
|
ctx.ensure_object(dict)
|
|
206
250
|
ctx.obj["extensions_dir"] = ext_dir
|
|
251
|
+
ctx.obj["verbose_help"] = verbose_help
|
|
207
252
|
|
|
208
253
|
# Register discovery commands
|
|
209
254
|
register_discovery_commands(cli, registry)
|
|
@@ -228,7 +273,8 @@ def main(prog_name: str | None = None) -> None:
|
|
|
228
273
|
"""
|
|
229
274
|
ext_dir = _extract_extensions_dir()
|
|
230
275
|
cmd_dir = _extract_commands_dir()
|
|
231
|
-
|
|
276
|
+
bind_path = _extract_binding_path()
|
|
277
|
+
cli = create_cli(extensions_dir=ext_dir, prog_name=prog_name, commands_dir=cmd_dir, binding_path=bind_path)
|
|
232
278
|
cli(standalone_mode=True)
|
|
233
279
|
|
|
234
280
|
|
|
@@ -27,11 +27,38 @@ if TYPE_CHECKING:
|
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger("apcore_cli.cli")
|
|
29
29
|
|
|
30
|
-
BUILTIN_COMMANDS = ["
|
|
30
|
+
BUILTIN_COMMANDS = ["completion", "describe", "exec", "init", "list", "man"]
|
|
31
31
|
|
|
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(
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
import click
|
|
@@ -59,6 +60,10 @@ def register_init_command(cli: click.Group) -> None:
|
|
|
59
60
|
|
|
60
61
|
MODULE_ID is the module identifier (e.g., ops.deploy, user.create).
|
|
61
62
|
"""
|
|
63
|
+
if output_dir is not None and ".." in Path(output_dir).parts:
|
|
64
|
+
click.echo("Error: Output directory must not contain '..' path components.", err=True)
|
|
65
|
+
sys.exit(2)
|
|
66
|
+
|
|
62
67
|
# Parse module_id into parts
|
|
63
68
|
parts = module_id.rsplit(".", 1)
|
|
64
69
|
if len(parts) == 2:
|
|
@@ -59,7 +59,7 @@ def _generate_bash_completion(prog_name: str) -> str:
|
|
|
59
59
|
"\n"
|
|
60
60
|
" if [[ ${COMP_CWORD} -eq 1 ]]; then\n"
|
|
61
61
|
f" local all_ids=$({groups_and_top_cmd})\n"
|
|
62
|
-
' local builtins="exec list
|
|
62
|
+
' local builtins="completion describe exec init list man"\n'
|
|
63
63
|
' COMPREPLY=( $(compgen -W "${builtins} ${all_ids}" -- ${cur}) )\n'
|
|
64
64
|
" return 0\n"
|
|
65
65
|
" fi\n"
|
|
@@ -124,6 +124,7 @@ def _generate_zsh_completion(prog_name: str) -> str:
|
|
|
124
124
|
" 'list:List available modules'\n"
|
|
125
125
|
" 'describe:Show module metadata and schema'\n"
|
|
126
126
|
" 'completion:Generate shell completion script'\n"
|
|
127
|
+
" 'init:Scaffolding commands'\n"
|
|
127
128
|
" 'man:Generate man page'\n"
|
|
128
129
|
" )\n"
|
|
129
130
|
"\n"
|
|
@@ -208,6 +209,8 @@ def _generate_fish_completion(prog_name: str) -> str:
|
|
|
208
209
|
f'complete -c {quoted} -n "__fish_use_subcommand"'
|
|
209
210
|
' -a completion -d "Generate shell completion script"\n'
|
|
210
211
|
f'complete -c {quoted} -n "__fish_use_subcommand"'
|
|
212
|
+
' -a init -d "Scaffolding commands"\n'
|
|
213
|
+
f'complete -c {quoted} -n "__fish_use_subcommand"'
|
|
211
214
|
' -a man -d "Generate man page"\n'
|
|
212
215
|
f'complete -c {quoted} -n "__fish_use_subcommand"'
|
|
213
216
|
f' -a "({groups_and_top_cmd})" -d "Module group or command"\n'
|
|
@@ -304,6 +307,9 @@ def _generate_man_page(command_name: str, command: click.Command | None, prog_na
|
|
|
304
307
|
"Global apcore logging verbosity. One of: DEBUG, INFO, WARNING, ERROR. "
|
|
305
308
|
"Used as fallback when \\fBAPCORE_CLI_LOGGING_LEVEL\\fR is not set. Default: WARNING."
|
|
306
309
|
)
|
|
310
|
+
sections.append(".TP")
|
|
311
|
+
sections.append("\\fBAPCORE_AUTH_API_KEY\\fR")
|
|
312
|
+
sections.append("API key for authenticating with the apcore registry.")
|
|
307
313
|
|
|
308
314
|
sections.append(".SH EXIT CODES")
|
|
309
315
|
exit_codes = [
|
|
@@ -333,6 +339,187 @@ def _generate_man_page(command_name: str, command: click.Command | None, prog_na
|
|
|
333
339
|
return "\n".join(sections)
|
|
334
340
|
|
|
335
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
|
+
cli.params.append(
|
|
506
|
+
click.Option(
|
|
507
|
+
["--man"],
|
|
508
|
+
is_flag=True,
|
|
509
|
+
default=False,
|
|
510
|
+
hidden=True,
|
|
511
|
+
help="Output man page in roff format (use with --help).",
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Pre-parse: if both --help and --man in argv, generate man page and exit
|
|
516
|
+
args = sys.argv[1:]
|
|
517
|
+
if "--man" in args and ("--help" in args or "-h" in args):
|
|
518
|
+
roff = build_program_man_page(cli, prog_name, version, description, docs_url)
|
|
519
|
+
click.echo(roff)
|
|
520
|
+
sys.exit(0)
|
|
521
|
+
|
|
522
|
+
|
|
336
523
|
def register_shell_commands(cli: click.Group, prog_name: str = "apcore-cli") -> None:
|
|
337
524
|
"""Register completion and man commands."""
|
|
338
525
|
|
|
@@ -380,7 +567,7 @@ def register_shell_commands(cli: click.Group, prog_name: str = "apcore-cli") ->
|
|
|
380
567
|
parent_group = parent.command
|
|
381
568
|
cmd = parent_group.commands.get(command) if isinstance(parent_group, click.Group) else None
|
|
382
569
|
|
|
383
|
-
known_builtins = {"
|
|
570
|
+
known_builtins = {"completion", "describe", "exec", "init", "list", "man"}
|
|
384
571
|
if cmd is None and command not in known_builtins:
|
|
385
572
|
click.echo(f"Error: Unknown command '{command}'.", err=True)
|
|
386
573
|
sys.exit(2)
|
|
@@ -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
|