171 lines
5.3 KiB
C#
171 lines
5.3 KiB
C#
using System.Text.Json;
|
|
|
|
namespace Sdt.Config;
|
|
|
|
public static class WorkspaceLoader
|
|
{
|
|
public const string FileName = "sdt-workspace.json";
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
AllowTrailingCommas = true,
|
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Walks up from <paramref name="startDir"/> (or CWD) to find sdt-workspace.json.
|
|
/// Returns null if not found.
|
|
/// </summary>
|
|
public static (WorkspaceConfig Config, string WorkspaceRoot)? FindAndLoad(string? startDir = null)
|
|
{
|
|
var dir = new DirectoryInfo(startDir ?? Directory.GetCurrentDirectory());
|
|
while (dir is not null)
|
|
{
|
|
var candidate = Path.Combine(dir.FullName, FileName);
|
|
if (File.Exists(candidate))
|
|
{
|
|
try
|
|
{
|
|
var json = File.ReadAllText(candidate);
|
|
var config = JsonSerializer.Deserialize<WorkspaceConfig>(json, JsonOptions)
|
|
?? throw new InvalidOperationException($"{FileName} deserialized to null.");
|
|
return (config, dir.FullName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Failed to parse {FileName} at {candidate}: {ex.Message}", ex);
|
|
}
|
|
}
|
|
dir = dir.Parent!;
|
|
}
|
|
|
|
// No workspace file found; synthesize one by scanning nearby project roots.
|
|
return TryAutoDiscover(startDir ?? Directory.GetCurrentDirectory());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the absolute project root for a workspace project entry.
|
|
/// </summary>
|
|
public static string ResolveProjectRoot(string workspaceRoot, WorkspaceProject project)
|
|
=> Path.GetFullPath(Path.IsPathRooted(project.Path)
|
|
? project.Path
|
|
: Path.Combine(workspaceRoot, project.Path));
|
|
|
|
public static string GetWorkspaceFilePath(string workspaceRoot)
|
|
=> Path.Combine(workspaceRoot, FileName);
|
|
|
|
public static void Save(string workspaceRoot, WorkspaceConfig workspace)
|
|
{
|
|
var path = GetWorkspaceFilePath(workspaceRoot);
|
|
var saveOptions = new JsonSerializerOptions(JsonOptions)
|
|
{
|
|
WriteIndented = true
|
|
};
|
|
var json = JsonSerializer.Serialize(workspace, saveOptions);
|
|
File.WriteAllText(path, json + Environment.NewLine);
|
|
}
|
|
|
|
private static (WorkspaceConfig Config, string WorkspaceRoot)? TryAutoDiscover(string startDir)
|
|
{
|
|
LoadedProjectConfig? loaded;
|
|
try
|
|
{
|
|
loaded = ConfigLoader.FindAndLoad(startDir);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (loaded is null)
|
|
return null;
|
|
|
|
var currentRoot = loaded.ProjectRoot;
|
|
var parent = Directory.GetParent(currentRoot);
|
|
var workspaceRoot = parent?.FullName ?? currentRoot;
|
|
|
|
var roots = DiscoverProjectRoots(workspaceRoot, currentRoot);
|
|
if (roots.Count == 0)
|
|
return null;
|
|
|
|
var projects = new List<WorkspaceProject>();
|
|
foreach (var root in roots)
|
|
{
|
|
LoadedProjectConfig? cfg;
|
|
try
|
|
{
|
|
cfg = ConfigLoader.FindAndLoad(root);
|
|
}
|
|
catch
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var name = cfg?.Config.Name;
|
|
projects.Add(new WorkspaceProject
|
|
{
|
|
Name = string.IsNullOrWhiteSpace(name) ? new DirectoryInfo(root).Name : name!,
|
|
Description = $"Auto-discovered at {root}",
|
|
Path = Path.GetRelativePath(workspaceRoot, root),
|
|
});
|
|
}
|
|
|
|
return (
|
|
new WorkspaceConfig
|
|
{
|
|
Name = "SDT Auto Workspace",
|
|
Projects = projects
|
|
},
|
|
workspaceRoot);
|
|
}
|
|
|
|
private static List<string> DiscoverProjectRoots(string workspaceRoot, string currentRoot)
|
|
{
|
|
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var excluded = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"bin",
|
|
"obj",
|
|
".git",
|
|
".venv",
|
|
"node_modules",
|
|
};
|
|
|
|
void AddIfProject(string path)
|
|
{
|
|
var full = Path.GetFullPath(path);
|
|
if (File.Exists(Path.Combine(full, "devtool.json")))
|
|
set.Add(full);
|
|
}
|
|
|
|
AddIfProject(currentRoot);
|
|
AddIfProject(workspaceRoot);
|
|
|
|
try
|
|
{
|
|
foreach (var dir in Directory.EnumerateDirectories(workspaceRoot))
|
|
{
|
|
if (excluded.Contains(Path.GetFileName(dir)))
|
|
continue;
|
|
|
|
AddIfProject(dir);
|
|
foreach (var sub in Directory.EnumerateDirectories(dir))
|
|
{
|
|
if (excluded.Contains(Path.GetFileName(sub)))
|
|
continue;
|
|
AddIfProject(sub);
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Ignore inaccessible directories during auto-discovery.
|
|
}
|
|
|
|
return set.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToList();
|
|
}
|
|
}
|