elva-cli 0.0.4__tar.gz → 0.2.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 (57) hide show
  1. elva_cli-0.2.0/PKG-INFO +175 -0
  2. elva_cli-0.2.0/README.md +148 -0
  3. {elva_cli-0.0.4 → elva_cli-0.2.0}/pyproject.toml +1 -0
  4. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/_version.py +2 -2
  5. elva_cli-0.2.0/src/elva_cli/commands/config.py +25 -0
  6. elva_cli-0.2.0/src/elva_cli/context.py +148 -0
  7. elva_cli-0.2.0/src/elva_cli/core/services/config.py +67 -0
  8. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/main.py +52 -9
  9. elva_cli-0.2.0/src/elva_cli/registry.py +46 -0
  10. elva_cli-0.2.0/src/elva_cli/settings/__init__.py +6 -0
  11. elva_cli-0.2.0/src/elva_cli/settings/loader.py +154 -0
  12. elva_cli-0.2.0/src/elva_cli/settings/models.py +38 -0
  13. elva_cli-0.2.0/src/elva_cli/settings/paths.py +40 -0
  14. elva_cli-0.2.0/src/elva_cli/ui/console.py +33 -0
  15. elva_cli-0.2.0/src/elva_cli/ui/output.py +65 -0
  16. elva_cli-0.2.0/src/elva_cli/ui/prompts.py +91 -0
  17. elva_cli-0.2.0/src/elva_cli/ui/renderables/__init__.py +12 -0
  18. elva_cli-0.2.0/src/elva_cli/ui/renderables/base.py +37 -0
  19. elva_cli-0.2.0/src/elva_cli/ui/renderables/config.py +41 -0
  20. elva_cli-0.2.0/src/elva_cli/ui/theme.py +15 -0
  21. elva_cli-0.2.0/tests/cli/test_config_command.py +84 -0
  22. elva_cli-0.2.0/tests/cli/test_lazy_imports.py +45 -0
  23. elva_cli-0.2.0/tests/cli/test_never_blocks.py +124 -0
  24. elva_cli-0.2.0/tests/cli/test_output_streams.py +110 -0
  25. elva_cli-0.2.0/tests/unit/test_boundary.py +107 -0
  26. elva_cli-0.2.0/tests/unit/test_context.py +57 -0
  27. elva_cli-0.2.0/tests/unit/test_interactivity.py +53 -0
  28. elva_cli-0.2.0/tests/unit/test_output.py +116 -0
  29. elva_cli-0.2.0/tests/unit/test_prompts.py +79 -0
  30. elva_cli-0.2.0/tests/unit/test_settings_loader.py +148 -0
  31. elva_cli-0.0.4/PKG-INFO +0 -84
  32. elva_cli-0.0.4/README.md +0 -58
  33. elva_cli-0.0.4/src/elva_cli/context.py +0 -5
  34. elva_cli-0.0.4/src/elva_cli/registry.py +0 -5
  35. elva_cli-0.0.4/src/elva_cli/settings/__init__.py +0 -5
  36. elva_cli-0.0.4/src/elva_cli/settings/paths.py +0 -16
  37. elva_cli-0.0.4/src/elva_cli/ui/prompts.py +0 -9
  38. elva_cli-0.0.4/src/elva_cli/ui/renderables/__init__.py +0 -4
  39. {elva_cli-0.0.4 → elva_cli-0.2.0}/.gitignore +0 -0
  40. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/__init__.py +0 -0
  41. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/__main__.py +0 -0
  42. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/auth/__init__.py +0 -0
  43. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/commands/__init__.py +0 -0
  44. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/core/__init__.py +0 -0
  45. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/core/api/__init__.py +0 -0
  46. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/core/services/__init__.py +0 -0
  47. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/core/spec/__init__.py +0 -0
  48. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/errors.py +0 -0
  49. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/logging.py +0 -0
  50. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/telemetry.py +0 -0
  51. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/ui/__init__.py +0 -0
  52. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/ui/views/__init__.py +0 -0
  53. {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/update.py +0 -0
  54. {elva_cli-0.0.4 → elva_cli-0.2.0}/tests/cli/test_cli_exit_codes.py +0 -0
  55. {elva_cli-0.0.4 → elva_cli-0.2.0}/tests/unit/test_error_boundary.py +0 -0
  56. {elva_cli-0.0.4 → elva_cli-0.2.0}/tests/unit/test_errors.py +0 -0
  57. {elva_cli-0.0.4 → elva_cli-0.2.0}/tests/unit/test_exit_codes.py +0 -0
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.5
2
+ Name: elva-cli
3
+ Version: 0.2.0
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: pydantic>=2.7
21
+ Requires-Dist: typer<1.0,>=0.15
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy>=1.11; extra == 'dev'
24
+ Requires-Dist: pytest>=8.2; extra == 'dev'
25
+ Requires-Dist: ruff>=0.6; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Elva CLI
29
+
30
+ Manage your [Elva](https://getelva.ai) API projects from the terminal: import specs,
31
+ inspect collections, and generate MCP servers without opening a browser.
32
+
33
+ > **Early alpha.** The command surface is still taking shape. This release ships
34
+ > `--version` and `--help` only; the first working commands land in `0.1.0`.
35
+
36
+ ## Install
37
+
38
+ Requires Python 3.11 or newer.
39
+
40
+ ```bash
41
+ uv tool install elva-cli
42
+ ```
43
+
44
+ Or with [pipx](https://pipx.pypa.io/), if you already use it:
45
+
46
+ ```bash
47
+ pipx install elva-cli
48
+ ```
49
+
50
+ Either way, `elva` is then available from any directory:
51
+
52
+ ```bash
53
+ elva --version
54
+ elva --help
55
+ ```
56
+
57
+ To try it without installing anything:
58
+
59
+ ```bash
60
+ uvx --from elva-cli elva --version
61
+ ```
62
+
63
+ Don't have `uv`? It is a single command and no prerequisites:
64
+
65
+ ```bash
66
+ curl -fsSL https://astral.sh/uv/install.sh | sh # macOS, Linux
67
+ powershell -c "irm https://astral.sh/uv/install.ps1|iex" # Windows
68
+ ```
69
+
70
+ ### Upgrade
71
+
72
+ ```bash
73
+ uv tool upgrade elva-cli # or: pipx upgrade elva-cli
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ Settings can come from several places. Highest priority wins:
79
+
80
+ 1. Command flags: `--workspace`, `--collection`, `--profile`
81
+ 2. Environment: `ELVA_WORKSPACE`, `ELVA_COLLECTION`, `ELVA_PROFILE`, `ELVA_TIMEOUT`
82
+ 3. `elva.json` in your project
83
+ 4. The selected profile in your user config
84
+ 5. Your user config
85
+ 6. Built in defaults
86
+
87
+ ### Project file
88
+
89
+ Commit an `elva.json` next to your spec and stop repeating flags:
90
+
91
+ ```json
92
+ {
93
+ "workspace": "payments-team",
94
+ "collection": "payments-api"
95
+ }
96
+ ```
97
+
98
+ It is found by walking up from the current directory to the repo root, so it works
99
+ from any subfolder. Keep secrets out of it, it is meant to be committed.
100
+
101
+ ### User config and profiles
102
+
103
+ | Platform | Location |
104
+ |---|---|
105
+ | Linux | `~/.config/elva/config.json` |
106
+ | macOS | `~/Library/Application Support/elva/config.json` |
107
+ | Windows | `%LOCALAPPDATA%\elva\config.json` |
108
+
109
+ A profile is a named set of defaults. Useful when you work across more than one
110
+ workspace and do not want a project file for each:
111
+
112
+ ```json
113
+ {
114
+ "profiles": {
115
+ "work": { "workspace": "work-team", "collection": "work-api" },
116
+ "side": { "workspace": "side-team" }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ```bash
122
+ elva --profile work collection list
123
+ ```
124
+
125
+ A project file beats a profile, so a repo with its own `elva.json` always wins over
126
+ whichever profile you have selected.
127
+
128
+ ### Seeing what was resolved
129
+
130
+ When something targets the wrong place, these two answer it:
131
+
132
+ ```bash
133
+ elva config path # which files were read, and whether they exist
134
+ elva config list # each value, and which layer set it
135
+ ```
136
+
137
+ ```
138
+ $ elva --profile work config list
139
+ collection work-api profile:work
140
+ profile work flag
141
+ timeout 30.0 default
142
+ workspace work-team profile:work
143
+
144
+ profiles: side, work
145
+ ```
146
+
147
+ Every command also takes `--json` for scripting:
148
+
149
+ ```
150
+ $ elva --profile work --json config list
151
+ {
152
+ "profile": "work",
153
+ "settings": [
154
+ { "key": "collection", "value": "work-api", "origin": "profile:work" },
155
+ { "key": "workspace", "value": "work-team", "origin": "profile:work" }
156
+ ],
157
+ "profiles": ["side", "work"]
158
+ }
159
+ ```
160
+
161
+ Data goes to stdout and everything else to stderr, so `elva --json ... | jq` is always
162
+ clean. `--quiet` drops hints and warnings but keeps data and errors. `--no-color` and
163
+ `NO_COLOR` turn off styling.
164
+
165
+ ## Requirements
166
+
167
+ - Python 3.11 or newer (bundled automatically if you install via `uv tool`)
168
+ - An [Elva](https://getelva.ai) account
169
+
170
+ ## Links
171
+
172
+ - [Elva](https://getelva.ai)
173
+ - [Issues](https://github.com/Theneo-Inc/theneo-elva-cli/issues)
174
+ - [Exit codes](docs/exit-codes.md), for scripting and CI
175
+ - [Contributing](CONTRIBUTING.md)
@@ -0,0 +1,148 @@
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
+ ## Configuration
50
+
51
+ Settings can come from several places. Highest priority wins:
52
+
53
+ 1. Command flags: `--workspace`, `--collection`, `--profile`
54
+ 2. Environment: `ELVA_WORKSPACE`, `ELVA_COLLECTION`, `ELVA_PROFILE`, `ELVA_TIMEOUT`
55
+ 3. `elva.json` in your project
56
+ 4. The selected profile in your user config
57
+ 5. Your user config
58
+ 6. Built in defaults
59
+
60
+ ### Project file
61
+
62
+ Commit an `elva.json` next to your spec and stop repeating flags:
63
+
64
+ ```json
65
+ {
66
+ "workspace": "payments-team",
67
+ "collection": "payments-api"
68
+ }
69
+ ```
70
+
71
+ It is found by walking up from the current directory to the repo root, so it works
72
+ from any subfolder. Keep secrets out of it, it is meant to be committed.
73
+
74
+ ### User config and profiles
75
+
76
+ | Platform | Location |
77
+ |---|---|
78
+ | Linux | `~/.config/elva/config.json` |
79
+ | macOS | `~/Library/Application Support/elva/config.json` |
80
+ | Windows | `%LOCALAPPDATA%\elva\config.json` |
81
+
82
+ A profile is a named set of defaults. Useful when you work across more than one
83
+ workspace and do not want a project file for each:
84
+
85
+ ```json
86
+ {
87
+ "profiles": {
88
+ "work": { "workspace": "work-team", "collection": "work-api" },
89
+ "side": { "workspace": "side-team" }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ```bash
95
+ elva --profile work collection list
96
+ ```
97
+
98
+ A project file beats a profile, so a repo with its own `elva.json` always wins over
99
+ whichever profile you have selected.
100
+
101
+ ### Seeing what was resolved
102
+
103
+ When something targets the wrong place, these two answer it:
104
+
105
+ ```bash
106
+ elva config path # which files were read, and whether they exist
107
+ elva config list # each value, and which layer set it
108
+ ```
109
+
110
+ ```
111
+ $ elva --profile work config list
112
+ collection work-api profile:work
113
+ profile work flag
114
+ timeout 30.0 default
115
+ workspace work-team profile:work
116
+
117
+ profiles: side, work
118
+ ```
119
+
120
+ Every command also takes `--json` for scripting:
121
+
122
+ ```
123
+ $ elva --profile work --json config list
124
+ {
125
+ "profile": "work",
126
+ "settings": [
127
+ { "key": "collection", "value": "work-api", "origin": "profile:work" },
128
+ { "key": "workspace", "value": "work-team", "origin": "profile:work" }
129
+ ],
130
+ "profiles": ["side", "work"]
131
+ }
132
+ ```
133
+
134
+ Data goes to stdout and everything else to stderr, so `elva --json ... | jq` is always
135
+ clean. `--quiet` drops hints and warnings but keeps data and errors. `--no-color` and
136
+ `NO_COLOR` turn off styling.
137
+
138
+ ## Requirements
139
+
140
+ - Python 3.11 or newer (bundled automatically if you install via `uv tool`)
141
+ - An [Elva](https://getelva.ai) account
142
+
143
+ ## Links
144
+
145
+ - [Elva](https://getelva.ai)
146
+ - [Issues](https://github.com/Theneo-Inc/theneo-elva-cli/issues)
147
+ - [Exit codes](docs/exit-codes.md), for scripting and CI
148
+ - [Contributing](CONTRIBUTING.md)
@@ -24,6 +24,7 @@ classifiers = [
24
24
  dependencies = [
25
25
  "typer>=0.15,<1.0",
26
26
  "platformdirs>=4.2",
27
+ "pydantic>=2.7",
27
28
  ]
28
29
 
29
30
  [project.optional-dependencies]
@@ -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.4'
22
- __version_tuple__ = version_tuple = (0, 0, 4)
21
+ __version__ = version = '0.2.0'
22
+ __version_tuple__ = version_tuple = (0, 2, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from elva_cli.context import get_ctx
6
+
7
+ app = typer.Typer(name="config", help="Inspect resolved configuration.", no_args_is_help=True)
8
+
9
+
10
+ @app.command("path")
11
+ def path(click_ctx: typer.Context) -> None:
12
+ """Show every file the CLI reads configuration from."""
13
+ from elva_cli.core.services.config import describe_paths
14
+
15
+ ctx = get_ctx(click_ctx)
16
+ ctx.out.result(describe_paths(ctx.resolution))
17
+
18
+
19
+ @app.command("list")
20
+ def list_(click_ctx: typer.Context) -> None:
21
+ """Show each setting, its value, and which layer set it."""
22
+ from elva_cli.core.services.config import describe_values
23
+
24
+ ctx = get_ctx(click_ctx)
25
+ ctx.out.result(describe_values(ctx.resolution))
@@ -0,0 +1,148 @@
1
+ """The Ctx object, built once in the root callback and passed to every command.
2
+
3
+ Commands read settings from here instead of touching the environment or the
4
+ filesystem themselves, so configuration is resolved in exactly one place.
5
+
6
+ Resolution is a cached_property because it pulls in pydantic. `elva --version`
7
+ must not pay for that.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from dataclasses import dataclass
14
+ from functools import cached_property
15
+ from typing import TYPE_CHECKING
16
+
17
+ from elva_cli.errors import ElvaError
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Mapping
21
+ from pathlib import Path
22
+
23
+ import typer
24
+ from rich.console import Console
25
+
26
+ from elva_cli.settings.loader import Resolution
27
+ from elva_cli.settings.models import Settings
28
+ from elva_cli.ui.output import Output
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class GlobalOptions:
33
+ profile: str | None = None
34
+ base_url: str | None = None
35
+ workspace: str | None = None
36
+ collection: str | None = None
37
+ json_output: bool = False
38
+ quiet: bool = False
39
+ color: bool | None = None
40
+ assume_yes: bool = False
41
+
42
+
43
+ CI_VARS = (
44
+ "CI",
45
+ "GITHUB_ACTIONS",
46
+ "GITLAB_CI",
47
+ "CIRCLECI",
48
+ "BUILDKITE",
49
+ "JENKINS_URL",
50
+ "TF_BUILD",
51
+ "TEAMCITY_VERSION",
52
+ )
53
+
54
+ _FALSEY = frozenset({"", "0", "false", "no", "off"})
55
+
56
+
57
+ class Ctx:
58
+ def __init__(
59
+ self,
60
+ options: GlobalOptions,
61
+ *,
62
+ cwd: Path,
63
+ env: Mapping[str, str],
64
+ tty: bool | None = None,
65
+ ) -> None:
66
+ self.options = options
67
+ self.cwd = cwd
68
+ self.env = env
69
+ self._tty = tty
70
+
71
+ @property
72
+ def assume_yes(self) -> bool:
73
+ return self.options.assume_yes
74
+
75
+ @property
76
+ def is_ci(self) -> bool:
77
+ return any(self.env.get(var, "").strip().lower() not in _FALSEY for var in CI_VARS)
78
+
79
+ @property
80
+ def is_tty(self) -> bool:
81
+ """Both streams, because a prompt needs to read and to be seen."""
82
+ if self._tty is not None:
83
+ return self._tty
84
+ return sys.stdin.isatty() and sys.stdout.isatty()
85
+
86
+ @property
87
+ def interactive(self) -> bool:
88
+ """Whether it is safe to ask the user a question.
89
+
90
+ The single place this is decided. --json means a machine is reading, so a
91
+ prompt would corrupt the output as well as hang. --quiet is not included:
92
+ it lowers the noise floor, it does not say nobody is there.
93
+ """
94
+ if self.options.json_output or self.is_ci:
95
+ return False
96
+ return self.is_tty
97
+
98
+ @property
99
+ def color(self) -> bool | None:
100
+ """NO_COLOR wins over everything except an explicit --color."""
101
+ if self.options.color is None and self.env.get("NO_COLOR"):
102
+ return False
103
+ return self.options.color
104
+
105
+ @cached_property
106
+ def consoles(self) -> tuple[Console, Console]:
107
+ from elva_cli.ui.console import build_consoles
108
+
109
+ return build_consoles(color=self.color, quiet=self.options.quiet)
110
+
111
+ @cached_property
112
+ def out(self) -> Output:
113
+ from elva_cli.ui.output import Output
114
+
115
+ stdout, stderr = self.consoles
116
+ return Output(
117
+ stdout=stdout,
118
+ stderr=stderr,
119
+ json_mode=self.options.json_output,
120
+ quiet=self.options.quiet,
121
+ )
122
+
123
+ @cached_property
124
+ def resolution(self) -> Resolution:
125
+ from elva_cli.settings.loader import resolve
126
+
127
+ return resolve(
128
+ overrides={
129
+ "profile": self.options.profile,
130
+ "base_url": self.options.base_url,
131
+ "workspace": self.options.workspace,
132
+ "collection": self.options.collection,
133
+ },
134
+ env=self.env,
135
+ cwd=self.cwd,
136
+ )
137
+
138
+ @property
139
+ def settings(self) -> Settings:
140
+ return self.resolution.settings
141
+
142
+
143
+ def get_ctx(click_ctx: typer.Context) -> Ctx:
144
+ ctx = click_ctx.obj
145
+ if not isinstance(ctx, Ctx):
146
+ msg = "no Ctx on the context; the root callback did not run"
147
+ raise ElvaError(msg)
148
+ return ctx
@@ -0,0 +1,67 @@
1
+ """Use cases for inspecting configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from elva_cli.settings import paths
9
+
10
+ if TYPE_CHECKING:
11
+ from elva_cli.settings.loader import Resolution
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ConfigFileInfo:
16
+ kind: str
17
+ path: str
18
+ exists: bool
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ConfigPaths:
23
+ config_dir: str
24
+ cache_dir: str
25
+ files: list[ConfigFileInfo] = field(default_factory=list)
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class SettingValue:
30
+ key: str
31
+ value: Any
32
+ origin: str
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class ConfigValues:
37
+ profile: str
38
+ settings: list[SettingValue] = field(default_factory=list)
39
+ profiles: list[str] = field(default_factory=list)
40
+
41
+
42
+ def describe_paths(resolution: Resolution) -> ConfigPaths:
43
+ """Report every location the CLI consulted, whether or not it existed."""
44
+ return ConfigPaths(
45
+ config_dir=str(paths.config_dir()),
46
+ cache_dir=str(paths.cache_dir()),
47
+ files=[
48
+ ConfigFileInfo(kind=f.kind, path=str(f.path), exists=f.exists) for f in resolution.files
49
+ ],
50
+ )
51
+
52
+
53
+ def describe_values(resolution: Resolution) -> ConfigValues:
54
+ """Report each resolved setting alongside the layer that set it."""
55
+ settings = resolution.settings
56
+ return ConfigValues(
57
+ profile=settings.profile,
58
+ settings=[
59
+ SettingValue(
60
+ key=key,
61
+ value=getattr(settings, key),
62
+ origin=resolution.origins[key],
63
+ )
64
+ for key in sorted(type(settings).model_fields)
65
+ ],
66
+ profiles=list(resolution.profiles),
67
+ )
@@ -5,16 +5,17 @@ from __future__ import annotations
5
5
  import os
6
6
  import platform
7
7
  import sys
8
- from typing import TYPE_CHECKING, Protocol, TypeGuard
8
+ from pathlib import Path
9
+ from typing import Protocol, TypeGuard
9
10
 
10
11
  import typer
11
12
 
13
+ from elva_cli.context import Ctx, GlobalOptions
12
14
  from elva_cli.errors import ElvaError, ExitCode
13
-
14
- if TYPE_CHECKING:
15
- from pathlib import Path
15
+ from elva_cli.registry import LazyGroup
16
16
 
17
17
  app = typer.Typer(
18
+ cls=LazyGroup,
18
19
  name="elva",
19
20
  help="Elva - CLI for Theneo Elva.",
20
21
  no_args_is_help=True,
@@ -35,6 +36,31 @@ def _version_callback(value: bool) -> None:
35
36
 
36
37
  @app.callback()
37
38
  def root(
39
+ click_ctx: typer.Context,
40
+ profile: str | None = typer.Option(
41
+ None, "--profile", envvar="ELVA_PROFILE", help="Named set of defaults from your config."
42
+ ),
43
+ # Internal escape hatch for Theneo development and CI against staging. Users
44
+ # only ever have prod, so it stays out of --help and out of the README.
45
+ base_url: str | None = typer.Option(None, "--base-url", envvar="ELVA_BASE_URL", hidden=True),
46
+ workspace: str | None = typer.Option(
47
+ None, "--workspace", "-w", envvar="ELVA_WORKSPACE", help="Workspace to act on."
48
+ ),
49
+ collection: str | None = typer.Option(
50
+ None, "--collection", "-c", envvar="ELVA_COLLECTION", help="Collection to act on."
51
+ ),
52
+ json_output: bool = typer.Option(
53
+ False, "--json", help="Emit machine readable JSON instead of formatted output."
54
+ ),
55
+ quiet: bool = typer.Option(
56
+ False, "--quiet", "-q", help="Suppress hints and warnings. Data and errors still print."
57
+ ),
58
+ assume_yes: bool = typer.Option(
59
+ False, "--yes", "-y", help="Answer every confirmation with yes. Required in CI."
60
+ ),
61
+ color: bool | None = typer.Option(
62
+ None, "--color/--no-color", help="Force or disable colour. Honours NO_COLOR."
63
+ ),
38
64
  version: bool = typer.Option(
39
65
  False,
40
66
  "--version",
@@ -44,14 +70,31 @@ def root(
44
70
  help="Show the current build version.",
45
71
  ),
46
72
  ) -> None:
47
- pass
73
+ click_ctx.obj = Ctx(
74
+ GlobalOptions(
75
+ profile=profile,
76
+ base_url=base_url,
77
+ workspace=workspace,
78
+ collection=collection,
79
+ json_output=json_output,
80
+ quiet=quiet,
81
+ color=color,
82
+ assume_yes=assume_yes,
83
+ ),
84
+ cwd=Path.cwd(),
85
+ env=os.environ,
86
+ )
48
87
 
49
88
 
50
89
  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)
90
+ """Render a user-facing error to stderr.
91
+
92
+ Goes through ui/ so errors look the same wherever they come from, but does not
93
+ need a Ctx: the boundary has to work when building the Ctx is what failed.
94
+ """
95
+ from elva_cli.ui.output import report_error
96
+
97
+ report_error(error, color=False if os.environ.get("NO_COLOR") else None)
55
98
 
56
99
 
57
100
  def write_crash(exc: BaseException) -> Path | None: