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