tray: --import-profile merges a profile document into the user config

RioJoy.Tray --import-profile <profile.json> merges a single-profile
document (a repo's profiles/*.json) into %APPDATA%\RIOJoy\config.json
via ConfigStore.ImportProfile: same-name profiles are replaced in place
(FindProfile's case-insensitive convention), everything else appended,
all other config content preserved. Refuses (exit 3) while a tray
instance is running - a live tray rewrites the config from memory and
would silently discard the import. Documents that a Name must be stated
in the file itself: RioProfile defaults Name to 'Unnamed', so the import
checks the raw JSON, not the deserialized object. This is the profile
install story for bench machines and pods (previously: hand-paste into
the Profiles array).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-30 09:33:25 -05:00
co-authored by Claude Fable 5
parent 6967e66837
commit 3512c89dca
4 changed files with 176 additions and 3 deletions
+44
View File
@@ -52,4 +52,48 @@ public static class ConfigStore
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
return File.Exists(path) ? Deserialize(File.ReadAllText(path)) : new AppConfig();
}
/// <summary>
/// Deserialize a single-profile document (the shape shipped in a repo's
/// <c>profiles/*.json</c>) with the same serializer settings as the config.
/// </summary>
public static RioProfile DeserializeProfile(string json)
{
if (string.IsNullOrWhiteSpace(json)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(json));
return JsonConvert.DeserializeObject<RioProfile>(json, Options)
?? throw new JsonSerializationException("Profile JSON deserialized to null.");
}
/// <summary>
/// Merge the single-profile file at <paramref name="profilePath"/> into the
/// config at <paramref name="configPath"/> (created with defaults if absent).
/// A profile with the same name (case-insensitive, the
/// <see cref="AppConfig.FindProfile"/> convention) is replaced in place;
/// otherwise the profile is appended. All other config content is preserved.
/// </summary>
public static ProfileImportResult ImportProfile(string configPath, string profilePath)
{
string profileJson = File.ReadAllText(profilePath);
RioProfile profile = DeserializeProfile(profileJson);
// Name keys the merge, and RioProfile defaults it to "Unnamed" — so a
// document that never states one must be rejected, not merged under the
// class default.
if (Newtonsoft.Json.Linq.JObject.Parse(profileJson).Value<string>("Name") is not { Length: > 0 })
throw new JsonSerializationException($"Profile file '{profilePath}' has no Name.");
AppConfig config = Load(configPath);
int existing = config.Profiles.FindIndex(
p => string.Equals(p.Name, profile.Name, StringComparison.OrdinalIgnoreCase));
bool replaced = existing >= 0;
if (replaced)
config.Profiles[existing] = profile;
else
config.Profiles.Add(profile);
Save(config, configPath);
return new ProfileImportResult(profile.Name!, replaced);
}
}
/// <summary>Outcome of <see cref="ConfigStore.ImportProfile"/>.</summary>
public sealed record ProfileImportResult(string Name, bool Replaced);