feat(sync): add --local-file output target#317
Conversation
Add sync flag to write generated sync overrides into fnox.local.toml next to the active config path, with --global conflict guard. Update sync docs/usage and add regression tests for local-file write path + custom --config directory behavior.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request enhances the Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a --local-file flag for the sync command, which directs sync overrides to a fnox.local.toml file. This is a well-implemented feature that improves local development workflows. The changes are comprehensive, including updates to the CLI logic, documentation, and the addition of thorough regression tests. My review found a minor code duplication in src/commands/sync.rs for constructing the output message suffix, and I've provided a suggestion to refactor it for better maintainability. Otherwise, the changes look good.
Greptile SummaryThis PR adds a Key changes:
Style notes (not blocking):
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant CLI
participant SyncCommand
participant Config
participant FS
User->>CLI: fnox sync --local-file [--config path]
CLI->>Config: load_smart(cli.config)
Config-->>CLI: merged_config
CLI->>SyncCommand: run(cli, merged_config)
SyncCommand->>Config: find_local_config(cwd, profile)
Config->>FS: path.exists() checks (up to 6)
FS-->>Config: candidate path
Config-->>SyncCommand: effective_config_path
SyncCommand->>Config: local_override_filename(effective_config_path)
alt basename is fnox.toml or .fnox.toml
Config-->>SyncCommand: Some("fnox.local.toml" / ".fnox.local.toml")
else unsupported filename
Config-->>SyncCommand: None → Error returned
end
SyncCommand->>SyncCommand: filter & resolve secrets
alt --dry-run
SyncCommand-->>User: "[dry-run] Would sync N secrets ... (local-file)"
else write path
SyncCommand->>FS: create_dir_all(config_dir)
SyncCommand->>FS: write synced secrets to fnox.local.toml
SyncCommand-->>User: "Synced N secrets ... (local-file)"
end
User->>CLI: fnox get SECRET
CLI->>Config: load_with_recursion(cwd)
Config->>FS: load fnox.toml (secret definition)
Config->>FS: load fnox.local.toml (sync cache)
Config-->>CLI: merged config with sync overrides
CLI-->>User: decrypted secret value
Last reviewed commit: 5ce3b1e |
| fn test_local_override_filename_matches_standard_config_names() { | ||
| assert_eq!( | ||
| local_override_filename(Path::new("nested/fnox.toml")), | ||
| Some("fnox.local.toml") | ||
| ); | ||
| assert_eq!( | ||
| local_override_filename(Path::new("nested/.fnox.toml")), | ||
| Some(".fnox.local.toml") | ||
| ); |
There was a problem hiding this comment.
Test name claims "standard config names" but includes a non-standard path
test_local_override_filename_matches_standard_config_names asserts that nested/fnox.toml returns Some("fnox.local.toml"). The function itself behaves correctly (it only inspects the basename), but nested/fnox.toml is not a standard config path — it is a path with a directory prefix that, when passed to load_smart, takes the else branch and loads only the explicit file, meaning the adjacent fnox.local.toml is never picked up.
Including this case under "standard config names" may give future readers the false impression that this path is a supported input for the --local-file feature. Consider moving it to a separate test named something like test_local_override_filename_basename_only_match and pairing it with a comment that makes the limitation explicit:
#[test]
fn test_local_override_filename_basename_only_match() {
// The function inspects only the basename. Note that paths with directory
// components (e.g. "nested/fnox.toml") are technically accepted here but
// the --local-file feature is NOT supported for such paths because
// load_smart does not pick up the adjacent local override file.
assert_eq!(
local_override_filename(Path::new("nested/fnox.toml")),
Some("fnox.local.toml")
);
}| ### `--local-file` | ||
|
|
||
| Write sync overrides to the local override file next to the config file |
There was a problem hiding this comment.
--local-file help text omits critical constraint
The flag's help text only says "Write sync overrides to the local override file next to the config file", but it silently accepts paths like --config nested/fnox.toml (the basename check in local_override_filename passes), writes nested/fnox.local.toml, yet those secrets are never read back because load_smart takes the direct-load path for any path with directory components.
The constraint that only bare fnox.toml / .fnox.toml config filenames are supported is documented in docs/guide/hierarchical-config.md, but a user reading the command reference first will see no warning. The same omission is present in docs/cli/commands.json and the fnox.usage.kdl help string.
Consider adding the restriction to all three help strings, e.g.:
| ### `--local-file` | |
| Write sync overrides to the local override file next to the config file | |
| ### `--local-file` | |
| Write sync overrides to the local override file (`fnox.local.toml` / `.fnox.local.toml`) next to the config file. Only supported when the active config basename is `fnox.toml` or `.fnox.toml`. |
| let effective_config_path = | ||
| if cli.config == std::path::Path::new(config::DEFAULT_CONFIG_FILENAME) { | ||
| let current_dir = std::env::current_dir().map_err(|e| { | ||
| FnoxError::Config(format!("Failed to get current directory: {}", e)) | ||
| })?; | ||
| let candidate = config::find_local_config(¤t_dir, Some(&profile)); | ||
| if local_override_filename(&candidate).is_some() { | ||
| candidate | ||
| } else { | ||
| cli.config.clone() | ||
| } | ||
| } else { | ||
| cli.config.clone() | ||
| }; |
There was a problem hiding this comment.
effective_config_path computed unconditionally regardless of --local-file
find_local_config performs filesystem I/O (up to ~6 path.exists() calls) on every fnox sync invocation even when --local-file is not set. effective_config_path is only consumed inside the two self.local_file-gated branches (lines 67–77 and 241–251), so this work is entirely wasted for the common case.
Consider guarding the block:
let effective_config_path = if self.local_file {
if cli.config == std::path::Path::new(config::DEFAULT_CONFIG_FILENAME) {
let current_dir = std::env::current_dir().map_err(|e| {
FnoxError::Config(format!("Failed to get current directory: {}", e))
})?;
let candidate = config::find_local_config(¤t_dir, Some(&profile));
if local_override_filename(&candidate).is_some() {
candidate
} else {
cli.config.clone()
}
} else {
cli.config.clone()
}
} else {
cli.config.clone() // unused, but avoids Option wrapping
};…fnox into feat/sync-local-file
| let local_override_filename = self | ||
| .local_file | ||
| .then(|| { | ||
| local_override_filename(&effective_config_path).ok_or_else(|| { | ||
| FnoxError::Config(format!( | ||
| "--local-file requires --config to be 'fnox.toml' or '.fnox.toml'; '{}' would not load the adjacent local override file", | ||
| effective_config_path.display() | ||
| )) | ||
| }) | ||
| }) | ||
| .transpose()?; |
There was a problem hiding this comment.
local_override_filename variable shadows imported function
The let local_override_filename binding on line 67 shadows the local_override_filename function imported at line 2. Rust resolves name lookups in scope order, so any call to local_override_filename(...) after this point within run() would resolve to the Option<&'static str> value, not the function — resulting in a type error instead of a call. It doesn't cause a runtime bug today (the function isn't called again after line 77), but it's a maintainability hazard: a future refactor that calls the helper again after this point would get a confusing compile error rather than the expected behaviour.
Consider renaming the local binding to avoid the collision:
| let local_override_filename = self | |
| .local_file | |
| .then(|| { | |
| local_override_filename(&effective_config_path).ok_or_else(|| { | |
| FnoxError::Config(format!( | |
| "--local-file requires --config to be 'fnox.toml' or '.fnox.toml'; '{}' would not load the adjacent local override file", | |
| effective_config_path.display() | |
| )) | |
| }) | |
| }) | |
| .transpose()?; | |
| let local_override_target = self | |
| .local_file | |
| .then(|| { | |
| local_override_filename(&effective_config_path).ok_or_else(|| { | |
| FnoxError::Config(format!( | |
| "--local-file requires --config to be 'fnox.toml' or '.fnox.toml'; '{}' would not load the adjacent local override file", | |
| effective_config_path.display() | |
| )) | |
| }) | |
| }) | |
| .transpose()?; |
Then update the later usages at lines 249 and 307 accordingly (e.g. local_override_target.expect(...)).
| let effective_config_path = | ||
| if cli.config == std::path::Path::new(config::DEFAULT_CONFIG_FILENAME) { | ||
| let current_dir = std::env::current_dir().map_err(|e| { | ||
| FnoxError::Config(format!("Failed to get current directory: {}", e)) | ||
| })?; | ||
| let candidate = config::find_local_config(¤t_dir, Some(&profile)); | ||
| if local_override_filename(&candidate).is_some() { | ||
| candidate | ||
| } else { | ||
| cli.config.clone() | ||
| } | ||
| } else { | ||
| cli.config.clone() | ||
| }; |
There was a problem hiding this comment.
effective_config_path computed unconditionally
find_local_config performs up to ~6 path.exists() filesystem calls on every fnox sync invocation, even when --local-file is not set. effective_config_path is only consumed inside the two self.local_file-gated branches (lines 67–77 and 241–251), so this I/O is entirely wasted for the common case.
Wrapping the block in an if self.local_file { ... } guard would make it a no-op unless the flag is actually passed:
let effective_config_path = if self.local_file {
if cli.config == std::path::Path::new(config::DEFAULT_CONFIG_FILENAME) {
let current_dir = std::env::current_dir().map_err(|e| {
FnoxError::Config(format!("Failed to get current directory: {}", e))
})?;
let candidate = config::find_local_config(¤t_dir, Some(&profile));
if local_override_filename(&candidate).is_some() {
candidate
} else {
cli.config.clone()
}
} else {
cli.config.clone()
}
} else {
PathBuf::new() // unused; never accessed when local_file is false
};| #[test] | ||
| fn test_local_override_filename_matches_standard_config_names() { | ||
| assert_eq!( | ||
| local_override_filename(Path::new("nested/fnox.toml")), | ||
| Some("fnox.local.toml") | ||
| ); | ||
| assert_eq!( | ||
| local_override_filename(Path::new("nested/.fnox.toml")), | ||
| Some(".fnox.local.toml") | ||
| ); | ||
| } |
There was a problem hiding this comment.
Misleading test name includes a non-standard path
test_local_override_filename_matches_standard_config_names asserts that nested/fnox.toml returns Some("fnox.local.toml"). The function itself is correct (it only inspects the basename), but nested/fnox.toml is not a standard config path in the context of --local-file — it has a directory component that causes load_smart to take the direct-load branch and ignore adjacent local override files.
Including this case under "standard config names" may give future readers the false impression that paths with directory components are fully supported. Consider moving it to a dedicated test that makes the basename-only match behaviour explicit:
#[test]
fn test_local_override_filename_basename_only_match() {
// The function inspects only the basename. Paths with directory components
// are accepted here but are NOT valid --local-file inputs at the CLI level
// because load_smart would not load the adjacent fnox.local.toml for them.
assert_eq!(
local_override_filename(Path::new("nested/fnox.toml")),
Some("fnox.local.toml")
);
}|
@jdx the PR is ready for manual review I'd say 👍 |
### 🚀 Features - **(sync)** add --local-file output target by [@florian-lackner365](https://github.com/florian-lackner365) in [#317](#317) ### 🐛 Bug Fixes - properly handle auth prompt in batch providers by [@johnpyp](https://github.com/johnpyp) in [#349](#349) ### 🛡️ Security - **(mcp)** redact secret values from exec output by [@jdx](https://github.com/jdx) in [#357](#357) ### 📦️ Dependency Updates - lock file maintenance by [@renovate[bot]](https://github.com/renovate[bot]) in [#344](#344) - update jdx/mise-action digest to 5228313 by [@renovate[bot]](https://github.com/renovate[bot]) in [#351](#351) - update swatinem/rust-cache digest to e18b497 by [@renovate[bot]](https://github.com/renovate[bot]) in [#352](#352) - update taiki-e/upload-rust-binary-action digest to 381995c by [@renovate[bot]](https://github.com/renovate[bot]) in [#353](#353) - update dependency vue to v3.5.30 by [@renovate[bot]](https://github.com/renovate[bot]) in [#354](#354) - update rust crate openssl-sys to v0.9.112 by [@renovate[bot]](https://github.com/renovate[bot]) in [#355](#355) - update rust crate clap to v4.6.0 by [@renovate[bot]](https://github.com/renovate[bot]) in [#356](#356) ### New Contributors - @florian-lackner365 made their first contribution in [#317](#317) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Add sync flag to write generated sync overrides into fnox.local.toml next to the active config path, with --global conflict guard.
Update sync docs/usage and add regression tests for local-file write path + custom --config directory behavior.
closes #316