Skip to content

Commit 2d5af33

Browse files
committed
refactor(psutils): 迁移 GitHub CLI 下载器 helper
1 parent 884b6d3 commit 2d5af33

17 files changed

Lines changed: 705 additions & 269 deletions

File tree

.trellis/spec/psutils/package/shared-config-resolver.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- `Get-ConfigValue -Values <hashtable> -Name <string> [-DefaultValue <object>]`
2727
- `Resolve-ConfigEnvPlaceholder -Value <string> -Context <string>`
2828
- `Resolve-ConfigPath -Path <string> -BasePath <string> -Context <string>`
29+
- `Resolve-ConfigPlatformValue -Value <object> -Platform <pscustomobject> -Label <string> [-AllowScalar]`
2930
- `ConvertTo-ConfigKeyName -Name <string>`
3031
- `ConvertFrom-ConfigCliParameters -Parameters <hashtable> [-ExcludeKeys <string[]>]`
3132
- Scoped env:
@@ -43,6 +44,7 @@
4344
- `Get-ConfigValue` performs shallow case-insensitive lookup only; it must not expand paths, environment variables, nested paths, or normalize key names.
4445
- `Resolve-ConfigEnvPlaceholder` expands `${VAR}` and `%VAR%`; missing `${VAR}` throws with context instead of silently preserving the placeholder.
4546
- `Resolve-ConfigPath` expands env placeholders, supports `~`, resolves relative paths against `BasePath`, and returns an absolute path. It does not validate existence or create directories.
47+
- `Resolve-ConfigPlatformValue` reads platform maps in `<os>-<arch>` -> `<os>` -> `default` order; scalar strings are accepted only when `-AllowScalar` is explicitly set.
4648
- Missing file sources return an empty table by default; `-ErrorOnMissing` changes that to `配置文件不存在: <path>`.
4749
- `Invoke-WithScopedEnvironment` must restore overwritten variables and remove newly created variables even when the script block throws.
4850
- `psutils/modules/config.psm1` must export public resolver functions and must not contain a second implementation of the parser.
@@ -60,6 +62,7 @@
6062
| CLI parameter value is `$null` or whitespace | Omit it from merged config |
6163
| `${VAR}` placeholder references a missing env var | Throw `环境变量未设置: VAR(context)` |
6264
| `Resolve-ConfigPath` receives an empty path | Throw `路径配置不能为空: context` |
65+
| Platform map is a scalar without `-AllowScalar` | Throw `<label> 需要按平台配置` |
6366

6467
### 5. Good/Base/Bad Cases
6568

.trellis/tasks/05-21-pwsh-shared-helper-extraction-round2/design.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,29 @@
22

33
## Scope
44

5-
本轮处理上一轮建议顺序的第 2-5 项:
5+
本轮处理上一轮建议顺序的第 1-5 项:
66

7-
1. rclone 配置 helper 迁移。
8-
2. Docker Compose helper 抽取。
9-
3. JSON/manifest/原子写入工具。
10-
4. 文件同步/备份 helper。
7+
1. GitHub CLI 下载器通用 helper 迁移。
8+
2. rclone 配置 helper 迁移。
9+
3. Docker Compose helper 抽取。
10+
4. JSON/manifest/原子写入工具。
11+
5. 文件同步/备份 helper。
1112

12-
`Install-GitHubCli.ps1` 明确不改。
13+
`Install-GitHubCli.ps1` 保留对外函数名,底层委托 `psutils`
14+
15+
## GitHub CLI Download Helpers
16+
17+
GitHub CLI 下载器中的配置对象转换、平台描述、平台映射读取、归档解压、候选文件查找、
18+
可执行文件安装、PATH 检查与提示都属于通用基础设施。迁移到:
19+
20+
* `psutils/modules/config.psm1`
21+
* `psutils/modules/os.psm1`
22+
* `psutils/modules/filesystem.psm1`
23+
* `psutils/modules/install.psm1`
24+
* `psutils/modules/env.psm1`
25+
26+
脚本内保留 `ConvertTo-GitHubCliHashtable``New-GitHubCliPlatform``Expand-GitHubCliArchive`
27+
等旧函数名作为兼容 wrapper。
1328

1429
## Config Helpers
1530

