docs: TUI 与入口重组的设计记录(4 条 ADR + 验收锚点)

grilling 三轮把设计树走完了,这里把它落成文件,避免决策只活在对话里。

ADR-0010 零依赖自研 TUI:三个候选库全部实测排除 —— ConsoleGuiTools 的 PSGallery 元数据声明最低 7.2(5.1 装不上);Spectre.Console 0.49.1 能在 5.1 加载但要塞 4 个第三方 DLL 且不支持鼠标;Terminal.Gui 1.15.0 能反射接线到真的渲染出窗口,但 Application.Shutdown() 在两端都抛 NRE。同时记下两条实测坑:无控制台时 $Host.UI.SupportsVirtualTerminal 会撒谎返回 True(第一道闸门必须是 [Console]::IsOutputRedirected),以及 5.1 的 RawUI.LengthInBufferCells 对中文返回 2、对边框字符返回 6(都不能用来排版)。

ADR-0011 进度回调这个例外:它是注入点不是状态,无头路径为 $null 时行为与今天完全一致。

ADR-0012 入口改名与垫片:这次留垫片而 Common.psm1 直接删,是因为前者是外部接口(断了是静默没用)后者是内部实现(断了当场报错)。并纠正了自己的一个伪前提 —— 本机实测没有注册任何 BakNRet 计划任务。

ADR-0013 TUI 写配置:外科式改写 + 校验通过才原子替换。硬事实是 SoftwareCatalog.psd1 根本不能被 Import-PowerShellDataFile 读入(动态表达式),任何"解析成对象再序列化"的方案都会毁掉表达式与注释。

.scratch/tui/spec.md 是这次改造的验收锚点:9 条已定决策 + 三轮的判据 + 风险表。

