-
Notifications
You must be signed in to change notification settings - Fork 974
Expand file tree
/
Copy pathinstall.ps1
More file actions
201 lines (186 loc) · 8.01 KB
/
Copy pathinstall.ps1
File metadata and controls
201 lines (186 loc) · 8.01 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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
$repo = "iOfficeAI/OfficeCLI"
$asset = "officecli-win-x64.exe"
$binary = "officecli.exe"
# Mirror primary, github fallback. The mirror is exercised first so issues
# surface there fast; github is the safety net when CF or the mirror is
# unreachable.
$mirrorBase = "https://d.officecli.ai"
$githubReleaseBase = "https://github.com/$repo/releases/latest/download"
$githubRawBase = "https://raw.githubusercontent.com/$repo/main"
function Fetch-WithFallback {
param([string]$Primary, [string]$Fallback, [string]$OutFile)
try {
Invoke-WebRequest -Uri $Primary -OutFile $OutFile -TimeoutSec 30 -ErrorAction Stop
Write-Host " (via mirror)"
return $true
} catch {
Write-Host " mirror unreachable, falling back to github..."
try {
Invoke-WebRequest -Uri $Fallback -OutFile $OutFile -TimeoutSec 300 -ErrorAction Stop
return $true
} catch {
return $false
}
}
}
# Resolve-Version
# Discover the latest release tag (vX.Y.Z) by following the /releases/latest
# redirect and reading the final tag URL. Mirror first, github fallback.
# Returns the tag on success, $null on failure. This lets us download from the
# IMMUTABLE versioned path instead of the mutable /releases/latest/download/
# path — see download section below.
function Resolve-Version {
foreach ($base in @("$mirrorBase/releases/latest", "https://github.com/$repo/releases/latest")) {
try {
$resp = Invoke-WebRequest -Uri $base -MaximumRedirection 5 -TimeoutSec 30 -ErrorAction Stop
$finalUrl = $resp.BaseResponse.ResponseUri.AbsoluteUri
if ($finalUrl -match '/releases/tag/(v[0-9]+\.[0-9]+\.[0-9]+)') {
return $matches[1]
}
} catch {
if ($_.Exception.Response -and $_.Exception.Response.ResponseUri) {
$finalUrl = $_.Exception.Response.ResponseUri.AbsoluteUri
if ($finalUrl -match '/releases/tag/(v[0-9]+\.[0-9]+\.[0-9]+)') {
return $matches[1]
}
}
}
}
return $null
}
$source = $null
# Resolve the latest tag up-front so we download from the IMMUTABLE versioned
# path (/releases/download/vX.Y.Z/asset) instead of the mutable
# /releases/latest/download/ path. The latter is CDN-cached for up to 4h, so
# right after a release it can serve the PREVIOUS binary together with a
# self-consistent stale SHA256SUMS — which passes checksum and installs an old
# version despite printing success. The versioned URL is pinned + immutable.
$version = Resolve-Version
if ($version) {
Write-Host "Latest version: $version"
$mirrorAssetBase = "$mirrorBase/releases/download/$version"
$githubAssetBase = "https://github.com/$repo/releases/download/$version"
} else {
Write-Host "Could not resolve latest version; falling back to 'latest' path."
$mirrorAssetBase = "$mirrorBase/releases/latest/download"
$githubAssetBase = $githubReleaseBase
}
# Step 1: Try downloading (mirror first, github fallback)
$tempFile = "$env:TEMP\$binary"
Write-Host "Downloading OfficeCLI..."
if (Fetch-WithFallback "$mirrorAssetBase/$asset" "$githubAssetBase/$asset" $tempFile) {
# Verify checksum if available
$checksumOk = $false
$checksumFile = "$env:TEMP\officecli-SHA256SUMS"
if (Fetch-WithFallback "$mirrorAssetBase/SHA256SUMS" "$githubAssetBase/SHA256SUMS" $checksumFile) {
$checksumContent = Get-Content $checksumFile
# Match the filename column EXACTLY (not a regex/substring): `-match` is
# an unanchored regex where `.`/`-` are metacharacters, so it could match
# the wrong manifest line (or several) and pick a wrong hash — failing an
# otherwise-valid update. Mirrors the C# self-updater's MatchChecksumManifest.
$expected = $null
foreach ($line in $checksumContent) {
$parts = ($line.Trim() -split '\s+')
if ($parts.Length -ge 2 -and $parts[1] -eq $asset) { $expected = $parts[0]; break }
}
if ($expected) {
$actual = (Get-FileHash -Path $tempFile -Algorithm SHA256).Hash.ToLower()
if ($expected -eq $actual) {
$checksumOk = $true
Write-Host "Checksum verified."
} else {
Write-Host "Checksum mismatch! Expected: $expected, Got: $actual"
Remove-Item -Force $tempFile, $checksumFile -ErrorAction SilentlyContinue
exit 1
}
}
Remove-Item -Force $checksumFile -ErrorAction SilentlyContinue
} else {
Write-Host "Checksum file not available, skipping verification."
}
$output = & $tempFile --version 2>&1
if ($LASTEXITCODE -eq 0) {
$source = $tempFile
Write-Host "Download verified."
} else {
Write-Host "Downloaded file is not a valid OfficeCLI binary."
Remove-Item -Force $tempFile -ErrorAction SilentlyContinue
}
} else {
Write-Host "Download failed."
}
# Step 2: Fallback to local files
if (-not $source) {
Write-Host "Looking for local binary..."
$candidates = @(".\$asset", ".\$binary", ".\bin\$asset", ".\bin\$binary", ".\bin\release\$asset", ".\bin\release\$binary")
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
$output = & $candidate --version 2>&1
if ($LASTEXITCODE -eq 0) {
$source = $candidate
Write-Host "Found valid binary at $candidate"
break
}
}
}
}
if (-not $source) {
Write-Host "Error: Could not find a valid OfficeCLI binary."
Write-Host "Download manually from: https://github.com/$repo/releases"
exit 1
}
# Step 3: Install
$existing = Get-Command $binary -ErrorAction SilentlyContinue
if ($existing) {
$installDir = Split-Path $existing.Source
Write-Host "Found existing installation at $($existing.Source), upgrading..."
} else {
$installDir = "$env:LOCALAPPDATA\OfficeCLI"
}
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
Copy-Item -Force $source "$installDir\$binary"
Remove-Item -Force $tempFile -ErrorAction SilentlyContinue
# Add to PATH if not already there
$currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($currentPath -notlike "*$installDir*") {
[Environment]::SetEnvironmentVariable("Path", "$currentPath;$installDir", "User")
Write-Host "Added $installDir to PATH (restart your terminal to take effect)."
}
# Step 4: Install AI agent skills (first install only)
$skillMarker = "$installDir\.officecli-skills-installed"
if (-not (Test-Path $skillMarker)) {
$skillTargets = @()
$tools = @{
"$env:USERPROFILE\.claude" = "Claude Code"
"$env:USERPROFILE\.copilot" = "GitHub Copilot"
"$env:USERPROFILE\.agents" = "Codex CLI"
"$env:USERPROFILE\.cursor" = "Cursor"
"$env:USERPROFILE\.windsurf" = "Windsurf"
"$env:USERPROFILE\.minimax" = "MiniMax CLI"
"$env:USERPROFILE\.openclaw" = "OpenClaw"
"$env:USERPROFILE\.nanobot\workspace" = "NanoBot"
"$env:USERPROFILE\.zeroclaw\workspace" = "ZeroClaw"
"$env:USERPROFILE\.hermes" = "Hermes Agent"
}
foreach ($dir in $tools.Keys) {
if (Test-Path $dir) {
$skillTargets += "$dir\skills\officecli"
Write-Host "$($tools[$dir]) detected."
}
}
if ($skillTargets.Count -gt 0) {
Write-Host "Downloading officecli skill..."
$tempSkill = "$env:TEMP\officecli-skill.md"
if (Fetch-WithFallback "$mirrorBase/SKILL.md" "$githubRawBase/SKILL.md" $tempSkill) {
foreach ($target in $skillTargets) {
New-Item -ItemType Directory -Force -Path $target | Out-Null
Copy-Item -Force $tempSkill "$target\SKILL.md"
Write-Host " Installed: $target\SKILL.md"
}
Remove-Item -Force $tempSkill -ErrorAction SilentlyContinue
}
}
New-Item -ItemType File -Force -Path $skillMarker | Out-Null
}
Write-Host "OfficeCLI installed successfully!"
Write-Host "Run 'officecli --help' to get started."