.trellis/tasks/05-21-pwsh-shared-helper-extraction-round2/implement.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,32 @@
55
1. 启动任务
66
* 完成 `prd.md``design.md``implement.md`
77
* 执行 `task.py start`
8-
2. rclone config helper 迁移
8+
2. GitHub CLI 下载器 helper 迁移
9+
* 配置表、路径、平台映射 wrapper 委托 `psutils`
10+
* 归档解压、候选文件查找、可执行文件安装、PATH 检查与提示委托共享 helper。
11+
* 保留脚本入口与原函数名。
12+
3. rclone config helper 迁移
913
* `ConvertTo-RcloneOpsHashtable` 委托 `ConvertTo-ConfigHashtable`
1014
* `Get-RcloneOpsConfigValue` 委托 `Get-ConfigValue`
1115
* `Resolve-RcloneOpsEnvPlaceholder` 委托 `Resolve-ConfigEnvPlaceholder`,非字符串保持原样。
12-
3. Docker Compose helper
16+
4. Docker Compose helper
1317
* 新增 `psutils/modules/docker.psm1` 和测试。
1418
* 选择一个已有 start 脚本迁移到共享 helper。
15-
4. JSON helper
19+
5. JSON helper
1620
* 新增 `psutils/modules/json.psm1` 和测试。
1721
* 更新 `psutils/psutils.psd1`
18-
5. File helper
22+
6. File helper
1923
* 更新 `psutils/modules/filesystem.psm1` 和测试。
2024
* `Sync-ClaudeConfig.ps1` 复用 JSON/文件 helper。
21-
6. 验证
25+
7. 验证
2226
* targeted Pester。
2327
* `pnpm qa`
2428
* `pnpm test:pwsh:all`
25-
7. 提交
29+
8. 提交
2630
* Conventional Commits,中文 subject。
2731

2832
## Review Gate
2933

30-
* `Install-GitHubCli.ps1` 没有出现在本轮 diff
34+
* `Install-GitHubCli.ps1` 只保留领域逻辑与兼容 wrapper,不再维护通用 helper 实现
3135
* 新增 `psutils` helper 不包含 rclone/Claude/具体服务领域语义。
3236
* Docker Compose 只迁移有测试或行为足够简单的调用点。

.trellis/tasks/05-21-pwsh-shared-helper-extraction-round2/prd.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## Requirements
88

9-
- 跳过前一轮建议中的第 1 项:不修改 `scripts/pwsh/download/Install-GitHubCli.ps1`
9+
- 补做前一轮建议中的第 1 项:迁移 `scripts/pwsh/download/Install-GitHubCli.ps1` 中可复用的通用基础设施
1010
- 迁移 rclone 运维脚本中的配置 helper,优先复用 `psutils/modules/config.psm1`
1111
- 抽取 Docker Compose 相关通用 helper,并至少迁移一个已有 start 脚本调用点。
1212
- 抽取 JSON/manifest/原子写入 helper,不包含 Claude、Tailscale、rclone 等领域规则。
@@ -15,7 +15,7 @@
1515

1616
## Acceptance Criteria
1717

18-
- [ ] `Install-GitHubCli.ps1` 未被修改
18+
- [ ] `Install-GitHubCli.ps1` 保留现有脚本入口和测试可见函数名,但通用配置、平台、归档、PATH 与安装 helper 复用 `psutils`
1919
- [ ] `rclone-ops.ps1` 不再维护重复的 env placeholder / hashtable / case-insensitive lookup 实现。
2020
- [ ] 至少一个 Docker Compose start 脚本复用共享 helper,原测试行为保持兼容。
2121
- [ ] `psutils` 提供 JSON 原子读写或稳定键 helper,并有 Pester 覆盖。
@@ -24,4 +24,4 @@
2424

2525
## Notes
2626

27-
- 用户明确指定“2 3 4 5 改一下,1 先不改了”,此处的 1 指上一轮建议顺序中的 GitHub CLI 下载器迁移。
27+
- 用户最初指定“2 3 4 5 改一下,1 先不改了”,随后补充“第一项也改一下”;此处的 1 指上一轮建议顺序中的 GitHub CLI 下载器迁移。

