acli-spec 0.1.4__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.
- acli_spec-0.1.4/.gitignore +38 -0
- acli_spec-0.1.4/PKG-INFO +82 -0
- acli_spec-0.1.4/README.md +52 -0
- acli_spec-0.1.4/pyproject.toml +117 -0
- acli_spec-0.1.4/src/acli/__init__.py +36 -0
- acli_spec-0.1.4/src/acli/app.py +179 -0
- acli_spec-0.1.4/src/acli/cli.py +365 -0
- acli_spec-0.1.4/src/acli/cli_folder.py +96 -0
- acli_spec-0.1.4/src/acli/command.py +61 -0
- acli_spec-0.1.4/src/acli/errors.py +84 -0
- acli_spec-0.1.4/src/acli/exit_codes.py +35 -0
- acli_spec-0.1.4/src/acli/introspect.py +147 -0
- acli_spec-0.1.4/src/acli/output.py +120 -0
- acli_spec-0.1.4/src/acli/py.typed +0 -0
- acli_spec-0.1.4/src/acli/skill.py +169 -0
- acli_spec-0.1.4/src/acli/templates/main.py.tpl +34 -0
- acli_spec-0.1.4/src/acli/templates/pyproject.toml.tpl +13 -0
- acli_spec-0.1.4/tests/__init__.py +0 -0
- acli_spec-0.1.4/tests/test_app.py +299 -0
- acli_spec-0.1.4/tests/test_cli.py +418 -0
- acli_spec-0.1.4/tests/test_cli_folder.py +130 -0
- acli_spec-0.1.4/tests/test_command.py +80 -0
- acli_spec-0.1.4/tests/test_errors.py +56 -0
- acli_spec-0.1.4/tests/test_exit_codes.py +35 -0
- acli_spec-0.1.4/tests/test_introspect.py +102 -0
- acli_spec-0.1.4/tests/test_output.py +187 -0
- acli_spec-0.1.4/tests/test_skill.py +163 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
.eggs/
|
|
10
|
+
*.whl
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# Testing / coverage
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
|
|
23
|
+
# IDE
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
*.swp
|
|
27
|
+
*.swo
|
|
28
|
+
*~
|
|
29
|
+
|
|
30
|
+
# OS
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
33
|
+
|
|
34
|
+
# ACLI generated
|
|
35
|
+
.cli/
|
|
36
|
+
|
|
37
|
+
# MkDocs
|
|
38
|
+
site/
|
acli_spec-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: acli-spec
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Python SDK for the ACLI (Agent-friendly CLI) specification
|
|
5
|
+
Project-URL: Homepage, https://github.com/alpibrusl/acli
|
|
6
|
+
Project-URL: Documentation, https://alpibrusl.github.io/acli
|
|
7
|
+
Project-URL: Repository, https://github.com/alpibrusl/acli
|
|
8
|
+
Project-URL: Issues, https://github.com/alpibrusl/acli/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/alpibrusl/acli/blob/main/CHANGELOG.md
|
|
10
|
+
Author: ACLI Contributors
|
|
11
|
+
License-Expression: EUPL-1.2
|
|
12
|
+
Keywords: agent,ai,cli,specification
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: typer>=0.9.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# acli-spec
|
|
32
|
+
|
|
33
|
+
Python SDK for the [ACLI (Agent-friendly CLI) specification](../../ACLI_SPEC.md).
|
|
34
|
+
|
|
35
|
+
Build CLI tools that AI agents can discover, learn, and use autonomously.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install acli-spec
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from acli import ACLIApp, acli_command, OutputFormat
|
|
48
|
+
import typer
|
|
49
|
+
|
|
50
|
+
app = ACLIApp(name="myapp", version="1.0.0")
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
@acli_command(
|
|
54
|
+
examples=[
|
|
55
|
+
("Run a task", "myapp run --file task.yaml"),
|
|
56
|
+
("Dry-run a task", "myapp run --file task.yaml --dry-run"),
|
|
57
|
+
],
|
|
58
|
+
idempotent=False,
|
|
59
|
+
)
|
|
60
|
+
def run(
|
|
61
|
+
file: Path = typer.Option(..., help="Path to task file. type:path"),
|
|
62
|
+
dry_run: bool = typer.Option(False, help="Preview without executing."),
|
|
63
|
+
output: OutputFormat = typer.Option(OutputFormat.text, help="Output format."),
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Execute a task from a YAML file."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
app.run()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## What you get automatically
|
|
73
|
+
|
|
74
|
+
- `introspect` command with full command tree as JSON
|
|
75
|
+
- `.cli/` folder generation (README, examples, schemas)
|
|
76
|
+
- JSON error envelope on `--output json`
|
|
77
|
+
- Semantic exit codes (0-9)
|
|
78
|
+
- `--version` with semver output
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
[EUPL-1.2](../../LICENSE)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# acli-spec
|
|
2
|
+
|
|
3
|
+
Python SDK for the [ACLI (Agent-friendly CLI) specification](../../ACLI_SPEC.md).
|
|
4
|
+
|
|
5
|
+
Build CLI tools that AI agents can discover, learn, and use autonomously.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install acli-spec
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from acli import ACLIApp, acli_command, OutputFormat
|
|
18
|
+
import typer
|
|
19
|
+
|
|
20
|
+
app = ACLIApp(name="myapp", version="1.0.0")
|
|
21
|
+
|
|
22
|
+
@app.command()
|
|
23
|
+
@acli_command(
|
|
24
|
+
examples=[
|
|
25
|
+
("Run a task", "myapp run --file task.yaml"),
|
|
26
|
+
("Dry-run a task", "myapp run --file task.yaml --dry-run"),
|
|
27
|
+
],
|
|
28
|
+
idempotent=False,
|
|
29
|
+
)
|
|
30
|
+
def run(
|
|
31
|
+
file: Path = typer.Option(..., help="Path to task file. type:path"),
|
|
32
|
+
dry_run: bool = typer.Option(False, help="Preview without executing."),
|
|
33
|
+
output: OutputFormat = typer.Option(OutputFormat.text, help="Output format."),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Execute a task from a YAML file."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
app.run()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## What you get automatically
|
|
43
|
+
|
|
44
|
+
- `introspect` command with full command tree as JSON
|
|
45
|
+
- `.cli/` folder generation (README, examples, schemas)
|
|
46
|
+
- JSON error envelope on `--output json`
|
|
47
|
+
- Semantic exit codes (0-9)
|
|
48
|
+
- `--version` with semver output
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
[EUPL-1.2](../../LICENSE)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "acli-spec"
|
|
7
|
+
version = "0.1.4"
|
|
8
|
+
description = "Python SDK for the ACLI (Agent-friendly CLI) specification"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "EUPL-1.2"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "ACLI Contributors" }]
|
|
13
|
+
keywords = ["cli", "agent", "ai", "specification"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"typer>=0.9.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
acli = "acli.cli:main"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/alpibrusl/acli"
|
|
34
|
+
Documentation = "https://alpibrusl.github.io/acli"
|
|
35
|
+
Repository = "https://github.com/alpibrusl/acli"
|
|
36
|
+
Issues = "https://github.com/alpibrusl/acli/issues"
|
|
37
|
+
Changelog = "https://github.com/alpibrusl/acli/blob/main/CHANGELOG.md"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8.0",
|
|
42
|
+
"pytest-cov>=5.0",
|
|
43
|
+
"ruff>=0.4",
|
|
44
|
+
"mypy>=1.10",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/acli"]
|
|
49
|
+
|
|
50
|
+
# ── Ruff ──────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
target-version = "py310"
|
|
54
|
+
line-length = 99
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = [
|
|
58
|
+
"F", # pyflakes
|
|
59
|
+
"E", "W", # pycodestyle
|
|
60
|
+
"I", # isort
|
|
61
|
+
"N", # pep8-naming
|
|
62
|
+
"UP", # pyupgrade
|
|
63
|
+
"B", # flake8-bugbear
|
|
64
|
+
"A", # flake8-builtins
|
|
65
|
+
"SIM", # flake8-simplify
|
|
66
|
+
"T20", # flake8-print
|
|
67
|
+
"PT", # flake8-pytest-style
|
|
68
|
+
"RUF", # ruff-specific
|
|
69
|
+
"S", # flake8-bandit (security)
|
|
70
|
+
"C4", # flake8-comprehensions
|
|
71
|
+
"DTZ", # flake8-datetimez
|
|
72
|
+
"PIE", # flake8-pie
|
|
73
|
+
"RET", # flake8-return
|
|
74
|
+
"TCH", # flake8-type-checking
|
|
75
|
+
"ARG", # flake8-unused-arguments
|
|
76
|
+
"PLC", "PLE", "PLW", # pylint
|
|
77
|
+
]
|
|
78
|
+
ignore = [
|
|
79
|
+
"S101", # allow assert in tests
|
|
80
|
+
"BLE001", # allow broad exception catches (needed for introspect resilience)
|
|
81
|
+
"B008", # typer.Option() in defaults is standard Typer pattern
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint.per-file-ignores]
|
|
85
|
+
"tests/**/*.py" = ["ARG001", "ARG002", "ARG005", "S", "T20", "PLC0415", "TC003", "PT017", "SIM105"]
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint.isort]
|
|
88
|
+
known-first-party = ["acli"]
|
|
89
|
+
|
|
90
|
+
# ── Mypy ──────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
[tool.mypy]
|
|
93
|
+
python_version = "3.10"
|
|
94
|
+
strict = true
|
|
95
|
+
warn_return_any = true
|
|
96
|
+
warn_unused_configs = true
|
|
97
|
+
disallow_untyped_defs = true
|
|
98
|
+
|
|
99
|
+
[[tool.mypy.overrides]]
|
|
100
|
+
module = ["typer", "typer.*"]
|
|
101
|
+
ignore_missing_imports = true
|
|
102
|
+
|
|
103
|
+
[[tool.mypy.overrides]]
|
|
104
|
+
module = ["acli.cli"]
|
|
105
|
+
disallow_untyped_decorators = false
|
|
106
|
+
|
|
107
|
+
# ── Pytest ────────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
[tool.pytest.ini_options]
|
|
110
|
+
testpaths = ["tests"]
|
|
111
|
+
addopts = [
|
|
112
|
+
"--strict-markers",
|
|
113
|
+
"--cov=acli",
|
|
114
|
+
"--cov-report=term-missing",
|
|
115
|
+
"--cov-report=html:htmlcov",
|
|
116
|
+
"--cov-fail-under=90",
|
|
117
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""ACLI — Agent-friendly CLI Python SDK."""
|
|
2
|
+
|
|
3
|
+
from acli.app import ACLIApp
|
|
4
|
+
from acli.command import CommandExample, CommandMeta, acli_command
|
|
5
|
+
from acli.errors import (
|
|
6
|
+
ACLIError,
|
|
7
|
+
ConflictError,
|
|
8
|
+
InvalidArgsError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
PreconditionError,
|
|
11
|
+
suggest_flag,
|
|
12
|
+
)
|
|
13
|
+
from acli.exit_codes import ExitCode
|
|
14
|
+
from acli.output import OutputFormat, emit, error_envelope, success_envelope
|
|
15
|
+
from acli.skill import generate_skill
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ACLIApp",
|
|
19
|
+
"ACLIError",
|
|
20
|
+
"CommandExample",
|
|
21
|
+
"CommandMeta",
|
|
22
|
+
"ConflictError",
|
|
23
|
+
"ExitCode",
|
|
24
|
+
"InvalidArgsError",
|
|
25
|
+
"NotFoundError",
|
|
26
|
+
"OutputFormat",
|
|
27
|
+
"PreconditionError",
|
|
28
|
+
"acli_command",
|
|
29
|
+
"emit",
|
|
30
|
+
"error_envelope",
|
|
31
|
+
"generate_skill",
|
|
32
|
+
"success_envelope",
|
|
33
|
+
"suggest_flag",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.4"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""ACLIApp — the main application class wrapping Typer per ACLI spec §8."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from acli.cli_folder import generate_cli_folder, needs_update
|
|
13
|
+
from acli.errors import ACLIError
|
|
14
|
+
from acli.exit_codes import ExitCode
|
|
15
|
+
from acli.introspect import build_command_tree
|
|
16
|
+
from acli.output import OutputFormat, emit, error_envelope, success_envelope
|
|
17
|
+
from acli.skill import generate_skill
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ACLIApp:
|
|
21
|
+
"""ACLI-compliant application wrapper around Typer.
|
|
22
|
+
|
|
23
|
+
Automatically registers the ``introspect`` command, enforces JSON error
|
|
24
|
+
envelopes, and generates the ``.cli/`` folder.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
name: str,
|
|
30
|
+
version: str,
|
|
31
|
+
*,
|
|
32
|
+
cli_dir: Path | None = None,
|
|
33
|
+
**typer_kwargs: Any,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.name = name
|
|
36
|
+
self.version = version
|
|
37
|
+
self.cli_dir = cli_dir
|
|
38
|
+
self._typer = typer.Typer(name=name, help=typer_kwargs.pop("help", None), **typer_kwargs)
|
|
39
|
+
self._register_introspect()
|
|
40
|
+
self._register_version()
|
|
41
|
+
self._register_skill()
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def typer_app(self) -> typer.Typer:
|
|
45
|
+
"""Access the underlying Typer instance."""
|
|
46
|
+
return self._typer
|
|
47
|
+
|
|
48
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
def command(self, *args: Any, **kwargs: Any) -> Any:
|
|
51
|
+
"""Register a command — proxy to typer.command()."""
|
|
52
|
+
return self._typer.command(*args, **kwargs)
|
|
53
|
+
|
|
54
|
+
def add_typer(self, *args: Any, **kwargs: Any) -> None:
|
|
55
|
+
"""Add a sub-group — proxy to typer.add_typer()."""
|
|
56
|
+
self._typer.add_typer(*args, **kwargs)
|
|
57
|
+
|
|
58
|
+
def run(self) -> None:
|
|
59
|
+
"""Run the application with ACLI error handling."""
|
|
60
|
+
try:
|
|
61
|
+
self._typer()
|
|
62
|
+
except ACLIError as exc:
|
|
63
|
+
self._handle_acli_error(exc)
|
|
64
|
+
except SystemExit:
|
|
65
|
+
raise
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
self._handle_unexpected_error(exc)
|
|
68
|
+
|
|
69
|
+
def get_command_tree(self) -> dict[str, Any]:
|
|
70
|
+
"""Build the introspection command tree."""
|
|
71
|
+
return build_command_tree(self._typer, self.name, self.version)
|
|
72
|
+
|
|
73
|
+
# ── Built-in commands ─────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def _register_introspect(self) -> None:
|
|
76
|
+
@self._typer.command(name="introspect", hidden=True)
|
|
77
|
+
def introspect(
|
|
78
|
+
acli_version: bool = typer.Option(
|
|
79
|
+
False, "--acli-version", help="Show only the ACLI spec version. type:bool"
|
|
80
|
+
),
|
|
81
|
+
output: OutputFormat = typer.Option(
|
|
82
|
+
OutputFormat.json, "--output", help="Output format. type:enum[text|json|table]"
|
|
83
|
+
),
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Output the full command tree as JSON for agent consumption."""
|
|
86
|
+
if acli_version:
|
|
87
|
+
if output == OutputFormat.json:
|
|
88
|
+
json.dump({"acli_version": "0.1.0"}, sys.stdout)
|
|
89
|
+
sys.stdout.write("\n")
|
|
90
|
+
else:
|
|
91
|
+
sys.stdout.write("acli 0.1.0\n")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
tree = self.get_command_tree()
|
|
95
|
+
|
|
96
|
+
# Update .cli/ if needed
|
|
97
|
+
if needs_update(tree, self.cli_dir):
|
|
98
|
+
generate_cli_folder(tree, self.cli_dir)
|
|
99
|
+
|
|
100
|
+
emit(success_envelope("introspect", tree, version=self.version), output)
|
|
101
|
+
|
|
102
|
+
def _register_version(self) -> None:
|
|
103
|
+
@self._typer.command(name="version", hidden=True)
|
|
104
|
+
def version_cmd(
|
|
105
|
+
output: OutputFormat = typer.Option(
|
|
106
|
+
OutputFormat.text,
|
|
107
|
+
"--output",
|
|
108
|
+
help="Output format. type:enum[text|json|table]",
|
|
109
|
+
),
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Show version information."""
|
|
112
|
+
if output == OutputFormat.json:
|
|
113
|
+
data = {
|
|
114
|
+
"tool": self.name,
|
|
115
|
+
"version": self.version,
|
|
116
|
+
"acli_version": "0.1.0",
|
|
117
|
+
}
|
|
118
|
+
emit(success_envelope("version", data, version=self.version), output)
|
|
119
|
+
else:
|
|
120
|
+
sys.stdout.write(f"{self.name} {self.version}\n")
|
|
121
|
+
sys.stdout.write("acli 0.1.0\n")
|
|
122
|
+
|
|
123
|
+
# Update .cli/ if needed
|
|
124
|
+
tree = self.get_command_tree()
|
|
125
|
+
if needs_update(tree, self.cli_dir):
|
|
126
|
+
generate_cli_folder(tree, self.cli_dir)
|
|
127
|
+
|
|
128
|
+
def _register_skill(self) -> None:
|
|
129
|
+
@self._typer.command(name="skill", hidden=True)
|
|
130
|
+
def skill_cmd(
|
|
131
|
+
out: str = typer.Option(
|
|
132
|
+
"",
|
|
133
|
+
"--out",
|
|
134
|
+
help="Write skill file to this path instead of stdout. type:path",
|
|
135
|
+
),
|
|
136
|
+
output: OutputFormat = typer.Option(
|
|
137
|
+
OutputFormat.text,
|
|
138
|
+
"--output",
|
|
139
|
+
help="Output format. type:enum[text|json|table]",
|
|
140
|
+
),
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Generate a SKILLS.md file for agent bootstrapping."""
|
|
143
|
+
tree = self.get_command_tree()
|
|
144
|
+
target = Path(out) if out else None
|
|
145
|
+
content = generate_skill(tree, target_path=target)
|
|
146
|
+
|
|
147
|
+
if output == OutputFormat.json:
|
|
148
|
+
data = {"path": str(target) if target else None, "content": content}
|
|
149
|
+
emit(success_envelope("skill", data, version=self.version), output)
|
|
150
|
+
elif target:
|
|
151
|
+
sys.stdout.write(f"Skill file written to {target}\n")
|
|
152
|
+
else:
|
|
153
|
+
sys.stdout.write(content)
|
|
154
|
+
|
|
155
|
+
# ── Error handling ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def _handle_acli_error(self, exc: ACLIError) -> None:
|
|
158
|
+
cmd_name = exc.command or self.name
|
|
159
|
+
envelope = error_envelope(
|
|
160
|
+
cmd_name,
|
|
161
|
+
code=exc.code.name,
|
|
162
|
+
message=str(exc),
|
|
163
|
+
hint=exc.hint,
|
|
164
|
+
docs=exc.docs,
|
|
165
|
+
version=self.version,
|
|
166
|
+
)
|
|
167
|
+
emit(envelope, OutputFormat.json)
|
|
168
|
+
raise SystemExit(exc.code.value)
|
|
169
|
+
|
|
170
|
+
def _handle_unexpected_error(self, exc: Exception) -> None:
|
|
171
|
+
envelope = error_envelope(
|
|
172
|
+
self.name,
|
|
173
|
+
code="GENERAL_ERROR",
|
|
174
|
+
message=str(exc),
|
|
175
|
+
hint="This is an unexpected error. Please report it.",
|
|
176
|
+
version=self.version,
|
|
177
|
+
)
|
|
178
|
+
emit(envelope, OutputFormat.json)
|
|
179
|
+
raise SystemExit(ExitCode.GENERAL_ERROR)
|