@join-pantheon/cli 0.31.2 → 0.32.0-rc.2

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.
@@ -0,0 +1,29 @@
1
+ {
2
+ "format": 1,
3
+ "built_at": "2026-10-03T12:09:02.195Z",
4
+ "swiftc": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)",
5
+ "helpers": {
6
+ "pantheon-tray": {
7
+ "source": "menubar.swift",
8
+ "source_sha256": "f079629099a81f9b679ef4133d897ceb09025f675099c5f941951604df3531fa",
9
+ "sha256": "cc081120555f528c83b38951663d37e8394e3a3e119a19b0bb3b2a737f4d617f",
10
+ "archs": [
11
+ "arm64",
12
+ "x86_64"
13
+ ],
14
+ "target": "macos11",
15
+ "version": "2"
16
+ },
17
+ "pantheon-hud": {
18
+ "source": "main.swift",
19
+ "source_sha256": "bcf55a19b658d1f09a9d117c76030393ab08a2569062dd7413f884ca05f3a0eb",
20
+ "sha256": "0414b397a533940e3a5c324add6250e1ed71919e67e462d11dcb2a1d0c2f9b41",
21
+ "archs": [
22
+ "arm64",
23
+ "x86_64"
24
+ ],
25
+ "target": "macos11",
26
+ "version": "4"
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,554 @@
1
+ # Pantheon installer for Windows (Windows PowerShell 5.1 and PowerShell 7).
2
+ #
3
+ # In PowerShell:
4
+ # irm https://relay.joinpantheon.network/install.ps1 | iex (install only)
5
+ # $env:PANTHEON_ARGS = "join '<invite link>'"; irm https://relay.joinpantheon.network/install.ps1 | iex
6
+ # & ([scriptblock]::Create((irm https://relay.joinpantheon.network/install.ps1))) join '<invite link>'
7
+ # (not under Constrained Language Mode, which refuses [scriptblock]::Create
8
+ # before this script can explain: there, use the `irm | iex` forms)
9
+ #
10
+ # In Command Prompt (cmd.exe), as one line:
11
+ # powershell -NoProfile -ExecutionPolicy Bypass -Command "$env:PANTHEON_ARGS='join <invite link>'; irm https://relay.joinpantheon.network/install.ps1 | iex"
12
+ #
13
+ # One line for either window (what `pantheon invite` prints): PowerShell would
14
+ # expand the $env: above inside the outer double quotes before the inner
15
+ # PowerShell runs, so this sets the variable without a $, and with a cmdlet
16
+ # that Constrained Language Mode still allows:
17
+ # powershell -NoProfile -ExecutionPolicy Bypass -Command "Set-Item Env:PANTHEON_ARGS 'join <invite link>'; irm https://relay.joinpantheon.network/install.ps1 | iex"
18
+ #
19
+ # `irm | iex` cannot pass arguments, so the arguments for pantheon travel in
20
+ # $env:PANTHEON_ARGS, which reads the same from PowerShell and from cmd.exe.
21
+ # The scriptblock form passes them as real arguments instead. When both are
22
+ # given, the real arguments win. PANTHEON_ARGS is cleared once read, so a later
23
+ # plain install in the same window does not repeat a `join`.
24
+ #
25
+ # What it does, and nothing else:
26
+ # 1. downloads one pinned Node release (the win-x64 or win-arm64 zip) from
27
+ # nodejs.org, checks its sha256 against the one pinned in this file, and
28
+ # unpacks it into %LOCALAPPDATA%\Pantheon\runtime\node. That Node is
29
+ # Pantheon's own; a Node you already have is not touched.
30
+ # 2. installs the Pantheon CLI with THAT Node's npm into that same folder.
31
+ # No administrator rights, no execution-policy change, no WSL.
32
+ # 3. writes %LOCALAPPDATA%\Pantheon\bin\pantheon.cmd, which runs that Node.
33
+ # 4. puts %LOCALAPPDATA%\Pantheon\bin first on your user PATH, and on this
34
+ # window's PATH.
35
+ # 5. runs `pantheon <the arguments you gave>`.
36
+ #
37
+ # Running it again is safe: whatever is already right is left as it is. Two
38
+ # copies at once are safe too: the second waits for the first.
39
+ # The relay that serves this file fills in the Pantheon version below.
40
+
41
+ # Constrained Language Mode (an IT policy: AppLocker or WDAC) refuses the .NET
42
+ # calls this installer needs. Said plainly, before the first one fails with an
43
+ # error about "core types". `throw` makes `powershell -Command ... | iex` exit 1
44
+ # (Write-Error inside iex does not), and leaves an open window open.
45
+ if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') {
46
+ Write-Host 'pantheon install failed: PowerShell on this computer runs in Constrained Language Mode (set by your IT), which this installer cannot run in. Ask IT to allow it, or if you have Node.js 22.13 or newer: npm install -g @join-pantheon/cli' -ForegroundColor Red
47
+ Remove-Item Env:PANTHEON_ARGS -ErrorAction SilentlyContinue
48
+ throw 'Pantheon was not installed (Constrained Language Mode).'
49
+ }
50
+
51
+ # The arguments of the scriptblock form; empty under `irm | iex`.
52
+ $PantheonInstallArgs = @()
53
+ try { if ($null -ne $args) { $PantheonInstallArgs = @($args) } } catch { $PantheonInstallArgs = @() }
54
+
55
+ # The result travels in $State rather than as output: anything a function
56
+ # returns is captured from its output stream, and pantheon's own output must
57
+ # reach the console untouched.
58
+ function Install-Pantheon {
59
+ param([string[]]$Arguments = @(), [hashtable]$State)
60
+
61
+ $PantheonVersion = '__PANTHEON_VERSION__'
62
+ $NodeVersion = '22.23.3'
63
+ # The sha256 of each Node zip this script may install, from nodejs.org's
64
+ # SHASUMS256.txt for $NodeVersion. A download is checked against THESE, never
65
+ # against a sums file fetched beside it, which a mirror could serve to match
66
+ # whatever it sends. Bumping $NodeVersion means new pins: scripts/node-pins.mjs.
67
+ $NodePins = @{
68
+ 'node-v22.23.3-win-arm64.zip' = '33dad22e4cef5ee8f9fbb1b0d037fdacd0e56d12a4580f0d63f68b894deab535'
69
+ 'node-v22.23.3-win-x64.zip' = '2b0ff57b049cda1bbcea2240eec20467018713c1efe1f7360c2681859b90ed71'
70
+ }
71
+ # Another copy of nodejs.org/dist (a company mirror), if nodejs.org is blocked.
72
+ $NodeDist = if ($env:PANTHEON_NODE_MIRROR) { $env:PANTHEON_NODE_MIRROR.TrimEnd('/') } else { 'https://nodejs.org/dist' }
73
+ $Site = 'https://relay.joinpantheon.network'
74
+
75
+ $ErrorActionPreference = 'Stop'
76
+ # Invoke-WebRequest's progress bar makes Windows PowerShell 5.1 downloads many times slower.
77
+ $ProgressPreference = 'SilentlyContinue'
78
+ # Every failure is one plain line: what failed, and what to do about it.
79
+ function Fail([string]$Message) { throw "PANTHEON_FAIL:$Message" }
80
+ function Say([string]$Message) { Write-Host $Message }
81
+ function Note([string]$Message) { Write-Host "note: $Message" -ForegroundColor Yellow }
82
+
83
+ # One argument string as the C runtime (and so node.exe) splits it back up.
84
+ function ConvertTo-ArgLine([string[]]$Items) {
85
+ ($Items | ForEach-Object {
86
+ if ($_ -eq '') { '""' }
87
+ elseif ($_ -notmatch '[\s"]') { $_ }
88
+ else { '"' + (($_ -replace '(\\*)"', '$1$1\"') -replace '(\\+)$', '$1$1') + '"' }
89
+ }) -join ' '
90
+ }
91
+
92
+ # Run a program with its output into a log file and nothing on screen. A
93
+ # Process object, because in Windows PowerShell 5.1 redirecting a native
94
+ # command's stderr under ErrorActionPreference=Stop turns its first warning
95
+ # line into a terminating error.
96
+ function Invoke-Logged([string]$Exe, [string[]]$Items, [string]$LogFile) {
97
+ $r = Invoke-Quiet $Exe $Items
98
+ [IO.File]::WriteAllText($LogFile, $r.out + $r.err)
99
+ return $r.code
100
+ }
101
+ # The same, with the output handed back instead: @{ code; out; err }.
102
+ function Invoke-Quiet([string]$Exe, [string[]]$Items) {
103
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
104
+ $psi.FileName = $Exe
105
+ $psi.Arguments = ConvertTo-ArgLine $Items
106
+ $psi.UseShellExecute = $false
107
+ $psi.CreateNoWindow = $true
108
+ $psi.RedirectStandardInput = $true
109
+ $psi.RedirectStandardOutput = $true
110
+ $psi.RedirectStandardError = $true
111
+ $p = [System.Diagnostics.Process]::Start($psi)
112
+ $p.StandardInput.Close()
113
+ $out = $p.StandardOutput.ReadToEndAsync()
114
+ $err = $p.StandardError.ReadToEndAsync()
115
+ $p.WaitForExit()
116
+ return @{ code = $p.ExitCode; out = $out.Result; err = $err.Result }
117
+ }
118
+
119
+ try {
120
+ if ($PantheonVersion -like '*__*') {
121
+ Fail "this copy of install.ps1 has no version filled in. Use the one the relay serves: irm $Site/install.ps1 | iex"
122
+ }
123
+
124
+ # ---- what this machine is ----------------------------------------------
125
+
126
+ $State.step = 'checking this machine'
127
+ if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
128
+ Fail "this installer is for Windows. On macOS or Linux: curl -fsSL $Site/install.sh | sh"
129
+ }
130
+ if ([Environment]::OSVersion.Version.Major -lt 10) {
131
+ Fail "Node $NodeVersion needs Windows 10, Windows Server 2016 or newer."
132
+ }
133
+ $arch = $null
134
+ try { $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() } catch { $arch = $null }
135
+ if (-not $arch) { $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } }
136
+ switch -regex ($arch) {
137
+ '^(X64|AMD64)$' { $nodeArch = 'x64'; break }
138
+ '^ARM64$' { $nodeArch = 'arm64'; break }
139
+ default { Fail "this processor ($arch) has no Node $NodeVersion build for Windows. Pantheon runs on x64 and arm64." }
140
+ }
141
+
142
+ # A cloud workspace can install Pantheon, but what it installs goes away with
143
+ # the session. The same names src/host-env.ts looks for.
144
+ $cloud = @('CODESPACES', 'CLOUDENV_ENVIRONMENT_ID', 'REMOTE_CONTAINERS', 'DEVCONTAINER', 'GITPOD_WORKSPACE_ID', 'CLOUD_SHELL', 'REPL_ID', 'KUBERNETES_SERVICE_HOST') |
145
+ Where-Object { [Environment]::GetEnvironmentVariable($_) }
146
+ if ($cloud) {
147
+ Note "this looks like a cloud workspace, not the computer you work on (`$$($cloud -join ', $') set). Pantheon will install, but it goes away with this session."
148
+ }
149
+
150
+ # Windows PowerShell 5.1 may still offer only TLS 1.0 to nodejs.org.
151
+ try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { }
152
+
153
+ # Both spellings, so Invoke-WebRequest, npm and Pantheon see the same proxy.
154
+ foreach ($pair in @(@('HTTPS_PROXY', 'https_proxy'), @('HTTP_PROXY', 'http_proxy'), @('NO_PROXY', 'no_proxy'))) {
155
+ $v = [Environment]::GetEnvironmentVariable($pair[0]); if (-not $v) { $v = [Environment]::GetEnvironmentVariable($pair[1]) }
156
+ if ($v) { [Environment]::SetEnvironmentVariable($pair[0], $v, 'Process'); [Environment]::SetEnvironmentVariable($pair[1], $v, 'Process') }
157
+ }
158
+ $proxy = $env:HTTPS_PROXY
159
+
160
+ $localAppData = [Environment]::GetFolderPath('LocalApplicationData')
161
+ if (-not $localAppData) { $localAppData = $env:LOCALAPPDATA }
162
+ if (-not $localAppData) { Fail 'LOCALAPPDATA is not set, so there is nowhere to install. Run this from your own Windows account.' }
163
+ $root = Join-Path $localAppData 'Pantheon'
164
+ $runtime = Join-Path $root 'runtime'
165
+ $nodeDir = Join-Path $runtime 'node'
166
+ $nodeExe = Join-Path $nodeDir 'node.exe'
167
+ $npmCli = Join-Path $nodeDir 'node_modules\npm\bin\npm-cli.js'
168
+ $pkgDir = Join-Path $nodeDir 'node_modules\@join-pantheon\cli'
169
+ $entry = Join-Path $pkgDir 'bin\pantheon.js'
170
+ $binDir = Join-Path $root 'bin'
171
+ $launcher = Join-Path $binDir 'pantheon.cmd'
172
+ $log = Join-Path $root 'install.log'
173
+
174
+ $State.step = "creating $root"
175
+ try { New-Item -ItemType Directory -Force -Path $runtime, $binDir | Out-Null }
176
+ catch { Fail "could not create $runtime ($($_.Exception.Message)). Check that the folder is yours and the disk has space, then run this again." }
177
+
178
+ # ---- one installer at a time -------------------------------------------
179
+
180
+ # Two installers at once (a second window, a line pasted twice) would swap
181
+ # the runtime under each other. The lock is a file this process holds open
182
+ # with no sharing; Windows lets go of it when the process ends, however it
183
+ # ends (Ctrl-C, a closed window, Task Manager), so it is never stale. The
184
+ # second installer waits for the first, then finds everything in place.
185
+ $State.step = 'waiting for another Pantheon install'
186
+ $lockFile = Join-Path $runtime 'install.lock'
187
+ $waited = 0
188
+ while (-not $State.lock) {
189
+ try { $State.lock = [IO.File]::Open($lockFile, [IO.FileMode]::OpenOrCreate, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) }
190
+ catch {
191
+ $ex = $_.Exception
192
+ while ($ex.InnerException) { $ex = $ex.InnerException }
193
+ if ($ex -isnot [IO.IOException]) { throw }
194
+ if ($waited -eq 0) { Say 'Another Pantheon install is running; waiting for it to finish...' }
195
+ if ($waited -ge 600) { Fail 'another Pantheon install has been running for over 10 minutes. Let it finish or close its window, then run this again.' }
196
+ Start-Sleep -Seconds 1
197
+ $waited++
198
+ }
199
+ }
200
+ # What an installer stopped by a power cut left behind (see $tmp below).
201
+ Get-ChildItem -LiteralPath $runtime -Directory -Force -ErrorAction SilentlyContinue |
202
+ Where-Object { $_.Name -match '^t[0-9a-f]{6}$' } |
203
+ ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }
204
+
205
+ function Get-Download([string]$Url, [string]$OutFile) {
206
+ $params = @{ Uri = $Url; OutFile = $OutFile; UseBasicParsing = $true }
207
+ if ($proxy) { $params.Proxy = $proxy; $params.ProxyUseDefaultCredentials = $true }
208
+ $last = ''
209
+ for ($i = 1; $i -le 3; $i++) {
210
+ try { Invoke-WebRequest @params; return } catch { $last = $_.Exception.Message; Start-Sleep -Seconds 2 }
211
+ }
212
+ Fail "could not download $Url ($last). Check your internet connection; behind a proxy set HTTPS_PROXY; then run this again."
213
+ }
214
+
215
+ # ---- 1. Pantheon's own Node --------------------------------------------
216
+
217
+ $haveNode = ''
218
+ if (Test-Path -LiteralPath $nodeExe) { try { $haveNode = (& $nodeExe --version) 2>$null } catch { $haveNode = '' } }
219
+ if ($haveNode -eq "v$NodeVersion") {
220
+ Say "Node v$NodeVersion for Pantheon is already in $nodeDir."
221
+ } else {
222
+ $zipName = "node-v$NodeVersion-win-$nodeArch.zip"
223
+ # Short names on purpose: npm's deepest file plus this folder must stay
224
+ # under Windows PowerShell's 260-character path limit.
225
+ $tmp = Join-Path $runtime ('t' + [Guid]::NewGuid().ToString('N').Substring(0, 6))
226
+ New-Item -ItemType Directory -Force -Path $tmp | Out-Null
227
+ $aside = $null
228
+ try {
229
+ $State.step = "downloading Node v$NodeVersion"
230
+ Say "Downloading Node v$NodeVersion for Pantheon (win-$nodeArch)..."
231
+ $zip = Join-Path $tmp $zipName
232
+ $want = $NodePins[$zipName]
233
+ if (-not $want) { Fail "this installer has no checksum for $zipName, so it cannot install Node here. Install Node 22.13 or newer yourself, then: npm install -g @join-pantheon/cli@$PantheonVersion" }
234
+ Get-Download "$NodeDist/v$NodeVersion/$zipName" $zip
235
+
236
+ $State.step = 'checking the Node download'
237
+ # .NET, not Get-FileHash: that is a script function in Microsoft.PowerShell.Utility,
238
+ # and Windows PowerShell 5.1 started from PowerShell 7 (the line pasted into pwsh)
239
+ # inherits pwsh's module path and cannot load it ("Get-FileHash is not recognized").
240
+ $stream = [IO.File]::OpenRead($zip)
241
+ try {
242
+ $sha = [Security.Cryptography.SHA256]::Create()
243
+ $got = (($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '')
244
+ } finally { $stream.Dispose() }
245
+ if ($got -ne $want) { Fail "the Node download does not match its published sha256 (expected $want, got $got), so it was not used. Something between you and nodejs.org changed it; run this again on another network." }
246
+
247
+ # Unpacked entry by entry with the archive's top folder stripped, so no
248
+ # path is ever longer than it will be once installed.
249
+ $State.step = 'unpacking Node'
250
+ $new = Join-Path $tmp 'n'
251
+ try { Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop } catch { }
252
+ try { Add-Type -AssemblyName System.IO.Compression -ErrorAction Stop } catch { }
253
+ $archive = [System.IO.Compression.ZipFile]::OpenRead($zip)
254
+ try {
255
+ foreach ($e in $archive.Entries) {
256
+ $rel = $e.FullName -replace '^[^/\\]+[/\\]', ''
257
+ if (-not $rel -or $rel -eq $e.FullName) { continue }
258
+ $dest = Join-Path $new ($rel -replace '/', '\')
259
+ if ($e.FullName.EndsWith('/') -or $e.FullName.EndsWith('\')) { New-Item -ItemType Directory -Force -Path $dest | Out-Null; continue }
260
+ New-Item -ItemType Directory -Force -Path (Split-Path -Parent $dest) | Out-Null
261
+ [System.IO.Compression.ZipFileExtensions]::ExtractToFile($e, $dest, $true)
262
+ }
263
+ } finally { $archive.Dispose() }
264
+ $newNode = Join-Path $new 'node.exe'
265
+ if (-not (Test-Path -LiteralPath $newNode)) { Fail 'the Node archive did not have the layout expected. Run this again; if it repeats, send this line to sid@joinpantheon.network' }
266
+ try { $ok = (& $newNode --version) 2>$null } catch { $ok = '' }
267
+ if ($ok -ne "v$NodeVersion") { Fail "Node v$NodeVersion does not run on this Windows. Install Node 22.13 or newer yourself, then: npm install -g @join-pantheon/cli@$PantheonVersion" }
268
+
269
+ # The CLI from the old runtime moves into the new one with it, so the
270
+ # pantheon command keeps working until npm below has run.
271
+ $oldPkgs = Join-Path $nodeDir 'node_modules\@join-pantheon'
272
+ if (Test-Path -LiteralPath $oldPkgs) {
273
+ try { Copy-Item -LiteralPath $oldPkgs -Destination (Join-Path $new 'node_modules') -Recurse -Force } catch { }
274
+ }
275
+
276
+ # Swapped by rename, never unpacked over: a half-written runtime is never
277
+ # the one an agent window starts.
278
+ $State.step = 'putting Node in place'
279
+ $aside = $null
280
+ if (Test-Path -LiteralPath $nodeDir) {
281
+ $aside = Join-Path $tmp 'old'
282
+ # A virus scanner looking at the new or old node.exe holds it for a
283
+ # moment: a few tries before telling the person something is open.
284
+ $moved = $false
285
+ for ($i = 1; $i -le 5 -and -not $moved; $i++) {
286
+ try { Move-Item -LiteralPath $nodeDir -Destination $aside; $moved = $true } catch { Start-Sleep -Seconds 2 }
287
+ }
288
+ if (-not $moved) { Fail "Pantheon's Node in $nodeDir is in use. Close your agent windows (Claude Code, Cursor, VS Code) and any pantheon command, then run this again." }
289
+ }
290
+ $placed = $false; $lastErr = ''
291
+ for ($i = 1; $i -le 5 -and -not $placed; $i++) {
292
+ try { Move-Item -LiteralPath $new -Destination $nodeDir; $placed = $true } catch { $lastErr = $_.Exception.Message; Start-Sleep -Seconds 2 }
293
+ }
294
+ if (-not $placed) {
295
+ if ($aside) { try { Move-Item -LiteralPath $aside -Destination $nodeDir } catch { } }
296
+ Fail "could not put the new Node in $nodeDir ($lastErr). Check free disk space, then run this again."
297
+ }
298
+ Say "Node v$NodeVersion is in $nodeDir."
299
+ } finally {
300
+ # Stopped between the two moves: the old runtime goes back, so the
301
+ # pantheon command keeps working.
302
+ if ($aside -and -not (Test-Path -LiteralPath $nodeDir) -and (Test-Path -LiteralPath $aside)) {
303
+ try { Move-Item -LiteralPath $aside -Destination $nodeDir } catch { }
304
+ }
305
+ Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
306
+ }
307
+ }
308
+
309
+ # ---- 2. the Pantheon CLI, with that Node's own npm -----------------------
310
+
311
+ # The version of the CLI in the runtime, but only when it is whole: its
312
+ # package.json names that version AND it starts and says so. A run stopped
313
+ # halfway through npm can leave the first without the second; that is
314
+ # installed again, never called done.
315
+ $manifest = Join-Path $pkgDir 'package.json'
316
+ $installedCli = {
317
+ $v = ''
318
+ if ((Test-Path -LiteralPath $manifest) -and (Test-Path -LiteralPath $entry)) {
319
+ try { $v = [string](Get-Content -LiteralPath $manifest -Raw | ConvertFrom-Json).version } catch { $v = '' }
320
+ if ($v -and (Invoke-Quiet $nodeExe @($entry, '--version')).out.Trim() -ne $v) { $v = '' }
321
+ }
322
+ $v
323
+ }
324
+
325
+ # What npm installs. PANTHEON_INSTALL_SPEC swaps the registry package for a
326
+ # package file on this machine, so a release candidate goes through this
327
+ # exact script before its version is on npm. Only a full path on a drive
328
+ # (C:\...\x.tgz) is taken: no URL, no \\server share, no git or registry
329
+ # spec, so it can never send an install anywhere npm would not have gone.
330
+ # Whoever can set it can already set npm_config_registry or NODE_OPTIONS
331
+ # for this window, so it opens no new way in; and what it installs must
332
+ # answer as $PantheonVersion (checked below), or the install fails.
333
+ $spec = "@join-pantheon/cli@$PantheonVersion"
334
+ if ($env:PANTHEON_INSTALL_SPEC) {
335
+ $s = $env:PANTHEON_INSTALL_SPEC
336
+ if ($s -notmatch '^[A-Za-z]:\\.*\.tgz$') { Fail "PANTHEON_INSTALL_SPEC must be the full path of a package .tgz on this machine (C:\...\x.tgz), not $s. Remove it to install from npm: Remove-Item Env:PANTHEON_INSTALL_SPEC" }
337
+ if (-not (Test-Path -LiteralPath $s -PathType Leaf)) { Fail "PANTHEON_INSTALL_SPEC is $s, which is not a file. Remove it to install from npm: Remove-Item Env:PANTHEON_INSTALL_SPEC" }
338
+ $spec = $s
339
+ Note "installing Pantheon $PantheonVersion from $s, not from npm (PANTHEON_INSTALL_SPEC is set)."
340
+ }
341
+
342
+ # The Windows certificate store, as well as Node's own: on a network that
343
+ # inspects secure traffic, the company's root is only there. npm needs it to
344
+ # install and pantheon needs it to reach its relay, so the launcher below
345
+ # runs Node with it too (when this Node has the flag; the pinned one does).
346
+ $ca = @()
347
+ if ((Invoke-Quiet $nodeExe @('--use-system-ca', '-e', '0')).code -eq 0) { $ca = @('--use-system-ca') }
348
+
349
+ $haveCli = & $installedCli
350
+ if ($haveCli -eq $PantheonVersion) {
351
+ Say "Pantheon $PantheonVersion is already installed."
352
+ } else {
353
+ $State.step = "installing Pantheon $PantheonVersion"
354
+ Say "Installing Pantheon $PantheonVersion..."
355
+ # npm.cmd only runs node.exe on npm-cli.js; doing that directly keeps
356
+ # cmd.exe, and its way with % and & in a path, out of it. The prefix is
357
+ # named, because the Node zip's own npmrc sends global installs to
358
+ # %APPDATA%\npm; the cache is Pantheon's own, so a broken npm cache
359
+ # elsewhere cannot fail this. --use-system-ca (when this Node has it)
360
+ # trusts the Windows certificate store, as Invoke-WebRequest just did,
361
+ # which is what a TLS-inspecting company proxy needs.
362
+ $env:Path = "$nodeDir;$env:Path"
363
+ $npmArgs = $ca + @($npmCli, 'install', '-g', '--prefix', $nodeDir, '--cache', (Join-Path $runtime 'npm-cache'),
364
+ $spec, '--no-audit', '--no-fund', '--loglevel=error', '--fetch-timeout=60000', '--fetch-retries=2')
365
+ $code = Invoke-Logged $nodeExe $npmArgs $log
366
+ if ($code -ne 0) {
367
+ $why = (Get-Content -LiteralPath $log | Where-Object { $_ -match '^npm (error|ERR!) ' } | Select-Object -First 2 | ForEach-Object { $_ -replace '^npm (error|ERR!) ', '' }) -join '; '
368
+ if (-not $why) { $why = "npm exited $code" }
369
+ if ((Get-Content -LiteralPath $log -Raw) -match 'ENOSPC') {
370
+ Fail "npm could not install @join-pantheon/cli@$PantheonVersion because the disk is full ($why). Free some disk space, then run this again."
371
+ }
372
+ Fail "npm could not install @join-pantheon/cli@$PantheonVersion ($why). Behind a proxy, set HTTPS_PROXY; if your network inspects TLS, set NODE_EXTRA_CA_CERTS to your company's CA file. Full log: $log"
373
+ }
374
+ if (-not (Test-Path -LiteralPath $entry)) { Fail "npm finished but $entry is not there. Full log: $log (send it to sid@joinpantheon.network)" }
375
+ $haveCli = & $installedCli
376
+ if ($haveCli -ne $PantheonVersion) { Fail "npm finished, but the Pantheon in $pkgDir does not start as $PantheonVersion$(if ($haveCli) { " (it is $haveCli)" }). Run the same command again; if it fails the same way, send this line and $log to sid@joinpantheon.network" }
377
+ Say "Pantheon $PantheonVersion is installed."
378
+ }
379
+
380
+ # ---- 3. the launcher -----------------------------------------------------
381
+
382
+ # %~dp0 is the folder this .cmd is in, so the file holds no path of its own:
383
+ # nothing in it depends on PATH, and a profile folder with a % or a
384
+ # non-ASCII letter in its name (which cmd.exe would read in the console's
385
+ # code page) cannot break it.
386
+ $State.step = "writing $launcher"
387
+ $cmdText = "@echo off`r`n" +
388
+ "rem The pantheon command, written by the Pantheon installer. It runs Pantheon's own`r`n" +
389
+ "rem Node by absolute path, so it never depends on which node comes first on PATH.`r`n" +
390
+ "`"%~dp0..\runtime\node\node.exe`" $(if ($ca.Count) { '--use-system-ca ' })`"%~dp0..\runtime\node\node_modules\@join-pantheon\cli\bin\pantheon.js`" %*`r`n"
391
+ $current = if (Test-Path -LiteralPath $launcher) { [IO.File]::ReadAllText($launcher) } else { '' }
392
+ if ($current -ne $cmdText) {
393
+ try { [IO.File]::WriteAllText($launcher, $cmdText, [Text.Encoding]::ASCII) }
394
+ catch { Fail "could not write $launcher ($($_.Exception.Message)). Close any window running pantheon, then run this again." }
395
+ }
396
+
397
+ # ---- 4. PATH -------------------------------------------------------------
398
+
399
+ $State.step = 'adding Pantheon to your PATH'
400
+ $same = { param($a, $b) ([Environment]::ExpandEnvironmentVariables($a).TrimEnd('\')) -ieq ([Environment]::ExpandEnvironmentVariables($b).TrimEnd('\')) }
401
+ # Read raw from the registry: [Environment]::GetEnvironmentVariable expands
402
+ # %USERPROFILE%-style entries, and writing that back would freeze them.
403
+ $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
404
+ try {
405
+ $raw = [string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
406
+ $parts = @($raw -split ';' | Where-Object { $_ -ne '' })
407
+ if ($parts.Count -eq 0 -or -not (& $same $parts[0] $binDir)) {
408
+ $rest = @($parts | Where-Object { -not (& $same $_ $binDir) })
409
+ $key.SetValue('Path', ((@($binDir) + $rest) -join ';'), [Microsoft.Win32.RegistryValueKind]::ExpandString)
410
+ # SetEnvironmentVariable(..., 'User') is what tells Explorer and new
411
+ # terminals that the user environment changed (WM_SETTINGCHANGE).
412
+ [Environment]::SetEnvironmentVariable('PANTHEON_PATH_UPDATED', '1', 'User')
413
+ [Environment]::SetEnvironmentVariable('PANTHEON_PATH_UPDATED', $null, 'User')
414
+ Say "Put $binDir first on your user PATH."
415
+ }
416
+ } finally { $key.Close() }
417
+ # This window: the bin folder first, and the runtime's own folder (added
418
+ # for npm above) taken off again, as no new terminal will have it.
419
+ $env:Path = (@($binDir) + @($env:Path -split ';' | Where-Object { $_ -ne '' -and -not (& $same $_ $binDir) -and -not (& $same $_ $nodeDir) })) -join ';'
420
+
421
+ # An older pantheon (npm -g, a checkout) is left exactly as it is. A
422
+ # pantheon on the SYSTEM PATH still comes before any user PATH entry.
423
+ $other = Get-Command pantheon -All -ErrorAction SilentlyContinue |
424
+ Where-Object { $_.Source -and -not $_.Source.StartsWith($root + '\', [StringComparison]::OrdinalIgnoreCase) } | Select-Object -First 1
425
+ if ($other) { Note "another pantheon is installed at $($other.Source) and was left as it is. New terminals run $launcher first, unless that one is on the system PATH." }
426
+
427
+ # ---- 5. hand over to pantheon ------------------------------------------
428
+
429
+ $State.lock.Dispose()
430
+ $State.lock = $null
431
+ if ($Arguments.Count -eq 0) {
432
+ Say ''
433
+ Say "Pantheon $PantheonVersion is installed. Open a new terminal, then:"
434
+ Say ' pantheon start create a team'
435
+ Say " pantheon join '<invite link>' join a teammate's team"
436
+ $State.code = 0
437
+ return
438
+ }
439
+ Say ''
440
+ # node.exe directly, not the .cmd: PowerShell hands arguments to a .cmd
441
+ # through cmd.exe, which would cut an invite at a & or expand a %. And
442
+ # Start-Process -NoNewWindow, so pantheon gets this console itself: a real
443
+ # terminal for the sign-in questions, not a pipe PowerShell reads from.
444
+ $State.step = 'starting pantheon'
445
+ $line = ConvertTo-ArgLine (@($entry) + $Arguments)
446
+ $noConsole = $false
447
+ try { $noConsole = [Console]::IsInputRedirected } catch { $noConsole = $false }
448
+ if (-not $noConsole) {
449
+ $p = Start-Process -FilePath $nodeExe -ArgumentList $line -NoNewWindow -PassThru
450
+ $null = $p.Handle # without this, ExitCode is empty once the process is gone
451
+ $p.WaitForExit()
452
+ $State.code = $p.ExitCode
453
+ return
454
+ }
455
+
456
+ # No console to type into (CI, input piped in). pantheon still runs: it
457
+ # never asks a question without a terminal (every prompt checks
458
+ # process.stdin.isTTY) and works on flags alone, so a join to a team's own
459
+ # relay, or from a machine already signed in, needs no answers. Its input
460
+ # is an empty file, so nothing can wait on it. What it cannot do here is
461
+ # sign in: on the hosted relay join and start stop at once, before anything
462
+ # is changed, with "sign in first" (or the relay's NOT_SIGNED_IN). When that
463
+ # is how it ended and config.json still has no account, the last line says
464
+ # the one thing to do; any other failure keeps pantheon's own words last.
465
+ $quiet = Join-Path ([IO.Path]::GetTempPath()) ('pantheon-' + [Guid]::NewGuid().ToString('N').Substring(0, 8))
466
+ New-Item -ItemType Directory -Force -Path $quiet | Out-Null
467
+ try {
468
+ $in = Join-Path $quiet 'in'; $errFile = Join-Path $quiet 'err'
469
+ [IO.File]::WriteAllText($in, '')
470
+ $p = Start-Process -FilePath $nodeExe -ArgumentList $line -NoNewWindow -PassThru -RedirectStandardInput $in -RedirectStandardError $errFile
471
+ $null = $p.Handle
472
+ $p.WaitForExit()
473
+ $State.code = $p.ExitCode
474
+ $said = if (Test-Path -LiteralPath $errFile) { [IO.File]::ReadAllText($errFile) } else { '' }
475
+ if ($said) { [Console]::Error.Write($said) }
476
+ if ($State.code -ne 0 -and $said -match 'sign in first|NOT_SIGNED_IN' -and $Arguments[0] -in @('join', 'start')) {
477
+ $pantheonHome = if ($env:PANTHEON_HOME) { $env:PANTHEON_HOME } else { Join-Path ([Environment]::GetFolderPath('UserProfile')) '.pantheon' }
478
+ $signedIn = $false
479
+ try { $signedIn = [bool]((Get-Content -LiteralPath (Join-Path $pantheonHome 'config.json') -Raw | ConvertFrom-Json).account) } catch { $signedIn = $false }
480
+ if (-not $signedIn) {
481
+ # Quoted only where PowerShell needs it, so it reads as typed.
482
+ $show = ($Arguments | ForEach-Object { if ($_ -match '^[A-Za-z0-9_./:=@+,-]+$') { $_ } else { "'" + ($_ -replace "'", "''") + "'" } }) -join ' '
483
+ $what = if ($Arguments[0] -eq 'join') { 'joining' } else { 'creating your team' }
484
+ Say ''
485
+ Say "Pantheon is installed. To finish $what, open PowerShell and run: pantheon $show"
486
+ }
487
+ }
488
+ } finally {
489
+ Remove-Item -LiteralPath $quiet -Recurse -Force -ErrorAction SilentlyContinue
490
+ }
491
+ } catch {
492
+ $m = $_.Exception.Message
493
+ if ($m -like 'PANTHEON_FAIL:*') { $m = $m.Substring('PANTHEON_FAIL:'.Length) }
494
+ else { $m = "it stopped while $($State.step) ($m). Run the same command again; if it fails the same way, send this line to sid@joinpantheon.network" }
495
+ Write-Host "pantheon install failed: $m" -ForegroundColor Red
496
+ $State.code = 1
497
+ } finally {
498
+ # Every way out, Ctrl-C included, lets go of the install lock.
499
+ if ($State.lock) { $State.lock.Dispose(); $State.lock = $null }
500
+ }
501
+ }
502
+
503
+ # `join '<link>'` as two arguments: whitespace splits, and single or double
504
+ # quotes keep a value with spaces in it together.
505
+ function Split-PantheonArgs([string]$Line) {
506
+ $out = New-Object System.Collections.Generic.List[string]
507
+ $cur = New-Object System.Text.StringBuilder
508
+ $quote = [char]0
509
+ $have = $false
510
+ foreach ($c in $Line.ToCharArray()) {
511
+ if ($quote -ne [char]0) {
512
+ if ($c -eq $quote) { $quote = [char]0 } else { [void]$cur.Append($c) }
513
+ } elseif ($c -eq "'" -or $c -eq '"') { $quote = $c; $have = $true }
514
+ elseif ([char]::IsWhiteSpace($c)) { if ($have -or $cur.Length) { $out.Add($cur.ToString()); [void]$cur.Clear(); $have = $false } }
515
+ else { [void]$cur.Append($c) }
516
+ }
517
+ if ($have -or $cur.Length) { $out.Add($cur.ToString()) }
518
+ return , $out.ToArray()
519
+ }
520
+
521
+ # Whether this PowerShell was started to run one command and end
522
+ # (`powershell -Command "... | iex"` from cmd.exe), where the exit code is the
523
+ # result, rather than an open window, which `exit` would close. PowerShell
524
+ # takes any prefix of a parameter's name, after -, -- or /; -NoExit anywhere
525
+ # keeps the window open, so then it is never one-shot.
526
+ function Test-PantheonOneShot([string[]]$CommandLine) {
527
+ $flags = @($CommandLine | Select-Object -Skip 1 | Where-Object { $_ -match '^(--?|/)[A-Za-z]+$' } |
528
+ ForEach-Object { ($_ -replace '^(--?|/)', '').ToLowerInvariant() })
529
+ $prefixOf = { param($f, $name, $min) $f.Length -ge $min -and $name.StartsWith($f) }
530
+ if ($flags | Where-Object { & $prefixOf $_ 'noexit' 3 }) { return $false }
531
+ foreach ($f in $flags) {
532
+ if ((& $prefixOf $f 'command' 1) -or (& $prefixOf $f 'commandwithargs' 8) -or $f -eq 'cwa' -or
533
+ (& $prefixOf $f 'encodedcommand' 1) -or $f -eq 'ec') { return $true }
534
+ }
535
+ return $false
536
+ }
537
+
538
+ if ($PantheonInstallArgs.Count -eq 0 -and $env:PANTHEON_ARGS) {
539
+ $PantheonInstallArgs = Split-PantheonArgs $env:PANTHEON_ARGS
540
+ }
541
+ Remove-Item Env:PANTHEON_ARGS -ErrorAction SilentlyContinue
542
+ $PantheonInstallState = @{ code = 1; step = 'starting' }
543
+ Install-Pantheon -Arguments $PantheonInstallArgs -State $PantheonInstallState
544
+ $PantheonInstallCode = [int]$PantheonInstallState.code
545
+
546
+ # `powershell -Command "... | iex"` from cmd.exe: the exit code is the result.
547
+ # In an open PowerShell window `exit` would close the window, so there the
548
+ # result is left in $LASTEXITCODE instead.
549
+ $PantheonOneShot = $false
550
+ try { $PantheonOneShot = Test-PantheonOneShot ([Environment]::GetCommandLineArgs()) } catch { $PantheonOneShot = $false }
551
+ Remove-Item Function:Install-Pantheon, Function:Split-PantheonArgs, Function:Test-PantheonOneShot -ErrorAction SilentlyContinue
552
+ Remove-Variable PantheonInstallArgs, PantheonInstallState -ErrorAction SilentlyContinue
553
+ $global:LASTEXITCODE = $PantheonInstallCode
554
+ if ($PantheonOneShot) { exit $PantheonInstallCode }