验收:Parse 两版仍绿。
This commit is contained in:
Shuery committed 2026-09-27 15:27:01 +08:00
1 parent 187549e2af
commit 60f2ae3e0f
25 files changed
+1708

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
param([Parameter(Mandatory=$true)][string]$OutFile, [Parameter(Mandatory=$true)][string]$Work)
$lines = New-Object System.Collections.Generic.List[string]
function L($s) { $lines.Add($s) }
L ("PSVersion: " + $PSVersionTable.PSVersion + " CLR " + [System.Environment]::Version)
L ("Work: " + $Work)
$tgNet472 = Join-Path $Work 'tg115\lib\net472\Terminal.Gui.dll'
$tgNs21 = Join-Path $Work 'tg115\lib\netstandard2.1\Terminal.Gui.dll'
$tgNs20 = Join-Path $Work 'tg115\lib\netstandard2.0\Terminal.Gui.dll'
$nstack = Join-Path $Work 'nstack\lib\netstandard2.0\NStack.dll'
$scNs20 = Join-Path $Work 'sc049\lib\netstandard2.0\Spectre.Console.dll'
L ""
L "### STEP 1: try [Reflection.Assembly]::LoadFrom on Terminal.Gui net472 (5.1's native TFM) WITHOUT dependencies ###"
try { $a = [System.Reflection.Assembly]::LoadFrom($tgNet472); L (" LoadFrom tg/net472 -> OK: " + $a.FullName) } catch { L (" LoadFrom tg/net472 FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message) }
L ""
L "### STEP 2: try GetTypes() -- this forces dependency resolution ###"
if ($a) {
try { $ts = $a.GetTypes(); L (" GetTypes() -> OK, " + $ts.Count + " types. Sample: " + (($ts | Select-Object -First 5 | ForEach-Object { $_.FullName }) -join ', ')) }
catch {
L (" GetTypes() FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException
while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException }
$le = $_.Exception.LoaderExceptions
if ($le) { foreach ($x in $le) { L (" loaderEx: " + $x.Message) } }
}
try { $appType = $a.GetType('Terminal.Gui.Application'); L (" GetType('Terminal.Gui.Application') -> " + $(if ($appType) { 'FOUND: ' + $appType.FullName } else { 'null' })) } catch { L (" GetType(Application) THREW: " + $_.Exception.Message) }
}
L ""
L "### STEP 3: load NStack.Core first (the declared dependency), then Terminal.Gui ###"
try { $ns = [System.Reflection.Assembly]::LoadFrom($nstack); L (" LoadFrom NStack.Core/netstandard2.0 -> " + $ns.FullName) } catch { L (" NStack load FAILED: " + $_.Exception.Message) }
try {
$a2 = [System.Reflection.Assembly]::LoadFrom($tgNet472)
$ts2 = $a2.GetTypes()
L (" after NStack preload: GetTypes() -> OK, " + $ts2.Count + " types")
$appType = $a2.GetType('Terminal.Gui.Application')
L (" Terminal.Gui.Application -> " + $(if ($appType) { 'FOUND' } else { 'null' }))
} catch {
L (" after NStack preload: FAILED " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$le = $_.Exception.LoaderExceptions; if ($le) { foreach ($x in $le) { L (" loaderEx: " + $x.Message) } }
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException }
}
L ""
L "### STEP 4: ACTUAL INSTANTIATION + a real TUI win. Does Application.Init() work in 5.1? ###"
$didInit = $false
try {
$a3 = [System.Reflection.Assembly]::LoadFrom($tgNet472)
$t = $a3.GetType('Terminal.Gui.Application')
L (" Application type: " + $(if ($t) { $t.FullName } else { 'null' }))
if ($t) {
$init = $t.GetMethod('Init', [Type[]]@())
L (" Application.Init() method found: " + [bool]$init)
if ($init) {
if ([Console]::IsOutputRedirected) {
L " SKIPPING Init() because stdout is redirected (it would hang / corrupt output)"
} else {
$init.Invoke($null, @()) | Out-Null
$didInit = $true
L " Application.Init() -> SUCCEEDED on this host"
}
}
}
} catch {
L (" Application.Init probe FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message)
$ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException }
}
if ($didInit) {
try {
$t = ([System.Reflection.Assembly]::LoadFrom($tgNet472)).GetType('Terminal.Gui.Application')
$shutdown = $t.GetMethod('Shutdown', [Type[]]@())
if ($shutdown) { $shutdown.Invoke($null, @()) | Out-Null; L " Application.Shutdown() -> called" }
} catch { L (" Shutdown failed: " + $_.Exception.Message) }
}
L ""
L "### STEP 5: Spectre.Console netstandard2.0 in this host ###"
try {
$sc = [System.Reflection.Assembly]::LoadFrom($scNs20)
L (" LoadFrom Spectre.Console/netstandard2.0 -> " + $sc.FullName)
try { $t2 = $sc.GetTypes(); L (" GetTypes() -> OK, " + $t2.Count + " types") } catch { L (" GetTypes() FAILED: " + $_.Exception.Message); $ie = $_.Exception.InnerException; while ($ie) { L (" inner: " + $ie.GetType().Name + ': ' + $ie.Message); $ie = $ie.InnerException } }
try {
$ansiType = $sc.GetType('Spectre.Console.AnsiConsole')
L (" Spectre.Console.AnsiConsole -> " + $(if ($ansiType) { 'FOUND' } else { 'null' }))
if ($ansiType) {
$w = $ansiType.GetMethod('Write', [Type[]]@([string]))
L (" AnsiConsole.Write(string) overload: " + [bool]$w)
}
} catch { L (" AnsiConsole probe FAILED: " + $_.Exception.Message) }
} catch { L (" LoadFrom Spectre.Console FAILED: " + $_.Exception.GetType().Name + ': ' + $_.Exception.Message) }
L ""
L "### STEP 6: Add-Type -Path variant (what a user would literally type) ###"
try { Add-Type -Path $tgNet472 -ErrorAction Stop; L " Add-Type -Path Terminal.Gui(net472) -> OK" } catch { L (" Add-Type -Path Terminal.Gui(net472) FAILED: " + $_.Exception.Message) }
try { Add-Type -Path $scNs20 -ErrorAction Stop; L " Add-Type -Path Spectre.Console(netstandard2.0) -> OK" } catch { L (" Add-Type -Path Spectre.Console(ns2.0) FAILED: " + $_.Exception.Message) }
try { Add-Type -Path $tgNs21 -ErrorAction Stop; L " Add-Type -Path Terminal.Gui(netstandard2.1) -> OK" } catch { L (" Add-Type -Path Terminal.Gui(ns2.1) FAILED: " + $_.Exception.Message) }
L (" (tg netstandard2.0 path exists? " + (Test-Path $tgNs20) + ")")
[System.IO.File]::WriteAllLines($OutFile, $lines, (New-Object System.Text.UTF8Encoding($false)))