psutils/modules/config.psm1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Export-ModuleMember -Function @(
2020
'Get-ConfigValue'
2121
'Resolve-ConfigEnvPlaceholder'
2222
'Resolve-ConfigPath'
23+
'Resolve-ConfigPlatformValue'
2324
'ConvertTo-ConfigKeyName'
2425
'ConvertFrom-ConfigCliParameters'
2526
'Read-ConfigPowerShellDataFile'

psutils/modules/env.psm1

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ function Sync-PathFromBash {
567567
$source = 'bash-nologin-bashrc'
568568
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($bashPathOutput)) {
569569
Write-Warning "非登录模式获取 PATH 失败,尝试显式加载 ~/.bashrc。"
570-
$bashPathOutput = bash --noprofile --norc -c 'source ~/.bashrc 2>/dev/null; echo $PATH'
570+
$bashPathOutput = bash '--noprofile' '--norc' '-c' 'source ~/.bashrc 2>/dev/null; echo $PATH'
571571
$source = 'bash-nologin-bashrc-fallback'
572572
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($bashPathOutput)) {
573573
$msg = "无法从非登录模式获取 PATH。请检查 Bash 安装或 .bashrc 配置。"
@@ -662,5 +662,134 @@ function Sync-PathFromBash {
662662
}
663663
}
664664

665+
function Test-DirectoryInPath {
666+
<#
667+
.SYNOPSIS
668+
判断目录是否已经位于 PATH 中。
669+
670+
.DESCRIPTION
671+
将目标目录和 PATH 中的条目都规范化为完整路径后比较。Windows 平台默认忽略大小写,
672+
Linux/macOS 默认区分大小写;调用方也可以显式传入比较方式。
673+
674+
.PARAMETER Directory
675+
待检查的目录路径。
676+
677+
.PARAMETER PathValue
678+
可选 PATH 字符串;默认读取当前进程 PATH。
679+
680+
.PARAMETER Comparison
681+
路径比较方式;未传入时根据平台自动选择。
682+
683+
.OUTPUTS
684+
bool
685+
目录已在 PATH 中时返回 true。
686+
#>
687+
[CmdletBinding()]
688+
param(
689+
[Parameter(Mandatory)]
690+
[string]$Directory,
691+
692+
[AllowNull()]
693+
[string]$PathValue = $env:PATH,
694+
695+
[AllowNull()]
696+
[object]$Comparison = $null
697+
)
698+
699+
if ([string]::IsNullOrWhiteSpace($PathValue)) {
700+
return $false
701+
}
702+
703+
[System.StringComparison]$effectiveComparison = if ($null -ne $Comparison) {
704+
if ($Comparison -is [System.StringComparison]) {
705+
$Comparison
706+
}
707+
else {
708+
[System.StringComparison]::Parse([System.StringComparison], [string]$Comparison, $true)
709+
}
710+
}
711+
elseif ($IsWindows) {
712+
[System.StringComparison]::OrdinalIgnoreCase
713+
}
714+
else {
715+
[System.StringComparison]::Ordinal
716+
}
717+
718+
$target = [System.IO.Path]::GetFullPath($Directory).TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
719+
foreach ($entry in ($PathValue -split [regex]::Escape([System.IO.Path]::PathSeparator))) {
720+
if ([string]::IsNullOrWhiteSpace($entry)) {
721+
continue
722+
}
723+
724+
try {
725+
$entryPath = [System.IO.Path]::GetFullPath($entry.Trim()).TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
726+
}
727+
catch {
728+
continue
729+
}
730+
731+
if ([string]::Equals($target, $entryPath, $effectiveComparison)) {
732+
return $true
733+
}
734+
}
735+
736+
return $false
737+
}
738+
739+
function Get-PathAddHint {
740+
<#
741+
.SYNOPSIS
742+
生成平台化 PATH 添加提示。
743+
744+
.DESCRIPTION
745+
根据平台输出适合展示给用户的 PATH 添加命令或操作方法。该函数只生成文本,
746+
不修改环境变量。
747+
748+
.PARAMETER Directory
749+
需要加入 PATH 的目录。
750+
751+
.PARAMETER OperatingSystem
752+
目标操作系统,支持 `windows`、`linux`、`macos`。
753+
754+
.OUTPUTS
755+
string[]
756+
返回多行提示文本。
757+
#>
758+
[CmdletBinding()]
759+
param(
760+
[Parameter(Mandatory)]
761+
[string]$Directory,
762+
763+
[ValidateSet('windows', 'linux', 'macos')]
764+
[string]$OperatingSystem = $(if ($IsWindows) { 'windows' } elseif ($IsMacOS) { 'macos' } else { 'linux' })
765+
)
766+
767+
$escapedForSingleQuote = $Directory -replace "'", "''"
768+
switch ($OperatingSystem) {
769+
'windows' {
770+
return @(
771+
'安装目录尚未在 PATH 中。可在 PowerShell 中执行:',
772+
"[Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path', 'User') + ';$escapedForSingleQuote', 'User')",
773+
'然后重新打开终端。'
774+
)
775+
}
776+
'macos' {
777+
return @(
778+
'安装目录尚未在 PATH 中。zsh 用户可执行:',
779+
"mkdir -p '$escapedForSingleQuote'",
780+
('echo ''export PATH="{0}:$PATH"'' >> ~/.zshrc' -f $escapedForSingleQuote),
781+
'然后执行 source ~/.zshrc 或重新打开终端。'
782+
)
783+
}
784+
default {
785+
return @(
786+
'安装目录尚未在 PATH 中。bash/zsh 用户可执行:',
787+
"mkdir -p '$escapedForSingleQuote'",
788+
('echo ''export PATH="{0}:$PATH"'' >> ~/.profile' -f $escapedForSingleQuote),
789+
'然后执行 source ~/.profile 或重新打开终端。'
790+
)
791+
}
792+
}
793+
}
665794

666-
Export-ModuleMember -Function Get-Dotenv, Install-Dotenv, Import-EnvPath, Set-EnvPath, Add-EnvPath, Get-EnvParam, Remove-FromEnvPath, Sync-PathFromBash
795+
Export-ModuleMember -Function Get-Dotenv, Install-Dotenv, Import-EnvPath, Set-EnvPath, Add-EnvPath, Get-EnvParam, Remove-FromEnvPath, Sync-PathFromBash, Test-DirectoryInPath, Get-PathAddHint

0 commit comments

Comments
 (0)