apcore-cli 0.9.0__tar.gz → 0.10.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.
Files changed (101) hide show
  1. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/CHANGELOG.md +40 -0
  2. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/PKG-INFO +3 -3
  3. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/pyproject.toml +3 -3
  4. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/config.py +9 -6
  5. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/factory.py +22 -28
  6. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/init_cmd.py +6 -0
  7. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/output.py +1 -17
  8. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/shell.py +0 -43
  9. apcore_cli-0.10.1/tests/shell_test_utils.py +49 -0
  10. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_e2e.py +2 -1
  11. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_integration.py +2 -1
  12. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_shell.py +2 -1
  13. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_toolkit_integration.py +30 -67
  14. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/.github/CODEOWNERS +0 -0
  15. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/.github/copilot-ignore +0 -0
  16. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/.github/workflows/ci.yml +0 -0
  17. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/.gitignore +0 -0
  18. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/.gitmessage +0 -0
  19. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/.pre-commit-config.yaml +0 -0
  20. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/CLAUDE.md +0 -0
  21. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/LICENSE +0 -0
  22. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/Makefile +0 -0
  23. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/README.md +0 -0
  24. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/README.md +0 -0
  25. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/extensions/math/add.py +0 -0
  26. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/extensions/math/multiply.py +0 -0
  27. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/extensions/sysutil/disk.py +0 -0
  28. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/extensions/sysutil/env.py +0 -0
  29. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/extensions/sysutil/info.py +0 -0
  30. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/extensions/text/reverse.py +0 -0
  31. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/extensions/text/upper.py +0 -0
  32. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/extensions/text/wordcount.py +0 -0
  33. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/examples/run_examples.sh +0 -0
  34. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/approval-gate.md +0 -0
  35. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/config-resolver.md +0 -0
  36. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/core-dispatcher.md +0 -0
  37. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/discovery.md +0 -0
  38. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/exposure-filtering.md +0 -0
  39. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/grouped-commands.md +0 -0
  40. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/output-formatter.md +0 -0
  41. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/overview.md +0 -0
  42. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/schema-parser.md +0 -0
  43. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/security-manager.md +0 -0
  44. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/shell-integration.md +0 -0
  45. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/planning/state.json +0 -0
  46. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/__init__.py +0 -0
  47. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/__main__.py +0 -0
  48. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/_sandbox_runner.py +0 -0
  49. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/approval.py +0 -0
  50. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/builtin_group.py +0 -0
  51. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/cli.py +0 -0
  52. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/discovery.py +0 -0
  53. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/display_helpers.py +0 -0
  54. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/exit_codes.py +0 -0
  55. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/exposure.py +0 -0
  56. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/ref_resolver.py +0 -0
  57. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/schema_parser.py +0 -0
  58. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/security/__init__.py +0 -0
  59. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/security/audit.py +0 -0
  60. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/security/auth.py +0 -0
  61. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/security/config_encryptor.py +0 -0
  62. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/security/sandbox.py +0 -0
  63. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/strategy.py +0 -0
  64. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/system_cmd.py +0 -0
  65. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/system_usage.py +0 -0
  66. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/src/apcore_cli/validate.py +0 -0
  67. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/__init__.py +0 -0
  68. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/conformance/__init__.py +0 -0
  69. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/conformance/test_apcli_visibility.py +0 -0
  70. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/conformance/test_snake_case_kwargs.py +0 -0
  71. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/conftest.py +0 -0
  72. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_apcli_integration.py +0 -0
  73. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_approval.py +0 -0
  74. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_bugfixes.py +0 -0
  75. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_builtin_group.py +0 -0
  76. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_cli.py +0 -0
  77. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_config.py +0 -0
  78. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_discovery.py +0 -0
  79. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_discovery_fe13.py +0 -0
  80. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_display_helpers.py +0 -0
  81. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_exit_codes.py +0 -0
  82. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_exposure.py +0 -0
  83. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_factory_fe13.py +0 -0
  84. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_init_cmd.py +0 -0
  85. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_list_command_filters.py +0 -0
  86. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_output.py +0 -0
  87. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_output_format_exec.py +0 -0
  88. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_output_format_markdown_skill.py +0 -0
  89. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_public_api.py +0 -0
  90. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_ref_resolver.py +0 -0
  91. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_sandbox_runner.py +0 -0
  92. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_schema_parser.py +0 -0
  93. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_security/__init__.py +0 -0
  94. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_security/test_audit.py +0 -0
  95. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_security/test_auth.py +0 -0
  96. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_security/test_config_encryptor.py +0 -0
  97. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_security/test_sandbox.py +0 -0
  98. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_strategy.py +0 -0
  99. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_system_cmd.py +0 -0
  100. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_system_usage.py +0 -0
  101. {apcore_cli-0.9.0 → apcore_cli-0.10.1}/tests/test_validate.py +0 -0
