- 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
61 lines
1.8 KiB
C#
61 lines
1.8 KiB
C#
using System.Diagnostics;
|
|
|
|
namespace Sdt.Runner;
|
|
|
|
public sealed record RunResult(int ExitCode, TimeSpan Elapsed)
|
|
{
|
|
public bool Success => ExitCode == 0;
|
|
}
|
|
|
|
public static class ProcessRunner
|
|
{
|
|
/// <summary>
|
|
/// Runs a command with the given args, streaming stdout/stderr via <paramref name="onOutput"/>.
|
|
/// onOutput receives (line, isStderr).
|
|
/// </summary>
|
|
public static async Task<RunResult> RunAsync(
|
|
string command,
|
|
IEnumerable<string> args,
|
|
string workingDir,
|
|
Action<string, bool> onOutput,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = command,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
WorkingDirectory = workingDir,
|
|
};
|
|
|
|
foreach (var arg in args)
|
|
psi.ArgumentList.Add(arg);
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
using var process = new Process { StartInfo = psi };
|
|
process.Start();
|
|
|
|
var stdoutTask = DrainAsync(process.StandardOutput, line => onOutput(line, false), cancellationToken);
|
|
var stderrTask = DrainAsync(process.StandardError, line => onOutput(line, true), cancellationToken);
|
|
|
|
await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false);
|
|
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
sw.Stop();
|
|
return new RunResult(process.ExitCode, sw.Elapsed);
|
|
}
|
|
|
|
private static async Task DrainAsync(StreamReader reader, Action<string> emit, CancellationToken ct)
|
|
{
|
|
string? line;
|
|
while ((line = await reader.ReadLineAsync(ct).ConfigureAwait(false)) is not null
|
|
&& !ct.IsCancellationRequested)
|
|
{
|
|
emit(line);
|
|
}
|
|
}
|
|
}
|