envrcctl 0.2.0__tar.gz → 0.2.2__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.
- {envrcctl-0.2.0 → envrcctl-0.2.2}/PKG-INFO +50 -61
- {envrcctl-0.2.0 → envrcctl-0.2.2}/README.md +19 -34
- envrcctl-0.2.2/pyproject.toml +56 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/cli.py +15 -44
- envrcctl-0.2.0/.gitignore +0 -11
- envrcctl-0.2.0/pyproject.toml +0 -31
- envrcctl-0.2.0/scripts/build_macos_auth_helper.sh +0 -43
- envrcctl-0.2.0/scripts/generate_completions.py +0 -35
- envrcctl-0.2.0/scripts/macos/envrcctl-macos-auth.swift +0 -340
- {envrcctl-0.2.0 → envrcctl-0.2.2}/LICENSE +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/completions/envrcctl.bash +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/completions/envrcctl.fish +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/completions/envrcctl.zsh +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/__init__.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/audit.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/auth.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/command_runner.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/envrc.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/envrcctl-macos-auth +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/errors.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/keychain.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/main.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/managed_block.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/secrets.py +0 -0
- {envrcctl-0.2.0 → envrcctl-0.2.2}/src/envrcctl/secretservice.py +0 -0
|
@@ -1,35 +1,39 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: envrcctl
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Manage .envrc with managed blocks and OS-backed secrets.
|
|
5
|
+
Author: Rio Fujita
|
|
6
|
+
Author-email: Rio Fujita <rio_github@rio.st>
|
|
5
7
|
License: MIT License
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
License-File: LICENSE
|
|
27
|
-
Requires-Python: >=3.14
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Rio Fujita
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
28
|
Requires-Dist: typer>=0.24.1
|
|
29
|
+
Requires-Dist: pytest>=9 ; extra == 'test'
|
|
30
|
+
Requires-Dist: pytest-cov>=7 ; extra == 'test'
|
|
31
|
+
Requires-Dist: bandit>=1.7.10 ; extra == 'test'
|
|
32
|
+
Requires-Python: >=3.14
|
|
33
|
+
Project-URL: Homepage, https://github.com/rioriost/envrcctl
|
|
34
|
+
Project-URL: Issues, https://github.com/rioriost/envrcctl/issues
|
|
35
|
+
Project-URL: Repository, https://github.com/rioriost/envrcctl
|
|
29
36
|
Provides-Extra: test
|
|
30
|
-
Requires-Dist: bandit>=1.7.10; extra == 'test'
|
|
31
|
-
Requires-Dist: pytest-cov>=7.0.0; extra == 'test'
|
|
32
|
-
Requires-Dist: pytest>=9.0.2; extra == 'test'
|
|
33
37
|
Description-Content-Type: text/markdown
|
|
34
38
|
|
|
35
39
|
# envrcctl
|
|
@@ -63,16 +67,21 @@ It is designed for macOS first, with Linux support via SecretService.
|
|
|
63
67
|
|
|
64
68
|
## Installation
|
|
65
69
|
|
|
66
|
-
### macOS (Homebrew)
|
|
70
|
+
### macOS (Homebrew, Apple Silicon)
|
|
67
71
|
|
|
68
72
|
Tap and install:
|
|
69
73
|
|
|
70
74
|
```sh
|
|
71
|
-
brew tap rioriost/
|
|
75
|
+
brew tap rioriost/tap
|
|
72
76
|
brew install envrcctl
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
This Homebrew path is intended for:
|
|
80
|
+
|
|
81
|
+
- Apple Silicon (`arm64`) Macs
|
|
82
|
+
- macOS installs that should not require a full Xcode.app build dependency
|
|
83
|
+
|
|
84
|
+
Intel Macs are not a target for this Homebrew distribution path.
|
|
76
85
|
|
|
77
86
|
Install direnv with Homebrew:
|
|
78
87
|
|
|
@@ -92,26 +101,25 @@ pipx install envrcctl
|
|
|
92
101
|
uv tool install envrcctl
|
|
93
102
|
```
|
|
94
103
|
|
|
95
|
-
###
|
|
96
|
-
|
|
97
|
-
```sh
|
|
98
|
-
git clone <REPO_URL>
|
|
99
|
-
cd envrcctl
|
|
100
|
-
uv sync
|
|
101
|
-
uv run python -m envrcctl.main --help
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Build the macOS auth helper (macOS only)
|
|
104
|
+
### About the macOS auth helper (Apple Silicon macOS only)
|
|
105
105
|
|
|
106
106
|
The macOS device owner authentication flow requires a native helper named
|
|
107
107
|
`envrcctl-macos-auth`.
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
Homebrew on Apple Silicon is intended to install this helper automatically, so
|
|
110
|
+
you should not need to build it yourself in the common case.
|
|
111
|
+
|
|
112
|
+
Manual helper installation is still useful when:
|
|
113
|
+
|
|
114
|
+
- you want to place the helper in a custom location
|
|
115
|
+
- you are not using the Apple Silicon Homebrew distribution path
|
|
116
|
+
|
|
117
|
+
If you are building the helper yourself, use Apple Silicon (`arm64`) macOS and place the binary at either:
|
|
110
118
|
|
|
111
119
|
- `src/envrcctl/envrcctl-macos-auth`
|
|
112
120
|
- or a custom path set via `ENVRCCTL_MACOS_AUTH_HELPER`
|
|
113
121
|
|
|
114
|
-
Example build flow:
|
|
122
|
+
Example build flow on Apple Silicon macOS:
|
|
115
123
|
|
|
116
124
|
```sh
|
|
117
125
|
swiftc -O -framework LocalAuthentication -framework Security \
|
|
@@ -120,20 +128,6 @@ swiftc -O -framework LocalAuthentication -framework Security \
|
|
|
120
128
|
chmod +x src/envrcctl/envrcctl-macos-auth
|
|
121
129
|
```
|
|
122
130
|
|
|
123
|
-
You can also use the repository build script:
|
|
124
|
-
|
|
125
|
-
```sh
|
|
126
|
-
sh scripts/build_macos_auth_helper.sh
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
If you want to write the helper to a custom location, pass the source and output paths explicitly:
|
|
130
|
-
|
|
131
|
-
```sh
|
|
132
|
-
sh scripts/build_macos_auth_helper.sh \
|
|
133
|
-
scripts/macos/envrcctl-macos-auth.swift \
|
|
134
|
-
/usr/local/bin/envrcctl-macos-auth
|
|
135
|
-
```
|
|
136
|
-
|
|
137
131
|
If you install the helper elsewhere, set:
|
|
138
132
|
|
|
139
133
|
```sh
|
|
@@ -380,12 +374,7 @@ uv run python scripts/generate_completions.py
|
|
|
380
374
|
- Audit integrity is based on a hash chain and can be checked with `envrcctl audit verify`
|
|
381
375
|
- The tool refuses to write to world-writable `.envrc`
|
|
382
376
|
|
|
383
|
-
## Development
|
|
384
377
|
|
|
385
|
-
```sh
|
|
386
|
-
uv sync
|
|
387
|
-
.venv/bin/envrcctl --help
|
|
388
|
-
```
|
|
389
378
|
|
|
390
379
|
## Acknowledgements
|
|
391
380
|
|
|
@@ -29,16 +29,21 @@ It is designed for macOS first, with Linux support via SecretService.
|
|
|
29
29
|
|
|
30
30
|
## Installation
|
|
31
31
|
|
|
32
|
-
### macOS (Homebrew)
|
|
32
|
+
### macOS (Homebrew, Apple Silicon)
|
|
33
33
|
|
|
34
34
|
Tap and install:
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
brew tap rioriost/
|
|
37
|
+
brew tap rioriost/tap
|
|
38
38
|
brew install envrcctl
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
This Homebrew path is intended for:
|
|
42
|
+
|
|
43
|
+
- Apple Silicon (`arm64`) Macs
|
|
44
|
+
- macOS installs that should not require a full Xcode.app build dependency
|
|
45
|
+
|
|
46
|
+
Intel Macs are not a target for this Homebrew distribution path.
|
|
42
47
|
|
|
43
48
|
Install direnv with Homebrew:
|
|
44
49
|
|
|
@@ -58,26 +63,25 @@ pipx install envrcctl
|
|
|
58
63
|
uv tool install envrcctl
|
|
59
64
|
```
|
|
60
65
|
|
|
61
|
-
###
|
|
62
|
-
|
|
63
|
-
```sh
|
|
64
|
-
git clone <REPO_URL>
|
|
65
|
-
cd envrcctl
|
|
66
|
-
uv sync
|
|
67
|
-
uv run python -m envrcctl.main --help
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Build the macOS auth helper (macOS only)
|
|
66
|
+
### About the macOS auth helper (Apple Silicon macOS only)
|
|
71
67
|
|
|
72
68
|
The macOS device owner authentication flow requires a native helper named
|
|
73
69
|
`envrcctl-macos-auth`.
|
|
74
70
|
|
|
75
|
-
|
|
71
|
+
Homebrew on Apple Silicon is intended to install this helper automatically, so
|
|
72
|
+
you should not need to build it yourself in the common case.
|
|
73
|
+
|
|
74
|
+
Manual helper installation is still useful when:
|
|
75
|
+
|
|
76
|
+
- you want to place the helper in a custom location
|
|
77
|
+
- you are not using the Apple Silicon Homebrew distribution path
|
|
78
|
+
|
|
79
|
+
If you are building the helper yourself, use Apple Silicon (`arm64`) macOS and place the binary at either:
|
|
76
80
|
|
|
77
81
|
- `src/envrcctl/envrcctl-macos-auth`
|
|
78
82
|
- or a custom path set via `ENVRCCTL_MACOS_AUTH_HELPER`
|
|
79
83
|
|
|
80
|
-
Example build flow:
|
|
84
|
+
Example build flow on Apple Silicon macOS:
|
|
81
85
|
|
|
82
86
|
```sh
|
|
83
87
|
swiftc -O -framework LocalAuthentication -framework Security \
|
|
@@ -86,20 +90,6 @@ swiftc -O -framework LocalAuthentication -framework Security \
|
|
|
86
90
|
chmod +x src/envrcctl/envrcctl-macos-auth
|
|
87
91
|
```
|
|
88
92
|
|
|
89
|
-
You can also use the repository build script:
|
|
90
|
-
|
|
91
|
-
```sh
|
|
92
|
-
sh scripts/build_macos_auth_helper.sh
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
If you want to write the helper to a custom location, pass the source and output paths explicitly:
|
|
96
|
-
|
|
97
|
-
```sh
|
|
98
|
-
sh scripts/build_macos_auth_helper.sh \
|
|
99
|
-
scripts/macos/envrcctl-macos-auth.swift \
|
|
100
|
-
/usr/local/bin/envrcctl-macos-auth
|
|
101
|
-
```
|
|
102
|
-
|
|
103
93
|
If you install the helper elsewhere, set:
|
|
104
94
|
|
|
105
95
|
```sh
|
|
@@ -346,12 +336,7 @@ uv run python scripts/generate_completions.py
|
|
|
346
336
|
- Audit integrity is based on a hash chain and can be checked with `envrcctl audit verify`
|
|
347
337
|
- The tool refuses to write to world-writable `.envrc`
|
|
348
338
|
|
|
349
|
-
## Development
|
|
350
339
|
|
|
351
|
-
```sh
|
|
352
|
-
uv sync
|
|
353
|
-
.venv/bin/envrcctl --help
|
|
354
|
-
```
|
|
355
340
|
|
|
356
341
|
## Acknowledgements
|
|
357
342
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "envrcctl"
|
|
3
|
+
version = "0.2.2"
|
|
4
|
+
description = "Manage .envrc with managed blocks and OS-backed secrets."
|
|
5
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
6
|
+
license = { file = "LICENSE" }
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Rio Fujita", email = "rio_github@rio.st" },
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer>=0.24.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.uv.build-backend]
|
|
16
|
+
module-root = "src"
|
|
17
|
+
source-include = [
|
|
18
|
+
"completions/**",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/rioriost/envrcctl"
|
|
23
|
+
Issues = "https://github.com/rioriost/envrcctl/issues"
|
|
24
|
+
Repository = "https://github.com/rioriost/envrcctl"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
envrcctl = "envrcctl.main:main"
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = [
|
|
31
|
+
"pytest>=9",
|
|
32
|
+
"pytest-cov>=7",
|
|
33
|
+
"bandit>=1.7.10",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"build>=1.2.2",
|
|
39
|
+
"twine>=6.1.0",
|
|
40
|
+
"ruff>=0.12.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.9.27,<0.10.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
python_files = ["test_*.py"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
target-version = "py314"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
@@ -217,17 +217,13 @@ def _write_envrc(doc, block: ManagedBlock) -> None:
|
|
|
217
217
|
_ensure_not_world_writable(path)
|
|
218
218
|
warn = write_envrc(path, doc, block)
|
|
219
219
|
if warn:
|
|
220
|
-
raise EnvrcctlError(
|
|
221
|
-
".envrc is world-writable after write. Fix permissions and retry."
|
|
222
|
-
)
|
|
220
|
+
raise EnvrcctlError(".envrc is world-writable after write. Fix permissions and retry.")
|
|
223
221
|
|
|
224
222
|
|
|
225
223
|
@app.command()
|
|
226
224
|
def init(
|
|
227
225
|
yes: bool = typer.Option(False, "--yes", help="Confirm modifying existing .envrc."),
|
|
228
|
-
inject: bool = typer.Option(
|
|
229
|
-
False, "--inject", help="Add inject line to managed block."
|
|
230
|
-
),
|
|
226
|
+
inject: bool = typer.Option(False, "--inject", help="Add inject line to managed block."),
|
|
231
227
|
) -> None:
|
|
232
228
|
"""Create .envrc if missing and insert managed block."""
|
|
233
229
|
|
|
@@ -264,9 +260,7 @@ def inherit(state: str = typer.Argument(..., help="on/off")) -> None:
|
|
|
264
260
|
def set(
|
|
265
261
|
var: str,
|
|
266
262
|
value: str,
|
|
267
|
-
inject: bool = typer.Option(
|
|
268
|
-
False, "--inject", help="Add inject line to managed block."
|
|
269
|
-
),
|
|
263
|
+
inject: bool = typer.Option(False, "--inject", help="Add inject line to managed block."),
|
|
270
264
|
) -> None:
|
|
271
265
|
"""Set a non-secret export in the managed block."""
|
|
272
266
|
|
|
@@ -329,14 +323,10 @@ def list_exports() -> None:
|
|
|
329
323
|
def secret_set(
|
|
330
324
|
var: str,
|
|
331
325
|
account: str = typer.Option(..., "--account", help="Keychain account name."),
|
|
332
|
-
service: str = typer.Option(
|
|
333
|
-
DEFAULT_SERVICE, "--service", help="Keychain service name."
|
|
334
|
-
),
|
|
326
|
+
service: str = typer.Option(DEFAULT_SERVICE, "--service", help="Keychain service name."),
|
|
335
327
|
kind: str = typer.Option("runtime", "--kind", help="Secret kind (runtime/admin)."),
|
|
336
328
|
stdin: bool = typer.Option(False, "--stdin", help="Read secret from stdin."),
|
|
337
|
-
inject: bool = typer.Option(
|
|
338
|
-
False, "--inject", help="Add inject line to managed block."
|
|
339
|
-
),
|
|
329
|
+
inject: bool = typer.Option(False, "--inject", help="Add inject line to managed block."),
|
|
340
330
|
) -> None:
|
|
341
331
|
"""Store a secret and add its reference to the managed block."""
|
|
342
332
|
|
|
@@ -380,8 +370,7 @@ def secret_unset(var: str) -> None:
|
|
|
380
370
|
parsed = parse_ref(ref)
|
|
381
371
|
|
|
382
372
|
shared_ref_in_use = any(
|
|
383
|
-
name != var and other_ref == ref
|
|
384
|
-
for name, other_ref in block.secret_refs.items()
|
|
373
|
+
name != var and other_ref == ref for name, other_ref in block.secret_refs.items()
|
|
385
374
|
)
|
|
386
375
|
|
|
387
376
|
if not shared_ref_in_use:
|
|
@@ -448,9 +437,7 @@ def secret_get(
|
|
|
448
437
|
typer.echo(value)
|
|
449
438
|
return
|
|
450
439
|
|
|
451
|
-
auth_reason = _require_secret_access_auth(
|
|
452
|
-
f"Access secret {var} with envrcctl"
|
|
453
|
-
)
|
|
440
|
+
auth_reason = _require_secret_access_auth(f"Access secret {var} with envrcctl")
|
|
454
441
|
value = _get_secret_value(backend, parsed, auth_reason)
|
|
455
442
|
_record_secret_access_event(
|
|
456
443
|
action="secret_get",
|
|
@@ -556,9 +543,7 @@ def exec_cmd(
|
|
|
556
543
|
command = list(ctx.args)
|
|
557
544
|
try:
|
|
558
545
|
if not ctx.args:
|
|
559
|
-
raise EnvrcctlError(
|
|
560
|
-
"No command provided. Use -- to separate the command."
|
|
561
|
-
)
|
|
546
|
+
raise EnvrcctlError("No command provided. Use -- to separate the command.")
|
|
562
547
|
if not _is_interactive():
|
|
563
548
|
if sys.platform == "darwin":
|
|
564
549
|
raise EnvrcctlError(
|
|
@@ -575,9 +560,7 @@ def exec_cmd(
|
|
|
575
560
|
missing = selected_keys - available_keys
|
|
576
561
|
if missing:
|
|
577
562
|
missing_list = ", ".join(sorted(missing))
|
|
578
|
-
raise EnvrcctlError(
|
|
579
|
-
f"Secrets not found in managed block: {missing_list}"
|
|
580
|
-
)
|
|
563
|
+
raise EnvrcctlError(f"Secrets not found in managed block: {missing_list}")
|
|
581
564
|
|
|
582
565
|
env = os.environ.copy()
|
|
583
566
|
for name, value in block.exports.items():
|
|
@@ -634,9 +617,7 @@ def audit_list(
|
|
|
634
617
|
action: str | None = typer.Option(None, "--action", help="Filter by audit action."),
|
|
635
618
|
var: str | None = typer.Option(None, "--var", help="Filter by variable name."),
|
|
636
619
|
status: str | None = typer.Option(None, "--status", help="Filter by audit status."),
|
|
637
|
-
json_output: bool = typer.Option(
|
|
638
|
-
False, "--json", help="Emit matching events as JSON."
|
|
639
|
-
),
|
|
620
|
+
json_output: bool = typer.Option(False, "--json", help="Emit matching events as JSON."),
|
|
640
621
|
) -> None:
|
|
641
622
|
"""List recent audit events."""
|
|
642
623
|
|
|
@@ -713,9 +694,7 @@ def audit_show(
|
|
|
713
694
|
index: int | None = typer.Option(
|
|
714
695
|
None, "--index", min=0, help="Show an event by zero-based index."
|
|
715
696
|
),
|
|
716
|
-
json_output: bool = typer.Option(
|
|
717
|
-
False, "--json", help="Emit the selected event as JSON."
|
|
718
|
-
),
|
|
697
|
+
json_output: bool = typer.Option(False, "--json", help="Emit the selected event as JSON."),
|
|
719
698
|
) -> None:
|
|
720
699
|
"""Show one audit event in detail."""
|
|
721
700
|
|
|
@@ -911,9 +890,7 @@ def doctor() -> None:
|
|
|
911
890
|
)
|
|
912
891
|
warnings += 1
|
|
913
892
|
|
|
914
|
-
before_clean, before_exports, before_secrets = extract_unmanaged_exports(
|
|
915
|
-
doc.before
|
|
916
|
-
)
|
|
893
|
+
before_clean, before_exports, before_secrets = extract_unmanaged_exports(doc.before)
|
|
917
894
|
after_clean, after_exports, after_secrets = extract_unmanaged_exports(doc.after)
|
|
918
895
|
unmanaged = {**before_exports, **after_exports}
|
|
919
896
|
unmanaged_secrets = {**before_secrets, **after_secrets}
|
|
@@ -983,12 +960,8 @@ def doctor() -> None:
|
|
|
983
960
|
|
|
984
961
|
@app.command()
|
|
985
962
|
def migrate(
|
|
986
|
-
yes: bool = typer.Option(
|
|
987
|
-
|
|
988
|
-
),
|
|
989
|
-
inject: bool = typer.Option(
|
|
990
|
-
False, "--inject", help="Add inject line to managed block."
|
|
991
|
-
),
|
|
963
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm migrating unmanaged exports."),
|
|
964
|
+
inject: bool = typer.Option(False, "--inject", help="Add inject line to managed block."),
|
|
992
965
|
) -> None:
|
|
993
966
|
"""Move unmanaged exports into the managed block."""
|
|
994
967
|
|
|
@@ -999,9 +972,7 @@ def migrate(
|
|
|
999
972
|
doc = load_envrc(path)
|
|
1000
973
|
block = ensure_managed_block(doc)
|
|
1001
974
|
|
|
1002
|
-
before_clean, before_exports, before_secrets = extract_unmanaged_exports(
|
|
1003
|
-
doc.before
|
|
1004
|
-
)
|
|
975
|
+
before_clean, before_exports, before_secrets = extract_unmanaged_exports(doc.before)
|
|
1005
976
|
after_clean, after_exports, after_secrets = extract_unmanaged_exports(doc.after)
|
|
1006
977
|
|
|
1007
978
|
if before_exports or after_exports or before_secrets or after_secrets:
|
envrcctl-0.2.0/.gitignore
DELETED
envrcctl-0.2.0/pyproject.toml
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "envrcctl"
|
|
3
|
-
version = "0.2.0"
|
|
4
|
-
description = "Manage .envrc with managed blocks and OS-backed secrets."
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
license = { file = "LICENSE" }
|
|
7
|
-
requires-python = ">=3.14"
|
|
8
|
-
dependencies = [
|
|
9
|
-
"typer>=0.24.1",
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
[project.scripts]
|
|
13
|
-
envrcctl = "envrcctl.main:main"
|
|
14
|
-
|
|
15
|
-
[build-system]
|
|
16
|
-
requires = ["hatchling>=1.24.2"]
|
|
17
|
-
build-backend = "hatchling.build"
|
|
18
|
-
|
|
19
|
-
[tool.hatch.build.targets.wheel]
|
|
20
|
-
packages = ["src/envrcctl"]
|
|
21
|
-
include = ["completions/**", "src/envrcctl/envrcctl-macos-auth"]
|
|
22
|
-
|
|
23
|
-
[tool.hatch.build.targets.sdist]
|
|
24
|
-
include = ["src/envrcctl/**", "completions/**", "scripts/**", "README.md", "LICENSE", "pyproject.toml"]
|
|
25
|
-
|
|
26
|
-
[project.optional-dependencies]
|
|
27
|
-
test = [
|
|
28
|
-
"pytest>=9.0.2",
|
|
29
|
-
"pytest-cov>=7.0.0",
|
|
30
|
-
"bandit>=1.7.10",
|
|
31
|
-
]
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
set -eu
|
|
3
|
-
|
|
4
|
-
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
|
5
|
-
REPO_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
-
SWIFT_SOURCE="${1:-$REPO_ROOT/scripts/macos/envrcctl-macos-auth.swift}"
|
|
7
|
-
OUTPUT_PATH="${2:-$REPO_ROOT/src/envrcctl/envrcctl-macos-auth}"
|
|
8
|
-
|
|
9
|
-
if [ "$(uname -s)" != "Darwin" ]; then
|
|
10
|
-
echo "This helper can only be built on macOS." >&2
|
|
11
|
-
exit 1
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
if ! command -v swiftc >/dev/null 2>&1; then
|
|
15
|
-
echo "swiftc not found. Install Xcode Command Line Tools first." >&2
|
|
16
|
-
exit 1
|
|
17
|
-
fi
|
|
18
|
-
|
|
19
|
-
if [ ! -f "$SWIFT_SOURCE" ]; then
|
|
20
|
-
echo "Swift source not found: $SWIFT_SOURCE" >&2
|
|
21
|
-
echo "Pass the source path as the first argument or create scripts/macos/envrcctl-macos-auth.swift." >&2
|
|
22
|
-
exit 1
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
mkdir -p "$(dirname "$OUTPUT_PATH")"
|
|
26
|
-
|
|
27
|
-
echo "Building macOS auth helper..."
|
|
28
|
-
echo " source: $SWIFT_SOURCE"
|
|
29
|
-
echo " output: $OUTPUT_PATH"
|
|
30
|
-
|
|
31
|
-
swiftc \
|
|
32
|
-
-O \
|
|
33
|
-
-framework LocalAuthentication \
|
|
34
|
-
-framework Security \
|
|
35
|
-
"$SWIFT_SOURCE" \
|
|
36
|
-
-o "$OUTPUT_PATH"
|
|
37
|
-
|
|
38
|
-
chmod 755 "$OUTPUT_PATH"
|
|
39
|
-
|
|
40
|
-
echo "Build complete: $OUTPUT_PATH"
|
|
41
|
-
echo
|
|
42
|
-
echo "You can override the helper path at runtime with:"
|
|
43
|
-
echo " ENVRCCTL_MACOS_AUTH_HELPER=$OUTPUT_PATH"
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from click.shell_completion import get_completion_class
|
|
6
|
-
from typer.main import get_command
|
|
7
|
-
|
|
8
|
-
from envrcctl.cli import app
|
|
9
|
-
|
|
10
|
-
SHELLS = ("bash", "zsh", "fish")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def main() -> None:
|
|
14
|
-
repo_root = Path(__file__).resolve().parents[1]
|
|
15
|
-
output_dir = repo_root / "completions"
|
|
16
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
17
|
-
|
|
18
|
-
command = get_command(app)
|
|
19
|
-
complete_var = "_ENVRCCTL_COMPLETE"
|
|
20
|
-
|
|
21
|
-
for shell in SHELLS:
|
|
22
|
-
comp_cls = get_completion_class(shell)
|
|
23
|
-
if comp_cls is None:
|
|
24
|
-
raise RuntimeError(f"Unsupported shell: {shell}")
|
|
25
|
-
comp = comp_cls(command, {}, "envrcctl", complete_var)
|
|
26
|
-
content = comp.source()
|
|
27
|
-
if not content.strip():
|
|
28
|
-
raise RuntimeError(f"Failed to generate {shell} completion.")
|
|
29
|
-
if not content.endswith("\n"):
|
|
30
|
-
content += "\n"
|
|
31
|
-
(output_dir / f"envrcctl.{shell}").write_text(content, encoding="utf-8")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if __name__ == "__main__":
|
|
35
|
-
main()
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import LocalAuthentication
|
|
3
|
-
import Security
|
|
4
|
-
|
|
5
|
-
enum HelperError: Error, LocalizedError {
|
|
6
|
-
case invalidArguments(String)
|
|
7
|
-
case authenticationUnavailable(String)
|
|
8
|
-
case authenticationFailed(String)
|
|
9
|
-
case keychainFailure(String)
|
|
10
|
-
case decodeFailure
|
|
11
|
-
case inputReadFailure(String)
|
|
12
|
-
case outputEncodeFailure
|
|
13
|
-
|
|
14
|
-
var errorDescription: String? {
|
|
15
|
-
switch self {
|
|
16
|
-
case .invalidArguments(let message):
|
|
17
|
-
return message
|
|
18
|
-
case .authenticationUnavailable(let message):
|
|
19
|
-
return message
|
|
20
|
-
case .authenticationFailed(let message):
|
|
21
|
-
return message
|
|
22
|
-
case .keychainFailure(let message):
|
|
23
|
-
return message
|
|
24
|
-
case .decodeFailure:
|
|
25
|
-
return "Keychain item contains non-UTF-8 data."
|
|
26
|
-
case .inputReadFailure(let message):
|
|
27
|
-
return message
|
|
28
|
-
case .outputEncodeFailure:
|
|
29
|
-
return "Failed to encode JSON response."
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
struct Arguments {
|
|
35
|
-
let authorizeOnly: Bool
|
|
36
|
-
let service: String?
|
|
37
|
-
let account: String?
|
|
38
|
-
let inputJSONPath: String?
|
|
39
|
-
let reason: String
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
struct BulkRequest: Decodable {
|
|
43
|
-
let items: [BulkRequestItem]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
struct BulkRequestItem: Decodable {
|
|
47
|
-
let service: String
|
|
48
|
-
let account: String
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
struct BulkResponse: Encodable {
|
|
52
|
-
let items: [BulkResponseItem]
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
struct BulkResponseItem: Encodable {
|
|
56
|
-
let service: String
|
|
57
|
-
let account: String
|
|
58
|
-
let value: String
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private func printErrorAndExit(_ error: Error) -> Never {
|
|
62
|
-
let message: String
|
|
63
|
-
if let helperError = error as? LocalizedError, let description = helperError.errorDescription {
|
|
64
|
-
message = description
|
|
65
|
-
} else {
|
|
66
|
-
message = "macOS authentication helper failed."
|
|
67
|
-
}
|
|
68
|
-
FileHandle.standardError.write(Data((message + "\n").utf8))
|
|
69
|
-
exit(1)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private func printHelpAndExit() -> Never {
|
|
73
|
-
let help = """
|
|
74
|
-
Usage:
|
|
75
|
-
envrcctl-macos-auth --authorize-only --reason <text>
|
|
76
|
-
envrcctl-macos-auth --service <service> --account <account> --reason <text>
|
|
77
|
-
envrcctl-macos-auth --input-json <path|- > --reason <text>
|
|
78
|
-
|
|
79
|
-
Options:
|
|
80
|
-
--authorize-only Require device owner authentication only.
|
|
81
|
-
--service Keychain service name.
|
|
82
|
-
--account Keychain account name.
|
|
83
|
-
--input-json JSON file path or '-' for stdin for bulk reads.
|
|
84
|
-
--reason Localized reason shown in the auth prompt.
|
|
85
|
-
--help Show this help.
|
|
86
|
-
|
|
87
|
-
Bulk JSON input:
|
|
88
|
-
{
|
|
89
|
-
"items": [
|
|
90
|
-
{ "service": "st.rio.envrcctl", "account": "openai:prod" },
|
|
91
|
-
{ "service": "st.rio.envrcctl", "account": "github:prod" }
|
|
92
|
-
]
|
|
93
|
-
}
|
|
94
|
-
"""
|
|
95
|
-
print(help)
|
|
96
|
-
exit(0)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private func parseArguments(_ argv: [String]) throws -> Arguments {
|
|
100
|
-
var authorizeOnly = false
|
|
101
|
-
var service: String?
|
|
102
|
-
var account: String?
|
|
103
|
-
var inputJSONPath: String?
|
|
104
|
-
var reason: String?
|
|
105
|
-
|
|
106
|
-
var index = 1
|
|
107
|
-
while index < argv.count {
|
|
108
|
-
let arg = argv[index]
|
|
109
|
-
switch arg {
|
|
110
|
-
case "--authorize-only":
|
|
111
|
-
authorizeOnly = true
|
|
112
|
-
index += 1
|
|
113
|
-
case "--service":
|
|
114
|
-
guard index + 1 < argv.count else {
|
|
115
|
-
throw HelperError.invalidArguments("Missing value for --service.")
|
|
116
|
-
}
|
|
117
|
-
service = argv[index + 1]
|
|
118
|
-
index += 2
|
|
119
|
-
case "--account":
|
|
120
|
-
guard index + 1 < argv.count else {
|
|
121
|
-
throw HelperError.invalidArguments("Missing value for --account.")
|
|
122
|
-
}
|
|
123
|
-
account = argv[index + 1]
|
|
124
|
-
index += 2
|
|
125
|
-
case "--input-json":
|
|
126
|
-
guard index + 1 < argv.count else {
|
|
127
|
-
throw HelperError.invalidArguments("Missing value for --input-json.")
|
|
128
|
-
}
|
|
129
|
-
inputJSONPath = argv[index + 1]
|
|
130
|
-
index += 2
|
|
131
|
-
case "--reason":
|
|
132
|
-
guard index + 1 < argv.count else {
|
|
133
|
-
throw HelperError.invalidArguments("Missing value for --reason.")
|
|
134
|
-
}
|
|
135
|
-
reason = argv[index + 1]
|
|
136
|
-
index += 2
|
|
137
|
-
case "--help", "-h":
|
|
138
|
-
printHelpAndExit()
|
|
139
|
-
default:
|
|
140
|
-
throw HelperError.invalidArguments("Unknown argument: \(arg)")
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
guard let reason, !reason.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
145
|
-
throw HelperError.invalidArguments("A non-empty --reason is required.")
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if authorizeOnly {
|
|
149
|
-
if service != nil || account != nil || inputJSONPath != nil {
|
|
150
|
-
throw HelperError.invalidArguments(
|
|
151
|
-
"--authorize-only cannot be combined with --service, --account, or --input-json."
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
return Arguments(
|
|
155
|
-
authorizeOnly: true,
|
|
156
|
-
service: nil,
|
|
157
|
-
account: nil,
|
|
158
|
-
inputJSONPath: nil,
|
|
159
|
-
reason: reason
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
let hasSingle = service != nil || account != nil
|
|
164
|
-
let hasBulk = inputJSONPath != nil
|
|
165
|
-
|
|
166
|
-
if hasSingle && hasBulk {
|
|
167
|
-
throw HelperError.invalidArguments(
|
|
168
|
-
"--input-json cannot be combined with --service or --account."
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if hasBulk {
|
|
173
|
-
return Arguments(
|
|
174
|
-
authorizeOnly: false,
|
|
175
|
-
service: nil,
|
|
176
|
-
account: nil,
|
|
177
|
-
inputJSONPath: inputJSONPath,
|
|
178
|
-
reason: reason
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
guard let service, !service.isEmpty else {
|
|
183
|
-
throw HelperError.invalidArguments("--service is required.")
|
|
184
|
-
}
|
|
185
|
-
guard let account, !account.isEmpty else {
|
|
186
|
-
throw HelperError.invalidArguments("--account is required.")
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return Arguments(
|
|
190
|
-
authorizeOnly: false,
|
|
191
|
-
service: service,
|
|
192
|
-
account: account,
|
|
193
|
-
inputJSONPath: nil,
|
|
194
|
-
reason: reason
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private func authenticate(reason: String) throws -> LAContext {
|
|
199
|
-
let context = LAContext()
|
|
200
|
-
context.localizedCancelTitle = "Cancel"
|
|
201
|
-
|
|
202
|
-
var canEvaluateError: NSError?
|
|
203
|
-
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &canEvaluateError) else {
|
|
204
|
-
let message =
|
|
205
|
-
canEvaluateError?.localizedDescription
|
|
206
|
-
?? "Device owner authentication is unavailable."
|
|
207
|
-
throw HelperError.authenticationUnavailable(message)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
let semaphore = DispatchSemaphore(value: 0)
|
|
211
|
-
var authError: Error?
|
|
212
|
-
|
|
213
|
-
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, error in
|
|
214
|
-
if !success {
|
|
215
|
-
authError = error ?? HelperError.authenticationFailed("Authentication failed.")
|
|
216
|
-
}
|
|
217
|
-
semaphore.signal()
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
semaphore.wait()
|
|
221
|
-
|
|
222
|
-
if let authError {
|
|
223
|
-
if let laError = authError as? LAError {
|
|
224
|
-
switch laError.code {
|
|
225
|
-
case .userCancel, .userFallback, .systemCancel, .appCancel:
|
|
226
|
-
throw HelperError.authenticationFailed("Authentication cancelled.")
|
|
227
|
-
default:
|
|
228
|
-
throw HelperError.authenticationFailed(laError.localizedDescription)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
throw HelperError.authenticationFailed(authError.localizedDescription)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return context
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private func readSecret(service: String, account: String, context: LAContext) throws -> String {
|
|
238
|
-
let query: [String: Any] = [
|
|
239
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
240
|
-
kSecAttrService as String: service,
|
|
241
|
-
kSecAttrAccount as String: account,
|
|
242
|
-
kSecReturnData as String: true,
|
|
243
|
-
kSecMatchLimit as String: kSecMatchLimitOne,
|
|
244
|
-
kSecUseAuthenticationContext as String: context,
|
|
245
|
-
]
|
|
246
|
-
|
|
247
|
-
var item: CFTypeRef?
|
|
248
|
-
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
249
|
-
|
|
250
|
-
guard status == errSecSuccess else {
|
|
251
|
-
let message = SecCopyErrorMessageString(status, nil) as String? ?? "Keychain read failed."
|
|
252
|
-
throw HelperError.keychainFailure(message)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
guard let data = item as? Data else {
|
|
256
|
-
throw HelperError.keychainFailure("Keychain returned an unexpected item type.")
|
|
257
|
-
}
|
|
258
|
-
guard let value = String(data: data, encoding: .utf8) else {
|
|
259
|
-
throw HelperError.decodeFailure
|
|
260
|
-
}
|
|
261
|
-
return value
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private func readBulkRequest(from path: String) throws -> BulkRequest {
|
|
265
|
-
let data: Data
|
|
266
|
-
if path == "-" {
|
|
267
|
-
data = FileHandle.standardInput.readDataToEndOfFile()
|
|
268
|
-
if data.isEmpty {
|
|
269
|
-
throw HelperError.inputReadFailure("No JSON input received on stdin.")
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
let url = URL(fileURLWithPath: path)
|
|
273
|
-
do {
|
|
274
|
-
data = try Data(contentsOf: url)
|
|
275
|
-
} catch {
|
|
276
|
-
throw HelperError.inputReadFailure("Failed to read JSON input: \(path)")
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
do {
|
|
281
|
-
let request = try JSONDecoder().decode(BulkRequest.self, from: data)
|
|
282
|
-
if request.items.isEmpty {
|
|
283
|
-
throw HelperError.invalidArguments("Bulk JSON input must include at least one item.")
|
|
284
|
-
}
|
|
285
|
-
for item in request.items {
|
|
286
|
-
if item.service.isEmpty || item.account.isEmpty {
|
|
287
|
-
throw HelperError.invalidArguments(
|
|
288
|
-
"Each bulk request item must include non-empty service and account values."
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return request
|
|
293
|
-
} catch let helperError as HelperError {
|
|
294
|
-
throw helperError
|
|
295
|
-
} catch {
|
|
296
|
-
throw HelperError.invalidArguments("Failed to decode bulk JSON input.")
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private func writeBulkResponse(_ response: BulkResponse) throws {
|
|
301
|
-
let encoder = JSONEncoder()
|
|
302
|
-
encoder.outputFormatting = [.sortedKeys]
|
|
303
|
-
guard let data = try? encoder.encode(response) else {
|
|
304
|
-
throw HelperError.outputEncodeFailure
|
|
305
|
-
}
|
|
306
|
-
FileHandle.standardOutput.write(data)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
do {
|
|
310
|
-
let args = try parseArguments(CommandLine.arguments)
|
|
311
|
-
let context = try authenticate(reason: args.reason)
|
|
312
|
-
|
|
313
|
-
if args.authorizeOnly {
|
|
314
|
-
exit(0)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if let inputJSONPath = args.inputJSONPath {
|
|
318
|
-
let request = try readBulkRequest(from: inputJSONPath)
|
|
319
|
-
let items = try request.items.map { item in
|
|
320
|
-
BulkResponseItem(
|
|
321
|
-
service: item.service,
|
|
322
|
-
account: item.account,
|
|
323
|
-
value: try readSecret(
|
|
324
|
-
service: item.service, account: item.account, context: context)
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
try writeBulkResponse(BulkResponse(items: items))
|
|
328
|
-
exit(0)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
guard let service = args.service, let account = args.account else {
|
|
332
|
-
throw HelperError.invalidArguments("Both --service and --account are required.")
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
let secret = try readSecret(service: service, account: account, context: context)
|
|
336
|
-
FileHandle.standardOutput.write(Data(secret.utf8))
|
|
337
|
-
exit(0)
|
|
338
|
-
} catch {
|
|
339
|
-
printErrorAndExit(error)
|
|
340
|
-
}
|
|
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
|