saveCliConfig function
Saves CliConfig to ~/.fah/config.yaml.
Merge-before-write (issue #221): several fa processes run concurrently
on the same machine, each with a config parsed at boot. A whole-file
rewrite from such a possibly-hours-old snapshot clobbers whatever the
other processes saved since — this machine lost the kimi_me custom
provider repeatedly to exactly that. So the save re-reads the on-disk
file first and merges the customProviders: section as a name-keyed
union (the caller's entries win per name; on-disk entries the caller
never loaded survive). Every other section is caller-wins, as before.
An unparseable on-disk file makes the save REFUSE with a loud ConfigException instead of clobbering the file with defaults (E3). The write itself is atomic (unique temp file + rename): a concurrent reader never observes a torn document and a crash mid-write leaves the previous file intact.
Implementation
Future<void> saveCliConfig(String homeDir, CliConfig config) async {
final dir = Directory('$homeDir/.fah');
if (!dir.existsSync()) dir.createSync(recursive: true);
final file = File('${dir.path}/config.yaml');
await _serializedConfigWrite(file.path, () async {
final diskText = file.existsSync() ? file.readAsStringSync() : '';
final merged = _mergeWithOnDisk(file, config, diskText);
final tmp = File('${file.path}.tmp.$pid.${_configTmpCounter++}');
await tmp.writeAsString(_preserveDiskSections(diskText, merged.toYaml()));
try {
await tmp.rename(file.path);
} on FileSystemException {
// Windows cannot rename over an existing file.
if (!Platform.isWindows) rethrow;
try {
await file.delete();
} on PathNotFoundException {
// Raced away — the rename below recreates it.
}
await tmp.rename(file.path);
}
});
}