apcore-cli 0.2.2__tar.gz → 0.3.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.2.2 → apcore_cli-0.3.1}/CHANGELOG.md +71 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/PKG-INFO +40 -7
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/README.md +38 -5
- apcore_cli-0.3.1/commands/ops.py +4 -0
- apcore_cli-0.3.1/planning/grouped-commands.md +235 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/state.json +19 -1
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/pyproject.toml +2 -2
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/__main__.py +87 -11
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/cli.py +252 -10
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/discovery.py +28 -3
- apcore_cli-0.3.1/src/apcore_cli/display_helpers.py +30 -0
- apcore_cli-0.3.1/src/apcore_cli/init_cmd.py +159 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/output.py +71 -9
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/shell.py +110 -5
- apcore_cli-0.3.1/tests/test_cli.py +882 -0
- apcore_cli-0.3.1/tests/test_discovery.py +297 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_e2e.py +10 -0
- apcore_cli-0.3.1/tests/test_init_cmd.py +82 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_output.py +34 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_shell.py +47 -0
- apcore_cli-0.2.2/tests/test_cli.py +0 -442
- apcore_cli-0.2.2/tests/test_discovery.py +0 -148
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/.github/CODEOWNERS +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/.github/copilot-ignore +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/.github/workflows/ci.yml +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/.gitignore +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/.gitmessage +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/.pre-commit-config.yaml +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/CLAUDE.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/examples/extensions/math/add.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/examples/extensions/math/multiply.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/examples/extensions/sysutil/disk.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/examples/extensions/sysutil/env.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/examples/extensions/sysutil/info.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/examples/extensions/text/reverse.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/examples/extensions/text/upper.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/examples/extensions/text/wordcount.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/examples/run_examples.sh +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/approval-gate.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/config-resolver.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/core-dispatcher.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/discovery.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/output-formatter.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/overview.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/schema-parser.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/security-manager.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/planning/shell-integration.md +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/__init__.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/_sandbox_runner.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/approval.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/config.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/ref_resolver.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/schema_parser.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/security/__init__.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/security/audit.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/security/auth.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/security/config_encryptor.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/src/apcore_cli/security/sandbox.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/__init__.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/conftest.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_approval.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_bugfixes.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_config.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_integration.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_ref_resolver.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_schema_parser.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_security/__init__.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_security/test_audit.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_security/test_auth.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_security/test_config_encryptor.py +0 -0
- {apcore_cli-0.2.2 → apcore_cli-0.3.1}/tests/test_security/test_sandbox.py +0 -0
|
@@ -5,6 +5,77 @@ 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.3.1] - 2026-03-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **DisplayResolver integration** — `__main__.py` integrates `DisplayResolver` from `apcore-toolkit` (optional) when `--binding` option is provided; gracefully skipped when not installed.
|
|
13
|
+
- **`init` to `BUILTIN_COMMANDS`** — `init` subcommand is now registered in the builtin commands set.
|
|
14
|
+
- **`APCORE_AUTH_API_KEY` to man page** — environment variable documented in generated roff man page.
|
|
15
|
+
- **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`).
|
|
16
|
+
- **Path traversal validation for `--dir` in `init` command** — rejects paths containing `..` segments to prevent directory escape (`init_cmd.py`).
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **`RegistryWriter` API call** — constructor now called without parameters; fixes `TypeError` introduced by upstream API change.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `apcore` dependency bumped to `>=0.14.0`.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## [0.3.0] - 2026-03-23
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Display overlay routing** (§5.13) — `LazyModuleGroup` now reads `metadata["display"]["cli"]` for alias and description when building the command list and routing `get_command()`. Commands are exposed under their CLI alias instead of raw module_id.
|
|
33
|
+
- `_alias_map`: built from `metadata["display"]["cli"]["alias"]` (with module_id fallback), enabling `apcore-cli alias-name` invocation.
|
|
34
|
+
- `_descriptor_cache`: populated during alias map build to avoid double `registry.get_definition()` calls in `get_command()`.
|
|
35
|
+
- `_alias_map_built` flag only set on successful build, allowing retry after transient registry errors.
|
|
36
|
+
- **Display overlay in JSON output** — `format_module_list(..., "json")` now reads `metadata["display"]["cli"]` for `id`, `description`, and `tags`, consistent with the table output branch.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- `_ERROR_CODE_MAP.get(error_code, 1)`: guarded with `isinstance(error_code, str)` to prevent `None`-key lookup.
|
|
41
|
+
- Runtime companion: `apcore-toolkit >= 0.4.0` enables `DisplayResolver` and `ConventionScanner` (graceful fallback when not installed).
|
|
42
|
+
|
|
43
|
+
### Tests
|
|
44
|
+
|
|
45
|
+
- `TestDisplayOverlayAliasRouting` (6 tests): `list_commands` uses CLI alias, `get_command` by alias, cache hit path, module_id fallback, `build_module_command` alias and description.
|
|
46
|
+
- `test_format_list_json_uses_display_overlay`: JSON output uses display overlay alias/description/tags.
|
|
47
|
+
- `test_format_list_json_falls_back_to_scanner_when_no_overlay`: JSON output falls back to scanner values.
|
|
48
|
+
|
|
49
|
+
### Added (Grouped Commands — FE-09)
|
|
50
|
+
|
|
51
|
+
- **`GroupedModuleGroup(LazyModuleGroup)`** — organizes modules into nested `click.Group` subcommands based on namespace prefixes. Auto-groups by first `.` segment, with `display.cli.group` override from binding.yaml.
|
|
52
|
+
- `_resolve_group()` — 3-tier group resolution: explicit `display.cli.group` > first `.` segment of CLI alias > top-level.
|
|
53
|
+
- `_build_group_map()` — lazy, idempotent group map builder with builtin collision detection and shell-safe group name validation.
|
|
54
|
+
- `format_help()` — collapsed root help with Commands, Modules, and Groups sections (with command counts).
|
|
55
|
+
- **`_LazyGroup(click.Group)`** — nested group that lazily builds subcommands from module descriptors.
|
|
56
|
+
- **`list --flat` flag** — opt-in flat display mode for `list` command; default is now grouped display.
|
|
57
|
+
- **`format_grouped_module_list()`** — Rich table output grouped by namespace.
|
|
58
|
+
- **Updated shell completions** — bash/zsh/fish completion scripts handle two-level group/command structure.
|
|
59
|
+
|
|
60
|
+
### Changed (Grouped Commands)
|
|
61
|
+
|
|
62
|
+
- `create_cli()` now uses `GroupedModuleGroup` instead of `LazyModuleGroup`.
|
|
63
|
+
|
|
64
|
+
### Tests (Grouped Commands)
|
|
65
|
+
|
|
66
|
+
- 48 new tests: `TestResolveGroup` (8+), `TestBuildGroupMap` (5+), `TestGroupedModuleGroupRouting` (7), `TestLazyGroupInner` (4), `TestGroupedHelpDisplay` (5), `TestCreateCliGrouped` (1), `TestGroupedE2E` (5), `TestGroupedDiscovery` (7+), `TestGroupedCompletion` (6).
|
|
67
|
+
|
|
68
|
+
### Added (Convention Module Discovery — §5.14)
|
|
69
|
+
|
|
70
|
+
- **`apcore-cli init module <id>`** — scaffolding command with `--style` (decorator, convention, binding) and `--description` options. Generates module templates in the appropriate directory.
|
|
71
|
+
- **`--commands-dir` CLI option** — path to a convention commands directory. When set, `ConventionScanner` from `apcore-toolkit` scans for plain functions and registers them as modules.
|
|
72
|
+
|
|
73
|
+
### Tests (Convention Module Discovery)
|
|
74
|
+
|
|
75
|
+
- 6 new tests in `tests/test_init_cmd.py` covering all three styles and options.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
8
79
|
## [0.2.2] - 2026-03-22
|
|
9
80
|
|
|
10
81
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apcore-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -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
|
|
@@ -45,7 +45,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
|
|
|
45
45
|
|
|
46
46
|
[](LICENSE)
|
|
47
47
|
[](https://python.org)
|
|
48
|
-
[]()
|
|
49
49
|
|
|
50
50
|
| | |
|
|
51
51
|
|---|---|
|
|
@@ -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
|
|
|
@@ -155,6 +155,37 @@ def cli():
|
|
|
155
155
|
cli()
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
## Adding Custom Commands
|
|
159
|
+
|
|
160
|
+
### Fastest way (30 seconds)
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
apcore-cli init module ops.deploy -d "Deploy to environment"
|
|
164
|
+
# Edit the generated file, add your logic
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Zero-import way (convention discovery)
|
|
168
|
+
|
|
169
|
+
Drop a plain Python function into `commands/`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# commands/deploy.py
|
|
173
|
+
def deploy(env: str, tag: str = "latest") -> dict:
|
|
174
|
+
"""Deploy the app to the given environment."""
|
|
175
|
+
return {"status": "deployed", "env": env}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Then run with `--commands-dir commands/`:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
apcore-cli --commands-dir commands/ deploy deploy --env prod
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The `init module` command supports three styles via `--style`:
|
|
185
|
+
- **convention** (default) — generates a plain Python function in the commands directory
|
|
186
|
+
- **decorator** — generates a `@module`-decorated function in the extensions directory
|
|
187
|
+
- **binding** — generates a `.binding.yaml` file
|
|
188
|
+
|
|
158
189
|
## Integration with Existing Projects
|
|
159
190
|
|
|
160
191
|
### Typical apcore project structure
|
|
@@ -286,6 +317,8 @@ cli:
|
|
|
286
317
|
## Features
|
|
287
318
|
|
|
288
319
|
- **Auto-discovery** -- all modules in the extensions directory are found and exposed as CLI commands
|
|
320
|
+
- **Display overlay** -- `metadata["display"]["cli"]` controls CLI command names, descriptions, and guidance per module (§5.13); set via `binding_path` in `create_cli()` / `fastapi-apcore`
|
|
321
|
+
- **Grouped commands** -- modules with dots in their names are auto-grouped into nested subcommands (`apcore-cli product list` instead of `apcore-cli product.list`); `display.cli.group` in binding.yaml overrides the auto-detected group
|
|
289
322
|
- **Auto-generated flags** -- JSON Schema `input_schema` is converted to `--flag value` CLI options with type validation
|
|
290
323
|
- **Boolean flag pairs** -- `--verbose` / `--no-verbose` from `"type": "boolean"` schema properties
|
|
291
324
|
- **Enum choices** -- `"enum": ["json", "csv"]` becomes `--format json` with Click validation
|
|
@@ -304,8 +337,8 @@ cli:
|
|
|
304
337
|
|
|
305
338
|
| apcore | CLI |
|
|
306
339
|
|--------|-----|
|
|
307
|
-
| `
|
|
308
|
-
| `description` | `--help` text |
|
|
340
|
+
| `metadata["display"]["cli"]["alias"]` or `module_id` | Command name — auto-grouped by first `.` segment (`apcore-cli product get`) |
|
|
341
|
+
| `metadata["display"]["cli"]["description"]` or `description` | `--help` text |
|
|
309
342
|
| `input_schema.properties` | CLI flags (`--a`, `--b`) |
|
|
310
343
|
| `input_schema.required` | Validated post-collection via `jsonschema.validate()` (required fields shown as `[required]` in `--help`) |
|
|
311
344
|
| `annotations.requires_approval` | HITL approval prompt |
|
|
@@ -418,7 +451,7 @@ apcore-cli --extensions-dir ./extensions greet.hello --name Alice --greeting Hi
|
|
|
418
451
|
git clone https://github.com/aiperceivable/apcore-cli-python.git
|
|
419
452
|
cd apcore-cli-python
|
|
420
453
|
pip install -e ".[dev]"
|
|
421
|
-
pytest #
|
|
454
|
+
pytest # 319+ tests
|
|
422
455
|
pytest --cov # with coverage report
|
|
423
456
|
bash examples/run_examples.sh # run all examples
|
|
424
457
|
```
|
|
@@ -8,7 +8,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
|
|
|
8
8
|
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](https://python.org)
|
|
11
|
-
[]()
|
|
12
12
|
|
|
13
13
|
| | |
|
|
14
14
|
|---|---|
|
|
@@ -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
|
|
|
@@ -118,6 +118,37 @@ def cli():
|
|
|
118
118
|
cli()
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
## Adding Custom Commands
|
|
122
|
+
|
|
123
|
+
### Fastest way (30 seconds)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
apcore-cli init module ops.deploy -d "Deploy to environment"
|
|
127
|
+
# Edit the generated file, add your logic
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Zero-import way (convention discovery)
|
|
131
|
+
|
|
132
|
+
Drop a plain Python function into `commands/`:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
# commands/deploy.py
|
|
136
|
+
def deploy(env: str, tag: str = "latest") -> dict:
|
|
137
|
+
"""Deploy the app to the given environment."""
|
|
138
|
+
return {"status": "deployed", "env": env}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Then run with `--commands-dir commands/`:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
apcore-cli --commands-dir commands/ deploy deploy --env prod
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The `init module` command supports three styles via `--style`:
|
|
148
|
+
- **convention** (default) — generates a plain Python function in the commands directory
|
|
149
|
+
- **decorator** — generates a `@module`-decorated function in the extensions directory
|
|
150
|
+
- **binding** — generates a `.binding.yaml` file
|
|
151
|
+
|
|
121
152
|
## Integration with Existing Projects
|
|
122
153
|
|
|
123
154
|
### Typical apcore project structure
|
|
@@ -249,6 +280,8 @@ cli:
|
|
|
249
280
|
## Features
|
|
250
281
|
|
|
251
282
|
- **Auto-discovery** -- all modules in the extensions directory are found and exposed as CLI commands
|
|
283
|
+
- **Display overlay** -- `metadata["display"]["cli"]` controls CLI command names, descriptions, and guidance per module (§5.13); set via `binding_path` in `create_cli()` / `fastapi-apcore`
|
|
284
|
+
- **Grouped commands** -- modules with dots in their names are auto-grouped into nested subcommands (`apcore-cli product list` instead of `apcore-cli product.list`); `display.cli.group` in binding.yaml overrides the auto-detected group
|
|
252
285
|
- **Auto-generated flags** -- JSON Schema `input_schema` is converted to `--flag value` CLI options with type validation
|
|
253
286
|
- **Boolean flag pairs** -- `--verbose` / `--no-verbose` from `"type": "boolean"` schema properties
|
|
254
287
|
- **Enum choices** -- `"enum": ["json", "csv"]` becomes `--format json` with Click validation
|
|
@@ -267,8 +300,8 @@ cli:
|
|
|
267
300
|
|
|
268
301
|
| apcore | CLI |
|
|
269
302
|
|--------|-----|
|
|
270
|
-
| `
|
|
271
|
-
| `description` | `--help` text |
|
|
303
|
+
| `metadata["display"]["cli"]["alias"]` or `module_id` | Command name — auto-grouped by first `.` segment (`apcore-cli product get`) |
|
|
304
|
+
| `metadata["display"]["cli"]["description"]` or `description` | `--help` text |
|
|
272
305
|
| `input_schema.properties` | CLI flags (`--a`, `--b`) |
|
|
273
306
|
| `input_schema.required` | Validated post-collection via `jsonschema.validate()` (required fields shown as `[required]` in `--help`) |
|
|
274
307
|
| `annotations.requires_approval` | HITL approval prompt |
|
|
@@ -381,7 +414,7 @@ apcore-cli --extensions-dir ./extensions greet.hello --name Alice --greeting Hi
|
|
|
381
414
|
git clone https://github.com/aiperceivable/apcore-cli-python.git
|
|
382
415
|
cd apcore-cli-python
|
|
383
416
|
pip install -e ".[dev]"
|
|
384
|
-
pytest #
|
|
417
|
+
pytest # 319+ tests
|
|
385
418
|
pytest --cov # with coverage report
|
|
386
419
|
bash examples/run_examples.sh # run all examples
|
|
387
420
|
```
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Implementation Plan: Grouped CLI Commands
|
|
2
|
+
|
|
3
|
+
**Priority**: P0
|
|
4
|
+
**Source Spec**: `../apcore-cli/docs/features/grouped-commands.md`
|
|
5
|
+
**Module Paths**: `apcore_cli/cli.py`, `apcore_cli/__main__.py`, `apcore_cli/discovery.py`, `apcore_cli/output.py`, `apcore_cli/shell.py`
|
|
6
|
+
**Dependencies**: Core Dispatcher (FE-01), Display Overlay (§5.13)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Tasks
|
|
11
|
+
|
|
12
|
+
### Task 1: `_resolve_group()` — group resolution logic
|
|
13
|
+
**Status**: pending
|
|
14
|
+
**Type**: RED-GREEN-REFACTOR
|
|
15
|
+
|
|
16
|
+
**RED** — Write failing tests in `tests/test_cli.py` (class `TestResolveGroup`):
|
|
17
|
+
- `test_resolve_group_explicit_group`: descriptor with `display.cli.group = "admin"`, `display.cli.alias = "create"` → returns `("admin", "create")`
|
|
18
|
+
- `test_resolve_group_explicit_group_no_alias`: descriptor with `display.cli.group = "admin"` but no alias → returns `("admin", module_id)`
|
|
19
|
+
- `test_resolve_group_opt_out_empty_string`: descriptor with `display.cli.group = ""`, `display.cli.alias = "healthcheck"` → returns `(None, "healthcheck")`
|
|
20
|
+
- `test_resolve_group_auto_from_alias_dot`: descriptor with `display.cli.alias = "user.list"`, no group → returns `("user", "list")`
|
|
21
|
+
- `test_resolve_group_auto_from_module_id_dot`: module_id `"user.create"`, no display overlay → returns `("user", "create")`
|
|
22
|
+
- `test_resolve_group_no_dot_top_level`: module_id `"standalone"`, no display overlay → returns `(None, "standalone")`
|
|
23
|
+
- `test_resolve_group_multi_dot_first_only`: module_id `"a.b.c"`, no display overlay → returns `("a", "b.c")`
|
|
24
|
+
- `test_resolve_group_empty_module_id_warns`: module_id `""` → returns `(None, "")`, WARNING logged
|
|
25
|
+
|
|
26
|
+
**GREEN** — Implement `_resolve_group(module_id, descriptor)` as a static method on `GroupedModuleGroup`:
|
|
27
|
+
1. Read `display = _get_display(descriptor)`, `cli_display = display.get("cli") or {}`.
|
|
28
|
+
2. `explicit_group = cli_display.get("group")`.
|
|
29
|
+
3. If `explicit_group` is non-empty string: return `(explicit_group, cli_display.get("alias") or module_id)`.
|
|
30
|
+
4. If `explicit_group == ""`: return `(None, cli_display.get("alias") or module_id)`.
|
|
31
|
+
5. `cli_name = cli_display.get("alias") or module_id`.
|
|
32
|
+
6. If `"." in cli_name`: `group, _, cmd = cli_name.partition(".")` → return `(group, cmd)`.
|
|
33
|
+
7. Else: return `(None, cli_name)`.
|
|
34
|
+
|
|
35
|
+
**REFACTOR** — None expected.
|
|
36
|
+
|
|
37
|
+
**Verification**: `pytest tests/test_cli.py::TestResolveGroup -v`
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### Task 2: `_build_group_map()` and `GroupedModuleGroup.__init__`
|
|
42
|
+
**Status**: pending
|
|
43
|
+
**Type**: RED-GREEN-REFACTOR
|
|
44
|
+
|
|
45
|
+
**RED** — Write failing tests in `tests/test_cli.py` (class `TestBuildGroupMap`):
|
|
46
|
+
- `test_build_group_map_three_groups`: Registry has `product.list`, `product.get`, `user.create`, `user.list`, `standalone` → `_group_map = {"product": 2 entries, "user": 2 entries}`, `_top_level_modules = {"standalone": 1 entry}`
|
|
47
|
+
- `test_build_group_map_idempotent`: Call twice → second call is a no-op (check registry.list call count = 1)
|
|
48
|
+
- `test_build_group_map_builtin_collision_warns`: Module `list.something` exists → WARNING logged about collision with built-in command `list`
|
|
49
|
+
- `test_build_group_map_failure_allows_retry`: Registry raises on first call → `_group_map_built` stays False, second call retries
|
|
50
|
+
- `test_build_group_map_with_display_overlay_group`: descriptor with `display.cli.group = "admin"`, `display.cli.alias = "create"` → grouped under "admin" with command name "create"
|
|
51
|
+
|
|
52
|
+
**GREEN** — Implement:
|
|
53
|
+
- `GroupedModuleGroup(LazyModuleGroup)` with `__init__` adding `_group_map`, `_top_level_modules`, `_group_cache`, `_group_map_built`.
|
|
54
|
+
- `_build_group_map()`:
|
|
55
|
+
1. Guard: if `_group_map_built`, return.
|
|
56
|
+
2. Call `self._build_alias_map()` (parent method populates descriptor cache).
|
|
57
|
+
3. Iterate `self._registry.list()`, get descriptors from cache, call `_resolve_group`.
|
|
58
|
+
4. Partition into `_group_map` and `_top_level_modules`.
|
|
59
|
+
5. Check group name collisions with `BUILTIN_COMMANDS`, log warnings.
|
|
60
|
+
6. Set `_group_map_built = True` inside try block.
|
|
61
|
+
|
|
62
|
+
**REFACTOR** — None expected.
|
|
63
|
+
|
|
64
|
+
**Verification**: `pytest tests/test_cli.py::TestBuildGroupMap -v`
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### Task 3: `list_commands()` and `get_command()` overrides
|
|
69
|
+
**Status**: pending
|
|
70
|
+
**Type**: RED-GREEN-REFACTOR
|
|
71
|
+
|
|
72
|
+
**RED** — Write failing tests in `tests/test_cli.py` (class `TestGroupedModuleGroupRouting`):
|
|
73
|
+
- `test_list_commands_shows_groups_and_top_level`: With product (2 modules) + user (2 modules) + standalone → returns sorted `[completion, describe, exec, list, man, product, standalone, user]`
|
|
74
|
+
- `test_get_command_returns_lazy_group`: `get_command(ctx, "product")` → returns a `click.Group` instance (the `_LazyGroup`)
|
|
75
|
+
- `test_get_command_returns_top_level_command`: `get_command(ctx, "standalone")` → returns a `click.Command` (not a group)
|
|
76
|
+
- `test_get_command_returns_builtin`: `get_command(ctx, "list")` → returns built-in list command (not a group named "list")
|
|
77
|
+
- `test_get_command_unknown_returns_none`: `get_command(ctx, "nonexistent")` → returns `None`
|
|
78
|
+
- `test_get_command_caches_lazy_group`: Two calls to `get_command(ctx, "product")` → same object
|
|
79
|
+
|
|
80
|
+
**GREEN** — Override `list_commands()` and `get_command()` on `GroupedModuleGroup`:
|
|
81
|
+
- `list_commands`: build group map, return sorted(builtins + group names (excluding collisions) + top-level module names).
|
|
82
|
+
- `get_command`: check builtins → check group cache → check group map (create `_LazyGroup`) → check top-level modules → None.
|
|
83
|
+
|
|
84
|
+
**REFACTOR** — None expected.
|
|
85
|
+
|
|
86
|
+
**Verification**: `pytest tests/test_cli.py::TestGroupedModuleGroupRouting -v`
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### Task 4: `_LazyGroup` — nested group commands
|
|
91
|
+
**Status**: pending
|
|
92
|
+
**Type**: RED-GREEN-REFACTOR
|
|
93
|
+
|
|
94
|
+
**RED** — Write failing tests in `tests/test_cli.py` (class `TestLazyGroup`):
|
|
95
|
+
- `test_lazy_group_list_commands`: `_LazyGroup` with members `{"list": ..., "get": ..., "create": ...}` → `list_commands` returns `["create", "get", "list"]`
|
|
96
|
+
- `test_lazy_group_get_command`: `get_command(ctx, "list")` → returns a `click.Command` built from the descriptor
|
|
97
|
+
- `test_lazy_group_get_command_not_found`: `get_command(ctx, "nonexistent")` → returns `None`
|
|
98
|
+
- `test_lazy_group_caches_commands`: Two calls to `get_command(ctx, "list")` → same object
|
|
99
|
+
- `test_lazy_group_command_execution`: Via `CliRunner`, invoke `apcore-cli product list --category food` → executor called with correct module_id and args
|
|
100
|
+
|
|
101
|
+
**GREEN** — Implement `_LazyGroup(click.Group)`:
|
|
102
|
+
- `__init__`: store `members`, `executor`, `help_text_max_length`, init `_cmd_cache`.
|
|
103
|
+
- `list_commands`: return `sorted(self._members.keys())`.
|
|
104
|
+
- `get_command`: check cache → lookup in members → `build_module_command` → cache → return.
|
|
105
|
+
|
|
106
|
+
**REFACTOR** — None expected.
|
|
107
|
+
|
|
108
|
+
**Verification**: `pytest tests/test_cli.py::TestLazyGroup -v`
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### Task 5: `format_help()` — collapsed root help display
|
|
113
|
+
**Status**: pending
|
|
114
|
+
**Type**: RED-GREEN-REFACTOR
|
|
115
|
+
|
|
116
|
+
**RED** — Write failing tests in `tests/test_cli.py` (class `TestGroupedHelpDisplay`):
|
|
117
|
+
- `test_root_help_shows_groups_section`: Via `CliRunner --help`, output contains "Groups:" section header
|
|
118
|
+
- `test_root_help_shows_group_with_count`: Output contains `product` with "(3 commands)" or similar count
|
|
119
|
+
- `test_root_help_shows_top_level_modules`: Output contains "Modules:" section with standalone command
|
|
120
|
+
- `test_root_help_shows_builtin_commands`: Output contains "Commands:" with exec, list, describe, etc.
|
|
121
|
+
- `test_group_help_shows_commands`: Via `CliRunner`, `apcore-cli product --help` shows individual commands (list, get, create)
|
|
122
|
+
|
|
123
|
+
**GREEN** — Override `format_help()` on `GroupedModuleGroup`:
|
|
124
|
+
1. Build group map.
|
|
125
|
+
2. Use Click's `HelpFormatter` to write sections: Options, Commands (builtins), Modules (top-level), Groups (with counts).
|
|
126
|
+
3. `_LazyGroup` uses default Click help formatting (shows its commands normally).
|
|
127
|
+
|
|
128
|
+
**REFACTOR** — None expected.
|
|
129
|
+
|
|
130
|
+
**Verification**: `pytest tests/test_cli.py::TestGroupedHelpDisplay -v`
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### Task 6: Wire `GroupedModuleGroup` into `create_cli()`
|
|
135
|
+
**Status**: pending
|
|
136
|
+
**Type**: RED-GREEN-REFACTOR
|
|
137
|
+
|
|
138
|
+
**RED** — Write failing tests in `tests/test_cli.py` (class `TestCreateCliGrouped`):
|
|
139
|
+
- `test_create_cli_uses_grouped_module_group`: Call `create_cli(extensions_dir=...)` → returned group is instance of `GroupedModuleGroup`
|
|
140
|
+
|
|
141
|
+
**GREEN** — Change `__main__.py`:
|
|
142
|
+
- Import `GroupedModuleGroup` instead of (or in addition to) `LazyModuleGroup`.
|
|
143
|
+
- Change `cls=LazyModuleGroup` → `cls=GroupedModuleGroup` in the `@click.group()` decorator.
|
|
144
|
+
|
|
145
|
+
**REFACTOR** — None expected.
|
|
146
|
+
|
|
147
|
+
**Verification**: `pytest tests/test_cli.py::TestCreateCliGrouped -v`
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### Task 7: Discovery `list --flat` and `describe group.command`
|
|
152
|
+
**Status**: pending
|
|
153
|
+
**Type**: RED-GREEN-REFACTOR
|
|
154
|
+
|
|
155
|
+
**RED** — Write failing tests in `tests/test_discovery.py` (class `TestGroupedDiscovery`):
|
|
156
|
+
- `test_list_flat_flag`: `apcore-cli list --flat` → output matches flat table (all modules, no grouping)
|
|
157
|
+
- `test_list_default_grouped_display`: `apcore-cli list` with grouped modules → output shows group headers with commands underneath
|
|
158
|
+
- `test_describe_group_dot_command`: `apcore-cli describe product.list` → resolves to the correct module, shows metadata
|
|
159
|
+
- `test_describe_full_module_id`: `apcore-cli describe product.list_products.get` → works with canonical module_id
|
|
160
|
+
|
|
161
|
+
**GREEN** — Modify `discovery.py`:
|
|
162
|
+
- `list_cmd`: add `--flat` flag. When not flat, group modules by their display group and render grouped output.
|
|
163
|
+
- `describe_cmd`: before `validate_module_id`, try to resolve `group.command` notation → scan registry for matching module_id.
|
|
164
|
+
|
|
165
|
+
Modify `output.py`:
|
|
166
|
+
- Add `format_grouped_module_list()` that renders modules grouped under section headers.
|
|
167
|
+
|
|
168
|
+
**REFACTOR** — Ensure `format_module_list()` still works for `--flat` path.
|
|
169
|
+
|
|
170
|
+
**Verification**: `pytest tests/test_discovery.py::TestGroupedDiscovery -v`
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### Task 8: Shell completion for nested groups
|
|
175
|
+
**Status**: pending
|
|
176
|
+
**Type**: RED-GREEN-REFACTOR
|
|
177
|
+
|
|
178
|
+
**RED** — Write failing tests in `tests/test_shell.py` (class `TestGroupedCompletion`):
|
|
179
|
+
- `test_bash_completion_includes_groups`: Generated bash completion for position 1 includes group names
|
|
180
|
+
- `test_bash_completion_nested_commands`: At position 2 after a group name, completes with group's commands
|
|
181
|
+
- `test_zsh_completion_includes_groups`: Generated zsh completion includes group names
|
|
182
|
+
- `test_fish_completion_includes_groups`: Generated fish completion includes group names and nested subcommands
|
|
183
|
+
|
|
184
|
+
**GREEN** — Modify `shell.py`:
|
|
185
|
+
- Update `_generate_bash_completion`: position 1 completes with builtins + group names + top-level modules; position 2 after a group name completes with that group's commands.
|
|
186
|
+
- Update `_generate_zsh_completion` and `_generate_fish_completion` similarly.
|
|
187
|
+
- Accept `registry` parameter (or group instance) to get group/command lists dynamically.
|
|
188
|
+
|
|
189
|
+
**REFACTOR** — Extract common group/command list generation.
|
|
190
|
+
|
|
191
|
+
**Verification**: `pytest tests/test_shell.py::TestGroupedCompletion -v`
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### Task 9: Integration tests — end-to-end grouped invocation
|
|
196
|
+
**Status**: pending
|
|
197
|
+
**Type**: RED-GREEN-REFACTOR
|
|
198
|
+
|
|
199
|
+
**RED** — Write failing tests in `tests/test_cli.py` (class `TestGroupedE2E`):
|
|
200
|
+
- `test_grouped_invocation_product_get`: Via `CliRunner`, `apcore-cli product get --id 123` → executor called with correct module_id
|
|
201
|
+
- `test_single_command_group_works`: `apcore-cli health check` → executor called with `health.check` module_id
|
|
202
|
+
- `test_top_level_module_works`: `apcore-cli standalone --key val` → executor called with `standalone` module_id
|
|
203
|
+
- `test_unknown_group_exits_2`: `apcore-cli nonexistent` → exit code 2
|
|
204
|
+
- `test_unknown_command_in_group_exits_2`: `apcore-cli product nonexistent` → exit code 2
|
|
205
|
+
|
|
206
|
+
**GREEN** — No new production code (this validates the full stack from tasks 1–8).
|
|
207
|
+
|
|
208
|
+
**REFACTOR** — Fix any issues found during integration.
|
|
209
|
+
|
|
210
|
+
**Verification**: `pytest tests/test_cli.py::TestGroupedE2E -v`
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Implementation Order
|
|
215
|
+
|
|
216
|
+
Execute tasks sequentially: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9
|
|
217
|
+
|
|
218
|
+
Tasks 1–4 are the core grouped commands engine (cli.py only).
|
|
219
|
+
Task 5 is the help display.
|
|
220
|
+
Task 6 wires it in.
|
|
221
|
+
Tasks 7–8 update downstream features.
|
|
222
|
+
Task 9 is the integration test sweep.
|
|
223
|
+
|
|
224
|
+
## Files Modified
|
|
225
|
+
|
|
226
|
+
| File | Tasks | Changes |
|
|
227
|
+
|------|-------|---------|
|
|
228
|
+
| `src/apcore_cli/cli.py` | 1–5 | Add `GroupedModuleGroup`, `_LazyGroup`, `_resolve_group`, `_build_group_map`, `format_help` |
|
|
229
|
+
| `src/apcore_cli/__main__.py` | 6 | Change `cls=LazyModuleGroup` → `cls=GroupedModuleGroup` |
|
|
230
|
+
| `src/apcore_cli/discovery.py` | 7 | Add `--flat` flag, `group.command` resolution in `describe` |
|
|
231
|
+
| `src/apcore_cli/output.py` | 7 | Add `format_grouped_module_list()` |
|
|
232
|
+
| `src/apcore_cli/shell.py` | 8 | Update completion generators for two-level groups |
|
|
233
|
+
| `tests/test_cli.py` | 1–6, 9 | ~30 new tests across 7 test classes |
|
|
234
|
+
| `tests/test_discovery.py` | 7 | ~4 new tests |
|
|
235
|
+
| `tests/test_shell.py` | 8 | ~4 new tests |
|
|
@@ -50,6 +50,23 @@
|
|
|
50
50
|
"plan": "shell-integration.md",
|
|
51
51
|
"priority": "P2",
|
|
52
52
|
"depends_on": ["core-dispatcher", "schema-parser"]
|
|
53
|
+
},
|
|
54
|
+
"grouped-commands": {
|
|
55
|
+
"status": "completed",
|
|
56
|
+
"plan": "grouped-commands.md",
|
|
57
|
+
"priority": "P0",
|
|
58
|
+
"depends_on": ["core-dispatcher", "discovery", "output-formatter", "shell-integration"],
|
|
59
|
+
"tasks": {
|
|
60
|
+
"task-1-resolve-group": "completed",
|
|
61
|
+
"task-2-build-group-map": "completed",
|
|
62
|
+
"task-3-list-get-command": "completed",
|
|
63
|
+
"task-4-lazy-group": "completed",
|
|
64
|
+
"task-5-format-help": "completed",
|
|
65
|
+
"task-6-wire-create-cli": "completed",
|
|
66
|
+
"task-7-discovery-grouped": "completed",
|
|
67
|
+
"task-8-shell-completion": "completed",
|
|
68
|
+
"task-9-e2e-integration": "completed"
|
|
69
|
+
}
|
|
53
70
|
}
|
|
54
71
|
},
|
|
55
72
|
"implementation_order": [
|
|
@@ -60,6 +77,7 @@
|
|
|
60
77
|
"discovery",
|
|
61
78
|
"approval-gate",
|
|
62
79
|
"security-manager",
|
|
63
|
-
"shell-integration"
|
|
80
|
+
"shell-integration",
|
|
81
|
+
"grouped-commands"
|
|
64
82
|
]
|
|
65
83
|
}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apcore-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
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",
|