@wlfi-agent/cli 1.4.17 → 1.4.18
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.
- package/Cargo.lock +5 -0
- package/README.md +61 -28
- package/crates/vault-cli-admin/src/io_utils.rs +149 -1
- package/crates/vault-cli-admin/src/main.rs +639 -16
- package/crates/vault-cli-admin/src/shared_config.rs +18 -18
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
- package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
- package/crates/vault-cli-admin/src/tui.rs +1205 -120
- package/crates/vault-cli-agent/Cargo.toml +1 -0
- package/crates/vault-cli-agent/src/io_utils.rs +163 -2
- package/crates/vault-cli-agent/src/main.rs +648 -32
- package/crates/vault-cli-daemon/Cargo.toml +4 -0
- package/crates/vault-cli-daemon/src/main.rs +617 -67
- package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
- package/crates/vault-daemon/src/persistence.rs +637 -100
- package/crates/vault-daemon/src/tests.rs +1013 -3
- package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
- package/crates/vault-domain/src/nonce.rs +4 -0
- package/crates/vault-domain/src/tests.rs +616 -0
- package/crates/vault-policy/src/engine.rs +55 -32
- package/crates/vault-policy/src/tests.rs +195 -0
- package/crates/vault-sdk-agent/src/lib.rs +415 -22
- package/crates/vault-signer/Cargo.toml +3 -0
- package/crates/vault-signer/src/lib.rs +266 -40
- package/crates/vault-transport-unix/src/lib.rs +653 -5
- package/crates/vault-transport-xpc/src/tests.rs +531 -3
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
- package/dist/cli.cjs +663 -190
- package/dist/cli.cjs.map +1 -1
- package/package.json +5 -2
- package/packages/cache/.turbo/turbo-build.log +20 -20
- package/packages/cache/coverage/clover.xml +529 -394
- package/packages/cache/coverage/coverage-final.json +2 -2
- package/packages/cache/coverage/index.html +21 -21
- package/packages/cache/coverage/src/client/index.html +1 -1
- package/packages/cache/coverage/src/client/index.ts.html +1 -1
- package/packages/cache/coverage/src/errors/index.html +1 -1
- package/packages/cache/coverage/src/errors/index.ts.html +12 -12
- package/packages/cache/coverage/src/index.html +1 -1
- package/packages/cache/coverage/src/index.ts.html +1 -1
- package/packages/cache/coverage/src/service/index.html +21 -21
- package/packages/cache/coverage/src/service/index.ts.html +769 -313
- package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
- package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
- package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
- package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
- package/packages/cache/dist/index.cjs +2 -2
- package/packages/cache/dist/index.js +1 -1
- package/packages/cache/dist/service/index.cjs +2 -2
- package/packages/cache/dist/service/index.js +1 -1
- package/packages/cache/node_modules/.bin/tsc +2 -2
- package/packages/cache/node_modules/.bin/tsserver +2 -2
- package/packages/cache/node_modules/.bin/tsup +2 -2
- package/packages/cache/node_modules/.bin/tsup-node +2 -2
- package/packages/cache/node_modules/.bin/vitest +4 -4
- package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/cache/src/service/index.test.ts +165 -19
- package/packages/cache/src/service/index.ts +38 -1
- package/packages/config/.turbo/turbo-build.log +4 -4
- package/packages/config/dist/index.cjs +0 -17
- package/packages/config/dist/index.cjs.map +1 -1
- package/packages/config/src/index.ts +0 -17
- package/packages/rpc/.turbo/turbo-build.log +11 -11
- package/packages/rpc/dist/index.cjs +0 -17
- package/packages/rpc/dist/index.cjs.map +1 -1
- package/packages/rpc/src/index.js +1 -0
- package/packages/ui/node_modules/.bin/tsc +2 -2
- package/packages/ui/node_modules/.bin/tsserver +2 -2
- package/packages/ui/node_modules/.bin/tsup +2 -2
- package/packages/ui/node_modules/.bin/tsup-node +2 -2
- package/scripts/install-cli-launcher.mjs +37 -0
- package/scripts/install-rust-binaries.mjs +47 -0
- package/scripts/run-tests-isolated.mjs +210 -0
- package/src/cli.ts +310 -50
- package/src/lib/admin-reset.ts +15 -30
- package/src/lib/admin-setup.ts +246 -55
- package/src/lib/agent-auth-migrate.ts +5 -1
- package/src/lib/asset-broadcast.ts +15 -4
- package/src/lib/config-amounts.ts +6 -4
- package/src/lib/hidden-tty-prompt.js +1 -0
- package/src/lib/hidden-tty-prompt.ts +105 -0
- package/src/lib/keychain.ts +1 -0
- package/src/lib/local-admin-access.ts +4 -29
- package/src/lib/rust.ts +129 -33
- package/src/lib/signed-tx.ts +1 -0
- package/src/lib/sudo.ts +15 -5
- package/src/lib/wallet-profile.ts +3 -0
- package/src/lib/wallet-setup.ts +52 -0
- package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
- package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
package/Cargo.lock
CHANGED
|
@@ -3777,6 +3777,7 @@ name = "wlfi-agent-agent"
|
|
|
3777
3777
|
version = "0.1.0"
|
|
3778
3778
|
dependencies = [
|
|
3779
3779
|
"anyhow",
|
|
3780
|
+
"async-trait",
|
|
3780
3781
|
"clap",
|
|
3781
3782
|
"hex",
|
|
3782
3783
|
"libc",
|
|
@@ -3799,8 +3800,11 @@ name = "wlfi-agent-daemon"
|
|
|
3799
3800
|
version = "0.1.0"
|
|
3800
3801
|
dependencies = [
|
|
3801
3802
|
"anyhow",
|
|
3803
|
+
"async-trait",
|
|
3804
|
+
"chacha20poly1305",
|
|
3802
3805
|
"clap",
|
|
3803
3806
|
"core-foundation",
|
|
3807
|
+
"hex",
|
|
3804
3808
|
"libc",
|
|
3805
3809
|
"nix",
|
|
3806
3810
|
"reqwest",
|
|
@@ -3816,6 +3820,7 @@ dependencies = [
|
|
|
3816
3820
|
"vault-domain",
|
|
3817
3821
|
"vault-signer",
|
|
3818
3822
|
"vault-transport-unix",
|
|
3823
|
+
"x25519-dalek",
|
|
3819
3824
|
"zeroize",
|
|
3820
3825
|
]
|
|
3821
3826
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
WLFI Agentic SDK is a root-managed local signing daemon with policy enforcement, a single `wlfi-agent` CLI, and an optional relay + web approval flow.
|
|
4
4
|
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- macOS
|
|
10
|
+
- Rust toolchain on `PATH` (`cargo`, `rustc`) with Rust `1.87.0` or newer
|
|
11
|
+
- Xcode Command Line Tools (`xcode-select --install`)
|
|
12
|
+
|
|
13
|
+
### Install from npm
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i -g @wlfi-agent/cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`npm i -g @wlfi-agent/cli` builds the local Rust runtime during `postinstall`. If the prerequisites above are already installed, this is the normal one-step install path. If `cargo` or the macOS Command Line Tools are missing, installation fails immediately and tells you how to install the missing prerequisite before retrying.
|
|
20
|
+
|
|
21
|
+
### Work from this repo
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm install
|
|
25
|
+
npm run build
|
|
26
|
+
npm run install:cli-launcher
|
|
27
|
+
npm run install:rust-binaries
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
On macOS, add `export PATH="$HOME/.wlfi_agent/bin:$PATH"` to `~/.zshrc`, then reload your shell with `source ~/.zshrc`.
|
|
31
|
+
|
|
32
|
+
On Linux, add `export PATH="$HOME/.wlfi_agent/bin:$PATH"` to your shell startup file such as `~/.bashrc`, `~/.zshrc`, or `~/.profile`, then reload that file or open a new shell.
|
|
33
|
+
|
|
34
|
+
`npm run install:cli-launcher` installs the `wlfi-agent` launcher into `~/.wlfi_agent/bin`, and `npm run install:rust-binaries` installs the Rust runtime into the same directory.
|
|
35
|
+
|
|
36
|
+
### Reinstall Rust daemon
|
|
37
|
+
|
|
38
|
+
If you update Rust daemon code, rerun `npm run install:rust-binaries` so the root-managed daemon uses the new installed binaries under `~/.wlfi_agent/bin`.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
5
42
|
The main user path is:
|
|
6
43
|
|
|
7
44
|
1. run `wlfi-agent admin setup`
|
|
@@ -15,6 +52,7 @@ User-facing examples below avoid shell env vars on purpose. Prefer prompts, conf
|
|
|
15
52
|
|
|
16
53
|
- `wlfi-agent admin setup`
|
|
17
54
|
- first-run setup
|
|
55
|
+
- `--reuse-existing-wallet` reattaches the current local vault when you need to recover the daemon or refresh local credentials without creating a fresh wallet
|
|
18
56
|
- stores the vault password in macOS System Keychain
|
|
19
57
|
- installs the root LaunchDaemon
|
|
20
58
|
- creates a vault key + agent key
|
|
@@ -41,49 +79,36 @@ User-facing examples below avoid shell env vars on purpose. Prefer prompts, conf
|
|
|
41
79
|
- `wlfi-agent daemon`
|
|
42
80
|
- not a user entrypoint; daemon lifecycle is managed by `wlfi-agent admin setup`
|
|
43
81
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
### Prerequisites
|
|
47
|
-
|
|
48
|
-
- macOS
|
|
49
|
-
- Rust toolchain on `PATH` (`cargo`, `rustc`) with Rust `1.87.0` or newer
|
|
50
|
-
- Xcode Command Line Tools (`xcode-select --install`)
|
|
82
|
+
## Easiest wallet setup
|
|
51
83
|
|
|
52
|
-
|
|
84
|
+
Run this once:
|
|
53
85
|
|
|
54
86
|
```bash
|
|
55
|
-
|
|
87
|
+
wlfi-agent admin setup
|
|
56
88
|
```
|
|
57
89
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
### Work from this repo
|
|
90
|
+
Preview the exact sanitized setup plan first:
|
|
61
91
|
|
|
62
92
|
```bash
|
|
63
|
-
|
|
64
|
-
npm run build
|
|
65
|
-
npm run install:rust-binaries
|
|
93
|
+
wlfi-agent admin setup --plan
|
|
66
94
|
```
|
|
67
95
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
## Easiest wallet setup
|
|
96
|
+
The preview is read-only. It does not prompt for the vault password, does not touch sudo, and does not mutate wallet or policy state. It prints the planned Rust command, trust preflight results, overwrite risk, and the password transport mode that would be used for the real setup.
|
|
71
97
|
|
|
72
|
-
|
|
98
|
+
During a real `wlfi-agent admin setup`, you may be prompted for two different secrets:
|
|
73
99
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```
|
|
100
|
+
- `Vault password`: the wallet password you choose for encrypted local state
|
|
101
|
+
- `macOS admin password for sudo`: your macOS login/admin password, used only when setup needs elevated privileges to install or recover the root LaunchDaemon
|
|
77
102
|
|
|
78
|
-
|
|
103
|
+
If the local vault already exists and you only need to recover the managed daemon or refresh local setup state, reuse the current wallet instead of creating a fresh one:
|
|
79
104
|
|
|
80
105
|
```bash
|
|
81
|
-
wlfi-agent admin setup --
|
|
106
|
+
wlfi-agent admin setup --reuse-existing-wallet
|
|
82
107
|
```
|
|
83
108
|
|
|
84
|
-
|
|
109
|
+
This reuse path keeps the current vault address, prompts for `REUSE` in interactive mode, and still requires `--yes` in non-interactive mode.
|
|
85
110
|
|
|
86
|
-
|
|
111
|
+
After that, the command:
|
|
87
112
|
|
|
88
113
|
- installs or refreshes the root daemon
|
|
89
114
|
- waits for the daemon to come up
|
|
@@ -372,10 +397,18 @@ When relay is configured and a request requires manual approval:
|
|
|
372
397
|
3. the frontend encrypts the vault password + decision to the daemon’s advertised X25519 public key
|
|
373
398
|
4. the relay queues the encrypted update
|
|
374
399
|
5. the daemon polls, decrypts, applies the decision, and reports status back
|
|
375
|
-
6. the original
|
|
400
|
+
6. for `wlfi-agent transfer --broadcast`, `wlfi-agent transfer-native --broadcast`, and `wlfi-agent approve --broadcast`, the original CLI command keeps waiting on that same approval request and continues automatically after approval
|
|
401
|
+
7. commands outside those broadcast flows still print the approval details and exit, so operators can approve or reject the request separately
|
|
376
402
|
|
|
377
403
|
If the frontend link is unavailable, operators can always fall back to the local admin CLI approval command printed by the agent CLI.
|
|
378
404
|
|
|
405
|
+
For the auto-waiting broadcast flows above:
|
|
406
|
+
|
|
407
|
+
- do not rerun the original command after approving in the browser
|
|
408
|
+
- the CLI polls every 2 seconds for up to 5 minutes
|
|
409
|
+
- if the daemon returns a different approval request id while waiting, the CLI stops and tells you to inspect the approval status before rerunning
|
|
410
|
+
- approval details and waiting events go to `stderr`; the final successful `--json` result still goes to `stdout`
|
|
411
|
+
|
|
379
412
|
## Operational notes
|
|
380
413
|
|
|
381
414
|
- The daemon state file lives at `/var/db/wlfi-agent/daemon-state.enc` and is intended to be root-only.
|
|
@@ -438,7 +471,7 @@ wlfi-agent admin tui
|
|
|
438
471
|
wlfi-agent admin uninstall
|
|
439
472
|
wlfi-agent admin get-relay-config
|
|
440
473
|
wlfi-agent admin list-manual-approval-requests
|
|
441
|
-
wlfi-agent wallet
|
|
474
|
+
wlfi-agent wallet
|
|
442
475
|
npm run install:rust-binaries
|
|
443
476
|
pnpm build
|
|
444
477
|
pnpm typecheck
|
|
@@ -62,9 +62,11 @@ fn read_secret_from_reader(mut reader: impl Read, label: &str) -> Result<String>
|
|
|
62
62
|
#[cfg(test)]
|
|
63
63
|
mod tests {
|
|
64
64
|
use super::{
|
|
65
|
-
|
|
65
|
+
emit_output, ensure_output_parent, print_status, read_secret_from_reader,
|
|
66
|
+
resolve_output_target, resolve_vault_password, should_print_status, validate_password,
|
|
66
67
|
write_output_file,
|
|
67
68
|
};
|
|
69
|
+
use crate::{OutputFormat, OutputTarget};
|
|
68
70
|
use std::fs;
|
|
69
71
|
use std::io::Cursor;
|
|
70
72
|
use std::path::PathBuf;
|
|
@@ -95,12 +97,57 @@ mod tests {
|
|
|
95
97
|
assert!(err.to_string().contains("must not exceed"));
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
#[test]
|
|
101
|
+
fn read_secret_from_reader_trims_newlines_and_accepts_valid_secret() {
|
|
102
|
+
let secret =
|
|
103
|
+
read_secret_from_reader(Cursor::new(b"vault-secret\r\n".to_vec()), "vault password")
|
|
104
|
+
.expect("valid stdin secret");
|
|
105
|
+
assert_eq!(secret, "vault-secret");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn validate_password_accepts_non_empty_secret() {
|
|
110
|
+
let password = validate_password("vault-secret".to_string(), "prompt")
|
|
111
|
+
.expect("valid password");
|
|
112
|
+
assert_eq!(password, "vault-secret");
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
#[test]
|
|
99
116
|
fn resolve_vault_password_requires_stdin_in_non_interactive_mode() {
|
|
100
117
|
let err = resolve_vault_password(false, true).expect_err("must fail");
|
|
101
118
|
assert!(err.to_string().contains("use --vault-password-stdin"));
|
|
102
119
|
}
|
|
103
120
|
|
|
121
|
+
#[test]
|
|
122
|
+
fn resolve_output_target_covers_stdout_and_file_paths() {
|
|
123
|
+
let stdout_target = resolve_output_target(None, false).expect("default stdout");
|
|
124
|
+
assert!(matches!(stdout_target, OutputTarget::Stdout));
|
|
125
|
+
|
|
126
|
+
let file_target = resolve_output_target(Some(PathBuf::from("out.json")), false)
|
|
127
|
+
.expect("file output target");
|
|
128
|
+
assert!(matches!(
|
|
129
|
+
file_target,
|
|
130
|
+
OutputTarget::File {
|
|
131
|
+
path,
|
|
132
|
+
overwrite: false
|
|
133
|
+
} if path == PathBuf::from("out.json")
|
|
134
|
+
));
|
|
135
|
+
|
|
136
|
+
let err = resolve_output_target(Some(PathBuf::from("-")), true).expect_err("must fail");
|
|
137
|
+
assert!(err.to_string().contains("--overwrite cannot be used with --output -"));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[test]
|
|
141
|
+
fn emit_output_to_stdout_and_print_status_cover_text_paths() {
|
|
142
|
+
emit_output("stdout output", &OutputTarget::Stdout).expect("stdout output");
|
|
143
|
+
assert!(should_print_status(OutputFormat::Text, false));
|
|
144
|
+
assert!(!should_print_status(OutputFormat::Json, false));
|
|
145
|
+
assert!(!should_print_status(OutputFormat::Text, true));
|
|
146
|
+
print_status("status output", OutputFormat::Text, false);
|
|
147
|
+
print_status("quiet output", OutputFormat::Text, true);
|
|
148
|
+
print_status("json output", OutputFormat::Json, false);
|
|
149
|
+
}
|
|
150
|
+
|
|
104
151
|
#[test]
|
|
105
152
|
#[cfg(unix)]
|
|
106
153
|
fn ensure_output_parent_rejects_symlinked_parent_directory() {
|
|
@@ -188,6 +235,46 @@ mod tests {
|
|
|
188
235
|
fs::remove_dir_all(&root).expect("cleanup temp tree");
|
|
189
236
|
}
|
|
190
237
|
|
|
238
|
+
#[test]
|
|
239
|
+
#[cfg(unix)]
|
|
240
|
+
fn ensure_output_parent_rejects_directory_path() {
|
|
241
|
+
use std::os::unix::fs::PermissionsExt;
|
|
242
|
+
|
|
243
|
+
let root = temp_path("vault-cli-admin-output-dir-path");
|
|
244
|
+
fs::create_dir_all(&root).expect("create root");
|
|
245
|
+
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
|
|
246
|
+
.expect("secure root permissions");
|
|
247
|
+
|
|
248
|
+
let err = ensure_output_parent(&root).expect_err("directory path must fail");
|
|
249
|
+
assert!(err
|
|
250
|
+
.to_string()
|
|
251
|
+
.contains("is a directory; provide a file path"));
|
|
252
|
+
|
|
253
|
+
fs::remove_dir_all(&root).expect("cleanup temp tree");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
#[cfg(unix)]
|
|
258
|
+
fn ensure_output_parent_allows_sticky_world_writable_ancestor() {
|
|
259
|
+
use std::os::unix::fs::PermissionsExt;
|
|
260
|
+
|
|
261
|
+
let root = temp_path("vault-cli-admin-output-sticky-ancestor");
|
|
262
|
+
let shared = root.join("shared");
|
|
263
|
+
let nested = shared.join("nested");
|
|
264
|
+
fs::create_dir_all(&nested).expect("create nested directory");
|
|
265
|
+
fs::set_permissions(&shared, fs::Permissions::from_mode(0o1777))
|
|
266
|
+
.expect("set sticky shared permissions");
|
|
267
|
+
fs::set_permissions(&nested, fs::Permissions::from_mode(0o700))
|
|
268
|
+
.expect("set secure nested permissions");
|
|
269
|
+
|
|
270
|
+
ensure_output_parent(&nested.join("output.json"))
|
|
271
|
+
.expect("sticky world-writable ancestor should be allowed");
|
|
272
|
+
|
|
273
|
+
fs::set_permissions(&shared, fs::Permissions::from_mode(0o700))
|
|
274
|
+
.expect("restore cleanup permissions");
|
|
275
|
+
fs::remove_dir_all(&root).expect("cleanup temp tree");
|
|
276
|
+
}
|
|
277
|
+
|
|
191
278
|
#[test]
|
|
192
279
|
#[cfg(unix)]
|
|
193
280
|
fn write_output_file_overwrite_replaces_existing_hard_link_instead_of_mutating_shared_inode() {
|
|
@@ -218,6 +305,67 @@ mod tests {
|
|
|
218
305
|
|
|
219
306
|
fs::remove_dir_all(&root).expect("cleanup temp tree");
|
|
220
307
|
}
|
|
308
|
+
|
|
309
|
+
#[test]
|
|
310
|
+
#[cfg(unix)]
|
|
311
|
+
fn write_output_file_creates_new_file_and_rejects_existing_without_overwrite() {
|
|
312
|
+
use std::os::unix::fs::PermissionsExt;
|
|
313
|
+
|
|
314
|
+
let root = temp_path("vault-cli-admin-output-create-file");
|
|
315
|
+
fs::create_dir_all(&root).expect("create root directory");
|
|
316
|
+
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
|
|
317
|
+
.expect("secure root directory permissions");
|
|
318
|
+
|
|
319
|
+
let output_path = root.join("output.json");
|
|
320
|
+
write_output_file(&output_path, "created", false).expect("write new output file");
|
|
321
|
+
assert_eq!(
|
|
322
|
+
fs::read_to_string(&output_path).expect("read created output"),
|
|
323
|
+
"created\n"
|
|
324
|
+
);
|
|
325
|
+
assert_eq!(
|
|
326
|
+
fs::metadata(&output_path)
|
|
327
|
+
.expect("metadata")
|
|
328
|
+
.permissions()
|
|
329
|
+
.mode()
|
|
330
|
+
& 0o777,
|
|
331
|
+
0o600
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
let err = write_output_file(&output_path, "second", false)
|
|
335
|
+
.expect_err("existing file without overwrite must fail");
|
|
336
|
+
assert!(err
|
|
337
|
+
.to_string()
|
|
338
|
+
.contains("already exists; pass --overwrite to replace it"));
|
|
339
|
+
|
|
340
|
+
fs::remove_dir_all(&root).expect("cleanup temp tree");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[test]
|
|
344
|
+
#[cfg(unix)]
|
|
345
|
+
fn emit_output_to_file_target_writes_expected_content() {
|
|
346
|
+
use std::os::unix::fs::PermissionsExt;
|
|
347
|
+
|
|
348
|
+
let root = temp_path("vault-cli-admin-emit-output-file");
|
|
349
|
+
fs::create_dir_all(&root).expect("create root directory");
|
|
350
|
+
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
|
|
351
|
+
.expect("secure root directory permissions");
|
|
352
|
+
|
|
353
|
+
let output_path = root.join("output.json");
|
|
354
|
+
emit_output(
|
|
355
|
+
"file output",
|
|
356
|
+
&OutputTarget::File {
|
|
357
|
+
path: output_path.clone(),
|
|
358
|
+
overwrite: false,
|
|
359
|
+
},
|
|
360
|
+
)
|
|
361
|
+
.expect("emit output to file");
|
|
362
|
+
assert_eq!(
|
|
363
|
+
fs::read_to_string(&output_path).expect("read emitted file"),
|
|
364
|
+
"file output\n"
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
fs::remove_dir_all(&root).expect("cleanup temp tree");
|
|
368
|
+
}
|
|
221
369
|
}
|
|
222
370
|
|
|
223
371
|
pub(crate) fn resolve_output_format(
|