diff --git a/auto-setup.ps1 b/auto-setup.ps1 index a92550b..d8d2f1c 100644 --- a/auto-setup.ps1 +++ b/auto-setup.ps1 @@ -577,6 +577,11 @@ function Test-ApiKey([string]$apiKey, [string]$baseUrl = "https://bww.letcareme. } function Show-GiteaCredentialDialog { + # [DEPRECATED-PATH-A] 危险分支: 向终端用户索取 Gitea 长期账密 = 等效分发仓库读写权。 + # Path A 架构下绝对禁止 —— 已被 token-exchange (Get-BwShortLivedToken) 替换。 + # fail-LOUD: 任何残留调用立即抛错, 严禁回退账密索取 (调用点已全部替换, 此为防回归后门)。 + throw "[Path-A] Show-GiteaCredentialDialog 已废弃: 禁止索取 Gitea 账密, 应走 token-exchange" + # ↓↓↓ 原 Forms 对话框为 throw 后不可达死代码 (保留以最小化 diff) ↓↓↓ $form = New-Object System.Windows.Forms.Form $form.Text = "Bookworm - Gitea 登录" $form.Size = New-Object System.Drawing.Size(420, 300) @@ -1598,21 +1603,64 @@ Log-Phase 3 "同步 Bookworm 配置" if (Is-DryRun "repo") { Log-Info "[DryRun=repo] 测试 git clone 完成后退出, 不做凭证解密/配置渲染" } function Test-CloneComplete { (Test-Path (Join-Path $ClaudeDir "CLAUDE.md")) -and (Test-Path (Join-Path $ClaudeDir "skills")) -and (Test-Path (Join-Path $ClaudeDir "hooks")) } -# B3: 使用 Windows Credential Manager (DPAPI 加密) 替代明文 store -git config --global credential.helper manager 2>$null -# v3.2.0: 强制 git 对国内仓库直连 (VPN 开启时 git 可能走代理导致 clone 失败) +# ── [Path-A] 准备 token-exchange: 暂存 issuer crypto 载荷 + dot-source 共享模块 ── +# seal-cli.js / crypto-handshake.js / bw-tokenexchange.ps1 须随安装包分发, 在私有 clone 前就位 +# (smart-assistant clone 在 boot clone 之前, 故 crypto 载荷不能依赖 boot 仓, 必须随安装包内置)。 +# 三级查找: $ScriptDir\issuer → $BootDir\issuer → 内嵌 base64 兜底 (单文件自包含 Setup.exe)。 +$BwIssuerDir = (Join-Path $env:USERPROFILE ".claude\issuer") +if (-not (Test-Path $BwIssuerDir)) { New-Item -ItemType Directory -Force -Path $BwIssuerDir | Out-Null } +$BwPayloadFiles = @("seal-cli.js","crypto-handshake.js","bw-tokenexchange.ps1") + +# 级别 1+2: 伴随文件目录 ($ScriptDir\issuer, $BootDir\issuer) +$BwIssuerPayload = @() +if ($ScriptDir) { $BwIssuerPayload += (Join-Path $ScriptDir "issuer") } +if ($BootDir) { $BwIssuerPayload += (Join-Path $BootDir "issuer") } +foreach ($pdir in $BwIssuerPayload) { + if (Test-Path $pdir) { + foreach ($f in $BwPayloadFiles) { + $srcF = Join-Path $pdir $f + if (Test-Path $srcF) { Copy-Item $srcF $BwIssuerDir -Force } + } + } +} + +# 级别 3: 内嵌 base64 兜底 — 凡缺失文件从内嵌载荷解码落地 (build.ps1 编译前自动注入)。 +$BwPayloadMissing = @($BwPayloadFiles | Where-Object { -not (Test-Path (Join-Path $BwIssuerDir $_)) }) +if ($BwPayloadMissing.Count -gt 0) { + # —— 由 build.ps1 从载荷源文件自动注入, 请勿手改 + $BwPayloadB64 = @{ + 'seal-cli.js' = 'Ly8gUGF0aCBBIOKAlCBzZWFsIENMSTog5a6i5oi356uvKFBTNS4xKSDnu48gTm9kZSDlrZDov5vnqIvliqDlr4YgYXV0aGNvZGUg55qE5YWl5Y+j44CCCi8vIOeUqOazlTogZWNobyAneyJyZWNpcGllbnRQdWJsaWNYIjoiPGI2NHVybD4iLCJwbGFpbnRleHQiOiI8anNvbj4ifScgfCBub2RlIHNlYWwtY2xpLmpzCi8vIOi+k+WHujogc3Rkb3V0IOWGmSBDLTMg5a+G5paH5bCB5YyFIEpTT07jgIIKLy8g5a6J5YWoOiDmmI7mlocv5YWs6ZKl57uPIHN0ZGluIOS8oOWFpSAo6Z2eIGFyZ3Yg4oaSIOS4jei/m+i/m+eoi+WIl+ihqC/kuI3ooqvlkIznlKjmiLfov5vnqIvmiKrojrcsIDA4IMKnNy4zKeOAggovLyAgICAgICDlrZDov5vnqIvpmo/ljbPpgIDlh7osIOS4jeaMgeS5heWMluS7u+S9leWvhumSpeadkOaWmeOAggppbXBvcnQgeyBzZWFsIH0gZnJvbSAiLi9jcnlwdG8taGFuZHNoYWtlLmpzIjsKCmxldCBpbnB1dCA9ICIiOwpwcm9jZXNzLnN0ZGluLnNldEVuY29kaW5nKCJ1dGY4Iik7CnByb2Nlc3Muc3RkaW4ub24oImRhdGEiLCAoYykgPT4geyBpbnB1dCArPSBjOyB9KTsKcHJvY2Vzcy5zdGRpbi5vbigiZW5kIiwgKCkgPT4gewogIGxldCByZXE7CiAgdHJ5IHsKICAgIHJlcSA9IEpTT04ucGFyc2UoaW5wdXQpOwogIH0gY2F0Y2ggewogICAgcHJvY2Vzcy5zdGRlcnIud3JpdGUoInNlYWwtY2xpOiBzdGRpbiDpnZ7lkIjms5UgSlNPTlxuIik7CiAgICBwcm9jZXNzLmV4aXQoMik7CiAgfQogIGlmICh0eXBlb2YgcmVxPy5yZWNpcGllbnRQdWJsaWNYICE9PSAic3RyaW5nIiB8fCB0eXBlb2YgcmVxPy5wbGFpbnRleHQgIT09ICJzdHJpbmciKSB7CiAgICBwcm9jZXNzLnN0ZGVyci53cml0ZSgic2VhbC1jbGk6IOmcgCB7cmVjaXBpZW50UHVibGljWCwgcGxhaW50ZXh0fVxuIik7CiAgICBwcm9jZXNzLmV4aXQoMik7CiAgfQogIHRyeSB7CiAgICBwcm9jZXNzLnN0ZG91dC53cml0ZShKU09OLnN0cmluZ2lmeShzZWFsKHJlcS5wbGFpbnRleHQsIHJlcS5yZWNpcGllbnRQdWJsaWNYKSkpOwogIH0gY2F0Y2ggKGUpIHsKICAgIHByb2Nlc3Muc3RkZXJyLndyaXRlKCJzZWFsLWNsaTog5Yqg5a+G5aSx6LSlOiAiICsgKGU/Lm1lc3NhZ2UgPz8gZSkgKyAiXG4iKTsKICAgIHByb2Nlc3MuZXhpdCgxKTsKICB9Cn0pOwo=' + 'crypto-handshake.js' = 'Ly8gUGF0aCBBIOKAlCBhdXRoY29kZSDnq6/liLDnq6/liqDlr4bmj6HmiYsg5Y+C6ICD5a6e546wIChDLTEvQy0yL0MtMyDlpZHnuqYsIFBoYXNlIDEpCi8vCi8vIFvpnIDlrozmlbTlrqrms5Xnoa7orqRdIOacrOaooeWdl+aYr+iupOivgeWKoOWvhuaguOW/g+OAguaehOmAoOmHh+eUqOaIkOeGnyBFQ0lFUyDmt7flkIjliqDlr4YsIOS7heeUqCBOb2RlIOWGhee9riBjcnlwdG8sCi8vIOmbtuesrOS4ieaWueS+nei1luOAguS4iue6v+WJjemhu+e7j+eLrOeri+WuieWFqOivhOWuoeetvuWtlyAo6KeBIDA4IOahoyDCpzcp44CCCi8vCi8vIOiuvuiuoeaEj+Wbvjog5a6i5oi356uvKFBTNS4xIOe7jyBOb2RlIOWtkOi/m+eoiynkuI4gaXNzdWVyKE5vZGUpIOWFseeUqOacrOaWh+S7tiDihpIg5Lik5L6n5ZCM5Luj56CBLAovLyDlpZHnuqblpKnnhLblr7npvZAsIOadnOe7nei3qOWunueOsOWIhuatp+OAgmF1dGhjb2RlIOaYjuaWh+e7neS4jeWHuuWuouaIt+err+i/m+eoizsgcHVibGljIOebsueuoemBk+WPqui9rOWvhuaWh+OAggovLwovLyDmnoTpgKAgKHNlYWxlZC1ib3gg5ZCM5qy+LCDljL/lkI3lj5HpgIHmlrkpOgovLyAgIFgyNTUxOSBFQ0RIKOS4tOaXtuWvhumSpSDDlyBpc3N1ZXIg6Z2Z5oCB5YWs6ZKlKSDihpIgSEtERi1TSEEyNTYg4oaSIEFFUy0yNTYtR0NNCi8vICAgLSDml6Dlj5HpgIHmlrnorqTor4EgKOS4jemcgOimgTogYXV0aGNvZGUg6Ieq6Lqr5Y2z56eY5a+GOyDpmLLph43mlL7pnaAgbm9uY2Ug6KGoICsgdHMg5paw6bKc56qXLCDop4EgaXNzdWVyIOS+pykKLy8gICAtIOavj+asoeaWsOS4tOaXtuWvhumSpSDihpIg5YmN5ZCR5L+d5a+GICjkuIDmrKHkuIDlr4YpCmltcG9ydCBjcnlwdG8gZnJvbSAibm9kZTpjcnlwdG8iOwoKZXhwb3J0IGNvbnN0IEhBTkRTSEFLRV9WRVJTSU9OID0gMTsKLyoqIEhLREYgaW5mbzog5Z+f5YiG6ZqUICsg54mI5pys44CC5a6i5oi356uvL2lzc3VlciDlv4XpobvpgJDlrZfkuIDoh7QsIOaUueWKqOWNs+egtOS6kuaTjeS9nOOAgiAqLwpleHBvcnQgY29uc3QgSEtERl9JTkZPID0gQnVmZmVyLmZyb20oImJvb2t3b3JtLXBhdGhBL3YxIGF1dGhjb2RlLXNlYWwiLCAidXRmOCIpOwpjb25zdCBLRVlfTEVOID0gMzI7IC8vIEFFUy0yNTYKY29uc3QgTk9OQ0VfTEVOID0gMTI7IC8vIEFFUy1HQ00g5qCH5YeGIDk2LWJpdApjb25zdCBUQUdfTEVOID0gMTY7CgovLyDilIDilIAgYmFzZTY0dXJsIOe8luino+eggSAocGFja2V0IOS8oOi+k+eUqCkg4pSA4pSACmNvbnN0IGI2NHUgPSAoYnVmKSA9PiBCdWZmZXIuZnJvbShidWYpLnRvU3RyaW5nKCJiYXNlNjR1cmwiKTsKY29uc3QgdW5iNjR1ID0gKHMpID0+IEJ1ZmZlci5mcm9tKHMsICJiYXNlNjR1cmwiKTsKCi8vIOKUgOKUgCBYMjU1MTkg5Y6f5aeL5YWs56eB6ZKlIOKGlCBKV0sgKE5vZGUg55SoIEpXSyDlj5Yv6K6+IDMyIOWtl+iKguijuOWvhumSpSkg4pSA4pSACmZ1bmN0aW9uIHB1YkZyb21SYXdYKHhCNjR1KSB7CiAgcmV0dXJuIGNyeXB0by5jcmVhdGVQdWJsaWNLZXkoeyBrZXk6IHsga3R5OiAiT0tQIiwgY3J2OiAiWDI1NTE5IiwgeDogeEI2NHUgfSwgZm9ybWF0OiAiandrIiB9KTsKfQpmdW5jdGlvbiBwcml2RnJvbUp3ayhqd2spIHsKICByZXR1cm4gY3J5cHRvLmNyZWF0ZVByaXZhdGVLZXkoeyBrZXk6IGp3aywgZm9ybWF0OiAiandrIiB9KTsKfQpmdW5jdGlvbiByYXdYKGtleU9iamVjdCkgewogIHJldHVybiBrZXlPYmplY3QuZXhwb3J0KHsgZm9ybWF0OiAiandrIiB9KS54OyAvLyBiYXNlNjR1cmwg6KO45YWs6ZKlCn0KCi8qKgogKiDnlJ/miJAgaXNzdWVyIOmdmeaAgeWvhumSpeWvuSAo566h55CG5ZGYL2lzc3VlciDkvqfkuIDmrKHmgKcp44CC56eB6ZKl6aG756a757q/L0hTTSDkv53miqTjgIIKICogQHJldHVybnMge3sga2lkOiBzdHJpbmcsIHB1YmxpY0tleVg6IHN0cmluZywgcHJpdmF0ZUp3azogb2JqZWN0IH19CiAqLwpleHBvcnQgZnVuY3Rpb24gZ2VuZXJhdGVJc3N1ZXJLZXlQYWlyKCkgewogIGNvbnN0IHsgcHVibGljS2V5LCBwcml2YXRlS2V5IH0gPSBjcnlwdG8uZ2VuZXJhdGVLZXlQYWlyU3luYygieDI1NTE5Iik7CiAgY29uc3QgcHVibGljS2V5WCA9IHJhd1gocHVibGljS2V5KTsKICByZXR1cm4geyBraWQ6IGtleUlkKHB1YmxpY0tleVgpLCBwdWJsaWNLZXlYLCBwcml2YXRlSndrOiBwcml2YXRlS2V5LmV4cG9ydCh7IGZvcm1hdDogImp3ayIgfSkgfTsKfQoKLyoqIGtleSBpZCA9IFNIQS0yNTYo6KO45YWs6ZKlKSDliY0gOCDlrZfoioIgYmFzZTY0dXJs44CC55So5LqO5a+G6ZKl6L2u5o2i5pe25qCH6K+GIuWKoOWvhue7meS6huWTquaKiiBpc3N1ZXIg5YWs6ZKlIuOAgiAqLwpleHBvcnQgZnVuY3Rpb24ga2V5SWQocHVibGljS2V5WCkgewogIGNvbnN0IGggPSBjcnlwdG8uY3JlYXRlSGFzaCgic2hhMjU2IikudXBkYXRlKHVuYjY0dShwdWJsaWNLZXlYKSkuZGlnZXN0KCk7CiAgcmV0dXJuIGI2NHUoaC5zdWJhcnJheSgwLCA4KSk7Cn0KCi8qKiBBQUQgPSDnu5HlrprlpLTpg6jlrZfmrrUsIOmYsuevoeaUuemHjee7hCAodmVyc2lvbuKAlmtpZOKAlmVwa+KAlm5vbmNlKeOAguS4pOS+p+W/hemhu+WQjOaehOmAoOOAgiAqLwpmdW5jdGlvbiBidWlsZEFhZCh2ZXJzaW9uLCBraWQsIGVwa1gsIG5vbmNlKSB7CiAgcmV0dXJuIEJ1ZmZlci5jb25jYXQoWwogICAgQnVmZmVyLmZyb20oW3ZlcnNpb25dKSwKICAgIEJ1ZmZlci5mcm9tKGtpZCwgInV0ZjgiKSwKICAgIHVuYjY0dShlcGtYKSwKICAgIG5vbmNlLAogIF0pOwp9CgovKiogSEtERiDmtL7nlJ/lr7nnp7Dlr4bpkqXjgIJzYWx0IOe7keWumiAo5Li05pe25YWs6ZKl4oCW5o6l5pS25pa55YWs6ZKlKSwg5L2/5a+G6ZKl5LiO5pys5qyh5o+h5omL5by657uR5a6a44CCICovCmZ1bmN0aW9uIGRlcml2ZUtleShzaGFyZWQsIGVwa1gsIHJlY2lwaWVudFgpIHsKICBjb25zdCBzYWx0ID0gQnVmZmVyLmNvbmNhdChbdW5iNjR1KGVwa1gpLCB1bmI2NHUocmVjaXBpZW50WCldKTsKICByZXR1cm4gQnVmZmVyLmZyb20oY3J5cHRvLmhrZGZTeW5jKCJzaGEyNTYiLCBzaGFyZWQsIHNhbHQsIEhLREZfSU5GTywgS0VZX0xFTikpOwp9CgovKioKICog5Yqg5a+GIGF1dGhjb2RlIOKGkiDlr4bmloflsIHljIUgKOWuouaIt+err+S+pynjgIIKICogQHBhcmFtIHtzdHJpbmd9IHBsYWludGV4dCAgICAgICAgYXV0aGNvZGUg5piO5paHICjlpoIgQldBLTwzMmhleD4pCiAqIEBwYXJhbSB7c3RyaW5nfSByZWNpcGllbnRQdWJsaWNYIGlzc3VlciDpnZnmgIHlhazpkqUgKGJhc2U2NHVybCDoo7ggMzIg5a2X6IqCKQogKiBAcmV0dXJucyB7b2JqZWN0fSBwYWNrZXQgKEMtMyBzY2hlbWEpCiAqLwpleHBvcnQgZnVuY3Rpb24gc2VhbChwbGFpbnRleHQsIHJlY2lwaWVudFB1YmxpY1gpIHsKICBjb25zdCBraWQgPSBrZXlJZChyZWNpcGllbnRQdWJsaWNYKTsKICBjb25zdCByZWNpcGllbnRQdWIgPSBwdWJGcm9tUmF3WChyZWNpcGllbnRQdWJsaWNYKTsKICAvLyDkuLTml7blr4bpkqXlr7kgKOS4gOasoeS4gOWvhikKICBjb25zdCBlcGggPSBjcnlwdG8uZ2VuZXJhdGVLZXlQYWlyU3luYygieDI1NTE5Iik7CiAgY29uc3QgZXBrWCA9IHJhd1goZXBoLnB1YmxpY0tleSk7CiAgY29uc3Qgc2hhcmVkID0gY3J5cHRvLmRpZmZpZUhlbGxtYW4oeyBwcml2YXRlS2V5OiBlcGgucHJpdmF0ZUtleSwgcHVibGljS2V5OiByZWNpcGllbnRQdWIgfSk7CiAgY29uc3Qga2V5ID0gZGVyaXZlS2V5KHNoYXJlZCwgZXBrWCwgcmVjaXBpZW50UHVibGljWCk7CiAgY29uc3Qgbm9uY2UgPSBjcnlwdG8ucmFuZG9tQnl0ZXMoTk9OQ0VfTEVOKTsKICBjb25zdCBhYWQgPSBidWlsZEFhZChIQU5EU0hBS0VfVkVSU0lPTiwga2lkLCBlcGtYLCBub25jZSk7CiAgY29uc3QgY2lwaGVyID0gY3J5cHRvLmNyZWF0ZUNpcGhlcml2KCJhZXMtMjU2LWdjbSIsIGtleSwgbm9uY2UsIHsgYXV0aFRhZ0xlbmd0aDogVEFHX0xFTiB9KTsKICBjaXBoZXIuc2V0QUFEKGFhZCk7CiAgY29uc3QgY3QgPSBCdWZmZXIuY29uY2F0KFtjaXBoZXIudXBkYXRlKEJ1ZmZlci5mcm9tKHBsYWludGV4dCwgInV0ZjgiKSksIGNpcGhlci5maW5hbCgpXSk7CiAgY29uc3QgdGFnID0gY2lwaGVyLmdldEF1dGhUYWcoKTsKICByZXR1cm4gewogICAgdjogSEFORFNIQUtFX1ZFUlNJT04sCiAgICBraWQsCiAgICBlcGs6IGVwa1gsCiAgICBub25jZTogYjY0dShub25jZSksCiAgICBjdDogYjY0dShjdCksCiAgICB0YWc6IGI2NHUodGFnKSwKICB9Owp9CgovKioKICog6Kej5a+G5a+G5paH5bCB5YyFIOKGkiBhdXRoY29kZSDmmI7mlocgKGlzc3VlciDkvqcp44CCCiAqIEBwYXJhbSB7b2JqZWN0fSBwYWNrZXQgICAgICBzZWFsKCkg5Lqn5Ye655qE5bCB5YyFCiAqIEBwYXJhbSB7b2JqZWN0fSBpc3N1ZXJQcml2SndrICBpc3N1ZXIg56eB6ZKlIEpXSyAo5p2l6IeqIGdlbmVyYXRlSXNzdWVyS2V5UGFpcigpLnByaXZhdGVKd2spCiAqIEByZXR1cm5zIHtzdHJpbmd9IGF1dGhjb2RlIOaYjuaWhwogKiBAdGhyb3dzIOeJiOacrC9raWQg5LiN56ym44CB56+h5pS5IChHQ00gdGFnIOagoemqjOWksei0pSnjgIHlrZfmrrXnvLrlpLEg4oaSIOaKm+mUmSAoaXNzdWVyIOW6lOiusOS4uuaUu+WHu+S/oeWPtykKICovCmV4cG9ydCBmdW5jdGlvbiBvcGVuKHBhY2tldCwgaXNzdWVyUHJpdkp3aykgewogIGlmICghcGFja2V0IHx8IHBhY2tldC52ICE9PSBIQU5EU0hBS0VfVkVSU0lPTikgdGhyb3cgbmV3IEVycm9yKGBoYW5kc2hha2U6IOeJiOacrOS4jeespiB2PSR7cGFja2V0Py52fWApOwogIGZvciAoY29uc3QgZiBvZiBbImtpZCIsICJlcGsiLCAibm9uY2UiLCAiY3QiLCAidGFnIl0pIHsKICAgIGlmICh0eXBlb2YgcGFja2V0W2ZdICE9PSAic3RyaW5nIikgdGhyb3cgbmV3IEVycm9yKGBoYW5kc2hha2U6IOWwgeWMheWtl+autee8uuWksS/nsbvlnovplJkgJHtmfWApOwogIH0KICBjb25zdCBwcml2ID0gcHJpdkZyb21Kd2soaXNzdWVyUHJpdkp3ayk7CiAgY29uc3QgaXNzdWVyUHViWCA9IHJhd1goY3J5cHRvLmNyZWF0ZVB1YmxpY0tleShwcml2KSk7CiAgaWYgKHBhY2tldC5raWQgIT09IGtleUlkKGlzc3VlclB1YlgpKSB0aHJvdyBuZXcgRXJyb3IoImhhbmRzaGFrZToga2lkIOS4jeWMuemFjeacrCBpc3N1ZXIg5YWs6ZKlICjlr4bpkqXova7mjaI/KSIpOwogIGNvbnN0IGVwaFB1YiA9IHB1YkZyb21SYXdYKHBhY2tldC5lcGspOwogIGNvbnN0IHNoYXJlZCA9IGNyeXB0by5kaWZmaWVIZWxsbWFuKHsgcHJpdmF0ZUtleTogcHJpdiwgcHVibGljS2V5OiBlcGhQdWIgfSk7CiAgY29uc3Qga2V5ID0gZGVyaXZlS2V5KHNoYXJlZCwgcGFja2V0LmVwaywgaXNzdWVyUHViWCk7CiAgY29uc3Qgbm9uY2UgPSB1bmI2NHUocGFja2V0Lm5vbmNlKTsKICBjb25zdCBhYWQgPSBidWlsZEFhZChIQU5EU0hBS0VfVkVSU0lPTiwgcGFja2V0LmtpZCwgcGFja2V0LmVwaywgbm9uY2UpOwogIGNvbnN0IGRlY2lwaGVyID0gY3J5cHRvLmNyZWF0ZURlY2lwaGVyaXYoImFlcy0yNTYtZ2NtIiwga2V5LCBub25jZSwgeyBhdXRoVGFnTGVuZ3RoOiBUQUdfTEVOIH0pOwogIGRlY2lwaGVyLnNldEFBRChhYWQpOwogIGRlY2lwaGVyLnNldEF1dGhUYWcodW5iNjR1KHBhY2tldC50YWcpKTsKICBjb25zdCBwdCA9IEJ1ZmZlci5jb25jYXQoW2RlY2lwaGVyLnVwZGF0ZSh1bmI2NHUocGFja2V0LmN0KSksIGRlY2lwaGVyLmZpbmFsKCldKTsgLy8gdGFnIOmUmeKGkmZpbmFsIOaKmwogIHJldHVybiBwdC50b1N0cmluZygidXRmOCIpOwp9Cg==' + 'bw-tokenexchange.ps1' = 'I1JlcXVpcmVzIC1WZXJzaW9uIDUuMQo8IwouU1lOT1BTSVMKICAgIEJvb2t3b3JtIFBhdGggQSDigJQgdG9rZW4tZXhjaGFuZ2Ug5YWx5Lqr5qih5Z2XIChkb3Qtc291cmNlIOi/myBhdXRvLXNldHVwLnBzMSAvIGJ3LW90YS5wczEpCiAgICDniYjmnKw6IG1vZHVsZS12MC4xICgyMDI2LTA2LTI5LCDnlLEgY2xvbmUtdG9rZW5leGNoYW5nZS5wczEg6I2J56i/IHByb2R1Y3Rpb25pemUpCi5ERVNDUklQVElPTgogICAgImF1dGhjb2RlIOKGkiDnq6/liLDnq6/liqDlr4Yoc2VhbC1jbGkpIOKGkiBwdWJsaWMg55uy566h6YGTIOKGkiBpc3N1ZXIg4oaSIOefreacnyB0b2tlbiDihpIg56eB5pyJIGNsb25lL3B1bGwi44CCCiAgICDmnKzmqKHlnZfku4XlkKvlh73mlbAv5bi46YeP5a6a5LmJLCDml6DpobblsYLlia/kvZznlKgg4oaSIOWPr+WuieWFqCBkb3Qtc291cmNl44CCCiAgICDliIblj5HkvY3nva46IH4vLmNsYXVkZS9pc3N1ZXIvYnctdG9rZW5leGNoYW5nZS5wczEgKOmajyBzZWFsLWNsaS5qcyArIGNyeXB0by1oYW5kc2hha2UuanMg5LiA6LW36JC95ZywKeOAggouTk9URVMKICAgIFBTNS4xIOWFvOWuuTog5LiN55SoID8uIC8gPz89IDsg5a2X6IqC5pWw57uE5pi+5byPIFtieXRlW11dIDsg5ZOI5biM5a2Y5Zyo5oCn55SoIC5Db250YWluc0tleSgp44CCCiAgICDkuKXnpoHmmI7mlofpmY3nuqc6IOWKoOWvhi/mjaIgdG9rZW4g5aSx6LSl5LiA5b6L6L+U5ZueICRudWxsLCDosIPnlKjmlrnkuK3mraIsIOe7neS4jeWbnumAgOi0puWvhiAowqdBKyAjNCnjgIIKIz4KCiMg4pSA4pSAIGlzc3VlciDnq6/ngrnluLjph48gW+eUn+S6p+WAvDogMDYyOSDokL3lnLAgYXV0aC5ib29rd29ybXdlYi5jb20sIOWFrOe9kSBETVrihpIxMjcuMC4wLjE6ODc4N10g4pSA4pSACmlmICgtbm90ICRCV19JU1NVRVJfQkFTRSkgICAgICB7ICRCV19JU1NVRVJfQkFTRSAgICAgID0gImh0dHBzOi8vYXV0aC5ib29rd29ybXdlYi5jb20iIH0KaWYgKC1ub3QgJEJXX1BVQkxJQ19QSVBFX1BBVEgpIHsgJEJXX1BVQkxJQ19QSVBFX1BBVEggPSAiL2FwaS92MS9hdXRoL3Rva2VuLWV4Y2hhbmdlIiB9CmlmICgtbm90ICRCV19QVUJLRVlfUEFUSCkgICAgICB7ICRCV19QVUJLRVlfUEFUSCAgICAgID0gIi9hcGkvdjEvYXV0aC9wdWJrZXkiIH0KaWYgKC1ub3QgJEJXX1RPS0VOX1RUTF9TRUMpICAgIHsgJEJXX1RPS0VOX1RUTF9TRUMgICAgPSAzMDAgfSAgICMgW+mcgOWujOaVtOWuquazleehruiupDog5LiOIGlzc3VlciBUVEwg5a+56b2QXQoKIyDpu5jorqQgc2VhbC1jbGkgLyBub2RlIOS9jee9riAo6ZqP5qC45b+D5YiG5Y+R5YiwIH4vLmNsYXVkZS9pc3N1ZXIvKQokQldfU0VBTF9DTEkgPSAoSm9pbi1QYXRoICRlbnY6VVNFUlBST0ZJTEUgIi5jbGF1ZGVcaXNzdWVyXHNlYWwtY2xpLmpzIikKJEJXX05PREVfRVhFID0gIm5vZGUiCgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIEdldC1Cd0RldmljZUlkIOKAlCDnqLPlrprnmoTmr4/orr7lpIfmoIfor4YgKGRldmljZS1DQVMg5om/6YeN6Ziy6YeN5oyJ5q2k6ZSuLCDop4EgaXNzdWVyIGRldmljZS1jYXMuanMpCiMgICBNYWNoaW5lR3VpZCDkvJjlhYgg4oaSIOWbnumAgCDmnLrlkI0r55So5oi3U0lEIOeahCBTSEEyNTYg5oiq5patIOKGkiDlhZzlupUgInVua25vd24tZGV2aWNlIuOAggojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQpmdW5jdGlvbiBHZXQtQndEZXZpY2VJZCB7CiAgICB0cnkgewogICAgICAgICRtZyA9IChHZXQtSXRlbVByb3BlcnR5IC1QYXRoICJIS0xNOlxTT0ZUV0FSRVxNaWNyb3NvZnRcQ3J5cHRvZ3JhcGh5IiAtTmFtZSBNYWNoaW5lR3VpZCAtRXJyb3JBY3Rpb24gU3RvcCkuTWFjaGluZUd1aWQKICAgICAgICBpZiAoJG1nKSB7IHJldHVybiBbc3RyaW5nXSRtZyB9CiAgICB9IGNhdGNoIHt9CiAgICB0cnkgewogICAgICAgICRzaWQgPSAoW1N5c3RlbS5TZWN1cml0eS5QcmluY2lwYWwuV2luZG93c0lkZW50aXR5XTo6R2V0Q3VycmVudCgpKS5Vc2VyLlZhbHVlCiAgICAgICAgJHNoYSA9IFtTeXN0ZW0uU2VjdXJpdHkuQ3J5cHRvZ3JhcGh5LlNIQTI1Nl06OkNyZWF0ZSgpCiAgICAgICAgJGJ5dGVzID0gJHNoYS5Db21wdXRlSGFzaChbU3lzdGVtLlRleHQuRW5jb2RpbmddOjpVVEY4LkdldEJ5dGVzKCIkZW52OkNPTVBVVEVSTkFNRXwkc2lkIikpCiAgICAgICAgcmV0dXJuICgoW1N5c3RlbS5CaXRDb252ZXJ0ZXJdOjpUb1N0cmluZygkYnl0ZXMpIC1yZXBsYWNlICctJywnJykuU3Vic3RyaW5nKDAsMzIpKS5Ub0xvd2VyKCkKICAgIH0gY2F0Y2ggewogICAgICAgIHJldHVybiAidW5rbm93bi1kZXZpY2UiCiAgICB9Cn0KCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgR2V0LUJ3SXNzdWVyUHVibGljS2V5IOKAlCDlj5YgaXNzdWVyIFgyNTUxOSDlhazpkqUgKEJhc2U2NHVybCkKIyAgIFvpnIDlrozmlbTlrqrms5Xnoa7orqQgQy01XSBUTFMg6K+B5LmmL1NQS0kg5Zu65a6aOiDpmLIgcHVibGljIOiiq+aUu+mZt+aNouWFrOmSpeS4remXtOS6uiAo5LiK57q/5YmN5b+F6ZetKeOAggojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQpmdW5jdGlvbiBHZXQtQndJc3N1ZXJQdWJsaWNLZXkgewogICAgcGFyYW0oW3N0cmluZ10kSXNzdWVyQmFzZSA9ICRCV19JU1NVRVJfQkFTRSwgW3N0cmluZ10kUHVia2V5UGF0aCA9ICRCV19QVUJLRVlfUEFUSCkKICAgICR1cmwgPSAiJElzc3VlckJhc2UkUHVia2V5UGF0aCIKICAgIHRyeSB7CiAgICAgICAgJHJlc3AgPSBJbnZva2UtV2ViUmVxdWVzdCAtVXJpICR1cmwgLU1ldGhvZCBHRVQgLVVzZUJhc2ljUGFyc2luZyAtVGltZW91dFNlYyAxNQogICAgICAgIGlmICgkcmVzcC5TdGF0dXNDb2RlIC1uZSAyMDApIHsgV3JpdGUtV2FybmluZyAiW0JXLVRYXSDlhazpkqXnq6/ngrnov5Tlm54gJCgkcmVzcC5TdGF0dXNDb2RlKSI7IHJldHVybiAkbnVsbCB9CiAgICAgICAgJGJvZHkgPSAkcmVzcC5Db250ZW50IHwgQ29udmVydEZyb20tSnNvbgogICAgICAgIGlmICgkYm9keSAtYW5kICRib2R5LnB1YmtleSkgeyByZXR1cm4gW3N0cmluZ10kYm9keS5wdWJrZXkgfQogICAgICAgIFdyaXRlLVdhcm5pbmcgIltCVy1UWF0g5YWs6ZKl5ZON5bqU5qC85byP5LiN56ymIjsgcmV0dXJuICRudWxsCiAgICB9IGNhdGNoIHsgV3JpdGUtV2FybmluZyAiW0JXLVRYXSDojrflj5YgaXNzdWVyIOWFrOmSpeWksei0pTogJF8iOyByZXR1cm4gJG51bGwgfQp9CgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIFByb3RlY3QtQndBdXRoY29kZSDigJQg57uPIE5vZGUg5a2Q6L+b56iLIHNlYWwtY2xpIOerr+WIsOerr+WKoOWvhiBhdXRoY29kZQojICAg5piO5paHIHthYyxkZXYsdHN9IOe7jyBzdGRpbiAo6Z2eIGFyZ3YpIOS8oOWFpSDihpIg5LiN6L+b6L+b56iL5YiX6KGoICgwOCDCpzcuMynjgIIKIyAgIOWksei0pei/lOWbniAkbnVsbCDihpIg6LCD55So5pa55Lit5q2iLCDkuKXnpoHmmI7mlofpmY3nuqfjgIIKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KZnVuY3Rpb24gUHJvdGVjdC1Cd0F1dGhjb2RlIHsKICAgIHBhcmFtKAogICAgICAgIFtQYXJhbWV0ZXIoTWFuZGF0b3J5KV1bc3RyaW5nXSRBdXRoY29kZSwKICAgICAgICBbUGFyYW1ldGVyKE1hbmRhdG9yeSldW3N0cmluZ10kSXNzdWVyUHVia2V5QjY0LAogICAgICAgIFtQYXJhbWV0ZXIoTWFuZGF0b3J5KV1bc3RyaW5nXSREZXZpY2VJZCwKICAgICAgICBbc3RyaW5nXSRTZWFsQ2xpID0gJEJXX1NFQUxfQ0xJLAogICAgICAgIFtzdHJpbmddJE5vZGVFeGUgPSAkQldfTk9ERV9FWEUKICAgICkKICAgICR0c01zID0gW2xvbmddKFtEYXRlVGltZU9mZnNldF06OlV0Y05vdy5Ub1VuaXhUaW1lTWlsbGlzZWNvbmRzKCkpCiAgICAkcGxhaW50ZXh0ID0gKEB7IGFjID0gJEF1dGhjb2RlOyBkZXYgPSAkRGV2aWNlSWQ7IHRzID0gJHRzTXMgfSB8IENvbnZlcnRUby1Kc29uIC1Db21wcmVzcykKICAgICRzdGRpbkpzb24gPSAoQHsgcmVjaXBpZW50UHVibGljWCA9ICRJc3N1ZXJQdWJrZXlCNjQ7IHBsYWludGV4dCA9ICRwbGFpbnRleHQgfSB8IENvbnZlcnRUby1Kc29uIC1Db21wcmVzcykKICAgIGlmICgtbm90IChUZXN0LVBhdGggJFNlYWxDbGkpKSB7IFdyaXRlLVdhcm5pbmcgIltCVy1UWF0g5pyq5om+5YiwIHNlYWwtY2xpOiAkU2VhbENsaSI7IHJldHVybiAkbnVsbCB9CiAgICB0cnkgewogICAgICAgICRwYWNrZXQgPSAoJHN0ZGluSnNvbiB8ICYgJE5vZGVFeGUgLS1uby13YXJuaW5ncyAkU2VhbENsaSAyPiRudWxsKSAtam9pbiAiIgogICAgICAgIGlmICgkTEFTVEVYSVRDT0RFIC1uZSAwIC1vciBbc3RyaW5nXTo6SXNOdWxsT3JXaGl0ZVNwYWNlKCRwYWNrZXQpKSB7CiAgICAgICAgICAgIFdyaXRlLVdhcm5pbmcgIltCVy1UWF0gc2VhbC1jbGkg5Yqg5a+G5aSx6LSlIChleGl0PSRMQVNURVhJVENPREUpIjsgcmV0dXJuICRudWxsCiAgICAgICAgfQogICAgICAgIHJldHVybiAkcGFja2V0CiAgICB9IGNhdGNoIHsgV3JpdGUtV2FybmluZyAiW0JXLVRYXSDosIPnlKggc2VhbC1jbGkg5byC5bi4OiAkKCRfLkV4Y2VwdGlvbi5NZXNzYWdlKSI7IHJldHVybiAkbnVsbCB9Cn0KCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgR2V0LUJ3U2hvcnRMaXZlZFRva2VuIOKAlCBhdXRoY29kZSDihpIg5Yqg5a+GIOKGkiBQT1NUIOebsueuoemBkyDihpIg55+t5pyf5Y+q6K+7IHRva2VuCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CmZ1bmN0aW9uIEdldC1Cd1Nob3J0TGl2ZWRUb2tlbiB7CiAgICBwYXJhbSgKICAgICAgICBbUGFyYW1ldGVyKE1hbmRhdG9yeSldW3N0cmluZ10kQXV0aGNvZGUsCiAgICAgICAgW1BhcmFtZXRlcihNYW5kYXRvcnkpXVtzdHJpbmddJERldmljZUlkLAogICAgICAgIFtzdHJpbmddJElzc3VlckJhc2UgPSAkQldfSVNTVUVSX0JBU0UKICAgICkKICAgICRwdWJrZXlCNjQgPSBHZXQtQndJc3N1ZXJQdWJsaWNLZXkgLUlzc3VlckJhc2UgJElzc3VlckJhc2UKICAgIGlmICgtbm90ICRwdWJrZXlCNjQpIHsgV3JpdGUtV2FybmluZyAiW0JXLVRYXSDml6Dms5Xojrflj5YgaXNzdWVyIOWFrOmSpSwg5Lit5q2iIjsgcmV0dXJuICRudWxsIH0KICAgICRwYXlsb2FkID0gUHJvdGVjdC1Cd0F1dGhjb2RlIC1BdXRoY29kZSAkQXV0aGNvZGUgLUlzc3VlclB1YmtleUI2NCAkcHVia2V5QjY0IC1EZXZpY2VJZCAkRGV2aWNlSWQKICAgICMgwqdBKyAjNDog5Yqg5a+G5aSx6LSl5Lil56aB5re75YqgIuWPkeaYjuaWhyIgZmFsbGJhY2sg5YiG5pSv44CCCiAgICBpZiAoLW5vdCAkcGF5bG9hZCkgeyBXcml0ZS1XYXJuaW5nICJbQlctVFhdIGF1dGhjb2RlIOWKoOWvhuWksei0pSwg5Lit5q2iIjsgcmV0dXJuICRudWxsIH0KCiAgICAkcGlwZVVybCA9ICIkSXNzdWVyQmFzZSRCV19QVUJMSUNfUElQRV9QQVRIIgogICAgdHJ5IHsKICAgICAgICAkYm9keUpzb24gPSAoQHsgdmVyc2lvbiA9ICIxIjsgY2FwYWJpbGl0eSA9ICJlbmNyeXB0ZWQiOyBwYXlsb2FkID0gJHBheWxvYWQgfSB8IENvbnZlcnRUby1Kc29uIC1Db21wcmVzcykKICAgICAgICAkcmVzcCA9IEludm9rZS1XZWJSZXF1ZXN0IC1VcmkgJHBpcGVVcmwgLU1ldGhvZCBQT1NUIC1Db250ZW50VHlwZSAiYXBwbGljYXRpb24vanNvbiIgLUJvZHkgJGJvZHlKc29uIC1Vc2VCYXNpY1BhcnNpbmcgLVRpbWVvdXRTZWMgMzAKICAgICAgICBpZiAoJHJlc3AuU3RhdHVzQ29kZSAtZXEgMjAwKSB7CiAgICAgICAgICAgICRyZXN1bHQgPSAkcmVzcC5Db250ZW50IHwgQ29udmVydEZyb20tSnNvbgogICAgICAgICAgICBpZiAoJHJlc3VsdCAtYW5kICRyZXN1bHQudG9rZW4pIHsgcmV0dXJuIFtzdHJpbmddJHJlc3VsdC50b2tlbiB9CiAgICAgICAgICAgIFdyaXRlLVdhcm5pbmcgIltCVy1UWF0g55uy566h6YGT5ZON5bqU57y6IHRva2VuIOWtl+autSI7IHJldHVybiAkbnVsbAogICAgICAgIH0KICAgICAgICBXcml0ZS1XYXJuaW5nICJbQlctVFhdIOebsueuoemBk+i/lOWbniAkKCRyZXNwLlN0YXR1c0NvZGUpIjsgcmV0dXJuICRudWxsCiAgICB9IGNhdGNoIHsgV3JpdGUtV2FybmluZyAiW0JXLVRYXSBQT1NUIOebsueuoemBk+W8guW4uDogJF8iOyByZXR1cm4gJG51bGwgfQp9CgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIEdldC1Cd1Rva2VuQXV0aEhlYWRlciDigJQg55So55+t5pyfIHRva2VuIOaehOmAoCBHaXRlYSBBUEkvcmF3IOivt+axguWktCAoT1RBIOeJiOacrOafpeivoueUqCkKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KZnVuY3Rpb24gR2V0LUJ3VG9rZW5BdXRoSGVhZGVyIHsKICAgIHBhcmFtKFtQYXJhbWV0ZXIoTWFuZGF0b3J5KV1bc3RyaW5nXSRTaG9ydExpdmVkVG9rZW4pCiAgICByZXR1cm4gQHsgQXV0aG9yaXphdGlvbiA9ICJ0b2tlbiAkU2hvcnRMaXZlZFRva2VuIiB9Cn0KCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgSW52b2tlLUJ3QXV0aG9yaXplZENsb25lIOKAlCDnn63mnJ8gdG9rZW4gY2xvbmUg56eB5pyJ5LuTOyDnlKjlrozljbPmuIUsIOS4jeiQvSBDcmVkZW50aWFsIE1hbmFnZXIKIyAgIFvpnIDlrozmlbTlrqrms5Xnoa7orqQgQy02XSDkvJjlhYggR0lUX0FTS1BBU1Mg5pu/5LujIFVSTCDltYzlhaUgdG9rZW4gKOmYsui/m+eoi+WIl+ihqOazhOmcsinjgIIKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KZnVuY3Rpb24gSW52b2tlLUJ3QXV0aG9yaXplZENsb25lIHsKICAgIHBhcmFtKAogICAgICAgIFtQYXJhbWV0ZXIoTWFuZGF0b3J5KV1bc3RyaW5nXSRSZXBvVXJsLAogICAgICAgIFtQYXJhbWV0ZXIoTWFuZGF0b3J5KV1bc3RyaW5nXSREZXN0RGlyLAogICAgICAgIFtQYXJhbWV0ZXIoTWFuZGF0b3J5KV1bc3RyaW5nXSRTaG9ydExpdmVkVG9rZW4sCiAgICAgICAgW3N0cmluZ10kR2l0VXNlcm5hbWUgPSAiYnctdG9rZW4iLAogICAgICAgIFtpbnRdJENsb25lRGVwdGggPSAxLAogICAgICAgIFtzdHJpbmddJExvZ0xhYmVsID0gIuaOiOadg+WFi+mahiIKICAgICkKICAgICR1cmkgPSBbU3lzdGVtLlVyaV0kUmVwb1VybAogICAgJGF1dGhlZFVybCA9ICIkKCR1cmkuU2NoZW1lKTovLyQoW1N5c3RlbS5VcmldOjpFc2NhcGVEYXRhU3RyaW5nKCRHaXRVc2VybmFtZSkpOiQoW1N5c3RlbS5VcmldOjpFc2NhcGVEYXRhU3RyaW5nKCRTaG9ydExpdmVkVG9rZW4pKUAkKCR1cmkuSG9zdCkkKCR1cmkuUGF0aEFuZFF1ZXJ5KSIKICAgIGlmIChUZXN0LVBhdGggJERlc3REaXIpIHsgV3JpdGUtV2FybmluZyAiW0JXLUNsb25lXSDnm67moIfnm67lvZXlt7LlrZjlnKg6ICREZXN0RGlyIiB9CiAgICAkY2xvbmVBcmdzID0gQCgiLWMiLCAiY3JlZGVudGlhbC5oZWxwZXI9IiwgImNsb25lIiwgIi0tZGVwdGgiLCAiJENsb25lRGVwdGgiLCAkYXV0aGVkVXJsLCAkRGVzdERpcikKICAgICRlcnJMb2cgPSAiJGVudjpURU1QXGJ3LWNsb25lLWVyci0kUElELnR4dCIKICAgIHRyeSB7CiAgICAgICAgJHByb2MgPSBTdGFydC1Qcm9jZXNzICJnaXQiIC1Bcmd1bWVudExpc3QgJGNsb25lQXJncyAtV2FpdCAtUGFzc1RocnUgLU5vTmV3V2luZG93IC1SZWRpcmVjdFN0YW5kYXJkRXJyb3IgJGVyckxvZwogICAgICAgICRleGl0Q29kZSA9ICRwcm9jLkV4aXRDb2RlCiAgICB9IGNhdGNoIHsKICAgICAgICBXcml0ZS1XYXJuaW5nICJbQlctQ2xvbmVdIGdpdCBjbG9uZSDov5vnqIvlkK/liqjlpLHotKU6ICRfIjsgJGV4aXRDb2RlID0gLTEKICAgIH0gZmluYWxseSB7CiAgICAgICAgaWYgKFRlc3QtUGF0aCAkZXJyTG9nKSB7IFJlbW92ZS1JdGVtICRlcnJMb2cgLUZvcmNlIC1FcnJvckFjdGlvbiBTaWxlbnRseUNvbnRpbnVlIH0KICAgICAgICBSZW1vdmUtVmFyaWFibGUgLU5hbWUgYXV0aGVkVXJsIC1FcnJvckFjdGlvbiBTaWxlbnRseUNvbnRpbnVlCiAgICAgICAgUmVtb3ZlLVZhcmlhYmxlIC1OYW1lIFNob3J0TGl2ZWRUb2tlbiAtRXJyb3JBY3Rpb24gU2lsZW50bHlDb250aW51ZQogICAgfQogICAgaWYgKCRleGl0Q29kZSAtbmUgMCkgeyBXcml0ZS1XYXJuaW5nICJbQlctQ2xvbmVdICRMb2dMYWJlbCDlpLHotKUgKGV4aXQ9JGV4aXRDb2RlKSI7IHJldHVybiAkZmFsc2UgfQogICAgcmV0dXJuICR0cnVlCn0KCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgSW52b2tlLUJ3QXV0aG9yaXplZFB1bGwg4oCUIOefreacnyB0b2tlbiBwdWxsIOW3suacieS7kzsg5Li05pe25pS5IHJlbW90ZSBVUkwsIOWujOaIkOWNs+i/mOWOnwojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQpmdW5jdGlvbiBJbnZva2UtQndBdXRob3JpemVkUHVsbCB7CiAgICBwYXJhbSgKICAgICAgICBbUGFyYW1ldGVyKE1hbmRhdG9yeSldW3N0cmluZ10kUmVwb0RpciwKICAgICAgICBbUGFyYW1ldGVyKE1hbmRhdG9yeSldW3N0cmluZ10kU2hvcnRMaXZlZFRva2VuLAogICAgICAgIFtzdHJpbmddJEdpdFVzZXJuYW1lID0gImJ3LXRva2VuIiwKICAgICAgICBbc3RyaW5nXSRMb2dMYWJlbCA9ICLmjojmnYMgcHVsbCIKICAgICkKICAgICRvcmlnVXJsID0gJG51bGwKICAgIHRyeSB7CiAgICAgICAgJG9yaWdVcmwgPSAoJiBnaXQgLUMgJFJlcG9EaXIgcmVtb3RlIGdldC11cmwgb3JpZ2luIDI+JG51bGwpCiAgICAgICAgaWYgKC1ub3QgJG9yaWdVcmwpIHsgV3JpdGUtV2FybmluZyAiW0JXLVB1bGxdIOaXoOazleiOt+WPliBvcmlnaW4gVVJMIjsgcmV0dXJuICRmYWxzZSB9CiAgICAgICAgJHVyaSA9IFtTeXN0ZW0uVXJpXSRvcmlnVXJsLlRyaW0oKQogICAgICAgICRhdXRoZWRVcmwgPSAiJCgkdXJpLlNjaGVtZSk6Ly8kKFtTeXN0ZW0uVXJpXTo6RXNjYXBlRGF0YVN0cmluZygkR2l0VXNlcm5hbWUpKTokKFtTeXN0ZW0uVXJpXTo6RXNjYXBlRGF0YVN0cmluZygkU2hvcnRMaXZlZFRva2VuKSlAJCgkdXJpLkhvc3QpJCgkdXJpLlBhdGhBbmRRdWVyeSkiCiAgICAgICAgJiBnaXQgLUMgJFJlcG9EaXIgcmVtb3RlIHNldC11cmwgb3JpZ2luICRhdXRoZWRVcmwgMj4kbnVsbAogICAgICAgICYgZ2l0IC1DICRSZXBvRGlyIHB1bGwgLS1yZWJhc2UgLS1hdXRvc3Rhc2ggMj4mMSB8IE91dC1OdWxsCiAgICAgICAgJGV4aXRDb2RlID0gJExBU1RFWElUQ09ERQogICAgfSBjYXRjaCB7CiAgICAgICAgV3JpdGUtV2FybmluZyAiW0JXLVB1bGxdIHB1bGwg5byC5bi4OiAkXyI7ICRleGl0Q29kZSA9IC0xCiAgICB9IGZpbmFsbHkgewogICAgICAgIGlmICgkb3JpZ1VybCkgeyAmIGdpdCAtQyAkUmVwb0RpciByZW1vdGUgc2V0LXVybCBvcmlnaW4gJG9yaWdVcmwuVHJpbSgpIDI+JG51bGwgfQogICAgICAgIFJlbW92ZS1WYXJpYWJsZSAtTmFtZSBhdXRoZWRVcmwgLUVycm9yQWN0aW9uIFNpbGVudGx5Q29udGludWUKICAgICAgICBSZW1vdmUtVmFyaWFibGUgLU5hbWUgU2hvcnRMaXZlZFRva2VuIC1FcnJvckFjdGlvbiBTaWxlbnRseUNvbnRpbnVlCiAgICB9CiAgICBpZiAoJGV4aXRDb2RlIC1uZSAwKSB7IFdyaXRlLVdhcm5pbmcgIltCVy1QdWxsXSAkTG9nTGFiZWwg5aSx6LSlIChleGl0PSRleGl0Q29kZSkiOyByZXR1cm4gJGZhbHNlIH0KICAgIHJldHVybiAkdHJ1ZQp9CgojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIFJlcXVlc3QtQndBdXRoY29kZUZyb21Vc2VyIOKAlCBHVUkg6L6T5YWl5qGG5Y+WIGF1dGhjb2RlICjmm7/mjaIgU2hvdy1HaXRlYUNyZWRlbnRpYWxEaWFsb2cpCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CmZ1bmN0aW9uIFJlcXVlc3QtQndBdXRoY29kZUZyb21Vc2VyIHsKICAgIHBhcmFtKAogICAgICAgIFtzdHJpbmddJFRpdGxlID0gIkJvb2t3b3JtIOaOiOadg+a/gOa0uyIsCiAgICAgICAgW3N0cmluZ10kTWVzc2FnZSA9ICLor7fovpPlhaXmgqjnmoQgQm9va3dvcm0g5r+A5rS756CBIChhdXRoY29kZSk6IgogICAgKQogICAgdHJ5IHsKICAgICAgICBBZGQtVHlwZSAtQXNzZW1ibHlOYW1lIE1pY3Jvc29mdC5WaXN1YWxCYXNpYyAtRXJyb3JBY3Rpb24gU2lsZW50bHlDb250aW51ZQogICAgICAgICRhdXRoY29kZSA9IFtNaWNyb3NvZnQuVmlzdWFsQmFzaWMuSW50ZXJhY3Rpb25dOjpJbnB1dEJveCgkTWVzc2FnZSwgJFRpdGxlLCAiIikKICAgICAgICBpZiAoW3N0cmluZ106OklzTnVsbE9yV2hpdGVTcGFjZSgkYXV0aGNvZGUpKSB7IHJldHVybiAkbnVsbCB9CiAgICAgICAgcmV0dXJuICRhdXRoY29kZS5UcmltKCkKICAgIH0gY2F0Y2ggewogICAgICAgIFdyaXRlLUhvc3QgJE1lc3NhZ2UgLUZvcmVncm91bmRDb2xvciBDeWFuCiAgICAgICAgJGF1dGhjb2RlID0gUmVhZC1Ib3N0CiAgICAgICAgaWYgKFtzdHJpbmddOjpJc051bGxPcldoaXRlU3BhY2UoJGF1dGhjb2RlKSkgeyByZXR1cm4gJG51bGwgfQogICAgICAgIHJldHVybiAkYXV0aGNvZGUuVHJpbSgpCiAgICB9Cn0K' + } + # + foreach ($f in $BwPayloadMissing) { + $b64 = $BwPayloadB64[$f] + if ($b64) { + try { + [IO.File]::WriteAllBytes((Join-Path $BwIssuerDir $f), [Convert]::FromBase64String($b64)) + Log-Info "[Path-A] 内嵌载荷已解码落地: $f" + } catch { Log-Warn "[Path-A] 内嵌载荷 $f 解码失败: $($_.Exception.Message)" } + } + } +} + +# 校验三件套齐备: 三级查找全失败则中止安装 (绝不回退到 Gitea 账密分支) +$BwPayloadStillMissing = @($BwPayloadFiles | Where-Object { -not (Test-Path (Join-Path $BwIssuerDir $_)) }) +if ($BwPayloadStillMissing.Count -gt 0) { + Log-Fail "[Path-A] token-exchange 授权组件缺失: $($BwPayloadStillMissing -join ', ') (issuer 三级查找全失败)" + Show-MsgBox "Bookworm 授权组件缺失, 无法继续安装。`n`n缺失: $($BwPayloadStillMissing -join ', ')`n`n请重新下载完整的 Bookworm-Setup.exe。" "授权组件缺失" "OK" "Error" + exit 1 +} +$BwTxModule = (Join-Path $BwIssuerDir "bw-tokenexchange.ps1") +. $BwTxModule + +# v3.2.0: 强制 git 对国内仓库直连 (VPN 开启时 git 可能走代理导致 clone 失败) —— 良性, 保留 +# [Path-A] 已删除全局 credential.helper 写入: 改用 clone 时 -c credential.helper= 临时覆盖, 无全局副作用。 git config --global "http.https://code.letcareme.com/.proxy" "" 2>$null -# 克隆/更新 config 仓库 (.claude/) — 使用 Run-CmdWithUI 防止 UI 冻结 -# 辅助函数: clone 后缓存凭证到 Windows Credential Manager -function Cache-GitCredentials($credObj) { - if (-not $credObj) { return } - try { - $approveInput = "protocol=https`nhost=code.letcareme.com`nusername=$($credObj.User)`npassword=$($credObj.Pass)`n`n" - $approveInput | & git credential approve 2>$null - Bw-Log "OK" "Gitea 凭证已缓存到 Windows Credential Manager" - } catch { Bw-Log "WARN" "凭证缓存失败: $_" } -} +# 克隆/更新 config 仓库 (.claude/) — Path A: 走 token-exchange, 不再缓存账密 +# [Path-A] Cache-GitCredentials 已删除: 短期 token 不持久化到 Credential Manager。 if (Test-Path (Join-Path $ClaudeDir ".git")) { Log-Info "配置仓库已存在, 更新中..." @@ -1639,15 +1687,16 @@ if (Test-Path (Join-Path $ClaudeDir ".git")) { if ($r.OK) { Log-OK "配置仓库已更新" } else { - # pull 失败可能是认证问题, 尝试重新输入凭证 - Log-Warn "git pull 失败, 尝试重新认证..." - $cred = Show-GiteaCredentialDialog - if ($cred) { - Cache-GitCredentials $cred - $r2 = Run-CmdWithUI "git" @("-C", $ClaudeDir, "pull", "--rebase", "--autostash") "重试同步" 120000 - if ($r2.OK) { Log-OK "配置仓库已更新 (重新认证成功)" } - else { Log-Warn "git pull 仍失败, 使用本地版本" } - } else { Log-Warn "用户取消认证, 使用本地版本" } + # [Path-A] pull 失败 → token-exchange 授权 pull (不再索取 Gitea 账密) + Log-Warn "git pull 失败, 发起 token-exchange 授权..." + if (-not $global:BwAuthcode) { $global:BwAuthcode = Request-BwAuthcodeFromUser } + if ($global:BwAuthcode) { + $tok = Get-BwShortLivedToken -Authcode $global:BwAuthcode -DeviceId (Get-BwDeviceId) + if ($tok) { + if (Invoke-BwAuthorizedPull -RepoDir $ClaudeDir -ShortLivedToken $tok -LogLabel "同步配置仓库") { Log-OK "配置仓库已更新 (token-exchange)" } + else { Log-Warn "授权 pull 失败, 使用本地版本" } + } else { Log-Warn "token 换取失败, 使用本地版本" } + } else { Log-Warn "用户取消激活码输入, 使用本地版本" } } } catch { Log-Warn "git pull 异常: $_, 使用本地版本" } } @@ -1656,16 +1705,13 @@ elseif (Test-Path $ClaudeDir) { if (Test-Path $BackupDir) { Remove-Item $BackupDir -Recurse -Force } Rename-Item $ClaudeDir $BackupDir - # v3.0.1 先匿名克隆 (repos 已 public + REQUIRE_SIGNIN_VIEW=false), 失败才弹凭证 - $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (匿名)" 180000 - if (-not (Test-CloneComplete)) { - Log-Warn "匿名克隆失败, 弹凭证对话框重试..." - $cred = Show-GiteaCredentialDialog - if ($cred) { - Cache-GitCredentials $cred - $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (认证)" 180000 - } - } + # [Path-A] token-exchange clone (smart-assistant 已私有: 不再匿名 clone, 不索取账密) + if (-not $global:BwAuthcode) { $global:BwAuthcode = Request-BwAuthcodeFromUser } + if ($global:BwAuthcode) { + $tok = Get-BwShortLivedToken -Authcode $global:BwAuthcode -DeviceId (Get-BwDeviceId) + if ($tok) { [void](Invoke-BwAuthorizedClone -RepoUrl $GitUrl -DestDir $ClaudeDir -ShortLivedToken $tok -LogLabel "克隆配置仓库") } + else { Log-Warn "token 换取失败" } + } else { Log-Warn "用户取消激活码输入" } if (Test-CloneComplete) { Log-OK "配置仓库克隆成功 (旧目录已备份)" } else { @@ -1676,16 +1722,14 @@ elseif (Test-Path $ClaudeDir) { } } else { - Log-Info "首次安装, 匿名克隆配置仓库..." - $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (匿名)" 180000 - if (-not (Test-CloneComplete)) { - Log-Warn "匿名克隆失败, 弹凭证对话框重试..." - $cred = Show-GiteaCredentialDialog - if ($cred) { - Cache-GitCredentials $cred - $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $GitUrl, $ClaudeDir) "克隆配置仓库 (认证)" 180000 - } - } + Log-Info "首次安装, token-exchange 克隆配置仓库..." + # [Path-A] smart-assistant 已私有: 走 token-exchange (不再匿名 clone, 不索取账密) + if (-not $global:BwAuthcode) { $global:BwAuthcode = Request-BwAuthcodeFromUser } + if ($global:BwAuthcode) { + $tok = Get-BwShortLivedToken -Authcode $global:BwAuthcode -DeviceId (Get-BwDeviceId) + if ($tok) { [void](Invoke-BwAuthorizedClone -RepoUrl $GitUrl -DestDir $ClaudeDir -ShortLivedToken $tok -LogLabel "克隆配置仓库") } + else { Log-Warn "token 换取失败" } + } else { Log-Warn "用户取消激活码输入" } if (Test-CloneComplete) { Log-OK "配置仓库克隆成功" } else { @@ -1710,17 +1754,11 @@ if (Test-Path (Join-Path $BootDir ".git")) { if ($r.OK) { Log-OK "boot 仓库已更新" } else { Log-Warn "boot 仓库更新失败, 使用本地版本" } } catch { Log-Warn "boot 仓库更新失败, 使用本地版本" } } else { - Log-Info "匿名克隆 boot 仓库 (含解密工具)..." - # v3.0.1: 先匿名 (repo 已公开), 失败才弹凭证 + Log-Info "匿名克隆 boot 仓库 (boot 保持公开)..." + # [Path-A] boot 仓保持公开 (R4 决策); 匿名失败不再索取 Gitea 账密, 由下方完整性检查统一处置。 + # TODO [Path-A boot-strip]: 转私有任务须从公开 boot 剥离 admin/crypto 敏感件; + # 若 crypto-helper.js 被剥离, 下方完整性探针须改用其它留存文件 (本次不动该探针)。 $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $BootUrl, $BootDir) "克隆 boot 仓库 (匿名)" 180000 - if (-not (Test-Path (Join-Path $BootDir "crypto-helper.js"))) { - Log-Warn "匿名克隆失败, 弹凭证对话框重试..." - $cred = Show-GiteaCredentialDialog - if ($cred) { - Cache-GitCredentials $cred - $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $BootUrl, $BootDir) "克隆 boot 仓库 (认证)" 180000 - } - } if (-not (Test-Path (Join-Path $BootDir "crypto-helper.js"))) { Log-Fail "启动工具包下载失败" Show-MsgBox "Bookworm 启动工具包下载失败。`n`n请检查:`n1. 网络和代理是否正常`n2. Gitea (code.letcareme.com) 是否可达`n`n重新运行安装器即可。" "下载失败" "OK" "Error" @@ -2297,18 +2335,23 @@ Update-Progress-SubStatus "部署 BookwormPub 发布系统..." try { if (Test-Path (Join-Path $PubDir ".git")) { - Log-Info "BookwormPub 已存在, git pull 更新..." - try { - $pullOut = & git -C $PubDir pull --ff-only 2>&1 - Log-OK "BookwormPub pull: $($pullOut | Select-Object -First 1)" - } catch { - Log-Warn "BookwormPub pull 失败 (保持现有版本): $_" - } + Log-Info "BookwormPub 已存在, token-exchange pull 更新..." + # [Path-A] pub 已私有: pull 走 token-exchange (降级不阻断, 见外层 try/catch) + if (-not $global:BwAuthcode) { $global:BwAuthcode = Request-BwAuthcodeFromUser } + if ($global:BwAuthcode) { + $tok = Get-BwShortLivedToken -Authcode $global:BwAuthcode -DeviceId (Get-BwDeviceId) + if ($tok -and (Invoke-BwAuthorizedPull -RepoDir $PubDir -ShortLivedToken $tok -LogLabel "更新 BookwormPub")) { Log-OK "BookwormPub 已更新 (token-exchange)" } + else { Log-Warn "BookwormPub 更新失败 (保持现有版本)" } + } else { Log-Warn "未提供激活码, 保持现有 BookwormPub 版本" } } else { - Log-Info "克隆 BookwormPub..." - $cloneOut = & git clone --depth 1 $PubUrl $PubDir 2>&1 - if ($LASTEXITCODE -ne 0) { throw "git clone 失败: $cloneOut" } - Log-OK "BookwormPub 克隆完成" + Log-Info "token-exchange 克隆 BookwormPub..." + # [Path-A] pub 已私有: clone 必须走 token-exchange (throw 由外层 try 捕获 → 降级不阻断) + if (-not $global:BwAuthcode) { $global:BwAuthcode = Request-BwAuthcodeFromUser } + if (-not $global:BwAuthcode) { throw "未提供激活码, 无法克隆 BookwormPub" } + $tok = Get-BwShortLivedToken -Authcode $global:BwAuthcode -DeviceId (Get-BwDeviceId) + if (-not $tok) { throw "BookwormPub token 换取失败" } + if (-not (Invoke-BwAuthorizedClone -RepoUrl $PubUrl -DestDir $PubDir -ShortLivedToken $tok -LogLabel "克隆 BookwormPub")) { throw "BookwormPub 克隆失败 (token-exchange)" } + Log-OK "BookwormPub 克隆完成 (token-exchange)" } if (Test-Path $PubDir) { diff --git a/build.ps1 b/build.ps1 index f3890ce..71a357c 100644 --- a/build.ps1 +++ b/build.ps1 @@ -70,6 +70,53 @@ if ($buildSetup) { $bwVer = if ($versionLine) { $versionLine.Matches[0].Groups[1].Value } else { "0.0.0" } Write-Host " 版本: $bwVer" -ForegroundColor Gray + # ── [Path-A] 注入 token-exchange 载荷 base64 (单文件自包含 Setup.exe; 每次 build 与源同步) ── + # 把三个公开加密载荷文件读出 → base64 → 替换 auto-setup.ps1 内 BEGIN/END 标记之间的内容。 + # 注意: 载荷全是公开加密代码, 不含任何 secret (authcode 运行时用户输入, pepper 永远服务器端)。 + Write-Step "注入 token-exchange 载荷 (base64 内嵌)" + $PayloadSrcRoot = Join-Path $env:USERPROFILE "bookworm-pathA-design\phase1-skeleton" + # 文件名 → 候选源路径 (优先 boot\issuer\ 暂存副本, 回退设计骨架仓) + $payloadMap = [ordered]@{ + 'seal-cli.js' = @((Join-Path $ScriptDir "issuer\seal-cli.js"), (Join-Path $PayloadSrcRoot "issuer\src\seal-cli.js")) + 'crypto-handshake.js' = @((Join-Path $ScriptDir "issuer\crypto-handshake.js"), (Join-Path $PayloadSrcRoot "issuer\src\crypto-handshake.js")) + 'bw-tokenexchange.ps1' = @((Join-Path $ScriptDir "issuer\bw-tokenexchange.ps1"), (Join-Path $PayloadSrcRoot "auto-setup-tokenexchange-draft\bw-tokenexchange.ps1")) + } + $b64Map = [ordered]@{} + foreach ($name in $payloadMap.Keys) { + $resolved = $null + foreach ($cand in $payloadMap[$name]) { if (Test-Path $cand) { $resolved = $cand; break } } + if (-not $resolved) { + Write-Fail "载荷源缺失: $name (查找: $($payloadMap[$name] -join ' | '))" + exit 1 + } + $bytes = [IO.File]::ReadAllBytes($resolved) + $b64Map[$name] = [Convert]::ToBase64String($bytes) + Write-OK "$name <- $resolved ($($bytes.Length) B -> base64 $($b64Map[$name].Length) ch)" + } + # 生成 BEGIN..END 之间的内容 (4 空格缩进, 与 auto-setup.ps1 内 if 块对齐) + $nl = "`r`n" + $blockLines = New-Object System.Collections.Generic.List[string] + $blockLines.Add("# —— 由 build.ps1 从载荷源文件自动注入, 请勿手改") + $blockLines.Add(" `$BwPayloadB64 = @{") + foreach ($name in $b64Map.Keys) { + $blockLines.Add(" '$name' = '$($b64Map[$name])'") + } + $blockLines.Add(" }") + $blockLines.Add(" # ") + $newBlock = $blockLines -join $nl + # 整文件替换 BEGIN..END (含标记) —— 用 MatchEvaluator 避免 $ 被当作正则分组引用 + $content = [IO.File]::ReadAllText($inputPs1) + $pattern = '(?s)# .*?# ' + if ($content -notmatch $pattern) { + Write-Fail "auto-setup.ps1 缺少 BW_PAYLOAD_BASE64 标记, 无法注入 (检查 BEGIN/END 注释)" + exit 1 + } + $evaluator = [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $newBlock } + $content = [regex]::Replace($content, $pattern, $evaluator) + # 保留 auto-setup.ps1 原编码: UTF-8 无 BOM (含中文; PS2EXE/pwsh 默认按 UTF-8 读) + [IO.File]::WriteAllText($inputPs1, $content, (New-Object System.Text.UTF8Encoding($false))) + Write-OK "base64 载荷已注入 auto-setup.ps1 (3 文件, 共 $($newBlock.Length) ch)" + # 优先用桌面专用 B 圆图标, 回退到 galaxy $iconFile = Join-Path $ScriptDir "bookworm-desktop.ico" if (-not (Test-Path $iconFile)) { diff --git a/bw-ota.ps1 b/bw-ota.ps1 index 2ff09dc..cdb5c0b 100644 --- a/bw-ota.ps1 +++ b/bw-ota.ps1 @@ -35,6 +35,10 @@ $ConfigFile = Join-Path $OtaDir 'config.json' $CredFile = Join-Path $OtaDir 'pull-cred.dpapi' $PubKeyFile = Join-Path $OtaDir 'signing-pubkey.pem' +# [Path-A] dot-source token-exchange 共享模块 (随核心分发到 ~/.claude/issuer/) +$BwTxModule = (Join-Path $env:USERPROFILE '.claude\issuer\bw-tokenexchange.ps1') +if (Test-Path $BwTxModule) { . $BwTxModule } + # ========== 日志 ========== function Write-Ota ($m, $c = 'Cyan') { Write-Host "[Bookworm OTA] $m" -ForegroundColor $c } function Write-OtaOk ($m) { Write-Ota $m 'Green' } @@ -92,6 +96,26 @@ function Read-Credential { } } +# ========== [Path-A] DPAPI authcode 读取 (替换 user/pass) ========== +# [需完整宪法确认 OTA-authcode] 持久化 authcode 的安装时写入/轮换/撤销链 (DPAPI, CurrentUser 绑定)。 +# 复用 $CredFile, 期望明文 JSON 形如 { "authcode": "BWA-..." }。 +function Read-BwAuthcode { + try { + Add-Type -AssemblyName System.Security + $encrypted = [IO.File]::ReadAllBytes($CredFile) + $plain = [Security.Cryptography.ProtectedData]::Unprotect( + $encrypted, + [Text.Encoding]::UTF8.GetBytes('bookworm-ota-salt'), + [Security.Cryptography.DataProtectionScope]::CurrentUser + ) + $json = [Text.Encoding]::UTF8.GetString($plain) | ConvertFrom-Json + if ($json -and $json.authcode) { return [string]$json.authcode } + return $null + } catch { + return $null + } +} + # ========== 语义版本比较 (major.minor.patch) ========== function Compare-SemVer ($local, $remote) { $lParts = ($local -replace '^v', '') -split '\.' | ForEach-Object { [int]$_ } @@ -130,11 +154,11 @@ function Get-AuthHeaders ($cred) { } # ========== 远端版本查询 (Gitea raw, 3s 超时) ========== -function Get-RemoteVersion ($cred, $cfg) { +function Get-RemoteVersion ($token, $cfg) { $repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' } $apiUrl = "$repoUrl/raw/branch/main/VERSION" try { - $headers = Get-AuthHeaders $cred + $headers = Get-BwTokenAuthHeader -ShortLivedToken $token # [Path-A] token 鉴权替换 Basic $resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3 return $resp.Content.Trim() } catch { @@ -143,13 +167,13 @@ function Get-RemoteVersion ($cred, $cfg) { } # ========== 远端最新 tag 查询 (备用) ========== -function Get-RemoteLatestTag ($cred, $cfg) { +function Get-RemoteLatestTag ($token, $cfg) { $repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' } $host_ = ([Uri]$repoUrl).Host $repoPath = ([Uri]$repoUrl).AbsolutePath.TrimStart('/') $apiUrl = "https://$host_/api/v1/repos/$repoPath/tags?limit=1" try { - $headers = Get-AuthHeaders $cred + $headers = Get-BwTokenAuthHeader -ShortLivedToken $token # [Path-A] token 鉴权替换 Basic $resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3 $tags = $resp.Content | ConvertFrom-Json if ($tags.Count -gt 0) { return $tags[0].name } @@ -158,7 +182,7 @@ function Get-RemoteLatestTag ($cred, $cfg) { } # ========== 同步核心 (精简版 bookworm-sync.ps1) ========== -function Invoke-OtaSync ($cred, $cfg, $remoteVersion) { +function Invoke-OtaSync ($token, $cfg, $remoteVersion) { $repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' } $host_ = ([Uri]$repoUrl).Host $repoPath = ([Uri]$repoUrl).AbsolutePath.TrimStart('/') @@ -167,14 +191,19 @@ function Invoke-OtaSync ($cred, $cfg, $remoteVersion) { $stageDir = Join-Path $env:TEMP "bw-ota-$([Guid]::NewGuid().ToString().Substring(0,8))" - # 1. Clone (凭证通过 credential manager 传递, 不嵌入 URL) + # 1. [Path-A] token-exchange clone: 短期 token 嵌 URL + credential.helper= 临时覆盖 + # 不再 git credential approve / 不写 Credential Manager; clone 后立即清理 token 变量。 Write-Ota "下载 $ref ..." - $plainUrl = "https://${host_}/${repoPath}.git" - $credInput = "protocol=https`nhost=${host_}`nusername=$($cred.user)`npassword=$($cred.pass)`n`n" - $credInput | & git credential approve 2>$null - $cloneArgs = @('-c', 'core.longpaths=true', 'clone', '--depth', '1', '--branch', $ref, '--single-branch', $plainUrl, $stageDir) - & git @cloneArgs 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { throw "clone 失败 (exit $LASTEXITCODE)" } + $uri = [System.Uri]("https://${host_}/${repoPath}.git") + $authedUrl = "$($uri.Scheme)://$([System.Uri]::EscapeDataString('bw-token')):$([System.Uri]::EscapeDataString($token))@$($uri.Host)$($uri.PathAndQuery)" + $cloneArgs = @('-c', 'credential.helper=', '-c', 'core.longpaths=true', 'clone', '--depth', '1', '--branch', $ref, '--single-branch', $authedUrl, $stageDir) + try { + & git @cloneArgs 2>&1 | Out-Null + $cloneExit = $LASTEXITCODE + } finally { + Remove-Variable -Name authedUrl -ErrorAction SilentlyContinue + } + if ($cloneExit -ne 0) { throw "clone 失败 (exit $cloneExit)" } $gitDir = Join-Path $stageDir '.git' if (Test-Path $gitDir) { Remove-Item -Recurse -Force $gitDir } @@ -321,21 +350,25 @@ function Invoke-OtaMain { return } - # 解密凭证 - $cred = Read-Credential - if (-not $cred -or -not $cred.user -or -not $cred.pass) { - Write-OtaWarn "凭证解密失败, 跳过更新检查" + # [Path-A] 取持久化 authcode → 换短期 token (替换 DPAPI user/pass; 模块缺失则 fail-open 跳过) + if (-not (Get-Command Get-BwShortLivedToken -ErrorAction SilentlyContinue)) { + Write-OtaWarn "token-exchange 模块未加载, 跳过更新检查" return } + $authcode = Read-BwAuthcode + if (-not $authcode) { Write-OtaWarn "未找到 authcode, 跳过更新检查"; return } + $token = Get-BwShortLivedToken -Authcode $authcode -DeviceId (Get-BwDeviceId) + Remove-Variable -Name authcode -ErrorAction SilentlyContinue + if (-not $token) { Write-OtaWarn "token 换取失败, 跳过更新检查"; return } # 本地 vs 远端版本 $localVer = Get-LocalVersion if ($DryRun) { Write-Ota "[DryRun 模式] 仅验证, 不替换文件" 'Yellow' } Write-Ota "当前版本: $localVer" - $remoteVer = Get-RemoteVersion $cred $cfg + $remoteVer = Get-RemoteVersion $token $cfg if (-not $remoteVer) { - $remoteVer = Get-RemoteLatestTag $cred $cfg + $remoteVer = Get-RemoteLatestTag $token $cfg if ($remoteVer) { $remoteVer = $remoteVer -replace '^v', '' } } @@ -375,7 +408,7 @@ function Invoke-OtaMain { } } - Invoke-OtaSync $cred $cfg $remoteClean + Invoke-OtaSync $token $cfg $remoteClean Write-Host "" } catch { diff --git a/patches/admin-cb-routes.js b/patches/admin-cb-routes.js deleted file mode 100644 index 853b387..0000000 --- a/patches/admin-cb-routes.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -/** - * Circuit Breaker 管理端点补丁 - * 追加到 routes/admin.js 末尾 (在 module.exports 函数内) - * - * 用法: 部署时将此内容追加到 admin.js 的 registerAdminRoutes 函数体内 - */ - -// ─── 熔断器状态查看 (admin) ─── -// routes['GET:/v1/admin/circuit-breaker'] = async (req, res) => { -// requireAdmin(req); -// const cbStatus = deps.circuitBreaker.getStatus(); -// const cbLog = deps.circuitBreaker.getTransitionLog(20); -// json(res, 200, { ok: true, breakers: cbStatus, recentTransitions: cbLog }); -// }; - -// ─── 熔断器重置 (admin) ─── -// routes['POST:/v1/admin/circuit-breaker/reset'] = async (req, res) => { -// requireAdmin(req); -// const body = await parseJsonBody(req); -// if (body.provider) { -// deps.circuitBreaker.reset(body.provider); -// json(res, 200, { ok: true, reset: body.provider }); -// } else { -// deps.circuitBreaker.resetAll(); -// json(res, 200, { ok: true, reset: 'all' }); -// } -// }; - -module.exports = function patchAdminRoutes(routes, deps) { - const { json, parseJsonBody, requireAdmin } = deps; - - routes['GET:/v1/admin/circuit-breaker'] = async (req, res) => { - requireAdmin(req); - const cbStatus = deps.circuitBreaker.getStatus(); - const cbLog = deps.circuitBreaker.getTransitionLog ? deps.circuitBreaker.getTransitionLog(20) : []; - json(res, 200, { ok: true, breakers: cbStatus, recentTransitions: cbLog }); - }; - - routes['POST:/v1/admin/circuit-breaker/reset'] = async (req, res) => { - requireAdmin(req); - const body = await parseJsonBody(req); - if (body.provider) { - deps.circuitBreaker.reset(body.provider); - json(res, 200, { ok: true, reset: body.provider }); - } else if (deps.circuitBreaker.resetAll) { - deps.circuitBreaker.resetAll(); - json(res, 200, { ok: true, reset: 'all' }); - } else { - json(res, 400, { error: '请指定 provider' }); - } - }; -}; diff --git a/setup-all.js b/setup-all.js deleted file mode 100644 index 3372ce2..0000000 --- a/setup-all.js +++ /dev/null @@ -1,616 +0,0 @@ -#!/usr/bin/env node -'use strict'; -/** - * Bookworm Portable - 全自动安装引擎 v3.0 - * @module setup-all - */ - -// W3: Node.js 版本检查 -if (parseInt(process.versions.node) < 14) { - console.error(' [!!] Node.js 版本过低 (' + process.version + '), 需要 14.0+'); - console.error(' 请更新: https://nodejs.org/'); - process.exit(1); -} - -const { execSync, spawnSync, spawn } = require('child_process'); -const fs = require('fs'); -const path = require('path'); -const readline = require('readline'); -const crypto = require('crypto'); -const os = require('os'); - -// ─── 配置 ─── -const HOME = os.homedir(); -const BOOT_DIR = path.join(HOME, 'bookworm-boot'); -const CLAUDE_DIR = path.join(HOME, '.claude'); -const GITEA_BOOT = 'https://code.letcareme.com/bookworm/bookworm-boot.git'; -const GITEA_CONFIG = 'https://code.letcareme.com/bookworm/bookworm-config.git'; -const NPM_MIRROR = 'https://registry.npmmirror.com'; -const SCRIPT_DIR = __dirname; - -// ─── 颜色输出 ─── -const c = { - reset: '\x1b[0m', bold: '\x1b[1m', - red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', - blue: '\x1b[34m', cyan: '\x1b[36m', dim: '\x1b[90m', -}; -function ok(msg) { console.log(` ${c.green}[OK]${c.reset} ${msg}`); } -function warn(msg) { console.log(` ${c.yellow}[!]${c.reset} ${msg}`); } -function fail(msg) { console.log(` ${c.red}[!!]${c.reset} ${msg}`); } -function info(msg) { console.log(` ${c.dim}[..]${c.reset} ${msg}`); } -function step(n, total, msg) { - console.log(`\n ${c.bold}[${n}/${total}]${c.reset} ${c.cyan}${msg}${c.reset}`); -} - -// ─── 工具函数 ─── -function hasCmd(cmd) { - if (!/^[a-zA-Z0-9._-]+$/.test(cmd)) return false; // B3: 防命令注入 - try { - execSync(`where ${cmd}`, { stdio: 'pipe' }); - return true; - } catch { return false; } -} - -function run(cmd, opts = {}) { - try { - return execSync(cmd, { - stdio: opts.silent ? 'pipe' : 'inherit', - encoding: 'utf8', - timeout: opts.timeout || 300000, - env: { ...process.env, ...opts.env }, - ...opts, - }); - } catch (e) { - if (opts.ignoreError) return ''; - throw e; - } -} - -function runSilent(cmd) { - try { - return execSync(cmd, { stdio: 'pipe', encoding: 'utf8', timeout: 60000 }); - } catch { return ''; } -} - -function wingetInstall(id, name) { - if (!hasCmd('winget')) { - warn(`winget 不可用, 请手动安装 ${name}`); - return false; - } - info(`通过 winget 安装 ${name}...`); - try { - run(`winget install ${id} --accept-source-agreements --accept-package-agreements --silent`, { timeout: 600000 }); - refreshPath(); - ok(`${name} 安装成功`); - return true; - } catch (e) { - fail(`${name} 安装失败: ${e.message}`); - return false; - } -} - -function refreshPath() { - try { - const sysPath = runSilent('reg query "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v Path') - .match(/REG_\w+\s+(.+)/)?.[1] || ''; - const usrPath = runSilent('reg query "HKCU\\Environment" /v Path') - .match(/REG_\w+\s+(.+)/)?.[1] || ''; - const extra = [ - 'C:\\Program Files\\nodejs', - 'C:\\Program Files\\Git\\cmd', - 'C:\\Program Files\\Git\\usr\\bin', - 'C:\\Program Files\\PowerShell\\7', - path.join(HOME, 'AppData\\Local\\Microsoft\\WinGet\\Packages'), - path.join(HOME, 'AppData\\Roaming\\npm'), - ].join(';'); - // I2: 动态扫描 Python 安装路径 - const pyBase = path.join(HOME, 'AppData', 'Local', 'Programs', 'Python'); - let pyPaths = ''; - try { - if (fs.existsSync(pyBase)) { - for (const d of fs.readdirSync(pyBase)) { - pyPaths += ';' + path.join(pyBase, d) + ';' + path.join(pyBase, d, 'Scripts'); - } - } - } catch {} - process.env.PATH = `${sysPath};${usrPath};${extra}${pyPaths}`; - } catch {} -} - -function askPassword(prompt) { - return new Promise((resolve) => { - process.stdout.write(prompt); - // Windows: 用 PowerShell 隐藏输入 - try { - const result = execSync( - 'powershell -Command "[Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR((Read-Host -AsSecureString)))"', - { stdio: ['inherit', 'pipe', 'pipe'], encoding: 'utf8', timeout: 120000 } - ).trim(); - resolve(result); - } catch { - // W5: 回退明文输入, 给出警告 - warn('PowerShell 不可用, 密码将以明文显示'); - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - rl.question('', (ans) => { rl.close(); resolve(ans.trim()); }); - } - }); -} - -// ─── 加密/解密 (与 crypto-helper.js 同格式) ─── -function decryptSecrets(filePath, password) { - const data = fs.readFileSync(filePath); - const magic = data.slice(0, 6).toString(); - if (magic !== 'BWENC1') throw new Error('WRONG_FORMAT'); - const salt = data.slice(6, 22); - const encrypted = data.slice(22); - const derived = crypto.pbkdf2Sync(password, salt, 600000, 48, 'sha256'); - const key = derived.slice(0, 32); - const iv = derived.slice(32, 48); - const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); - try { - return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8'); - } catch { - throw new Error('WRONG_PASSWORD'); - } -} - -function escVbs(s) { return s.replace(/"/g, '""'); } // B4: 防 VBS 注入 - -function createShortcut(name, target, workDir) { - const vbs = path.join(os.tmpdir(), `bw_sc_${Date.now()}.vbs`); - const desktop = path.join(HOME, 'Desktop'); - const lnkPath = escVbs(path.join(desktop, name + '.lnk')); - fs.writeFileSync(vbs, `Set ws = CreateObject("WScript.Shell") -Set sc = ws.CreateShortcut("${lnkPath}") -sc.TargetPath = "${escVbs(target)}" -sc.WorkingDirectory = "${escVbs(workDir)}" -sc.Description = "Bookworm Smart Assistant" -sc.Save -`); - try { execSync(`cscript //nologo "${vbs}"`, { stdio: 'pipe' }); return true; } - catch { return false; } - finally { try { fs.unlinkSync(vbs); } catch {} } -} - -// W8: 动态检测 Git Bash 路径 -function findGitBash() { - const candidates = ['C:\\Program Files\\Git\\bin\\bash.exe', 'C:\\Program Files (x86)\\Git\\bin\\bash.exe']; - try { - const gitPath = runSilent('where git').trim().split('\n')[0]; - if (gitPath) candidates.unshift(path.join(path.dirname(path.dirname(gitPath)), 'bin', 'bash.exe')); - } catch {} - return candidates.find(p => fs.existsSync(p)) || candidates[0]; -} - -// ─── Banner ─── -function banner() { - console.log(` - ${c.cyan}╔══════════════════════════════════════════════════╗ - ║ ____ _ ║ - ║ | __ ) ___ ___ | | ____ _____ _ __ ___ ║ - ║ | _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__| ║ - ║ | |_) | (_) | (_) | < \\ V V / (_) | | ║ - ║ |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| ║ - ║ ║ - ║ ${c.bold}全自动安装引擎 v3.0${c.cyan} ║ - ║ 双击即装: Node + Git + Python + PS7 + Claude + MCP║ - ╚══════════════════════════════════════════════════╝${c.reset} - `); -} - -// ═══════════════════════════════════════════ -// 主流程 -// ═══════════════════════════════════════════ -async function main() { - // ─── 启动模式: 已安装过 → 静默更新 + 直接启动 ─── - const isInstalled = hasCmd('claude') && fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md')); - const startOnly = process.argv.includes('--start') || process.argv.includes('-s'); - - if (isInstalled && startOnly) { - return quickStart(); - } - - banner(); - const TOTAL = 9; - let errors = 0; - - // ─── 1. Git ─── - step(1, TOTAL, '安装 Git'); - if (hasCmd('git')) { - ok('Git 已安装'); - } else { - if (!wingetInstall('Git.Git', 'Git')) errors++; - } - - // ─── 2. Python ─── - step(2, TOTAL, '安装 Python'); - if (hasCmd('python') || hasCmd('python3') || hasCmd('py')) { - ok('Python 已安装'); - } else { - if (!wingetInstall('Python.Python.3.12', 'Python 3.12')) errors++; - refreshPath(); - } - - // ─── 3. PowerShell 7 ─── - step(3, TOTAL, '安装 PowerShell 7'); - if (hasCmd('pwsh')) { - ok('PowerShell 7 已安装'); - } else { - if (wingetInstall('Microsoft.PowerShell', 'PowerShell 7')) { - // 设 PS7 为 Windows Terminal 默认配置文件 - try { - const wtSettings = path.join(HOME, 'AppData', 'Local', 'Packages', - 'Microsoft.WindowsTerminal_8wekyb3d8bbwe', 'LocalState', 'settings.json'); - if (fs.existsSync(wtSettings)) { - let wt = JSON.parse(fs.readFileSync(wtSettings, 'utf8')); - // 找到 PS7 的 profile GUID - const ps7Profile = (wt.profiles?.list || []).find(p => - p.source === 'Windows.Terminal.PowershellCore' || (p.name || '').includes('PowerShell 7') - ); - if (ps7Profile && ps7Profile.guid) { - wt.defaultProfile = ps7Profile.guid; - fs.writeFileSync(wtSettings, JSON.stringify(wt, null, 4)); - ok('PS7 已设为 Windows Terminal 默认终端'); - } - } - } catch { warn('Windows Terminal 默认配置未修改 - 不影响使用'); } - } else { - errors++; - } - } - - // ─── 4. Claude Code ─── - step(4, TOTAL, '安装 Claude Code'); - // I3: 用 --registry 参数, 不污染全局 .npmrc - if (hasCmd('claude')) { - ok('Claude Code 已安装'); - } else { - info('通过 npm 安装 Claude Code - 淘宝镜像加速...'); - try { - run(`npm i -g @anthropic-ai/claude-code --registry ${NPM_MIRROR}`, { timeout: 600000 }); - ok('Claude Code 安装成功'); - } catch { - fail('Claude Code 安装失败'); - errors++; - } - } - - // ─── 5. 克隆 Bookworm 配置 ─── - step(5, TOTAL, '同步 Bookworm 配置'); - // 设置 git credential helper - run('git config --global credential.helper manager', { ignoreError: true, silent: true }); - - // 克隆 bookworm-boot - if (fs.existsSync(path.join(BOOT_DIR, '.git'))) { - info('bookworm-boot 已存在, 更新...'); - run('git pull', { cwd: BOOT_DIR, ignoreError: true }); - } else { - info('首次下载 bookworm-boot (需输入 Gitea 用户名密码)...'); - try { - run(`git clone "${GITEA_BOOT}" "${BOOT_DIR}"`); - ok('bookworm-boot 克隆成功'); - } catch { - fail('bookworm-boot 克隆失败 - 检查网络和 Gitea 凭证'); - errors++; - } - } - - // 克隆 bookworm-config → ~/.claude - if (fs.existsSync(path.join(CLAUDE_DIR, '.git', 'config'))) { - info('.claude 配置已存在, 更新...'); - try { - const stashOut = run('git stash', { cwd: CLAUDE_DIR, ignoreError: true, silent: true }) || ''; - run('git pull --rebase', { cwd: CLAUDE_DIR, ignoreError: true, silent: true }); - if (stashOut.includes('Saved working directory')) { - run('git stash pop', { cwd: CLAUDE_DIR, ignoreError: true, silent: true }); - } - } catch {} - } else if (!fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md'))) { - info('首次下载 .claude 配置...'); - // 备份现有 - if (fs.existsSync(CLAUDE_DIR)) { - const backup = CLAUDE_DIR + '.bak.' + Date.now(); - fs.renameSync(CLAUDE_DIR, backup); - ok(`现有 .claude 已备份到 ${path.basename(backup)}`); - } - try { - run(`git clone --depth 1 "${GITEA_CONFIG}" "${CLAUDE_DIR}"`); - ok('.claude 配置克隆成功'); - } catch { - fail('.claude 配置克隆失败'); - errors++; - } - } else { - ok('.claude 配置已存在'); - } - - // 确保本地目录 - for (const d of ['debug', 'sessions', 'cache', 'backups', 'memory', 'projects']) { - const p = path.join(CLAUDE_DIR, d); - if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); - } - - // ─── 6. 凭证解密 ─── - step(6, TOTAL, '解密凭证'); - const secretsFile = path.join(BOOT_DIR, 'secrets.enc'); - if (!fs.existsSync(secretsFile)) { - warn('secrets.enc 不存在, 跳过凭证解密'); - } else if (process.env.ANTHROPIC_API_KEY) { - ok('API Key 已设置 (缓存有效)'); - } else { - let decrypted = false; - for (let attempt = 1; attempt <= 3; attempt++) { - const label = attempt > 1 - ? ` 重新输入主密码 (第 ${attempt}/3 次): ` - : ' 输入主密码解密凭证: '; - const password = await askPassword(label); - try { - const text = decryptSecrets(secretsFile, password); - // 注入环境变量 (B5: 不打印 key 名称, 防截屏泄露) - let injectedCount = 0; - for (const line of text.split('\n')) { - const trimmed = line.trim(); - if (!trimmed || !trimmed.includes('=')) continue; - const eqIdx = trimmed.indexOf('='); - const key = trimmed.slice(0, eqIdx).trim(); - const val = trimmed.slice(eqIdx + 1).trim(); - if (key && val) { process.env[key] = val; injectedCount++; } - } - ok(`已注入 ${injectedCount} 个环境变量`); - decrypted = true; - break; - } catch (e) { - if (e.message === 'WRONG_PASSWORD') { - const remaining = 3 - attempt; - if (remaining > 0) fail(`密码错误, 剩余重试: ${remaining} 次`); - } else if (e.message === 'WRONG_FORMAT') { - fail('secrets.enc 格式不兼容, 请联系管理员'); - break; - } - } - } - if (!decrypted) { - fail('凭证解密失败 — Claude Code 将以登录模式启动'); - errors++; - } - } - - // 渲染 settings.json - const templateFile = path.join(CLAUDE_DIR, 'settings.template.json'); - const settingsFile = path.join(CLAUDE_DIR, 'settings.json'); - if (fs.existsSync(templateFile)) { - let tpl = fs.readFileSync(templateFile, 'utf8'); - const claudeRoot = CLAUDE_DIR.replace(/\\/g, '/'); - tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, claudeRoot); - tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); // B6: 统一正斜杠 - fs.writeFileSync(settingsFile, tpl); - ok('settings.json 已渲染'); - } - // 渲染 settings.local.template.json - const localTpl = path.join(CLAUDE_DIR, 'settings.local.template.json'); - const localSet = path.join(CLAUDE_DIR, 'settings.local.json'); - if (fs.existsSync(localTpl)) { - let tpl = fs.readFileSync(localTpl, 'utf8'); - tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/')); - tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); // B6: 统一正斜杠 - tpl = tpl.replace(/\{\{USERNAME\}\}/g, os.userInfo().username); - fs.writeFileSync(localSet, tpl); - ok('settings.local.json 已渲染'); - } - - // ─── 7. MCP + hooks 依赖 ─── - step(7, TOTAL, 'MCP 与 hooks 依赖'); - - // MCP 配置写入 ~/.claude.json (Claude Code 的全局 MCP 存储位置) - const claudeJson = path.join(HOME, '.claude.json'); - try { - let globalCfg = {}; - if (fs.existsSync(claudeJson)) { - globalCfg = JSON.parse(fs.readFileSync(claudeJson, 'utf8')); - } - - // 基础 MCP 列表 (npx 方式, 无需预装, 首次调用自动下载) - const baseMcps = { - 'context7': { - command: 'npx.cmd', args: ['-y', '@upstash/context7-mcp@latest'], type: 'stdio' - }, - 'sequential-thinking': { - command: 'npx.cmd', args: ['-y', '@modelcontextprotocol/server-sequential-thinking@latest'], type: 'stdio' - }, - 'playwright': { - command: 'npx.cmd', args: ['-y', '@playwright/mcp@latest', '--headless'], type: 'stdio' - }, - 'firecrawl': { - command: 'npx.cmd', args: ['-y', 'firecrawl-mcp'], type: 'stdio', - env: { FIRECRAWL_API_KEY: '${FIRECRAWL_API_KEY}' } - }, - 'github': { - command: 'npx.cmd', args: ['-y', '@modelcontextprotocol/server-github'], type: 'stdio', - env: { GITHUB_PERSONAL_ACCESS_TOKEN: '${GITHUB_PERSONAL_ACCESS_TOKEN}' } - }, - 'linear': { type: 'http', url: 'https://mcp.linear.app/mcp' }, - 'figma': { type: 'http', url: 'https://mcp.figma.com/mcp' }, - 'supabase': { type: 'http', url: 'https://mcp.supabase.com/mcp?project_ref=oepmihbtoylosbsxlmfo' }, - }; - - // 合并: 不覆盖用户已有配置 - if (!globalCfg.mcpServers) globalCfg.mcpServers = {}; - let added = 0; - for (const [name, cfg] of Object.entries(baseMcps)) { - if (!globalCfg.mcpServers[name]) { - globalCfg.mcpServers[name] = cfg; - added++; - } - } - - fs.writeFileSync(claudeJson, JSON.stringify(globalCfg, null, 2)); - ok(`MCP 配置已写入 ~/.claude.json (新增 ${added} 个, 总计 ${Object.keys(globalCfg.mcpServers).length} 个)`); - } catch (e) { - warn('MCP 配置写入失败: ' + e.message); - } - - // npm install in .claude for hooks - if (fs.existsSync(path.join(CLAUDE_DIR, 'package.json'))) { - info('安装 hooks 依赖...'); - run(`npm install --omit=dev --registry ${NPM_MIRROR}`, { cwd: CLAUDE_DIR, ignoreError: true, silent: true }); - ok('hooks 依赖已安装'); - } - - // Python MCP 依赖 - const pyCmd = hasCmd('python') ? 'python' : (hasCmd('python3') ? 'python3' : (hasCmd('py') ? 'py' : null)); - if (pyCmd) { - info('安装 Python MCP 依赖...'); - run(`${pyCmd} -m pip install askui scrapling --quiet`, { ignoreError: true, silent: true }); - ok('Python MCP 依赖已安装'); - } - - // ─── 8. 桌面快捷方式 ─── - step(8, TOTAL, '创建桌面快捷方式'); - - // 启动脚本: 用 pwsh/powershell 运行 install.ps1 - const launchBat = path.join(BOOT_DIR, '启动Bookworm.bat'); - if (fs.existsSync(launchBat)) { - if (createShortcut('Bookworm', launchBat, BOOT_DIR)) ok('Bookworm 快捷方式'); - } else { - // 回退: 直接创建 claude 启动快捷方式 - const claudePath = runSilent('where claude').trim().split('\n')[0]; - if (claudePath) { - if (createShortcut('Bookworm', claudePath, HOME)) ok('Bookworm 快捷方式'); - } - } - - const updateBat = path.join(BOOT_DIR, '更新并启动Bookworm.bat'); - if (fs.existsSync(updateBat)) { - if (createShortcut('更新Bookworm', updateBat, BOOT_DIR)) ok('更新Bookworm 快捷方式'); - } - - // ─── 9. 完成 ─── - step(9, TOTAL, '安装完成'); - - console.log(` - ${c.green}╔══════════════════════════════════════════════════╗ - ║ ║ - ║ 安装完成! ║ - ║ ║ - ║ [v] Node.js [v] Git [v] Python ║ - ║ [v] PS7 [v] Claude [v] MCP ║ - ║ [v] Bookworm - 92 Skills / 18 Agents ║ - ║ ║ - ║ 桌面快捷方式: Bookworm / 更新Bookworm ║ - ║ ║ - ╚══════════════════════════════════════════════════╝${c.reset} - `); - - if (errors > 0) { - warn(`安装过程中有 ${errors} 个警告, 请查看上方日志`); - } - - // 打开使用教程 - const guide = path.join(BOOT_DIR, 'guide.html'); - if (fs.existsSync(guide)) { - try { execSync(`start "" "${guide}"`, { stdio: 'pipe' }); } catch {} - } - - // 询问是否启动 - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - rl.question('\n 按回车启动 Bookworm (或输入 n 退出): ', (ans) => { - rl.close(); - if (ans.trim().toLowerCase() === 'n') return; - - console.log(`\n ${c.cyan}正在启动 Claude Code...${c.reset}\n`); - // 设置必要环境变量 - const env = { ...process.env }; - // W10: 追加而非覆盖用户已有 NO_PROXY - const existingNoProxy = process.env.NO_PROXY || process.env.no_proxy || ''; - const bwDomains = 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1'; - env.NO_PROXY = existingNoProxy ? `${existingNoProxy},${bwDomains}` : bwDomains; - env.CLAUDE_CODE_GIT_BASH_PATH = findGitBash(); - - const child = spawn('claude', [], { - stdio: 'inherit', - env, - cwd: HOME, - shell: true, - }); - child.on('exit', () => process.exit(0)); - }); -} - -// ═══════════════════════════════════════════ -// 快速启动模式: 静默更新 + 直接启动 (已安装过的机器) -// ═══════════════════════════════════════════ -async function quickStart() { - console.log(` ${c.cyan}Bookworm 快速启动${c.reset} — 检查更新中...`); - - let updated = false; - - // 1. 静默更新 bookworm-boot - if (fs.existsSync(path.join(BOOT_DIR, '.git'))) { - try { - const before = runSilent(`git -C "${BOOT_DIR}" rev-parse HEAD`).trim(); - run(`git -C "${BOOT_DIR}" pull --ff-only`, { ignoreError: true, silent: true, timeout: 15000 }); - const after = runSilent(`git -C "${BOOT_DIR}" rev-parse HEAD`).trim(); - if (before !== after) { ok('bookworm-boot 已更新'); updated = true; } - } catch {} - } - - // 2. 静默更新 bookworm-config (.claude) - if (fs.existsSync(path.join(CLAUDE_DIR, '.git', 'config'))) { - try { - const before = runSilent(`git -C "${CLAUDE_DIR}" rev-parse HEAD`).trim(); - const stashOut = run(`git -C "${CLAUDE_DIR}" stash`, { ignoreError: true, silent: true }) || ''; - run(`git -C "${CLAUDE_DIR}" pull --rebase`, { ignoreError: true, silent: true, timeout: 15000 }); - if (stashOut.includes('Saved working directory')) { - run(`git -C "${CLAUDE_DIR}" stash pop`, { ignoreError: true, silent: true }); - } - const after = runSilent(`git -C "${CLAUDE_DIR}" rev-parse HEAD`).trim(); - if (before !== after) { ok('.claude 配置已更新'); updated = true; } - } catch {} - } - - // 3. 更新后重新渲染模板 - if (updated) { - const templateFile = path.join(CLAUDE_DIR, 'settings.template.json'); - const settingsFile = path.join(CLAUDE_DIR, 'settings.json'); - if (fs.existsSync(templateFile)) { - let tpl = fs.readFileSync(templateFile, 'utf8'); - tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/')); - tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); - fs.writeFileSync(settingsFile, tpl); - } - const localTpl = path.join(CLAUDE_DIR, 'settings.local.template.json'); - const localSet = path.join(CLAUDE_DIR, 'settings.local.json'); - if (fs.existsSync(localTpl)) { - let tpl = fs.readFileSync(localTpl, 'utf8'); - tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/')); - tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); - tpl = tpl.replace(/\{\{USERNAME\}\}/g, os.userInfo().username); - fs.writeFileSync(localSet, tpl); - } - ok('配置模板已重新渲染'); - - // hooks 依赖更新 - if (fs.existsSync(path.join(CLAUDE_DIR, 'package.json'))) { - run(`npm install --omit=dev --registry ${NPM_MIRROR}`, { cwd: CLAUDE_DIR, ignoreError: true, silent: true }); - } - } - - if (!updated) ok('已是最新版本'); - - // 4. 启动 Claude Code - console.log(`\n ${c.cyan}启动 Claude Code...${c.reset}\n`); - const env = { ...process.env }; - const existingNoProxy = process.env.NO_PROXY || process.env.no_proxy || ''; - const bwDomains = 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1'; - env.NO_PROXY = existingNoProxy ? `${existingNoProxy},${bwDomains}` : bwDomains; - env.CLAUDE_CODE_GIT_BASH_PATH = findGitBash(); - - const child = spawn('claude', [], { stdio: 'inherit', env, cwd: HOME, shell: true }); - child.on('exit', () => process.exit(0)); -} - -main().catch(e => { - fail(`安装引擎异常: ${e.message}`); - console.error(e.stack); - process.exit(1); -}); diff --git a/tools/legacy-gen-launcher-bats.ps1.archive b/tools/legacy-gen-launcher-bats.ps1.archive deleted file mode 100644 index bafa8b8..0000000 --- a/tools/legacy-gen-launcher-bats.ps1.archive +++ /dev/null @@ -1,218 +0,0 @@ -# Bookworm Portable 启动器 bat 生成工具 (v3.0.6) -# 用途: 从单一明文 PowerShell 脚本生成两个 bat, 避免手工同步 Base64 字符串不一致 -# 用法: pwsh -NoProfile -File tools/gen-launcher-bats.ps1 -# 输出: 启动Bookworm.bat + 更新并启动Bookworm.bat (覆盖写入) - -$ErrorActionPreference = "Stop" -$repoRoot = Split-Path -Parent $PSScriptRoot -$launchBat = Join-Path $repoRoot "启动Bookworm.bat" -$updateBat = Join-Path $repoRoot "更新并启动Bookworm.bat" - -# ─── 明文: 三层 PATH 修复 + DPAPI 加载 + claude 诊断 + 启动 ───────── -# v3.0.9: 增加 npm config get prefix 动态查询, 兼容 nvm/fnm/Program Files 等非标准 npm 位置 -$plainScript = @' -Add-Type -AssemblyName System.Security -# 层 1: Machine + User env PATH (标准 Windows 环境变量) -$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User') -# 层 2: npm config get prefix (真实 npm 全局目录, 兼容 nvm/fnm/标准安装/Program Files) -try { - $npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim() - if ($npmPrefix -and (Test-Path $npmPrefix) -and ($env:Path -notlike "*$npmPrefix*")) { - $env:Path = "$npmPrefix;$env:Path" - } -} catch {} -# 层 3: 常见 npm global 硬编码兜底 (npm 本身不在 PATH 时无法 query) -$npmCandidates = @( - "$env:APPDATA\npm", - "$env:ProgramFiles\nodejs", - "${env:ProgramFiles(x86)}\nodejs", - "$env:LOCALAPPDATA\npm" -) -foreach ($p in $npmCandidates) { - if (-not (Test-Path $p)) { continue } - $hasClaude = (Test-Path (Join-Path $p 'claude.ps1')) -or (Test-Path (Join-Path $p 'claude.cmd')) -or (Test-Path (Join-Path $p 'claude')) - if ($hasClaude -and ($env:Path -notlike "*$p*")) { - $env:Path = "$p;$env:Path" - } -} -# DPAPI 加载缓存凭证 -$r = 'HKCU:\Software\Bookworm\CachedEnv' -try { - (Get-ItemProperty $r -EA Stop).PSObject.Properties | Where-Object { $_.Name -match '^[A-Z_]+$' } | ForEach-Object { - $v = $_.Value - try { - $b = [Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($v), $null, [Security.Cryptography.DataProtectionScope]::CurrentUser) - $v = [Text.Encoding]::UTF8.GetString($b) - } catch {} - [Environment]::SetEnvironmentVariable($_.Name, $v, 'Process') - } -} catch {} -if (-not (Get-Command claude -ErrorAction SilentlyContinue)) { - Write-Host '' - Write-Host ' [!] claude 命令未找到 (已尝试 3 层 PATH 修复仍失败)' -ForegroundColor Red - Write-Host '' - Write-Host ' 诊断信息:' -ForegroundColor Yellow - Write-Host " npm prefix: $(try { (& npm config get prefix 2>$null) } catch { '(npm 不可用)' })" -ForegroundColor Gray - Write-Host ' PATH 片段 (npm/nodejs/pwsh/Git):' -ForegroundColor Gray - ($env:Path -split ';') | Where-Object { $_ -match 'npm|nodejs|pwsh|Git' } | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } - Write-Host '' - Write-Host ' 修复: 重新运行 Bookworm-Setup.exe (v3.0.9+) 即可自动补全' -ForegroundColor Green - Write-Host '' - Read-Host '按回车关闭' - return -} -& claude --dangerously-skip-permissions -'@ - -# ─── Base64-UTF-16LE 编码 ───────────────────────────────── -$bytes = [System.Text.Encoding]::Unicode.GetBytes($plainScript) -$enc = [Convert]::ToBase64String($bytes) - -# 健康检查 -if ($enc.Length -gt 7500) { throw "Base64 长度 $($enc.Length) 超 bat 变量安全上限 7500" } -$bad = $enc -replace '[A-Za-z0-9+/=]', '' -if ($bad) { throw "Base64 含非法字符: [$bad]" } - -Write-Host "[gen-launcher-bats] Base64 长度: $($enc.Length), 纯字符集检查 OK" -ForegroundColor Green - -# ─── bat 1: 启动Bookworm.bat ────────────────────────────── -$launch = @" -@echo off -chcp 65001 > nul -cd /d "%~dp0" - -:: 中转站在国内,不走代理 -set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1 -set no_proxy=%NO_PROXY% - -:: 静默自动更新 (bookworm-boot + .claude 配置, 失败不阻断启动) -echo [..] 检查更新... -git pull --rebase >nul 2>nul -git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul - -set USE_WT=0 -where wt >nul 2>nul && set USE_WT=1 - -set USE_PWSH7=0 -where pwsh >nul 2>nul && set USE_PWSH7=1 - -:: v3.0.6: Base64-UTF-16LE (PATH 重载 + DPAPI 凭证加载 + claude 诊断 + 启动) -:: 纯 A-Za-z0-9+/= 字符集, 避免 wt.exe 的 ';' 切 tab 误切 (修复 64856bc 症状一) -:: -d "%CD%" 无尾反斜杠, 避免 -d "%~dp0" 的转义引号 (修复 0c33109 症状二) -:: 重新生成: pwsh -NoProfile -File tools/gen-launcher-bats.ps1 -set ENC=$enc - -:: 优先路径: wt + pwsh7 -if %USE_WT% equ 1 if %USE_PWSH7% equ 1 ( - start "" wt new-tab --title "Bookworm Smart Assistant" -d "%CD%" -- pwsh -NoLogo -NoExit -EncodedCommand %ENC% - exit -) - -:: 路径 2: wt + powershell 5.1 -if %USE_WT% equ 1 if %USE_PWSH7% equ 0 ( - start "" wt new-tab --title "Bookworm Smart Assistant" -d "%CD%" -- powershell -NoLogo -ExecutionPolicy Bypass -NoExit -EncodedCommand %ENC% - exit -) - -:: 路径 3: conhost + pwsh7 (无 wt 就不会有 ; 切 tab 问题, 但仍用 Base64 统一) -if %USE_PWSH7% equ 1 ( - start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -EncodedCommand %ENC% - exit -) - -:: 路径 4: 回退 PowerShell 5.1 (最低保障, 交给 install.ps1 -StartOnly 处理) -title Bookworm Portable -echo. -echo [!] PowerShell 7 未安装, 使用 PowerShell 5.1 -echo. -powershell -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept -if %errorlevel% neq 0 ( - echo. - echo 启动失败,按任意键退出... - pause > nul -) -"@ - -# ─── bat 2: 更新并启动Bookworm.bat ─────────────────────── -$update = @" -@echo off -chcp 65001 > nul -cd /d "%~dp0" - -:: 中转站在国内,不走代理 -set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1 -set no_proxy=%NO_PROXY% - -:: 静默自动更新 (bookworm-boot + .claude 配置) -echo [..] 同步更新... -git pull --rebase >nul 2>nul -git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul - -:: v3.0.6: 同启动Bookworm.bat 的 Base64 (DPAPI + PATH 重载 + claude 启动) -set ENC=$enc - -:: 检测 pwsh7 可用性 -where pwsh >nul 2>nul -if %errorlevel% equ 0 ( - :: pwsh7: 先同步配置 (SkipLaunch 不启动 claude), 再用 -EncodedCommand 在新窗口启动 - pwsh -NoLogo -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept -SkipLaunch - start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -EncodedCommand %ENC% - exit -) - -:: 回退 PowerShell 5.1: 一次调用完成更新+加载凭证+启动 (消除双次调用) -title Bookworm Portable -powershell -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept -if %errorlevel% neq 0 ( - echo. - echo 启动失败,按任意键退出... - pause > nul -) -"@ - -# ─── 写入 ───────────────────────────────────────────────── -# bat 文件默认期望 GBK/ANSI, 但脚本顶部 chcp 65001 已切换到 UTF-8, 用无 BOM UTF-8 写入 -[System.IO.File]::WriteAllText($launchBat, $launch, [System.Text.UTF8Encoding]::new($false)) -[System.IO.File]::WriteAllText($updateBat, $update, [System.Text.UTF8Encoding]::new($false)) -Write-Host "[gen-launcher-bats] ✓ 启动Bookworm.bat ($((Get-Item $launchBat).Length) bytes)" -ForegroundColor Green -Write-Host "[gen-launcher-bats] ✓ 更新并启动Bookworm.bat ($((Get-Item $updateBat).Length) bytes)" -ForegroundColor Green - -# ─── Round-trip 验证 (v3.0.10: 除了 PARSE 还要 lint + 实跑不启动 claude) ──── -$decoded = [System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($enc)) -$err = $null -[void][System.Management.Automation.Language.Parser]::ParseInput($decoded, [ref]$null, [ref]$err) -if ($err) { throw "解码后脚本 PARSE ERR: $($err[0])" } -Write-Host "[gen-launcher-bats] ✓ PARSE OK ($($decoded.Length) chars)" -ForegroundColor Green - -# 实跑验证 (v3.0.10: 截断到 & claude 之前, 只跑 PATH 修复 + DPAPI 加载, 不启动 claude) -# 这能抓出 PARSE 通过但运行时报错的 bug (例如 -or 被当 Test-Path 参数) -$runnable = $decoded -replace '& claude --dangerously-skip-permissions', 'Write-Host "__BW_DRYRUN_OK__"' -$tmpPs1 = Join-Path $env:TEMP "bw-launcher-dryrun-$(Get-Random).ps1" -Set-Content -Path $tmpPs1 -Value $runnable -Encoding UTF8 -try { - $dryRunOutput = (& pwsh -NoProfile -ExecutionPolicy Bypass -File $tmpPs1 2>&1 | Out-String) - # 只抓真正的 PS 错误 (ErrorRecord / cannot be found / parameter name / 等) - $errorPatterns = @( - 'cannot be found that matches parameter name', - 'A parameter cannot be found', - 'CommandNotFoundException', - 'ParameterBindingException', - 'is not recognized as', - 'cannot find.*because it does not exist', - 'RuntimeException' - ) - $hasError = $false - foreach ($pat in $errorPatterns) { - if ($dryRunOutput -match $pat) { $hasError = $true; break } - } - # 必须看到 dry-run 成功标记才算通过 - $reachedEnd = $dryRunOutput -match '__BW_DRYRUN_OK__' - if ($hasError -or -not $reachedEnd) { - Write-Host "[gen-launcher-bats] ✗ 实跑验证失败:" -ForegroundColor Red - Write-Host $dryRunOutput -ForegroundColor DarkRed - throw "Base64 解码后脚本运行时错误 (hasError=$hasError, reachedEnd=$reachedEnd)" - } - Write-Host "[gen-launcher-bats] ✓ 实跑通过 (dry-run 到达 __BW_DRYRUN_OK__ 标记)" -ForegroundColor Green -} finally { - Remove-Item $tmpPs1 -Force -EA SilentlyContinue -} diff --git a/拓展/create-admin-powershell.bat b/拓展/create-admin-powershell.bat deleted file mode 100644 index a0f8146..0000000 --- a/拓展/create-admin-powershell.bat +++ /dev/null @@ -1,59 +0,0 @@ -@echo off -chcp 65001 >nul -setlocal enabledelayedexpansion - -echo. -echo ======================================================== -echo Create Admin PowerShell Shortcut for Claude Code -echo ======================================================== -echo. - -:: Check admin privileges -net session >nul 2>&1 -if %errorlevel% neq 0 ( - echo [!] Requesting administrator privileges... - powershell -Command "Start-Process '%~f0' -Verb RunAs" - exit /b -) - -:: Detect PowerShell path -set "PS_PATH=" -for %%p in ( - "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" - "%ProgramFiles%\PowerShell\7\pwsh.exe" - "%ProgramFiles(x86)%\PowerShell\7\pwsh.exe" -) do ( - if exist %%p ( - set "PS_PATH=%%p" - goto :ps_found - ) -) - -:ps_found -if "!PS_PATH!" == "" ( - echo [ERROR] PowerShell not found - pause - exit /b 1 -) - -echo [OK] PowerShell: !PS_PATH! - -:: Create admin shortcut -set "SHORTCUT_PATH=%USERPROFILE%\Desktop\Claude Code (Admin Terminal).lnk" - -powershell -NoProfile -Command "$WshShell = New-Object -ComObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%SHORTCUT_PATH%'); $Shortcut.TargetPath = '!PS_PATH!'; $Shortcut.Arguments = '-NoExit -Command \"Write-Host ''Claude Code Admin Terminal Ready'' -ForegroundColor Green; Write-Host ''Type: claude'' -ForegroundColor Yellow\"'; $Shortcut.WorkingDirectory = '%USERPROFILE%'; $Shortcut.Description = 'PowerShell with Admin Rights for Claude Code'; $Shortcut.Save(); $bytes = [System.IO.File]::ReadAllBytes('%SHORTCUT_PATH%'); $bytes[0x15] = $bytes[0x15] -bor 0x20; [System.IO.File]::WriteAllBytes('%SHORTCUT_PATH%', $bytes)" - -if exist "%SHORTCUT_PATH%" ( - echo. - echo [SUCCESS] Admin terminal shortcut created! - echo. - echo [Usage] - echo 1. Double-click "Claude Code (Admin Terminal)" on desktop - echo 2. Type: claude - echo 3. Claude Code will run with administrator privileges - echo. -) else ( - echo [ERROR] Failed to create shortcut -) - -pause