@@ -5,6 +5,46 @@ 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.10.1] - 2026-06-15
9
+
10
+ ### Changed
11
+
12
+ - **Required runtime bumped to apcore 0.24.0 and apcore-toolkit 0.8.1.** Dependency
13
+ floors in `pyproject.toml` raised from `apcore>=0.22.0` / `apcore-toolkit>=0.8.0`
14
+ to `apcore>=0.24.0` / `apcore-toolkit>=0.8.1`, tracking the aligned apcore 0.24.0
15
+ and apcore-toolkit 0.8.1 releases. **No source changes** — the full test suite
16
+ passes unchanged.
17
+
18
+ The apcore 0.22.0 → 0.24.0 delta does not touch any surface the CLI consumes:
19
+ - **Per-instance `ToggleState` isolation (#71)** — the CLI never constructs
20
+ `ToggleState`/`APCore` nor calls the free `is_module_disabled()`; module
21
+ toggling is delegated to the `system.control.toggle_feature` module via
22
+ `Executor.call()`.
23
+ - **Default AI error-recovery metadata (#70)** and **error `details` snake_case
24
+ key alignment (A-D-019)** — the CLI reads error fields (`details`, `suggestion`,
25
+ `ai_guidance`, `retryable`, `user_fixable`) via `getattr()` and passes `details`
26
+ through verbatim, so it is agnostic to both new defaults and inner-key casing.
27
+ - **`Registry.list()` / `get_definition()` / `Executor.call()` / `call_with_trace()`
28
+ / `set_approval_handler()`** signatures are unchanged across the delta; descriptor
29
+ fields are read defensively with fallbacks.
30
+ - Out of scope and unused by the CLI: `CircuitBreakerMiddleware`, `A2ASubscriber`,
31
+ DLQ/`original_event`, `apcore.Config.validate()`, `Context.create()`, redaction
32
+ utilities, `EventEmitter`.
33
+
34
+ ## [0.10.0] - 2026-05-18
35
+
36
+ ### Changed — BREAKING
37
+
38
+ - **Removed graceful ImportError fallbacks for `apcore` and `apcore-toolkit` (resolves 6.2).** Both packages are declared as required runtime dependencies in `pyproject.toml` (`apcore>=0.21.0`, `apcore-toolkit>=0.7.0`) — the prior `try: from apcore_toolkit import X; except ImportError: logger.warning(...); return` pattern in `factory.py` and the `try: from apcore import Config; except (ImportError, AttributeError): return False` pattern in `config.py` were self-contradictory: the dep was hard-required by the package manifest but soft-degraded at runtime. The fallbacks have been removed; missing or too-old `apcore` / `apcore-toolkit` now fails fast at import time with `ModuleNotFoundError`, matching the manifest contract.
39
+ - Sites cleaned up: `factory.py:581-616` (DisplayResolver/RegistryWriter/ConventionScanner/BindingLoader), `output.py:144-146/270-272` (format_module/format_modules), `config.py:32-35` (apcore.Config).
40
+ - Toolkit symbols are now imported once at the top of `factory.py` and `output.py`.
41
+ - The `_TOOLKIT_MISSING_HINT` constant in `output.py` is removed (no longer reachable).
42
+ - **Tests updated**: two tests that asserted the removed fallback behaviour (`test_toolkit_missing_logs_warning_and_returns`, `test_binding_loader_missing_warns_but_continues`) are deleted; the import-time failure mode is covered by Python's standard `ModuleNotFoundError`. Remaining `_apply_toolkit_integration` tests now patch `apcore_cli.factory.{BindingLoader,DisplayResolver,RegistryWriter,ConventionScanner}` instead of `apcore_toolkit.X` (standard "patch where the name is used" Python mock convention now that the imports are static).
43
+
44
+ ### Migration
45
+
46
+ - If your environment previously relied on the soft-degrade behaviour (apcore-cli running with `apcore-toolkit` uninstalled), install the package: `pip install apcore-toolkit>=0.7.0`. With the manifest already declaring it required, this should already be satisfied by any standard `pip install apcore-cli` invocation.
47
+
8
48
  ## [0.9.0] - 2026-05-13
9
49
 
10
50
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apcore-cli
3
- Version: 0.9.0
3
+ Version: 0.10.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
@@ -21,8 +21,8 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Requires-Python: >=3.11
24
- Requires-Dist: apcore-toolkit>=0.7.0
25
- Requires-Dist: apcore>=0.21.0
24
+ Requires-Dist: apcore-toolkit>=0.8.1
25
+ Requires-Dist: apcore>=0.24.0
26
26
  Requires-Dist: click>=8.1
27
27
  Requires-Dist: cryptography>=41.0
28
28
  Requires-Dist: jsonschema>=4.20
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "apcore-cli"
7
- version = "0.9.0"
7
+ version = "0.10.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,8 +26,8 @@ classifiers = [
26
26
  "Environment :: Console",
27
27
  ]
28
28
  dependencies = [
29
- "apcore>=0.21.0",
30
- "apcore-toolkit>=0.7.0",
29
+ "apcore>=0.24.0",
30
+ "apcore-toolkit>=0.8.1",
31
31
  "click>=8.1",
32
32
  "jsonschema>=4.20",
33
33
  "rich>=13.0",
@@ -26,13 +26,16 @@ def register_config_namespace() -> bool:
26
26
  Public helper mirroring apcore-cli-typescript's ``registerConfigNamespace``
27
27
  and apcore-cli-rust's equivalent registration call site so embedders can
28
28
  invoke registration explicitly. Returns ``True`` when registration
29
- succeeded, ``False`` when apcore is missing or too old (which silently
30
- skips registration — matching the prior inline behaviour).
29
+ succeeded.
30
+
31
+ ``apcore`` is a required runtime dependency
32
+ (``pyproject.toml`` pins ``apcore>=0.21.0``) so the import is performed
33
+ locally rather than guarded — a missing or too-old apcore is a hard
34
+ install error, not a silent no-op. The prior graceful-fallback path
35
+ was removed in the 6.2 fix.
31
36
  """
32
- try:
33
- from apcore import Config
34
- except (ImportError, AttributeError):
35
- return False
37
+ from apcore import Config
38
+
36
39
  Config.register_namespace(
37
40
  name="apcore-cli",
38
41
  schema=None,
@@ -18,6 +18,14 @@ from typing import Any
18
18
 
19
19
  import click
20
20
 
21
+ # apcore-toolkit is a REQUIRED runtime dependency (pyproject.toml pins
22
+ # `apcore-toolkit>=0.7.0`). Static import — no ImportError fallback. If
23
+ # toolkit is missing the import fails at module load time with a clear
24
+ # ModuleNotFoundError, matching the pyproject contract. See CHANGELOG for
25
+ # the 6.2 fix that removed the prior graceful-fallback path.
26
+ from apcore_toolkit import BindingLoader, DisplayResolver, RegistryWriter
27
+ from apcore_toolkit.convention_scanner import ConventionScanner
28
+
21
29
  from apcore_cli.builtin_group import ApcliGroup
22
30
  from apcore_cli.cli import GroupedModuleGroup, set_audit_logger, set_verbose_help
23
31
  from apcore_cli.config import ConfigResolver
@@ -571,25 +579,18 @@ def _apply_toolkit_integration(
571
579
  single registration path ensures ``--allowed-prefix`` protection
572
580
  applies to both sources consistently.
573
581
 
574
- Silently no-op if ``apcore-toolkit`` is not installed; individual
575
- optional features (``BindingLoader`` in toolkit < 0.5) degrade to
576
- a WARNING and are skipped.
582
+ Toolkit symbols are imported statically at module top — the package
583
+ is a required runtime dep (``pyproject.toml`` pins
584
+ ``apcore-toolkit>=0.7.0``). Operational failures inside scanner /
585
+ loader / writer calls still degrade to a WARNING.
577
586
  """
578
587
  if commands_dir is None and binding_path is None:
579
588
  return
580
589
 
581
- try:
582
- from apcore_toolkit import DisplayResolver, RegistryWriter
583
- except ImportError:
584
- logger.warning("apcore-toolkit not installed — toolkit features unavailable")
585
- return
586
-
587
590
  scanned: list[Any] = []
588
591
 
589
592
  if commands_dir is not None:
590
593
  try:
591
- from apcore_toolkit.convention_scanner import ConventionScanner
592
-
593
594
  scanner = ConventionScanner()
594
595
  scanned.extend(scanner.scan(commands_dir))
595
596
  except Exception as e:
@@ -597,23 +598,16 @@ def _apply_toolkit_integration(
597
598
 
598
599
  if binding_path is not None:
599
600
  try:
600
- from apcore_toolkit import BindingLoader
601
- except ImportError:
602
- # apcore-toolkit < 0.5.0 — silently skip the overlay (parity with
603
- # TS main.ts:761 "apcore-toolkit < 0.5.0 (no BindingLoader)").
604
- logger.warning("apcore-toolkit < 0.5.0: BindingLoader unavailable, --binding skipped")
605
- else:
606
- try:
607
- loader = BindingLoader()
608
- loaded = loader.load(binding_path)
609
- scanned.extend(loaded)
610
- logger.info(
611
- "BindingLoader: parsed %d module(s) from %s",
612
- len(loaded),
613
- binding_path,
614
- )
615
- except Exception as e:
616
- logger.warning("BindingLoader failed on '%s': %s", binding_path, e)
601
+ loader = BindingLoader()
602
+ loaded = loader.load(binding_path)
603
+ scanned.extend(loaded)
604
+ logger.info(
605
+ "BindingLoader: parsed %d module(s) from %s",
606
+ len(loaded),
607
+ binding_path,
608
+ )
609
+ except Exception as e:
610
+ logger.warning("BindingLoader failed on '%s': %s", binding_path, e)
617
611
 
618
612
  if not scanned:
619
613
  return
@@ -7,6 +7,12 @@ from pathlib import Path
7
7
 
8
8
  import click
9
9
 
10
+ # Templates emit `# TODO: implement` placeholders into the GENERATED user
11
+ # module. These are user-facing scaffolding — not SDK source-level TODOs —
12
+ # and intentionally match the parity templates in
13
+ # `../apcore-cli-typescript/src/init-cmd.ts` and
14
+ # `../apcore-cli-rust/src/init_cmd.rs`. Audit tools that grep for "TODO"
15
+ # should treat occurrences inside the template literals below as expected.
10
16
  _DECORATOR_TEMPLATE = '''"""Module: {module_id}"""
11
17
 
12
18
  from apcore import module
@@ -8,7 +8,7 @@ import sys
8
8
  from typing import TYPE_CHECKING, Any
9
9
 
10
10
  import click
11
- from apcore_toolkit import format_csv, format_jsonl
11
+ from apcore_toolkit import format_csv, format_jsonl, format_module, format_modules
12
12
  from rich.console import Console
13
13
  from rich.panel import Panel
14
14
  from rich.syntax import Syntax
@@ -23,12 +23,6 @@ if TYPE_CHECKING:
23
23
  logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
- _TOOLKIT_MISSING_HINT = (
27
- "The 'markdown' and 'skill' output formats require the apcore-toolkit "
28
- "extra. Install with: pip install 'apcore-cli[toolkit]'"
29
- )
30
-
31
-
32
26
  def _to_rows_for_tabular(value: Any) -> list[dict] | None:
33
27
  """Coerce an exec result into the row-shape expected by the toolkit's
34
28
  tabular formatters (csv / jsonl). Returns ``None`` for shapes that don't
@@ -140,11 +134,6 @@ def format_module_list(
140
134
 
141
135
  Console().print(table)
142
136
  elif format in ("markdown", "skill"):
143
- try:
144
- from apcore_toolkit import format_modules
145
- except ImportError as exc: # pragma: no cover - exercised via integration only
146
- raise click.ClickException(_TOOLKIT_MISSING_HINT) from exc
147
-
148
137
  scanned = [_descriptor_to_scanned(m) for m in modules]
149
138
  click.echo(format_modules(scanned, style=format, display=True))
150
139
  elif format in ("json", "csv", "yaml", "jsonl"):
@@ -266,11 +255,6 @@ def format_module_detail(module_def: ModuleDescriptor, format: str) -> None:
266
255
  click.echo(f"\nTags: {', '.join(tags)}")
267
256
 
268
257
  elif format in ("markdown", "skill"):
269
- try:
270
- from apcore_toolkit import format_module
271
- except ImportError as exc: # pragma: no cover - exercised via integration only
272
- raise click.ClickException(_TOOLKIT_MISSING_HINT) from exc
273
-
274
258
  click.echo(format_module(_descriptor_to_scanned(module_def), style=format, display=True))
275
259
 
276
260
  elif format == "json":
@@ -597,46 +597,3 @@ def register_completion_command(apcli_group: click.Group, prog_name: str = "apco
597
597
  click.echo(generators[shell]())
598
598
 
599
599
  _ = completion_cmd
600
-
601
-
602
- def register_shell_commands(cli: click.Group, prog_name: str = "apcore-cli") -> None:
603
- """Legacy wrapper — registers ``completion`` and ``man`` on the given group.
604
-
605
- FE-13 canonical wiring attaches ``completion`` to the ``apcli`` group via
606
- :func:`register_completion_command`; ``man`` remains at the root per
607
- spec §4.1 (meta-commands stay at root). This shim preserves the pre-v0.7
608
- flat shape for existing tests.
609
- """
610
- register_completion_command(cli, prog_name=prog_name)
611
-
612
- @cli.command("man")
613
- @click.argument("command")
614
- @click.pass_context
615
- def man_cmd(ctx: click.Context, command: str) -> None:
616
- """Generate a roff man page for COMMAND and print it to stdout.
617
-
618
- \b
619
- View immediately:
620
- PROG man list | man -
621
- PROG man describe | col -bx | less
622
-
623
- Install system-wide:
624
- PROG man list > /usr/local/share/man/man1/PROG-list.1
625
- mandb # (Linux) or /usr/libexec/makewhatis # (macOS)
626
- """
627
- parent = ctx.parent
628
- if parent is None:
629
- click.echo(f"Error: Unknown command '{command}'.", err=True)
630
- sys.exit(2)
631
-
632
- resolved_prog = ctx.find_root().info_name or prog_name
633
- parent_group = parent.command
634
- cmd = parent_group.commands.get(command) if isinstance(parent_group, click.Group) else None
635
-
636
- known_builtins = {"completion", "describe", "exec", "init", "list", "man"}
637
- if cmd is None and command not in known_builtins:
638
- click.echo(f"Error: Unknown command '{command}'.", err=True)
639
- sys.exit(2)
640
-
641
- roff = _generate_man_page(command, cmd, resolved_prog)
642
- click.echo(roff)
@@ -0,0 +1,49 @@
1
+ """Test-only helper that builds the pre-v0.7 *flat* CLI shape.
2
+
3
+ The production CLI (``factory.create_cli``) registers ``completion`` under the
4
+ ``apcli`` builtin group and exposes ``man`` as a root ``--man`` overlay
5
+ (``configure_man_help``). This helper instead reproduces the legacy flat shape —
6
+ ``completion`` and ``man`` as root subcommands — purely so the completion
7
+ generators and the man-page builder can be exercised through a CLI in tests.
8
+
9
+ Relocated from ``apcore_cli.shell.register_shell_commands`` (audit D9-001): the
10
+ shim had zero production callers and was removed from the shipped package to
11
+ match apcore-cli-rust and apcore-cli-typescript, both of which dropped the
12
+ equivalent wrapper in v0.7.0 / FE-13. Keeping it here preserves the behavioral
13
+ coverage without shipping dead code.
14
+ """
15
+
16
+ import sys
17
+
18
+ import click
19
+
20
+ from apcore_cli.shell import _generate_man_page, register_completion_command
21
+
22
+
23
+ def register_shell_commands(cli: click.Group, prog_name: str = "apcore-cli") -> None:
24
+ """Attach flat ``completion`` and ``man`` subcommands to *cli* (test-only)."""
25
+ register_completion_command(cli, prog_name=prog_name)
26
+
27
+ @cli.command("man")
28
+ @click.argument("command")
29
+ @click.pass_context
30
+ def man_cmd(ctx: click.Context, command: str) -> None:
31
+ """Generate a roff man page for COMMAND and print it to stdout."""
32
+ parent = ctx.parent
33
+ if parent is None:
34
+ click.echo(f"Error: Unknown command '{command}'.", err=True)
35
+ sys.exit(2)
36
+
37
+ resolved_prog = ctx.find_root().info_name or prog_name
38
+ parent_group = parent.command
39
+ cmd = parent_group.commands.get(command) if isinstance(parent_group, click.Group) else None
40
+
41
+ known_builtins = {"completion", "describe", "exec", "init", "list", "man"}
42
+ if cmd is None and command not in known_builtins:
43
+ click.echo(f"Error: Unknown command '{command}'.", err=True)
44
+ sys.exit(2)
45
+
46
+ roff = _generate_man_page(command, cmd, resolved_prog)
47
+ click.echo(roff)
48
+
49
+ _ = man_cmd
@@ -15,7 +15,8 @@ from click.testing import CliRunner
15
15
 
16
16
  from apcore_cli.cli import LazyModuleGroup, build_module_command, set_audit_logger
17
17
  from apcore_cli.discovery import register_discovery_commands
18
- from apcore_cli.shell import register_shell_commands
18
+
19
+ from .shell_test_utils import register_shell_commands
19
20
 
20
21
  # ---------------------------------------------------------------------------
21
22
  # Real apcore module definitions
@@ -9,7 +9,8 @@ from click.testing import CliRunner
9
9
 
10
10
  from apcore_cli.cli import LazyModuleGroup, build_module_command, set_audit_logger
11
11
  from apcore_cli.discovery import register_discovery_commands
12
- from apcore_cli.shell import register_shell_commands
12
+
13
+ from .shell_test_utils import register_shell_commands
13
14
 
14
15
 
15
16
  def _make_module_def(
@@ -9,9 +9,10 @@ from apcore_cli.shell import (
9
9
  _generate_zsh_completion,
10
10
  build_program_man_page,
11
11
  configure_man_help,
12
- register_shell_commands,
13
12
  )
14
13
 
14
+ from .shell_test_utils import register_shell_commands
15
+
15
16
  _PROG = "apcore-cli"
16
17
 
17
18
 
@@ -61,9 +61,9 @@ class TestApplyToolkitIntegration:
61
61
  fake_writer_cls = MagicMock(return_value=fake_writer)
62
62
 
63
63
  with (
64
- patch("apcore_toolkit.BindingLoader", fake_loader_cls),
65
- patch("apcore_toolkit.DisplayResolver", fake_resolver_cls),
66
- patch("apcore_toolkit.RegistryWriter", fake_writer_cls),
64
+ patch("apcore_cli.factory.BindingLoader", fake_loader_cls),
65
+ patch("apcore_cli.factory.DisplayResolver", fake_resolver_cls),
66
+ patch("apcore_cli.factory.RegistryWriter", fake_writer_cls),
67
67
  ):
68
68
  registry = MagicMock()
69
69
  _apply_toolkit_integration(
@@ -92,9 +92,9 @@ class TestApplyToolkitIntegration:
92
92
  fake_writer.write.return_value = []
93
93
 
94
94
  with (
95
- patch("apcore_toolkit.BindingLoader", MagicMock(return_value=fake_loader)),
96
- patch("apcore_toolkit.DisplayResolver", MagicMock(return_value=MagicMock(resolve=lambda m, **kw: m))),
97
- patch("apcore_toolkit.RegistryWriter", MagicMock(return_value=fake_writer)),
95
+ patch("apcore_cli.factory.BindingLoader", MagicMock(return_value=fake_loader)),
96
+ patch("apcore_cli.factory.DisplayResolver", MagicMock(return_value=MagicMock(resolve=lambda m, **kw: m))),
97
+ patch("apcore_cli.factory.RegistryWriter", MagicMock(return_value=fake_writer)),
98
98
  ):
99
99
  _apply_toolkit_integration(
100
100
  MagicMock(),
@@ -121,10 +121,10 @@ class TestApplyToolkitIntegration:
121
121
  fake_writer.write.return_value = []
122
122
 
123
123
  with (
124
- patch("apcore_toolkit.convention_scanner.ConventionScanner", fake_scanner_cls),
125
- patch("apcore_toolkit.BindingLoader", MagicMock(return_value=fake_loader)),
126
- patch("apcore_toolkit.DisplayResolver", MagicMock(return_value=MagicMock(resolve=lambda m, **kw: m))),
127
- patch("apcore_toolkit.RegistryWriter", MagicMock(return_value=fake_writer)),
124
+ patch("apcore_cli.factory.ConventionScanner", fake_scanner_cls),
125
+ patch("apcore_cli.factory.BindingLoader", MagicMock(return_value=fake_loader)),
126
+ patch("apcore_cli.factory.DisplayResolver", MagicMock(return_value=MagicMock(resolve=lambda m, **kw: m))),
127
+ patch("apcore_cli.factory.RegistryWriter", MagicMock(return_value=fake_writer)),
128
128
  ):
129
129
  _apply_toolkit_integration(
130
130
  MagicMock(),
@@ -138,54 +138,17 @@ class TestApplyToolkitIntegration:
138
138
  assert scan_module in written
139
139
  assert bind_module in written
140
140
 
141
- def test_toolkit_missing_logs_warning_and_returns(self, caplog):
142
- import builtins
143
- import logging as pylogging
144
-
145
- real_import = builtins.__import__
146
-
147
- def fail_toolkit(name, *args, **kw):
148
- if name == "apcore_toolkit":
149
- raise ImportError("no toolkit")
150
- return real_import(name, *args, **kw)
151
-
152
- with (
153
- caplog.at_level(pylogging.WARNING, logger="apcore_cli"),
154
- patch("builtins.__import__", side_effect=fail_toolkit),
155
- ):
156
- _apply_toolkit_integration(
157
- MagicMock(),
158
- commands_dir="/cmds",
159
- binding_path=None,
160
- allowed_prefixes=None,
161
- )
162
- assert "apcore-toolkit not installed" in caplog.text
163
-
164
- def test_binding_loader_missing_warns_but_continues(self, caplog):
165
- """toolkit < 0.5.0 lacks BindingLoader — match TS parity: warn + skip."""
166
- import builtins
167
- import logging as pylogging
168
-
169
- real_import = builtins.__import__
170
-
171
- def partial_import(name, globals_=None, locals_=None, fromlist=(), level=0):
172
- # Simulate toolkit installed but missing BindingLoader: when the
173
- # factory tries `from apcore_toolkit import BindingLoader`, raise.
174
- if name == "apcore_toolkit" and "BindingLoader" in (fromlist or ()):
175
- raise ImportError("BindingLoader not in 0.4.x")
176
- return real_import(name, globals_, locals_, fromlist, level)
177
-
178
- with (
179
- caplog.at_level(pylogging.WARNING, logger="apcore_cli"),
180
- patch("builtins.__import__", side_effect=partial_import),
181
- ):
182
- _apply_toolkit_integration(
183
- MagicMock(),
184
- commands_dir=None,
185
- binding_path="/bindings",
186
- allowed_prefixes=None,
187
- )
188
- assert "BindingLoader unavailable" in caplog.text
141
+ # Removed in 0.10.0 (resolves 6.2): the prior graceful-fallback paths
142
+ # for "apcore-toolkit missing" and "apcore-toolkit < 0.5.0 (no
143
+ # BindingLoader)" no longer exist — apcore-toolkit >= 0.7.0 is a hard
144
+ # runtime dependency declared in pyproject.toml, so a missing or
145
+ # too-old toolkit is a ModuleNotFoundError at import time, not a
146
+ # silently logged WARNING. The two former tests
147
+ # (`test_toolkit_missing_logs_warning_and_returns` and
148
+ # `test_binding_loader_missing_warns_but_continues`) are intentionally
149
+ # deleted — they asserted behaviour that the fix removed. The
150
+ # import-time failure mode is already covered by Python's standard
151
+ # ModuleNotFoundError contract and does not need a bespoke test.
189
152
 
190
153
  def test_binding_loader_failure_is_soft(self, caplog):
191
154
  """BindingLoader.load() raises → WARN, don't crash create_cli."""
@@ -199,9 +162,9 @@ class TestApplyToolkitIntegration:
199
162
 
200
163
  with (
201
164
  caplog.at_level(pylogging.WARNING, logger="apcore_cli"),
202
- patch("apcore_toolkit.BindingLoader", MagicMock(return_value=fake_loader)),
203
- patch("apcore_toolkit.DisplayResolver", MagicMock()),
204
- patch("apcore_toolkit.RegistryWriter", MagicMock()),
165
+ patch("apcore_cli.factory.BindingLoader", MagicMock(return_value=fake_loader)),
166
+ patch("apcore_cli.factory.DisplayResolver", MagicMock()),
167
+ patch("apcore_cli.factory.RegistryWriter", MagicMock()),
205
168
  ):
206
169
  _apply_toolkit_integration(
207
170
  MagicMock(),
@@ -219,9 +182,9 @@ class TestApplyToolkitIntegration:
219
182
  fake_writer = MagicMock()
220
183
 
221
184
  with (
222
- patch("apcore_toolkit.BindingLoader", MagicMock(return_value=fake_loader)),
223
- patch("apcore_toolkit.DisplayResolver", MagicMock()),
224
- patch("apcore_toolkit.RegistryWriter", MagicMock(return_value=fake_writer)),
185
+ patch("apcore_cli.factory.BindingLoader", MagicMock(return_value=fake_loader)),
186
+ patch("apcore_cli.factory.DisplayResolver", MagicMock()),
187
+ patch("apcore_cli.factory.RegistryWriter", MagicMock(return_value=fake_writer)),
225
188
  ):
226
189
  _apply_toolkit_integration(
227
190
  MagicMock(),
@@ -320,9 +283,9 @@ class TestBindingPathStandaloneE2E:
320
283
  fake_writer.write.return_value = []
321
284
 
322
285
  with (
323
- patch("apcore_toolkit.BindingLoader", MagicMock(return_value=fake_loader)),
324
- patch("apcore_toolkit.DisplayResolver", MagicMock(return_value=MagicMock(resolve=lambda m, **kw: m))),
325
- patch("apcore_toolkit.RegistryWriter", MagicMock(return_value=fake_writer)),
286
+ patch("apcore_cli.factory.BindingLoader", MagicMock(return_value=fake_loader)),
287
+ patch("apcore_cli.factory.DisplayResolver", MagicMock(return_value=MagicMock(resolve=lambda m, **kw: m))),
288
+ patch("apcore_cli.factory.RegistryWriter", MagicMock(return_value=fake_writer)),
326
289
  ):
327
290
  cli = create_cli(
328
291
  extensions_dir=str(tmp_path),
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes