Skip to content

Commit a3a9add

Browse files
tercelclaude
andcommitted
feat: add DisplayResolver integration, grouped completion, path validation, and sync fixes for v0.3.0
- Integrate DisplayResolver from apcore-toolkit (optional) with --binding option - Add "init" to BUILTIN_COMMANDS, add APCORE_AUTH_API_KEY to man page - Add grouped shell completion with _APCORE_GRP support - Add path traversal validation for --dir in init command - Fix RegistryWriter API call (constructor takes no params) - Bump apcore dependency to >=0.14.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d20f549 commit a3a9add

File tree

6 files changed

+45
-8
lines changed

6 files changed

+45
-8
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
4848
pip install apcore-cli
4949
```
5050

51-
Requires Python 3.11+ and `apcore >= 0.13.0`.
51+
Requires Python 3.11+ and `apcore >= 0.14.0`.
5252

5353
## Quick Start
5454

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "apcore-cli"
7-
version = "0.3.0"
7+
version = "0.3.1"
88
description = "Terminal adapter for apcore — execute AI-Perceivable modules from the command line"
99
readme = "README.md"
1010
license = "Apache-2.0"

src/apcore_cli/__main__.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,16 @@ def _extract_commands_dir(argv: list[str] | None = None) -> str | None:
4545
return _extract_argv_option(argv, "--commands-dir")
4646

4747

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+
4853
def create_cli(
4954
extensions_dir: str | None = None,
5055
prog_name: str | None = None,
5156
commands_dir: str | None = None,
57+
binding_path: str | None = None,
5258
) -> click.Group:
5359
"""Create the CLI application.
5460
@@ -62,6 +68,9 @@ def create_cli(
6268
commands_dir: Directory containing convention-based modules.
6369
When set, scans for plain-function modules and registers
6470
them via ConventionScanner (requires apcore-toolkit).
71+
binding_path: Path to binding.yaml file or directory for display resolution.
72+
When set, applies DisplayResolver to convention-scanned modules
73+
(requires apcore-toolkit).
6574
"""
6675
if prog_name is None:
6776
prog_name = os.path.basename(sys.argv[0]) or "apcore-cli"
@@ -138,8 +147,17 @@ def create_cli(
138147
conv_scanner = ConventionScanner()
139148
conv_modules = conv_scanner.scan(commands_dir)
140149
if conv_modules:
141-
writer = RegistryWriter(registry=registry)
142-
writer.write(conv_modules)
150+
if binding_path is not None:
151+
try:
152+
from apcore_toolkit import DisplayResolver
153+
154+
display_resolver = DisplayResolver()
155+
conv_modules = display_resolver.resolve(conv_modules, binding_path=binding_path)
156+
logger.info("DisplayResolver: applied binding from %s", binding_path)
157+
except ImportError:
158+
logger.warning("DisplayResolver not available in apcore-toolkit")
159+
writer = RegistryWriter()
160+
writer.write(conv_modules, registry)
143161
logger.info("Convention scanner: registered %d modules from %s", len(conv_modules), commands_dir)
144162
except ImportError:
145163
logger.warning("apcore-toolkit not installed — convention module scanning unavailable")
@@ -182,6 +200,12 @@ def create_cli(
182200
default=None,
183201
help="Path to convention-based commands directory.",
184202
)
203+
@click.option(
204+
"--binding",
205+
"binding_opt",
206+
default=None,
207+
help="Path to binding.yaml file or directory for display resolution.",
208+
)
185209
@click.option(
186210
"--log-level",
187211
default=None,
@@ -193,6 +217,7 @@ def cli(
193217
ctx: click.Context,
194218
extensions_dir_opt: str | None = None,
195219
commands_dir_opt: str | None = None,
220+
binding_opt: str | None = None,
196221
log_level: str | None = None,
197222
) -> None:
198223
if log_level is not None:
@@ -228,7 +253,8 @@ def main(prog_name: str | None = None) -> None:
228253
"""
229254
ext_dir = _extract_extensions_dir()
230255
cmd_dir = _extract_commands_dir()
231-
cli = create_cli(extensions_dir=ext_dir, prog_name=prog_name, commands_dir=cmd_dir)
256+
bind_path = _extract_binding_path()
257+
cli = create_cli(extensions_dir=ext_dir, prog_name=prog_name, commands_dir=cmd_dir, binding_path=bind_path)
232258
cli(standalone_mode=True)
233259

234260

src/apcore_cli/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
logger = logging.getLogger("apcore_cli.cli")
2929

30-
BUILTIN_COMMANDS = ["exec", "list", "describe", "completion", "man"]
30+
BUILTIN_COMMANDS = ["completion", "describe", "exec", "init", "list", "man"]
3131

3232
# Module-level audit logger, set during CLI init
3333
_audit_logger: AuditLogger | None = None

src/apcore_cli/init_cmd.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import sys
56
from pathlib import Path
67

78
import click
@@ -59,6 +60,10 @@ def init_module(module_id: str, style: str, output_dir: str | None, description:
5960
6061
MODULE_ID is the module identifier (e.g., ops.deploy, user.create).
6162
"""
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+
6267
# Parse module_id into parts
6368
parts = module_id.rsplit(".", 1)
6469
if len(parts) == 2:

src/apcore_cli/shell.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def _generate_bash_completion(prog_name: str) -> str:
5959
"\n"
6060
" if [[ ${COMP_CWORD} -eq 1 ]]; then\n"
6161
f" local all_ids=$({groups_and_top_cmd})\n"
62-
' local builtins="exec list describe completion man"\n'
62+
' local builtins="completion describe exec init list man"\n'
6363
' COMPREPLY=( $(compgen -W "${builtins} ${all_ids}" -- ${cur}) )\n'
6464
" return 0\n"
6565
" fi\n"
@@ -124,6 +124,7 @@ def _generate_zsh_completion(prog_name: str) -> str:
124124
" 'list:List available modules'\n"
125125
" 'describe:Show module metadata and schema'\n"
126126
" 'completion:Generate shell completion script'\n"
127+
" 'init:Scaffolding commands'\n"
127128
" 'man:Generate man page'\n"
128129
" )\n"
129130
"\n"
@@ -208,6 +209,8 @@ def _generate_fish_completion(prog_name: str) -> str:
208209
f'complete -c {quoted} -n "__fish_use_subcommand"'
209210
' -a completion -d "Generate shell completion script"\n'
210211
f'complete -c {quoted} -n "__fish_use_subcommand"'
212+
' -a init -d "Scaffolding commands"\n'
213+
f'complete -c {quoted} -n "__fish_use_subcommand"'
211214
' -a man -d "Generate man page"\n'
212215
f'complete -c {quoted} -n "__fish_use_subcommand"'
213216
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
304307
"Global apcore logging verbosity. One of: DEBUG, INFO, WARNING, ERROR. "
305308
"Used as fallback when \\fBAPCORE_CLI_LOGGING_LEVEL\\fR is not set. Default: WARNING."
306309
)
310+
sections.append(".TP")
311+
sections.append("\\fBAPCORE_AUTH_API_KEY\\fR")
312+
sections.append("API key for authenticating with the apcore registry.")
307313

308314
sections.append(".SH EXIT CODES")
309315
exit_codes = [
@@ -380,7 +386,7 @@ def man_cmd(ctx: click.Context, command: str) -> None:
380386
parent_group = parent.command
381387
cmd = parent_group.commands.get(command) if isinstance(parent_group, click.Group) else None
382388

383-
known_builtins = {"list", "describe", "completion", "man"}
389+
known_builtins = {"completion", "describe", "exec", "init", "list", "man"}
384390
if cmd is None and command not in known_builtins:
385391
click.echo(f"Error: Unknown command '{command}'.", err=True)
386392
sys.exit(2)

0 commit comments

Comments
 (0)