envctl 2.3.2__tar.gz → 2.3.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.
- {envctl-2.3.2/src/envctl.egg-info → envctl-2.3.4}/PKG-INFO +81 -1
- {envctl-2.3.2 → envctl-2.3.4}/README.md +80 -0
- {envctl-2.3.2 → envctl-2.3.4}/pyproject.toml +2 -2
- envctl-2.3.4/src/envctl/adapters/process_environment.py +17 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/app.py +15 -2
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/check/command.py +5 -2
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/explain/command.py +4 -0
- envctl-2.3.4/src/envctl/cli/commands/export/command.py +38 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/inspect/command.py +5 -2
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/remove/command.py +2 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/run/command.py +6 -3
- envctl-2.3.4/src/envctl/cli/commands/sync/command.py +35 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/presenters/__init__.py +2 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/presenters/action_presenter.py +21 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/presenters/resolution_presenter.py +10 -1
- envctl-2.3.4/src/envctl/cli/presenters/run_presenter.py +11 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/prompts/confirmation_prompts.py +3 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/runtime.py +8 -1
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/serializers.py +12 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/config/loader.py +3 -24
- envctl-2.3.4/src/envctl/config/profile_resolution.py +54 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/contract.py +12 -2
- envctl-2.3.4/src/envctl/domain/expansion.py +101 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/operations.py +12 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/resolution.py +6 -0
- envctl-2.3.4/src/envctl/repository/profile_repository.py +140 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/add_service.py +13 -15
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/check_service.py +4 -1
- envctl-2.3.4/src/envctl/services/doctor_service.py +161 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/export_service.py +28 -6
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/fill_service.py +13 -13
- envctl-2.3.4/src/envctl/services/group_selection_service.py +65 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/inspect_service.py +4 -1
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/profile_service.py +14 -40
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/rebind_service.py +6 -8
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/remove_service.py +35 -32
- envctl-2.3.4/src/envctl/services/resolution_service.py +506 -0
- envctl-2.3.4/src/envctl/services/run_service.py +127 -0
- envctl-2.3.4/src/envctl/services/set_service.py +35 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/status_service.py +6 -2
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/sync_service.py +24 -7
- envctl-2.3.4/src/envctl/services/unset_service.py +35 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/vault_service.py +51 -31
- envctl-2.3.4/src/envctl/utils/projection_rendering.py +77 -0
- {envctl-2.3.2 → envctl-2.3.4/src/envctl.egg-info}/PKG-INFO +81 -1
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl.egg-info/SOURCES.txt +7 -0
- envctl-2.3.2/src/envctl/cli/commands/export/command.py +0 -16
- envctl-2.3.2/src/envctl/cli/commands/sync/command.py +0 -16
- envctl-2.3.2/src/envctl/services/doctor_service.py +0 -177
- envctl-2.3.2/src/envctl/services/resolution_service.py +0 -189
- envctl-2.3.2/src/envctl/services/run_service.py +0 -56
- envctl-2.3.2/src/envctl/services/set_service.py +0 -35
- envctl-2.3.2/src/envctl/services/unset_service.py +0 -35
- {envctl-2.3.2 → envctl-2.3.4}/LICENSE +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/setup.cfg +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/__main__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/adapters/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/adapters/dotenv.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/adapters/editor.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/adapters/git.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/adapters/input.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/callbacks.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/add/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/add/command.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/check/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/config/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/config/app.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/doctor/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/doctor/command.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/explain/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/export/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/fill/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/fill/command.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/init/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/init/command.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/inspect/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/profile/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/profile/app.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/profile/commands/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/profile/commands/copy.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/profile/commands/create.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/profile/commands/list.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/profile/commands/path.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/profile/commands/remove.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/project/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/project/app.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/project/commands/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/project/commands/bind.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/project/commands/rebind.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/project/commands/repair.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/project/commands/unbind.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/remove/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/run/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/set/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/set/command.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/status/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/status/command.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/sync/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/unset/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/unset/command.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/vault/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/vault/app.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/vault/commands/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/vault/commands/check.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/vault/commands/edit.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/vault/commands/path.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/vault/commands/prune.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/commands/vault/commands/show.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/decorators.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/presenters/common.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/presenters/doctor_presenter.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/presenters/profile_presenter.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/presenters/project_presenter.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/presenters/status_presenter.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/presenters/vault_presenter.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/cli/prompts/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/config/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/config/defaults.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/config/writer.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/constants.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/app_config.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/contract_inference.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/doctor.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/project.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/runtime.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/domain/status.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/errors.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/repository/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/repository/contract_repository.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/repository/project_context.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/repository/state_repository.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/bind_service.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/config_service.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/context_service.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/explain_service.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/init_service.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/repair_service.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/services/unbind_service.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/__init__.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/atomic.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/filesystem.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/masking.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/output.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/project_ids.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/project_names.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/project_paths.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/shells.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl/utils/tilde.py +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl.egg-info/dependency_links.txt +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl.egg-info/entry_points.txt +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl.egg-info/requires.txt +0 -0
- {envctl-2.3.2 → envctl-2.3.4}/src/envctl.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envctl
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.4
|
|
4
4
|
Summary: Local environment control plane for contract-driven development workflows
|
|
5
5
|
Author-email: Alessandro Barbagallo <alessbarb@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -97,6 +97,8 @@ envctl check # validate against the contract
|
|
|
97
97
|
envctl run -- python app.py # run with env injected
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
If you use `envctl run -- docker run ...`, `envctl` injects into the Docker client process, not directly into the container. Forward container variables explicitly with `-e`, `--env`, or `--env-file`.
|
|
101
|
+
|
|
100
102
|
---
|
|
101
103
|
|
|
102
104
|
## Why not just `.env.local`?
|
|
@@ -134,6 +136,13 @@ Think of it like this:
|
|
|
134
136
|
|
|
135
137
|
> the repo defines the rules, your machine provides the data, and envctl builds the final environment.
|
|
136
138
|
|
|
139
|
+
Resolution now includes placeholder expansion as part of the runtime model, so `check`, `inspect`,
|
|
140
|
+
`run`, `sync`, and `export` all see the same final value.
|
|
141
|
+
|
|
142
|
+
Contracts can also attach an optional human-facing `group` label to variables for organization,
|
|
143
|
+
filtering, and dotenv section rendering. `group` is not a namespace, is not hierarchical, and does
|
|
144
|
+
not change resolution or dependency semantics.
|
|
145
|
+
|
|
137
146
|
---
|
|
138
147
|
|
|
139
148
|
### Example contract
|
|
@@ -162,6 +171,12 @@ variables:
|
|
|
162
171
|
format: json
|
|
163
172
|
required: false
|
|
164
173
|
sensitive: false
|
|
174
|
+
APP_URL:
|
|
175
|
+
type: string
|
|
176
|
+
required: true
|
|
177
|
+
sensitive: false
|
|
178
|
+
group: Application
|
|
179
|
+
default: http://${APP_NAME}:${PORT}
|
|
165
180
|
```
|
|
166
181
|
|
|
167
182
|
This file describes what exists.
|
|
@@ -169,6 +184,71 @@ It never contains real values.
|
|
|
169
184
|
|
|
170
185
|
---
|
|
171
186
|
|
|
187
|
+
## Variable expansion
|
|
188
|
+
|
|
189
|
+
`envctl` supports explicit placeholder expansion with `${VAR}` during resolution.
|
|
190
|
+
|
|
191
|
+
That means the expansion happens before projection, so the effective expanded value is what:
|
|
192
|
+
|
|
193
|
+
* `inspect` shows
|
|
194
|
+
* `check` validates
|
|
195
|
+
* `run` injects
|
|
196
|
+
* `sync` writes
|
|
197
|
+
* `export` prints
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
|
|
201
|
+
```dotenv
|
|
202
|
+
INFRA_NEO4J_USER=neo4j
|
|
203
|
+
INFRA_NEO4J_PASSWORD=super-secret
|
|
204
|
+
INFRA_NEO4J_AUTH=${INFRA_NEO4J_USER}/${INFRA_NEO4J_PASSWORD}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`INFRA_NEO4J_AUTH` resolves to the final runtime value, not the literal expression.
|
|
208
|
+
|
|
209
|
+
Rules:
|
|
210
|
+
|
|
211
|
+
* only `${VAR}` is supported in v1
|
|
212
|
+
* `$VAR` stays literal
|
|
213
|
+
* if `VAR` is a declared envctl key, envctl resolves that key first
|
|
214
|
+
* otherwise envctl falls back to the current process environment
|
|
215
|
+
* `${HOME}` works when `HOME` exists in the current process environment
|
|
216
|
+
* malformed placeholders or unresolved references make resolution invalid
|
|
217
|
+
|
|
218
|
+
Compatibility notes:
|
|
219
|
+
|
|
220
|
+
* before this feature, `${HOME}` stayed literal
|
|
221
|
+
* now `${HOME}` is expanded during resolution
|
|
222
|
+
* `${...}` literal escaping is not supported in v1
|
|
223
|
+
|
|
224
|
+
## Optional groups
|
|
225
|
+
|
|
226
|
+
Each contract variable may define an optional `group` label:
|
|
227
|
+
|
|
228
|
+
```yaml
|
|
229
|
+
variables:
|
|
230
|
+
DATABASE_URL:
|
|
231
|
+
type: url
|
|
232
|
+
required: true
|
|
233
|
+
sensitive: true
|
|
234
|
+
group: Database
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`group` is used only for:
|
|
238
|
+
|
|
239
|
+
* organization in the contract
|
|
240
|
+
* CLI targeting with `--group`
|
|
241
|
+
* grouped dotenv output from `sync` and `export --format dotenv`
|
|
242
|
+
|
|
243
|
+
It does not:
|
|
244
|
+
|
|
245
|
+
* create namespaces
|
|
246
|
+
* affect `${VAR}` expansion rules
|
|
247
|
+
* restrict cross-variable references
|
|
248
|
+
* imply hierarchy, inheritance, or prefix matching
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
172
252
|
## Profiles
|
|
173
253
|
|
|
174
254
|
Instead of juggling multiple `.env` files:
|
|
@@ -58,6 +58,8 @@ envctl check # validate against the contract
|
|
|
58
58
|
envctl run -- python app.py # run with env injected
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
If you use `envctl run -- docker run ...`, `envctl` injects into the Docker client process, not directly into the container. Forward container variables explicitly with `-e`, `--env`, or `--env-file`.
|
|
62
|
+
|
|
61
63
|
---
|
|
62
64
|
|
|
63
65
|
## Why not just `.env.local`?
|
|
@@ -95,6 +97,13 @@ Think of it like this:
|
|
|
95
97
|
|
|
96
98
|
> the repo defines the rules, your machine provides the data, and envctl builds the final environment.
|
|
97
99
|
|
|
100
|
+
Resolution now includes placeholder expansion as part of the runtime model, so `check`, `inspect`,
|
|
101
|
+
`run`, `sync`, and `export` all see the same final value.
|
|
102
|
+
|
|
103
|
+
Contracts can also attach an optional human-facing `group` label to variables for organization,
|
|
104
|
+
filtering, and dotenv section rendering. `group` is not a namespace, is not hierarchical, and does
|
|
105
|
+
not change resolution or dependency semantics.
|
|
106
|
+
|
|
98
107
|
---
|
|
99
108
|
|
|
100
109
|
### Example contract
|
|
@@ -123,6 +132,12 @@ variables:
|
|
|
123
132
|
format: json
|
|
124
133
|
required: false
|
|
125
134
|
sensitive: false
|
|
135
|
+
APP_URL:
|
|
136
|
+
type: string
|
|
137
|
+
required: true
|
|
138
|
+
sensitive: false
|
|
139
|
+
group: Application
|
|
140
|
+
default: http://${APP_NAME}:${PORT}
|
|
126
141
|
```
|
|
127
142
|
|
|
128
143
|
This file describes what exists.
|
|
@@ -130,6 +145,71 @@ It never contains real values.
|
|
|
130
145
|
|
|
131
146
|
---
|
|
132
147
|
|
|
148
|
+
## Variable expansion
|
|
149
|
+
|
|
150
|
+
`envctl` supports explicit placeholder expansion with `${VAR}` during resolution.
|
|
151
|
+
|
|
152
|
+
That means the expansion happens before projection, so the effective expanded value is what:
|
|
153
|
+
|
|
154
|
+
* `inspect` shows
|
|
155
|
+
* `check` validates
|
|
156
|
+
* `run` injects
|
|
157
|
+
* `sync` writes
|
|
158
|
+
* `export` prints
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
|
|
162
|
+
```dotenv
|
|
163
|
+
INFRA_NEO4J_USER=neo4j
|
|
164
|
+
INFRA_NEO4J_PASSWORD=super-secret
|
|
165
|
+
INFRA_NEO4J_AUTH=${INFRA_NEO4J_USER}/${INFRA_NEO4J_PASSWORD}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`INFRA_NEO4J_AUTH` resolves to the final runtime value, not the literal expression.
|
|
169
|
+
|
|
170
|
+
Rules:
|
|
171
|
+
|
|
172
|
+
* only `${VAR}` is supported in v1
|
|
173
|
+
* `$VAR` stays literal
|
|
174
|
+
* if `VAR` is a declared envctl key, envctl resolves that key first
|
|
175
|
+
* otherwise envctl falls back to the current process environment
|
|
176
|
+
* `${HOME}` works when `HOME` exists in the current process environment
|
|
177
|
+
* malformed placeholders or unresolved references make resolution invalid
|
|
178
|
+
|
|
179
|
+
Compatibility notes:
|
|
180
|
+
|
|
181
|
+
* before this feature, `${HOME}` stayed literal
|
|
182
|
+
* now `${HOME}` is expanded during resolution
|
|
183
|
+
* `${...}` literal escaping is not supported in v1
|
|
184
|
+
|
|
185
|
+
## Optional groups
|
|
186
|
+
|
|
187
|
+
Each contract variable may define an optional `group` label:
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
variables:
|
|
191
|
+
DATABASE_URL:
|
|
192
|
+
type: url
|
|
193
|
+
required: true
|
|
194
|
+
sensitive: true
|
|
195
|
+
group: Database
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`group` is used only for:
|
|
199
|
+
|
|
200
|
+
* organization in the contract
|
|
201
|
+
* CLI targeting with `--group`
|
|
202
|
+
* grouped dotenv output from `sync` and `export --format dotenv`
|
|
203
|
+
|
|
204
|
+
It does not:
|
|
205
|
+
|
|
206
|
+
* create namespaces
|
|
207
|
+
* affect `${VAR}` expansion rules
|
|
208
|
+
* restrict cross-variable references
|
|
209
|
+
* imply hierarchy, inheritance, or prefix matching
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
133
213
|
## Profiles
|
|
134
214
|
|
|
135
215
|
Instead of juggling multiple `.env` files:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "envctl"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.4"
|
|
8
8
|
description = "Local environment control plane for contract-driven development workflows"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -71,7 +71,7 @@ where = ["src"]
|
|
|
71
71
|
|
|
72
72
|
[tool.pytest.ini_options]
|
|
73
73
|
testpaths = ["tests"]
|
|
74
|
-
addopts = "-q --strict-markers --strict-config"
|
|
74
|
+
addopts = "-q --strict-markers --strict-config --import-mode=importlib"
|
|
75
75
|
pythonpath = ["src"]
|
|
76
76
|
|
|
77
77
|
[tool.ruff]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Access to process environment variables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProcessEnvironmentProvider:
|
|
10
|
+
"""Resolve variables from the current process environment."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, environ: Mapping[str, str] | None = None) -> None:
|
|
13
|
+
self._environ = environ if environ is not None else os.environ
|
|
14
|
+
|
|
15
|
+
def get(self, key: str) -> str | None:
|
|
16
|
+
"""Return one process environment value."""
|
|
17
|
+
return self._environ.get(key)
|
|
@@ -24,7 +24,8 @@ from envctl.cli.commands.sync import sync_command
|
|
|
24
24
|
from envctl.cli.commands.unset import unset_command
|
|
25
25
|
from envctl.cli.commands.vault import vault_app
|
|
26
26
|
from envctl.cli.runtime import set_cli_state
|
|
27
|
-
from envctl.config.loader import
|
|
27
|
+
from envctl.config.loader import load_config
|
|
28
|
+
from envctl.config.profile_resolution import resolve_active_profile
|
|
28
29
|
from envctl.domain.runtime import OutputFormat
|
|
29
30
|
|
|
30
31
|
VERSION_OPTION = typer.Option(
|
|
@@ -46,6 +47,12 @@ PROFILE_OPTION = typer.Option(
|
|
|
46
47
|
"-p",
|
|
47
48
|
help="Select the active environment profile.",
|
|
48
49
|
)
|
|
50
|
+
GROUP_OPTION = typer.Option(
|
|
51
|
+
None,
|
|
52
|
+
"--group",
|
|
53
|
+
"-g",
|
|
54
|
+
help="Target only variables whose contract group matches LABEL exactly.",
|
|
55
|
+
)
|
|
49
56
|
|
|
50
57
|
app = typer.Typer(help="envctl - local environment control plane")
|
|
51
58
|
app.add_typer(config_app, name="config")
|
|
@@ -60,16 +67,22 @@ def main(
|
|
|
60
67
|
version: bool = VERSION_OPTION,
|
|
61
68
|
json_output: bool = JSON_OPTION,
|
|
62
69
|
profile: str | None = PROFILE_OPTION,
|
|
70
|
+
group: str | None = GROUP_OPTION,
|
|
63
71
|
) -> None:
|
|
64
72
|
"""envctl - local environment control plane."""
|
|
65
73
|
del version
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
config = load_config()
|
|
76
|
+
active_profile = resolve_active_profile(
|
|
77
|
+
profile,
|
|
78
|
+
config_default_profile=config.default_profile,
|
|
79
|
+
)
|
|
68
80
|
|
|
69
81
|
set_cli_state(
|
|
70
82
|
ctx,
|
|
71
83
|
output_format=OutputFormat.JSON if json_output else OutputFormat.TEXT,
|
|
72
84
|
profile=active_profile,
|
|
85
|
+
group=group.strip() or None if group is not None else None,
|
|
73
86
|
)
|
|
74
87
|
|
|
75
88
|
|
|
@@ -6,7 +6,7 @@ import typer
|
|
|
6
6
|
|
|
7
7
|
from envctl.cli.decorators import handle_errors
|
|
8
8
|
from envctl.cli.presenters import render_resolution_view
|
|
9
|
-
from envctl.cli.runtime import get_active_profile, is_json_output
|
|
9
|
+
from envctl.cli.runtime import get_active_profile, get_selected_group, is_json_output
|
|
10
10
|
from envctl.cli.serializers import (
|
|
11
11
|
emit_json,
|
|
12
12
|
serialize_project_context,
|
|
@@ -24,7 +24,8 @@ def _is_check_ok(*, is_valid: bool, unknown_keys: list[str] | tuple[str, ...]) -
|
|
|
24
24
|
@handle_errors
|
|
25
25
|
def check_command() -> None:
|
|
26
26
|
"""Validate the current project environment against the contract."""
|
|
27
|
-
|
|
27
|
+
selected_group = get_selected_group()
|
|
28
|
+
context, active_profile, report = run_check(get_active_profile(), group=selected_group)
|
|
28
29
|
ok = _is_check_ok(
|
|
29
30
|
is_valid=report.is_valid,
|
|
30
31
|
unknown_keys=report.unknown_keys,
|
|
@@ -37,6 +38,7 @@ def check_command() -> None:
|
|
|
37
38
|
"command": "check",
|
|
38
39
|
"data": {
|
|
39
40
|
"active_profile": active_profile,
|
|
41
|
+
"selected_group": selected_group,
|
|
40
42
|
"context": serialize_project_context(context),
|
|
41
43
|
"report": serialize_resolution_report(report),
|
|
42
44
|
},
|
|
@@ -48,6 +50,7 @@ def check_command() -> None:
|
|
|
48
50
|
|
|
49
51
|
render_resolution_view(
|
|
50
52
|
profile=active_profile,
|
|
53
|
+
group=selected_group,
|
|
51
54
|
report=report,
|
|
52
55
|
)
|
|
53
56
|
|
|
@@ -34,8 +34,12 @@ def explain_command(key: str = typer.Argument(...)) -> None:
|
|
|
34
34
|
profile=active_profile,
|
|
35
35
|
key=item.key,
|
|
36
36
|
source=item.source,
|
|
37
|
+
raw_value=item.raw_value,
|
|
37
38
|
value=item.value,
|
|
38
39
|
masked=item.masked,
|
|
40
|
+
expansion_status=item.expansion_status,
|
|
41
|
+
expansion_refs=item.expansion_refs,
|
|
42
|
+
expansion_error=item.expansion_error.detail if item.expansion_error is not None else None,
|
|
39
43
|
valid=item.valid,
|
|
40
44
|
detail=item.detail,
|
|
41
45
|
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Export command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from envctl.cli.decorators import handle_errors, text_output_only
|
|
10
|
+
from envctl.cli.presenters import render_export_output
|
|
11
|
+
from envctl.cli.runtime import get_active_profile, get_selected_group
|
|
12
|
+
from envctl.services.export_service import run_export
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@handle_errors
|
|
16
|
+
@text_output_only("export")
|
|
17
|
+
def export_command(
|
|
18
|
+
format: Literal["shell", "dotenv"] = typer.Option(
|
|
19
|
+
"shell",
|
|
20
|
+
"--format",
|
|
21
|
+
help=(
|
|
22
|
+
"Choose the export format: 'shell' prints shell export lines, "
|
|
23
|
+
"'dotenv' prints KEY=value lines to stdout."
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Print the resolved environment as shell export lines."""
|
|
28
|
+
_context, active_profile, rendered = run_export(
|
|
29
|
+
get_active_profile(),
|
|
30
|
+
format=format,
|
|
31
|
+
group=get_selected_group(),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if format == "dotenv":
|
|
35
|
+
print(rendered, end="")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
render_export_output(profile=active_profile, rendered=rendered)
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from envctl.cli.decorators import handle_errors
|
|
6
6
|
from envctl.cli.presenters import render_resolution_view
|
|
7
|
-
from envctl.cli.runtime import get_active_profile, is_json_output
|
|
7
|
+
from envctl.cli.runtime import get_active_profile, get_selected_group, is_json_output
|
|
8
8
|
from envctl.cli.serializers import (
|
|
9
9
|
emit_json,
|
|
10
10
|
serialize_project_context,
|
|
@@ -16,7 +16,8 @@ from envctl.services.inspect_service import run_inspect
|
|
|
16
16
|
@handle_errors
|
|
17
17
|
def inspect_command() -> None:
|
|
18
18
|
"""Inspect the resolved environment."""
|
|
19
|
-
|
|
19
|
+
selected_group = get_selected_group()
|
|
20
|
+
context, active_profile, report = run_inspect(get_active_profile(), group=selected_group)
|
|
20
21
|
|
|
21
22
|
if is_json_output():
|
|
22
23
|
emit_json(
|
|
@@ -25,6 +26,7 @@ def inspect_command() -> None:
|
|
|
25
26
|
"command": "inspect",
|
|
26
27
|
"data": {
|
|
27
28
|
"active_profile": active_profile,
|
|
29
|
+
"selected_group": selected_group,
|
|
28
30
|
"context": serialize_project_context(context),
|
|
29
31
|
"report": serialize_resolution_report(report),
|
|
30
32
|
},
|
|
@@ -34,5 +36,6 @@ def inspect_command() -> None:
|
|
|
34
36
|
|
|
35
37
|
render_resolution_view(
|
|
36
38
|
profile=active_profile,
|
|
39
|
+
group=selected_group,
|
|
37
40
|
report=report,
|
|
38
41
|
)
|
|
@@ -45,7 +45,9 @@ def remove_command(
|
|
|
45
45
|
key=key,
|
|
46
46
|
contract_path=result.repo_contract_path,
|
|
47
47
|
removed_from_contract=result.removed_from_contract,
|
|
48
|
+
inspected_profiles=result.inspected_profiles,
|
|
48
49
|
removed_from_profiles=result.removed_from_profiles,
|
|
50
|
+
missing_from_profiles=result.missing_from_profiles,
|
|
49
51
|
affected_paths=result.affected_paths,
|
|
50
52
|
repo_root=context.repo_root,
|
|
51
53
|
)
|
|
@@ -5,7 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from envctl.cli.decorators import handle_errors, text_output_only
|
|
8
|
-
from envctl.cli.
|
|
8
|
+
from envctl.cli.presenters.run_presenter import render_run_warnings
|
|
9
|
+
from envctl.cli.runtime import get_active_profile, get_selected_group
|
|
9
10
|
from envctl.services.run_service import run_command
|
|
10
11
|
|
|
11
12
|
COMMAND_ARGUMENT = typer.Argument(...)
|
|
@@ -15,8 +16,10 @@ COMMAND_ARGUMENT = typer.Argument(...)
|
|
|
15
16
|
@text_output_only("run")
|
|
16
17
|
def run_command_cli(command: list[str] = COMMAND_ARGUMENT) -> None:
|
|
17
18
|
"""Run a child process with the resolved environment injected."""
|
|
18
|
-
_context,
|
|
19
|
+
_context, result = run_command(
|
|
19
20
|
command,
|
|
20
21
|
get_active_profile(),
|
|
22
|
+
group=get_selected_group(),
|
|
21
23
|
)
|
|
22
|
-
|
|
24
|
+
render_run_warnings(result.warnings)
|
|
25
|
+
raise typer.Exit(code=result.exit_code)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Sync command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from envctl.cli.decorators import handle_errors, requires_writable_runtime
|
|
10
|
+
from envctl.cli.presenters import render_sync_result
|
|
11
|
+
from envctl.cli.runtime import get_active_profile, get_selected_group
|
|
12
|
+
from envctl.services.sync_service import run_sync
|
|
13
|
+
|
|
14
|
+
OUTPUT_OPTION = typer.Option(
|
|
15
|
+
None,
|
|
16
|
+
"--output",
|
|
17
|
+
help=(
|
|
18
|
+
"Write the generated dotenv file to PATH instead of the default "
|
|
19
|
+
"profile-derived repo-local target."
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@handle_errors
|
|
25
|
+
@requires_writable_runtime("sync")
|
|
26
|
+
def sync_command(
|
|
27
|
+
output: Path | None = OUTPUT_OPTION,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Materialize the resolved environment into the repository env file."""
|
|
30
|
+
_context, active_profile, target_path = run_sync(
|
|
31
|
+
get_active_profile(),
|
|
32
|
+
output_path=output,
|
|
33
|
+
group=get_selected_group(),
|
|
34
|
+
)
|
|
35
|
+
render_sync_result(profile=active_profile, target_path=target_path)
|
|
@@ -32,6 +32,7 @@ from envctl.cli.presenters.resolution_presenter import (
|
|
|
32
32
|
render_resolution,
|
|
33
33
|
render_resolution_view,
|
|
34
34
|
)
|
|
35
|
+
from envctl.cli.presenters.run_presenter import render_run_warnings
|
|
35
36
|
from envctl.cli.presenters.status_presenter import (
|
|
36
37
|
render_status,
|
|
37
38
|
render_status_view,
|
|
@@ -71,6 +72,7 @@ __all__ = [
|
|
|
71
72
|
"render_remove_result",
|
|
72
73
|
"render_resolution",
|
|
73
74
|
"render_resolution_view",
|
|
75
|
+
"render_run_warnings",
|
|
74
76
|
"render_set_result",
|
|
75
77
|
"render_status",
|
|
76
78
|
"render_status_view",
|
|
@@ -74,8 +74,12 @@ def render_explain_value(
|
|
|
74
74
|
profile: str,
|
|
75
75
|
key: str,
|
|
76
76
|
source: str,
|
|
77
|
+
raw_value: str | None,
|
|
77
78
|
value: str,
|
|
78
79
|
masked: bool,
|
|
80
|
+
expansion_status: str,
|
|
81
|
+
expansion_refs: tuple[str, ...],
|
|
82
|
+
expansion_error: str | None,
|
|
79
83
|
valid: bool,
|
|
80
84
|
detail: str | None,
|
|
81
85
|
) -> None:
|
|
@@ -85,7 +89,14 @@ def render_explain_value(
|
|
|
85
89
|
print_kv("profile", profile)
|
|
86
90
|
print_kv("key", key)
|
|
87
91
|
print_kv("source", source)
|
|
92
|
+
if raw_value is not None:
|
|
93
|
+
print_kv("raw_value", raw_value)
|
|
88
94
|
print_kv("value", shown_value)
|
|
95
|
+
print_kv("expansion_status", expansion_status)
|
|
96
|
+
if expansion_refs:
|
|
97
|
+
print_kv("expansion_refs", ", ".join(expansion_refs))
|
|
98
|
+
if expansion_error is not None:
|
|
99
|
+
print_kv("expansion_error", expansion_error)
|
|
89
100
|
print_kv("valid", "yes" if valid else "no")
|
|
90
101
|
|
|
91
102
|
if detail:
|
|
@@ -166,7 +177,9 @@ def render_remove_result(
|
|
|
166
177
|
key: str,
|
|
167
178
|
contract_path: Path,
|
|
168
179
|
removed_from_contract: bool,
|
|
180
|
+
inspected_profiles: tuple[str, ...],
|
|
169
181
|
removed_from_profiles: tuple[str, ...],
|
|
182
|
+
missing_from_profiles: tuple[str, ...],
|
|
170
183
|
affected_paths: tuple[Path, ...],
|
|
171
184
|
repo_root: Path,
|
|
172
185
|
) -> None:
|
|
@@ -178,6 +191,14 @@ def render_remove_result(
|
|
|
178
191
|
"removed_from_profiles",
|
|
179
192
|
", ".join(removed_from_profiles) if removed_from_profiles else "none",
|
|
180
193
|
)
|
|
194
|
+
print_kv(
|
|
195
|
+
"inspected_profiles",
|
|
196
|
+
", ".join(inspected_profiles) if inspected_profiles else "none",
|
|
197
|
+
)
|
|
198
|
+
print_kv(
|
|
199
|
+
"missing_from_profiles",
|
|
200
|
+
", ".join(missing_from_profiles) if missing_from_profiles else "none",
|
|
201
|
+
)
|
|
181
202
|
|
|
182
203
|
if affected_paths:
|
|
183
204
|
print_kv("affected_paths", ", ".join(str(path) for path in affected_paths))
|
|
@@ -54,7 +54,13 @@ def _render_resolved_values(report: ResolutionReport) -> None:
|
|
|
54
54
|
item = report.values[key]
|
|
55
55
|
shown_value = mask_value(item.value) if item.masked else item.value
|
|
56
56
|
suffix = "" if item.valid else f" — invalid: {item.detail or 'unknown reason'}"
|
|
57
|
-
|
|
57
|
+
expansion_suffix = ""
|
|
58
|
+
if item.expansion_status == "expanded":
|
|
59
|
+
refs = ", ".join(item.expansion_refs)
|
|
60
|
+
expansion_suffix = f" [expanded{': ' + refs if refs else ''}]"
|
|
61
|
+
elif item.expansion_status == "error" and item.expansion_error is not None:
|
|
62
|
+
expansion_suffix = f" [expansion error: {item.expansion_error.kind}]"
|
|
63
|
+
typer.echo(f" {key} = {shown_value} ({item.source}){expansion_suffix}{suffix}")
|
|
58
64
|
|
|
59
65
|
|
|
60
66
|
def render_resolution(report: ResolutionReport) -> None:
|
|
@@ -71,8 +77,11 @@ def render_resolution(report: ResolutionReport) -> None:
|
|
|
71
77
|
def render_resolution_view(
|
|
72
78
|
*,
|
|
73
79
|
profile: str,
|
|
80
|
+
group: str | None,
|
|
74
81
|
report: ResolutionReport,
|
|
75
82
|
) -> None:
|
|
76
83
|
"""Render one resolved environment view including the active profile."""
|
|
77
84
|
print_kv("profile", profile)
|
|
85
|
+
if group is not None:
|
|
86
|
+
print_kv("group", group)
|
|
78
87
|
render_resolution(report)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Presenters for run command advisories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from envctl.utils.output import print_warning
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_run_warnings(warnings: tuple[str, ...]) -> None:
|
|
9
|
+
"""Render run command warnings."""
|
|
10
|
+
for warning in warnings:
|
|
11
|
+
print_warning(warning)
|
|
@@ -25,6 +25,9 @@ def build_remove_confirmation_message(key: str, plan: RemovePlan) -> str:
|
|
|
25
25
|
if plan.present_in_other_profiles:
|
|
26
26
|
lines.append(f"- also present in: {', '.join(plan.present_in_other_profiles)}")
|
|
27
27
|
|
|
28
|
+
if plan.absent_in_other_profiles:
|
|
29
|
+
lines.append(f"- not present in: {', '.join(plan.absent_in_other_profiles)}")
|
|
30
|
+
|
|
28
31
|
return "\n".join(lines)
|
|
29
32
|
|
|
30
33
|
|
|
@@ -17,6 +17,7 @@ class CliState:
|
|
|
17
17
|
|
|
18
18
|
output_format: OutputFormat = OutputFormat.TEXT
|
|
19
19
|
profile: str = DEFAULT_PROFILE
|
|
20
|
+
group: str | None = None
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def set_cli_state(
|
|
@@ -24,9 +25,10 @@ def set_cli_state(
|
|
|
24
25
|
*,
|
|
25
26
|
output_format: OutputFormat,
|
|
26
27
|
profile: str = DEFAULT_PROFILE,
|
|
28
|
+
group: str | None = None,
|
|
27
29
|
) -> None:
|
|
28
30
|
"""Persist the CLI state on the Typer/Click context."""
|
|
29
|
-
ctx.obj = CliState(output_format=output_format, profile=profile)
|
|
31
|
+
ctx.obj = CliState(output_format=output_format, profile=profile, group=group)
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
def get_cli_state() -> CliState:
|
|
@@ -52,6 +54,11 @@ def get_active_profile() -> str:
|
|
|
52
54
|
return get_cli_state().profile
|
|
53
55
|
|
|
54
56
|
|
|
57
|
+
def get_selected_group() -> str | None:
|
|
58
|
+
"""Return the active CLI group filter."""
|
|
59
|
+
return get_cli_state().group
|
|
60
|
+
|
|
61
|
+
|
|
55
62
|
def get_command_path() -> str | None:
|
|
56
63
|
"""Return the current command path when available.
|
|
57
64
|
|
|
@@ -46,12 +46,24 @@ def serialize_project_context(context: ProjectContext) -> dict[str, Any]:
|
|
|
46
46
|
|
|
47
47
|
def serialize_resolved_value(item: ResolvedValue) -> dict[str, Any]:
|
|
48
48
|
"""Serialize one resolved value."""
|
|
49
|
+
shown_raw_value = None if item.raw_value is None or item.masked else item.raw_value
|
|
49
50
|
shown_value = mask_value(item.value) if item.masked else item.value
|
|
50
51
|
return {
|
|
51
52
|
"key": item.key,
|
|
53
|
+
"raw_value": shown_raw_value,
|
|
52
54
|
"value": shown_value,
|
|
53
55
|
"source": item.source,
|
|
54
56
|
"masked": item.masked,
|
|
57
|
+
"expansion_status": item.expansion_status,
|
|
58
|
+
"expansion_refs": list(item.expansion_refs),
|
|
59
|
+
"expansion_error": (
|
|
60
|
+
{
|
|
61
|
+
"kind": item.expansion_error.kind,
|
|
62
|
+
"detail": item.expansion_error.detail,
|
|
63
|
+
}
|
|
64
|
+
if item.expansion_error is not None
|
|
65
|
+
else None
|
|
66
|
+
),
|
|
55
67
|
"valid": item.valid,
|
|
56
68
|
"detail": item.detail,
|
|
57
69
|
}
|