apcore-cli 0.4.1__tar.gz → 0.5.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.
Files changed (70) hide show
  1. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/PKG-INFO +2 -2
  2. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/pyproject.toml +2 -2
  3. apcore_cli-0.5.0/src/apcore_cli/__init__.py +27 -0
  4. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/__main__.py +4 -1
  5. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/cli.py +14 -0
  6. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/config.py +21 -3
  7. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/shell.py +45 -1
  8. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_config.py +44 -0
  9. apcore_cli-0.4.1/src/apcore_cli/__init__.py +0 -9
  10. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/.github/CODEOWNERS +0 -0
  11. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/.github/copilot-ignore +0 -0
  12. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/.github/workflows/ci.yml +0 -0
  13. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/.gitignore +0 -0
  14. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/.gitmessage +0 -0
  15. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/.pre-commit-config.yaml +0 -0
  16. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/CHANGELOG.md +0 -0
  17. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/CLAUDE.md +0 -0
  18. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/README.md +0 -0
  19. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/commands/ops.py +0 -0
  20. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/examples/extensions/math/add.py +0 -0
  21. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/examples/extensions/math/multiply.py +0 -0
  22. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/examples/extensions/sysutil/disk.py +0 -0
  23. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/examples/extensions/sysutil/env.py +0 -0
  24. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/examples/extensions/sysutil/info.py +0 -0
  25. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/examples/extensions/text/reverse.py +0 -0
  26. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/examples/extensions/text/upper.py +0 -0
  27. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/examples/extensions/text/wordcount.py +0 -0
  28. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/examples/run_examples.sh +0 -0
  29. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/approval-gate.md +0 -0
  30. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/config-resolver.md +0 -0
  31. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/core-dispatcher.md +0 -0
  32. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/discovery.md +0 -0
  33. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/grouped-commands.md +0 -0
  34. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/output-formatter.md +0 -0
  35. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/overview.md +0 -0
  36. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/schema-parser.md +0 -0
  37. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/security-manager.md +0 -0
  38. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/shell-integration.md +0 -0
  39. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/planning/state.json +0 -0
  40. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/_sandbox_runner.py +0 -0
  41. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/approval.py +0 -0
  42. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/discovery.py +0 -0
  43. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/display_helpers.py +0 -0
  44. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/init_cmd.py +0 -0
  45. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/output.py +0 -0
  46. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/ref_resolver.py +0 -0
  47. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/schema_parser.py +0 -0
  48. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/security/__init__.py +0 -0
  49. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/security/audit.py +0 -0
  50. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/security/auth.py +0 -0
  51. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/security/config_encryptor.py +0 -0
  52. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/src/apcore_cli/security/sandbox.py +0 -0
  53. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/__init__.py +0 -0
  54. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/conftest.py +0 -0
  55. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_approval.py +0 -0
  56. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_bugfixes.py +0 -0
  57. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_cli.py +0 -0
  58. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_discovery.py +0 -0
  59. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_e2e.py +0 -0
  60. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_init_cmd.py +0 -0
  61. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_integration.py +0 -0
  62. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_output.py +0 -0
  63. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_ref_resolver.py +0 -0
  64. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_schema_parser.py +0 -0
  65. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_security/__init__.py +0 -0
  66. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_security/test_audit.py +0 -0
  67. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_security/test_auth.py +0 -0
  68. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_security/test_config_encryptor.py +0 -0
  69. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_security/test_sandbox.py +0 -0
  70. {apcore_cli-0.4.1 → apcore_cli-0.5.0}/tests/test_shell.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apcore-cli
3
- Version: 0.4.1
3
+ Version: 0.5.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.14.0
23
+ Requires-Dist: apcore>=0.15.1
24
24
  Requires-Dist: click>=8.1
25
25
  Requires-Dist: cryptography>=41.0
26
26
  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.4.1"
7
+ version = "0.5.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.14.0",
29
+ "apcore>=0.15.1",
30
30
  "click>=8.1",
31
31
  "jsonschema>=4.20",
32
32
  "rich>=13.0",
@@ -0,0 +1,27 @@
1
+ """apcore-cli: CLI adapter for the apcore module ecosystem."""
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version as _get_version
5
+
6
+ try:
7
+ __version__ = _get_version("apcore-cli")
8
+ except PackageNotFoundError:
9
+ __version__ = "unknown"
10
+
11
+ # Config Bus namespace registration (apcore >= 0.15.0)
12
+ try:
13
+ from apcore import Config
14
+
15
+ Config.register_namespace(
16
+ name="apcore-cli",
17
+ schema=None,
18
+ env_prefix="APCORE_CLI",
19
+ defaults={
20
+ "stdin_buffer_limit": 10_485_760,
21
+ "auto_approve": False,
22
+ "help_text_max_length": 1000,
23
+ "logging_level": "WARNING",
24
+ },
25
+ )
26
+ except (ImportError, AttributeError):
27
+ pass # apcore < 0.15.0 or not installed
@@ -13,7 +13,7 @@ from apcore_cli.cli import GroupedModuleGroup, set_audit_logger, set_verbose_hel
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
16
- from apcore_cli.shell import register_shell_commands
16
+ from apcore_cli.shell import configure_man_help, register_shell_commands
17
17
 
