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,
};
///
/// Walks up from (or CWD) to find sdt-workspace.json.
/// Returns null if not found.
///
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(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());
}
///
/// Resolves the absolute project root for a workspace project entry.
///
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();
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 DiscoverProjectRoots(string workspaceRoot, string currentRoot)
{
var set = new HashSet(StringComparer.OrdinalIgnoreCase);
var excluded = new HashSet(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();
}
}