elva-cli 0.0.2__tar.gz → 0.0.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.
Files changed (34) hide show
  1. elva_cli-0.0.4/PKG-INFO +84 -0
  2. elva_cli-0.0.4/README.md +58 -0
  3. {elva_cli-0.0.2 → elva_cli-0.0.4}/pyproject.toml +2 -1
  4. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/_version.py +2 -2
  5. elva_cli-0.0.4/src/elva_cli/errors.py +90 -0
  6. elva_cli-0.0.4/src/elva_cli/main.py +139 -0
  7. elva_cli-0.0.4/src/elva_cli/settings/paths.py +16 -0
  8. elva_cli-0.0.4/tests/cli/test_cli_exit_codes.py +48 -0
  9. elva_cli-0.0.4/tests/unit/test_error_boundary.py +126 -0
  10. elva_cli-0.0.4/tests/unit/test_errors.py +68 -0
  11. elva_cli-0.0.4/tests/unit/test_exit_codes.py +34 -0
  12. elva_cli-0.0.2/PKG-INFO +0 -131
  13. elva_cli-0.0.2/README.md +0 -106
  14. elva_cli-0.0.2/src/elva_cli/errors.py +0 -9
  15. elva_cli-0.0.2/src/elva_cli/main.py +0 -55
  16. {elva_cli-0.0.2 → elva_cli-0.0.4}/.gitignore +0 -0
  17. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/__init__.py +0 -0
  18. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/__main__.py +0 -0
  19. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/auth/__init__.py +0 -0
  20. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/commands/__init__.py +0 -0
  21. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/context.py +0 -0
  22. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/core/__init__.py +0 -0
  23. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/core/api/__init__.py +0 -0
  24. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/core/services/__init__.py +0 -0
  25. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/core/spec/__init__.py +0 -0
  26. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/logging.py +0 -0
  27. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/registry.py +0 -0
  28. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/settings/__init__.py +0 -0
  29. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/telemetry.py +0 -0
  30. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/ui/__init__.py +0 -0
  31. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/ui/prompts.py +0 -0
  32. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/ui/renderables/__init__.py +0 -0
  33. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/ui/views/__init__.py +0 -0
  34. {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/update.py +0 -0
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.5
2
+ Name: elva-cli
3
+ Version: 0.0.4
4
+ Summary: Elva - CLI for Theneo Elva
5
+ Project-URL: Homepage, https://getelva.ai
6
+ Project-URL: Source, https://github.com/Theneo-Inc/theneo-elva-cli
7
+ Project-URL: Issues, https://github.com/Theneo-Inc/theneo-elva-cli/issues
8
+ Author-email: Theneo <support@theneo.io>
9
+ Keywords: api,cli,documentation,elva,openapi,theneo
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Documentation
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: platformdirs>=4.2
20
+ Requires-Dist: typer<1.0,>=0.15
21
+ Provides-Extra: dev
22
+ Requires-Dist: mypy>=1.11; extra == 'dev'
23
+ Requires-Dist: pytest>=8.2; extra == 'dev'
24
+ Requires-Dist: ruff>=0.6; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Elva CLI
28
+
29
+ Manage your [Elva](https://getelva.ai) API projects from the terminal: import specs,
30
+ inspect collections, and generate MCP servers without opening a browser.
31
+
32
+ > **Early alpha.** The command surface is still taking shape. This release ships
33
+ > `--version` and `--help` only; the first working commands land in `0.1.0`.
34
+
35
+ ## Install
36
+
37
+ Requires Python 3.11 or newer.
38
+
39
+ ```bash
40
+ uv tool install elva-cli
41
+ ```
42
+
43
+ Or with [pipx](https://pipx.pypa.io/), if you already use it:
44
+
45
+ ```bash
46
+ pipx install elva-cli
47
+ ```
48
+
49
+ Either way, `elva` is then available from any directory:
50
+
51
+ ```bash
52
+ elva --version
53
+ elva --help
54
+ ```
55
+
56
+ To try it without installing anything:
57
+
58
+ ```bash
59
+ uvx --from elva-cli elva --version
60
+ ```
61
+
62
+ Don't have `uv`? It is a single command and no prerequisites:
63
+
64
+ ```bash
65
+ curl -fsSL https://astral.sh/uv/install.sh | sh # macOS, Linux
66
+ powershell -c "irm https://astral.sh/uv/install.ps1|iex" # Windows
67
+ ```
68
+
69
+ ### Upgrade
70
+
71
+ ```bash
72
+ uv tool upgrade elva-cli # or: pipx upgrade elva-cli
73
+ ```
74
+
75
+ ## Requirements
76
+
77
+ - Python 3.11 or newer (bundled automatically if you install via `uv tool`)
78
+ - An [Elva](https://getelva.ai) account
79
+
80
+ ## Links
81
+
82
+ - [Elva](https://getelva.ai)
83
+ - [Issues](https://github.com/Theneo-Inc/theneo-elva-cli/issues)
84
+ - [Contributing](CONTRIBUTING.md)
@@ -0,0 +1,58 @@
1
+ # Elva CLI
2
+
3
+ Manage your [Elva](https://getelva.ai) API projects from the terminal: import specs,
4
+ inspect collections, and generate MCP servers without opening a browser.
5
+
6
+ > **Early alpha.** The command surface is still taking shape. This release ships
7
+ > `--version` and `--help` only; the first working commands land in `0.1.0`.
8
+
9
+ ## Install
10
+
11
+ Requires Python 3.11 or newer.
12
+
13
+ ```bash
14
+ uv tool install elva-cli
15
+ ```
16
+
17
+ Or with [pipx](https://pipx.pypa.io/), if you already use it:
18
+
19
+ ```bash
20
+ pipx install elva-cli
21
+ ```
22
+
23
+ Either way, `elva` is then available from any directory:
24
+
25
+ ```bash
26
+ elva --version
27
+ elva --help
28
+ ```
29
+
30
+ To try it without installing anything:
31
+
32
+ ```bash
33
+ uvx --from elva-cli elva --version
34
+ ```
35
+
36
+ Don't have `uv`? It is a single command and no prerequisites:
37
+
38
+ ```bash
39
+ curl -fsSL https://astral.sh/uv/install.sh | sh # macOS, Linux
40
+ powershell -c "irm https://astral.sh/uv/install.ps1|iex" # Windows
41
+ ```
42
+
43
+ ### Upgrade
44
+
45
+ ```bash
46
+ uv tool upgrade elva-cli # or: pipx upgrade elva-cli
47
+ ```
48
+
49
+ ## Requirements
50
+
51
+ - Python 3.11 or newer (bundled automatically if you install via `uv tool`)
52
+ - An [Elva](https://getelva.ai) account
53
+
54
+ ## Links
55
+
56
+ - [Elva](https://getelva.ai)
57
+ - [Issues](https://github.com/Theneo-Inc/theneo-elva-cli/issues)
58
+ - [Contributing](CONTRIBUTING.md)
@@ -23,6 +23,7 @@ classifiers = [
23
23
 
24
24
  dependencies = [
25
25
  "typer>=0.15,<1.0",
26
+ "platformdirs>=4.2",
26
27
  ]
27
28
 
28
29
  [project.optional-dependencies]
@@ -91,7 +92,7 @@ ban-relative-imports = "all"
91
92
  [tool.mypy]
92
93
  python_version = "3.11"
93
94
  strict = true
94
- files = ["src"]
95
+ files = ["src", "tests"]
95
96
  warn_unreachable = true
96
97
  enable_error_code = ["ignore-without-code", "redundant-expr"]
97
98
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.0.2'
22
- __version_tuple__ = version_tuple = (0, 0, 2)
21
+ __version__ = version = '0.0.4'
22
+ __version_tuple__ = version_tuple = (0, 0, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,90 @@
1
+ """Error taxonomy and the exit-code contract.
2
+
3
+ Exit codes are a public API that pipelines branch on. Never renumber a shipped
4
+ value, and never collapse VALIDATION into UNEXPECTED, callers rely on the
5
+ difference between "your spec is wrong" and "the tool broke".
6
+
7
+ Every ElvaError carries a stable machine code, a human message, and where one
8
+ exists, the next action to take. Anything that escapes as a bare Exception is a
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import enum
14
+
15
+
16
+ class ExitCode(enum.IntEnum):
17
+ """Process exit statuses. Documented in docs/exit-codes.md."""
18
+
19
+ OK = 0
20
+ UNEXPECTED = 1
21
+ USAGE = 2
22
+ AUTH = 3
23
+ VALIDATION = 4
24
+ API = 5
25
+ INTERRUPTED = 130
26
+
27
+
28
+ class ElvaError(Exception):
29
+ """Base class for every failure the user is meant to see."""
30
+
31
+ code: str = "ELVA_ERROR"
32
+ exit_code: ExitCode = ExitCode.UNEXPECTED
33
+ default_hint: str | None = None
34
+
35
+ def __init__(
36
+ self,
37
+ message: str,
38
+ *,
39
+ hint: str | None = None,
40
+ code: str | None = None,
41
+ exit_code: ExitCode | None = None,
42
+ ) -> None:
43
+ super().__init__(message)
44
+ self.message = message
45
+ self.hint = self.default_hint if hint is None else hint
46
+ if code is not None:
47
+ self.code = code
48
+ if exit_code is not None:
49
+ self.exit_code = exit_code
50
+
51
+ def __str__(self) -> str:
52
+ return self.message
53
+
54
+
55
+ class UsageError(ElvaError):
56
+ """The command was invoked wrongly, or needs an answer it cannot ask for."""
57
+
58
+ code = "ELVA_USAGE"
59
+ exit_code = ExitCode.USAGE
60
+ default_hint = "Run 'elva --help' to see the available commands and options."
61
+
62
+
63
+ class ConfigError(ElvaError):
64
+ """Configuration is missing or malformed."""
65
+
66
+ code = "ELVA_CONFIG"
67
+ exit_code = ExitCode.USAGE
68
+
69
+
70
+ class AuthError(ElvaError):
71
+ """Not authenticated, or the stored credentials no longer work."""
72
+
73
+ code = "ELVA_AUTH"
74
+ exit_code = ExitCode.AUTH
75
+ default_hint = "Run 'elva auth login' to sign in."
76
+
77
+
78
+ class ValidationError(ElvaError):
79
+ """The input spec is invalid. The CLI itself worked correctly."""
80
+
81
+ code = "ELVA_VALIDATION"
82
+ exit_code = ExitCode.VALIDATION
83
+
84
+
85
+ class ApiError(ElvaError):
86
+ """The Elva API could not be reached, or returned a server error."""
87
+
88
+ code = "ELVA_API"
89
+ exit_code = ExitCode.API
90
+ default_hint = "Check your connection and try again."
@@ -0,0 +1,139 @@
1
+ """Typer root and the single error boundary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ import sys
8
+ from typing import TYPE_CHECKING, Protocol, TypeGuard
9
+
10
+ import typer
11
+
12
+ from elva_cli.errors import ElvaError, ExitCode
13
+
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
17
+ app = typer.Typer(
18
+ name="elva",
19
+ help="Elva - CLI for Theneo Elva.",
20
+ no_args_is_help=True,
21
+ pretty_exceptions_enable=False,
22
+ context_settings={"help_option_names": ["-h", "--help"]},
23
+ )
24
+
25
+
26
+ def _version_callback(value: bool) -> None:
27
+ if not value:
28
+ return
29
+ from elva_cli import __version__
30
+
31
+ machine = f"{platform.system().lower()}-{platform.machine()}"
32
+ typer.echo(f"elva {__version__} (python {platform.python_version()}, {machine})")
33
+ raise typer.Exit(ExitCode.OK)
34
+
35
+
36
+ @app.callback()
37
+ def root(
38
+ version: bool = typer.Option(
39
+ False,
40
+ "--version",
41
+ "-V",
42
+ callback=_version_callback,
43
+ is_eager=True,
44
+ help="Show the current build version.",
45
+ ),
46
+ ) -> None:
47
+ pass
48
+
49
+
50
+ def report(error: ElvaError) -> None:
51
+ """Render a user-facing error to stderr as code, message, then next action."""
52
+ typer.secho(f"{error.code}: {error.message}", err=True, fg=typer.colors.RED)
53
+ if error.hint:
54
+ typer.secho(f" -> {error.hint}", err=True, dim=True)
55
+
56
+
57
+ def write_crash(exc: BaseException) -> Path | None:
58
+ """Persist a traceback for an unexpected failure and return its path.
59
+
60
+ Deliberately records no argv: a crash report is written to disk and kept, and
61
+ a mistyped secret on a command line must not outlive the process.
62
+ """
63
+ import time
64
+ import traceback
65
+
66
+ from elva_cli import __version__
67
+ from elva_cli.settings.paths import crash_dir
68
+
69
+ try:
70
+ directory = crash_dir()
71
+ directory.mkdir(parents=True, exist_ok=True)
72
+ target = directory / f"crash-{int(time.time())}-{os.getpid()}.log"
73
+ target.write_text(
74
+ f"elva {__version__}\n"
75
+ f"python {platform.python_version()} on {platform.platform()}\n\n"
76
+ + "".join(traceback.format_exception(exc)),
77
+ encoding="utf-8",
78
+ )
79
+ except OSError:
80
+ return None
81
+ return target
82
+
83
+
84
+ class _FrameworkError(Protocol):
85
+ """The shape every vendored Click exception exposes."""
86
+
87
+ exit_code: int
88
+
89
+ def show(self) -> None: ...
90
+
91
+
92
+ def _is_framework_error(exc: BaseException) -> TypeGuard[_FrameworkError]:
93
+ """Recognise a Typer/Click argument-parsing failure."""
94
+ return (
95
+ type(exc).__module__.startswith("typer")
96
+ and callable(getattr(exc, "show", None))
97
+ and isinstance(getattr(exc, "exit_code", None), int)
98
+ )
99
+
100
+
101
+ def _run() -> int:
102
+ try:
103
+ app(standalone_mode=False)
104
+ except typer.Exit as exc:
105
+ return int(exc.exit_code)
106
+ except typer.Abort:
107
+ return int(ExitCode.INTERRUPTED)
108
+ except ElvaError as exc:
109
+ report(exc)
110
+ return int(exc.exit_code)
111
+ except KeyboardInterrupt:
112
+ typer.secho("interrupted", err=True, dim=True)
113
+ return int(ExitCode.INTERRUPTED)
114
+ except Exception as exc:
115
+ if _is_framework_error(exc):
116
+ exc.show()
117
+ return int(exc.exit_code)
118
+ path = write_crash(exc)
119
+ report(
120
+ ElvaError(
121
+ f"unexpected error: {type(exc).__name__}: {exc}",
122
+ code="ELVA_CRASH",
123
+ hint=(
124
+ f"Details written to {path}. Please include that file when reporting this."
125
+ if path
126
+ else "Please report this, including the command you ran."
127
+ ),
128
+ )
129
+ )
130
+ return int(ExitCode.UNEXPECTED)
131
+ return int(ExitCode.OK)
132
+
133
+
134
+ def main() -> None:
135
+ sys.exit(_run())
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import platformdirs
6
+
7
+ APP_NAME = "elva"
8
+
9
+
10
+ def cache_dir() -> Path:
11
+ """Per-user cache location: XDG on Linux, ~/Library on macOS, %LOCALAPPDATA% on Windows."""
12
+ return Path(platformdirs.user_cache_dir(APP_NAME, appauthor=False))
13
+
14
+
15
+ def crash_dir() -> Path:
16
+ return cache_dir() / "crashes"
@@ -0,0 +1,48 @@
1
+ """End-to-end exit codes, run as a real subprocess.
2
+
3
+ Uses `python -m elva_cli` rather than the `elva` script so the tests do not
4
+ depend on the console script being on PATH.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ import sys
11
+
12
+
13
+ def run(*args: str) -> subprocess.CompletedProcess[str]:
14
+ return subprocess.run(
15
+ [sys.executable, "-m", "elva_cli", *args],
16
+ capture_output=True,
17
+ text=True,
18
+ check=False,
19
+ )
20
+
21
+
22
+ def test_version_exits_ok() -> None:
23
+ result = run("--version")
24
+ assert result.returncode == 0
25
+ assert result.stdout.startswith("elva ")
26
+
27
+
28
+ def test_help_exits_ok() -> None:
29
+ assert run("--help").returncode == 0
30
+
31
+
32
+ def test_no_args_is_help_and_exits_usage() -> None:
33
+ assert run().returncode == 2
34
+
35
+
36
+ def test_unknown_option_exits_usage() -> None:
37
+ result = run("--definitely-not-an-option")
38
+ assert result.returncode == 2
39
+
40
+
41
+ def test_unknown_command_exits_usage() -> None:
42
+ assert run("definitely-not-a-command").returncode == 2
43
+
44
+
45
+ def test_no_traceback_ever_reaches_the_user() -> None:
46
+ for args in ([], ["--definitely-not-an-option"], ["definitely-not-a-command"]):
47
+ result = run(*args)
48
+ assert "Traceback (most recent call last)" not in result.stderr
@@ -0,0 +1,126 @@
1
+ """The single error boundary in main._run().
2
+
3
+ Every failure mode is exercised here rather than through subprocesses, so the
4
+ mapping from exception to exit code is asserted directly.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Never
10
+
11
+ import pytest
12
+ import typer
13
+
14
+ from elva_cli import main
15
+ from elva_cli.errors import ApiError, AuthError, ElvaError, ExitCode, ValidationError
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+ from pathlib import Path
20
+
21
+
22
+ def _app_raising(exc: BaseException) -> Callable[..., Never]:
23
+ def fake_app(*_args: object, **_kwargs: object) -> Never:
24
+ raise exc
25
+
26
+ return fake_app
27
+
28
+
29
+ def test_success_returns_ok(monkeypatch: pytest.MonkeyPatch) -> None:
30
+ monkeypatch.setattr(main, "app", lambda **_kwargs: None)
31
+ assert main._run() == ExitCode.OK
32
+
33
+
34
+ def test_typer_exit_code_is_passed_through(monkeypatch: pytest.MonkeyPatch) -> None:
35
+ monkeypatch.setattr(main, "app", _app_raising(typer.Exit(0)))
36
+ assert main._run() == ExitCode.OK
37
+
38
+
39
+ def test_abort_maps_to_interrupted(monkeypatch: pytest.MonkeyPatch) -> None:
40
+ monkeypatch.setattr(main, "app", _app_raising(typer.Abort()))
41
+ assert main._run() == ExitCode.INTERRUPTED
42
+
43
+
44
+ def test_keyboard_interrupt_maps_to_130(monkeypatch: pytest.MonkeyPatch) -> None:
45
+ monkeypatch.setattr(main, "app", _app_raising(KeyboardInterrupt()))
46
+ assert main._run() == ExitCode.INTERRUPTED
47
+
48
+
49
+ def test_framework_usage_error_maps_to_usage(monkeypatch: pytest.MonkeyPatch) -> None:
50
+ """Typer vendors Click, so its parsing errors are recognised by protocol."""
51
+ monkeypatch.setattr(main, "app", _app_raising(typer.BadParameter("no such option")))
52
+ assert main._run() == ExitCode.USAGE
53
+
54
+
55
+ @pytest.mark.parametrize(
56
+ ("error", "expected"),
57
+ [
58
+ (AuthError("expired"), ExitCode.AUTH),
59
+ (ValidationError("bad spec"), ExitCode.VALIDATION),
60
+ (ApiError("502"), ExitCode.API),
61
+ (ElvaError("generic"), ExitCode.UNEXPECTED),
62
+ ],
63
+ )
64
+ def test_elva_errors_use_their_own_exit_code(
65
+ monkeypatch: pytest.MonkeyPatch, error: ElvaError, expected: ExitCode
66
+ ) -> None:
67
+ monkeypatch.setattr(main, "app", _app_raising(error))
68
+ assert main._run() == expected
69
+
70
+
71
+ def test_elva_error_prints_code_message_and_hint(
72
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
73
+ ) -> None:
74
+ monkeypatch.setattr(main, "app", _app_raising(AuthError("session expired")))
75
+ main._run()
76
+ err = capsys.readouterr().err
77
+ assert "ELVA_AUTH" in err
78
+ assert "session expired" in err
79
+ assert "elva auth login" in err
80
+
81
+
82
+ def test_unexpected_exception_writes_a_crash_file_and_no_traceback(
83
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
84
+ ) -> None:
85
+ monkeypatch.setattr("elva_cli.settings.paths.cache_dir", lambda: tmp_path, raising=True)
86
+ monkeypatch.setattr(main, "app", _app_raising(RuntimeError("kaboom")))
87
+
88
+ assert main._run() == ExitCode.UNEXPECTED
89
+
90
+ err = capsys.readouterr().err
91
+ assert "kaboom" in err
92
+ assert "Traceback (most recent call last)" not in err
93
+
94
+ crashes = list((tmp_path / "crashes").glob("crash-*.log"))
95
+ assert len(crashes) == 1
96
+ assert str(crashes[0]) in err
97
+
98
+ contents = crashes[0].read_text(encoding="utf-8")
99
+ assert "Traceback (most recent call last)" in contents
100
+ assert "RuntimeError: kaboom" in contents
101
+
102
+
103
+ def test_crash_file_records_no_argv(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
104
+ """A crash report outlives the process; a secret on the command line must not."""
105
+ monkeypatch.setattr("elva_cli.settings.paths.cache_dir", lambda: tmp_path, raising=True)
106
+ monkeypatch.setattr("sys.argv", ["elva", "mcp", "create", "--secret", "hunter2"])
107
+ monkeypatch.setattr(main, "app", _app_raising(RuntimeError("kaboom")))
108
+
109
+ main._run()
110
+
111
+ crash = next(iter((tmp_path / "crashes").glob("crash-*.log")))
112
+ contents = crash.read_text(encoding="utf-8")
113
+ assert "hunter2" not in contents
114
+
115
+
116
+ def test_unwritable_crash_dir_still_reports_the_error(
117
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
118
+ ) -> None:
119
+ def boom() -> Path:
120
+ raise OSError("read-only filesystem")
121
+
122
+ monkeypatch.setattr("elva_cli.settings.paths.cache_dir", boom, raising=True)
123
+ monkeypatch.setattr(main, "app", _app_raising(RuntimeError("kaboom")))
124
+
125
+ assert main._run() == ExitCode.UNEXPECTED
126
+ assert "kaboom" in capsys.readouterr().err
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from elva_cli.errors import (
6
+ ApiError,
7
+ AuthError,
8
+ ConfigError,
9
+ ElvaError,
10
+ ExitCode,
11
+ UsageError,
12
+ ValidationError,
13
+ )
14
+
15
+ EVERY_ERROR = [ElvaError, UsageError, ConfigError, AuthError, ValidationError, ApiError]
16
+
17
+
18
+ @pytest.mark.parametrize("cls", EVERY_ERROR)
19
+ def test_every_error_carries_a_code_and_message(cls: type[ElvaError]) -> None:
20
+ error = cls("something went wrong")
21
+ assert error.message == "something went wrong"
22
+ assert str(error) == "something went wrong"
23
+ assert error.code.startswith("ELVA_")
24
+ assert isinstance(error.exit_code, ExitCode)
25
+
26
+
27
+ @pytest.mark.parametrize("cls", EVERY_ERROR)
28
+ def test_every_error_is_catchable_as_elva_error(cls: type[ElvaError]) -> None:
29
+ with pytest.raises(ElvaError):
30
+ raise cls("boom")
31
+
32
+
33
+ def test_subclass_codes_and_exit_codes() -> None:
34
+ assert (UsageError("x").code, UsageError("x").exit_code) == ("ELVA_USAGE", ExitCode.USAGE)
35
+ assert (ConfigError("x").code, ConfigError("x").exit_code) == ("ELVA_CONFIG", ExitCode.USAGE)
36
+ assert (AuthError("x").code, AuthError("x").exit_code) == ("ELVA_AUTH", ExitCode.AUTH)
37
+ assert (ValidationError("x").code, ValidationError("x").exit_code) == (
38
+ "ELVA_VALIDATION",
39
+ ExitCode.VALIDATION,
40
+ )
41
+ assert (ApiError("x").code, ApiError("x").exit_code) == ("ELVA_API", ExitCode.API)
42
+
43
+
44
+ def test_default_hint_supplies_a_next_action() -> None:
45
+ assert AuthError("session expired").hint == "Run 'elva auth login' to sign in."
46
+ assert UsageError("bad flag").hint is not None
47
+ assert ApiError("timed out").hint is not None
48
+
49
+
50
+ def test_explicit_hint_overrides_the_default() -> None:
51
+ assert AuthError("nope", hint="Set ELVA_TOKEN.").hint == "Set ELVA_TOKEN."
52
+
53
+
54
+ def test_hint_can_be_specific_where_no_default_makes_sense() -> None:
55
+ assert ValidationError("bad spec").hint is None
56
+ assert ValidationError("bad spec", hint="Fix line 12.").hint == "Fix line 12."
57
+
58
+
59
+ def test_code_and_exit_code_can_be_overridden_per_instance() -> None:
60
+ error = ElvaError("odd", code="ELVA_WEIRD", exit_code=ExitCode.API)
61
+ assert error.code == "ELVA_WEIRD"
62
+ assert error.exit_code == ExitCode.API
63
+
64
+
65
+ def test_overriding_an_instance_does_not_mutate_the_class() -> None:
66
+ ElvaError("odd", code="ELVA_WEIRD", exit_code=ExitCode.API)
67
+ assert ElvaError("plain").code == "ELVA_ERROR"
68
+ assert ElvaError("plain").exit_code == ExitCode.UNEXPECTED
@@ -0,0 +1,34 @@
1
+ """The exit-code contract.
2
+
3
+ These assertions exist to fail loudly if anyone renumbers a shipped code. Exit
4
+ codes are a public API: CI pipelines and agents branch on them, so changing one
5
+ breaks callers we cannot see.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from elva_cli.errors import ExitCode
11
+
12
+
13
+ def test_exact_values_are_frozen() -> None:
14
+ assert ExitCode.OK.value == 0
15
+ assert ExitCode.UNEXPECTED.value == 1
16
+ assert ExitCode.USAGE.value == 2
17
+ assert ExitCode.AUTH.value == 3
18
+ assert ExitCode.VALIDATION.value == 4
19
+ assert ExitCode.API.value == 5
20
+ assert ExitCode.INTERRUPTED.value == 130
21
+
22
+
23
+ def test_validation_is_distinct_from_unexpected() -> None:
24
+ """The whole point of code 4: "your spec is wrong" != "the tool broke"."""
25
+ assert len({ExitCode.VALIDATION.value, ExitCode.UNEXPECTED.value}) == 2
26
+
27
+
28
+ def test_values_are_unique() -> None:
29
+ values = [code.value for code in ExitCode]
30
+ assert len(values) == len(set(values))
31
+
32
+
33
+ def test_codes_are_valid_posix_statuses() -> None:
34
+ assert all(0 <= code.value <= 255 for code in ExitCode)
elva_cli-0.0.2/PKG-INFO DELETED
@@ -1,131 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: elva-cli
3
- Version: 0.0.2
4
- Summary: Elva - CLI for Theneo Elva
5
- Project-URL: Homepage, https://getelva.ai
6
- Project-URL: Source, https://github.com/Theneo-Inc/theneo-elva-cli
7
- Project-URL: Issues, https://github.com/Theneo-Inc/theneo-elva-cli/issues
8
- Author-email: Theneo <support@theneo.io>
9
- Keywords: api,cli,documentation,elva,openapi,theneo
10
- Classifier: Development Status :: 3 - Alpha
11
- Classifier: Environment :: Console
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Topic :: Software Development :: Documentation
17
- Classifier: Typing :: Typed
18
- Requires-Python: >=3.11
19
- Requires-Dist: typer<1.0,>=0.15
20
- Provides-Extra: dev
21
- Requires-Dist: mypy>=1.11; extra == 'dev'
22
- Requires-Dist: pytest>=8.2; extra == 'dev'
23
- Requires-Dist: ruff>=0.6; extra == 'dev'
24
- Description-Content-Type: text/markdown
25
-
26
- # Elva CLI
27
-
28
- CLI for Theneo Editor
29
-
30
- ## Install
31
-
32
- Requires Python 3.11 or newer. Pick one:
33
-
34
- ```bash
35
- uv tool install elva-cli # recommended
36
- pipx install elva-cli # equivalent, if you already use pipx
37
- ```
38
-
39
- `elva` is then available from any directory, no virtualenv to activate:
40
-
41
- ```bash
42
- elva --version
43
- elva --help
44
- ```
45
-
46
- To try it without installing anything:
47
-
48
- ```bash
49
- uvx --from elva-cli elva --version
50
- ```
51
-
52
- ### Why not `pip install`?
53
-
54
- `pip install elva-cli` into a system Python is blocked on Ubuntu, Debian, Fedora and
55
- Homebrew macOS by [PEP 668](https://peps.python.org/pep-0668/), which reports
56
- `externally-managed-environment`. `uv tool` and `pipx` sidestep it by installing into
57
- an isolated environment for you and putting `elva` on your `PATH` -- which is what you
58
- want for a command-line tool anyway. Inside an already-activated virtualenv, plain
59
- `pip install elva-cli` works fine.
60
-
61
- ### Upgrade
62
-
63
- ```bash
64
- uv tool upgrade elva-cli # or: pipx upgrade elva-cli
65
- ```
66
-
67
-
68
- ## Development
69
-
70
- Requires Python 3.11 or newer.
71
-
72
- ```bash
73
- git clone https://github.com/Theneo-Inc/theneo-elva-cli.git
74
- cd theneo-elva-cli
75
-
76
- python3 -m venv .venv
77
- source .venv/bin/activate # Windows: .venv\Scripts\activate
78
- pip install -e ".[dev]"
79
- ```
80
-
81
- `-e` installs in editable mode, so your source edits take effect immediately with
82
- no reinstall.
83
-
84
- Run it:
85
-
86
- ```bash
87
- elva --version
88
- elva --help
89
- ```
90
-
91
- ```
92
- $ elva --version
93
- elva 0.0.1 (python 3.12.3, linux-x86_64)
94
- ```
95
-
96
- `python -m elva_cli --version` runs the same entry point, if you prefer that form.
97
-
98
-
99
- ### With uv (faster)
100
-
101
- If you have [uv](https://docs.astral.sh/uv/), it replaces the venv and pip steps:
102
-
103
- ```bash
104
- uv sync --extra dev
105
- uv run elva --version
106
- ```
107
-
108
- #### Running `elva` from any directory (development)
109
-
110
- To activate everywhere locally:
111
-
112
- ```bash
113
- uv tool install --editable ~/Desktop/theneo-elva-cli
114
- ```
115
-
116
- ### Checks
117
-
118
- ```bash
119
- ruff check . # lint
120
- ruff format . # format
121
- mypy # types, strict
122
- ```
123
-
124
- ### Version numbers
125
-
126
- The version comes from the git tag via `hatch-vcs` -- there is nothing to bump by hand.
127
- An untagged checkout reports something like `0.0.post1.dev2+gc657326`, which PyPI will
128
- not accept; a tagged one reports a clean `0.0.1`.
129
-
130
- To cut a release, see [RELEASING.md](RELEASING.md).
131
-
elva_cli-0.0.2/README.md DELETED
@@ -1,106 +0,0 @@
1
- # Elva CLI
2
-
3
- CLI for Theneo Editor
4
-
5
- ## Install
6
-
7
- Requires Python 3.11 or newer. Pick one:
8
-
9
- ```bash
10
- uv tool install elva-cli # recommended
11
- pipx install elva-cli # equivalent, if you already use pipx
12
- ```
13
-
14
- `elva` is then available from any directory, no virtualenv to activate:
15
-
16
- ```bash
17
- elva --version
18
- elva --help
19
- ```
20
-
21
- To try it without installing anything:
22
-
23
- ```bash
24
- uvx --from elva-cli elva --version
25
- ```
26
-
27
- ### Why not `pip install`?
28
-
29
- `pip install elva-cli` into a system Python is blocked on Ubuntu, Debian, Fedora and
30
- Homebrew macOS by [PEP 668](https://peps.python.org/pep-0668/), which reports
31
- `externally-managed-environment`. `uv tool` and `pipx` sidestep it by installing into
32
- an isolated environment for you and putting `elva` on your `PATH` -- which is what you
33
- want for a command-line tool anyway. Inside an already-activated virtualenv, plain
34
- `pip install elva-cli` works fine.
35
-
36
- ### Upgrade
37
-
38
- ```bash
39
- uv tool upgrade elva-cli # or: pipx upgrade elva-cli
40
- ```
41
-
42
-
43
- ## Development
44
-
45
- Requires Python 3.11 or newer.
46
-
47
- ```bash
48
- git clone https://github.com/Theneo-Inc/theneo-elva-cli.git
49
- cd theneo-elva-cli
50
-
51
- python3 -m venv .venv
52
- source .venv/bin/activate # Windows: .venv\Scripts\activate
53
- pip install -e ".[dev]"
54
- ```
55
-
56
- `-e` installs in editable mode, so your source edits take effect immediately with
57
- no reinstall.
58
-
59
- Run it:
60
-
61
- ```bash
62
- elva --version
63
- elva --help
64
- ```
65
-
66
- ```
67
- $ elva --version
68
- elva 0.0.1 (python 3.12.3, linux-x86_64)
69
- ```
70
-
71
- `python -m elva_cli --version` runs the same entry point, if you prefer that form.
72
-
73
-
74
- ### With uv (faster)
75
-
76
- If you have [uv](https://docs.astral.sh/uv/), it replaces the venv and pip steps:
77
-
78
- ```bash
79
- uv sync --extra dev
80
- uv run elva --version
81
- ```
82
-
83
- #### Running `elva` from any directory (development)
84
-
85
- To activate everywhere locally:
86
-
87
- ```bash
88
- uv tool install --editable ~/Desktop/theneo-elva-cli
89
- ```
90
-
91
- ### Checks
92
-
93
- ```bash
94
- ruff check . # lint
95
- ruff format . # format
96
- mypy # types, strict
97
- ```
98
-
99
- ### Version numbers
100
-
101
- The version comes from the git tag via `hatch-vcs` -- there is nothing to bump by hand.
102
- An untagged checkout reports something like `0.0.post1.dev2+gc657326`, which PyPI will
103
- not accept; a tagged one reports a clean `0.0.1`.
104
-
105
- To cut a release, see [RELEASING.md](RELEASING.md).
106
-
@@ -1,9 +0,0 @@
1
- """Error taxonomy and the exit-code contract.
2
-
3
- Exit codes are a public API that pipelines branch on: 0 ok, 1 unexpected, 2 usage,
4
- 3 auth, 4 spec failed validation, 5 network/API, 130 interrupted. Never renumber a
5
- shipped code, and never collapse 4 into 1 -- callers rely on the difference
6
- between "your spec is wrong" and "the tool broke".
7
-
8
- Every ElvaError carries a stable machine code, a message, and where one exists,
9
- the next action to take."""
@@ -1,55 +0,0 @@
1
- """Typer root and the single error boundary.
2
-
3
- Nothing here does work. It resolves global options into a Ctx, dispatches, and
4
- turns whatever comes back into an exit code. Every user-visible failure path in
5
- the CLI funnels through here.
6
-
7
- Currently wired: --version and --help. Global flags, the Ctx and the error
8
- boundary land next.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import platform
14
-
15
- import typer
16
-
17
- app = typer.Typer(
18
- name="elva",
19
- help="Elva - CLI for Theneo Elva.",
20
- no_args_is_help=True,
21
- pretty_exceptions_enable=False,
22
- context_settings={"help_option_names": ["-h", "--help"]},
23
- )
24
-
25
-
26
- def _version_callback(value: bool) -> None:
27
- if not value:
28
- return
29
- from elva_cli import __version__
30
-
31
- machine = f"{platform.system().lower()}-{platform.machine()}"
32
- typer.echo(f"elva {__version__} (python {platform.python_version()}, {machine})")
33
- raise typer.Exit(0)
34
-
35
-
36
- @app.callback()
37
- def root(
38
- version: bool = typer.Option(
39
- False,
40
- "--version",
41
- "-V",
42
- callback=_version_callback,
43
- is_eager=True,
44
- help="Show the current build version.",
45
- ),
46
- ) -> None:
47
- pass
48
-
49
-
50
- def main() -> None:
51
- app()
52
-
53
-
54
- if __name__ == "__main__":
55
- main()
File without changes