set method
Writes value (rendered as a YAML scalar) at the dotted key in the
resolved scope file. The edited text is validated with the real
parsers BEFORE the write, so a bad value throws and nothing is
persisted. Missing files are created with the minimal section (never a
half-written document).
Implementation
Future<ConfigSetResult> set(
String key,
String value, {
ConfigScope? scope,
}) async {
final segments = _parseKey(key);
if (value.trim().isEmpty) {
throw ConfigException(
'empty value for "$key" — removing a key is a manual file edit',
);
}
// Platform-inapplicable keys are refused with the host-capability
// reason — never written as dead config (issue #29 AC11/E13).
final stdio = _stdioServerRefusal(segments, _tryDecodeJson(value));
if (stdio != null) {
throw ConfigException(_mcpStdioNote(stdio));
}
final resolved = await resolveWriteScope(scope, key: key);
if (resolved == ConfigScope.project &&
!_projectSections.contains(segments.first)) {
throw ConfigException(
'"${segments.first}" is only read from the user file — the project '
'file participates in ${_projectSections.join('/')} only. Write it '
'to the user file: fa config set $key <value> --global',
);
}
final file = switch (resolved) {
ConfigScope.project => projectConfigPath,
ConfigScope.global => globalConfigPath,
};
if (file == null) {
throw const ConfigException(
'global scope unavailable on this host (no home directory)',
);
}
final text = await _readTextOrNull(file) ?? '';
final oldDisplay = _lookupDisplay(text, segments) ?? '(absent)';
// A JSON array/object value renders as a yaml block (list-valued keys
// — `customProviders`, the `roles:` chains, `redact:` lists); any
// other value is the single scalar line `upsertYamlPath` always wrote.
final leafLines = configLeafLines(value, depth: segments.length - 1);
final edited = upsertYamlPath(text, segments, leafLines);
// New or previously newline-less files still end with a newline.
final normalized = edited.isEmpty || edited.endsWith('\n')
? edited
: '$edited\n';
// Never persist a file the next boot would reject.
final errors = <ConfigDiagnostic>[];
_collectDiagnostics(normalized, file, errors, <ConfigDiagnostic>[]);
if (errors.isNotEmpty) {
throw ConfigException(errors.map((e) => e.message).join('; '));
}
switch (await env.writeFile(file, normalized)) {
case Err(:final error):
throw ConfigException('cannot write $file: $error');
case Ok():
break;
}
return ConfigSetResult(
key: key,
file: file,
scope: resolved.name,
oldDisplay: oldDisplay,
newDisplay: value,
application: applicationNote(segments.first),
);
}