saveCliConfig function

Future<void> saveCliConfig(
  1. String homeDir,
  2. CliConfig config
)

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);
    }
  });
}