@adelie/server 0.5.8 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/types.d.ts +9 -2
- package/dist/api/types.js +1 -1
- package/dist/{chunk-6IXN3YH4.js → chunk-7ROH3XZA.js} +1 -1
- package/dist/chunk-7ROH3XZA.js.map +1 -0
- package/dist/index.js +40 -13
- package/dist/index.js.map +1 -1
- package/dist/install.ps1 +492 -492
- package/dist/install.sh +1 -1
- package/package.json +4 -4
- package/web-dist/assets/{addon-clipboard-Cr1IGZn2.js → addon-clipboard-D6ACHn2x.js} +1 -1
- package/web-dist/assets/{addon-fit-C_pn1ddy.js → addon-fit-BNT9upP3.js} +1 -1
- package/web-dist/assets/{addon-web-links-CqNDWe_S.js → addon-web-links-uRWSI5hx.js} +1 -1
- package/web-dist/assets/{highlighter-D222XDsD.js → highlighter-3eUkkT9M.js} +2 -2
- package/web-dist/assets/{highlighter-core-B2uV9rSe.js → highlighter-core-fZoV9Mwi.js} +1 -1
- package/web-dist/assets/index-DyoWh67L.css +1 -0
- package/web-dist/assets/index-SMg48wJB.js +1091 -0
- package/web-dist/assets/{xterm-C3by7jJ-.js → xterm-DJs_GVCC.js} +1 -1
- package/web-dist/index.html +2 -2
- package/dist/chunk-6IXN3YH4.js.map +0 -1
- package/web-dist/assets/index-Bi9AMLAl.js +0 -1090
- package/web-dist/assets/index-DiDPf1zX.css +0 -1
package/dist/install.ps1
CHANGED
|
@@ -1,492 +1,492 @@
|
|
|
1
|
-
# Adelie one-line installer for Windows.
|
|
2
|
-
#
|
|
3
|
-
# irm https://github.com/lmliheng/Adelie/releases/latest/download/install.ps1 | iex
|
|
4
|
-
#
|
|
5
|
-
# Options:
|
|
6
|
-
# $env:PENGUIN_VERSION = "vX.Y.Z" choose a version (same as -Version vX.Y.Z); a published Release
|
|
7
|
-
# installer defaults to its own version, an unstamped source copy to latest
|
|
8
|
-
# $env:PENGUIN_INSTALL_DIR = "<dir>" install dir; default $env:USERPROFILE\.adelie
|
|
9
|
-
# $env:PENGUIN_ARCHIVE = "<file>" install a local Release zip without network access (same as -ArchivePath)
|
|
10
|
-
# $env:PENGUIN_DOWNLOAD_SOURCE = "auto|github" choose the online source; default auto (GitHub Releases)
|
|
11
|
-
# $env:PENGUIN_DOWNLOAD_BASE_URL = "https://..." exact online asset directory selected by the stable forwarder
|
|
12
|
-
# $env:PENGUIN_DOWNLOAD_FALLBACK_BASE_URL = "https://..." fallback for PENGUIN_DOWNLOAD_BASE_URL
|
|
13
|
-
# -NoModifyPath do not add <install>\bin to the user Path; for a second installation
|
|
14
|
-
# beside the one the `penguin` command belongs to
|
|
15
|
-
#
|
|
16
|
-
# Each Release attaches exactly one Windows artifact: penguin-win32-x64.zip, a shallow installer
|
|
17
|
-
# bundle holding install.cmd, this script, the program payload (payload.zip) and the payload's
|
|
18
|
-
# checksum. Online installs download that bundle and verify it against its published .sha256;
|
|
19
|
-
# offline installs transfer the same single file, extract it once (the outer layer is flat, so
|
|
20
|
-
# no deep paths are created) and double-click install.cmd. Both paths verify the payload
|
|
21
|
-
# checksum sealed inside the bundle, then expand the payload straight into the short staging
|
|
22
|
-
# directory. Releases up to v0.1.5 shipped the program tree directly (top-level penguin\);
|
|
23
|
-
# such zips are still accepted, from -Version pins and -ArchivePath files alike.
|
|
24
|
-
#
|
|
25
|
-
# There is no -Universal on Windows: where the zip is unsuitable, install Node.js >= 24 and run
|
|
26
|
-
# `npm install -g @adelie/cli` instead.
|
|
27
|
-
#
|
|
28
|
-
# The data dir (%USERPROFILE%\.adelie\data by default) sits under the install home but is never touched by
|
|
29
|
-
# reinstall/upgrade (which only replace bin/lib/web/node/git). Upgrading = re-running this installer.
|
|
30
|
-
# The launcher exports the root it lives under, so an install made before the rename keeps its data.
|
|
31
|
-
#
|
|
32
|
-
# Docs: https://lmliheng.github.io/Adelie/docs/quickstart-cli
|
|
33
|
-
param(
|
|
34
|
-
[string]$Version = "",
|
|
35
|
-
[string]$InstallDir = "",
|
|
36
|
-
[string]$ArchivePath = "",
|
|
37
|
-
[switch]$NoModifyPath
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
$ErrorActionPreference = "Stop"
|
|
41
|
-
$ProgressPreference = "SilentlyContinue" # Invoke-WebRequest progress rendering slows downloads massively on PS 5.1
|
|
42
|
-
|
|
43
|
-
$Repo = "https://github.com/lmliheng/Adelie"
|
|
44
|
-
$GitHubReleaseRoot = "$Repo/releases/download"
|
|
45
|
-
$GitHubLatestBase = "$Repo/releases/latest/download"
|
|
46
|
-
$Asset = "penguin-win32-x64.zip"
|
|
47
|
-
$PayloadName = "payload.zip"
|
|
48
|
-
# The release workflow replaces this token with the immutable tag before publishing both the
|
|
49
|
-
# standalone installer and the copy sealed inside the Windows bundle.
|
|
50
|
-
$EmbeddedReleaseVersion = "
|
|
51
|
-
|
|
52
|
-
function Fail([string]$Message) {
|
|
53
|
-
# `throw` rather than `exit`: a caller that runs this installer as an in-memory script block
|
|
54
|
-
# (rather than as a file) would otherwise have `exit` terminate its whole PowerShell session.
|
|
55
|
-
# `throw` aborts cleanly in both file and script-block runs.
|
|
56
|
-
throw "error: $Message"
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# Lists a zip's top-level entry names without extracting (PS 5.1-safe; Expand-Archive itself
|
|
60
|
-
# uses the same .NET type). Used to tell an installer bundle (contains payload.zip) from a
|
|
61
|
-
# program archive (payload.zip itself, or a pre-0.1.6 release zip with a top-level penguin\).
|
|
62
|
-
function Get-ZipEntryNames([string]$ZipPath) {
|
|
63
|
-
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
64
|
-
$Zip = [IO.Compression.ZipFile]::OpenRead($ZipPath)
|
|
65
|
-
try {
|
|
66
|
-
return @($Zip.Entries | ForEach-Object { $_.FullName })
|
|
67
|
-
} finally {
|
|
68
|
-
$Zip.Dispose()
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
# Verifies a file against a sha256sum-format checksum file (`<hex> <filename>`; the first
|
|
73
|
-
# token is the hash). Checksums are never optional: every install path either downloads the
|
|
74
|
-
# published .sha256 or reads the one sealed inside the bundle.
|
|
75
|
-
function Assert-Sha256([string]$FilePath, [string]$ShaPath, [string]$Label) {
|
|
76
|
-
$Expected = ((Get-Content -LiteralPath $ShaPath -Raw).Trim() -split "\s+")[0]
|
|
77
|
-
if (-not $Expected) { Fail "checksum file is empty or malformed: $ShaPath" }
|
|
78
|
-
$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $FilePath).Hash
|
|
79
|
-
if ($Actual -ine $Expected) { Fail "checksum mismatch for $([IO.Path]::GetFileName($FilePath))." }
|
|
80
|
-
Write-Host "$Label checksum OK."
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function Assert-HttpsUrl([string]$Name, [string]$Value) {
|
|
84
|
-
try { $Uri = [Uri]$Value } catch { Fail "$Name is not a valid URL" }
|
|
85
|
-
if (-not $Uri.IsAbsoluteUri -or $Uri.Scheme -ne "https") {
|
|
86
|
-
Fail "$Name must be an absolute HTTPS URL"
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function Test-ReleaseTag([string]$Value) {
|
|
91
|
-
return $Value -match '^v[0-9A-Za-z][0-9A-Za-z._-]*$'
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function Get-DownloadSourceLabel([string]$BaseUrl) {
|
|
95
|
-
try { $HostName = ([Uri]$BaseUrl).Host } catch { return "configured mirror" }
|
|
96
|
-
if ($HostName -eq "github.com") { return "GitHub" }
|
|
97
|
-
return "configured mirror"
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function Get-ReleasePair(
|
|
101
|
-
[string]$BaseUrl,
|
|
102
|
-
[string]$ZipPath,
|
|
103
|
-
[string]$ShaPath
|
|
104
|
-
) {
|
|
105
|
-
$Label = Get-DownloadSourceLabel $BaseUrl
|
|
106
|
-
Write-Host "Downloading $Asset from $Label ..."
|
|
107
|
-
Remove-Item -LiteralPath $ZipPath, $ShaPath -Force -ErrorAction SilentlyContinue
|
|
108
|
-
try {
|
|
109
|
-
Invoke-WebRequest -Uri "$BaseUrl/$Asset" -OutFile $ZipPath -UseBasicParsing
|
|
110
|
-
Invoke-WebRequest -Uri "$BaseUrl/$Asset.sha256" -OutFile $ShaPath -UseBasicParsing
|
|
111
|
-
return $true
|
|
112
|
-
} catch {
|
|
113
|
-
Remove-Item -LiteralPath $ZipPath, $ShaPath -Force -ErrorAction SilentlyContinue
|
|
114
|
-
return $false
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function Restore-PreviousInstall(
|
|
119
|
-
[string]$InstallDir,
|
|
120
|
-
[string]$OldDir,
|
|
121
|
-
[string[]]$MovedOld,
|
|
122
|
-
[string[]]$MovedNew
|
|
123
|
-
) {
|
|
124
|
-
foreach ($d in @($MovedNew)) {
|
|
125
|
-
if (-not $d) { continue }
|
|
126
|
-
$Current = Join-Path $InstallDir $d
|
|
127
|
-
if (Test-Path -LiteralPath $Current) {
|
|
128
|
-
Remove-Item -LiteralPath $Current -Recurse -Force -ErrorAction Stop
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
foreach ($d in @($MovedOld)) {
|
|
132
|
-
if (-not $d) { continue }
|
|
133
|
-
$Previous = Join-Path $OldDir $d
|
|
134
|
-
if (Test-Path -LiteralPath $Previous) {
|
|
135
|
-
Move-Item -LiteralPath $Previous -Destination (Join-Path $InstallDir $d) -ErrorAction Stop
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
# --- Resolve options (parameters win over env vars, mirroring install.sh's --version) ---
|
|
141
|
-
if (-not $Version) { $Version = if ($env:PENGUIN_VERSION) { $env:PENGUIN_VERSION } else { "" } }
|
|
142
|
-
if (-not $InstallDir) {
|
|
143
|
-
$InstallDir = if ($env:PENGUIN_INSTALL_DIR) { $env:PENGUIN_INSTALL_DIR } else { Join-Path $env:USERPROFILE ".adelie" }
|
|
144
|
-
}
|
|
145
|
-
if (-not $ArchivePath) {
|
|
146
|
-
$ArchivePath = if ($env:PENGUIN_ARCHIVE) { $env:PENGUIN_ARCHIVE } else { "" }
|
|
147
|
-
}
|
|
148
|
-
$DownloadBaseUrl = if ($env:PENGUIN_DOWNLOAD_BASE_URL) {
|
|
149
|
-
$env:PENGUIN_DOWNLOAD_BASE_URL.TrimEnd('/')
|
|
150
|
-
} else {
|
|
151
|
-
""
|
|
152
|
-
}
|
|
153
|
-
$DownloadFallbackBaseUrl = if ($env:PENGUIN_DOWNLOAD_FALLBACK_BASE_URL) {
|
|
154
|
-
$env:PENGUIN_DOWNLOAD_FALLBACK_BASE_URL.TrimEnd('/')
|
|
155
|
-
} else {
|
|
156
|
-
""
|
|
157
|
-
}
|
|
158
|
-
if (-not $DownloadBaseUrl) { $DownloadFallbackBaseUrl = "" }
|
|
159
|
-
$SourceMode = if ($env:PENGUIN_DOWNLOAD_SOURCE) {
|
|
160
|
-
$env:PENGUIN_DOWNLOAD_SOURCE.ToLowerInvariant()
|
|
161
|
-
} else {
|
|
162
|
-
"auto"
|
|
163
|
-
}
|
|
164
|
-
# An extracted installer bundle keeps install.cmd, this script, payload.zip and its checksum
|
|
165
|
-
# together. `$PSScriptRoot` is empty for the documented `irm ... | iex` path, so online installs
|
|
166
|
-
# do not accidentally pick up an unrelated archive from the caller's current directory.
|
|
167
|
-
if (-not $ArchivePath -and $PSScriptRoot) {
|
|
168
|
-
$SiblingArchive = Join-Path $PSScriptRoot $PayloadName
|
|
169
|
-
if (Test-Path -LiteralPath $SiblingArchive -PathType Leaf) { $ArchivePath = $SiblingArchive }
|
|
170
|
-
}
|
|
171
|
-
if ($ArchivePath -and $Version) {
|
|
172
|
-
Fail "-ArchivePath/PENGUIN_ARCHIVE cannot be combined with -Version/PENGUIN_VERSION"
|
|
173
|
-
}
|
|
174
|
-
if ($Version -and -not (Test-ReleaseTag $Version)) {
|
|
175
|
-
Fail "invalid release version: $Version"
|
|
176
|
-
}
|
|
177
|
-
if ($SourceMode -notin @("auto", "github")) {
|
|
178
|
-
Fail "PENGUIN_DOWNLOAD_SOURCE must be auto or github"
|
|
179
|
-
}
|
|
180
|
-
$ResolvedReleaseVersion = if ($Version) {
|
|
181
|
-
$Version
|
|
182
|
-
} elseif (Test-ReleaseTag $EmbeddedReleaseVersion) {
|
|
183
|
-
$EmbeddedReleaseVersion
|
|
184
|
-
} else {
|
|
185
|
-
""
|
|
186
|
-
}
|
|
187
|
-
if ($DownloadBaseUrl) {
|
|
188
|
-
Assert-HttpsUrl "PENGUIN_DOWNLOAD_BASE_URL" $DownloadBaseUrl
|
|
189
|
-
}
|
|
190
|
-
if ($DownloadBaseUrl -and $DownloadFallbackBaseUrl) {
|
|
191
|
-
Assert-HttpsUrl "PENGUIN_DOWNLOAD_FALLBACK_BASE_URL" $DownloadFallbackBaseUrl
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
# --- Platform preconditions: 64-bit Windows; the only Windows package is x64 (ARM64 runs it emulated) ---
|
|
195
|
-
if (-not [Environment]::Is64BitOperatingSystem) {
|
|
196
|
-
Fail "32-bit Windows is not supported. Install Node.js >= 24 and use: npm install -g @adelie/cli"
|
|
197
|
-
}
|
|
198
|
-
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
|
|
199
|
-
Write-Host "note: no native ARM64 package yet; installing the x64 package (runs via emulation)."
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
# PowerShell 5.1 defaults to TLS 1.0 on older systems; GitHub requires TLS 1.2+.
|
|
203
|
-
try {
|
|
204
|
-
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
205
|
-
} catch {
|
|
206
|
-
# .NET builds where the enum is immutable already default to TLS 1.2+.
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
# --- Download. An explicit forwarder/configured URL wins. Otherwise a stamped Release installer
|
|
210
|
-
# uses its own immutable version; an unstamped source-tree installer falls back to the
|
|
211
|
-
# release GitHub currently marks latest. ---
|
|
212
|
-
$Tmp = Join-Path ([IO.Path]::GetTempPath()) "penguin-install-$PID"
|
|
213
|
-
if (Test-Path $Tmp) { Remove-Item -Recurse -Force $Tmp }
|
|
214
|
-
New-Item -ItemType Directory -Path $Tmp | Out-Null
|
|
215
|
-
# Pre-declare so the finally block can read them even when an early failure skipped the
|
|
216
|
-
# assignments (the user's session may run this under Set-StrictMode via the forwarder).
|
|
217
|
-
$Staging = $null
|
|
218
|
-
$OldDir = $null
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
$UsingLocalArchive = [bool]$ArchivePath
|
|
222
|
-
if ($UsingLocalArchive) {
|
|
223
|
-
try {
|
|
224
|
-
$ZipPath = (Resolve-Path -LiteralPath $ArchivePath -ErrorAction Stop).Path
|
|
225
|
-
} catch {
|
|
226
|
-
Fail "local archive not found: $ArchivePath"
|
|
227
|
-
}
|
|
228
|
-
$ArchiveName = [IO.Path]::GetFileName($ZipPath)
|
|
229
|
-
Write-Host "Using local archive $ZipPath ..."
|
|
230
|
-
} else {
|
|
231
|
-
$BaseUrl = ""
|
|
232
|
-
$FallbackBaseUrl = $DownloadFallbackBaseUrl
|
|
233
|
-
if ($DownloadBaseUrl) {
|
|
234
|
-
$BaseUrl = $DownloadBaseUrl
|
|
235
|
-
} elseif ($ResolvedReleaseVersion) {
|
|
236
|
-
$BaseUrl = "$GitHubReleaseRoot/$ResolvedReleaseVersion"
|
|
237
|
-
} else {
|
|
238
|
-
$BaseUrl = $GitHubLatestBase
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
# Online: download the canonical bundle; the published checksum is mandatory.
|
|
242
|
-
$ZipPath = Join-Path $Tmp $Asset
|
|
243
|
-
$ArchiveName = $Asset
|
|
244
|
-
$ShaPath = Join-Path $Tmp "$Asset.sha256"
|
|
245
|
-
if (-not (Get-ReleasePair $BaseUrl $ZipPath $ShaPath)) {
|
|
246
|
-
if ($FallbackBaseUrl -and $FallbackBaseUrl -ne $BaseUrl) {
|
|
247
|
-
Write-Host "Primary download source unavailable; trying $(Get-DownloadSourceLabel $FallbackBaseUrl) ..."
|
|
248
|
-
if (-not (Get-ReleasePair $FallbackBaseUrl $ZipPath $ShaPath)) {
|
|
249
|
-
Fail "download failed from both the primary source and its fallback. Check your network, then retry."
|
|
250
|
-
}
|
|
251
|
-
} else {
|
|
252
|
-
Fail "download failed from $(Get-DownloadSourceLabel $BaseUrl). Check the version tag and your network, then retry."
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
Assert-Sha256 $ZipPath $ShaPath "Bundle"
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
# --- Resolve the program payload. An installer bundle (contains payload.zip) is opened flat
|
|
259
|
-
# and its sealed payload checksum verified; a program archive (payload.zip itself, or a
|
|
260
|
-
# pre-0.1.6 release zip with a top-level penguin\) is used directly. ---
|
|
261
|
-
$ArchiveShape = if ((Get-ZipEntryNames $ZipPath) -contains $PayloadName) { "bundle" } else { "program" }
|
|
262
|
-
if ($ArchiveShape -eq "bundle") {
|
|
263
|
-
# A local bundle is self-verifying through its sealed payload checksum; when the published
|
|
264
|
-
# outer .sha256 was transferred alongside it, verify that layer too.
|
|
265
|
-
if ($UsingLocalArchive -and (Test-Path -LiteralPath "$ZipPath.sha256" -PathType Leaf)) {
|
|
266
|
-
Assert-Sha256 $ZipPath "$ZipPath.sha256" "Bundle"
|
|
267
|
-
}
|
|
268
|
-
$BundleDir = Join-Path $Tmp "bundle"
|
|
269
|
-
New-Item -ItemType Directory -Path $BundleDir | Out-Null
|
|
270
|
-
Write-Host "Opening installer bundle ..."
|
|
271
|
-
# The outer layer is flat (four files), so this extraction never creates deep paths.
|
|
272
|
-
Expand-Archive -LiteralPath $ZipPath -DestinationPath $BundleDir -Force
|
|
273
|
-
$ZipPath = Join-Path $BundleDir $PayloadName
|
|
274
|
-
if (-not (Test-Path -LiteralPath $ZipPath -PathType Leaf)) {
|
|
275
|
-
Fail "unexpected bundle layout: $PayloadName missing."
|
|
276
|
-
}
|
|
277
|
-
if (-not (Test-Path -LiteralPath "$ZipPath.sha256" -PathType Leaf)) {
|
|
278
|
-
Fail "unexpected bundle layout: $PayloadName.sha256 missing."
|
|
279
|
-
}
|
|
280
|
-
Assert-Sha256 $ZipPath "$ZipPath.sha256" "Payload"
|
|
281
|
-
} elseif ($UsingLocalArchive) {
|
|
282
|
-
# Program archive from disk: an adjacent checksum is required — its own, or the canonical
|
|
283
|
-
# asset checksum next to a renamed legacy file. (An online download was already verified
|
|
284
|
-
# against the published .sha256 above.)
|
|
285
|
-
$ShaPath = "$ZipPath.sha256"
|
|
286
|
-
if (-not (Test-Path -LiteralPath $ShaPath -PathType Leaf) -and
|
|
287
|
-
$ArchiveName -ine $Asset) {
|
|
288
|
-
$CanonicalShaPath = Join-Path ([IO.Path]::GetDirectoryName($ZipPath)) "$Asset.sha256"
|
|
289
|
-
if (Test-Path -LiteralPath $CanonicalShaPath -PathType Leaf) {
|
|
290
|
-
$ShaPath = $CanonicalShaPath
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
if (-not (Test-Path -LiteralPath $ShaPath -PathType Leaf)) {
|
|
294
|
-
Fail "offline checksum file not found: $ShaPath"
|
|
295
|
-
}
|
|
296
|
-
Assert-Sha256 $ZipPath $ShaPath "Payload"
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
# --- Extract into staging, then swap by same-volume renames. Keep the previous dirs in .old.$PID
|
|
300
|
-
# until the installed command runs successfully; any move or launch failure restores them.
|
|
301
|
-
# The data dir (%USERPROFILE%\.adelie\data by default) is never part of the swap. ---
|
|
302
|
-
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
|
303
|
-
$Staging = Join-Path $InstallDir ".staging.$PID"
|
|
304
|
-
$OldDir = Join-Path $InstallDir ".old.$PID"
|
|
305
|
-
if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging }
|
|
306
|
-
if (Test-Path $OldDir) { Remove-Item -Recurse -Force $OldDir }
|
|
307
|
-
New-Item -ItemType Directory -Path $Staging | Out-Null
|
|
308
|
-
|
|
309
|
-
Write-Host "Extracting ..."
|
|
310
|
-
Expand-Archive -LiteralPath $ZipPath -DestinationPath $Staging -Force
|
|
311
|
-
$NewRoot = Join-Path $Staging "penguin"
|
|
312
|
-
if (-not (Test-Path $NewRoot)) { Fail "unexpected archive layout: top-level penguin\ missing." }
|
|
313
|
-
if (-not (Test-Path (Join-Path $NewRoot "bin"))) { Fail "unexpected archive layout: penguin\bin missing." }
|
|
314
|
-
$ManifestPath = Join-Path $NewRoot "package-manifest.json"
|
|
315
|
-
if (Test-Path -LiteralPath $ManifestPath -PathType Leaf) {
|
|
316
|
-
try {
|
|
317
|
-
$PackageManifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
|
|
318
|
-
} catch {
|
|
319
|
-
Fail "package manifest is malformed: $($_.Exception.Message)"
|
|
320
|
-
}
|
|
321
|
-
$TargetProperty = $PackageManifest.PSObject.Properties["target"]
|
|
322
|
-
if ($null -eq $TargetProperty -or -not $TargetProperty.Value) {
|
|
323
|
-
Fail "package manifest is malformed: target missing."
|
|
324
|
-
}
|
|
325
|
-
if ([string]$TargetProperty.Value -ine "win32-x64") {
|
|
326
|
-
Fail "package target mismatch: expected win32-x64, found $($TargetProperty.Value)."
|
|
327
|
-
}
|
|
328
|
-
} elseif ($UsingLocalArchive -and $ArchiveShape -eq "program" -and
|
|
329
|
-
$ArchiveName -ine $Asset -and $ArchiveName -ine $PayloadName) {
|
|
330
|
-
Fail "a renamed local archive must contain package-manifest.json; use the original filename for legacy packages."
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
$Dirs = @("bin", "lib", "web", "node", "git")
|
|
334
|
-
$MovedOld = @()
|
|
335
|
-
$MovedNew = @()
|
|
336
|
-
New-Item -ItemType Directory -Path $OldDir | Out-Null
|
|
337
|
-
try {
|
|
338
|
-
foreach ($d in $Dirs) {
|
|
339
|
-
$Existing = Join-Path $InstallDir $d
|
|
340
|
-
if (Test-Path -LiteralPath $Existing) {
|
|
341
|
-
Move-Item -LiteralPath $Existing -Destination (Join-Path $OldDir $d)
|
|
342
|
-
$MovedOld += $d
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
foreach ($d in $Dirs) {
|
|
346
|
-
$Src = Join-Path $NewRoot $d
|
|
347
|
-
if (Test-Path -LiteralPath $Src) {
|
|
348
|
-
Move-Item -LiteralPath $Src -Destination (Join-Path $InstallDir $d)
|
|
349
|
-
$MovedNew += $d
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
# --- Launcher shim: shipped in the payload; (re)generated only when missing. The two
|
|
354
|
-
# variables it fills in use Adelie's spelling, with the pre-rename PENGUIN_WEB_DIST
|
|
355
|
-
# still honored, exactly like scripts/launchers/penguin.cmd — a shim regenerated here
|
|
356
|
-
# must not behave differently from the one the payload ships. There is
|
|
357
|
-
# deliberately no penguin.ps1 launcher — PowerShell would prefer it over penguin.cmd on
|
|
358
|
-
# PATH, and client Windows defaults to the Restricted execution policy, so a .ps1
|
|
359
|
-
# launcher makes the plain `penguin` command fail with "running scripts is disabled".
|
|
360
|
-
# Batch files are policy-exempt and both PowerShell and cmd.exe resolve penguin.cmd.
|
|
361
|
-
# (bin\ is swapped wholesale above, so upgrading also removes the penguin.ps1 that
|
|
362
|
-
# pre-0.1.6 payloads shipped.) ---
|
|
363
|
-
$CmdShim = Join-Path $InstallDir "bin\penguin.cmd"
|
|
364
|
-
if (-not (Test-Path -LiteralPath $CmdShim)) {
|
|
365
|
-
@(
|
|
366
|
-
'@echo off'
|
|
367
|
-
'setlocal'
|
|
368
|
-
'set "DIR=%~dp0.."'
|
|
369
|
-
'if not defined ADELIE_WEB_DIST if defined PENGUIN_WEB_DIST set "ADELIE_WEB_DIST=%PENGUIN_WEB_DIST%"'
|
|
370
|
-
'if not defined ADELIE_WEB_DIST set "ADELIE_WEB_DIST=%DIR%\web"'
|
|
371
|
-
'if exist "%DIR%\git\usr\bin\sh.exe" set "ADELIE_BUNDLED_SHELL=%DIR%\git\usr\bin\sh.exe"'
|
|
372
|
-
'if exist "%DIR%\node\node.exe" ('
|
|
373
|
-
' "%DIR%\node\node.exe" "%DIR%\lib\dist\penguin.js" %*'
|
|
374
|
-
') else ('
|
|
375
|
-
' node "%DIR%\lib\dist\penguin.js" %*'
|
|
376
|
-
')'
|
|
377
|
-
'exit /b %ERRORLEVEL%'
|
|
378
|
-
) | Set-Content -LiteralPath $CmdShim -Encoding ascii
|
|
379
|
-
}
|
|
380
|
-
if (-not (Test-Path -LiteralPath $CmdShim)) { Fail "install incomplete: $CmdShim missing." }
|
|
381
|
-
|
|
382
|
-
# Verify from the final path before deleting the backup. Keep stderr visible so platform
|
|
383
|
-
# policy, permission and runtime errors are not disguised as an "unknown" version.
|
|
384
|
-
# Windows PowerShell 5.1 turns native stderr into NativeCommandError records. Temporarily
|
|
385
|
-
# keep those non-terminating so the real stderr stays visible and the exit code remains the
|
|
386
|
-
# authoritative result; restore Stop immediately afterwards for installer operations.
|
|
387
|
-
$PreviousErrorActionPreference = $ErrorActionPreference
|
|
388
|
-
try {
|
|
389
|
-
$ErrorActionPreference = "Continue"
|
|
390
|
-
$VersionOutput = @(& $CmdShim --version)
|
|
391
|
-
$VersionExitCode = $LASTEXITCODE
|
|
392
|
-
} finally {
|
|
393
|
-
$ErrorActionPreference = $PreviousErrorActionPreference
|
|
394
|
-
}
|
|
395
|
-
if ($VersionExitCode -ne 0) {
|
|
396
|
-
Fail "installed Adelie failed to run (exit code $VersionExitCode). See the error above."
|
|
397
|
-
}
|
|
398
|
-
$InstalledVersion = $VersionOutput | Select-Object -First 1
|
|
399
|
-
if ([string]::IsNullOrWhiteSpace([string]$InstalledVersion)) {
|
|
400
|
-
Fail "installed Adelie returned an empty version."
|
|
401
|
-
}
|
|
402
|
-
} catch {
|
|
403
|
-
$InstallFailure = $_
|
|
404
|
-
try {
|
|
405
|
-
Restore-PreviousInstall -InstallDir $InstallDir -OldDir $OldDir -MovedOld $MovedOld -MovedNew $MovedNew
|
|
406
|
-
Write-Host "Previous Adelie installation restored."
|
|
407
|
-
} catch {
|
|
408
|
-
throw "error: installation failed and automatic rollback was incomplete. Original error: $($InstallFailure.Exception.Message) Rollback error: $($_.Exception.Message) Previous files may remain in $OldDir"
|
|
409
|
-
}
|
|
410
|
-
throw $InstallFailure
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
Remove-Item -LiteralPath $OldDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
414
|
-
if (Test-Path -LiteralPath $OldDir) {
|
|
415
|
-
Write-Host "warning: could not fully remove $OldDir; delete it after closing running penguin processes."
|
|
416
|
-
}
|
|
417
|
-
} finally {
|
|
418
|
-
Remove-Item -LiteralPath $Tmp -Recurse -Force -ErrorAction SilentlyContinue
|
|
419
|
-
if ($Staging -and (Test-Path -LiteralPath $Staging)) {
|
|
420
|
-
Remove-Item -LiteralPath $Staging -Recurse -Force -ErrorAction SilentlyContinue
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
# --- User PATH: append <install>\bin once only after the installed command is known to work.
|
|
425
|
-
# Go through the registry, not [Environment]::*EnvironmentVariable: GetEnvironmentVariable
|
|
426
|
-
# expands REG_EXPAND_SZ and SetEnvironmentVariable writes back REG_SZ, which would
|
|
427
|
-
# irreversibly hard-code a user's %USERPROFILE%-style Path entries. Read the raw
|
|
428
|
-
# (unexpanded) value, append to it, and write it back with its original value kind.
|
|
429
|
-
# The registry only exists on Windows; skip the block elsewhere (functional test runs
|
|
430
|
-
# of this script on pwsh/Linux — where the old API was a silent no-op anyway). ---
|
|
431
|
-
# -NoModifyPath skips it: `penguin` resolves to the first bin directory on the
|
|
432
|
-
# Path, and a second installation's entry there would answer whenever the first is absent.
|
|
433
|
-
$BinDir = Join-Path $InstallDir "bin"
|
|
434
|
-
$PathUpdateMessage = ""
|
|
435
|
-
if (-not $NoModifyPath -and $env:OS -eq "Windows_NT") {
|
|
436
|
-
$EnvKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Environment", $true)
|
|
437
|
-
if ($null -eq $EnvKey) { $EnvKey = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey("Environment") }
|
|
438
|
-
try {
|
|
439
|
-
# Missing Path value: create it as REG_EXPAND_SZ (the kind Windows itself uses for Path).
|
|
440
|
-
$Kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
|
|
441
|
-
try { $Kind = $EnvKey.GetValueKind("Path") } catch {}
|
|
442
|
-
$RawPath = [string]$EnvKey.GetValue("Path", "", [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
|
443
|
-
# Membership is checked per entry after expansion, so both literal and %VAR%-style
|
|
444
|
-
# spellings of the bin dir count as already present; the append itself stays raw.
|
|
445
|
-
$OnPath = @($RawPath -split ";" | Where-Object { $_ } | ForEach-Object {
|
|
446
|
-
[Environment]::ExpandEnvironmentVariables($_).TrimEnd("\")
|
|
447
|
-
}) -contains $BinDir.TrimEnd("\")
|
|
448
|
-
if (-not $OnPath) {
|
|
449
|
-
$NewPath = if ($RawPath -and -not $RawPath.EndsWith(";")) { "$RawPath;$BinDir" } else { "$RawPath$BinDir" }
|
|
450
|
-
$EnvKey.SetValue("Path", $NewPath, $Kind)
|
|
451
|
-
# A raw registry write does not broadcast WM_SETTINGCHANGE, so Explorer — and every
|
|
452
|
-
# terminal window launched from it afterwards — would keep the stale Path until the next
|
|
453
|
-
# logon. Broadcast it the way [Environment]::SetEnvironmentVariable would; best-effort,
|
|
454
|
-
# a failure only means new terminals need a fresh logon to see the Path.
|
|
455
|
-
try {
|
|
456
|
-
if (-not ("PenguinInstaller.NativeMethods" -as [type])) {
|
|
457
|
-
Add-Type -Namespace PenguinInstaller -Name NativeMethods -MemberDefinition @'
|
|
458
|
-
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
|
|
459
|
-
public static extern System.IntPtr SendMessageTimeout(System.IntPtr hWnd, uint Msg, System.UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out System.UIntPtr lpdwResult);
|
|
460
|
-
'@
|
|
461
|
-
}
|
|
462
|
-
$BroadcastResult = [UIntPtr]::Zero
|
|
463
|
-
# HWND_BROADCAST (0xffff), WM_SETTINGCHANGE (0x1a), SMTO_ABORTIFHUNG (0x2), 5s timeout.
|
|
464
|
-
[PenguinInstaller.NativeMethods]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero,
|
|
465
|
-
"Environment", 2, 5000, [ref]$BroadcastResult) | Out-Null
|
|
466
|
-
} catch {
|
|
467
|
-
}
|
|
468
|
-
$PathUpdateMessage = "note: installation succeeded and $BinDir was appended to your user Path. Open a new terminal window so 'penguin' is found (a new tab of an already-running terminal keeps the old Path)."
|
|
469
|
-
}
|
|
470
|
-
} finally {
|
|
471
|
-
$EnvKey.Close()
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
# Make `penguin` work in this session too.
|
|
475
|
-
$PenguinCommand = "penguin"
|
|
476
|
-
if (-not $NoModifyPath) {
|
|
477
|
-
if (($env:Path -split ";") -notcontains $BinDir) { $env:Path = "$env:Path;$BinDir" }
|
|
478
|
-
} else {
|
|
479
|
-
$PenguinCommand = "& `"$(Join-Path $BinDir "penguin.cmd")`""
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
Write-Host ""
|
|
483
|
-
Write-Host "Adelie $InstalledVersion installed to $InstallDir"
|
|
484
|
-
if ($PathUpdateMessage) {
|
|
485
|
-
Write-Host ""
|
|
486
|
-
Write-Host $PathUpdateMessage
|
|
487
|
-
}
|
|
488
|
-
Write-Host ""
|
|
489
|
-
Write-Host "Get started:"
|
|
490
|
-
Write-Host " $PenguinCommand --help # all commands"
|
|
491
|
-
Write-Host " $PenguinCommand web # start the Web UI at http://127.0.0.1:4000 (a first-login link is printed on first start)"
|
|
492
|
-
Write-Host " $PenguinCommand server # headless server (PORT / HOST to override)"
|
|
1
|
+
# Adelie one-line installer for Windows.
|
|
2
|
+
#
|
|
3
|
+
# irm https://github.com/lmliheng/Adelie/releases/latest/download/install.ps1 | iex
|
|
4
|
+
#
|
|
5
|
+
# Options:
|
|
6
|
+
# $env:PENGUIN_VERSION = "vX.Y.Z" choose a version (same as -Version vX.Y.Z); a published Release
|
|
7
|
+
# installer defaults to its own version, an unstamped source copy to latest
|
|
8
|
+
# $env:PENGUIN_INSTALL_DIR = "<dir>" install dir; default $env:USERPROFILE\.adelie
|
|
9
|
+
# $env:PENGUIN_ARCHIVE = "<file>" install a local Release zip without network access (same as -ArchivePath)
|
|
10
|
+
# $env:PENGUIN_DOWNLOAD_SOURCE = "auto|github" choose the online source; default auto (GitHub Releases)
|
|
11
|
+
# $env:PENGUIN_DOWNLOAD_BASE_URL = "https://..." exact online asset directory selected by the stable forwarder
|
|
12
|
+
# $env:PENGUIN_DOWNLOAD_FALLBACK_BASE_URL = "https://..." fallback for PENGUIN_DOWNLOAD_BASE_URL
|
|
13
|
+
# -NoModifyPath do not add <install>\bin to the user Path; for a second installation
|
|
14
|
+
# beside the one the `penguin` command belongs to
|
|
15
|
+
#
|
|
16
|
+
# Each Release attaches exactly one Windows artifact: penguin-win32-x64.zip, a shallow installer
|
|
17
|
+
# bundle holding install.cmd, this script, the program payload (payload.zip) and the payload's
|
|
18
|
+
# checksum. Online installs download that bundle and verify it against its published .sha256;
|
|
19
|
+
# offline installs transfer the same single file, extract it once (the outer layer is flat, so
|
|
20
|
+
# no deep paths are created) and double-click install.cmd. Both paths verify the payload
|
|
21
|
+
# checksum sealed inside the bundle, then expand the payload straight into the short staging
|
|
22
|
+
# directory. Releases up to v0.1.5 shipped the program tree directly (top-level penguin\);
|
|
23
|
+
# such zips are still accepted, from -Version pins and -ArchivePath files alike.
|
|
24
|
+
#
|
|
25
|
+
# There is no -Universal on Windows: where the zip is unsuitable, install Node.js >= 24 and run
|
|
26
|
+
# `npm install -g @adelie/cli` instead.
|
|
27
|
+
#
|
|
28
|
+
# The data dir (%USERPROFILE%\.adelie\data by default) sits under the install home but is never touched by
|
|
29
|
+
# reinstall/upgrade (which only replace bin/lib/web/node/git). Upgrading = re-running this installer.
|
|
30
|
+
# The launcher exports the root it lives under, so an install made before the rename keeps its data.
|
|
31
|
+
#
|
|
32
|
+
# Docs: https://lmliheng.github.io/Adelie/docs/quickstart-cli
|
|
33
|
+
param(
|
|
34
|
+
[string]$Version = "",
|
|
35
|
+
[string]$InstallDir = "",
|
|
36
|
+
[string]$ArchivePath = "",
|
|
37
|
+
[switch]$NoModifyPath
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
$ErrorActionPreference = "Stop"
|
|
41
|
+
$ProgressPreference = "SilentlyContinue" # Invoke-WebRequest progress rendering slows downloads massively on PS 5.1
|
|
42
|
+
|
|
43
|
+
$Repo = "https://github.com/lmliheng/Adelie"
|
|
44
|
+
$GitHubReleaseRoot = "$Repo/releases/download"
|
|
45
|
+
$GitHubLatestBase = "$Repo/releases/latest/download"
|
|
46
|
+
$Asset = "penguin-win32-x64.zip"
|
|
47
|
+
$PayloadName = "payload.zip"
|
|
48
|
+
# The release workflow replaces this token with the immutable tag before publishing both the
|
|
49
|
+
# standalone installer and the copy sealed inside the Windows bundle.
|
|
50
|
+
$EmbeddedReleaseVersion = "__PENGUIN_RELEASE_VERSION__"
|
|
51
|
+
|
|
52
|
+
function Fail([string]$Message) {
|
|
53
|
+
# `throw` rather than `exit`: a caller that runs this installer as an in-memory script block
|
|
54
|
+
# (rather than as a file) would otherwise have `exit` terminate its whole PowerShell session.
|
|
55
|
+
# `throw` aborts cleanly in both file and script-block runs.
|
|
56
|
+
throw "error: $Message"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Lists a zip's top-level entry names without extracting (PS 5.1-safe; Expand-Archive itself
|
|
60
|
+
# uses the same .NET type). Used to tell an installer bundle (contains payload.zip) from a
|
|
61
|
+
# program archive (payload.zip itself, or a pre-0.1.6 release zip with a top-level penguin\).
|
|
62
|
+
function Get-ZipEntryNames([string]$ZipPath) {
|
|
63
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
64
|
+
$Zip = [IO.Compression.ZipFile]::OpenRead($ZipPath)
|
|
65
|
+
try {
|
|
66
|
+
return @($Zip.Entries | ForEach-Object { $_.FullName })
|
|
67
|
+
} finally {
|
|
68
|
+
$Zip.Dispose()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Verifies a file against a sha256sum-format checksum file (`<hex> <filename>`; the first
|
|
73
|
+
# token is the hash). Checksums are never optional: every install path either downloads the
|
|
74
|
+
# published .sha256 or reads the one sealed inside the bundle.
|
|
75
|
+
function Assert-Sha256([string]$FilePath, [string]$ShaPath, [string]$Label) {
|
|
76
|
+
$Expected = ((Get-Content -LiteralPath $ShaPath -Raw).Trim() -split "\s+")[0]
|
|
77
|
+
if (-not $Expected) { Fail "checksum file is empty or malformed: $ShaPath" }
|
|
78
|
+
$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $FilePath).Hash
|
|
79
|
+
if ($Actual -ine $Expected) { Fail "checksum mismatch for $([IO.Path]::GetFileName($FilePath))." }
|
|
80
|
+
Write-Host "$Label checksum OK."
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Assert-HttpsUrl([string]$Name, [string]$Value) {
|
|
84
|
+
try { $Uri = [Uri]$Value } catch { Fail "$Name is not a valid URL" }
|
|
85
|
+
if (-not $Uri.IsAbsoluteUri -or $Uri.Scheme -ne "https") {
|
|
86
|
+
Fail "$Name must be an absolute HTTPS URL"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Test-ReleaseTag([string]$Value) {
|
|
91
|
+
return $Value -match '^v[0-9A-Za-z][0-9A-Za-z._-]*$'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Get-DownloadSourceLabel([string]$BaseUrl) {
|
|
95
|
+
try { $HostName = ([Uri]$BaseUrl).Host } catch { return "configured mirror" }
|
|
96
|
+
if ($HostName -eq "github.com") { return "GitHub" }
|
|
97
|
+
return "configured mirror"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function Get-ReleasePair(
|
|
101
|
+
[string]$BaseUrl,
|
|
102
|
+
[string]$ZipPath,
|
|
103
|
+
[string]$ShaPath
|
|
104
|
+
) {
|
|
105
|
+
$Label = Get-DownloadSourceLabel $BaseUrl
|
|
106
|
+
Write-Host "Downloading $Asset from $Label ..."
|
|
107
|
+
Remove-Item -LiteralPath $ZipPath, $ShaPath -Force -ErrorAction SilentlyContinue
|
|
108
|
+
try {
|
|
109
|
+
Invoke-WebRequest -Uri "$BaseUrl/$Asset" -OutFile $ZipPath -UseBasicParsing
|
|
110
|
+
Invoke-WebRequest -Uri "$BaseUrl/$Asset.sha256" -OutFile $ShaPath -UseBasicParsing
|
|
111
|
+
return $true
|
|
112
|
+
} catch {
|
|
113
|
+
Remove-Item -LiteralPath $ZipPath, $ShaPath -Force -ErrorAction SilentlyContinue
|
|
114
|
+
return $false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Restore-PreviousInstall(
|
|
119
|
+
[string]$InstallDir,
|
|
120
|
+
[string]$OldDir,
|
|
121
|
+
[string[]]$MovedOld,
|
|
122
|
+
[string[]]$MovedNew
|
|
123
|
+
) {
|
|
124
|
+
foreach ($d in @($MovedNew)) {
|
|
125
|
+
if (-not $d) { continue }
|
|
126
|
+
$Current = Join-Path $InstallDir $d
|
|
127
|
+
if (Test-Path -LiteralPath $Current) {
|
|
128
|
+
Remove-Item -LiteralPath $Current -Recurse -Force -ErrorAction Stop
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
foreach ($d in @($MovedOld)) {
|
|
132
|
+
if (-not $d) { continue }
|
|
133
|
+
$Previous = Join-Path $OldDir $d
|
|
134
|
+
if (Test-Path -LiteralPath $Previous) {
|
|
135
|
+
Move-Item -LiteralPath $Previous -Destination (Join-Path $InstallDir $d) -ErrorAction Stop
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# --- Resolve options (parameters win over env vars, mirroring install.sh's --version) ---
|
|
141
|
+
if (-not $Version) { $Version = if ($env:PENGUIN_VERSION) { $env:PENGUIN_VERSION } else { "" } }
|
|
142
|
+
if (-not $InstallDir) {
|
|
143
|
+
$InstallDir = if ($env:PENGUIN_INSTALL_DIR) { $env:PENGUIN_INSTALL_DIR } else { Join-Path $env:USERPROFILE ".adelie" }
|
|
144
|
+
}
|
|
145
|
+
if (-not $ArchivePath) {
|
|
146
|
+
$ArchivePath = if ($env:PENGUIN_ARCHIVE) { $env:PENGUIN_ARCHIVE } else { "" }
|
|
147
|
+
}
|
|
148
|
+
$DownloadBaseUrl = if ($env:PENGUIN_DOWNLOAD_BASE_URL) {
|
|
149
|
+
$env:PENGUIN_DOWNLOAD_BASE_URL.TrimEnd('/')
|
|
150
|
+
} else {
|
|
151
|
+
""
|
|
152
|
+
}
|
|
153
|
+
$DownloadFallbackBaseUrl = if ($env:PENGUIN_DOWNLOAD_FALLBACK_BASE_URL) {
|
|
154
|
+
$env:PENGUIN_DOWNLOAD_FALLBACK_BASE_URL.TrimEnd('/')
|
|
155
|
+
} else {
|
|
156
|
+
""
|
|
157
|
+
}
|
|
158
|
+
if (-not $DownloadBaseUrl) { $DownloadFallbackBaseUrl = "" }
|
|
159
|
+
$SourceMode = if ($env:PENGUIN_DOWNLOAD_SOURCE) {
|
|
160
|
+
$env:PENGUIN_DOWNLOAD_SOURCE.ToLowerInvariant()
|
|
161
|
+
} else {
|
|
162
|
+
"auto"
|
|
163
|
+
}
|
|
164
|
+
# An extracted installer bundle keeps install.cmd, this script, payload.zip and its checksum
|
|
165
|
+
# together. `$PSScriptRoot` is empty for the documented `irm ... | iex` path, so online installs
|
|
166
|
+
# do not accidentally pick up an unrelated archive from the caller's current directory.
|
|
167
|
+
if (-not $ArchivePath -and $PSScriptRoot) {
|
|
168
|
+
$SiblingArchive = Join-Path $PSScriptRoot $PayloadName
|
|
169
|
+
if (Test-Path -LiteralPath $SiblingArchive -PathType Leaf) { $ArchivePath = $SiblingArchive }
|
|
170
|
+
}
|
|
171
|
+
if ($ArchivePath -and $Version) {
|
|
172
|
+
Fail "-ArchivePath/PENGUIN_ARCHIVE cannot be combined with -Version/PENGUIN_VERSION"
|
|
173
|
+
}
|
|
174
|
+
if ($Version -and -not (Test-ReleaseTag $Version)) {
|
|
175
|
+
Fail "invalid release version: $Version"
|
|
176
|
+
}
|
|
177
|
+
if ($SourceMode -notin @("auto", "github")) {
|
|
178
|
+
Fail "PENGUIN_DOWNLOAD_SOURCE must be auto or github"
|
|
179
|
+
}
|
|
180
|
+
$ResolvedReleaseVersion = if ($Version) {
|
|
181
|
+
$Version
|
|
182
|
+
} elseif (Test-ReleaseTag $EmbeddedReleaseVersion) {
|
|
183
|
+
$EmbeddedReleaseVersion
|
|
184
|
+
} else {
|
|
185
|
+
""
|
|
186
|
+
}
|
|
187
|
+
if ($DownloadBaseUrl) {
|
|
188
|
+
Assert-HttpsUrl "PENGUIN_DOWNLOAD_BASE_URL" $DownloadBaseUrl
|
|
189
|
+
}
|
|
190
|
+
if ($DownloadBaseUrl -and $DownloadFallbackBaseUrl) {
|
|
191
|
+
Assert-HttpsUrl "PENGUIN_DOWNLOAD_FALLBACK_BASE_URL" $DownloadFallbackBaseUrl
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# --- Platform preconditions: 64-bit Windows; the only Windows package is x64 (ARM64 runs it emulated) ---
|
|
195
|
+
if (-not [Environment]::Is64BitOperatingSystem) {
|
|
196
|
+
Fail "32-bit Windows is not supported. Install Node.js >= 24 and use: npm install -g @adelie/cli"
|
|
197
|
+
}
|
|
198
|
+
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
|
|
199
|
+
Write-Host "note: no native ARM64 package yet; installing the x64 package (runs via emulation)."
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# PowerShell 5.1 defaults to TLS 1.0 on older systems; GitHub requires TLS 1.2+.
|
|
203
|
+
try {
|
|
204
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
205
|
+
} catch {
|
|
206
|
+
# .NET builds where the enum is immutable already default to TLS 1.2+.
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# --- Download. An explicit forwarder/configured URL wins. Otherwise a stamped Release installer
|
|
210
|
+
# uses its own immutable version; an unstamped source-tree installer falls back to the
|
|
211
|
+
# release GitHub currently marks latest. ---
|
|
212
|
+
$Tmp = Join-Path ([IO.Path]::GetTempPath()) "penguin-install-$PID"
|
|
213
|
+
if (Test-Path $Tmp) { Remove-Item -Recurse -Force $Tmp }
|
|
214
|
+
New-Item -ItemType Directory -Path $Tmp | Out-Null
|
|
215
|
+
# Pre-declare so the finally block can read them even when an early failure skipped the
|
|
216
|
+
# assignments (the user's session may run this under Set-StrictMode via the forwarder).
|
|
217
|
+
$Staging = $null
|
|
218
|
+
$OldDir = $null
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
$UsingLocalArchive = [bool]$ArchivePath
|
|
222
|
+
if ($UsingLocalArchive) {
|
|
223
|
+
try {
|
|
224
|
+
$ZipPath = (Resolve-Path -LiteralPath $ArchivePath -ErrorAction Stop).Path
|
|
225
|
+
} catch {
|
|
226
|
+
Fail "local archive not found: $ArchivePath"
|
|
227
|
+
}
|
|
228
|
+
$ArchiveName = [IO.Path]::GetFileName($ZipPath)
|
|
229
|
+
Write-Host "Using local archive $ZipPath ..."
|
|
230
|
+
} else {
|
|
231
|
+
$BaseUrl = ""
|
|
232
|
+
$FallbackBaseUrl = $DownloadFallbackBaseUrl
|
|
233
|
+
if ($DownloadBaseUrl) {
|
|
234
|
+
$BaseUrl = $DownloadBaseUrl
|
|
235
|
+
} elseif ($ResolvedReleaseVersion) {
|
|
236
|
+
$BaseUrl = "$GitHubReleaseRoot/$ResolvedReleaseVersion"
|
|
237
|
+
} else {
|
|
238
|
+
$BaseUrl = $GitHubLatestBase
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Online: download the canonical bundle; the published checksum is mandatory.
|
|
242
|
+
$ZipPath = Join-Path $Tmp $Asset
|
|
243
|
+
$ArchiveName = $Asset
|
|
244
|
+
$ShaPath = Join-Path $Tmp "$Asset.sha256"
|
|
245
|
+
if (-not (Get-ReleasePair $BaseUrl $ZipPath $ShaPath)) {
|
|
246
|
+
if ($FallbackBaseUrl -and $FallbackBaseUrl -ne $BaseUrl) {
|
|
247
|
+
Write-Host "Primary download source unavailable; trying $(Get-DownloadSourceLabel $FallbackBaseUrl) ..."
|
|
248
|
+
if (-not (Get-ReleasePair $FallbackBaseUrl $ZipPath $ShaPath)) {
|
|
249
|
+
Fail "download failed from both the primary source and its fallback. Check your network, then retry."
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
Fail "download failed from $(Get-DownloadSourceLabel $BaseUrl). Check the version tag and your network, then retry."
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
Assert-Sha256 $ZipPath $ShaPath "Bundle"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# --- Resolve the program payload. An installer bundle (contains payload.zip) is opened flat
|
|
259
|
+
# and its sealed payload checksum verified; a program archive (payload.zip itself, or a
|
|
260
|
+
# pre-0.1.6 release zip with a top-level penguin\) is used directly. ---
|
|
261
|
+
$ArchiveShape = if ((Get-ZipEntryNames $ZipPath) -contains $PayloadName) { "bundle" } else { "program" }
|
|
262
|
+
if ($ArchiveShape -eq "bundle") {
|
|
263
|
+
# A local bundle is self-verifying through its sealed payload checksum; when the published
|
|
264
|
+
# outer .sha256 was transferred alongside it, verify that layer too.
|
|
265
|
+
if ($UsingLocalArchive -and (Test-Path -LiteralPath "$ZipPath.sha256" -PathType Leaf)) {
|
|
266
|
+
Assert-Sha256 $ZipPath "$ZipPath.sha256" "Bundle"
|
|
267
|
+
}
|
|
268
|
+
$BundleDir = Join-Path $Tmp "bundle"
|
|
269
|
+
New-Item -ItemType Directory -Path $BundleDir | Out-Null
|
|
270
|
+
Write-Host "Opening installer bundle ..."
|
|
271
|
+
# The outer layer is flat (four files), so this extraction never creates deep paths.
|
|
272
|
+
Expand-Archive -LiteralPath $ZipPath -DestinationPath $BundleDir -Force
|
|
273
|
+
$ZipPath = Join-Path $BundleDir $PayloadName
|
|
274
|
+
if (-not (Test-Path -LiteralPath $ZipPath -PathType Leaf)) {
|
|
275
|
+
Fail "unexpected bundle layout: $PayloadName missing."
|
|
276
|
+
}
|
|
277
|
+
if (-not (Test-Path -LiteralPath "$ZipPath.sha256" -PathType Leaf)) {
|
|
278
|
+
Fail "unexpected bundle layout: $PayloadName.sha256 missing."
|
|
279
|
+
}
|
|
280
|
+
Assert-Sha256 $ZipPath "$ZipPath.sha256" "Payload"
|
|
281
|
+
} elseif ($UsingLocalArchive) {
|
|
282
|
+
# Program archive from disk: an adjacent checksum is required — its own, or the canonical
|
|
283
|
+
# asset checksum next to a renamed legacy file. (An online download was already verified
|
|
284
|
+
# against the published .sha256 above.)
|
|
285
|
+
$ShaPath = "$ZipPath.sha256"
|
|
286
|
+
if (-not (Test-Path -LiteralPath $ShaPath -PathType Leaf) -and
|
|
287
|
+
$ArchiveName -ine $Asset) {
|
|
288
|
+
$CanonicalShaPath = Join-Path ([IO.Path]::GetDirectoryName($ZipPath)) "$Asset.sha256"
|
|
289
|
+
if (Test-Path -LiteralPath $CanonicalShaPath -PathType Leaf) {
|
|
290
|
+
$ShaPath = $CanonicalShaPath
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (-not (Test-Path -LiteralPath $ShaPath -PathType Leaf)) {
|
|
294
|
+
Fail "offline checksum file not found: $ShaPath"
|
|
295
|
+
}
|
|
296
|
+
Assert-Sha256 $ZipPath $ShaPath "Payload"
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# --- Extract into staging, then swap by same-volume renames. Keep the previous dirs in .old.$PID
|
|
300
|
+
# until the installed command runs successfully; any move or launch failure restores them.
|
|
301
|
+
# The data dir (%USERPROFILE%\.adelie\data by default) is never part of the swap. ---
|
|
302
|
+
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
|
303
|
+
$Staging = Join-Path $InstallDir ".staging.$PID"
|
|
304
|
+
$OldDir = Join-Path $InstallDir ".old.$PID"
|
|
305
|
+
if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging }
|
|
306
|
+
if (Test-Path $OldDir) { Remove-Item -Recurse -Force $OldDir }
|
|
307
|
+
New-Item -ItemType Directory -Path $Staging | Out-Null
|
|
308
|
+
|
|
309
|
+
Write-Host "Extracting ..."
|
|
310
|
+
Expand-Archive -LiteralPath $ZipPath -DestinationPath $Staging -Force
|
|
311
|
+
$NewRoot = Join-Path $Staging "penguin"
|
|
312
|
+
if (-not (Test-Path $NewRoot)) { Fail "unexpected archive layout: top-level penguin\ missing." }
|
|
313
|
+
if (-not (Test-Path (Join-Path $NewRoot "bin"))) { Fail "unexpected archive layout: penguin\bin missing." }
|
|
314
|
+
$ManifestPath = Join-Path $NewRoot "package-manifest.json"
|
|
315
|
+
if (Test-Path -LiteralPath $ManifestPath -PathType Leaf) {
|
|
316
|
+
try {
|
|
317
|
+
$PackageManifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
|
|
318
|
+
} catch {
|
|
319
|
+
Fail "package manifest is malformed: $($_.Exception.Message)"
|
|
320
|
+
}
|
|
321
|
+
$TargetProperty = $PackageManifest.PSObject.Properties["target"]
|
|
322
|
+
if ($null -eq $TargetProperty -or -not $TargetProperty.Value) {
|
|
323
|
+
Fail "package manifest is malformed: target missing."
|
|
324
|
+
}
|
|
325
|
+
if ([string]$TargetProperty.Value -ine "win32-x64") {
|
|
326
|
+
Fail "package target mismatch: expected win32-x64, found $($TargetProperty.Value)."
|
|
327
|
+
}
|
|
328
|
+
} elseif ($UsingLocalArchive -and $ArchiveShape -eq "program" -and
|
|
329
|
+
$ArchiveName -ine $Asset -and $ArchiveName -ine $PayloadName) {
|
|
330
|
+
Fail "a renamed local archive must contain package-manifest.json; use the original filename for legacy packages."
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
$Dirs = @("bin", "lib", "web", "node", "git")
|
|
334
|
+
$MovedOld = @()
|
|
335
|
+
$MovedNew = @()
|
|
336
|
+
New-Item -ItemType Directory -Path $OldDir | Out-Null
|
|
337
|
+
try {
|
|
338
|
+
foreach ($d in $Dirs) {
|
|
339
|
+
$Existing = Join-Path $InstallDir $d
|
|
340
|
+
if (Test-Path -LiteralPath $Existing) {
|
|
341
|
+
Move-Item -LiteralPath $Existing -Destination (Join-Path $OldDir $d)
|
|
342
|
+
$MovedOld += $d
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
foreach ($d in $Dirs) {
|
|
346
|
+
$Src = Join-Path $NewRoot $d
|
|
347
|
+
if (Test-Path -LiteralPath $Src) {
|
|
348
|
+
Move-Item -LiteralPath $Src -Destination (Join-Path $InstallDir $d)
|
|
349
|
+
$MovedNew += $d
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# --- Launcher shim: shipped in the payload; (re)generated only when missing. The two
|
|
354
|
+
# variables it fills in use Adelie's spelling, with the pre-rename PENGUIN_WEB_DIST
|
|
355
|
+
# still honored, exactly like scripts/launchers/penguin.cmd — a shim regenerated here
|
|
356
|
+
# must not behave differently from the one the payload ships. There is
|
|
357
|
+
# deliberately no penguin.ps1 launcher — PowerShell would prefer it over penguin.cmd on
|
|
358
|
+
# PATH, and client Windows defaults to the Restricted execution policy, so a .ps1
|
|
359
|
+
# launcher makes the plain `penguin` command fail with "running scripts is disabled".
|
|
360
|
+
# Batch files are policy-exempt and both PowerShell and cmd.exe resolve penguin.cmd.
|
|
361
|
+
# (bin\ is swapped wholesale above, so upgrading also removes the penguin.ps1 that
|
|
362
|
+
# pre-0.1.6 payloads shipped.) ---
|
|
363
|
+
$CmdShim = Join-Path $InstallDir "bin\penguin.cmd"
|
|
364
|
+
if (-not (Test-Path -LiteralPath $CmdShim)) {
|
|
365
|
+
@(
|
|
366
|
+
'@echo off'
|
|
367
|
+
'setlocal'
|
|
368
|
+
'set "DIR=%~dp0.."'
|
|
369
|
+
'if not defined ADELIE_WEB_DIST if defined PENGUIN_WEB_DIST set "ADELIE_WEB_DIST=%PENGUIN_WEB_DIST%"'
|
|
370
|
+
'if not defined ADELIE_WEB_DIST set "ADELIE_WEB_DIST=%DIR%\web"'
|
|
371
|
+
'if exist "%DIR%\git\usr\bin\sh.exe" set "ADELIE_BUNDLED_SHELL=%DIR%\git\usr\bin\sh.exe"'
|
|
372
|
+
'if exist "%DIR%\node\node.exe" ('
|
|
373
|
+
' "%DIR%\node\node.exe" "%DIR%\lib\dist\penguin.js" %*'
|
|
374
|
+
') else ('
|
|
375
|
+
' node "%DIR%\lib\dist\penguin.js" %*'
|
|
376
|
+
')'
|
|
377
|
+
'exit /b %ERRORLEVEL%'
|
|
378
|
+
) | Set-Content -LiteralPath $CmdShim -Encoding ascii
|
|
379
|
+
}
|
|
380
|
+
if (-not (Test-Path -LiteralPath $CmdShim)) { Fail "install incomplete: $CmdShim missing." }
|
|
381
|
+
|
|
382
|
+
# Verify from the final path before deleting the backup. Keep stderr visible so platform
|
|
383
|
+
# policy, permission and runtime errors are not disguised as an "unknown" version.
|
|
384
|
+
# Windows PowerShell 5.1 turns native stderr into NativeCommandError records. Temporarily
|
|
385
|
+
# keep those non-terminating so the real stderr stays visible and the exit code remains the
|
|
386
|
+
# authoritative result; restore Stop immediately afterwards for installer operations.
|
|
387
|
+
$PreviousErrorActionPreference = $ErrorActionPreference
|
|
388
|
+
try {
|
|
389
|
+
$ErrorActionPreference = "Continue"
|
|
390
|
+
$VersionOutput = @(& $CmdShim --version)
|
|
391
|
+
$VersionExitCode = $LASTEXITCODE
|
|
392
|
+
} finally {
|
|
393
|
+
$ErrorActionPreference = $PreviousErrorActionPreference
|
|
394
|
+
}
|
|
395
|
+
if ($VersionExitCode -ne 0) {
|
|
396
|
+
Fail "installed Adelie failed to run (exit code $VersionExitCode). See the error above."
|
|
397
|
+
}
|
|
398
|
+
$InstalledVersion = $VersionOutput | Select-Object -First 1
|
|
399
|
+
if ([string]::IsNullOrWhiteSpace([string]$InstalledVersion)) {
|
|
400
|
+
Fail "installed Adelie returned an empty version."
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
$InstallFailure = $_
|
|
404
|
+
try {
|
|
405
|
+
Restore-PreviousInstall -InstallDir $InstallDir -OldDir $OldDir -MovedOld $MovedOld -MovedNew $MovedNew
|
|
406
|
+
Write-Host "Previous Adelie installation restored."
|
|
407
|
+
} catch {
|
|
408
|
+
throw "error: installation failed and automatic rollback was incomplete. Original error: $($InstallFailure.Exception.Message) Rollback error: $($_.Exception.Message) Previous files may remain in $OldDir"
|
|
409
|
+
}
|
|
410
|
+
throw $InstallFailure
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
Remove-Item -LiteralPath $OldDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
414
|
+
if (Test-Path -LiteralPath $OldDir) {
|
|
415
|
+
Write-Host "warning: could not fully remove $OldDir; delete it after closing running penguin processes."
|
|
416
|
+
}
|
|
417
|
+
} finally {
|
|
418
|
+
Remove-Item -LiteralPath $Tmp -Recurse -Force -ErrorAction SilentlyContinue
|
|
419
|
+
if ($Staging -and (Test-Path -LiteralPath $Staging)) {
|
|
420
|
+
Remove-Item -LiteralPath $Staging -Recurse -Force -ErrorAction SilentlyContinue
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# --- User PATH: append <install>\bin once only after the installed command is known to work.
|
|
425
|
+
# Go through the registry, not [Environment]::*EnvironmentVariable: GetEnvironmentVariable
|
|
426
|
+
# expands REG_EXPAND_SZ and SetEnvironmentVariable writes back REG_SZ, which would
|
|
427
|
+
# irreversibly hard-code a user's %USERPROFILE%-style Path entries. Read the raw
|
|
428
|
+
# (unexpanded) value, append to it, and write it back with its original value kind.
|
|
429
|
+
# The registry only exists on Windows; skip the block elsewhere (functional test runs
|
|
430
|
+
# of this script on pwsh/Linux — where the old API was a silent no-op anyway). ---
|
|
431
|
+
# -NoModifyPath skips it: `penguin` resolves to the first bin directory on the
|
|
432
|
+
# Path, and a second installation's entry there would answer whenever the first is absent.
|
|
433
|
+
$BinDir = Join-Path $InstallDir "bin"
|
|
434
|
+
$PathUpdateMessage = ""
|
|
435
|
+
if (-not $NoModifyPath -and $env:OS -eq "Windows_NT") {
|
|
436
|
+
$EnvKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Environment", $true)
|
|
437
|
+
if ($null -eq $EnvKey) { $EnvKey = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey("Environment") }
|
|
438
|
+
try {
|
|
439
|
+
# Missing Path value: create it as REG_EXPAND_SZ (the kind Windows itself uses for Path).
|
|
440
|
+
$Kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
|
|
441
|
+
try { $Kind = $EnvKey.GetValueKind("Path") } catch {}
|
|
442
|
+
$RawPath = [string]$EnvKey.GetValue("Path", "", [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
|
443
|
+
# Membership is checked per entry after expansion, so both literal and %VAR%-style
|
|
444
|
+
# spellings of the bin dir count as already present; the append itself stays raw.
|
|
445
|
+
$OnPath = @($RawPath -split ";" | Where-Object { $_ } | ForEach-Object {
|
|
446
|
+
[Environment]::ExpandEnvironmentVariables($_).TrimEnd("\")
|
|
447
|
+
}) -contains $BinDir.TrimEnd("\")
|
|
448
|
+
if (-not $OnPath) {
|
|
449
|
+
$NewPath = if ($RawPath -and -not $RawPath.EndsWith(";")) { "$RawPath;$BinDir" } else { "$RawPath$BinDir" }
|
|
450
|
+
$EnvKey.SetValue("Path", $NewPath, $Kind)
|
|
451
|
+
# A raw registry write does not broadcast WM_SETTINGCHANGE, so Explorer — and every
|
|
452
|
+
# terminal window launched from it afterwards — would keep the stale Path until the next
|
|
453
|
+
# logon. Broadcast it the way [Environment]::SetEnvironmentVariable would; best-effort,
|
|
454
|
+
# a failure only means new terminals need a fresh logon to see the Path.
|
|
455
|
+
try {
|
|
456
|
+
if (-not ("PenguinInstaller.NativeMethods" -as [type])) {
|
|
457
|
+
Add-Type -Namespace PenguinInstaller -Name NativeMethods -MemberDefinition @'
|
|
458
|
+
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
|
|
459
|
+
public static extern System.IntPtr SendMessageTimeout(System.IntPtr hWnd, uint Msg, System.UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out System.UIntPtr lpdwResult);
|
|
460
|
+
'@
|
|
461
|
+
}
|
|
462
|
+
$BroadcastResult = [UIntPtr]::Zero
|
|
463
|
+
# HWND_BROADCAST (0xffff), WM_SETTINGCHANGE (0x1a), SMTO_ABORTIFHUNG (0x2), 5s timeout.
|
|
464
|
+
[PenguinInstaller.NativeMethods]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero,
|
|
465
|
+
"Environment", 2, 5000, [ref]$BroadcastResult) | Out-Null
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
$PathUpdateMessage = "note: installation succeeded and $BinDir was appended to your user Path. Open a new terminal window so 'penguin' is found (a new tab of an already-running terminal keeps the old Path)."
|
|
469
|
+
}
|
|
470
|
+
} finally {
|
|
471
|
+
$EnvKey.Close()
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
# Make `penguin` work in this session too.
|
|
475
|
+
$PenguinCommand = "penguin"
|
|
476
|
+
if (-not $NoModifyPath) {
|
|
477
|
+
if (($env:Path -split ";") -notcontains $BinDir) { $env:Path = "$env:Path;$BinDir" }
|
|
478
|
+
} else {
|
|
479
|
+
$PenguinCommand = "& `"$(Join-Path $BinDir "penguin.cmd")`""
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
Write-Host ""
|
|
483
|
+
Write-Host "Adelie $InstalledVersion installed to $InstallDir"
|
|
484
|
+
if ($PathUpdateMessage) {
|
|
485
|
+
Write-Host ""
|
|
486
|
+
Write-Host $PathUpdateMessage
|
|
487
|
+
}
|
|
488
|
+
Write-Host ""
|
|
489
|
+
Write-Host "Get started:"
|
|
490
|
+
Write-Host " $PenguinCommand --help # all commands"
|
|
491
|
+
Write-Host " $PenguinCommand web # start the Web UI at http://127.0.0.1:4000 (a first-login link is printed on first start)"
|
|
492
|
+
Write-Host " $PenguinCommand server # headless server (PORT / HOST to override)"
|