set method

Future<ConfigSetResult> set(
  1. String key,
  2. String value, {
  3. ConfigScope? scope,
})

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