envrcctl 0.2.2__tar.gz → 0.2.3__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 (52) hide show
  1. envrcctl-0.2.3/.gitignore +15 -0
  2. {envrcctl-0.2.2 → envrcctl-0.2.3}/PKG-INFO +10 -30
  3. {envrcctl-0.2.2 → envrcctl-0.2.3}/pyproject.toml +18 -7
  4. envrcctl-0.2.3/scripts/build_macos_auth_helper.sh +48 -0
  5. envrcctl-0.2.3/scripts/generate_completions.py +35 -0
  6. envrcctl-0.2.3/scripts/macos/envrcctl-macos-auth.swift +340 -0
  7. envrcctl-0.2.3/scripts/package_for_ship.sh +6 -0
  8. envrcctl-0.2.3/scripts/release_artifacts.py +286 -0
  9. envrcctl-0.2.3/tests/__init__.py +3 -0
  10. envrcctl-0.2.3/tests/conftest.py +15 -0
  11. envrcctl-0.2.3/tests/helpers/__init__.py +3 -0
  12. envrcctl-0.2.3/tests/helpers/cli_support.py +34 -0
  13. envrcctl-0.2.3/tests/test_audit.py +830 -0
  14. envrcctl-0.2.3/tests/test_audit_cli.py +364 -0
  15. envrcctl-0.2.3/tests/test_auth.py +181 -0
  16. envrcctl-0.2.3/tests/test_cli.py +81 -0
  17. envrcctl-0.2.3/tests/test_cli_doctor.py +213 -0
  18. envrcctl-0.2.3/tests/test_cli_errors.py +485 -0
  19. envrcctl-0.2.3/tests/test_cli_eval.py +84 -0
  20. envrcctl-0.2.3/tests/test_cli_exec.py +361 -0
  21. envrcctl-0.2.3/tests/test_cli_helpers.py +150 -0
  22. envrcctl-0.2.3/tests/test_cli_inject.py +222 -0
  23. envrcctl-0.2.3/tests/test_cli_migrate.py +50 -0
  24. envrcctl-0.2.3/tests/test_cli_secret_get.py +304 -0
  25. envrcctl-0.2.3/tests/test_cli_secret_list.py +66 -0
  26. envrcctl-0.2.3/tests/test_cli_secret_set_unset.py +138 -0
  27. envrcctl-0.2.3/tests/test_command_runner.py +51 -0
  28. envrcctl-0.2.3/tests/test_envrc.py +147 -0
  29. envrcctl-0.2.3/tests/test_keychain.py +609 -0
  30. envrcctl-0.2.3/tests/test_main.py +30 -0
  31. envrcctl-0.2.3/tests/test_managed_block.py +68 -0
  32. envrcctl-0.2.3/tests/test_secrets.py +186 -0
  33. envrcctl-0.2.3/tests/test_secretservice.py +91 -0
  34. envrcctl-0.2.3/tests/test_smoke.py +4 -0
  35. {envrcctl-0.2.2 → envrcctl-0.2.3}/LICENSE +0 -0
  36. {envrcctl-0.2.2 → envrcctl-0.2.3}/README.md +0 -0
  37. {envrcctl-0.2.2 → envrcctl-0.2.3}/completions/envrcctl.bash +0 -0
  38. {envrcctl-0.2.2 → envrcctl-0.2.3}/completions/envrcctl.fish +0 -0
  39. {envrcctl-0.2.2 → envrcctl-0.2.3}/completions/envrcctl.zsh +0 -0
  40. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/__init__.py +0 -0
  41. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/audit.py +0 -0
  42. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/auth.py +0 -0
  43. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/cli.py +0 -0
  44. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/command_runner.py +0 -0
  45. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/envrc.py +0 -0
  46. /envrcctl-0.2.2/src/envrcctl/envrcctl-macos-auth → /envrcctl-0.2.3/src/envrcctl/envrcctl-macos-auth.bak +0 -0
  47. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/errors.py +0 -0
  48. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/keychain.py +0 -0
  49. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/main.py +0 -0
  50. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/managed_block.py +0 -0
  51. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/secrets.py +0 -0
  52. {envrcctl-0.2.2 → envrcctl-0.2.3}/src/envrcctl/secretservice.py +0 -0
