journal/Journal.DevTool/Tui/WorkspaceScreen.cs
stan44 5b383858ae feat(sdt): add Journal.DevTool TUI, devtool.json, and justfile
- Journal.DevTool: C# TUI dev tool (SDT) using Spectre.Console
  - Phosphor green (#00FF41) terminal aesthetic with amber/red accents
  - Grouped build target menu driven by devtool.json config
  - Topological dependency resolver for DependsOn chains
  - Live stdout/stderr streaming per target step
  - Interactive environment variable editor (dropdown + free-text)
  - Toolchain management screen: Python venv create/install/upgrade, Node npm install
  - Workspace switcher: reads sdt-workspace.json, hot-switches between projects
  - Outputs as 'sdt' executable (net10.0)

- devtool.json: project config for SDT
  - All build targets: sidecar, web, webgateway, tauri, tauri-nsis, all (virtual)
  - Dev targets: run-gateway
  - Test targets: test (smoke), gate (migration)
  - Cache targets: nuget-export, nuget-import
  - Toolchains: Python 3.14 (cpu/gpu/nlp profiles) + Node/npm (Journal.App)
  - Env vars: AI provider, log level, NLP backend, path overrides

- justfile: just command runner recipes wrapping existing scripts
  - Correct dependency ordering (sidecar before tauri, web before webgateway)
  - OS-aware runtime detection (win-x64 / linux-x64)
  - Recipes: sidecar, web, webgateway, tauri, tauri-nsis, all, run, dev-app, test, gate, build, nuget-export/import, sdt

- Journal.slnx: added Journal.DevTool project
2026-02-27 13:12:36 -06:00

109 lines
4.3 KiB
C#

using Sdt.Config;
using Spectre.Console;
namespace Sdt.Tui;
public sealed class WorkspaceScreen
{
private readonly WorkspaceConfig _workspace;
private readonly string _workspaceRoot;
private readonly string _currentProjectRoot;
public WorkspaceScreen(WorkspaceConfig workspace, string workspaceRoot, string currentProjectRoot)
{
_workspace = workspace;
_workspaceRoot = workspaceRoot;
_currentProjectRoot = currentProjectRoot;
}
/// <summary>
/// Shows the project switcher. Returns the absolute path to the selected project root,
/// or null if the user cancelled.
/// </summary>
public string? SelectProject()
{
AnsiConsole.Clear();
AnsiConsole.Write(Theme.SectionRule("WORKSPACE — " + _workspace.Name));
AnsiConsole.WriteLine();
var projects = _workspace.Projects;
if (projects.Count == 0)
{
AnsiConsole.MarkupLine(Theme.Warn("No projects defined in sdt-workspace.json."));
AnsiConsole.MarkupLine(Theme.Faint("Add entries to the \"projects\" array."));
AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back..."));
Console.ReadKey(intercept: true);
return null;
}
// Build choice list with current project marked
var choices = new List<WorkspaceMenuItem>();
foreach (var proj in projects)
{
var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj);
var devtoolPath = Path.Combine(absPath, "devtool.json");
var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase);
var exists = File.Exists(devtoolPath);
var label = isCurrent
? $"[bold {Theme.GreenBold}]► {Markup.Escape(proj.Name)}[/] [{Theme.GreenDim}](current)[/]"
: $"[{Theme.Green}] {Markup.Escape(proj.Name)}[/]";
var desc = !exists
? $" [{Theme.Red}]devtool.json not found[/]"
: string.IsNullOrWhiteSpace(proj.Description)
? $" [{Theme.GreenDim}]{Markup.Escape(absPath)}[/]"
: $" [{Theme.GreenDim}]{Markup.Escape(proj.Description)}[/]";
choices.Add(new WorkspaceMenuItem(label + "\n" + desc, absPath, exists && !isCurrent));
}
choices.Add(new WorkspaceMenuItem($"[{Theme.GreenDim}]← Cancel[/]", null, true));
// Show project table for overview
var table = new Table()
.Border(TableBorder.Rounded)
.BorderStyle(Theme.DimStyle)
.AddColumn(new TableColumn($"[{Theme.Amber}]Project[/]"))
.AddColumn(new TableColumn($"[{Theme.Amber}]Path[/]"))
.AddColumn(new TableColumn($"[{Theme.Amber}]Status[/]").Width(12));
foreach (var proj in projects)
{
var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj);
var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase);
var hasConfig = File.Exists(Path.Combine(absPath, "devtool.json"));
table.AddRow(
isCurrent
? $"[bold {Theme.GreenBold}]► {Markup.Escape(proj.Name)}[/]"
: Theme.G(proj.Name),
Theme.Faint(proj.Path),
hasConfig ? Theme.Ok("ready") : Theme.Fail("no config"));
}
AnsiConsole.Write(table);
AnsiConsole.WriteLine();
var switchable = choices.Where(c => c.Selectable).ToList();
if (switchable.Count == 1) // only Cancel
{
AnsiConsole.MarkupLine(Theme.Warn("No other projects available to switch to."));
AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back..."));
Console.ReadKey(intercept: true);
return null;
}
var selected = AnsiConsole.Prompt(
new SelectionPrompt<WorkspaceMenuItem>()
.Title($"[{Theme.Green}]Switch to project:[/]")
.PageSize(15)
.UseConverter(m => m.Display)
.AddChoices(switchable));
return selected.AbsPath; // null = cancelled
}
private sealed record WorkspaceMenuItem(string Display, string? AbsPath, bool Selectable);
}