-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall-github-plugin.ps1
More file actions
169 lines (157 loc) · 8.96 KB
/
Copy pathinstall-github-plugin.ps1
File metadata and controls
169 lines (157 loc) · 8.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# install-github-plugin.ps1 — 通用 GitHub → DSH 插件安装器
#
# 用途: 输入一个 GitHub 仓库 URL,自动完成:
# 1. 下载仓库 zip(优先 Node fetch —— 本机 schannel TLS 损坏 / hosts 拦截 GitHub 域名时 git/curl 不可用)
# 2. 解压、定位 plugin.js(或 *.plugin.js)
# 3. 结构校验(含 `return {` 插件对象形态) + 语法编译校验(经 Node new Function)
# 4. 源码留档到 <OutDir>/<repo>/(plugin.js + README + install-info.json)
# 5. 输出人类可读摘要 + 机器可读 __INSTALL_JSON__ 行(供动态工具解析)
#
# 用法:
# .\install-github-plugin.ps1 -RepoUrl "https://github.com/ABccgh/imakb"
# .\install-github-plugin.ps1 -RepoUrl "https://github.com/ABccgh/imakb" -OutDir "plugins" -Branch "main" -Json
#
# 之后在 DSH 会话内: 读取 <OutDir>/<repo>/plugin.js,把全文作为 code.host 交给 cordis_define,
# 再 cordis_run 即完成注册(本脚本不含凭据)。
param(
[Parameter(Mandatory = $true)]
[string]$RepoUrl,
[string]$OutDir = 'plugins',
[string]$Branch = '',
[switch]$Json
)
$ErrorActionPreference = 'Stop'
$stdout = [System.Collections.Generic.List[string]]::new()
function Emit([string]$line) { $stdout.Add($line); Write-Output $line }
$result = @{ ok = $false; repo = ''; owner = ''; branch = ''; downloaded = ''; bytes = 0; zipPath = ''; pluginFiles = @(); validated = @(); errors = @(); archivedDir = '' }
try {
# ---------- 1. 解析仓库 URL ----------
$clean = $RepoUrl.Trim()
$clean = $clean -replace '^git\+', ''
$clean = $clean -replace '\.git$', ''
$m = [regex]::Match($clean, 'github\.com[:/]([^/]+)/([^/#?]+)')
if (-not $m.Success) { throw "无法从 URL 解析 owner/repo: $RepoUrl" }
$owner = $m.Groups[1].Value
$repo = $m.Groups[2].Value
$result.owner = $owner; $result.repo = $repo
# 从 /tree/<branch> 或 ?ref= 提取分支
if (-not $Branch) {
$tm = [regex]::Match($clean, '/tree/([^/?#]+)')
if ($tm.Success) { $Branch = $tm.Groups[1].Value }
}
$branches = @()
if ($Branch) { $branches += $Branch }
$branches += 'main', 'master'
$zipPath = Join-Path $env:TEMP ("dsh-plugin-{0}-{1}.zip" -f $repo, [guid]::NewGuid().ToString('N').Substring(0, 8))
# ---------- 2. 下载 zip ----------
$downloaded = $false
$lastErr = ''
$nodeErrs = [System.Collections.Generic.List[string]]::new()
foreach ($b in $branches) {
if ($downloaded) { break }
$url = "https://codeload.github.com/$owner/$repo/zip/refs/heads/$b"
# 方式 A: Node fetch(OpenSSL,不受 schannel/hosts 影响)
if (Get-Command node -ErrorAction SilentlyContinue) {
# JS 内路径一律用正斜杠,避免反斜杠被当作转义符
$zipJsPath = $zipPath.Replace('\', '/')
$js = "const fs=require('fs');fetch('" + $url + "',{redirect:'follow'}).then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.arrayBuffer()}).then(b=>{fs.writeFileSync('" + $zipJsPath + "',Buffer.from(b));console.log('OK '+b.byteLength)}).catch(e=>{console.error('FAIL '+e.message);process.exit(1)})"
$out = & node -e $js 2>&1 | Out-String
if ($LASTEXITCODE -eq 0 -and $out -match 'OK (\d+)' -and (Test-Path $zipPath) -and (Get-Item $zipPath).Length -gt 0) {
$result.downloaded = "node-fetch ($b)"; $result.bytes = [int64]$Matches[1]; $result.branch = $b; $downloaded = $true; break
} else {
$nodeErrs.Add("node-fetch($b): $($out.Trim())")
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
}
}
# 方式 B: curl.exe(可能因 schannel 失败,仅作后备)
if (-not $downloaded -and (Get-Command curl.exe -ErrorAction SilentlyContinue)) {
curl.exe -sSL -o $zipPath $url 2>$null
if ($LASTEXITCODE -eq 0 -and (Test-Path $zipPath) -and (Get-Item $zipPath).Length -gt 0) {
$result.downloaded = "curl ($b)"; $result.bytes = (Get-Item $zipPath).Length; $result.branch = $b; $downloaded = $true; break
} else {
$lastErr = "curl: exit $LASTEXITCODE"
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
}
}
}
if (-not $downloaded) {
$detail = if ($nodeErrs.Count) { $nodeErrs -join ' | ' } else { $lastErr }
throw "下载失败(已尝试分支: $($branches -join ', ')): $detail"
}
$result.zipPath = $zipPath
# ---------- 3. 解压 ----------
$extractDir = Join-Path $env:TEMP ("dsh-plugin-extract-{0}" -f [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
# 仓库 zip 通常含单层顶层目录(如 imakb-main/),取它作为仓库根
$repoRoot = $extractDir
$topDirs = @(Get-ChildItem -LiteralPath $extractDir -Directory -Force -ErrorAction SilentlyContinue)
if ($topDirs.Count -eq 1) { $repoRoot = $topDirs[0].FullName }
# ---------- 4. 定位 + 校验 plugin 文件 ----------
$pluginFiles = @(Get-ChildItem -LiteralPath $repoRoot -Recurse -Force -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -eq 'plugin.js' -or $_.Name -like '*.plugin.js' -or ($_.DirectoryName -eq $repoRoot -and $_.Extension -eq '.js' -and $_.Name -notmatch '\.(test|spec)\.') })
$pluginFiles = @($pluginFiles | Select-Object -Unique -First 10)
if ($pluginFiles.Count -eq 0) { throw "未找到 plugin.js(仓库结构可能不是 DSH 插件): $RepoUrl" }
$result.pluginFiles = @($pluginFiles | ForEach-Object { $_.FullName.Substring($repoRoot.Length).TrimStart('\', '/') })
$validated = @()
foreach ($pf in $pluginFiles) {
$content = Get-Content -Raw -LiteralPath $pf.FullName
$rec = @{ rel = $pf.FullName.Substring($repoRoot.Length).TrimStart('\', '/'); hasReturnPlugin = $false; compiles = $false; error = '' }
if ($content -match 'return\s*\{') { $rec.hasReturnPlugin = $true }
if (Get-Command node -ErrorAction SilentlyContinue) {
$checkJsPath = $pf.FullName.Replace('\', '/')
$checkJs = "try{new Function(require('fs').readFileSync('" + $checkJsPath + "','utf8'));console.log('COMPILE_OK')}catch(e){console.error('COMPILE_ERR '+e.message);process.exit(1)}"
$co = & node -e $checkJs 2>&1 | Out-String
if ($LASTEXITCODE -eq 0 -and $co -match 'COMPILE_OK') { $rec.compiles = $true } else { $rec.error = "语法编译失败: $($co.Trim())" }
}
$validated += $rec
}
$result.validated = $validated
# ---------- 5. 留档到 <OutDir>/<repo>/ ----------
$destRoot = if ($OutDir -match '^[a-zA-Z]:[\\/]|^\\\\') { $OutDir } else { Join-Path (Get-Location) $OutDir }
$destDir = Join-Path $destRoot $repo
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
$kept = @()
foreach ($pf in $pluginFiles) {
$rel = $pf.FullName.Substring($repoRoot.Length).TrimStart('\', '/')
$target = Join-Path $destDir $rel
New-Item -ItemType Directory -Path (Split-Path $target) -Force | Out-Null
Copy-Item -LiteralPath $pf.FullName -Destination $target -Force
$kept += $rel
}
$readme = Get-ChildItem -LiteralPath $repoRoot -Recurse -Force -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^readme\.?(md|txt)?$' } | Select-Object -First 1
if ($readme) { Copy-Item -LiteralPath $readme.FullName -Destination (Join-Path $destDir 'README.md') -Force }
$info = [ordered]@{
source_url = $RepoUrl; owner = $owner; repo = $repo; branch = $result.branch
downloaded_via = $result.downloaded; bytes = $result.bytes; installed_at = (Get-Date).ToString('o')
plugin_files = $kept
validated = @($validated | ForEach-Object { [ordered]@{ file = $_.rel; has_return_plugin = $_.hasReturnPlugin; compiles = $_.compiles; error = $_.error } })
}
$info | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $destDir 'install-info.json') -Encoding UTF8
$result.archivedDir = $destDir
# ---------- 6. 输出 ----------
$bad = @($validated | Where-Object { -not $_.hasReturnPlugin -or -not $_.compiles })
$result.ok = ($bad.Count -eq 0)
if (-not $Json) {
Emit "== GitHub → DSH 插件安装器 =="
Emit "仓库: $owner/$repo 分支: $($result.branch) 下载: $($result.downloaded) ($($result.bytes) bytes)"
foreach ($v in $validated) {
$flags = if ($v.hasReturnPlugin) { 'return{✓' } else { 'return{✗' }
$flags += if ($v.compiles) { '} 编译✓' } else { '} 编译✗' }
Emit ("插件文件: {0} [{1}]" -f $v.rel, $flags)
if ($v.error) { Emit " 警告: $($v.error)" }
}
Emit "留档目录: $destDir"
Emit "下一步(在 DSH 会话内):"
Emit " 1) 把 $destDir\plugin.js 全文作为 code.host 交给 cordis_define(idPrefix 建议 3-6 位小写字母)"
Emit " 2) cordis_run 运行返回的 pluginId/packageId"
if ($bad.Count -gt 0) { Emit "注意: 有 $($bad.Count) 个文件未通过校验,请人工检查结构" }
}
$result | ConvertTo-Json -Depth 6 -Compress | ForEach-Object { Write-Output "__INSTALL_JSON__$_" }
}
catch {
$result.errors += $_.Exception.Message
if (-not $Json) { Write-Output "安装失败: $($_.Exception.Message)" }
$result | ConvertTo-Json -Depth 6 -Compress | ForEach-Object { Write-Output "__INSTALL_JSON__$_" }
exit 1
}