@@ -0,0 +1,15 @@
1
+
2
+ .venv/
3
+ .agent/
4
+ .rules
5
+ __pycache__/
6
+ *.py[cod]
7
+ .pytest_cache/
8
+ .coverage
9
+ htmlcov/
10
+ .DS_Store
11
+ .envrc
12
+ dist/helper/
13
+ dist/*-arm64.tar.gz
14
+ dist/*-darwin-arm64.tar.gz
15
+ src/*.egg-info/
@@ -1,39 +1,19 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: envrcctl
3
- Version: 0.2.2
3
+ Version: 0.2.3
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>
7
- License: MIT License
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
- 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
5
  Project-URL: Homepage, https://github.com/rioriost/envrcctl
34
6
  Project-URL: Issues, https://github.com/rioriost/envrcctl/issues
35
7
  Project-URL: Repository, https://github.com/rioriost/envrcctl
8
+ Author-email: Rio Fujita <rio_github@rio.st>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.14
12
+ Requires-Dist: typer>=0.24.1
36
13
  Provides-Extra: test
14
+ Requires-Dist: bandit>=1.7.10; extra == 'test'
15
+ Requires-Dist: pytest-cov>=7; extra == 'test'
16
+ Requires-Dist: pytest>=9; extra == 'test'
37
17
  Description-Content-Type: text/markdown
38
18
 
39
19
  # envrcctl
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "envrcctl"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Manage .envrc with managed blocks and OS-backed secrets."
5
5
  readme = { file = "README.md", content-type = "text/markdown" }
6
- license = { file = "LICENSE" }
6
+ license = "MIT"
7
7
  requires-python = ">=3.14"
8
8
  authors = [
9
9
  { name = "Rio Fujita", email = "rio_github@rio.st" },
@@ -12,10 +12,21 @@ dependencies = [
12
12
  "typer>=0.24.1",
13
13
  ]
14
14
 
15
- [tool.uv.build-backend]
16
- module-root = "src"
17
- source-include = [
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/envrcctl"]
17
+
18
+ [tool.hatch.build.targets.sdist]
19
+ include = [
20
+ "src/**",
21
+ "tests/**",
22
+ "scripts/**",
18
23
  "completions/**",
24
+ "README.md",
25
+ "LICENSE",
26
+ "pyproject.toml",
27
+ ]
28
+ exclude = [
29
+ "src/envrcctl/envrcctl-macos-auth",
19
30
  ]
20
31
 
21
32
  [project.urls]
@@ -41,8 +52,8 @@ dev = [
41
52
  ]
42
53
 
43
54
  [build-system]
44
- requires = ["uv_build>=0.9.27,<0.10.0"]
45
- build-backend = "uv_build"
55
+ requires = ["hatchling>=1.25.0"]
56
+ build-backend = "hatchling.build"
46
57
 
47
58
  [tool.pytest.ini_options]
48
59
  testpaths = ["tests"]
@@ -0,0 +1,48 @@
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 [ "$(uname -m)" != "arm64" ]; then
15
+ echo "This helper only supports Apple Silicon (arm64) macOS." >&2
16
+ exit 1
17
+ fi
18
+
19
+ if ! command -v swiftc >/dev/null 2>&1; then
20
+ echo "swiftc not found. Install Xcode Command Line Tools first." >&2
21
+ exit 1
22
+ fi
23
+
24
+ if [ ! -f "$SWIFT_SOURCE" ]; then
25
+ echo "Swift source not found: $SWIFT_SOURCE" >&2
26
+ echo "Pass the source path as the first argument or create scripts/macos/envrcctl-macos-auth.swift." >&2
27
+ exit 1
28
+ fi
29
+
30
+ mkdir -p "$(dirname "$OUTPUT_PATH")"
31
+
32
+ echo "Building macOS auth helper for Apple Silicon (arm64)..."
33
+ echo " source: $SWIFT_SOURCE"
34
+ echo " output: $OUTPUT_PATH"
35
+
36
+ swiftc \
37
+ -O \
38
+ -framework LocalAuthentication \
39
+ -framework Security \
40
+ "$SWIFT_SOURCE" \
41
+ -o "$OUTPUT_PATH"
42
+
43
+ chmod 755 "$OUTPUT_PATH"
44
+
45
+ echo "Build complete: $OUTPUT_PATH"
46
+ echo
47
+ echo "You can override the helper path at runtime with:"
48
+ echo " ENVRCCTL_MACOS_AUTH_HELPER=$OUTPUT_PATH"
@@ -0,0 +1,35 @@
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()
@@ -0,0 +1,340 @@
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
+ }
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ echo "scripts/package_for_ship.sh is obsolete." >&2
5
+ echo "Run 'make release-artifacts' from the repository root instead." >&2
6
+ exit 1