18
18
  logger = logging.getLogger("apcore_cli")
19
19
 
@@ -256,6 +256,9 @@ def create_cli(
256
256
  # Register shell integration commands
257
257
  register_shell_commands(cli, prog_name=prog_name)
258
258
 
259
+ # Register --help --man support
260
+ configure_man_help(cli, prog_name, __version__)
261
+
259
262
  # Register init scaffolding command
260
263
  from apcore_cli.init_cmd import register_init_command
261
264
 
@@ -343,6 +343,13 @@ class GroupedModuleGroup(LazyModuleGroup):
343
343
  with formatter.section("Groups"):
344
344
  formatter.write_dl(group_records)
345
345
 
346
+ # Footer hints for discoverability
347
+ formatter.write_paragraph()
348
+ formatter.write(
349
+ "Use --help --verbose to show all options (including built-in apcore options).\n"
350
+ "Use --help --man to display a formatted man page."
351
+ )
352
+
346
353
 
347
354
  # Error code mapping from apcore error codes to CLI exit codes
348
355
  _ERROR_CODE_MAP = {
@@ -359,6 +366,13 @@ _ERROR_CODE_MAP = {
359
366
  "MODULE_EXECUTE_ERROR": 1,
360
367
  "MODULE_TIMEOUT": 1,
361
368
  "ACL_DENIED": 77,
369
+ # Config Bus errors (apcore >= 0.15.0)
370
+ "CONFIG_NAMESPACE_RESERVED": 78,
371
+ "CONFIG_NAMESPACE_DUPLICATE": 78,
372
+ "CONFIG_ENV_PREFIX_CONFLICT": 78,
373
+ "CONFIG_MOUNT_ERROR": 66,
374
+ "CONFIG_BIND_ERROR": 65,
375
+ "ERROR_FORMATTER_DUPLICATE": 70,
362
376
  }
363
377
 
364
378
 
@@ -16,6 +16,15 @@ class ConfigResolver:
16
16
  CLI flag > Environment variable > Config file > Default.
17
17
  """
18
18
 
19
+ # Namespace key -> legacy key mapping for backward compatibility
20
+ _NAMESPACE_TO_LEGACY: dict[str, str] = {
21
+ "apcore-cli.stdin_buffer_limit": "cli.stdin_buffer_limit",
22
+ "apcore-cli.auto_approve": "cli.auto_approve",
23
+ "apcore-cli.help_text_max_length": "cli.help_text_max_length",
24
+ "apcore-cli.logging_level": "logging.level",
25
+ }
26
+ _LEGACY_TO_NAMESPACE: dict[str, str] = {v: k for k, v in _NAMESPACE_TO_LEGACY.items()}
27
+
19
28
  DEFAULTS: dict[str, Any] = {
20
29
  "extensions.root": "./extensions",
21
30
  "logging.level": "WARNING",
@@ -23,6 +32,11 @@ class ConfigResolver:
23
32
  "cli.stdin_buffer_limit": 10_485_760, # 10 MB
24
33
  "cli.auto_approve": False,
25
34
  "cli.help_text_max_length": 1000,
35
+ # Namespace-mode aliases (apcore >= 0.15.0 Config Bus)
36
+ "apcore-cli.stdin_buffer_limit": 10_485_760,
37
+ "apcore-cli.auto_approve": False,
38
+ "apcore-cli.help_text_max_length": 1000,
39
+ "apcore-cli.logging_level": "WARNING",
26
40
  }
27
41
 
28
42
  def __init__(
@@ -53,9 +67,13 @@ class ConfigResolver:
53
67
  if env_value is not None and env_value != "":
54
68
  return env_value
55
69
 
56
- # Tier 3: Config file
57
- if self._config_file is not None and key in self._config_file:
58
- return self._config_file[key]
70
+ # Tier 3: Config file (try both namespace and legacy keys)
71
+ if self._config_file is not None:
72
+ if key in self._config_file:
73
+ return self._config_file[key]
74
+ alt_key = self._NAMESPACE_TO_LEGACY.get(key) or self._LEGACY_TO_NAMESPACE.get(key)
75
+ if alt_key and alt_key in self._config_file:
76
+ return self._config_file[alt_key]
59
77
 
60
78
  # Tier 4: Default
61
79
  return self.DEFAULTS.get(key)
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  import re
6
7
  import shlex
8
+ import subprocess
7
9
  import sys
8
10
  from datetime import date
9
11
 
@@ -480,6 +482,48 @@ def build_program_man_page(
480
482
  return "\n".join(s)
481
483
 
482
484
 
485
+ def _render_man_page(roff: str) -> None:
486
+ """Render a roff man page to stdout.
487
+
488
+ When stdout is a TTY, attempts to render through mandoc or groff and
489
+ pipe through a pager for formatted display. When stdout is not a TTY
490
+ (piped or redirected), outputs raw roff for file redirection.
491
+ """
492
+ roff_bytes = roff.encode()
493
+ if not sys.stdout.isatty():
494
+ sys.stdout.write(roff)
495
+ return
496
+
497
+ # Try mandoc first (macOS/BSD), then groff
498
+ renderers = [
499
+ ["mandoc", "-a"],
500
+ ["groff", "-man", "-Tutf8"],
501
+ ]
502
+ for cmd in renderers:
503
+ try:
504
+ result = subprocess.run(
505
+ cmd,
506
+ input=roff_bytes,
507
+ capture_output=True,
508
+ )
509
+ except FileNotFoundError:
510
+ continue
511
+ if result.returncode == 0 and result.stdout:
512
+ pager = os.environ.get("PAGER", "less")
513
+ try:
514
+ subprocess.run(
515
+ [pager, "-R"],
516
+ input=result.stdout,
517
+ )
518
+ return
519
+ except FileNotFoundError:
520
+ # Pager not found — fall through to raw output
521
+ break
522
+
523
+ # Fallback: raw roff output
524
+ sys.stdout.write(roff)
525
+
526
+
483
527
  def configure_man_help(
484
528
  cli: click.Group,
485
529
  prog_name: str,
@@ -520,7 +564,7 @@ def configure_man_help(
520
564
  args = sys.argv[1:]
521
565
  if "--man" in args and ("--help" in args or "-h" in args):
522
566
  roff = build_program_man_page(cli, prog_name, version, description, docs_url)
523
- click.echo(roff)
567
+ _render_man_page(roff)
524
568
  sys.exit(0)
525
569
 
526
570
 
@@ -140,3 +140,47 @@ class TestConfigFileLoading:
140
140
  resolver = ConfigResolver(config_path="/nonexistent/apcore.yaml")
141
141
  result = resolver._flatten_dict({"a": {"b": {"c": "deep_value"}}})
142
142
  assert result == {"a.b.c": "deep_value"}
143
+
144
+
145
+ class TestNamespaceAwareConfigResolution:
146
+ """Config Bus namespace ↔ legacy key fallback (apcore >= 0.15.0)."""
147
+
148
+ def test_defaults_contain_namespace_keys(self):
149
+ resolver = ConfigResolver()
150
+ for key in [
151
+ "apcore-cli.stdin_buffer_limit",
152
+ "apcore-cli.auto_approve",
153
+ "apcore-cli.help_text_max_length",
154
+ "apcore-cli.logging_level",
155
+ ]:
156
+ assert key in resolver.DEFAULTS, f"Missing namespace default: {key}"
157
+
158
+ def test_resolve_namespace_key_from_legacy_config_file(self, tmp_path, clean_env):
159
+ """Querying 'apcore-cli.stdin_buffer_limit' finds 'cli.stdin_buffer_limit' in file."""
160
+ config_file = tmp_path / "apcore.yaml"
161
+ config_file.write_text("cli:\n stdin_buffer_limit: 5242880\n")
162
+ resolver = ConfigResolver(config_path=str(config_file))
163
+ result = resolver.resolve("apcore-cli.stdin_buffer_limit")
164
+ assert result == 5242880
165
+
166
+ def test_resolve_legacy_key_from_namespace_config_file(self, tmp_path, clean_env):
167
+ """Querying 'cli.auto_approve' finds 'apcore-cli.auto_approve' in file."""
168
+ config_file = tmp_path / "apcore.yaml"
169
+ config_file.write_text("apcore-cli:\n auto_approve: true\n")
170
+ resolver = ConfigResolver(config_path=str(config_file))
171
+ result = resolver.resolve("cli.auto_approve")
172
+ assert result is True
173
+
174
+ def test_direct_key_takes_precedence_over_alternate(self, tmp_path, clean_env):
175
+ """When both keys exist in file, the directly-queried key wins."""
176
+ config_file = tmp_path / "apcore.yaml"
177
+ config_file.write_text("cli:\n help_text_max_length: 500\n" "apcore-cli:\n help_text_max_length: 2000\n")
178
+ resolver = ConfigResolver(config_path=str(config_file))
179
+ assert resolver.resolve("cli.help_text_max_length") == 500
180
+ assert resolver.resolve("apcore-cli.help_text_max_length") == 2000
181
+
182
+ def test_namespace_mapping_is_bidirectional(self):
183
+ resolver = ConfigResolver()
184
+ assert len(resolver._NAMESPACE_TO_LEGACY) == len(resolver._LEGACY_TO_NAMESPACE)
185
+ for ns_key, legacy_key in resolver._NAMESPACE_TO_LEGACY.items():
186
+ assert resolver._LEGACY_TO_NAMESPACE[legacy_key] == ns_key
@@ -1,9 +0,0 @@
1
- """apcore-cli: CLI adapter for the apcore module ecosystem."""
2
-
3
- from importlib.metadata import PackageNotFoundError
4
- from importlib.metadata import version as _get_version
5
-
6
- try:
7
- __version__ = _get_version("apcore-cli")
8
- except PackageNotFoundError:
9
- __version__ = "unknown"
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