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.
- elva_cli-0.0.4/PKG-INFO +84 -0
- elva_cli-0.0.4/README.md +58 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/pyproject.toml +2 -1
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/_version.py +2 -2
- elva_cli-0.0.4/src/elva_cli/errors.py +90 -0
- elva_cli-0.0.4/src/elva_cli/main.py +139 -0
- elva_cli-0.0.4/src/elva_cli/settings/paths.py +16 -0
- elva_cli-0.0.4/tests/cli/test_cli_exit_codes.py +48 -0
- elva_cli-0.0.4/tests/unit/test_error_boundary.py +126 -0
- elva_cli-0.0.4/tests/unit/test_errors.py +68 -0
- elva_cli-0.0.4/tests/unit/test_exit_codes.py +34 -0
- elva_cli-0.0.2/PKG-INFO +0 -131
- elva_cli-0.0.2/README.md +0 -106
- elva_cli-0.0.2/src/elva_cli/errors.py +0 -9
- elva_cli-0.0.2/src/elva_cli/main.py +0 -55
- {elva_cli-0.0.2 → elva_cli-0.0.4}/.gitignore +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/__main__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/auth/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/commands/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/context.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/core/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/core/api/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/core/services/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/core/spec/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/logging.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/registry.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/settings/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/telemetry.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/ui/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/ui/prompts.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/ui/renderables/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/ui/views/__init__.py +0 -0
- {elva_cli-0.0.2 → elva_cli-0.0.4}/src/elva_cli/update.py +0 -0
elva_cli-0.0.4/PKG-INFO
ADDED
|
@@ -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)
|
elva_cli-0.0.4/README.md
ADDED
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|