using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; namespace Amanotes { internal static class ManifestEditor { private const string ManifestPath = "Packages/manifest.json"; internal static string Read() { if (!File.Exists(ManifestPath)) return ""; return File.ReadAllText(ManifestPath); } internal static void Write(string json) { // Safety: validate that no scopedRegistry has an empty scopes array // Unity rejects manifests with "scopes": [] json = ValidateScopedRegistries(json); File.WriteAllText(ManifestPath, json); } /// /// Removes any scoped registry entry that has an empty scopes array. /// This prevents Unity's "scopedRegistries[N].scopes must contain at least 1 items" error. /// private static string ValidateScopedRegistries(string json) { var registriesKey = "\"scopedRegistries\""; int regIdx = json.IndexOf(registriesKey); if (regIdx < 0) return json; // Find the opening [ of the scopedRegistries array int arrayStart = json.IndexOf('[', regIdx); if (arrayStart < 0) return json; // Find the matching ] by tracking bracket depth int depth = 0; int arrayEnd = -1; for (int i = arrayStart; i < json.Length; i++) { if (json[i] == '[') depth++; else if (json[i] == ']') { depth--; if (depth == 0) { arrayEnd = i; break; } } } if (arrayEnd < 0) return json; // Check for empty scopes pattern: "scopes": [] or "scopes": [\n\s*] var emptyScopes = Regex.Match(json.Substring(regIdx, arrayEnd - regIdx + 1), "\"scopes\"\\s*:\\s*\\[\\s*\\]"); if (!emptyScopes.Success) return json; // Found an empty scopes array — remove the entire registry object containing it // Find the registry object boundaries int scopesPos = regIdx + emptyScopes.Index; // Walk backwards to find the opening { of this registry object int objStart = -1; int braceCount = 0; for (int i = scopesPos; i >= arrayStart; i--) { if (json[i] == '}') braceCount++; else if (json[i] == '{') { braceCount--; if (braceCount < 0) { objStart = i; break; } } } if (objStart < 0) return json; // Find the closing } of this registry object int objEnd = -1; braceCount = 0; for (int i = objStart; i <= arrayEnd; i++) { if (json[i] == '{') braceCount++; else if (json[i] == '}') { braceCount--; if (braceCount == 0) { objEnd = i; break; } } } if (objEnd < 0) return json; // Include trailing comma if present, or leading comma int removeStart = objStart; int removeEnd = objEnd; // Check for trailing comma after the object int afterObj = objEnd + 1; while (afterObj < arrayEnd && (json[afterObj] == ' ' || json[afterObj] == '\n' || json[afterObj] == '\r' || json[afterObj] == '\t')) afterObj++; if (afterObj < arrayEnd && json[afterObj] == ',') { removeEnd = afterObj; } else { // No trailing comma — check for leading comma before this object int beforeObj = objStart - 1; while (beforeObj > arrayStart && (json[beforeObj] == ' ' || json[beforeObj] == '\n' || json[beforeObj] == '\r' || json[beforeObj] == '\t')) beforeObj--; if (beforeObj > arrayStart && json[beforeObj] == ',') removeStart = beforeObj; } // Also trim surrounding whitespace/newlines for clean output while (removeStart > arrayStart + 1 && (json[removeStart - 1] == ' ' || json[removeStart - 1] == '\n' || json[removeStart - 1] == '\r' || json[removeStart - 1] == '\t')) removeStart--; // Keep at least one newline before next element if (removeStart > arrayStart + 1) removeStart++; json = json.Remove(removeStart, removeEnd - removeStart + 1); // Recursively check for more empty scopes (in case multiple registries are affected) return ValidateScopedRegistries(json); } internal static bool Exists() => File.Exists(ManifestPath); internal static string GetValue(string json, string packageName) { var m = Regex.Match(json, $"\"{Regex.Escape(packageName)}\"\\s*:\\s*\"([^\"]+)\""); return m.Success ? m.Groups[1].Value : null; } internal static string SetValue(string json, string packageName, string newValue) { var pattern = $"\"{Regex.Escape(packageName)}\"\\s*:\\s*\"([^\"]+)\""; var match = Regex.Match(json, pattern); if (!match.Success) return json; return json.Replace(match.Value, $"\"{packageName}\": \"{newValue}\""); } internal static string AddEntry(string json, string packageName, string value) { var idx = json.IndexOf("\"dependencies\""); if (idx < 0) return json; var braceIdx = json.IndexOf('{', idx); if (braceIdx < 0) return json; var entry = $"\n \"{packageName}\": \"{value}\","; return json.Insert(braceIdx + 1, entry); } internal static string RemoveEntry(string json, string packageName) { // Only remove the package line from the "dependencies" block. // Never touch scopedRegistries or other sections. var lines = json.Split('\n'); var result = new List(); bool inDependencies = false; int braceDepth = 0; for (int i = 0; i < lines.Length; i++) { var trimmed = lines[i].TrimStart(); if (!inDependencies) { if (trimmed.StartsWith("\"dependencies\"")) { inDependencies = true; braceDepth = 0; // Count braces on this line for (int c = 0; c < lines[i].Length; c++) { if (lines[i][c] == '{') braceDepth++; else if (lines[i][c] == '}') braceDepth--; } } result.Add(lines[i]); continue; } // Inside dependencies block for (int c = 0; c < lines[i].Length; c++) { if (lines[i][c] == '{') braceDepth++; else if (lines[i][c] == '}') braceDepth--; } if (braceDepth <= 0) { // Leaving dependencies block inDependencies = false; result.Add(lines[i]); continue; } // Inside dependencies — skip line if it matches the package if (lines[i].Contains($"\"{packageName}\"")) continue; result.Add(lines[i]); } // Clean up trailing comma before closing brace in dependencies return CleanTrailingComma(string.Join("\n", result)); } /// /// Fixes trailing comma before a closing brace/bracket which is invalid JSON. /// Only cleans within the dependencies block. /// private static string CleanTrailingComma(string json) { // Match: comma followed by optional whitespace/newlines then closing brace return Regex.Replace(json, @",(\s*\n\s*})", "$1"); } /// /// Checks if a package exists in the dependencies section only. /// Does NOT match scopedRegistries scopes or other sections. /// internal static bool Contains(string json, string packageName) { return GetValue(json, packageName) != null; } } }